diff --git a/.github/workflows/ultramodern-workspace-gates.yml b/.github/workflows/ultramodern-workspace-gates.yml index 83726ce47..acb8556a5 100644 --- a/.github/workflows/ultramodern-workspace-gates.yml +++ b/.github/workflows/ultramodern-workspace-gates.yml @@ -153,6 +153,14 @@ jobs: working-directory: app run: mise exec -- pnpm test:integration + - name: Install Chromium and its runtime dependencies + working-directory: app + run: mise exec -- pnpm --filter @app/shell-super-app exec playwright install --with-deps chromium + + - name: Run parallel authenticated browser tests without retries + working-directory: app + run: mise exec -- pnpm --filter @app/shell-super-app test:e2e + - name: Show bounded service diagnostics after failure if: failure() working-directory: app diff --git a/app/apps/shell-super-app/api/auth/service.ts b/app/apps/shell-super-app/api/auth/service.ts index 9b4f3ab43..227174e96 100644 --- a/app/apps/shell-super-app/api/auth/service.ts +++ b/app/apps/shell-super-app/api/auth/service.ts @@ -1053,18 +1053,29 @@ const assembleAuthenticationService = ( }; }; -export const makeAuthenticationService = assembleAuthenticationService; +export interface AuthenticationServiceOptions { + readonly allowFixtureSignUp?: boolean; +} -export const AuthenticationServiceLive = Layer.effect( - AuthenticationService, - Effect.gen(function* makeAuthenticationServiceEffect() { +/** + * Builds the authentication service from AuthConfig, AuthDatabase, and PrincipalResolver in the + * caller's Effect context. Better Auth invokes its session hooks as Promises, so the resolver bridge + * is captured here and never leaves this module. + */ +export const makeAuthenticationService = Effect.fn('AuthenticationService.make')( + function* makeAuthenticationServiceEffect(options: AuthenticationServiceOptions = {}) { const configuration = yield* AuthConfig; const database = yield* AuthDatabase; const resolver = yield* PrincipalResolver; const effectContext = yield* Effect.context(); - const runResolverEffect = Effect.runPromiseWith(effectContext); - return makeAuthenticationService(configuration, database.adapter, resolver, { - runResolverEffect, + return assembleAuthenticationService(configuration, database.adapter, resolver, { + ...options, + runResolverEffect: Effect.runPromiseWith(effectContext), }); - }), + }, +); + +export const AuthenticationServiceLive = Layer.effect( + AuthenticationService, + makeAuthenticationService(), ); diff --git a/app/apps/shell-super-app/api/modules/installed-module-catalog.ts b/app/apps/shell-super-app/api/modules/installed-module-catalog.ts index d148e2455..a3271eda3 100644 --- a/app/apps/shell-super-app/api/modules/installed-module-catalog.ts +++ b/app/apps/shell-super-app/api/modules/installed-module-catalog.ts @@ -29,10 +29,6 @@ const unavailableErrorFields = { reason: Schema.String, }; -const invokePromiseWithoutSignal = - (operation: () => PromiseLike) => - (_signal: AbortSignal): PromiseLike => - operation(); const InstalledModuleCatalogUnavailableErrorSchema = Schema.TaggedStruct( 'InstalledModuleCatalogUnavailableError', unavailableErrorFields, @@ -141,7 +137,8 @@ const readResponseChunks = ( > => Effect.tryPromise({ catch: unavailable, - try: invokePromiseWithoutSignal(reader.read.bind(reader)), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign stream Promise boundary. + try: () => reader.read(), }).pipe( Effect.timeout(timeout), Effect.flatMap((next) => { @@ -199,10 +196,8 @@ const readBoundedContract = Effect.fn('ShellInstalledModuleCatalog.readBoundedCo const text = yield* Effect.acquireUseRelease( Effect.succeed(reader), (bodyReader) => collectResponseBody(bodyReader, maxBytes, timeout), - (bodyReader) => - Effect.promise( - invokePromiseWithoutSignal(bodyReader.cancel.bind(bodyReader, undefined)), - ).pipe(Effect.ignore), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign stream Promise boundary. + (bodyReader) => Effect.promise(() => bodyReader.cancel()).pipe(Effect.ignore), ); return yield* decodeContractDocument(text).pipe(Effect.mapError((cause) => invalid(cause))); }, diff --git a/app/apps/shell-super-app/package.json b/app/apps/shell-super-app/package.json index 886d6949e..1efbc9363 100644 --- a/app/apps/shell-super-app/package.json +++ b/app/apps/shell-super-app/package.json @@ -21,8 +21,8 @@ "serve": "modern serve", "stage:bootstrap-demo": "sh scripts/bootstrap-stage-demo.sh", "test:e2e": "playwright test", - "test:integration": "node --test tests/integration/*.test.ts", - "test:unit": "rstest", + "test:integration": "rstest --project integration", + "test:unit": "rstest --project unit", "typecheck": "node ../../scripts/ultramodern-typecheck.mts --project tsconfig.json" }, "dependencies": { @@ -57,7 +57,7 @@ "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12", "@playwright/test": "1.61.0", "@rsbuild/plugin-tailwindcss": "^2.0.3", - "@rstest/core": "0.11.10", + "@rstest/core": "0.11.11", "@testing-library/dom": "10.4.1", "@testing-library/react": "16.3.2", "@testing-library/user-event": "14.6.1", @@ -71,7 +71,8 @@ "tailwindcss": "^4.3.2", "typescript": "7.0.2", "wrangler": "4.110.0", - "zephyr-rspack-plugin": "1.2.4" + "zephyr-rspack-plugin": "1.2.4", + "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc" }, "modernjs": { "preset": "presetUltramodern", diff --git a/app/apps/shell-super-app/playwright.config.ts b/app/apps/shell-super-app/playwright.config.ts index 42559bc7e..f2d9a09d3 100644 --- a/app/apps/shell-super-app/playwright.config.ts +++ b/app/apps/shell-super-app/playwright.config.ts @@ -1,3 +1,5 @@ +/// +import { availableParallelism } from 'node:os'; import path from 'node:path'; import { APP_ENV_PATH } from '@app/core-runtime/workspace-environment'; import { defineConfig, devices } from '@playwright/test'; @@ -30,7 +32,10 @@ const repositoryRoot = path.resolve(process.cwd(), '../../..'); const e2eSourceRevision = '0000000000000000000000000000000000000001'; export default defineConfig({ + // Preserve one native Core module instance and let Node strip its type-only class fields. + build: { external: ['**/packages/core-runtime/**'] }, forbidOnly: continuousIntegration, + fullyParallel: true, projects: [ { name: 'chromium', @@ -38,11 +43,11 @@ export default defineConfig({ }, ], reporter: 'line', - retries: continuousIntegration ? 2 : 0, + retries: 0, testDir: './tests/e2e', use: { baseURL: origin, - trace: 'on-first-retry', + trace: 'retain-on-failure', }, webServer: [ { @@ -54,7 +59,7 @@ export default defineConfig({ ULTRAMODERN_SOURCE_REVISION: e2eSourceRevision, }, reuseExistingServer: !continuousIntegration, - url: 'http://127.0.0.1:4102/en', + url: 'http://127.0.0.1:4102/party-registry-api/party-registry/readiness', }, { command: 'pnpm dev', @@ -62,4 +67,5 @@ export default defineConfig({ url: `${origin}/en`, }, ], + workers: Math.max(1, availableParallelism() - 1), }); diff --git a/app/apps/shell-super-app/rstest.config.ts b/app/apps/shell-super-app/rstest.config.ts index df0ea039e..794670f46 100644 --- a/app/apps/shell-super-app/rstest.config.ts +++ b/app/apps/shell-super-app/rstest.config.ts @@ -1,7 +1,10 @@ import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { withModernConfig } from '@modern-js/adapter-rstest'; import { defineConfig } from '@rstest/core'; +import type { Rspack } from '@rstest/core'; import { Result, Schema } from 'effect'; import { @@ -52,22 +55,62 @@ const encodedSiteUrl = Result.getOrThrow( Schema.encodeResult(siteUrlJsonSchema)('http://localhost:3020'), ); +const coreRuntimeRoot = fileURLToPath(new URL('../../packages/core-runtime/', import.meta.url)); +// Generated owner modules are imported natively from disk, so core-runtime must be one Node +// instance shared by the test bundle and those modules (brand symbols, private fields). +const externalizeCoreRuntime = ( + { context, request }: Rspack.ExternalItemFunctionData, + resolveExternal: (error?: Error, external?: string) => void, +): void => { + if (request === undefined || context === undefined) { + resolveExternal(); + return; + } + if (/^@app\/core-runtime(?:\/|$)/u.test(request)) { + resolveExternal(undefined, `module-import ${request}`); + return; + } + const resolved = request.startsWith('.') ? path.resolve(context, request) : undefined; + if (resolved !== undefined && resolved.startsWith(coreRuntimeRoot)) { + resolveExternal(undefined, `module-import ${pathToFileURL(resolved).href}`); + return; + } + resolveExternal(); +}; + +// SWC rejects every generic arrow function in the imported `scripts/**/*.mts` files (even +// `(...)`) under its default mts/cts parser mode. The parser key is missing from the +// bundled swc types, so the object is declared here instead of inline. +const swc = { jsc: { parser: { disallowAmbiguousJsxLike: false, syntax: 'typescript' } } } as const; + export default defineConfig({ - clearMocks: true, - extends: withModernConfig({ - configPath: './modern.rstest.config.ts', - }), - include: ['tests/unit/**/*.{test,spec}.?(c|m)[jt]s?(x)'], - output: { - module: false, - }, - restoreMocks: true, - source: { - define: { - ULTRAMODERN_GATEWAY_AUDIENCE_TOPOLOGY: encodedReferenceTopology, - ULTRAMODERN_MODULE_DEPLOYMENT_ALLOWLIST: encodedModuleDeploymentAllowlist, - ULTRAMODERN_SITE_URL: encodedSiteUrl, + projects: [ + { + clearMocks: true, + extends: withModernConfig({ + configPath: './modern.rstest.config.ts', + }), + include: ['tests/unit/**/*.{test,spec}.?(c|m)[jt]s?(x)'], + name: 'unit', + output: { + module: false, + }, + restoreMocks: true, + source: { + define: { + ULTRAMODERN_GATEWAY_AUDIENCE_TOPOLOGY: encodedReferenceTopology, + ULTRAMODERN_MODULE_DEPLOYMENT_ALLOWLIST: encodedModuleDeploymentAllowlist, + ULTRAMODERN_SITE_URL: encodedSiteUrl, + }, + }, + testEnvironment: 'happy-dom', + }, + { + include: ['tests/integration/**/*.test.ts'], + name: 'integration', + testEnvironment: 'node', + testTimeout: 30_000, + tools: { rspack: { externals: [externalizeCoreRuntime] }, swc }, }, - }, - testEnvironment: 'happy-dom', + ], }); diff --git a/app/apps/shell-super-app/src/routes/[lang]/login/page.tsx b/app/apps/shell-super-app/src/routes/[lang]/login/page.tsx index 3dc9036b8..1044b2948 100644 --- a/app/apps/shell-super-app/src/routes/[lang]/login/page.tsx +++ b/app/apps/shell-super-app/src/routes/[lang]/login/page.tsx @@ -9,7 +9,7 @@ import { Toaster, useToast } from '@techsio/ui-kit/molecules/toast'; import { useRef, useState } from 'react'; import { signIn } from '../../../api/auth-client.ts'; import type { ShellAuthenticationClientError } from '../../../api/auth-client.ts'; -import { runBrowserEffect } from '../../../runtime/browser-effect-runtime.ts'; +import { browserRuntime } from '../../../runtime/browser-effect-runtime.ts'; import { SignInPayloadSchema } from '../../../../shared/api.ts'; import { UltramodernRouteHead } from '../../ultramodern-route-head'; @@ -109,7 +109,7 @@ const LoginPage = () => { } setSubmitting(true); - void runBrowserEffect( + void browserRuntime.runPromise( signIn(credentials.value, { locale: language }).pipe( Effect.matchEffect({ onFailure: (error) => diff --git a/app/apps/shell-super-app/src/routes/[lang]/modules/[moduleId]/page.data.ts b/app/apps/shell-super-app/src/routes/[lang]/modules/[moduleId]/page.data.ts index e6f69ec96..29ab57e3d 100644 --- a/app/apps/shell-super-app/src/routes/[lang]/modules/[moduleId]/page.data.ts +++ b/app/apps/shell-super-app/src/routes/[lang]/modules/[moduleId]/page.data.ts @@ -1,10 +1,10 @@ import { Effect, Match, Predicate, Schema } from 'effect'; -import type { Config } from 'effect'; +import type { Cause, Config } from 'effect'; import { ResolveModuleTargetPayloadSchema } from '../../../../../shared/api.ts'; import type { ResolvedModuleTarget } from '../../../../../shared/api.ts'; import { resolveModuleTarget } from '../../../../api/auth-client.ts'; import type { ShellTargetClientError } from '../../../../api/auth-client.ts'; -import { runBrowserEffect } from '../../../../runtime/browser-effect-runtime.ts'; +import { browserRuntime } from '../../../../runtime/browser-effect-runtime.ts'; import { shellAuthenticationClientOptionsFromRequest } from '../../../shell-authentication-client-options.ts'; import { loadHomePageModel } from '../../page.data.ts'; import type { HomePageModel } from '../../page.data.ts'; @@ -96,45 +96,46 @@ const safeState = ( Match.exhaustive, ); -export const loader = ({ +export const loadModulePageModel = ({ params, request, routeParams = {}, -}: ModuleTargetLoaderArguments): Promise => - runBrowserEffect( - Effect.tryPromise(() => loadHomePageModel(request)).pipe( - Effect.timeout('30 seconds'), - Effect.flatMap((shell) => { - if (shell.state !== 'authenticated') { - return Effect.succeed({ - shell, - state: shell.state === 'unavailable' ? 'unavailable' : 'selection_required', - }); - } - const boundedRouteParams = selectRouteParams(routeParams, Object.keys(routeParams)); - return shellAuthenticationClientOptionsFromRequest(request).pipe( - Effect.flatMap((options) => - Schema.decodeUnknownEffect(ResolveModuleTargetPayloadSchema)( - withOptionalProperty( - {}, - params.entrypointKey !== undefined, - 'entrypointKey', - params.entrypointKey, - { moduleId: params.moduleId }, - ), - ).pipe(Effect.flatMap((payload) => resolveModuleTarget(payload, options))), - ), - Effect.map((target): ModuleTargetPageModel => ({ - routeParams: boundedRouteParams, - shell, - state: 'resolved', - target, - })), - Effect.matchEffect({ - onFailure: (error) => Effect.succeed(safeState(error, shell)), - onSuccess: Effect.succeed, - }), - ); - }), - ), +}: ModuleTargetLoaderArguments): Effect.Effect => + loadHomePageModel(request).pipe( + Effect.timeout('30 seconds'), + Effect.flatMap((shell) => { + if (shell.state !== 'authenticated') { + return Effect.succeed({ + shell, + state: shell.state === 'unavailable' ? 'unavailable' : 'selection_required', + }); + } + const boundedRouteParams = selectRouteParams(routeParams, Object.keys(routeParams)); + return shellAuthenticationClientOptionsFromRequest(request).pipe( + Effect.flatMap((options) => + Schema.decodeUnknownEffect(ResolveModuleTargetPayloadSchema)( + withOptionalProperty( + {}, + params.entrypointKey !== undefined, + 'entrypointKey', + params.entrypointKey, + { moduleId: params.moduleId }, + ), + ).pipe(Effect.flatMap((payload) => resolveModuleTarget(payload, options))), + ), + Effect.map((target): ModuleTargetPageModel => ({ + routeParams: boundedRouteParams, + shell, + state: 'resolved', + target, + })), + Effect.matchEffect({ + onFailure: (error) => Effect.succeed(safeState(error, shell)), + onSuccess: Effect.succeed, + }), + ); + }), ); + +export const loader = (input: ModuleTargetLoaderArguments): Promise => + browserRuntime.runPromise(loadModulePageModel(input), { signal: input.request.signal }); diff --git a/app/apps/shell-super-app/src/routes/[lang]/modules/[moduleId]/page.tsx b/app/apps/shell-super-app/src/routes/[lang]/modules/[moduleId]/page.tsx index f3ac9e1b6..0d66e147c 100644 --- a/app/apps/shell-super-app/src/routes/[lang]/modules/[moduleId]/page.tsx +++ b/app/apps/shell-super-app/src/routes/[lang]/modules/[moduleId]/page.tsx @@ -5,7 +5,7 @@ import { Effect, Predicate } from 'effect'; import { useEffect, useState } from 'react'; import type { ApprovedVerticalPageComponent } from '../../../../api/vertical-clients.ts'; import { findApprovedVerticalPageClient } from '../../../../api/vertical-clients.ts'; -import { runBrowserEffect } from '../../../../runtime/browser-effect-runtime.ts'; +import { browserRuntime } from '../../../../runtime/browser-effect-runtime.ts'; import { resolveThenLoadModuleTarget, settleModuleEntrypointLoad, @@ -38,7 +38,7 @@ const ResolvedTarget = ({ return; } let current = true; - void runBrowserEffect( + void browserRuntime.runPromise( resolveThenLoadModuleTarget(Effect.succeed(model.target), () => settleModuleEntrypointLoad( client.load, diff --git a/app/apps/shell-super-app/src/routes/[lang]/page.data.ts b/app/apps/shell-super-app/src/routes/[lang]/page.data.ts index 1f56c8fe7..879258cbb 100644 --- a/app/apps/shell-super-app/src/routes/[lang]/page.data.ts +++ b/app/apps/shell-super-app/src/routes/[lang]/page.data.ts @@ -17,7 +17,7 @@ import type { ShellNavigationItem, ShellUnavailableDeployment, } from '../../../shared/api.ts'; -import { runBrowserEffect } from '../../runtime/browser-effect-runtime.ts'; +import { browserRuntime } from '../../runtime/browser-effect-runtime.ts'; import { shellAuthenticationClientOptionsFromRequest } from '../shell-authentication-client-options.ts'; interface HomeLoaderArguments { @@ -89,113 +89,109 @@ const tenantRead = (error: AvailableTenantsClientError, tenantId: SafeTenantIden ); export const loadHomePageModel = (request: Request) => - runBrowserEffect( - shellAuthenticationClientOptionsFromRequest(request).pipe( - Effect.flatMap((options) => - currentSession(options).pipe( - Effect.flatMap((session) => { - if (session.state === 'anonymous') { - return Effect.succeed(anonymousModel); - } - const legalEntities = - session.state === 'authenticated' - ? availableLegalEntities(options).pipe( - Effect.map((response) => ({ - items: response.legalEntities, - state: 'available' as const, - })), - Effect.orElseSucceed(() => ({ - items: [] as const, - state: 'unavailable' as const, - })), - ) - : Effect.succeed({ - items: - session.state === 'selection_required' - ? session.availableLegalEntities - : ([] as const), + shellAuthenticationClientOptionsFromRequest(request).pipe( + Effect.flatMap((options) => + currentSession(options).pipe( + Effect.flatMap((session) => { + if (session.state === 'anonymous') { + return Effect.succeed(anonymousModel); + } + const legalEntities = + session.state === 'authenticated' + ? availableLegalEntities(options).pipe( + Effect.map((response) => ({ + items: response.legalEntities, state: 'available' as const, - }); - const navigation = - session.state === 'authenticated' - ? shellComposition(options).pipe( - Effect.map((composition) => ({ - items: - composition.state === 'available' ? composition.navigation : ([] as const), - state: 'available' as const, - unavailableDeployments: - composition.state === 'available' - ? composition.unavailableDeployments - : ([] as const), - })), - Effect.matchEffect({ - onFailure: (error) => Effect.succeed(unavailableNavigation(error)), - onSuccess: Effect.succeed, - }), - ) - : Effect.succeed({ + })), + Effect.orElseSucceed(() => ({ items: [] as const, + state: 'unavailable' as const, + })), + ) + : Effect.succeed({ + items: + session.state === 'selection_required' + ? session.availableLegalEntities + : ([] as const), + state: 'available' as const, + }); + const navigation = + session.state === 'authenticated' + ? shellComposition(options).pipe( + Effect.map((composition) => ({ + items: + composition.state === 'available' ? composition.navigation : ([] as const), state: 'available' as const, - unavailableDeployments: [] as const, - }); - return Effect.all( - { - legalEntities, - navigation, - tenants: availableTenants(options).pipe( - Effect.map(({ tenants }) => ({ items: tenants, state: 'available' as const })), + unavailableDeployments: + composition.state === 'available' + ? composition.unavailableDeployments + : ([] as const), + })), Effect.matchEffect({ - onFailure: (error) => - Effect.succeed(tenantRead(error, session.identity.tenantId)), + onFailure: (error) => Effect.succeed(unavailableNavigation(error)), onSuccess: Effect.succeed, }), - ), - }, - { concurrency: 3 }, - ).pipe( - Effect.map( - ({ legalEntities: choices, navigation: items, tenants }): HomePageModel => { - if (tenants.state === 'stale') { - return anonymousModel; - } - const model = { - contextState: session.state, - identity: session.identity, - legalEntities: choices, - navigation: items, - state: 'authenticated' as const, - tenants, - }; - return session.state === 'authenticated' - ? { ...model, selectedLegalEntityId: session.identity.legalEntityId } - : model; - }, + ) + : Effect.succeed({ + items: [] as const, + state: 'available' as const, + unavailableDeployments: [] as const, + }); + return Effect.all( + { + legalEntities, + navigation, + tenants: availableTenants(options).pipe( + Effect.map(({ tenants }) => ({ items: tenants, state: 'available' as const })), + Effect.matchEffect({ + onFailure: (error) => + Effect.succeed(tenantRead(error, session.identity.tenantId)), + onSuccess: Effect.succeed, + }), ), - ); - }), - ), + }, + { concurrency: 3 }, + ).pipe( + Effect.map(({ legalEntities: choices, navigation: items, tenants }): HomePageModel => { + if (tenants.state === 'stale') { + return anonymousModel; + } + const model = { + contextState: session.state, + identity: session.identity, + legalEntities: choices, + navigation: items, + state: 'authenticated' as const, + tenants, + }; + return session.state === 'authenticated' + ? { ...model, selectedLegalEntityId: session.identity.legalEntityId } + : model; + }), + ); + }), ), - Effect.matchEffect({ - onFailure: (error) => - Effect.succeed( - Match.value(error).pipe( - Match.tag('InvalidCredentialsProblem', () => anonymousModel), - Match.tag( - 'AuthenticationInternalProblem', - 'AuthenticationUnavailableProblem', - 'ConfigError', - 'HttpClientError', - 'OntosIdentityForbiddenProblem', - 'SchemaError', - () => unavailableModel, - ), - Match.exhaustive, + ), + Effect.matchEffect({ + onFailure: (error) => + Effect.succeed( + Match.value(error).pipe( + Match.tag('InvalidCredentialsProblem', () => anonymousModel), + Match.tag( + 'AuthenticationInternalProblem', + 'AuthenticationUnavailableProblem', + 'ConfigError', + 'HttpClientError', + 'OntosIdentityForbiddenProblem', + 'SchemaError', + () => unavailableModel, ), + Match.exhaustive, ), - onSuccess: Effect.succeed, - }), - ), + ), + onSuccess: Effect.succeed, + }), ); export const loader = ({ request }: HomeLoaderArguments): Promise => - loadHomePageModel(request); + browserRuntime.runPromise(loadHomePageModel(request), { signal: request.signal }); diff --git a/app/apps/shell-super-app/src/routes/[lang]/resources/[moduleId]/[resourceType]/[resourceId]/page.data.ts b/app/apps/shell-super-app/src/routes/[lang]/resources/[moduleId]/[resourceType]/[resourceId]/page.data.ts index 281c5ad87..987c29771 100644 --- a/app/apps/shell-super-app/src/routes/[lang]/resources/[moduleId]/[resourceType]/[resourceId]/page.data.ts +++ b/app/apps/shell-super-app/src/routes/[lang]/resources/[moduleId]/[resourceType]/[resourceId]/page.data.ts @@ -2,7 +2,7 @@ import { Effect, Match, Schema } from 'effect'; import { ResourceRefSchema } from '../../../../../../../shared/api.ts'; import type { ShellResourceResponse } from '../../../../../../../shared/api.ts'; import { resourceDetail } from '../../../../../../api/auth-client.ts'; -import { runBrowserEffect } from '../../../../../../runtime/browser-effect-runtime.ts'; +import { browserRuntime } from '../../../../../../runtime/browser-effect-runtime.ts'; import { shellAuthenticationClientOptionsFromRequest } from '../../../../../shell-authentication-client-options.ts'; import { loadHomePageModel } from '../../../../page.data.ts'; import type { HomePageModel } from '../../../../page.data.ts'; @@ -29,8 +29,8 @@ export type ResourcePageModel = }; export const loader = ({ params, request }: ResourceLoaderArguments): Promise => - runBrowserEffect( - Effect.tryPromise(() => loadHomePageModel(request)).pipe( + browserRuntime.runPromise( + loadHomePageModel(request).pipe( Effect.timeout('30 seconds'), Effect.flatMap((shell) => { if (shell.state !== 'authenticated') { @@ -79,4 +79,5 @@ export const loader = ({ params, request }: ResourceLoaderArguments): Promise; const ResourceDetails = ({ mediaState, @@ -20,8 +21,8 @@ const ResourceDetails = ({ onAttach, }: { readonly mediaState: MediaState; - readonly model: Extract; - readonly onAttach: () => Promise; + readonly model: ReadyResourceModel; + readonly onAttach: () => void; }) => { const { t } = useModernI18n(); return ( @@ -47,9 +48,7 @@ const ResourceDetails = ({ disabled={!model.resource.media.enabled || mediaState === 'pending'} isLoading={mediaState === 'pending'} loadingText={t('shell.resource.media.pending')} - onClick={() => { - void onAttach(); - }} + onClick={onAttach} type="button" > {t('shell.resource.media.attach')} @@ -106,13 +105,10 @@ const ResourcePage = () => { const controls = useShellControls( model.shell.state === 'authenticated' ? model.shell : undefined, ); - const handleMediaAttachment = (): Promise => { - if (model.state !== 'ready') { - return Promise.resolve(); - } + const handleMediaAttachment = (ref: ReadyResourceModel['resource']['ref']) => { setMediaState('pending'); - return runBrowserEffect( - attachResourceMedia(model.resource.ref).pipe( + void browserRuntime.runPromise( + attachResourceMedia(ref).pipe( Effect.matchEffect({ onFailure: (error) => Effect.sync(() => { @@ -143,7 +139,11 @@ const ResourcePage = () => { {t(`shell.resource.${model.state}`)} ) : ( - + handleMediaAttachment(model.resource.ref)} + /> ); return ( { export const loader = ({ request }: SearchLoaderArguments): Promise => { const query = (searchFromRequest(request).q ?? '').trim(); - return runBrowserEffect( - Effect.tryPromise(() => loadHomePageModel(request)).pipe( + return browserRuntime.runPromise( + loadHomePageModel(request).pipe( Effect.timeout('30 seconds'), Effect.flatMap((shell) => { if (shell.state !== 'authenticated') { @@ -96,5 +96,6 @@ export const loader = ({ request }: SearchLoaderArguments): Promise @@ -88,7 +88,7 @@ export const useShellControls = (model: AuthenticatedHomePageModel | undefined) ) => { setPending(true); setFailed(false); - void runBrowserEffect( + void browserRuntime.runPromise( switching.pipe( Effect.matchEffect({ onFailure: (error) => Effect.succeed(switchFailureState(error)), diff --git a/app/apps/shell-super-app/src/runtime/browser-effect-runtime.ts b/app/apps/shell-super-app/src/runtime/browser-effect-runtime.ts index 473cdc43e..3e72701dd 100644 --- a/app/apps/shell-super-app/src/runtime/browser-effect-runtime.ts +++ b/app/apps/shell-super-app/src/runtime/browser-effect-runtime.ts @@ -4,12 +4,10 @@ const browserTracer = Tracer.make({ span: (options) => new Tracer.NativeSpan(options), }); -const browserRuntime = ManagedRuntime.make( +export const browserRuntime = ManagedRuntime.make( Layer.mergeAll( Logger.layer([Logger.defaultLogger]), Layer.succeed(Tracer.Tracer, browserTracer), Layer.succeed(References.MinimumLogLevel, 'Info'), ), ); - -export const runBrowserEffect = browserRuntime.runPromise; diff --git a/app/apps/shell-super-app/tests/e2e/auth-fixture.ts b/app/apps/shell-super-app/tests/e2e/auth-fixture.ts index 692c5c9a6..eb3bcdb59 100644 --- a/app/apps/shell-super-app/tests/e2e/auth-fixture.ts +++ b/app/apps/shell-super-app/tests/e2e/auth-fixture.ts @@ -1,10 +1,17 @@ -import { setTimeout as delay } from 'node:timers/promises'; +import { Crypto, Effect, Redacted, Schema } from 'effect'; +import { NodeCrypto } from '@effect/platform-node'; +import { deadlineInterceptor, v1 } from '@authzed/authzed-node'; +import { loadSpiceDbConfig } from '../../../../packages/core-runtime/src/permissions/config.ts'; +import { + toLegalEntityAccessObjectId, + toModuleAccessObjectId, +} from '../../../../packages/core-runtime/src/permissions/context-access.ts'; +import { acquirePoolResource, makeAuthDatabase } from '../../api/auth/db/client.ts'; +import { configureDatabasePool } from '../../../../packages/core-runtime/src/db/pool-configuration.ts'; +import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.ts'; import { APP_ENV_PATH } from '@app/core-runtime/workspace-environment'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import { betterAuth } from 'better-auth'; -import { drizzleAdapter } from '@better-auth/drizzle-adapter/relations-v2'; import { eq, inArray } from 'drizzle-orm'; -import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; import { coreRelations, @@ -16,203 +23,292 @@ import { tenants, } from '../../../../packages/core-runtime/src/db/schema.ts'; import { loadAuthConfig } from '../../api/auth/config.ts'; -import { - account, - authDatabaseSchema, - authRelations, - session, - user, -} from '../../api/auth/db/schema.ts'; +import { account, session, user } from '../../api/auth/db/schema.ts'; -export const e2eCredentials = { - email: 'e2e.user@example.test', - password: 'e2e-correct-horse-battery-staple', -} as const; +const contactsModuleId = 'party.registry'; -export const e2eTenants = { - first: { - legalEntityId: '55000000-0000-4000-8000-000000000001', - name: 'E2E Alpha tenant', - principalId: '60000000-0000-4000-8000-000000000001', - tenantId: '50000000-0000-4000-8000-000000000001', - }, - second: { - legalEntityId: '55000000-0000-4000-8000-000000000002', - name: 'E2E Zeta tenant', - principalId: '60000000-0000-4000-8000-000000000002', - tenantId: '50000000-0000-4000-8000-000000000002', - }, -} as const; +// Real server deadlines are what bound this fixture's finalizers: a stuck statement would +// otherwise keep a client checked out and hold `pool.end()` open past the acquisition +// deadline. Keep the shared connection bound and shorten the statement bound below that +// deadline. Never use query_timeout or a Promise race -- neither cancels server work. +const e2ePoolDeadlines = { connectionTimeoutMillis: 5000, statement_timeout: 10_000 } as const; -export const createAuthenticationFixture = async () => { - const { - baseUrl: baseURL, - connectionString, - secret, - } = await runEffectTestPromise(loadAuthConfig({ envPath: APP_ENV_PATH })); +class E2eAuthorizationFixtureError extends Schema.TaggedError()( + 'E2eAuthorizationFixtureError', + { reason: Schema.String }, +) {} - const corePool = new Pool({ connectionString }); - const authPool = new Pool({ connectionString }); - const coreDatabase = drizzle({ client: corePool, relations: coreRelations }); - const authDatabase = drizzle({ client: authPool, relations: authRelations }); - const authentication = betterAuth({ - baseURL, - database: drizzleAdapter(authDatabase, { - provider: 'pg', - schema: authDatabaseSchema, - }), - emailAndPassword: { - autoSignIn: false, - enabled: true, - }, - secret, - trustedOrigins: [baseURL, 'http://127.0.0.1:3020'], - }); +interface FixtureTenant { + readonly legalEntityId: string; + readonly name: string; + readonly principalId: string; + readonly tenantId: string; +} - const cleanup = async () => { - // Authenticated shell reads write evidence asynchronously. Let those writes - // settle, then remove their E2E-owned rows before the referenced identities. - await delay(250); - await coreDatabase - .delete(dataAccessEvents) - .where( - inArray(dataAccessEvents.principalId, [ - e2eTenants.first.principalId, - e2eTenants.second.principalId, - ]), - ); - const existingUsers = await authDatabase - .select({ id: user.id }) - .from(user) - .where(eq(user.email, e2eCredentials.email)); +interface FixtureTenants { + readonly first: FixtureTenant; + readonly second: FixtureTenant; +} - await Promise.all( - existingUsers.map(async (existingUser) => { - await authDatabase.delete(session).where(eq(session.userId, existingUser.id)); - await authDatabase.delete(account).where(eq(account.userId, existingUser.id)); - await authDatabase.delete(user).where(eq(user.id, existingUser.id)); - }), +// Database module activation does not grant access. Own the complete authorization +// chain for these disposable E2E identities instead of relying on bootstrap seeds. +const provisionContactsAccess = Effect.fn('provisionContactsAccess')( + function* provisionContactsAccessEffect(e2eTenants: FixtureTenants) { + const configuration = yield* loadSpiceDbConfig(); + if (!configuration.endpoint.startsWith('localhost:')) { + return yield* Effect.fail( + new E2eAuthorizationFixtureError({ + reason: 'E2E authorization fixtures require localhost SpiceDB', + }), + ); + } + const client = yield* Effect.acquireRelease( + Effect.sync(() => + v1.NewClient( + configuration.preSharedKey, + configuration.endpoint, + configuration.insecureLocal + ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED + : v1.ClientSecurity.SECURE, + undefined, + { interceptors: [deadlineInterceptor(5000)] }, + ), + ), + (acquired) => Effect.sync(() => acquired.close()), ); - // A page read can finish its asynchronous evidence write while auth rows - // are being removed. Clear that final E2E-owned batch before deleting the - // binding referenced by the evidence foreign key. - await coreDatabase - .delete(dataAccessEvents) - .where( - inArray(dataAccessEvents.principalId, [ - e2eTenants.first.principalId, - e2eTenants.second.principalId, - ]), + const relationships = yield* Effect.forEach( + Object.values(e2eTenants), + ({ legalEntityId, principalId, tenantId }) => + Effect.gen(function* makeContactsRelationships() { + const entityObject = toLegalEntityAccessObjectId(tenantId, legalEntityId); + const moduleObject = toModuleAccessObjectId(tenantId, legalEntityId, contactsModuleId); + if (entityObject === undefined || moduleObject === undefined) { + return yield* Effect.fail( + new E2eAuthorizationFixtureError({ + reason: 'Invalid E2E authorization object identifier', + }), + ); + } + return ( + [ + ['tenant', tenantId, 'member', 'principal', principalId], + ['legal_entity', entityObject, 'tenant', 'tenant', tenantId], + ['legal_entity', entityObject, 'member', 'principal', principalId], + ['module_access', moduleObject, 'legal_entity', 'legal_entity', entityObject], + ['module_access', moduleObject, 'accessor', 'principal', principalId], + ] as const + ).map(([resourceType, resourceId, relation, subjectType, subjectId]) => + v1.Relationship.create({ + relation, + resource: { objectId: resourceId, objectType: resourceType }, + subject: { object: { objectId: subjectId, objectType: subjectType } }, + }), + ); + }), + { concurrency: 'unbounded' }, + ); + const update = (operation: v1.RelationshipUpdate_Operation) => + Effect.tryPromise( + async () => + await client.promises.writeRelationships( + v1.WriteRelationshipsRequest.create({ + updates: relationships.flat().map((relationship) => ({ operation, relationship })), + }), + ), ); - await Promise.all( - existingUsers.map((existingUser) => - coreDatabase + // Register first so even an indeterminate write acknowledgement is cleaned up. + yield* Effect.addFinalizer(() => + update(v1.RelationshipUpdate_Operation.DELETE).pipe(Effect.orDie), + ); + return yield* update(v1.RelationshipUpdate_Operation.TOUCH).pipe(Effect.uninterruptible); + }, +); + +export const createAuthenticationFixture = Effect.fn('createAuthenticationFixture')( + function* createAuthenticationFixtureEffect() { + const crypto = yield* Crypto.Crypto; + const fixtureId = yield* crypto.randomUUIDv4; + const e2eCredentials = { + email: `e2e.${fixtureId}@example.test`, + password: 'e2e-correct-horse-battery-staple', + }; + const makeTenant = (name: string) => + Effect.all({ + legalEntityId: crypto.randomUUIDv4, + name: Effect.succeed(name), + principalId: crypto.randomUUIDv4, + tenantId: crypto.randomUUIDv4, + }); + const e2eTenants = yield* Effect.all({ + first: makeTenant('E2E Alpha tenant'), + second: makeTenant('E2E Zeta tenant'), + }); + const { + baseUrl: baseURL, + connectionString, + secret, + } = yield* loadAuthConfig({ envPath: APP_ENV_PATH }); + + const corePoolConfiguration = yield* configureDatabasePool( + Redacted.make(connectionString), + e2ePoolDeadlines, + ); + const corePool = yield* acquirePoolResource(() => new Pool(corePoolConfiguration)); + const coreDatabase = yield* makeTestDatabaseFromPool(corePool, coreRelations); + const { adapter, executor: authDatabase } = yield* makeAuthDatabase({ connectionString }); + const authentication = betterAuth({ + baseURL, + database: adapter, + emailAndPassword: { + autoSignIn: false, + enabled: true, + }, + secret, + trustedOrigins: [baseURL, 'http://127.0.0.1:3020'], + }); + + const cleanup = Effect.fn('cleanupAuthenticationFixture')( + function* cleanupAuthenticationFixtureEffect() { + const tenantIds = Object.values(e2eTenants).map(({ tenantId }) => tenantId); + const principalIds = Object.values(e2eTenants).map(({ principalId }) => principalId); + // Authenticated shell reads write evidence asynchronously. Let those writes + // settle, then remove their E2E-owned rows before the referenced identities. + yield* Effect.sleep('250 millis'); + yield* coreDatabase + .delete(dataAccessEvents) + .where(inArray(dataAccessEvents.principalId, principalIds)); + const existingUsers = yield* authDatabase + .select({ id: user.id }) + .from(user) + .where(eq(user.email, e2eCredentials.email)); + + yield* Effect.all( + existingUsers.map((existingUser) => + Effect.gen(function* removeExistingAuthUser() { + yield* authDatabase.delete(session).where(eq(session.userId, existingUser.id)); + yield* authDatabase.delete(account).where(eq(account.userId, existingUser.id)); + yield* authDatabase.delete(user).where(eq(user.id, existingUser.id)); + }), + ), + { concurrency: 'unbounded', discard: true }, + ); + // A page read can finish its asynchronous evidence write while auth rows + // are being removed. Clear that final E2E-owned batch before deleting the + // binding referenced by the evidence foreign key. + yield* coreDatabase + .delete(dataAccessEvents) + .where(inArray(dataAccessEvents.principalId, principalIds)); + yield* Effect.all( + existingUsers.map((existingUser) => + coreDatabase + .delete(principalAuthBindings) + .where(eq(principalAuthBindings.providerSubjectId, existingUser.id)), + ), + { concurrency: 'unbounded', discard: true }, + ); + yield* coreDatabase .delete(principalAuthBindings) - .where(eq(principalAuthBindings.providerSubjectId, existingUser.id)), - ), + .where(inArray(principalAuthBindings.principalId, principalIds)); + yield* coreDatabase + .delete(tenantModuleStates) + .where(inArray(tenantModuleStates.tenantId, tenantIds)); + yield* coreDatabase.delete(legalEntities).where(inArray(legalEntities.tenantId, tenantIds)); + yield* coreDatabase.delete(principals).where(inArray(principals.principalId, principalIds)); + yield* coreDatabase.delete(tenants).where(inArray(tenants.tenantId, tenantIds)); + }, ); - const tenantIds = Object.values(e2eTenants).map(({ tenantId }) => tenantId); - const principalIds = Object.values(e2eTenants).map(({ principalId }) => principalId); - await coreDatabase - .delete(principalAuthBindings) - .where(inArray(principalAuthBindings.principalId, principalIds)); - await coreDatabase - .delete(tenantModuleStates) - .where(inArray(tenantModuleStates.tenantId, tenantIds)); - await coreDatabase.delete(legalEntities).where(inArray(legalEntities.tenantId, tenantIds)); - await coreDatabase.delete(principals).where(inArray(principals.principalId, principalIds)); - await coreDatabase.delete(tenants).where(inArray(tenants.tenantId, tenantIds)); - }; - await cleanup(); - const createdUser = await authentication.api.signUpEmail({ - body: { - email: e2eCredentials.email, - name: 'E2E user', - password: e2eCredentials.password, - }, - }); - await coreDatabase.insert(tenants).values([ - { - defaultLocale: 'en', - name: e2eTenants.first.name, - slug: 'e2e-alpha-tenant', - status: 'active', - tenantId: e2eTenants.first.tenantId, - }, - { - defaultLocale: 'en', - name: e2eTenants.second.name, - slug: 'e2e-zeta-tenant', - status: 'active', - tenantId: e2eTenants.second.tenantId, - }, - ]); - await coreDatabase.insert(principals).values([ - { - displayName: 'E2E user', - kind: 'human', - principalId: e2eTenants.first.principalId, - status: 'active', - tenantId: e2eTenants.first.tenantId, - }, - { - displayName: 'E2E user second tenant', - kind: 'human', - principalId: e2eTenants.second.principalId, - status: 'active', - tenantId: e2eTenants.second.tenantId, - }, - ]); - await coreDatabase.insert(legalEntities).values([ - { - legalEntityId: e2eTenants.first.legalEntityId, - legalName: 'E2E Alpha company', - registrationCountry: 'CZ', - registrationNumber: 'E2E-ALPHA', - status: 'active', - tenantId: e2eTenants.first.tenantId, - }, - { - legalEntityId: e2eTenants.second.legalEntityId, - legalName: 'E2E Zeta company', - registrationCountry: 'CZ', - registrationNumber: 'E2E-ZETA', - status: 'active', - tenantId: e2eTenants.second.tenantId, - }, - ]); - await coreDatabase.insert(principalAuthBindings).values([ - { - createdAt: new Date('2026-01-01T00:00:00.000Z'), - principalId: e2eTenants.first.principalId, - provider: 'better_auth', - providerSubjectId: createdUser.user.id, - status: 'active', - subjectType: 'user', - tenantId: e2eTenants.first.tenantId, - }, - { - createdAt: new Date('2026-02-01T00:00:00.000Z'), - principalId: e2eTenants.second.principalId, - provider: 'better_auth', - providerSubjectId: createdUser.user.id, - status: 'active', - subjectType: 'user', - tenantId: e2eTenants.second.tenantId, - }, - ]); - await coreDatabase.insert(tenantModuleStates).values([ - { moduleKey: 'party.registry', state: 'active', tenantId: e2eTenants.first.tenantId }, - { moduleKey: 'party.registry', state: 'active', tenantId: e2eTenants.second.tenantId }, - { moduleKey: 'e2e-first-module', state: 'active', tenantId: e2eTenants.first.tenantId }, - { moduleKey: 'e2e-second-module', state: 'active', tenantId: e2eTenants.second.tenantId }, - ]); - return async () => { - try { - await cleanup(); - } finally { - await Promise.all([authPool.end(), corePool.end()]); - } - }; -}; + yield* Effect.addFinalizer(() => cleanup().pipe(Effect.orDie)); + const createdUser = yield* Effect.tryPromise( + async () => + await authentication.api.signUpEmail({ + body: { + email: e2eCredentials.email, + name: 'E2E user', + password: e2eCredentials.password, + }, + }), + ).pipe(Effect.uninterruptible); + yield* coreDatabase.insert(tenants).values([ + { + defaultLocale: 'en', + name: e2eTenants.first.name, + slug: `e2e-alpha-${fixtureId}`, + status: 'active', + tenantId: e2eTenants.first.tenantId, + }, + { + defaultLocale: 'en', + name: e2eTenants.second.name, + slug: `e2e-zeta-${fixtureId}`, + status: 'active', + tenantId: e2eTenants.second.tenantId, + }, + ]); + yield* coreDatabase.insert(principals).values([ + { + displayName: 'E2E user', + kind: 'human', + principalId: e2eTenants.first.principalId, + status: 'active', + tenantId: e2eTenants.first.tenantId, + }, + { + displayName: 'E2E user second tenant', + kind: 'human', + principalId: e2eTenants.second.principalId, + status: 'active', + tenantId: e2eTenants.second.tenantId, + }, + ]); + yield* coreDatabase.insert(legalEntities).values([ + { + legalEntityId: e2eTenants.first.legalEntityId, + legalName: 'E2E Alpha company', + registrationCountry: 'CZ', + registrationNumber: 'E2E-ALPHA', + status: 'active', + tenantId: e2eTenants.first.tenantId, + }, + { + legalEntityId: e2eTenants.second.legalEntityId, + legalName: 'E2E Zeta company', + registrationCountry: 'CZ', + registrationNumber: 'E2E-ZETA', + status: 'active', + tenantId: e2eTenants.second.tenantId, + }, + ]); + yield* coreDatabase.insert(principalAuthBindings).values([ + { + createdAt: new Date('2026-01-01T00:00:00.000Z'), + principalId: e2eTenants.first.principalId, + provider: 'better_auth', + providerSubjectId: createdUser.user.id, + status: 'active', + subjectType: 'user', + tenantId: e2eTenants.first.tenantId, + }, + { + createdAt: new Date('2026-02-01T00:00:00.000Z'), + principalId: e2eTenants.second.principalId, + provider: 'better_auth', + providerSubjectId: createdUser.user.id, + status: 'active', + subjectType: 'user', + tenantId: e2eTenants.second.tenantId, + }, + ]); + yield* coreDatabase.insert(tenantModuleStates).values([ + { moduleKey: contactsModuleId, state: 'active', tenantId: e2eTenants.first.tenantId }, + { moduleKey: contactsModuleId, state: 'active', tenantId: e2eTenants.second.tenantId }, + { moduleKey: 'e2e-first-module', state: 'active', tenantId: e2eTenants.first.tenantId }, + { moduleKey: 'e2e-second-module', state: 'active', tenantId: e2eTenants.second.tenantId }, + ]); + yield* provisionContactsAccess(e2eTenants); + return { credentials: e2eCredentials, tenants: e2eTenants }; + }, + Effect.provide(NodeCrypto.layer), +); + +export type AuthenticationFixture = Effect.Success>; diff --git a/app/apps/shell-super-app/tests/e2e/login.spec.ts b/app/apps/shell-super-app/tests/e2e/login.spec.ts index 372580dd2..c38226a9c 100644 --- a/app/apps/shell-super-app/tests/e2e/login.spec.ts +++ b/app/apps/shell-super-app/tests/e2e/login.spec.ts @@ -1,11 +1,34 @@ -import { Predicate } from 'effect'; -import { expect, test } from '@playwright/test'; +import { Effect, Predicate } from 'effect'; +import { expect, test as base } from '@playwright/test'; import type { Page } from '@playwright/test'; import { shellAuthenticationApiContract } from '../../shared/api.ts'; -import { createAuthenticationFixture, e2eCredentials, e2eTenants } from './auth-fixture.ts'; +import { createAuthenticationFixture } from './auth-fixture.ts'; +import type { AuthenticationFixture } from './auth-fixture.ts'; const hydratedLoginForm = (page: Page) => page.locator('form[data-e2e-hydrated-login="true"]'); +// SSR already exposes a visible trigger. Zag's menu becomes interactive after hydration +// installs React props and moves its matching content portal to document.body. +const waitForInteractiveAccountMenu = async (page: Page) => { + await page.waitForFunction(() => { + const trigger = document.querySelector( + 'button[data-scope="menu"][data-part="trigger"]', + ); + if (trigger === null) { + return false; + } + if (!Object.keys(trigger).some((key) => key.startsWith('__reactProps$'))) { + return false; + } + const positioner = document.querySelector('[data-scope="menu"][data-part="positioner"]'); + const content = positioner?.querySelector('[data-scope="menu"][data-part="content"]'); + return ( + positioner?.parentElement === document.body && + content?.getAttribute('id') === trigger.getAttribute('aria-controls') + ); + }); +}; + const gotoHydratedLogin = async (page: Page, language: 'cs' | 'en') => { await page.goto(`/${language}/login`); await page.waitForFunction(() => { @@ -37,7 +60,11 @@ const gotoHydratedLogin = async (page: Page, language: 'cs' | 'en') => { }); }; -const login = async (page: Page, language: 'cs' | 'en') => { +const login = async ( + page: Page, + language: 'cs' | 'en', + credentials: AuthenticationFixture['credentials'], +) => { await gotoHydratedLogin(page, language); const form = hydratedLoginForm(page); const labels = @@ -49,22 +76,36 @@ const login = async (page: Page, language: 'cs' | 'en') => { submit: 'Přihlásit se', url: /\/cs\/?$/u, }; - await form.getByRole('textbox', { name: labels.login }).fill(e2eCredentials.email); - await form.getByLabel(labels.password).fill(e2eCredentials.password); + await form.getByRole('textbox', { name: labels.login }).fill(credentials.email); + await form.getByLabel(labels.password).fill(credentials.password); await form.getByRole('button', { name: labels.submit }).click(); await expect(page).toHaveURL(labels.url); }; -let cleanupFixture: (() => Promise) | undefined; - -test.beforeAll( - async () => - await createAuthenticationFixture().then((cleanup) => { - cleanupFixture = cleanup; - }), -); - -test.afterAll(async () => await cleanupFixture?.()); +// Only this real Playwright worker fixture runs Effect. Each worker owns its +// identities; Playwright still gives every test an independent browser context. +// +// Playwright's setup timeout abandons the callback before `use` and skips its teardown. +// Let Effect own the acquisition deadline and finish scoped cleanup instead; test-body +// timeouts stay enabled. Foreign operations retain their real server deadlines. +const workerFixtureAcquisitionTimeout = '25 seconds'; + +const test = base.extend, { authentication: AuthenticationFixture }>({ + authentication: [ + async ({ browserName: _browserName }, use) => { + await Effect.runPromise( + Effect.gen(function* useAuthenticationFixture() { + const fixture = yield* Effect.timeout( + createAuthenticationFixture(), + workerFixtureAcquisitionTimeout, + ); + yield* Effect.tryPromise(async () => await use(fixture)); + }).pipe(Effect.scoped), + ); + }, + { scope: 'worker', timeout: 0 }, + ], +}); test('renders the exact anonymous English and Czech home states', async ({ page }) => { const expectAnonymousHome = async (language: string, label: string) => { @@ -99,13 +140,13 @@ test('keeps English and Czech login pages free of authenticated dashboard chrome await expectDashboardAbsent(); }); -test('shows one generic error for invalid English credentials', async ({ page }) => +test('shows one generic error for invalid English credentials', async ({ authentication, page }) => await gotoHydratedLogin(page, 'en') .then( async () => await hydratedLoginForm(page) .getByRole('textbox', { name: /^Login\s*\*$/u }) - .fill(e2eCredentials.email), + .fill(authentication.credentials.email), ) .then( async () => @@ -122,7 +163,10 @@ test('shows one generic error for invalid English credentials', async ({ page }) ]), )); -test('logs a user in without any server-error response', async ({ page }, testInfo) => { +test('logs a user in without any server-error response', async ({ + authentication, + page, +}, testInfo) => { const { baseURL } = testInfo.project.use; if (!Predicate.isString(baseURL)) { throw new TypeError('The login E2E test requires a configured base URL'); @@ -146,8 +190,8 @@ test('logs a user in without any server-error response', async ({ page }, testIn await gotoHydratedLogin(page, 'en'); const form = hydratedLoginForm(page); - await form.getByRole('textbox', { name: /^Login\s*\*$/u }).fill(e2eCredentials.email); - await form.getByLabel(/^Password/u).fill(e2eCredentials.password); + await form.getByRole('textbox', { name: /^Login\s*\*$/u }).fill(authentication.credentials.email); + await form.getByLabel(/^Password/u).fill(authentication.credentials.password); const signInResponsePromise = page.waitForResponse( (response) => @@ -160,13 +204,16 @@ test('logs a user in without any server-error response', async ({ page }, testIn expect(signInResponse.status(), 'The sign-in endpoint should accept valid credentials').toBe(200); await expect(page).toHaveURL(/\/en\/?$/u); await expect(page.getByRole('button', { name: 'E2E user' })).toBeVisible(); - await expect(page.getByText(e2eCredentials.email)).toBeVisible(); + await expect(page.getByText(authentication.credentials.email)).toBeVisible(); await expect(page.getByRole('complementary', { name: 'Dashboard sidebar' })).toBeVisible(); await expect(page.locator('header[aria-label="Dashboard header"]')).toBeVisible(); expect(serverErrors, 'Login and the authenticated page must not return HTTP 5xx').toEqual([]); }); -test('loads localized English and Czech Contacts pages only after login', async ({ page }) => { +test('loads localized English and Czech Contacts pages only after login', async ({ + authentication, + page, +}) => { const pageErrors: string[] = []; page.on('pageerror', (error) => pageErrors.push(error.message)); @@ -175,7 +222,7 @@ test('loads localized English and Czech Contacts pages only after login', async await page.goto('/cs/contacts'); await expect(page.getByRole('heading', { name: 'Contacts' })).toHaveCount(0); - await login(page, 'cs'); + await login(page, 'cs', authentication.credentials); await expect(page.getByText('Nasazení modulu je dočasně nedostupné.')).toHaveCount(0); const contactsLink = page.locator('a[href="/cs/contacts"]'); @@ -235,9 +282,10 @@ test('loads localized English and Czech Contacts pages only after login', async }); test('keeps authenticated Shell chrome on search and guarded direct-target routes', async ({ + authentication, page, }) => { - await login(page, 'en'); + await login(page, 'en', authentication.credentials); const expectPersistentShell = async (path: string, status: string) => { await page.goto(path); @@ -257,6 +305,7 @@ test('keeps authenticated Shell chrome on search and guarded direct-target route }); test('persists an English session, logs out, clears the cookie, and stays anonymous', async ({ + authentication, page, }) => await gotoHydratedLogin(page, 'en') @@ -264,13 +313,13 @@ test('persists an English session, logs out, clears the cookie, and stays anonym async () => await hydratedLoginForm(page) .getByRole('textbox', { name: /^Login\s*\*$/u }) - .fill(e2eCredentials.email), + .fill(authentication.credentials.email), ) .then( async () => await hydratedLoginForm(page) .getByLabel(/^Password/u) - .fill(e2eCredentials.password), + .fill(authentication.credentials.password), ) .then(async () => await hydratedLoginForm(page).getByRole('button', { name: 'Login' }).click()) .then(async () => await expect(page).toHaveURL(/\/en\/?$/u)) @@ -278,11 +327,12 @@ test('persists an English session, logs out, clears the cookie, and stays anonym async () => await Promise.all([ expect(page.getByRole('button', { name: 'E2E user' })).toBeVisible(), - expect(page.getByText(e2eCredentials.email)).toBeVisible(), + expect(page.getByText(authentication.credentials.email)).toBeVisible(), expect(page.getByRole('link', { name: 'Home' })).toHaveCount(1), ]), ) .then(async () => await page.reload()) + .then(async () => await waitForInteractiveAccountMenu(page)) .then(async () => await expect(page.getByRole('button', { name: 'E2E user' })).toBeVisible()) .then(async () => await page.getByRole('button', { name: 'E2E user' }).click()) .then(async () => await page.getByRole('menuitem', { name: 'Logout' }).click()) @@ -300,13 +350,14 @@ test('persists an English session, logs out, clears the cookie, and stays anonym .then(async () => await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible())); test('switches tenant by pointer, fully reloads, and persists the selected context', async ({ + authentication, page, }) => { - await login(page, 'en'); + await login(page, 'en', authentication.credentials); const tenant = page.getByRole('combobox', { name: 'Current tenant' }); - await expect(tenant).toContainText(e2eTenants.first.name); - await expect(page.getByText(e2eTenants.first.tenantId)).toBeVisible(); + await expect(tenant).toContainText(authentication.tenants.first.name); + await expect(page.getByText(authentication.tenants.first.tenantId)).toBeVisible(); await tenant.click(); const switchResponsePromise = page.waitForResponse( (response) => @@ -315,29 +366,30 @@ test('switches tenant by pointer, fully reloads, and persists the selected conte ); await Promise.all([ page.waitForEvent('framenavigated', { predicate: (frame) => frame === page.mainFrame() }), - page.getByRole('option', { name: e2eTenants.second.name }).click(), + page.getByRole('option', { name: authentication.tenants.second.name }).click(), ]); const switchResponse = await switchResponsePromise; expect(switchResponse.status()).toBe(200); await expect(page.getByRole('combobox', { name: 'Current tenant' })).toContainText( - e2eTenants.second.name, + authentication.tenants.second.name, ); await expect(page.getByRole('button', { name: 'E2E user second tenant' })).toBeVisible(); - await expect(page.getByText(e2eTenants.second.principalId)).toBeVisible(); - await expect(page.getByText(e2eTenants.second.tenantId)).toBeVisible(); + await expect(page.getByText(authentication.tenants.second.principalId)).toBeVisible(); + await expect(page.getByText(authentication.tenants.second.tenantId)).toBeVisible(); await page.reload(); await expect(page.getByRole('combobox', { name: 'Current tenant' })).toContainText( - e2eTenants.second.name, + authentication.tenants.second.name, ); await expect(page.getByRole('button', { name: 'E2E user second tenant' })).toBeVisible(); }); test('retains Czech tenant context after one failed switch and supports keyboard retry', async ({ + authentication, page, }) => { let failSwitch = true; - await login(page, 'cs'); + await login(page, 'cs', authentication.credentials); await page.route(`**${shellAuthenticationApiContract.switchTenantPath}`, async (route) => { if (failSwitch) { failSwitch = false; @@ -348,16 +400,16 @@ test('retains Czech tenant context after one failed switch and supports keyboard }); const tenant = page.getByRole('combobox', { name: 'Aktuální tenant' }); - await expect(tenant).toContainText(e2eTenants.first.name); + await expect(tenant).toContainText(authentication.tenants.first.name); await tenant.click(); - await page.getByRole('option', { name: e2eTenants.second.name }).click(); + await page.getByRole('option', { name: authentication.tenants.second.name }).click(); await expect(page.getByText('Přepnutí tenantu selhalo. Zkuste to znovu.')).toBeVisible(); - await expect(tenant).toContainText(e2eTenants.first.name); - await expect(page.getByText(e2eTenants.first.tenantId)).toBeVisible(); + await expect(tenant).toContainText(authentication.tenants.first.name); + await expect(page.getByText(authentication.tenants.first.tenantId)).toBeVisible(); await tenant.focus(); await page.keyboard.press('Enter'); - const secondTenantOption = page.getByRole('option', { name: e2eTenants.second.name }); + const secondTenantOption = page.getByRole('option', { name: authentication.tenants.second.name }); await expect(secondTenantOption).toBeVisible(); const tenantListbox = page.getByRole('listbox'); await tenantListbox.press('End'); @@ -369,11 +421,12 @@ test('retains Czech tenant context after one failed switch and supports keyboard tenantListbox.press('Enter'), ]); await expect(page.getByRole('combobox', { name: 'Aktuální tenant' })).toContainText( - e2eTenants.second.name, + authentication.tenants.second.name, ); }); test('keeps keyboard logout operable after a Czech failure and succeeds on retry', async ({ + authentication, page, }) => { let failLogout = true; @@ -381,13 +434,15 @@ test('keeps keyboard logout operable after a Czech failure and succeeds on retry await gotoHydratedLogin(page, 'cs') .then( async () => - await hydratedLoginForm(page).locator('input[name="login"]').fill(e2eCredentials.email), + await hydratedLoginForm(page) + .locator('input[name="login"]') + .fill(authentication.credentials.email), ) .then( async () => await hydratedLoginForm(page) .locator('input[name="password"]') - .fill(e2eCredentials.password), + .fill(authentication.credentials.password), ) .then( async () => @@ -456,10 +511,11 @@ test('keeps the login form keyboard- and mobile-usable', async ({ page }) => { }); test('keeps the authenticated dashboard reachable without horizontal overflow at 375px', async ({ + authentication, page, }) => { await page.setViewportSize({ height: 667, width: 375 }); - await login(page, 'en'); + await login(page, 'en', authentication.credentials); await page.route( `**${shellAuthenticationApiContract.switchTenantPath}`, async (route) => await route.abort('failed'), @@ -473,11 +529,11 @@ test('keeps the authenticated dashboard reachable without horizontal overflow at const tenant = page.getByRole('combobox', { name: 'Current tenant' }); await expect(tenant).toBeInViewport(); await tenant.click(); - const secondTenant = page.getByRole('option', { name: e2eTenants.second.name }); + const secondTenant = page.getByRole('option', { name: authentication.tenants.second.name }); await expect(secondTenant).toBeInViewport(); await secondTenant.click(); await expect(page.getByText('Tenant switching failed. Try again.')).toBeInViewport(); - await expect(tenant).toContainText(e2eTenants.first.name); + await expect(tenant).toContainText(authentication.tenants.first.name); expect( await page.evaluate( () => document.documentElement.scrollWidth <= document.documentElement.clientWidth, diff --git a/app/apps/shell-super-app/tests/e2e/worker-fixture-lifetime.spec.ts b/app/apps/shell-super-app/tests/e2e/worker-fixture-lifetime.spec.ts new file mode 100644 index 000000000..9b15b4166 --- /dev/null +++ b/app/apps/shell-super-app/tests/e2e/worker-fixture-lifetime.spec.ts @@ -0,0 +1,41 @@ +import { Cause, Effect } from 'effect'; +import { expect, test as base } from '@playwright/test'; + +// Playwright must not abandon setup before Effect closes its scope. The stalled +// acquisition must finalize before reporting its typed timeout and must never complete. +const stalledAcquisitionDeadline = '250 millis'; + +const test = base.extend, { stalledAcquisition: readonly string[] }>({ + stalledAcquisition: [ + async ({ browserName: _browserName }, use) => { + const events: string[] = []; + // Mirrors the real fixture: finalizers are registered before the work that can stall. + const acquisition = Effect.gen(function* stalledAcquisitionEffect() { + yield* Effect.addFinalizer(() => + Effect.sync(() => { + events.push('finalizer'); + }), + ); + return yield* Effect.never; + }); + const failure = await Effect.runPromise( + Effect.flip( + Effect.gen(function* useStalledAcquisition() { + yield* Effect.timeout(acquisition, stalledAcquisitionDeadline); + events.push('acquired'); + }).pipe(Effect.scoped), + ), + ); + expect(failure).toBeInstanceOf(Cause.TimeoutError); + events.push('reported timeout'); + await use(events); + }, + { scope: 'worker', timeout: 0 }, + ], +}); + +test('finishes installed finalizers before reporting a stalled acquisition', ({ + stalledAcquisition, +}) => { + expect(stalledAcquisition).toEqual(['finalizer', 'reported timeout']); +}); diff --git a/app/apps/shell-super-app/tests/integration/auth-runtime.test.ts b/app/apps/shell-super-app/tests/integration/auth-runtime.test.ts index f9b490d0c..20e86e545 100644 --- a/app/apps/shell-super-app/tests/integration/auth-runtime.test.ts +++ b/app/apps/shell-super-app/tests/integration/auth-runtime.test.ts @@ -1,25 +1,17 @@ -import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.ts'; import { purgeFixtureRows } from '../../../../packages/core-runtime/tests/support/fixture-cleanup.ts'; -import { Scope as NativeScope, Exit as NativeExit, Effect, Layer, Predicate, Schema } from 'effect'; -import { - runEffectTestSync as runNativeSync, - makeEffectTestCallback as nativeTestCallback, - runEffectTestPromise, -} from '@app/core-runtime/testing/effect-runtime'; - -import assert from 'node:assert/strict'; -// @effect-diagnostics asyncFunction:off processEnv:off -- Existing compatibility boundary; expires: 2026-12-31. +import { expect, it } from 'effect-rstest'; +import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.ts'; +import { Effect, Layer, Predicate, Schema } from 'effect'; import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; -import test, { after as afterNativeDatabase } from 'node:test'; import { and, eq, inArray, sql } from 'drizzle-orm'; -import { makeAuthDatabase } from '../../api/auth/db/client.ts'; - +import { AuthDatabase, makeAuthDatabase } from '../../api/auth/db/client.ts'; import { exportJWK, generateKeyPair, jwtVerify } from 'jose'; import { Pool } from 'pg'; import { + PrincipalResolver, PrincipalResolverUnavailableError, ContextAccess, LegalEntityContext, @@ -43,7 +35,7 @@ import { tenantModuleStates, tenants, } from '../../../../packages/core-runtime/src/db/schema.ts'; -import { loadAuthConfig } from '../../api/auth/config.ts'; +import { AuthConfig, loadAuthConfig } from '../../api/auth/config.ts'; import { parseGatewayIssuerConfig } from '../../api/auth/gateway-issuer-config.ts'; import { makeGatewayIssuerLayer } from '../../api/auth/gateway-issuer.ts'; import type { GatewayIssuerLayerOptions } from '../../api/auth/gateway-issuer.ts'; @@ -52,8 +44,9 @@ import { AuthenticationService, makeAuthenticationService } from '../../api/auth import { makeShellAuthenticationApiRuntime } from '../../api/index.ts'; import { renderActionPrincipalServer } from '../../../../scripts/scaffolding/microvertical-action-boundary/scaffold.mts'; -const nativeDatabaseScope = runNativeSync(NativeScope.make()); - +type AuthenticationRuntimeHandler = ReturnType< + ReturnType['createHandler'] +>; const email = 'better-auth-runtime@example.test'; const password = 'correct-horse-battery-staple'; const tenantId = '30000000-0000-4000-8000-000000000001'; @@ -63,7 +56,6 @@ const appRoot = path.resolve(import.meta.dirname, '..', '..', '..', '..'); const fixtureLegalEntityId = '35000000-0000-4000-8000-000000000001'; const fixtureAuthBindingId = '45000000-0000-4000-8000-000000000001'; const PrincipalIdSchema = Schema.String.pipe(Schema.brand('PrincipalId')); - const IdentityResponseSchema = Schema.Struct({ identity: Schema.Struct({ email: Schema.String, principalId: PrincipalIdSchema }), }); @@ -74,7 +66,6 @@ const SessionResponseSchema = Schema.Struct({ const RetryableProblemSchema = Schema.Struct({ retryable: Schema.optional(Schema.Boolean) }); const TokenResponseSchema = Schema.Struct({ token: Schema.String }); const DefectProblemSchema = Schema.Struct({ detail: Schema.optional(Schema.String) }); - const legalEntitySelectionOptions = { contextAccess: { legalEntities: ({ legalEntityIds }: { readonly legalEntityIds: readonly string[] }) => @@ -93,58 +84,52 @@ const legalEntitySelectionOptions = { ? Effect.succeed({ legalEntityId, legalName: 'Fixture legal entity' }) : Effect.die('missing fixture legal entity'), }, - runResolverEffect: runEffectTestPromise, } as const; const contextAccessLayer = Layer.succeed(ContextAccess, legalEntitySelectionOptions.contextAccess); const authenticationContextLayer = Layer.mergeAll( contextAccessLayer, Layer.succeed(LegalEntityContext, legalEntitySelectionOptions.legalEntityContext), ); - const cookieHeader = (setCookieHeaders: readonly string[]) => setCookieHeaders.map((header) => header.split(';')[0]).join('; '); - const headerValue = (headers: Headers, name: string): string => headers.get(name) ?? ''; const optionalText = (value: string | undefined): string => value ?? ''; -/** - * Once a principal binding is revoked or removed, the very next session resolution must fail - * closed with the identity forbidden error rather than degrade to an anonymous or stale session. - */ -const assertSessionForbidden = async ( +/** A revoked or missing binding must fail closed on the next session resolution. */ +const assertSessionForbidden = Effect.fnUntraced(function* assertSessionForbidden( resolution: Effect.Effect, -): Promise => { - const failure = await runEffectTestPromise(Effect.flip(resolution)); - assert.ok(Schema.is(Schema.TaggedStruct('OntosIdentityForbiddenError', {}))(failure)); -}; - -/** - * Verifies an issued gateway assertion against the issuer key pair and decodes its trusted - * principal so each gateway test keeps its own expectations instead of the jose plumbing. - */ -const verifiedGatewayAssertion = async ( +) { + const failure = yield* Effect.flip(resolution); + expect(Predicate.isTagged(failure, 'OntosIdentityForbiddenError')).toBe(true); +}); +/** Verify the issued assertion while retaining each caller's principal expectations. */ +const verifiedGatewayAssertion = Effect.fnUntraced(function* verifiedGatewayAssertion( assertionResponse: Response, publicKey: Parameters[1], -): Promise<{ principal: typeof TrustedPrincipalContextSchema.Type; token: string }> => { - const assertion = Schema.decodeUnknownSync(TokenResponseSchema)(await assertionResponse.json()); - const verified = await jwtVerify(assertion.token, publicKey, { - algorithms: ['EdDSA'], - audience: 'inventory-stock', - currentDate: new Date(1_700_000_001_000), - issuer: 'https://shell.example.test', - }); +) { + const assertion = yield* Schema.decodeUnknownEffect(TokenResponseSchema)( + yield* Effect.tryPromise(() => assertionResponse.json()), + ); + const verified = yield* Effect.tryPromise(() => + jwtVerify(assertion.token, publicKey, { + algorithms: ['EdDSA'], + audience: 'inventory-stock', + currentDate: new Date(1_700_000_001_000), + issuer: 'https://shell.example.test', + }), + ); return { - principal: Schema.decodeUnknownSync(TrustedPrincipalContextSchema)( + principal: yield* Schema.decodeUnknownEffect(TrustedPrincipalContextSchema)( verified.payload['principal'], ), token: assertion.token, }; -}; +}); const assertOptionalField = ( value: Value | null | undefined, key: Key, expected: string | null, ): void => { - assert.equal(value?.[key], expected); + expect(value?.[key]).toBe(expected); }; const installedCatalog = (moduleIds: readonly string[]): InstalledModuleCatalog => @@ -157,7 +142,6 @@ const installedCatalog = (moduleIds: readonly string[]): InstalledModuleCatalog moduleIds: Object.freeze([...moduleIds]), outboxSubscriptions: Object.freeze([]), }); - const installedPageCatalog = (): InstalledModuleCatalog => buildInstalledModuleCatalog([ { @@ -256,66 +240,59 @@ const installedPageCatalog = (): InstalledModuleCatalog => expectedAppId: 'inventory-stock', }, ]); - -test('creates, resolves, persists, revokes, and signs out a Better Auth session', async () => { - const configuration = await runEffectTestPromise(loadAuthConfig()); - const corePool = new Pool({ connectionString: configuration.connectionString }); - const coreDatabase = await runEffectTestPromise( - makeTestDatabaseFromPool(corePool, coreRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ), - ); - const authPersistence = await runEffectTestPromise( - makeAuthDatabase(configuration).pipe(NativeScope.provide(nativeDatabaseScope)), - ); - const authDatabase = authPersistence.executor; - const resolver = makePrincipalResolver({ executor: coreDatabase }); - const authentication = makeAuthenticationService( - configuration, - authPersistence.adapter, - resolver, - { +it.live( + 'creates, resolves, persists, revokes, and signs out a Better Auth session', + Effect.fnUntraced(function* runIntegration1() { + const configuration = yield* loadAuthConfig(); + const corePool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: configuration.connectionString })), + (pool) => Effect.promise(() => pool.end()), + ); + const coreDatabase = yield* makeTestDatabaseFromPool(corePool, coreRelations); + const authPersistence = yield* makeAuthDatabase(configuration); + const authDatabase = authPersistence.executor; + const resolver = makePrincipalResolver({ executor: coreDatabase }); + const authentication = yield* makeAuthenticationService({ allowFixtureSignUp: true, - runResolverEffect: legalEntitySelectionOptions.runResolverEffect, - }, - ); - const authenticationLayer = Layer.succeed(AuthenticationService, authentication); - const moduleStateLayer = Layer.succeed( - TenantModuleStateService, - makeTenantModuleStateService({ executor: coreDatabase }), - ); - const handlers: { readonly dispose: () => Promise }[] = []; - const generatedFixtureRoot = await mkdtemp(path.join(tmpdir(), 'ontos-auth-runtime-')); - - const cleanup = async () => { - await runEffectTestPromise( - purgeFixtureRows([ + }).pipe( + Effect.provideService(AuthConfig, configuration), + Effect.provideService(AuthDatabase, authPersistence), + Effect.provideService(PrincipalResolver, resolver), + ); + const authenticationLayer = Layer.succeed(AuthenticationService, authentication); + const moduleStateLayer = Layer.succeed( + TenantModuleStateService, + makeTenantModuleStateService({ executor: coreDatabase }), + ); + const handlers: AuthenticationRuntimeHandler[] = []; + const generatedFixtureRoot = yield* Effect.tryPromise(() => + mkdtemp(path.join(tmpdir(), 'ontos-auth-runtime-')), + ); + const cleanup = Effect.fnUntraced(function* runIntegration2() { + yield* purgeFixtureRows([ coreDatabase.delete(dataAccessEvents).where(eq(dataAccessEvents.tenantId, tenantId)), coreDatabase.delete(auditEvents).where(eq(auditEvents.tenantId, tenantId)), coreDatabase.delete(actionInvocations).where(eq(actionInvocations.tenantId, tenantId)), - ]), - ); - const existingUsers = await runEffectTestPromise( - authDatabase.select({ id: user.id }).from(user).where(eq(user.email, email)), - ); - - await Promise.all( - existingUsers.map(async (existingUser) => { - await runEffectTestPromise( - purgeFixtureRows([ - coreDatabase - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.providerSubjectId, existingUser.id)), - authDatabase.delete(session).where(eq(session.userId, existingUser.id)), - authDatabase.delete(account).where(eq(account.userId, existingUser.id)), - authDatabase.delete(user).where(eq(user.id, existingUser.id)), - ]), - ); - }), - ); - - await runEffectTestPromise( - purgeFixtureRows([ + ]); + const existingUsers = yield* authDatabase + .select({ id: user.id }) + .from(user) + .where(eq(user.email, email)); + yield* Effect.all( + existingUsers.map( + Effect.fnUntraced(function* runIntegration3(existingUser) { + yield* purgeFixtureRows([ + coreDatabase + .delete(principalAuthBindings) + .where(eq(principalAuthBindings.providerSubjectId, existingUser.id)), + authDatabase.delete(session).where(eq(session.userId, existingUser.id)), + authDatabase.delete(account).where(eq(account.userId, existingUser.id)), + authDatabase.delete(user).where(eq(user.id, existingUser.id)), + ]); + }), + ), + ); + yield* purgeFixtureRows([ coreDatabase .delete(principalAuthBindings) .where(eq(principalAuthBindings.principalId, principalId)), @@ -329,81 +306,80 @@ test('creates, resolves, persists, revokes, and signs out a Better Auth session' .where(eq(legalEntities.legalEntityId, fixtureLegalEntityId)), coreDatabase.delete(tenants).where(eq(tenants.tenantId, tenantId)), coreDatabase.delete(tenants).where(eq(tenants.tenantId, foreignTenantId)), - ]), - ); - }; - - try { - await cleanup(); - const betterAuthUserId = await runEffectTestPromise( - authentication.createFixtureUser(email, 'Runtime fixture', password), - ); - await runEffectTestPromise( - coreDatabase.insert(tenants).values({ - defaultLocale: 'en', - name: 'Authentication runtime tenant', - slug: 'authentication-runtime-tenant', - status: 'active', - tenantId, - }), - ); - await runEffectTestPromise( - coreDatabase.insert(tenants).values({ - defaultLocale: 'en', - name: 'Foreign authentication runtime tenant', - slug: 'foreign-authentication-runtime-tenant', - status: 'active', - tenantId: foreignTenantId, - }), - ); - await runEffectTestPromise( - coreDatabase.insert(principals).values({ - displayName: 'Runtime fixture', - kind: 'human', - principalId, - status: 'active', - tenantId, - }), - ); - await runEffectTestPromise( - coreDatabase.insert(legalEntities).values({ - legalEntityId: fixtureLegalEntityId, - legalName: 'Fixture legal entity', - registrationCountry: 'CZ', - registrationNumber: 'AUTH-RUNTIME-1', - status: 'active', - tenantId, - }), - ); - await runEffectTestPromise( - coreDatabase.insert(principalAuthBindings).values({ - principalAuthBindingId: fixtureAuthBindingId, - principalId, - provider: 'better_auth', - providerSubjectId: betterAuthUserId, - status: 'active', - subjectType: 'user', - tenantId, - }), - ); - await runEffectTestPromise( - coreDatabase.insert(tenantModuleStates).values([ - { moduleKey: 'testing1', state: 'active', tenantId }, - { moduleKey: 'testing.pages', state: 'active', tenantId }, - { moduleKey: 'stale-non-installed', state: 'active', tenantId }, - { moduleKey: 'inactive-installed', state: 'suspended', tenantId }, - { moduleKey: 'testing1', state: 'active', tenantId: foreignTenantId }, - ]), - ); - + ]); + }); + yield* Effect.acquireRelease( + Effect.void, + Effect.fnUntraced(function* integrationEffect4() { + yield* Effect.all( + handlers.map( + Effect.fnUntraced(function* runIntegration5({ dispose }) { + return yield* Effect.tryPromise(() => dispose()); + }), + ), + ); + yield* Effect.tryPromise(() => rm(generatedFixtureRoot, { force: true, recursive: true })); + yield* cleanup(); + }, Effect.orDie), + ); + yield* cleanup(); + const betterAuthUserId = yield* authentication.createFixtureUser( + email, + 'Runtime fixture', + password, + ); + yield* coreDatabase.insert(tenants).values({ + defaultLocale: 'en', + name: 'Authentication runtime tenant', + slug: 'authentication-runtime-tenant', + status: 'active', + tenantId, + }); + yield* coreDatabase.insert(tenants).values({ + defaultLocale: 'en', + name: 'Foreign authentication runtime tenant', + slug: 'foreign-authentication-runtime-tenant', + status: 'active', + tenantId: foreignTenantId, + }); + yield* coreDatabase.insert(principals).values({ + displayName: 'Runtime fixture', + kind: 'human', + principalId, + status: 'active', + tenantId, + }); + yield* coreDatabase.insert(legalEntities).values({ + legalEntityId: fixtureLegalEntityId, + legalName: 'Fixture legal entity', + registrationCountry: 'CZ', + registrationNumber: 'AUTH-RUNTIME-1', + status: 'active', + tenantId, + }); + yield* coreDatabase.insert(principalAuthBindings).values({ + principalAuthBindingId: fixtureAuthBindingId, + principalId, + provider: 'better_auth', + providerSubjectId: betterAuthUserId, + status: 'active', + subjectType: 'user', + tenantId, + }); + yield* coreDatabase.insert(tenantModuleStates).values([ + { moduleKey: 'testing1', state: 'active', tenantId }, + { moduleKey: 'testing.pages', state: 'active', tenantId }, + { moduleKey: 'stale-non-installed', state: 'active', tenantId }, + { moduleKey: 'inactive-installed', state: 'suspended', tenantId }, + { moduleKey: 'testing1', state: 'active', tenantId: foreignTenantId }, + ]); const requestHeaders = new Headers({ origin: configuration.baseUrl, }); - const invalid = await runEffectTestPromise( - Effect.flip(authentication.signIn(email, 'wrong-password', requestHeaders)), + const invalid = yield* Effect.flip( + authentication.signIn(email, 'wrong-password', requestHeaders), ); - assert.ok(Schema.is(Schema.TaggedStruct('InvalidCredentialsError', {}))(invalid)); - + expect(Predicate.isTagged(invalid, 'InvalidCredentialsError')).toBe(true); const anonymousRuntime = makeShellAuthenticationApiRuntime( authenticationLayer, makeGatewayIssuerLayer({ @@ -419,50 +395,57 @@ test('creates, resolves, persists, revokes, and signs out a Better Auth session' ); const unavailableHandler = anonymousRuntime.createHandler(); handlers.push(unavailableHandler); - const anonymousGatewayResponse = await unavailableHandler.handler( - new Request(`${configuration.baseUrl}/auth/gateway-context`, { - body: JSON.stringify({ audience: 'inventory-stock' }), - headers: { 'content-type': 'application/json', origin: configuration.baseUrl }, - method: 'POST', - }), + const anonymousGatewayResponse = yield* Effect.tryPromise(() => + unavailableHandler.handler( + new Request(`${configuration.baseUrl}/auth/gateway-context`, { + body: JSON.stringify({ audience: 'inventory-stock' }), + headers: { 'content-type': 'application/json', origin: configuration.baseUrl }, + method: 'POST', + }), + ), ); - assert.equal(anonymousGatewayResponse.status, 401); - assert.match(headerValue(anonymousGatewayResponse.headers, 'www-authenticate'), /^Bearer/u); - - const anonymousModulesResponse = await unavailableHandler.handler( - new Request(`${configuration.baseUrl}/shell/composition`, { - headers: { origin: configuration.baseUrl }, - }), + expect(anonymousGatewayResponse.status).toBe(401); + expect(headerValue(anonymousGatewayResponse.headers, 'www-authenticate')).toMatch(/^Bearer/u); + const anonymousModulesResponse = yield* Effect.tryPromise(() => + unavailableHandler.handler( + new Request(`${configuration.baseUrl}/shell/composition`, { + headers: { origin: configuration.baseUrl }, + }), + ), ); - assert.equal(anonymousModulesResponse.status, 401); - assert.match(headerValue(anonymousModulesResponse.headers, 'www-authenticate'), /^Bearer/u); - const anonymousPageResponse = await unavailableHandler.handler( - new Request(`${configuration.baseUrl}/shell/module-target`, { - body: JSON.stringify({ - entrypointKey: 'testing.pages.page.customers', - moduleId: 'testing.pages', + expect(anonymousModulesResponse.status).toBe(401); + expect(headerValue(anonymousModulesResponse.headers, 'www-authenticate')).toMatch(/^Bearer/u); + const anonymousPageResponse = yield* Effect.tryPromise(() => + unavailableHandler.handler( + new Request(`${configuration.baseUrl}/shell/module-target`, { + body: JSON.stringify({ + entrypointKey: 'testing.pages.page.customers', + moduleId: 'testing.pages', + }), + headers: { 'content-type': 'application/json', origin: configuration.baseUrl }, + method: 'POST', }), - headers: { 'content-type': 'application/json', origin: configuration.baseUrl }, - method: 'POST', - }), + ), ); - assert.equal(anonymousPageResponse.status, 401); - assert.match(headerValue(anonymousPageResponse.headers, 'www-authenticate'), /^Bearer/u); - - const signInResponse = await unavailableHandler.handler( - new Request(`${configuration.baseUrl}/auth/sign-in`, { - body: JSON.stringify({ email, password }), - headers: { 'content-type': 'application/json', origin: configuration.baseUrl }, - method: 'POST', - }), + expect(anonymousPageResponse.status).toBe(401); + expect(headerValue(anonymousPageResponse.headers, 'www-authenticate')).toMatch(/^Bearer/u); + const signInResponse = yield* Effect.tryPromise(() => + unavailableHandler.handler( + new Request(`${configuration.baseUrl}/auth/sign-in`, { + body: JSON.stringify({ email, password }), + headers: { 'content-type': 'application/json', origin: configuration.baseUrl }, + method: 'POST', + }), + ), + ); + expect(signInResponse.status).toBe(200); + const signedIn = Schema.decodeUnknownSync(IdentityResponseSchema)( + yield* Effect.tryPromise(() => signInResponse.json()), ); - assert.equal(signInResponse.status, 200); - const signedIn = Schema.decodeUnknownSync(IdentityResponseSchema)(await signInResponse.json()); const signedInCookies = signInResponse.headers.getSetCookie(); - assert.equal(signedIn.identity.email, email); - assert.equal(signedIn.identity.principalId, principalId); - assert.ok(signedInCookies.length > 0); - + expect(signedIn.identity.email).toBe(email); + expect(signedIn.identity.principalId).toBe(principalId); + expect(signedInCookies.length > 0).toBe(true); const authenticatedHeaders = new Headers({ cookie: cookieHeader(signedInCookies), origin: configuration.baseUrl, @@ -477,14 +460,11 @@ test('creates, resolves, persists, revokes, and signs out a Better Auth session' }), method: 'POST', }); - const current = await runEffectTestPromise( - authentication - .currentSession(authenticatedHeaders) - .pipe(Effect.provide(authenticationContextLayer)), - ); + const current = yield* authentication + .currentSession(authenticatedHeaders) + .pipe(Effect.provide(authenticationContextLayer)); assertOptionalField(current.identity, 'tenantId', tenantId); - assert.notEqual(current.identity, undefined); - + expect(current.identity).not.toBe(undefined); const pageRuntime = makeShellAuthenticationApiRuntime( authenticationLayer, makeGatewayIssuerLayer({ @@ -499,26 +479,25 @@ test('creates, resolves, persists, revokes, and signs out a Better Auth session' contextAccessLayer, ).createHandler(); handlers.push(pageRuntime); - const exactPageResponse = await pageRuntime.handler(exactPageRequest()); - assert.equal(exactPageResponse.status, 200); - assert.deepEqual(await exactPageResponse.json(), { + const exactPageResponse = yield* Effect.tryPromise(() => + pageRuntime.handler(exactPageRequest()), + ); + expect(exactPageResponse.status).toBe(200); + expect(yield* Effect.tryPromise(() => exactPageResponse.json())).toEqual({ appId: 'inventory-stock', componentKey: 'testing.pages.page-customers', entrypointKey: 'testing.pages.page.customers', moduleId: 'testing.pages', writable: true, }); - const missingPageResponse = await pageRuntime.handler( - exactPageRequest('testing.pages.page.missing'), - ); - assert.equal(missingPageResponse.status, 404); - - const authenticatedContext = await runEffectTestPromise( - authentication - .resolveShellContext(authenticatedHeaders) - .pipe(Effect.provide(authenticationContextLayer)), - ); - assert.equal(authenticatedContext.state, 'authenticated'); + const missingPageResponse = yield* Effect.tryPromise(() => + pageRuntime.handler(exactPageRequest('testing.pages.page.missing')), + ); + expect(missingPageResponse.status).toBe(404); + const authenticatedContext = yield* authentication + .resolveShellContext(authenticatedHeaders) + .pipe(Effect.provide(authenticationContextLayer)); + expect(authenticatedContext.state).toBe('authenticated'); if (authenticatedContext.state !== 'authenticated') { throw new Error('The exact-page boundary fixture must resolve an authenticated context'); } @@ -551,15 +530,15 @@ test('creates, resolves, persists, revokes, and signs out a Better Auth session' contextAccessLayer, ).createHandler(); handlers.push(selectionRequiredRuntime); - const selectionRequiredPageResponse = - await selectionRequiredRuntime.handler(exactPageRequest()); - assert.equal(selectionRequiredPageResponse.status, 409); - assert.equal( - Schema.decodeUnknownSync(ProblemStatusSchema)(await selectionRequiredPageResponse.json()) - .status, - 409, - ); - + const selectionRequiredPageResponse = yield* Effect.tryPromise(() => + selectionRequiredRuntime.handler(exactPageRequest()), + ); + expect(selectionRequiredPageResponse.status).toBe(409); + expect( + Schema.decodeUnknownSync(ProblemStatusSchema)( + yield* Effect.tryPromise(() => selectionRequiredPageResponse.json()), + ).status, + ).toBe(409); const deniedPageRuntime = makeShellAuthenticationApiRuntime( authenticationLayer, makeGatewayIssuerLayer({ @@ -578,38 +557,42 @@ test('creates, resolves, persists, revokes, and signs out a Better Auth session' }), ).createHandler(); handlers.push(deniedPageRuntime); - const deniedPageResponse = await deniedPageRuntime.handler(exactPageRequest()); - assert.equal(deniedPageResponse.status, 403); - assert.equal( - Schema.decodeUnknownSync(ProblemStatusSchema)(await deniedPageResponse.json()).status, - 403, - ); - - const currentSessionResponse = await unavailableHandler.handler( - new Request(`${configuration.baseUrl}/auth/session`, { - headers: authenticatedHeaders, - }), - ); - assert.equal(currentSessionResponse.status, 200); - assert.equal( - Schema.decodeUnknownSync(SessionResponseSchema)(await currentSessionResponse.json()).identity - ?.principalId, - principalId, + const deniedPageResponse = yield* Effect.tryPromise(() => + deniedPageRuntime.handler(exactPageRequest()), + ); + expect(deniedPageResponse.status).toBe(403); + expect( + Schema.decodeUnknownSync(ProblemStatusSchema)( + yield* Effect.tryPromise(() => deniedPageResponse.json()), + ).status, + ).toBe(403); + const currentSessionResponse = yield* Effect.tryPromise(() => + unavailableHandler.handler( + new Request(`${configuration.baseUrl}/auth/session`, { + headers: authenticatedHeaders, + }), + ), ); - - const missingIdempotencyResponse = await unavailableHandler.handler( - new Request(`${configuration.baseUrl}/auth/identity/api-keys/self`, { - body: JSON.stringify({ name: 'must-not-be-created' }), - headers: { - 'content-type': 'application/json', - cookie: headerValue(authenticatedHeaders, 'cookie'), - origin: configuration.baseUrl, - }, - method: 'POST', - }), + expect(currentSessionResponse.status).toBe(200); + expect( + Schema.decodeUnknownSync(SessionResponseSchema)( + yield* Effect.tryPromise(() => currentSessionResponse.json()), + ).identity?.principalId, + ).toBe(principalId); + const missingIdempotencyResponse = yield* Effect.tryPromise(() => + unavailableHandler.handler( + new Request(`${configuration.baseUrl}/auth/identity/api-keys/self`, { + body: JSON.stringify({ name: 'must-not-be-created' }), + headers: { + 'content-type': 'application/json', + cookie: headerValue(authenticatedHeaders, 'cookie'), + origin: configuration.baseUrl, + }, + method: 'POST', + }), + ), ); - assert.equal(missingIdempotencyResponse.status, 428); - + expect(missingIdempotencyResponse.status).toBe(428); const deniedIdentityAdministrationRuntime = makeShellAuthenticationApiRuntime( authenticationLayer, makeGatewayIssuerLayer({ @@ -648,78 +631,75 @@ test('creates, resolves, persists, revokes, and signs out a Better Auth session' }), ).createHandler(); handlers.push(deniedIdentityAdministrationRuntime); - const deniedIdentityAdministrationResponse = await deniedIdentityAdministrationRuntime.handler( - new Request(`${configuration.baseUrl}/auth/identity/principals`, { - body: JSON.stringify({ displayName: 'Denied managed identity', kind: 'service' }), - headers: { - 'content-type': 'application/json', - cookie: headerValue(authenticatedHeaders, 'cookie'), - 'idempotency-key': 'denied-managed-identity', - origin: configuration.baseUrl, - }, - method: 'POST', - }), - ); - assert.equal(deniedIdentityAdministrationResponse.status, 403); - const [deniedIdentityInvocation] = await runEffectTestPromise( - coreDatabase - .select({ - actionInvocationId: actionInvocations.actionInvocationId, - status: actionInvocations.status, - }) - .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, 'denied-managed-identity')) - .limit(1), + const deniedIdentityAdministrationResponse = yield* Effect.tryPromise(() => + deniedIdentityAdministrationRuntime.handler( + new Request(`${configuration.baseUrl}/auth/identity/principals`, { + body: JSON.stringify({ displayName: 'Denied managed identity', kind: 'service' }), + headers: { + 'content-type': 'application/json', + cookie: headerValue(authenticatedHeaders, 'cookie'), + 'idempotency-key': 'denied-managed-identity', + origin: configuration.baseUrl, + }, + method: 'POST', + }), + ), ); + expect(deniedIdentityAdministrationResponse.status).toBe(403); + const [deniedIdentityInvocation] = yield* coreDatabase + .select({ + actionInvocationId: actionInvocations.actionInvocationId, + status: actionInvocations.status, + }) + .from(actionInvocations) + .where(eq(actionInvocations.idempotencyKey, 'denied-managed-identity')) + .limit(1); assertOptionalField(deniedIdentityInvocation, 'status', 'rejected'); if (deniedIdentityInvocation === undefined) { throw new Error('The denied identity Action did not persist its invocation'); } - const [deniedIdentityAudit] = await runEffectTestPromise( - coreDatabase - .select({ - eventType: auditEvents.eventType, - outcomeCode: auditEvents.outcomeCode, - }) - .from(auditEvents) - .where(eq(auditEvents.actionInvocationId, deniedIdentityInvocation.actionInvocationId)) - .limit(1), - ); - assert.deepEqual(deniedIdentityAudit, { + const [deniedIdentityAudit] = yield* coreDatabase + .select({ + eventType: auditEvents.eventType, + outcomeCode: auditEvents.outcomeCode, + }) + .from(auditEvents) + .where(eq(auditEvents.actionInvocationId, deniedIdentityInvocation.actionInvocationId)) + .limit(1); + expect(deniedIdentityAudit).toEqual({ eventType: 'action.rejected', outcomeCode: 'spicedb_permission_denied', }); - - const activeModulesResponse = await unavailableHandler.handler( - new Request(`${configuration.baseUrl}/shell/composition`, { - headers: authenticatedHeaders, - }), + const activeModulesResponse = yield* Effect.tryPromise(() => + unavailableHandler.handler( + new Request(`${configuration.baseUrl}/shell/composition`, { + headers: authenticatedHeaders, + }), + ), ); - assert.equal(activeModulesResponse.status, 200); - assert.deepEqual(await activeModulesResponse.json(), { + expect(activeModulesResponse.status).toBe(200); + expect(yield* Effect.tryPromise(() => activeModulesResponse.json())).toEqual({ navigation: [], state: 'available', unavailableDeployments: [], }); - const [compositionEvidence] = await runEffectTestPromise( - coreDatabase - .select({ - authBindingId: dataAccessEvents.authBindingId, - evidencePayloadJson: dataAccessEvents.evidencePayloadJson, - outcome: dataAccessEvents.outcome, - outcomeCode: dataAccessEvents.outcomeCode, - queryHash: dataAccessEvents.queryHash, - resultCount: dataAccessEvents.resultCount, - }) - .from(dataAccessEvents) - .where( - and( - eq(dataAccessEvents.tenantId, tenantId), - eq(dataAccessEvents.evidencePolicyKey, 'core.shell.composition.evidence.v1'), - ), + const [compositionEvidence] = yield* coreDatabase + .select({ + authBindingId: dataAccessEvents.authBindingId, + evidencePayloadJson: dataAccessEvents.evidencePayloadJson, + outcome: dataAccessEvents.outcome, + outcomeCode: dataAccessEvents.outcomeCode, + queryHash: dataAccessEvents.queryHash, + resultCount: dataAccessEvents.resultCount, + }) + .from(dataAccessEvents) + .where( + and( + eq(dataAccessEvents.tenantId, tenantId), + eq(dataAccessEvents.evidencePolicyKey, 'core.shell.composition.evidence.v1'), ), - ); - assert.deepEqual(compositionEvidence, { + ); + expect(compositionEvidence).toEqual({ authBindingId: fixtureAuthBindingId, evidencePayloadJson: null, outcome: 'allowed', @@ -727,7 +707,6 @@ test('creates, resolves, persists, revokes, and signs out a Better Auth session' queryHash: null, resultCount: 0, }); - const refreshingRuntime = makeShellAuthenticationApiRuntime( Layer.succeed(AuthenticationService, { ...authentication, @@ -778,16 +757,15 @@ test('creates, resolves, persists, revokes, and signs out a Better Auth session' contextAccessLayer, ).createHandler(); handlers.push(refreshingRuntime); - const refreshedModulesResponse = await refreshingRuntime.handler( - new Request(`${configuration.baseUrl}/shell/composition`), + const refreshedModulesResponse = yield* Effect.tryPromise(() => + refreshingRuntime.handler(new Request(`${configuration.baseUrl}/shell/composition`)), ); - assert.equal(refreshedModulesResponse.status, 200); - assert.ok( + expect(refreshedModulesResponse.status).toBe(200); + expect( refreshedModulesResponse.headers .getSetCookie() .some((header) => header.startsWith('refreshed-session=value')), - ); - + ).toBe(true); const unavailableModuleStates = { getTenantModuleStates: () => Effect.fail( @@ -827,46 +805,54 @@ test('creates, resolves, persists, revokes, and signs out a Better Auth session' () => unavailableModuleStates, ).createHandler(); handlers.push(unavailableModulesHandler); - const unavailableModulesResponse = await unavailableModulesHandler.handler( - new Request(`${configuration.baseUrl}/shell/composition`, { - headers: authenticatedHeaders, - }), - ); - assert.equal(unavailableModulesResponse.status, 503); - const unavailableModulesProblem = await unavailableModulesResponse.text(); - assert.doesNotMatch(unavailableModulesProblem, /SQL|30000000|40000000/u); - const unavailablePageResponse = await unavailableModulesHandler.handler(exactPageRequest()); - assert.equal(unavailablePageResponse.status, 503); - assert.equal( - Schema.decodeUnknownSync(ProblemStatusSchema)(await unavailablePageResponse.json()).status, - 503, + const unavailableModulesResponse = yield* Effect.tryPromise(() => + unavailableModulesHandler.handler( + new Request(`${configuration.baseUrl}/shell/composition`, { + headers: authenticatedHeaders, + }), + ), ); - - const unavailableGatewayResponse = await unavailableHandler.handler( - new Request(`${configuration.baseUrl}/auth/gateway-context`, { - body: JSON.stringify({ audience: 'inventory-stock' }), - headers: new Headers({ - 'content-type': 'application/json', - cookie: cookieHeader(signedInCookies), - origin: configuration.baseUrl, + expect(unavailableModulesResponse.status).toBe(503); + const unavailableModulesProblem = yield* Effect.tryPromise(() => + unavailableModulesResponse.text(), + ); + expect(unavailableModulesProblem).not.toMatch(/SQL|30000000|40000000/u); + const unavailablePageResponse = yield* Effect.tryPromise(() => + unavailableModulesHandler.handler(exactPageRequest()), + ); + expect(unavailablePageResponse.status).toBe(503); + expect( + Schema.decodeUnknownSync(ProblemStatusSchema)( + yield* Effect.tryPromise(() => unavailablePageResponse.json()), + ).status, + ).toBe(503); + const unavailableGatewayResponse = yield* Effect.tryPromise(() => + unavailableHandler.handler( + new Request(`${configuration.baseUrl}/auth/gateway-context`, { + body: JSON.stringify({ audience: 'inventory-stock' }), + headers: new Headers({ + 'content-type': 'application/json', + cookie: cookieHeader(signedInCookies), + origin: configuration.baseUrl, + }), + method: 'POST', }), - method: 'POST', - }), + ), ); - assert.equal(unavailableGatewayResponse.status, 503); - assert.match( - headerValue(unavailableGatewayResponse.headers, 'content-type'), + expect(unavailableGatewayResponse.status).toBe(503); + expect(headerValue(unavailableGatewayResponse.headers, 'content-type')).toMatch( /application\/problem\+json/u, ); - assert.equal( - Schema.decodeUnknownSync(RetryableProblemSchema)(await unavailableGatewayResponse.json()) - .retryable, - true, + expect( + Schema.decodeUnknownSync(RetryableProblemSchema)( + yield* Effect.tryPromise(() => unavailableGatewayResponse.json()), + ).retryable, + ).toBe(true); + const pair = yield* Effect.tryPromise(() => + generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true }), ); - - const pair = await generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true }); - const privateJwk = await exportJWK(pair.privateKey); - const publicJwk = await exportJWK(pair.publicKey); + const privateJwk = yield* Effect.tryPromise(() => exportJWK(pair.privateKey)); + const publicJwk = yield* Effect.tryPromise(() => exportJWK(pair.publicKey)); const issuerDependencies: GatewayIssuerLayerOptions = { currentTimeSeconds: Effect.succeed(1_700_000_000), generateJti: Effect.succeed('60000000-0000-4000-8000-000000000001'), @@ -893,105 +879,137 @@ test('creates, resolves, persists, revokes, and signs out a Better Auth session' contextAccessLayer, ).createHandler(); handlers.push(issuingHandler); - const assertionResponse = await issuingHandler.handler( - new Request(`${configuration.baseUrl}/auth/gateway-context`, { - body: JSON.stringify({ audience: 'inventory-stock' }), - headers: new Headers({ - 'content-type': 'application/json', - cookie: cookieHeader(signedInCookies), - origin: configuration.baseUrl, + const assertionResponse = yield* Effect.tryPromise(() => + issuingHandler.handler( + new Request(`${configuration.baseUrl}/auth/gateway-context`, { + body: JSON.stringify({ audience: 'inventory-stock' }), + headers: new Headers({ + 'content-type': 'application/json', + cookie: cookieHeader(signedInCookies), + origin: configuration.baseUrl, + }), + method: 'POST', }), - method: 'POST', - }), + ), ); - assert.equal(assertionResponse.status, 200, await assertionResponse.clone().text()); - const { principal: verifiedPrincipal, token: assertionToken } = await verifiedGatewayAssertion( + expect( + assertionResponse.status, + yield* Effect.tryPromise(() => assertionResponse.clone().text()), + ).toBe(200); + const { principal: verifiedPrincipal, token: assertionToken } = yield* verifiedGatewayAssertion( assertionResponse, pair.publicKey, ); - assert.equal(verifiedPrincipal.authBindingId, fixtureAuthBindingId); - assert.match(optionalText(verifiedPrincipal.authContextRef), /^better-auth-session:/u); - assert.equal(verifiedPrincipal.authMethod, 'session'); - assert.equal(verifiedPrincipal.legalEntityId, fixtureLegalEntityId); - assert.equal(verifiedPrincipal.principalId, principalId); - assert.equal(verifiedPrincipal.tenantId, tenantId); - - await mkdir(path.join(generatedFixtureRoot, 'node_modules', '@app'), { recursive: true }); - await symlink( - path.join(appRoot, 'packages/core-runtime'), - path.join(generatedFixtureRoot, 'node_modules/@app/core-runtime'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/shared-contracts'), - path.join(generatedFixtureRoot, 'node_modules/@app/shared-contracts'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/gateway-principal-verifier'), - path.join(generatedFixtureRoot, 'node_modules/@app/gateway-principal-verifier'), - 'dir', - ); - await symlink( - path.join(appRoot, 'node_modules/effect'), - path.join(generatedFixtureRoot, 'node_modules/effect'), - 'dir', - ); - await symlink( - path.join(appRoot, 'apps/shell-super-app/node_modules/jose'), - path.join(generatedFixtureRoot, 'node_modules/jose'), - 'dir', + expect(verifiedPrincipal.authBindingId).toBe(fixtureAuthBindingId); + expect(optionalText(verifiedPrincipal.authContextRef)).toMatch(/^better-auth-session:/u); + expect(verifiedPrincipal.authMethod).toBe('session'); + expect(verifiedPrincipal.legalEntityId).toBe(fixtureLegalEntityId); + expect(verifiedPrincipal.principalId).toBe(principalId); + expect(verifiedPrincipal.tenantId).toBe(tenantId); + yield* Effect.tryPromise(() => + mkdir(path.join(generatedFixtureRoot, 'node_modules', '@app'), { recursive: true }), + ); + yield* Effect.tryPromise(() => + symlink( + path.join(appRoot, 'packages/core-runtime'), + path.join(generatedFixtureRoot, 'node_modules/@app/core-runtime'), + 'dir', + ), + ); + yield* Effect.tryPromise(() => + symlink( + path.join(appRoot, 'packages/shared-contracts'), + path.join(generatedFixtureRoot, 'node_modules/@app/shared-contracts'), + 'dir', + ), + ); + yield* Effect.tryPromise(() => + symlink( + path.join(appRoot, 'packages/gateway-principal-verifier'), + path.join(generatedFixtureRoot, 'node_modules/@app/gateway-principal-verifier'), + 'dir', + ), + ); + yield* Effect.tryPromise(() => + symlink( + path.join(appRoot, 'node_modules/effect'), + path.join(generatedFixtureRoot, 'node_modules/effect'), + 'dir', + ), + ); + yield* Effect.tryPromise(() => + symlink( + path.join(appRoot, 'apps/shell-super-app/node_modules/jose'), + path.join(generatedFixtureRoot, 'node_modules/jose'), + 'dir', + ), ); const generatedVerifierPath = path.join(generatedFixtureRoot, 'action-principal.ts'); - await writeFile( - generatedVerifierPath, - renderActionPrincipalServer({ appId: 'inventory-stock' }), - 'utf-8', - ); - const generatedVerifier = await import(pathToFileURL(generatedVerifierPath).href); - const { verifyActionPrincipal } = generatedVerifier; - assert.ok(Predicate.isFunction(verifyActionPrincipal)); - const generatedPrincipal = Schema.decodeUnknownSync(TrustedPrincipalContextSchema)( - await runEffectTestPromise( - verifyActionPrincipal(`Bearer ${assertionToken}`, { - currentTimeSeconds: Effect.succeed(1_700_000_001), - environment: { - ONTOS_GATEWAY_ISSUER: 'https://shell.example.test', - ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify({ - keys: [ - { - ...publicJwk, - alg: 'EdDSA', - kid: 'integration-current', - use: 'sig', - }, - ], - }), - }, - redemption: { consume: () => Effect.void }, - }), + yield* Effect.tryPromise(() => + writeFile( + generatedVerifierPath, + renderActionPrincipalServer({ appId: 'inventory-stock' }), + 'utf-8', ), ); - assert.equal(generatedPrincipal.authBindingId, fixtureAuthBindingId); - assert.match(optionalText(generatedPrincipal.authContextRef), /^better-auth-session:/u); - assert.equal(generatedPrincipal.authMethod, 'session'); - assert.equal(generatedPrincipal.legalEntityId, fixtureLegalEntityId); - assert.equal(generatedPrincipal.principalId, principalId); - assert.equal(generatedPrincipal.tenantId, tenantId); - - const invalidAudienceResponse = await issuingHandler.handler( - new Request(`${configuration.baseUrl}/auth/gateway-context`, { - body: JSON.stringify({ audience: 'billing' }), - headers: new Headers({ - 'content-type': 'application/json', - cookie: cookieHeader(signedInCookies), - origin: configuration.baseUrl, - }), - method: 'POST', + const generatedVerifier = yield* Effect.tryPromise( + () => import(pathToFileURL(generatedVerifierPath).href), + ); + type GeneratedVerifier = ( + authorization: string, + options: { + readonly currentTimeSeconds: Effect.Effect; + readonly environment: Readonly>; + readonly redemption: { + readonly consume: () => Effect.Effect; + }; + }, + ) => Effect.Effect; + const verifyActionPrincipal = Schema.decodeUnknownSync( + Schema.declare((value): value is GeneratedVerifier => + Predicate.isFunction(value), + ), + )(generatedVerifier.verifyActionPrincipal); + expect(Predicate.isFunction(verifyActionPrincipal)).toBe(true); + const generatedPrincipal = Schema.decodeUnknownSync(TrustedPrincipalContextSchema)( + yield* verifyActionPrincipal(`Bearer ${assertionToken}`, { + currentTimeSeconds: Effect.succeed(1_700_000_001), + environment: { + ONTOS_GATEWAY_ISSUER: 'https://shell.example.test', + ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify({ + keys: [ + { + ...publicJwk, + alg: 'EdDSA', + kid: 'integration-current', + use: 'sig', + }, + ], + }), + }, + redemption: { consume: () => Effect.void }, }), ); - assert.equal(invalidAudienceResponse.status, 400); - + expect(generatedPrincipal.authBindingId).toBe(fixtureAuthBindingId); + expect(optionalText(generatedPrincipal.authContextRef)).toMatch(/^better-auth-session:/u); + expect(generatedPrincipal.authMethod).toBe('session'); + expect(generatedPrincipal.legalEntityId).toBe(fixtureLegalEntityId); + expect(generatedPrincipal.principalId).toBe(principalId); + expect(generatedPrincipal.tenantId).toBe(tenantId); + const invalidAudienceResponse = yield* Effect.tryPromise(() => + issuingHandler.handler( + new Request(`${configuration.baseUrl}/auth/gateway-context`, { + body: JSON.stringify({ audience: 'billing' }), + headers: new Headers({ + 'content-type': 'application/json', + cookie: cookieHeader(signedInCookies), + origin: configuration.baseUrl, + }), + method: 'POST', + }), + ), + ); + expect(invalidAudienceResponse.status).toBe(400); const defectHandler = makeShellAuthenticationApiRuntime( authenticationLayer, makeGatewayIssuerLayer({ @@ -1004,201 +1022,185 @@ test('creates, resolves, persists, revokes, and signs out a Better Auth session' contextAccessLayer, ).createHandler(); handlers.push(defectHandler); - const defectResponse = await defectHandler.handler( - new Request(`${configuration.baseUrl}/auth/gateway-context`, { - body: JSON.stringify({ audience: 'inventory-stock' }), - headers: new Headers({ - 'content-type': 'application/json', - cookie: cookieHeader(signedInCookies), - origin: configuration.baseUrl, - 'x-correlation-id': 'integration-correlation-id', + const defectResponse = yield* Effect.tryPromise(() => + defectHandler.handler( + new Request(`${configuration.baseUrl}/auth/gateway-context`, { + body: JSON.stringify({ audience: 'inventory-stock' }), + headers: new Headers({ + 'content-type': 'application/json', + cookie: cookieHeader(signedInCookies), + origin: configuration.baseUrl, + 'x-correlation-id': 'integration-correlation-id', + }), + method: 'POST', }), - method: 'POST', - }), + ), ); - assert.equal(defectResponse.status, 500); - assert.match( - headerValue(defectResponse.headers, 'content-type'), + expect(defectResponse.status).toBe(500); + expect(headerValue(defectResponse.headers, 'content-type')).toMatch( /application\/problem\+json/u, ); const defectProblem = Schema.decodeUnknownSync(DefectProblemSchema)( - await defectResponse.json(), - ); - assert.equal(defectProblem.detail, 'Gateway authentication could not complete.'); - assert.doesNotMatch(JSON.stringify(defectProblem), /deliberate gateway test defect/u); - - const stillAuthenticated = await runEffectTestPromise( - authentication - .currentSession(authenticatedHeaders) - .pipe(Effect.provide(authenticationContextLayer)), + yield* Effect.tryPromise(() => defectResponse.json()), ); + expect(defectProblem.detail).toBe('Gateway authentication could not complete.'); + expect(JSON.stringify(defectProblem)).not.toMatch(/deliberate gateway test defect/u); + const stillAuthenticated = yield* authentication + .currentSession(authenticatedHeaders) + .pipe(Effect.provide(authenticationContextLayer)); assertOptionalField(stillAuthenticated.identity, 'principalId', principalId); - - await runEffectTestPromise( - coreDatabase - .update(principalAuthBindings) - .set({ revokedAt: new Date('2026-09-01T00:00:00.000Z'), status: 'revoked' }) - .where(eq(principalAuthBindings.providerSubjectId, betterAuthUserId)), - ); - await assertSessionForbidden( + yield* coreDatabase + .update(principalAuthBindings) + .set({ revokedAt: new Date('2026-09-01T00:00:00.000Z'), status: 'revoked' }) + .where(eq(principalAuthBindings.providerSubjectId, betterAuthUserId)); + yield* assertSessionForbidden( authentication .currentSession(authenticatedHeaders) .pipe(Effect.provide(authenticationContextLayer)), ); - const forbiddenModulesResponse = await unavailableHandler.handler( - new Request(`${configuration.baseUrl}/shell/composition`, { - headers: authenticatedHeaders, - }), - ); - assert.equal(forbiddenModulesResponse.status, 401); - assert.match(headerValue(forbiddenModulesResponse.headers, 'www-authenticate'), /^Bearer/u); - assert.doesNotMatch(await forbiddenModulesResponse.text(), /30000000|40000000/u); - - await runEffectTestPromise( - coreDatabase - .update(principalAuthBindings) - .set({ revokedAt: null, status: 'active' }) - .where(eq(principalAuthBindings.providerSubjectId, betterAuthUserId)), + const forbiddenModulesResponse = yield* Effect.tryPromise(() => + unavailableHandler.handler( + new Request(`${configuration.baseUrl}/shell/composition`, { + headers: authenticatedHeaders, + }), + ), ); - const signOutResponse = await unavailableHandler.handler( - new Request(`${configuration.baseUrl}/auth/sign-out`, { - headers: authenticatedHeaders, - method: 'POST', - }), + expect(forbiddenModulesResponse.status).toBe(401); + expect(headerValue(forbiddenModulesResponse.headers, 'www-authenticate')).toMatch(/^Bearer/u); + expect(yield* Effect.tryPromise(() => forbiddenModulesResponse.text())).not.toMatch( + /30000000|40000000/u, + ); + yield* coreDatabase + .update(principalAuthBindings) + .set({ revokedAt: null, status: 'active' }) + .where(eq(principalAuthBindings.providerSubjectId, betterAuthUserId)); + const signOutResponse = yield* Effect.tryPromise(() => + unavailableHandler.handler( + new Request(`${configuration.baseUrl}/auth/sign-out`, { + headers: authenticatedHeaders, + method: 'POST', + }), + ), ); - assert.equal(signOutResponse.status, 200); + expect(signOutResponse.status).toBe(200); const signedOutCookies = signOutResponse.headers.getSetCookie(); - assert.ok(signedOutCookies.length >= 3); - assert.ok(signedOutCookies.every((header) => !header.includes(password))); - - const anonymous = await runEffectTestPromise( - authentication - .currentSession( - new Headers({ - cookie: cookieHeader(signedOutCookies), - origin: configuration.baseUrl, - }), - ) - .pipe(Effect.provide(authenticationContextLayer)), + expect(signedOutCookies.length >= 3).toBe(true); + expect(signedOutCookies.every((header) => !header.includes(password))).toBe(true); + const anonymous = yield* authentication + .currentSession( + new Headers({ + cookie: cookieHeader(signedOutCookies), + origin: configuration.baseUrl, + }), + ) + .pipe(Effect.provide(authenticationContextLayer)); + expect(anonymous.identity).toBe(null); + const expiredModulesResponse = yield* Effect.tryPromise(() => + unavailableHandler.handler( + new Request(`${configuration.baseUrl}/shell/composition`, { + headers: authenticatedHeaders, + }), + ), ); - assert.equal(anonymous.identity, null); - const expiredModulesResponse = await unavailableHandler.handler( - new Request(`${configuration.baseUrl}/shell/composition`, { - headers: authenticatedHeaders, - }), + expect(expiredModulesResponse.status).toBe(401); + expect(yield* Effect.tryPromise(() => expiredModulesResponse.text())).not.toMatch( + /30000000|40000000/u, ); - assert.equal(expiredModulesResponse.status, 401); - assert.doesNotMatch(await expiredModulesResponse.text(), /30000000|40000000/u); - } finally { - await Promise.all(handlers.map(async ({ dispose }) => await dispose())); - await rm(generatedFixtureRoot, { force: true, recursive: true }); - await cleanup(); - await corePool.end(); - } -}); - -test('selects, lists, switches, revalidates, and upgrades a multi-tenant session', async () => { - const multiEmail = 'better-auth-multi-tenant@example.test'; - const firstTenantId = '31000000-0000-4000-8000-000000000001'; - const secondTenantId = '31000000-0000-4000-8000-000000000002'; - const firstPrincipalId = '41000000-0000-4000-8000-000000000001'; - const secondPrincipalId = '41000000-0000-4000-8000-000000000002'; - const firstLegalEntityId = '36000000-0000-4000-8000-000000000001'; - const secondLegalEntityId = '36000000-0000-4000-8000-000000000002'; - const firstAuthBindingId = '46000000-0000-4000-8000-000000000001'; - const secondAuthBindingId = '46000000-0000-4000-8000-000000000002'; - const legalEntityByTenant = new Map([ - [firstTenantId, { legalEntityId: firstLegalEntityId, legalName: 'First legal entity' }], - [secondTenantId, { legalEntityId: secondLegalEntityId, legalName: 'Second legal entity' }], - ]); - const multiLegalEntitySelectionOptions = { - contextAccess: legalEntitySelectionOptions.contextAccess, - legalEntityContext: { - listActiveForTenant: (selectedTenantId: string) => - Effect.succeed(legalEntityByTenant.get(selectedTenantId)).pipe( - Effect.map((selected) => (selected === undefined ? [] : [selected])), - ), - validateSelection: (selectedTenantId: string, legalEntityId: string) => { - const selected = legalEntityByTenant.get(selectedTenantId); - return selected?.legalEntityId === legalEntityId - ? Effect.succeed(selected) - : Effect.die('missing multi-tenant fixture legal entity'); + }), +); +it.live( + 'selects, lists, switches, revalidates, and upgrades a multi-tenant session', + Effect.fnUntraced(function* runIntegration6() { + const multiEmail = 'better-auth-multi-tenant@example.test'; + const firstTenantId = '31000000-0000-4000-8000-000000000001'; + const secondTenantId = '31000000-0000-4000-8000-000000000002'; + const firstPrincipalId = '41000000-0000-4000-8000-000000000001'; + const secondPrincipalId = '41000000-0000-4000-8000-000000000002'; + const firstLegalEntityId = '36000000-0000-4000-8000-000000000001'; + const secondLegalEntityId = '36000000-0000-4000-8000-000000000002'; + const firstAuthBindingId = '46000000-0000-4000-8000-000000000001'; + const secondAuthBindingId = '46000000-0000-4000-8000-000000000002'; + const legalEntityByTenant = new Map([ + [firstTenantId, { legalEntityId: firstLegalEntityId, legalName: 'First legal entity' }], + [secondTenantId, { legalEntityId: secondLegalEntityId, legalName: 'Second legal entity' }], + ]); + const multiLegalEntitySelectionOptions = { + contextAccess: legalEntitySelectionOptions.contextAccess, + legalEntityContext: { + listActiveForTenant: (selectedTenantId: string) => + Effect.succeed(legalEntityByTenant.get(selectedTenantId)).pipe( + Effect.map((selected) => (selected === undefined ? [] : [selected])), + ), + validateSelection: (selectedTenantId: string, legalEntityId: string) => { + const selected = legalEntityByTenant.get(selectedTenantId); + return selected?.legalEntityId === legalEntityId + ? Effect.succeed(selected) + : Effect.die('missing multi-tenant fixture legal entity'); + }, }, - }, - runResolverEffect: runEffectTestPromise, - } as const; - const multiContextAccessLayer = Layer.succeed( - ContextAccess, - multiLegalEntitySelectionOptions.contextAccess, - ); - const multiAuthenticationContextLayer = Layer.mergeAll( - multiContextAccessLayer, - Layer.succeed(LegalEntityContext, multiLegalEntitySelectionOptions.legalEntityContext), - ); - const configuration = await runEffectTestPromise(loadAuthConfig()); - const databaseConnections = await runEffectTestPromise(loadDatabaseConnectionPair()); - const adminPool = new Pool({ - connectionString: databaseConnections.admin.connectionString, - }); - const corePool = new Pool({ connectionString: configuration.connectionString }); - const coreDatabase = await runEffectTestPromise( - makeTestDatabaseFromPool(corePool, coreRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ), - ); - const authPersistence = await runEffectTestPromise( - makeAuthDatabase(configuration).pipe(NativeScope.provide(nativeDatabaseScope)), - ); - const authDatabase = authPersistence.executor; - const adminAuthDatabase = await runEffectTestPromise( - makeTestDatabaseFromPool(adminPool, authRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ), - ); - const resolver = makePrincipalResolver({ executor: coreDatabase }); - const authentication = makeAuthenticationService( - configuration, - authPersistence.adapter, - resolver, - { - allowFixtureSignUp: true, - runResolverEffect: multiLegalEntitySelectionOptions.runResolverEffect, - }, - ); - const moduleStateLayer = Layer.succeed( - TenantModuleStateService, - makeTenantModuleStateService({ executor: coreDatabase }), - ); - const handlers: { readonly dispose: () => Promise }[] = []; - - const fixtureTenants = [firstTenantId, secondTenantId]; - // Ordered child-before-parent so every delete respects the owned foreign keys. - const cleanup = async (): Promise => { - await runEffectTestPromise( - purgeFixtureRows([ - coreDatabase - .delete(dataAccessEvents) - .where(inArray(dataAccessEvents.tenantId, fixtureTenants)), - ]), + } as const; + const multiContextAccessLayer = Layer.succeed( + ContextAccess, + multiLegalEntitySelectionOptions.contextAccess, ); - const existingUsers = await runEffectTestPromise( - authDatabase.select({ id: user.id }).from(user).where(eq(user.email, multiEmail)), - ); - const existingUserIds = existingUsers.map(({ id }) => id); - if (existingUserIds.length > 0) { - await runEffectTestPromise( - purgeFixtureRows([ + const multiAuthenticationContextLayer = Layer.mergeAll( + multiContextAccessLayer, + Layer.succeed(LegalEntityContext, multiLegalEntitySelectionOptions.legalEntityContext), + ); + const configuration = yield* loadAuthConfig(); + const databaseConnections = yield* loadDatabaseConnectionPair(); + const adminPool = yield* Effect.acquireRelease( + Effect.sync( + () => + new Pool({ + connectionString: databaseConnections.admin.connectionString, + }), + ), + (pool) => Effect.tryPromise(() => pool.end()).pipe(Effect.orDie), + ); + const corePool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: configuration.connectionString })), + (pool) => Effect.tryPromise(() => pool.end()).pipe(Effect.orDie), + ); + const coreDatabase = yield* makeTestDatabaseFromPool(corePool, coreRelations); + const authPersistence = yield* makeAuthDatabase(configuration); + const authDatabase = authPersistence.executor; + const adminAuthDatabase = yield* makeTestDatabaseFromPool(adminPool, authRelations); + const resolver = makePrincipalResolver({ executor: coreDatabase }); + const authentication = yield* makeAuthenticationService({ + allowFixtureSignUp: true, + }).pipe( + Effect.provideService(AuthConfig, configuration), + Effect.provideService(AuthDatabase, authPersistence), + Effect.provideService(PrincipalResolver, resolver), + ); + const moduleStateLayer = Layer.succeed( + TenantModuleStateService, + makeTenantModuleStateService({ executor: coreDatabase }), + ); + const handlers: AuthenticationRuntimeHandler[] = []; + const fixtureTenants = [firstTenantId, secondTenantId]; + // Ordered child-before-parent within the owned fixture rows. + const cleanup = Effect.fnUntraced(function* runIntegration7() { + yield* coreDatabase + .delete(dataAccessEvents) + .where(inArray(dataAccessEvents.tenantId, fixtureTenants)); + const existingUsers = yield* authDatabase + .select({ id: user.id }) + .from(user) + .where(eq(user.email, multiEmail)); + const existingUserIds = existingUsers.map(({ id }) => id); + if (existingUserIds.length > 0) { + yield* purgeFixtureRows([ coreDatabase .delete(principalAuthBindings) .where(inArray(principalAuthBindings.providerSubjectId, existingUserIds)), authDatabase.delete(session).where(inArray(session.userId, existingUserIds)), authDatabase.delete(account).where(inArray(account.userId, existingUserIds)), authDatabase.delete(user).where(inArray(user.id, existingUserIds)), - ]), - ); - } - await runEffectTestPromise( - purgeFixtureRows([ + ]); + } + yield* purgeFixtureRows([ coreDatabase .delete(tenantModuleStates) .where(inArray(tenantModuleStates.tenantId, fixtureTenants)), @@ -1209,115 +1211,116 @@ test('selects, lists, switches, revalidates, and upgrades a multi-tenant session .delete(legalEntities) .where(inArray(legalEntities.legalEntityId, [firstLegalEntityId, secondLegalEntityId])), coreDatabase.delete(tenants).where(inArray(tenants.tenantId, fixtureTenants)), - ]), - ); - }; - - try { - await cleanup(); - const betterAuthUserId = await runEffectTestPromise( - authentication.createFixtureUser(multiEmail, 'Multi tenant fixture', password), - ); - await runEffectTestPromise( - coreDatabase.insert(tenants).values([ - { - defaultLocale: 'en', - name: 'Zeta tenant', - slug: 'multi-zeta-tenant', - status: 'active', - tenantId: firstTenantId, - }, - { - defaultLocale: 'en', - name: 'Alpha tenant', - slug: 'multi-alpha-tenant', - status: 'active', - tenantId: secondTenantId, - }, - ]), - ); - await runEffectTestPromise( - coreDatabase.insert(principals).values([ - { - displayName: 'First tenant principal', - kind: 'human', - principalId: firstPrincipalId, - status: 'active', - tenantId: firstTenantId, - }, - { - displayName: 'Second tenant principal', - kind: 'human', - principalId: secondPrincipalId, - status: 'active', - tenantId: secondTenantId, - }, - ]), - ); - await runEffectTestPromise( - coreDatabase.insert(legalEntities).values([ - { - legalEntityId: firstLegalEntityId, - legalName: 'First legal entity', - registrationCountry: 'CZ', - registrationNumber: 'AUTH-MULTI-1', - status: 'active', - tenantId: firstTenantId, - }, - { - legalEntityId: secondLegalEntityId, - legalName: 'Second legal entity', - registrationCountry: 'CZ', - registrationNumber: 'AUTH-MULTI-2', - status: 'active', - tenantId: secondTenantId, - }, - ]), - ); - await runEffectTestPromise( - coreDatabase.insert(principalAuthBindings).values([ - { - createdAt: new Date('2026-01-01T00:00:00.000Z'), - principalAuthBindingId: firstAuthBindingId, - principalId: firstPrincipalId, - provider: 'better_auth', - providerSubjectId: betterAuthUserId, - status: 'active', - subjectType: 'user', - tenantId: firstTenantId, - }, - { - createdAt: new Date('2026-02-01T00:00:00.000Z'), - principalAuthBindingId: secondAuthBindingId, - principalId: secondPrincipalId, - provider: 'better_auth', - providerSubjectId: betterAuthUserId, - status: 'active', - subjectType: 'user', - tenantId: secondTenantId, - }, - ]), - ); - await runEffectTestPromise( - coreDatabase.insert(tenantModuleStates).values([ - { moduleKey: 'first-module', state: 'active', tenantId: firstTenantId }, - { moduleKey: 'second-module', state: 'active', tenantId: secondTenantId }, - ]), + ]); + }); + yield* Effect.acquireRelease( + Effect.void, + Effect.fnUntraced(function* integrationEffect8() { + yield* Effect.all( + handlers.map( + Effect.fnUntraced(function* runIntegration9({ dispose }) { + return yield* Effect.tryPromise(() => dispose()); + }), + ), + ); + yield* cleanup(); + }, Effect.orDie), ); - - const signIn = await runEffectTestPromise( - authentication.signIn(multiEmail, password, new Headers({ origin: configuration.baseUrl })), + yield* cleanup(); + const betterAuthUserId = yield* authentication.createFixtureUser( + multiEmail, + 'Multi tenant fixture', + password, ); - assert.equal(signIn.identity.tenantId, firstTenantId); - assert.equal(signIn.identity.principalId, firstPrincipalId); + yield* coreDatabase.insert(tenants).values([ + { + defaultLocale: 'en', + name: 'Zeta tenant', + slug: 'multi-zeta-tenant', + status: 'active', + tenantId: firstTenantId, + }, + { + defaultLocale: 'en', + name: 'Alpha tenant', + slug: 'multi-alpha-tenant', + status: 'active', + tenantId: secondTenantId, + }, + ]); + yield* coreDatabase.insert(principals).values([ + { + displayName: 'First tenant principal', + kind: 'human', + principalId: firstPrincipalId, + status: 'active', + tenantId: firstTenantId, + }, + { + displayName: 'Second tenant principal', + kind: 'human', + principalId: secondPrincipalId, + status: 'active', + tenantId: secondTenantId, + }, + ]); + yield* coreDatabase.insert(legalEntities).values([ + { + legalEntityId: firstLegalEntityId, + legalName: 'First legal entity', + registrationCountry: 'CZ', + registrationNumber: 'AUTH-MULTI-1', + status: 'active', + tenantId: firstTenantId, + }, + { + legalEntityId: secondLegalEntityId, + legalName: 'Second legal entity', + registrationCountry: 'CZ', + registrationNumber: 'AUTH-MULTI-2', + status: 'active', + tenantId: secondTenantId, + }, + ]); + yield* coreDatabase.insert(principalAuthBindings).values([ + { + createdAt: new Date('2026-01-01T00:00:00.000Z'), + principalAuthBindingId: firstAuthBindingId, + principalId: firstPrincipalId, + provider: 'better_auth', + providerSubjectId: betterAuthUserId, + status: 'active', + subjectType: 'user', + tenantId: firstTenantId, + }, + { + createdAt: new Date('2026-02-01T00:00:00.000Z'), + principalAuthBindingId: secondAuthBindingId, + principalId: secondPrincipalId, + provider: 'better_auth', + providerSubjectId: betterAuthUserId, + status: 'active', + subjectType: 'user', + tenantId: secondTenantId, + }, + ]); + yield* coreDatabase.insert(tenantModuleStates).values([ + { moduleKey: 'first-module', state: 'active', tenantId: firstTenantId }, + { moduleKey: 'second-module', state: 'active', tenantId: secondTenantId }, + ]); + const signIn = yield* authentication.signIn( + multiEmail, + password, + new Headers({ origin: configuration.baseUrl }), + ); + expect(signIn.identity.tenantId).toBe(firstTenantId); + expect(signIn.identity.principalId).toBe(firstPrincipalId); const authenticatedCookie = cookieHeader(signIn.setCookieHeaders); const authenticatedHeaders = new Headers({ cookie: authenticatedCookie, origin: configuration.baseUrl, }); - // Every tenant-switch case posts the same authenticated envelope; only the target tenant and - // the optional correlation header vary between the authorization, resolver, and persistence - // failures asserted below. + // Tenant switching varies only the target and optional correlation header. const tenantSwitchRequest = (target: string, extraHeaders: Record = {}) => new Request(`${configuration.baseUrl}/auth/tenant/switch`, { body: JSON.stringify({ tenantId: target }), @@ -1329,19 +1332,18 @@ test('selects, lists, switches, revalidates, and upgrades a multi-tenant session }), method: 'POST', }); - const readActiveTenantIds = async () => - await runEffectTestPromise( - authDatabase - .select({ activeTenantId: session.activeTenantId }) - .from(session) - .where(eq(session.userId, betterAuthUserId)), - ); + const readActiveTenantIds = () => + authDatabase + .select({ activeTenantId: session.activeTenantId }) + .from(session) + .where(eq(session.userId, betterAuthUserId)); - const initialSessions = await readActiveTenantIds(); + const initialSessions = yield* readActiveTenantIds(); assertOptionalField(initialSessions[0], 'activeTenantId', firstTenantId); - - const pair = await generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true }); - const privateJwk = await exportJWK(pair.privateKey); + const pair = yield* Effect.tryPromise(() => + generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true }), + ); + const privateJwk = yield* Effect.tryPromise(() => exportJWK(pair.privateKey)); const runtime = makeShellAuthenticationApiRuntime( Layer.succeed(AuthenticationService, authentication), makeGatewayIssuerLayer({ @@ -1367,67 +1369,71 @@ test('selects, lists, switches, revalidates, and upgrades a multi-tenant session multiContextAccessLayer, ).createHandler(); handlers.push(runtime); - - const anonymousAvailableResponse = await runtime.handler( - new Request(`${configuration.baseUrl}/auth/tenants`, { - headers: { origin: configuration.baseUrl }, - }), + const anonymousAvailableResponse = yield* Effect.tryPromise(() => + runtime.handler( + new Request(`${configuration.baseUrl}/auth/tenants`, { + headers: { origin: configuration.baseUrl }, + }), + ), ); - assert.equal(anonymousAvailableResponse.status, 401); - assert.match(headerValue(anonymousAvailableResponse.headers, 'www-authenticate'), /^Bearer /u); - - const availableResponse = await runtime.handler( - new Request(`${configuration.baseUrl}/auth/tenants`, { headers: authenticatedHeaders }), + expect(anonymousAvailableResponse.status).toBe(401); + expect(headerValue(anonymousAvailableResponse.headers, 'www-authenticate')).toMatch( + /^Bearer /u, ); - assert.equal(availableResponse.status, 200); - assert.deepEqual(await availableResponse.json(), { + const availableResponse = yield* Effect.tryPromise(() => + runtime.handler( + new Request(`${configuration.baseUrl}/auth/tenants`, { headers: authenticatedHeaders }), + ), + ); + expect(availableResponse.status).toBe(200); + expect(yield* Effect.tryPromise(() => availableResponse.json())).toEqual({ tenants: [ { name: 'Alpha tenant', tenantId: secondTenantId }, { name: 'Zeta tenant', tenantId: firstTenantId }, ], }); - assert.doesNotMatch( - JSON.stringify(authentication.availableTenants(authenticatedHeaders)), + const availableTenants = yield* authentication + .availableTenants(authenticatedHeaders) + .pipe(Effect.provide(multiAuthenticationContextLayer)); + expect(JSON.stringify(availableTenants)).not.toMatch( /principalId|sessionId|token|bindingId|password/u, ); - - const firstModules = await runtime.handler( - new Request(`${configuration.baseUrl}/shell/composition`, { headers: authenticatedHeaders }), + const firstModules = yield* Effect.tryPromise(() => + runtime.handler( + new Request(`${configuration.baseUrl}/shell/composition`, { + headers: authenticatedHeaders, + }), + ), ); - assert.deepEqual(await firstModules.json(), { + expect(yield* Effect.tryPromise(() => firstModules.json())).toEqual({ navigation: [], state: 'available', unavailableDeployments: [], }); - - const forbiddenResponse = await runtime.handler( - tenantSwitchRequest('31000000-0000-4000-8000-000000000099'), + const forbiddenResponse = yield* Effect.tryPromise(() => + runtime.handler(tenantSwitchRequest('31000000-0000-4000-8000-000000000099')), ); - assert.equal(forbiddenResponse.status, 403); - const sessionsAfterForbiddenSwitch = await readActiveTenantIds(); + expect(forbiddenResponse.status).toBe(403); + const sessionsAfterForbiddenSwitch = yield* readActiveTenantIds(); assertOptionalField(sessionsAfterForbiddenSwitch[0], 'activeTenantId', firstTenantId); - - await runEffectTestPromise( - coreDatabase - .update(principals) - .set({ status: 'disabled' }) - .where(eq(principals.principalId, secondPrincipalId)), - ); - const inactiveTargetResponse = await runtime.handler(tenantSwitchRequest(secondTenantId)); - assert.equal(inactiveTargetResponse.status, 403); - const sessionsAfterInactiveSwitch = await readActiveTenantIds(); + yield* coreDatabase + .update(principals) + .set({ status: 'disabled' }) + .where(eq(principals.principalId, secondPrincipalId)); + const inactiveTargetResponse = yield* Effect.tryPromise(() => + runtime.handler(tenantSwitchRequest(secondTenantId)), + ); + expect(inactiveTargetResponse.status).toBe(403); + const sessionsAfterInactiveSwitch = yield* readActiveTenantIds(); assertOptionalField(sessionsAfterInactiveSwitch[0], 'activeTenantId', firstTenantId); - await runEffectTestPromise( - coreDatabase - .update(principals) - .set({ status: 'active' }) - .where(eq(principals.principalId, secondPrincipalId)), - ); - - const resolverUnavailableAuthentication = makeAuthenticationService( - configuration, - authPersistence.adapter, - { + yield* coreDatabase + .update(principals) + .set({ status: 'active' }) + .where(eq(principals.principalId, secondPrincipalId)); + const resolverUnavailableAuthentication = yield* makeAuthenticationService({}).pipe( + Effect.provideService(AuthConfig, configuration), + Effect.provideService(AuthDatabase, authPersistence), + Effect.provideService(PrincipalResolver, { ...resolver, resolveBetterAuthUserForTenant: (userId, selectedTenantId) => selectedTenantId === secondTenantId @@ -1435,8 +1441,7 @@ test('selects, lists, switches, revalidates, and upgrades a multi-tenant session new PrincipalResolverUnavailableError({ reason: 'Injected resolver outage' }), ) : resolver.resolveBetterAuthUserForTenant(userId, selectedTenantId), - }, - { runResolverEffect: multiLegalEntitySelectionOptions.runResolverEffect }, + }), ); const resolverUnavailableRuntime = makeShellAuthenticationApiRuntime( Layer.succeed(AuthenticationService, resolverUnavailableAuthentication), @@ -1449,18 +1454,16 @@ test('selects, lists, switches, revalidates, and upgrades a multi-tenant session moduleStateLayer, ).createHandler(); handlers.push(resolverUnavailableRuntime); - const resolverUnavailableResponse = await resolverUnavailableRuntime.handler( - tenantSwitchRequest(secondTenantId), + const resolverUnavailableResponse = yield* Effect.tryPromise(() => + resolverUnavailableRuntime.handler(tenantSwitchRequest(secondTenantId)), ); - assert.equal(resolverUnavailableResponse.status, 503); - const sessionsAfterResolverFailure = await readActiveTenantIds(); + expect(resolverUnavailableResponse.status).toBe(503); + const sessionsAfterResolverFailure = yield* readActiveTenantIds(); assertOptionalField(sessionsAfterResolverFailure[0], 'activeTenantId', firstTenantId); - // Drizzle has no query-builder failure injection. This temporary trigger raises PostgreSQL's // connection-failure class for the fixed test tenant through the real Better Auth adapter path. - await runEffectTestPromise( - adminAuthDatabase.execute( - sql.raw(` + yield* adminAuthDatabase.execute( + sql.raw(` CREATE OR REPLACE FUNCTION auth.tenant_switch_test_fail_persistence() RETURNS trigger LANGUAGE plpgsql @@ -1473,112 +1476,108 @@ test('selects, lists, switches, revalidates, and upgrades a multi-tenant session END; $function$ `), - ), ); - await runEffectTestPromise( - adminAuthDatabase.execute( - sql.raw(` + yield* adminAuthDatabase.execute( + sql.raw(` CREATE TRIGGER tenant_switch_test_persistence_failure BEFORE UPDATE ON auth.session FOR EACH ROW EXECUTE FUNCTION auth.tenant_switch_test_fail_persistence() `), - ), ); - try { - const persistenceUnavailableResponse = await runtime.handler( - tenantSwitchRequest(secondTenantId), - ); - assert.equal(persistenceUnavailableResponse.status, 503); - const sessionsAfterPersistenceFailure = await readActiveTenantIds(); - assertOptionalField(sessionsAfterPersistenceFailure[0], 'activeTenantId', firstTenantId); - } finally { - await runEffectTestPromise( - adminAuthDatabase.execute( - sql.raw(` + yield* Effect.scoped( + Effect.gen(function* integrationEffect10() { + yield* Effect.acquireRelease( + Effect.void, + Effect.fnUntraced(function* integrationEffect11() { + yield* adminAuthDatabase.execute( + sql.raw(` DROP TRIGGER IF EXISTS tenant_switch_test_persistence_failure ON auth.session `), - ), - ); - await runEffectTestPromise( - adminAuthDatabase.execute( - sql.raw(` + ); + yield* adminAuthDatabase.execute( + sql.raw(` DROP FUNCTION IF EXISTS auth.tenant_switch_test_fail_persistence() `), - ), - ); - } - - const sessionsBeforeSwitch = await runEffectTestPromise( - authDatabase - .select({ activeLegalEntityId: session.activeLegalEntityId }) - .from(session) - .where(eq(session.userId, betterAuthUserId)), + ); + }, Effect.orDie), + ); + const persistenceUnavailableResponse = yield* Effect.tryPromise(() => + runtime.handler(tenantSwitchRequest(secondTenantId)), + ); + expect(persistenceUnavailableResponse.status).toBe(503); + const sessionsAfterPersistenceFailure = yield* readActiveTenantIds(); + assertOptionalField(sessionsAfterPersistenceFailure[0], 'activeTenantId', firstTenantId); + }), ); + const sessionsBeforeSwitch = yield* authDatabase + .select({ activeLegalEntityId: session.activeLegalEntityId }) + .from(session) + .where(eq(session.userId, betterAuthUserId)); assertOptionalField(sessionsBeforeSwitch[0], 'activeLegalEntityId', firstLegalEntityId); - - const switchResponse = await runtime.handler(tenantSwitchRequest(secondTenantId)); - assert.equal(switchResponse.status, 200); - assert.deepEqual(await switchResponse.json(), { selectedTenantId: secondTenantId }); - const sessionsAfterSwitch = await runEffectTestPromise( - authDatabase - .select({ - activeLegalEntityId: session.activeLegalEntityId, - activeTenantId: session.activeTenantId, - }) - .from(session) - .where(eq(session.userId, betterAuthUserId)), + const switchResponse = yield* Effect.tryPromise(() => + runtime.handler(tenantSwitchRequest(secondTenantId)), ); + expect(switchResponse.status).toBe(200); + expect(yield* Effect.tryPromise(() => switchResponse.json())).toEqual({ + selectedTenantId: secondTenantId, + }); + const sessionsAfterSwitch = yield* authDatabase + .select({ + activeLegalEntityId: session.activeLegalEntityId, + activeTenantId: session.activeTenantId, + }) + .from(session) + .where(eq(session.userId, betterAuthUserId)); assertOptionalField(sessionsAfterSwitch[0], 'activeTenantId', secondTenantId); assertOptionalField(sessionsAfterSwitch[0], 'activeLegalEntityId', null); - const currentSessionAfterSwitch = await runEffectTestPromise( - authentication - .currentSession(authenticatedHeaders) - .pipe(Effect.provide(multiAuthenticationContextLayer)), - ); + const currentSessionAfterSwitch = yield* authentication + .currentSession(authenticatedHeaders) + .pipe(Effect.provide(multiAuthenticationContextLayer)); assertOptionalField(currentSessionAfterSwitch.identity, 'principalId', secondPrincipalId); - const idempotentSwitch = await runEffectTestPromise( - authentication - .switchTenant(secondTenantId, authenticatedHeaders) - .pipe(Effect.provide(multiAuthenticationContextLayer)), - ); - assert.equal(idempotentSwitch.selectedTenantId, secondTenantId); - - const secondModules = await runtime.handler( - new Request(`${configuration.baseUrl}/shell/composition`, { headers: authenticatedHeaders }), + const idempotentSwitch = yield* authentication + .switchTenant(secondTenantId, authenticatedHeaders) + .pipe(Effect.provide(multiAuthenticationContextLayer)); + expect(idempotentSwitch.selectedTenantId).toBe(secondTenantId); + const secondModules = yield* Effect.tryPromise(() => + runtime.handler( + new Request(`${configuration.baseUrl}/shell/composition`, { + headers: authenticatedHeaders, + }), + ), ); - assert.deepEqual(await secondModules.json(), { + expect(yield* Effect.tryPromise(() => secondModules.json())).toEqual({ navigation: [], state: 'available', unavailableDeployments: [], }); - const assertionResponse = await runtime.handler( - new Request(`${configuration.baseUrl}/auth/gateway-context`, { - body: JSON.stringify({ audience: 'inventory-stock' }), - headers: new Headers({ - 'content-type': 'application/json', - cookie: authenticatedCookie, - origin: configuration.baseUrl, + const assertionResponse = yield* Effect.tryPromise(() => + runtime.handler( + new Request(`${configuration.baseUrl}/auth/gateway-context`, { + body: JSON.stringify({ audience: 'inventory-stock' }), + headers: new Headers({ + 'content-type': 'application/json', + cookie: authenticatedCookie, + origin: configuration.baseUrl, + }), + method: 'POST', }), - method: 'POST', - }), + ), ); - const { principal: verifiedPrincipal } = await verifiedGatewayAssertion( + const { principal: verifiedPrincipal } = yield* verifiedGatewayAssertion( assertionResponse, pair.publicKey, ); - assert.equal(verifiedPrincipal.authBindingId, secondAuthBindingId); - assert.match(optionalText(verifiedPrincipal.authContextRef), /^better-auth-session:/u); - assert.equal(verifiedPrincipal.authMethod, 'session'); - assert.equal(verifiedPrincipal.legalEntityId, secondLegalEntityId); - assert.equal(verifiedPrincipal.principalId, secondPrincipalId); - assert.equal(verifiedPrincipal.tenantId, secondTenantId); - + expect(verifiedPrincipal.authBindingId).toBe(secondAuthBindingId); + expect(optionalText(verifiedPrincipal.authContextRef)).toMatch(/^better-auth-session:/u); + expect(verifiedPrincipal.authMethod).toBe('session'); + expect(verifiedPrincipal.legalEntityId).toBe(secondLegalEntityId); + expect(verifiedPrincipal.principalId).toBe(secondPrincipalId); + expect(verifiedPrincipal.tenantId).toBe(secondTenantId); // A non-unavailability persistence rejection is an unexpected defect. The real Better Auth // adapter must roll it back, while each owning HTTP boundary logs and returns a redacted 500. - await runEffectTestPromise( - adminAuthDatabase.execute( - sql.raw(` + yield* adminAuthDatabase.execute( + sql.raw(` CREATE OR REPLACE FUNCTION auth.tenant_switch_test_fail_internal_persistence() RETURNS trigger LANGUAGE plpgsql @@ -1591,134 +1590,110 @@ test('selects, lists, switches, revalidates, and upgrades a multi-tenant session END; $function$ `), - ), ); - await runEffectTestPromise( - adminAuthDatabase.execute( - sql.raw(` + yield* adminAuthDatabase.execute( + sql.raw(` CREATE TRIGGER tenant_switch_test_internal_persistence_failure BEFORE UPDATE ON auth.session FOR EACH ROW EXECUTE FUNCTION auth.tenant_switch_test_fail_internal_persistence() `), - ), ); - try { - const unexpectedSwitchResponse = await runtime.handler( - tenantSwitchRequest(firstTenantId, { - 'x-correlation-id': 'unexpected-switch-persistence-test', - }), - ); - assert.equal(unexpectedSwitchResponse.status, 500); - assert.doesNotMatch( - await unexpectedSwitchResponse.text(), - /secret auth persistence defect|P0001/u, - ); - const sessionsAfterUnexpectedSwitchFailure = await readActiveTenantIds(); - assertOptionalField( - sessionsAfterUnexpectedSwitchFailure[0], - 'activeTenantId', - secondTenantId, - ); - - await runEffectTestPromise( - authDatabase - .update(session) - .set({ activeTenantId: null }) - .where(eq(session.userId, betterAuthUserId)), - ); - const unexpectedLegacyUpgradeResponse = await runtime.handler( - new Request(`${configuration.baseUrl}/auth/session`, { - headers: new Headers({ - cookie: authenticatedCookie, - origin: configuration.baseUrl, - 'x-correlation-id': 'unexpected-legacy-upgrade-test', - }), - }), - ); - assert.equal(unexpectedLegacyUpgradeResponse.status, 500); - assert.doesNotMatch( - await unexpectedLegacyUpgradeResponse.text(), - /secret auth persistence defect|P0001/u, - ); - const sessionsAfterUnexpectedLegacyUpgrade = await readActiveTenantIds(); - assertOptionalField(sessionsAfterUnexpectedLegacyUpgrade[0], 'activeTenantId', null); - } finally { - await runEffectTestPromise( - adminAuthDatabase.execute( - sql.raw(` + yield* Effect.scoped( + Effect.gen(function* integrationEffect12() { + yield* Effect.acquireRelease( + Effect.void, + Effect.fnUntraced(function* integrationEffect13() { + yield* adminAuthDatabase.execute( + sql.raw(` DROP TRIGGER IF EXISTS tenant_switch_test_internal_persistence_failure ON auth.session `), - ), - ); - await runEffectTestPromise( - adminAuthDatabase.execute( - sql.raw(` + ); + yield* adminAuthDatabase.execute( + sql.raw(` DROP FUNCTION IF EXISTS auth.tenant_switch_test_fail_internal_persistence() `), - ), - ); - } - - const upgradedSession = await runEffectTestPromise( - authentication - .currentSession(authenticatedHeaders) - .pipe(Effect.provide(multiAuthenticationContextLayer)), + ); + }, Effect.orDie), + ); + const unexpectedSwitchResponse = yield* Effect.tryPromise(() => + runtime.handler( + tenantSwitchRequest(firstTenantId, { + 'x-correlation-id': 'unexpected-switch-persistence-test', + }), + ), + ); + expect(unexpectedSwitchResponse.status).toBe(500); + expect(yield* Effect.tryPromise(() => unexpectedSwitchResponse.text())).not.toMatch( + /secret auth persistence defect|P0001/u, + ); + const sessionsAfterUnexpectedSwitchFailure = yield* readActiveTenantIds(); + assertOptionalField( + sessionsAfterUnexpectedSwitchFailure[0], + 'activeTenantId', + secondTenantId, + ); + yield* authDatabase + .update(session) + .set({ activeTenantId: null }) + .where(eq(session.userId, betterAuthUserId)); + const unexpectedLegacyUpgradeResponse = yield* Effect.tryPromise(() => + runtime.handler( + new Request(`${configuration.baseUrl}/auth/session`, { + headers: new Headers({ + cookie: authenticatedCookie, + origin: configuration.baseUrl, + 'x-correlation-id': 'unexpected-legacy-upgrade-test', + }), + }), + ), + ); + expect(unexpectedLegacyUpgradeResponse.status).toBe(500); + expect(yield* Effect.tryPromise(() => unexpectedLegacyUpgradeResponse.text())).not.toMatch( + /secret auth persistence defect|P0001/u, + ); + const sessionsAfterUnexpectedLegacyUpgrade = yield* readActiveTenantIds(); + assertOptionalField(sessionsAfterUnexpectedLegacyUpgrade[0], 'activeTenantId', null); + }), ); + const upgradedSession = yield* authentication + .currentSession(authenticatedHeaders) + .pipe(Effect.provide(multiAuthenticationContextLayer)); assertOptionalField(upgradedSession.identity, 'tenantId', firstTenantId); - const upgradedSessionRows = await readActiveTenantIds(); + const upgradedSessionRows = yield* readActiveTenantIds(); assertOptionalField(upgradedSessionRows[0], 'activeTenantId', firstTenantId); - - await runEffectTestPromise( - authentication - .switchTenant(secondTenantId, authenticatedHeaders) - .pipe(Effect.provide(multiAuthenticationContextLayer)), - ); - await runEffectTestPromise( - coreDatabase - .update(principalAuthBindings) - .set({ revokedAt: new Date('2026-09-01T00:00:00.000Z'), status: 'revoked' }) - .where(eq(principalAuthBindings.tenantId, secondTenantId)), - ); - await assertSessionForbidden( - authentication - .currentSession(authenticatedHeaders) - .pipe(Effect.provide(multiAuthenticationContextLayer)), - ); - await runEffectTestPromise( - coreDatabase - .update(principalAuthBindings) - .set({ revokedAt: null, status: 'active' }) - .where(eq(principalAuthBindings.tenantId, secondTenantId)), - ); - const restoredSession = await runEffectTestPromise( + yield* authentication + .switchTenant(secondTenantId, authenticatedHeaders) + .pipe(Effect.provide(multiAuthenticationContextLayer)); + yield* coreDatabase + .update(principalAuthBindings) + .set({ revokedAt: new Date('2026-09-01T00:00:00.000Z'), status: 'revoked' }) + .where(eq(principalAuthBindings.tenantId, secondTenantId)); + yield* assertSessionForbidden( authentication .currentSession(authenticatedHeaders) .pipe(Effect.provide(multiAuthenticationContextLayer)), ); + yield* coreDatabase + .update(principalAuthBindings) + .set({ revokedAt: null, status: 'active' }) + .where(eq(principalAuthBindings.tenantId, secondTenantId)); + const restoredSession = yield* authentication + .currentSession(authenticatedHeaders) + .pipe(Effect.provide(multiAuthenticationContextLayer)); assertOptionalField(restoredSession.identity, 'tenantId', secondTenantId); - // Production evidence retains referenced bindings. Clear only this fixture's evidence so the // resolver can still prove that an existing selected session rejects a genuinely missing row. - await runEffectTestPromise( + yield* purgeFixtureRows([ coreDatabase.delete(dataAccessEvents).where(eq(dataAccessEvents.tenantId, secondTenantId)), - ); - await runEffectTestPromise( coreDatabase .delete(principalAuthBindings) .where(eq(principalAuthBindings.tenantId, secondTenantId)), - ); - await assertSessionForbidden( + ]); + yield* assertSessionForbidden( authentication .currentSession(authenticatedHeaders) .pipe(Effect.provide(multiAuthenticationContextLayer)), ); - } finally { - await Promise.all(handlers.map(async ({ dispose }) => await dispose())); - await cleanup(); - await Promise.all([adminPool.end(), corePool.end()]); - } -}); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), + }), ); diff --git a/app/apps/shell-super-app/tests/integration/generated-owner-fixture.ts b/app/apps/shell-super-app/tests/integration/generated-owner-fixture.ts index 078e30eef..fb5da75cf 100644 --- a/app/apps/shell-super-app/tests/integration/generated-owner-fixture.ts +++ b/app/apps/shell-super-app/tests/integration/generated-owner-fixture.ts @@ -1,7 +1,7 @@ -import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises'; +import { Effect, FileSystem } from 'effect'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import { runScaffold } from '../../../../scripts/scaffolding/cli.mts'; +import { runScaffoldEffect } from '../../../../scripts/scaffolding/cli.mts'; import { MODULE_MANIFEST_RESOURCE_SLOT_END, MODULE_MANIFEST_RESOURCE_SLOT_START, @@ -18,15 +18,16 @@ export const GENERATED_OWNER = { const json = (value: Value): string => `${JSON.stringify(value, null, 2)}\n`; const appRoot = path.resolve(import.meta.dirname, '..', '..', '..', '..'); -const writeFixtureFile = async ( +const writeFixtureFile = Effect.fn('writeFixtureFile')(function* writeFixtureFileEffect( root: string, relativePath: string, content: string, -): Promise => { +) { + const fileSystem = yield* FileSystem.FileSystem; const filePath = path.join(root, relativePath); - await mkdir(path.dirname(filePath), { recursive: true }); - await writeFile(filePath, content, 'utf-8'); -}; + yield* fileSystem.makeDirectory(path.dirname(filePath), { recursive: true }); + yield* fileSystem.writeFileString(filePath, content); +}); const replaceRequired = (source: string, current: string, replacement: string): string => { if (!source.includes(current)) { @@ -35,23 +36,23 @@ const replaceRequired = (source: string, current: string, replacement: string): return source.replace(current, replacement); }; -const createWorkspace = async (root: string): Promise => { - await writeFixtureFile( +const createWorkspace = Effect.fn('createWorkspace')(function* createWorkspaceEffect(root: string) { + yield* writeFixtureFile( root, 'package.json', json({ name: 'generated-owner-fixture', private: true }), ); - await writeFixtureFile( + yield* writeFixtureFile( root, `verticals/${GENERATED_OWNER.slug}/module-federation.config.ts`, 'export default { exposes: {} };\n', ); - await writeFixtureFile( + yield* writeFixtureFile( root, `verticals/${GENERATED_OWNER.slug}/tsconfig.json`, json({ compilerOptions: { composite: true }, include: ['api', 'shared', 'src'] }), ); - await writeFixtureFile( + yield* writeFixtureFile( root, `verticals/${GENERATED_OWNER.slug}/package.json`, json({ @@ -74,14 +75,14 @@ const createWorkspace = async (root: string): Promise => { version: '0.0.0', }), ); - await writeFixtureFile( + yield* writeFixtureFile( root, `verticals/${GENERATED_OWNER.slug}/shared/api.ts`, `import { HttpApi } from '@modern-js/plugin-bff/effect-client'; export const isolationOwnerApi = HttpApi.make('IsolationOwnerApi'); `, ); - await writeFixtureFile( + yield* writeFixtureFile( root, `verticals/${GENERATED_OWNER.slug}/api/index.ts`, `import { defineEffectBff, HttpApiBuilder, Layer } from '@modern-js/plugin-bff/effect-edge'; @@ -94,12 +95,12 @@ const layer = HttpApiBuilder.layer(isolationOwnerApi).pipe( export default defineEffectBff({ api: isolationOwnerApi, layer }); `, ); - await writeFixtureFile( + yield* writeFixtureFile( root, `verticals/${GENERATED_OWNER.slug}/src/routes/ultramodern-route-head.tsx`, 'export const UltramodernRouteHead = () => null;\n', ); - await writeFixtureFile( + yield* writeFixtureFile( root, 'topology/reference-topology.json', json({ @@ -116,53 +117,55 @@ export default defineEffectBff({ api: isolationOwnerApi, layer }); ], }), ); -}; +}); -const linkRuntimeDependencies = async (root: string): Promise => { - await mkdir(path.join(root, 'node_modules', '@app'), { recursive: true }); - await mkdir(path.join(root, 'node_modules', '@modern-js'), { recursive: true }); - await Promise.all([ - symlink( - path.join(appRoot, 'packages/core-runtime'), - path.join(root, 'node_modules/@app/core-runtime'), - 'dir', - ), - symlink( - path.join(appRoot, 'packages/shared-contracts'), - path.join(root, 'node_modules/@app/shared-contracts'), - 'dir', - ), - symlink( - path.join(appRoot, 'packages/gateway-principal-verifier'), - path.join(root, 'node_modules/@app/gateway-principal-verifier'), - 'dir', - ), - symlink( - path.join(appRoot, 'apps/shell-super-app/node_modules/@modern-js/plugin-bff'), - path.join(root, 'node_modules/@modern-js/plugin-bff'), - 'dir', - ), - symlink( - path.join(appRoot, 'apps/shell-super-app/node_modules/drizzle-orm'), - path.join(root, 'node_modules/drizzle-orm'), - 'dir', - ), - symlink( - path.join(appRoot, 'node_modules/effect'), - path.join(root, 'node_modules/effect'), - 'dir', - ), - symlink( - path.join(appRoot, 'apps/shell-super-app/node_modules/jose'), - path.join(root, 'node_modules/jose'), - 'dir', - ), - ]); -}; +const linkRuntimeDependencies = Effect.fn('linkRuntimeDependencies')( + function* linkRuntimeDependenciesEffect(root: string) { + const fileSystem = yield* FileSystem.FileSystem; + yield* fileSystem.makeDirectory(path.join(root, 'node_modules', '@app'), { recursive: true }); + yield* fileSystem.makeDirectory(path.join(root, 'node_modules', '@modern-js'), { + recursive: true, + }); + yield* Effect.all( + [ + fileSystem.symlink( + path.join(appRoot, 'packages/core-runtime'), + path.join(root, 'node_modules/@app/core-runtime'), + ), + fileSystem.symlink( + path.join(appRoot, 'packages/shared-contracts'), + path.join(root, 'node_modules/@app/shared-contracts'), + ), + fileSystem.symlink( + path.join(appRoot, 'packages/gateway-principal-verifier'), + path.join(root, 'node_modules/@app/gateway-principal-verifier'), + ), + fileSystem.symlink( + path.join(appRoot, 'apps/shell-super-app/node_modules/@modern-js/plugin-bff'), + path.join(root, 'node_modules/@modern-js/plugin-bff'), + ), + fileSystem.symlink( + path.join(appRoot, 'apps/shell-super-app/node_modules/drizzle-orm'), + path.join(root, 'node_modules/drizzle-orm'), + ), + fileSystem.symlink( + path.join(appRoot, 'node_modules/effect'), + path.join(root, 'node_modules/effect'), + ), + fileSystem.symlink( + path.join(appRoot, 'apps/shell-super-app/node_modules/jose'), + path.join(root, 'node_modules/jose'), + ), + ], + { concurrency: 'unbounded', discard: true }, + ); + }, +); -const addResourceType = async (root: string): Promise => { +const addResourceType = Effect.fn('addResourceType')(function* addResourceTypeEffect(root: string) { + const fileSystem = yield* FileSystem.FileSystem; const manifestPath = path.join(root, `verticals/${GENERATED_OWNER.slug}/vertical.manifest.ts`); - const manifest = await readFile(manifestPath, 'utf-8'); + const manifest = yield* fileSystem.readFileString(manifestPath); const withResourceType = replaceRequired( manifest, ` ${MODULE_MANIFEST_RESOURCE_SLOT_START} @@ -202,7 +205,7 @@ const addResourceType = async (root: string): Promise => { }, ],`, ); - await writeFile( + yield* fileSystem.writeFileString( manifestPath, replaceRequired( withResourceDetail, @@ -223,18 +226,18 @@ const addResourceType = async (root: string): Promise => { }, ],`, ), - 'utf-8', ); -}; +}); -const adaptContract = async ( +const adaptContract = Effect.fn('adaptContract')(function* adaptContractEffect( root: string, name: 'resource-detail' | 'resource-list', request: string, response: string, -): Promise => { +) { + const fileSystem = yield* FileSystem.FileSystem; const contractPath = path.join(root, `verticals/${GENERATED_OWNER.slug}/shared/apis/${name}.ts`); - let contract = await readFile(contractPath, 'utf-8'); + let contract = yield* fileSystem.readFileString(contractPath); const type = name === 'resource-detail' ? 'ResourceDetail' : 'ResourceList'; contract = replaceRequired( contract, @@ -246,8 +249,8 @@ const adaptContract = async ( `export const ${type}ResponseSchema = Schema.Struct({ ok: Schema.Literal(true) });`, `export const ${type}ResponseSchema = ${response};`, ); - await writeFile(contractPath, contract, 'utf-8'); -}; + yield* fileSystem.writeFileString(contractPath, contract); +}); const ownerRepositorySource = (schemaName: string): string => ` // Test-owned adaptation of Codesmith-generated disposable owner artifacts. @@ -303,7 +306,7 @@ import { defineRead, defineTenantModuleEntrypoint, } from '@app/core-runtime'; -import { Effect } from 'effect'; +import { Effect, FileSystem } from 'effect'; import { ResourceDetailRequestSchema, ResourceDetailResponseSchema } from '../../shared/apis/resource-detail.ts'; import { generatedOwnerHandlerCounts } from '../isolation/instrumentation.ts'; import { makeOwnerRepository } from '../isolation/owner-repository.ts'; @@ -354,7 +357,7 @@ export const resourceDetailRead = defineRead( const listReadSource = ` // @generated by OntOS Codesmith module-api v1 import { ReadHandlerUnavailable, defineRead, defineTenantModuleEntrypoint } from '@app/core-runtime'; -import { Effect } from 'effect'; +import { Effect, FileSystem } from 'effect'; import { ResourceListRequestSchema, ResourceListResponseSchema } from '../../shared/apis/resource-list.ts'; import { generatedOwnerHandlerCounts } from '../isolation/instrumentation.ts'; import { makeOwnerRepository } from '../isolation/owner-repository.ts'; @@ -409,7 +412,7 @@ const searchReadSource = ` // @generated by OntOS Codesmith Governed Contribution v1 // @ontos-contribution-kind search-provider import { ReadHandlerUnavailable, defineRead, defineTenantModuleEntrypoint } from '@app/core-runtime'; -import { Effect } from 'effect'; +import { Effect, FileSystem } from 'effect'; import { RecordsProviderRequestSchema, RecordsProviderResponseSchema } from '../../shared/apis/records-search.ts'; import { generatedOwnerHandlerCounts } from '../isolation/instrumentation.ts'; import { makeOwnerRepository } from '../isolation/owner-repository.ts'; @@ -511,9 +514,12 @@ export const createRecordAction = defineAction( ); `; -const adaptGeneratedOwner = async (root: string, schemaName: string): Promise => { +const adaptGeneratedOwner = Effect.fn('adaptGeneratedOwner')(function* adaptGeneratedOwnerEffect( + root: string, + schemaName: string, +) { const verticalRoot = `verticals/${GENERATED_OWNER.slug}`; - await adaptContract( + yield* adaptContract( root, 'resource-detail', 'Schema.Struct({ resourceId: Schema.String.check(Schema.isUUID()) })', @@ -522,7 +528,7 @@ const adaptGeneratedOwner = async (root: string, schemaName: string): Promise Promise; - readonly root: string; - readonly verticalRoot: string; -} + yield* Effect.all( + [ + writeFixtureFile( + root, + `${verticalRoot}/src/isolation/instrumentation.ts`, + instrumentationSource, + ), + writeFixtureFile( + root, + `${verticalRoot}/src/isolation/owner-repository.ts`, + ownerRepositorySource(schemaName), + ), + writeFixtureFile(root, `${verticalRoot}/src/api/resource-detail.read.ts`, detailReadSource), + writeFixtureFile(root, `${verticalRoot}/src/api/resource-list.read.ts`, listReadSource), + writeFixtureFile(root, `${verticalRoot}/src/search/records.provider.ts`, searchReadSource), + writeFixtureFile(root, `${verticalRoot}/src/actions/create-record.action.ts`, actionSource), + ], + { concurrency: 'unbounded', discard: true }, + ); +}); -export const createGeneratedOwnerFixture = async ( - schemaName: string, -): Promise => { - const root = await mkdtemp(path.join(tmpdir(), 'ontos-generated-owner-')); - try { - await createWorkspace(root); - await runScaffold( +export const createGeneratedOwnerFixture = Effect.fn('createGeneratedOwnerFixture')( + function* createGeneratedOwnerFixtureEffect(schemaName: string) { + const fileSystem = yield* FileSystem.FileSystem; + const root = yield* fileSystem.makeTempDirectoryScoped({ + directory: tmpdir(), + prefix: 'ontos-generated-owner-', + }); + yield* createWorkspace(root); + yield* runScaffoldEffect( 'module-contract', ['--vertical', GENERATED_OWNER.slug, '--module', GENERATED_OWNER.moduleId], { workspaceRoot: root }, ); - await addResourceType(root); - await runScaffold( + yield* addResourceType(root); + yield* runScaffoldEffect( 'action', [ '--vertical', @@ -587,7 +592,7 @@ export const createGeneratedOwnerFixture = async ( ], { workspaceRoot: root }, ); - await runScaffold( + yield* runScaffoldEffect( 'module-api', [ '--vertical', @@ -601,7 +606,7 @@ export const createGeneratedOwnerFixture = async ( ], { workspaceRoot: root }, ); - await runScaffold( + yield* runScaffoldEffect( 'module-api', [ '--vertical', @@ -615,7 +620,7 @@ export const createGeneratedOwnerFixture = async ( ], { workspaceRoot: root }, ); - await runScaffold( + yield* runScaffoldEffect( 'search-provider', [ '--vertical', @@ -631,15 +636,11 @@ export const createGeneratedOwnerFixture = async ( ], { workspaceRoot: root }, ); - await adaptGeneratedOwner(root, schemaName); - await linkRuntimeDependencies(root); + yield* adaptGeneratedOwner(root, schemaName); + yield* linkRuntimeDependencies(root); return { - dispose: async () => await rm(root, { force: true, recursive: true }), root, verticalRoot: path.join(root, 'verticals', GENERATED_OWNER.slug), }; - } catch (error) { - await rm(root, { force: true, recursive: true }); - throw error; - } -}; + }, +); diff --git a/app/apps/shell-super-app/tests/integration/generated-owner-isolation.test.ts b/app/apps/shell-super-app/tests/integration/generated-owner-isolation.test.ts index 1bb9104f0..57dc30494 100644 --- a/app/apps/shell-super-app/tests/integration/generated-owner-isolation.test.ts +++ b/app/apps/shell-super-app/tests/integration/generated-owner-isolation.test.ts @@ -1,35 +1,26 @@ import { makeContextAccessDouble } from '../support/context-access-double.ts'; +import { TestClock } from 'effect/testing'; +import { expect, it } from 'effect-rstest'; +import { NodeServices } from '@effect/platform-node'; import { makeFaultInjectableCoreDatabase, TestQueryHook, } from '../../../../packages/core-runtime/tests/support/database-faults.ts'; import { SqlError, UnknownError } from 'effect/unstable/sql/SqlError'; - import { - Scope as NativeScope, - Exit as NativeExit, Clock, Config, ConfigProvider, Effect, Layer, Logger, - ManagedRuntime, Predicate, Redacted, Schema, } from 'effect'; -import { - runEffectTestPromise, - runEffectTestSync as runNativeSync, - makeEffectTestCallback as nativeTestCallback, -} from '@app/core-runtime/testing/effect-runtime'; - -import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import { readFile } from 'node:fs/promises'; import { pathToFileURL } from 'node:url'; -import test, { after as afterNativeDatabase } from 'node:test'; import { v1 } from '@authzed/authzed-node'; import { ContextAccess, @@ -53,16 +44,13 @@ import type { } from '@app/core-runtime'; import { defineEffectBff, HttpApiBuilder } from '@modern-js/plugin-bff/effect-edge'; import type { EffectRuntimeLayer } from '@modern-js/plugin-bff/effect-edge'; - import { HttpApi } from 'effect/unstable/httpapi'; -import { TestClock } from 'effect/testing'; import { exportJWK, generateKeyPair } from 'jose'; import { Pool } from 'pg'; import { GatewayPrincipalVerifierLive } from '../../../../packages/gateway-principal-verifier/src/server.ts'; import { makeActionRepository } from '../../../../packages/core-runtime/src/actions/repository.ts'; import { makeActionRuntime } from '../../../../packages/core-runtime/src/actions/runtime.ts'; import { loadDatabaseConnectionPair } from '../../../../packages/core-runtime/src/db/config.ts'; - import { makeModuleEntrypointGateway } from '../../../../packages/core-runtime/src/modules/module-entrypoint-gateway.ts'; import { makeModuleStateGate } from '../../../../packages/core-runtime/src/modules/module-state-gate.ts'; import { makeTenantModuleStateService } from '../../../../packages/core-runtime/src/modules/tenant-module-state-service.ts'; @@ -106,8 +94,6 @@ import { import type { ShellResourceGateways } from '../../api/modules/shell-resources.ts'; import { GENERATED_OWNER, createGeneratedOwnerFixture } from './generated-owner-fixture.ts'; -const nativeDatabaseScope = runNativeSync(NativeScope.make()); - const withOptionalProperty = < Base extends object, Key extends PropertyKey, @@ -120,7 +106,6 @@ const withOptionalProperty = < value: Value, trailing: Trailing, ) => (condition ? { ...base, [key]: value, ...trailing } : { ...base, ...trailing }); - const TestSpiceDbConfig = Config.all({ endpoint: Config.string('SPICEDB_ENDPOINT').pipe(Config.withDefault('localhost:50051')), insecureLocal: Config.boolean('SPICEDB_INSECURE').pipe(Config.withDefault(true)), @@ -134,16 +119,17 @@ const TestSpiceDbConfig = Config.all({ preSharedKey: Redacted.value(preSharedKey), })), ); - const testGatewayAssertionRedemption: GatewayAssertionRedemption = { consume: () => Effect.void, }; - -interface OwnerHttpHandler { - readonly dispose: () => Promise; - readonly handler: (request: Request) => Promise; -} - +type OwnerHttpHandler = ReturnType['createHandler']>; +const disposeOwnerHandlers = (handlers: readonly OwnerHttpHandler[]) => + Effect.forEach( + handlers, + (handler) => + Effect.tryPromise(() => handler.dispose()).pipe(Effect.catchCause(() => Effect.void)), + { concurrency: 'unbounded', discard: true }, + ); const OwnerDetailSchema = Schema.Struct({ fields: Schema.Array(Schema.Struct({ label: Schema.String, value: Schema.String })), title: Schema.String, @@ -161,12 +147,16 @@ const OwnerTimelineSchema = Schema.Struct({ const OwnerSearchSchema = Schema.Array( Schema.Struct({ ref: ResourceRefSchema, title: Schema.String }), ); - interface GeneratedOwnerModules { // Generated source is imported from a temporary path, so TypeScript cannot retain the private // Action-registration symbols across the dynamic module boundary. Runtime checks below prove it. readonly action: ReturnType[number]; - readonly counts: { action: number; detail: number; list: number; search: number }; + readonly counts: { + action: number; + detail: number; + list: number; + search: number; + }; readonly detail: OwnerHttpHandler; readonly list: OwnerHttpHandler; readonly search: OwnerHttpHandler; @@ -184,11 +174,9 @@ interface GeneratedOwnerModules { readonly searchClient: boolean; }; } - type OwnerApi = HttpApi.Top; type OwnerGroupLayer = Layer.Layer; const DynamicModuleSchema = Schema.Record(Schema.String, Schema.Unknown); -type DynamicModule = typeof DynamicModuleSchema.Type; const OwnerApiSchema = Schema.declare(HttpApi.isHttpApi); const OwnerGroupLayerSchema = Schema.declare(Layer.isLayer); const VerticalRuntimeRegistrationSchema = Schema.declare( @@ -207,14 +195,12 @@ const EffectRuntimeLayerSchema = Schema.declare( (value): value is EffectRuntimeLayer => Predicate.isObjectKeyword(value), ); const isEffectRuntimeLayer = Schema.is(EffectRuntimeLayerSchema); - const requiredValue = (value: Value | null | undefined, label: string): Value => { if (value === undefined || value === null) { throw new TypeError(`${label} is required by the generated-owner fixture`); } return value; }; - const isOperationContextDenied = Schema.is( Schema.Struct({ _tag: Schema.Literal('OperationContextDenied') }), ); @@ -224,7 +210,6 @@ const isCreateRecordRejected = Schema.is( const isActionHandlerExecutionError = Schema.is( Schema.Struct({ _tag: Schema.Literal('ActionHandlerExecutionError') }), ); - const relationship = ( resourceType: string, resourceId: string, @@ -239,10 +224,8 @@ const relationship = ( object: v1.ObjectReference.create({ objectId: subjectId, objectType: subjectType }), }), }); - const makeCatalog = (contract: OntosModuleDeploymentContract): InstalledModuleCatalog => buildInstalledModuleCatalog([{ contract, expectedAppId: GENERATED_OWNER.appId }]); - const makeOwnerHandler = ( api: OwnerApi, group: OwnerGroupLayer, @@ -269,13 +252,17 @@ const makeOwnerHandler = ( const handler: OwnerHttpHandler = bff.createHandler(); return handler; }; - -const loadClientWiring = async (entrypoints: ReturnType) => { - const [detailClient, listClient, searchClient] = await Promise.all([ - entrypoints.api['resource-detail']?.(), - entrypoints.api['resource-list']?.(), - entrypoints.search['records']?.(), - ]); +const loadClientWiring = Effect.fnUntraced(function* loadClientWiring( + entrypoints: ReturnType, +) { + const [detailClient, listClient, searchClient] = yield* Effect.all( + [ + Effect.tryPromise(() => Promise.resolve(entrypoints.api['resource-detail']?.())), + Effect.tryPromise(() => Promise.resolve(entrypoints.api['resource-list']?.())), + Effect.tryPromise(() => Promise.resolve(entrypoints.search['records']?.())), + ], + { concurrency: 'unbounded' }, + ); return { action: true, detailClient: @@ -295,20 +282,20 @@ const loadClientWiring = async (entrypoints: ReturnType, configLayer: Layer.Layer, -): Promise => { - const load = async (relativePath: string): Promise => { - const importedModule: unknown = await import( - pathToFileURL(`${verticalRoot}/${relativePath}`).href +) { + const load = Effect.fnUntraced(function* runIntegration2(relativePath: string) { + const importedModule: unknown = yield* Effect.tryPromise( + () => import(pathToFileURL(`${verticalRoot}/${relativePath}`).href), ); - return Schema.decodeUnknownSync(DynamicModuleSchema)(importedModule); - }; + return yield* Schema.decodeUnknownEffect(DynamicModuleSchema)(importedModule); + }); const [ detailApi, detailServer, @@ -319,23 +306,26 @@ const loadGeneratedOwner = async ( verifier, state, registrationOwner, - ] = await Promise.all([ - load('shared/apis/resource-detail.ts'), - load('api/resource-detail-read-server.ts'), - load('shared/apis/resource-list.ts'), - load('api/resource-list-read-server.ts'), - load('shared/apis/records-search.ts'), - load('api/records-search-server.ts'), - load('api/auth/action-principal.ts'), - load('src/isolation/instrumentation.ts'), - load('vertical.registration.ts'), - ]); - const registration = Schema.decodeUnknownSync(VerticalRuntimeRegistrationSchema)( + ] = yield* Effect.all( + [ + load('shared/apis/resource-detail.ts'), + load('api/resource-detail-read-server.ts'), + load('shared/apis/resource-list.ts'), + load('api/resource-list-read-server.ts'), + load('shared/apis/records-search.ts'), + load('api/records-search-server.ts'), + load('api/auth/action-principal.ts'), + load('src/isolation/instrumentation.ts'), + load('vertical.registration.ts'), + ], + { concurrency: 'unbounded' }, + ); + const registration = yield* Schema.decodeUnknownEffect(VerticalRuntimeRegistrationSchema)( registrationOwner['isolationOwnerRegistration'], ); const actions = getVerticalRuntimeActions(registration); const entrypoints = getVerticalRuntimeEntrypoints(registration); - const wiring = await loadClientWiring(entrypoints); + const wiring = yield* loadClientWiring(entrypoints); const generatedAction = actions.find( ({ descriptor }) => descriptor.actionKey === GENERATED_OWNER.actionKey, ); @@ -344,73 +334,86 @@ const loadGeneratedOwner = async ( } return { action: generatedAction, - counts: Schema.decodeUnknownSync(OwnerCountsSchema)(state['generatedOwnerHandlerCounts']), + counts: yield* Schema.decodeUnknownEffect(OwnerCountsSchema)( + state['generatedOwnerHandlerCounts'], + ), detail: makeOwnerHandler( - Schema.decodeUnknownSync(OwnerApiSchema)(detailApi['ResourceDetailApi']), - Schema.decodeUnknownSync(OwnerGroupLayerSchema)(detailServer['resourceDetailReadApiLive']), + yield* Schema.decodeUnknownEffect(OwnerApiSchema)(detailApi['ResourceDetailApi']), + yield* Schema.decodeUnknownEffect(OwnerGroupLayerSchema)( + detailServer['resourceDetailReadApiLive'], + ), runtime, loggerLayer, configLayer, ), list: makeOwnerHandler( - Schema.decodeUnknownSync(OwnerApiSchema)(listApi['ResourceListApi']), - Schema.decodeUnknownSync(OwnerGroupLayerSchema)(listServer['resourceListReadApiLive']), + yield* Schema.decodeUnknownEffect(OwnerApiSchema)(listApi['ResourceListApi']), + yield* Schema.decodeUnknownEffect(OwnerGroupLayerSchema)( + listServer['resourceListReadApiLive'], + ), runtime, loggerLayer, configLayer, ), search: makeOwnerHandler( - Schema.decodeUnknownSync(OwnerApiSchema)(searchApi['RecordsSearchApi']), - Schema.decodeUnknownSync(OwnerGroupLayerSchema)(searchServer['recordsReadApiLive']), + yield* Schema.decodeUnknownEffect(OwnerApiSchema)(searchApi['RecordsSearchApi']), + yield* Schema.decodeUnknownEffect(OwnerGroupLayerSchema)(searchServer['recordsReadApiLive']), runtime, loggerLayer, configLayer, ), - verifyActionPrincipal: Schema.decodeUnknownSync(OwnerVerifierSchema)( + verifyActionPrincipal: yield* Schema.decodeUnknownEffect(OwnerVerifierSchema)( verifier['verifyActionPrincipal'], ), wiring, }; -}; - -const requestOwner = async ( +}); +const requestOwner = Effect.fnUntraced(function* runIntegration3( handler: OwnerHttpHandler, path: string, payload: Payload, authorization: string, correlationId: string, -): Promise => - await handler.handler( - new Request(`https://isolation-owner.example.test${path}`, { - body: JSON.stringify(payload), - headers: { - authorization, - 'content-type': 'application/json', - 'x-correlation-id': correlationId, - }, - method: 'POST', - }), +) { + return yield* Effect.tryPromise(() => + handler.handler( + new Request(`https://isolation-owner.example.test${path}`, { + body: JSON.stringify(payload), + headers: { + authorization, + 'content-type': 'application/json', + 'x-correlation-id': correlationId, + }, + method: 'POST', + }), + ), ); - -const decodeResponse = async >( - response: Response, - schema: ResponseSchema, -): Promise => Schema.decodeUnknownSync(schema)(await response.json()); - -const createOwnerSchema = async (admin: Pool, schemaName: string): Promise => { +}); +const decodeResponse = Effect.fnUntraced(function* runIntegration4< + ResponseSchema extends Schema.ConstraintDecoder, +>(response: Response, schema: ResponseSchema) { + return yield* Schema.decodeUnknownEffect(schema)(yield* Effect.tryPromise(() => response.json())); +}); +const createOwnerSchema = Effect.fnUntraced(function* runIntegration5( + admin: Pool, + schemaName: string, +) { const tenantPredicate = `tenant_id = nullif(current_setting('ontos.tenant_id', true), '')::uuid`; const entityPredicate = `${tenantPredicate} and legal_entity_id = nullif(current_setting('ontos.legal_entity_id', true), '')::uuid`; // Dynamic identifiers are generated locally from UUID hex and never accept external input. - await admin.query(`create schema ${schemaName}`); - await admin.query(` + yield* Effect.tryPromise(() => admin.query(`create schema ${schemaName}`)); + yield* Effect.tryPromise(() => + admin.query(` create table ${schemaName}.tenant_records ( tenant_id uuid not null, resource_id uuid not null, title text not null, primary key (tenant_id, resource_id) ) - `); - await admin.query(` + `), + ); + yield* Effect.tryPromise(() => + admin.query(` create table ${schemaName}.entity_records ( tenant_id uuid not null, legal_entity_id uuid not null, @@ -418,48 +421,70 @@ const createOwnerSchema = async (admin: Pool, schemaName: string): Promise title text not null, primary key (tenant_id, legal_entity_id, resource_id) ) - `); - const configureTable = async (table: string, predicate: string): Promise => { - await admin.query(`alter table ${schemaName}.${table} enable row level security`); - await admin.query(`alter table ${schemaName}.${table} force row level security`); - await admin.query( - `create policy ${table}_select on ${schemaName}.${table} for select to ontos_runtime using (${predicate})`, + `), + ); + const configureTable = Effect.fnUntraced(function* runIntegration6( + table: string, + predicate: string, + ) { + yield* Effect.tryPromise(() => + admin.query(`alter table ${schemaName}.${table} enable row level security`), + ); + yield* Effect.tryPromise(() => + admin.query(`alter table ${schemaName}.${table} force row level security`), + ); + yield* Effect.tryPromise(() => + admin.query( + `create policy ${table}_select on ${schemaName}.${table} for select to ontos_runtime using (${predicate})`, + ), ); - await admin.query( - `create policy ${table}_insert on ${schemaName}.${table} for insert to ontos_runtime with check (${predicate})`, + yield* Effect.tryPromise(() => + admin.query( + `create policy ${table}_insert on ${schemaName}.${table} for insert to ontos_runtime with check (${predicate})`, + ), ); - await admin.query( - `create policy ${table}_update on ${schemaName}.${table} for update to ontos_runtime using (${predicate}) with check (${predicate})`, + yield* Effect.tryPromise(() => + admin.query( + `create policy ${table}_update on ${schemaName}.${table} for update to ontos_runtime using (${predicate}) with check (${predicate})`, + ), ); - await admin.query( - `create policy ${table}_delete on ${schemaName}.${table} for delete to ontos_runtime using (${predicate})`, + yield* Effect.tryPromise(() => + admin.query( + `create policy ${table}_delete on ${schemaName}.${table} for delete to ontos_runtime using (${predicate})`, + ), ); - }; - await Promise.all([ - configureTable('tenant_records', tenantPredicate), - configureTable('entity_records', entityPredicate), - ]); - await admin.query(`grant usage on schema ${schemaName} to ontos_runtime`); - await admin.query( - `grant select, insert, update, delete on all tables in schema ${schemaName} to ontos_runtime`, + }); + yield* Effect.all( + [ + configureTable('tenant_records', tenantPredicate), + configureTable('entity_records', entityPredicate), + ], + { concurrency: 'unbounded' }, ); -}; - + yield* Effect.tryPromise(() => + admin.query(`grant usage on schema ${schemaName} to ontos_runtime`), + ); + yield* Effect.tryPromise(() => + admin.query( + `grant select, insert, update, delete on all tables in schema ${schemaName} to ontos_runtime`, + ), + ); +}); type CoreDatabaseService = Parameters[0]; type RuntimeActionRegistration = ActionRegistration< Schema.ConstraintDecoder, Schema.ConstraintDecoder, - Schema.ConstraintDecoder<{ readonly _tag: string }>, + Schema.ConstraintDecoder<{ + readonly _tag: string; + }>, DomainEventContractMap, string, unknown >; - const RuntimeActionRegistrationSchema = Schema.declare( (value): value is RuntimeActionRegistration => Predicate.isObjectKeyword(value), ); const isRuntimeActionRegistration = Schema.is(RuntimeActionRegistrationSchema); - const failingEvidenceDatabase = (database: CoreDatabaseService): CoreDatabaseService => { const transactionOverride = { transaction: (runInTransaction, configuration) => @@ -488,17 +513,15 @@ const failingEvidenceDatabase = (database: CoreDatabaseService): CoreDatabaseSer ); return { executor }; }; - const capturedLoggerLayer = (entries: string[]) => Logger.layer([ Logger.make((options) => { entries.push(JSON.stringify(Logger.formatStructured.log(options))); }), ]); - -const ignorePromiseFailure = (operation: () => Promise): Effect.Effect => - Effect.tryPromise({ catch: () => null, try: operation }).pipe(Effect.ignore); - +const ignoreOperationFailure = ( + operation: () => Effect.Effect, +): Effect.Effect => operation().pipe(Effect.ignore); const principal = ( tenantId: string, legalEntityId: string, @@ -512,140 +535,157 @@ const principal = ( principalId, tenantId, }); - -test('Codesmith composes the disposable owner Action and receiving read BFFs', async () => { - const fixture = await createGeneratedOwnerFixture( - `generated_owner_${randomUUID().replaceAll('-', '')}`, - ); - const contract = await deriveOntosModuleDeploymentContract({ - vertical: GENERATED_OWNER.slug, - workspaceRoot: fixture.root, - }); - const compileRuntime: ReadRuntimeService = { - runRead: () => Effect.die(new Error('The compile fixture must not execute a governed read')), - }; - const generated = await loadGeneratedOwner( - fixture.verticalRoot, - compileRuntime, - capturedLoggerLayer([]), - TestClock.layer(), - ); - try { - assert.deepEqual(makeCatalog(contract).getByModuleId(GENERATED_OWNER.moduleId), contract); - assert.equal(generated.action.descriptor.actionKey, GENERATED_OWNER.actionKey); - assert.equal(generated.action.descriptor.legalEntityScope, 'required'); - assert.deepEqual(generated.counts, { action: 0, detail: 0, list: 0, search: 0 }); - assert.deepEqual(generated.wiring, { +it.live( + 'Codesmith composes the disposable owner Action and receiving read BFFs', + Effect.fnUntraced(function* runIntegration7() { + const fixture = yield* createGeneratedOwnerFixture( + `generated_owner_${randomUUID().replaceAll('-', '')}`, + ).pipe(Effect.provide(NodeServices.layer)); + const contract = yield* deriveOntosModuleDeploymentContract({ + vertical: GENERATED_OWNER.slug, + workspaceRoot: fixture.root, + }).pipe(Effect.provide(NodeServices.layer)); + const compileRuntime: ReadRuntimeService = { + runRead: () => Effect.die(new Error('The compile fixture must not execute a governed read')), + }; + const generated = yield* loadGeneratedOwner( + fixture.verticalRoot, + compileRuntime, + capturedLoggerLayer([]), + TestClock.layer(), + ); + yield* Effect.acquireRelease( + Effect.void, + Effect.fnUntraced(function* integrationEffect8() { + yield* disposeOwnerHandlers([generated.detail, generated.list, generated.search]); + }, Effect.orDie), + ); + expect(makeCatalog(contract).getByModuleId(GENERATED_OWNER.moduleId)).toEqual(contract); + expect(generated.action.descriptor.actionKey).toBe(GENERATED_OWNER.actionKey); + expect(generated.action.descriptor.legalEntityScope).toBe('required'); + expect(generated.counts).toEqual({ action: 0, detail: 0, list: 0, search: 0 }); + expect(generated.wiring).toEqual({ action: true, detailClient: true, listClient: true, searchClient: true, }); - } finally { - await Promise.allSettled([ - generated.detail.dispose(), - generated.list.dispose(), - generated.search.dispose(), - ]); - await fixture.dispose(); - } -}); - -test('generated owner enforces tenant and legal-entity isolation through Shell, BFF, CoreSDK, SpiceDB, and RLS', async () => { - const schemaName = `generated_owner_${randomUUID().replaceAll('-', '')}`; - const tenantA = randomUUID(); - const tenantB = randomUUID(); - const entityA1 = randomUUID(); - const entityA2 = randomUUID(); - const entityB1 = randomUUID(); - const entityB2 = randomUUID(); - const principalA = randomUUID(); - const principalB = randomUUID(); - const bindingA = randomUUID(); - const bindingB = randomUUID(); - const collidingResourceId = randomUUID(); - const deniedResourceId = randomUUID(); - const testClockLayer = TestClock.layer(); - const effectRuntime = ManagedRuntime.make(testClockLayer); - const connections = await effectRuntime.runPromise(loadDatabaseConnectionPair()); - assert.equal(connections.runtime.user, 'ontos_runtime'); - const admin = new Pool({ connectionString: connections.admin.connectionString }); - // Shell and the independently deployed owner hold separate nested read transactions in this - // in-process fixture, so the shared test pool needs more than one physical connection. - const runtimePool = new Pool({ connectionString: connections.runtime.connectionString, max: 4 }); - const runtimeDatabase = await runEffectTestPromise( - makeFaultInjectableCoreDatabase(connections.runtime).pipe( - NativeScope.provide(nativeDatabaseScope), - ), - ); - const fixture = await createGeneratedOwnerFixture(schemaName); - const contract = await deriveOntosModuleDeploymentContract({ - vertical: GENERATED_OWNER.slug, - workspaceRoot: fixture.root, - }); - const capturedLogs: string[] = []; - const loggerLayer = capturedLoggerLayer(capturedLogs); - const testSpiceDb = await effectRuntime.runPromise(TestSpiceDbConfig); - const spiceAdmin = v1.NewClient( - testSpiceDb.preSharedKey, - testSpiceDb.endpoint, - testSpiceDb.insecureLocal - ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED - : v1.ClientSecurity.SECURE, - ); - const permissionClient = createSpiceDbPermissionClient(testSpiceDb, SPICEDB_CHECK_TIMEOUT_MS); - const contextAccess = makeContextAccess(permissionClient); - const moduleStates = makeTenantModuleStateService(runtimeDatabase); - const moduleStateGate = makeModuleStateGate(moduleStates); - const moduleGateway = makeModuleEntrypointGateway(moduleStateGate); - const scopeResolver = makeOperationalScopeResolver( - makeOperationalScopeRepository(runtimeDatabase), - contextAccess, - ); - const readRuntime = makeReadRuntime(runtimeDatabase, moduleGateway, scopeResolver, contextAccess); - const keyPair = await generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true }); - const privateJwk = await exportJWK(keyPair.privateKey); - const publicJwk = await exportJWK(keyPair.publicKey); - const issuerConfiguration: GatewayIssuerConfigValue = { - issuer: 'https://shell.isolation.test', - privateJwk: { - alg: 'EdDSA', - crv: 'Ed25519', - d: requiredValue(privateJwk.d, 'Private JWK scalar'), - kid: 'generated-owner-test', - kty: 'OKP', - use: 'sig', - x: requiredValue(privateJwk.x, 'Private JWK public coordinate'), - }, - }; - const verifierEnvironment = { - ONTOS_GATEWAY_ISSUER: issuerConfiguration.issuer, - ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify({ - keys: [ - { - ...publicJwk, - alg: 'EdDSA', - kid: issuerConfiguration.privateJwk.kid, - use: 'sig', - }, - ], - }), - }; - const verifierConfigLayer = Layer.merge( - testClockLayer, - ConfigProvider.layer(ConfigProvider.fromUnknown(verifierEnvironment)), - ); - const generated = await loadGeneratedOwner( - fixture.verticalRoot, - readRuntime, - loggerLayer, - verifierConfigLayer, - ); - const handlers: OwnerHttpHandler[] = [generated.detail, generated.list, generated.search]; - let assertionCount = 0; - const issueAuthorization = async (principalContext: TrustedPrincipalContext) => - await effectRuntime.runPromise( - issueGatewayContextAssertion({ + }), +); +it.live( + 'generated owner enforces tenant and legal-entity isolation through Shell, BFF, CoreSDK, SpiceDB, and RLS', + Effect.fnUntraced(function* runIntegration9() { + const schemaName = `generated_owner_${randomUUID().replaceAll('-', '')}`; + const tenantA = randomUUID(); + const tenantB = randomUUID(); + const entityA1 = randomUUID(); + const entityA2 = randomUUID(); + const entityB1 = randomUUID(); + const entityB2 = randomUUID(); + const principalA = randomUUID(); + const principalB = randomUUID(); + const bindingA = randomUUID(); + const bindingB = randomUUID(); + const collidingResourceId = randomUUID(); + const deniedResourceId = randomUUID(); + const testClockLayer = TestClock.layer(); + const connections = yield* loadDatabaseConnectionPair(); + expect(connections.runtime.user).toBe('ontos_runtime'); + const admin = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), + (pool) => Effect.tryPromise(() => pool.end()).pipe(Effect.orDie), + ); + // Shell and the independently deployed owner hold separate nested read transactions in this + // in-process fixture, so the shared test pool needs more than one physical connection. + const runtimePool = yield* Effect.acquireRelease( + Effect.sync( + () => + new Pool({ + connectionString: connections.runtime.connectionString, + max: 4, + }), + ), + (pool) => Effect.tryPromise(() => pool.end()).pipe(Effect.orDie), + ); + const runtimeDatabase = yield* makeFaultInjectableCoreDatabase(connections.runtime); + const fixture = yield* createGeneratedOwnerFixture(schemaName).pipe( + Effect.provide(NodeServices.layer), + ); + const contract = yield* deriveOntosModuleDeploymentContract({ + vertical: GENERATED_OWNER.slug, + workspaceRoot: fixture.root, + }).pipe(Effect.provide(NodeServices.layer)); + const capturedLogs: string[] = []; + const loggerLayer = capturedLoggerLayer(capturedLogs); + const testSpiceDb = yield* TestSpiceDbConfig; + const spiceAdmin = v1.NewClient( + testSpiceDb.preSharedKey, + testSpiceDb.endpoint, + testSpiceDb.insecureLocal + ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED + : v1.ClientSecurity.SECURE, + ); + const permissionClient = createSpiceDbPermissionClient(testSpiceDb, SPICEDB_CHECK_TIMEOUT_MS); + const contextAccess = makeContextAccess(permissionClient); + const moduleStates = makeTenantModuleStateService(runtimeDatabase); + const moduleStateGate = makeModuleStateGate(moduleStates); + const moduleGateway = makeModuleEntrypointGateway(moduleStateGate); + const scopeResolver = makeOperationalScopeResolver( + makeOperationalScopeRepository(runtimeDatabase), + contextAccess, + ); + const readRuntime = makeReadRuntime( + runtimeDatabase, + moduleGateway, + scopeResolver, + contextAccess, + ); + const keyPair = yield* Effect.tryPromise(() => + generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true }), + ); + const privateJwk = yield* Effect.tryPromise(() => exportJWK(keyPair.privateKey)); + const publicJwk = yield* Effect.tryPromise(() => exportJWK(keyPair.publicKey)); + const issuerConfiguration: GatewayIssuerConfigValue = { + issuer: 'https://shell.isolation.test', + privateJwk: { + alg: 'EdDSA', + crv: 'Ed25519', + d: requiredValue(privateJwk.d, 'Private JWK scalar'), + kid: 'generated-owner-test', + kty: 'OKP', + use: 'sig', + x: requiredValue(privateJwk.x, 'Private JWK public coordinate'), + }, + }; + const verifierEnvironment = { + ONTOS_GATEWAY_ISSUER: issuerConfiguration.issuer, + ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify({ + keys: [ + { + ...publicJwk, + alg: 'EdDSA', + kid: issuerConfiguration.privateJwk.kid, + use: 'sig', + }, + ], + }), + }; + const verifierConfigLayer = Layer.merge( + testClockLayer, + ConfigProvider.layer(ConfigProvider.fromUnknown(verifierEnvironment)), + ); + const generated = yield* loadGeneratedOwner( + fixture.verticalRoot, + readRuntime, + loggerLayer, + verifierConfigLayer, + ); + const handlers: OwnerHttpHandler[] = [generated.detail, generated.list, generated.search]; + let assertionCount = 0; + const issueAuthorization = Effect.fnUntraced(function* runIntegration10( + principalContext: TrustedPrincipalContext, + ) { + return yield* issueGatewayContextAssertion({ audience: GENERATED_OWNER.appId, principal: principalContext, }).pipe( @@ -663,134 +703,269 @@ test('generated owner enforces tenant and legal-entity isolation through Shell, }), ), Effect.map(({ token }) => `Bearer ${token}`), - ), - ); - const principalA1 = principal(tenantA, entityA1, principalA, bindingA); - const principalB1 = principal(tenantB, entityB1, principalB, bindingB); - const issueProviderAuthorization = async (context: TrustedPrincipalContext) => - await issueAuthorization( - withOptionalProperty( + Effect.provide(testClockLayer), + ); + }); + const principalA1 = principal(tenantA, entityA1, principalA, bindingA); + const principalB1 = principal(tenantB, entityB1, principalB, bindingB); + const issueProviderAuthorization = Effect.fnUntraced(function* runIntegration11( + context: TrustedPrincipalContext, + ) { + return yield* issueAuthorization( withOptionalProperty( withOptionalProperty( withOptionalProperty( - { - authMethod: context.authMethod, - principalId: context.principalId, - tenantId: context.tenantId, - }, - context.authBindingId !== undefined, - 'authBindingId', - context.authBindingId, + withOptionalProperty( + { + authMethod: context.authMethod, + principalId: context.principalId, + tenantId: context.tenantId, + }, + context.authBindingId !== undefined, + 'authBindingId', + context.authBindingId, + {}, + ), + context.authContextRef !== undefined, + 'authContextRef', + context.authContextRef, {}, ), - context.authContextRef !== undefined, - 'authContextRef', - context.authContextRef, + context.impersonatedByPrincipalId !== undefined, + 'impersonatedByPrincipalId', + context.impersonatedByPrincipalId, {}, ), - context.impersonatedByPrincipalId !== undefined, - 'impersonatedByPrincipalId', - context.impersonatedByPrincipalId, + context.legalEntityId !== undefined, + 'legalEntityId', + context.legalEntityId, {}, ), - context.legalEntityId !== undefined, - 'legalEntityId', - context.legalEntityId, - {}, + ); + }); + const resourceRef = yield* Schema.decodeUnknownEffect(ResourceRefSchema)({ + moduleId: GENERATED_OWNER.moduleId, + resourceId: collidingResourceId, + resourceType: GENERATED_OWNER.resourceType, + }); + const touchedObjects: readonly [string, string][] = [ + ['tenant', tenantA], + ['tenant', tenantB], + [ + 'legal_entity', + requiredValue(toLegalEntityAccessObjectId(tenantA, entityA1), 'Tenant A legal entity'), + ], + [ + 'legal_entity', + requiredValue(toLegalEntityAccessObjectId(tenantB, entityB1), 'Tenant B legal entity'), + ], + [ + 'module_access', + requiredValue( + toModuleAccessObjectId(tenantA, entityA1, GENERATED_OWNER.moduleId), + 'Tenant A module access', + ), + ], + [ + 'module_access', + requiredValue( + toModuleAccessObjectId(tenantB, entityB1, GENERATED_OWNER.moduleId), + 'Tenant B module access', + ), + ], + [ + 'resource', + requiredValue( + toResourceAccessObjectId(tenantA, entityA1, resourceRef), + 'Tenant A resource', + ), + ], + [ + 'resource', + requiredValue( + toResourceAccessObjectId(tenantB, entityB1, resourceRef), + 'Tenant B resource', + ), + ], + ['action', toSpiceDbActionObjectId(GENERATED_OWNER.actionKey)], + ]; + yield* Effect.acquireRelease( + Effect.void, + Effect.fnUntraced(function* integrationEffect12() { + yield* disposeOwnerHandlers(handlers); + yield* Effect.forEach( + touchedObjects.toReversed(), + ([resourceType, resourceId]) => + Effect.tryPromise(() => + spiceAdmin.promises.deleteRelationships( + v1.DeleteRelationshipsRequest.create({ + relationshipFilter: v1.RelationshipFilter.create({ + optionalResourceId: resourceId, + resourceType, + }), + }), + ), + ).pipe(Effect.catchCause(() => Effect.void)), + { concurrency: 1, discard: true }, + ); + permissionClient.close(); + spiceAdmin.close(); + const cleanupQueries = [ + Effect.fnUntraced(function* runIntegration15() { + return yield* Effect.tryPromise(() => + admin.query('delete from core.outbox_messages where tenant_id in ($1, $2)', [ + tenantA, + tenantB, + ]), + ); + }), + Effect.fnUntraced(function* runIntegration16() { + return yield* Effect.tryPromise(() => + admin.query('delete from core.domain_events where tenant_id in ($1, $2)', [ + tenantA, + tenantB, + ]), + ); + }), + Effect.fnUntraced(function* runIntegration17() { + return yield* Effect.tryPromise(() => + admin.query('delete from core.data_access_events where tenant_id in ($1, $2)', [ + tenantA, + tenantB, + ]), + ); + }), + Effect.fnUntraced(function* runIntegration18() { + return yield* Effect.tryPromise(() => + admin.query('delete from core.audit_events where tenant_id in ($1, $2)', [ + tenantA, + tenantB, + ]), + ); + }), + Effect.fnUntraced(function* runIntegration19() { + return yield* Effect.tryPromise(() => + admin.query('delete from core.action_invocations where tenant_id in ($1, $2)', [ + tenantA, + tenantB, + ]), + ); + }), + Effect.fnUntraced(function* runIntegration20() { + return yield* Effect.tryPromise(() => + admin.query('delete from core.tenant_module_states where tenant_id in ($1, $2)', [ + tenantA, + tenantB, + ]), + ); + }), + Effect.fnUntraced(function* runIntegration21() { + return yield* Effect.tryPromise(() => + admin.query('delete from core.principal_auth_bindings where tenant_id in ($1, $2)', [ + tenantA, + tenantB, + ]), + ); + }), + Effect.fnUntraced(function* runIntegration22() { + return yield* Effect.tryPromise(() => + admin.query('delete from core.principals where tenant_id in ($1, $2)', [ + tenantA, + tenantB, + ]), + ); + }), + Effect.fnUntraced(function* runIntegration23() { + return yield* Effect.tryPromise(() => + admin.query('delete from core.legal_entities where tenant_id in ($1, $2)', [ + tenantA, + tenantB, + ]), + ); + }), + Effect.fnUntraced(function* runIntegration24() { + return yield* Effect.tryPromise(() => + admin.query('delete from core.tenants where tenant_id in ($1, $2)', [ + tenantA, + tenantB, + ]), + ); + }), + Effect.fnUntraced(function* runIntegration25() { + return yield* Effect.tryPromise(() => + admin.query(`drop schema if exists ${schemaName} cascade`), + ); + }), + ]; + yield* Effect.forEach(cleanupQueries, ignoreOperationFailure, { + concurrency: 1, + discard: true, + }); + }, Effect.orDie), + ); + yield* createOwnerSchema(admin, schemaName); + yield* Effect.tryPromise(() => + admin.query( + `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $3, 'Generated tenant A', 'active', 'en'), ($2, $4, 'Generated tenant B', 'active', 'en')`, + [tenantA, tenantB, `generated-a-${tenantA}`, `generated-b-${tenantB}`], ), ); - const resourceRef = Schema.decodeUnknownSync(ResourceRefSchema)({ - moduleId: GENERATED_OWNER.moduleId, - resourceId: collidingResourceId, - resourceType: GENERATED_OWNER.resourceType, - }); - const touchedObjects: readonly [string, string][] = [ - ['tenant', tenantA], - ['tenant', tenantB], - [ - 'legal_entity', - requiredValue(toLegalEntityAccessObjectId(tenantA, entityA1), 'Tenant A legal entity'), - ], - [ - 'legal_entity', - requiredValue(toLegalEntityAccessObjectId(tenantB, entityB1), 'Tenant B legal entity'), - ], - [ - 'module_access', - requiredValue( - toModuleAccessObjectId(tenantA, entityA1, GENERATED_OWNER.moduleId), - 'Tenant A module access', - ), - ], - [ - 'module_access', - requiredValue( - toModuleAccessObjectId(tenantB, entityB1, GENERATED_OWNER.moduleId), - 'Tenant B module access', + yield* Effect.tryPromise(() => + admin.query( + `insert into core.legal_entities (legal_entity_id, tenant_id, legal_name, registration_country, registration_number, status) values ($1, $5, 'A1', 'CZ', $7, 'active'), ($2, $5, 'A2', 'CZ', $8, 'active'), ($3, $6, 'B1', 'CZ', $9, 'active'), ($4, $6, 'B2', 'CZ', $10, 'active')`, + [ + entityA1, + entityA2, + entityB1, + entityB2, + tenantA, + tenantB, + `A1-${entityA1}`, + `A2-${entityA2}`, + `B1-${entityB1}`, + `B2-${entityB2}`, + ], ), - ], - [ - 'resource', - requiredValue(toResourceAccessObjectId(tenantA, entityA1, resourceRef), 'Tenant A resource'), - ], - [ - 'resource', - requiredValue(toResourceAccessObjectId(tenantB, entityB1, resourceRef), 'Tenant B resource'), - ], - ['action', toSpiceDbActionObjectId(GENERATED_OWNER.actionKey)], - ]; - - try { - await createOwnerSchema(admin, schemaName); - await admin.query( - `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $3, 'Generated tenant A', 'active', 'en'), ($2, $4, 'Generated tenant B', 'active', 'en')`, - [tenantA, tenantB, `generated-a-${tenantA}`, `generated-b-${tenantB}`], - ); - await admin.query( - `insert into core.legal_entities (legal_entity_id, tenant_id, legal_name, registration_country, registration_number, status) values ($1, $5, 'A1', 'CZ', $7, 'active'), ($2, $5, 'A2', 'CZ', $8, 'active'), ($3, $6, 'B1', 'CZ', $9, 'active'), ($4, $6, 'B2', 'CZ', $10, 'active')`, - [ - entityA1, - entityA2, - entityB1, - entityB2, - tenantA, - tenantB, - `A1-${entityA1}`, - `A2-${entityA2}`, - `B1-${entityB1}`, - `B2-${entityB2}`, - ], ); - await admin.query( - `insert into core.principals (principal_id, tenant_id, kind, display_name, status) values ($1, $3, 'human', 'Generated principal A', 'active'), ($2, $4, 'human', 'Generated principal B', 'active')`, - [principalA, principalB, tenantA, tenantB], + yield* Effect.tryPromise(() => + admin.query( + `insert into core.principals (principal_id, tenant_id, kind, display_name, status) values ($1, $3, 'human', 'Generated principal A', 'active'), ($2, $4, 'human', 'Generated principal B', 'active')`, + [principalA, principalB, tenantA, tenantB], + ), ); - await admin.query( - `insert into core.principal_auth_bindings (principal_auth_binding_id, tenant_id, principal_id, provider, subject_type, provider_subject_id, status) values ($1, $3, $5, 'better_auth', 'user', $7, 'active'), ($2, $4, $6, 'better_auth', 'user', $8, 'active')`, - [ - bindingA, - bindingB, - tenantA, - tenantB, - principalA, - principalB, - `user-${principalA}`, - `user-${principalB}`, - ], + yield* Effect.tryPromise(() => + admin.query( + `insert into core.principal_auth_bindings (principal_auth_binding_id, tenant_id, principal_id, provider, subject_type, provider_subject_id, status) values ($1, $3, $5, 'better_auth', 'user', $7, 'active'), ($2, $4, $6, 'better_auth', 'user', $8, 'active')`, + [ + bindingA, + bindingB, + tenantA, + tenantB, + principalA, + principalB, + `user-${principalA}`, + `user-${principalB}`, + ], + ), ); - await admin.query( - `insert into core.tenant_module_states (tenant_id, module_key, state) values ($1, $3, 'active'), ($2, $3, 'active')`, - [tenantA, tenantB, GENERATED_OWNER.moduleId], + yield* Effect.tryPromise(() => + admin.query( + `insert into core.tenant_module_states (tenant_id, module_key, state) values ($1, $3, 'active'), ($2, $3, 'active')`, + [tenantA, tenantB, GENERATED_OWNER.moduleId], + ), ); - await admin.query( - `insert into ${schemaName}.tenant_records (tenant_id, resource_id, title) values ($1, $3, 'Tenant A list'), ($2, $3, 'Tenant B list')`, - [tenantA, tenantB, collidingResourceId], + yield* Effect.tryPromise(() => + admin.query( + `insert into ${schemaName}.tenant_records (tenant_id, resource_id, title) values ($1, $3, 'Tenant A list'), ($2, $3, 'Tenant B list')`, + [tenantA, tenantB, collidingResourceId], + ), ); - await admin.query( - `insert into ${schemaName}.entity_records (tenant_id, legal_entity_id, resource_id, title) values ($1, $2, $7, 'A1 searchable'), ($1, $3, $7, 'A2 searchable'), ($4, $5, $7, 'B1 searchable'), ($4, $6, $7, 'B2 searchable')`, - [tenantA, entityA1, entityA2, tenantB, entityB1, entityB2, collidingResourceId], + yield* Effect.tryPromise(() => + admin.query( + `insert into ${schemaName}.entity_records (tenant_id, legal_entity_id, resource_id, title) values ($1, $2, $7, 'A1 searchable'), ($1, $3, $7, 'A2 searchable'), ($4, $5, $7, 'B1 searchable'), ($4, $6, $7, 'B2 searchable')`, + [tenantA, entityA1, entityA2, tenantB, entityB1, entityB2, collidingResourceId], + ), ); - const legalA = requiredValue( toLegalEntityAccessObjectId(tenantA, entityA1), 'Tenant A legal entity', @@ -834,185 +1009,174 @@ test('generated owner enforces tenant and legal-entity isolation through Shell, relationship('action', actionId, 'restriction', 'action', actionId), relationship('action', actionId, 'executor', 'principal', principalA), ]; - await spiceAdmin.promises.writeRelationships( - v1.WriteRelationshipsRequest.create({ - updates: relationships.map((item) => - v1.RelationshipUpdate.create({ - operation: v1.RelationshipUpdate_Operation.TOUCH, - relationship: item, - }), - ), - }), + yield* Effect.tryPromise(() => + spiceAdmin.promises.writeRelationships( + v1.WriteRelationshipsRequest.create({ + updates: relationships.map((item) => + v1.RelationshipUpdate.create({ + operation: v1.RelationshipUpdate_Operation.TOUCH, + relationship: item, + }), + ), + }), + ), ); - - const ownerSearchProbe = await requestOwner( + const ownerSearchProbe = yield* requestOwner( generated.search, `/${GENERATED_OWNER.moduleId}/search/records`, { query: 'searchable' }, - await issueAuthorization(principalA1), + yield* issueAuthorization(principalA1), randomUUID(), ); - const ownerSearchProbeBody = await decodeResponse(ownerSearchProbe, OwnerSearchSchema); - assert.equal(ownerSearchProbe.status, 200, JSON.stringify(ownerSearchProbeBody)); - assert.deepEqual( - ownerSearchProbeBody.map(({ title }) => title), - ['A1 searchable'], - ); - + const ownerSearchProbeBody = yield* decodeResponse(ownerSearchProbe, OwnerSearchSchema); + expect(ownerSearchProbe.status, JSON.stringify(ownerSearchProbeBody)).toBe(200); + expect(ownerSearchProbeBody.map(({ title }) => title)).toEqual(['A1 searchable']); const catalog = makeCatalog(contract); const gateway = { resource: { - detail: ({ - authorization, - correlationId, - ref, - }: Parameters[0]) => - Effect.tryPromise({ - catch: () => new ShellProviderUnavailableError(), - try: async () => { - const response = await requestOwner( - generated.detail, - '/reads/resource-detail', - { resourceId: ref.resourceId }, - authorization, - correlationId, - ); - if (!response.ok) { - throw new Error('Owner detail request failed'); - } - return await decodeResponse(response, OwnerDetailSchema); - }, - }), - timeline: ({ - authorization, - correlationId, - ref, - }: Parameters[0]) => - Effect.tryPromise({ - catch: () => new ShellProviderUnavailableError(), - try: async () => { - const response = await requestOwner( - generated.list, - '/reads/resource-list', - { resourceId: ref.resourceId }, - authorization, - correlationId, - ); - if (!response.ok) { - throw new Error('Owner list request failed'); - } - const timeline = await decodeResponse(response, OwnerTimelineSchema); - return Schema.encodeSync(OwnerTimelineSchema)(timeline); - }, - }), + detail: Effect.fnUntraced( + function* integrationEffect26({ + authorization, + correlationId, + ref, + }: Parameters[0]) { + const response = yield* requestOwner( + generated.detail, + '/reads/resource-detail', + { resourceId: ref.resourceId }, + authorization, + correlationId, + ); + if (!response.ok) { + throw new Error('Owner detail request failed'); + } + return yield* decodeResponse(response, OwnerDetailSchema); + }, + Effect.catchCause(() => Effect.fail(new ShellProviderUnavailableError())), + ), + timeline: Effect.fnUntraced( + function* integrationEffect27({ + authorization, + correlationId, + ref, + }: Parameters[0]) { + const response = yield* requestOwner( + generated.list, + '/reads/resource-list', + { resourceId: ref.resourceId }, + authorization, + correlationId, + ); + if (!response.ok) { + throw new Error('Owner list request failed'); + } + const timeline = yield* decodeResponse(response, OwnerTimelineSchema); + return Schema.encodeSync(OwnerTimelineSchema)(timeline); + }, + Effect.catchCause(() => Effect.fail(new ShellProviderUnavailableError())), + ), }, search: { - search: ({ - authorization, - correlationId, - query, - }: Parameters[0]) => - Effect.tryPromise({ - catch: () => new ShellProviderUnavailableError(), - try: async () => { - const response = await requestOwner( - generated.search, - `/${GENERATED_OWNER.moduleId}/search/records`, - { query }, - authorization, - correlationId, - ); - if (!response.ok) { - throw new Error('Owner search request failed'); - } - return await decodeResponse(response, OwnerSearchSchema); - }, - }), + search: Effect.fnUntraced( + function* integrationEffect28({ + authorization, + correlationId, + query, + }: Parameters[0]) { + const response = yield* requestOwner( + generated.search, + `/${GENERATED_OWNER.moduleId}/search/records`, + { query }, + authorization, + correlationId, + ); + if (!response.ok) { + throw new Error('Owner search request failed'); + } + return yield* decodeResponse(response, OwnerSearchSchema); + }, + Effect.catchCause(() => Effect.fail(new ShellProviderUnavailableError())), + ), }, } satisfies ShellResourceGateways; - assert.deepEqual( - await effectRuntime.runPromise( - moduleStates.getTenantModuleStates(tenantA, [GENERATED_OWNER.moduleId]), - ), - [{ moduleKey: GENERATED_OWNER.moduleId, state: 'active' }], - ); - assert.deepEqual( - await effectRuntime.runPromise( - contextAccess.modules({ - legalEntityId: entityA1, - moduleIds: [GENERATED_OWNER.moduleId], - principalId: principalA, - tenantId: tenantA, - }), - ), - [{ decision: 'allowed', key: GENERATED_OWNER.moduleId }], - ); - assert.deepEqual( - await effectRuntime.runPromise( - contextAccess.resources({ - legalEntityId: entityA1, - principalId: principalA, - resources: [resourceRef], - tenantId: tenantA, - }), - ), - [ - { - decision: 'allowed', - key: `${GENERATED_OWNER.moduleId}:${GENERATED_OWNER.resourceType}:${collidingResourceId}`, - }, - ], - ); - assert.deepEqual( - await effectRuntime.runPromise( - gateway.search.search({ - appId: GENERATED_OWNER.appId, - authorization: await issueAuthorization(principalA1), - correlationId: randomUUID(), - query: 'searchable', - searchKey: `${GENERATED_OWNER.moduleId}.records`, - }), - ), - [ - { - ref: resourceRef, - title: 'A1 searchable', - }, - ], - ); + expect(yield* moduleStates.getTenantModuleStates(tenantA, [GENERATED_OWNER.moduleId])).toEqual([ + { moduleKey: GENERATED_OWNER.moduleId, state: 'active' }, + ]); + expect( + yield* contextAccess.modules({ + legalEntityId: entityA1, + moduleIds: [GENERATED_OWNER.moduleId], + principalId: principalA, + tenantId: tenantA, + }), + ).toEqual([{ decision: 'allowed', key: GENERATED_OWNER.moduleId }]); + expect( + yield* contextAccess.resources({ + legalEntityId: entityA1, + principalId: principalA, + resources: [resourceRef], + tenantId: tenantA, + }), + ).toEqual([ + { + decision: 'allowed', + key: `${GENERATED_OWNER.moduleId}:${GENERATED_OWNER.resourceType}:${collidingResourceId}`, + }, + ]); + expect( + yield* gateway.search.search({ + appId: GENERATED_OWNER.appId, + authorization: yield* issueAuthorization(principalA1), + correlationId: randomUUID(), + query: 'searchable', + searchKey: `${GENERATED_OWNER.moduleId}.records`, + }), + ).toEqual([ + { + ref: resourceRef, + title: 'A1 searchable', + }, + ]); const directShellSearch = makeShellSearch( { catalog: Effect.succeed(catalog), contextAccess, - issueAssertion: ({ context }) => - Effect.tryPromise({ - catch: () => new ShellProviderUnavailableError(), - try: async () => await issueProviderAuthorization(context), - }), + issueAssertion: Effect.fnUntraced( + function* integrationEffect29({ + context, + }: { + readonly context: TrustedPrincipalContext; + }) { + return yield* issueProviderAuthorization(context); + }, + Effect.catchCause(() => Effect.fail(new ShellProviderUnavailableError())), + ), moduleStates, }, gateway.search, ); - assert.deepEqual( - await effectRuntime.runPromise( - directShellSearch.search( - { ...principalA1, correlationId: randomUUID(), legalEntityId: entityA1 }, - 'searchable', - ), + expect( + yield* directShellSearch.search( + { ...principalA1, correlationId: randomUUID(), legalEntityId: entityA1 }, + 'searchable', ), - { - partial: false, - results: [{ kind: 'resource', ref: resourceRef, title: 'A1 searchable' }], - }, - ); + ).toEqual({ + partial: false, + results: [{ kind: 'resource', ref: resourceRef, title: 'A1 searchable' }], + }); const shellLayer = createShellGovernedReadsLayer( gateway, { - issueAssertion: ({ context }) => - Effect.tryPromise({ - catch: () => new ShellProviderUnavailableError(), - try: async () => await issueProviderAuthorization(context), - }), + issueAssertion: Effect.fnUntraced( + function* integrationEffect30({ + context, + }: { + readonly context: TrustedPrincipalContext; + }) { + return yield* issueProviderAuthorization(context); + }, + Effect.catchCause(() => Effect.fail(new ShellProviderUnavailableError())), + ), }, (transaction) => makeTenantModuleStateService({ executor: transaction }), ).pipe( @@ -1027,128 +1191,107 @@ test('generated owner enforces tenant and legal-entity isolation through Shell, ), ), ); - const shellReads = await effectRuntime.runPromise( - ShellGovernedReads.pipe(Effect.provide(shellLayer)), - ); - - const searchA = await effectRuntime.runPromise( - shellReads.search({ - correlationId: randomUUID(), - principal: principalA1, - query: 'searchable', - }), - ); - const detailA = await effectRuntime.runPromise( - shellReads.resourceDetail({ - correlationId: randomUUID(), - principal: principalA1, - ref: resourceRef, - }), - ); - const searchB = await effectRuntime.runPromise( - shellReads.search({ - correlationId: randomUUID(), - principal: principalB1, - query: 'searchable', - }), - ); - const detailB = await effectRuntime.runPromise( - shellReads.resourceDetail({ - correlationId: randomUUID(), - principal: principalB1, - ref: resourceRef, - }), - ); - assert.deepEqual( - searchA.results.map(({ title }) => title), - ['A1 searchable'], - ); - assert.equal(detailA.detail.title, 'A1 searchable'); - assert.deepEqual( - detailA.timeline.map(({ summary }) => summary), - ['Tenant A list'], - ); - assert.deepEqual( - searchB.results.map(({ title }) => title), - ['B1 searchable'], - ); - assert.equal(detailB.detail.title, 'B1 searchable'); - assert.deepEqual( - detailB.timeline.map(({ summary }) => summary), - ['Tenant B list'], - ); - assert.equal(assertionCount, 9, 'every provider attempt must receive a fresh assertion'); - + const shellReads = yield* ShellGovernedReads.pipe(Effect.provide(shellLayer)); + const searchA = yield* shellReads.search({ + correlationId: randomUUID(), + principal: principalA1, + query: 'searchable', + }); + const detailA = yield* shellReads.resourceDetail({ + correlationId: randomUUID(), + principal: principalA1, + ref: resourceRef, + }); + const searchB = yield* shellReads.search({ + correlationId: randomUUID(), + principal: principalB1, + query: 'searchable', + }); + const detailB = yield* shellReads.resourceDetail({ + correlationId: randomUUID(), + principal: principalB1, + ref: resourceRef, + }); + expect(searchA.results.map(({ title }) => title)).toEqual(['A1 searchable']); + expect(detailA.detail.title).toBe('A1 searchable'); + expect(detailA.timeline.map(({ summary }) => summary)).toEqual(['Tenant A list']); + expect(searchB.results.map(({ title }) => title)).toEqual(['B1 searchable']); + expect(detailB.detail.title).toBe('B1 searchable'); + expect(detailB.timeline.map(({ summary }) => summary)).toEqual(['Tenant B list']); + expect(assertionCount, 'every provider attempt must receive a fresh assertion').toBe(9); capturedLogs.length = 0; const beforeForgedShell = { ...generated.counts }; - await assert.rejects( - effectRuntime.runPromise( - shellReads.resourceDetail({ - correlationId: randomUUID(), - principal: principal(tenantA, entityA2, principalA, bindingA), - ref: resourceRef, - }), + expect( + isOperationContextDenied( + yield* Effect.flip( + shellReads.resourceDetail({ + correlationId: randomUUID(), + principal: principal(tenantA, entityA2, principalA, bindingA), + ref: resourceRef, + }), + ), ), - isOperationContextDenied, - ); - await assert.rejects( - effectRuntime.runPromise( - shellReads.resourceDetail({ - correlationId: randomUUID(), - principal: principal(tenantB, entityB1, principalA, bindingA), - ref: resourceRef, - }), + ).toBe(true); + expect( + isOperationContextDenied( + yield* Effect.flip( + shellReads.resourceDetail({ + correlationId: randomUUID(), + principal: principal(tenantB, entityB1, principalA, bindingA), + ref: resourceRef, + }), + ), ), - isOperationContextDenied, - ); - assert.deepEqual(generated.counts, beforeForgedShell); - assert.equal(assertionCount, 9); - - await Promise.all( + ).toBe(true); + expect(generated.counts).toEqual(beforeForgedShell); + expect(assertionCount).toBe(9); + yield* Effect.all( [ principal(tenantA, entityA2, principalA, bindingA), principal(tenantB, entityB1, principalA, bindingA), - ].map(async (forgedPrincipal) => { - const authorization = await issueAuthorization(forgedPrincipal); - const response = await requestOwner( - generated.detail, - '/reads/resource-detail', - { resourceId: collidingResourceId }, - authorization, - randomUUID(), - ); - assert.equal(response.status, 403); - const problem = JSON.stringify(await response.json()); - assert.doesNotMatch( - problem, - new RegExp([tenantA, tenantB, entityA2, entityB1].join('|'), 'u'), - ); - assert.doesNotMatch(problem, /postgres|spicedb|permission check|row-level/iu); - }), + ].map( + Effect.fnUntraced(function* runIntegration31(forgedPrincipal) { + const authorization = yield* issueAuthorization(forgedPrincipal); + const response = yield* requestOwner( + generated.detail, + '/reads/resource-detail', + { resourceId: collidingResourceId }, + authorization, + randomUUID(), + ); + expect(response.status).toBe(403); + const problem = JSON.stringify(yield* Effect.tryPromise(() => response.json())); + expect(problem).not.toMatch( + new RegExp([tenantA, tenantB, entityA2, entityB1].join('|'), 'u'), + ); + expect(problem).not.toMatch(/postgres|spicedb|permission check|row-level/iu); + }), + ), ); - assert.deepEqual(generated.counts, beforeForgedShell); - + expect(generated.counts).toEqual(beforeForgedShell); const deniedBefore = generated.counts.detail; - const deniedAuthorization = await issueAuthorization(principalA1); - const deniedResponse = await requestOwner( + const deniedAuthorization = yield* issueAuthorization(principalA1); + const deniedResponse = yield* requestOwner( generated.detail, '/reads/resource-detail', { resourceId: deniedResourceId }, deniedAuthorization, randomUUID(), ); - assert.equal(deniedResponse.status, 403); - assert.equal(generated.counts.detail, deniedBefore); - const deniedEvidence = await admin.query<{ - outcome: string; - outcome_code: string; - query_hash: null; - result_count: number; - }>( - `select outcome, outcome_code, query_hash, result_count from core.data_access_events where tenant_id = $1 and target_resource_id = $2`, - [tenantA, deniedResourceId], + expect(deniedResponse.status).toBe(403); + expect(generated.counts.detail).toBe(deniedBefore); + const deniedEvidence = yield* Effect.tryPromise(() => + admin.query<{ + outcome: string; + outcome_code: string; + query_hash: null; + result_count: number; + }>( + `select outcome, outcome_code, query_hash, result_count from core.data_access_events where tenant_id = $1 and target_resource_id = $2`, + [tenantA, deniedResourceId], + ), ); - assert.deepEqual(deniedEvidence.rows, [ + expect(deniedEvidence.rows).toEqual([ { outcome: 'denied', outcome_code: 'spicedb_permission_denied', @@ -1156,7 +1299,6 @@ test('generated owner enforces tenant and legal-entity isolation through Shell, result_count: 0, }, ]); - const unavailableContextAccess = makeContextAccessDouble('unavailable'); const unavailableResolver: OperationalScopeResolverService = makeOperationalScopeResolver( makeOperationalScopeRepository(runtimeDatabase), @@ -1168,7 +1310,7 @@ test('generated owner enforces tenant and legal-entity isolation through Shell, unavailableResolver, unavailableContextAccess, ); - const unavailableOwner = await loadGeneratedOwner( + const unavailableOwner = yield* loadGeneratedOwner( fixture.verticalRoot, unavailableRuntime, loggerLayer, @@ -1176,27 +1318,25 @@ test('generated owner enforces tenant and legal-entity isolation through Shell, ); handlers.push(unavailableOwner.detail, unavailableOwner.list, unavailableOwner.search); const unavailableBefore = generated.counts.detail; - const unavailableResponse = await requestOwner( + const unavailableResponse = yield* requestOwner( unavailableOwner.detail, '/reads/resource-detail', { resourceId: collidingResourceId }, - await issueAuthorization(principalA1), + yield* issueAuthorization(principalA1), randomUUID(), ); - assert.equal(unavailableResponse.status, 503); - assert.equal(generated.counts.detail, unavailableBefore); - assert.doesNotMatch( - JSON.stringify(await unavailableResponse.json()), + expect(unavailableResponse.status).toBe(503); + expect(generated.counts.detail).toBe(unavailableBefore); + expect(JSON.stringify(yield* Effect.tryPromise(() => unavailableResponse.json()))).not.toMatch( /postgres|spicedb|permission check|row-level/iu, ); - const evidenceFailureRuntime = makeReadRuntime( failingEvidenceDatabase(runtimeDatabase), moduleGateway, scopeResolver, contextAccess, ); - const evidenceFailureOwner = await loadGeneratedOwner( + const evidenceFailureOwner = yield* loadGeneratedOwner( fixture.verticalRoot, evidenceFailureRuntime, loggerLayer, @@ -1207,16 +1347,17 @@ test('generated owner enforces tenant and legal-entity isolation through Shell, evidenceFailureOwner.list, evidenceFailureOwner.search, ); - const evidenceFailureResponse = await requestOwner( + const evidenceFailureResponse = yield* requestOwner( evidenceFailureOwner.detail, '/reads/resource-detail', { resourceId: collidingResourceId }, - await issueAuthorization(principalA1), + yield* issueAuthorization(principalA1), randomUUID(), ); - assert.equal(evidenceFailureResponse.status, 503); - assert.doesNotMatch(JSON.stringify(await evidenceFailureResponse.json()), /A1 searchable/u); - + expect(evidenceFailureResponse.status).toBe(503); + expect( + JSON.stringify(yield* Effect.tryPromise(() => evidenceFailureResponse.json())), + ).not.toMatch(/A1 searchable/u); const actionRuntime = makeActionRuntime( runtimeDatabase, makeActionRepository(), @@ -1228,7 +1369,7 @@ test('generated owner enforces tenant and legal-entity isolation through Shell, throw new TypeError('Generated Action registration is missing its runtime handler'); } const actionRegistration = generated.action; - const invokeAction = async ( + const invokeAction = Effect.fnUntraced(function* runIntegration32( trustedPrincipal: TrustedPrincipalContext, payload: { readonly legalEntityId: string; @@ -1237,34 +1378,32 @@ test('generated owner enforces tenant and legal-entity isolation through Shell, readonly title: string; }, idempotencyKey: string, - ) => { - const authorization = await issueAuthorization(trustedPrincipal); - const verified = await effectRuntime.runPromise( - generated.verifyActionPrincipal(authorization, { + ) { + const authorization = yield* issueAuthorization(trustedPrincipal); + const verified = yield* generated + .verifyActionPrincipal(authorization, { environment: verifierEnvironment, redemption: testGatewayAssertionRedemption, - }), - ); - return await effectRuntime.runPromise( - actionRuntime - .runAction({ - payload, - principal: verified, - registration: actionRegistration, - transport: { - correlationId: randomUUID(), - idempotencyKey, - targetModuleKey: GENERATED_OWNER.moduleId, - targetResourceId: payload.resourceId, - targetResourceType: GENERATED_OWNER.resourceType, - }, - }) - .pipe(Effect.provide(loggerLayer)), - ); - }; + }) + .pipe(Effect.provide(testClockLayer)); + return yield* actionRuntime + .runAction({ + payload, + principal: verified, + registration: actionRegistration, + transport: { + correlationId: randomUUID(), + idempotencyKey, + targetModuleKey: GENERATED_OWNER.moduleId, + targetResourceId: payload.resourceId, + targetResourceType: GENERATED_OWNER.resourceType, + }, + }) + .pipe(Effect.provide(loggerLayer)); + }); const validWriteId = randomUUID(); - assert.deepEqual( - await invokeAction( + expect( + yield* invokeAction( principalA1, { legalEntityId: entityA1, @@ -1274,9 +1413,8 @@ test('generated owner enforces tenant and legal-entity isolation through Shell, }, randomUUID(), ), - { created: true }, - ); - await Promise.all( + ).toEqual({ created: true }); + yield* Effect.all( [ { legalEntityId: entityA2, @@ -1291,201 +1429,109 @@ test('generated owner enforces tenant and legal-entity isolation through Shell, title: 'forbidden tenant write', }, ].map( - async (payload) => - await assert.rejects( - invokeAction(principalA1, payload, randomUUID()), - isCreateRecordRejected, - ), + Effect.fnUntraced(function* runIntegration33(payload) { + expect( + isCreateRecordRejected( + yield* Effect.flip(invokeAction(principalA1, payload, randomUUID())), + ), + ).toBe(true); + }), ), ); const beforeForgedAction = generated.counts.action; - await assert.rejects( - invokeAction( - principal(tenantA, entityA2, principalA, bindingA), - { - legalEntityId: entityA2, - resourceId: randomUUID(), - tenantId: tenantA, - title: 'forged action scope', - }, - randomUUID(), + expect( + isOperationContextDenied( + yield* Effect.flip( + invokeAction( + principal(tenantA, entityA2, principalA, bindingA), + { + legalEntityId: entityA2, + resourceId: randomUUID(), + tenantId: tenantA, + title: 'forged action scope', + }, + randomUUID(), + ), + ), ), - isOperationContextDenied, - ); - assert.equal(generated.counts.action, beforeForgedAction); - - await assert.rejects( - invokeAction( - principalA1, - { - legalEntityId: entityA1, - resourceId: randomUUID(), - tenantId: tenantA, - title: 'trigger safe logging defect', - }, - randomUUID(), + ).toBe(true); + expect(generated.counts.action).toBe(beforeForgedAction); + expect( + isActionHandlerExecutionError( + yield* Effect.flip( + invokeAction( + principalA1, + { + legalEntityId: entityA1, + resourceId: randomUUID(), + tenantId: tenantA, + title: 'trigger safe logging defect', + }, + randomUUID(), + ), + ), + ), + ).toBe(true); + const ownerRows = yield* Effect.tryPromise(() => + admin.query<{ + legal_entity_id: string; + tenant_id: string; + title: string; + }>( + `select tenant_id, legal_entity_id, title from ${schemaName}.entity_records order by title`, ), - isActionHandlerExecutionError, - ); - - const ownerRows = await admin.query<{ - legal_entity_id: string; - tenant_id: string; - title: string; - }>(`select tenant_id, legal_entity_id, title from ${schemaName}.entity_records order by title`); - assert.equal( - ownerRows.rows.some(({ title }) => title === 'A1 action write'), - true, - ); - assert.equal( - ownerRows.rows.some(({ title }) => title.startsWith('forbidden')), - false, - ); - - const allowedEvidence = await admin.query<{ - evidence_policy_key: string; - outcome: string; - query_hash: null; - }>( - `select evidence_policy_key, outcome, query_hash from core.data_access_events where tenant_id in ($1, $2) and outcome = 'allowed' order by evidence_policy_key`, - [tenantA, tenantB], - ); - assert.ok(allowedEvidence.rows.length >= 10); - assert.equal( - allowedEvidence.rows.every(({ outcome }) => outcome === 'allowed'), - true, ); - assert.equal( - allowedEvidence.rows.every(({ query_hash }) => query_hash === null), - true, + expect(ownerRows.rows.some(({ title }) => title === 'A1 action write')).toBe(true); + expect(ownerRows.rows.some(({ title }) => title.startsWith('forbidden'))).toBe(false); + const allowedEvidence = yield* Effect.tryPromise(() => + admin.query<{ + evidence_policy_key: string; + outcome: string; + query_hash: null; + }>( + `select evidence_policy_key, outcome, query_hash from core.data_access_events where tenant_id in ($1, $2) and outcome = 'allowed' order by evidence_policy_key`, + [tenantA, tenantB], + ), ); - - const unscopedEntityRows = await runtimePool.query( - `select * from ${schemaName}.entity_records`, + expect(allowedEvidence.rows.length >= 10).toBe(true); + expect(allowedEvidence.rows.every(({ outcome }) => outcome === 'allowed')).toBe(true); + expect(allowedEvidence.rows.every(({ query_hash }) => query_hash === null)).toBe(true); + const unscopedEntityRows = yield* Effect.tryPromise(() => + runtimePool.query(`select * from ${schemaName}.entity_records`), ); - assert.equal( + expect( unscopedEntityRows.rowCount, - 0, 'a reused pooled connection must not retain transaction-local scope', + ).toBe(0); + const unscopedTenantRows = yield* Effect.tryPromise(() => + runtimePool.query(`select * from ${schemaName}.tenant_records`), ); - const unscopedTenantRows = await runtimePool.query( - `select * from ${schemaName}.tenant_records`, + expect(unscopedTenantRows.rowCount).toBe(0); + expect(capturedLogs.length > 0, 'the generated-owner path must capture runtime logs').toBe( + true, ); - assert.equal(unscopedTenantRows.rowCount, 0); - - assert.ok(capturedLogs.length > 0, 'the generated-owner path must capture runtime logs'); const capturedLogText = capturedLogs.join('\n'); - assert.match(capturedLogText, /Unexpected Action execution defect/u); - assert.doesNotMatch( - capturedLogText, + expect(capturedLogText).toMatch(/Unexpected Action execution defect/u); + expect(capturedLogText).not.toMatch( new RegExp( [tenantB, entityA2, entityB1, entityB2, principalB, bindingB, deniedResourceId].join('|'), 'u', ), ); - assert.doesNotMatch( - capturedLogText, + expect(capturedLogText).not.toMatch( /postgres|spicedb|row-level|database operation scope|permission check/iu, ); - - const generatedActionSource = await readFile( - `${fixture.verticalRoot}/src/actions/create-record.action.ts`, - 'utf-8', - ); - const generatedServerSource = await readFile( - `${fixture.verticalRoot}/api/resource-detail-read-server.ts`, - 'utf-8', - ); - assert.match(generatedActionSource, /@generated by OntOS Codesmith Action/u); - assert.match(generatedActionSource, /legalEntityScope: 'required'/u); - assert.match(generatedServerSource, /authenticateOperationPrincipal/u); - assert.match(generatedServerSource, /makeGovernedReadHttpHandler\(\{/u); - assert.match(generatedServerSource, /registration: resourceDetailRead/u); - assert.doesNotMatch(generatedServerSource, /yield\* ReadRuntime|\.runRead\(/u); - } finally { - await Promise.allSettled(handlers.map(async ({ dispose }) => await dispose())); - await effectRuntime.runPromise( - Effect.forEach( - touchedObjects.toReversed(), - ([resourceType, resourceId]) => - ignorePromiseFailure( - async () => - await spiceAdmin.promises.deleteRelationships( - v1.DeleteRelationshipsRequest.create({ - relationshipFilter: v1.RelationshipFilter.create({ - optionalResourceId: resourceId, - resourceType, - }), - }), - ), - ), - { concurrency: 1, discard: true }, - ), + const generatedActionSource = yield* Effect.tryPromise(() => + readFile(`${fixture.verticalRoot}/src/actions/create-record.action.ts`, 'utf-8'), ); - permissionClient.close(); - spiceAdmin.close(); - const cleanupQueries = [ - async () => - await admin.query('delete from core.outbox_messages where tenant_id in ($1, $2)', [ - tenantA, - tenantB, - ]), - async () => - await admin.query('delete from core.domain_events where tenant_id in ($1, $2)', [ - tenantA, - tenantB, - ]), - async () => - await admin.query('delete from core.data_access_events where tenant_id in ($1, $2)', [ - tenantA, - tenantB, - ]), - async () => - await admin.query('delete from core.audit_events where tenant_id in ($1, $2)', [ - tenantA, - tenantB, - ]), - async () => - await admin.query('delete from core.action_invocations where tenant_id in ($1, $2)', [ - tenantA, - tenantB, - ]), - async () => - await admin.query('delete from core.tenant_module_states where tenant_id in ($1, $2)', [ - tenantA, - tenantB, - ]), - async () => - await admin.query('delete from core.principal_auth_bindings where tenant_id in ($1, $2)', [ - tenantA, - tenantB, - ]), - async () => - await admin.query('delete from core.principals where tenant_id in ($1, $2)', [ - tenantA, - tenantB, - ]), - async () => - await admin.query('delete from core.legal_entities where tenant_id in ($1, $2)', [ - tenantA, - tenantB, - ]), - async () => - await admin.query('delete from core.tenants where tenant_id in ($1, $2)', [ - tenantA, - tenantB, - ]), - async () => await admin.query(`drop schema if exists ${schemaName} cascade`), - ]; - await effectRuntime.runPromise( - Effect.forEach(cleanupQueries, ignorePromiseFailure, { concurrency: 1, discard: true }), + const generatedServerSource = yield* Effect.tryPromise(() => + readFile(`${fixture.verticalRoot}/api/resource-detail-read-server.ts`, 'utf-8'), ); - await runtimePool.end(); - await admin.end(); - await fixture.dispose(); - await effectRuntime.dispose(); - } -}); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), + expect(generatedActionSource).toMatch(/@generated by OntOS Codesmith Action/u); + expect(generatedActionSource).toMatch(/legalEntityScope: 'required'/u); + expect(generatedServerSource).toMatch(/authenticateOperationPrincipal/u); + expect(generatedServerSource).toMatch(/makeGovernedReadHttpHandler\(\{/u); + expect(generatedServerSource).toMatch(/registration: resourceDetailRead/u); + expect(generatedServerSource).not.toMatch(/yield\* ReadRuntime|\.runRead\(/u); + }), ); diff --git a/app/apps/shell-super-app/tests/integration/identity-modes-runtime.test.ts b/app/apps/shell-super-app/tests/integration/identity-modes-runtime.test.ts index 6091fc84b..b3622f266 100644 --- a/app/apps/shell-super-app/tests/integration/identity-modes-runtime.test.ts +++ b/app/apps/shell-super-app/tests/integration/identity-modes-runtime.test.ts @@ -1,18 +1,10 @@ -import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.ts'; import { purgeFixtureRows } from '../../../../packages/core-runtime/tests/support/fixture-cleanup.ts'; -import { Scope as NativeScope, Exit as NativeExit, Context, Effect, Predicate } from 'effect'; -import { - runEffectTestSync as runNativeSync, - makeEffectTestCallback as nativeTestCallback, - runEffectTestPromise, -} from '@app/core-runtime/testing/effect-runtime'; - -import assert from 'node:assert/strict'; +import { expect, it } from 'effect-rstest'; +import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.ts'; +import { Context, Effect, Predicate } from 'effect'; import { randomUUID } from 'node:crypto'; -import test, { after as afterNativeDatabase } from 'node:test'; import { and, eq, inArray } from 'drizzle-orm'; import { AuthDatabase, makeAuthDatabase } from '../../api/auth/db/client.ts'; - import { exportJWK, generateKeyPair, jwtVerify } from 'jose'; import { Pool } from 'pg'; import { @@ -66,8 +58,6 @@ import { import { AuthenticationService, makeAuthenticationService } from '../../api/auth/service.ts'; import { makeIdentityLifecycleService } from '../../api/auth/identity-lifecycle.ts'; -const nativeDatabaseScope = runNativeSync(NativeScope.make()); - const cookieHeader = (setCookieHeaders: readonly string[]): string => { const cookies = new Map(); for (const header of setCookieHeaders) { @@ -79,102 +69,110 @@ const cookieHeader = (setCookieHeaders: readonly string[]): string => { } return [...cookies.values()].join('; '); }; - -void test('verifies provider keys and completes live support impersonation with durable stopped evidence', async (context) => { - const baseConfiguration = await runEffectTestPromise(loadAuthConfig()); - const corePool = new Pool({ connectionString: baseConfiguration.connectionString }); - const authPersistence = await runEffectTestPromise( - makeAuthDatabase(baseConfiguration).pipe(NativeScope.provide(nativeDatabaseScope)), - ); - const authDatabase = authPersistence.executor; - const coreDatabase = await runEffectTestPromise( - makeTestDatabaseFromPool(corePool, coreRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ), - ); - const principalManagementRepository = principalManagementRepositoryFromTransaction(coreDatabase); - const providePrincipalManagementRepository = ( - effect: Effect.Effect, - ) => - effect.pipe( - Effect.provideService(PrincipalManagementRepository, principalManagementRepository), - ); - const tenantId = randomUUID(); - const originalPrincipalId = randomUUID(); - const targetPrincipalId = randomUUID(); - const secondAdministratorPrincipalId = randomUUID(); - const originalAuthBindingId = randomUUID(); - const targetAuthBindingId = randomUUID(); - const secondAdministratorAuthBindingId = randomUUID(); - const originalEmail = `support-original-${randomUUID()}@example.test`; - const targetEmail = `support-target-${randomUUID()}@example.test`; - const secondAdministratorEmail = `identity-admin-${randomUUID()}@example.test`; - const password = 'correct-horse-battery-staple'; - const resolver = makePrincipalResolver({ executor: coreDatabase }); - let supportPermissionAllowed = true; - const allowedContextAccess = { - legalEntities: () => Effect.succeed([]), - modules: () => Effect.succeed([]), - resources: () => Effect.succeed([]), - tenants: ({ - permission, - tenantIds, - }: { - readonly permission: - | 'access' - | 'impersonate' - | 'manage_identity' - | 'manage_party_identity' - | 'manage_party_relationships' - | 'merge_party_identity' - | 'read_party_identity' - | 'review_party_identity'; - readonly tenantIds: readonly string[]; - }) => - Effect.succeed( - tenantIds.map((key) => ({ - decision: - permission === 'impersonate' && !supportPermissionAllowed - ? ('denied' as const) - : ('allowed' as const), - key, - })), - ), - }; - const provideContextAccess = ( - effect: Effect.Effect, - ) => effect.pipe(Effect.provideService(ContextAccess, allowedContextAccess)); - const operationalScope = makeOperationalScopeResolver( - makeOperationalScopeRepository({ executor: coreDatabase }), - allowedContextAccess, - ); - const actionRuntime = makeActionRuntime( - { executor: coreDatabase }, - makeActionRepository(), - { checkActionPermission: () => Effect.succeed('allowed' as const) }, - operationalScope, - { ...openActionRuntimeOptions, contextAccess: allowedContextAccess }, - ); - const fixtureAuthentication = makeAuthenticationService( - baseConfiguration, - authPersistence.adapter, - resolver, - { allowFixtureSignUp: true, runResolverEffect: runEffectTestPromise }, - ); - let originalUserId = ''; - let targetUserId = ''; - let secondAdministratorUserId = ''; - const cleanup = async () => { - if ( - originalUserId.length > 0 || - targetUserId.length > 0 || - secondAdministratorUserId.length > 0 - ) { - const ids = [originalUserId, targetUserId, secondAdministratorUserId].filter( - (id) => id.length > 0, +it.live.each([ + { + name: 'finds stale pending API keys with current and legacy metadata orders', + pendingCleanupOnly: true, + }, + { + name: 'verifies provider keys and completes live support impersonation with durable stopped evidence', + pendingCleanupOnly: false, + }, +])( + '$name', + Effect.fnUntraced(function* runIntegration1({ pendingCleanupOnly }) { + const baseConfiguration = yield* loadAuthConfig(); + const corePool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: baseConfiguration.connectionString })), + (pool) => Effect.tryPromise(() => pool.end()).pipe(Effect.orDie), + ); + const authPersistence = yield* makeAuthDatabase(baseConfiguration); + const authDatabase = authPersistence.executor; + const coreDatabase = yield* makeTestDatabaseFromPool(corePool, coreRelations); + const principalManagementRepository = + principalManagementRepositoryFromTransaction(coreDatabase); + const providePrincipalManagementRepository = ( + effect: Effect.Effect, + ) => + effect.pipe( + Effect.provideService(PrincipalManagementRepository, principalManagementRepository), ); - await runEffectTestPromise( - purgeFixtureRows([ + const tenantId = randomUUID(); + const originalPrincipalId = randomUUID(); + const targetPrincipalId = randomUUID(); + const secondAdministratorPrincipalId = randomUUID(); + const originalAuthBindingId = randomUUID(); + const targetAuthBindingId = randomUUID(); + const secondAdministratorAuthBindingId = randomUUID(); + const originalEmail = `support-original-${randomUUID()}@example.test`; + const targetEmail = `support-target-${randomUUID()}@example.test`; + const secondAdministratorEmail = `identity-admin-${randomUUID()}@example.test`; + const password = 'correct-horse-battery-staple'; + const resolver = makePrincipalResolver({ executor: coreDatabase }); + let supportPermissionAllowed = true; + const allowedContextAccess = { + legalEntities: () => Effect.succeed([]), + modules: () => Effect.succeed([]), + resources: () => Effect.succeed([]), + tenants: ({ + permission, + tenantIds, + }: { + readonly permission: + | 'access' + | 'impersonate' + | 'manage_identity' + | 'manage_party_identity' + | 'manage_party_relationships' + | 'merge_party_identity' + | 'read_party_identity' + | 'review_party_identity'; + readonly tenantIds: readonly string[]; + }) => + Effect.succeed( + tenantIds.map((key) => ({ + decision: + permission === 'impersonate' && !supportPermissionAllowed + ? ('denied' as const) + : ('allowed' as const), + key, + })), + ), + }; + const provideContextAccess = ( + effect: Effect.Effect, + ) => effect.pipe(Effect.provideService(ContextAccess, allowedContextAccess)); + const operationalScope = makeOperationalScopeResolver( + makeOperationalScopeRepository({ executor: coreDatabase }), + allowedContextAccess, + ); + const actionRuntime = makeActionRuntime( + { executor: coreDatabase }, + makeActionRepository(), + { checkActionPermission: () => Effect.succeed('allowed' as const) }, + operationalScope, + { ...openActionRuntimeOptions, contextAccess: allowedContextAccess }, + ); + const fixtureAuthentication = yield* makeAuthenticationService({ + allowFixtureSignUp: true, + }).pipe( + Effect.provideService(AuthConfig, baseConfiguration), + Effect.provideService(AuthDatabase, authPersistence), + Effect.provideService(PrincipalResolver, resolver), + ); + let originalUserId = ''; + let targetUserId = ''; + let secondAdministratorUserId = ''; + const cleanup = Effect.fnUntraced(function* runIntegration2() { + if ( + originalUserId.length > 0 || + targetUserId.length > 0 || + secondAdministratorUserId.length > 0 + ) { + const ids = [originalUserId, targetUserId, secondAdministratorUserId].filter( + (id) => id.length > 0, + ); + yield* purgeFixtureRows([ authDatabase .delete(supportImpersonationRecovery) .where(eq(supportImpersonationRecovery.tenantId, tenantId)), @@ -182,11 +180,9 @@ void test('verifies provider keys and completes live support impersonation with authDatabase.delete(session).where(inArray(session.userId, ids)), authDatabase.delete(account).where(inArray(account.userId, ids)), authDatabase.delete(user).where(inArray(user.id, ids)), - ]), - ); - } - await runEffectTestPromise( - purgeFixtureRows([ + ]); + } + yield* purgeFixtureRows([ coreDatabase.delete(dataAccessEvents).where(eq(dataAccessEvents.tenantId, tenantId)), coreDatabase.delete(auditEvents).where(eq(auditEvents.tenantId, tenantId)), coreDatabase.delete(actionInvocations).where(eq(actionInvocations.tenantId, tenantId)), @@ -195,327 +191,289 @@ void test('verifies provider keys and completes live support impersonation with .where(eq(principalAuthBindings.tenantId, tenantId)), coreDatabase.delete(principals).where(eq(principals.tenantId, tenantId)), coreDatabase.delete(tenants).where(eq(tenants.tenantId, tenantId)), - ]), - ); - }; - - try { - originalUserId = await runEffectTestPromise( - fixtureAuthentication.createFixtureUser(originalEmail, 'Support original', password), - ); - targetUserId = await runEffectTestPromise( - fixtureAuthentication.createFixtureUser(targetEmail, 'Support target', password), - ); - secondAdministratorUserId = await runEffectTestPromise( - fixtureAuthentication.createFixtureUser( - secondAdministratorEmail, - 'Second identity administrator', - password, - ), - ); - await runEffectTestPromise( - coreDatabase.insert(tenants).values({ - defaultLocale: 'en', - name: 'Identity modes Auth integration', - slug: `identity-modes-auth-${tenantId}`, + ]); + }); + yield* Effect.acquireRelease( + Effect.void, + Effect.fnUntraced(function* integrationEffect3() { + yield* cleanup(); + }, Effect.orDie), + ); + originalUserId = yield* fixtureAuthentication.createFixtureUser( + originalEmail, + 'Support original', + password, + ); + targetUserId = yield* fixtureAuthentication.createFixtureUser( + targetEmail, + 'Support target', + password, + ); + secondAdministratorUserId = yield* fixtureAuthentication.createFixtureUser( + secondAdministratorEmail, + 'Second identity administrator', + password, + ); + yield* coreDatabase.insert(tenants).values({ + defaultLocale: 'en', + name: 'Identity modes Auth integration', + slug: `identity-modes-auth-${tenantId}`, + status: 'active', + tenantId, + }); + yield* coreDatabase.insert(principals).values([ + { + displayName: 'Support original', + kind: 'human', + principalId: originalPrincipalId, status: 'active', tenantId, - }), - ); - await runEffectTestPromise( - coreDatabase.insert(principals).values([ - { - displayName: 'Support original', - kind: 'human', - principalId: originalPrincipalId, - status: 'active', - tenantId, - }, - { - displayName: 'Support target', - kind: 'human', - principalId: targetPrincipalId, - status: 'active', - tenantId, - }, - { - displayName: 'Second identity administrator', - kind: 'human', - principalId: secondAdministratorPrincipalId, - status: 'active', - tenantId, - }, - ]), - ); - await runEffectTestPromise( - coreDatabase.insert(principalAuthBindings).values([ - { - principalAuthBindingId: originalAuthBindingId, - principalId: originalPrincipalId, - provider: 'better_auth', - providerSubjectId: originalUserId, - status: 'active', - subjectType: 'user', - tenantId, - }, - { - principalAuthBindingId: targetAuthBindingId, - principalId: targetPrincipalId, - provider: 'better_auth', - providerSubjectId: targetUserId, - status: 'active', - subjectType: 'user', - tenantId, - }, - { - principalAuthBindingId: secondAdministratorAuthBindingId, - principalId: secondAdministratorPrincipalId, - provider: 'better_auth', - providerSubjectId: secondAdministratorUserId, - status: 'active', - subjectType: 'user', - tenantId, - }, - ]), - ); + }, + { + displayName: 'Support target', + kind: 'human', + principalId: targetPrincipalId, + status: 'active', + tenantId, + }, + { + displayName: 'Second identity administrator', + kind: 'human', + principalId: secondAdministratorPrincipalId, + status: 'active', + tenantId, + }, + ]); + yield* coreDatabase.insert(principalAuthBindings).values([ + { + principalAuthBindingId: originalAuthBindingId, + principalId: originalPrincipalId, + provider: 'better_auth', + providerSubjectId: originalUserId, + status: 'active', + subjectType: 'user', + tenantId, + }, + { + principalAuthBindingId: targetAuthBindingId, + principalId: targetPrincipalId, + provider: 'better_auth', + providerSubjectId: targetUserId, + status: 'active', + subjectType: 'user', + tenantId, + }, + { + principalAuthBindingId: secondAdministratorAuthBindingId, + principalId: secondAdministratorPrincipalId, + provider: 'better_auth', + providerSubjectId: secondAdministratorUserId, + status: 'active', + subjectType: 'user', + tenantId, + }, + ]); const configuration = { ...baseConfiguration, supportUserIds: [originalUserId], }; - const authentication = makeAuthenticationService( - configuration, - authPersistence.adapter, - resolver, - { - runResolverEffect: runEffectTestPromise, - }, + const authentication = yield* makeAuthenticationService({}).pipe( + Effect.provideService(AuthConfig, configuration), + Effect.provideService(AuthDatabase, authPersistence), + Effect.provideService(PrincipalResolver, resolver), ); - const signedIn = await runEffectTestPromise( - authentication.signIn( - originalEmail, - password, - new Headers({ origin: configuration.baseUrl }), - ), + const signedIn = yield* authentication.signIn( + originalEmail, + password, + new Headers({ origin: configuration.baseUrl }), ); const originalHeaders = new Headers({ cookie: cookieHeader(signedIn.setCookieHeaders), origin: configuration.baseUrl, }); - - const keys = await runEffectTestPromise( - makeApiKeyService().pipe( - Effect.provideService(AuthConfig, configuration), - Effect.provideService(AuthDatabase, authPersistence), - ), + const keys = yield* makeApiKeyService().pipe( + Effect.provideService(AuthConfig, configuration), + Effect.provideService(AuthDatabase, authPersistence), ); - const resolvedOriginal = await runEffectTestPromise( - provideContextAccess(authentication.resolveTenantContext(originalHeaders)), + const resolvedOriginal = yield* provideContextAccess( + authentication.resolveTenantContext(originalHeaders), ); - assert.equal(resolvedOriginal.state, 'authenticated'); + expect(resolvedOriginal.state).toBe('authenticated'); if (resolvedOriginal.state !== 'authenticated') { throw new Error('The live original session did not resolve'); } - await context.test( - 'finds stale pending API keys with current and legacy metadata orders', - async () => { + if (pendingCleanupOnly) { + yield* Effect.gen(function* integrationEffect4() { const nowEpochMillis = 1_800_000_000_000; const lifecycleOperationId = randomUUID(); - const pending = await runEffectTestPromise( - keys.issue(originalHeaders, { + const pending = yield* keys.issue(originalHeaders, { + issuerPrincipalId: originalPrincipalId, + lifecycleOperationId, + name: 'Pending cleanup integration key', + tenantId, + }); + yield* authDatabase + .update(apikey) + .set({ createdAt: new Date(nowEpochMillis - 10 * 60 * 1000) }) + .where(eq(apikey.id, pending.providerKeyId)); + expect( + yield* keys.pendingCleanup({ issuerPrincipalId: originalPrincipalId, - lifecycleOperationId, - name: 'Pending cleanup integration key', + lifecycleOperationId: randomUUID(), + nowEpochMillis, tenantId, }), - ); - await runEffectTestPromise( - authDatabase - .update(apikey) - .set({ createdAt: new Date(nowEpochMillis - 10 * 60 * 1000) }) - .where(eq(apikey.id, pending.providerKeyId)), - ); - - assert.deepEqual( - await runEffectTestPromise( - keys.pendingCleanup({ - issuerPrincipalId: originalPrincipalId, - lifecycleOperationId: randomUUID(), - nowEpochMillis, - tenantId, - }), - ), - { hasMore: false, providerKeyIds: [pending.providerKeyId] }, - ); - await runEffectTestPromise( - authDatabase - .update(apikey) - .set({ - metadata: JSON.stringify({ - issuerPrincipalId: originalPrincipalId, - lifecycleOperationId, - ontosLifecycle: 'binding_pending_v1', - tenantId, - }), - }) - .where(eq(apikey.id, pending.providerKeyId)), - ); - assert.deepEqual( - await runEffectTestPromise( - keys.pendingCleanup({ + ).toEqual({ hasMore: false, providerKeyIds: [pending.providerKeyId] }); + yield* authDatabase + .update(apikey) + .set({ + metadata: JSON.stringify({ issuerPrincipalId: originalPrincipalId, - lifecycleOperationId: randomUUID(), - nowEpochMillis, + lifecycleOperationId, + ontosLifecycle: 'binding_pending_v1', tenantId, }), - ), - { hasMore: false, providerKeyIds: [pending.providerKeyId] }, - ); - await runEffectTestPromise(keys.setEnabled(pending.providerKeyId, false)); - await runEffectTestPromise(keys.clearPendingCleanup(pending.providerKeyId)); - }, - ); + }) + .where(eq(apikey.id, pending.providerKeyId)); + expect( + yield* keys.pendingCleanup({ + issuerPrincipalId: originalPrincipalId, + lifecycleOperationId: randomUUID(), + nowEpochMillis, + tenantId, + }), + ).toEqual({ hasMore: false, providerKeyIds: [pending.providerKeyId] }); + yield* keys.setEnabled(pending.providerKeyId, false); + yield* keys.clearPendingCleanup(pending.providerKeyId); + }); + return; + } const lifecycle = makeIdentityLifecycleService(actionRuntime, keys, resolver); - const issued = await runEffectTestPromise( - lifecycle.issue({ - correlationId: randomUUID(), - idempotencyKey: `identity-integration-key-${randomUUID()}`, - name: 'Identity integration key', - principal: resolvedOriginal.principal, - requestHeaders: originalHeaders, - }), - ); - const verified = await runEffectTestPromise(keys.verify(issued.secret)); + const issued = yield* lifecycle.issue({ + correlationId: randomUUID(), + idempotencyKey: `identity-integration-key-${randomUUID()}`, + name: 'Identity integration key', + principal: resolvedOriginal.principal, + requestHeaders: originalHeaders, + }); + const verified = yield* keys.verify(issued.secret); const apiKeyAuthBindingId = issued.authBindingId; - const apiKeyIdentity = await runEffectTestPromise( - resolver.resolveBetterAuthApiKey(verified.providerKeyId), + const apiKeyIdentity = yield* resolver.resolveBetterAuthApiKey(verified.providerKeyId); + const { privateKey, publicKey } = yield* Effect.tryPromise(() => + generateKeyPair('EdDSA', { + crv: 'Ed25519', + extractable: true, + }), ); - const { privateKey, publicKey } = await generateKeyPair('EdDSA', { - crv: 'Ed25519', - extractable: true, - }); - const privateJwk = await exportJWK(privateKey); - const assertion = await runEffectTestPromise( - issueGatewayContextAssertion({ - audience: 'identity-integration', - principal: { - authBindingId: apiKeyIdentity.authBindingId, - authContextRef: `better-auth-api-key:${verified.providerKeyId}`, - authMethod: 'api_key', - principalId: apiKeyIdentity.principalId, - tenantId: apiKeyIdentity.tenantId, - }, - }).pipe( - Effect.provide( - makeGatewayIssuerLayer({ - currentTimeSeconds: Effect.succeed(1_800_000_000), - generateJti: Effect.succeed(randomUUID()), - loadAudiences: Effect.succeed(new Set(['identity-integration'])), - loadConfig: Effect.succeed({ - issuer: 'https://shell.identity-integration.test', - privateJwk: { - alg: 'EdDSA', - crv: 'Ed25519', - d: privateJwk.d ?? '', - kid: 'identity-integration-key', - kty: 'OKP', - use: 'sig', - x: privateJwk.x ?? '', - }, - }), + const privateJwk = yield* Effect.tryPromise(() => exportJWK(privateKey)); + const assertion = yield* issueGatewayContextAssertion({ + audience: 'identity-integration', + principal: { + authBindingId: apiKeyIdentity.authBindingId, + authContextRef: `better-auth-api-key:${verified.providerKeyId}`, + authMethod: 'api_key', + principalId: apiKeyIdentity.principalId, + tenantId: apiKeyIdentity.tenantId, + }, + }).pipe( + Effect.provide( + makeGatewayIssuerLayer({ + currentTimeSeconds: Effect.succeed(1_800_000_000), + generateJti: Effect.succeed(randomUUID()), + loadAudiences: Effect.succeed(new Set(['identity-integration'])), + loadConfig: Effect.succeed({ + issuer: 'https://shell.identity-integration.test', + privateJwk: { + alg: 'EdDSA', + crv: 'Ed25519', + d: privateJwk.d ?? '', + kid: 'identity-integration-key', + kty: 'OKP', + use: 'sig', + x: privateJwk.x ?? '', + }, }), - ), + }), ), ); - const verifiedAssertion = await jwtVerify(assertion.token, publicKey, { - algorithms: ['EdDSA'], - audience: 'identity-integration', - currentDate: new Date(1_800_000_001_000), - issuer: 'https://shell.identity-integration.test', - }); - assert.deepEqual(verifiedAssertion.payload['principal'], { + const verifiedAssertion = yield* Effect.tryPromise(() => + jwtVerify(assertion.token, publicKey, { + algorithms: ['EdDSA'], + audience: 'identity-integration', + currentDate: new Date(1_800_000_001_000), + issuer: 'https://shell.identity-integration.test', + }), + ); + expect(verifiedAssertion.payload['principal']).toEqual({ authBindingId: apiKeyAuthBindingId, authContextRef: `better-auth-api-key:${verified.providerKeyId}`, authMethod: 'api_key', principalId: originalPrincipalId, tenantId, }); - assert.equal(JSON.stringify(verifiedAssertion.payload).includes(issued.secret), false); - await runEffectTestPromise( - providePrincipalManagementRepository( - actionRuntime.runAction({ - payload: { displayName: 'API-key evidence target', kind: 'service' }, - principal: { - authBindingId: apiKeyAuthBindingId, - authContextRef: `better-auth-api-key:${verified.providerKeyId}`, - authMethod: 'api_key', - principalId: originalPrincipalId, - tenantId, - }, - registration: createNonHumanPrincipalAction, - transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, - }), - ), - ); - await runEffectTestPromise(keys.setEnabled(verified.providerKeyId, false)); - const invalidKey = await runEffectTestPromise(Effect.flip(keys.verify(issued.secret))); - assert.ok(Predicate.isTagged(invalidKey, 'ApiKeyCredentialInvalidError')); - - const managedPrincipal = await runEffectTestPromise( - providePrincipalManagementRepository( - lifecycle.createNonHumanPrincipal({ - correlationId: randomUUID(), - idempotencyKey: randomUUID(), - payload: { displayName: 'Cross-admin integration', kind: 'integration' }, - principal: resolvedOriginal.principal, - }), - ), + expect(JSON.stringify(verifiedAssertion.payload).includes(issued.secret)).toBe(false); + yield* providePrincipalManagementRepository( + actionRuntime.runAction({ + payload: { displayName: 'API-key evidence target', kind: 'service' }, + principal: { + authBindingId: apiKeyAuthBindingId, + authContextRef: `better-auth-api-key:${verified.providerKeyId}`, + authMethod: 'api_key', + principalId: originalPrincipalId, + tenantId, + }, + registration: createNonHumanPrincipalAction, + transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, + }), ); - const managedKey = await runEffectTestPromise( - lifecycle.issue({ + yield* keys.setEnabled(verified.providerKeyId, false); + const invalidKey = yield* Effect.flip(keys.verify(issued.secret)); + expect(Predicate.isTagged(invalidKey, 'ApiKeyCredentialInvalidError')).toBe(true); + const managedPrincipal = yield* providePrincipalManagementRepository( + lifecycle.createNonHumanPrincipal({ correlationId: randomUUID(), idempotencyKey: randomUUID(), - managedPrincipalId: managedPrincipal.principalId, - name: 'Cross-admin key', + payload: { displayName: 'Cross-admin integration', kind: 'integration' }, principal: resolvedOriginal.principal, - requestHeaders: originalHeaders, }), ); - const secondAdministratorSignIn = await runEffectTestPromise( - authentication.signIn( - secondAdministratorEmail, - password, - new Headers({ origin: configuration.baseUrl }), - ), - ); - const secondAdministratorContext = await runEffectTestPromise( - provideContextAccess( - authentication.resolveTenantContext( - new Headers({ - cookie: cookieHeader(secondAdministratorSignIn.setCookieHeaders), - origin: configuration.baseUrl, - }), - ), + const managedKey = yield* lifecycle.issue({ + correlationId: randomUUID(), + idempotencyKey: randomUUID(), + managedPrincipalId: managedPrincipal.principalId, + name: 'Cross-admin key', + principal: resolvedOriginal.principal, + requestHeaders: originalHeaders, + }); + const secondAdministratorSignIn = yield* authentication.signIn( + secondAdministratorEmail, + password, + new Headers({ origin: configuration.baseUrl }), + ); + const secondAdministratorContext = yield* provideContextAccess( + authentication.resolveTenantContext( + new Headers({ + cookie: cookieHeader(secondAdministratorSignIn.setCookieHeaders), + origin: configuration.baseUrl, + }), ), ); - assert.equal(secondAdministratorContext.state, 'authenticated'); + expect(secondAdministratorContext.state).toBe('authenticated'); if (secondAdministratorContext.state !== 'authenticated') { throw new Error('The second live tenant administrator did not resolve'); } - const crossAdminDisabled = await runEffectTestPromise( - lifecycle.setStatus({ - authBindingId: managedKey.authBindingId, - correlationId: randomUUID(), - expectedStatus: 'active', - idempotencyKey: randomUUID(), - managedPrincipalId: managedPrincipal.principalId, - newStatus: 'disabled', - principal: secondAdministratorContext.principal, - reason: 'Cross-admin lifecycle integration proof', - }), - ); - assert.equal(crossAdminDisabled.enabled, false); - assert.equal(crossAdminDisabled.cleanupPending, false); - + const crossAdminDisabled = yield* lifecycle.setStatus({ + authBindingId: managedKey.authBindingId, + correlationId: randomUUID(), + expectedStatus: 'active', + idempotencyKey: randomUUID(), + managedPrincipalId: managedPrincipal.principalId, + newStatus: 'disabled', + principal: secondAdministratorContext.principal, + reason: 'Cross-admin lifecycle integration proof', + }); + expect(crossAdminDisabled.enabled).toBe(false); + expect(crossAdminDisabled.cleanupPending).toBe(false); const supportRecoveryPrincipal = makeSupportRecoveryPrincipalContextResolver({ executor: coreDatabase, }); @@ -533,116 +491,100 @@ void test('verifies provider keys and completes live support impersonation with Context.add(SupportImpersonationStoreService, makeSupportImpersonationStore(authDatabase)), ), ); - const started = await runEffectTestPromise( - provideContextAccess( - providePrincipalManagementRepository( - support - .start({ - idempotencyKey: randomUUID(), - reason: 'Investigating a tenant support request', - requestHeaders: originalHeaders, - targetPrincipalId, - }) - .pipe(Effect.provideService(SupportImpersonationCorrelationId, randomUUID())), - ), + const started = yield* provideContextAccess( + providePrincipalManagementRepository( + support + .start({ + idempotencyKey: randomUUID(), + reason: 'Investigating a tenant support request', + requestHeaders: originalHeaders, + targetPrincipalId, + }) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, randomUUID())), ), ); const impersonatedHeaders = new Headers({ cookie: cookieHeader(started.setCookieHeaders), origin: configuration.baseUrl, }); - const [impersonationSession] = await runEffectTestPromise( - authDatabase - .select({ actionId: session.impersonationActionId, id: session.id }) - .from(session) - .where(and(eq(session.userId, targetUserId), eq(session.impersonatedBy, originalUserId))) - .limit(1), - ); - assert.notEqual(impersonationSession, undefined); + const [impersonationSession] = yield* authDatabase + .select({ actionId: session.impersonationActionId, id: session.id }) + .from(session) + .where(and(eq(session.userId, targetUserId), eq(session.impersonatedBy, originalUserId))) + .limit(1); + expect(impersonationSession).not.toBe(undefined); if (impersonationSession === undefined) { throw new TypeError('The support impersonation session was not persisted'); } - assert.equal(Predicate.isString(impersonationSession.actionId), true); + expect(Predicate.isString(impersonationSession.actionId)).toBe(true); if (!Predicate.isString(impersonationSession.actionId)) { throw new TypeError('The approved support start did not persist its Action correlation'); } - await runEffectTestPromise( - authDatabase - .update(session) - .set({ impersonationActionId: null }) - .where(eq(session.id, impersonationSession.id)), - ); - const incompleteImpersonation = await runEffectTestPromise( - Effect.flip(provideContextAccess(authentication.resolveTenantContext(impersonatedHeaders))), - ); - assert.ok(Predicate.isTagged(incompleteImpersonation, 'OntosIdentityForbiddenError')); - await runEffectTestPromise( - authDatabase - .update(session) - .set({ impersonationActionId: impersonationSession.actionId }) - .where(eq(session.id, impersonationSession.id)), + yield* authDatabase + .update(session) + .set({ impersonationActionId: null }) + .where(eq(session.id, impersonationSession.id)); + const incompleteImpersonation = yield* Effect.flip( + provideContextAccess(authentication.resolveTenantContext(impersonatedHeaders)), ); - await runEffectTestPromise( - authDatabase - .update(session) - .set({ impersonationReason: 'Tampered support reason' }) - .where(eq(session.id, impersonationSession.id)), + expect(Predicate.isTagged(incompleteImpersonation, 'OntosIdentityForbiddenError')).toBe(true); + yield* authDatabase + .update(session) + .set({ impersonationActionId: impersonationSession.actionId }) + .where(eq(session.id, impersonationSession.id)); + yield* authDatabase + .update(session) + .set({ impersonationReason: 'Tampered support reason' }) + .where(eq(session.id, impersonationSession.id)); + const mismatchedImpersonationReason = yield* Effect.flip( + provideContextAccess(authentication.resolveTenantContext(impersonatedHeaders)), ); - const mismatchedImpersonationReason = await runEffectTestPromise( - Effect.flip(provideContextAccess(authentication.resolveTenantContext(impersonatedHeaders))), + expect(Predicate.isTagged(mismatchedImpersonationReason, 'OntosIdentityForbiddenError')).toBe( + true, ); - assert.ok(Predicate.isTagged(mismatchedImpersonationReason, 'OntosIdentityForbiddenError')); - await runEffectTestPromise( - authDatabase - .update(session) - .set({ impersonationReason: 'Investigating a tenant support request' }) - .where(eq(session.id, impersonationSession.id)), + yield* authDatabase + .update(session) + .set({ impersonationReason: 'Investigating a tenant support request' }) + .where(eq(session.id, impersonationSession.id)); + const impersonated = yield* provideContextAccess( + authentication.resolveTenantContext(impersonatedHeaders), ); - const impersonated = await runEffectTestPromise( - provideContextAccess(authentication.resolveTenantContext(impersonatedHeaders)), - ); - assert.equal(impersonated.state, 'authenticated'); + expect(impersonated.state).toBe('authenticated'); if (impersonated.state === 'authenticated') { - assert.equal(impersonated.principal.authMethod, 'support_impersonation'); - assert.equal(impersonated.principal.principalId, targetPrincipalId); - assert.equal(impersonated.principal.impersonatedByPrincipalId, originalPrincipalId); - await runEffectTestPromise( - providePrincipalManagementRepository( - actionRuntime.runAction({ - payload: { displayName: 'Support evidence target', kind: 'integration' }, - principal: impersonated.principal, - registration: createNonHumanPrincipalAction, - transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, - }), - ), + expect(impersonated.principal.authMethod).toBe('support_impersonation'); + expect(impersonated.principal.principalId).toBe(targetPrincipalId); + expect(impersonated.principal.impersonatedByPrincipalId).toBe(originalPrincipalId); + yield* providePrincipalManagementRepository( + actionRuntime.runAction({ + payload: { displayName: 'Support evidence target', kind: 'integration' }, + principal: impersonated.principal, + registration: createNonHumanPrincipalAction, + transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, + }), ); } supportPermissionAllowed = false; - const revokedImpersonation = await runEffectTestPromise( - Effect.flip(provideContextAccess(authentication.resolveTenantContext(impersonatedHeaders))), + const revokedImpersonation = yield* Effect.flip( + provideContextAccess(authentication.resolveTenantContext(impersonatedHeaders)), ); - assert.ok(Predicate.isTagged(revokedImpersonation, 'OntosIdentityForbiddenError')); - const stopped = await runEffectTestPromise( - provideContextAccess( - providePrincipalManagementRepository( - support - .stop({ - idempotencyKey: randomUUID(), - requestHeaders: impersonatedHeaders, - }) - .pipe(Effect.provideService(SupportImpersonationCorrelationId, randomUUID())), - ), + expect(Predicate.isTagged(revokedImpersonation, 'OntosIdentityForbiddenError')).toBe(true); + const stopped = yield* provideContextAccess( + providePrincipalManagementRepository( + support + .stop({ + idempotencyKey: randomUUID(), + requestHeaders: impersonatedHeaders, + }) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, randomUUID())), ), ); - assert.equal(stopped.checkpointPending, false); - assert.ok(stopped.setCookieHeaders.length > 0); - const checkpoints = await runEffectTestPromise( - coreDatabase - .select({ evidence: auditEvents.evidenceJson }) - .from(auditEvents) - .where(eq(auditEvents.tenantId, tenantId)), - ); - assert.deepEqual( + expect(stopped.checkpointPending).toBe(false); + expect(stopped.setCookieHeaders.length > 0).toBe(true); + const checkpoints = yield* coreDatabase + .select({ evidence: auditEvents.evidenceJson }) + .from(auditEvents) + .where(eq(auditEvents.tenantId, tenantId)); + expect( checkpoints .flatMap(({ evidence }) => Predicate.isObjectKeyword(evidence) && @@ -653,20 +595,17 @@ void test('verifies provider keys and completes live support impersonation with : [], ) .toSorted(), - ['requested', 'started', 'stopped'], - ); - const identityEvidence = await runEffectTestPromise( - coreDatabase - .select({ - authBindingId: auditEvents.authBindingId, - authMethod: auditEvents.authMethod, - impersonatedByPrincipalId: auditEvents.impersonatedByPrincipalId, - principalId: auditEvents.principalId, - }) - .from(auditEvents) - .where(eq(auditEvents.tenantId, tenantId)), - ); - assert.ok( + ).toEqual(['requested', 'started', 'stopped']); + const identityEvidence = yield* coreDatabase + .select({ + authBindingId: auditEvents.authBindingId, + authMethod: auditEvents.authMethod, + impersonatedByPrincipalId: auditEvents.impersonatedByPrincipalId, + principalId: auditEvents.principalId, + }) + .from(auditEvents) + .where(eq(auditEvents.tenantId, tenantId)); + expect( identityEvidence.some( (evidence) => evidence.authMethod === 'api_key' && @@ -674,8 +613,8 @@ void test('verifies provider keys and completes live support impersonation with evidence.principalId === originalPrincipalId && evidence.impersonatedByPrincipalId === null, ), - ); - assert.ok( + ).toBe(true); + expect( identityEvidence.some( (evidence) => evidence.authMethod === 'support_impersonation' && @@ -683,16 +622,8 @@ void test('verifies provider keys and completes live support impersonation with evidence.principalId === targetPrincipalId && evidence.impersonatedByPrincipalId === originalPrincipalId, ), - ); - const recovery = await runEffectTestPromise( - authDatabase.select().from(supportImpersonationRecovery), - ); - assert.equal(recovery.length, 0); - } finally { - await cleanup(); - await corePool.end(); - } -}); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), + ).toBe(true); + const recovery = yield* authDatabase.select().from(supportImpersonationRecovery); + expect(recovery.length).toBe(0); + }), ); diff --git a/app/apps/shell-super-app/tests/integration/module-catalog-runtime.test.ts b/app/apps/shell-super-app/tests/integration/module-catalog-runtime.test.ts index 9a986a003..5dd8e7d9b 100644 --- a/app/apps/shell-super-app/tests/integration/module-catalog-runtime.test.ts +++ b/app/apps/shell-super-app/tests/integration/module-catalog-runtime.test.ts @@ -1,6 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { OntosModuleDeploymentContractSchema, defineAction, @@ -12,7 +10,7 @@ import { getVerticalRuntimeActions, getVerticalRuntimeOutboxWorkers, } from '@app/core-runtime'; -import { Effect, Function as Fn, Schema } from 'effect'; +import { Effect, Schema } from 'effect'; import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/unstable/httpapi'; import { makeEffectHttpApiClient } from '@modern-js/plugin-bff/effect-client'; import { deriveDeploymentAllowlist } from '../../api/modules/deployment-allowlist.ts'; @@ -84,7 +82,6 @@ const contract = ( runtime: { outboxSubscriptions: overrides.outboxSubscriptions ?? [] }, schemaVersion: '2', }); - const PropertyApi = HttpApi.make('PropertyApi').add( HttpApiGroup.make('property').add(HttpApiEndpoint.get('listUnits', '/units')), ); @@ -92,7 +89,6 @@ const PropertyRegistryUnitIdSchema = Schema.String.pipe(Schema.brand('PropertyRe const DocumentsCenterDocumentIdSchema = Schema.String.pipe( Schema.brand('DocumentsCenterDocumentId'), ); - const PropertyAction = defineAction( { accessEvidencePolicy: { @@ -120,7 +116,6 @@ const PropertyAction = defineAction( }, () => Effect.succeed({ renamed: true }), ); - const PropertyOutboxWorker = defineOutboxWorker( { consumerModuleKey: 'property.registry', @@ -145,9 +140,7 @@ const PropertyOutboxWorker = defineOutboxWorker( }, () => Effect.void, ); - const PropertyDashboard = () => null; - const propertyManifest = defineOntosModuleManifest({ activation: { defaultState: 'inactive', @@ -182,153 +175,119 @@ const propertyManifest = defineOntosModuleManifest({ }, }, }); - const propertyRuntimeRegistration = defineVerticalRuntimeRegistration({ actions: [PropertyAction], manifest: propertyManifest, outboxWorkers: [PropertyOutboxWorker], }); - const propertySafeRuntime = extractVerticalRuntimeSafeDescriptors(propertyRuntimeRegistration); - const ContractDocumentJsonSchema = Schema.fromJsonString(OntosModuleDeploymentContractSchema); - -type EffectTestCallback = () => Promise; - -const runEffectTest = (effect: Effect.Effect): EffectTestCallback => - Fn.flow(Fn.constant(effect), runEffectTestPromise); - -const makeContractFetch = ( - documents: ReadonlyMap, - requests: Map, -): ModuleContractFetch => - Fn.flow( - (input: Parameters[0]) => - Effect.gen(function* encodeContractResponse() { - const { url } = new Request(input); - const document = documents.get(url); - if (document === undefined) { - return new Response(null, { status: 404 }); - } - requests.set(url, (requests.get(url) ?? 0) + 1); - const encodedDocument = yield* Schema.encodeUnknownEffect(ContractDocumentJsonSchema)( - document, - ); - return new Response(encodedDocument, { - headers: { 'content-type': 'application/json' }, - }); - }), - runEffectTestPromise, - ); - -void test( +const makeContractFetch = + (documents: ReadonlyMap, requests: Map): ModuleContractFetch => + (input) => { + const { url } = new Request(input); + const document = documents.get(url); + if (document === undefined) { + return Promise.resolve(new Response(null, { status: 404 })); + } + requests.set(url, (requests.get(url) ?? 0) + 1); + const encodedDocument = Schema.encodeUnknownSync(ContractDocumentJsonSchema)(document); + return Promise.resolve( + new Response(encodedDocument, { headers: { 'content-type': 'application/json' } }), + ); + }; +it.effect( 'keeps discovered metadata separate from one complete owner-local runtime', - runEffectTest( - Effect.gen(function* verifyInstalledModuleCatalogRuntime() { - const propertyUrl = 'https://property-registry.test/.well-known/ontos-module-manifest.json'; - const documentsUrl = 'https://documents-center.test/.well-known/ontos-module-manifest.json'; - const requests = new Map(); - const contractFetch = makeContractFetch( - new Map([ - [ - propertyUrl, - contract('property-registry', 'property.registry', { - actions: propertySafeRuntime.actions, - api: [{ key: 'property.registry.api', operationKeys: ['property.listUnits'] }], - components: [ - { - expose: './Dashboard', - key: 'property.registry.dashboard', - mfBoundaryId: 'verticalPropertyRegistry', - }, - ], - outboxSubscriptions: propertySafeRuntime.outboxSubscriptions, - }), - ], - [documentsUrl, contract('documents-center', 'documents.center')], - ]), - requests, - ); - - const allowlist = yield* deriveDeploymentAllowlist({ + Effect.fnUntraced(function* runIntegration1() { + const propertyUrl = 'https://property-registry.test/.well-known/ontos-module-manifest.json'; + const documentsUrl = 'https://documents-center.test/.well-known/ontos-module-manifest.json'; + const requests = new Map(); + const contractFetch = makeContractFetch( + new Map([ + [ + propertyUrl, + contract('property-registry', 'property.registry', { + actions: propertySafeRuntime.actions, + api: [{ key: 'property.registry.api', operationKeys: ['property.listUnits'] }], + components: [ + { + expose: './Dashboard', + key: 'property.registry.dashboard', + mfBoundaryId: 'verticalPropertyRegistry', + }, + ], + outboxSubscriptions: propertySafeRuntime.outboxSubscriptions, + }), + ], + [documentsUrl, contract('documents-center', 'documents.center')], + ]), + requests, + ); + const allowlist = yield* deriveDeploymentAllowlist({ + environment: 'development', + overlay: { environment: 'development', - overlay: { - environment: 'development', - ontosModuleManifests: { - 'documents-center': documentsUrl, - 'property-registry': propertyUrl, - }, - schemaVersion: 1, + ontosModuleManifests: { + 'documents-center': documentsUrl, + 'property-registry': propertyUrl, }, - topology: { - verticals: [ - { id: 'property-registry', kind: 'vertical' }, - { id: 'documents-center', kind: 'vertical' }, - ], - }, - }); - const loader = makeInstalledModuleCatalogLoader(allowlist, contractFetch); - const first = yield* loader; - const second = yield* loader; - - assert.strictEqual(first, second); - assert.equal(requests.get(propertyUrl), 1); - assert.equal(requests.get(documentsUrl), 1); - assert.equal( - first.getByDeploymentAppId('property-registry')?.manifest.module.id, - 'property.registry', - ); - assert.equal(first.getByModuleId('property.registry')?.deployment.appId, 'property-registry'); - assert.deepEqual(first.moduleIds, ['documents.center', 'property.registry']); - const tenantStates = [ - { moduleKey: 'property.registry', state: 'active' }, - { moduleKey: 'documents.center', state: 'inactive' }, - ] as const; - assert.deepEqual( - tenantStates - .filter( - ({ moduleKey, state }) => state === 'active' && first.moduleIds.includes(moduleKey), - ) - .map(({ moduleKey }) => moduleKey), - ['property.registry'], - ); - - assert.strictEqual(getVerticalRuntimeActions(propertyRuntimeRegistration)[0], PropertyAction); - assert.strictEqual( - getVerticalRuntimeOutboxWorkers(propertyRuntimeRegistration)[0], - PropertyOutboxWorker, - ); - assert.deepEqual(Object.keys(propertyRuntimeRegistration), ['moduleId']); - - let matchedSubscriptions: readonly object[] = []; - yield* matchInstalledOutboxMessagesOnce(first, (input) => { - matchedSubscriptions = input.subscriptions; - return Effect.succeed({ deliveriesCreated: 1, messagesMatched: 1 }); - }); - assert.deepEqual(matchedSubscriptions, propertySafeRuntime.outboxSubscriptions); - - const propertyClientReference = makeEffectHttpApiClient(PropertyApi, { - baseUrl: new URL('/api', propertyUrl), - }); - assert.equal(Effect.isEffect(propertyClientReference), true); - assert.deepEqual( - first.getByModuleId('property.registry')?.manifest.publicSurface.components, - [ - { - expose: './Dashboard', - key: 'property.registry.dashboard', - mfBoundaryId: 'verticalPropertyRegistry', - }, + schemaVersion: 1, + }, + topology: { + verticals: [ + { id: 'property-registry', kind: 'vertical' }, + { id: 'documents-center', kind: 'vertical' }, ], - ); - - const serialized = yield* Schema.encodeUnknownEffect(ContractDocumentJsonSchema)( - first.getByModuleId('property.registry'), - ); - assert.equal(serialized.includes('payloadSchema'), false); - assert.equal(serialized.includes('leaseDurationMs'), false); - assert.equal(serialized.includes('PropertyDashboard'), false); - assert.equal(serialized.includes('handler'), false); - }), - ), + }, + }); + const loader = makeInstalledModuleCatalogLoader(allowlist, contractFetch); + const first = yield* loader; + const second = yield* loader; + expect(first).toBe(second); + expect(requests.get(propertyUrl)).toBe(1); + expect(requests.get(documentsUrl)).toBe(1); + expect(first.getByDeploymentAppId('property-registry')?.manifest.module.id).toBe( + 'property.registry', + ); + expect(first.getByModuleId('property.registry')?.deployment.appId).toBe('property-registry'); + expect(first.moduleIds).toEqual(['documents.center', 'property.registry']); + const tenantStates = [ + { moduleKey: 'property.registry', state: 'active' }, + { moduleKey: 'documents.center', state: 'inactive' }, + ] as const; + expect( + tenantStates + .filter(({ moduleKey, state }) => state === 'active' && first.moduleIds.includes(moduleKey)) + .map(({ moduleKey }) => moduleKey), + ).toEqual(['property.registry']); + expect(getVerticalRuntimeActions(propertyRuntimeRegistration)[0]).toBe(PropertyAction); + expect(getVerticalRuntimeOutboxWorkers(propertyRuntimeRegistration)[0]).toBe( + PropertyOutboxWorker, + ); + expect(Object.keys(propertyRuntimeRegistration)).toEqual(['moduleId']); + let matchedSubscriptions: readonly object[] = []; + yield* matchInstalledOutboxMessagesOnce(first, (input) => { + matchedSubscriptions = input.subscriptions; + return Effect.succeed({ deliveriesCreated: 1, messagesMatched: 1 }); + }); + expect(matchedSubscriptions).toEqual(propertySafeRuntime.outboxSubscriptions); + const propertyClientReference = makeEffectHttpApiClient(PropertyApi, { + baseUrl: new URL('/api', propertyUrl), + }); + expect(Effect.isEffect(propertyClientReference)).toBe(true); + expect(first.getByModuleId('property.registry')?.manifest.publicSurface.components).toEqual([ + { + expose: './Dashboard', + key: 'property.registry.dashboard', + mfBoundaryId: 'verticalPropertyRegistry', + }, + ]); + const serialized = yield* Schema.encodeUnknownEffect(ContractDocumentJsonSchema)( + first.getByModuleId('property.registry'), + ); + expect(serialized.includes('payloadSchema')).toBe(false); + expect(serialized.includes('leaseDurationMs')).toBe(false); + expect(serialized.includes('PropertyDashboard')).toBe(false); + expect(serialized.includes('handler')).toBe(false); + }), ); diff --git a/app/apps/shell-super-app/tests/integration/module-federation-i18n-runtime.test.ts b/app/apps/shell-super-app/tests/integration/module-federation-i18n-runtime.test.ts index 0c67f3555..c61a63493 100644 --- a/app/apps/shell-super-app/tests/integration/module-federation-i18n-runtime.test.ts +++ b/app/apps/shell-super-app/tests/integration/module-federation-i18n-runtime.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs'; import { createRequire, registerHooks } from 'node:module'; -import { strict as assert } from 'node:assert'; -import test from 'node:test'; +import { describe, expect, it } from 'effect-rstest'; +import { Effect } from 'effect'; import * as Schema from 'effect/Schema'; const shellConfigUrl = new URL('../../module-federation.config.ts', import.meta.url); @@ -29,26 +29,32 @@ registerHooks({ }, }); -test('Shell and Party Registry share the i18n runtime that owns the federated provider context', async () => { - const [{ default: shellConfig }, { default: partyRegistryConfig }] = await Promise.all([ - import(shellConfigUrl.href), - import(partyRegistryConfigUrl.href), - ]); - const require = createRequire(shellConfigUrl); - const { version: i18nVersion } = Schema.decodeUnknownSync( - Schema.Struct({ version: Schema.String }), - )(require('@modern-js/plugin-i18n/package.json')); - const expectedSharedRuntime = { - import: '@modern-js/plugin-i18n/runtime/no-react-i18next', - requiredVersion: i18nVersion, - singleton: true, - strictVersion: true, - treeShaking: false, - }; +describe('module-federation-i18n-runtime', () => { + it.effect( + 'Shell and Party Registry share the i18n runtime that owns the federated provider context', + () => + Effect.gen(function* sharesFederatedI18nRuntime() { + const [{ default: shellConfig }, { default: partyRegistryConfig }] = yield* Effect.promise( + () => Promise.all([import(shellConfigUrl.href), import(partyRegistryConfigUrl.href)]), + ); + const require = createRequire(shellConfigUrl); + const { version: i18nVersion } = Schema.decodeUnknownSync( + Schema.Struct({ version: Schema.String }), + )(require('@modern-js/plugin-i18n/package.json')); + const expectedSharedRuntime = { + import: '@modern-js/plugin-i18n/runtime/no-react-i18next', + requiredVersion: i18nVersion, + singleton: true, + strictVersion: true, + treeShaking: false, + }; - assert.deepEqual(shellConfig.shared?.['@modern-js/plugin-i18n/runtime'], expectedSharedRuntime); - assert.deepEqual( - partyRegistryConfig.shared?.['@modern-js/plugin-i18n/runtime'], - expectedSharedRuntime, + expect(shellConfig.shared?.['@modern-js/plugin-i18n/runtime']).toEqual( + expectedSharedRuntime, + ); + expect(partyRegistryConfig.shared?.['@modern-js/plugin-i18n/runtime']).toEqual( + expectedSharedRuntime, + ); + }), ); }); diff --git a/app/apps/shell-super-app/tests/integration/stage-demo-bootstrap.test.ts b/app/apps/shell-super-app/tests/integration/stage-demo-bootstrap.test.ts index 5bfdc4bf6..614c0fde5 100644 --- a/app/apps/shell-super-app/tests/integration/stage-demo-bootstrap.test.ts +++ b/app/apps/shell-super-app/tests/integration/stage-demo-bootstrap.test.ts @@ -1,89 +1,89 @@ -import assert from 'node:assert/strict'; +import { describe, expect, it, rstest } from 'effect-rstest'; import { randomUUID } from 'node:crypto'; -import test from 'node:test'; import { memoryAdapter } from 'better-auth/adapters/memory'; import { Cause, Deferred, Effect, Exit, Fiber } from 'effect'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import { loadAuthConfig } from '../../api/auth/config.ts'; import { AuthDatabase, makeAuthDatabase } from '../../api/auth/db/client.ts'; import { bootstrapStageDemo } from '../../api/auth/stage-demo-bootstrap-runtime-infrastructure.ts'; -void test( - 'stage bootstrap waits for non-cancellable SDK writes before closing its database scope', - { timeout: 10_000 }, - async (context) => { - const store = { account: [], session: [], user: [], verification: [] }; - const sdkStarted = Deferred.makeUnsafe(); - const sdkSettlement = Promise.withResolvers(); - const sdkAdapter = memoryAdapter(store); - let databaseClosed = false; - const configuration = await runEffectTestPromise(loadAuthConfig()); - const program = Effect.scoped( - Effect.gen(function* bootstrapWithDelayedSdk() { - yield* Effect.addFinalizer(() => - Effect.sync(() => { - databaseClosed = true; - }), - ); - const database = yield* makeAuthDatabase(configuration); - return yield* bootstrapStageDemo({ - accounts: [ - { - email: `stage-first-${randomUUID()}@example.test`, - password: randomUUID(), - principalDisplayName: 'First', - }, - { - email: `stage-second-${randomUUID()}@example.test`, - password: randomUUID(), - principalDisplayName: 'Second', - }, - ], - authBaseUrl: configuration.baseUrl, - authSecret: configuration.secret, - databaseAdminUrl: configuration.connectionString, - }).pipe( - Effect.provideService(AuthDatabase, { - adapter: (options) => { - const adapter = sdkAdapter(options); - const create = adapter.create.bind(adapter); - context.mock.method( - adapter, - 'create', - async (input: Parameters[0]) => { - if (input.model === 'user') { - Deferred.doneUnsafe(sdkStarted, Effect.succeed(null)); - await sdkSettlement.promise; - } - return await create(input); +describe('stage-demo-bootstrap', () => { + it.live( + 'stage bootstrap waits for non-cancellable SDK writes before closing its database scope', + () => + Effect.gen(function* waitsForSdkSettlement() { + const store = { account: [], session: [], user: [], verification: [] }; + const sdkStarted = Deferred.makeUnsafe(); + const sdkSettlement = Promise.withResolvers(); + const sdkAdapter = memoryAdapter(store); + let databaseClosed = false; + const configuration = yield* loadAuthConfig(); + const program = Effect.scoped( + Effect.gen(function* bootstrapWithDelayedSdk() { + yield* Effect.addFinalizer(() => + Effect.sync(() => { + databaseClosed = true; + }), + ); + const database = yield* makeAuthDatabase(configuration); + return yield* bootstrapStageDemo({ + accounts: [ + { + email: `stage-first-${randomUUID()}@example.test`, + password: randomUUID(), + principalDisplayName: 'First', + }, + { + email: `stage-second-${randomUUID()}@example.test`, + password: randomUUID(), + principalDisplayName: 'Second', + }, + ], + authBaseUrl: configuration.baseUrl, + authSecret: configuration.secret, + databaseAdminUrl: configuration.connectionString, + }).pipe( + Effect.provideService(AuthDatabase, { + adapter: (options) => { + const adapter = sdkAdapter(options); + const create = adapter.create.bind(adapter); + rstest + .spyOn(adapter, 'create') + .mockImplementation((input: Parameters[0]) => { + if (input.model === 'user') { + Deferred.doneUnsafe(sdkStarted, Effect.succeed(null)); + // oxlint-disable-next-line sonarjs/no-nested-functions -- SDK settlement continuation stays inside its adapter mock. + return sdkSettlement.promise.then(() => create(input)); + } + return create(input); + }); + return adapter; }, - ); - return adapter; - }, - executor: database.executor, + executor: database.executor, + }), + ); }), ); + const outcome = yield* Effect.gen(function* interruptPendingBootstrap() { + const bootstrap = yield* program.pipe(Effect.forkChild); + yield* Deferred.await(sdkStarted).pipe(Effect.raceFirst(Fiber.join(bootstrap))); + const interruption = yield* Fiber.interrupt(bootstrap).pipe(Effect.forkChild); + yield* Effect.yieldNow; + const pending = bootstrap.pollUnsafe() === undefined; + const closedBeforeSettlement = databaseClosed; + sdkSettlement.resolve(null); + yield* Fiber.join(interruption); + return { closedBeforeSettlement, exit: yield* Fiber.await(bootstrap), pending }; + }).pipe(Effect.ensuring(Effect.sync(() => sdkSettlement.resolve(null)))); + expect(outcome.pending).toBe(true); + expect(outcome.closedBeforeSettlement).toBe(false); + expect(Exit.isFailure(outcome.exit)).toBe(true); + if (Exit.isFailure(outcome.exit)) { + expect(Cause.hasInterrupts(outcome.exit.cause)).toBe(true); + } + expect(databaseClosed).toBe(true); + expect(store.user).toHaveLength(1); + expect(store.account).toHaveLength(1); }), - ); - const outcome = await runEffectTestPromise( - Effect.gen(function* interruptPendingBootstrap() { - const bootstrap = yield* program.pipe(Effect.forkChild); - yield* Deferred.await(sdkStarted).pipe(Effect.raceFirst(Fiber.join(bootstrap))); - const interruption = yield* Fiber.interrupt(bootstrap).pipe(Effect.forkChild); - yield* Effect.yieldNow; - const pending = bootstrap.pollUnsafe() === undefined; - const closedBeforeSettlement = databaseClosed; - sdkSettlement.resolve(null); - yield* Fiber.join(interruption); - return { closedBeforeSettlement, exit: yield* Fiber.await(bootstrap), pending }; - }).pipe(Effect.ensuring(Effect.sync(() => sdkSettlement.resolve(null)))), - ); - assert.equal(outcome.pending, true); - assert.equal(outcome.closedBeforeSettlement, false); - assert.ok(Exit.isFailure(outcome.exit)); - assert.equal(Cause.hasInterrupts(outcome.exit.cause), true); - assert.equal(databaseClosed, true); - assert.equal(store.user.length, 1); - assert.equal(store.account.length, 1); - }, -); + 10_000, + ); +}); diff --git a/app/apps/shell-super-app/tests/support/impersonation-service-doubles.ts b/app/apps/shell-super-app/tests/support/impersonation-service-doubles.ts index 929983af0..52cb7dfe2 100644 --- a/app/apps/shell-super-app/tests/support/impersonation-service-doubles.ts +++ b/app/apps/shell-super-app/tests/support/impersonation-service-doubles.ts @@ -1,3 +1,4 @@ +import { rs } from 'effect-rstest'; import { Effect } from 'effect'; import type { SupportAuthProvider, @@ -7,10 +8,6 @@ import type { AuthenticationServiceContract } from '../../api/auth/service.ts'; const unconfiguredEffect = (operation: string) => Effect.die(`${operation} is not configured in this test`); -const unconfiguredPromise = async (operation: string) => { - throw new Error(`${operation} is not configured in this test`); -}; - const authenticationDefaults: AuthenticationServiceContract = { availableTenants: () => unconfiguredEffect('availableTenants'), createFixtureUser: () => unconfiguredEffect('createFixtureUser'), @@ -24,9 +21,15 @@ const authenticationDefaults: AuthenticationServiceContract = { }; const providerDefaults: SupportAuthProvider['api'] = { - getSession: async () => await unconfiguredPromise('getSession'), - impersonateUser: async () => await unconfiguredPromise('impersonateUser'), - stopImpersonating: async () => await unconfiguredPromise('stopImpersonating'), + getSession: rs + .fn() + .mockRejectedValue(new Error('getSession is not configured in this test')), + impersonateUser: rs + .fn() + .mockRejectedValue(new Error('impersonateUser is not configured in this test')), + stopImpersonating: rs + .fn() + .mockRejectedValue(new Error('stopImpersonating is not configured in this test')), }; const storeDefaults: SupportImpersonationStore = { diff --git a/app/apps/shell-super-app/tests/unit/api-index.test.ts b/app/apps/shell-super-app/tests/unit/api-index.test.ts index 65a0f4f84..beda26f30 100644 --- a/app/apps/shell-super-app/tests/unit/api-index.test.ts +++ b/app/apps/shell-super-app/tests/unit/api-index.test.ts @@ -1,5 +1,5 @@ import { HttpServerResponse } from '@modern-js/plugin-bff/effect-edge'; -import { expect, test } from '@rstest/core'; +import { expect, test } from 'effect-rstest'; import { noStoreResponse } from '../../api/index.ts'; test('marks freshly issued API-key responses as non-cacheable', () => { diff --git a/app/apps/shell-super-app/tests/unit/auth-boundary.test.ts b/app/apps/shell-super-app/tests/unit/auth-boundary.test.ts index 39c7c52f2..e34ac4f04 100644 --- a/app/apps/shell-super-app/tests/unit/auth-boundary.test.ts +++ b/app/apps/shell-super-app/tests/unit/auth-boundary.test.ts @@ -1,5 +1,5 @@ import fs from 'node:fs'; -import { expect, test } from '@rstest/core'; +import { expect, test } from 'effect-rstest'; import { Schema } from 'effect'; const workspaceRoot = new URL('../../../../', import.meta.url); diff --git a/app/apps/shell-super-app/tests/unit/auth-config.test.ts b/app/apps/shell-super-app/tests/unit/auth-config.test.ts index 5f2090974..24e2df607 100644 --- a/app/apps/shell-super-app/tests/unit/auth-config.test.ts +++ b/app/apps/shell-super-app/tests/unit/auth-config.test.ts @@ -1,8 +1,10 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { expect, test } from '@rstest/core'; -import { Effect } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Predicate, Schema } from 'effect'; import { parseAuthConfig } from '../../api/auth/config.ts'; -import { parseGatewayIssuerConfig } from '../../api/auth/gateway-issuer-config.ts'; +import { + GatewayIssuerConfigError, + parseGatewayIssuerConfig, +} from '../../api/auth/gateway-issuer-config.ts'; const validEnvironment = { BETTER_AUTH_SECRET: 'a-secure-test-secret-with-more-than-32-characters', @@ -11,42 +13,39 @@ const validEnvironment = { DATABASE_URL: 'postgresql://ontos:ontos@localhost:5433/ontos', }; -test('parses trusted origins and derives local cookie security', async () => - await runEffectTestPromise(parseAuthConfig(validEnvironment)).then((configuration) => { +it.effect('parses trusted origins and derives local cookie security', () => + Effect.gen(function* parsesOrigins() { + const configuration = yield* parseAuthConfig(validEnvironment); expect(configuration.secureCookies).toBe(false); expect(configuration.trustedOrigins).toEqual([ 'http://localhost:3020', 'https://preview.example.test', ]); - })); - -test('requires a strong secret and PostgreSQL URL in the typed error channel', async () => - await Promise.all([ - runEffectTestPromise( - Effect.flip( - parseAuthConfig({ - ...validEnvironment, - BETTER_AUTH_SECRET: 'short', - }), - ), - ), - runEffectTestPromise( - Effect.flip( - parseAuthConfig({ - ...validEnvironment, - DATABASE_URL: 'https://example.test/not-postgres', - }), - ), - ), - ]).then(([secretError, databaseError]) => { - expect(secretError._tag).toBe('AuthConfigError'); - expect(databaseError._tag).toBe('AuthConfigError'); - })); - -test('keeps gateway signing configuration independent from Better Auth configuration', async () => { - const authentication = await runEffectTestPromise(parseAuthConfig(validEnvironment)); - const gatewayError = await runEffectTestPromise(Effect.flip(parseGatewayIssuerConfig({}))); - - expect(authentication.baseUrl).toBe('http://localhost:3020'); - expect(gatewayError._tag).toBe('GatewayIssuerConfigError'); -}); + }), +); +it.effect('requires a strong secret and PostgreSQL URL in the typed error channel', () => + Effect.gen(function* validatesCredentials() { + const [secretError, databaseError] = yield* Effect.all( + [ + Effect.flip(parseAuthConfig({ ...validEnvironment, BETTER_AUTH_SECRET: 'short' })), + Effect.flip( + parseAuthConfig({ + ...validEnvironment, + DATABASE_URL: 'https://example.test/not-postgres', + }), + ), + ], + { concurrency: 'unbounded' }, + ); + expect(Predicate.isTagged(secretError, 'AuthConfigError')).toBe(true); + expect(Predicate.isTagged(databaseError, 'AuthConfigError')).toBe(true); + }), +); +it.effect('keeps gateway signing configuration independent from Better Auth configuration', () => + Effect.gen(function* independentSigning() { + const authentication = yield* parseAuthConfig(validEnvironment); + const gatewayError = yield* Effect.flip(parseGatewayIssuerConfig({})); + expect(authentication.baseUrl).toBe('http://localhost:3020'); + expect(Schema.is(GatewayIssuerConfigError)(gatewayError)).toBe(true); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/auth-contract.test.ts b/app/apps/shell-super-app/tests/unit/auth-contract.test.ts index d94f8d2cd..cb61ec884 100644 --- a/app/apps/shell-super-app/tests/unit/auth-contract.test.ts +++ b/app/apps/shell-super-app/tests/unit/auth-contract.test.ts @@ -1,6 +1,5 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { expect, test } from '@rstest/core'; -import { DateTime, Effect, Schema, SchemaAST } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { DateTime, Effect, Schema, SchemaAST, Predicate, Struct } from 'effect'; import { AuthenticationUnavailableProblemSchema, CurrentSessionSchema, @@ -43,7 +42,7 @@ const problemTag = (schema: Schema.Top): SchemaAST.LiteralValue => { return tag.literal; }; -test('publishes authentication, identity lifecycle, and gateway operations', () => { +it('publishes authentication, identity lifecycle, and gateway operations', () => { const authenticationEndpoints = Object.keys( ShellAuthenticationApi.groups.authentication.endpoints, ).toSorted(); @@ -122,7 +121,7 @@ test('publishes authentication, identity lifecycle, and gateway operations', () ); }); -test('preserves migrated Shell Problem Details wire shapes and ordered membership', () => { +it('preserves migrated Shell Problem Details wire shapes and ordered membership', () => { const unavailable = { _tag: 'AuthenticationUnavailableProblem', detail: 'Authentication is temporarily unavailable. Please retry.', @@ -146,13 +145,19 @@ test('preserves migrated Shell Problem Details wire shapes and ordered membershi title: 'Rate limited', type: 'https://ontos.dev/problems/shell-rate-limited', } as const; - expect(Schema.decodeUnknownSync(AuthenticationUnavailableProblemSchema)(unavailable)).toEqual( + const decodedUnavailable = Schema.decodeUnknownSync(AuthenticationUnavailableProblemSchema)( unavailable, ); - expect(Schema.decodeUnknownSync(TenantCapabilityUnavailableProblemSchema)(retryable)).toEqual( + expect(Schema.is(AuthenticationUnavailableProblemSchema)(decodedUnavailable)).toBe(true); + expect(Struct.omit(decodedUnavailable, ['_tag'])).toEqual(Struct.omit(unavailable, ['_tag'])); + const decodedRetryable = Schema.decodeUnknownSync(TenantCapabilityUnavailableProblemSchema)( retryable, ); - expect(Schema.decodeUnknownSync(ShellRateLimitedProblemSchema)(rateLimited)).toEqual(rateLimited); + expect(Schema.is(TenantCapabilityUnavailableProblemSchema)(decodedRetryable)).toBe(true); + expect(Struct.omit(decodedRetryable, ['_tag'])).toEqual(Struct.omit(retryable, ['_tag'])); + const decodedRateLimited = Schema.decodeUnknownSync(ShellRateLimitedProblemSchema)(rateLimited); + expect(Schema.is(ShellRateLimitedProblemSchema)(decodedRateLimited)).toBe(true); + expect(Struct.omit(decodedRateLimited, ['_tag'])).toEqual(Struct.omit(rateLimited, ['_tag'])); expect(() => Schema.decodeUnknownSync(AuthenticationUnavailableProblemSchema, { onExcessProperty: 'error', @@ -168,86 +173,82 @@ test('preserves migrated Shell Problem Details wire shapes and ordered membershi ]); }); -test('decodes a missing identity idempotency header so handlers can return declared 428', async () => { - await expect( - runEffectTestPromise(Schema.decodeUnknownEffect(IdentityRequestHeadersSchema)({})), - ).resolves.toEqual({}); - await expect( - runEffectTestPromise( - Effect.flip( +it.effect('decodes a missing identity idempotency header so handlers can return declared 428', () => + Effect.gen(function* testProgram1() { + expect(yield* Schema.decodeUnknownEffect(IdentityRequestHeadersSchema)({})).toEqual({}); + expect( + yield* Effect.flip( Schema.decodeUnknownEffect(IdentityRequestHeadersSchema)({ 'idempotency-key': '', }), ), - ), - ).resolves.toBeDefined(); -}); + ).toBeDefined(); + }), +); -test('publishes exact legal-entity endpoints with an ID-only switch payload', async () => { - const { availableLegalEntities, switchLegalEntity } = - ShellAuthenticationApi.groups.legalEntities.endpoints; - expect({ method: availableLegalEntities.method, path: availableLegalEntities.path }).toEqual({ - method: 'GET', - path: '/auth/legal-entities', - }); - expect({ method: switchLegalEntity.method, path: switchLegalEntity.path }).toEqual({ - method: 'POST', - path: '/auth/legal-entity/switch', - }); - const legalEntityId = '35000000-0000-4000-8000-000000000001'; - expect( - await runEffectTestPromise( - Schema.decodeUnknownEffect(SwitchLegalEntityPayloadSchema)({ +it.effect('publishes exact legal-entity endpoints with an ID-only switch payload', () => + Effect.gen(function* testProgram2() { + const { availableLegalEntities, switchLegalEntity } = + ShellAuthenticationApi.groups.legalEntities.endpoints; + expect({ method: availableLegalEntities.method, path: availableLegalEntities.path }).toEqual({ + method: 'GET', + path: '/auth/legal-entities', + }); + expect({ method: switchLegalEntity.method, path: switchLegalEntity.path }).toEqual({ + method: 'POST', + path: '/auth/legal-entity/switch', + }); + const legalEntityId = '35000000-0000-4000-8000-000000000001'; + expect( + yield* Schema.decodeUnknownEffect(SwitchLegalEntityPayloadSchema)({ authorization: 'must-not-pass', legalEntityId, tenantId: 'must-not-pass', }), - ), - ).toEqual({ legalEntityId }); - expect( - await runEffectTestPromise( - Schema.decodeUnknownEffect(AvailableLegalEntitiesResponseSchema)({ + ).toEqual({ legalEntityId }); + expect( + yield* Schema.decodeUnknownEffect(AvailableLegalEntitiesResponseSchema)({ legalEntities: [{ legalEntityId, legalName: 'Alpha', token: 'must-not-pass' }], selectedLegalEntityId: legalEntityId, state: 'authenticated', }), - ), - ).toEqual({ - legalEntities: [{ legalEntityId, legalName: 'Alpha' }], - selectedLegalEntityId: legalEntityId, - state: 'authenticated', - }); -}); + ).toEqual({ + legalEntities: [{ legalEntityId, legalName: 'Alpha' }], + selectedLegalEntityId: legalEntityId, + state: 'authenticated', + }); + }), +); -test('decodes an optional exact page entrypoint without accepting private routing fields', async () => { - expect( - await runEffectTestPromise( - Schema.decodeUnknownEffect(ResolveModuleTargetPayloadSchema)({ - entrypointKey: 'contacts.core.page.customers', - importPath: 'must-not-pass', - moduleId: 'contacts.core', - routePath: '/contacts/customers', - }), - ), - ).toEqual({ entrypointKey: 'contacts.core.page.customers', moduleId: 'contacts.core' }); - expect( - await runEffectTestPromise( - Schema.decodeUnknownEffect(ResolveModuleTargetPayloadSchema)({ moduleId: 'contacts.core' }), - ), - ).toEqual({ moduleId: 'contacts.core' }); - await expect( - runEffectTestPromise( - Effect.flip( - Schema.decodeUnknownEffect(ResolveModuleTargetPayloadSchema)({ - entrypointKey: '../private-page', +it.effect( + 'decodes an optional exact page entrypoint without accepting private routing fields', + () => + Effect.gen(function* testProgram3() { + expect( + yield* Schema.decodeUnknownEffect(ResolveModuleTargetPayloadSchema)({ + entrypointKey: 'contacts.core.page.customers', + importPath: 'must-not-pass', moduleId: 'contacts.core', + routePath: '/contacts/customers', }), - ), - ), - ).resolves.toBeDefined(); -}); + ).toEqual({ entrypointKey: 'contacts.core.page.customers', moduleId: 'contacts.core' }); + expect( + yield* Schema.decodeUnknownEffect(ResolveModuleTargetPayloadSchema)({ + moduleId: 'contacts.core', + }), + ).toEqual({ moduleId: 'contacts.core' }); + expect( + yield* Effect.flip( + Schema.decodeUnknownEffect(ResolveModuleTargetPayloadSchema)({ + entrypointKey: '../private-page', + moduleId: 'contacts.core', + }), + ), + ).toBeDefined(); + }), +); -test('publishes exact tenant methods, paths, and declared failure statuses', () => { +it('publishes exact tenant methods, paths, and declared failure statuses', () => { const { availableTenants, switchTenant } = ShellAuthenticationApi.groups.tenants.endpoints; expect({ method: availableTenants.method, path: availableTenants.path }).toEqual({ method: 'GET', @@ -261,7 +262,7 @@ test('publishes exact tenant methods, paths, and declared failure statuses', () expect(statuses(switchTenant)).toEqual([401, 403, 500, 503]); }); -test('publishes the exhaustive identity failure status contract', () => { +it('publishes the exhaustive identity failure status contract', () => { for (const endpoint of Object.values(ShellAuthenticationApi.groups.identity.endpoints)) { const identityStatuses = [...endpoint.error] .map((schema) => schema.ast.annotations?.['httpApiStatus']) @@ -271,11 +272,11 @@ test('publishes the exhaustive identity failure status contract', () => { } }); -test('validates tenant UUIDs and strips all non-contract fields', async () => { - const tenantId = '30000000-0000-4000-8000-000000000001'; - expect( - await runEffectTestPromise( - Schema.decodeUnknownEffect(AvailableTenantsResponseSchema)({ +it.effect('validates tenant UUIDs and strips all non-contract fields', () => + Effect.gen(function* testProgram4() { + const tenantId = '30000000-0000-4000-8000-000000000001'; + expect( + yield* Schema.decodeUnknownEffect(AvailableTenantsResponseSchema)({ tenants: [ { bindingId: 'must-not-pass', @@ -287,65 +288,59 @@ test('validates tenant UUIDs and strips all non-contract fields', async () => { }, ], }), - ), - ).toEqual({ tenants: [{ name: 'Alpha tenant', tenantId }] }); - expect( - await runEffectTestPromise( - Schema.decodeUnknownEffect(SwitchTenantResponseSchema)({ + ).toEqual({ tenants: [{ name: 'Alpha tenant', tenantId }] }); + expect( + yield* Schema.decodeUnknownEffect(SwitchTenantResponseSchema)({ principalId: 'must-not-pass', selectedTenantId: tenantId, sessionId: 'must-not-pass', }), - ), - ).toEqual({ selectedTenantId: tenantId }); - expect( - await runEffectTestPromise(Schema.decodeUnknownEffect(SwitchTenantPayloadSchema)({ tenantId })), - ).toEqual({ tenantId }); - const invalidPayload = await runEffectTestPromise( - Effect.flip(Schema.decodeUnknownEffect(SwitchTenantPayloadSchema)({ tenantId: 'not-a-uuid' })), - ); - expect(invalidPayload._tag).toBe('SchemaError'); -}); + ).toEqual({ selectedTenantId: tenantId }); + expect(yield* Schema.decodeUnknownEffect(SwitchTenantPayloadSchema)({ tenantId })).toEqual({ + tenantId, + }); + const invalidPayload = yield* Effect.flip( + Schema.decodeUnknownEffect(SwitchTenantPayloadSchema)({ tenantId: 'not-a-uuid' }), + ); + expect(Predicate.isTagged(invalidPayload, 'SchemaError')).toBe(true); + }), +); -test('rejects malformed credentials through Effect Schema', async () => { - const error = await runEffectTestPromise( - Effect.flip( +it.effect('rejects malformed credentials through Effect Schema', () => + Effect.gen(function* testProgram5() { + const error = yield* Effect.flip( Schema.decodeUnknownEffect(SignInPayloadSchema)({ email: '', password: '', }), - ), - ); - expect(error._tag).toBe('SchemaError'); -}); + ); + expect(Predicate.isTagged(error, 'SchemaError')).toBe(true); + }), +); -test('requires lifecycle reasons and strips provider-private API key identifiers', async () => { - const principalId = '00000000-0000-4000-8000-000000000001'; - const authBindingId = '00000000-0000-4000-8000-000000000002'; - const createdAt = '2026-08-09T00:00:00.000Z'; - const missingPrincipalReason = await runEffectTestPromise( - Effect.flip( +it.effect('requires lifecycle reasons and strips provider-private API key identifiers', () => + Effect.gen(function* testProgram6() { + const principalId = '00000000-0000-4000-8000-000000000001'; + const authBindingId = '00000000-0000-4000-8000-000000000002'; + const createdAt = '2026-08-09T00:00:00.000Z'; + const missingPrincipalReason = yield* Effect.flip( Schema.decodeUnknownEffect(ChangePrincipalStatusPayloadSchema)({ expectedStatus: 'active', newStatus: 'disabled', principalId, }), - ), - ); - const missingRevocationReason = await runEffectTestPromise( - Effect.flip( + ); + const missingRevocationReason = yield* Effect.flip( Schema.decodeUnknownEffect(SetApiKeyStatusPayloadSchema)({ authBindingId, expectedStatus: 'active', newStatus: 'revoked', }), - ), - ); - expect(missingPrincipalReason._tag).toBe('SchemaError'); - expect(missingRevocationReason._tag).toBe('SchemaError'); + ); + expect(Predicate.isTagged(missingPrincipalReason, 'SchemaError')).toBe(true); + expect(Predicate.isTagged(missingRevocationReason, 'SchemaError')).toBe(true); - const lifecycle = await runEffectTestPromise( - Schema.decodeUnknownEffect(ApiKeyLifecycleResponseSchema)({ + const lifecycle = yield* Schema.decodeUnknownEffect(ApiKeyLifecycleResponseSchema)({ authBindingId, cleanupPending: false, createdAt, @@ -355,33 +350,31 @@ test('requires lifecycle reasons and strips provider-private API key identifiers name: null, providerKeyId: 'private-provider-key-id', start: 'onto', - }), - ); - expect(lifecycle).toEqual({ - authBindingId, - cleanupPending: false, - createdAt: DateTime.makeUnsafe(createdAt), - enabled: true, - expiresAt: null, - name: null, - start: 'onto', - }); - expect( - await runEffectTestPromise(Schema.encodeEffect(ApiKeyLifecycleResponseSchema)(lifecycle)), - ).toEqual({ - authBindingId, - cleanupPending: false, - createdAt, - enabled: true, - expiresAt: null, - name: null, - start: 'onto', - }); -}); + }); + expect(lifecycle).toEqual({ + authBindingId, + cleanupPending: false, + createdAt: DateTime.makeUnsafe(createdAt), + enabled: true, + expiresAt: null, + name: null, + start: 'onto', + }); + expect(yield* Schema.encodeEffect(ApiKeyLifecycleResponseSchema)(lifecycle)).toEqual({ + authBindingId, + cleanupPending: false, + createdAt, + enabled: true, + expiresAt: null, + name: null, + start: 'onto', + }); + }), +); -test('decodes only safe current-session identity fields', async () => { - const session = await runEffectTestPromise( - Schema.decodeUnknownEffect(CurrentSessionSchema)({ +it.effect('decodes only safe current-session identity fields', () => + Effect.gen(function* testProgram7() { + const session = yield* Schema.decodeUnknownEffect(CurrentSessionSchema)({ identity: { displayName: 'Ada', email: 'ada@example.test', @@ -393,17 +386,17 @@ test('decodes only safe current-session identity fields', async () => { token: 'must-not-pass', }, state: 'authenticated', - }), - ); - expect(session).toEqual({ - identity: { - displayName: 'Ada', - email: 'ada@example.test', - legalEntityId: '35000000-0000-4000-8000-000000000001', - legalName: 'Alpha legal entity', - principalId: 'principal-id', - tenantId: 'tenant-id', - }, - state: 'authenticated', - }); -}); + }); + expect(session).toEqual({ + identity: { + displayName: 'Ada', + email: 'ada@example.test', + legalEntityId: '35000000-0000-4000-8000-000000000001', + legalName: 'Alpha legal entity', + principalId: 'principal-id', + tenantId: 'tenant-id', + }, + state: 'authenticated', + }); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/auth-db-client.test.ts b/app/apps/shell-super-app/tests/unit/auth-db-client.test.ts index ba1d6ce19..367f417cf 100644 --- a/app/apps/shell-super-app/tests/unit/auth-db-client.test.ts +++ b/app/apps/shell-super-app/tests/unit/auth-db-client.test.ts @@ -1,27 +1,27 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { expect, test } from '@rstest/core'; +import { expect, it } from 'effect-rstest'; import { Effect } from 'effect'; import type { PoolResource } from '../../api/auth/db/client.ts'; import { acquirePoolResource } from '../../api/auth/db/client.ts'; -test('ends the pool resource without arguments when its scope closes', async () => { - const recorded: number[] = []; - const fake: PoolResource = { - async end(...args: []) { - recorded.push(args.length); - }, - }; +it.effect('ends the pool resource without arguments when its scope closes', () => + Effect.gen(function* testProgram1() { + const recorded: number[] = []; + const fake: PoolResource = { + end(...args: []) { + recorded.push(args.length); + return Promise.resolve(); + }, + }; - const acquired = await runEffectTestPromise( - Effect.scoped( + const acquired = yield* Effect.scoped( Effect.gen(function* acquireResource() { const resource = yield* acquirePoolResource(() => fake); expect(recorded).toEqual([]); return resource; }), - ), - ); + ); - expect(acquired).toBe(fake); - expect(recorded).toEqual([0]); -}); + expect(acquired).toBe(fake); + expect(recorded).toEqual([0]); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/auth-schema.test.ts b/app/apps/shell-super-app/tests/unit/auth-schema.test.ts index fafb3ee0d..5724c2d17 100644 --- a/app/apps/shell-super-app/tests/unit/auth-schema.test.ts +++ b/app/apps/shell-super-app/tests/unit/auth-schema.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@rstest/core'; +import { expect, test } from 'effect-rstest'; import { getColumns } from 'drizzle-orm'; import { AUTH_SCHEMA_NAME, diff --git a/app/apps/shell-super-app/tests/unit/browser-effect-runtime.test.ts b/app/apps/shell-super-app/tests/unit/browser-effect-runtime.test.ts index 378f5df48..7988b03c7 100644 --- a/app/apps/shell-super-app/tests/unit/browser-effect-runtime.test.ts +++ b/app/apps/shell-super-app/tests/unit/browser-effect-runtime.test.ts @@ -1,30 +1,55 @@ -import { expect, test } from '@rstest/core'; -import { Effect } from 'effect'; -import { runBrowserEffect } from '../../src/runtime/browser-effect-runtime.ts'; - -test('preserves Effect success and failure behavior at the browser boundary', async () => { - const failure = { _tag: 'ExpectedFailure' } as const; - - await expect(runBrowserEffect(Effect.succeed('ready'))).resolves.toBe('ready'); - await expect(runBrowserEffect(Effect.fail(failure))).rejects.toBe(failure); -}); - -test('interrupts the running Effect when its AbortSignal is aborted', async () => { - const controller = new AbortController(); - let finalized = false; - const request = runBrowserEffect( - Effect.never.pipe( - Effect.ensuring( - Effect.sync(() => { - finalized = true; - }), - ), - ), - { signal: controller.signal }, +import { expect, it } from 'effect-rstest'; +import { Deferred, Effect, Exit, Fiber, Schema } from 'effect'; +import { browserRuntime } from '../../src/runtime/browser-effect-runtime.ts'; + +class ExpectedFailure extends Schema.TaggedError()('ExpectedFailure', {}) {} + +/** Forks on the real browser runtime, interrupting on scope close so a failed assertion leaks no fiber. */ +const forkOnBrowserRuntime = ( + program: Effect.Effect, + options?: Effect.RunOptions, +) => + Effect.acquireRelease( + Effect.sync(() => browserRuntime.runFork(program, options)), + (fiber) => Fiber.interrupt(fiber), ); - controller.abort(); +it.effect('carries success values out of the browser runtime', () => + Effect.gen(function* browserRuntimeSuccess() { + const fiber = yield* forkOnBrowserRuntime(Effect.succeed('ready')); + + expect(yield* Fiber.join(fiber)).toBe('ready'); + }), +); + +it.effect('keeps the typed failure identity of a browser runtime program', () => + Effect.gen(function* browserRuntimeTypedFailure() { + const failure = new ExpectedFailure(); + + const fiber = yield* forkOnBrowserRuntime(Effect.fail(failure)); + + expect(yield* Effect.flip(Fiber.join(fiber))).toBe(failure); + }), +); + +it.effect('interrupts the running Effect when its AbortSignal is aborted', () => + Effect.gen(function* browserRuntimeInterruption() { + const controller = new AbortController(); + const finalized: string[] = []; + const finalizersInstalled = yield* Deferred.make<'installed'>(); + const fiber = yield* forkOnBrowserRuntime( + Effect.andThen(Deferred.succeed(finalizersInstalled, 'installed'), Effect.never).pipe( + Effect.ensuring(Effect.sync(() => finalized.push('inner'))), + Effect.ensuring(Effect.sync(() => finalized.push('outer'))), + ), + { signal: controller.signal }, + ); + yield* Deferred.await(finalizersInstalled); + + controller.abort(); + const exit = yield* Fiber.await(fiber); - await expect(request).rejects.toBeTruthy(); - expect(finalized).toBe(true); -}); + expect(Exit.hasInterrupts(exit)).toBe(true); + expect(finalized).toEqual(['inner', 'outer']); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/configuration-provider.test.ts b/app/apps/shell-super-app/tests/unit/configuration-provider.test.ts index 46dac9d52..5a7f09ebf 100644 --- a/app/apps/shell-super-app/tests/unit/configuration-provider.test.ts +++ b/app/apps/shell-super-app/tests/unit/configuration-provider.test.ts @@ -1,5 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { expect, rs, test } from '@rstest/core'; +import { expect, it, rs } from 'effect-rstest'; import { Config, ConfigProvider, Effect } from 'effect'; import { loadAuthConfig } from '../../api/auth/config.ts'; import { loadGatewayIssuerConfig } from '../../api/auth/gateway-issuer-config.ts'; @@ -10,23 +9,23 @@ rs.mock('../../api/auth/environment-file-provider.ts', () => ({ loadEnvironmentFileProvider: rs.fn(() => Effect.succeed(ConfigProvider.fromEnvRecord({}))), })); -test('explicit environment overrides file values and absent keys fall back to the file', async () => { - rs.mocked(loadEnvironmentFileProvider).mockReturnValue( - Effect.succeed(ConfigProvider.fromEnvRecord({ SECRET: 'file-secret', URL: 'file-url' })), - ); - const result = await runEffectTestPromise( - loadConfigurationProvider( +it.effect('explicit environment overrides file values and absent keys fall back to the file', () => + Effect.gen(function* explicitEnvironment() { + rs.mocked(loadEnvironmentFileProvider).mockReturnValue( + Effect.succeed(ConfigProvider.fromEnvRecord({ SECRET: 'file-secret', URL: 'file-url' })), + ); + const result = yield* loadConfigurationProvider( { environment: { URL: 'explicit-url' }, envPath: 'fixture-path' }, () => 'unreadable', ).pipe( Effect.flatMap((provider) => Config.all({ secret: Config.string('SECRET'), url: Config.string('URL') }).parse(provider), ), - ), - ); - expect(result).toEqual({ secret: 'file-secret', url: 'explicit-url' }); - expect(loadEnvironmentFileProvider).toHaveBeenCalledWith('fixture-path', expect.any(Function)); -}); + ); + expect(result).toEqual({ secret: 'file-secret', url: 'explicit-url' }); + expect(loadEnvironmentFileProvider).toHaveBeenCalledWith('fixture-path', expect.any(Function)); + }), +); const read = ( options: { readonly environment?: Readonly>> } = {}, @@ -35,27 +34,28 @@ const read = ( Effect.flatMap((provider) => Config.string('ONTOS_PROVIDER_TEST').parse(provider)), ); -test('an explicit empty environment does not fall through to process values', async () => { - rs.stubEnv('ONTOS_PROVIDER_TEST', 'process-value'); - rs.mocked(loadEnvironmentFileProvider).mockReturnValue( - Effect.succeed(ConfigProvider.fromEnvRecord({ ONTOS_PROVIDER_TEST: 'file-value' })), - ); - try { - expect(await runEffectTestPromise(read())).toBe('process-value'); - expect(await runEffectTestPromise(read({ environment: {} }))).toBe('file-value'); - } finally { - rs.unstubAllEnvs(); - } -}); +it.effect('an explicit empty environment does not fall through to process values', () => + Effect.gen(function* emptyEnvironment() { + yield* Effect.acquireRelease( + Effect.sync(() => rs.stubEnv('ONTOS_PROVIDER_TEST', 'process-value')), + () => Effect.sync(() => rs.unstubAllEnvs()), + ); + rs.mocked(loadEnvironmentFileProvider).mockReturnValue( + Effect.succeed(ConfigProvider.fromEnvRecord({ ONTOS_PROVIDER_TEST: 'file-value' })), + ); + expect(yield* read()).toBe('process-value'); + expect(yield* read({ environment: {} })).toBe('file-value'); + }), +); -test('file loading failures retain the parser-specific typed error and safe reason', async () => { - rs.mocked(loadEnvironmentFileProvider).mockImplementation((_path, failure) => - Effect.fail(failure()), - ); - const authFailure = await runEffectTestPromise(Effect.flip(loadAuthConfig({ environment: {} }))); - const gatewayFailure = await runEffectTestPromise( - Effect.flip(loadGatewayIssuerConfig({ environment: {} })), - ); - expect(authFailure.reason).toBe('Unable to load the root authentication environment'); - expect(gatewayFailure.reason).toBe('Unable to load the Shell gateway signing environment'); -}); +it.effect('file loading failures retain the parser-specific typed error and safe reason', () => + Effect.gen(function* fileFailure() { + rs.mocked(loadEnvironmentFileProvider).mockImplementation((_path, failure) => + Effect.fail(failure()), + ); + const authFailure = yield* Effect.flip(loadAuthConfig({ environment: {} })); + const gatewayFailure = yield* Effect.flip(loadGatewayIssuerConfig({ environment: {} })); + expect(authFailure.reason).toBe('Unable to load the root authentication environment'); + expect(gatewayFailure.reason).toBe('Unable to load the Shell gateway signing environment'); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/deployment-allowlist.test.ts b/app/apps/shell-super-app/tests/unit/deployment-allowlist.test.ts index 48557f470..d99a2f0ee 100644 --- a/app/apps/shell-super-app/tests/unit/deployment-allowlist.test.ts +++ b/app/apps/shell-super-app/tests/unit/deployment-allowlist.test.ts @@ -1,5 +1,6 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { expect, test } from '@rstest/core'; +import { Effect } from 'effect'; + +import { expect, it } from 'effect-rstest'; import { deriveDeploymentAllowlist } from '../../api/modules/deployment-allowlist.ts'; import { createModuleDeploymentAllowlistBuildInput } from '../../module-deployment-allowlist.config.ts'; @@ -24,23 +25,23 @@ const validUrls = { 'property-registry': 'http://127.0.0.1:4101/.well-known/ontos-module-manifest.json', }; -test('derives an immutable, topology-authorized and deterministically ordered allowlist', async () => { - const allowlist = await runEffectTestPromise( - deriveDeploymentAllowlist({ +it.effect('derives an immutable, topology-authorized and deterministically ordered allowlist', () => + Effect.gen(function* testProgram1() { + const allowlist = yield* deriveDeploymentAllowlist({ environment: 'development', overlay: overlay(validUrls), topology, - }), - ); - expect(allowlist.entries.map(({ appId }) => appId)).toEqual([ - 'documents-center', - 'property-registry', - ]); - expect(Object.isFrozen(allowlist)).toBe(true); - expect(Object.isFrozen(allowlist.entries)).toBe(true); -}); + }); + expect(allowlist.entries.map(({ appId }) => appId)).toEqual([ + 'documents-center', + 'property-registry', + ]); + expect(Object.isFrozen(allowlist)).toBe(true); + expect(Object.isFrozen(allowlist.entries)).toBe(true); + }), +); -test.each([ +it.effect.each([ ['missing topology entry', { 'property-registry': validUrls['property-registry'] }], ['unknown shell entry', { ...validUrls, 'shell-super-app': validUrls['property-registry'] }], [ @@ -57,50 +58,52 @@ test.each([ ], ['fragment', { ...validUrls, 'property-registry': `${validUrls['property-registry']}#private` }], ['arbitrary path', { ...validUrls, 'property-registry': 'http://localhost:4101/private.json' }], -])('rejects %s configuration without authorizing a fetch', async (_label, manifests) => { - await expect( - runEffectTestPromise( - deriveDeploymentAllowlist({ - environment: 'development', - overlay: overlay(manifests), - topology, - }), - ), - ).rejects.toMatchObject({ code: 'deployment_allowlist_invalid' }); -}); +] as const)('rejects %s configuration without authorizing a fetch', ([_label, manifests]) => + Effect.gen(function* testProgram2() { + expect( + yield* Effect.flip( + deriveDeploymentAllowlist({ + environment: 'development', + overlay: overlay(manifests), + topology, + }), + ), + ).toMatchObject({ code: 'deployment_allowlist_invalid' }); + }), +); -test('requires HTTPS outside loopback development', async () => { - const productionUrls = { - 'documents-center': 'https://documents.example.test/.well-known/ontos-module-manifest.json', - 'property-registry': 'https://property.example.test/.well-known/ontos-module-manifest.json', - }; - await expect( - runEffectTestPromise( - deriveDeploymentAllowlist({ - environment: 'production', - overlay: overlay( - { - ...productionUrls, - 'property-registry': validUrls['property-registry'], - }, - 'production', - ), - topology, - }), - ), - ).rejects.toMatchObject({ code: 'deployment_allowlist_invalid' }); - await expect( - runEffectTestPromise( - deriveDeploymentAllowlist({ +it.effect('requires HTTPS outside loopback development', () => + Effect.gen(function* testProgram3() { + const productionUrls = { + 'documents-center': 'https://documents.example.test/.well-known/ontos-module-manifest.json', + 'property-registry': 'https://property.example.test/.well-known/ontos-module-manifest.json', + }; + expect( + yield* Effect.flip( + deriveDeploymentAllowlist({ + environment: 'production', + overlay: overlay( + { + ...productionUrls, + 'property-registry': validUrls['property-registry'], + }, + 'production', + ), + topology, + }), + ), + ).toMatchObject({ code: 'deployment_allowlist_invalid' }); + expect( + yield* deriveDeploymentAllowlist({ environment: 'production', overlay: overlay(productionUrls, 'production'), topology, }), - ), - ).resolves.toMatchObject({ entries: expect.any(Array) }); -}); + ).toMatchObject({ entries: expect.any(Array) }); + }), +); -test('builds production discovery from deployment URL configuration, never the development overlay', () => { +it('builds production discovery from deployment URL configuration, never the development overlay', () => { const productionTopology = { verticals: [ { diff --git a/app/apps/shell-super-app/tests/unit/gateway-issuer.test.ts b/app/apps/shell-super-app/tests/unit/gateway-issuer.test.ts index b43e3913e..68429e33e 100644 --- a/app/apps/shell-super-app/tests/unit/gateway-issuer.test.ts +++ b/app/apps/shell-super-app/tests/unit/gateway-issuer.test.ts @@ -1,5 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { expect, rs, test } from '@rstest/core'; +import { expect, rs, it } from 'effect-rstest'; import { Effect, Exit, Fiber, Predicate } from 'effect'; import { TestClock } from 'effect/testing'; import { decodeJwt, decodeProtectedHeader, exportJWK, generateKeyPair, jwtVerify } from 'jose'; @@ -36,31 +35,34 @@ const principal = { tenantId: '50000000-0000-4000-8000-000000000001', }; -const makeConfiguration = async (): Promise<{ +const makeConfiguration = (): Effect.Effect<{ readonly configuration: GatewayIssuerConfigValue; readonly publicKey: CryptoKey; -}> => { - const { privateKey, publicKey } = await generateKeyPair('EdDSA', { - crv: 'Ed25519', - extractable: true, - }); - const privateJwk = await exportJWK(privateKey); - return { - configuration: { - issuer, - privateJwk: { - alg: 'EdDSA', +}> => + Effect.gen(function* testProgram1() { + const { privateKey, publicKey } = yield* Effect.promise(() => + generateKeyPair('EdDSA', { crv: 'Ed25519', - d: privateJwk.d ?? '', - kid: 'current-2026-08', - kty: 'OKP', - use: 'sig', - x: privateJwk.x ?? '', + extractable: true, + }), + ); + const privateJwk = yield* Effect.promise(() => exportJWK(privateKey)); + return { + configuration: { + issuer, + privateJwk: { + alg: 'EdDSA', + crv: 'Ed25519', + d: privateJwk.d ?? '', + kid: 'current-2026-08', + kty: 'OKP', + use: 'sig', + x: privateJwk.x ?? '', + }, }, - }, - publicKey, - }; -}; + publicKey, + }; + }); const dependencies = ( configuration: GatewayIssuerConfigValue, @@ -81,154 +83,162 @@ const issueGatewayContextAssertionWith = ( options: GatewayIssuerLayerOptions, ) => issueGatewayContextAssertion(input).pipe(Effect.provide(makeGatewayIssuerLayer(options))); -test('memoises configuration within the refresh window and issues signed assertions', async () => { - const { configuration, publicKey } = await makeConfiguration(); - let configurationLoads = 0; - const layer = makeGatewayIssuerLayer( - dependencies(configuration, { - loadConfig: Effect.sync(() => { - configurationLoads += 1; - return configuration; +it.effect('memoises configuration within the refresh window and issues signed assertions', () => + Effect.gen(function* testProgram2() { + const { configuration, publicKey } = yield* makeConfiguration(); + let configurationLoads = 0; + const layer = makeGatewayIssuerLayer( + dependencies(configuration, { + loadConfig: Effect.sync(() => { + configurationLoads += 1; + return configuration; + }), }), - }), - ); - const [result] = await runEffectTestPromise( - Effect.all( + ); + const [result] = yield* Effect.all( [ issueGatewayContextAssertion({ audience: 'property-registry', principal }), issueGatewayContextAssertion({ audience: 'property-registry', principal }), ], { concurrency: 2 }, - ).pipe(Effect.provide(layer)), - ); - const header = decodeProtectedHeader(result.token); - const claims = decodeJwt(result.token); - const verified = await jwtVerify(result.token, publicKey, { - algorithms: ['EdDSA'], - audience: 'property-registry', - currentDate: new Date(1_700_000_001_000), - issuer, - }); + ).pipe(Effect.provide(layer)); + const header = decodeProtectedHeader(result.token); + const claims = decodeJwt(result.token); + const verified = yield* Effect.promise(() => + jwtVerify(result.token, publicKey, { + algorithms: ['EdDSA'], + audience: 'property-registry', + currentDate: new Date(1_700_000_001_000), + issuer, + }), + ); - expect(result.expiresAt).toBe(1_700_000_300); - expect(header).toEqual({ alg: 'EdDSA', kid: 'current-2026-08', typ: 'JWT' }); - expect(claims).toEqual({ - aud: 'property-registry', - exp: 1_700_000_300, - iat: 1_700_000_000, - iss: issuer, - jti: '60000000-0000-4000-8000-000000000001', - principal, - sub: principal.principalId, - ver: 1, - }); - expect(verified.payload['principal']).toEqual(principal); - expect(JSON.stringify(claims)).not.toMatch( - /email|displayName|credential|cookie|sessionToken|actionKey|permission|policy|businessPayload/u, - ); - expect(configurationLoads).toBe(1); -}); + expect(result.expiresAt).toBe(1_700_000_300); + expect(header).toEqual({ alg: 'EdDSA', kid: 'current-2026-08', typ: 'JWT' }); + expect(claims).toEqual({ + aud: 'property-registry', + exp: 1_700_000_300, + iat: 1_700_000_000, + iss: issuer, + jti: '60000000-0000-4000-8000-000000000001', + principal, + sub: principal.principalId, + ver: 1, + }); + expect(verified.payload['principal']).toEqual(principal); + expect(JSON.stringify(claims)).not.toMatch( + /email|displayName|credential|cookie|sessionToken|actionKey|permission|policy|businessPayload/u, + ); + expect(configurationLoads).toBe(1); + }), +); -test('shares cached configuration across concurrent valid issuances', async () => { - const { configuration, publicKey } = await makeConfiguration(); - let loadConfigCount = 0; - const layer = makeGatewayIssuerLayer( - dependencies(configuration, { - loadConfig: Effect.sync(() => { - loadConfigCount += 1; - return configuration; +it.effect('shares cached configuration across concurrent valid issuances', () => + Effect.gen(function* testProgram3() { + const { configuration, publicKey } = yield* makeConfiguration(); + let loadConfigCount = 0; + const layer = makeGatewayIssuerLayer( + dependencies(configuration, { + loadConfig: Effect.sync(() => { + loadConfigCount += 1; + return configuration; + }), + }), + ); + yield* Effect.addFinalizer(() => + Effect.sync(() => { + rs.restoreAllMocks(); }), - }), - ); - const importKey = rs.spyOn(globalThis.crypto.subtle, 'importKey'); - let results: readonly { readonly token: string }[]; - try { - results = await runEffectTestPromise( - Effect.all( - Array.from({ length: 8 }, () => - issueGatewayContextAssertion({ audience: 'property-registry', principal }), - ), - { concurrency: 8 }, - ).pipe(Effect.provide(layer)), ); + const importKey = rs.spyOn(globalThis.crypto.subtle, 'importKey'); + const results = yield* Effect.all( + Array.from({ length: 8 }, () => + issueGatewayContextAssertion({ audience: 'property-registry', principal }), + ), + { concurrency: 8 }, + ).pipe(Effect.provide(layer)); // The signing key is imported once and shared: the slot serialises concurrent first callers. expect(importKey).toHaveBeenCalledTimes(1); - } finally { - importKey.mockRestore(); - } - expect(results.length).toBe(8); - expect(loadConfigCount).toBe(1); - await Promise.all( - results.map(async (result) => { - const verified = await jwtVerify(result.token, publicKey, { + expect(results.length).toBe(8); + expect(loadConfigCount).toBe(1); + yield* Effect.all( + results.map((result) => + Effect.gen(function* testProgram4() { + const verified = yield* Effect.promise(() => + jwtVerify(result.token, publicKey, { + algorithms: ['EdDSA'], + audience: 'property-registry', + currentDate: new Date(1_700_000_001_000), + issuer, + }), + ); + expect(verified.payload['principal']).toEqual(principal); + }), + ), + { concurrency: 'unbounded' }, + ); + }), +); + +it.effect('allows the next issuance after interrupting a pending key import', () => + Effect.gen(function* testProgram5() { + const { configuration, publicKey } = yield* makeConfiguration(); + const started = Promise.withResolvers(); + const blocked = Promise.withResolvers(); + yield* Effect.addFinalizer(() => + Effect.sync(() => { + blocked.resolve(publicKey); + rs.restoreAllMocks(); + }), + ); + rs.spyOn(globalThis.crypto.subtle, 'importKey').mockImplementationOnce(() => { + started.resolve(true); + return blocked.promise; + }); + const result = yield* Effect.gen(function* interruptedImport() { + const gatewayIssuer = yield* GatewayIssuer; + const first = yield* gatewayIssuer + .issue({ audience: 'property-registry', principal }) + .pipe(Effect.forkChild); + yield* Effect.promise(() => started.promise); + yield* Fiber.interrupt(first); + expect(Exit.isFailure(yield* Fiber.await(first))).toBe(true); + return yield* gatewayIssuer.issue({ audience: 'property-registry', principal }); + }).pipe(Effect.provide(makeGatewayIssuerLayer(dependencies(configuration)))); + yield* Effect.promise(() => + jwtVerify(result.token, publicKey, { algorithms: ['EdDSA'], audience: 'property-registry', currentDate: new Date(1_700_000_001_000), issuer, - }); - expect(verified.payload['principal']).toEqual(principal); - }), - ); -}); - -test('allows the next issuance after interrupting a pending key import', async () => { - const { configuration, publicKey } = await makeConfiguration(); - const started = Promise.withResolvers(); - const blocked = Promise.withResolvers(); - const importKey = rs - .spyOn(globalThis.crypto.subtle, 'importKey') - .mockImplementationOnce(async () => { - started.resolve(true); - return await blocked.promise; - }); - try { - const result = await runEffectTestPromise( - Effect.gen(function* interruptedImport() { - const gatewayIssuer = yield* GatewayIssuer; - const first = yield* gatewayIssuer - .issue({ audience: 'property-registry', principal }) - .pipe(Effect.forkChild); - yield* Effect.promise(async () => await started.promise); - yield* Fiber.interrupt(first); - expect(Exit.isFailure(yield* Fiber.await(first))).toBe(true); - return yield* gatewayIssuer.issue({ audience: 'property-registry', principal }); - }).pipe(Effect.provide(makeGatewayIssuerLayer(dependencies(configuration)))), + }), ); - await jwtVerify(result.token, publicKey, { - algorithms: ['EdDSA'], - audience: 'property-registry', - currentDate: new Date(1_700_000_001_000), - issuer, - }); - } finally { - blocked.resolve(publicKey); - importKey.mockRestore(); - } -}); + }), +); -test('refreshes configuration after 30 seconds and replaces the rotated signing key', async () => { - const { configuration: initialConfiguration, publicKey: initialPublicKey } = - await makeConfiguration(); - const { configuration: generatedRotatedConfiguration, publicKey: rotatedPublicKey } = - await makeConfiguration(); - const rotatedConfiguration = { - ...generatedRotatedConfiguration, - privateJwk: { - ...generatedRotatedConfiguration.privateJwk, - kid: 'rotated-2026-09', - }, - }; - let loadConfigCount = 0; - const layer = makeGatewayIssuerLayer( - dependencies(initialConfiguration, { - loadConfig: Effect.sync(() => { - loadConfigCount += 1; - return loadConfigCount === 1 ? initialConfiguration : rotatedConfiguration; +it.effect('refreshes configuration after 30 seconds and replaces the rotated signing key', () => + Effect.gen(function* testProgram6() { + const { configuration: initialConfiguration, publicKey: initialPublicKey } = + yield* makeConfiguration(); + const { configuration: generatedRotatedConfiguration, publicKey: rotatedPublicKey } = + yield* makeConfiguration(); + const rotatedConfiguration = { + ...generatedRotatedConfiguration, + privateJwk: { + ...generatedRotatedConfiguration.privateJwk, + kid: 'rotated-2026-09', + }, + }; + let loadConfigCount = 0; + const layer = makeGatewayIssuerLayer( + dependencies(initialConfiguration, { + loadConfig: Effect.sync(() => { + loadConfigCount += 1; + return loadConfigCount === 1 ? initialConfiguration : rotatedConfiguration; + }), }), - }), - ); - const [initialResult, rotatedResult] = await runEffectTestPromise( - Effect.gen(function* gatewayRotationSequence() { + ); + const [initialResult, rotatedResult] = yield* Effect.gen(function* gatewayRotationSequence() { const initial = yield* issueGatewayContextAssertion({ audience: 'property-registry', principal, @@ -245,42 +255,48 @@ test('refreshes configuration after 30 seconds and replaces the rotated signing principal, }); return [initial, rotated] as const; - }).pipe(Effect.provide(layer), Effect.provide(TestClock.layer())), - ); - const initialHeader = decodeProtectedHeader(initialResult.token); - const rotatedHeader = decodeProtectedHeader(rotatedResult.token); + }).pipe(Effect.provide(layer), Effect.provide(TestClock.layer())); + const initialHeader = decodeProtectedHeader(initialResult.token); + const rotatedHeader = decodeProtectedHeader(rotatedResult.token); - await jwtVerify(initialResult.token, initialPublicKey, { - algorithms: ['EdDSA'], - audience: 'property-registry', - currentDate: new Date(1_700_000_001_000), - issuer, - }); - await jwtVerify(rotatedResult.token, rotatedPublicKey, { - algorithms: ['EdDSA'], - audience: 'property-registry', - currentDate: new Date(1_700_000_001_000), - issuer, - }); + yield* Effect.promise(() => + jwtVerify(initialResult.token, initialPublicKey, { + algorithms: ['EdDSA'], + audience: 'property-registry', + currentDate: new Date(1_700_000_001_000), + issuer, + }), + ); + yield* Effect.promise(() => + jwtVerify(rotatedResult.token, rotatedPublicKey, { + algorithms: ['EdDSA'], + audience: 'property-registry', + currentDate: new Date(1_700_000_001_000), + issuer, + }), + ); - expect(loadConfigCount).toBe(2); - expect(rotatedHeader.kid).toBe(rotatedConfiguration.privateJwk.kid); - expect(rotatedHeader.kid).not.toBe(initialHeader.kid); -}); + expect(loadConfigCount).toBe(2); + expect(rotatedHeader.kid).toBe(rotatedConfiguration.privateJwk.kid); + expect(rotatedHeader.kid).not.toBe(initialHeader.kid); + }), +); -test('does not cache configuration failures', async () => { - const { configuration } = await makeConfiguration(); - let loadConfigCount = 0; - const layer = makeGatewayIssuerLayer( - dependencies(configuration, { - loadConfig: Effect.suspend(() => { - loadConfigCount += 1; - return loadConfigCount === 1 ? parseGatewayIssuerConfig({}) : Effect.succeed(configuration); +it.effect('does not cache configuration failures', () => + Effect.gen(function* testProgram7() { + const { configuration } = yield* makeConfiguration(); + let loadConfigCount = 0; + const layer = makeGatewayIssuerLayer( + dependencies(configuration, { + loadConfig: Effect.suspend(() => { + loadConfigCount += 1; + return loadConfigCount === 1 + ? parseGatewayIssuerConfig({}) + : Effect.succeed(configuration); + }), }), - }), - ); - const [configurationError, result] = await runEffectTestPromise( - Effect.gen(function* gatewayFailureSequence() { + ); + const [configurationError, result] = yield* Effect.gen(function* gatewayFailureSequence() { const configurationFailure = yield* Effect.flip( issueGatewayContextAssertion({ audience: 'property-registry', principal }), ); @@ -289,80 +305,88 @@ test('does not cache configuration failures', async () => { principal, }); return [configurationFailure, issuedResult] as const; - }).pipe(Effect.provide(layer)), - ); - expect(configurationError.stage).toBe('configuration'); - expect(result.token.length).toBeGreaterThan(0); - expect(loadConfigCount).toBe(2); -}); + }).pipe(Effect.provide(layer)); + expect(configurationError.stage).toBe('configuration'); + expect(result.token.length).toBeGreaterThan(0); + expect(loadConfigCount).toBe(2); + }), +); -test('retries a failed key import on the next issuance', async () => { - const { configuration, publicKey } = await makeConfiguration(); - const importKey = rs - .spyOn(globalThis.crypto.subtle, 'importKey') - .mockRejectedValueOnce(new Error('transient import failure')); - try { - const [error, result] = await runEffectTestPromise( - Effect.gen(function* retryImport() { - const failed = yield* Effect.flip( - issueGatewayContextAssertion({ audience: 'property-registry', principal }), - ); - const issued = yield* issueGatewayContextAssertion({ - audience: 'property-registry', - principal, - }); - return [failed, issued] as const; - }).pipe(Effect.provide(makeGatewayIssuerLayer(dependencies(configuration)))), +it.effect('retries a failed key import on the next issuance', () => + Effect.gen(function* testProgram8() { + const { configuration, publicKey } = yield* makeConfiguration(); + yield* Effect.addFinalizer(() => + Effect.sync(() => { + rs.restoreAllMocks(); + }), ); + rs.spyOn(globalThis.crypto.subtle, 'importKey').mockRejectedValueOnce( + new Error('transient import failure'), + ); + const [error, result] = yield* Effect.gen(function* retryImport() { + const failed = yield* Effect.flip( + issueGatewayContextAssertion({ audience: 'property-registry', principal }), + ); + const issued = yield* issueGatewayContextAssertion({ + audience: 'property-registry', + principal, + }); + return [failed, issued] as const; + }).pipe(Effect.provide(makeGatewayIssuerLayer(dependencies(configuration)))); expect(error.stage).toBe('signing'); - await jwtVerify(result.token, publicKey, { - algorithms: ['EdDSA'], - audience: 'property-registry', - currentDate: new Date(1_700_000_001_000), - issuer, - }); - } finally { - importKey.mockRestore(); - } -}); + yield* Effect.promise(() => + jwtVerify(result.token, publicKey, { + algorithms: ['EdDSA'], + audience: 'property-registry', + currentDate: new Date(1_700_000_001_000), + issuer, + }), + ); + }), +); -test('fails closed for unknown audiences and invalid Effect-managed time', async () => { - const { configuration } = await makeConfiguration(); - const audienceErrors = await Promise.all( - [ - Effect.flip( - issueGatewayContextAssertionWith( - { audience: 'billing', principal }, - dependencies(configuration), +it.effect('fails closed for unknown audiences and invalid Effect-managed time', () => + Effect.gen(function* testProgram9() { + const { configuration } = yield* makeConfiguration(); + const audienceErrors = yield* Effect.all( + [ + Effect.flip( + issueGatewayContextAssertionWith( + { audience: 'billing', principal }, + dependencies(configuration), + ), ), - ), - Effect.flip( - issueGatewayContextAssertionWith( - { audience: 'property.registry', principal }, - dependencies(configuration), + Effect.flip( + issueGatewayContextAssertionWith( + { audience: 'property.registry', principal }, + dependencies(configuration), + ), ), + ].map((effect) => + Effect.gen(function* testProgram10() { + return yield* effect; + }), ), - ].map(async (effect) => await runEffectTestPromise(effect)), - ); - const timeError = await runEffectTestPromise( - Effect.flip( + { concurrency: 'unbounded' }, + ); + const timeError = yield* Effect.flip( issueGatewayContextAssertionWith( { audience: 'property-registry', principal }, dependencies(configuration, { currentTimeSeconds: Effect.succeed(-1) }), ), - ), - ); + ); - expect(audienceErrors.every((error) => error.code === 'gateway_audience_invalid')).toBe(true); - expect(audienceErrors.every((error) => error.stage === 'audience')).toBe(true); - expect(timeError.code).toBe('gateway_issuer_unavailable'); - expect(timeError.stage).toBe('clock'); -}); + expect(audienceErrors.every((error) => error.code === 'gateway_audience_invalid')).toBe(true); + expect(audienceErrors.every((error) => error.stage === 'audience')).toBe(true); + expect(timeError.code).toBe('gateway_issuer_unavailable'); + expect(timeError.stage).toBe('clock'); + }), +); -test('rejects transport correlation or any other excess principal claim', async () => { - const { configuration } = await makeConfiguration(); - const error = await runEffectTestPromise( - Effect.flip( +it.effect('rejects transport correlation or any other excess principal claim', () => + Effect.gen(function* testProgram11() { + const { configuration } = yield* makeConfiguration(); + const error = yield* Effect.flip( issueGatewayContextAssertionWith( { audience: 'property-registry', @@ -370,26 +394,24 @@ test('rejects transport correlation or any other excess principal claim', async }, dependencies(configuration), ), - ), - ); - expect(error.code).toBe('gateway_issuer_unavailable'); - expect(error.stage).toBe('principal'); -}); + ); + expect(error.code).toBe('gateway_issuer_unavailable'); + expect(error.stage).toBe('principal'); + }), +); -test('identifies configuration and signing failures without exposing key material', async () => { - const { configuration } = await makeConfiguration(); - const configurationError = await runEffectTestPromise( - Effect.flip( +it.effect('identifies configuration and signing failures without exposing key material', () => + Effect.gen(function* testProgram12() { + const { configuration } = yield* makeConfiguration(); + const configurationError = yield* Effect.flip( issueGatewayContextAssertionWith( { audience: 'property-registry', principal }, dependencies(configuration, { loadConfig: parseGatewayIssuerConfig({}), }), ), - ), - ); - const signingError = await runEffectTestPromise( - Effect.flip( + ); + const signingError = yield* Effect.flip( issueGatewayContextAssertionWith( { audience: 'property-registry', principal }, dependencies({ @@ -397,36 +419,36 @@ test('identifies configuration and signing failures without exposing key materia privateJwk: { ...configuration.privateJwk, d: 'invalid' }, }), ), - ), - ); + ); - expect(configurationError.stage).toBe('configuration'); - expect(signingError.stage).toBe('signing'); - expect(configurationError.reason).not.toContain('ONTOS_GATEWAY_PRIVATE_JWK'); - expect(signingError.reason).not.toContain(configuration.privateJwk.d); -}); + expect(configurationError.stage).toBe('configuration'); + expect(signingError.stage).toBe('signing'); + expect(configurationError.reason).not.toContain('ONTOS_GATEWAY_PRIVATE_JWK'); + expect(signingError.reason).not.toContain(configuration.privateJwk.d); + }), +); -test('rejects missing configuration, HMAC keys, non-Ed25519 keys, and missing key IDs', async () => { - const invalidJwks = [ - undefined, - { alg: 'HS256', d: 'secret', kid: 'hmac', kty: 'oct', use: 'sig', x: 'secret' }, - { - alg: 'EdDSA', - crv: 'X25519', - d: 'private', - kid: 'wrong-curve', - kty: 'OKP', - use: 'sig', - x: 'public', - }, - { alg: 'EdDSA', crv: 'Ed25519', d: 'private', kty: 'OKP', use: 'sig', x: 'public' }, - ]; +it.effect('rejects missing configuration, HMAC keys, non-Ed25519 keys, and missing key IDs', () => + Effect.gen(function* testProgram13() { + const invalidJwks = [ + undefined, + { alg: 'HS256', d: 'secret', kid: 'hmac', kty: 'oct', use: 'sig', x: 'secret' }, + { + alg: 'EdDSA', + crv: 'X25519', + d: 'private', + kid: 'wrong-curve', + kty: 'OKP', + use: 'sig', + x: 'public', + }, + { alg: 'EdDSA', crv: 'Ed25519', d: 'private', kty: 'OKP', use: 'sig', x: 'public' }, + ]; - const errors = await Promise.all( - invalidJwks.map( - async (privateJwk) => - await runEffectTestPromise( - Effect.flip( + const errors = yield* Effect.all( + invalidJwks.map((privateJwk) => + Effect.gen(function* testProgram14() { + return yield* Effect.flip( parseGatewayIssuerConfig( withOptionalProperty( { @@ -438,9 +460,13 @@ test('rejects missing configuration, HMAC keys, non-Ed25519 keys, and missing ke {}, ), ), - ), - ), - ), - ); - expect(errors.every((error) => Predicate.isTagged(error, 'GatewayIssuerConfigError'))).toBe(true); -}); + ); + }), + ), + { concurrency: 'unbounded' }, + ); + expect(errors.every((error) => Predicate.isTagged(error, 'GatewayIssuerConfigError'))).toBe( + true, + ); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/identity-lifecycle.test.ts b/app/apps/shell-super-app/tests/unit/identity-lifecycle.test.ts index 4e6587352..fc8ea0f66 100644 --- a/app/apps/shell-super-app/tests/unit/identity-lifecycle.test.ts +++ b/app/apps/shell-super-app/tests/unit/identity-lifecycle.test.ts @@ -1,12 +1,10 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import { expect, test } from '@rstest/core'; +import { expect, it } from 'effect-rstest'; import { ActionTransactionError, IdentityTargetInvalidError, PrincipalBindingMissingError, } from '@app/core-runtime'; -import { Effect, Redacted } from 'effect'; +import { Effect, Redacted, Predicate } from 'effect'; import { ApiKeyProviderUnavailableError, ApiKeyStateInconsistentError, @@ -58,53 +56,53 @@ const pendingMetadata = ( tenantId: scope.tenantId ?? principal.tenantId, }); -test('compensates a failed Core bind and never exposes the provider key identifier', async () => { - const disabled: string[] = []; - const bindFailure = new IdentityTargetInvalidError({ - code: 'identity_target_invalid', - reason: 'The requested binding target is invalid', - }); - const service = makeIdentityLifecycleService( - makeActionRuntimeDouble([actionDomainFailure(bindFailure)]).runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: () => Effect.void, - issue: () => Effect.succeed(issued), - pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), - setEnabled: (keyId) => { - disabled.push(keyId); - return Effect.succeed({ ...issued, providerKeyId: keyId }); - }, - }), - resolver, - ); +it.effect('compensates a failed Core bind and never exposes the provider key identifier', () => + Effect.gen(function* testProgram1() { + const disabled: string[] = []; + const bindFailure = new IdentityTargetInvalidError({ + code: 'identity_target_invalid', + reason: 'The requested binding target is invalid', + }); + const service = makeIdentityLifecycleService( + makeActionRuntimeDouble([actionDomainFailure(bindFailure)]).runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: () => Effect.void, + issue: () => Effect.succeed(issued), + pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), + setEnabled: (keyId) => { + disabled.push(keyId); + return Effect.succeed({ ...issued, providerKeyId: keyId }); + }, + }), + resolver, + ); - const failure = await runEffectTestPromise( - Effect.flip( + const failure = yield* Effect.flip( service.issue({ correlationId: 'correlation-1', idempotencyKey: 'issue-1', principal, requestHeaders: new Headers(), }), - ), - ); - expect(failure).toBe(bindFailure); - expect(failure._tag).toBe('IdentityTargetInvalidError'); - expect(disabled).toEqual(['private-provider-key-id']); -}); - -test('preserves resolver lifecycle failures instead of rewriting them as an outage', async () => { - const resolverFailure = new PrincipalBindingMissingError(); - const service = makeIdentityLifecycleService( - makeActionRuntimeDouble([actionDefect('must not run')]).runtime, - makeApiKeyServiceDouble(), - makePrincipalResolverDouble({ - loadApiKeyBindingForAdministration: () => Effect.fail(resolverFailure), - }), - ); + ); + expect(failure).toBe(bindFailure); + expect(Predicate.isTagged(failure, 'IdentityTargetInvalidError')).toBe(true); + expect(disabled).toEqual(['private-provider-key-id']); + }), +); + +it.effect('preserves resolver lifecycle failures instead of rewriting them as an outage', () => + Effect.gen(function* testProgram2() { + const resolverFailure = new PrincipalBindingMissingError(); + const service = makeIdentityLifecycleService( + makeActionRuntimeDouble([actionDefect('must not run')]).runtime, + makeApiKeyServiceDouble(), + makePrincipalResolverDouble({ + loadApiKeyBindingForAdministration: () => Effect.fail(resolverFailure), + }), + ); - const failure = await runEffectTestPromise( - Effect.flip( + const failure = yield* Effect.flip( service.setStatus({ authBindingId: '00000000-0000-4000-8000-000000000005', correlationId: 'correlation-resolver-failure', @@ -113,32 +111,32 @@ test('preserves resolver lifecycle failures instead of rewriting them as an outa newStatus: 'disabled', principal, }), - ), - ); - - expect(failure).toBe(resolverFailure); - expect(failure._tag).toBe('PrincipalBindingMissingError'); -}); - -test('preserves a typed Core status-transition failure before touching provider state', async () => { - const actionFailure = new IdentityTargetInvalidError({ - code: 'identity_target_invalid', - reason: 'The requested status transition is invalid', - }); - let providerCalls = 0; - const service = makeIdentityLifecycleService( - makeActionRuntimeDouble([actionDomainFailure(actionFailure)]).runtime, - makeApiKeyServiceDouble({ - setEnabled: () => { - providerCalls += 1; - return Effect.succeed(issued); - }, - }), - resolver, - ); + ); + + expect(failure).toBe(resolverFailure); + expect(Predicate.isTagged(failure, 'PrincipalBindingMissingError')).toBe(true); + }), +); + +it.effect('preserves a typed Core status-transition failure before touching provider state', () => + Effect.gen(function* testProgram3() { + const actionFailure = new IdentityTargetInvalidError({ + code: 'identity_target_invalid', + reason: 'The requested status transition is invalid', + }); + let providerCalls = 0; + const service = makeIdentityLifecycleService( + makeActionRuntimeDouble([actionDomainFailure(actionFailure)]).runtime, + makeApiKeyServiceDouble({ + setEnabled: () => { + providerCalls += 1; + return Effect.succeed(issued); + }, + }), + resolver, + ); - const failure = await runEffectTestPromise( - Effect.flip( + const failure = yield* Effect.flip( service.setStatus({ authBindingId: '00000000-0000-4000-8000-000000000005', correlationId: 'correlation-core-failure', @@ -147,15 +145,15 @@ test('preserves a typed Core status-transition failure before touching provider newStatus: 'disabled', principal, }), - ), - ); + ); - expect(failure).toBe(actionFailure); - expect(failure._tag).toBe('IdentityTargetInvalidError'); - expect(providerCalls).toBe(0); -}); + expect(failure).toBe(actionFailure); + expect(Predicate.isTagged(failure, 'IdentityTargetInvalidError')).toBe(true); + expect(providerCalls).toBe(0); + }), +); -test('reconciles only expired pending leases in the trusted tenant and issuer scope', () => { +it('reconciles only expired pending leases in the trusted tenant and issuer scope', () => { const nowEpochMillis = new Date('2026-08-09T12:00:00.000Z').getTime(); const selected = classifyPendingApiKeyCleanup( [ @@ -200,283 +198,293 @@ test('reconciles only expired pending leases in the trusted tenant and issuer sc expect(selected).toEqual(['abandoned-key']); }); -test('returns a secret only after bind succeeds and strips the private provider key identifier', async () => { - const service = makeIdentityLifecycleService( - makeActionRuntimeDouble([ +it.effect( + 'returns a secret only after bind succeeds and strips the private provider key identifier', + () => + Effect.gen(function* testProgram4() { + const service = makeIdentityLifecycleService( + makeActionRuntimeDouble([ + actionSuccess({ + authBindingId: '00000000-0000-4000-8000-000000000004', + status: 'active', + }), + ]).runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: () => Effect.void, + issue: () => Effect.succeed(issued), + pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), + setEnabled: () => Effect.succeed(issued), + }), + resolver, + ); + + const result = yield* service.issue({ + correlationId: 'correlation-2', + idempotencyKey: 'issue-2', + principal, + requestHeaders: new Headers(), + }); + expect(result.secret).toBe('ontos-secret'); + expect(Object.hasOwn(result, 'providerKeyId')).toBe(false); + }), +); + +it.effect('revokes the replacement before failing when closing the old Core binding fails', () => + Effect.gen(function* testProgram5() { + const actionRuntime = makeActionRuntimeDouble([ actionSuccess({ authBindingId: '00000000-0000-4000-8000-000000000004', status: 'active', }), - ]).runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: () => Effect.void, - issue: () => Effect.succeed(issued), - pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), - setEnabled: () => Effect.succeed(issued), - }), - resolver, - ); - - const result = await runEffectTestPromise( - service.issue({ - correlationId: 'correlation-2', - idempotencyKey: 'issue-2', - principal, - requestHeaders: new Headers(), - }), - ); - expect(result.secret).toBe('ontos-secret'); - expect(Object.hasOwn(result, 'providerKeyId')).toBe(false); -}); - -test('revokes the replacement before failing when closing the old Core binding fails', async () => { - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ - authBindingId: '00000000-0000-4000-8000-000000000004', - status: 'active', - }), - actionCoreFailure(actionTransactionFailure('old binding unavailable')), - actionSuccess({ previousStatus: 'active', status: 'revoked' }), - ]); - const disabled: string[] = []; - const service = makeIdentityLifecycleService( - actionRuntime.runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: () => Effect.void, - issue: () => Effect.succeed(issued), - metadata: () => Effect.succeed(issued), - pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), - setEnabled: (keyId, enabled) => { - if (!enabled) { - disabled.push(keyId); - } - return Effect.succeed({ ...issued, providerKeyId: keyId }); - }, - }), - resolver, - ); + actionCoreFailure(actionTransactionFailure('old binding unavailable')), + actionSuccess({ previousStatus: 'active', status: 'revoked' }), + ]); + const disabled: string[] = []; + const service = makeIdentityLifecycleService( + actionRuntime.runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: () => Effect.void, + issue: () => Effect.succeed(issued), + metadata: () => Effect.succeed(issued), + pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), + setEnabled: (keyId, enabled) => { + if (!enabled) { + disabled.push(keyId); + } + return Effect.succeed({ ...issued, providerKeyId: keyId }); + }, + }), + resolver, + ); + + expect( + yield* Effect.flip( + service.rotate({ + correlationId: 'correlation-3', + idempotencyKey: 'rotate-1', + oldAuthBindingId: '00000000-0000-4000-8000-000000000005', + principal, + reason: 'Scheduled credential rotation', + requestHeaders: new Headers(), + }), + ), + ).toBeDefined(); + expect(actionRuntime.invocationCount()).toBe(3); + expect(disabled).toEqual(['old-provider-key-id']); + }), +); + +it.effect( + 'returns the replacement secret when both old closure and replacement rollback are unavailable', + () => + Effect.gen(function* testProgram6() { + const actionRuntime = makeActionRuntimeDouble([ + actionSuccess({ + authBindingId: '00000000-0000-4000-8000-000000000004', + status: 'active', + }), + actionCoreFailure(actionTransactionFailure('Core unavailable')), + actionCoreFailure(actionTransactionFailure('Core unavailable')), + ]); + const service = makeIdentityLifecycleService( + actionRuntime.runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: () => Effect.void, + issue: () => Effect.succeed(issued), + pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), + }), + resolver, + ); - await expect( - runEffectTestPromise( - service.rotate({ - correlationId: 'correlation-3', - idempotencyKey: 'rotate-1', + const result = yield* service.rotate({ + correlationId: 'correlation-4', + idempotencyKey: 'rotate-2', oldAuthBindingId: '00000000-0000-4000-8000-000000000005', principal, reason: 'Scheduled credential rotation', requestHeaders: new Headers(), - }), - ), - ).rejects.toBeDefined(); - expect(actionRuntime.invocationCount()).toBe(3); - expect(disabled).toEqual(['old-provider-key-id']); -}); - -test('returns the replacement secret when both old closure and replacement rollback are unavailable', async () => { - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ - authBindingId: '00000000-0000-4000-8000-000000000004', - status: 'active', - }), - actionCoreFailure(actionTransactionFailure('Core unavailable')), - actionCoreFailure(actionTransactionFailure('Core unavailable')), - ]); - const service = makeIdentityLifecycleService( - actionRuntime.runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: () => Effect.void, - issue: () => Effect.succeed(issued), - pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), - }), - resolver, - ); - - const result = await runEffectTestPromise( - service.rotate({ - correlationId: 'correlation-4', - idempotencyKey: 'rotate-2', - oldAuthBindingId: '00000000-0000-4000-8000-000000000005', - principal, - reason: 'Scheduled credential rotation', - requestHeaders: new Headers(), - }), - ); - expect(result.secret).toBe('ontos-secret'); - expect(result.cleanupPending).toBe(true); - expect(actionRuntime.invocationCount()).toBe(3); -}); - -test('returns the replacement secret when old Core closure committed but provider state is unavailable', async () => { - let resolverCalls = 0; - const providerUnavailable = new ApiKeyProviderUnavailableError({ - code: 'api_key_provider_unavailable', - reason: 'The provider is unavailable', - }); - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ - authBindingId: '00000000-0000-4000-8000-000000000004', - status: 'active', - }), - actionSuccess({ previousStatus: 'active', status: 'revoked' }), - ]); - const service = makeIdentityLifecycleService( - actionRuntime.runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: () => Effect.void, - issue: () => Effect.succeed(issued), - metadata: () => Effect.fail(providerUnavailable), - pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), - setEnabled: (keyId, enabled) => - keyId === 'old-provider-key-id' && !enabled - ? Effect.fail(providerUnavailable) - : Effect.succeed({ ...issued, providerKeyId: keyId }), - }), - makePrincipalResolverDouble({ - loadApiKeyBindingForAdministration: () => { - resolverCalls += 1; - return Effect.succeed({ - providerSubjectId: 'old-provider-key-id', - status: resolverCalls === 1 ? 'active' : 'revoked', - }); - }, - }), - ); - - const result = await runEffectTestPromise( - service.rotate({ - correlationId: 'correlation-old-core-closed', - idempotencyKey: 'rotate-old-core-closed', - oldAuthBindingId: '00000000-0000-4000-8000-000000000005', - principal, - reason: 'Scheduled credential rotation', - requestHeaders: new Headers(), - }), - ); - - expect(result.secret).toBe('ontos-secret'); - expect(result.cleanupPending).toBe(true); - expect(actionRuntime.invocationCount()).toBe(2); - expect(resolverCalls).toBe(2); -}); - -test('does not return a replacement secret after rollback definitely revoked its Core binding', async () => { - let replacementReads = 0; - const providerUnavailable = new ApiKeyProviderUnavailableError({ - code: 'api_key_provider_unavailable', - reason: 'The provider is unavailable', - }); - const oldFailure = new IdentityTargetInvalidError({ - code: 'identity_target_invalid', - reason: 'The old binding could not be closed', - }); - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ - authBindingId: '00000000-0000-4000-8000-000000000004', - status: 'active', - }), - actionDomainFailure(oldFailure), - actionSuccess({ previousStatus: 'active', status: 'revoked' }), - ]); - const service = makeIdentityLifecycleService( - actionRuntime.runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: () => Effect.void, - issue: () => Effect.succeed(issued), - metadata: () => Effect.fail(providerUnavailable), - pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), - setEnabled: () => Effect.fail(providerUnavailable), + }); + expect(result.secret).toBe('ontos-secret'); + expect(result.cleanupPending).toBe(true); + expect(actionRuntime.invocationCount()).toBe(3); }), - makePrincipalResolverDouble({ - loadApiKeyBindingForAdministration: (input) => { - if (input.authBindingId === '00000000-0000-4000-8000-000000000004') { - replacementReads += 1; - return Effect.succeed({ - providerSubjectId: 'replacement-provider-key-id', - status: replacementReads === 1 ? 'active' : 'revoked', - }); - } - return Effect.succeed({ - providerSubjectId: 'old-provider-key-id', +); + +it.effect( + 'returns the replacement secret when old Core closure committed but provider state is unavailable', + () => + Effect.gen(function* testProgram7() { + let resolverCalls = 0; + const providerUnavailable = new ApiKeyProviderUnavailableError({ + code: 'api_key_provider_unavailable', + reason: 'The provider is unavailable', + }); + const actionRuntime = makeActionRuntimeDouble([ + actionSuccess({ + authBindingId: '00000000-0000-4000-8000-000000000004', status: 'active', - }); - }, - }), - ); + }), + actionSuccess({ previousStatus: 'active', status: 'revoked' }), + ]); + const service = makeIdentityLifecycleService( + actionRuntime.runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: () => Effect.void, + issue: () => Effect.succeed(issued), + metadata: () => Effect.fail(providerUnavailable), + pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), + setEnabled: (keyId, enabled) => + keyId === 'old-provider-key-id' && !enabled + ? Effect.fail(providerUnavailable) + : Effect.succeed({ ...issued, providerKeyId: keyId }), + }), + makePrincipalResolverDouble({ + loadApiKeyBindingForAdministration: () => { + resolverCalls += 1; + return Effect.succeed({ + providerSubjectId: 'old-provider-key-id', + status: resolverCalls === 1 ? 'active' : 'revoked', + }); + }, + }), + ); - const failure = await runEffectTestPromise( - Effect.flip( - service.rotate({ - correlationId: 'correlation-definite-replacement-rollback', - idempotencyKey: 'definite-replacement-rollback', + const result = yield* service.rotate({ + correlationId: 'correlation-old-core-closed', + idempotencyKey: 'rotate-old-core-closed', oldAuthBindingId: '00000000-0000-4000-8000-000000000005', principal, reason: 'Scheduled credential rotation', requestHeaders: new Headers(), - }), - ), - ); + }); - expect(failure).toBe(oldFailure); - expect(actionRuntime.invocationCount()).toBe(3); - expect(replacementReads).toBe(2); -}); - -test('cleans one bounded pending batch and requires a retry before issuing another key', async () => { - const disabled: string[] = []; - let issueCalls = 0; - const service = makeIdentityLifecycleService( - makeActionRuntimeDouble([actionDefect('must not bind')]).runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: () => Effect.void, - issue: () => { - issueCalls += 1; - return Effect.succeed(issued); - }, - pendingCleanup: () => Effect.succeed({ hasMore: true, providerKeyIds: ['bounded-orphan'] }), - setEnabled: (keyId, enabled) => { - if (!enabled) { - disabled.push(keyId); - } - return Effect.succeed({ ...issued, providerKeyId: keyId }); - }, + expect(result.secret).toBe('ontos-secret'); + expect(result.cleanupPending).toBe(true); + expect(actionRuntime.invocationCount()).toBe(2); + expect(resolverCalls).toBe(2); }), - makePrincipalResolverDouble({ - resolveBetterAuthApiKey: () => Effect.fail(new PrincipalBindingMissingError()), +); + +it.effect( + 'does not return a replacement secret after rollback definitely revoked its Core binding', + () => + Effect.gen(function* testProgram8() { + let replacementReads = 0; + const providerUnavailable = new ApiKeyProviderUnavailableError({ + code: 'api_key_provider_unavailable', + reason: 'The provider is unavailable', + }); + const oldFailure = new IdentityTargetInvalidError({ + code: 'identity_target_invalid', + reason: 'The old binding could not be closed', + }); + const actionRuntime = makeActionRuntimeDouble([ + actionSuccess({ + authBindingId: '00000000-0000-4000-8000-000000000004', + status: 'active', + }), + actionDomainFailure(oldFailure), + actionSuccess({ previousStatus: 'active', status: 'revoked' }), + ]); + const service = makeIdentityLifecycleService( + actionRuntime.runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: () => Effect.void, + issue: () => Effect.succeed(issued), + metadata: () => Effect.fail(providerUnavailable), + pendingCleanup: () => Effect.succeed({ hasMore: false, providerKeyIds: [] }), + setEnabled: () => Effect.fail(providerUnavailable), + }), + makePrincipalResolverDouble({ + loadApiKeyBindingForAdministration: (input) => { + if (input.authBindingId === '00000000-0000-4000-8000-000000000004') { + replacementReads += 1; + return Effect.succeed({ + providerSubjectId: 'replacement-provider-key-id', + status: replacementReads === 1 ? 'active' : 'revoked', + }); + } + return Effect.succeed({ + providerSubjectId: 'old-provider-key-id', + status: 'active', + }); + }, + }), + ); + + const failure = yield* Effect.flip( + service.rotate({ + correlationId: 'correlation-definite-replacement-rollback', + idempotencyKey: 'definite-replacement-rollback', + oldAuthBindingId: '00000000-0000-4000-8000-000000000005', + principal, + reason: 'Scheduled credential rotation', + requestHeaders: new Headers(), + }), + ); + + expect(failure).toBe(oldFailure); + expect(actionRuntime.invocationCount()).toBe(3); + expect(replacementReads).toBe(2); }), - ); +); + +it.effect('cleans one bounded pending batch and requires a retry before issuing another key', () => + Effect.gen(function* testProgram9() { + const disabled: string[] = []; + let issueCalls = 0; + const service = makeIdentityLifecycleService( + makeActionRuntimeDouble([actionDefect('must not bind')]).runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: () => Effect.void, + issue: () => { + issueCalls += 1; + return Effect.succeed(issued); + }, + pendingCleanup: () => Effect.succeed({ hasMore: true, providerKeyIds: ['bounded-orphan'] }), + setEnabled: (keyId, enabled) => { + if (!enabled) { + disabled.push(keyId); + } + return Effect.succeed({ ...issued, providerKeyId: keyId }); + }, + }), + makePrincipalResolverDouble({ + resolveBetterAuthApiKey: () => Effect.fail(new PrincipalBindingMissingError()), + }), + ); - const failure = await runEffectTestPromise( - Effect.flip( + const failure = yield* Effect.flip( service.issue({ correlationId: 'correlation-bounded-cleanup', idempotencyKey: 'bounded-cleanup', principal, requestHeaders: new Headers(), }), - ), - ); - - expect(failure._tag).toBe('IdentityLifecycleOperationError'); - expect(disabled).toEqual(['bounded-orphan']); - expect(issueCalls).toBe(0); -}); - -test('retries provider cleanup without repeating an already committed Core transition', async () => { - const actionRuntime = makeActionRuntimeDouble([actionDefect(new Error('must not run'))]); - const service = makeIdentityLifecycleService( - actionRuntime.runtime, - makeApiKeyServiceDouble({ - metadata: () => Effect.succeed({ ...issued, enabled: false }), - setEnabled: () => Effect.succeed({ ...issued, enabled: false }), - }), - makePrincipalResolverDouble({ - loadApiKeyBindingForAdministration: () => - Effect.succeed({ providerSubjectId: 'old-provider-key-id', status: 'revoked' }), - }), - ); + ); + + expect(Predicate.isTagged(failure, 'IdentityLifecycleOperationError')).toBe(true); + expect(disabled).toEqual(['bounded-orphan']); + expect(issueCalls).toBe(0); + }), +); + +it.effect('retries provider cleanup without repeating an already committed Core transition', () => + Effect.gen(function* testProgram10() { + const actionRuntime = makeActionRuntimeDouble([actionDefect(new Error('must not run'))]); + const service = makeIdentityLifecycleService( + actionRuntime.runtime, + makeApiKeyServiceDouble({ + metadata: () => Effect.succeed({ ...issued, enabled: false }), + setEnabled: () => Effect.succeed({ ...issued, enabled: false }), + }), + makePrincipalResolverDouble({ + loadApiKeyBindingForAdministration: () => + Effect.succeed({ providerSubjectId: 'old-provider-key-id', status: 'revoked' }), + }), + ); - const result = await runEffectTestPromise( - service.setStatus({ + const result = yield* service.setStatus({ authBindingId: '00000000-0000-4000-8000-000000000005', correlationId: 'correlation-5', expectedStatus: 'active', @@ -484,93 +492,97 @@ test('retries provider cleanup without repeating an already committed Core trans newStatus: 'revoked', principal, reason: 'Retry provider cleanup', - }), - ); - expect(result.cleanupPending).toBe(false); - expect(actionRuntime.invocationCount()).toBe(0); -}); + }); + expect(result.cleanupPending).toBe(false); + expect(actionRuntime.invocationCount()).toBe(0); + }), +); + +it.effect( + 'preserves provider metadata failure after a safe Core disable instead of fabricating state', + () => + Effect.gen(function* testProgram11() { + const metadataFailure = new ApiKeyStateInconsistentError({ + code: 'api_key_state_inconsistent', + reason: 'The provider key row is missing', + }); + const actionRuntime = makeActionRuntimeDouble([ + actionSuccess({ previousStatus: 'active', status: 'disabled' }), + ]); + const service = makeIdentityLifecycleService( + actionRuntime.runtime, + makeApiKeyServiceDouble({ + metadata: () => Effect.fail(metadataFailure), + setEnabled: () => + Effect.fail( + new ApiKeyProviderUnavailableError({ + code: 'api_key_provider_unavailable', + reason: 'The provider is unavailable', + }), + ), + }), + resolver, + ); + + const failure = yield* Effect.flip( + service.setStatus({ + authBindingId: '00000000-0000-4000-8000-000000000005', + correlationId: 'correlation-provider-metadata-failure', + expectedStatus: 'active', + idempotencyKey: 'disable-provider-metadata-failure', + newStatus: 'disabled', + principal, + reason: 'Disable a missing provider key', + }), + ); -test('preserves provider metadata failure after a safe Core disable instead of fabricating state', async () => { - const metadataFailure = new ApiKeyStateInconsistentError({ - code: 'api_key_state_inconsistent', - reason: 'The provider key row is missing', - }); - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ previousStatus: 'active', status: 'disabled' }), - ]); - const service = makeIdentityLifecycleService( - actionRuntime.runtime, - makeApiKeyServiceDouble({ - metadata: () => Effect.fail(metadataFailure), - setEnabled: () => - Effect.fail( - new ApiKeyProviderUnavailableError({ - code: 'api_key_provider_unavailable', - reason: 'The provider is unavailable', - }), - ), + expect(failure).toBe(metadataFailure); + expect(actionRuntime.invocationCount()).toBe(1); }), - resolver, - ); +); + +it.effect( + 'reconciles a provider key left pending by failed bind compensation before retrying issue', + () => + Effect.gen(function* testProgram12() { + const disabled: string[] = []; + const cleared: string[] = []; + const service = makeIdentityLifecycleService( + makeActionRuntimeDouble([ + actionSuccess({ + authBindingId: '00000000-0000-4000-8000-000000000004', + status: 'active', + }), + ]).runtime, + makeApiKeyServiceDouble({ + clearPendingCleanup: (keyId) => { + cleared.push(keyId); + return Effect.void; + }, + issue: () => Effect.succeed(issued), + pendingCleanup: () => + Effect.succeed({ hasMore: false, providerKeyIds: ['orphan-provider-key-id'] }), + setEnabled: (keyId, enabled) => { + if (!enabled) { + disabled.push(keyId); + } + return Effect.succeed({ ...issued, providerKeyId: keyId }); + }, + }), + makePrincipalResolverDouble({ + resolveBetterAuthApiKey: () => Effect.fail(new PrincipalBindingMissingError()), + }), + ); - const failure = await runEffectTestPromise( - Effect.flip( - service.setStatus({ - authBindingId: '00000000-0000-4000-8000-000000000005', - correlationId: 'correlation-provider-metadata-failure', - expectedStatus: 'active', - idempotencyKey: 'disable-provider-metadata-failure', - newStatus: 'disabled', + const result = yield* service.issue({ + correlationId: 'correlation-6', + idempotencyKey: 'issue-retry', principal, - reason: 'Disable a missing provider key', - }), - ), - ); - - expect(failure).toBe(metadataFailure); - expect(actionRuntime.invocationCount()).toBe(1); -}); - -test('reconciles a provider key left pending by failed bind compensation before retrying issue', async () => { - const disabled: string[] = []; - const cleared: string[] = []; - const service = makeIdentityLifecycleService( - makeActionRuntimeDouble([ - actionSuccess({ - authBindingId: '00000000-0000-4000-8000-000000000004', - status: 'active', - }), - ]).runtime, - makeApiKeyServiceDouble({ - clearPendingCleanup: (keyId) => { - cleared.push(keyId); - return Effect.void; - }, - issue: () => Effect.succeed(issued), - pendingCleanup: () => - Effect.succeed({ hasMore: false, providerKeyIds: ['orphan-provider-key-id'] }), - setEnabled: (keyId, enabled) => { - if (!enabled) { - disabled.push(keyId); - } - return Effect.succeed({ ...issued, providerKeyId: keyId }); - }, - }), - makePrincipalResolverDouble({ - resolveBetterAuthApiKey: () => Effect.fail(new PrincipalBindingMissingError()), - }), - ); + requestHeaders: new Headers(), + }); - const result = await runEffectTestPromise( - service.issue({ - correlationId: 'correlation-6', - idempotencyKey: 'issue-retry', - principal, - requestHeaders: new Headers(), + expect(result.secret).toBe('ontos-secret'); + expect(disabled).toEqual(['orphan-provider-key-id']); + expect(cleared).toEqual(['orphan-provider-key-id', 'private-provider-key-id']); }), - ); - - expect(result.secret).toBe('ontos-secret'); - expect(disabled).toEqual(['orphan-provider-key-id']); - expect(cleared).toEqual(['orphan-provider-key-id', 'private-provider-key-id']); -}); +); diff --git a/app/apps/shell-super-app/tests/unit/impersonation-service.test.ts b/app/apps/shell-super-app/tests/unit/impersonation-service.test.ts index 12069313e..848040be4 100644 --- a/app/apps/shell-super-app/tests/unit/impersonation-service.test.ts +++ b/app/apps/shell-super-app/tests/unit/impersonation-service.test.ts @@ -1,7 +1,6 @@ import { makeContextAccessDouble } from '../support/context-access-double.ts'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import { expect, test } from '@rstest/core'; +import { TestClock } from 'effect/testing'; +import { expect, it } from 'effect-rstest'; import { ActionRuntime, ActionAlreadyCommitted, @@ -126,57 +125,56 @@ const supportRecoveryPrincipal: SupportRecoveryPrincipalContextResolverService = const provider = (impersonated: boolean): SupportAuthProvider => ({ api: { - getSession: async () => ({ - headers: new Headers(), - response: { - session: impersonated - ? { - activeTenantId: tenantId, - id: impersonationSessionId, - impersonatedBy: 'original-provider-user', - impersonationActionId: 'impersonation-action', - impersonationOriginalAuthBindingId: originalAuthBindingId, - impersonationOriginalPrincipalId: originalPrincipalId, - impersonationOriginalSessionId: restoredSessionId, - impersonationReason: 'Investigate support request', - impersonationTargetPrincipalId: targetPrincipalId, - } - : { activeTenantId: tenantId, id: restoredSessionId }, - user: { id: 'original-provider-user' }, - }, - }), - impersonateUser: async () => { - throw new Error('not used'); - }, - stopImpersonating: async () => { + getSession: () => + Promise.resolve({ + headers: new Headers(), + response: { + session: impersonated + ? { + activeTenantId: tenantId, + id: impersonationSessionId, + impersonatedBy: 'original-provider-user', + impersonationActionId: 'impersonation-action', + impersonationOriginalAuthBindingId: originalAuthBindingId, + impersonationOriginalPrincipalId: originalPrincipalId, + impersonationOriginalSessionId: restoredSessionId, + impersonationReason: 'Investigate support request', + impersonationTargetPrincipalId: targetPrincipalId, + } + : { activeTenantId: tenantId, id: restoredSessionId }, + user: { id: 'original-provider-user' }, + }, + }), + impersonateUser: () => Promise.reject(new Error('not used')), + stopImpersonating: () => { const headers = new Headers(); headers.append('set-cookie', 'session=restored; Path=/; HttpOnly'); - return { + return Promise.resolve({ headers, response: { session: { id: restoredSessionId } }, - }; + }); }, }, }); -test('preserves definite requested-checkpoint errors for their declared HTTP mapping', async () => { - const failures = [ - new ActionPermissionDenied({ - code: 'action_permission_denied', - reason: 'The Action permission was denied', - }), - new IdentityTargetInvalidError({ - code: 'identity_target_invalid', - reason: 'The support target is invalid', - }), - new ActionAlreadyCommitted({ - code: 'action_already_committed', - invocationId: 'invocation-id', - reason: 'The requested checkpoint was already committed', - }), - ]; - await runEffectTestPromise( - Effect.forEach( +it.effect('preserves definite requested-checkpoint errors for their declared HTTP mapping', () => + Effect.gen(function* testProgram1() { + const failures = [ + new ActionPermissionDenied({ + code: 'action_permission_denied', + reason: 'The Action permission was denied', + }), + new IdentityTargetInvalidError({ + code: 'identity_target_invalid', + reason: 'The support target is invalid', + }), + new ActionAlreadyCommitted({ + code: 'action_already_committed', + invocationId: 'invocation-id', + reason: 'The requested checkpoint was already committed', + }), + ]; + yield* Effect.forEach( failures, (failure) => Effect.gen(function* assertRequestedCheckpointFailure() { @@ -209,9 +207,9 @@ test('preserves definite requested-checkpoint errors for their declared HTTP map }), configuration, provider: makeSupportAuthProviderDouble({ - impersonateUser: async () => { + impersonateUser: () => { providerCalls += 1; - throw new Error('must not create a session'); + return Promise.reject(new Error('must not create a session')); }, }), resolver: makePrincipalResolverDouble({ @@ -241,69 +239,70 @@ test('preserves definite requested-checkpoint errors for their declared HTTP map expect(providerCalls).toBe(0); }), { concurrency: 1, discard: true }, - ), - ); -}); + ); + }), +); -test('removes the provider session and recovery when started evidence cannot commit', async () => { - const startedFailure = new ActionPermissionDenied({ - code: 'action_permission_denied', - reason: 'The started checkpoint was denied', - }); - const deletedTables: string[] = []; - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ checkpoint: 'requested', recorded: true }), - actionCoreFailure(startedFailure), - ]); - const service = makeService({ - actionRuntime: actionRuntime.runtime, - authentication: makeAuthenticationServiceDouble({ - resolveTenantContext: () => - Effect.succeed({ - identity: { - displayName: 'Original administrator', - email: 'original@example.test', - principalId: originalPrincipalId, - tenantId, - }, - principal: { - authBindingId: originalAuthBindingId, - authContextRef: `better-auth-session:${restoredSessionId}`, - authMethod: 'session', - principalId: originalPrincipalId, - tenantId, - }, - setCookieHeaders: [], - state: 'authenticated', - }), - }), - configuration, - provider: makeSupportAuthProviderDouble({ - impersonateUser: async () => ({ - headers: new Headers(), - response: { session: { id: impersonationSessionId } }, +it.effect('removes the provider session and recovery when started evidence cannot commit', () => + Effect.gen(function* testProgram2() { + const startedFailure = new ActionPermissionDenied({ + code: 'action_permission_denied', + reason: 'The started checkpoint was denied', + }); + const deletedTables: string[] = []; + const actionRuntime = makeActionRuntimeDouble([ + actionSuccess({ checkpoint: 'requested', recorded: true }), + actionCoreFailure(startedFailure), + ]); + const service = makeService({ + actionRuntime: actionRuntime.runtime, + authentication: makeAuthenticationServiceDouble({ + resolveTenantContext: () => + Effect.succeed({ + identity: { + displayName: 'Original administrator', + email: 'original@example.test', + principalId: originalPrincipalId, + tenantId, + }, + principal: { + authBindingId: originalAuthBindingId, + authContextRef: `better-auth-session:${restoredSessionId}`, + authMethod: 'session', + principalId: originalPrincipalId, + tenantId, + }, + setCookieHeaders: [], + state: 'authenticated', + }), }), - }), - resolver: makePrincipalResolverDouble({ - resolveBetterAuthUserForPrincipal: () => Effect.succeed('target-provider-user'), - }), - store: makeSupportImpersonationStoreDouble({ - deleteRecovery: () => - Effect.sync(() => { - deletedTables.push('deleted'); - }), - deleteSession: () => - Effect.sync(() => { - deletedTables.push('deleted'); - }), - insertRecovery: () => Effect.void, - updateImpersonationSession: () => Effect.void, - }), - supportRecoveryPrincipal, - }); + configuration, + provider: makeSupportAuthProviderDouble({ + impersonateUser: () => + Promise.resolve({ + headers: new Headers(), + response: { session: { id: impersonationSessionId } }, + }), + }), + resolver: makePrincipalResolverDouble({ + resolveBetterAuthUserForPrincipal: () => Effect.succeed('target-provider-user'), + }), + store: makeSupportImpersonationStoreDouble({ + deleteRecovery: () => + Effect.sync(() => { + deletedTables.push('deleted'); + }), + deleteSession: () => + Effect.sync(() => { + deletedTables.push('deleted'); + }), + insertRecovery: () => Effect.void, + updateImpersonationSession: () => Effect.void, + }), + supportRecoveryPrincipal, + }); - const failure = await runEffectTestPromise( - Effect.flip( + const failure = yield* Effect.flip( service .start({ idempotencyKey: 'started-compensation', @@ -317,270 +316,281 @@ test('removes the provider session and recovery when started evidence cannot com 'correlation-started-compensation', ), ), - ), - ); + ); - expect(failure).toBe(startedFailure); - expect(actionRuntime.invocationCount()).toBe(2); - expect(deletedTables).toHaveLength(2); -}); + expect(failure).toBe(startedFailure); + expect(actionRuntime.invocationCount()).toBe(2); + expect(deletedTables).toHaveLength(2); + }), +); -test('persists stop recovery before provider restoration and returns restored cookies on evidence failure', async () => { - let recovery: SupportRecoveryRecord | undefined; - let resolverCalled = false; - const transactionFailure = new ActionTransactionError({ - code: 'action_transaction_failed', - reason: 'The stopped checkpoint transaction failed', - }); - const service = makeService({ - actionRuntime: makeActionRuntimeDouble([actionCoreFailure(transactionFailure)]).runtime, - authentication: makeAuthenticationServiceDouble(), - configuration, - provider: provider(true), - resolver: makePrincipalResolverDouble({ - resolveBetterAuthUserForTenant: () => { - resolverCalled = true; - return Effect.die('disabled principal'); - }, - }), - store: makeSupportImpersonationStoreDouble({ - deleteSession: () => Effect.void, - insertRecovery: (value) => - Effect.sync(() => { - recovery = value; +it.effect( + 'persists stop recovery before provider restoration and returns restored cookies on evidence failure', + () => + Effect.gen(function* testProgram3() { + let recovery: SupportRecoveryRecord | undefined; + let resolverCalled = false; + const transactionFailure = new ActionTransactionError({ + code: 'action_transaction_failed', + reason: 'The stopped checkpoint transaction failed', + }); + const service = makeService({ + actionRuntime: makeActionRuntimeDouble([actionCoreFailure(transactionFailure)]).runtime, + authentication: makeAuthenticationServiceDouble(), + configuration, + provider: provider(true), + resolver: makePrincipalResolverDouble({ + resolveBetterAuthUserForTenant: () => { + resolverCalled = true; + return Effect.die('disabled principal'); + }, }), - }), - supportRecoveryPrincipal, - }); - - const result = await runEffectTestPromise( - service - .stop({ - idempotencyKey: 'stop-request-1', - requestHeaders: new Headers(), - }) - .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-1')), - ); + store: makeSupportImpersonationStoreDouble({ + deleteSession: () => Effect.void, + insertRecovery: (value) => + Effect.sync(() => { + recovery = value; + }), + }), + supportRecoveryPrincipal, + }); - expect(recovery).toEqual( - expect.objectContaining({ - impersonationSessionId, - originalAuthBindingId, - originalPrincipalId, - targetPrincipalId, - tenantId, - }), - ); - expect(result.checkpointPending).toBe(true); - expect(result.setCookieHeaders).toEqual(['session=restored; Path=/; HttpOnly']); - expect(resolverCalled).toBe(false); -}); + const result = yield* service + .stop({ + idempotencyKey: 'stop-request-1', + requestHeaders: new Headers(), + }) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-1')); -test('terminates the target session before retrying stopped evidence from the restored session', async () => { - const recovery = { - actionId: 'impersonation-action', - createdAt: new Date('2026-08-09T00:00:00.000Z'), - impersonationSessionId, - originalAuthBindingId, - originalPrincipalId, - originalSessionId: restoredSessionId, - reason: 'Investigate support request', - targetPrincipalId, - tenantId, - }; - let recoveryDeleted = false; - let targetSessionActive = true; - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ checkpoint: 'stopped', recorded: true }), - ]); - const service = makeService({ - actionRuntime: actionRuntime.runtime, - authentication: makeAuthenticationServiceDouble(), - configuration, - provider: provider(false), - resolver: makePrincipalResolverDouble(), - store: makeSupportImpersonationStoreDouble({ - deleteRecovery: () => - Effect.sync(() => { - recoveryDeleted = true; - }), - deleteSession: () => - Effect.sync(() => { - targetSessionActive = false; + expect(recovery).toEqual( + expect.objectContaining({ + impersonationSessionId, + originalAuthBindingId, + originalPrincipalId, + targetPrincipalId, + tenantId, }), - loadRecoveries: () => Effect.succeed([recovery]), + ); + expect(result.checkpointPending).toBe(true); + expect(result.setCookieHeaders).toEqual(['session=restored; Path=/; HttpOnly']); + expect(resolverCalled).toBe(false); }), - supportRecoveryPrincipal, - }); - - const result = await runEffectTestPromise( - service - .stop({ - idempotencyKey: 'stop-request-2', - requestHeaders: new Headers(), - }) - .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-2')), - ); - - expect(actionRuntime.payloads[0]).toEqual({ - checkpoint: 'stopped', - originalPrincipalId, - reason: 'Investigate support request', - sessionRef: `better-auth-session:${impersonationSessionId}`, - targetPrincipalId, - }); - expect(result.checkpointPending).toBe(false); - expect(targetSessionActive).toBe(false); - expect(recoveryDeleted).toBe(true); -}); +); -test('completes every pending checkpoint correlated to the restored session', async () => { - const secondImpersonationSessionId = 'second-impersonated-session-id'; - const recoveries = [ - { - actionId: 'impersonation-action-one', - createdAt: new Date('2026-08-09T00:00:00.000Z'), - impersonationSessionId, - originalAuthBindingId, - originalPrincipalId, - originalSessionId: restoredSessionId, - reason: 'First support request', - targetPrincipalId, - tenantId, - }, - { - actionId: 'impersonation-action-two', - createdAt: new Date('2026-08-09T00:01:00.000Z'), - impersonationSessionId: secondImpersonationSessionId, - originalAuthBindingId, - originalPrincipalId, - originalSessionId: restoredSessionId, - reason: 'Second support request', - targetPrincipalId: '30000000-0000-4000-8000-000000000002', - tenantId, - }, - ]; - let deleteCount = 0; - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ checkpoint: 'stopped', recorded: true }), - actionSuccess({ checkpoint: 'stopped', recorded: true }), - ]); - const service = makeService({ - actionRuntime: actionRuntime.runtime, - authentication: makeAuthenticationServiceDouble(), - configuration, - provider: provider(false), - resolver: makePrincipalResolverDouble(), - store: makeSupportImpersonationStoreDouble({ - deleteRecovery: () => - Effect.sync(() => { - deleteCount += 1; - }), - deleteSession: () => - Effect.sync(() => { - deleteCount += 1; +it.effect( + 'terminates the target session before retrying stopped evidence from the restored session', + () => + Effect.gen(function* testProgram4() { + const recovery = { + actionId: 'impersonation-action', + createdAt: new Date('2026-08-09T00:00:00.000Z'), + impersonationSessionId, + originalAuthBindingId, + originalPrincipalId, + originalSessionId: restoredSessionId, + reason: 'Investigate support request', + targetPrincipalId, + tenantId, + }; + let recoveryDeleted = false; + let targetSessionActive = true; + const actionRuntime = makeActionRuntimeDouble([ + actionSuccess({ checkpoint: 'stopped', recorded: true }), + ]); + const service = makeService({ + actionRuntime: actionRuntime.runtime, + authentication: makeAuthenticationServiceDouble(), + configuration, + provider: provider(false), + resolver: makePrincipalResolverDouble(), + store: makeSupportImpersonationStoreDouble({ + deleteRecovery: () => + Effect.sync(() => { + recoveryDeleted = true; + }), + deleteSession: () => + Effect.sync(() => { + targetSessionActive = false; + }), + loadRecoveries: () => Effect.succeed([recovery]), }), - loadRecoveries: () => Effect.succeed(recoveries), + supportRecoveryPrincipal, + }); + + const result = yield* service + .stop({ + idempotencyKey: 'stop-request-2', + requestHeaders: new Headers(), + }) + .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-2')); + + expect(actionRuntime.payloads[0]).toEqual({ + checkpoint: 'stopped', + originalPrincipalId, + reason: 'Investigate support request', + sessionRef: `better-auth-session:${impersonationSessionId}`, + targetPrincipalId, + }); + expect(result.checkpointPending).toBe(false); + expect(targetSessionActive).toBe(false); + expect(recoveryDeleted).toBe(true); }), - supportRecoveryPrincipal, - }); +); - const result = await runEffectTestPromise( - service +it.effect('completes every pending checkpoint correlated to the restored session', () => + Effect.gen(function* testProgram5() { + const secondImpersonationSessionId = 'second-impersonated-session-id'; + const recoveries = [ + { + actionId: 'impersonation-action-one', + createdAt: new Date('2026-08-09T00:00:00.000Z'), + impersonationSessionId, + originalAuthBindingId, + originalPrincipalId, + originalSessionId: restoredSessionId, + reason: 'First support request', + targetPrincipalId, + tenantId, + }, + { + actionId: 'impersonation-action-two', + createdAt: new Date('2026-08-09T00:01:00.000Z'), + impersonationSessionId: secondImpersonationSessionId, + originalAuthBindingId, + originalPrincipalId, + originalSessionId: restoredSessionId, + reason: 'Second support request', + targetPrincipalId: '30000000-0000-4000-8000-000000000002', + tenantId, + }, + ]; + let deleteCount = 0; + const actionRuntime = makeActionRuntimeDouble([ + actionSuccess({ checkpoint: 'stopped', recorded: true }), + actionSuccess({ checkpoint: 'stopped', recorded: true }), + ]); + const service = makeService({ + actionRuntime: actionRuntime.runtime, + authentication: makeAuthenticationServiceDouble(), + configuration, + provider: provider(false), + resolver: makePrincipalResolverDouble(), + store: makeSupportImpersonationStoreDouble({ + deleteRecovery: () => + Effect.sync(() => { + deleteCount += 1; + }), + deleteSession: () => + Effect.sync(() => { + deleteCount += 1; + }), + loadRecoveries: () => Effect.succeed(recoveries), + }), + supportRecoveryPrincipal, + }); + + const result = yield* service .stop({ idempotencyKey: 'stop-request-3', requestHeaders: new Headers(), }) - .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-3')), - ); + .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-3')); - expect(actionRuntime.payloads).toEqual([ - expect.objectContaining({ sessionRef: `better-auth-session:${impersonationSessionId}` }), - expect.objectContaining({ sessionRef: `better-auth-session:${secondImpersonationSessionId}` }), - ]); - expect(result.checkpointPending).toBe(false); - expect(deleteCount).toBe(4); -}); + expect(actionRuntime.payloads).toEqual([ + expect.objectContaining({ sessionRef: `better-auth-session:${impersonationSessionId}` }), + expect.objectContaining({ + sessionRef: `better-auth-session:${secondImpersonationSessionId}`, + }), + ]); + expect(result.checkpointPending).toBe(false); + expect(deleteCount).toBe(4); + }), +); -test('persists and completes stopped evidence on the first stop after impersonation expiry', async () => { - const expiredToken = 'expired-impersonation-token'; - const signedToken = encodeURIComponent( - `${expiredToken}.${await makeSignature(expiredToken, configuration.secret)}`, - ); - let persistedRecovery: SupportRecoveryRecord | undefined; - let deleteCalls = 0; - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ checkpoint: 'stopped', recorded: true }), - ]); - const service = makeService({ - actionRuntime: actionRuntime.runtime, - authentication: makeAuthenticationServiceDouble(), - configuration, - provider: makeSupportAuthProviderDouble({ - getSession: async () => ({ headers: new Headers(), response: null }), - }), - resolver: makePrincipalResolverDouble(), - store: makeSupportImpersonationStoreDouble({ - deleteRecovery: () => - Effect.sync(() => { - deleteCalls += 1; - }), - deleteSession: () => - Effect.sync(() => { - deleteCalls += 1; +it.effect( + 'persists and completes stopped evidence on the first stop after impersonation expiry', + () => + Effect.gen(function* testProgram6() { + const expiredToken = 'expired-impersonation-token'; + const signedToken = encodeURIComponent( + `${expiredToken}.${yield* Effect.promise(() => makeSignature(expiredToken, configuration.secret))}`, + ); + let persistedRecovery: SupportRecoveryRecord | undefined; + let deleteCalls = 0; + const actionRuntime = makeActionRuntimeDouble([ + actionSuccess({ checkpoint: 'stopped', recorded: true }), + ]); + const service = makeService({ + actionRuntime: actionRuntime.runtime, + authentication: makeAuthenticationServiceDouble(), + configuration, + provider: makeSupportAuthProviderDouble({ + getSession: () => Promise.resolve({ headers: new Headers(), response: null }), }), - insertRecovery: (value) => - Effect.sync(() => { - persistedRecovery = value; + resolver: makePrincipalResolverDouble(), + store: makeSupportImpersonationStoreDouble({ + deleteRecovery: () => + Effect.sync(() => { + deleteCalls += 1; + }), + deleteSession: () => + Effect.sync(() => { + deleteCalls += 1; + }), + insertRecovery: (value) => + Effect.sync(() => { + persistedRecovery = value; + }), + loadExpiredRecovery: () => + Effect.succeed( + Option.some({ + actionId: 'expired-impersonation-action', + impersonationSessionId, + originalAuthBindingId, + originalPrincipalId, + originalSessionId: restoredSessionId, + reason: 'Investigate support request', + targetPrincipalId, + tenantId, + }), + ), }), - loadExpiredRecovery: () => - Effect.succeed( - Option.some({ - actionId: 'expired-impersonation-action', - impersonationSessionId, - originalAuthBindingId, - originalPrincipalId, - originalSessionId: restoredSessionId, - reason: 'Investigate support request', - targetPrincipalId, - tenantId, + supportRecoveryPrincipal, + }); + + const result = yield* service + .stop({ + idempotencyKey: 'first-expired-stop', + requestHeaders: new Headers({ + cookie: `better-auth.session_token=${signedToken}`, }), - ), - }), - supportRecoveryPrincipal, - }); + }) + .pipe( + Effect.provideService( + SupportImpersonationCorrelationId, + 'correlation-first-expired-stop', + ), + ); - const result = await runEffectTestPromise( - service - .stop({ - idempotencyKey: 'first-expired-stop', - requestHeaders: new Headers({ - cookie: `better-auth.session_token=${signedToken}`, + expect(persistedRecovery).toEqual( + expect.objectContaining({ + actionId: 'expired-impersonation-action', + impersonationSessionId, + originalSessionId: restoredSessionId, }), - }) - .pipe( - Effect.provideService(SupportImpersonationCorrelationId, 'correlation-first-expired-stop'), - ), - ); - - expect(persistedRecovery).toEqual( - expect.objectContaining({ - actionId: 'expired-impersonation-action', - impersonationSessionId, - originalSessionId: restoredSessionId, + ); + expect(actionRuntime.payloads[0]).toEqual({ + checkpoint: 'stopped', + originalPrincipalId, + reason: 'Investigate support request', + sessionRef: `better-auth-session:${impersonationSessionId}`, + targetPrincipalId, + }); + expect(result.checkpointPending).toBe(false); + expect(result.setCookieHeaders.every((header) => header.includes('Max-Age=0'))).toBe(true); + expect(deleteCalls).toBe(2); }), - ); - expect(actionRuntime.payloads[0]).toEqual({ - checkpoint: 'stopped', - originalPrincipalId, - reason: 'Investigate support request', - sessionRef: `better-auth-session:${impersonationSessionId}`, - targetPrincipalId, - }); - expect(result.checkpointPending).toBe(false); - expect(result.setCookieHeaders.every((header) => header.includes('Max-Age=0'))).toBe(true); - expect(deleteCalls).toBe(2); -}); +); const makeLostResponseRecoveryService = (recovery: SupportRecoveryRecord, expiresAt: Date) => { let deleted = false; @@ -592,7 +602,7 @@ const makeLostResponseRecoveryService = (recovery: SupportRecoveryRecord, expire authentication: makeAuthenticationServiceDouble(), configuration, provider: makeSupportAuthProviderDouble({ - getSession: async () => ({ headers: new Headers(), response: null }), + getSession: () => Promise.resolve({ headers: new Headers(), response: null }), }), resolver: makePrincipalResolverDouble(), store: makeSupportImpersonationStoreDouble({ @@ -615,195 +625,208 @@ const makeLostResponseRecoveryService = (recovery: SupportRecoveryRecord, expire return { actionRuntime, deleted: () => deleted, service }; }; -test('restores the original session and stopped checkpoint after the provider response is lost', async () => { - const originalSessionToken = 'original-session-token'; - const adminValue = `${originalSessionToken}:true`; - const adminCookie = encodeURIComponent( - `${adminValue}.${await makeSignature(adminValue, configuration.secret)}`, - ); - const requestHeaders = new Headers({ - cookie: `better-auth.admin_session=${adminCookie}; better-auth.session_token=deleted`, - }); - const recovery = { - actionId: 'impersonation-action', - createdAt: new Date('2026-08-09T00:00:00.000Z'), - impersonationSessionId, - originalAuthBindingId, - originalPrincipalId, - originalSessionId: restoredSessionId, - reason: 'Investigate support request', - targetPrincipalId, - tenantId, - }; - const { actionRuntime, deleted, service } = makeLostResponseRecoveryService( - recovery, - new Date('2099-01-01T00:00:00.000Z'), - ); - - const result = await runEffectTestPromise( - service - .stop({ - idempotencyKey: 'stop-response-loss', - requestHeaders, - }) - .pipe(Effect.provideService(SupportImpersonationCorrelationId, 'correlation-response-loss')), - ); - - expect(result.active).toBe(false); - expect(result.checkpointPending).toBe(false); - expect(actionRuntime.invocationCount()).toBe(1); - expect(deleted()).toBe(true); - const restoredSessionCookie = result.setCookieHeaders.find((header) => - header.startsWith('better-auth.session_token='), - ); - expect(restoredSessionCookie).toBeDefined(); - expect(restoredSessionCookie?.includes('Max-Age=')).toBe(false); - const dontRememberCookie = result.setCookieHeaders.find((header) => - header.startsWith('better-auth.dont_remember='), - ); - expect(dontRememberCookie).toBeDefined(); - expect(dontRememberCookie?.includes('Max-Age=0')).toBe(false); - expect( - result.setCookieHeaders.some( - (header) => header.startsWith('better-auth.admin_session=') && header.includes('Max-Age=0'), - ), - ).toBe(true); -}); - -test('completes stopped recovery when a lost response leaves only an expired original session', async () => { - const originalSessionToken = 'expired-original-session-token'; - const adminValue = `${originalSessionToken}:`; - const adminCookie = encodeURIComponent( - `${adminValue}.${await makeSignature(adminValue, configuration.secret)}`, - ); - const recovery = { - actionId: 'expired-original-action', - createdAt: new Date('2026-08-09T00:00:00.000Z'), - impersonationSessionId, - originalAuthBindingId, - originalPrincipalId, - originalSessionId: restoredSessionId, - reason: 'Investigate support request', - targetPrincipalId, - tenantId, - }; - const { actionRuntime, deleted, service } = makeLostResponseRecoveryService( - recovery, - new Date('2000-01-01T00:00:00.000Z'), - ); - - const result = await runEffectTestPromise( - service - .stop({ - idempotencyKey: 'stop-expired-lost-response', - requestHeaders: new Headers({ - cookie: `better-auth.admin_session=${adminCookie}`, - }), - }) - .pipe( - Effect.provideService( - SupportImpersonationCorrelationId, - 'correlation-expired-lost-response', +it.effect( + 'restores the original session and stopped checkpoint after the provider response is lost', + () => + Effect.gen(function* testProgram7() { + const originalSessionToken = 'original-session-token'; + const adminValue = `${originalSessionToken}:true`; + const adminCookie = encodeURIComponent( + `${adminValue}.${yield* Effect.promise(() => makeSignature(adminValue, configuration.secret))}`, + ); + const requestHeaders = new Headers({ + cookie: `better-auth.admin_session=${adminCookie}; better-auth.session_token=deleted`, + }); + const recovery = { + actionId: 'impersonation-action', + createdAt: new Date('2026-08-09T00:00:00.000Z'), + impersonationSessionId, + originalAuthBindingId, + originalPrincipalId, + originalSessionId: restoredSessionId, + reason: 'Investigate support request', + targetPrincipalId, + tenantId, + }; + const { actionRuntime, deleted, service } = makeLostResponseRecoveryService( + recovery, + new Date('2099-01-01T00:00:00.000Z'), + ); + + const result = yield* service + .stop({ + idempotencyKey: 'stop-response-loss', + requestHeaders, + }) + .pipe( + Effect.provideService(SupportImpersonationCorrelationId, 'correlation-response-loss'), + ); + + expect(result.active).toBe(false); + expect(result.checkpointPending).toBe(false); + expect(actionRuntime.invocationCount()).toBe(1); + expect(deleted()).toBe(true); + const restoredSessionCookie = result.setCookieHeaders.find((header) => + header.startsWith('better-auth.session_token='), + ); + expect(restoredSessionCookie).toBeDefined(); + expect(restoredSessionCookie?.includes('Max-Age=')).toBe(false); + const dontRememberCookie = result.setCookieHeaders.find((header) => + header.startsWith('better-auth.dont_remember='), + ); + expect(dontRememberCookie).toBeDefined(); + expect(dontRememberCookie?.includes('Max-Age=0')).toBe(false); + expect( + result.setCookieHeaders.some( + (header) => + header.startsWith('better-auth.admin_session=') && header.includes('Max-Age=0'), ), - ), - ); + ).toBe(true); + }), +); - expect(result.active).toBe(false); - expect(result.checkpointPending).toBe(false); - expect(actionRuntime.invocationCount()).toBe(1); - expect(deleted()).toBe(true); - expect(result.setCookieHeaders.every((header) => header.includes('Max-Age=0'))).toBe(true); -}); +it.effect( + 'completes stopped recovery when a lost response leaves only an expired original session', + () => + Effect.gen(function* testProgram8() { + yield* TestClock.setTime(new Date('2026-09-08T00:00:00.000Z').getTime()); + const originalSessionToken = 'expired-original-session-token'; + const adminValue = `${originalSessionToken}:`; + const adminCookie = encodeURIComponent( + `${adminValue}.${yield* Effect.promise(() => makeSignature(adminValue, configuration.secret))}`, + ); + const recovery = { + actionId: 'expired-original-action', + createdAt: new Date('2026-08-09T00:00:00.000Z'), + impersonationSessionId, + originalAuthBindingId, + originalPrincipalId, + originalSessionId: restoredSessionId, + reason: 'Investigate support request', + targetPrincipalId, + tenantId, + }; + const { actionRuntime, deleted, service } = makeLostResponseRecoveryService( + recovery, + new Date('2000-01-01T00:00:00.000Z'), + ); + + const result = yield* service + .stop({ + idempotencyKey: 'stop-expired-lost-response', + requestHeaders: new Headers({ + cookie: `better-auth.admin_session=${adminCookie}`, + }), + }) + .pipe( + Effect.provideService( + SupportImpersonationCorrelationId, + 'correlation-expired-lost-response', + ), + ); -test('clears a mismatched restored session and completes recovery from the recorded original', async () => { - let deleted = false; - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ checkpoint: 'stopped', recorded: true }), - ]); - const service = makeService({ - actionRuntime: actionRuntime.runtime, - authentication: makeAuthenticationServiceDouble(), - configuration, - provider: makeSupportAuthProviderDouble({ - ...provider(true).api, - stopImpersonating: async () => { - const headers = new Headers(); - headers.append('set-cookie', 'better-auth.session_token=unexpected; Path=/; HttpOnly'); - return { - headers, - response: { session: { id: 'unexpected-restored-session' } }, - }; - }, + expect(result.active).toBe(false); + expect(result.checkpointPending).toBe(false); + expect(actionRuntime.invocationCount()).toBe(1); + expect(deleted()).toBe(true); + expect(result.setCookieHeaders.every((header) => header.includes('Max-Age=0'))).toBe(true); }), - resolver: makePrincipalResolverDouble(), - store: makeSupportImpersonationStoreDouble({ - deleteRecovery: () => - Effect.sync(() => { - deleted = true; - }), - insertRecovery: () => Effect.void, - }), - supportRecoveryPrincipal, - }); +); - const result = await runEffectTestPromise( - service - .stop({ - idempotencyKey: 'stop-mismatched-restore', - requestHeaders: new Headers(), - }) - .pipe( - Effect.provideService(SupportImpersonationCorrelationId, 'correlation-mismatched-restore'), - ), - ); +it.effect( + 'clears a mismatched restored session and completes recovery from the recorded original', + () => + Effect.gen(function* testProgram9() { + let deleted = false; + const actionRuntime = makeActionRuntimeDouble([ + actionSuccess({ checkpoint: 'stopped', recorded: true }), + ]); + const service = makeService({ + actionRuntime: actionRuntime.runtime, + authentication: makeAuthenticationServiceDouble(), + configuration, + provider: makeSupportAuthProviderDouble({ + ...provider(true).api, + stopImpersonating: () => { + const headers = new Headers(); + headers.append('set-cookie', 'better-auth.session_token=unexpected; Path=/; HttpOnly'); + return Promise.resolve({ + headers, + response: { session: { id: 'unexpected-restored-session' } }, + }); + }, + }), + resolver: makePrincipalResolverDouble(), + store: makeSupportImpersonationStoreDouble({ + deleteRecovery: () => + Effect.sync(() => { + deleted = true; + }), + insertRecovery: () => Effect.void, + }), + supportRecoveryPrincipal, + }); - expect(result.checkpointPending).toBe(false); - expect(actionRuntime.invocationCount()).toBe(1); - expect(deleted).toBe(true); - expect(result.setCookieHeaders.every((header) => header.includes('Max-Age=0'))).toBe(true); - expect(result.setCookieHeaders.some((header) => header.includes('unexpected'))).toBe(false); -}); + const result = yield* service + .stop({ + idempotencyKey: 'stop-mismatched-restore', + requestHeaders: new Headers(), + }) + .pipe( + Effect.provideService( + SupportImpersonationCorrelationId, + 'correlation-mismatched-restore', + ), + ); -test('deletes the impersonation session and clears cookies when original restoration fails', async () => { - let deleteCalls = 0; - const checkpointFailure = new ActionPermissionDenied({ - code: 'action_permission_denied', - reason: 'The stopped checkpoint was denied', - }); - const service = makeService({ - actionRuntime: makeActionRuntimeDouble([actionCoreFailure(checkpointFailure)]).runtime, - authentication: makeAuthenticationServiceDouble(), - configuration, - provider: makeSupportAuthProviderDouble({ - ...provider(true).api, - stopImpersonating: async () => { - throw new Error('admin session expired'); - }, + expect(result.checkpointPending).toBe(false); + expect(actionRuntime.invocationCount()).toBe(1); + expect(deleted).toBe(true); + expect(result.setCookieHeaders.every((header) => header.includes('Max-Age=0'))).toBe(true); + expect(result.setCookieHeaders.some((header) => header.includes('unexpected'))).toBe(false); }), - resolver: makePrincipalResolverDouble(), - store: makeSupportImpersonationStoreDouble({ - deleteSession: () => - Effect.sync(() => { - deleteCalls += 1; +); + +it.effect( + 'deletes the impersonation session and clears cookies when original restoration fails', + () => + Effect.gen(function* testProgram10() { + let deleteCalls = 0; + const checkpointFailure = new ActionPermissionDenied({ + code: 'action_permission_denied', + reason: 'The stopped checkpoint was denied', + }); + const service = makeService({ + actionRuntime: makeActionRuntimeDouble([actionCoreFailure(checkpointFailure)]).runtime, + authentication: makeAuthenticationServiceDouble(), + configuration, + provider: makeSupportAuthProviderDouble({ + ...provider(true).api, + stopImpersonating: () => Promise.reject(new Error('admin session expired')), }), - insertRecovery: () => Effect.void, - }), - supportRecoveryPrincipal, - }); + resolver: makePrincipalResolverDouble(), + store: makeSupportImpersonationStoreDouble({ + deleteSession: () => + Effect.sync(() => { + deleteCalls += 1; + }), + insertRecovery: () => Effect.void, + }), + supportRecoveryPrincipal, + }); - const result = await runEffectTestPromise( - service - .stop({ - idempotencyKey: 'stop-expired-original', - requestHeaders: new Headers(), - }) - .pipe( - Effect.provideService(SupportImpersonationCorrelationId, 'correlation-expired-original'), - ), - ); + const result = yield* service + .stop({ + idempotencyKey: 'stop-expired-original', + requestHeaders: new Headers(), + }) + .pipe( + Effect.provideService(SupportImpersonationCorrelationId, 'correlation-expired-original'), + ); - expect(result.active).toBe(false); - expect(result.checkpointPending).toBe(true); - expect(deleteCalls).toBe(1); - expect(result.setCookieHeaders.every((header) => header.includes('Max-Age=0'))).toBe(true); -}); + expect(result.active).toBe(false); + expect(result.checkpointPending).toBe(true); + expect(deleteCalls).toBe(1); + expect(result.setCookieHeaders.every((header) => header.includes('Max-Age=0'))).toBe(true); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/installed-module-catalog.test.ts b/app/apps/shell-super-app/tests/unit/installed-module-catalog.test.ts index 2587d5c89..b03fc931c 100644 --- a/app/apps/shell-super-app/tests/unit/installed-module-catalog.test.ts +++ b/app/apps/shell-super-app/tests/unit/installed-module-catalog.test.ts @@ -1,7 +1,5 @@ import { makeModuleContractFixture } from '../../../../packages/core-runtime/src/testing/module-contract.ts'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off preferSchemaOverJson:off -- Existing compatibility boundary; expires: 2026-12-31. -import { expect, test } from '@rstest/core'; +import { expect, it } from 'effect-rstest'; import { Effect, Predicate } from 'effect'; import type { DeploymentAllowlist } from '../../api/modules/deployment-allowlist.ts'; import { @@ -39,101 +37,184 @@ const response = (value: Value, init: ResponseInit = {}): Response => { }); }; -test('loads two independent deployment contracts once and preserves both identities', async () => { - const requests: string[] = []; - const documents = new Map([ - [ - 'https://property.example.test/.well-known/ontos-module-manifest.json', - contract('property-registry', 'property.registry'), - ], - [ - 'https://documents.example.test/.well-known/ontos-module-manifest.json', - contract('documents-center', 'documents.center'), - ], - ]); - const loader = makeInstalledModuleCatalogLoader( - allowlist([ - { appId: 'property-registry', contractUrl: [...documents.keys()][0] ?? '' }, - { appId: 'documents-center', contractUrl: [...documents.keys()][1] ?? '' }, - ]), - async (url, init) => { - const normalized = new Request(url).url; - requests.push(normalized); - expect(init?.redirect).toBe('manual'); - return response(documents.get(normalized)); - }, - ); - const [first, concurrent, cached] = await Promise.all([ - runEffectTestPromise(loader), - runEffectTestPromise(loader), - runEffectTestPromise(loader), - ]); - expect(first).toBe(concurrent); - expect(first).toBe(cached); - expect(requests).toHaveLength(2); - expect(first.moduleIds).toEqual(['documents.center', 'property.registry']); - expect(first.getByDeploymentAppId('property-registry')?.manifest.module.id).toBe( - 'property.registry', - ); - expect(first.getByModuleId('property.registry')?.deployment.appId).toBe('property-registry'); -}); - -test('keeps a healthy deployment available on cold start when another is unreachable', async () => { - const loader = makeInstalledModuleCatalogLoader( - allowlist([ - { - appId: 'property-registry', - contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', +it.effect('loads two independent deployment contracts once and preserves both identities', () => + Effect.gen(function* verifyCase1() { + const requests: string[] = []; + const documents = new Map([ + [ + 'https://property.example.test/.well-known/ontos-module-manifest.json', + contract('property-registry', 'property.registry'), + ], + [ + 'https://documents.example.test/.well-known/ontos-module-manifest.json', + contract('documents-center', 'documents.center'), + ], + ]); + const loader = makeInstalledModuleCatalogLoader( + allowlist([ + { appId: 'property-registry', contractUrl: [...documents.keys()][0] ?? '' }, + { appId: 'documents-center', contractUrl: [...documents.keys()][1] ?? '' }, + ]), + (url, init) => { + const normalized = new Request(url).url; + requests.push(normalized); + expect(init?.redirect).toBe('manual'); + return Promise.resolve(response(documents.get(normalized))); }, - { - appId: 'documents-center', - contractUrl: 'https://documents.example.test/.well-known/ontos-module-manifest.json', + ); + const [first, concurrent, cached] = yield* Effect.all([loader, loader, loader], { + concurrency: 'unbounded', + }); + expect(first).toBe(concurrent); + expect(first).toBe(cached); + expect(requests).toHaveLength(2); + expect(first.moduleIds).toEqual(['documents.center', 'property.registry']); + expect(first.getByDeploymentAppId('property-registry')?.manifest.module.id).toBe( + 'property.registry', + ); + expect(first.getByModuleId('property.registry')?.deployment.appId).toBe('property-registry'); + }), +); + +it.effect('keeps a healthy deployment available on cold start when another is unreachable', () => + Effect.gen(function* verifyCase2() { + const loader = makeInstalledModuleCatalogLoader( + allowlist([ + { + appId: 'property-registry', + contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', + }, + { + appId: 'documents-center', + contractUrl: 'https://documents.example.test/.well-known/ontos-module-manifest.json', + }, + ]), + (url) => { + const appId = new Request(url).url.includes('property') + ? 'property-registry' + : 'documents-center'; + if (appId === 'property-registry') { + return Promise.reject(new Error('deployment unreachable')); + } + return Promise.resolve(response(contract(appId, 'documents.center'))); }, - ]), - async (url) => { - const appId = new Request(url).url.includes('property') - ? 'property-registry' - : 'documents-center'; - if (appId === 'property-registry') { - throw new Error('deployment unreachable'); - } - return response(contract(appId, 'documents.center')); - }, - ); + ); - const catalog = await runEffectTestPromise(loader); + const catalog = yield* loader; - expect(catalog.moduleIds).toEqual(['documents.center']); - expect(catalog.deploymentStatuses).toEqual([ - { appId: 'documents-center', moduleId: 'documents.center', status: 'available' }, - { appId: 'property-registry', reason: 'unavailable', status: 'unavailable' }, - ]); -}); + expect(catalog.moduleIds).toEqual(['documents.center']); + expect(catalog.deploymentStatuses).toEqual([ + { appId: 'documents-center', moduleId: 'documents.center', status: 'available' }, + { appId: 'property-registry', reason: 'unavailable', status: 'unavailable' }, + ]); + }), +); -test.each([ - [ - 'unavailable', - async () => { - throw new Error('secret host failure'); - }, - 'unavailable', - ], - ['redirect', async () => response({}, { status: 302 }), 'unavailable'], +const unavailableResponses = [ + ['unavailable', () => Promise.reject(new Error('secret host failure')), 'unavailable'], + ['redirect', () => Promise.resolve(response({}, { status: 302 })), 'unavailable'], [ 'non-JSON', - async () => response('{}', { headers: { 'content-type': 'text/html' } }), + () => Promise.resolve(response('{}', { headers: { 'content-type': 'text/html' } })), 'incompatible', ], - ['malformed JSON', async () => response('{broken'), 'incompatible'], - ['invalid schema', async () => response({ schemaVersion: '0' }), 'incompatible'], + ['malformed JSON', () => Promise.resolve(response('{broken')), 'incompatible'], + ['invalid schema', () => Promise.resolve(response({ schemaVersion: '0' })), 'incompatible'], [ 'mismatched app', - async () => response(contract('documents-center', 'property.registry')), + () => Promise.resolve(response(contract('documents-center', 'property.registry'))), 'incompatible', ], -])( - 'reports a typed deployment status for %s responses', - async (_label, fetcher, expectedReason) => { +] as const; +for (const [label, fetcher, expectedReason] of unavailableResponses) { + it.effect(`reports a typed deployment status for ${label} responses`, () => + Effect.gen(function* verifyCase3() { + const loader = makeInstalledModuleCatalogLoader( + allowlist([ + { + appId: 'property-registry', + contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', + }, + ]), + fetcher, + ); + const catalog = yield* loader; + expect(catalog.moduleIds).toEqual([]); + expect(catalog.deploymentStatuses).toEqual([ + { appId: 'property-registry', reason: expectedReason, status: 'unavailable' }, + ]); + }), + ); +} + +it.live( + 'classifies oversized, timed-out, and duplicate-module deployments without caching failures', + () => + Effect.gen(function* verifyCase4() { + let attempts = 0; + const one: DeploymentAllowlist['entries'][number] = { + appId: 'property-registry', + contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', + }; + const oversized = makeInstalledModuleCatalogLoader( + allowlist([one]), + () => Promise.resolve(response('x'.repeat(64))), + { maxBytes: 32 }, + ); + expect(yield* oversized).toMatchObject({ + deploymentStatuses: [ + { appId: 'property-registry', reason: 'unavailable', status: 'unavailable' }, + ], + }); + + const timedOut = makeInstalledModuleCatalogLoader( + allowlist([one]), + (_url, init) => { + const pending = Promise.withResolvers(); + init?.signal?.addEventListener('abort', () => pending.reject(new Error('aborted')), { + once: true, + }); + return Promise.resolve(pending.promise); + }, + { timeoutMs: 10 }, + ); + expect(yield* timedOut).toMatchObject({ + deploymentStatuses: [ + { appId: 'property-registry', reason: 'timeout', status: 'unavailable' }, + ], + }); + + const duplicate = makeInstalledModuleCatalogLoader( + allowlist([ + one, + { + appId: 'documents-center', + contractUrl: 'https://documents.example.test/.well-known/ontos-module-manifest.json', + }, + ]), + (url) => { + attempts += 1; + return Promise.resolve( + new Request(url).url.includes('property') + ? response(contract('property-registry', 'shared.module')) + : response(contract('documents-center', 'shared.module')), + ); + }, + ); + expect(yield* duplicate).toMatchObject({ + deploymentStatuses: [ + { appId: 'documents-center', reason: 'incompatible', status: 'unavailable' }, + { appId: 'property-registry', reason: 'incompatible', status: 'unavailable' }, + ], + }); + yield* duplicate; + expect(attempts).toBe(4); + }), +); + +it.effect('recovers a deployment on a later read and caches only the fully healthy result', () => + Effect.gen(function* verifyCase5() { + let requests = 0; const loader = makeInstalledModuleCatalogLoader( allowlist([ { @@ -141,135 +222,62 @@ test.each([ contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', }, ]), - fetcher, + () => { + requests += 1; + if (requests === 1) { + return Promise.reject(new Error('temporarily unreachable')); + } + return Promise.resolve(response(contract('property-registry', 'property.registry'))); + }, ); - const catalog = await runEffectTestPromise(loader); - expect(catalog.moduleIds).toEqual([]); - expect(catalog.deploymentStatuses).toEqual([ - { appId: 'property-registry', reason: expectedReason, status: 'unavailable' }, - ]); - }, -); - -test('classifies oversized, timed-out, and duplicate-module deployments without caching failures', async () => { - let attempts = 0; - const one: DeploymentAllowlist['entries'][number] = { - appId: 'property-registry', - contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', - }; - const oversized = makeInstalledModuleCatalogLoader( - allowlist([one]), - async () => response('x'.repeat(64)), - { maxBytes: 32 }, - ); - await expect(runEffectTestPromise(oversized)).resolves.toMatchObject({ - deploymentStatuses: [ - { appId: 'property-registry', reason: 'unavailable', status: 'unavailable' }, - ], - }); - const timedOut = makeInstalledModuleCatalogLoader( - allowlist([one]), - async (_url, init) => { - const pending = Promise.withResolvers(); - init?.signal?.addEventListener('abort', () => pending.reject(new Error('aborted')), { - once: true, - }); - return await pending.promise; - }, - { timeoutMs: 10 }, - ); - await expect(runEffectTestPromise(timedOut)).resolves.toMatchObject({ - deploymentStatuses: [{ appId: 'property-registry', reason: 'timeout', status: 'unavailable' }], - }); + const degraded = yield* loader; + const recovered = yield* loader; + const cached = yield* loader; - const duplicate = makeInstalledModuleCatalogLoader( - allowlist([ - one, - { - appId: 'documents-center', - contractUrl: 'https://documents.example.test/.well-known/ontos-module-manifest.json', - }, - ]), - async (url) => { - attempts += 1; - return new Request(url).url.includes('property') - ? response(contract('property-registry', 'shared.module')) - : response(contract('documents-center', 'shared.module')); - }, - ); - await expect(runEffectTestPromise(duplicate)).resolves.toMatchObject({ - deploymentStatuses: [ - { appId: 'documents-center', reason: 'incompatible', status: 'unavailable' }, - { appId: 'property-registry', reason: 'incompatible', status: 'unavailable' }, - ], - }); - await runEffectTestPromise(duplicate); - expect(attempts).toBe(4); -}); + expect(degraded.deploymentStatuses).toEqual([ + { appId: 'property-registry', reason: 'unavailable', status: 'unavailable' }, + ]); + expect(recovered.deploymentStatuses).toEqual([ + { appId: 'property-registry', moduleId: 'property.registry', status: 'available' }, + ]); + expect(cached).toBe(recovered); + expect(requests).toBe(2); + }), +); -test('recovers a deployment on a later read and caches only the fully healthy result', async () => { - let requests = 0; - const loader = makeInstalledModuleCatalogLoader( - allowlist([ - { - appId: 'property-registry', - contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', - }, - ]), - async () => { +it.effect('recreates the complete cache by constructing a new deployment-revision Layer', () => + Effect.gen(function* verifyCase6() { + let requests = 0; + const fetcher = () => { requests += 1; - if (requests === 1) { - throw new Error('temporarily unreachable'); - } - return response(contract('property-registry', 'property.registry')); - }, - ); - - const degraded = await runEffectTestPromise(loader); - const recovered = await runEffectTestPromise(loader); - const cached = await runEffectTestPromise(loader); - - expect(degraded.deploymentStatuses).toEqual([ - { appId: 'property-registry', reason: 'unavailable', status: 'unavailable' }, - ]); - expect(recovered.deploymentStatuses).toEqual([ - { appId: 'property-registry', moduleId: 'property.registry', status: 'available' }, - ]); - expect(cached).toBe(recovered); - expect(requests).toBe(2); -}); - -test('recreates the complete cache by constructing a new deployment-revision Layer', async () => { - let requests = 0; - const fetcher = async () => { - requests += 1; - return response(contract('property-registry', 'property.registry')); - }; - const firstRevision = makeInstalledModuleCatalogLayer( - allowlist([ - { - appId: 'property-registry', - contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', - }, - ]), - fetcher, - ); - const secondRevision = makeInstalledModuleCatalogLayer( - Object.freeze({ - ...allowlist([ + return Promise.resolve(response(contract('property-registry', 'property.registry'))); + }; + const firstRevision = makeInstalledModuleCatalogLayer( + allowlist([ { appId: 'property-registry', contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', }, ]), - revision: 'revision-2', - }), - fetcher, - ); + fetcher, + ); + const secondRevision = makeInstalledModuleCatalogLayer( + Object.freeze({ + ...allowlist([ + { + appId: 'property-registry', + contractUrl: 'https://property.example.test/.well-known/ontos-module-manifest.json', + }, + ]), + revision: 'revision-2', + }), + fetcher, + ); - await runEffectTestPromise(installedModuleCatalog.pipe(Effect.provide(firstRevision))); - await runEffectTestPromise(installedModuleCatalog.pipe(Effect.provide(firstRevision))); - await runEffectTestPromise(installedModuleCatalog.pipe(Effect.provide(secondRevision))); - expect(requests).toBe(2); -}); + yield* installedModuleCatalog.pipe(Effect.provide(firstRevision)); + yield* installedModuleCatalog.pipe(Effect.provide(firstRevision)); + yield* installedModuleCatalog.pipe(Effect.provide(secondRevision)); + expect(requests).toBe(2); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/installed-outbox-matcher.test.ts b/app/apps/shell-super-app/tests/unit/installed-outbox-matcher.test.ts index d117af760..ae17d4a95 100644 --- a/app/apps/shell-super-app/tests/unit/installed-outbox-matcher.test.ts +++ b/app/apps/shell-super-app/tests/unit/installed-outbox-matcher.test.ts @@ -1,6 +1,5 @@ import { makeModuleContractFixture } from '../../../../packages/core-runtime/src/testing/module-contract.ts'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { expect, test } from '@rstest/core'; +import { expect, it } from 'effect-rstest'; import { buildInstalledModuleCatalog } from '@app/core-runtime'; import { Effect } from 'effect'; import { matchInstalledOutboxMessagesOnce } from '../../api/modules/installed-outbox-matcher.ts'; @@ -8,35 +7,35 @@ import { matchInstalledOutboxMessagesOnce } from '../../api/modules/installed-ou const contract = (appId: string, moduleId: string, outboxSubscriptions: readonly object[] = []) => makeModuleContractFixture({ appId, moduleId, outboxSubscriptions }); -test('passes a dormant subscription with an absent producer to Core matching', async () => { - const subscription = { - consumerModuleKey: 'property.registry', - entrypoint: { - access: 'background', - authorization: { kind: 'owner_local_background' }, - entrypointKey: 'property.registry.document-projector', - moduleKey: 'property.registry', - role: 'worker', - scope: 'tenant', - }, - producerModuleKey: 'documents.center', - topic: 'documents.center.created', - workerKey: 'property.registry.document-projector', - } as const; - const catalog = buildInstalledModuleCatalog([ - { - contract: contract('property-registry', 'property.registry', [subscription]), - expectedAppId: 'property-registry', - }, - ]); - let received: unknown; - const result = await runEffectTestPromise( - matchInstalledOutboxMessagesOnce(catalog, (input) => { +it.effect('passes a dormant subscription with an absent producer to Core matching', () => + Effect.gen(function* verifyCase1() { + const subscription = { + consumerModuleKey: 'property.registry', + entrypoint: { + access: 'background', + authorization: { kind: 'owner_local_background' }, + entrypointKey: 'property.registry.document-projector', + moduleKey: 'property.registry', + role: 'worker', + scope: 'tenant', + }, + producerModuleKey: 'documents.center', + topic: 'documents.center.created', + workerKey: 'property.registry.document-projector', + } as const; + const catalog = buildInstalledModuleCatalog([ + { + contract: contract('property-registry', 'property.registry', [subscription]), + expectedAppId: 'property-registry', + }, + ]); + let received: unknown; + const result = yield* matchInstalledOutboxMessagesOnce(catalog, (input) => { received = input.subscriptions; return Effect.succeed({ deliveriesCreated: 1, messagesMatched: 1 }); - }), - ); + }); - expect(received).toEqual([subscription]); - expect(result).toEqual({ deliveriesCreated: 1, messagesMatched: 1 }); -}); + expect(received).toEqual([subscription]); + expect(result).toEqual({ deliveriesCreated: 1, messagesMatched: 1 }); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/installed-verticals.test.ts b/app/apps/shell-super-app/tests/unit/installed-verticals.test.ts index 1ca2ca6e7..5d8bcfd16 100644 --- a/app/apps/shell-super-app/tests/unit/installed-verticals.test.ts +++ b/app/apps/shell-super-app/tests/unit/installed-verticals.test.ts @@ -1,6 +1,5 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import fs from 'node:fs'; -import { expect, test } from '@rstest/core'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema } from 'effect'; import { deriveInstalledVerticalIds, @@ -9,52 +8,57 @@ import { } from '../../api/verticals/installed-verticals.ts'; import { DeploymentAllowlistTopologySchema } from '../../api/modules/deployment-allowlist.ts'; -test('derives installed vertical IDs from the injected topology without hardcoded registrations', async () => { - const topology = Schema.decodeUnknownSync(DeploymentAllowlistTopologySchema)( - JSON.parse( - fs.readFileSync( - new URL('../../../../topology/reference-topology.json', import.meta.url), - 'utf-8', - ), - ), - ); - const expectedInstalledIds = await runEffectTestPromise(deriveInstalledVerticalIds(topology)); +it.effect( + 'derives installed vertical IDs from the injected topology without hardcoded registrations', + () => + Effect.gen(function* verifyCase1() { + const topology = Schema.decodeUnknownSync(DeploymentAllowlistTopologySchema)( + JSON.parse( + fs.readFileSync( + new URL('../../../../topology/reference-topology.json', import.meta.url), + 'utf-8', + ), + ), + ); + const expectedInstalledIds = yield* deriveInstalledVerticalIds(topology); - expect([...expectedInstalledIds]).toEqual(['party-registry']); - expect(expectedInstalledIds.has('party.registry')).toBe(false); - expect([...(await runEffectTestPromise(installedVerticalIds))]).toEqual([ - ...expectedInstalledIds, - ]); - const valid = await runEffectTestPromise( - deriveInstalledVerticalIds({ - sharedPackages: [{ id: 'shared-contracts', kind: 'package' }], - shell: { id: 'shell-super-app', kind: 'shell' }, - verticals: [ - { id: 'property-registry', kind: 'vertical' }, - { id: 'future-generated', kind: 'vertical' }, - ], + expect([...expectedInstalledIds]).toEqual(['party-registry']); + expect(expectedInstalledIds.has('party.registry')).toBe(false); + expect([...(yield* installedVerticalIds)]).toEqual([...expectedInstalledIds]); + const valid = yield* deriveInstalledVerticalIds({ + sharedPackages: [{ id: 'shared-contracts', kind: 'package' }], + shell: { id: 'shell-super-app', kind: 'shell' }, + verticals: [ + { id: 'property-registry', kind: 'vertical' }, + { id: 'future-generated', kind: 'vertical' }, + ], + }); + expect([...valid]).toEqual(['property-registry', 'future-generated']); + expect(valid.has('property.registry')).toBe(false); }), - ); - expect([...valid]).toEqual(['property-registry', 'future-generated']); - expect(valid.has('property.registry')).toBe(false); -}); +); -test('rejects malformed, non-vertical, invalid, and duplicate installed entries', async () => { - const inputs = [ - {}, - { verticals: [{ id: 'shell-super-app', kind: 'shell' }] }, - { verticals: [{ id: '../inventory', kind: 'vertical' }] }, - { - verticals: [ - { id: 'inventory-stock', kind: 'vertical' }, - { id: 'inventory-stock', kind: 'vertical' }, - ], - }, - ]; - const errors = await Promise.all( - inputs.map( - async (input) => await runEffectTestPromise(Effect.flip(deriveInstalledVerticalIds(input))), - ), - ); - expect(errors.every(Schema.is(InstalledVerticalTopologyError))).toBe(true); -}); +it.effect('rejects malformed, non-vertical, invalid, and duplicate installed entries', () => + Effect.gen(function* verifyCase2() { + const inputs = [ + {}, + { verticals: [{ id: 'shell-super-app', kind: 'shell' }] }, + { verticals: [{ id: '../inventory', kind: 'vertical' }] }, + { + verticals: [ + { id: 'inventory-stock', kind: 'vertical' }, + { id: 'inventory-stock', kind: 'vertical' }, + ], + }, + ]; + const errors = yield* Effect.all( + inputs.map((input) => + Effect.gen(function* verifyCase3() { + return yield* Effect.flip(deriveInstalledVerticalIds(input)); + }), + ), + { concurrency: 'unbounded' }, + ); + expect(errors.every(Schema.is(InstalledVerticalTopologyError))).toBe(true); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/layout.test.tsx b/app/apps/shell-super-app/tests/unit/layout.test.tsx index 9bb964656..fcf0e24a2 100644 --- a/app/apps/shell-super-app/tests/unit/layout.test.tsx +++ b/app/apps/shell-super-app/tests/unit/layout.test.tsx @@ -1,10 +1,11 @@ -import { afterEach, expect, rstest, test } from '@rstest/core'; +import { afterEach, expect, it, rstest, test } from 'effect-rstest'; import { cleanup, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Menu as ActualMenu } from '@techsio/ui-kit/molecules/menu' with { rstest: 'importActual' }; import { Select as ActualSelect } from '@techsio/ui-kit/molecules/select' with { rstest: 'importActual', }; +import { Effect } from 'effect'; import type { ComponentProps, ReactNode } from 'react'; import { AppIdSchema } from '../../shared/api'; import Layout from '../../src/routes/layout'; @@ -290,100 +291,106 @@ test('shows failed installed deployments as disabled identities with typed reaso expect(screen.queryByRole('link', { name: 'legacy-center' })).toBeNull(); }); -test('renders the account Menu last and dispatches only the logout command by keyboard', async () => { - const onLogout = rstest.fn(); - const user = userEvent.setup(); - render( - - Content - , - ); - - const header = document.querySelector('header[aria-label="Dashboard header"]'); - const trigger = screen.getByRole('button', { name: 'Ada Lovelace' }); - expect(header?.lastElementChild?.contains(trigger)).toBe(true); - const accountMenu = header?.lastElementChild; - expect(accountMenu instanceof HTMLElement ? accountMenu.dataset['position'] : undefined).toBe( - 'end', - ); - - trigger.focus(); - await user.keyboard('{Enter}'); - const commands = await screen.findAllByRole('menuitem'); - expect(commands).toHaveLength(1); - expect(commands[0]?.textContent).toBe('Logout'); - await user.keyboard('{ArrowDown}{Enter}'); - expect(onLogout).toHaveBeenCalledTimes(1); +it.effect('renders the account Menu last and dispatches only the logout command by keyboard', () => + Effect.gen(function* accountKeyboardLogout() { + const onLogout = rstest.fn(); + const user = userEvent.setup(); + render( + + Content + , + ); - accountMenuSelectHandlers.at(-1)?.({ value: 'unexpected' }); - expect(onLogout).toHaveBeenCalledTimes(1); -}); + const header = document.querySelector('header[aria-label="Dashboard header"]'); + const trigger = screen.getByRole('button', { name: 'Ada Lovelace' }); + expect(header?.lastElementChild?.contains(trigger)).toBe(true); + const accountMenu = header?.querySelector(':scope > :last-child'); + expect(accountMenu?.dataset['position']).toBe('end'); + + trigger.focus(); + yield* Effect.promise(() => user.keyboard('{Enter}')); + const commands = yield* Effect.promise(() => screen.findAllByRole('menuitem')); + expect(commands).toHaveLength(1); + expect(commands[0]?.textContent).toBe('Logout'); + yield* Effect.promise(() => user.keyboard('{ArrowDown}{Enter}')); + expect(onLogout).toHaveBeenCalledTimes(1); + + accountMenuSelectHandlers.at(-1)?.({ value: 'unexpected' }); + expect(onLogout).toHaveBeenCalledTimes(1); + }), +); -test('retains the account trigger and disables the sole command while logout is pending', async () => { - const onLogout = rstest.fn(); - const user = userEvent.setup(); - render( - - Content - , - ); +it.effect('retains the account trigger and disables the sole command while logout is pending', () => + Effect.gen(function* pendingLogoutCommand() { + const onLogout = rstest.fn(); + const user = userEvent.setup(); + render( + + Content + , + ); - const trigger = screen.getByRole('button', { name: 'Ada Lovelace' }); - await user.click(trigger); - const command = await screen.findByRole('menuitem', { name: 'Logging out…' }); - expect(command.getAttribute('aria-disabled')).toBe('true'); - await user.click(command); - expect(onLogout).not.toHaveBeenCalled(); -}); + const trigger = screen.getByRole('button', { name: 'Ada Lovelace' }); + yield* Effect.promise(() => user.click(trigger)); + const command = yield* Effect.promise(() => + screen.findByRole('menuitem', { name: 'Logging out…' }), + ); + expect(command.getAttribute('aria-disabled')).toBe('true'); + yield* Effect.promise(() => user.click(command)); + expect(onLogout).not.toHaveBeenCalled(); + }), +); -test('renders complete ordered tenant items and dispatches keyboard selection once', async () => { - const onTenantChange = rstest.fn(); - const user = userEvent.setup(); - render( - - Content - , - ); +it.effect('renders complete ordered tenant items and dispatches keyboard selection once', () => + Effect.gen(function* tenantKeyboardSelection() { + const onTenantChange = rstest.fn(); + const user = userEvent.setup(); + render( + + Content + , + ); - const trigger = screen.getByRole('combobox', { name: 'Current tenant' }); - await user.click(trigger); - const options = await screen.findAllByRole('option'); - expect(options.map((option) => option.textContent)).toEqual(['Alpha tenant', 'Zeta tenant']); - expect(options.map((option) => option.dataset['value'])).toEqual(['tenant-1', 'tenant-2']); - expect(options.every((option) => option.querySelector('span') !== null)).toBe(true); - await user.keyboard('{ArrowDown}{Enter}'); - expect(onTenantChange).toHaveBeenCalledWith('tenant-2'); - expect(onTenantChange).toHaveBeenCalledTimes(1); - - tenantValueChangeHandlers.at(-1)?.({ - items: [tenantProps.tenantChoices[0]], - value: ['tenant-1'], - }); - tenantValueChangeHandlers.at(-1)?.({ items: [], value: [] }); - expect(onTenantChange).toHaveBeenCalledTimes(1); -}); + const trigger = screen.getByRole('combobox', { name: 'Current tenant' }); + yield* Effect.promise(() => user.click(trigger)); + const options = yield* Effect.promise(() => screen.findAllByRole('option')); + expect(options.map((option) => option.textContent)).toEqual(['Alpha tenant', 'Zeta tenant']); + expect(options.map((option) => option.dataset['value'])).toEqual(['tenant-1', 'tenant-2']); + expect(options.every((option) => option.querySelector('span') !== null)).toBe(true); + yield* Effect.promise(() => user.keyboard('{ArrowDown}{Enter}')); + expect(onTenantChange).toHaveBeenCalledWith('tenant-2'); + expect(onTenantChange).toHaveBeenCalledTimes(1); + + tenantValueChangeHandlers.at(-1)?.({ + items: [tenantProps.tenantChoices[0]], + value: ['tenant-1'], + }); + tenantValueChangeHandlers.at(-1)?.({ items: [], value: [] }); + expect(onTenantChange).toHaveBeenCalledTimes(1); + }), +); test('disables unavailable, one-choice, and pending tenant states with associated feedback', () => { const { rerender } = render( diff --git a/app/apps/shell-super-app/tests/unit/legal-entity-selection.test.ts b/app/apps/shell-super-app/tests/unit/legal-entity-selection.test.ts index 94fe34ba9..02824ee32 100644 --- a/app/apps/shell-super-app/tests/unit/legal-entity-selection.test.ts +++ b/app/apps/shell-super-app/tests/unit/legal-entity-selection.test.ts @@ -1,9 +1,7 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import { test } from '@rstest/core'; +import { expect, it } from 'effect-rstest'; import { ContextAccess, LegalEntityContext } from '@app/core-runtime'; import type { ContextAccessService, LegalEntityContextService } from '@app/core-runtime'; -import { Effect, Layer } from 'effect'; +import { Effect, Layer, Predicate } from 'effect'; import { resolveAuthorizedLegalEntities, validateAuthorizedLegalEntity, @@ -54,17 +52,15 @@ const provideSelectionServices = ( ), ); -test('auto-selects the only authorized entity and preserves an exact saved choice', async () => { - const only = await runEffectTestPromise( - provideSelectionServices( +it.effect('auto-selects the only authorized entity and preserves an exact saved choice', () => + Effect.gen(function* verifyCase1() { + const only = yield* provideSelectionServices( resolveAuthorizedLegalEntities({ principalId, tenantId }), context(), access({ [alpha.legalEntityId]: 'allowed' }), - ), - ); - assert.deepEqual(only, { available: [alpha], selected: alpha, state: 'selected' }); - const saved = await runEffectTestPromise( - provideSelectionServices( + ); + expect(only).toEqual({ available: [alpha], selected: alpha, state: 'selected' }); + const saved = yield* provideSelectionServices( resolveAuthorizedLegalEntities({ principalId, savedLegalEntityId: beta.legalEntityId, @@ -72,48 +68,42 @@ test('auto-selects the only authorized entity and preserves an exact saved choic }), context(), access({ [alpha.legalEntityId]: 'allowed', [beta.legalEntityId]: 'allowed' }), - ), - ); - assert.deepEqual(saved, { available: [alpha, beta], selected: beta, state: 'selected' }); -}); + ); + expect(saved).toEqual({ available: [alpha, beta], selected: beta, state: 'selected' }); + }), +); -test('requires a choice for several entities and blocks zero definite grants', async () => { - assert.deepEqual( - await runEffectTestPromise( - provideSelectionServices( +it.effect('requires a choice for several entities and blocks zero definite grants', () => + Effect.gen(function* verifyCase2() { + expect( + yield* provideSelectionServices( resolveAuthorizedLegalEntities({ principalId, tenantId }), context(), access({ [alpha.legalEntityId]: 'allowed', [beta.legalEntityId]: 'allowed' }), ), - ), - { available: [alpha, beta], state: 'selection_required' }, - ); - assert.deepEqual( - await runEffectTestPromise( - provideSelectionServices( + ).toEqual({ available: [alpha, beta], state: 'selection_required' }); + expect( + yield* provideSelectionServices( resolveAuthorizedLegalEntities({ principalId, tenantId }), context(), access({}), ), - ), - { available: [], state: 'access_blocked' }, - ); -}); + ).toEqual({ available: [], state: 'access_blocked' }); + }), +); -test('fails closed for authorization uncertainty and validates a switch independently', async () => { - const unavailable = await runEffectTestPromise( - Effect.flip( +it.effect('fails closed for authorization uncertainty and validates a switch independently', () => + Effect.gen(function* verifyCase3() { + const unavailable = yield* Effect.flip( provideSelectionServices( resolveAuthorizedLegalEntities({ principalId, tenantId }), context(), access({ [alpha.legalEntityId]: 'unavailable' }), ), - ), - ); - assert.equal(unavailable._tag, 'LegalEntitySelectionUnavailableError'); - assert.deepEqual( - await runEffectTestPromise( - provideSelectionServices( + ); + expect(Predicate.isTagged(unavailable, 'LegalEntitySelectionUnavailableError')).toBe(true); + expect( + yield* provideSelectionServices( validateAuthorizedLegalEntity({ legalEntityId: beta.legalEntityId, principalId, @@ -122,7 +112,6 @@ test('fails closed for authorization uncertainty and validates a switch independ context(), access({ [beta.legalEntityId]: 'allowed' }), ), - ), - beta, - ); -}); + ).toEqual(beta); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/module-entrypoint-loader.test.ts b/app/apps/shell-super-app/tests/unit/module-entrypoint-loader.test.ts index 1582e3c7f..8596179db 100644 --- a/app/apps/shell-super-app/tests/unit/module-entrypoint-loader.test.ts +++ b/app/apps/shell-super-app/tests/unit/module-entrypoint-loader.test.ts @@ -1,5 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { expect, test } from '@rstest/core'; +import { expect, it } from 'effect-rstest'; import { Clock, Effect, Fiber, Function as Fn, Match, Predicate, Schema } from 'effect'; import { TestClock } from 'effect/testing'; import { @@ -23,6 +22,8 @@ import { settleModuleEntrypointLoads, } from '../../src/routes/module-entrypoint-loader.ts'; +const remoteDefault = () => null; + const trustedContext: TrustedPrincipalContext = { authMethod: 'session', principalId: '10000000-0000-4000-8000-000000000001', @@ -117,71 +118,60 @@ const component = defineTenantModuleEntrypoint({ const compatibleRemoteModule = (value: { readonly default: unknown }) => Predicate.isFunction(value.default); -type EffectTestCallback = () => Promise; - -const runEffectTest = (effect: Effect.Effect): EffectTestCallback => - Fn.flow(Fn.constant(effect), runEffectTestPromise); - -test( - 'prepares one complete trusted composition and invokes allowed lazy loaders', - runEffectTest( - Effect.gen(function* verifyCompleteComposition() { - let batches = 0; - let loads = 0; - const gateway = makeFakeGateway({ - onPrepare: (entrypoints) => { - batches += 1; - expect(entrypoints).toEqual([page, component]); - }, - }); - const result = yield* loadModuleEntrypointComposition( +it.effect('prepares one complete trusted composition and invokes allowed lazy loaders', () => + Effect.gen(function* verifyCompleteComposition() { + let batches = 0; + let loads = 0; + const gateway = makeFakeGateway({ + onPrepare: (entrypoints) => { + batches += 1; + expect(entrypoints).toEqual([page, component]); + }, + }); + const result = yield* loadModuleEntrypointComposition( + gateway, + trustedContext, + [page, component].map((entrypoint) => ({ + authorize: Effect.void, + entrypoint, + load: Effect.sync(() => { + loads += 1; + return `loaded-${loads}`; + }), + })), + ); + expect(result).toEqual(['loaded-1', 'loaded-2']); + expect(batches).toBe(1); + }), +); + +it.effect('checks the complete composition before authorizing or invoking any loader', () => + Effect.gen(function* verifyDeniedComposition() { + let authorizations = 0; + let loads = 0; + const gateway = makeFakeGateway({ + deniedEntrypointKeys: new Set([component.entrypointKey]), + }); + const error = yield* Effect.flip( + loadModuleEntrypointComposition( gateway, trustedContext, [page, component].map((entrypoint) => ({ - authorize: Effect.void, + authorize: Effect.sync(() => { + authorizations += 1; + }), entrypoint, load: Effect.sync(() => { loads += 1; - return `loaded-${loads}`; + return loads; }), })), - ); - expect(result).toEqual(['loaded-1', 'loaded-2']); - expect(batches).toBe(1); - }), - ), -); - -test( - 'checks the complete composition before authorizing or invoking any loader', - runEffectTest( - Effect.gen(function* verifyDeniedComposition() { - let authorizations = 0; - let loads = 0; - const gateway = makeFakeGateway({ - deniedEntrypointKeys: new Set([component.entrypointKey]), - }); - const error = yield* Effect.flip( - loadModuleEntrypointComposition( - gateway, - trustedContext, - [page, component].map((entrypoint) => ({ - authorize: Effect.sync(() => { - authorizations += 1; - }), - entrypoint, - load: Effect.sync(() => { - loads += 1; - return loads; - }), - })), - ), - ); - expect(error).toMatchObject({ _tag: 'ModuleStateDeniedError' }); - expect(authorizations).toBe(0); - expect(loads).toBe(0); - }), - ), + ), + ); + expect(Schema.is(ModuleStateDeniedError)(error)).toBe(true); + expect(authorizations).toBe(0); + expect(loads).toBe(0); + }), ); class RemoteLoadUnavailable extends Schema.TaggedError()( @@ -204,41 +194,38 @@ const mapFakeUnavailableUiState = ( Match.exhaustive, ); -test( - 'preserves typed gate and remote-load failures for exhaustive UI mapping', - runEffectTest( - Effect.gen(function* verifyTypedFailures() { - const gateFailure = yield* Effect.flip( - loadModuleEntrypointComposition(makeFakeGateway({ unavailable: true }), trustedContext, [ - { authorize: Effect.void, entrypoint: page, load: Effect.succeed('unreachable') }, - ]), - ); - expect(mapFakeUnavailableUiState(gateFailure)).toBe('unavailable'); - - const remoteFailure = yield* Effect.flip( - loadModuleEntrypointComposition(makeFakeGateway(), trustedContext, [ - { - authorize: Effect.void, - entrypoint: page, - load: Effect.fail(new RemoteLoadUnavailable()), - }, - ]), - ); - expect(remoteFailure).toEqual(new RemoteLoadUnavailable()); - expect(mapFakeUnavailableUiState(remoteFailure)).toBe('unavailable'); - }), - ), +it.effect('preserves typed gate and remote-load failures for exhaustive UI mapping', () => + Effect.gen(function* verifyTypedFailures() { + const gateFailure = yield* Effect.flip( + loadModuleEntrypointComposition(makeFakeGateway({ unavailable: true }), trustedContext, [ + { authorize: Effect.void, entrypoint: page, load: Effect.succeed('unreachable') }, + ]), + ); + expect(mapFakeUnavailableUiState(gateFailure)).toBe('unavailable'); + + const remoteFailure = yield* Effect.flip( + loadModuleEntrypointComposition(makeFakeGateway(), trustedContext, [ + { + authorize: Effect.void, + entrypoint: page, + load: Effect.fail(new RemoteLoadUnavailable()), + }, + ]), + ); + expect(remoteFailure).toEqual(new RemoteLoadUnavailable()); + expect(mapFakeUnavailableUiState(remoteFailure)).toBe('unavailable'); + }), ); -test( +it.live( 'settles browser entrypoint success, rejection, incompatibility, and timeout independently', - runEffectTest( + () => Effect.gen(function* verifySettledLoads() { const pending = Promise.withResolvers<{ readonly default: () => null }>(); const [ready, unavailable, incompatible, timedOut] = yield* Effect.all( [ settleModuleEntrypointLoad( - Fn.constant(Promise.resolve({ default: () => null })), + Fn.constant(Promise.resolve({ default: remoteDefault })), compatibleRemoteModule, 50, ), @@ -262,120 +249,126 @@ test( expect(incompatible).toEqual({ reason: 'incompatible', state: 'unavailable' }); expect(timedOut).toEqual({ reason: 'timeout', state: 'unavailable' }); }), - ), ); -test( - 'settles several browser entrypoints without one failure hiding healthy loads', - runEffectTest( - Effect.gen(function* verifySettledLoadCollection() { - const results = yield* settleModuleEntrypointLoads([ - { - identity: 'documents-center/page', - isCompatible: compatibleRemoteModule, - load: Fn.constant(Promise.resolve({ default: () => null })), - timeoutMs: 50, - }, - { - identity: 'property-registry/page', - isCompatible: compatibleRemoteModule, - load: async () => { - throw new Error('remote unavailable'); - }, - timeoutMs: 50, - }, - { - identity: 'throwing-validator/page', - isCompatible: () => { - throw new TypeError('malformed runtime value'); - }, - load: Fn.constant(Promise.resolve({ default: () => null })), - timeoutMs: 50, - }, - ]); +it.effect('settles several browser entrypoints without one failure hiding healthy loads', () => + Effect.gen(function* verifySettledLoadCollection() { + const results = yield* settleModuleEntrypointLoads([ + { + identity: 'documents-center/page', + isCompatible: compatibleRemoteModule, + load: Fn.constant(Promise.resolve({ default: remoteDefault })), + timeoutMs: 50, + }, + { + identity: 'property-registry/page', + isCompatible: compatibleRemoteModule, - expect(results).toEqual([ - { - identity: 'documents-center/page', - state: 'ready', - value: expect.objectContaining({ default: expect.any(Function) }), - }, - { - identity: 'property-registry/page', - reason: 'unavailable', - state: 'unavailable', - }, - { - identity: 'throwing-validator/page', - reason: 'incompatible', - state: 'unavailable', + load: () => Promise.reject(new Error('remote unavailable')), + timeoutMs: 50, + }, + { + identity: 'throwing-validator/page', + isCompatible: () => { + throw new TypeError('malformed runtime value'); }, - ]); - }), - ), + load: Fn.constant(Promise.resolve({ default: remoteDefault })), + timeoutMs: 50, + }, + ]); + + expect(results).toEqual([ + { + identity: 'documents-center/page', + state: 'ready', + value: expect.objectContaining({ default: expect.any(Function) }), + }, + { + identity: 'property-registry/page', + reason: 'unavailable', + state: 'unavailable', + }, + { + identity: 'throwing-validator/page', + reason: 'incompatible', + state: 'unavailable', + }, + ]); + }), ); interface RemoteModule { readonly default: () => null; } -test('never starts a queued load whose deadline expired before a permit became available', async () => { - const pendingLoads: PromiseWithResolvers[] = []; - const firstWindowStarted = Promise.withResolvers(); - let loadCount = 0; - const resultsPromise = runEffectTestPromise( - settleModuleEntrypointLoads( +it.live('never starts a queued load whose deadline expired before a permit became available', () => + Effect.gen(function* verifyCase1() { + const pendingLoads: PromiseWithResolvers[] = []; + const firstWindowStarted = Promise.withResolvers(); + let loadCount = 0; + const resultsFiber = yield* Effect.forkChild( + settleModuleEntrypointLoads( + Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ + identity: `module-${index}/page`, + isCompatible: compatibleRemoteModule, + + load: () => { + const pending = Promise.withResolvers(); + pendingLoads.push(pending); + loadCount += 1; + if (loadCount === MODULE_LOAD_CONCURRENCY) { + firstWindowStarted.resolve(null); + } + return Promise.resolve(pending.promise); + }, + // The first window must outlive runner jitter so every slot is really held; only the + // queued load carries the short deadline that expires before any permit frees up. + timeoutMs: index < MODULE_LOAD_CONCURRENCY ? 500 : 10, + })), + ), + ); + + yield* Effect.promise(() => firstWindowStarted.promise); + expect(loadCount).toBe(MODULE_LOAD_CONCURRENCY); + expect(pendingLoads).toHaveLength(MODULE_LOAD_CONCURRENCY); + + const results = yield* Fiber.join(resultsFiber); + expect(results).toEqual( Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ identity: `module-${index}/page`, - isCompatible: compatibleRemoteModule, - load: async () => { - const pending = Promise.withResolvers(); - pendingLoads.push(pending); - loadCount += 1; - if (loadCount === MODULE_LOAD_CONCURRENCY) { - firstWindowStarted.resolve(null); - } - return await pending.promise; - }, - // The first window must outlive runner jitter so every slot is really held; only the - // queued load carries the short deadline that expires before any permit frees up. - timeoutMs: index < MODULE_LOAD_CONCURRENCY ? 500 : 10, + reason: 'timeout', + state: 'unavailable', })), - ), - ); - - await firstWindowStarted.promise; - expect(loadCount).toBe(MODULE_LOAD_CONCURRENCY); - expect(pendingLoads).toHaveLength(MODULE_LOAD_CONCURRENCY); + ); + expect(loadCount).toBe(MODULE_LOAD_CONCURRENCY); - const results = await resultsPromise; - expect(results).toEqual( - Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ - identity: `module-${index}/page`, - reason: 'timeout', - state: 'unavailable', - })), - ); - expect(loadCount).toBe(MODULE_LOAD_CONCURRENCY); - - for (const pending of pendingLoads) { - pending.resolve({ default: () => null }); - } - await Promise.all(pendingLoads.map(async ({ promise }) => await promise)); - await runEffectTestPromise(Effect.yieldNow); - expect(loadCount).toBe(MODULE_LOAD_CONCURRENCY); -}); + for (const pending of pendingLoads) { + pending.resolve({ default: () => null }); + } + yield* Effect.all( + pendingLoads.map(({ promise }) => + Effect.gen(function* verifyCase2() { + return yield* Effect.promise(() => promise); + }), + ), + { concurrency: 'unbounded' }, + ); + yield* Effect.yieldNow; + expect(loadCount).toBe(MODULE_LOAD_CONCURRENCY); + }), +); -test('never starts an expired queued load when synchronous work delays deadline timers', async () => { - const started: number[] = []; - // Use the live clock: TestClock would not advance while the JavaScript thread is blocked. - const results = await runEffectTestPromise( - Clock.clockWith((clock) => +it.live('never starts an expired queued load when synchronous work delays deadline timers', () => + Effect.gen(function* verifyCase3() { + const started: number[] = []; + // Use the live clock: TestClock would not advance while the JavaScript thread is blocked. + const results = yield* Clock.clockWith((clock) => settleModuleEntrypointLoads( Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ identity: `module-${index}/page`, isCompatible: compatibleRemoteModule, - load: async () => { + + load: () => { started.push(index); if (index === 0) { const unblockAt = clock.currentTimeMillisUnsafe() + 40; @@ -384,68 +377,63 @@ test('never starts an expired queued load when synchronous work delays deadline // Intentionally keep the event loop blocked. } } - return { default: () => null }; + return Promise.resolve({ default: remoteDefault }); }, timeoutMs: index === MODULE_LOAD_CONCURRENCY ? 10 : 5000, })), ), - ), - ); + ); - expect(started).toContain(0); - expect(started).not.toContain(MODULE_LOAD_CONCURRENCY); - expect(results[MODULE_LOAD_CONCURRENCY]).toEqual({ - identity: `module-${MODULE_LOAD_CONCURRENCY}/page`, - reason: 'timeout', - state: 'unavailable', - }); -}); + expect(started).toContain(0); + expect(started).not.toContain(MODULE_LOAD_CONCURRENCY); + expect(results[MODULE_LOAD_CONCURRENCY]).toEqual({ + identity: `module-${MODULE_LOAD_CONCURRENCY}/page`, + reason: 'timeout', + state: 'unavailable', + }); + }), +); -test( - 'abandons queued loads when the caller is interrupted', - runEffectTest( - Effect.gen(function* verifyInterruptedCaller() { - const pendingLoads = Array.from({ length: MODULE_LOAD_CONCURRENCY }, () => - Promise.withResolvers(), - ); - const firstWindowStarted = Promise.withResolvers(); - const started: number[] = []; - const caller = yield* Effect.forkChild( - settleModuleEntrypointLoads( - Array.from({ length: 12 }, (_, index) => ({ - identity: `module-${index}/page`, - isCompatible: compatibleRemoteModule, - load: async () => { - started.push(index); - if (started.length === MODULE_LOAD_CONCURRENCY) { - firstWindowStarted.resolve(null); - } - return await (pendingLoads[index]?.promise ?? - Promise.resolve({ default: () => null })); - }, - })), - ), - ); +it.effect('abandons queued loads when the caller is interrupted', () => + Effect.gen(function* verifyInterruptedCaller() { + const pendingLoads = Array.from({ length: MODULE_LOAD_CONCURRENCY }, () => + Promise.withResolvers(), + ); + const firstWindowStarted = Promise.withResolvers(); + const started: number[] = []; + const caller = yield* Effect.forkChild( + settleModuleEntrypointLoads( + Array.from({ length: 12 }, (_, index) => ({ + identity: `module-${index}/page`, + isCompatible: compatibleRemoteModule, - yield* Effect.promise(async () => await firstWindowStarted.promise); - expect(started).toEqual([0, 1, 2, 3, 4, 5, 6, 7]); - yield* Fiber.interrupt(caller); - for (const pending of pendingLoads) { - pending.resolve({ default: () => null }); - } - yield* Effect.promise( - async () => await Promise.all(pendingLoads.map(async ({ promise }) => await promise)), - ); - yield* TestClock.adjust('1 millis'); - expect(started).toHaveLength(8); - expect(started).toEqual([0, 1, 2, 3, 4, 5, 6, 7]); - }).pipe(Effect.provide(TestClock.layer())), - ), + load: () => { + started.push(index); + if (started.length === MODULE_LOAD_CONCURRENCY) { + firstWindowStarted.resolve(null); + } + return pendingLoads[index]?.promise ?? Promise.resolve({ default: remoteDefault }); + }, + })), + ), + ); + + yield* Effect.promise(() => firstWindowStarted.promise); + expect(started).toEqual([0, 1, 2, 3, 4, 5, 6, 7]); + yield* Fiber.interrupt(caller); + for (const pending of pendingLoads) { + pending.resolve({ default: () => null }); + } + yield* Effect.promise(() => Promise.all(pendingLoads.map(({ promise }) => promise))); + yield* TestClock.adjust('1 millis'); + expect(started).toHaveLength(8); + expect(started).toEqual([0, 1, 2, 3, 4, 5, 6, 7]); + }).pipe(Effect.provide(TestClock.layer())), ); -test( +it.effect( 'holds a running timed-out load permit until settlement then releases it to a live queued load', - runEffectTest( + () => Effect.gen(function* verifyPermitRelease() { const pendingLoads = Array.from({ length: MODULE_LOAD_CONCURRENCY }, () => Promise.withResolvers(), @@ -458,7 +446,8 @@ test( Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ identity: `module-${index}/page`, isCompatible: compatibleRemoteModule, - load: async () => { + + load: () => { events.push(`started-${index}`); if (index === MODULE_LOAD_CONCURRENCY - 1) { firstWindowStarted.resolve(null); @@ -466,25 +455,28 @@ test( const pending = pendingLoads[index]; if (pending === undefined) { queuedLoadStarted.resolve(null); - return { default: () => null }; + return Promise.resolve({ default: remoteDefault }); } - const value = await pending.promise; - events.push(`settled-${index}`); - return value; + return Promise.resolve( + pending.promise.then((value) => { + events.push(`settled-${index}`); + return value; + }), + ); }, timeoutMs: index === 0 ? 10 : 1000, })), ), ); - yield* Effect.promise(async () => await firstWindowStarted.promise); + yield* Effect.promise(() => firstWindowStarted.promise); yield* TestClock.adjust('20 millis'); expect(events).toEqual( Array.from({ length: MODULE_LOAD_CONCURRENCY }, (_, index) => `started-${index}`), ); pendingLoads[0]?.resolve({ default: () => null }); - yield* Effect.promise(async () => await queuedLoadStarted.promise); + yield* Effect.promise(() => queuedLoadStarted.promise); expect(events.slice(-2)).toEqual(['settled-0', `started-${MODULE_LOAD_CONCURRENCY}`]); for (const pending of pendingLoads) { pending.resolve({ default: () => null }); @@ -497,51 +489,56 @@ test( }); expect(results.slice(1).every(({ state }) => state === 'ready')).toBe(true); }).pipe(Effect.provide(TestClock.layer())), - ), ); -test('does not surface a late remote rejection after a timeout', async () => { - const pending = Promise.withResolvers(); - const loadSettled = Promise.withResolvers(); - const result = await runEffectTestPromise( - settleModuleEntrypointLoads([ +it.effect('does not surface a late remote rejection after a timeout', () => + Effect.gen(function* verifyLateRemoteRejection() { + const pending = Promise.withResolvers(); + const loadStarted = Promise.withResolvers(); + const loadSettled = Promise.withResolvers(); + const resultFiber = yield* Effect.forkChild( + settleModuleEntrypointLoads([ + { + identity: 'late-rejection/page', + isCompatible: compatibleRemoteModule, + + load: () => { + loadStarted.resolve(null); + return Promise.resolve( + pending.promise.finally(() => { + loadSettled.resolve(null); + }), + ); + }, + timeoutMs: 10, + }, + ]), + ); + yield* Effect.promise(() => loadStarted.promise); + yield* TestClock.adjust('10 millis'); + const result = yield* Fiber.join(resultFiber); + expect(result).toEqual([ { identity: 'late-rejection/page', - isCompatible: compatibleRemoteModule, - load: async () => { - try { - return await pending.promise; - } finally { - loadSettled.resolve(null); - } - }, - timeoutMs: 10, + reason: 'timeout', + state: 'unavailable', }, - ]), - ); - - expect(result).toEqual([ - { - identity: 'late-rejection/page', - reason: 'timeout', - state: 'unavailable', - }, - ]); - - pending.reject(new Error('remote unavailable')); - await loadSettled.promise; - expect(result).toEqual([ - { - identity: 'late-rejection/page', - reason: 'timeout', - state: 'unavailable', - }, - ]); -}); + ]); + pending.reject(new Error('remote unavailable')); + yield* Effect.promise(() => loadSettled.promise); + expect(result).toEqual([ + { + identity: 'late-rejection/page', + reason: 'timeout', + state: 'unavailable', + }, + ]); + }).pipe(Effect.provide(TestClock.layer())), +); -test.each(['selection_required', 'not_found', 'forbidden', 'unavailable'] as const)( +it.effect.each(['selection_required', 'not_found', 'forbidden', 'unavailable'] as const)( 'never invokes a remote loader after a %s target resolution', - Fn.flow((outcome) => { + (outcome) => { let loads = 0; return Effect.gen(function* verifyRejectedTargetResolution() { const failure = yield* Effect.flip( @@ -555,23 +552,20 @@ test.each(['selection_required', 'not_found', 'forbidden', 'unavailable'] as con expect(failure).toEqual({ outcome }); expect(loads).toBe(0); }); - }, runEffectTestPromise), + }, ); -test( - 'invokes the lazy registry only after receiving an approved target', - runEffectTest( - Effect.gen(function* verifyApprovedTargetResolution() { - let loads = 0; - const target = { appId: 'inventory-app', componentKey: 'inventory.stock.page' }; - const result = yield* resolveThenLoadModuleTarget(Effect.succeed(target), (approved) => - Effect.sync(() => { - loads += 1; - return approved.componentKey; - }), - ); - expect(result).toBe('inventory.stock.page'); - expect(loads).toBe(1); - }), - ), +it.effect('invokes the lazy registry only after receiving an approved target', () => + Effect.gen(function* verifyApprovedTargetResolution() { + let loads = 0; + const target = { appId: 'inventory-app', componentKey: 'inventory.stock.page' }; + const result = yield* resolveThenLoadModuleTarget(Effect.succeed(target), (approved) => + Effect.sync(() => { + loads += 1; + return approved.componentKey; + }), + ); + expect(result).toBe('inventory.stock.page'); + expect(loads).toBe(1); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/routes/authenticated-shell-fixture.ts b/app/apps/shell-super-app/tests/unit/routes/authenticated-shell-fixture.ts new file mode 100644 index 000000000..777f4db79 --- /dev/null +++ b/app/apps/shell-super-app/tests/unit/routes/authenticated-shell-fixture.ts @@ -0,0 +1,21 @@ +import { Schema } from 'effect'; +import { LegalEntityIdSchema, SafeTenantIdentitySchema } from '../../../shared/api.ts'; +import type { AuthenticatedHomePageModel } from '../../../src/routes/[lang]/page.data.ts'; + +/** The authenticated shell every route page test renders its own model on top of. */ +export const authenticatedShellFixture = (): AuthenticatedHomePageModel => ({ + contextState: 'authenticated', + identity: Schema.decodeUnknownSync(SafeTenantIdentitySchema)({ + displayName: 'Ada Lovelace', + email: 'ada@example.test', + principalId: 'principal-1', + tenantId: 'tenant-1', + }), + legalEntities: { items: [], state: 'available' }, + navigation: { items: [], state: 'available', unavailableDeployments: [] }, + selectedLegalEntityId: Schema.decodeUnknownSync(LegalEntityIdSchema)( + '20000000-0000-4000-8000-000000000001', + ), + state: 'authenticated', + tenants: { items: [], state: 'available' }, +}); diff --git a/app/apps/shell-super-app/tests/unit/routes/home/loader.test.ts b/app/apps/shell-super-app/tests/unit/routes/home/loader.test.ts index 9eee655be..1a056225f 100644 --- a/app/apps/shell-super-app/tests/unit/routes/home/loader.test.ts +++ b/app/apps/shell-super-app/tests/unit/routes/home/loader.test.ts @@ -1,10 +1,9 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { beforeEach, expect, rstest, test } from '@rstest/core'; +import { beforeEach, expect, rstest, it } from 'effect-rstest'; import { ConfigProvider, Effect } from 'effect'; import * as actualAuthClient from '../../../../src/api/auth-client.ts' with { rstest: 'importActual', }; -import { loader } from '../../../../src/routes/[lang]/page.data.ts'; +import { loadHomePageModel } from '../../../../src/routes/[lang]/page.data.ts'; const { availableLegalEntitiesMock, @@ -28,18 +27,6 @@ rstest.mock('../../../../src/api/auth-client.ts', () => ({ shellComposition: shellCompositionMock, })); -rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ - runBrowserEffect: async (effect: Effect.Effect) => - await runEffectTestPromise( - effect.pipe( - Effect.provideService( - ConfigProvider.ConfigProvider, - ConfigProvider.fromUnknown(browserConfigValuesMock()), - ), - ), - ), -})); - const identity = { displayName: 'Ada Lovelace', email: 'ada@example.test', @@ -67,13 +54,23 @@ const request = () => headers: { cookie: 'session=test-session' }, }); -const withBetterAuthUrl = async ( +const loadModel = ({ request: input }: { readonly request: Request }) => + Effect.suspend(() => + loadHomePageModel(input).pipe( + Effect.provideService( + ConfigProvider.ConfigProvider, + ConfigProvider.fromUnknown(browserConfigValuesMock()), + ), + ), + ); +const withBetterAuthUrl = ( baseUrl: string, - operation: () => Promise, -): Promise => { - browserConfigValuesMock.mockReturnValueOnce({ BETTER_AUTH_URL: baseUrl }); - return await operation(); -}; + operation: () => Effect.Effect, +) => + Effect.suspend(() => { + browserConfigValuesMock.mockReturnValueOnce({ BETTER_AUTH_URL: baseUrl }); + return operation(); + }); beforeEach(() => { browserConfigValuesMock.mockReturnValue({}); @@ -98,132 +95,160 @@ beforeEach(() => { ); }); -test('resolves trusted context before returning one serializable composition', async () => { - expect(await loader({ request: request() })).toEqual({ - contextState: 'authenticated', - identity, - legalEntities: { - items: [{ legalEntityId: 'legal-1', legalName: 'Alpha company' }], - state: 'available', - }, - navigation: { items: navigation, state: 'available', unavailableDeployments: [] }, - selectedLegalEntityId: 'legal-1', - state: 'authenticated', - tenants: { - items: [ - { name: 'Alpha tenant', tenantId: 'tenant-1' }, - { name: 'Zeta tenant', tenantId: 'tenant-2' }, - ], - state: 'available', - }, - }); - expect(currentSessionMock.mock.invocationCallOrder[0]).toBeLessThan( - shellCompositionMock.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY, - ); -}); - -test('does not request composition for an anonymous session', async () => { - currentSessionMock.mockReturnValueOnce(Effect.succeed({ state: 'anonymous' as const })); - expect(await loader({ request: request() })).toEqual({ state: 'anonymous' }); - expect(shellCompositionMock).not.toHaveBeenCalled(); - expect(availableTenantsMock).not.toHaveBeenCalled(); -}); - -test('does not invent a selected legal entity while a tenant session requires selection', async () => { - const tenantIdentity = { - displayName: identity.displayName, - email: identity.email, - principalId: identity.principalId, - tenantId: identity.tenantId, - }; - currentSessionMock.mockReturnValueOnce( - Effect.succeed({ - availableLegalEntities: [{ legalEntityId: 'legal-1', legalName: 'Alpha company' }], +it.effect('resolves trusted context before returning one serializable composition', () => + Effect.gen(function* verifyCase1() { + expect(yield* loadModel({ request: request() })).toEqual({ + contextState: 'authenticated', + identity, + legalEntities: { + items: [{ legalEntityId: 'legal-1', legalName: 'Alpha company' }], + state: 'available', + }, + navigation: { items: navigation, state: 'available', unavailableDeployments: [] }, + selectedLegalEntityId: 'legal-1', + state: 'authenticated', + tenants: { + items: [ + { name: 'Alpha tenant', tenantId: 'tenant-1' }, + { name: 'Zeta tenant', tenantId: 'tenant-2' }, + ], + state: 'available', + }, + }); + expect(currentSessionMock.mock.invocationCallOrder[0]).toBeLessThan( + shellCompositionMock.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY, + ); + }), +); + +it.effect('does not request composition for an anonymous session', () => + Effect.gen(function* verifyCase2() { + currentSessionMock.mockReturnValueOnce(Effect.succeed({ state: 'anonymous' as const })); + expect(yield* loadModel({ request: request() })).toEqual({ state: 'anonymous' }); + expect(shellCompositionMock).not.toHaveBeenCalled(); + expect(availableTenantsMock).not.toHaveBeenCalled(); + }), +); + +it.effect('does not invent a selected legal entity while a tenant session requires selection', () => + Effect.gen(function* verifyCase3() { + const tenantIdentity = { + displayName: identity.displayName, + email: identity.email, + principalId: identity.principalId, + tenantId: identity.tenantId, + }; + currentSessionMock.mockReturnValueOnce( + Effect.succeed({ + availableLegalEntities: [{ legalEntityId: 'legal-1', legalName: 'Alpha company' }], + identity: tenantIdentity, + state: 'selection_required' as const, + }), + ); + const model = yield* loadModel({ request: request() }); + expect(model).toMatchObject({ + contextState: 'selection_required', identity: tenantIdentity, - state: 'selection_required' as const, + legalEntities: { + items: [{ legalEntityId: 'legal-1', legalName: 'Alpha company' }], + state: 'available', + }, + state: 'authenticated', + }); + expect(model).not.toHaveProperty('selectedLegalEntityId'); + expect(shellCompositionMock).not.toHaveBeenCalled(); + expect(availableLegalEntitiesMock).not.toHaveBeenCalled(); + }), +); + +it.effect('uses the configured HTTPS origin for the server-side session request', () => + Effect.gen(function* verifyCase4() { + currentSessionMock.mockReturnValueOnce(Effect.succeed({ state: 'anonymous' as const })); + + yield* withBetterAuthUrl('https://shell.stage.example.test', () => + Effect.gen(function* verifyCase5() { + return yield* loadModel({ request: new Request('http://shell.stage.example.test/en') }); + }), + ); + + expect(currentSessionMock.mock.calls.at(-1)?.[0]?.baseUrl.toString()).toBe( + 'https://shell.stage.example.test/shell-super-app-api', + ); + }), +); + +it.effect('keeps the configured local HTTP origin for the server-side session request', () => + Effect.gen(function* verifyCase6() { + currentSessionMock.mockReturnValueOnce(Effect.succeed({ state: 'anonymous' as const })); + + yield* withBetterAuthUrl('http://localhost:3020', () => + Effect.gen(function* verifyCase7() { + return yield* loadModel({ request: new Request('http://localhost:3020/en') }); + }), + ); + + expect(currentSessionMock.mock.calls.at(-1)?.[0]?.baseUrl.toString()).toBe( + 'http://localhost:3020/shell-super-app-api', + ); + }), +); + +it.effect('maps composition failure to unavailable without discarding verified context', () => + Effect.gen(function* verifyCase8() { + shellCompositionMock.mockReturnValueOnce( + Effect.fail({ _tag: 'ShellCapabilityUnavailableProblem' }), + ); + expect(yield* loadModel({ request: request() })).toMatchObject({ + contextState: 'authenticated', + identity, + navigation: { items: [], state: 'unavailable' }, + state: 'authenticated', + }); + }), +); + +it.effect('maps tenant failure to the current-tenant fallback without discarding composition', () => + Effect.gen(function* verifyCase9() { + availableTenantsMock.mockReturnValueOnce( + Effect.fail({ _tag: 'TenantCapabilityUnavailableProblem' }), + ); + expect(yield* loadModel({ request: request() })).toMatchObject({ + navigation: { items: navigation, state: 'available' }, + tenants: { items: [{ name: 'tenant-1', tenantId: 'tenant-1' }], state: 'unavailable' }, + }); + }), +); + +it.effect( + 'keeps legal-entity acquisition failure explicit without claiming choices are available', + () => + Effect.gen(function* verifyCase10() { + availableLegalEntitiesMock.mockReturnValueOnce( + Effect.fail({ _tag: 'TenantCapabilityUnavailableProblem' }), + ); + expect(yield* loadModel({ request: request() })).toMatchObject({ + legalEntities: { items: [], state: 'unavailable' }, + state: 'authenticated', + }); }), - ); - const model = await loader({ request: request() }); - expect(model).toMatchObject({ - contextState: 'selection_required', - identity: tenantIdentity, - legalEntities: { - items: [{ legalEntityId: 'legal-1', legalName: 'Alpha company' }], - state: 'available', - }, - state: 'authenticated', - }); - expect(model).not.toHaveProperty('selectedLegalEntityId'); - expect(shellCompositionMock).not.toHaveBeenCalled(); - expect(availableLegalEntitiesMock).not.toHaveBeenCalled(); -}); - -test('uses the configured HTTPS origin for the server-side session request', async () => { - currentSessionMock.mockReturnValueOnce(Effect.succeed({ state: 'anonymous' as const })); - - await withBetterAuthUrl( - 'https://shell.stage.example.test', - async () => await loader({ request: new Request('http://shell.stage.example.test/en') }), - ); - - expect(currentSessionMock.mock.calls.at(-1)?.[0]?.baseUrl.toString()).toBe( - 'https://shell.stage.example.test/shell-super-app-api', - ); -}); - -test('keeps the configured local HTTP origin for the server-side session request', async () => { - currentSessionMock.mockReturnValueOnce(Effect.succeed({ state: 'anonymous' as const })); - - await withBetterAuthUrl( - 'http://localhost:3020', - async () => await loader({ request: new Request('http://localhost:3020/en') }), - ); - - expect(currentSessionMock.mock.calls.at(-1)?.[0]?.baseUrl.toString()).toBe( - 'http://localhost:3020/shell-super-app-api', - ); -}); - -test('maps composition failure to unavailable without discarding verified context', async () => { - shellCompositionMock.mockReturnValueOnce( - Effect.fail({ _tag: 'ShellCapabilityUnavailableProblem' }), - ); - expect(await loader({ request: request() })).toMatchObject({ - contextState: 'authenticated', - identity, - navigation: { items: [], state: 'unavailable' }, - state: 'authenticated', - }); -}); - -test('maps tenant failure to the current-tenant fallback without discarding composition', async () => { - availableTenantsMock.mockReturnValueOnce( - Effect.fail({ _tag: 'TenantCapabilityUnavailableProblem' }), - ); - expect(await loader({ request: request() })).toMatchObject({ - navigation: { items: navigation, state: 'available' }, - tenants: { items: [{ name: 'tenant-1', tenantId: 'tenant-1' }], state: 'unavailable' }, - }); -}); - -test('keeps legal-entity acquisition failure explicit without claiming choices are available', async () => { - availableLegalEntitiesMock.mockReturnValueOnce( - Effect.fail({ _tag: 'TenantCapabilityUnavailableProblem' }), - ); - expect(await loader({ request: request() })).toMatchObject({ - legalEntities: { items: [], state: 'unavailable' }, - state: 'authenticated', - }); -}); - -test('does not collapse an authentication infrastructure failure into an anonymous session', async () => { - currentSessionMock.mockReturnValueOnce(Effect.fail({ _tag: 'AuthenticationUnavailableProblem' })); - expect(await loader({ request: request() })).toEqual({ state: 'unavailable' }); -}); - -test('tears down stale authenticated data when tenant context requires authentication', async () => { - availableTenantsMock.mockReturnValueOnce( - Effect.fail({ _tag: 'TenantAuthenticationRequiredProblem' }), - ); - expect(await loader({ request: request() })).toEqual({ state: 'anonymous' }); -}); +); + +it.effect( + 'does not collapse an authentication infrastructure failure into an anonymous session', + () => + Effect.gen(function* verifyCase11() { + currentSessionMock.mockReturnValueOnce( + Effect.fail({ _tag: 'AuthenticationUnavailableProblem' }), + ); + expect(yield* loadModel({ request: request() })).toEqual({ state: 'unavailable' }); + }), +); + +it.effect('tears down stale authenticated data when tenant context requires authentication', () => + Effect.gen(function* verifyCase12() { + availableTenantsMock.mockReturnValueOnce( + Effect.fail({ _tag: 'TenantAuthenticationRequiredProblem' }), + ); + expect(yield* loadModel({ request: request() })).toEqual({ state: 'anonymous' }); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx index 957199f87..4bb85c58b 100644 --- a/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx +++ b/app/apps/shell-super-app/tests/unit/routes/home/page.test.tsx @@ -1,5 +1,7 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { afterEach, beforeEach, expect, rstest, test } from '@rstest/core'; +import { browserRuntime } from '../../../../src/runtime/browser-effect-runtime.ts' with { + rstest: 'importActual', +}; +import { afterEach, beforeEach, expect, rstest, it } from 'effect-rstest'; import { cleanup, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Effect, Schema } from 'effect'; @@ -18,14 +20,19 @@ import { import { HomeView } from '../../../../src/routes/[lang]/page.tsx'; import type { HomePageModel } from '../../../../src/routes/[lang]/page.data.ts'; -const { navigateMock, runBrowserEffectMock, signOutMock, switchLegalEntityMock, switchTenantMock } = - rstest.hoisted(() => ({ - navigateMock: rstest.fn(), - runBrowserEffectMock: rstest.fn(), - signOutMock: rstest.fn(), - switchLegalEntityMock: rstest.fn(), - switchTenantMock: rstest.fn(), - })); +const { + browserRunPromiseMock, + navigateMock, + signOutMock, + switchLegalEntityMock, + switchTenantMock, +} = rstest.hoisted(() => ({ + browserRunPromiseMock: rstest.fn(), + navigateMock: rstest.fn(), + signOutMock: rstest.fn(), + switchLegalEntityMock: rstest.fn(), + switchTenantMock: rstest.fn(), +})); const translations = new Map( Object.entries({ @@ -87,7 +94,7 @@ rstest.mock('../../../../src/api/auth-client.ts', () => ({ })); rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ - runBrowserEffect: runBrowserEffectMock, + browserRuntime: { runPromise: browserRunPromiseMock }, })); const principalId = Schema.decodeUnknownSync(PrincipalIdSchema)( @@ -151,9 +158,7 @@ const authenticatedModel = (): HomePageModel => ({ beforeEach(() => { navigateMock.mockResolvedValue(undefined); - runBrowserEffectMock.mockImplementation( - async (effect: Effect.Effect) => await runEffectTestPromise(effect), - ); + browserRunPromiseMock.mockImplementation(browserRuntime.runPromise); signOutMock.mockReturnValue(Effect.succeed({ signedOut: true })); switchTenantMock.mockReturnValue(Effect.succeed({ selectedTenantId: tenantId2 })); switchLegalEntityMock.mockReturnValue(Effect.succeed({ selectedLegalEntityId: legalEntityId2 })); @@ -164,13 +169,13 @@ afterEach(() => { rstest.clearAllMocks(); }); -test('anonymous home exposes only the localized login action', () => { +it('anonymous home exposes only the localized login action', () => { render(); expect(screen.getByRole('link', { name: 'Login' }).getAttribute('href')).toBe('/en/login'); expect(screen.queryByRole('banner')).toBeNull(); }); -test('authenticated home renders server-composed navigation and selected legal context', () => { +it('authenticated home renders server-composed navigation and selected legal context', () => { render(); expect(screen.getByRole('link', { name: 'Inventory' }).getAttribute('href')).toBe( '/en/modules/inventory.stock', @@ -180,48 +185,81 @@ test('authenticated home renders server-composed navigation and selected legal c expect(screen.queryByText('inventory.stock')).toBeNull(); }); -test('successful tenant switch performs a full document reload', async () => { - const user = userEvent.setup(); - render(); - await user.click(screen.getByRole('combobox', { name: 'Current tenant' })); - await user.click(await screen.findByRole('option', { name: 'Zeta tenant' })); - await waitFor(() => - expect(switchTenantMock).toHaveBeenCalledWith({ tenantId: tenantId2 }, { locale: 'en' }), - ); - await waitFor(() => expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '.' })); -}); +it.live('successful tenant switch performs a full document reload', () => + Effect.gen(function* successfulTenantSwitchPerformsAFull() { + const user = userEvent.setup(); + render(); + yield* Effect.promise(() => + user.click(screen.getByRole('combobox', { name: 'Current tenant' })), + ); + const tenantOption = yield* Effect.promise(() => + screen.findByRole('option', { name: 'Zeta tenant' }), + ); + yield* Effect.promise(() => user.click(tenantOption)); + yield* Effect.promise(() => + waitFor(() => + expect(switchTenantMock).toHaveBeenCalledWith({ tenantId: tenantId2 }, { locale: 'en' }), + ), + ); + yield* Effect.promise(() => + waitFor(() => expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '.' })), + ); + }), +); -test('successful legal-entity switch performs a full document reload', async () => { - const user = userEvent.setup(); - render(); - await user.click(screen.getByRole('combobox', { name: 'Current legal entity' })); - await user.click(await screen.findByRole('option', { name: 'Beta company' })); - await waitFor(() => - expect(switchLegalEntityMock).toHaveBeenCalledWith( - { legalEntityId: legalEntityId2 }, - { locale: 'en' }, - ), - ); - await waitFor(() => expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '.' })); -}); +it.live('successful legal-entity switch performs a full document reload', () => + Effect.gen(function* successfulLegalEntitySwitchPerformsA() { + const user = userEvent.setup(); + render(); + yield* Effect.promise(() => + user.click(screen.getByRole('combobox', { name: 'Current legal entity' })), + ); + const legalEntityOption = yield* Effect.promise(() => + screen.findByRole('option', { name: 'Beta company' }), + ); + yield* Effect.promise(() => user.click(legalEntityOption)); + yield* Effect.promise(() => + waitFor(() => + expect(switchLegalEntityMock).toHaveBeenCalledWith( + { legalEntityId: legalEntityId2 }, + { locale: 'en' }, + ), + ), + ); + yield* Effect.promise(() => + waitFor(() => expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '.' })), + ); + }), +); -test('search submission navigates to the localized Shell search route', async () => { - const user = userEvent.setup(); - render(); - await user.type(screen.getByLabelText('Search this legal entity'), 'Unit 1'); - await user.click(screen.getByRole('button', { name: 'Search' })); - expect(navigateMock).toHaveBeenCalledWith({ to: '/en/search?q=Unit%201' }); -}); +it.live('search submission navigates to the localized Shell search route', () => + Effect.gen(function* searchSubmissionNavigatesToTheLocalized() { + const user = userEvent.setup(); + render(); + yield* Effect.promise(() => + user.type(screen.getByLabelText('Search this legal entity'), 'Unit 1'), + ); + yield* Effect.promise(() => user.click(screen.getByRole('button', { name: 'Search' }))); + expect(navigateMock).toHaveBeenCalledWith({ to: '/en/search?q=Unit%201' }); + }), +); -test('logout clears the authenticated composition together', async () => { - const user = userEvent.setup(); - render(); - await user.click(screen.getByRole('button', { name: 'Ada Lovelace' })); - await user.click(await screen.findByRole('menuitem', { name: 'Logout' })); - await waitFor(() => - expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '/en/login' }), - ); -}); +it.live('logout clears the authenticated composition together', () => + Effect.gen(function* logoutClearsTheAuthenticatedCompositionTogether() { + const user = userEvent.setup(); + render(); + yield* Effect.promise(() => user.click(screen.getByRole('button', { name: 'Ada Lovelace' }))); + const logoutItem = yield* Effect.promise(() => + screen.findByRole('menuitem', { name: 'Logout' }), + ); + yield* Effect.promise(() => user.click(logoutItem)); + yield* Effect.promise(() => + waitFor(() => + expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '/en/login' }), + ), + ); + }), +); const tenantAuthenticationRequired = Schema.decodeUnknownSync( TenantAuthenticationRequiredProblemSchema, @@ -306,31 +344,39 @@ const switchFailureCases: SwitchFailureCase[] = [ }, ]; -test.each(switchFailureCases)( +it.live.each(switchFailureCases)( 'settles $name into its own selector without leaving it pending', - async ({ comboboxName, failedText, failure, optionName, pendingText, reloads, switchMock }) => { - switchMock.mockReturnValue(Effect.fail(failure)); - const user = userEvent.setup(); - render(); + ({ comboboxName, failedText, failure, optionName, pendingText, reloads, switchMock }) => + Effect.gen(function* settlesTheSwitchFailureIntoItsOwnSelector() { + switchMock.mockReturnValue(Effect.fail(failure)); + const user = userEvent.setup(); + render(); - await user.click(screen.getByRole('combobox', { name: comboboxName })); - await user.click(await screen.findByRole('option', { name: optionName })); - await waitFor(() => expect(switchMock).toHaveBeenCalledTimes(1)); + yield* Effect.promise(() => user.click(screen.getByRole('combobox', { name: comboboxName }))); + const option = yield* Effect.promise(() => screen.findByRole('option', { name: optionName })); + yield* Effect.promise(() => user.click(option)); + yield* Effect.promise(() => waitFor(() => expect(switchMock).toHaveBeenCalledTimes(1))); - if (reloads) { - await waitFor(() => - expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '.' }), - ); - await waitFor(() => expect(screen.queryByText(pendingText)).toBeNull()); - expect(screen.queryByText(failedText)).toBeNull(); - } else { - await waitFor(() => expect(screen.getByText(failedText)).toBeTruthy()); - expect(navigateMock).not.toHaveBeenCalled(); - expect(screen.queryByText(pendingText)).toBeNull(); - } + if (reloads) { + yield* Effect.promise(() => + waitFor(() => + expect(navigateMock).toHaveBeenCalledWith({ reloadDocument: true, to: '.' }), + ), + ); + yield* Effect.promise(() => + waitFor(() => expect(screen.queryByText(pendingText)).toBeNull()), + ); + expect(screen.queryByText(failedText)).toBeNull(); + } else { + yield* Effect.promise(() => + waitFor(() => expect(screen.getByText(failedText)).toBeTruthy()), + ); + expect(navigateMock).not.toHaveBeenCalled(); + expect(screen.queryByText(pendingText)).toBeNull(); + } - expect(screen.getByRole('combobox', { name: comboboxName }).hasAttribute('disabled')).toBe( - false, - ); - }, + expect(screen.getByRole('combobox', { name: comboboxName }).hasAttribute('disabled')).toBe( + false, + ); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/routes/login/locales.test.ts b/app/apps/shell-super-app/tests/unit/routes/login/locales.test.ts index 059935c02..8477e2f5e 100644 --- a/app/apps/shell-super-app/tests/unit/routes/login/locales.test.ts +++ b/app/apps/shell-super-app/tests/unit/routes/login/locales.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@rstest/core'; +import { expect, test } from 'effect-rstest'; import cs from '../../../../locales/cs/shell.json'; import en from '../../../../locales/en/shell.json'; import { ultramodernRouteMetadata } from '../../../../src/routes/ultramodern-route-metadata'; diff --git a/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx index eae9573be..c66e1a300 100644 --- a/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx +++ b/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx @@ -1,21 +1,22 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { afterEach, beforeEach, expect, rstest, test } from '@rstest/core'; +import { browserRuntime } from '../../../../src/runtime/browser-effect-runtime.ts' with { + rstest: 'importActual', +}; +import { afterEach, beforeEach, expect, rstest, it } from 'effect-rstest'; import { Effect, Redacted } from 'effect'; import { cleanup, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { toaster } from '@techsio/ui-kit/molecules/toast'; import LoginPage from '../../../../src/routes/[lang]/login/page'; -const { navigateMock, runBrowserEffectMock, signInMock } = rstest.hoisted(() => ({ - navigateMock: rstest.fn(async () => {}), - runBrowserEffectMock: rstest.fn(), +const { browserRunPromiseMock, navigateMock, signInMock } = rstest.hoisted(() => ({ + browserRunPromiseMock: rstest.fn(), + navigateMock: rstest.fn(), signInMock: rstest.fn(), })); beforeEach(() => { - runBrowserEffectMock.mockImplementation( - async (effect: Effect.Effect) => await runEffectTestPromise(effect), - ); + navigateMock.mockImplementation(() => Promise.resolve()); + browserRunPromiseMock.mockImplementation(browserRuntime.runPromise); signInMock.mockReturnValue( Effect.succeed({ identity: { @@ -65,7 +66,7 @@ rstest.mock('../../../../src/api/auth-client.ts', () => ({ })); rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ - runBrowserEffect: runBrowserEffectMock, + browserRuntime: { runPromise: browserRunPromiseMock }, })); const getLogin = () => screen.getByRole('textbox', { name: 'Login *' }); @@ -81,7 +82,7 @@ afterEach(() => { rstest.clearAllMocks(); }); -test('shows the required login controls through the UI kit', () => { +it('shows the required login controls through the UI kit', () => { render(); const login = getLogin(); @@ -101,6 +102,20 @@ test('shows the required login controls through the UI kit', () => { ); }); +const submitLogin = (login: string, password: string) => + Effect.gen(function* submitLoginEffect() { + const user = userEvent.setup(); + renderLogin(); + if (login.length > 0) { + yield* Effect.promise(() => user.type(getLogin(), login)); + } + if (password.length > 0) { + yield* Effect.promise(() => user.type(getPassword(), password)); + } + yield* Effect.promise(() => user.click(getSubmit())); + return user; + }); + interface LoginValidationCase { readonly focus: 'login' | 'password' | 'submit'; readonly login: string; @@ -116,19 +131,6 @@ const focusTargets = { submit: getSubmit, }; -const submitLogin = async (login: string, password: string) => { - const user = userEvent.setup(); - renderLogin(); - if (login.length > 0) { - await user.type(getLogin(), login); - } - if (password.length > 0) { - await user.type(getPassword(), password); - } - await user.click(getSubmit()); - return user; -}; - const validationCases: LoginValidationCase[] = [ { focus: 'login', @@ -172,70 +174,96 @@ const validationCases: LoginValidationCase[] = [ }, ]; -test.each(validationCases)( +it.effect.each(validationCases)( 'marks, explains and focuses exactly the missing fields when $name', - async ({ focus, login, loginInvalid, password, passwordInvalid }) => { - await submitLogin(login, password); - - const incompleteToasts = loginInvalid || passwordInvalid ? 1 : 0; - expect(getLogin().getAttribute('aria-invalid')).toBe(loginInvalid ? 'true' : null); - expect(getPassword().getAttribute('aria-invalid')).toBe(passwordInvalid ? 'true' : null); - expect(screen.queryAllByText('Enter your login.')).toHaveLength(loginInvalid ? 1 : 0); - expect(screen.queryAllByText('Enter your password.')).toHaveLength(passwordInvalid ? 1 : 0); - expect(screen.queryAllByText('Login details are incomplete')).toHaveLength(incompleteToasts); - expect(screen.queryAllByText('Fill in both required fields.')).toHaveLength(incompleteToasts); - expect(document.activeElement).toBe(focusTargets[focus]()); - }, -); + ({ focus, login, loginInvalid, password, passwordInvalid }) => + Effect.gen(function* marksExplainsAndFocusesTheMissingFields() { + yield* submitLogin(login, password); -test('creates one Toast per repeated invalid submission', async () => { - const user = await submitLogin('', ''); + const incompleteToasts = loginInvalid || passwordInvalid ? 1 : 0; + expect(getLogin().getAttribute('aria-invalid')).toBe(loginInvalid ? 'true' : null); + expect(getPassword().getAttribute('aria-invalid')).toBe(passwordInvalid ? 'true' : null); + expect(screen.queryAllByText('Enter your login.')).toHaveLength(loginInvalid ? 1 : 0); + expect(screen.queryAllByText('Enter your password.')).toHaveLength(passwordInvalid ? 1 : 0); + expect(screen.queryAllByText('Login details are incomplete')).toHaveLength(incompleteToasts); + expect(screen.queryAllByText('Fill in both required fields.')).toHaveLength(incompleteToasts); + expect(document.activeElement).toBe(focusTargets[focus]()); + }), +); - await user.click(getSubmit()); +it.effect('creates one Toast per repeated invalid submission', () => + Effect.gen(function* createsOneToastPerRepeatedInvalidSubmission() { + const user = yield* submitLogin('', ''); - expect(screen.getAllByText('Login details are incomplete')).toHaveLength(2); - expect(screen.getAllByText('Fill in both required fields.')).toHaveLength(2); -}); + yield* Effect.promise(() => user.click(getSubmit())); + expect(screen.getAllByText('Login details are incomplete')).toHaveLength(2); + expect(screen.getAllByText('Fill in both required fields.')).toHaveLength(2); + }), +); -test('clears stale errors after both fields are corrected', async () => { - const user = await submitLogin('', ''); +it.effect('clears stale errors after both fields are corrected', () => + Effect.gen(function* clearsStaleErrorsAfterBothFieldsCorrected() { + const user = yield* submitLogin('', ''); - await user.type(getLogin(), 'admin'); - await user.type(getPassword(), 'secret'); - await user.click(getSubmit()); + yield* Effect.promise(() => user.type(getLogin(), 'admin')); + yield* Effect.promise(() => user.type(getPassword(), 'secret')); + yield* Effect.promise(() => user.click(getSubmit())); + expect(getLogin().getAttribute('aria-invalid')).toBeNull(); + expect(getPassword().getAttribute('aria-invalid')).toBeNull(); + expect(screen.queryByText('Enter your login.')).toBeNull(); + expect(screen.queryByText('Enter your password.')).toBeNull(); + }), +); - expect(getLogin().getAttribute('aria-invalid')).toBeNull(); - expect(getPassword().getAttribute('aria-invalid')).toBeNull(); - expect(screen.queryByText('Enter your login.')).toBeNull(); - expect(screen.queryByText('Enter your password.')).toBeNull(); -}); +it.effect('runs the same validation when submitted with Enter', () => + Effect.gen(function* runsTheSameValidationWhenSubmittedWithEnter() { + const user = userEvent.setup(); + renderLogin(); -test('runs the same validation when submitted with Enter', async () => { - const user = userEvent.setup(); - renderLogin(); + yield* Effect.promise(() => user.click(getLogin())); + yield* Effect.promise(() => user.keyboard('{Enter}')); + expect(getLogin().getAttribute('aria-invalid')).toBe('true'); + expect(getPassword().getAttribute('aria-invalid')).toBe('true'); + expect(screen.getAllByText('Login details are incomplete')).toHaveLength(1); + expect(document.activeElement).toBe(getLogin()); + }), +); - await user.click(getLogin()); - await user.keyboard('{Enter}'); +it.effect('submits valid values through the Shell authentication client and navigates home', () => + Effect.gen(function* submitsValidValuesThroughShellAuthClient() { + yield* submitLogin('admin', 'secret'); - expect(getLogin().getAttribute('aria-invalid')).toBe('true'); - expect(getPassword().getAttribute('aria-invalid')).toBe('true'); - expect(screen.getAllByText('Login details are incomplete')).toHaveLength(1); - expect(document.activeElement).toBe(getLogin()); -}); + yield* Effect.promise(() => + waitFor(() => { + expect(signInMock).toHaveBeenCalledWith( + { + email: 'admin', + password: Redacted.make('secret'), + }, + { locale: 'en' }, + ); + expect(browserRunPromiseMock).toHaveBeenCalledTimes(1); + expect(navigateMock).toHaveBeenCalledWith({ to: '/en/' }); + expect(getSubmit().hasAttribute('disabled')).toBe(false); + expect(screen.queryByText('shell.login.error.internal')).toBeNull(); + expect(screen.queryByText('Login details are incomplete')).toBeNull(); + }), + ); + }), +); -test('submits valid values through the Shell authentication client and navigates home', async () => { - await submitLogin('admin', 'secret'); +it.effect('reports navigation failure and restores the login form after authentication', () => + Effect.gen(function* reportsNavigationFailure() { + navigateMock.mockRejectedValueOnce('Navigation failed'); + yield* submitLogin('admin', 'secret'); - await waitFor(() => { - expect(signInMock).toHaveBeenCalledWith( - { - email: 'admin', - password: Redacted.make('secret'), - }, - { locale: 'en' }, + yield* Effect.promise(() => + waitFor(() => { + expect(navigateMock).toHaveBeenCalledWith({ to: '/en/' }); + expect(screen.getByText('shell.login.error.internal')).toBeDefined(); + expect(getSubmit().hasAttribute('disabled')).toBe(false); + expect(document.activeElement).toBe(getLogin()); + }), ); - expect(runBrowserEffectMock).toHaveBeenCalledTimes(1); - expect(navigateMock).toHaveBeenCalledWith({ to: '/en/' }); - expect(screen.queryByText('Login details are incomplete')).toBeNull(); - }); -}); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/routes/modules/loader.test.ts b/app/apps/shell-super-app/tests/unit/routes/modules/loader.test.ts index fa82831ba..7980efa36 100644 --- a/app/apps/shell-super-app/tests/unit/routes/modules/loader.test.ts +++ b/app/apps/shell-super-app/tests/unit/routes/modules/loader.test.ts @@ -1,11 +1,11 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { beforeEach, expect, rstest, test } from '@rstest/core'; -import { Effect } from 'effect'; +import { beforeEach, expect, rstest, it } from 'effect-rstest'; +import { Cause, ConfigProvider, Deferred, Effect, Fiber } from 'effect'; +import { TestClock } from 'effect/testing'; import * as actualAuthClient from '../../../../src/api/auth-client.ts' with { rstest: 'importActual', }; import { - loader, + loadModulePageModel, selectRouteParams, } from '../../../../src/routes/[lang]/modules/[moduleId]/page.data.ts'; @@ -19,10 +19,6 @@ rstest.mock('../../../../src/api/auth-client.ts', () => ({ resolveModuleTarget: resolveModuleTargetMock, })); -rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ - runBrowserEffect: runEffectTestPromise, -})); - rstest.mock('../../../../src/routes/[lang]/page.data.ts', () => ({ loadHomePageModel: loadHomePageModelMock, })); @@ -52,8 +48,14 @@ const request = () => { return value; }; +/** The module program reads its origin from config; pin an empty provider so every case is identical. */ +const moduleModel = (input: Parameters[0]) => + loadModulePageModel(input).pipe( + Effect.provideService(ConfigProvider.ConfigProvider, ConfigProvider.fromUnknown({})), + ); + beforeEach(() => { - loadHomePageModelMock.mockResolvedValue(authenticatedShell); + loadHomePageModelMock.mockReturnValue(Effect.succeed(authenticatedShell)); resolveModuleTargetMock.mockReturnValue( Effect.succeed({ appId: 'party-registry', @@ -65,7 +67,7 @@ beforeEach(() => { ); }); -test('selects only declared safe route parameters and omits overlong values', () => { +it('selects only declared safe route parameters and omits overlong values', () => { expect( selectRouteParams( { @@ -79,70 +81,98 @@ test('selects only declared safe route parameters and omits overlong values', () ).toEqual({ id: 'party-1' }); }); -test('retains only declared bounded route parameters outside the resolved target identity', async () => { - await expect( - loader({ - params: { - entrypointKey: 'party.registry.page.contacts', - moduleId: 'party.registry', - }, - request: request(), - routeParams: { id: 'party-1' }, +it.effect( + 'retains only declared bounded route parameters outside the resolved target identity', + () => + Effect.gen(function* retainsOnlyDeclaredBoundedRouteParameters() { + expect( + yield* moduleModel({ + params: { + entrypointKey: 'party.registry.page.contacts', + moduleId: 'party.registry', + }, + request: request(), + routeParams: { id: 'party-1' }, + }), + ).toMatchObject({ + routeParams: { id: 'party-1' }, + state: 'resolved', + target: { + appId: 'party-registry', + componentKey: 'party.registry.page-contacts', + entrypointKey: 'party.registry.page.contacts', + moduleId: 'party.registry', + }, + }); + expect(resolveModuleTargetMock).toHaveBeenCalledWith( + { entrypointKey: 'party.registry.page.contacts', moduleId: 'party.registry' }, + expect.any(Object), + ); }), - ).resolves.toMatchObject({ - routeParams: { id: 'party-1' }, - state: 'resolved', - target: { - appId: 'party-registry', - componentKey: 'party.registry.page-contacts', - entrypointKey: 'party.registry.page.contacts', - moduleId: 'party.registry', - }, - }); - expect(resolveModuleTargetMock).toHaveBeenCalledWith( - { entrypointKey: 'party.registry.page.contacts', moduleId: 'party.registry' }, - expect.any(Object), - ); -}); +); -test('retains module landing behavior when no exact page entrypoint is supplied', async () => { - await expect( - loader({ params: { moduleId: 'party.registry' }, request: request() }), - ).resolves.toMatchObject({ routeParams: {} }); - expect(resolveModuleTargetMock).toHaveBeenCalledWith( - { moduleId: 'party.registry' }, - expect.any(Object), - ); -}); +it.effect('retains module landing behavior when no exact page entrypoint is supplied', () => + Effect.gen(function* retainsModuleLandingBehaviorWhenNo() { + expect( + yield* moduleModel({ params: { moduleId: 'party.registry' }, request: request() }), + ).toMatchObject({ routeParams: {} }); + expect(resolveModuleTargetMock).toHaveBeenCalledWith( + { moduleId: 'party.registry' }, + expect.any(Object), + ); + }), +); -test('does not request or load a private target before authentication', async () => { - loadHomePageModelMock.mockResolvedValueOnce({ state: 'anonymous' }); - await expect( - loader({ - params: { - entrypointKey: 'party.registry.page.contacts', - moduleId: 'party.registry', - }, - request: request(), - }), - ).resolves.toMatchObject({ state: 'selection_required' }); - expect(resolveModuleTargetMock).not.toHaveBeenCalled(); -}); +it.effect('does not request or load a private target before authentication', () => + Effect.gen(function* doesNotRequestOrLoadA() { + loadHomePageModelMock.mockReturnValueOnce(Effect.succeed({ state: 'anonymous' })); + expect( + yield* moduleModel({ + params: { + entrypointKey: 'party.registry.page.contacts', + moduleId: 'party.registry', + }, + request: request(), + }), + ).toMatchObject({ state: 'selection_required' }); + expect(resolveModuleTargetMock).not.toHaveBeenCalled(); + }), +); -test.each([ +it.effect.each([ ['ShellSelectionRequiredProblem', 'selection_required'], ['ShellTargetForbiddenProblem', 'forbidden'], ['ShellTargetNotFoundProblem', 'not_found'], ['ShellCapabilityUnavailableProblem', 'unavailable'], -] as const)('maps %s without returning a resolved private target', async (_tag, state) => { - resolveModuleTargetMock.mockReturnValueOnce(Effect.fail({ _tag })); - await expect( - loader({ - params: { - entrypointKey: 'party.registry.page.contacts', - moduleId: 'party.registry', - }, - request: request(), - }), - ).resolves.toMatchObject({ state }); -}); +] as const)('maps %s without returning a resolved private target', ([_tag, state]) => + Effect.gen(function* ShellSelectionRequiredProblem() { + resolveModuleTargetMock.mockReturnValueOnce(Effect.fail({ _tag })); + expect( + yield* moduleModel({ + params: { + entrypointKey: 'party.registry.page.contacts', + moduleId: 'party.registry', + }, + request: request(), + }), + ).toMatchObject({ state }); + }), +); + +it.effect('fails with the typed timeout instead of hanging on an unresponsive shell read', () => + Effect.gen(function* failsWithTheTypedTimeout() { + const entered = yield* Deferred.make<'entered'>(); + loadHomePageModelMock.mockReturnValueOnce( + Effect.andThen(Deferred.succeed(entered, 'entered'), Effect.never), + ); + const fiber = yield* Effect.forkChild( + moduleModel({ params: { moduleId: 'party.registry' }, request: request() }), + ); + yield* Deferred.await(entered); + + yield* TestClock.adjust('30 seconds'); + + expect(yield* Effect.flip(Fiber.join(fiber))).toBeInstanceOf(Cause.TimeoutError); + expect(resolveModuleTargetMock).not.toHaveBeenCalled(); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/routes/modules/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/modules/page.test.tsx index 40bfa0aba..2aedc649a 100644 --- a/app/apps/shell-super-app/tests/unit/routes/modules/page.test.tsx +++ b/app/apps/shell-super-app/tests/unit/routes/modules/page.test.tsx @@ -1,13 +1,9 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { afterEach, beforeEach, expect, rstest, test } from '@rstest/core'; +import { afterEach, beforeEach, expect, rstest, it } from 'effect-rstest'; import { cleanup, render, screen, waitFor } from '@testing-library/react'; -import { Schema } from 'effect'; +import { Effect, Schema } from 'effect'; import type { ReactNode } from 'react'; -import { - LegalEntityIdSchema, - ResolvedModuleTargetSchema, - SafeTenantIdentitySchema, -} from '../../../../shared/api.ts'; +import { ResolvedModuleTargetSchema } from '../../../../shared/api.ts'; +import { authenticatedShellFixture } from '../authenticated-shell-fixture.ts'; import ContactsPage from '../../../../src/routes/[lang]/contacts/page.tsx'; import ModuleTargetPage from '../../../../src/routes/[lang]/modules/[moduleId]/page.tsx'; import type { ModuleTargetPageModel } from '../../../../src/routes/[lang]/modules/[moduleId]/page.data.ts'; @@ -38,10 +34,6 @@ rstest.mock('@techsio/ui-kit/atoms/status-text', () => ({ StatusText: ({ children }: { readonly children: ReactNode }) => {children}, })); -rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ - runBrowserEffect: runEffectTestPromise, -})); - rstest.mock('../../../../src/api/vertical-clients.ts', () => ({ findApprovedVerticalPageClient: findApprovedVerticalPageClientMock, })); @@ -67,22 +59,7 @@ rstest.mock('../../../../src/routes/use-shell-controls.ts', () => ({ }), })); -const shell: ResolvedPageModel['shell'] = { - contextState: 'authenticated' as const, - identity: Schema.decodeUnknownSync(SafeTenantIdentitySchema)({ - displayName: 'Ada Lovelace', - email: 'ada@example.test', - principalId: 'principal-1', - tenantId: 'tenant-1', - }), - legalEntities: { items: [], state: 'available' as const }, - navigation: { items: [], state: 'available' as const, unavailableDeployments: [] }, - selectedLegalEntityId: Schema.decodeUnknownSync(LegalEntityIdSchema)( - '20000000-0000-4000-8000-000000000001', - ), - state: 'authenticated' as const, - tenants: { items: [], state: 'available' as const }, -}; +const shell: ResolvedPageModel['shell'] = authenticatedShellFixture(); const targetFixture = (componentKey: string, entrypointKey: string, writable = true) => Schema.decodeUnknownSync(ResolvedModuleTargetSchema)({ @@ -187,7 +164,7 @@ afterEach(() => { rstest.clearAllMocks(); }); -test.each(['selection_required', 'forbidden', 'not_found', 'unavailable'] as const)( +it.each(['selection_required', 'forbidden', 'not_found', 'unavailable'] as const)( 'does not consult or invoke the private registry for a %s exact-page response', (state) => { useLoaderDataMock.mockReturnValue({ shell, state } satisfies ModuleTargetPageModel); @@ -197,15 +174,19 @@ test.each(['selection_required', 'forbidden', 'not_found', 'unavailable'] as con }, ); -test('invokes the exact private page loader only after a resolved authenticated response', async () => { - useLoaderDataMock.mockReturnValue(resolvedModel); - render(); - expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(resolvedModel.target); - await waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)); - expect(await screen.findByText('contacts.core.page-customers:customer-1')).toBeTruthy(); -}); +it.live('invokes the exact private page loader only after a resolved authenticated response', () => + Effect.gen(function* invokesTheExactPrivatePageLoader() { + useLoaderDataMock.mockReturnValue(resolvedModel); + render(); + expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(resolvedModel.target); + yield* Effect.promise(() => waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1))); + expect( + yield* Effect.promise(() => screen.findByText('contacts.core.page-customers:customer-1')), + ).toBeTruthy(); + }), +); -test('reads loader data from the active Party Registry owner route', () => { +it('reads loader data from the active Party Registry owner route', () => { useLoaderDataMock.mockImplementation(({ from }: { readonly from: string }) => { if (from !== '/$lang/contacts') { throw new Error(`Invariant failed: Could not find an active match from "${from}"`); @@ -220,47 +201,62 @@ test('reads loader data from the active Party Registry owner route', () => { }); }); -test('maps an unreachable approved remote to its safe local diagnostic', async () => { - loadRemotePageMock.mockRejectedValueOnce(new Error('private remote error')); - useLoaderDataMock.mockReturnValue(resolvedModel); +it.live('maps an unreachable approved remote to its safe local diagnostic', () => + Effect.gen(function* mapsAnUnreachableApprovedRemoteTo() { + loadRemotePageMock.mockRejectedValueOnce(new Error('private remote error')); + useLoaderDataMock.mockReturnValue(resolvedModel); - render(); + render(); - expect(await screen.findByText('shell.moduleTarget.unavailable')).toBeTruthy(); -}); + expect( + yield* Effect.promise(() => screen.findByText('shell.moduleTarget.unavailable')), + ).toBeTruthy(); + }), +); -test('rejects a malformed remote module before React receives it', async () => { - loadRemotePageMock.mockResolvedValueOnce({ default: 'not a component' }); - useLoaderDataMock.mockReturnValue(resolvedModel); +it.live('rejects a malformed remote module before React receives it', () => + Effect.gen(function* rejectsAMalformedRemoteModuleBefore() { + loadRemotePageMock.mockResolvedValueOnce({ default: 'not a component' }); + useLoaderDataMock.mockReturnValue(resolvedModel); - render(); + render(); - expect(await screen.findByText('shell.moduleTarget.incompatible')).toBeTruthy(); - expect(remotePropsMock).not.toHaveBeenCalled(); -}); + expect( + yield* Effect.promise(() => screen.findByText('shell.moduleTarget.incompatible')), + ).toBeTruthy(); + expect(remotePropsMock).not.toHaveBeenCalled(); + }), +); -test('passes an empty route-parameter record to a resolved static page', async () => { - useLoaderDataMock.mockReturnValue({ ...resolvedModel, routeParams: {} }); - render(); - await waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)); - expect(await screen.findByText('contacts.core.page-customers:static')).toBeTruthy(); -}); +it.live('passes an empty route-parameter record to a resolved static page', () => + Effect.gen(function* passesAnEmptyRouteParameterRecord() { + useLoaderDataMock.mockReturnValue({ ...resolvedModel, routeParams: {} }); + render(); + yield* Effect.promise(() => waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1))); + expect( + yield* Effect.promise(() => screen.findByText('contacts.core.page-customers:static')), + ).toBeTruthy(); + }), +); -test.each(exactPageCases)( +it.live.each(exactPageCases)( 'loads the approved $componentKey remote once with its exact route context and resolved target', - async ({ componentKey, entrypointKey, renderedText, routeParams, writable }) => { - const exactModel: ResolvedPageModel = { - ...resolvedModel, - routeParams, - target: targetFixture(componentKey, entrypointKey, writable), - }; - useLoaderDataMock.mockReturnValue(exactModel); + ({ componentKey, entrypointKey, renderedText, routeParams, writable }) => + Effect.gen(function* loadsTheApprovedExactRemoteOnce() { + const exactModel: ResolvedPageModel = { + ...resolvedModel, + routeParams, + target: targetFixture(componentKey, entrypointKey, writable), + }; + useLoaderDataMock.mockReturnValue(exactModel); - render(); + render(); - expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(exactModel.target); - await waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)); - expect(remotePropsMock).toHaveBeenCalledWith({ routeParams, target: exactModel.target }); - expect(await screen.findByText(renderedText)).toBeTruthy(); - }, + expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(exactModel.target); + yield* Effect.promise(() => + waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)), + ); + expect(remotePropsMock).toHaveBeenCalledWith({ routeParams, target: exactModel.target }); + expect(yield* Effect.promise(() => screen.findByText(renderedText))).toBeTruthy(); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/routes/resources/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/resources/page.test.tsx new file mode 100644 index 000000000..4161f04a7 --- /dev/null +++ b/app/apps/shell-super-app/tests/unit/routes/resources/page.test.tsx @@ -0,0 +1,393 @@ +import { browserRuntime } from '../../../../src/runtime/browser-effect-runtime.ts' with { + rstest: 'importActual', +}; +import { afterEach, beforeEach, expect, rstest, it } from 'effect-rstest'; +import { act, cleanup, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Deferred, Effect, Schema } from 'effect'; +import type { ReactNode } from 'react'; +import { + ShellResourceResponseSchema, + ShellTargetForbiddenProblemSchema, +} from '../../../../shared/api.ts'; +import type { MediaAttachmentResponse, ShellResourceResponse } from '../../../../shared/api.ts'; +import { authenticatedShellFixture } from '../authenticated-shell-fixture.ts'; +import ResourcePage from '../../../../src/routes/[lang]/resources/[moduleId]/[resourceType]/[resourceId]/page.tsx'; +import type { ResourcePageModel } from '../../../../src/routes/[lang]/resources/[moduleId]/[resourceType]/[resourceId]/page.data.ts'; + +type ReadyModel = Extract; +type ClosedState = Exclude; + +interface DashboardPageProps { + readonly currentModuleId?: string; + readonly title: string; +} + +const { + attachResourceMediaMock, + browserRunPromiseMock, + dashboardRenders, + shellControlsMock, + useLoaderDataMock, +} = rstest.hoisted(() => { + const renders: DashboardPageProps[] = []; + return { + attachResourceMediaMock: rstest.fn(), + browserRunPromiseMock: rstest.fn(), + dashboardRenders: renders, + shellControlsMock: rstest.fn(), + useLoaderDataMock: rstest.fn(), + }; +}); + +const translations = new Map( + Object.entries({ + 'shell.auth.logout.failed': 'Logout failed', + 'shell.dashboard.unavailable': 'The dashboard is unavailable', + 'shell.resource.forbidden': 'You cannot open this resource', + 'shell.resource.media.absent': 'This resource has no media', + 'shell.resource.media.attach': 'Attach media', + 'shell.resource.media.failed': 'Attaching the media failed', + 'shell.resource.media.forbidden': 'You cannot attach media here', + 'shell.resource.media.pending': 'Attaching media…', + 'shell.resource.media.read_only': 'This module is read only', + 'shell.resource.media.success': 'Media attached', + 'shell.resource.media.title': 'Media', + 'shell.resource.media.unavailable': 'Media attachment is unavailable', + 'shell.resource.not_found': 'This resource does not exist', + 'shell.resource.selection_required': 'Select a legal entity first', + 'shell.resource.timeline.empty': 'No timeline entries yet', + 'shell.resource.timeline.lagging': 'The timeline is catching up', + 'shell.resource.timeline.title': 'Timeline', + 'shell.resource.title': 'Resource', + 'shell.resource.unavailable': 'This resource is unavailable', + }), +); + +rstest.mock('@modern-js/plugin-i18n/runtime', () => ({ + useModernI18n: () => ({ + language: 'en', + t: (key: string) => translations.get(key) ?? key, + }), +})); + +rstest.mock('@modern-js/plugin-tanstack/runtime', () => ({ + useLoaderData: useLoaderDataMock, +})); + +rstest.mock('../../../../src/api/auth-client.ts', () => ({ + attachResourceMedia: attachResourceMediaMock, +})); + +rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ + browserRuntime: { runPromise: browserRunPromiseMock }, +})); + +rstest.mock('../../../../src/routes/use-shell-controls.ts', () => ({ + useShellControls: shellControlsMock, +})); + +rstest.mock('../../../../src/routes/shell-frame.tsx', () => ({ + AuthenticatedDashboardLayout: ({ + children, + ...props + }: DashboardPageProps & { readonly children: ReactNode }) => { + dashboardRenders.push(props); + return ( +
+

{props.title}

+ {children} +
+ ); + }, +})); + +const shell: ReadyModel['shell'] = authenticatedShellFixture(); + +const attachedResponse: MediaAttachmentResponse = { attached: true }; + +const resourceFixture = ( + overrides: Partial> = {}, +): ShellResourceResponse => + Schema.decodeUnknownSync(ShellResourceResponseSchema)({ + detail: { + fields: [ + { label: 'Status', value: 'Open' }, + { label: 'Amount', value: '1 250,00 CZK' }, + ], + title: 'Invoice 42', + }, + media: { enabled: true, reason: 'available' }, + projectionLagging: false, + ref: { + moduleId: 'billing.invoices', + resourceId: 'invoice-42', + resourceType: 'invoice', + }, + timeline: [ + { + occurredAt: '2026-01-02T03:04:05.000Z', + summary: 'Invoice issued', + timelineEntryId: 'timeline-1', + }, + ], + ...overrides, + }); + +const readyModel = (resource: ShellResourceResponse = resourceFixture()): ReadyModel => ({ + resource, + shell, + state: 'ready', +}); + +const forbiddenProblem = Schema.decodeUnknownSync(ShellTargetForbiddenProblemSchema)({ + _tag: 'ShellTargetForbiddenProblem', + detail: 'The principal cannot attach media to this resource.', + status: 403, + title: 'Shell target forbidden', + type: 'https://ontos.dev/problems/shell-target-forbidden', +}); + +const attachButton = () => screen.getByRole('button'); + +const lastDashboardProps = (): DashboardPageProps => { + const props = dashboardRenders.at(-1); + if (props === undefined) { + throw new Error('The dashboard layout was never rendered.'); + } + return props; +}; + +const renderResourcePage = (model: ResourcePageModel) => { + useLoaderDataMock.mockReturnValue(model); + return render(); +}; + +beforeEach(() => { + browserRunPromiseMock.mockImplementation(browserRuntime.runPromise); + attachResourceMediaMock.mockReturnValue(Effect.succeed(attachedResponse)); + shellControlsMock.mockReturnValue({ + handleLegalEntityChange: rstest.fn(), + handleLogout: rstest.fn(), + handleSearch: rstest.fn(), + handleTenantChange: rstest.fn(), + legalEntitySwitchFailed: false, + legalEntitySwitchPending: false, + logoutFailed: false, + logoutPending: false, + tenantSwitchFailed: false, + tenantSwitchPending: false, + }); +}); + +afterEach(() => { + cleanup(); + dashboardRenders.length = 0; + rstest.clearAllMocks(); +}); + +it.each([ + { blockedText: 'The dashboard is unavailable', shellState: 'unavailable' as const }, + { blockedText: 'Select a legal entity first', shellState: 'anonymous' as const }, +])( + 'refuses the whole page for a $shellState shell without a dashboard or an attach seam', + ({ blockedText, shellState }) => { + renderResourcePage({ shell: { state: shellState }, state: 'unavailable' }); + + expect(screen.getByText(blockedText)).toBeTruthy(); + expect(screen.queryByRole('button')).toBeNull(); + expect(screen.queryByText('Media')).toBeNull(); + expect(dashboardRenders).toHaveLength(0); + expect(shellControlsMock).toHaveBeenCalledWith(undefined); + expect(attachResourceMediaMock).not.toHaveBeenCalled(); + expect(browserRunPromiseMock).not.toHaveBeenCalled(); + }, +); + +const closedStates: readonly { readonly closedText: string; readonly state: ClosedState }[] = [ + { closedText: 'You cannot open this resource', state: 'forbidden' }, + { closedText: 'This resource does not exist', state: 'not_found' }, + { closedText: 'Select a legal entity first', state: 'selection_required' }, + { closedText: 'This resource is unavailable', state: 'unavailable' }, +]; + +it.each(closedStates)( + 'keeps a $state resource inside the dashboard with no media seam at all', + ({ closedText, state }) => { + renderResourcePage({ shell, state }); + + expect(screen.getByText(closedText)).toBeTruthy(); + expect(screen.queryByRole('button')).toBeNull(); + expect(screen.queryByText('Media')).toBeNull(); + expect(screen.queryByText('Timeline')).toBeNull(); + expect(lastDashboardProps().title).toBe('Resource'); + expect(lastDashboardProps().currentModuleId).toBeUndefined(); + expect(shellControlsMock).toHaveBeenCalledWith(shell); + expect(attachResourceMediaMock).not.toHaveBeenCalled(); + expect(browserRunPromiseMock).not.toHaveBeenCalled(); + }, +); + +it('titles the dashboard from the resource and scopes it to the owning module', () => { + renderResourcePage(readyModel()); + + expect(lastDashboardProps().title).toBe('Invoice 42'); + expect(lastDashboardProps().currentModuleId).toBe('billing.invoices'); + expect(screen.getByRole('heading', { level: 2, name: 'Invoice 42' })).toBeTruthy(); + expect(screen.getByText('Status')).toBeTruthy(); + expect(screen.getByText('1 250,00 CZK')).toBeTruthy(); +}); + +it('renders one timeline entry per projection row with its ISO instant', () => { + renderResourcePage(readyModel()); + + expect(screen.getByText('2026-01-02T03:04:05.000Z')).toBeTruthy(); + expect(screen.getByText(/Invoice issued/u)).toBeTruthy(); + expect(screen.queryByText('No timeline entries yet')).toBeNull(); + expect(screen.queryByText('The timeline is catching up')).toBeNull(); +}); + +it('announces an empty and a lagging timeline projection', () => { + renderResourcePage(readyModel(resourceFixture({ projectionLagging: true, timeline: [] }))); + + expect(screen.getByText('No timeline entries yet')).toBeTruthy(); + expect(screen.getByText('The timeline is catching up')).toBeTruthy(); +}); + +const disabledMediaCases = [ + { blockedText: 'This module is read only', reason: 'read_only' as const }, + { blockedText: 'This resource has no media', reason: 'absent' as const }, + { blockedText: 'You cannot attach media here', reason: 'forbidden' as const }, + { blockedText: 'Media attachment is unavailable', reason: 'unavailable' as const }, +]; + +it.live.each(disabledMediaCases)( + 'disables the attach seam and explains a $reason media policy without running an Effect', + ({ blockedText, reason }) => + Effect.gen(function* refusesADisabledAttachSeam() { + const user = userEvent.setup(); + renderResourcePage(readyModel(resourceFixture({ media: { enabled: false, reason } }))); + + expect(screen.getByText(blockedText)).toBeTruthy(); + expect(attachButton().hasAttribute('disabled')).toBe(true); + + yield* Effect.promise(() => user.click(attachButton())); + + expect(attachResourceMediaMock).not.toHaveBeenCalled(); + expect(browserRunPromiseMock).not.toHaveBeenCalled(); + expect(screen.queryByText('Media attached')).toBeNull(); + expect(screen.queryByText('Attaching the media failed')).toBeNull(); + }), +); + +it('marks the attachment pending inside the very click that starts it', () => { + const gate = Deferred.makeUnsafe(); + attachResourceMediaMock.mockReturnValue(Deferred.await(gate)); + renderResourcePage(readyModel()); + + act(() => { + attachButton().click(); + }); + + expect(attachButton().hasAttribute('disabled')).toBe(true); + expect(screen.getByText('Attaching media…')).toBeTruthy(); +}); + +it('starts one attachment for a double click, because the first click already disabled the seam', () => { + const gate = Deferred.makeUnsafe(); + attachResourceMediaMock.mockReturnValue(Deferred.await(gate)); + renderResourcePage(readyModel()); + + act(() => { + attachButton().click(); + }); + act(() => { + attachButton().click(); + }); + + expect(attachResourceMediaMock).toHaveBeenCalledTimes(1); + expect(browserRunPromiseMock).toHaveBeenCalledTimes(1); +}); + +it.live('holds the attach seam disabled for the whole in-flight attachment', () => + Effect.gen(function* holdsTheAttachSeamDisabled() { + const gate = yield* Deferred.make(); + attachResourceMediaMock.mockReturnValue(Deferred.await(gate)); + const user = userEvent.setup(); + renderResourcePage(readyModel()); + + yield* Effect.promise(() => user.click(attachButton())); + yield* Effect.promise(() => + waitFor(() => expect(screen.getByText('Attaching media…')).toBeTruthy()), + ); + expect(attachButton().hasAttribute('disabled')).toBe(true); + + yield* Effect.promise(() => user.click(attachButton())); + expect(attachResourceMediaMock).toHaveBeenCalledTimes(1); + expect(browserRunPromiseMock).toHaveBeenCalledTimes(1); + + yield* Deferred.succeed(gate, attachedResponse); + yield* Effect.promise(() => + waitFor(() => expect(screen.getByText('Media attached')).toBeTruthy()), + ); + expect(attachButton().hasAttribute('disabled')).toBe(false); + }), +); + +it.live('attaches media for the loaded resource reference and reports success once', () => + Effect.gen(function* attachesMediaForTheLoadedResource() { + const user = userEvent.setup(); + const model = readyModel(); + renderResourcePage(model); + + yield* Effect.promise(() => user.click(attachButton())); + yield* Effect.promise(() => + waitFor(() => expect(screen.getByText('Media attached')).toBeTruthy()), + ); + + expect(attachResourceMediaMock).toHaveBeenCalledTimes(1); + expect(attachResourceMediaMock).toHaveBeenCalledWith(model.resource.ref); + expect(screen.queryByText('Attaching media…')).toBeNull(); + expect(screen.queryByText('Attaching the media failed')).toBeNull(); + expect(attachButton().hasAttribute('disabled')).toBe(false); + }), +); + +it.live('settles a typed attachment failure into its own status without a defect', () => + Effect.gen(function* settlesATypedAttachmentFailure() { + attachResourceMediaMock.mockReturnValue(Effect.fail(forbiddenProblem)); + const user = userEvent.setup(); + renderResourcePage(readyModel()); + + yield* Effect.promise(() => user.click(attachButton())); + yield* Effect.promise(() => + waitFor(() => expect(screen.getByText('Attaching the media failed')).toBeTruthy()), + ); + + expect(screen.queryByText('Media attached')).toBeNull(); + expect(screen.queryByText('Attaching media…')).toBeNull(); + expect(attachButton().hasAttribute('disabled')).toBe(false); + }), +); + +it.live('retries after a failure and replaces the failure status with success', () => + Effect.gen(function* retriesAfterAFailure() { + attachResourceMediaMock.mockReturnValueOnce(Effect.fail(forbiddenProblem)); + const user = userEvent.setup(); + renderResourcePage(readyModel()); + + yield* Effect.promise(() => user.click(attachButton())); + yield* Effect.promise(() => + waitFor(() => expect(screen.getByText('Attaching the media failed')).toBeTruthy()), + ); + + yield* Effect.promise(() => user.click(attachButton())); + yield* Effect.promise(() => + waitFor(() => expect(screen.getByText('Media attached')).toBeTruthy()), + ); + + expect(attachResourceMediaMock).toHaveBeenCalledTimes(2); + expect(browserRunPromiseMock).toHaveBeenCalledTimes(2); + expect(screen.queryByText('Attaching the media failed')).toBeNull(); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/shell-composition.test.ts b/app/apps/shell-super-app/tests/unit/shell-composition.test.ts index a862b37bb..a95887300 100644 --- a/app/apps/shell-super-app/tests/unit/shell-composition.test.ts +++ b/app/apps/shell-super-app/tests/unit/shell-composition.test.ts @@ -1,5 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { expect, test } from '@rstest/core'; +import { expect, it } from 'effect-rstest'; import { buildInstalledModuleCatalog, resolveInstalledModuleCatalog } from '@app/core-runtime'; import type { ContextAccessDecision, @@ -185,100 +184,102 @@ const contextAccess = ( const context = { legalEntityId, principalId, tenantId } as const; -test('composes one deterministic state and permission batch with lifecycle affordances', async () => { - let stateBatches = 0; - let permissionBatches = 0; - const composition = makeShellComposition({ - catalog: Effect.succeed(catalog()), - contextAccess: contextAccess( - { 'documents.center': 'allowed', 'property.registry': 'allowed' }, - () => (permissionBatches += 1), - ), - moduleStates: { - getTenantModuleStates: (_tenantId, moduleIds) => { - stateBatches += 1; - return Effect.succeed( - moduleIds.map((moduleKey) => ({ - moduleKey, - state: - moduleKey === 'documents.center' ? ('read_only' as const) : ('deprecated' as const), - })), - ); +it.effect('composes one deterministic state and permission batch with lifecycle affordances', () => + Effect.gen(function* composesOneDeterministicStateAndPermission() { + let stateBatches = 0; + let permissionBatches = 0; + const composition = makeShellComposition({ + catalog: Effect.succeed(catalog()), + contextAccess: contextAccess( + { 'documents.center': 'allowed', 'property.registry': 'allowed' }, + () => (permissionBatches += 1), + ), + moduleStates: { + getTenantModuleStates: (_tenantId, moduleIds) => { + stateBatches += 1; + return Effect.succeed( + moduleIds.map((moduleKey) => ({ + moduleKey, + state: + moduleKey === 'documents.center' ? ('read_only' as const) : ('deprecated' as const), + })), + ); + }, }, - }, - }); - const result = await runEffectTestPromise(composition.compose(context)); - expect(result).toEqual({ - navigation: [ + }); + const result = yield* composition.compose(context); + expect(result).toEqual({ + navigation: [ + { + appId: 'documents-center', + enabled: true, + groupKey: 'shell.navigation.modules', + href: '/documents-center', + label: 'Documents', + moduleId: 'documents.center', + order: 10, + state: 'read_only', + unavailable: false, + writable: false, + }, + { + appId: 'property-registry', + enabled: true, + groupKey: 'shell.navigation.modules', + href: '/property-registry', + label: 'Property', + moduleId: 'property.registry', + order: 20, + state: 'deprecated', + unavailable: false, + writable: false, + }, + ], + state: 'available', + unavailableDeployments: [], + }); + expect({ permissionBatches, stateBatches }).toEqual({ permissionBatches: 1, stateBatches: 1 }); + }), +); + +it.effect('keeps healthy navigation and exposes failed installed deployments separately', () => + Effect.gen(function* keepsHealthyNavigationAndExposesFailed() { + const degradedCatalog = resolveInstalledModuleCatalog([ { - appId: 'documents-center', - enabled: true, - groupKey: 'shell.navigation.modules', - href: '/documents-center', - label: 'Documents', - moduleId: 'documents.center', - order: 10, - state: 'read_only', - unavailable: false, - writable: false, + contract: deployment('documents-center', 'documents.center', 'Documents', 10), + expectedAppId: 'documents-center', + outcome: 'fetched', }, { - appId: 'property-registry', - enabled: true, - groupKey: 'shell.navigation.modules', - href: '/property-registry', - label: 'Property', - moduleId: 'property.registry', - order: 20, - state: 'deprecated', - unavailable: false, - writable: false, + expectedAppId: 'property-registry', + outcome: 'failed', + reason: 'timeout', }, - ], - state: 'available', - unavailableDeployments: [], - }); - expect({ permissionBatches, stateBatches }).toEqual({ permissionBatches: 1, stateBatches: 1 }); -}); - -test('keeps healthy navigation and exposes failed installed deployments separately', async () => { - const degradedCatalog = resolveInstalledModuleCatalog([ - { - contract: deployment('documents-center', 'documents.center', 'Documents', 10), - expectedAppId: 'documents-center', - outcome: 'fetched', - }, - { - expectedAppId: 'property-registry', - outcome: 'failed', - reason: 'timeout', - }, - ]); - const result = await runEffectTestPromise( - makeShellComposition({ + ]); + const result = yield* makeShellComposition({ catalog: Effect.succeed(degradedCatalog), contextAccess: contextAccess({ 'documents.center': 'allowed' }), moduleStates: { getTenantModuleStates: (_tenantId, moduleIds) => Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state: 'active' }))), }, - }).compose(context), - ); + }).compose(context); - expect(result.state).toBe('available'); - if (result.state !== 'available') { - throw new Error('expected an available degraded composition'); - } - expect(result.navigation.map(({ moduleId }) => moduleId)).toEqual(['documents.center']); - expect(result.unavailableDeployments).toEqual([ - { appId: 'property-registry', reason: 'timeout', status: 'unavailable' }, - ]); - expect(() => Schema.decodeUnknownSync(ShellCompositionSchema)(result)).not.toThrow(); -}); + expect(result.state).toBe('available'); + if (result.state !== 'available') { + throw new Error('expected an available degraded composition'); + } + expect(result.navigation.map(({ moduleId }) => moduleId)).toEqual(['documents.center']); + expect(result.unavailableDeployments).toEqual([ + { appId: 'property-registry', reason: 'timeout', status: 'unavailable' }, + ]); + expect(() => Schema.decodeUnknownSync(ShellCompositionSchema)(result)).not.toThrow(); + }), +); -test('normalizes number-like module order before returning the public composition', async () => { - const result = await runEffectTestPromise( - makeShellComposition({ +it.effect('normalizes number-like module order before returning the public composition', () => + Effect.gen(function* normalizesNumberLikeModuleOrderBefore() { + const result = yield* makeShellComposition({ catalog: Effect.succeed(catalogWithNumberLikeOrder()), contextAccess: contextAccess({ 'documents.center': 'allowed', @@ -288,19 +289,19 @@ test('normalizes number-like module order before returning the public compositio getTenantModuleStates: (_tenantId, moduleIds) => Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state: 'active' }))), }, - }).compose(context), - ); + }).compose(context); - expect(result.navigation.map(({ order }) => order)).toEqual([10, 20]); - expect(result.navigation.every(({ order }) => Object.is(order, Number(order)))).toBe(true); - expect(() => Schema.decodeUnknownSync(ShellCompositionSchema)(result)).not.toThrow(); -}); + expect(result.navigation.map(({ order }) => order)).toEqual([10, 20]); + expect(result.navigation.every(({ order }) => Object.is(order, Number(order)))).toBe(true); + expect(() => Schema.decodeUnknownSync(ShellCompositionSchema)(result)).not.toThrow(); + }), +); -test.each(['inactive', 'suspended', 'quarantined', 'archived'] as const)( +it.effect.each(['inactive', 'suspended', 'quarantined', 'archived'] as const)( 'hides the %s lifecycle from normal navigation', - async (state) => { - const result = await runEffectTestPromise( - makeShellComposition({ + (state) => + Effect.gen(function* inactive() { + const result = yield* makeShellComposition({ catalog: Effect.succeed(catalog()), contextAccess: contextAccess({ 'documents.center': 'allowed', @@ -310,15 +311,14 @@ test.each(['inactive', 'suspended', 'quarantined', 'archived'] as const)( getTenantModuleStates: (_tenantId, moduleIds) => Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state }))), }, - }).compose(context), - ); - expect(result).toEqual({ navigation: [], state: 'available', unavailableDeployments: [] }); - }, + }).compose(context); + expect(result).toEqual({ navigation: [], state: 'available', unavailableDeployments: [] }); + }), ); -test('omits definite denial while preserving unavailable authorization as disabled', async () => { - const result = await runEffectTestPromise( - makeShellComposition({ +it.effect('omits definite denial while preserving unavailable authorization as disabled', () => + Effect.gen(function* omitsDefiniteDenialWhilePreservingUnavailable() { + const result = yield* makeShellComposition({ catalog: Effect.succeed(catalog()), contextAccess: contextAccess({ 'documents.center': 'denied', @@ -328,121 +328,119 @@ test('omits definite denial while preserving unavailable authorization as disabl getTenantModuleStates: (_tenantId, moduleIds) => Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state: 'active' }))), }, - }).compose(context), - ); - expect(result.state).toBe('available'); - expect(result.navigation).toEqual([ - { - appId: 'property-registry', - enabled: false, - groupKey: 'shell.navigation.modules', - label: 'Property', - moduleId: 'property.registry', - order: 20, - state: 'active', - unavailable: true, - writable: true, - }, - ]); -}); + }).compose(context); + expect(result.state).toBe('available'); + expect(result.navigation).toEqual([ + { + appId: 'property-registry', + enabled: false, + groupKey: 'shell.navigation.modules', + label: 'Property', + moduleId: 'property.registry', + order: 20, + state: 'active', + unavailable: true, + writable: true, + }, + ]); + }), +); -test('resolves direct targets independently with exhaustive safe outcomes and historical reads', async () => { - let state: TenantModuleState = 'active'; - let decision: ContextAccessDecision = 'allowed'; - const mutableAccess = contextAccess({}); - const composition = makeShellComposition({ - catalog: Effect.succeed(catalog()), - contextAccess: { - ...mutableAccess, - modules: ({ moduleIds }) => Effect.succeed(moduleIds.map((key) => ({ decision, key }))), - }, - moduleStates: { - getTenantModuleStates: (_tenantId, moduleIds) => - Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state }))), - }, - }); - const resolved = await runEffectTestPromise( - composition.resolveModuleTarget(context, { moduleId: 'property.registry' }), - ); - expect(resolved.outcome).toBe('resolved'); - decision = 'denied'; - const forbidden = await runEffectTestPromise( - composition.resolveModuleTarget(context, { moduleId: 'property.registry' }), - ); - expect(forbidden.outcome).toBe('forbidden'); - decision = 'unavailable'; - const unavailable = await runEffectTestPromise( - composition.resolveModuleTarget(context, { moduleId: 'property.registry' }), - ); - expect(unavailable.outcome).toBe('unavailable'); - decision = 'allowed'; - state = 'archived'; - const archived = await runEffectTestPromise( - composition.resolveModuleTarget(context, { moduleId: 'property.registry' }), - ); - expect(archived.outcome).toBe('not_found'); - const historical = await runEffectTestPromise( - composition.resolveModuleTarget(context, { - access: 'historical_read', - moduleId: 'property.registry', +it.effect( + 'resolves direct targets independently with exhaustive safe outcomes and historical reads', + () => + Effect.gen(function* resolvesDirectTargetsIndependentlyWithExhaustive() { + let state: TenantModuleState = 'active'; + let decision: ContextAccessDecision = 'allowed'; + const mutableAccess = contextAccess({}); + const composition = makeShellComposition({ + catalog: Effect.succeed(catalog()), + contextAccess: { + ...mutableAccess, + modules: ({ moduleIds }) => Effect.succeed(moduleIds.map((key) => ({ decision, key }))), + }, + moduleStates: { + getTenantModuleStates: (_tenantId, moduleIds) => + Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state }))), + }, + }); + const resolved = yield* composition.resolveModuleTarget(context, { + moduleId: 'property.registry', + }); + expect(resolved.outcome).toBe('resolved'); + decision = 'denied'; + const forbidden = yield* composition.resolveModuleTarget(context, { + moduleId: 'property.registry', + }); + expect(forbidden.outcome).toBe('forbidden'); + decision = 'unavailable'; + const unavailable = yield* composition.resolveModuleTarget(context, { + moduleId: 'property.registry', + }); + expect(unavailable.outcome).toBe('unavailable'); + decision = 'allowed'; + state = 'archived'; + const archived = yield* composition.resolveModuleTarget(context, { + moduleId: 'property.registry', + }); + expect(archived.outcome).toBe('not_found'); + const historical = yield* composition.resolveModuleTarget(context, { + access: 'historical_read', + moduleId: 'property.registry', + }); + expect(historical.outcome).toBe('resolved'); + const selectionRequired = yield* composition.resolveModuleTarget( + { principalId, tenantId }, + { moduleId: 'property.registry' }, + ); + expect(selectionRequired.outcome).toBe('selection_required'); + const missing = yield* composition.resolveModuleTarget(context, { + moduleId: 'missing.module', + }); + expect(missing.outcome).toBe('not_found'); }), - ); - expect(historical.outcome).toBe('resolved'); - const selectionRequired = await runEffectTestPromise( - composition.resolveModuleTarget({ principalId, tenantId }, { moduleId: 'property.registry' }), - ); - expect(selectionRequired.outcome).toBe('selection_required'); - const missing = await runEffectTestPromise( - composition.resolveModuleTarget(context, { moduleId: 'missing.module' }), - ); - expect(missing.outcome).toBe('not_found'); -}); +); -test.each(['active', 'read_only', 'deprecated'] as const)( +it.effect.each(['active', 'read_only', 'deprecated'] as const)( 'resolves the exact page entrypoint in the %s lifecycle without changing module landing', - async (state) => { - const composition = makeShellComposition({ - catalog: Effect.succeed(catalogWithSecondPropertyPage()), - contextAccess: contextAccess({ - 'documents.center': 'allowed', - 'property.registry': 'allowed', - }), - moduleStates: { - getTenantModuleStates: (_tenantId, moduleIds) => - Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state }))), - }, - }); - const landing = await runEffectTestPromise( - composition.resolveModuleTarget(context, { moduleId: 'property.registry' }), - ); - const customers = await runEffectTestPromise( - composition.resolveModuleTarget(context, { + (state) => + Effect.gen(function* active() { + const composition = makeShellComposition({ + catalog: Effect.succeed(catalogWithSecondPropertyPage()), + contextAccess: contextAccess({ + 'documents.center': 'allowed', + 'property.registry': 'allowed', + }), + moduleStates: { + getTenantModuleStates: (_tenantId, moduleIds) => + Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state }))), + }, + }); + const landing = yield* composition.resolveModuleTarget(context, { + moduleId: 'property.registry', + }); + const customers = yield* composition.resolveModuleTarget(context, { entrypointKey: 'property.registry.page.customers', moduleId: 'property.registry', - }), - ); - expect(landing).toMatchObject({ - outcome: 'resolved', - page: { componentKey: 'property.registry.page-home' }, - }); - expect(customers).toMatchObject({ - outcome: 'resolved', - page: { componentKey: 'property.registry.page-customers' }, - writable: state === 'active', - }); - const missingPage = await runEffectTestPromise( - composition.resolveModuleTarget(context, { + }); + expect(landing).toMatchObject({ + outcome: 'resolved', + page: { componentKey: 'property.registry.page-home' }, + }); + expect(customers).toMatchObject({ + outcome: 'resolved', + page: { componentKey: 'property.registry.page-customers' }, + writable: state === 'active', + }); + const missingPage = yield* composition.resolveModuleTarget(context, { entrypointKey: 'property.registry.page.missing', moduleId: 'property.registry', - }), - ); - expect(missingPage.outcome).toBe('not_found'); - const crossOwnedPage = await runEffectTestPromise( - composition.resolveModuleTarget(context, { + }); + expect(missingPage.outcome).toBe('not_found'); + const crossOwnedPage = yield* composition.resolveModuleTarget(context, { entrypointKey: 'documents.center.page.home', moduleId: 'property.registry', - }), - ); - expect(crossOwnedPage.outcome).toBe('not_found'); - }, + }); + expect(crossOwnedPage.outcome).toBe('not_found'); + }), ); diff --git a/app/apps/shell-super-app/tests/unit/shell-governed-read-schemas.test.ts b/app/apps/shell-super-app/tests/unit/shell-governed-read-schemas.test.ts index d7f29f125..dd8d5165c 100644 --- a/app/apps/shell-super-app/tests/unit/shell-governed-read-schemas.test.ts +++ b/app/apps/shell-super-app/tests/unit/shell-governed-read-schemas.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from '@rstest/core'; +import { describe, expect, test } from 'effect-rstest'; import { Schema } from 'effect'; import { GovernedResolvedModuleTargetSchema, diff --git a/app/apps/shell-super-app/tests/unit/shell-resources.test.ts b/app/apps/shell-super-app/tests/unit/shell-resources.test.ts index 30f67bc2f..5735eca6d 100644 --- a/app/apps/shell-super-app/tests/unit/shell-resources.test.ts +++ b/app/apps/shell-super-app/tests/unit/shell-resources.test.ts @@ -1,5 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { expect, test } from '@rstest/core'; +import { expect, it } from 'effect-rstest'; import { buildInstalledModuleCatalog } from '@app/core-runtime'; import type { ContextAccessDecision, @@ -216,421 +215,443 @@ const dependencies = ( }; }; -test('search treats empty input as empty without touching providers', async () => { - let calls = 0; - const search = makeShellSearch(dependencies(), { - search: () => { - calls += 1; - return Effect.succeed([]); - }, - }); - await expect(runEffectTestPromise(search.search(context, ' '))).resolves.toEqual({ - partial: false, - results: [], - }); - expect(calls).toBe(0); -}); +it.effect('search treats empty input as empty without touching providers', () => + Effect.gen(function* searchTreatsEmptyInputAsEmpty() { + let calls = 0; + const search = makeShellSearch(dependencies(), { + search: () => { + calls += 1; + return Effect.succeed([]); + }, + }); + expect(yield* search.search(context, ' ')).toEqual({ + partial: false, + results: [], + }); + expect(calls).toBe(0); + }), +); -test('search keeps an eligible provider with zero candidates as a successful empty result', async () => { - const baseline = dependencies(); - const result = await runEffectTestPromise( - makeShellSearch( - { - ...baseline, - contextAccess: { - ...baseline.contextAccess, - resources: () => Effect.die('empty results must not authorize an empty resource batch'), +it.effect( + 'search keeps an eligible provider with zero candidates as a successful empty result', + () => + Effect.gen(function* searchKeepsAnEligibleProviderWith() { + const baseline = dependencies(); + const result = yield* makeShellSearch( + { + ...baseline, + contextAccess: { + ...baseline.contextAccess, + resources: () => Effect.die('empty results must not authorize an empty resource batch'), + }, }, - }, - { search: () => Effect.succeed([]) }, - ).search(context, 'unit'), - ); - expect(result).toEqual({ partial: false, results: [] }); -}); + { search: () => Effect.succeed([]) }, + ).search(context, 'unit'); + expect(result).toEqual({ partial: false, results: [] }); + }), +); -test('search filters resource denials and reports partial provider failure', async () => { - const result = await runEffectTestPromise( - makeShellSearch(dependencies('active', 'allowed', 'denied'), { +it.effect('search filters resource denials and reports partial provider failure', () => + Effect.gen(function* searchFiltersResourceDenialsAndReports() { + const result = yield* makeShellSearch(dependencies('active', 'allowed', 'denied'), { search: () => Effect.succeed([{ ref, title: 'Unit 1' }]), - }).search(context, ' unit '), - ); - expect(result).toEqual({ partial: false, results: [] }); + }).search(context, ' unit '); + expect(result).toEqual({ partial: false, results: [] }); - const installed = catalog(); - const [contract] = installed.contracts; - if (contract === undefined) { - throw new TypeError('The search fixture must install its module contract'); - } - const backupSearchKey = 'property.registry.backup-unit-search'; - const catalogWithBackupSearch = buildInstalledModuleCatalog([ - { - contract: { + const installed = catalog(); + const [contract] = installed.contracts; + if (contract === undefined) { + throw new TypeError('The search fixture must install its module contract'); + } + const backupSearchKey = 'property.registry.backup-unit-search'; + const catalogWithBackupSearch = buildInstalledModuleCatalog([ + { + contract: { + ...contract, + manifest: { + ...contract.manifest, + publicSurface: { + ...contract.manifest.publicSurface, + search: [ + ...contract.manifest.publicSurface.search, + { + accessFiltering: 'resource_permission' as const, + key: backupSearchKey, + owningModuleId: moduleId, + resourceType, + }, + ], + shellContributions: { + ...contract.manifest.publicSurface.shellContributions, + search: [ + ...contract.manifest.publicSurface.shellContributions.search, + { + contributionKey: 'property.registry.search.backup-unit', + entrypoint: { + ...entrypoint('search'), + entrypointKey: 'property.registry.search.backup', + }, + searchKey: backupSearchKey, + }, + ], + }, + }, + }, + }, + expectedAppId: 'property-registry', + }, + ]); + const partial = makeShellSearch( + { + ...dependencies(), + catalog: Effect.succeed(catalogWithBackupSearch), + }, + { + search: ({ searchKey }) => + searchKey === backupSearchKey + ? Effect.fail(new ShellProviderUnavailableError()) + : Effect.succeed([{ ref, title: 'Unit 1' }]), + }, + ); + expect(yield* partial.search(context, 'unit')).toEqual({ + partial: true, + results: [{ kind: 'resource', ref, title: 'Unit 1' }], + }); + }), +); + +it.effect( + 'tenant-scoped Party search needs no Legal Entity, forwards declared filters and preserves identity metadata', + () => + Effect.gen(function* tenantScopedPartySearchNeedsNo() { + const installed = catalog(); + const [contract] = installed.contracts; + if (contract === undefined) { + throw new Error('The test catalog must include one installed contract'); + } + const [partyResourceDescriptor] = contract.manifest.publicSurface.resourceTypes; + if (partyResourceDescriptor === undefined) { + throw new Error('The test catalog must include one resource type'); + } + const partyResourceType = 'party.registry.party'; + const partySearchKey = 'party.registry.party-search'; + const partyModuleId = 'party.registry'; + const partyContract = { ...contract, + deployment: { ...contract.deployment, appId: 'party-registry' }, manifest: { ...contract.manifest, + module: { ...contract.manifest.module, id: partyModuleId }, publicSurface: { ...contract.manifest.publicSurface, + actions: [], + api: [], + resourceTypes: [ + { + ...partyResourceDescriptor, + key: partyResourceType, + owningModuleId: partyModuleId, + }, + ], search: [ - ...contract.manifest.publicSurface.search, { - accessFiltering: 'resource_permission' as const, - key: backupSearchKey, - owningModuleId: moduleId, - resourceType, + accessFiltering: 'tenant_scope' as const, + key: partySearchKey, + owningModuleId: partyModuleId, + requestFilters: ['includeArchived'] as const, + resourceType: partyResourceType, + tenantPermission: 'read_party_identity' as const, }, ], shellContributions: { - ...contract.manifest.publicSurface.shellContributions, + mediaAttachments: [], + navigation: [], + pages: [], + publicComponents: [], + reports: [], + resourceDetails: [], search: [ - ...contract.manifest.publicSurface.shellContributions.search, { - contributionKey: 'property.registry.search.backup-unit', + contributionKey: 'party.registry.search.party', entrypoint: { - ...entrypoint('search'), - entrypointKey: 'property.registry.search.backup', + access: 'read' as const, + authorization: { + kind: 'context_permission' as const, + permission: 'module.access', + }, + entrypointKey: 'party.registry.search.party', + moduleKey: partyModuleId, + role: 'search' as const, + scope: 'tenant' as const, }, - searchKey: backupSearchKey, + searchKey: partySearchKey, }, ], + timelines: [], }, }, }, - }, - expectedAppId: 'property-registry', - }, - ]); - const partial = makeShellSearch( - { - ...dependencies(), - catalog: Effect.succeed(catalogWithBackupSearch), - }, - { - search: ({ searchKey }) => - searchKey === backupSearchKey - ? Effect.fail(new ShellProviderUnavailableError()) - : Effect.succeed([{ ref, title: 'Unit 1' }]), - }, - ); - await expect(runEffectTestPromise(partial.search(context, 'unit'))).resolves.toEqual({ - partial: true, - results: [{ kind: 'resource', ref, title: 'Unit 1' }], - }); -}); - -test('tenant-scoped Party search needs no Legal Entity, forwards declared filters and preserves identity metadata', async () => { - const installed = catalog(); - const [contract] = installed.contracts; - if (contract === undefined) { - throw new Error('The test catalog must include one installed contract'); - } - const [partyResourceDescriptor] = contract.manifest.publicSurface.resourceTypes; - if (partyResourceDescriptor === undefined) { - throw new Error('The test catalog must include one resource type'); - } - const partyResourceType = 'party.registry.party'; - const partySearchKey = 'party.registry.party-search'; - const partyModuleId = 'party.registry'; - const partyContract = { - ...contract, - deployment: { ...contract.deployment, appId: 'party-registry' }, - manifest: { - ...contract.manifest, - module: { ...contract.manifest.module, id: partyModuleId }, - publicSurface: { - ...contract.manifest.publicSurface, - actions: [], - api: [], - resourceTypes: [ - { - ...partyResourceDescriptor, - key: partyResourceType, - owningModuleId: partyModuleId, + }; + const partyCatalog = buildInstalledModuleCatalog([ + { contract: partyContract, expectedAppId: 'party-registry' }, + ]); + const calls: unknown[] = []; + const baseline = dependencies(); + const result = yield* makeShellSearch( + { + ...baseline, + catalog: Effect.succeed(partyCatalog), + contextAccess: { + ...baseline.contextAccess, + modules: () => Effect.die('tenant-scoped search must not require module access'), + resources: () => Effect.die('tenant-scoped search must not require resource access'), + tenants: ({ permission, tenantIds }) => { + calls.push({ permission, tenantIds }); + return Effect.succeed([{ decision: 'allowed', key: tenantId }]); + }, }, - ], - search: [ + }, + { + search: (input) => { + calls.push(input); + return Effect.succeed([ + { + archived: true, + matchedViaAlias: true, + ref: { + moduleId: partyModuleId, + resourceId: 'party-1', + resourceType: partyResourceType, + tenantId, + }, + title: 'Canonical Party', + }, + ]); + }, + }, + ).search(tenantContext, { includeArchived: true, query: ' party ', role: 'CUSTOMER' }); + + expect(calls[0]).toEqual({ permission: 'read_party_identity', tenantIds: [tenantId] }); + expect(calls[1]).toMatchObject({ includeArchived: true, query: 'party' }); + expect(calls[1]).not.toHaveProperty('role'); + expect(result).toEqual({ + partial: false, + results: [ { - accessFiltering: 'tenant_scope' as const, - key: partySearchKey, - owningModuleId: partyModuleId, - requestFilters: ['includeArchived'] as const, - resourceType: partyResourceType, - tenantPermission: 'read_party_identity' as const, + archived: true, + kind: 'party', + matchedViaAlias: true, + ref: { + moduleId: partyModuleId, + resourceId: 'party-1', + resourceType: partyResourceType, + tenantId, + }, + title: 'Canonical Party', }, ], - shellContributions: { - mediaAttachments: [], - navigation: [], - pages: [], - publicComponents: [], - reports: [], - resourceDetails: [], - search: [ - { - contributionKey: 'party.registry.search.party', - entrypoint: { - access: 'read' as const, - authorization: { kind: 'context_permission' as const, permission: 'module.access' }, - entrypointKey: 'party.registry.search.party', - moduleKey: partyModuleId, - role: 'search' as const, - scope: 'tenant' as const, + }); + }), +); + +it.effect('search fails only when every eligible provider fails', () => + Effect.gen(function* searchFailsOnlyWhenEveryEligible() { + const effect = makeShellSearch(dependencies(), { + search: () => Effect.fail(new ShellProviderUnavailableError()), + }).search(context, 'unit'); + expect(Schema.is(ShellProviderUnavailableError)(yield* Effect.flip(effect))).toBe(true); + }), +); + +it.effect( + 'Counterparty search preserves both identities, selected scope, roles and collision metadata', + () => + Effect.gen(function* CounterpartySearchPreservesBothIdentitiesSelected() { + const [contract] = catalog().contracts; + if (contract === undefined) { + throw new Error('The test catalog must include one installed contract'); + } + const filteredCatalog = buildInstalledModuleCatalog([ + { + contract: { + ...contract, + manifest: { + ...contract.manifest, + publicSurface: { + ...contract.manifest.publicSurface, + search: contract.manifest.publicSurface.search.map((descriptor) => ({ + ...descriptor, + requestFilters: ['includeArchived', 'role'] as const, + })), }, - searchKey: partySearchKey, }, - ], - timelines: [], - }, - }, - }, - }; - const partyCatalog = buildInstalledModuleCatalog([ - { contract: partyContract, expectedAppId: 'party-registry' }, - ]); - const calls: unknown[] = []; - const baseline = dependencies(); - const result = await runEffectTestPromise( - makeShellSearch( - { - ...baseline, - catalog: Effect.succeed(partyCatalog), - contextAccess: { - ...baseline.contextAccess, - modules: () => Effect.die('tenant-scoped search must not require module access'), - resources: () => Effect.die('tenant-scoped search must not require resource access'), - tenants: ({ permission, tenantIds }) => { - calls.push({ permission, tenantIds }); - return Effect.succeed([{ decision: 'allowed', key: tenantId }]); }, + expectedAppId: 'property-registry', }, - }, - { - search: (input) => { - calls.push(input); - return Effect.succeed([ - { - archived: true, - matchedViaAlias: true, - ref: { - moduleId: partyModuleId, - resourceId: 'party-1', - resourceType: partyResourceType, - tenantId, - }, - title: 'Canonical Party', - }, - ]); - }, - }, - ).search(tenantContext, { includeArchived: true, query: ' party ', role: 'CUSTOMER' }), - ); - - expect(calls[0]).toEqual({ permission: 'read_party_identity', tenantIds: [tenantId] }); - expect(calls[1]).toMatchObject({ includeArchived: true, query: 'party' }); - expect(calls[1]).not.toHaveProperty('role'); - expect(result).toEqual({ - partial: false, - results: [ - { - archived: true, - kind: 'party', - matchedViaAlias: true, - ref: { - moduleId: partyModuleId, - resourceId: 'party-1', - resourceType: partyResourceType, - tenantId, + ]); + const counterpartyRef = { ...ref, tenantId }; + const canonicalPartyRef = { + ...ref, + resourceId: 'party-1', + resourceType: 'property.registry.party', + tenantId, + }; + const collision = { + counterpartyRefs: [counterpartyRef, { ...counterpartyRef, resourceId: 'unit-2' }], + kind: 'CANONICAL_PARTY_COUNTERPARTY_COLLISION', + }; + const value = { + collision, + currentRoles: ['CUSTOMER', 'SUPPLIER'], + legalEntity: { legalEntityId, tenantId }, + party: { + archived: true, + matchedViaAlias: true, + ref: canonicalPartyRef, + title: 'Canonical Party', }, - title: 'Canonical Party', - }, - ], - }); -}); - -test('search fails only when every eligible provider fails', async () => { - const effect = makeShellSearch(dependencies(), { - search: () => Effect.fail(new ShellProviderUnavailableError()), - }).search(context, 'unit'); - await expect(runEffectTestPromise(effect)).rejects.toBeInstanceOf(ShellProviderUnavailableError); -}); - -test('Counterparty search preserves both identities, selected scope, roles and collision metadata', async () => { - const [contract] = catalog().contracts; - if (contract === undefined) { - throw new Error('The test catalog must include one installed contract'); - } - const filteredCatalog = buildInstalledModuleCatalog([ - { - contract: { - ...contract, - manifest: { - ...contract.manifest, - publicSurface: { - ...contract.manifest.publicSurface, - search: contract.manifest.publicSurface.search.map((descriptor) => ({ - ...descriptor, - requestFilters: ['includeArchived', 'role'] as const, - })), + ref: counterpartyRef, + }; + const calls: unknown[] = []; + const search = makeShellSearch( + { ...dependencies(), catalog: Effect.succeed(filteredCatalog) }, + { + search: (input) => { + calls.push(input); + return Effect.succeed([value]); }, }, - }, - expectedAppId: 'property-registry', - }, - ]); - const counterpartyRef = { ...ref, tenantId }; - const canonicalPartyRef = { - ...ref, - resourceId: 'party-1', - resourceType: 'property.registry.party', - tenantId, - }; - const collision = { - counterpartyRefs: [counterpartyRef, { ...counterpartyRef, resourceId: 'unit-2' }], - kind: 'CANONICAL_PARTY_COUNTERPARTY_COLLISION', - }; - const value = { - collision, - currentRoles: ['CUSTOMER', 'SUPPLIER'], - legalEntity: { legalEntityId, tenantId }, - party: { - archived: true, - matchedViaAlias: true, - ref: canonicalPartyRef, - title: 'Canonical Party', - }, - ref: counterpartyRef, - }; - const calls: unknown[] = []; - const search = makeShellSearch( - { ...dependencies(), catalog: Effect.succeed(filteredCatalog) }, - { - search: (input) => { - calls.push(input); - return Effect.succeed([value]); - }, - }, - ); - const result = await runEffectTestPromise( - search.search(context, { includeArchived: true, query: 'canonical', role: 'CUSTOMER' }), - ); - expect(calls[0]).toMatchObject({ includeArchived: true, role: 'CUSTOMER' }); - expect(result).toEqual({ - partial: false, - results: [{ ...value, kind: 'counterparty', title: 'Canonical Party' }], - }); - expect(await runEffectTestPromise(search.search(tenantContext, 'canonical'))).toEqual({ - partial: false, - results: [], - }); - expect(calls).toHaveLength(1); - const baseline = dependencies(); - const redacted = await runEffectTestPromise( - makeShellSearch( - { - ...baseline, - catalog: Effect.succeed(filteredCatalog), - contextAccess: { - ...baseline.contextAccess, - resources: ({ resources }) => - Effect.succeed( - resources.map((resource) => ({ - decision: - resource.resourceId === 'unit-2' ? ('denied' as const) : ('allowed' as const), - key: `${resource.moduleId}:${resource.resourceType}:${resource.resourceId}`, - })), - ), + ); + const result = yield* search.search(context, { + includeArchived: true, + query: 'canonical', + role: 'CUSTOMER', + }); + expect(calls[0]).toMatchObject({ includeArchived: true, role: 'CUSTOMER' }); + expect(result).toEqual({ + partial: false, + results: [{ ...value, kind: 'counterparty', title: 'Canonical Party' }], + }); + expect(yield* search.search(tenantContext, 'canonical')).toEqual({ + partial: false, + results: [], + }); + expect(calls).toHaveLength(1); + const baseline = dependencies(); + const redacted = yield* makeShellSearch( + { + ...baseline, + catalog: Effect.succeed(filteredCatalog), + contextAccess: { + ...baseline.contextAccess, + resources: ({ resources }) => + Effect.succeed( + resources.map((resource) => ({ + decision: + resource.resourceId === 'unit-2' ? ('denied' as const) : ('allowed' as const), + key: `${resource.moduleId}:${resource.resourceType}:${resource.resourceId}`, + })), + ), + }, }, - }, - { search: () => Effect.succeed([value]) }, - ).search(context, 'canonical'), - ); - expect(JSON.stringify(redacted)).not.toContain('unit-2'); - expect(redacted.results[0]).not.toHaveProperty('collision'); -}); + { search: () => Effect.succeed([value]) }, + ).search(context, 'canonical'); + expect(JSON.stringify(redacted)).not.toContain('unit-2'); + expect(redacted.results[0]).not.toHaveProperty('collision'); + }), +); -test('treats a missing tenant module-state record as hidden rather than authorization uncertainty', async () => { - let calls = 0; - const hiddenDependencies = { - ...dependencies(), - moduleStates: { getTenantModuleStates: () => Effect.succeed([]) }, - }; - await expect( - runEffectTestPromise( - makeShellSearch(hiddenDependencies, { - search: () => { +it.effect( + 'treats a missing tenant module-state record as hidden rather than authorization uncertainty', + () => + Effect.gen(function* treatsAMissingTenantModuleState() { + let calls = 0; + const hiddenDependencies = { + ...dependencies(), + moduleStates: { getTenantModuleStates: () => Effect.succeed([]) }, + }; + expect( + yield* makeShellSearch(hiddenDependencies, { + search: () => { + calls += 1; + return Effect.succeed([{ ref, title: 'Unit 1' }]); + }, + }).search(context, 'unit'), + ).toEqual({ partial: false, results: [] }); + const gateway = { + detail: () => { calls += 1; - return Effect.succeed([{ ref, title: 'Unit 1' }]); + return Effect.succeed({ fields: [], title: 'Unit 1' }); }, - }).search(context, 'unit'), - ), - ).resolves.toEqual({ partial: false, results: [] }); - const gateway = { - detail: () => { - calls += 1; - return Effect.succeed({ fields: [], title: 'Unit 1' }); - }, - timeline: () => Effect.succeed({ entries: [], projectionLagging: false }), - }; - await expect( - runEffectTestPromise( - makeShellResourceDetail(hiddenDependencies, gateway).resolve(context, ref), - ), - ).resolves.toEqual({ outcome: 'not_found' }); - await expect(runEffectTestPromise(attachShellMedia(context, ref))).resolves.toEqual({ - outcome: 'unavailable', - }); - expect(calls).toBe(0); -}); - -test('search fails closed for module or resource authorization uncertainty', async () => { - await expect( - runEffectTestPromise( - makeShellSearch(dependencies('active', 'unavailable'), { - search: () => Effect.succeed([{ ref, title: 'Unit 1' }]), - }).search(context, 'unit'), - ), - ).rejects.toBeInstanceOf(ShellProviderUnavailableError); - await expect( - runEffectTestPromise( - makeShellSearch(dependencies('active', 'allowed', 'unavailable'), { - search: () => Effect.succeed([{ ref, title: 'Unit 1' }]), - }).search(context, 'unit'), - ), - ).rejects.toBeInstanceOf(ShellProviderUnavailableError); -}); + timeline: () => Effect.succeed({ entries: [], projectionLagging: false }), + }; + expect( + yield* makeShellResourceDetail(hiddenDependencies, gateway).resolve(context, ref), + ).toEqual({ outcome: 'not_found' }); + expect(yield* attachShellMedia(context, ref)).toEqual({ + outcome: 'unavailable', + }); + expect(calls).toBe(0); + }), +); -test('resource detail applies catalog, state, module and resource gates before providers', async () => { - let calls = 0; - const provider = { - detail: () => { - calls += 1; - return Effect.succeed({ fields: [], title: 'Unit 1' }); - }, - timeline: () => Effect.succeed({ entries: [], projectionLagging: false }), - }; - expect( - await runEffectTestPromise( - makeShellResourceDetail(dependencies('inactive'), provider).resolve(context, ref), - ), - ).toEqual({ outcome: 'not_found' }); - expect( - await runEffectTestPromise( - makeShellResourceDetail(dependencies('active', 'denied'), provider).resolve(context, ref), - ), - ).toEqual({ outcome: 'forbidden' }); - expect( - await runEffectTestPromise( - makeShellResourceDetail(dependencies('active', 'allowed', 'unavailable'), provider).resolve( - context, - ref, +it.effect('search fails closed for module or resource authorization uncertainty', () => + Effect.gen(function* searchFailsClosedForModuleOr() { + expect( + Schema.is(ShellProviderUnavailableError)( + yield* Effect.flip( + makeShellSearch(dependencies('active', 'unavailable'), { + search: () => Effect.succeed([{ ref, title: 'Unit 1' }]), + }).search(context, 'unit'), + ), ), - ), - ).toEqual({ outcome: 'unavailable' }); - expect(calls).toBe(0); -}); + ).toBe(true); + expect( + Schema.is(ShellProviderUnavailableError)( + yield* Effect.flip( + makeShellSearch(dependencies('active', 'allowed', 'unavailable'), { + search: () => Effect.succeed([{ ref, title: 'Unit 1' }]), + }).search(context, 'unit'), + ), + ), + ).toBe(true); + }), +); -test('resource detail sorts an authorized timeline and exposes projection lag', async () => { - const result = await runEffectTestPromise( - makeShellResourceDetail(dependencies(), { +it.effect( + 'resource detail applies catalog, state, module and resource gates before providers', + () => + Effect.gen(function* resourceDetailAppliesCatalogStateModule() { + let calls = 0; + const provider = { + detail: () => { + calls += 1; + return Effect.succeed({ fields: [], title: 'Unit 1' }); + }, + timeline: () => Effect.succeed({ entries: [], projectionLagging: false }), + }; + expect( + yield* makeShellResourceDetail(dependencies('inactive'), provider).resolve(context, ref), + ).toEqual({ outcome: 'not_found' }); + expect( + yield* makeShellResourceDetail(dependencies('active', 'denied'), provider).resolve( + context, + ref, + ), + ).toEqual({ outcome: 'forbidden' }); + expect( + yield* makeShellResourceDetail( + dependencies('active', 'allowed', 'unavailable'), + provider, + ).resolve(context, ref), + ).toEqual({ outcome: 'unavailable' }); + expect(calls).toBe(0); + }), +); + +it.effect('resource detail sorts an authorized timeline and exposes projection lag', () => + Effect.gen(function* resourceDetailSortsAnAuthorizedTimeline() { + const result = yield* makeShellResourceDetail(dependencies(), { detail: () => Effect.succeed({ fields: [], title: 'Unit 1' }), timeline: () => Effect.succeed({ @@ -640,72 +661,70 @@ test('resource detail sorts an authorized timeline and exposes projection lag', ], projectionLagging: true, }), - }).resolve(context, ref), - ); - expect(result).toEqual({ - detail: { fields: [], title: 'Unit 1' }, - media: { enabled: false, reason: 'unavailable' }, - outcome: 'resolved', - projectionLagging: true, - timeline: [ - { - occurredAt: DateTime.makeUnsafe('2026-02-01T00:00:00Z'), - summary: 'Updated', - timelineEntryId: '2', - }, - { - occurredAt: DateTime.makeUnsafe('2026-01-01T00:00:00Z'), - summary: 'Created', - timelineEntryId: '1', - }, - ], - }); - if (result.outcome !== 'resolved') { - throw new TypeError('The authorized resource fixture must resolve'); - } - await expect( - runEffectTestPromise( - Schema.encodeEffect(Schema.Array(ShellTimelineEntrySchema))(result.timeline), - ), - ).resolves.toEqual([ - { occurredAt: '2026-02-01T00:00:00.000Z', summary: 'Updated', timelineEntryId: '2' }, - { occurredAt: '2026-01-01T00:00:00.000Z', summary: 'Created', timelineEntryId: '1' }, - ]); -}); + }).resolve(context, ref); + expect(result).toEqual({ + detail: { fields: [], title: 'Unit 1' }, + media: { enabled: false, reason: 'unavailable' }, + outcome: 'resolved', + projectionLagging: true, + timeline: [ + { + occurredAt: DateTime.makeUnsafe('2026-02-01T00:00:00Z'), + summary: 'Updated', + timelineEntryId: '2', + }, + { + occurredAt: DateTime.makeUnsafe('2026-01-01T00:00:00Z'), + summary: 'Created', + timelineEntryId: '1', + }, + ], + }); + if (result.outcome !== 'resolved') { + throw new TypeError('The authorized resource fixture must resolve'); + } + expect( + yield* Schema.encodeEffect(Schema.Array(ShellTimelineEntrySchema))(result.timeline), + ).toEqual([ + { occurredAt: '2026-02-01T00:00:00.000Z', summary: 'Updated', timelineEntryId: '2' }, + { occurredAt: '2026-01-01T00:00:00.000Z', summary: 'Created', timelineEntryId: '1' }, + ]); + }), +); -test('media affordance remains unavailable until a generated Action exists', async () => { - const provider = { - detail: () => Effect.succeed({ fields: [], title: 'Unit 1' }), - timeline: () => Effect.succeed({ entries: [], projectionLagging: false }), - }; - await expect( - runEffectTestPromise( - makeShellResourceDetail(dependencies('read_only'), provider).resolve(context, ref), - ), - ).resolves.toMatchObject({ media: { enabled: false, reason: 'read_only' } }); - await expect( - runEffectTestPromise( - makeShellResourceDetail( +it.effect('media affordance remains unavailable until a generated Action exists', () => + Effect.gen(function* mediaAffordanceRemainsUnavailableUntilA() { + const provider = { + detail: () => Effect.succeed({ fields: [], title: 'Unit 1' }), + timeline: () => Effect.succeed({ entries: [], projectionLagging: false }), + }; + expect( + yield* makeShellResourceDetail(dependencies('read_only'), provider).resolve(context, ref), + ).toMatchObject({ media: { enabled: false, reason: 'read_only' } }); + expect( + yield* makeShellResourceDetail( dependencies('active', 'allowed', 'allowed', 'denied'), provider, ).resolve(context, ref), - ), - ).resolves.toMatchObject({ media: { enabled: false, reason: 'unavailable' } }); - await expect( - runEffectTestPromise(makeShellResourceDetail(dependencies(), provider).resolve(context, ref)), - ).resolves.toMatchObject({ media: { enabled: false, reason: 'unavailable' } }); -}); + ).toMatchObject({ media: { enabled: false, reason: 'unavailable' } }); + expect( + yield* makeShellResourceDetail(dependencies(), provider).resolve(context, ref), + ).toMatchObject({ media: { enabled: false, reason: 'unavailable' } }); + }), +); -test('media endpoint cannot invoke a provider mutation', async () => { - await expect(runEffectTestPromise(attachShellMedia(context, ref))).resolves.toEqual({ - outcome: 'unavailable', - }); -}); +it.effect('media endpoint cannot invoke a provider mutation', () => + Effect.gen(function* mediaEndpointCannotInvokeAProvider() { + expect(yield* attachShellMedia(context, ref)).toEqual({ + outcome: 'unavailable', + }); + }), +); -test('acquires a fresh audience-scoped assertion for each provider attempt', async () => { - const authorizations: string[] = []; - const result = await runEffectTestPromise( - makeShellResourceDetail(dependencies(), { +it.effect('acquires a fresh audience-scoped assertion for each provider attempt', () => + Effect.gen(function* acquiresAFreshAudienceScopedAssertion() { + const authorizations: string[] = []; + const result = yield* makeShellResourceDetail(dependencies(), { detail: ({ authorization }) => { authorizations.push(authorization); return Effect.succeed({ fields: [], title: 'Unit 1' }); @@ -714,8 +733,8 @@ test('acquires a fresh audience-scoped assertion for each provider attempt', asy authorizations.push(authorization); return Effect.succeed({ entries: [], projectionLagging: false }); }, - }).resolve(context, ref), - ); - expect(result.outcome).toBe('resolved'); - expect(authorizations).toEqual(['Bearer test-0', 'Bearer test-1']); -}); + }).resolve(context, ref); + expect(result.outcome).toBe('resolved'); + expect(authorizations).toEqual(['Bearer test-0', 'Bearer test-1']); + }), +); diff --git a/app/apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts b/app/apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts index 842363f40..6c701852e 100644 --- a/app/apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts +++ b/app/apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts @@ -1,6 +1,5 @@ -import { runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from 'effect-rstest'; import { readFile } from 'node:fs/promises'; -import { expect, test } from '@rstest/core'; import { Effect } from 'effect'; import { STAGE_DEMO_ACCOUNTS, @@ -19,126 +18,127 @@ const validEnvironment = { STAGE_SIAMPARK_PASSWORD: 'test-only-siampark-password', ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'stage', } as const; - -test('accepts the complete stage-only demo bootstrap configuration', () => { - expect(runEffectTestSync(parseStageDemoBootstrapConfig(validEnvironment))).toEqual({ - accounts: [ +it.effect('accepts the complete stage-only demo bootstrap configuration', () => + Effect.gen(function* acceptsTheCompleteStageonlyDemo() { + expect(yield* parseStageDemoBootstrapConfig(validEnvironment)).toEqual({ + accounts: [ + { + email: 'demo@test.com', + password: 'test-only-bootstrap-password', + principalDisplayName: 'Techsio Demo', + }, + { + email: 'siampark01@test.com', + password: 'test-only-siampark-password', + principalDisplayName: 'Siampark 01', + }, + ], + authBaseUrl: 'https://shell.stage.example.test', + authSecret: 'stage-auth-secret-with-at-least-32-characters', + databaseAdminUrl: 'postgresql://db:password@db:5432/db', + }); + }), +); +it.effect('defines both exact stage accounts without storing their passwords', () => + Effect.sync(() => { + expect(STAGE_DEMO_ACCOUNTS).toEqual([ { email: 'demo@test.com', - password: 'test-only-bootstrap-password', + passwordEnvironmentKey: 'STAGE_DEMO_PASSWORD', principalDisplayName: 'Techsio Demo', }, { email: 'siampark01@test.com', - password: 'test-only-siampark-password', + passwordEnvironmentKey: 'STAGE_SIAMPARK_PASSWORD', principalDisplayName: 'Siampark 01', }, - ], - authBaseUrl: 'https://shell.stage.example.test', - authSecret: 'stage-auth-secret-with-at-least-32-characters', - databaseAdminUrl: 'postgresql://db:password@db:5432/db', - }); -}); - -test('defines both exact stage accounts without storing their passwords', () => { - expect(STAGE_DEMO_ACCOUNTS).toEqual([ - { - email: 'demo@test.com', - passwordEnvironmentKey: 'STAGE_DEMO_PASSWORD', - principalDisplayName: 'Techsio Demo', - }, - { - email: 'siampark01@test.com', - passwordEnvironmentKey: 'STAGE_SIAMPARK_PASSWORD', - principalDisplayName: 'Siampark 01', - }, - ]); -}); - -test('refuses to provision outside stage or without an operator-supplied password', () => { - expect( - runEffectTestSync( - Effect.flip( + ]); + }), +); +it.effect('refuses to provision outside stage or without an operator-supplied password', () => + Effect.gen(function* refusesToProvisionOutsideStage() { + expect( + yield* Effect.flip( parseStageDemoBootstrapConfig({ ...validEnvironment, ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'production', }), ), - ), - ).toMatchObject({ reason: expect.stringMatching(/stage environment/u) }); - expect( - runEffectTestSync( - Effect.flip( + ).toMatchObject({ reason: expect.stringMatching(/stage environment/u) }); + expect( + yield* Effect.flip( parseStageDemoBootstrapConfig({ ...validEnvironment, STAGE_DEMO_PASSWORD: undefined, }), ), - ), - ).toMatchObject({ reason: expect.stringMatching(/STAGE_DEMO_PASSWORD/u) }); - expect( - runEffectTestSync( - Effect.flip( + ).toMatchObject({ reason: expect.stringMatching(/STAGE_DEMO_PASSWORD/u) }); + expect( + yield* Effect.flip( parseStageDemoBootstrapConfig({ ...validEnvironment, STAGE_SIAMPARK_PASSWORD: undefined, }), ), - ), - ).toMatchObject({ reason: expect.stringMatching(/STAGE_SIAMPARK_PASSWORD/u) }); -}); - -test('treats an exact record as idempotent and rejects conflicting state', () => { - const expected = { name: 'Techsio', slug: 'techsio', status: 'active' } as const; - expect(runEffectTestSync(classifyExactStageDemoRecord('tenant', undefined, expected))).toBe( - 'create', - ); - expect(runEffectTestSync(classifyExactStageDemoRecord('tenant', expected, expected))).toBe( - 'existing', - ); - expect( - runEffectTestSync( - Effect.flip( + ).toMatchObject({ reason: expect.stringMatching(/STAGE_SIAMPARK_PASSWORD/u) }); + }), +); +it.effect('treats an exact record as idempotent and rejects conflicting state', () => + Effect.gen(function* treatsAnExactRecordAsIdempotent() { + const expected = { name: 'Techsio', slug: 'techsio', status: 'active' } as const; + expect(yield* classifyExactStageDemoRecord('tenant', undefined, expected)).toBe('create'); + expect(yield* classifyExactStageDemoRecord('tenant', expected, expected)).toBe('existing'); + expect( + yield* Effect.flip( classifyExactStageDemoRecord('tenant', { ...expected, name: 'Other tenant' }, expected), ), - ), - ).toMatchObject({ reason: expect.stringMatching(/conflicts/u) }); -}); - -test('keeps the demo bootstrap operator-invoked and excludes its password from source', async () => { - const rootPackage = await readFile(new URL('../../../../package.json', import.meta.url), 'utf-8'); - const shellPackage = await readFile(new URL('../../package.json', import.meta.url), 'utf-8'); - const bootstrapCommand = await readFile( - new URL('../../scripts/bootstrap-stage-demo.sh', import.meta.url), - 'utf-8', - ); - const zerops = await readFile(new URL('../../../../zerops.yaml', import.meta.url), 'utf-8'); - const coreBootstrap = await readFile( - new URL( - '../../../../packages/core-runtime/src/install/stage-context-bootstrap.ts', - import.meta.url, - ), - 'utf-8', - ); - const shellBootstrap = await readFile( - new URL('../../api/auth/stage-demo-bootstrap-runtime-infrastructure.ts', import.meta.url), - 'utf-8', - ); - - expect(JSON.parse(rootPackage).scripts['stage:bootstrap-demo']).toBe( - 'pnpm --filter @app/shell-super-app stage:bootstrap-demo', - ); - expect(JSON.parse(shellPackage).scripts['stage:bootstrap-demo']).toBe( - 'sh scripts/bootstrap-stage-demo.sh', - ); - expect(bootstrapCommand).toMatch(/stty -echo/u); - expect(bootstrapCommand).toMatch(/STAGE_DEMO_PASSWORD/u); - expect(bootstrapCommand).toMatch(/STAGE_SIAMPARK_PASSWORD/u); - expect(zerops).not.toMatch(/start:.*stage:bootstrap-demo/u); - expect(zerops).not.toMatch(/^\s*STAGE_DEMO_PASSWORD:/mu); - expect(zerops).not.toMatch(/^\s*STAGE_SIAMPARK_PASSWORD:/mu); - expect(coreBootstrap).toMatch(/ULTRAMODERN_DEPLOYMENT_ENVIRONMENT/u); - expect(coreBootstrap).toMatch(/buildRelationships/u); - expect(shellBootstrap).toMatch(/reconcileStageContextBootstraps/u); - expect(shellBootstrap).not.toMatch(/contextKey/u); -}); + ).toMatchObject({ reason: expect.stringMatching(/conflicts/u) }); + }), +); +it.live('keeps the demo bootstrap operator-invoked and excludes its password from source', () => + Effect.gen(function* keepsTheDemoBootstrapOperatorinvoked() { + const rootPackage = yield* Effect.promise(() => + readFile(new URL('../../../../package.json', import.meta.url), 'utf-8'), + ); + const shellPackage = yield* Effect.promise(() => + readFile(new URL('../../package.json', import.meta.url), 'utf-8'), + ); + const bootstrapCommand = yield* Effect.promise(() => + readFile(new URL('../../scripts/bootstrap-stage-demo.sh', import.meta.url), 'utf-8'), + ); + const zerops = yield* Effect.promise(() => + readFile(new URL('../../../../zerops.yaml', import.meta.url), 'utf-8'), + ); + const coreBootstrap = yield* Effect.promise(() => + readFile( + new URL( + '../../../../packages/core-runtime/src/install/stage-context-bootstrap.ts', + import.meta.url, + ), + 'utf-8', + ), + ); + const shellBootstrap = yield* Effect.promise(() => + readFile( + new URL('../../api/auth/stage-demo-bootstrap-runtime-infrastructure.ts', import.meta.url), + 'utf-8', + ), + ); + expect(JSON.parse(rootPackage).scripts['stage:bootstrap-demo']).toBe( + 'pnpm --filter @app/shell-super-app stage:bootstrap-demo', + ); + expect(JSON.parse(shellPackage).scripts['stage:bootstrap-demo']).toBe( + 'sh scripts/bootstrap-stage-demo.sh', + ); + expect(bootstrapCommand).toMatch(/stty -echo/u); + expect(bootstrapCommand).toMatch(/STAGE_DEMO_PASSWORD/u); + expect(bootstrapCommand).toMatch(/STAGE_SIAMPARK_PASSWORD/u); + expect(zerops).not.toMatch(/start:.*stage:bootstrap-demo/u); + expect(zerops).not.toMatch(/^\s*STAGE_DEMO_PASSWORD:/mu); + expect(zerops).not.toMatch(/^\s*STAGE_SIAMPARK_PASSWORD:/mu); + expect(coreBootstrap).toMatch(/ULTRAMODERN_DEPLOYMENT_ENVIRONMENT/u); + expect(coreBootstrap).toMatch(/buildRelationships/u); + expect(shellBootstrap).toMatch(/reconcileStageContextBootstraps/u); + expect(shellBootstrap).not.toMatch(/contextKey/u); + }), +); diff --git a/app/docs/architecture/EFFECT_V4_ANTIPATTERN_AUDIT.md b/app/docs/architecture/EFFECT_V4_ANTIPATTERN_AUDIT.md index b2836a43c..774898efd 100644 --- a/app/docs/architecture/EFFECT_V4_ANTIPATTERN_AUDIT.md +++ b/app/docs/architecture/EFFECT_V4_ANTIPATTERN_AUDIT.md @@ -1,5 +1,10 @@ # Effect v4 anti-pattern audit +> Historical findings from the original audit. Implementation has changed since this snapshot. +> [Database Architecture](DATABASE.md) owns the current native Effect database and transaction +> model; the Promise bridge proposals below are superseded. Use focused architecture documents +> and executable policy checks to assess current behavior. + ## Verdict OntOS is **Effect-aware, but not yet Effect-native end to end**. diff --git a/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md b/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md index 266a5523c..f8dd185bc 100644 --- a/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md +++ b/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md @@ -153,8 +153,8 @@ Follow each rule link for its exact detection policy, defaults, exemptions, and architecture work beyond the local structural/document and schema detectors. - A8 template checks are lexical: arbitrary generated/dynamically assembled source and real scaffold quality still need generator tests and emitted-project gates. -- B2 flags raw runners/time control but does not install or certify an Effect test harness. - `@effect/vitest` was not added. +- B2 uses the upstream `effect-rstest` harness (`it.effect`/`it.live`/`it.layer`), + enforced by the `no-effect-run-in-tests` and restricted-imports gates. ## Audit exceptions preserved diff --git a/app/oxlint.config.ts b/app/oxlint.config.ts index 2d3b757bf..80c82d46e 100644 --- a/app/oxlint.config.ts +++ b/app/oxlint.config.ts @@ -1,3 +1,4 @@ +import { testRestrictedImports } from './tools/oxlint/effect-native/shared/test-restricted-imports.ts'; import { defineConfig } from 'oxlint'; import core from 'ultracite/oxlint/core'; import { jsPluginSettings, selectJsPlugins } from 'ultracite/oxlint/js-plugins'; @@ -193,10 +194,40 @@ export default defineConfig({ typeCheck: true, }, overrides: [ + { + files: ['**/*.{test,spec}.{ts,tsx,mts}'], + rules: { + // Sonar's hard-coded runner APIs do not recognize it.effect/it.live/it.layer. + 'sonarjs/no-empty-test-file': 'off', + // Effect.promise/tryPromise thunks must not be async. + 'typescript/promise-function-async': 'off', + }, + }, + { + excludeFiles: ['**/tests/e2e/**'], + files: [ + 'apps/**/tests/**', + 'verticals/**/tests/**', + 'packages/**/tests/**', + 'scripts/**/*.test.mts', + 'tools/**/tests/*.test.mts', + ], + rules: { + 'eslint/no-restricted-imports': [ + 'error', + { + paths: testRestrictedImports, + }, + ], + }, + }, { // This guarded test-only entrypoint composes real services with boundary fakes. // database-access:check rejects imports of it from production source. - files: ['packages/core-runtime/src/testing/**/*.ts'], + files: [ + 'packages/core-runtime/src/testing/**/*.ts', + 'apps/shell-super-app/tests/e2e/auth-fixture.ts', + ], rules: { 'anti-slop-effect/no-service-constructor-imports': 'off', }, @@ -253,28 +284,6 @@ export default defineConfig({ 'typescript/no-require-imports': 'off', }, }, - { - // Rollback sentinels and Date hashing are intentional nominal boundaries inside the runtime. - files: [ - 'packages/core-runtime/src/actions/repository.ts', - 'packages/core-runtime/src/actions/runtime.ts', - 'packages/core-runtime/src/reads/runtime.ts', - ], - rules: { - '@nkzw/no-instanceof': 'off', - }, - }, - { - // DOM constructors are the platform-provided nominal narrowing boundary in these browser tests. - files: [ - 'apps/shell-super-app/tests/unit/layout.test.tsx', - 'verticals/party-registry/tests/components/customer-create-page.test.tsx', - 'verticals/party-registry/tests/components/customer-edit-page.test.tsx', - ], - rules: { - '@nkzw/no-instanceof': 'off', - }, - }, { // Test registration deliberately returns an ignored promise, and test synchronization may use `.then`. files: [ @@ -773,7 +782,6 @@ export default defineConfig({ // can normalize declaration ordering and test doubles without mixing that churn into Issue 179. files: ['verticals/party-registry/**/*.ts'], rules: { - '@nkzw/no-instanceof': 'off', 'github/filenames-match-regex': 'off', 'github/js-class-name': 'off', 'import/export': 'off', @@ -816,7 +824,7 @@ export default defineConfig({ }, ], rules: { - '@nkzw/no-instanceof': 'error', + 'effect-native/no-instanceof': 'error', '@nkzw/require-use-effect-arguments': 'error', // Ultracite core already enforces these policies through Unicorn and Promise rules. 'github/array-foreach': 'off', diff --git a/app/package.json b/app/package.json index 6fa67c87e..efede0ae5 100644 --- a/app/package.json +++ b/app/package.json @@ -24,12 +24,12 @@ "local:initialize": "node ./scripts/initialize-local-development.mts", "test:unit": "pnpm -r --if-present run test:unit && pnpm -r --if-present run test:component", "test:integration": "pnpm -r --if-present run test:integration", - "test:scripts": "node --test scripts/tests/boundary-source-structure.test.mts scripts/local-environment-values.test.mts scripts/tests/audit-database-trust-boundaries.test.mts scripts/tests/authorization-rollout-contract.test.mts scripts/tests/check-authorization-readiness.test.mts scripts/tests/database-access-boundaries.test.mts scripts/tests/initialize-local-development.test.mts scripts/tests/locki-feature.test.mts scripts/tests/migrate-contacts-authorization.test.mts scripts/tests/module-entrypoint-boundaries.test.mts scripts/tests/plan-deployment-impact.test.mts scripts/tests/protected-entrypoint-inventory.test.mts scripts/tests/provision-current-action-authorization.test.mts scripts/tests/report-fail-closed-authorization-impact.test.mts scripts/tests/api-only-tooling.test.mts scripts/tests/generated-slot-entries.test.mts scripts/tests/root-environment.test.mts scripts/tests/typecheck-project-references.test.mts scripts/tests/ultramodern-command.test.mts scripts/tests/code-tools-i18n.test.mts scripts/tests/dependency-declarations.test.mts", - "test:lint-rules": "node --test tools/oxlint/effect-native/tests/*.test.mts", + "test:scripts": "rstest --project scripts", + "test:lint-rules": "rstest --project lint-rules", "typecheck:lint-rules": "tsc -p tools/oxlint/effect-native/tsconfig.json", "lint:effect": "node tools/oxlint/effect-native/report.mts", - "test:generation": "node --test scripts/scaffolding/tests/module-contract-generator.test.mts scripts/scaffolding/tests/resource-generator.test.mts scripts/scaffolding/tests/retire-contribution.test.mts scripts/scaffolding/tests/scaffold-generators.test.mts", - "test:deployment-impact": "node scripts/generate-outbox-worker-deployment.mjs && node --test scripts/tests/plan-deployment-impact.test.mts scripts/tests/outbox-worker-delivery.test.mts", + "test:generation": "rstest --project generation", + "test:deployment-impact": "node scripts/generate-outbox-worker-deployment.mjs && rstest --project scripts scripts/tests/plan-deployment-impact scripts/tests/outbox-worker-delivery", "deployment-impact:plan": "node ./scripts/plan-deployment-impact.mts", "action:test:unit": "pnpm --filter @app/core-runtime action:test:unit", "action:test:integration": "pnpm --filter @app/core-runtime action:test:integration", @@ -71,8 +71,8 @@ "database-access:check": "node ./scripts/check-database-access-boundaries.mts", "format": "oxfmt . '!repos/**'", "format:check": "oxfmt --check . '!repos/**'", - "lint": "oxlint apps verticals packages scripts", - "lint:fix": "oxlint apps verticals packages scripts --fix", + "lint": "oxlint apps verticals packages scripts tools/oxlint/effect-native/tests/*.test.mts", + "lint:fix": "oxlint apps verticals packages scripts tools/oxlint/effect-native/tests/*.test.mts --fix", "skills:install": "node ./scripts/bootstrap-agent-skills.mts", "skills:check": "node ./scripts/bootstrap-agent-skills.mts --check", "agents:refs:install": "node ./scripts/setup-agent-reference-repos.mts", @@ -87,7 +87,7 @@ "quality:audit": "node ./scripts/quality-audit.mts", "quality:audit:gate": "node ./scripts/quality-audit-gate.mts", "quality:check": "pnpm quality:audit && pnpm quality:audit:gate", - "quality:audit:test": "node --test ./scripts/tests/quality-audit.test.mts ./scripts/tests/quality-audit-model.test.mts ./scripts/tests/quality-audit-runtime-model.test.mts ./scripts/tests/quality-audit-gate.test.mts ./scripts/tests/quality-cli-lifecycle.test.mts ./scripts/tests/quality-audit-count-domain.test.mts" + "quality:audit:test": "rstest --project scripts scripts/tests/quality-audit.test.mts scripts/tests/quality-audit-model.test.mts scripts/tests/quality-audit-runtime-model.test.mts scripts/tests/quality-audit-gate.test.mts scripts/tests/quality-cli-lifecycle.test.mts scripts/tests/quality-audit-count-domain.test.mts" }, "dependencies": { "@authzed/authzed-node": "1.6.1", @@ -97,6 +97,8 @@ "@effect/sql-pg": "4.0.0-beta.107" }, "devDependencies": { + "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc", + "@rstest/core": "0.11.11", "@effect/platform-node": "4.0.0-beta.107", "@effect/tsgo": "0.19.0", "@noble/hashes": "2.2.0", @@ -127,7 +129,9 @@ "knip": "6.34.0", "jscpd": "5.1.2", "jsonc-parser": "3.3.1", - "fallow": "3.22.0" + "fallow": "3.22.0", + "@vercel/nft": "0.29.2", + "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12" }, "engines": { "node": ">=26", diff --git a/app/packages/core-runtime/package.json b/app/packages/core-runtime/package.json index 8fb9095af..21805f093 100644 --- a/app/packages/core-runtime/package.json +++ b/app/packages/core-runtime/package.json @@ -14,21 +14,20 @@ "./install/stage-context-bootstrap": "./src/install/stage-context-bootstrap.ts", "./outbox/worker": "./src/outbox/worker-entrypoint.ts", "./testing/actions": "./src/testing/actions.ts", - "./testing/effect-runtime": "./src/testing/effect-runtime.ts", "./workspace-environment": "./src/environment/workspace-environment.ts" }, "scripts": { "db:generate": "drizzle-kit generate --config drizzle.config.ts", "db:check": "drizzle-kit check --config drizzle.config.ts", "db:migrate": "drizzle-kit migrate --config drizzle.config.ts", - "db:test": "node --test tests/unit/*.test.ts tests/integration/*.test.ts", + "db:test": "rstest --project unit --project integration", "db:verify": "node scripts/verify-db-schema.mts", - "test:unit": "node --test tests/unit/*.test.ts", - "test:integration": "node --test tests/integration/*.test.ts", - "action:test:unit": "node --test tests/unit/action-*.test.ts", - "action:test:integration": "node --test tests/integration/action-*.test.ts", - "outbox:test:unit": "node --test tests/unit/outbox-*.test.ts", - "outbox:test:integration": "node --test tests/integration/outbox-runtime.test.ts", + "test:unit": "rstest --project unit", + "test:integration": "rstest --project integration", + "action:test:unit": "rstest --project unit tests/unit/action-", + "action:test:integration": "rstest --project integration tests/integration/action-", + "outbox:test:unit": "rstest --project unit tests/unit/outbox-", + "outbox:test:integration": "rstest --project integration tests/integration/outbox-", "typecheck": "node ../../scripts/ultramodern-typecheck.mts --project tsconfig.json" }, "dependencies": { @@ -42,6 +41,8 @@ "devDependencies": { "@types/node": "^20.19.43", "@types/pg": "8.20.0", - "drizzle-kit": "1.0.0-rc.5-ab785fc" + "drizzle-kit": "1.0.0-rc.5-ab785fc", + "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc", + "@rstest/core": "0.11.11" } } diff --git a/app/packages/core-runtime/rstest.config.ts b/app/packages/core-runtime/rstest.config.ts new file mode 100644 index 000000000..671f8b7a8 --- /dev/null +++ b/app/packages/core-runtime/rstest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + projects: [ + { include: ['tests/unit/**/*.test.ts'], name: 'unit', testEnvironment: 'node' }, + { + include: ['tests/integration/**/*.test.ts'], + name: 'integration', + testEnvironment: 'node', + testTimeout: 30_000, + }, + ], +}); diff --git a/app/packages/core-runtime/src/actions/repository.ts b/app/packages/core-runtime/src/actions/repository.ts index 4a18837b3..9ee326d8a 100644 --- a/app/packages/core-runtime/src/actions/repository.ts +++ b/app/packages/core-runtime/src/actions/repository.ts @@ -126,7 +126,6 @@ const normalizeNumberForHash = (value: number): CanonicalValue => [ 'number', Object.is(value, -0) ? '-0' : String(value), ]; -const isCanonicalDate = Schema.is(Schema.instanceOf(Date)); const normalizeForHash = (value: Value, seen: WeakSet): CanonicalValue => { if (value === undefined) { @@ -147,7 +146,7 @@ const normalizeForHash = (value: Value, seen: WeakSet): Canonical if (Predicate.isBigInt(value)) { return ['bigint', value.toString(10)]; } - if (isCanonicalDate(value)) { + if (Predicate.isDate(value)) { return ['date', value.toISOString()]; } if (Predicate.isObjectKeyword(value)) { diff --git a/app/packages/core-runtime/src/outbox/runtime.ts b/app/packages/core-runtime/src/outbox/runtime.ts index 69e27c5c8..969e8c6b2 100644 --- a/app/packages/core-runtime/src/outbox/runtime.ts +++ b/app/packages/core-runtime/src/outbox/runtime.ts @@ -282,9 +282,7 @@ const failOutboxDelivery = Effect.fn('OutboxRuntime.failDelivery')( outcome: string, ) { const status = yield* repository.fail(claim, reason, now).pipe( - Effect.catchTag('OutboxPersistenceError', (error) => - Effect.andThen(logUnexpectedPersistence(claim), Effect.fail(error)), - ), + Effect.tapErrorTag('OutboxPersistenceError', () => logUnexpectedPersistence(claim)), (effect) => withOutcomeSpan(effect, claim, outcome), ); return { @@ -371,9 +369,7 @@ const processNextOutboxDelivery = Effect.fn('makeOutboxRuntime.processNextDelive } yield* repository.complete(claim, execution.now).pipe( - Effect.catchTag('OutboxPersistenceError', (error) => - Effect.andThen(logUnexpectedPersistence(claim), Effect.fail(error)), - ), + Effect.tapErrorTag('OutboxPersistenceError', () => logUnexpectedPersistence(claim)), (effect) => withOutcomeSpan(effect, claim, 'success'), ); return { ...claimedState, succeeded: claimedState.succeeded + 1 }; diff --git a/app/packages/core-runtime/src/testing/actions.ts b/app/packages/core-runtime/src/testing/actions.ts index 552933087..037f33d2b 100644 --- a/app/packages/core-runtime/src/testing/actions.ts +++ b/app/packages/core-runtime/src/testing/actions.ts @@ -36,7 +36,6 @@ import type { TenantModuleState } from '../modules/tenant-module-state-service.t import { makeOperationalScopeResolver } from '../operations/context.ts'; import { OperationContextUnavailable } from '../operations/errors.ts'; import type { ContextAccessDecision, ContextAccessService } from '../permissions/context-access.ts'; -import { runEffectTestSync } from './effect-runtime.ts'; const actionTestServiceBinding: unique symbol = Symbol('test-action-service-binding'); const querySchema = Schema.Union([Schema.String, Schema.Struct({ text: Schema.String })]); @@ -146,7 +145,9 @@ const persistenceFailure = () => const queryRows = (result: { readonly rows: readonly object[] }) => result.rows; const sqlFailure = (cause: unknown) => new SqlError({ reason: new ConnectionError({ cause }) }); -const actionTestHarness = (options: ActionTestHarnessOptions = {}) => { +const actionTestHarness = Effect.fn('ActionTestHarness.make')(function* actionTestHarness( + options: ActionTestHarnessOptions = {}, +) { const invocations = new Map(); const idempotency = new Map(); const committed: FlushActionSuccessInput[] = []; @@ -323,23 +324,21 @@ const actionTestHarness = (options: ActionTestHarnessOptions = {}) => { } satisfies Connection; }); }); - const database = runEffectTestSync( - Effect.scoped( - Effect.gen(function* makeTestDatabase() { - const reactivity = yield* Reactivity.make; - const client = yield* PgClient.makeWith({ - acquirer: acquireConnection, - config: {}, - listenAcquirer: Effect.die('Notifications are unavailable in the Action test harness'), - transactionAcquirer: acquireConnection, - }).pipe(Effect.provideService(Reactivity.Reactivity, reactivity), Effect.orDie); - return { - executor: yield* makeWithDefaults({ relations: coreRelations }).pipe( - Effect.provideService(PgClient.PgClient, client), - ), - }; - }), - ), + const database = yield* Effect.scoped( + Effect.gen(function* makeTestDatabase() { + const reactivity = yield* Reactivity.make; + const client = yield* PgClient.makeWith({ + acquirer: acquireConnection, + config: {}, + listenAcquirer: Effect.die('Notifications are unavailable in the Action test harness'), + transactionAcquirer: acquireConnection, + }).pipe(Effect.provideService(Reactivity.Reactivity, reactivity), Effect.orDie); + return { + executor: yield* makeWithDefaults({ relations: coreRelations }).pipe( + Effect.provideService(PgClient.PgClient, client), + ), + }; + }), ); const contextAccess: ContextAccessService = { legalEntities: ({ legalEntityIds, permission }) => @@ -483,7 +482,7 @@ const actionTestHarness = (options: ActionTestHarnessOptions = {}) => { transactionCount, }), }); -}; +}); export const makeActionTestHarness: typeof actionTestHarness = actionTestHarness; diff --git a/app/packages/core-runtime/src/testing/effect-runtime.ts b/app/packages/core-runtime/src/testing/effect-runtime.ts deleted file mode 100644 index bc28969ae..000000000 --- a/app/packages/core-runtime/src/testing/effect-runtime.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** Repository-owned test entry points keep Effect execution behind one auditable boundary. */ -export { - makeEffectTestCallback, - runEffectTestPromise, - runEffectTestSync, -} from '../../tests/support/effect-runtime.ts'; diff --git a/app/packages/core-runtime/src/testing/live-operations.ts b/app/packages/core-runtime/src/testing/live-operations.ts index e30ebe475..839b8bb8f 100644 --- a/app/packages/core-runtime/src/testing/live-operations.ts +++ b/app/packages/core-runtime/src/testing/live-operations.ts @@ -66,7 +66,6 @@ export type LiveOperationFixtureConfiguration = class LiveOperationFixtureError extends Schema.TaggedError()( 'LiveOperationFixtureError', { - commitIndeterminate: Schema.optional(Schema.Literal(true)), reason: Schema.String, }, ) {} diff --git a/app/packages/core-runtime/tests/integration/action-permission.test.ts b/app/packages/core-runtime/tests/integration/action-permission.test.ts index a019fc5f1..9ad90d6f5 100644 --- a/app/packages/core-runtime/tests/integration/action-permission.test.ts +++ b/app/packages/core-runtime/tests/integration/action-permission.test.ts @@ -1,10 +1,9 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; +// oxlint-disable-next-line max-classes-per-file -- Effect requires class declarations for both the typed error and fixture service; remove when this fixture no longer needs its client service. +import { expect, it } from 'effect-rstest'; import { v1 } from '@authzed/authzed-node'; import { and, eq } from 'drizzle-orm'; -import { Effect, Exit, Schema, flow } from 'effect'; -import assert from 'node:assert/strict'; +import { Context, Effect, Layer, Exit, Schema, Predicate } from 'effect'; import { randomUUID } from 'node:crypto'; -import test, { after, before } from 'node:test'; import type { ActionHandlerContext } from '../../src/actions/context.ts'; import { defineAction } from '../../src/actions/definition.ts'; import { makeActionRepository } from '../../src/actions/repository.ts'; @@ -89,8 +88,6 @@ const otherTenantPrincipal = { tenantId: otherTenantId, } as const; -const spiceDbConfig = await runEffectTestPromise(loadSpiceDbConfig()); - const transport = (idempotencyKey: string, targetResourceId: string) => ({ correlationId: `permission-integration-${idempotencyKey}`, idempotencyKey, @@ -101,8 +98,8 @@ const transport = (idempotencyKey: string, targetResourceId: string) => ({ type ContextServiceContract = Parameters[0]; -const withDatabase = ( - operation: (database: ContextServiceContract) => Effect.Effect, +const withDatabase = ( + operation: (database: ContextServiceContract) => Effect.Effect, ) => Effect.scoped( Effect.gen(function* databaseScope() { @@ -112,18 +109,8 @@ const withDatabase = ( }), ); -const effectCallback = (effect: Effect.Effect) => - flow(() => Effect.asVoid(effect), runEffectTestPromise); - -const effectTest = (name: string, effect: Effect.Effect): void => { - test(name, effectCallback(effect)); -}; - const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); -const promiseEffect = (promise: PromiseLike): Effect.Effect => - Effect.promise(flow(() => promise)); - const relationshipActionKeys = new Set(); type ExecutorSubject = @@ -185,233 +172,6 @@ const tenantMembership = (membershipTenantId: string, membershipPrincipalId: str }), }); -const adminClient = v1.NewClient( - spiceDbConfig.preSharedKey, - spiceDbConfig.endpoint, - spiceDbConfig.insecureLocal - ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED - : v1.ClientSecurity.SECURE, -); - -Effect.gen(function* preparePermissionFixture() { - yield* promiseEffect( - adminClient.promises.writeSchema( - v1.WriteSchemaRequest.create({ schema: ONTOS_SPICEDB_SCHEMA }), - ), - ); - yield* withDatabase((database) => - Effect.gen(function* seedPermissionFixture() { - yield* database.executor.insert(tenants).values({ - defaultLocale: 'en', - name: 'Action Permission Integration', - slug: `action-permission-${tenantId}`, - status: 'active', - tenantId, - }); - yield* database.executor.insert(tenants).values({ - defaultLocale: 'en', - name: 'Other Action Permission Tenant', - slug: `action-permission-other-${otherTenantId}`, - status: 'active', - tenantId: otherTenantId, - }); - yield* database.executor.insert(legalEntities).values({ - legalEntityId, - legalName: 'Action Permission Integration', - registrationCountry: 'CZ', - registrationNumber: tenantId, - status: 'active', - tenantId, - }); - yield* database.executor.insert(principals).values({ - displayName: 'Action Permission Integration', - kind: 'human', - principalId, - status: 'active', - tenantId, - }); - yield* database.executor.insert(principals).values([ - { - displayName: 'Action Permission Non-member', - kind: 'human', - principalId: nonMemberPrincipalId, - status: 'active', - tenantId, - }, - { - displayName: 'Other Tenant Action Permission Member', - kind: 'human', - principalId: otherTenantPrincipalId, - status: 'active', - tenantId: otherTenantId, - }, - ]); - yield* database.executor.insert(principalAuthBindings).values([ - { - principalAuthBindingId, - principalId, - provider: 'better_auth', - providerSubjectId: `action-permission-${principalId}`, - status: 'active', - subjectType: 'user', - tenantId, - }, - { - principalAuthBindingId: nonMemberAuthBindingId, - principalId: nonMemberPrincipalId, - provider: 'better_auth', - providerSubjectId: `action-permission-${nonMemberPrincipalId}`, - status: 'active', - subjectType: 'user', - tenantId, - }, - { - principalAuthBindingId: otherTenantAuthBindingId, - principalId: otherTenantPrincipalId, - provider: 'better_auth', - providerSubjectId: `action-permission-${otherTenantPrincipalId}`, - status: 'active', - subjectType: 'user', - tenantId: otherTenantId, - }, - ]); - }), - ); - - yield* promiseEffect( - adminClient.promises.writeRelationships( - v1.WriteRelationshipsRequest.create({ - updates: [ - relationship(actionKeys.allowed, 'restriction'), - relationship(actionKeys.allowed, 'executor'), - relationship(actionKeys.membershipAllowed, 'executor', { - objectId: tenantId, - objectType: 'tenant', - optionalRelation: 'member', - }), - relationship(actionKeys.crossTenantDenied, 'executor', { - objectId: tenantId, - objectType: 'tenant', - optionalRelation: 'member', - }), - relationship(actionKeys.nonMemberDenied, 'executor', { - objectId: tenantId, - objectType: 'tenant', - optionalRelation: 'member', - }), - relationship(actionKeys.denied, 'restriction'), - relationship(actionKeys.concurrentDenied, 'restriction'), - tenantMembership(tenantId, principalId), - tenantMembership(otherTenantId, otherTenantPrincipalId), - ].map((item) => - v1.RelationshipUpdate.create({ - operation: v1.RelationshipUpdate_Operation.TOUCH, - relationship: item, - }), - ), - }), - ), - ); -}).pipe(effectCallback, before); - -Effect.gen(function* cleanPermissionFixture() { - const relationshipCleanupExit = yield* Effect.exit( - Effect.all( - [...relationshipActionKeys].map((actionKey) => - promiseEffect( - adminClient.promises.deleteRelationships( - v1.DeleteRelationshipsRequest.create({ - relationshipFilter: v1.RelationshipFilter.create({ - optionalResourceId: toSpiceDbActionObjectId(actionKey), - resourceType: 'action', - }), - }), - ), - ), - ), - { discard: true }, - ).pipe( - Effect.andThen( - Effect.all( - [tenantId, otherTenantId].map((membershipTenantId) => - promiseEffect( - adminClient.promises.deleteRelationships( - v1.DeleteRelationshipsRequest.create({ - relationshipFilter: v1.RelationshipFilter.create({ - optionalResourceId: membershipTenantId, - resourceType: 'tenant', - }), - }), - ), - ), - ), - { discard: true }, - ), - ), - Effect.ensuring(Effect.sync(() => adminClient.close())), - ), - ); - - yield* withDatabase((database) => - Effect.forEach( - [ - () => - database.executor - .delete(outboxMessages) - .where(eq(outboxMessages.tenantId, otherTenantId)), - () => - database.executor.delete(domainEvents).where(eq(domainEvents.tenantId, otherTenantId)), - () => - database.executor - .delete(dataAccessEvents) - .where(eq(dataAccessEvents.tenantId, otherTenantId)), - () => database.executor.delete(auditEvents).where(eq(auditEvents.tenantId, otherTenantId)), - () => - database.executor - .delete(tenantModuleStates) - .where(eq(tenantModuleStates.tenantId, otherTenantId)), - () => - database.executor - .delete(actionInvocations) - .where(eq(actionInvocations.tenantId, otherTenantId)), - () => database.executor.delete(outboxMessages).where(eq(outboxMessages.tenantId, tenantId)), - () => database.executor.delete(domainEvents).where(eq(domainEvents.tenantId, tenantId)), - () => - database.executor.delete(dataAccessEvents).where(eq(dataAccessEvents.tenantId, tenantId)), - () => database.executor.delete(auditEvents).where(eq(auditEvents.tenantId, tenantId)), - () => - database.executor - .delete(tenantModuleStates) - .where(eq(tenantModuleStates.tenantId, tenantId)), - () => - database.executor - .delete(actionInvocations) - .where(eq(actionInvocations.tenantId, tenantId)), - () => - database.executor - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.tenantId, otherTenantId)), - () => - database.executor - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.tenantId, tenantId)), - () => database.executor.delete(principals).where(eq(principals.tenantId, otherTenantId)), - () => database.executor.delete(principals).where(eq(principals.tenantId, tenantId)), - () => database.executor.delete(legalEntities).where(eq(legalEntities.tenantId, tenantId)), - () => database.executor.delete(tenants).where(eq(tenants.tenantId, tenantId)), - () => database.executor.delete(tenants).where(eq(tenants.tenantId, otherTenantId)), - ], - (query) => query(), - { concurrency: 1, discard: true }, - ), - ); - - if (Exit.isFailure(relationshipCleanupExit)) { - return yield* Effect.failCause(relationshipCleanupExit.cause); - } - return null; -}).pipe(effectCallback, after); - const NoDomainEvents = {}; interface PermissionActionServices { readonly transaction: ScopedTransactionExecutor; @@ -447,23 +207,267 @@ const registration = (actionKey: string, moduleStateKey: string, onExecute: () = resultSchema: Schema.Void, schemaVersion: '1', }, - (_payload, context: PermissionActionContext) => - Effect.gen(function* permissionIntegrationHandler() { - onExecute(); - yield* context.services.transaction - .insert(tenantModuleStates) - .values({ - moduleKey: moduleStateKey, - state: 'active', - tenantId: context.scope.tenantId, - }) - .pipe( - Effect.mapError(() => new TestWriteError({ reason: 'test business write failed' })), - ); - }), + Effect.fn(function* permissionIntegrationHandler(_payload, context: PermissionActionContext) { + onExecute(); + yield* context.services.transaction + .insert(tenantModuleStates) + .values({ + moduleKey: moduleStateKey, + state: 'active', + tenantId: context.scope.tenantId, + }) + .pipe(Effect.mapError(() => new TestWriteError({ reason: 'test business write failed' }))); + }), (transaction) => Effect.succeed({ transaction }), ); +class PermissionAdmin extends Context.Service>()( + '@app/core-runtime/tests/integration/action-permission.test/PermissionAdmin', +) {} +const PermissionFixture = Layer.effect( + PermissionAdmin, + Effect.gen(function* integrationProgram1() { + const spiceDbConfig = yield* loadSpiceDbConfig(); + const adminClient = v1.NewClient( + spiceDbConfig.preSharedKey, + spiceDbConfig.endpoint, + spiceDbConfig.insecureLocal + ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED + : v1.ClientSecurity.SECURE, + ); + + const prepare = Effect.gen(function* preparePermissionFixture() { + yield* Effect.promise(() => + adminClient.promises.writeSchema( + v1.WriteSchemaRequest.create({ schema: ONTOS_SPICEDB_SCHEMA }), + ), + ); + yield* withDatabase( + Effect.fn(function* seedPermissionFixture(database) { + yield* database.executor.insert(tenants).values({ + defaultLocale: 'en', + name: 'Action Permission Integration', + slug: `action-permission-${tenantId}`, + status: 'active', + tenantId, + }); + yield* database.executor.insert(tenants).values({ + defaultLocale: 'en', + name: 'Other Action Permission Tenant', + slug: `action-permission-other-${otherTenantId}`, + status: 'active', + tenantId: otherTenantId, + }); + yield* database.executor.insert(legalEntities).values({ + legalEntityId, + legalName: 'Action Permission Integration', + registrationCountry: 'CZ', + registrationNumber: tenantId, + status: 'active', + tenantId, + }); + yield* database.executor.insert(principals).values({ + displayName: 'Action Permission Integration', + kind: 'human', + principalId, + status: 'active', + tenantId, + }); + yield* database.executor.insert(principals).values([ + { + displayName: 'Action Permission Non-member', + kind: 'human', + principalId: nonMemberPrincipalId, + status: 'active', + tenantId, + }, + { + displayName: 'Other Tenant Action Permission Member', + kind: 'human', + principalId: otherTenantPrincipalId, + status: 'active', + tenantId: otherTenantId, + }, + ]); + yield* database.executor.insert(principalAuthBindings).values([ + { + principalAuthBindingId, + principalId, + provider: 'better_auth', + providerSubjectId: `action-permission-${principalId}`, + status: 'active', + subjectType: 'user', + tenantId, + }, + { + principalAuthBindingId: nonMemberAuthBindingId, + principalId: nonMemberPrincipalId, + provider: 'better_auth', + providerSubjectId: `action-permission-${nonMemberPrincipalId}`, + status: 'active', + subjectType: 'user', + tenantId, + }, + { + principalAuthBindingId: otherTenantAuthBindingId, + principalId: otherTenantPrincipalId, + provider: 'better_auth', + providerSubjectId: `action-permission-${otherTenantPrincipalId}`, + status: 'active', + subjectType: 'user', + tenantId: otherTenantId, + }, + ]); + }), + ); + + yield* Effect.promise(() => + adminClient.promises.writeRelationships( + v1.WriteRelationshipsRequest.create({ + updates: [ + relationship(actionKeys.allowed, 'restriction'), + relationship(actionKeys.allowed, 'executor'), + relationship(actionKeys.membershipAllowed, 'executor', { + objectId: tenantId, + objectType: 'tenant', + optionalRelation: 'member', + }), + relationship(actionKeys.crossTenantDenied, 'executor', { + objectId: tenantId, + objectType: 'tenant', + optionalRelation: 'member', + }), + relationship(actionKeys.nonMemberDenied, 'executor', { + objectId: tenantId, + objectType: 'tenant', + optionalRelation: 'member', + }), + relationship(actionKeys.denied, 'restriction'), + relationship(actionKeys.concurrentDenied, 'restriction'), + tenantMembership(tenantId, principalId), + tenantMembership(otherTenantId, otherTenantPrincipalId), + ].map((item) => + v1.RelationshipUpdate.create({ + operation: v1.RelationshipUpdate_Operation.TOUCH, + relationship: item, + }), + ), + }), + ), + ); + }); + + const cleanup = Effect.gen(function* cleanPermissionFixture() { + const relationshipCleanupExit = yield* Effect.exit( + Effect.all( + [...relationshipActionKeys].map((actionKey) => + Effect.promise(() => + adminClient.promises.deleteRelationships( + v1.DeleteRelationshipsRequest.create({ + relationshipFilter: v1.RelationshipFilter.create({ + optionalResourceId: toSpiceDbActionObjectId(actionKey), + resourceType: 'action', + }), + }), + ), + ), + ), + { discard: true }, + ).pipe( + Effect.andThen( + Effect.all( + [tenantId, otherTenantId].map((membershipTenantId) => + Effect.promise(() => + adminClient.promises.deleteRelationships( + v1.DeleteRelationshipsRequest.create({ + relationshipFilter: v1.RelationshipFilter.create({ + optionalResourceId: membershipTenantId, + resourceType: 'tenant', + }), + }), + ), + ), + ), + { discard: true }, + ), + ), + Effect.ensuring(Effect.sync(() => adminClient.close())), + ), + ); + + yield* withDatabase((database) => + Effect.forEach( + [ + () => + database.executor + .delete(outboxMessages) + .where(eq(outboxMessages.tenantId, otherTenantId)), + () => + database.executor + .delete(domainEvents) + .where(eq(domainEvents.tenantId, otherTenantId)), + () => + database.executor + .delete(dataAccessEvents) + .where(eq(dataAccessEvents.tenantId, otherTenantId)), + () => + database.executor.delete(auditEvents).where(eq(auditEvents.tenantId, otherTenantId)), + () => + database.executor + .delete(tenantModuleStates) + .where(eq(tenantModuleStates.tenantId, otherTenantId)), + () => + database.executor + .delete(actionInvocations) + .where(eq(actionInvocations.tenantId, otherTenantId)), + () => + database.executor.delete(outboxMessages).where(eq(outboxMessages.tenantId, tenantId)), + () => database.executor.delete(domainEvents).where(eq(domainEvents.tenantId, tenantId)), + () => + database.executor + .delete(dataAccessEvents) + .where(eq(dataAccessEvents.tenantId, tenantId)), + () => database.executor.delete(auditEvents).where(eq(auditEvents.tenantId, tenantId)), + () => + database.executor + .delete(tenantModuleStates) + .where(eq(tenantModuleStates.tenantId, tenantId)), + () => + database.executor + .delete(actionInvocations) + .where(eq(actionInvocations.tenantId, tenantId)), + () => + database.executor + .delete(principalAuthBindings) + .where(eq(principalAuthBindings.tenantId, otherTenantId)), + () => + database.executor + .delete(principalAuthBindings) + .where(eq(principalAuthBindings.tenantId, tenantId)), + () => + database.executor.delete(principals).where(eq(principals.tenantId, otherTenantId)), + () => database.executor.delete(principals).where(eq(principals.tenantId, tenantId)), + () => + database.executor.delete(legalEntities).where(eq(legalEntities.tenantId, tenantId)), + () => database.executor.delete(tenants).where(eq(tenants.tenantId, tenantId)), + () => database.executor.delete(tenants).where(eq(tenants.tenantId, otherTenantId)), + ], + (query) => query(), + { concurrency: 1, discard: true }, + ), + ); + + if (Exit.isFailure(relationshipCleanupExit)) { + return yield* Effect.failCause(relationshipCleanupExit.cause); + } + return null; + }); + + yield* Effect.acquireRelease(prepare, () => cleanup.pipe(Effect.orDie)); + return adminClient; + }), +); + interface ExecutionCounter { value: number; } @@ -475,59 +479,61 @@ const incrementExecution = (counter: ExecutionCounter): void => { const runWithLivePermission = ( database: ContextServiceContract, operation: (runtime: ReturnType) => Effect.Effect, - configuration: SpiceDbConfigValue = spiceDbConfig, -): Effect.Effect => - Effect.acquireUseRelease( - Effect.sync(() => createPermissionCheckClient(configuration, SPICEDB_CHECK_TIMEOUT_MS)), - (client) => - operation( - makeActionRuntime( - database, - makeActionRepository(), - makeActionPermissionService(client), - testOperationalScopeResolver, - openActionRuntimeOptions, - ), + configuration?: SpiceDbConfigValue, +) => + (configuration === undefined ? loadSpiceDbConfig() : Effect.succeed(configuration)).pipe( + Effect.flatMap((config) => + Effect.acquireUseRelease( + Effect.sync(() => createPermissionCheckClient(config, SPICEDB_CHECK_TIMEOUT_MS)), + (client) => + operation( + makeActionRuntime( + database, + makeActionRepository(), + makeActionPermissionService(client), + testOperationalScopeResolver, + openActionRuntimeOptions, + ), + ), + (client) => Effect.sync(() => client.close()), ), - (client) => Effect.sync(() => client.close()), + ), ); -effectTest( - 'allows direct Principal and Tenant-membership executor grants', - withDatabase((database) => - Effect.forEach( - [ - ['direct', actionKeys.allowed], - ['membership', actionKeys.membershipAllowed], - ] as const, - ([kind, actionKey]) => - Effect.gen(function* verifyAllowedAction() { - const executions: ExecutionCounter = { value: 0 }; - const moduleStateKey = `${actionPrefix}.state.${kind}`; - yield* runWithLivePermission(database, (runtime) => - runtime.runAction({ - payload: undefined, - principal, - registration: registration( - actionKey, - moduleStateKey, - incrementExecution.bind(undefined, executions), - ), - transport: transport(kind, moduleStateKey), - }), - ); - const rows = yield* database.executor - .select() - .from(tenantModuleStates) - .where(eq(tenantModuleStates.moduleKey, moduleStateKey)); - - assert.equal(executions.value, 1, kind); - assert.equal(rows.length, 1, kind); - }), - { concurrency: 1, discard: true }, - ), - ), -); +type DenialFailureStage = typeof DenialFailureStageSchema.Type; +const withDenialPersistenceFailure = ( + database: ContextServiceContract, + stage: DenialFailureStage, +): ContextServiceContract => { + const transaction: ContextServiceContract['executor']['transaction'] = (operation) => + database.executor.transaction((current) => { + const prefix = + stage === 'audit' + ? 'insert into "core"."audit_events"' + : 'update "core"."action_invocations"'; + return operation(current).pipe( + Effect.provideService(TestQueryHook, (statement) => + statement.startsWith(prefix) + ? Effect.fail( + new SqlError({ + reason: new UnknownError({ + cause: new Error('Injected SQL failure'), + message: `Injected denial ${stage} failure`, + }), + }), + ) + : Effect.void, + ), + ); + }); + const executor: ContextServiceContract['executor'] = Object.assign( + Object.create(database.executor), + { transaction }, + ); + return { executor }; +}; + +const DenialFailureStageSchema = Schema.Literals(['audit', 'invocation-update']); const runFailedAction = ( runtime: ReturnType, @@ -549,10 +555,43 @@ const runFailedAction = ( }), ); -effectTest( - 'persists one normalized terminal denial and no business or collected evidence', +const testProgram1 = () => withDatabase((database) => - Effect.gen(function* verifyTerminalDenial() { + Effect.forEach( + [ + ['direct', actionKeys.allowed], + ['membership', actionKeys.membershipAllowed], + ] as const, + Effect.fn(function* verifyAllowedAction([kind, actionKey]) { + const executions: ExecutionCounter = { value: 0 }; + const moduleStateKey = `${actionPrefix}.state.${kind}`; + yield* runWithLivePermission(database, (runtime) => + runtime.runAction({ + payload: undefined, + principal, + registration: registration( + actionKey, + moduleStateKey, + incrementExecution.bind(undefined, executions), + ), + transport: transport(kind, moduleStateKey), + }), + ); + const rows = yield* database.executor + .select() + .from(tenantModuleStates) + .where(eq(tenantModuleStates.moduleKey, moduleStateKey)); + + expect(executions.value, kind).toBe(1); + expect(rows.length, kind).toBe(1); + }), + { concurrency: 1, discard: true }, + ), + ); + +const testProgram2 = () => + withDatabase( + Effect.fn(function* verifyTerminalDenial(database) { const executions: ExecutionCounter = { value: 0 }; const key = 'missing'; const moduleStateKey = `${actionPrefix}.state.missing`; @@ -563,7 +602,10 @@ effectTest( .select() .from(actionInvocations) .where(eq(actionInvocations.idempotencyKey, key)); - assert.ok(invocation); + expect(invocation).toBeDefined(); + if (invocation === undefined) { + throw new Error('Expected invocation'); + } const [audits, businessRows, accesses, events, messages] = yield* Effect.all([ database.executor .select() @@ -587,39 +629,34 @@ effectTest( .where(eq(outboxMessages.tenantId, tenantId)), ]); - assert.equal(failure._tag, 'ActionPermissionDenied'); - assert.equal(failure.reason, 'The principal is not permitted to execute this Action'); - assert.equal(executions.value, 0); - assert.equal(invocation.status, 'rejected'); - assert.ok(invocation.completedAt); - assert.equal(businessRows.length, 0); - assert.equal(accesses.length, 0); - assert.equal(events.length, 0); - assert.equal(messages.length, 0); - assert.equal(audits.length, 1); - assert.deepEqual( - { - eventType: audits[0]?.eventType, - evidenceJson: audits[0]?.evidenceJson, - outcome: audits[0]?.outcome, - outcomeCode: audits[0]?.outcomeCode, - outcomeStage: audits[0]?.outcomeStage, - }, - { - eventType: 'action.rejected', - evidenceJson: { actionKey: actionKeys.missing }, - outcome: 'denied', - outcomeCode: 'spicedb_permission_denied', - outcomeStage: 'authz', - }, - ); - assert.equal(encodeJson(audits[0]).includes(spiceDbConfig.preSharedKey), false); + expect(Predicate.isTagged(failure, 'ActionPermissionDenied')).toBe(true); + expect(failure.reason).toBe('The principal is not permitted to execute this Action'); + expect(executions.value).toBe(0); + expect(invocation.status).toBe('rejected'); + expect(invocation.completedAt).toBeTruthy(); + expect(businessRows.length).toBe(0); + expect(accesses.length).toBe(0); + expect(events.length).toBe(0); + expect(messages.length).toBe(0); + expect(audits.length).toBe(1); + expect({ + eventType: audits[0]?.eventType, + evidenceJson: audits[0]?.evidenceJson, + outcome: audits[0]?.outcome, + outcomeCode: audits[0]?.outcomeCode, + outcomeStage: audits[0]?.outcomeStage, + }).toEqual({ + eventType: 'action.rejected', + evidenceJson: { actionKey: actionKeys.missing }, + outcome: 'denied', + outcomeCode: 'spicedb_permission_denied', + outcomeStage: 'authz', + }); + expect(encodeJson(audits[0]).includes((yield* loadSpiceDbConfig()).preSharedKey)).toBe(false); }), - ), -); + ); -effectTest( - 'denies a legacy marker without an executor and membership-set outsiders', +const testProgram3 = () => withDatabase((database) => Effect.forEach( [ @@ -627,37 +664,34 @@ effectTest( ['other-tenant', actionKeys.crossTenantDenied, otherTenantPrincipal], ['non-member', actionKeys.nonMemberDenied, nonMemberPrincipal], ] as const, - ([kind, actionKey, deniedPrincipal]) => - Effect.gen(function* verifyDeniedAction() { - const executions: ExecutionCounter = { value: 0 }; - const moduleStateKey = `${actionPrefix}.state.${kind}`; - const failure = yield* runWithLivePermission(database, (runtime) => - Effect.flip( - runtime.runAction({ - payload: undefined, - principal: deniedPrincipal, - registration: registration( - actionKey, - moduleStateKey, - incrementExecution.bind(undefined, executions), - ), - transport: transport(kind, moduleStateKey), - }), - ), - ); + Effect.fn(function* verifyDeniedAction([kind, actionKey, deniedPrincipal]) { + const executions: ExecutionCounter = { value: 0 }; + const moduleStateKey = `${actionPrefix}.state.${kind}`; + const failure = yield* runWithLivePermission(database, (runtime) => + Effect.flip( + runtime.runAction({ + payload: undefined, + principal: deniedPrincipal, + registration: registration( + actionKey, + moduleStateKey, + incrementExecution.bind(undefined, executions), + ), + transport: transport(kind, moduleStateKey), + }), + ), + ); - assert.equal(failure._tag, 'ActionPermissionDenied', kind); - assert.equal(executions.value, 0, kind); - }), + expect(Predicate.isTagged(failure, 'ActionPermissionDenied'), kind).toBe(true); + expect(executions.value, kind).toBe(0); + }), { concurrency: 1, discard: true }, ), - ), -); + ); -effectTest( - 'serializes concurrent denials into one Audit Event without executing the handler', - withDatabase((database) => - Effect.gen(function* verifyConcurrentDenials() { +const testProgram4 = () => + withDatabase( + Effect.fn(function* verifyConcurrentDenials(database) { const executions: ExecutionCounter = { value: 0 }; const key = 'concurrent-denied'; const moduleStateKey = `${actionPrefix}.state.concurrent-denied`; @@ -681,122 +715,89 @@ effectTest( .select() .from(actionInvocations) .where(eq(actionInvocations.idempotencyKey, key)); - assert.ok(invocation); + expect(invocation).toBeDefined(); + if (invocation === undefined) { + throw new Error('Expected invocation'); + } const audits = yield* database.executor .select() .from(auditEvents) .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)); - assert.deepEqual( - results.map((result) => result._tag), - ['ActionPermissionDenied', 'ActionPermissionDenied'], - ); - assert.equal(executions.value, 0); - assert.equal(invocation.status, 'rejected'); - assert.equal(audits.length, 1); + expect(results.length).toBe(2); + for (const result of results) { + expect(Predicate.isTagged(result, 'ActionPermissionDenied')).toBe(true); + } + expect(executions.value).toBe(0); + expect(invocation.status).toBe('rejected'); + expect(audits.length).toBe(1); }), - ), -); - -const DenialFailureStageSchema = Schema.Literals(['audit', 'invocation-update']); -type DenialFailureStage = typeof DenialFailureStageSchema.Type; - -const withDenialPersistenceFailure = ( - database: ContextServiceContract, - stage: DenialFailureStage, -): ContextServiceContract => { - const transaction: ContextServiceContract['executor']['transaction'] = (operation) => - database.executor.transaction((current) => { - const prefix = - stage === 'audit' - ? 'insert into "core"."audit_events"' - : 'update "core"."action_invocations"'; - return operation(current).pipe( - Effect.provideService(TestQueryHook, (statement) => - statement.startsWith(prefix) - ? Effect.fail( - new SqlError({ - reason: new UnknownError({ - cause: new Error('Injected SQL failure'), - message: `Injected denial ${stage} failure`, - }), - }), - ) - : Effect.void, - ), - ); - }); - const executor: ContextServiceContract['executor'] = Object.assign( - Object.create(database.executor), - { transaction }, ); - return { executor }; -}; -effectTest( - 'rolls back both denial evidence writes when either persistence step fails', +const testProgram5 = () => withDatabase((database) => Effect.forEach( ['audit', 'invocation-update'] as const, - (stage) => - Effect.gen(function* verifyDenialRollback() { - const executions: ExecutionCounter = { value: 0 }; - const key = `denial-${stage}`; - const actionKey = `${actionPrefix}.${stage}`; - const moduleStateKey = `${actionPrefix}.state.${stage}`; - yield* promiseEffect( - adminClient.promises.writeRelationships( - v1.WriteRelationshipsRequest.create({ - updates: [ - v1.RelationshipUpdate.create({ - operation: v1.RelationshipUpdate_Operation.TOUCH, - relationship: relationship(actionKey, 'restriction'), - }), - ], + Effect.fn(function* verifyDenialRollback(stage) { + const executions: ExecutionCounter = { value: 0 }; + const adminClient = yield* PermissionAdmin; + const key = `denial-${stage}`; + const actionKey = `${actionPrefix}.${stage}`; + const moduleStateKey = `${actionPrefix}.state.${stage}`; + yield* Effect.promise(() => + adminClient.promises.writeRelationships( + v1.WriteRelationshipsRequest.create({ + updates: [ + v1.RelationshipUpdate.create({ + operation: v1.RelationshipUpdate_Operation.TOUCH, + relationship: relationship(actionKey, 'restriction'), + }), + ], + }), + ), + ); + const failure = yield* runWithLivePermission( + withDenialPersistenceFailure(database, stage), + (runtime) => + Effect.flip( + runtime.runAction({ + payload: undefined, + principal, + registration: registration( + actionKey, + moduleStateKey, + incrementExecution.bind(undefined, executions), + ), + transport: transport(key, moduleStateKey), }), ), - ); - const failure = yield* runWithLivePermission( - withDenialPersistenceFailure(database, stage), - (runtime) => - Effect.flip( - runtime.runAction({ - payload: undefined, - principal, - registration: registration( - actionKey, - moduleStateKey, - incrementExecution.bind(undefined, executions), - ), - transport: transport(key, moduleStateKey), - }), - ), - ); - const [invocation] = yield* database.executor - .select() - .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, key)); - assert.ok(invocation); - const audits = yield* database.executor - .select() - .from(auditEvents) - .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)); - - assert.equal(failure._tag, 'ActionTransactionError', stage); - assert.equal(executions.value, 0, stage); - assert.equal(invocation.status, 'received', stage); - assert.equal(invocation.completedAt, null, stage); - assert.equal(audits.length, 0, stage); - }), + ); + const [invocation] = yield* database.executor + .select() + .from(actionInvocations) + .where(eq(actionInvocations.idempotencyKey, key)); + expect(invocation).toBeDefined(); + if (invocation === undefined) { + throw new Error('Expected invocation'); + } + const audits = yield* database.executor + .select() + .from(auditEvents) + .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)); + + expect(Predicate.isTagged(failure, 'ActionTransactionError'), stage).toBe(true); + expect(executions.value, stage).toBe(0); + expect(invocation.status, stage).toBe('received'); + expect(invocation.completedAt, stage).toBe(null); + expect(audits.length, stage).toBe(0); + }), { concurrency: 1, discard: true }, ), - ), -); + ); -effectTest( - 'fails closed for invalid SpiceDB credentials and leaves retryable received evidence', - withDatabase((database) => - Effect.gen(function* verifyUnavailablePermissionService() { +const testProgram6 = () => + withDatabase( + Effect.fn(function* verifyUnavailablePermissionService(database) { const executions: ExecutionCounter = { value: 0 }; const key = 'invalid-credentials'; const moduleStateKey = `${actionPrefix}.state.invalid-credentials`; @@ -804,13 +805,16 @@ effectTest( database, (runtime) => runFailedAction(runtime, actionKeys.unavailable, key, moduleStateKey, executions), - { ...spiceDbConfig, preSharedKey: 'invalid-integration-key' }, + { ...(yield* loadSpiceDbConfig()), preSharedKey: 'invalid-integration-key' }, ); const [invocation] = yield* database.executor .select() .from(actionInvocations) .where(eq(actionInvocations.idempotencyKey, key)); - assert.ok(invocation); + expect(invocation).toBeDefined(); + if (invocation === undefined) { + throw new Error('Expected invocation'); + } const audits = yield* database.executor .select() .from(auditEvents) @@ -821,12 +825,40 @@ effectTest( ), ); - assert.equal(failure._tag, 'ActionPermissionCheckError'); - assert.equal(failure.reason.includes('invalid-integration-key'), false); - assert.equal(executions.value, 0); - assert.equal(invocation.status, 'received'); - assert.equal(invocation.completedAt, null); - assert.equal(audits.length, 0); + expect(Predicate.isTagged(failure, 'ActionPermissionCheckError')).toBe(true); + expect(failure.reason.includes('invalid-integration-key')).toBe(false); + expect(executions.value).toBe(0); + expect(invocation.status).toBe('received'); + expect(invocation.completedAt).toBe(null); + expect(audits.length).toBe(0); }), - ), -); + ); + +it.layer(PermissionFixture, { excludeTestServices: true })('Action permissions', (suite) => { + suite.effect('allows direct Principal and Tenant-membership executor grants', testProgram1); + + suite.effect( + 'persists one normalized terminal denial and no business or collected evidence', + testProgram2, + ); + + suite.effect( + 'denies a legacy marker without an executor and membership-set outsiders', + testProgram3, + ); + + suite.effect( + 'serializes concurrent denials into one Audit Event without executing the handler', + testProgram4, + ); + + suite.effect( + 'rolls back both denial evidence writes when either persistence step fails', + testProgram5, + ); + + suite.effect( + 'fails closed for invalid SpiceDB credentials and leaves retryable received evidence', + testProgram6, + ); +}); diff --git a/app/packages/core-runtime/tests/integration/action-runtime.test.ts b/app/packages/core-runtime/tests/integration/action-runtime.test.ts index 84f3b35bb..f0d03a591 100644 --- a/app/packages/core-runtime/tests/integration/action-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/action-runtime.test.ts @@ -1,12 +1,9 @@ import { makeModuleContractFixture } from '../../src/testing/module-contract.ts'; -import { runEffectTestPromise, runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from 'effect-rstest'; import { ConnectionError, SqlError, UnknownError } from 'effect/unstable/sql/SqlError'; -import assert from 'node:assert/strict'; -// @effect-diagnostics asyncFunction:off globalDateInEffect:off -- Existing compatibility boundary; expires: 2026-12-31. import { and, eq } from 'drizzle-orm'; -import { Cause, Deferred, Effect, Exit, Fiber, Option, Schema } from 'effect'; +import { Cause, Deferred, Effect, Layer, Exit, Fiber, Option, Schema, Predicate } from 'effect'; import { randomUUID } from 'node:crypto'; -import test, { after, before } from 'node:test'; import type { ActionHandlerContext } from '../../src/actions/context.ts'; import { defineAction } from '../../src/actions/definition.ts'; import { ActionInvocationPersistenceError } from '../../src/actions/errors.ts'; @@ -72,8 +69,6 @@ const TestDomainRejected = Schema.TaggedError()('TestDom }); const TestStateIdSchema = Schema.String.pipe(Schema.brand('TestStateId')); -const FailureTagSchema = Schema.Struct({ _tag: Schema.String }); -const decodeFailureTag = Schema.decodeUnknownOption(FailureTagSchema); const ActionPolicyDeniedFailureSchema = Schema.TaggedStruct('ActionPolicyDenied', { policyReasonCode: Schema.String, reason: Schema.String, @@ -137,8 +132,8 @@ const allowedPermission = { checkActionPermission: () => Effect.succeed('allowed' as const), }; -const withDatabase = ( - execute: (database: ContextServiceContract) => Effect.Effect, +const withDatabase = ( + execute: (database: ContextServiceContract) => Effect.Effect, ) => Effect.scoped( Effect.gen(function* databaseScope() { @@ -163,7 +158,10 @@ const invocationEvidence = (database: ContextServiceContract, key: string) => .select() .from(actionInvocations) .where(eq(actionInvocations.idempotencyKey, key)); - assert.ok(invocation); + expect(invocation).toBeDefined(); + if (invocation === undefined) { + throw new Error('Expected invocation'); + } const audits = yield* database.executor .select() .from(auditEvents) @@ -214,11 +212,6 @@ const withEvidencePersistenceFailure = ( return withTransactionOverride(database, transactionOverride); }; -const databasePromise = async ( - execute: (database: ContextServiceContract) => PromiseLike, -): Promise => - await runEffectTestPromise(withDatabase((database) => Effect.promise(() => execute(database)))); - const liveModuleStateOptions = (database: ContextServiceContract) => { const moduleStateGate = makeModuleStateGate(makeTenantModuleStateService(database)); return { @@ -228,38 +221,32 @@ const liveModuleStateOptions = (database: ContextServiceContract) => { }; }; -before(async () => { - await databasePromise(async (database) => { - await runEffectTestPromise( - database.executor.insert(tenants).values({ +const prepare = (() => + withDatabase( + Effect.fn(function* integrationProgram1(database) { + yield* database.executor.insert(tenants).values({ defaultLocale: 'en', name: 'Action Runtime Integration', slug: `action-runtime-${tenantId}`, status: 'active', tenantId, - }), - ); - await runEffectTestPromise( - database.executor.insert(legalEntities).values({ + }); + yield* database.executor.insert(legalEntities).values({ legalEntityId, legalName: 'Action Runtime Integration', registrationCountry: 'CZ', registrationNumber: tenantId, status: 'active', tenantId, - }), - ); - await runEffectTestPromise( - database.executor.insert(principals).values({ + }); + yield* database.executor.insert(principals).values({ displayName: 'Action Runtime Integration', kind: 'human', principalId, status: 'active', tenantId, - }), - ); - await runEffectTestPromise( - database.executor.insert(principalAuthBindings).values({ + }); + yield* database.executor.insert(principalAuthBindings).values({ principalAuthBindingId: authBindingId, principalId, provider: 'better_auth', @@ -267,22 +254,19 @@ before(async () => { status: 'active', subjectType: 'user', tenantId, - }), - ); - await runEffectTestPromise( - database.executor.insert(tenantModuleStates).values({ + }); + yield* database.executor.insert(tenantModuleStates).values({ moduleKey: 'inventory.stock', state: 'active', tenantId, - }), - ); - }); -}); + }); + }), + ))(); -after(async () => { - await databasePromise(async (database) => { - await runEffectTestPromise( - Effect.forEach( +const cleanup = (() => + withDatabase( + Effect.fn(function* integrationProgram2(database) { + yield* Effect.forEach( [ outboxMessages, domainEvents, @@ -298,10 +282,9 @@ after(async () => { ], (table) => database.executor.delete(table).where(eq(table.tenantId, tenantId)), { discard: true }, - ), - ); - }); -}); + ); + }), + ))(); const TestDomainEvents = { 'test-state.changed': Schema.Struct({ value: Schema.String }), @@ -318,6 +301,7 @@ interface RegistrationOptions { readonly mode?: 'orphan-outbox' | 'reject' | 'success'; readonly moduleStateKey: string; readonly onExecute?: () => void; + readonly onExecuteEffect?: Effect.Effect; readonly policies?: readonly ActionPolicy<{ readonly value: string }, 'core.shell'>[]; } @@ -327,6 +311,7 @@ const makeRegistration = ({ mode = 'success', moduleStateKey, onExecute, + onExecuteEffect, policies = [], }: RegistrationOptions) => defineAction( @@ -354,159 +339,152 @@ const makeRegistration = ({ resultSchema: Schema.Struct({ stateId: TestStateIdSchema, value: Schema.String }), schemaVersion: '1', }, - (payload, context: TestActionContext) => - Effect.gen(function* integrationHandler() { - onExecute?.(); - const inserted = yield* context.services.transaction - .insert(tenantModuleStates) - .values({ - moduleKey: moduleStateKey, - state: 'active', - tenantId: context.scope.tenantId, - }) - .returning({ - tenantModuleStateId: tenantModuleStates.tenantModuleStateId, - }) - .pipe( - Effect.mapError( - () => new TestPersistenceError({ reason: 'test business write failed' }), - ), - ); - - yield* context.recordDataAccess({ - accessKind: 'read', - queryHash: `lookup-${moduleStateKey}`, - resultCount: 0, - servingModuleKey: 'core.shell', - targetModuleKey: 'core.shell', - targetResourceId: moduleStateKey, - targetResourceType: 'test-state', - }); + Effect.fn(function* integrationHandler(payload, context: TestActionContext) { + onExecute?.(); + if (onExecuteEffect !== undefined) { + yield* onExecuteEffect; + } + const inserted = yield* context.services.transaction + .insert(tenantModuleStates) + .values({ + moduleKey: moduleStateKey, + state: 'active', + tenantId: context.scope.tenantId, + }) + .returning({ + tenantModuleStateId: tenantModuleStates.tenantModuleStateId, + }) + .pipe( + Effect.mapError(() => new TestPersistenceError({ reason: 'test business write failed' })), + ); - if (mode === 'orphan-outbox') { - yield* context.addOutboxMessage(createDomainEventReference(), { - payloadJson: { value: payload.value }, - producerModuleKey: 'core.shell', - topic: 'test-state.project', - }); - } + yield* context.recordDataAccess({ + accessKind: 'read', + queryHash: `lookup-${moduleStateKey}`, + resultCount: 0, + servingModuleKey: 'core.shell', + targetModuleKey: 'core.shell', + targetResourceId: moduleStateKey, + targetResourceType: 'test-state', + }); - const domainEvent = yield* context.addDomainEvent({ - eventType: 'test-state.changed', - payloadJson: { value: payload.value }, - producerModuleKey: 'core.shell', - subjectModuleKey: 'core.shell', - subjectResourceId: moduleStateKey, - subjectResourceType: 'test-state', - }); - yield* context.addOutboxMessage(domainEvent, { + if (mode === 'orphan-outbox') { + yield* context.addOutboxMessage(createDomainEventReference(), { payloadJson: { value: payload.value }, producerModuleKey: 'core.shell', topic: 'test-state.project', }); + } - if (completionGate !== undefined) { - yield* Deferred.await(completionGate); - } - if (mode === 'reject') { - return yield* new TestDomainRejected({ reason: 'test domain rejection' }); - } - - const [row] = inserted; - if (row === undefined) { - return yield* new TestPersistenceError({ reason: 'test write returned no row' }); - } - return { - stateId: TestStateIdSchema.make(row.tenantModuleStateId), - value: payload.value, - }; - }), + const domainEvent = yield* context.addDomainEvent({ + eventType: 'test-state.changed', + payloadJson: { value: payload.value }, + producerModuleKey: 'core.shell', + subjectModuleKey: 'core.shell', + subjectResourceId: moduleStateKey, + subjectResourceType: 'test-state', + }); + yield* context.addOutboxMessage(domainEvent, { + payloadJson: { value: payload.value }, + producerModuleKey: 'core.shell', + topic: 'test-state.project', + }); + + if (completionGate !== undefined) { + yield* Deferred.await(completionGate); + } + if (mode === 'reject') { + return yield* new TestDomainRejected({ reason: 'test domain rejection' }); + } + + const [row] = inserted; + if (row === undefined) { + return yield* new TestPersistenceError({ reason: 'test write returned no row' }); + } + return { + stateId: TestStateIdSchema.make(row.tenantModuleStateId), + value: payload.value, + }; + }), (transaction) => Effect.succeed({ transaction }), ); -const failureTag = (exit: Exit.Exit): string | undefined => { - if (Exit.isSuccess(exit)) { - return undefined; - } - return Option.getOrUndefined( - Option.map( - Option.flatMap(Cause.findErrorOption(exit.cause), decodeFailureTag), - (failure) => failure._tag, - ), - ); -}; +const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); + +const hasFailure = (exit: Exit.Exit, tag: string): boolean => + Exit.isFailure(exit) && Option.exists(Cause.findErrorOption(exit.cause), Predicate.isTagged(tag)); -void test('rechecks business module state under the tenant lock and retries after Core recovery', async () => { - await databasePromise(async (database) => { - await runEffectTestPromise( - database.executor +const testProgram1 = () => + withDatabase( + Effect.fn(function* integrationProgram3(database) { + yield* database.executor .update(tenantModuleStates) .set({ state: 'active' }) - .where(eq(tenantModuleStates.moduleKey, 'inventory.stock')), - ); - - const policyReached = await runEffectTestPromise(Deferred.make()); - const continuePolicy = await runEffectTestPromise(Deferred.make()); - let handlerExecutions = 0; - const action = defineAction( - { - accessEvidencePolicy: { - captureMode: 'metadata_only', - policyKey: 'inventory.stock.concurrent-gate.v1', - }, - actionKey: 'inventory.stock.concurrent-gate', - auditProfile: 'standard', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineTenantModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'inventory.stock.concurrent-gate', - moduleKey: 'inventory.stock', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'inventory.stock', - payloadSchema: Schema.Void, - policies: [ - defineGlobalPolicy({ - evaluate: () => - Effect.gen(function* pauseBetweenGates() { + .where(eq(tenantModuleStates.moduleKey, 'inventory.stock')); + + const policyReached = yield* Deferred.make(); + const continuePolicy = yield* Deferred.make(); + let handlerExecutions = 0; + const action = defineAction( + { + accessEvidencePolicy: { + captureMode: 'metadata_only', + policyKey: 'inventory.stock.concurrent-gate.v1', + }, + actionKey: 'inventory.stock.concurrent-gate', + auditProfile: 'standard', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineTenantModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'inventory.stock.concurrent-gate', + moduleKey: 'inventory.stock', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'inventory.stock', + payloadSchema: Schema.Void, + policies: [ + defineGlobalPolicy({ + evaluate: Effect.fn(function* pauseBetweenGates() { yield* Deferred.succeed(policyReached, null); yield* Deferred.await(continuePolicy); }), - policyKey: 'global.pause-between-module-gates.v1', + policyKey: 'global.pause-between-module-gates.v1', + }), + ], + resultSchema: Schema.Void, + schemaVersion: '1', + }, + () => + Effect.sync(() => { + handlerExecutions += 1; }), - ], - resultSchema: Schema.Void, - schemaVersion: '1', - }, - () => - Effect.sync(() => { - handlerExecutions += 1; - }), - ); - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - liveModuleStateOptions(database), - ); - const firstAttempt = runEffectTestPromise( - Effect.exit( - runtime.runAction({ - payload: undefined, - principal, - registration: action, - transport: transport('business-module-concurrent-gate'), - }), - ), - ); - await runEffectTestPromise(Deferred.await(policyReached)); - await runEffectTestPromise( - runtime + ); + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + liveModuleStateOptions(database), + ); + const firstAttempt = yield* Effect.forkScoped( + Effect.exit( + runtime.runAction({ + payload: undefined, + principal, + registration: action, + transport: transport('business-module-concurrent-gate'), + }), + ), + ); + yield* Deferred.await(policyReached); + yield* runtime .runAction({ payload: { expectedState: 'active', @@ -523,31 +501,28 @@ void test('rechecks business module state under the tenant lock and retries afte load: Effect.succeed(inventoryInstalledCatalog), }), Effect.provideService(TenantModuleStateService, makeTenantModuleStateService(database)), - ), - ); - await runEffectTestPromise(Deferred.succeed(continuePolicy, null)); - const denied = await firstAttempt; - assert.equal(failureTag(denied), 'ModuleStateDeniedError'); - assert.equal(handlerExecutions, 0); - - const [openInvocation] = await runEffectTestPromise( - database.executor + ); + yield* Deferred.succeed(continuePolicy, null); + const denied = yield* Fiber.join(firstAttempt); + expect(hasFailure(denied, 'ModuleStateDeniedError')).toBe(true); + expect(handlerExecutions).toBe(0); + + const [openInvocation] = yield* database.executor .select() .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, 'business-module-concurrent-gate')), - ); - assert.ok(openInvocation); - assert.equal(openInvocation.completedAt, null); - const deniedEvidence = await runEffectTestPromise( - database.executor + .where(eq(actionInvocations.idempotencyKey, 'business-module-concurrent-gate')); + expect(openInvocation).toBeDefined(); + if (openInvocation === undefined) { + throw new Error('Expected openInvocation'); + } + expect(openInvocation.completedAt).toBe(null); + const deniedEvidence = yield* database.executor .select() .from(auditEvents) - .where(eq(auditEvents.actionInvocationId, openInvocation.actionInvocationId)), - ); - assert.equal(deniedEvidence.length, 0); + .where(eq(auditEvents.actionInvocationId, openInvocation.actionInvocationId)); + expect(deniedEvidence.length).toBe(0); - await runEffectTestPromise( - runtime + yield* runtime .runAction({ payload: { expectedState: 'suspended', @@ -564,34 +539,31 @@ void test('rechecks business module state under the tenant lock and retries afte load: Effect.succeed(inventoryInstalledCatalog), }), Effect.provideService(TenantModuleStateService, makeTenantModuleStateService(database)), - ), - ); - await runEffectTestPromise( - runtime.runAction({ + ); + yield* runtime.runAction({ payload: undefined, principal, registration: action, transport: transport('business-module-concurrent-gate'), - }), - ); - assert.equal(handlerExecutions, 1); - }); -}); + }); + expect(handlerExecutions).toBe(1); + }), + ); -void test('atomically commits business state, all success evidence, and the succeeded marker', async () => { +const testProgram2 = Effect.fn(function* integrationProgram4() { const key = 'atomic-success'; const moduleStateKey = `test.${key}.${tenantId}`; - await databasePromise(async (database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - const result = await runEffectTestPromise( - runtime.runAction({ + yield* withDatabase( + Effect.fn(function* integrationProgram5(database) { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + const result = yield* runtime.runAction({ payload: { value: 'committed' }, principal, registration: makeRegistration({ @@ -599,11 +571,9 @@ void test('atomically commits business state, all success evidence, and the succ moduleStateKey, }), transport: transport(key, moduleStateKey), - }), - ); + }); - const [states, invocations, audits, accesses, events, messages] = await runEffectTestPromise( - Effect.all([ + const [states, invocations, audits, accesses, events, messages] = yield* Effect.all([ database.executor .select() .from(tenantModuleStates) @@ -625,32 +595,30 @@ void test('atomically commits business state, all success evidence, and the succ .select() .from(outboxMessages) .where(eq(outboxMessages.tenantId, tenantId)), - ]), - ); - - assert.equal(result.value, 'committed'); - assert.equal(states.length, 1); - assert.equal(invocations[0]?.status, 'succeeded'); - assert.ok(invocations[0]?.completedAt); - assert.equal( - audits.filter((row) => row.actionInvocationId === invocations[0]?.actionInvocationId).length, - 1, - ); - assert.equal( - accesses.filter((row) => row.actionInvocationId === invocations[0]?.actionInvocationId) - .length, - 1, - ); - assert.equal(events.length, 1); - assert.equal( - messages.filter((row) => row.domainEventId === events[0]?.domainEventId).length, - 1, - ); - assert.equal((events[0]?.tenantSequenceNo ?? 0) > 0, true); - }); + ]); + + expect(result.value).toBe('committed'); + expect(states.length).toBe(1); + expect(invocations[0]?.status).toBe('succeeded'); + expect(invocations[0]?.completedAt).toBeTruthy(); + expect( + audits.filter((row) => row.actionInvocationId === invocations[0]?.actionInvocationId) + .length, + ).toBe(1); + expect( + accesses.filter((row) => row.actionInvocationId === invocations[0]?.actionInvocationId) + .length, + ).toBe(1); + expect(events.length).toBe(1); + expect(messages.filter((row) => row.domainEventId === events[0]?.domainEventId).length).toBe( + 1, + ); + expect((events[0]?.tenantSequenceNo ?? 0) > 0).toBe(true); + }), + ); }); -void test('commits allowed Policy checkpoints atomically before handler success evidence', async () => { +const testProgram3 = Effect.fn(function* integrationProgram6() { const key = 'policy-allowed'; const moduleStateKey = `test.${key}.${tenantId}`; const observed: string[] = []; @@ -662,16 +630,16 @@ void test('commits allowed Policy checkpoints atomically before handler success policyKey: 'global.integration-allowed.v1', }); - await databasePromise(async (database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - await runEffectTestPromise( - runtime.runAction({ + yield* withDatabase( + Effect.fn(function* integrationProgram7(database) { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + yield* runtime.runAction({ payload: { value: 'committed' }, principal, registration: makeRegistration({ @@ -681,26 +649,26 @@ void test('commits allowed Policy checkpoints atomically before handler success policies: [policy], }), transport: transport(key, moduleStateKey), - }), - ); - - const { audits, invocation } = await runEffectTestPromise(invocationEvidence(database, key)); - - assert.deepEqual(observed, ['policy', 'handler']); - assert.equal(invocation.status, 'succeeded'); - assert.equal(audits.length, 2); - const policyAudit = audits.find((row) => row.eventType === 'action.policy_checked'); - const executionAudit = audits.find((row) => row.eventType === 'action.executed'); - assert.equal(policyAudit?.outcome, 'allowed'); - assert.equal(policyAudit?.outcomeStage, 'policy'); - assert.equal(executionAudit?.outcome, 'succeeded'); - assert.equal(executionAudit?.outcomeStage, 'execution'); - assert.equal(JSON.stringify(policyAudit?.evidenceJson).includes('committed'), false); - assert.equal(JSON.stringify(policyAudit?.evidenceJson).includes(policy.policyKey), true); - }); + }); + + const { audits, invocation } = yield* invocationEvidence(database, key); + + expect(observed).toEqual(['policy', 'handler']); + expect(invocation.status).toBe('succeeded'); + expect(audits.length).toBe(2); + const policyAudit = audits.find((row) => row.eventType === 'action.policy_checked'); + const executionAudit = audits.find((row) => row.eventType === 'action.executed'); + expect(policyAudit?.outcome).toBe('allowed'); + expect(policyAudit?.outcomeStage).toBe('policy'); + expect(executionAudit?.outcome).toBe('succeeded'); + expect(executionAudit?.outcomeStage).toBe('execution'); + expect(encodeJson(policyAudit?.evidenceJson).includes('committed')).toBe(false); + expect(encodeJson(policyAudit?.evidenceJson).includes(policy.policyKey)).toBe(true); + }), + ); }); -void test('atomically rejects denied global and same-owner MicroVertical Policies without handler evidence', async () => { +const testProgram4 = Effect.fn(function* integrationProgram8() { const scenarios = [ { actionKey: 'shell.test.policy-denied-global', @@ -760,7 +728,10 @@ void test('atomically rejects denied global and same-owner MicroVertical Policie owningModuleKey: 'inventory.stock', payloadSchema: Schema.Struct({ value: Schema.String }), policies: [policy], - resultSchema: Schema.Struct({ stateId: TestStateIdSchema, value: Schema.String }), + resultSchema: Schema.Struct({ + stateId: TestStateIdSchema, + value: Schema.String, + }), schemaVersion: '1', }, (payload, _context: TestActionContext) => { @@ -778,156 +749,148 @@ void test('atomically rejects denied global and same-owner MicroVertical Policie }, ] as const; - await databasePromise(async (database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - const verifyScenarioAt = async (index: number): Promise => { - const scenario = scenarios[index]; - if (scenario === undefined) { - return; - } - let handlerExecutions = 0; - const beforeMessages = await runEffectTestPromise( - database.executor - .select() - .from(outboxMessages) - .where(eq(outboxMessages.tenantId, tenantId)), - ); - const actionEffect = runtime.runAction({ - payload: { value: 'must-not-persist' }, - principal, - registration: scenario.makeRegistration(() => { - handlerExecutions += 1; - }), - transport: transport(scenario.key, `test.${scenario.key}.${tenantId}`), - }); - const exit = await runEffectTestPromise(Effect.exit(actionEffect)); - const failure = Exit.isFailure(exit) - ? Option.flatMap(Cause.findErrorOption(exit.cause), decodeActionPolicyDeniedFailure) - : Option.none(); - assert.equal(failureTag(exit), 'ActionPolicyDenied'); - if (Option.isSome(failure)) { - assert.equal(failure.value.reason, scenario.reason); - assert.equal(failure.value.policyReasonCode, scenario.reasonCode); - } - assert.equal(handlerExecutions, 0); - - const [invocation] = await runEffectTestPromise( - database.executor - .select() - .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, scenario.key)), + yield* withDatabase( + Effect.fn(function* integrationProgram9(database) { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, ); - assert.ok(invocation); - const [audits, accesses, events, states] = await runEffectTestPromise( - Effect.all([ - database.executor - .select() - .from(auditEvents) - .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)), - database.executor + yield* Effect.forEach( + scenarios, + Effect.fn(function* integrationProgram10(scenario) { + let handlerExecutions = 0; + const beforeMessages = yield* database.executor .select() - .from(dataAccessEvents) - .where(eq(dataAccessEvents.actionInvocationId, invocation.actionInvocationId)), - database.executor + .from(outboxMessages) + .where(eq(outboxMessages.tenantId, tenantId)); + const actionEffect = runtime.runAction({ + payload: { value: 'must-not-persist' }, + principal, + registration: scenario.makeRegistration(() => { + handlerExecutions += 1; + }), + transport: transport(scenario.key, `test.${scenario.key}.${tenantId}`), + }); + const exit = yield* Effect.exit(actionEffect); + const failure = Exit.isFailure(exit) + ? Option.flatMap(Cause.findErrorOption(exit.cause), decodeActionPolicyDeniedFailure) + : Option.none(); + expect(hasFailure(exit, 'ActionPolicyDenied')).toBe(true); + if (Option.isSome(failure)) { + expect(failure.value.reason).toBe(scenario.reason); + expect(failure.value.policyReasonCode).toBe(scenario.reasonCode); + } + expect(handlerExecutions).toBe(0); + + const [invocation] = yield* database.executor .select() - .from(domainEvents) - .where(eq(domainEvents.actionInvocationId, invocation.actionInvocationId)), - database.executor + .from(actionInvocations) + .where(eq(actionInvocations.idempotencyKey, scenario.key)); + expect(invocation).toBeDefined(); + if (invocation === undefined) { + throw new Error('Expected invocation'); + } + const [audits, accesses, events, states] = yield* Effect.all([ + database.executor + .select() + .from(auditEvents) + .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)), + database.executor + .select() + .from(dataAccessEvents) + .where(eq(dataAccessEvents.actionInvocationId, invocation.actionInvocationId)), + database.executor + .select() + .from(domainEvents) + .where(eq(domainEvents.actionInvocationId, invocation.actionInvocationId)), + database.executor + .select() + .from(tenantModuleStates) + .where(eq(tenantModuleStates.moduleKey, `test.${scenario.key}.${tenantId}`)), + ]); + const messages = yield* database.executor .select() - .from(tenantModuleStates) - .where(eq(tenantModuleStates.moduleKey, `test.${scenario.key}.${tenantId}`)), - ]), - ); - const messages = await runEffectTestPromise( - database.executor - .select() - .from(outboxMessages) - .where(eq(outboxMessages.tenantId, tenantId)), + .from(outboxMessages) + .where(eq(outboxMessages.tenantId, tenantId)); + + expect(invocation.status).toBe('rejected'); + expect(invocation.completedAt).toBeTruthy(); + expect(audits.length).toBe(2); + for (const eventType of ['action.policy_checked', 'action.rejected']) { + const audit = audits.find((row) => row.eventType === eventType); + expect(audit?.outcome).toBe('denied'); + expect(audit?.outcomeStage).toBe('policy'); + expect(audit?.outcomeCode).toBe(scenario.reasonCode); + } + expect(encodeJson(audits).includes(scenario.reason)).toBe(false); + expect(accesses.length).toBe(0); + expect(events.length).toBe(0); + expect(states.length).toBe(0); + expect(messages.length).toBe(beforeMessages.length); + }), + { discard: true }, ); - - assert.equal(invocation.status, 'rejected'); - assert.ok(invocation.completedAt); - assert.equal(audits.length, 2); - for (const eventType of ['action.policy_checked', 'action.rejected']) { - const audit = audits.find((row) => row.eventType === eventType); - assert.equal(audit?.outcome, 'denied'); - assert.equal(audit?.outcomeStage, 'policy'); - assert.equal(audit?.outcomeCode, scenario.reasonCode); - } - assert.equal(JSON.stringify(audits).includes(scenario.reason), false); - assert.equal(accesses.length, 0); - assert.equal(events.length, 0); - assert.equal(states.length, 0); - assert.equal(messages.length, beforeMessages.length); - await verifyScenarioAt(index + 1); - }; - await verifyScenarioAt(0); - }); + }), + ); }); -void test('rolls back every denied-Policy finalization persistence failure', async () => { +const testProgram5 = Effect.fn(function* integrationProgram11() { const policy = defineGlobalPolicy<{ readonly value: string }>({ evaluate: () => Effect.fail(denyPolicy('blocked', 'This operation is blocked')), policyKey: 'global.blocked.v1', }); - await databasePromise(async (database) => { - const stages = ['audit', 'invocation-success'] as const; - const verifyStageAt = async (index: number): Promise => { - const stage = stages[index]; - if (stage === undefined) { - return; - } - const key = `policy-finalization-${stage}`; - let handlerExecutions = 0; - const runtime = makeActionRuntime( - withEvidencePersistenceFailure(database, stage), - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - const exit = await runEffectTestPromise( - Effect.exit( - runtime.runAction({ - payload: { value: 'must-not-persist' }, - principal, - registration: makeRegistration({ - actionKey: `shell.test.${key}`, - moduleStateKey: `test.${key}.${tenantId}`, - onExecute: () => { - handlerExecutions += 1; - }, - policies: [policy], + yield* withDatabase( + Effect.fn(function* integrationProgram12(database) { + const stages = ['audit', 'invocation-success'] as const; + yield* Effect.forEach( + stages, + Effect.fn(function* integrationProgram13(stage) { + const key = `policy-finalization-${stage}`; + let handlerExecutions = 0; + const runtime = makeActionRuntime( + withEvidencePersistenceFailure(database, stage), + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + const exit = yield* Effect.exit( + runtime.runAction({ + payload: { value: 'must-not-persist' }, + principal, + registration: makeRegistration({ + actionKey: `shell.test.${key}`, + moduleStateKey: `test.${key}.${tenantId}`, + onExecute: () => { + handlerExecutions += 1; + }, + policies: [policy], + }), + transport: transport(key), }), - transport: transport(key), - }), - ), - ); - const { audits, invocation } = await runEffectTestPromise(invocationEvidence(database, key)); - - assert.equal( - failureTag(exit), - 'ActionInvocationPersistenceError', - Exit.isFailure(exit) ? Cause.pretty(exit.cause) : 'success', + ); + const { audits, invocation } = yield* invocationEvidence(database, key); + + expect( + hasFailure(exit, 'ActionInvocationPersistenceError'), + Exit.isFailure(exit) ? Cause.pretty(exit.cause) : 'success', + ).toBeTruthy(); + expect(handlerExecutions).toBe(0); + expect(invocation.status).toBe('received'); + expect(invocation.completedAt).toBe(null); + expect(audits.length).toBe(0); + }), + { discard: true }, ); - assert.equal(handlerExecutions, 0); - assert.equal(invocation.status, 'received'); - assert.equal(invocation.completedAt, null); - assert.equal(audits.length, 0); - await verifyStageAt(index + 1); - }; - await verifyStageAt(0); - }); + }), + ); }); -void test('rolls back domain rejection, evidence persistence failure, and orphan outbox attempts', async () => { +const testProgram6 = Effect.fn(function* integrationProgram14() { const scenarios = [ { actionKey: 'shell.test.domain-rejection', @@ -949,24 +912,21 @@ void test('rolls back domain rejection, evidence persistence failure, and orphan }, ] as const; - await databasePromise(async (database) => { - const verifyScenarioAt = async (index: number): Promise => { - const scenario = scenarios[index]; - if (scenario === undefined) { - return; - } - const moduleStateKey = `test.${scenario.key}.${tenantId}`; - const runtime = makeActionRuntime( - scenario.key === 'evidence-failure' - ? withEvidencePersistenceFailure(database, 'audit') - : database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - const exit = await runEffectTestPromise( - Effect.exit( + yield* withDatabase((database) => + Effect.forEach( + scenarios, + Effect.fn(function* integrationProgram15(scenario) { + const moduleStateKey = `test.${scenario.key}.${tenantId}`; + const runtime = makeActionRuntime( + scenario.key === 'evidence-failure' + ? withEvidencePersistenceFailure(database, 'audit') + : database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + const exit = yield* Effect.exit( runtime.runAction({ payload: { value: scenario.key }, principal, @@ -977,64 +937,53 @@ void test('rolls back domain rejection, evidence persistence failure, and orphan }), transport: transport(scenario.key, moduleStateKey), }), - ), - ); + ); - const states = await runEffectTestPromise( - database.executor + const states = yield* database.executor .select() .from(tenantModuleStates) - .where(eq(tenantModuleStates.moduleKey, moduleStateKey)), - ); - const invocations = await runEffectTestPromise( - database.executor + .where(eq(tenantModuleStates.moduleKey, moduleStateKey)); + const invocations = yield* database.executor .select() .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, scenario.key)), - ); - const invocationId = invocations[0]?.actionInvocationId; - const committedEvidence = - invocationId === undefined - ? [] - : await runEffectTestPromise( - database.executor + .where(eq(actionInvocations.idempotencyKey, scenario.key)); + const invocationId = invocations[0]?.actionInvocationId; + const committedEvidence = + invocationId === undefined + ? [] + : yield* database.executor .select() .from(auditEvents) - .where(eq(auditEvents.actionInvocationId, invocationId)), - ); - const committedAccesses = - invocationId === undefined - ? [] - : await runEffectTestPromise( - database.executor + .where(eq(auditEvents.actionInvocationId, invocationId)); + const committedAccesses = + invocationId === undefined + ? [] + : yield* database.executor .select() .from(dataAccessEvents) - .where(eq(dataAccessEvents.actionInvocationId, invocationId)), - ); - const committedEvents = - invocationId === undefined - ? [] - : await runEffectTestPromise( - database.executor + .where(eq(dataAccessEvents.actionInvocationId, invocationId)); + const committedEvents = + invocationId === undefined + ? [] + : yield* database.executor .select() .from(domainEvents) - .where(eq(domainEvents.actionInvocationId, invocationId)), - ); - - assert.equal(failureTag(exit), scenario.expectedTag); - assert.equal(states.length, 0); - assert.equal(invocations[0]?.status, 'running'); - assert.equal(invocations[0]?.completedAt, null); - assert.equal(committedEvidence.length, 0); - assert.equal(committedAccesses.length, 0); - assert.equal(committedEvents.length, 0); - await verifyScenarioAt(index + 1); - }; - await verifyScenarioAt(0); - }); + .where(eq(domainEvents.actionInvocationId, invocationId)); + + expect(hasFailure(exit, scenario.expectedTag)).toBe(true); + expect(states.length).toBe(0); + expect(invocations[0]?.status).toBe('running'); + expect(invocations[0]?.completedAt).toBe(null); + expect(committedEvidence.length).toBe(0); + expect(committedAccesses.length).toBe(0); + expect(committedEvents.length).toBe(0); + }), + { discard: true }, + ), + ); }); -void test('rolls back every individual success-evidence persistence failure', async () => { +const testProgram7 = Effect.fn(function* integrationProgram16() { const stages: readonly EvidencePersistenceStage[] = [ 'audit', 'data-access', @@ -1047,29 +996,24 @@ void test('rolls back every individual success-evidence persistence failure', as policyKey: 'global.atomic-success-evidence.v1', }); - await databasePromise(async (database) => { - const verifyStageAt = async (index: number): Promise => { - const stage = stages[index]; - if (stage === undefined) { - return; - } - const key = `evidence-${stage}`; - const moduleStateKey = `test.${key}.${tenantId}`; - const beforeOutbox = await runEffectTestPromise( - database.executor + yield* withDatabase((database) => + Effect.forEach( + stages, + Effect.fn(function* integrationProgram17(stage) { + const key = `evidence-${stage}`; + const moduleStateKey = `test.${key}.${tenantId}`; + const beforeOutbox = yield* database.executor .select() .from(outboxMessages) - .where(eq(outboxMessages.tenantId, tenantId)), - ); - const runtime = makeActionRuntime( - withEvidencePersistenceFailure(database, stage), - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - const exit = await runEffectTestPromise( - Effect.exit( + .where(eq(outboxMessages.tenantId, tenantId)); + const runtime = makeActionRuntime( + withEvidencePersistenceFailure(database, stage), + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + const exit = yield* Effect.exit( runtime.runAction({ payload: { value: stage }, principal, @@ -1080,25 +1024,19 @@ void test('rolls back every individual success-evidence persistence failure', as }), transport: transport(key, moduleStateKey), }), - ), - ); + ); - const states = await runEffectTestPromise( - database.executor + const states = yield* database.executor .select() .from(tenantModuleStates) - .where(eq(tenantModuleStates.moduleKey, moduleStateKey)), - ); - const [invocation] = await runEffectTestPromise( - database.executor + .where(eq(tenantModuleStates.moduleKey, moduleStateKey)); + const [invocation] = yield* database.executor .select() .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, key)), - ); - assert.notEqual(invocation, undefined); - const invocationId = invocation?.actionInvocationId ?? ''; - const [audits, accesses, events, afterOutbox] = await runEffectTestPromise( - Effect.all([ + .where(eq(actionInvocations.idempotencyKey, key)); + expect(invocation).not.toBe(undefined); + const invocationId = invocation?.actionInvocationId ?? ''; + const [audits, accesses, events, afterOutbox] = yield* Effect.all([ database.executor .select() .from(auditEvents) @@ -1115,73 +1053,72 @@ void test('rolls back every individual success-evidence persistence failure', as .select() .from(outboxMessages) .where(eq(outboxMessages.tenantId, tenantId)), - ]), - ); + ]); - assert.equal(failureTag(exit), 'ActionTransactionError', stage); - assert.equal(states.length, 0, stage); - assert.equal(invocation?.status, 'running', stage); - assert.equal(invocation?.completedAt, null, stage); - assert.equal(audits.length, 0, stage); - assert.equal(accesses.length, 0, stage); - assert.equal(events.length, 0, stage); - assert.equal(afterOutbox.length, beforeOutbox.length, stage); - await verifyStageAt(index + 1); - }; - await verifyStageAt(0); - }); + expect(hasFailure(exit, 'ActionTransactionError'), stage).toBe(true); + expect(states.length, stage).toBe(0); + expect(invocation?.status, stage).toBe('running'); + expect(invocation?.completedAt, stage).toBe(null); + expect(audits.length, stage).toBe(0); + expect(accesses.length, stage).toBe(0); + expect(events.length, stage).toBe(0); + expect(afterOutbox.length, stage).toBe(beforeOutbox.length); + }), + { discard: true }, + ), + ); }); -void test('keeps Policy rejection terminal and deduplicates repeated and concurrent evidence', async () => { - await databasePromise(async (database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - let evaluations = 0; - let handlerExecutions = 0; - const policy = defineGlobalPolicy<{ readonly value: string }>({ - evaluate: () => { - evaluations += 1; - return Effect.fail(denyPolicy('terminal_rejection', 'This rejection is terminal')); - }, - policyKey: 'global.terminal-rejection.v1', - }); - const key = 'policy-terminal-retry'; - const action = makeRegistration({ - actionKey: 'shell.test.policy-terminal-retry', - moduleStateKey: `test.${key}.${tenantId}`, - onExecute: () => { - handlerExecutions += 1; - }, - policies: [policy], - }); - const input = { - payload: { value: 'same' }, - principal, - registration: action, - transport: transport(key), - }; - const first = await runEffectTestPromise(Effect.exit(runtime.runAction(input))); - const retry = await runEffectTestPromise(Effect.exit(runtime.runAction(input))); - const { audits, invocation } = await runEffectTestPromise(invocationEvidence(database, key)); - - assert.equal(failureTag(first), 'ActionPolicyDenied'); - assert.equal(failureTag(retry), 'ActionInvocationStateError'); - assert.equal(evaluations, 1); - assert.equal(handlerExecutions, 0); - assert.equal(invocation.status, 'rejected'); - assert.equal(audits.length, 2); - - let concurrentEvaluations = 0; - const concurrentKey = 'policy-terminal-concurrent'; - const concurrentPoliciesReached = await runEffectTestPromise(Deferred.make()); - const concurrentPolicy = defineGlobalPolicy<{ readonly value: string }>({ - evaluate: () => - Effect.gen(function* delayedDenial() { +const testProgram8 = () => + withDatabase( + Effect.fn(function* integrationProgram18(database) { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + let evaluations = 0; + let handlerExecutions = 0; + const policy = defineGlobalPolicy<{ readonly value: string }>({ + evaluate: () => { + evaluations += 1; + return Effect.fail(denyPolicy('terminal_rejection', 'This rejection is terminal')); + }, + policyKey: 'global.terminal-rejection.v1', + }); + const key = 'policy-terminal-retry'; + const action = makeRegistration({ + actionKey: 'shell.test.policy-terminal-retry', + moduleStateKey: `test.${key}.${tenantId}`, + onExecute: () => { + handlerExecutions += 1; + }, + policies: [policy], + }); + const input = { + payload: { value: 'same' }, + principal, + registration: action, + transport: transport(key), + }; + const first = yield* Effect.exit(runtime.runAction(input)); + const retry = yield* Effect.exit(runtime.runAction(input)); + const { audits, invocation } = yield* invocationEvidence(database, key); + + expect(hasFailure(first, 'ActionPolicyDenied')).toBe(true); + expect(hasFailure(retry, 'ActionInvocationStateError')).toBe(true); + expect(evaluations).toBe(1); + expect(handlerExecutions).toBe(0); + expect(invocation.status).toBe('rejected'); + expect(audits.length).toBe(2); + + let concurrentEvaluations = 0; + const concurrentKey = 'policy-terminal-concurrent'; + const concurrentPoliciesReached = yield* Deferred.make(); + const concurrentPolicy = defineGlobalPolicy<{ readonly value: string }>({ + evaluate: Effect.fn(function* delayedDenial() { concurrentEvaluations += 1; if (concurrentEvaluations === 2) { yield* Deferred.succeed(concurrentPoliciesReached, null); @@ -1189,371 +1126,384 @@ void test('keeps Policy rejection terminal and deduplicates repeated and concurr yield* Deferred.await(concurrentPoliciesReached); return yield* denyPolicy('concurrent_rejection', 'Concurrent request rejected'); }), - policyKey: 'global.concurrent-rejection.v1', - }); - const concurrentInput = { - payload: { value: 'same' }, - principal, - registration: makeRegistration({ - actionKey: 'shell.test.policy-terminal-concurrent', - moduleStateKey: `test.${concurrentKey}.${tenantId}`, - onExecute: () => { - handlerExecutions += 1; - }, - policies: [concurrentPolicy], - }), - transport: transport(concurrentKey), - }; - const concurrent = await Promise.all([ - runEffectTestPromise(Effect.exit(runtime.runAction(concurrentInput))), - runEffectTestPromise(Effect.exit(runtime.runAction(concurrentInput))), - ]); - const [concurrentInvocation] = await runEffectTestPromise( - database.executor + policyKey: 'global.concurrent-rejection.v1', + }); + const concurrentInput = { + payload: { value: 'same' }, + principal, + registration: makeRegistration({ + actionKey: 'shell.test.policy-terminal-concurrent', + moduleStateKey: `test.${concurrentKey}.${tenantId}`, + onExecute: () => { + handlerExecutions += 1; + }, + policies: [concurrentPolicy], + }), + transport: transport(concurrentKey), + }; + const concurrent = yield* Effect.all( + [ + Effect.exit(runtime.runAction(concurrentInput)), + Effect.exit(runtime.runAction(concurrentInput)), + ], + { concurrency: 'unbounded' }, + ); + const [concurrentInvocation] = yield* database.executor .select() .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, concurrentKey)), - ); - assert.ok(concurrentInvocation); - const concurrentAudits = await runEffectTestPromise( - database.executor + .where(eq(actionInvocations.idempotencyKey, concurrentKey)); + expect(concurrentInvocation).toBeDefined(); + if (concurrentInvocation === undefined) { + throw new Error('Expected concurrentInvocation'); + } + const concurrentAudits = yield* database.executor .select() .from(auditEvents) - .where(eq(auditEvents.actionInvocationId, concurrentInvocation.actionInvocationId)), - ); - - assert.deepEqual(concurrent.map(failureTag), ['ActionPolicyDenied', 'ActionPolicyDenied']); - assert.equal(concurrentEvaluations, 2); - assert.equal(handlerExecutions, 0); - assert.equal(concurrentInvocation.status, 'rejected'); - assert.equal(concurrentAudits.length, 2); - }); -}); + .where(eq(auditEvents.actionInvocationId, concurrentInvocation.actionInvocationId)); -void test('never lets a losing Policy denial replace a running or successful invocation', async () => { - await databasePromise(async (database) => { - const repository = makeActionRepository(); - const allowedRuntime = makeActionRuntime( - database, - repository, - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - const deniedRuntime = makeActionRuntime( - database, - repository, - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - const handlerStarted = await runEffectTestPromise(Deferred.make()); - const denialEvaluated = await runEffectTestPromise(Deferred.make()); - const key = 'policy-loses-to-success'; - const moduleStateKey = `test.${key}.${tenantId}`; - const actionKey = 'shell.test.policy-loses-to-success'; - const allowed = makeRegistration({ - actionKey, - completionGate: denialEvaluated, - moduleStateKey, - onExecute: () => { - runEffectTestSync(Deferred.succeed(handlerStarted, null)); - }, - }); - const denial = defineGlobalPolicy<{ readonly value: string }>({ - evaluate: () => - Deferred.succeed(denialEvaluated, null).pipe( - Effect.andThen(Effect.fail(denyPolicy('late_denial', 'This denial arrived too late'))), - ), - policyKey: 'global.late-denial.v1', - }); - const denied = makeRegistration({ - actionKey, - moduleStateKey, - policies: [denial], - }); - const sharedInput = { - payload: { value: 'same' }, - principal, - transport: transport(key, moduleStateKey), - }; - - const success = runEffectTestPromise( - allowedRuntime.runAction({ ...sharedInput, registration: allowed }), - ); - await runEffectTestPromise(Deferred.await(handlerStarted)); - const rejected = runEffectTestPromise( - Effect.exit(deniedRuntime.runAction({ ...sharedInput, registration: denied })), - ); - const [successResult, rejectedExit] = await Promise.all([success, rejected]); - const { audits, invocation } = await runEffectTestPromise(invocationEvidence(database, key)); - - assert.equal(successResult.value, 'same'); - assert.equal(failureTag(rejectedExit), 'ActionInvocationPersistenceError'); - assert.equal(invocation.status, 'succeeded'); - assert.equal(audits.filter((row) => row.eventType === 'action.rejected').length, 0); - }); -}); + expect(concurrent.length).toBe(2); + for (const outcome of concurrent) { + expect(hasFailure(outcome, 'ActionPolicyDenied')).toBe(true); + } + expect(concurrentEvaluations).toBe(2); + expect(handlerExecutions).toBe(0); + expect(concurrentInvocation.status).toBe('rejected'); + expect(concurrentAudits.length).toBe(2); + }), + ); -void test('serializes concurrent requests and enforces committed, open-retry, and hash-conflict behavior', async () => { - await runEffectTestPromise( - withDatabase((database) => - Effect.gen(function* concurrencyProof() { - const handlerStarted = yield* Deferred.make(); - const handlerRelease = yield* Deferred.make(); - const secondPermissionChecked = yield* Deferred.make(); - let permissionChecks = 0; - const concurrentAllowedPermission = { - checkActionPermission: () => - Effect.gen(function* recordPermissionCheck() { - permissionChecks += 1; - if (permissionChecks === 2) { - yield* Deferred.succeed(secondPermissionChecked, null); - } - return 'allowed' as const; - }), - }; - const runtime = makeActionRuntime( - database, - makeActionRepository(), - concurrentAllowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - let executions = 0; - const concurrentKey = 'concurrent-once'; - const concurrentModule = `test.concurrent.${tenantId}`; - const concurrentInput = { - payload: { value: 'same' }, - principal, - registration: makeRegistration({ - actionKey: 'shell.test.concurrent', - completionGate: handlerRelease, - moduleStateKey: concurrentModule, - onExecute: () => { - executions += 1; - runEffectTestSync(Deferred.succeed(handlerStarted, null)); - }, - }), - transport: transport(concurrentKey, concurrentModule), - }; - const firstAttempt = yield* Effect.exit(runtime.runAction(concurrentInput)).pipe( - Effect.forkChild, - ); - yield* Deferred.await(handlerStarted); - const secondAttempt = yield* Effect.exit(runtime.runAction(concurrentInput)).pipe( - Effect.forkChild, - ); - yield* Deferred.await(secondPermissionChecked); - yield* Deferred.succeed(handlerRelease, null); - const concurrentResults = yield* Effect.all([ - Fiber.join(firstAttempt), - Fiber.join(secondAttempt), - ]); +const testProgram9 = () => + withDatabase( + Effect.fn(function* integrationProgram19(database) { + const repository = makeActionRepository(); + const allowedRuntime = makeActionRuntime( + database, + repository, + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + const deniedRuntime = makeActionRuntime( + database, + repository, + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + const handlerStarted = yield* Deferred.make(); + const denialEvaluated = yield* Deferred.make(); + const key = 'policy-loses-to-success'; + const moduleStateKey = `test.${key}.${tenantId}`; + const actionKey = 'shell.test.policy-loses-to-success'; + const allowed = makeRegistration({ + actionKey, + completionGate: denialEvaluated, + moduleStateKey, + onExecuteEffect: Deferred.succeed(handlerStarted, null), + }); + const denial = defineGlobalPolicy<{ readonly value: string }>({ + evaluate: () => + Deferred.succeed(denialEvaluated, null).pipe( + Effect.andThen(Effect.fail(denyPolicy('late_denial', 'This denial arrived too late'))), + ), + policyKey: 'global.late-denial.v1', + }); + const denied = makeRegistration({ + actionKey, + moduleStateKey, + policies: [denial], + }); + const sharedInput = { + payload: { value: 'same' }, + principal, + transport: transport(key, moduleStateKey), + }; - assert.equal(executions, 1); - assert.equal(concurrentResults.filter(Exit.isSuccess).length, 1); - assert.deepEqual(concurrentResults.filter(Exit.isFailure).map(failureTag), [ - 'ActionAlreadyCommitted', - ]); + const success = yield* Effect.forkScoped( + allowedRuntime.runAction({ ...sharedInput, registration: allowed }), + ); + yield* Deferred.await(handlerStarted); + const rejected = yield* Effect.forkScoped( + Effect.exit(deniedRuntime.runAction({ ...sharedInput, registration: denied })), + ); + const [successResult, rejectedExit] = yield* Effect.all( + [Fiber.join(success), Fiber.join(rejected)], + { concurrency: 'unbounded' }, + ); + const { audits, invocation } = yield* invocationEvidence(database, key); - const committedRetry = yield* Effect.exit( - runtime.runAction({ - ...concurrentInput, - transport: { - ...concurrentInput.transport, - correlationId: 'integration-concurrent-retry', - traceId: 'retry-trace', - }, - }), - ); - assert.equal(failureTag(committedRetry), 'ActionAlreadyCommitted'); - assert.equal(executions, 1); + expect(successResult.value).toBe('same'); + expect(hasFailure(rejectedExit, 'ActionInvocationPersistenceError')).toBe(true); + expect(invocation.status).toBe('succeeded'); + expect(audits.filter((row) => row.eventType === 'action.rejected').length).toBe(0); + }), + ); - const conflict = yield* Effect.exit( - runtime.runAction({ - ...concurrentInput, - payload: { value: 'different' }, - }), - ); - assert.equal(failureTag(conflict), 'ActionRequestHashConflict'); +const testProgram10 = () => + withDatabase( + Effect.fn(function* concurrencyProof(database) { + const handlerStarted = yield* Deferred.make(); + const handlerRelease = yield* Deferred.make(); + const secondPermissionChecked = yield* Deferred.make(); + let permissionChecks = 0; + const concurrentAllowedPermission = { + checkActionPermission: Effect.fn(function* recordPermissionCheck() { + permissionChecks += 1; + if (permissionChecks === 2) { + yield* Deferred.succeed(secondPermissionChecked, null); + } + return 'allowed' as const; + }), + }; + const runtime = makeActionRuntime( + database, + makeActionRepository(), + concurrentAllowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + let executions = 0; + const concurrentKey = 'concurrent-once'; + const concurrentModule = `test.concurrent.${tenantId}`; + const concurrentInput = { + payload: { value: 'same' }, + principal, + registration: makeRegistration({ + actionKey: 'shell.test.concurrent', + completionGate: handlerRelease, + moduleStateKey: concurrentModule, + onExecute: () => { + executions += 1; + }, + onExecuteEffect: Deferred.succeed(handlerStarted, null), + }), + transport: transport(concurrentKey, concurrentModule), + }; + const firstAttempt = yield* Effect.exit(runtime.runAction(concurrentInput)).pipe( + Effect.forkChild, + ); + yield* Deferred.await(handlerStarted); + const secondAttempt = yield* Effect.exit(runtime.runAction(concurrentInput)).pipe( + Effect.forkChild, + ); + yield* Deferred.await(secondPermissionChecked); + yield* Deferred.succeed(handlerRelease, null); + const concurrentResults = yield* Effect.all([ + Fiber.join(firstAttempt), + Fiber.join(secondAttempt), + ]); + + expect(executions).toBe(1); + expect(concurrentResults.filter(Exit.isSuccess).length).toBe(1); + const failedResults = concurrentResults.filter(Exit.isFailure); + expect(failedResults.length).toBe(1); + for (const outcome of failedResults) { + expect(hasFailure(outcome, 'ActionAlreadyCommitted')).toBe(true); + } - const openKey = 'open-retry'; - const openModule = `test.open-retry.${tenantId}`; - const rejected = yield* Effect.exit( - runtime.runAction({ - payload: { value: 'retryable' }, - principal, - registration: makeRegistration({ - actionKey: 'shell.test.open-retry', - mode: 'reject', - moduleStateKey: openModule, - }), - transport: transport(openKey, openModule), - }), - ); - assert.equal(failureTag(rejected), 'TestDomainRejected'); + const committedRetry = yield* Effect.exit( + runtime.runAction({ + ...concurrentInput, + transport: { + ...concurrentInput.transport, + correlationId: 'integration-concurrent-retry', + traceId: 'retry-trace', + }, + }), + ); + expect(hasFailure(committedRetry, 'ActionAlreadyCommitted')).toBe(true); + expect(executions).toBe(1); + + const conflict = yield* Effect.exit( + runtime.runAction({ + ...concurrentInput, + payload: { value: 'different' }, + }), + ); + expect(hasFailure(conflict, 'ActionRequestHashConflict')).toBe(true); - const retried = yield* runtime.runAction({ + const openKey = 'open-retry'; + const openModule = `test.open-retry.${tenantId}`; + const rejected = yield* Effect.exit( + runtime.runAction({ payload: { value: 'retryable' }, principal, registration: makeRegistration({ actionKey: 'shell.test.open-retry', + mode: 'reject', moduleStateKey: openModule, }), transport: transport(openKey, openModule), - }); - assert.equal(retried.value, 'retryable'); - }), - ), - ); -}); + }), + ); + expect(hasFailure(rejected, 'TestDomainRejected')).toBe(true); -void test('serializes Domain Event allocation by tenant commit order', async () => { - await databasePromise(async (database) => { - const firstCommitRelease = await runEffectTestPromise(Deferred.make()); - const firstFlushed = await runEffectTestPromise(Deferred.make()); - const secondInsertStarted = await runEffectTestPromise(Deferred.make()); - const delayedTransaction = { - transaction: (transactionBody) => - database.executor.transaction((transaction) => - Effect.gen(function* delayCommit() { - const result = yield* transactionBody(transaction); - yield* Deferred.succeed(firstFlushed, null); - yield* Deferred.await(firstCommitRelease); - return result; - }), - ), - } satisfies Pick; - const delayedExecutor: ContextServiceContract['executor'] = Object.assign( - Object.create(database.executor), - delayedTransaction, - ); - const repository = makeActionRepository(); - const firstRuntime = makeActionRuntime( - { executor: delayedExecutor }, - repository, - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - const secondRuntime = makeActionRuntime( - database, - repository, - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - const firstModule = `test.sequence.first.${tenantId}`; - const secondModule = `test.sequence.second.${tenantId}`; - - const first = runEffectTestPromise( - firstRuntime.runAction({ - payload: { value: 'first' }, + const retried = yield* runtime.runAction({ + payload: { value: 'retryable' }, principal, registration: makeRegistration({ - actionKey: 'shell.test.sequence-first', - moduleStateKey: firstModule, + actionKey: 'shell.test.open-retry', + moduleStateKey: openModule, }), - transport: transport('sequence-first', firstModule), - }), - ); - await runEffectTestPromise(Deferred.await(firstFlushed)); + transport: transport(openKey, openModule), + }); + expect(retried.value).toBe('retryable'); + }), + ); - let secondCompleted = false; - const second = runEffectTestPromise( - secondRuntime - .runAction({ - payload: { value: 'second' }, +const testProgram11 = () => + withDatabase( + Effect.fn(function* integrationProgram20(database) { + const firstCommitRelease = yield* Deferred.make(); + const firstFlushed = yield* Deferred.make(); + const secondInsertStarted = yield* Deferred.make(); + const delayedTransaction = { + transaction: (transactionBody) => + database.executor.transaction( + Effect.fn(function* delayCommit(transaction) { + const result = yield* transactionBody(transaction); + yield* Deferred.succeed(firstFlushed, null); + yield* Deferred.await(firstCommitRelease); + return result; + }), + ), + } satisfies Pick; + const delayedExecutor: ContextServiceContract['executor'] = Object.assign( + Object.create(database.executor), + delayedTransaction, + ); + const repository = makeActionRepository(); + const firstRuntime = makeActionRuntime( + { executor: delayedExecutor }, + repository, + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + const secondRuntime = makeActionRuntime( + database, + repository, + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + const firstModule = `test.sequence.first.${tenantId}`; + const secondModule = `test.sequence.second.${tenantId}`; + + const first = yield* Effect.forkScoped( + firstRuntime.runAction({ + payload: { value: 'first' }, principal, registration: makeRegistration({ - actionKey: 'shell.test.sequence-second', - moduleStateKey: secondModule, + actionKey: 'shell.test.sequence-first', + moduleStateKey: firstModule, }), - transport: transport('sequence-second', secondModule), - }) - .pipe( - Effect.provideService(TestQueryHook, () => - Deferred.succeed(secondInsertStarted, null).pipe(Effect.asVoid), + transport: transport('sequence-first', firstModule), + }), + ); + yield* Deferred.await(firstFlushed); + + let secondCompleted = false; + const second = yield* Effect.forkScoped( + secondRuntime + .runAction({ + payload: { value: 'second' }, + principal, + registration: makeRegistration({ + actionKey: 'shell.test.sequence-second', + moduleStateKey: secondModule, + }), + transport: transport('sequence-second', secondModule), + }) + .pipe( + Effect.provideService(TestQueryHook, () => + Deferred.succeed(secondInsertStarted, null).pipe(Effect.asVoid), + ), + Effect.ensuring( + Effect.sync(() => { + secondCompleted = true; + }), + ), ), - ), - ).finally(() => { - secondCompleted = true; - }); + ); - await runEffectTestPromise(Deferred.await(secondInsertStarted)); - assert.equal(secondCompleted, false); + yield* Deferred.await(secondInsertStarted); + expect(secondCompleted).toBe(false); - runEffectTestSync(Deferred.succeed(firstCommitRelease, null)); - await Promise.all([first, second]); + yield* Deferred.succeed(firstCommitRelease, null); + yield* Effect.all([first, second].map(Fiber.join), { concurrency: 'unbounded' }); - const events = await runEffectTestPromise( - database.executor.select().from(domainEvents).where(eq(domainEvents.tenantId, tenantId)), - ); - const firstEvent = events.find((event) => event.subjectResourceId === firstModule); - const secondEvent = events.find((event) => event.subjectResourceId === secondModule); + const events = yield* database.executor + .select() + .from(domainEvents) + .where(eq(domainEvents.tenantId, tenantId)); + const firstEvent = events.find((event) => event.subjectResourceId === firstModule); + const secondEvent = events.find((event) => event.subjectResourceId === secondModule); + + expect(firstEvent).toBeDefined(); + if (firstEvent === undefined) { + throw new Error('Expected firstEvent'); + } + expect(secondEvent).toBeDefined(); + if (secondEvent === undefined) { + throw new Error('Expected secondEvent'); + } + expect(firstEvent.tenantSequenceNo < secondEvent.tenantSequenceNo).toBeTruthy(); + }), + ); - assert.ok(firstEvent); - assert.ok(secondEvent); - assert.ok(firstEvent.tenantSequenceNo < secondEvent.tenantSequenceNo); - }); -}); +const testProgram12 = () => + withDatabase( + Effect.fn(function* integrationProgram21(database) { + const repository = makeActionRepository(); + const key = 'lost-acknowledgement'; + const moduleStateKey = `test.lost-ack.${tenantId}`; + const actionRegistration = makeRegistration({ + actionKey: 'shell.test.lost-ack', + moduleStateKey, + }); -void test('resolves a lost commit acknowledgement from the durable succeeded marker', async () => { - await databasePromise(async (database) => { - const repository = makeActionRepository(); - const key = 'lost-acknowledgement'; - const moduleStateKey = `test.lost-ack.${tenantId}`; - const actionRegistration = makeRegistration({ - actionKey: 'shell.test.lost-ack', - moduleStateKey, - }); - - const acknowledgementLost = new SqlError({ - reason: new ConnectionError({ cause: { code: '08007' } }), - }); - const uncertainTransaction = { - transaction: (transactionBody) => - database.executor - .transaction(transactionBody) - .pipe(Effect.andThen(Effect.die(acknowledgementLost))), - } satisfies Pick; - const uncertainRuntime = makeActionRuntime( - withTransactionOverride(database, uncertainTransaction), - repository, - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - const first = await runEffectTestPromise( - Effect.exit( + const acknowledgementLost = new SqlError({ + reason: new ConnectionError({ cause: { code: '08007' } }), + }); + const uncertainTransaction = { + transaction: (transactionBody) => + database.executor + .transaction(transactionBody) + .pipe(Effect.andThen(Effect.die(acknowledgementLost))), + } satisfies Pick; + + const uncertainRuntime = makeActionRuntime( + withTransactionOverride(database, uncertainTransaction), + repository, + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + const first = yield* Effect.exit( uncertainRuntime.runAction({ payload: { value: 'committed-with-lost-ack' }, principal, registration: actionRegistration, transport: transport(key, moduleStateKey), }), - ), - ); - assert.equal(failureTag(first), 'ActionCommitIndeterminate'); - - const resolvingRuntime = makeActionRuntime( - database, - repository, - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - const invocations = await runEffectTestPromise( - database.executor + ); + expect(hasFailure(first, 'ActionCommitIndeterminate')).toBe(true); + + const resolvingRuntime = makeActionRuntime( + database, + repository, + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + const invocations = yield* database.executor .select() .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, key)), - ); - const invocationId = invocations[0]?.actionInvocationId; - assert.notEqual(invocationId, undefined); - const unauthorizedResolution = await runEffectTestPromise( - Effect.exit( + .where(eq(actionInvocations.idempotencyKey, key)); + const invocationId = invocations[0]?.actionInvocationId; + expect(invocationId).not.toBe(undefined); + const unauthorizedResolution = yield* Effect.exit( resolvingRuntime.resolveActionCommit({ invocationId, principal: { @@ -1561,259 +1511,289 @@ void test('resolves a lost commit acknowledgement from the durable succeeded mar principalId: randomUUID(), }, }), - ), - ); - const unavailableRuntime = makeActionRuntime( - database, - { - ...repository, - resolveInvocation: () => - Effect.fail( - new ActionInvocationPersistenceError({ - code: 'action_invocation_persistence_failed', - reason: 'test database unavailable', - }), - ), - }, - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - const unavailableResolution = await runEffectTestPromise( - Effect.exit( + ); + const unavailableRuntime = makeActionRuntime( + database, + { + ...repository, + resolveInvocation: () => + Effect.fail( + new ActionInvocationPersistenceError({ + code: 'action_invocation_persistence_failed', + reason: 'test database unavailable', + }), + ), + }, + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + const unavailableResolution = yield* Effect.exit( unavailableRuntime.resolveActionCommit({ invocationId, principal, }), - ), - ); - const committedResolution = await runEffectTestPromise( - Effect.exit( + ); + const committedResolution = yield* Effect.exit( resolvingRuntime.resolveActionCommit({ invocationId, principal, }), - ), - ); - const resolved = await runEffectTestPromise( - Effect.exit( + ); + const resolved = yield* Effect.exit( resolvingRuntime.runAction({ payload: { value: 'committed-with-lost-ack' }, principal, registration: actionRegistration, transport: transport(key, moduleStateKey), }), - ), - ); - const states = await runEffectTestPromise( - database.executor + ); + const states = yield* database.executor .select() .from(tenantModuleStates) - .where(eq(tenantModuleStates.moduleKey, moduleStateKey)), - ); - - assert.equal(failureTag(committedResolution), 'ActionAlreadyCommitted'); - assert.equal(failureTag(unauthorizedResolution), 'ActionInvocationNotFound'); - assert.equal(failureTag(unavailableResolution), 'ActionCommitIndeterminate'); - assert.equal(failureTag(resolved), 'ActionAlreadyCommitted'); - assert.equal(invocations[0]?.status, 'succeeded'); - assert.equal(states.length, 1); - - const openKey = 'lost-acknowledgement-open'; - const openModuleStateKey = `test.lost-ack-open.${tenantId}`; - const openRegistration = makeRegistration({ - actionKey: 'shell.test.lost-ack-open', - moduleStateKey: openModuleStateKey, - }); - const uncertainRollbackTransaction = { - transaction: (transactionBody) => - database.executor - .transaction((transaction) => - transactionBody(transaction).pipe( - Effect.andThen(Effect.die(new Error('force rollback after the transaction body'))), - ), - ) - .pipe(Effect.catchCause(() => Effect.die(acknowledgementLost))), - } satisfies Pick; - const uncertainOpenRuntime = makeActionRuntime( - withTransactionOverride(database, uncertainRollbackTransaction), - repository, - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - const openFirst = await runEffectTestPromise( - Effect.exit( + .where(eq(tenantModuleStates.moduleKey, moduleStateKey)); + + expect(hasFailure(committedResolution, 'ActionAlreadyCommitted')).toBe(true); + expect(hasFailure(unauthorizedResolution, 'ActionInvocationNotFound')).toBe(true); + expect(hasFailure(unavailableResolution, 'ActionCommitIndeterminate')).toBe(true); + expect(hasFailure(resolved, 'ActionAlreadyCommitted')).toBe(true); + expect(invocations[0]?.status).toBe('succeeded'); + expect(states.length).toBe(1); + + const openKey = 'lost-acknowledgement-open'; + const openModuleStateKey = `test.lost-ack-open.${tenantId}`; + const openRegistration = makeRegistration({ + actionKey: 'shell.test.lost-ack-open', + moduleStateKey: openModuleStateKey, + }); + const uncertainRollbackTransaction = { + transaction: (transactionBody) => + database.executor + .transaction((transaction) => + transactionBody(transaction).pipe( + Effect.andThen(Effect.die(new Error('force rollback after the transaction body'))), + ), + ) + .pipe(Effect.catchCause(() => Effect.die(acknowledgementLost))), + } satisfies Pick; + + const uncertainOpenRuntime = makeActionRuntime( + withTransactionOverride(database, uncertainRollbackTransaction), + repository, + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + const openFirst = yield* Effect.exit( uncertainOpenRuntime.runAction({ payload: { value: 'rolled-back-with-lost-ack' }, principal, registration: openRegistration, transport: transport(openKey, openModuleStateKey), }), - ), - ); - assert.equal(failureTag(openFirst), 'ActionCommitIndeterminate'); + ); + expect(hasFailure(openFirst, 'ActionCommitIndeterminate')).toBe(true); - const openInvocations = await runEffectTestPromise( - database.executor + const openInvocations = yield* database.executor .select() .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, openKey)), - ); - const openInvocationId = openInvocations[0]?.actionInvocationId; - assert.notEqual(openInvocationId, undefined); - const openResolution = await runEffectTestPromise( - resolvingRuntime.resolveActionCommit({ + .where(eq(actionInvocations.idempotencyKey, openKey)); + const openInvocationId = openInvocations[0]?.actionInvocationId; + expect(openInvocationId).not.toBe(undefined); + const openResolution = yield* resolvingRuntime.resolveActionCommit({ invocationId: openInvocationId, principal, - }), - ); - const openResolved = await runEffectTestPromise( - resolvingRuntime.runAction({ + }); + const openResolved = yield* resolvingRuntime.runAction({ payload: { value: 'rolled-back-with-lost-ack' }, principal, registration: openRegistration, transport: transport(openKey, openModuleStateKey), - }), - ); - const openStates = await runEffectTestPromise( - database.executor + }); + const openStates = yield* database.executor .select() .from(tenantModuleStates) - .where(eq(tenantModuleStates.moduleKey, openModuleStateKey)), - ); + .where(eq(tenantModuleStates.moduleKey, openModuleStateKey)); - assert.equal(openResolution._tag, 'ActionCommitOpen'); - assert.equal(openResolved.value, 'rolled-back-with-lost-ack'); - assert.equal(openStates.length, 1); - }); -}); + expect(Predicate.isTagged(openResolution, 'ActionCommitOpen')).toBe(true); + expect(openResolved.value).toBe('rolled-back-with-lost-ack'); + expect(openStates.length).toBe(1); + }), + ); -void test('persists no invocation or evidence for every non-writable business module state', async () => { - await databasePromise(async (database) => { - const moduleKey = 'inventory.state-matrix'; - await runEffectTestPromise( - database.executor +const testProgram13 = () => + withDatabase( + Effect.fn(function* integrationProgram22(database) { + const moduleKey = 'inventory.state-matrix'; + yield* database.executor .insert(tenantModuleStates) .values({ moduleKey, state: 'active', tenantId }) - .onConflictDoNothing(), - ); - let handlerExecutions = 0; - const action = defineAction( - { - accessEvidencePolicy: { - captureMode: 'metadata_only', - policyKey: 'inventory.state-matrix.write.v1', + .onConflictDoNothing(); + let handlerExecutions = 0; + const action = defineAction( + { + accessEvidencePolicy: { + captureMode: 'metadata_only', + policyKey: 'inventory.state-matrix.write.v1', + }, + actionKey: 'inventory.state-matrix.write', + auditProfile: 'standard', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineTenantModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'inventory.state-matrix.write', + moduleKey, + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: moduleKey, + payloadSchema: Schema.Void, + policies: [], + resultSchema: Schema.Void, + schemaVersion: '1', }, - actionKey: 'inventory.state-matrix.write', - auditProfile: 'standard', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineTenantModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'inventory.state-matrix.write', - moduleKey, - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: moduleKey, - payloadSchema: Schema.Void, - policies: [], - resultSchema: Schema.Void, - schemaVersion: '1', - }, - () => - Effect.sync(() => { - handlerExecutions += 1; - }), - ); - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - liveModuleStateOptions(database), - ); - await runEffectTestPromise( - runtime.runAction({ + () => + Effect.sync(() => { + handlerExecutions += 1; + }), + ); + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + liveModuleStateOptions(database), + ); + yield* runtime.runAction({ payload: undefined, principal, registration: action, transport: transport('module-state-active'), - }), - ); - assert.equal(handlerExecutions, 1); - - const deniedStates = [ - 'inactive', - 'read_only', - 'suspended', - 'quarantined', - 'deprecated', - 'archived', - ] as const; - const verifyDeniedStateAt = async (index: number): Promise => { - const state = deniedStates[index]; - if (state === undefined) { - return; - } - await runEffectTestPromise( - database.executor - .update(tenantModuleStates) - .set({ state }) - .where( - and( - eq(tenantModuleStates.tenantId, tenantId), - eq(tenantModuleStates.moduleKey, moduleKey), - ), - ), - ); - const idempotencyKey = `module-state-denied-${index}`; - const exit = await runEffectTestPromise( - Effect.exit( - runtime.runAction({ - payload: undefined, - principal, - registration: action, - transport: transport(idempotencyKey), - }), - ), - ); - assert.equal(failureTag(exit), 'ModuleStateDeniedError', state); - const invocations = await runEffectTestPromise( - database.executor - .select() - .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, idempotencyKey)), + }); + expect(handlerExecutions).toBe(1); + + const deniedStates = [ + 'inactive', + 'read_only', + 'suspended', + 'quarantined', + 'deprecated', + 'archived', + ] as const; + yield* Effect.forEach( + deniedStates, + Effect.fn(function* integrationProgram23(state, index) { + yield* database.executor + .update(tenantModuleStates) + .set({ state }) + .where( + and( + eq(tenantModuleStates.tenantId, tenantId), + eq(tenantModuleStates.moduleKey, moduleKey), + ), + ); + const idempotencyKey = `module-state-denied-${index}`; + const exit = yield* Effect.exit( + runtime.runAction({ + payload: undefined, + principal, + registration: action, + transport: transport(idempotencyKey), + }), + ); + expect(hasFailure(exit, 'ModuleStateDeniedError'), state).toBe(true); + const invocations = yield* database.executor + .select() + .from(actionInvocations) + .where(eq(actionInvocations.idempotencyKey, idempotencyKey)); + expect(invocations.length, state).toBe(0); + }), + { discard: true }, ); - assert.equal(invocations.length, 0, state); - await verifyDeniedStateAt(index + 1); - }; - await verifyDeniedStateAt(0); - await runEffectTestPromise( - database.executor + yield* database.executor .delete(tenantModuleStates) .where( and( eq(tenantModuleStates.tenantId, tenantId), eq(tenantModuleStates.moduleKey, moduleKey), ), - ), - ); - const missingExit = await runEffectTestPromise( - Effect.exit( + ); + const missingExit = yield* Effect.exit( runtime.runAction({ payload: undefined, principal, registration: action, transport: transport('module-state-missing'), }), - ), - ); - assert.equal(failureTag(missingExit), 'ModuleStateDeniedError'); - assert.equal(handlerExecutions, 1); - }); + ); + expect(hasFailure(missingExit, 'ModuleStateDeniedError')).toBe(true); + expect(handlerExecutions).toBe(1); + }), + ); + +it.layer(Layer.effectDiscard(Effect.acquireRelease(prepare, () => cleanup.pipe(Effect.orDie))), { + excludeTestServices: true, +})('Action runtime', (suite) => { + suite.effect( + 'rechecks business module state under the tenant lock and retries after Core recovery', + testProgram1, + ); + + suite.effect( + 'atomically commits business state, all success evidence, and the succeeded marker', + testProgram2, + ); + + suite.effect( + 'commits allowed Policy checkpoints atomically before handler success evidence', + testProgram3, + ); + + suite.effect( + 'atomically rejects denied global and same-owner MicroVertical Policies without handler evidence', + testProgram4, + ); + + suite.effect('rolls back every denied-Policy finalization persistence failure', testProgram5); + + suite.effect( + 'rolls back domain rejection, evidence persistence failure, and orphan outbox attempts', + testProgram6, + ); + + suite.effect('rolls back every individual success-evidence persistence failure', testProgram7); + + suite.effect( + 'keeps Policy rejection terminal and deduplicates repeated and concurrent evidence', + testProgram8, + ); + + suite.effect( + 'never lets a losing Policy denial replace a running or successful invocation', + testProgram9, + ); + + suite.effect( + 'serializes concurrent requests and enforces committed, open-retry, and hash-conflict behavior', + testProgram10, + ); + + suite.effect('serializes Domain Event allocation by tenant commit order', testProgram11); + + suite.effect( + 'resolves a lost commit acknowledgement from the durable succeeded marker', + testProgram12, + ); + + suite.effect( + 'persists no invocation or evidence for every non-writable business module state', + testProgram13, + ); }); diff --git a/app/packages/core-runtime/tests/integration/contacts-identity-migration.test.ts b/app/packages/core-runtime/tests/integration/contacts-identity-migration.test.ts index a211ee649..96a39b8f7 100644 --- a/app/packages/core-runtime/tests/integration/contacts-identity-migration.test.ts +++ b/app/packages/core-runtime/tests/integration/contacts-identity-migration.test.ts @@ -1,7 +1,6 @@ +import { expect, it } from 'effect-rstest'; import { NodeServices } from '@effect/platform-node'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Crypto, Effect, FileSystem, flow, ManagedRuntime, Schema } from 'effect'; +import { Crypto, Effect, FileSystem, Schema } from 'effect'; import { Pool } from 'pg'; import { loadDatabaseConnectionPair } from '../../src/db/config.ts'; @@ -54,19 +53,6 @@ const tableColumns = { type MigrationColumn = (typeof tableColumns)[keyof typeof tableColumns][number]; -const integrationRuntime = ManagedRuntime.make(NodeServices.layer); - -const effectTest = ( - name: string, - effect: Effect.Effect, -): void => { - test( - name, - flow(() => Effect.asVoid(effect), integrationRuntime.runPromise), - ); -}; - -const databaseEffect = (operation: PromiseLike) => Effect.tryPromise(() => operation); const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); const columnDefinitions = (columns: readonly MigrationColumn[]): string => @@ -78,7 +64,7 @@ const loadTableResult = ( table: string, columns: readonly MigrationColumn[], ) => - databaseEffect( + Effect.tryPromise(() => pool.query(`select * from ${quotedSchema}."${table}" order by record_id`), ).pipe(Effect.map((result) => ({ columns, result, table }))); @@ -94,13 +80,13 @@ const contactsIdentityMigrationProgram = Effect.gen(function* contactsIdentityMi const fileSystem = yield* FileSystem.FileSystem; const pool = yield* Effect.acquireRelease( Effect.sync(() => new Pool({ connectionString: configuration.admin.connectionString, max: 1 })), - (resource) => databaseEffect(resource.end()).pipe(Effect.orDie), + (resource) => Effect.tryPromise(() => resource.end()).pipe(Effect.orDie), ); const schema = `core_contacts_identity_${(yield* crypto.randomUUIDv4).replaceAll('-', '')}`; const quotedSchema = `"${schema}"`; yield* Effect.gen(function* exerciseContactsIdentityMigration() { - yield* databaseEffect(pool.query(`create schema ${quotedSchema}`)); - yield* databaseEffect( + yield* Effect.tryPromise(() => pool.query(`create schema ${quotedSchema}`)); + yield* Effect.tryPromise(() => pool.query( `create table ${quotedSchema}.tenant_module_states ( record_id text primary key, @@ -113,7 +99,7 @@ const contactsIdentityMigrationProgram = Effect.gen(function* contactsIdentityMi ), ); yield* runSequentially(Object.entries(tableColumns), ([table, columns]) => - databaseEffect( + Effect.tryPromise(() => pool.query( `create table ${quotedSchema}."${table}" ( record_id text primary key, @@ -126,7 +112,7 @@ const contactsIdentityMigrationProgram = Effect.gen(function* contactsIdentityMi const recordedAt = '2026-01-02T03:04:05.678Z'; const payload = { freeText: 'crm.core must remain untouched inside arbitrary JSON' }; const encodedPayload = encodeJson(payload); - yield* databaseEffect( + yield* Effect.tryPromise(() => pool.query( `insert into ${quotedSchema}.tenant_module_states (record_id, tenant_id, module_key, payload, recorded_at) @@ -152,7 +138,7 @@ const contactsIdentityMigrationProgram = Effect.gen(function* contactsIdentityMi const unrelatedPlaceholders = names .map((_, index) => `$${index + names.length + 1}`) .join(', '); - return databaseEffect( + return Effect.tryPromise(() => pool.query( `insert into ${quotedSchema}."${table}" (${quotedNames}) values (${placeholders}), (${unrelatedPlaceholders})`, @@ -180,10 +166,10 @@ const contactsIdentityMigrationProgram = Effect.gen(function* contactsIdentityMi .map((statement) => statement.trim()) .filter((statement) => statement.length > 0); yield* runSequentially([...statements, ...statements], (statement) => - databaseEffect(pool.query(statement)), + Effect.tryPromise(() => pool.query(statement)), ); - const stateResult = yield* databaseEffect( + const stateResult = yield* Effect.tryPromise(() => pool.query<{ module_key: string; payload: typeof payload; @@ -194,15 +180,14 @@ const contactsIdentityMigrationProgram = Effect.gen(function* contactsIdentityMi from ${quotedSchema}.tenant_module_states order by record_id`, ), ); - assert.deepEqual( + expect( stateResult.rows.map(({ module_key, record_id }) => ({ module_key, record_id })), - [ - { module_key: contactsModule, record_id: 'legacy-state' }, - { module_key: 'commerce.core', record_id: 'unrelated-state' }, - ], - ); - assert.deepEqual(stateResult.rows[0]?.payload, payload); - assert.equal(stateResult.rows[0]?.recorded_at.toISOString(), recordedAt); + ).toEqual([ + { module_key: contactsModule, record_id: 'legacy-state' }, + { module_key: 'commerce.core', record_id: 'unrelated-state' }, + ]); + expect(stateResult.rows[0]?.payload).toEqual(payload); + expect(stateResult.rows[0]?.recorded_at.toISOString()).toBe(recordedAt); const tableResults = yield* Effect.forEach( Object.entries(tableColumns), ([table, columns]) => loadTableResult(pool, quotedSchema, table, columns), @@ -210,21 +195,25 @@ const contactsIdentityMigrationProgram = Effect.gen(function* contactsIdentityMi ); for (const { columns, result, table } of tableResults) { const [migrated, unrelated] = result.rows; - assert.ok(migrated); - assert.ok(unrelated); + expect(migrated).toBeDefined(); + if (migrated === undefined) { + throw new Error('Expected migrated'); + } + expect(unrelated).toBeDefined(); + if (unrelated === undefined) { + throw new Error('Expected unrelated'); + } for (const column of columns) { - assert.match( - String(migrated[column]), + expect(String(migrated[column]), `${table}.${column} was not migrated`).toMatch( /^contacts\.core(?:\.|$)/u, - `${table}.${column} was not migrated`, ); - assert.equal(unrelated[column], 'commerce.core.record'); + expect(unrelated[column]).toBe('commerce.core.record'); } - assert.deepEqual(migrated.payload, payload); + expect(migrated.payload).toEqual(payload); } - yield* databaseEffect(pool.query(`truncate ${quotedSchema}.tenant_module_states`)); - yield* databaseEffect( + yield* Effect.tryPromise(() => pool.query(`truncate ${quotedSchema}.tenant_module_states`)); + yield* Effect.tryPromise(() => pool.query( `insert into ${quotedSchema}.tenant_module_states (record_id, tenant_id, module_key, payload, recorded_at) @@ -233,26 +222,31 @@ const contactsIdentityMigrationProgram = Effect.gen(function* contactsIdentityMi [legacyModule, contactsModule], ), ); - yield* databaseEffect(assert.rejects(pool.query(statements[0] ?? ''), /would collide/u)); - const collisionRows = yield* databaseEffect( + const collisionError = yield* Effect.flip( + Effect.tryPromise(() => pool.query(statements[0] ?? '')), + ); + expect(String(collisionError.cause)).toMatch(/would collide/u); + const collisionRows = yield* Effect.tryPromise(() => pool.query<{ module_key: string }>( `select module_key from ${quotedSchema}.tenant_module_states order by module_key`, ), ); - assert.deepEqual( - collisionRows.rows.map((row) => row.module_key), - [contactsModule, legacyModule], - ); + expect(collisionRows.rows.map((row) => row.module_key)).toEqual([contactsModule, legacyModule]); }).pipe( Effect.ensuring( - Effect.suspend(() => - databaseEffect(pool.query(`drop schema if exists ${quotedSchema} cascade`)), - ).pipe(Effect.orDie), + Effect.tryPromise(() => pool.query(`drop schema if exists ${quotedSchema} cascade`)).pipe( + Effect.orDie, + ), ), ); }).pipe(Effect.scoped); -effectTest( - 'Contacts Core identity migration is preserving, scoped, rerunnable, and collision-safe', - contactsIdentityMigrationProgram, +it.layer(NodeServices.layer, { excludeTestServices: true })( + 'contacts-identity-migration', + (suite) => { + suite.effect( + 'Contacts Core identity migration is preserving, scoped, rerunnable, and collision-safe', + () => contactsIdentityMigrationProgram, + ); + }, ); diff --git a/app/packages/core-runtime/tests/integration/context-access.test.ts b/app/packages/core-runtime/tests/integration/context-access.test.ts index af8f25f5a..615a04565 100644 --- a/app/packages/core-runtime/tests/integration/context-access.test.ts +++ b/app/packages/core-runtime/tests/integration/context-access.test.ts @@ -1,8 +1,7 @@ +import { expect, it } from 'effect-rstest'; import { NodeServices } from '@effect/platform-node'; -import assert from 'node:assert/strict'; -import test from 'node:test'; import { v1 } from '@authzed/authzed-node'; -import { Crypto, Effect, FileSystem, flow, ManagedRuntime } from 'effect'; +import { Crypto, Effect, FileSystem } from 'effect'; import { makeContextAccess, toLegalEntityAccessObjectId, @@ -15,18 +14,6 @@ import { } from '../../src/permissions/client.ts'; import { loadSpiceDbConfig } from '../../src/permissions/config.ts'; -const integrationRuntime = ManagedRuntime.make(NodeServices.layer); - -const effectTest = ( - name: string, - effect: Effect.Effect, -): void => { - test( - name, - flow(() => Effect.asVoid(effect), integrationRuntime.runPromise), - ); -}; - const spiceDbEffect = (operation: PromiseLike) => Effect.tryPromise(() => operation); const relationship = ( @@ -70,7 +57,7 @@ const contextAccessProgram = Effect.gen(function* contextAccessIntegration() { moduleObjectId === undefined || resourceObjectId === undefined ) { - assert.fail('Expected valid SpiceDB object identifiers'); + throw new Error('Expected valid SpiceDB object identifiers'); } const client = v1.NewClient( configuration.preSharedKey, @@ -85,7 +72,7 @@ const contextAccessProgram = Effect.gen(function* contextAccessIntegration() { const bootstrapLines = bootstrap.split('\n'); const schemaStart = bootstrapLines.indexOf('schema: |-') + 1; const schemaEnd = bootstrapLines.indexOf('relationships: |-'); - assert.ok(schemaStart > 0 && schemaEnd > schemaStart); + expect(schemaStart > 0 && schemaEnd > schemaStart).toBeTruthy(); const schemaBlock = bootstrapLines .slice(schemaStart, schemaEnd) .map((line) => line.replace(/^ {2}/u, '')) @@ -151,7 +138,7 @@ const contextAccessProgram = Effect.gen(function* contextAccessIntegration() { { concurrency: 'unbounded' }, ); for (const decisions of tenantDecisions) { - assert.deepEqual(decisions, [ + expect(decisions).toEqual([ { decision: 'allowed', key: tenantId }, { decision: 'denied', key: otherTenantId }, ]); @@ -168,28 +155,25 @@ const contextAccessProgram = Effect.gen(function* contextAccessIntegration() { { concurrency: 'unbounded' }, ); for (const decisions of legalEntityDecisions) { - assert.deepEqual(decisions, [ + expect(decisions).toEqual([ { decision: 'allowed', key: legalEntityId }, { decision: 'denied', key: otherLegalEntityId }, ]); } - assert.deepEqual( + expect( yield* access.modules({ legalEntityId, moduleIds: [moduleId], principalId, tenantId }), - [{ decision: 'allowed', key: moduleId }], - ); - assert.deepEqual( + ).toEqual([{ decision: 'allowed', key: moduleId }]); + expect( yield* access.modules({ legalEntityId, moduleIds: [moduleId], principalId, tenantId: otherTenantId, }), - [{ decision: 'denied', key: moduleId }], - ); - assert.deepEqual( + ).toEqual([{ decision: 'denied', key: moduleId }]); + expect( yield* access.resources({ legalEntityId, principalId, resources: [resource], tenantId }), - [{ decision: 'allowed', key: `${moduleId}:property.unit:${resource.resourceId}` }], - ); + ).toEqual([{ decision: 'allowed', key: `${moduleId}:property.unit:${resource.resourceId}` }]); }).pipe(Effect.ensuring(Effect.sync(() => permissionClient.close()))); }).pipe( Effect.ensuring( @@ -217,7 +201,9 @@ const contextAccessProgram = Effect.gen(function* contextAccessIntegration() { ); }); -effectTest( - 'isolates live legal-entity, module, and resource batches by tenant and entity', - contextAccessProgram, -); +it.layer(NodeServices.layer, { excludeTestServices: true })('context-access', (suite) => { + suite.effect( + 'isolates live legal-entity, module, and resource batches by tenant and entity', + () => contextAccessProgram, + ); +}); diff --git a/app/packages/core-runtime/tests/integration/identity-runtime.test.ts b/app/packages/core-runtime/tests/integration/identity-runtime.test.ts index e00e59de8..2c118321d 100644 --- a/app/packages/core-runtime/tests/integration/identity-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/identity-runtime.test.ts @@ -1,21 +1,9 @@ -import { - makeEffectTestCallback as nativeTestCallback, - makeEffectTestCallback, -} from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from 'effect-rstest'; import { v1 } from '@authzed/authzed-node'; import { and, eq, inArray } from 'drizzle-orm'; -import { - DateTime, - Effect, - Exit as NativeExit, - Scope as NativeScope, - Option, - Predicate, -} from 'effect'; -import assert from 'node:assert/strict'; +import { DateTime, Effect, Option, Predicate } from 'effect'; import { randomUUID } from 'node:crypto'; -import test, { after as afterNativeDatabase } from 'node:test'; import { Pool } from 'pg'; import { makeActionRepository } from '../../src/actions/repository.ts'; import { makeActionRuntime } from '../../src/actions/runtime.ts'; @@ -63,14 +51,8 @@ import { import { makeReadRuntime } from '../../src/reads/runtime.ts'; import { openActionRuntimeOptions } from '../support/action-runtime-options.ts'; import { makeTestDatabaseFromPool } from '../support/database.ts'; -import { runEffectTestSync as runNativeSync } from '../support/effect-runtime.ts'; import { openModuleEntrypointGateway } from '../support/open-module-entrypoint-gateway.ts'; -const nativeDatabaseScope = runNativeSync(NativeScope.make()); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), -); - const withOptionalProperty = < Base extends object, Key extends PropertyKey, @@ -99,583 +81,567 @@ const relationship = ( }), }); -const promiseEffect = (operation: () => PromiseLike) => - Effect.promise(() => operation()); -const effectTest = (name: string, effect: Effect.Effect): void => { - test(name, makeEffectTestCallback(effect)); -}; - -effectTest( +it.live( 'runs identity mutations and tenant-isolated administration through live Action and Read runtimes', - Effect.gen(function* identityRuntimeIntegration() { - const connections = yield* loadDatabaseConnectionPair(); - const spiceDbConfiguration = yield* loadSpiceDbConfig(); - const adminPool = new Pool({ connectionString: connections.admin.connectionString }); - const runtimePool = new Pool({ connectionString: connections.runtime.connectionString }); - const admin = yield* makeTestDatabaseFromPool(adminPool, coreRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ); - const runtimeDatabase = yield* makeTestDatabaseFromPool(runtimePool, coreRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ); - const principalManagementRepository = - principalManagementRepositoryFromTransaction(runtimeDatabase); - const runIdentityAction = ( - action: Effect.Effect, - ) => - action.pipe( - Effect.provideService(PrincipalManagementRepository, principalManagementRepository), - ); - const tenantId = randomUUID(); - const foreignTenantId = randomUUID(); - const administratorPrincipalId = randomUUID(); - const administratorAuthBindingId = randomUUID(); - const foreignPrincipalId = randomUUID(); - const supportTargetPrincipalId = randomUUID(); - const supportTargetAuthBindingId = randomUUID(); - const systemPrincipalId = randomUUID(); - const providerUserId = `identity-runtime-user-${randomUUID()}`; - const providerKeyId = `identity-runtime-key-${randomUUID()}`; - const selfProviderKeyId = `identity-runtime-self-key-${randomUUID()}`; - const supportTargetUserId = `identity-runtime-target-${randomUUID()}`; - const spiceDbClient = v1.NewClient( - spiceDbConfiguration.preSharedKey, - spiceDbConfiguration.endpoint, - spiceDbConfiguration.insecureLocal - ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED - : v1.ClientSecurity.SECURE, - ); - const permissionClient = createSpiceDbPermissionClient( - spiceDbConfiguration, - SPICEDB_CHECK_TIMEOUT_MS, - ); - const contextAccess = makeContextAccess(permissionClient); - const actionPermission = makeActionPermissionService(permissionClient); - const operationalScope = makeOperationalScopeResolver( - makeOperationalScopeRepository({ executor: runtimeDatabase }), - contextAccess, - ); - const actionRuntime = makeActionRuntime( - { executor: runtimeDatabase }, - makeActionRepository(), - actionPermission, - operationalScope, - { ...openActionRuntimeOptions, contextAccess }, - ); - const readRuntime = makeReadRuntime( - { executor: runtimeDatabase }, - openModuleEntrypointGateway, - operationalScope, - contextAccess, - ); - const principal = { - authBindingId: administratorAuthBindingId, - authContextRef: `better-auth-session:${randomUUID()}`, - authMethod: 'session' as const, - principalId: administratorPrincipalId, - tenantId, - }; - const identityActionKeys = [ - 'core.identity.bind-managed-api-key', - 'core.identity.bind-self-api-key', - 'core.identity.change-principal-status', - 'core.identity.create-non-human-principal', - 'core.identity.record-support-impersonation', - 'core.identity.set-managed-api-key-binding-status', - 'core.identity.set-self-api-key-binding-status', - ] as const; - const spiceDbRelationships = [ - relationship('tenant', tenantId, 'member', 'principal', administratorPrincipalId), - relationship('tenant', tenantId, 'identity_admin', 'principal', administratorPrincipalId), - relationship('tenant', tenantId, 'support', 'principal', administratorPrincipalId), - ...identityActionKeys.flatMap((actionKey) => { - const objectId = toSpiceDbActionObjectId(actionKey); - return [ - relationship('action', objectId, 'executor', 'principal', administratorPrincipalId), - relationship('action', objectId, 'executor', 'principal', systemPrincipalId), - ]; - }), - ]; - const cleanup = Effect.gen(function* cleanIdentityRuntimeFixtures() { - yield* admin.delete(dataAccessEvents).where(inArray(dataAccessEvents.tenantId, [tenantId])); - yield* admin.delete(auditEvents).where(inArray(auditEvents.tenantId, [tenantId])); - yield* admin.delete(actionInvocations).where(inArray(actionInvocations.tenantId, [tenantId])); - yield* admin - .delete(principalAuthBindings) - .where(inArray(principalAuthBindings.tenantId, [tenantId, foreignTenantId])); - yield* admin - .delete(principals) - .where(inArray(principals.tenantId, [tenantId, foreignTenantId])); - yield* admin.delete(tenants).where(inArray(tenants.tenantId, [tenantId, foreignTenantId])); - }); - - const exercise = Effect.gen(function* exerciseIdentityRuntime() { - const initialRelationshipsRequest = v1.WriteRelationshipsRequest.create({ - updates: spiceDbRelationships.map((item) => - v1.RelationshipUpdate.create({ - operation: v1.RelationshipUpdate_Operation.TOUCH, - relationship: item, - }), - ), - }); - yield* promiseEffect( - spiceDbClient.promises.writeRelationships.bind( - spiceDbClient.promises, - initialRelationshipsRequest, - ), + () => + Effect.gen(function* identityRuntimeIntegration() { + const connections = yield* loadDatabaseConnectionPair(); + const spiceDbConfiguration = yield* loadSpiceDbConfig(); + const adminPool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), + (pool) => Effect.promise(() => pool.end()).pipe(Effect.orDie), ); - yield* admin.insert(tenants).values([ - { - defaultLocale: 'en', - name: 'Identity runtime tenant', - slug: `identity-runtime-${tenantId}`, - status: 'active', - tenantId, - }, - { - defaultLocale: 'en', - name: 'Foreign identity runtime tenant', - slug: `identity-runtime-${foreignTenantId}`, - status: 'active', - tenantId: foreignTenantId, - }, - ]); - yield* admin.insert(principals).values([ - { - displayName: 'Identity administrator', - kind: 'human', - principalId: administratorPrincipalId, - status: 'active', - tenantId, - }, - { - displayName: 'Foreign managed service', - kind: 'service', - principalId: foreignPrincipalId, - status: 'active', - tenantId: foreignTenantId, - }, - { - displayName: 'Support target', - kind: 'human', - principalId: supportTargetPrincipalId, - status: 'active', - tenantId, - }, - { - displayName: 'Identity runtime system', - kind: 'system', - principalId: systemPrincipalId, - status: 'active', - tenantId, - }, - ]); - yield* admin.insert(principalAuthBindings).values([ - { - principalAuthBindingId: administratorAuthBindingId, - principalId: administratorPrincipalId, - provider: 'better_auth', - providerSubjectId: providerUserId, - status: 'active', - subjectType: 'user', - tenantId, - }, - { - principalAuthBindingId: supportTargetAuthBindingId, - principalId: supportTargetPrincipalId, - provider: 'better_auth', - providerSubjectId: supportTargetUserId, - status: 'active', - subjectType: 'user', - tenantId, - }, - ]); - - const created = yield* runIdentityAction( - actionRuntime.runAction({ - payload: { displayName: 'Managed runtime service', kind: 'service' }, - principal, - registration: createNonHumanPrincipalAction, - transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, - }), + const runtimePool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.runtime.connectionString })), + (pool) => Effect.promise(() => pool.end()).pipe(Effect.orDie), ); - const binding = yield* runIdentityAction( - actionRuntime.runAction({ - payload: { principalId: created.principalId, providerSubjectId: providerKeyId }, - principal, - registration: bindManagedApiKeyAction, - transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, - }), + const admin = yield* makeTestDatabaseFromPool(adminPool, coreRelations); + const runtimeDatabase = yield* makeTestDatabaseFromPool(runtimePool, coreRelations); + const principalManagementRepository = + principalManagementRepositoryFromTransaction(runtimeDatabase); + const runIdentityAction = ( + action: Effect.Effect, + ) => + action.pipe( + Effect.provideService(PrincipalManagementRepository, principalManagementRepository), + ); + const tenantId = randomUUID(); + const foreignTenantId = randomUUID(); + const administratorPrincipalId = randomUUID(); + const administratorAuthBindingId = randomUUID(); + const foreignPrincipalId = randomUUID(); + const supportTargetPrincipalId = randomUUID(); + const supportTargetAuthBindingId = randomUUID(); + const systemPrincipalId = randomUUID(); + const providerUserId = `identity-runtime-user-${randomUUID()}`; + const providerKeyId = `identity-runtime-key-${randomUUID()}`; + const selfProviderKeyId = `identity-runtime-self-key-${randomUUID()}`; + const supportTargetUserId = `identity-runtime-target-${randomUUID()}`; + const spiceDbClient = v1.NewClient( + spiceDbConfiguration.preSharedKey, + spiceDbConfiguration.endpoint, + spiceDbConfiguration.insecureLocal + ? v1.ClientSecurity.INSECURE_LOCALHOST_ALLOWED + : v1.ClientSecurity.SECURE, ); - yield* runIdentityAction( - actionRuntime.runAction({ - payload: { - authBindingId: binding.authBindingId, - expectedStatus: 'active', - newStatus: 'disabled', - principalId: created.principalId, - }, - principal, - registration: setManagedApiKeyBindingStatusAction, - transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, - }), + const permissionClient = createSpiceDbPermissionClient( + spiceDbConfiguration, + SPICEDB_CHECK_TIMEOUT_MS, ); - yield* runIdentityAction( - actionRuntime.runAction({ - payload: { - authBindingId: binding.authBindingId, - expectedStatus: 'disabled', - newStatus: 'active', - principalId: created.principalId, - }, - principal, - registration: setManagedApiKeyBindingStatusAction, - transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, - }), + const contextAccess = makeContextAccess(permissionClient); + const actionPermission = makeActionPermissionService(permissionClient); + const operationalScope = makeOperationalScopeResolver( + makeOperationalScopeRepository({ executor: runtimeDatabase }), + contextAccess, ); - yield* runIdentityAction( - actionRuntime.runAction({ - payload: { - expectedStatus: 'active', - newStatus: 'disabled', - principalId: created.principalId, - reason: 'Exercise disabled managed-principal state', - }, - principal, - registration: changePrincipalStatusAction, - transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, - }), + const actionRuntime = makeActionRuntime( + { executor: runtimeDatabase }, + makeActionRepository(), + actionPermission, + operationalScope, + { ...openActionRuntimeOptions, contextAccess }, ); - yield* runIdentityAction( - actionRuntime.runAction({ - payload: { - expectedStatus: 'disabled', - newStatus: 'active', - principalId: created.principalId, - }, - principal, - registration: changePrincipalStatusAction, - transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, - }), + const readRuntime = makeReadRuntime( + { executor: runtimeDatabase }, + openModuleEntrypointGateway, + operationalScope, + contextAccess, ); - const selfBinding = yield* runIdentityAction( - actionRuntime.runAction({ - payload: { providerSubjectId: selfProviderKeyId }, - principal, - registration: bindSelfApiKeyAction, - transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, + const principal = { + authBindingId: administratorAuthBindingId, + authContextRef: `better-auth-session:${randomUUID()}`, + authMethod: 'session' as const, + principalId: administratorPrincipalId, + tenantId, + }; + const identityActionKeys = [ + 'core.identity.bind-managed-api-key', + 'core.identity.bind-self-api-key', + 'core.identity.change-principal-status', + 'core.identity.create-non-human-principal', + 'core.identity.record-support-impersonation', + 'core.identity.set-managed-api-key-binding-status', + 'core.identity.set-self-api-key-binding-status', + ] as const; + const spiceDbRelationships = [ + relationship('tenant', tenantId, 'member', 'principal', administratorPrincipalId), + relationship('tenant', tenantId, 'identity_admin', 'principal', administratorPrincipalId), + relationship('tenant', tenantId, 'support', 'principal', administratorPrincipalId), + ...identityActionKeys.flatMap((actionKey) => { + const objectId = toSpiceDbActionObjectId(actionKey); + return [ + relationship('action', objectId, 'executor', 'principal', administratorPrincipalId), + relationship('action', objectId, 'executor', 'principal', systemPrincipalId), + ]; }), - ); - yield* runIdentityAction( - actionRuntime.runAction({ - payload: { - authBindingId: selfBinding.authBindingId, - expectedStatus: 'active', - newStatus: 'disabled', + ]; + const cleanup = Effect.gen(function* cleanIdentityRuntimeFixtures() { + yield* admin.delete(dataAccessEvents).where(inArray(dataAccessEvents.tenantId, [tenantId])); + yield* admin.delete(auditEvents).where(inArray(auditEvents.tenantId, [tenantId])); + yield* admin + .delete(actionInvocations) + .where(inArray(actionInvocations.tenantId, [tenantId])); + yield* admin + .delete(principalAuthBindings) + .where(inArray(principalAuthBindings.tenantId, [tenantId, foreignTenantId])); + yield* admin + .delete(principals) + .where(inArray(principals.tenantId, [tenantId, foreignTenantId])); + yield* admin.delete(tenants).where(inArray(tenants.tenantId, [tenantId, foreignTenantId])); + }); + + const exercise = Effect.gen(function* exerciseIdentityRuntime() { + const initialRelationshipsRequest = v1.WriteRelationshipsRequest.create({ + updates: spiceDbRelationships.map((item) => + v1.RelationshipUpdate.create({ + operation: v1.RelationshipUpdate_Operation.TOUCH, + relationship: item, + }), + ), + }); + yield* Effect.promise(() => + spiceDbClient.promises.writeRelationships(initialRelationshipsRequest), + ); + yield* admin.insert(tenants).values([ + { + defaultLocale: 'en', + name: 'Identity runtime tenant', + slug: `identity-runtime-${tenantId}`, + status: 'active', + tenantId, + }, + { + defaultLocale: 'en', + name: 'Foreign identity runtime tenant', + slug: `identity-runtime-${foreignTenantId}`, + status: 'active', + tenantId: foreignTenantId, + }, + ]); + yield* admin.insert(principals).values([ + { + displayName: 'Identity administrator', + kind: 'human', + principalId: administratorPrincipalId, + status: 'active', + tenantId, + }, + { + displayName: 'Foreign managed service', + kind: 'service', + principalId: foreignPrincipalId, + status: 'active', + tenantId: foreignTenantId, + }, + { + displayName: 'Support target', + kind: 'human', + principalId: supportTargetPrincipalId, + status: 'active', + tenantId, }, + { + displayName: 'Identity runtime system', + kind: 'system', + principalId: systemPrincipalId, + status: 'active', + tenantId, + }, + ]); + yield* admin.insert(principalAuthBindings).values([ + { + principalAuthBindingId: administratorAuthBindingId, + principalId: administratorPrincipalId, + provider: 'better_auth', + providerSubjectId: providerUserId, + status: 'active', + subjectType: 'user', + tenantId, + }, + { + principalAuthBindingId: supportTargetAuthBindingId, + principalId: supportTargetPrincipalId, + provider: 'better_auth', + providerSubjectId: supportTargetUserId, + status: 'active', + subjectType: 'user', + tenantId, + }, + ]); + + const created = yield* runIdentityAction( + actionRuntime.runAction({ + payload: { displayName: 'Managed runtime service', kind: 'service' }, + principal, + registration: createNonHumanPrincipalAction, + transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, + }), + ); + const binding = yield* runIdentityAction( + actionRuntime.runAction({ + payload: { principalId: created.principalId, providerSubjectId: providerKeyId }, + principal, + registration: bindManagedApiKeyAction, + transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, + }), + ); + yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + authBindingId: binding.authBindingId, + expectedStatus: 'active', + newStatus: 'disabled', + principalId: created.principalId, + }, + principal, + registration: setManagedApiKeyBindingStatusAction, + transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, + }), + ); + yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + authBindingId: binding.authBindingId, + expectedStatus: 'disabled', + newStatus: 'active', + principalId: created.principalId, + }, + principal, + registration: setManagedApiKeyBindingStatusAction, + transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, + }), + ); + yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + expectedStatus: 'active', + newStatus: 'disabled', + principalId: created.principalId, + reason: 'Exercise disabled managed-principal state', + }, + principal, + registration: changePrincipalStatusAction, + transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, + }), + ); + yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + expectedStatus: 'disabled', + newStatus: 'active', + principalId: created.principalId, + }, + principal, + registration: changePrincipalStatusAction, + transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, + }), + ); + const selfBinding = yield* runIdentityAction( + actionRuntime.runAction({ + payload: { providerSubjectId: selfProviderKeyId }, + principal, + registration: bindSelfApiKeyAction, + transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, + }), + ); + yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + authBindingId: selfBinding.authBindingId, + expectedStatus: 'active', + newStatus: 'disabled', + }, + principal, + registration: setSelfApiKeyBindingStatusAction, + transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, + }), + ); + yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + authBindingId: selfBinding.authBindingId, + expectedStatus: 'disabled', + newStatus: 'active', + }, + principal, + registration: setSelfApiKeyBindingStatusAction, + transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, + }), + ); + const listed = yield* readRuntime.runRead({ + input: { limit: 100, offset: 0 }, principal, - registration: setSelfApiKeyBindingStatusAction, - transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, - }), - ); - yield* runIdentityAction( - actionRuntime.runAction({ - payload: { + registration: managedPrincipalsRead, + transport: { correlationId: randomUUID() }, + }); + + expect(binding.status).toBe('active'); + expect( + listed.items.map(({ authBindingId, principalId: listedPrincipalId }) => ({ + authBindingId: Option.getOrThrow(authBindingId), + principalId: listedPrincipalId, + })), + ).toEqual([{ authBindingId: binding.authBindingId, principalId: created.principalId }]); + yield* readRuntime.runRead({ + input: { limit: 100, offset: 0 }, + principal: { authBindingId: selfBinding.authBindingId, - expectedStatus: 'disabled', - newStatus: 'active', + authContextRef: `better-auth-api-key:${selfProviderKeyId}`, + authMethod: 'api_key', + principalId: administratorPrincipalId, + tenantId, }, - principal, - registration: setSelfApiKeyBindingStatusAction, - transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, - }), - ); - const listed = yield* readRuntime.runRead({ - input: { limit: 100, offset: 0 }, - principal, - registration: managedPrincipalsRead, - transport: { correlationId: randomUUID() }, - }); + registration: managedPrincipalsRead, + transport: { correlationId: randomUUID() }, + }); + const committed = yield* admin + .select({ actionKey: actionInvocations.actionKey, status: actionInvocations.status }) + .from(actionInvocations) + .where(eq(actionInvocations.tenantId, tenantId)); + expect( + [ + ...new Set( + committed + .filter(({ status }) => status === 'succeeded') + .map(({ actionKey }) => actionKey), + ), + ].toSorted(), + ).toEqual( + identityActionKeys.filter((actionKey) => !actionKey.includes('support')).toSorted(), + ); + const [readEvidence] = yield* admin + .select({ resultCount: dataAccessEvents.resultCount }) + .from(dataAccessEvents) + .where( + and( + eq(dataAccessEvents.tenantId, tenantId), + eq(dataAccessEvents.evidencePolicyKey, 'core.identity.managed-principals.access.v1'), + ), + ); + expect(readEvidence?.resultCount).toBe(1); + const [apiKeyReadEvidence] = yield* admin + .select({ authBindingId: dataAccessEvents.authBindingId }) + .from(dataAccessEvents) + .where( + and( + eq(dataAccessEvents.tenantId, tenantId), + eq(dataAccessEvents.authMethod, 'api_key'), + ), + ); + expect(apiKeyReadEvidence?.authBindingId).toBe(selfBinding.authBindingId); - assert.equal(binding.status, 'active'); - assert.deepEqual( - listed.items.map(({ authBindingId, principalId: listedPrincipalId }) => ({ - authBindingId: Option.getOrThrow(authBindingId), - principalId: listedPrincipalId, - })), - [{ authBindingId: binding.authBindingId, principalId: created.principalId }], - ); - yield* readRuntime.runRead({ - input: { limit: 100, offset: 0 }, - principal: { - authBindingId: selfBinding.authBindingId, - authContextRef: `better-auth-api-key:${selfProviderKeyId}`, - authMethod: 'api_key', - principalId: administratorPrincipalId, + const systemPrincipal = yield* makeSystemPrincipalContextResolver({ + executor: runtimeDatabase, + }).resolve({ + principalId: systemPrincipalId, + registration: registerSystemWorkload({ jobKey: 'identity-runtime-integration' }), + runReference: randomUUID(), tenantId, - }, - registration: managedPrincipalsRead, - transport: { correlationId: randomUUID() }, - }); - const committed = yield* admin - .select({ actionKey: actionInvocations.actionKey, status: actionInvocations.status }) - .from(actionInvocations) - .where(eq(actionInvocations.tenantId, tenantId)); - assert.deepEqual( - [ - ...new Set( - committed - .filter(({ status }) => status === 'succeeded') - .map(({ actionKey }) => actionKey), - ), - ].toSorted(), - identityActionKeys.filter((actionKey) => !actionKey.includes('support')).toSorted(), - ); - const [readEvidence] = yield* admin - .select({ resultCount: dataAccessEvents.resultCount }) - .from(dataAccessEvents) - .where( - and( - eq(dataAccessEvents.tenantId, tenantId), - eq(dataAccessEvents.evidencePolicyKey, 'core.identity.managed-principals.access.v1'), + }); + const systemDenied = yield* runIdentityAction( + Effect.flip( + actionRuntime.runAction({ + payload: { displayName: 'Executor-only system integration', kind: 'integration' }, + principal: systemPrincipal, + registration: createNonHumanPrincipalAction, + transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, + }), ), ); - assert.equal(readEvidence?.resultCount, 1); - const [apiKeyReadEvidence] = yield* admin - .select({ authBindingId: dataAccessEvents.authBindingId }) - .from(dataAccessEvents) - .where( - and(eq(dataAccessEvents.tenantId, tenantId), eq(dataAccessEvents.authMethod, 'api_key')), + expect(Predicate.isTagged(systemDenied, 'ActionPermissionDenied')).toBe(true); + const systemTenantMember = relationship( + 'tenant', + tenantId, + 'member', + 'principal', + systemPrincipalId, ); - assert.equal(apiKeyReadEvidence?.authBindingId, selfBinding.authBindingId); - - const systemPrincipal = yield* makeSystemPrincipalContextResolver({ - executor: runtimeDatabase, - }).resolve({ - principalId: systemPrincipalId, - registration: registerSystemWorkload({ jobKey: 'identity-runtime-integration' }), - runReference: randomUUID(), - tenantId, - }); - const systemDenied = yield* runIdentityAction( - Effect.flip( + const systemIdentityAdministrator = relationship( + 'tenant', + tenantId, + 'identity_admin', + 'principal', + systemPrincipalId, + ); + spiceDbRelationships.push(systemTenantMember, systemIdentityAdministrator); + const systemRelationshipsRequest = v1.WriteRelationshipsRequest.create({ + updates: [systemTenantMember, systemIdentityAdministrator].map((item) => + v1.RelationshipUpdate.create({ + operation: v1.RelationshipUpdate_Operation.TOUCH, + relationship: item, + }), + ), + }); + yield* Effect.promise(() => + spiceDbClient.promises.writeRelationships(systemRelationshipsRequest), + ); + const systemCreated = yield* runIdentityAction( actionRuntime.runAction({ - payload: { displayName: 'Executor-only system integration', kind: 'integration' }, + payload: { displayName: 'System-created integration', kind: 'integration' }, principal: systemPrincipal, registration: createNonHumanPrincipalAction, transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, }), - ), - ); - assert.equal(systemDenied._tag, 'ActionPermissionDenied'); - const systemTenantMember = relationship( - 'tenant', - tenantId, - 'member', - 'principal', - systemPrincipalId, - ); - const systemIdentityAdministrator = relationship( - 'tenant', - tenantId, - 'identity_admin', - 'principal', - systemPrincipalId, - ); - spiceDbRelationships.push(systemTenantMember, systemIdentityAdministrator); - const systemRelationshipsRequest = v1.WriteRelationshipsRequest.create({ - updates: [systemTenantMember, systemIdentityAdministrator].map((item) => - v1.RelationshipUpdate.create({ - operation: v1.RelationshipUpdate_Operation.TOUCH, - relationship: item, - }), - ), - }); - yield* promiseEffect( - spiceDbClient.promises.writeRelationships.bind( - spiceDbClient.promises, - systemRelationshipsRequest, - ), - ); - const systemCreated = yield* runIdentityAction( - actionRuntime.runAction({ - payload: { displayName: 'System-created integration', kind: 'integration' }, + ); + expect(systemCreated.status).toBe('active'); + const systemRead = yield* readRuntime.runRead({ + input: { limit: 100, offset: 0 }, principal: systemPrincipal, - registration: createNonHumanPrincipalAction, - transport: { correlationId: randomUUID(), idempotencyKey: randomUUID() }, - }), - ); - assert.equal(systemCreated.status, 'active'); - const systemRead = yield* readRuntime.runRead({ - input: { limit: 100, offset: 0 }, - principal: systemPrincipal, - registration: managedPrincipalsRead, - transport: { correlationId: randomUUID() }, - }); - assert.ok(systemRead.items.length >= 2); + registration: managedPrincipalsRead, + transport: { correlationId: randomUUID() }, + }); + expect(systemRead.items.length >= 2).toBe(true); - const supportReason = 'Investigate a live support incident'; - const supportSessionRef = `better-auth-session:${randomUUID()}`; - yield* runIdentityAction( - Effect.forEach( - ['requested', 'started'] as const, - (checkpoint) => - actionRuntime.runAction({ - payload: withOptionalProperty( - { - checkpoint, - originalPrincipalId: administratorPrincipalId, - reason: supportReason, - }, - checkpoint === 'started', - 'sessionRef', - supportSessionRef, - { - targetPrincipalId: supportTargetPrincipalId, + const supportReason = 'Investigate a live support incident'; + const supportSessionRef = `better-auth-session:${randomUUID()}`; + yield* runIdentityAction( + Effect.forEach( + ['requested', 'started'] as const, + (checkpoint) => + actionRuntime.runAction({ + payload: withOptionalProperty( + { + checkpoint, + originalPrincipalId: administratorPrincipalId, + reason: supportReason, + }, + checkpoint === 'started', + 'sessionRef', + supportSessionRef, + { + targetPrincipalId: supportTargetPrincipalId, + }, + ), + principal, + registration: recordSupportImpersonationAction, + transport: { + correlationId: randomUUID(), + idempotencyKey: `support-live-${checkpoint}-${randomUUID()}`, }, - ), - principal, - registration: recordSupportImpersonationAction, - transport: { - correlationId: randomUUID(), - idempotencyKey: `support-live-${checkpoint}-${randomUUID()}`, - }, + }), + { concurrency: 1, discard: true }, + ), + ); + const supportRelationship = relationship( + 'tenant', + tenantId, + 'support', + 'principal', + administratorPrincipalId, + ); + const removeSupportRelationshipRequest = v1.WriteRelationshipsRequest.create({ + updates: [ + v1.RelationshipUpdate.create({ + operation: v1.RelationshipUpdate_Operation.DELETE, + relationship: supportRelationship, }), - { concurrency: 1, discard: true }, - ), - ); - const supportRelationship = relationship( - 'tenant', - tenantId, - 'support', - 'principal', - administratorPrincipalId, - ); - const removeSupportRelationshipRequest = v1.WriteRelationshipsRequest.create({ - updates: [ - v1.RelationshipUpdate.create({ - operation: v1.RelationshipUpdate_Operation.DELETE, - relationship: supportRelationship, - }), - ], - }); - yield* promiseEffect( - spiceDbClient.promises.writeRelationships.bind( - spiceDbClient.promises, - removeSupportRelationshipRequest, - ), - ); - yield* admin - .update(principalAuthBindings) - .set({ - revokedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-08-09T00:00:00.000Z')), - status: 'revoked', - }) - .where(eq(principalAuthBindings.principalAuthBindingId, administratorAuthBindingId)); - yield* admin - .update(principals) - .set({ status: 'disabled' }) - .where( - inArray(principals.principalId, [administratorPrincipalId, supportTargetPrincipalId]), + ], + }); + yield* Effect.promise(() => + spiceDbClient.promises.writeRelationships(removeSupportRelationshipRequest), ); - const recoveryPrincipal = yield* makeSupportRecoveryPrincipalContextResolver({ - executor: runtimeDatabase, - }).resolveStoppedImpersonation({ - originalAuthBindingId: administratorAuthBindingId, - originalPrincipalId: administratorPrincipalId, - originalSessionId: randomUUID(), - tenantId, - }); - const stopped = yield* runIdentityAction( - actionRuntime.runAction({ - payload: { - checkpoint: 'stopped', - originalPrincipalId: administratorPrincipalId, - reason: supportReason, - sessionRef: supportSessionRef, - targetPrincipalId: supportTargetPrincipalId, - }, - principal: recoveryPrincipal, - registration: recordSupportImpersonationAction, - transport: { - correlationId: randomUUID(), - idempotencyKey: `support-live-stopped-${randomUUID()}`, - }, - }), - ); - assert.deepEqual(stopped, { checkpoint: 'stopped', recorded: true }); - const supportAudits = yield* admin - .select({ evidence: auditEvents.evidenceJson }) - .from(auditEvents) - .where( - and(eq(auditEvents.tenantId, tenantId), eq(auditEvents.eventType, 'action.executed')), + yield* admin + .update(principalAuthBindings) + .set({ + revokedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-08-09T00:00:00.000Z')), + status: 'revoked', + }) + .where(eq(principalAuthBindings.principalAuthBindingId, administratorAuthBindingId)); + yield* admin + .update(principals) + .set({ status: 'disabled' }) + .where( + inArray(principals.principalId, [administratorPrincipalId, supportTargetPrincipalId]), + ); + const recoveryPrincipal = yield* makeSupportRecoveryPrincipalContextResolver({ + executor: runtimeDatabase, + }).resolveStoppedImpersonation({ + originalAuthBindingId: administratorAuthBindingId, + originalPrincipalId: administratorPrincipalId, + originalSessionId: randomUUID(), + tenantId, + }); + const stopped = yield* runIdentityAction( + actionRuntime.runAction({ + payload: { + checkpoint: 'stopped', + originalPrincipalId: administratorPrincipalId, + reason: supportReason, + sessionRef: supportSessionRef, + targetPrincipalId: supportTargetPrincipalId, + }, + principal: recoveryPrincipal, + registration: recordSupportImpersonationAction, + transport: { + correlationId: randomUUID(), + idempotencyKey: `support-live-stopped-${randomUUID()}`, + }, + }), ); - assert.deepEqual( - supportAudits - .map(({ evidence }) => - Predicate.isObjectKeyword(evidence) && evidence !== null && 'checkpoint' in evidence - ? evidence.checkpoint - : undefined, - ) - .filter((checkpoint): checkpoint is string => Predicate.isString(checkpoint)) - .toSorted(), - ['requested', 'started', 'stopped'], - ); - const supportAccess = yield* admin - .select({ count: dataAccessEvents.resultCount }) - .from(dataAccessEvents) - .where( - and( - eq(dataAccessEvents.tenantId, tenantId), - eq( - dataAccessEvents.evidencePolicyKey, - 'core.identity.record-support-impersonation.access.v1', + expect(stopped).toEqual({ checkpoint: 'stopped', recorded: true }); + const supportAudits = yield* admin + .select({ evidence: auditEvents.evidenceJson }) + .from(auditEvents) + .where( + and(eq(auditEvents.tenantId, tenantId), eq(auditEvents.eventType, 'action.executed')), + ); + expect( + supportAudits + .map(({ evidence }) => + Predicate.isObjectKeyword(evidence) && evidence !== null && 'checkpoint' in evidence + ? evidence.checkpoint + : undefined, + ) + .filter((checkpoint): checkpoint is string => Predicate.isString(checkpoint)) + .toSorted(), + ).toEqual(['requested', 'started', 'stopped']); + const supportAccess = yield* admin + .select({ count: dataAccessEvents.resultCount }) + .from(dataAccessEvents) + .where( + and( + eq(dataAccessEvents.tenantId, tenantId), + eq( + dataAccessEvents.evidencePolicyKey, + 'core.identity.record-support-impersonation.access.v1', + ), ), + ); + expect(supportAccess.length).toBe(6); + const succeededIdentityActions = yield* admin + .select({ actionKey: actionInvocations.actionKey }) + .from(actionInvocations) + .where( + and( + eq(actionInvocations.tenantId, tenantId), + eq(actionInvocations.status, 'succeeded'), + ), + ); + expect( + [...new Set(succeededIdentityActions.map(({ actionKey }) => actionKey))].toSorted(), + ).toEqual([...identityActionKeys].toSorted()); + }); + const cleanupRelationships = Effect.suspend(() => { + const request = v1.WriteRelationshipsRequest.create({ + updates: spiceDbRelationships.map((item) => + v1.RelationshipUpdate.create({ + operation: v1.RelationshipUpdate_Operation.DELETE, + relationship: item, + }), ), - ); - assert.equal(supportAccess.length, 6); - const succeededIdentityActions = yield* admin - .select({ actionKey: actionInvocations.actionKey }) - .from(actionInvocations) - .where( - and(eq(actionInvocations.tenantId, tenantId), eq(actionInvocations.status, 'succeeded')), - ); - assert.deepEqual( - [...new Set(succeededIdentityActions.map(({ actionKey }) => actionKey))].toSorted(), - [...identityActionKeys].toSorted(), - ); - }); - const cleanupRelationships = Effect.suspend(() => { - const request = v1.WriteRelationshipsRequest.create({ - updates: spiceDbRelationships.map((item) => - v1.RelationshipUpdate.create({ - operation: v1.RelationshipUpdate_Operation.DELETE, - relationship: item, + }); + return Effect.promise(() => spiceDbClient.promises.writeRelationships(request)); + }); + const release = cleanup.pipe( + Effect.ensuring(cleanupRelationships.pipe(Effect.orDie)), + Effect.ensuring( + Effect.sync(() => { + permissionClient.close(); + spiceDbClient.close(); }), ), - }); - return promiseEffect( - spiceDbClient.promises.writeRelationships.bind(spiceDbClient.promises, request), ); - }); - const release = cleanup.pipe( - Effect.ensuring(cleanupRelationships.pipe(Effect.orDie)), - Effect.ensuring( - Effect.sync(() => { - permissionClient.close(); - spiceDbClient.close(); - }), - ), - Effect.ensuring( - Effect.all( - [ - promiseEffect(adminPool.end.bind(adminPool)), - promiseEffect(runtimePool.end.bind(runtimePool)), - ], - { concurrency: 'unbounded' }, - ).pipe(Effect.orDie), - ), - ); - yield* exercise.pipe(Effect.ensuring(release.pipe(Effect.orDie))); - }), + yield* Effect.addFinalizer(() => release.pipe(Effect.orDie)); + yield* exercise; + }), ); diff --git a/app/packages/core-runtime/tests/integration/legal-entity-context.test.ts b/app/packages/core-runtime/tests/integration/legal-entity-context.test.ts index 096231989..d38733da7 100644 --- a/app/packages/core-runtime/tests/integration/legal-entity-context.test.ts +++ b/app/packages/core-runtime/tests/integration/legal-entity-context.test.ts @@ -1,9 +1,7 @@ -import { makeEffectTestCallback } from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from 'effect-rstest'; import { eq } from 'drizzle-orm'; -import { Effect } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { Effect, Predicate } from 'effect'; import { makeLegalEntityContext } from '../../src/auth/legal-entity-context.ts'; import { loadDatabaseConfig } from '../../src/db/config.ts'; import { legalEntities, tenants } from '../../src/db/schema.ts'; @@ -16,8 +14,7 @@ const activeTwo = '21000000-0000-4000-8000-000000000002'; const suspended = '21000000-0000-4000-8000-000000000003'; const foreign = '21000000-0000-4000-8000-000000000004'; -test( - 'lists and validates only active legal entities inside the exact tenant', +it.live('lists and validates only active legal entities inside the exact tenant', () => Effect.gen(function* legalEntityContextIntegration() { const configuration = yield* loadDatabaseConfig(); const { executor: database } = yield* makeCoreDatabase(configuration); @@ -29,71 +26,70 @@ test( yield* database.delete(tenants).where(eq(tenants.tenantId, tenantTwo)); }); - yield* cleanup; - yield* Effect.gen(function* exerciseLegalEntityContext() { - yield* database.insert(tenants).values([ - { - defaultLocale: 'en', - name: 'Legal context tenant one', - slug: 'legal-context-tenant-one', - status: 'active', - tenantId: tenantOne, - }, - { - defaultLocale: 'en', - name: 'Legal context tenant two', - slug: 'legal-context-tenant-two', - status: 'active', - tenantId: tenantTwo, - }, - ]); - yield* database.insert(legalEntities).values([ - { - legalEntityId: activeOne, - legalName: 'Zeta entity', - registrationCountry: 'CZ', - registrationNumber: 'LEGAL-CONTEXT-1', - status: 'active', - tenantId: tenantOne, - }, - { - legalEntityId: activeTwo, - legalName: 'Alpha entity', - registrationCountry: 'CZ', - registrationNumber: 'LEGAL-CONTEXT-2', - status: 'active', - tenantId: tenantOne, - }, - { - legalEntityId: suspended, - legalName: 'Suspended entity', - registrationCountry: 'CZ', - registrationNumber: 'LEGAL-CONTEXT-3', - status: 'suspended', - tenantId: tenantOne, - }, - { - legalEntityId: foreign, - legalName: 'Foreign entity', - registrationCountry: 'CZ', - registrationNumber: 'LEGAL-CONTEXT-4', - status: 'active', - tenantId: tenantTwo, - }, - ]); + yield* Effect.acquireRelease(cleanup, () => cleanup.pipe(Effect.orDie)); - assert.deepEqual(yield* context.listActiveForTenant(tenantOne), [ - { legalEntityId: activeTwo, legalName: 'Alpha entity' }, - { legalEntityId: activeOne, legalName: 'Zeta entity' }, - ]); - assert.deepEqual(yield* context.validateSelection(tenantOne, activeOne), { + yield* database.insert(tenants).values([ + { + defaultLocale: 'en', + name: 'Legal context tenant one', + slug: 'legal-context-tenant-one', + status: 'active', + tenantId: tenantOne, + }, + { + defaultLocale: 'en', + name: 'Legal context tenant two', + slug: 'legal-context-tenant-two', + status: 'active', + tenantId: tenantTwo, + }, + ]); + yield* database.insert(legalEntities).values([ + { legalEntityId: activeOne, legalName: 'Zeta entity', - }); - const inactiveError = yield* Effect.flip(context.validateSelection(tenantOne, suspended)); - assert.equal(inactiveError._tag, 'LegalEntityContextInactiveError'); - const missingError = yield* Effect.flip(context.validateSelection(tenantOne, foreign)); - assert.equal(missingError._tag, 'LegalEntityContextMissingError'); - }).pipe(Effect.ensuring(cleanup.pipe(Effect.orDie))); - }).pipe(Effect.scoped, makeEffectTestCallback), + registrationCountry: 'CZ', + registrationNumber: 'LEGAL-CONTEXT-1', + status: 'active', + tenantId: tenantOne, + }, + { + legalEntityId: activeTwo, + legalName: 'Alpha entity', + registrationCountry: 'CZ', + registrationNumber: 'LEGAL-CONTEXT-2', + status: 'active', + tenantId: tenantOne, + }, + { + legalEntityId: suspended, + legalName: 'Suspended entity', + registrationCountry: 'CZ', + registrationNumber: 'LEGAL-CONTEXT-3', + status: 'suspended', + tenantId: tenantOne, + }, + { + legalEntityId: foreign, + legalName: 'Foreign entity', + registrationCountry: 'CZ', + registrationNumber: 'LEGAL-CONTEXT-4', + status: 'active', + tenantId: tenantTwo, + }, + ]); + + expect(yield* context.listActiveForTenant(tenantOne)).toEqual([ + { legalEntityId: activeTwo, legalName: 'Alpha entity' }, + { legalEntityId: activeOne, legalName: 'Zeta entity' }, + ]); + expect(yield* context.validateSelection(tenantOne, activeOne)).toEqual({ + legalEntityId: activeOne, + legalName: 'Zeta entity', + }); + const inactiveError = yield* Effect.flip(context.validateSelection(tenantOne, suspended)); + expect(Predicate.isTagged(inactiveError, 'LegalEntityContextInactiveError')).toBe(true); + const missingError = yield* Effect.flip(context.validateSelection(tenantOne, foreign)); + expect(Predicate.isTagged(missingError, 'LegalEntityContextMissingError')).toBe(true); + }), ); diff --git a/app/packages/core-runtime/tests/integration/module-state-gate.test.ts b/app/packages/core-runtime/tests/integration/module-state-gate.test.ts index b3ea26cb5..e2142dd3e 100644 --- a/app/packages/core-runtime/tests/integration/module-state-gate.test.ts +++ b/app/packages/core-runtime/tests/integration/module-state-gate.test.ts @@ -1,10 +1,7 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. +import { expect, it } from 'effect-rstest'; import { and, eq } from 'drizzle-orm'; -import { Effect, Exit } from 'effect'; -import assert from 'node:assert/strict'; +import { Effect, Exit, Option, Predicate } from 'effect'; import { randomUUID } from 'node:crypto'; -import test from 'node:test'; import type { CoreDatabase } from '../../src/db/client.ts'; import { makeCoreDatabase } from '../../src/db/client.ts'; import { loadDatabaseConfig } from '../../src/db/config.ts'; @@ -23,22 +20,6 @@ import { type DatabaseService = (typeof CoreDatabase)['Service']; -const withDatabase = ( - operation: (database: DatabaseService) => Effect.Effect, -) => - Effect.scoped( - Effect.gen(function* moduleStateGateDatabaseScope() { - const configuration = yield* loadDatabaseConfig(); - const database = yield* makeCoreDatabase(configuration); - return yield* operation(database); - }), - ); - -const databasePromise = async ( - operation: (database: DatabaseService) => PromiseLike, -): Promise => - await runEffectTestPromise(withDatabase((database) => Effect.promise(() => operation(database)))); - const unavailableStateService = (reason: string): TenantModuleStateServiceContract => { const failure = new TenantModuleStateReadUnavailableError({ code: 'tenant_module_state_read_unavailable', @@ -51,15 +32,30 @@ const unavailableStateService = (reason: string): TenantModuleStateServiceContra }; }; -void test('batches tenant-isolated states once, rejects malformed/unavailable reads, and rechecks transactionally', async () => { - const tenantOne = randomUUID(); - const tenantTwo = randomUUID(); - const moduleKey = `gate.integration-${tenantOne}`; - const stateModuleKey = (state: (typeof TENANT_MODULE_STATES)[number]): string => - `${moduleKey}.${state.replaceAll('_', '-')}`; - await databasePromise(async (database) => { - await runEffectTestPromise( - database.executor.insert(tenants).values([ +it.live( + 'batches tenant-isolated states once, rejects malformed/unavailable reads, and rechecks transactionally', + () => + Effect.gen(function* moduleStateGate1() { + const tenantOne = randomUUID(); + const tenantTwo = randomUUID(); + const moduleKey = `gate.integration-${tenantOne}`; + const stateModuleKey = (state: (typeof TENANT_MODULE_STATES)[number]): string => + `${moduleKey}.${state.replaceAll('_', '-')}`; + const configuration = yield* loadDatabaseConfig(); + const database = yield* makeCoreDatabase(configuration); + yield* Effect.addFinalizer(() => + Effect.forEach( + [tenantModuleStates, tenants], + (table) => + Effect.forEach( + [tenantOne, tenantTwo], + (tenantId) => database.executor.delete(table).where(eq(table.tenantId, tenantId)), + { discard: true }, + ), + { discard: true }, + ).pipe(Effect.orDie), + ); + yield* database.executor.insert(tenants).values([ { defaultLocale: 'en', name: 'Gate Integration One', @@ -74,10 +70,8 @@ void test('batches tenant-isolated states once, rejects malformed/unavailable re status: 'active', tenantId: tenantTwo, }, - ]), - ); - await runEffectTestPromise( - database.executor.insert(tenantModuleStates).values([ + ]); + yield* database.executor.insert(tenantModuleStates).values([ { moduleKey, state: 'active', tenantId: tenantOne }, { moduleKey, state: 'quarantined', tenantId: tenantTwo }, ...TENANT_MODULE_STATES.map((state) => ({ @@ -85,10 +79,8 @@ void test('batches tenant-isolated states once, rejects malformed/unavailable re state, tenantId: tenantOne, })), - ]), - ); + ]); - try { let selects = 0; const countingExecutor: DatabaseService['executor'] = Object.create(database.executor); Object.defineProperty(countingExecutor, 'select', { @@ -115,12 +107,10 @@ void test('batches tenant-isolated states once, rejects malformed/unavailable re moduleKey, role: 'action', }); - const snapshot = await runEffectTestPromise( - gate.prepareSnapshot(tenantOne, [read, read, write]), - ); - await runEffectTestPromise(gate.check(snapshot, read)); - await runEffectTestPromise(gate.check(snapshot, read)); - assert.equal(selects, 1); + const snapshot = yield* gate.prepareSnapshot(tenantOne, [read, read, write]); + yield* gate.check(snapshot, read); + yield* gate.check(snapshot, read); + expect(selects).toBe(1); const persistedStateDescriptors = TENANT_MODULE_STATES.map((state) => defineTenantModuleEntrypoint({ @@ -132,38 +122,41 @@ void test('batches tenant-isolated states once, rejects malformed/unavailable re }), ); selects = 0; - const [firstPersistedDescriptor] = persistedStateDescriptors; - assert.ok(firstPersistedDescriptor); - const persistedStateSnapshot = await runEffectTestPromise( - gate.prepareSnapshot(tenantOne, [...persistedStateDescriptors, firstPersistedDescriptor]), + const firstPersistedDescriptor = Option.getOrThrow( + Option.fromNullishOr(persistedStateDescriptors[0]), ); - assert.equal(selects, 1); - const persistedStateExits = await Promise.all( - TENANT_MODULE_STATES.map(async (_, index) => { - const descriptor = persistedStateDescriptors[index]; - assert.ok(descriptor); - return await runEffectTestPromise( - Effect.exit(gate.check(persistedStateSnapshot, descriptor)), + expect(firstPersistedDescriptor).toBeDefined(); + const persistedStateSnapshot = yield* gate.prepareSnapshot(tenantOne, [ + ...persistedStateDescriptors, + firstPersistedDescriptor, + ]); + expect(selects).toBe(1); + const persistedStateExits = yield* Effect.forEach( + TENANT_MODULE_STATES, + (_, index) => { + const descriptor = Option.getOrThrow( + Option.fromNullishOr(persistedStateDescriptors[index]), ); - }), + expect(descriptor).toBeDefined(); + return Effect.exit(gate.check(persistedStateSnapshot, descriptor)); + }, + { concurrency: 'unbounded' }, ); for (const [index, state] of TENANT_MODULE_STATES.entries()) { - const descriptor = persistedStateDescriptors[index]; - assert.ok(descriptor); - const exit = persistedStateExits[index]; - assert.ok(exit); - assert.equal( - Exit.isSuccess(exit), + const descriptor = Option.getOrThrow( + Option.fromNullishOr(persistedStateDescriptors[index]), + ); + expect(descriptor).toBeDefined(); + const exit = Option.getOrThrow(Option.fromNullishOr(persistedStateExits[index])); + expect(exit).toBeDefined(); + expect(Exit.isSuccess(exit), state).toBe( decideModuleStateAccess(state, 'read') === 'allow', - state, ); } - const tenantTwoSnapshot = await runEffectTestPromise(gate.prepareSnapshot(tenantTwo, [read])); - const quarantined = await runEffectTestPromise( - Effect.flip(gate.check(tenantTwoSnapshot, read)), - ); - assert.equal(quarantined._tag, 'ModuleStateDeniedError'); + const tenantTwoSnapshot = yield* gate.prepareSnapshot(tenantTwo, [read]); + const quarantined = yield* Effect.flip(gate.check(tenantTwoSnapshot, read)); + expect(Predicate.isTagged(quarantined, 'ModuleStateDeniedError')).toBe(true); const missingDescriptor = defineTenantModuleEntrypoint({ access: 'read', @@ -172,71 +165,43 @@ void test('batches tenant-isolated states once, rejects malformed/unavailable re moduleKey: `${moduleKey}.missing-module`, role: 'page', }); - const missingSnapshot = await runEffectTestPromise( - gate.prepareSnapshot(tenantOne, [missingDescriptor]), - ); - const missing = await runEffectTestPromise( - Effect.flip(gate.check(missingSnapshot, missingDescriptor)), - ); - assert.equal(missing._tag, 'ModuleStateDeniedError'); + const missingSnapshot = yield* gate.prepareSnapshot(tenantOne, [missingDescriptor]); + const missing = yield* Effect.flip(gate.check(missingSnapshot, missingDescriptor)); + expect(Predicate.isTagged(missing, 'ModuleStateDeniedError')).toBe(true); - await runEffectTestPromise( - database.executor.transaction((transaction) => - gate.recheckWrite(transaction, tenantOne, write), - ), + yield* database.executor.transaction((transaction) => + gate.recheckWrite(transaction, tenantOne, write), ); - await runEffectTestPromise( - database.executor - .update(tenantModuleStates) - .set({ state: 'read_only' }) - .where( - and( - eq(tenantModuleStates.tenantId, tenantOne), - eq(tenantModuleStates.moduleKey, moduleKey), - ), + yield* database.executor + .update(tenantModuleStates) + .set({ state: 'read_only' }) + .where( + and( + eq(tenantModuleStates.tenantId, tenantOne), + eq(tenantModuleStates.moduleKey, moduleKey), ), + ); + const lockedDenial = yield* database.executor.transaction((transaction) => + Effect.flip(gate.recheckWrite(transaction, tenantOne, write)), ); - const lockedDenial = await runEffectTestPromise( - database.executor.transaction((transaction) => - Effect.flip(gate.recheckWrite(transaction, tenantOne, write)), - ), - ); - assert.equal(lockedDenial._tag, 'ModuleStateDeniedError'); + expect(Predicate.isTagged(lockedDenial, 'ModuleStateDeniedError')).toBe(true); - const unavailable = await runEffectTestPromise( - Effect.flip( - makeModuleStateGate(unavailableStateService('secret db failure')).prepareSnapshot( - tenantOne, - [read], - ), + const unavailable = yield* Effect.flip( + makeModuleStateGate(unavailableStateService('secret db failure')).prepareSnapshot( + tenantOne, + [read], ), ); - assert.equal(unavailable._tag, 'ModuleStateCheckUnavailableError'); - assert.doesNotMatch(unavailable.reason, /secret|db failure/u); + expect(Predicate.isTagged(unavailable, 'ModuleStateCheckUnavailableError')).toBe(true); + expect(unavailable.reason).not.toMatch(/secret|db failure/u); - const malformed = await runEffectTestPromise( - Effect.flip( - makeModuleStateGate(unavailableStateService('corrupt-storage-value')).prepareSnapshot( - tenantOne, - [read], - ), - ), - ); - assert.equal(malformed._tag, 'ModuleStateCheckUnavailableError'); - assert.doesNotMatch(malformed.reason, /corrupt|storage/u); - } finally { - await runEffectTestPromise( - Effect.forEach( - [tenantModuleStates, tenants], - (table) => - Effect.forEach( - [tenantOne, tenantTwo], - (tenantId) => database.executor.delete(table).where(eq(table.tenantId, tenantId)), - { discard: true }, - ), - { discard: true }, + const malformed = yield* Effect.flip( + makeModuleStateGate(unavailableStateService('corrupt-storage-value')).prepareSnapshot( + tenantOne, + [read], ), ); - } - }); -}); + expect(Predicate.isTagged(malformed, 'ModuleStateCheckUnavailableError')).toBe(true); + expect(malformed.reason).not.toMatch(/corrupt|storage/u); + }), +); diff --git a/app/packages/core-runtime/tests/integration/outbox-runtime.test.ts b/app/packages/core-runtime/tests/integration/outbox-runtime.test.ts index 760b6b129..029cf4eca 100644 --- a/app/packages/core-runtime/tests/integration/outbox-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/outbox-runtime.test.ts @@ -1,11 +1,8 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import { purgeFixtureRows } from '../support/fixture-cleanup.ts'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import { and, asc, eq } from 'drizzle-orm'; -import { DateTime, Effect, Option, Schema } from 'effect'; -import assert from 'node:assert/strict'; +import { expect, it } from 'effect-rstest'; import { randomUUID } from 'node:crypto'; -import test from 'node:test'; +import { and, asc, eq, inArray } from 'drizzle-orm'; +import { DateTime, Effect, Option, Schema, pipe } from 'effect'; import { makeCoreDatabase } from '../../src/db/client.ts'; import { loadDatabaseConfig } from '../../src/db/config.ts'; import { @@ -18,42 +15,17 @@ import { workerCheckpoints, } from '../../src/db/schema.ts'; import type { CoreDatabaseExecutor } from '../../src/db/types.ts'; +import { defineOutboxWorker } from '../../src/outbox/definition.ts'; import { defineTenantModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; import type { AnyOutboxWorkerRegistration } from '../../src/outbox/definition.ts'; -import { defineOutboxWorker } from '../../src/outbox/definition.ts'; import { OutboxClaimLostError } from '../../src/outbox/errors.ts'; -import type { OutboxClaim, OutboxRepositoryService } from '../../src/outbox/repository.ts'; import { makeOutboxRepository } from '../../src/outbox/repository.ts'; const MessageKeySchema = Schema.String.pipe(Schema.brand('MessageKey')); const payloadSchema = Schema.Struct({ messageKey: MessageKeySchema }); - const dateAt = (instant: string): Date => DateTime.toDateUtc(DateTime.makeUnsafe(instant)); - const advanceDate = (date: Date, milliseconds: number): Date => DateTime.makeUnsafe(date).pipe(DateTime.add({ milliseconds }), DateTime.toDateUtc); - -const claimNext = async ( - repository: OutboxRepositoryService, - registrations: readonly AnyOutboxWorkerRegistration[], - claimOwner: string, - now: Date, -): Promise> => - await runEffectTestPromise(repository.claimNext(registrations, claimOwner, now)); - -const forEachSequential = async ( - values: readonly Value[], - operation: (value: Value) => PromiseLike, - index = 0, -): Promise => { - const value = values[index]; - if (value === undefined) { - return; - } - await operation(value); - await forEachSequential(values, operation, index + 1); -}; - const makeWorker = ( workerKey: string, options: { @@ -86,289 +58,246 @@ const makeWorker = ( }, () => Effect.void, ); - const subscriptionOf = (registration: AnyOutboxWorkerRegistration) => registration.descriptor; - -const withDatabase = async ( - operation: (database: CoreDatabaseExecutor) => Promise, -): Promise => - await runEffectTestPromise( - Effect.scoped( - Effect.gen(function* databaseScope() { - const configuration = yield* loadDatabaseConfig(); - const database = yield* makeCoreDatabase(configuration); - return yield* Effect.promise(async () => await operation(database.executor)); - }), - ), - ); - -const insertTenant = async (database: CoreDatabaseExecutor): Promise => { - const tenantId = randomUUID(); - await runEffectTestPromise( - database.insert(tenants).values({ +const insertTenant = (database: CoreDatabaseExecutor) => + Effect.gen(function* insertTenantEffect() { + const tenantId = randomUUID(); + yield* database.insert(tenants).values({ defaultLocale: 'en', name: 'Outbox Runtime Integration', slug: `outbox-runtime-${tenantId}`, status: 'active', tenantId, - }), - ); - return tenantId; -}; - + }); + return tenantId; + }); const activateConsumer = (database: CoreDatabaseExecutor, tenantId: string, state = 'active') => database.insert(tenantModuleStates).values({ moduleKey: 'consumer', state, tenantId, }); - -const insertMessage = async ( +const insertMessage = ( database: CoreDatabaseExecutor, tenantId: string, topic = 'producer.message-created', messageKey = randomUUID(), -) => { - const [event] = await runEffectTestPromise( - database - .insert(domainEvents) - .values({ - eventType: topic, - payloadJson: { messageKey }, - producerModuleKey: 'producer', - subjectModuleKey: 'producer', - subjectResourceId: messageKey, - subjectResourceType: 'outbox-test', - tenantId, - }) - .returning({ - domainEventId: domainEvents.domainEventId, - tenantSequenceNo: domainEvents.tenantSequenceNo, - }), - ); - assert.ok(event); - const [message] = await runEffectTestPromise( - database - .insert(outboxMessages) - .values({ - domainEventId: event.domainEventId, - payloadJson: { messageKey }, - producerModuleKey: 'producer', - tenantId, - topic, - }) - .returning({ messageId: outboxMessages.outboxMessageId }), - ); - assert.ok(message); - return { ...event, ...message }; -}; - -const cleanupTenant = async (database: CoreDatabaseExecutor, tenantId: string): Promise => { - await runEffectTestPromise( - database.delete(workerCheckpoints).where(eq(workerCheckpoints.tenantId, tenantId)), - ); - const messageRows = await runEffectTestPromise( - database - .select({ messageId: outboxMessages.outboxMessageId }) - .from(outboxMessages) - .where(eq(outboxMessages.tenantId, tenantId)), - ); - await forEachSequential(messageRows, async ({ messageId }) => { - const deliveries = await runEffectTestPromise( - database - .select({ deliveryId: outboxDeliveries.outboxDeliveryId }) - .from(outboxDeliveries) - .where(eq(outboxDeliveries.outboxMessageId, messageId)), +) => + Effect.gen(function* insertMessageEffect() { + const event = Option.getOrThrow( + Option.fromNullishOr( + (yield* database + .insert(domainEvents) + .values({ + eventType: topic, + payloadJson: { messageKey }, + producerModuleKey: 'producer', + subjectModuleKey: 'producer', + subjectResourceId: messageKey, + subjectResourceType: 'outbox-test', + tenantId, + }) + .returning({ + domainEventId: domainEvents.domainEventId, + tenantSequenceNo: domainEvents.tenantSequenceNo, + }))[0], + ), ); - await forEachSequential(deliveries, async ({ deliveryId }) => { - await runEffectTestPromise( - database.delete(outboxAttempts).where(eq(outboxAttempts.outboxDeliveryId, deliveryId)), - ); - }); - await runEffectTestPromise( - database.delete(outboxDeliveries).where(eq(outboxDeliveries.outboxMessageId, messageId)), + expect(event).toBeDefined(); + const message = Option.getOrThrow( + Option.fromNullishOr( + (yield* database + .insert(outboxMessages) + .values({ + domainEventId: event.domainEventId, + payloadJson: { messageKey }, + producerModuleKey: 'producer', + tenantId, + topic, + }) + .returning({ messageId: outboxMessages.outboxMessageId }))[0], + ), ); + expect(message).toBeDefined(); + return { ...event, ...message }; }); - await runEffectTestPromise( - purgeFixtureRows([ +const cleanupTenant = (database: CoreDatabaseExecutor, tenantId: string) => + Effect.gen(function* cleanupTenantEffect() { + yield* database.delete(workerCheckpoints).where(eq(workerCheckpoints.tenantId, tenantId)); + const messageRows = yield* database + .select({ messageId: outboxMessages.outboxMessageId }) + .from(outboxMessages) + .where(eq(outboxMessages.tenantId, tenantId)); + const messageIds = messageRows.map(({ messageId }) => messageId); + const deliveries = yield* database + .select({ deliveryId: outboxDeliveries.outboxDeliveryId }) + .from(outboxDeliveries) + .where(inArray(outboxDeliveries.outboxMessageId, messageIds)); + yield* database.delete(outboxAttempts).where( + inArray( + outboxAttempts.outboxDeliveryId, + deliveries.map(({ deliveryId }) => deliveryId), + ), + ); + yield* database + .delete(outboxDeliveries) + .where(inArray(outboxDeliveries.outboxMessageId, messageIds)); + yield* purgeFixtureRows([ database.delete(outboxMessages).where(eq(outboxMessages.tenantId, tenantId)), database.delete(domainEvents).where(eq(domainEvents.tenantId, tenantId)), database.delete(tenantModuleStates).where(eq(tenantModuleStates.tenantId, tenantId)), database.delete(tenants).where(eq(tenants.tenantId, tenantId)), - ]), - ); -}; - -/** - * Every scenario owns one throwaway tenant and must drop it even when an assertion fails, so the - * tenant lifecycle stays here instead of being restated as a try/finally in each test. - */ -const withTenant = async ( - scenario: (database: CoreDatabaseExecutor, tenantId: string) => Promise, -): Promise => - await withDatabase(async (database) => { - const tenantId = await insertTenant(database); - try { - await scenario(database, tenantId); - } finally { - await cleanupTenant(database, tenantId); - } + ]); }); +/** Each test owns the database and tenant until its scoped finalizers complete. */ +const tenantFixture = Effect.gen(function* tenantFixture() { + const configuration = yield* loadDatabaseConfig(); + const { executor: database } = yield* makeCoreDatabase(configuration); + const tenantId = yield* Effect.acquireRelease(insertTenant(database), (id) => + cleanupTenant(database, id).pipe(Effect.orDie), + ); + return { database, tenantId }; +}); -/** - * The claim scenarios all start from a registered worker whose pending messages are already - * matched, and they all claim against a clock advanced past the seeded rows. - */ -const matchedWorker = async ( +/** Seed and match one worker against a clock advanced beyond its pending rows. */ +const matchedWorker = Effect.fn(function* matchedWorker( database: CoreDatabaseExecutor, tenantId: string, workerKey: string, options: Parameters[1] & { readonly messages?: number } = {}, -): Promise<{ - readonly messages: readonly Awaited>[]; - readonly now: Date; - readonly registration: AnyOutboxWorkerRegistration; - readonly repository: OutboxRepositoryService; -}> => { - await runEffectTestPromise(activateConsumer(database, tenantId)); - const messages: Awaited>[] = []; - await forEachSequential( - Array.from({ length: options.messages ?? 1 }, (_, index) => index), - async () => { - messages.push(await insertMessage(database, tenantId)); - }, +) { + yield* activateConsumer(database, tenantId); + const messages = yield* Effect.forEach(Array.from({ length: options.messages ?? 1 }), () => + insertMessage(database, tenantId), ); const registration = makeWorker(workerKey, options); const repository = makeOutboxRepository(database); - const now = advanceDate(await runEffectTestPromise(DateTime.nowAsDate), 1000); - await runEffectTestPromise(repository.matchUnmatched([subscriptionOf(registration)], now)); + const now = advanceDate(yield* DateTime.nowAsDate, 1000); + yield* repository.matchUnmatched([subscriptionOf(registration)], now); return { messages, now, registration, repository }; -}; +}); -void test('matches zero, one, or multiple exact workers once without historical backfill', async () => { - await withTenant(async (database, tenantId) => { - await insertMessage(database, tenantId); - await insertMessage(database, tenantId, 'producer.unmatched'); +it.live('matches zero, one, or multiple exact workers once without historical backfill', () => + Effect.gen(function* matchesZeroOneOrMultiple() { + const { database, tenantId } = yield* tenantFixture; + yield* insertMessage(database, tenantId); + yield* insertMessage(database, tenantId, 'producer.unmatched'); const repository = makeOutboxRepository(database); const workers = [makeWorker('consumer.alpha'), makeWorker('consumer.beta')]; - - const firstMatch = await runEffectTestPromise( - repository.matchUnmatched(workers.map(subscriptionOf), dateAt('2026-08-03T10:00:00Z')), - ); - assert.equal(firstMatch.deliveriesCreated, 2); - assert.ok(firstMatch.messagesMatched >= 2); - const repeatMatch = await runEffectTestPromise( - repository.matchUnmatched(workers.map(subscriptionOf), dateAt('2026-08-03T10:01:00Z')), - ); - assert.equal(repeatMatch.deliveriesCreated, 0); - const lateWorkerMatch = await runEffectTestPromise( - repository.matchUnmatched( - [...workers, makeWorker('consumer.late')].map(subscriptionOf), - dateAt('2026-08-03T10:02:00Z'), - ), - ); - assert.equal(lateWorkerMatch.deliveriesCreated, 0); - const deliveries = await runEffectTestPromise( - database - .select() - .from(outboxDeliveries) - .innerJoin( - outboxMessages, - eq(outboxMessages.outboxMessageId, outboxDeliveries.outboxMessageId), - ) - .where(eq(outboxMessages.tenantId, tenantId)), - ); - assert.equal(deliveries.length, 2); - assert.deepEqual(deliveries.map((row) => row.outbox_deliveries.workerKey).toSorted(), [ + const firstMatch = yield* repository.matchUnmatched( + workers.map(subscriptionOf), + dateAt('2026-08-03T10:00:00Z'), + ); + expect( + firstMatch.deliveriesCreated, + `Initial matcher batch processed ${firstMatch.messagesMatched} unmatched messages`, + ).toBe(2); + expect(firstMatch.messagesMatched >= 2).toBe(true); + const repeatMatch = yield* repository.matchUnmatched( + workers.map(subscriptionOf), + dateAt('2026-08-03T10:01:00Z'), + ); + expect(repeatMatch.deliveriesCreated).toBe(0); + const lateWorkerMatch = yield* repository.matchUnmatched( + [...workers, makeWorker('consumer.late')].map(subscriptionOf), + dateAt('2026-08-03T10:02:00Z'), + ); + expect(lateWorkerMatch.deliveriesCreated).toBe(0); + const deliveries = yield* database + .select() + .from(outboxDeliveries) + .innerJoin( + outboxMessages, + eq(outboxMessages.outboxMessageId, outboxDeliveries.outboxMessageId), + ) + .where(eq(outboxMessages.tenantId, tenantId)); + expect(deliveries.length).toBe(2); + expect(deliveries.map((row) => row.outbox_deliveries.workerKey).toSorted()).toEqual([ 'consumer.alpha', 'consumer.beta', ]); - const messages = await runEffectTestPromise( - database - .select({ matchedAt: outboxMessages.matchedAt }) - .from(outboxMessages) - .where(eq(outboxMessages.tenantId, tenantId)), - ); - assert.equal( - messages.every(({ matchedAt }) => matchedAt !== null), - true, - ); - }); -}); - -void test('matches the complete subscription catalog before owner-local processes claim work', async () => { - await withTenant(async (database, tenantId) => { - await runEffectTestPromise(activateConsumer(database, tenantId)); - await runEffectTestPromise( - database.insert(tenantModuleStates).values({ - moduleKey: 'reporting', - state: 'active', - tenantId, - }), - ); - await insertMessage(database, tenantId); + const messages = yield* database + .select({ matchedAt: outboxMessages.matchedAt }) + .from(outboxMessages) + .where(eq(outboxMessages.tenantId, tenantId)); + expect(messages.every(({ matchedAt }) => matchedAt !== null)).toBe(true); + }), +); +it.live('matches the complete subscription catalog before owner-local processes claim work', () => + Effect.gen(function* matchesTheCompleteSubscriptionCatalog() { + const { database, tenantId } = yield* tenantFixture; + yield* activateConsumer(database, tenantId); + yield* database.insert(tenantModuleStates).values({ + moduleKey: 'reporting', + state: 'active', + tenantId, + }); + yield* insertMessage(database, tenantId); const consumerWorker = makeWorker('consumer.local'); const reportingWorker = makeWorker('reporting.local', { consumerModuleKey: 'reporting', }); const repository = makeOutboxRepository(database); const subscriptions = [consumerWorker, reportingWorker].map(subscriptionOf); - - const matched = await runEffectTestPromise( - repository.matchUnmatched(subscriptions, dateAt('2026-08-03T10:00:00Z')), - ); - assert.equal(matched.deliveriesCreated, 2); - - const claimAt = await runEffectTestPromise(DateTime.nowAsDate); + const matched = yield* repository.matchUnmatched(subscriptions, dateAt('2026-08-03T10:00:00Z')); + expect( + matched.deliveriesCreated, + `Catalog matcher batch processed ${matched.messagesMatched} unmatched messages`, + ).toBe(2); + const claimAt = yield* DateTime.nowAsDate; const consumerClaim = Option.getOrNull( - await claimNext(repository, [consumerWorker], 'consumer-process', claimAt), + yield* repository.claimNext([consumerWorker], 'consumer-process', claimAt), ); const reportingClaim = Option.getOrNull( - await claimNext(repository, [reportingWorker], 'reporting-process', claimAt), - ); - assert.equal(consumerClaim?.workerKey, 'consumer.local'); - assert.equal(reportingClaim?.workerKey, 'reporting.local'); - }); -}); - -void test('gates claims on every non-active consumer state and permits one concurrent live claim', async () => { - await withTenant(async (database, tenantId) => { - await insertMessage(database, tenantId); - const registration = makeWorker('consumer.module-gated'); - const repository = makeOutboxRepository(database); - await runEffectTestPromise( - repository.matchUnmatched([subscriptionOf(registration)], dateAt('2026-08-03T11:00:00Z')), - ); - const claimAt = advanceDate(await runEffectTestPromise(DateTime.nowAsDate), 1000); - assert.equal( - Option.getOrNull(await claimNext(repository, [registration], 'runtime-a', claimAt)), - null, - ); - await runEffectTestPromise(activateConsumer(database, tenantId, 'inactive')); - await forEachSequential( - ['inactive', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived'] as const, - async (state) => { - await runEffectTestPromise( - database - .update(tenantModuleStates) - .set({ state }) - .where( - and( - eq(tenantModuleStates.tenantId, tenantId), - eq(tenantModuleStates.moduleKey, 'consumer'), + yield* repository.claimNext([reportingWorker], 'reporting-process', claimAt), + ); + expect(consumerClaim?.workerKey).toBe('consumer.local'); + expect(reportingClaim?.workerKey).toBe('reporting.local'); + }), +); +it.live( + 'gates claims on every non-active consumer state and permits one concurrent live claim', + () => + Effect.gen(function* gatesClaimsOnEveryNonactive() { + const { database, tenantId } = yield* tenantFixture; + yield* insertMessage(database, tenantId); + const registration = makeWorker('consumer.module-gated'); + const repository = makeOutboxRepository(database); + const matched = yield* repository.matchUnmatched( + [subscriptionOf(registration)], + dateAt('2026-08-03T11:00:00Z'), + ); + expect( + matched.deliveriesCreated, + `Module-gated matcher batch processed ${matched.messagesMatched} unmatched messages`, + ).toBe(1); + const claimAt = advanceDate(yield* DateTime.nowAsDate, 1000); + expect( + Option.getOrNull(yield* repository.claimNext([registration], 'runtime-a', claimAt)), + ).toBe(null); + yield* activateConsumer(database, tenantId, 'inactive'); + yield* pipe( + ['inactive', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived'] as const, + Effect.forEach((state) => + Effect.gen(function* checksInactiveState() { + yield* database + .update(tenantModuleStates) + .set({ state }) + .where( + and( + eq(tenantModuleStates.tenantId, tenantId), + eq(tenantModuleStates.moduleKey, 'consumer'), + ), + ); + expect( + Option.getOrNull( + yield* repository.claimNext([registration], `runtime-${state}`, claimAt), ), - ), - ); - assert.equal( - Option.getOrNull( - await claimNext(repository, [registration], `runtime-${state}`, claimAt), - ), - null, - ); - }, - ); - await runEffectTestPromise( - database + ).toBe(null); + }), + ), + ); + yield* database .update(tenantModuleStates) .set({ state: 'active' }) .where( @@ -376,215 +305,211 @@ void test('gates claims on every non-active consumer state and permits one concu eq(tenantModuleStates.tenantId, tenantId), eq(tenantModuleStates.moduleKey, 'consumer'), ), + ); + const claimOptions = yield* Effect.all( + [ + repository.claimNext([registration], 'runtime-a', claimAt), + repository.claimNext([registration], 'runtime-b', claimAt), + ], + { concurrency: 'unbounded' }, + ); + const claims = claimOptions.map(Option.getOrNull); + expect(claims.filter((candidate) => candidate !== null).length).toBe(1); + const claimed = Option.getOrThrow( + Option.fromNullishOr(claims.find((candidate) => candidate !== null)), + ); + expect(claimed).toBeDefined(); + const attempt = Option.getOrThrow( + Option.fromNullishOr( + (yield* database + .select() + .from(outboxAttempts) + .where(eq(outboxAttempts.outboxDeliveryId, claimed.deliveryId)))[0], ), - ); - const claimOptions = await Promise.all([ - claimNext(repository, [registration], 'runtime-a', claimAt), - claimNext(repository, [registration], 'runtime-b', claimAt), - ]); - const claims = claimOptions.map(Option.getOrNull); - assert.equal(claims.filter((candidate) => candidate !== null).length, 1); - const claimed = claims.find((candidate) => candidate !== null); - assert.ok(claimed); - const [attempt] = await runEffectTestPromise( - database - .select() - .from(outboxAttempts) - .where(eq(outboxAttempts.outboxDeliveryId, claimed.deliveryId)), - ); - assert.ok(attempt); - assert.equal(attempt.finishedAt, null); - }); -}); - -void test('reclaims only expired leases, abandons the old attempt, and rejects stale finalization', async () => { - await withTenant(async (database, tenantId) => { - const { - now: started, - registration, - repository, - } = await matchedWorker(database, tenantId, 'consumer.lease-proof'); - const first = Option.getOrNull( - await claimNext(repository, [registration], 'runtime-a', started), - ); - assert.ok(first); - assert.equal( - Option.getOrNull( - await claimNext(repository, [registration], 'runtime-b', advanceDate(started, 999)), - ), - null, - ); - const second = Option.getOrNull( - await claimNext(repository, [registration], 'runtime-b', advanceDate(started, 1001)), - ); - assert.ok(second); - assert.notEqual(second.claimId, first.claimId); - await assert.rejects( - runEffectTestPromise(repository.complete(first, advanceDate(started, 1002))), - Schema.is(OutboxClaimLostError), - ); - const attempts = await runEffectTestPromise( - database + ); + expect(attempt).toBeDefined(); + expect(attempt.finishedAt).toBe(null); + }), +); +it.live( + 'reclaims only expired leases, abandons the old attempt, and rejects stale finalization', + () => + Effect.gen(function* reclaimsOnlyExpiredLeasesAbandons() { + const { database, tenantId } = yield* tenantFixture; + const { + now: started, + registration, + repository, + } = yield* matchedWorker(database, tenantId, 'consumer.lease-proof'); + const first = Option.getOrThrow( + yield* repository.claimNext([registration], 'runtime-a', started), + ); + expect(first).toBeDefined(); + expect( + Option.getOrNull( + yield* repository.claimNext([registration], 'runtime-b', advanceDate(started, 999)), + ), + ).toBe(null); + const second = Option.getOrThrow( + yield* repository.claimNext([registration], 'runtime-b', advanceDate(started, 1001)), + ); + expect(second).toBeDefined(); + expect(second.claimId).not.toBe(first.claimId); + expect( + Schema.is(OutboxClaimLostError)( + yield* Effect.flip(repository.complete(first, advanceDate(started, 1002))), + ), + ).toBe(true); + const attempts = yield* database .select() .from(outboxAttempts) .where(eq(outboxAttempts.outboxDeliveryId, first.deliveryId)) - .orderBy(asc(outboxAttempts.startedAt)), - ); - assert.equal(attempts.length, 2); - assert.equal(attempts[0]?.errorMessage, 'Outbox Worker lease expired before completion'); - assert.ok(attempts[0]?.finishedAt); - assert.equal(attempts[1]?.finishedAt, null); - }); -}); - -void test('finishes an abandoned final attempt before dead-lettering its expired delivery', async () => { - await withTenant(async (database, tenantId) => { + .orderBy(asc(outboxAttempts.startedAt)); + expect(attempts.length).toBe(2); + expect(attempts[0]?.errorMessage).toBe('Outbox Worker lease expired before completion'); + expect(Option.isSome(Option.fromNullishOr(attempts[0]?.finishedAt))).toBe(true); + expect(attempts[1]?.finishedAt).toBe(null); + }), +); +it.live('finishes an abandoned final attempt before dead-lettering its expired delivery', () => + Effect.gen(function* finishesAnAbandonedFinalAttempt() { + const { database, tenantId } = yield* tenantFixture; const { now: started, registration, repository, - } = await matchedWorker(database, tenantId, 'consumer.final-lease', { maxAttempts: 1 }); - const claim = Option.getOrNull( - await claimNext(repository, [registration], 'runtime-a', started), + } = yield* matchedWorker(database, tenantId, 'consumer.final-lease', { maxAttempts: 1 }); + const claim = Option.getOrThrow( + yield* repository.claimNext([registration], 'runtime-a', started), ); - assert.ok(claim); - - assert.equal( + expect(claim).toBeDefined(); + expect( Option.getOrNull( - await claimNext(repository, [registration], 'runtime-b', advanceDate(started, 1001)), + yield* repository.claimNext([registration], 'runtime-b', advanceDate(started, 1001)), ), - null, - ); - const [delivery] = await runEffectTestPromise( - database - .select() - .from(outboxDeliveries) - .where(eq(outboxDeliveries.outboxDeliveryId, claim.deliveryId)), - ); - const [attempt] = await runEffectTestPromise( - database - .select() - .from(outboxAttempts) - .where(eq(outboxAttempts.outboxDeliveryId, claim.deliveryId)), - ); - assert.equal(delivery?.status, 'dead'); - assert.equal(attempt?.errorMessage, 'Outbox Worker lease expired before completion'); - assert.ok(attempt?.finishedAt); - }); -}); - -void test('finalizes success atomically and advances only through contiguous done deliveries', async () => { - await withTenant(async (database, tenantId) => { - const { - messages: [firstMessage, secondMessage], - now, - registration, - repository, - } = await matchedWorker(database, tenantId, 'consumer.checkpoint-proof', { messages: 2 }); - assert.ok(firstMessage); - assert.ok(secondMessage); - const first = Option.getOrNull(await claimNext(repository, [registration], 'runtime-a', now)); - const second = Option.getOrNull(await claimNext(repository, [registration], 'runtime-b', now)); - assert.ok(first); - assert.ok(second); - await runEffectTestPromise(repository.complete(second, advanceDate(now, 1))); - assert.deepEqual( - await runEffectTestPromise( - database.select().from(workerCheckpoints).where(eq(workerCheckpoints.tenantId, tenantId)), + ).toBe(null); + const [delivery] = yield* database + .select() + .from(outboxDeliveries) + .where(eq(outboxDeliveries.outboxDeliveryId, claim.deliveryId)); + const attempt = Option.getOrThrow( + Option.fromNullishOr( + (yield* database + .select() + .from(outboxAttempts) + .where(eq(outboxAttempts.outboxDeliveryId, claim.deliveryId)))[0], ), - [], - ); - await runEffectTestPromise(repository.complete(first, advanceDate(now, 2))); - const [checkpoint] = await runEffectTestPromise( - database.select().from(workerCheckpoints).where(eq(workerCheckpoints.tenantId, tenantId)), - ); - assert.ok(checkpoint); - assert.equal(checkpoint.consumerName, registration.descriptor.workerKey); - assert.equal(checkpoint.streamKey, 'producer:producer.message-created'); - assert.equal(checkpoint.lastTenantSequenceNo, secondMessage.tenantSequenceNo); - assert.ok(checkpoint.lastTenantSequenceNo > firstMessage.tenantSequenceNo); - const deliveries = await runEffectTestPromise( - database - .select() - .from(outboxDeliveries) - .innerJoin( - outboxMessages, - eq(outboxMessages.outboxMessageId, outboxDeliveries.outboxMessageId), - ) - .where(eq(outboxMessages.tenantId, tenantId)), - ); - assert.equal( - deliveries.every((row) => row.outbox_deliveries.status === 'done'), - true, - ); - assert.equal( - deliveries.every((row) => row.outbox_deliveries.claimedBy === null), - true, ); - }); -}); - -void test('schedules bounded retry, dead-letters exhaustion, stores safe errors, and never checkpoints failure', async () => { - await withTenant(async (database, tenantId) => { - const { now, registration, repository } = await matchedWorker( + expect(delivery?.status).toBe('dead'); + expect(attempt?.errorMessage).toBe('Outbox Worker lease expired before completion'); + expect(Option.isSome(Option.fromNullishOr(attempt?.finishedAt))).toBe(true); + }), +); +it.live('finalizes success atomically and advances only through contiguous done deliveries', () => + Effect.gen(function* finalizesSuccessAtomicallyAndAdvances() { + const { database, tenantId } = yield* tenantFixture; + const { messages, now, registration, repository } = yield* matchedWorker( database, tenantId, - 'consumer.retry-proof', - { maxAttempts: 2 }, - ); - const first = Option.getOrNull(await claimNext(repository, [registration], 'runtime-a', now)); - assert.ok(first); - assert.equal( - await runEffectTestPromise( - repository.fail(first, ' safe\nretry\tmessage ', advanceDate(now, 1)), - ), - 'pending', - ); - assert.equal( - Option.getOrNull( - await claimNext(repository, [registration], 'runtime-b', advanceDate(now, 999)), - ), - null, - ); - const second = Option.getOrNull( - await claimNext(repository, [registration], 'runtime-b', advanceDate(now, 1001)), - ); - assert.ok(second); - assert.equal( - await runEffectTestPromise( - repository.fail(second, 'terminal safe failure', advanceDate(now, 1002)), + 'consumer.checkpoint-proof', + { messages: 2 }, + ); + const firstMessage = Option.getOrThrow(Option.fromNullishOr(messages[0])); + const secondMessage = Option.getOrThrow(Option.fromNullishOr(messages[1])); + const first = Option.getOrThrow(yield* repository.claimNext([registration], 'runtime-a', now)); + const second = Option.getOrThrow(yield* repository.claimNext([registration], 'runtime-b', now)); + expect(first).toBeDefined(); + expect(second).toBeDefined(); + yield* repository.complete(second, advanceDate(now, 1)); + expect( + yield* database + .select() + .from(workerCheckpoints) + .where(eq(workerCheckpoints.tenantId, tenantId)), + ).toEqual([]); + yield* repository.complete(first, advanceDate(now, 2)); + const checkpoint = Option.getOrThrow( + Option.fromNullishOr( + (yield* database + .select() + .from(workerCheckpoints) + .where(eq(workerCheckpoints.tenantId, tenantId)))[0], ), - 'dead', ); - const [delivery] = await runEffectTestPromise( - database + expect(checkpoint).toBeDefined(); + expect(checkpoint.consumerName).toBe(registration.descriptor.workerKey); + expect(checkpoint.streamKey).toBe('producer:producer.message-created'); + expect(checkpoint.lastTenantSequenceNo).toBe(secondMessage.tenantSequenceNo); + expect( + Option.getOrThrow(Option.fromNullishOr(checkpoint.lastTenantSequenceNo)) > + firstMessage.tenantSequenceNo, + ).toBe(true); + const deliveries = yield* database + .select() + .from(outboxDeliveries) + .innerJoin( + outboxMessages, + eq(outboxMessages.outboxMessageId, outboxDeliveries.outboxMessageId), + ) + .where(eq(outboxMessages.tenantId, tenantId)); + expect(deliveries.every((row) => row.outbox_deliveries.status === 'done')).toBe(true); + expect(deliveries.every((row) => row.outbox_deliveries.claimedBy === null)).toBe(true); + }), +); +it.live( + 'schedules bounded retry, dead-letters exhaustion, stores safe errors, and never checkpoints failure', + () => + Effect.gen(function* schedulesBoundedRetryDeadlettersExhaustion() { + const { database, tenantId } = yield* tenantFixture; + const { now, registration, repository } = yield* matchedWorker( + database, + tenantId, + 'consumer.retry-proof', + { maxAttempts: 2 }, + ); + const first = Option.getOrThrow( + yield* repository.claimNext([registration], 'runtime-a', now), + ); + expect(first).toBeDefined(); + expect(yield* repository.fail(first, ' safe\nretry\tmessage ', advanceDate(now, 1))).toBe( + 'pending', + ); + expect( + Option.getOrNull( + yield* repository.claimNext([registration], 'runtime-b', advanceDate(now, 999)), + ), + ).toBe(null); + const second = Option.getOrThrow( + yield* repository.claimNext([registration], 'runtime-b', advanceDate(now, 1001)), + ); + expect(second).toBeDefined(); + expect(yield* repository.fail(second, 'terminal safe failure', advanceDate(now, 1002))).toBe( + 'dead', + ); + const [delivery] = yield* database .select() .from(outboxDeliveries) - .where(eq(outboxDeliveries.outboxDeliveryId, second.deliveryId)), - ); - assert.equal(delivery?.status, 'dead'); - assert.equal(delivery?.attemptsCount, 2); - const attempts = await runEffectTestPromise( - database + .where(eq(outboxDeliveries.outboxDeliveryId, second.deliveryId)); + expect(delivery?.status).toBe('dead'); + expect(delivery?.attemptsCount).toBe(2); + const attempts = yield* database .select() .from(outboxAttempts) .where(eq(outboxAttempts.outboxDeliveryId, second.deliveryId)) - .orderBy(asc(outboxAttempts.startedAt)), - ); - assert.deepEqual( - attempts.map(({ errorMessage }) => errorMessage), - ['safe retry message', 'terminal safe failure'], - ); - assert.deepEqual( - await runEffectTestPromise( - database.select().from(workerCheckpoints).where(eq(workerCheckpoints.tenantId, tenantId)), - ), - [], - ); - }); -}); - -void test('keeps test descriptor arrays compatible with the erased startup registry surface', () => { + .orderBy(asc(outboxAttempts.startedAt)); + expect(attempts.map(({ errorMessage }) => errorMessage)).toEqual([ + 'safe retry message', + 'terminal safe failure', + ]); + expect( + yield* database + .select() + .from(workerCheckpoints) + .where(eq(workerCheckpoints.tenantId, tenantId)), + ).toEqual([]); + }), +); +it('keeps test descriptor arrays compatible with the erased startup registry surface', () => { const registry: readonly AnyOutboxWorkerRegistration[] = [makeWorker('consumer.registry-proof')]; - assert.equal(registry[0]?.descriptor.workerKey, 'consumer.registry-proof'); + expect(registry[0]?.descriptor.workerKey).toBe('consumer.registry-proof'); }); diff --git a/app/packages/core-runtime/tests/integration/pool-deadlines.test.ts b/app/packages/core-runtime/tests/integration/pool-deadlines.test.ts index 1c84dffe3..e74342f70 100644 --- a/app/packages/core-runtime/tests/integration/pool-deadlines.test.ts +++ b/app/packages/core-runtime/tests/integration/pool-deadlines.test.ts @@ -1,94 +1,89 @@ -import assert from 'node:assert/strict'; -// @effect-diagnostics asyncFunction:off -- node:test and pg expose Promise-based integration seams. remove-when: shared Effect-native test and driver APIs land. -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Effect, Redacted } from 'effect'; import { Pool } from 'pg'; -import type { DatabaseError, PoolClient } from 'pg'; +import type { PoolClient } from 'pg'; import { loadDatabaseConfig } from '../../src/db/config.ts'; import { configureDatabasePool } from '../../src/db/pool-configuration.ts'; -type PostgresError = Pick; - -const hasSqlState = - (code: string) => - (error: PostgresError): boolean => - error.code === code; - -const hasTimeoutMessage = (error: PostgresError): boolean => /timeout/iu.test(error.message); - -const rollbackAndRelease = async (client: PoolClient | undefined): Promise => { - if (client === undefined) { - return; - } - await Promise.allSettled([client.query('rollback')]); - client.release(); -}; - -void test('applies PostgreSQL pool connection and statement deadlines', async () => { - // eslint-disable-next-line effect-native/no-effect-run-in-tests -- PostgreSQL integration tests bridge Effect configuration into node:test. remove-when: the shared itEffect/itLayer harness lands. - const { databaseConfiguration, poolConfiguration } = await Effect.runPromise( - Effect.gen(function* loadPoolConfiguration() { - const database = yield* loadDatabaseConfig(); - const pool = yield* configureDatabasePool(Redacted.make(database.connectionString), { - connectionTimeoutMillis: 200, - statement_timeout: 120, - }); - return { databaseConfiguration: database, poolConfiguration: pool }; - }), +const rollbackAndRelease = (client: PoolClient) => + Effect.tryPromise(() => client.query('rollback')).pipe( + Effect.ignore, + Effect.ensuring(Effect.sync(() => client.release())), ); - const pool = new Pool({ ...poolConfiguration, max: 1 }); - const blocker = new Pool({ ...poolConfiguration, max: 1 }); - try { - const client = await pool.connect(); - try { - const settings = await client.query<{ statement_timeout: string }>('show statement_timeout'); - assert.equal(settings.rows[0]?.statement_timeout, '120ms'); +it.live('applies PostgreSQL pool connection and statement deadlines', () => + Effect.gen(function* poolDeadlines1() { + const databaseConfiguration = yield* loadDatabaseConfig(); + const poolConfiguration = yield* configureDatabasePool( + Redacted.make(databaseConfiguration.connectionString), + { connectionTimeoutMillis: 200, statement_timeout: 120 }, + ); + const acquirePool = Effect.acquireRelease( + Effect.sync(() => new Pool({ ...poolConfiguration, max: 1 })), + (resource) => Effect.promise(() => resource.end()).pipe(Effect.orDie), + ); + const pool = yield* acquirePool; + const blocker = yield* acquirePool; - const identity = await client.query<{ current_user: string }>('select current_user'); - assert.equal(identity.rows[0]?.current_user, databaseConfiguration.user); + yield* Effect.scoped( + Effect.gen(function* poolDeadlines2() { + const client = yield* Effect.acquireRelease( + Effect.promise(() => pool.connect()), + (connection) => Effect.sync(() => connection.release()), + ); + const settings = yield* Effect.promise(() => + client.query<{ statement_timeout: string }>('show statement_timeout'), + ); + expect(settings.rows[0]?.statement_timeout).toBe('120ms'); - const pidResult = await client.query<{ pid: number }>('select pg_backend_pid() as pid'); - const pid = pidResult.rows[0]?.pid; - assert.ok(pid !== undefined); + const identity = yield* Effect.promise(() => + client.query<{ current_user: string }>('select current_user'), + ); + expect(identity.rows[0]?.current_user).toBe(databaseConfiguration.user); - await assert.rejects(client.query('select pg_sleep(1)'), hasSqlState('57014')); + const pidResult = yield* Effect.promise(() => + client.query<{ pid: number }>('select pg_backend_pid() as pid'), + ); + const pid = pidResult.rows[0]?.pid; + expect(pid !== undefined).toBe(true); - const afterCancellation = await client.query<{ ok: number; pid: number }>( - 'select pg_backend_pid() as pid, 1 as ok', - ); - assert.equal(afterCancellation.rows[0]?.pid, pid); - assert.equal(afterCancellation.rows[0]?.ok, 1); + const cancellation = yield* Effect.flip( + Effect.tryPromise(() => client.query('select pg_sleep(1)')), + ); + expect(cancellation.cause).toMatchObject({ code: '57014' }); - await assert.rejects(pool.connect(), hasTimeoutMessage); - } finally { - client.release(); - } + const afterCancellation = yield* Effect.promise(() => + client.query<{ ok: number; pid: number }>('select pg_backend_pid() as pid, 1 as ok'), + ); + expect(afterCancellation.rows[0]?.pid).toBe(pid); + expect(afterCancellation.rows[0]?.ok).toBe(1); - const holder = await blocker.connect(); - let waiter: PoolClient | undefined; - try { - await holder.query('begin'); - await holder.query('select pg_advisory_xact_lock(424242)'); + const timeout = yield* Effect.flip(Effect.tryPromise(() => pool.connect())); + expect(timeout.cause).toMatchObject({ message: expect.stringMatching(/timeout/iu) }); + }), + ); - waiter = await pool.connect(); - await waiter.query('begin'); - await assert.rejects( - waiter.query('select pg_advisory_xact_lock(424242)'), - hasSqlState('57014'), - ); + const holder = yield* Effect.acquireRelease( + Effect.promise(() => blocker.connect()), + rollbackAndRelease, + ); + yield* Effect.promise(() => holder.query('begin')); + yield* Effect.promise(() => holder.query('select pg_advisory_xact_lock(424242)')); - await waiter.query('rollback'); - await holder.query('rollback'); + const waiter = yield* Effect.acquireRelease( + Effect.promise(() => pool.connect()), + rollbackAndRelease, + ); + yield* Effect.promise(() => waiter.query('begin')); + const lockTimeout = yield* Effect.flip( + Effect.tryPromise(() => waiter.query('select pg_advisory_xact_lock(424242)')), + ); + expect(lockTimeout.cause).toMatchObject({ code: '57014' }); - await waiter.query('begin'); - await waiter.query('select pg_advisory_xact_lock(424242)'); - await waiter.query('rollback'); - } finally { - await rollbackAndRelease(waiter); - await rollbackAndRelease(holder); - } - } finally { - await Promise.allSettled([pool.end(), blocker.end()]); - } -}); + yield* Effect.promise(() => waiter.query('rollback')); + yield* Effect.promise(() => holder.query('rollback')); + yield* Effect.promise(() => waiter.query('begin')); + yield* Effect.promise(() => waiter.query('select pg_advisory_xact_lock(424242)')); + yield* Effect.promise(() => waiter.query('rollback')); + }), +); diff --git a/app/packages/core-runtime/tests/integration/principal-management.test.ts b/app/packages/core-runtime/tests/integration/principal-management.test.ts index 4a386080f..5ebd125b5 100644 --- a/app/packages/core-runtime/tests/integration/principal-management.test.ts +++ b/app/packages/core-runtime/tests/integration/principal-management.test.ts @@ -1,14 +1,9 @@ -import { - makeEffectTestCallback as nativeTestCallback, - runEffectTestPromise, -} from '@app/core-runtime/testing/effect-runtime'; +import { purgeFixtureRows } from '../support/fixture-cleanup.ts'; +import { expect, it } from 'effect-rstest'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. import { eq } from 'drizzle-orm'; -import { Effect, Exit as NativeExit, Predicate, Scope as NativeScope } from 'effect'; -import assert from 'node:assert/strict'; +import { Effect, Predicate, Schema } from 'effect'; import { randomUUID } from 'node:crypto'; -import test, { after as afterNativeDatabase } from 'node:test'; import { Pool } from 'pg'; import { bindApiKey, @@ -20,47 +15,36 @@ import { import { loadDatabaseConfig } from '../../src/db/config.ts'; import { coreRelations, principalAuthBindings, principals, tenants } from '../../src/db/schema.ts'; import { makeTestDatabaseFromPool } from '../support/database.ts'; -import { purgeFixtureRows } from '../support/fixture-cleanup.ts'; -import { runEffectTestSync as runNativeSync } from '../support/effect-runtime.ts'; -const nativeDatabaseScope = runNativeSync(NativeScope.make()); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), -); - -void test('persists managed key lifecycle without credential material and enforces global key cardinality', async () => { - const tenantId = randomUUID(); - const providerKeyId = `better-auth-principal-management-${randomUUID()}`; - const configuration = await runEffectTestPromise(loadDatabaseConfig()); - const pool = new Pool({ connectionString: configuration.connectionString }); - const database = await runEffectTestPromise( - makeTestDatabaseFromPool(pool, coreRelations).pipe(NativeScope.provide(nativeDatabaseScope)), - ); - const cleanup = async () => { - await runEffectTestPromise( - purgeFixtureRows([ +it.live( + 'persists managed key lifecycle without credential material and enforces global key cardinality', + () => + Effect.gen(function* principalManagement1() { + const tenantId = randomUUID(); + const providerKeyId = `better-auth-principal-management-${randomUUID()}`; + const configuration = yield* loadDatabaseConfig(); + const pool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: configuration.connectionString })), + (ownedPool) => Effect.promise(() => ownedPool.end()).pipe(Effect.orDie), + ); + const database = yield* makeTestDatabaseFromPool(pool, coreRelations); + const cleanup = purgeFixtureRows([ database .delete(principalAuthBindings) .where(eq(principalAuthBindings.providerSubjectId, providerKeyId)), database.delete(principals).where(eq(principals.tenantId, tenantId)), database.delete(tenants).where(eq(tenants.tenantId, tenantId)), - ]), - ); - }; + ]); - try { - await cleanup(); - await runEffectTestPromise( - database.insert(tenants).values({ + yield* Effect.acquireRelease(cleanup, () => cleanup.pipe(Effect.orDie)); + yield* database.insert(tenants).values({ defaultLocale: 'en', name: 'Principal management integration', slug: `principal-management-${tenantId}`, status: 'active', tenantId, - }), - ); - const first = await runEffectTestPromise( - database.transaction((transaction) => + }); + const first = yield* database.transaction((transaction) => createNonHumanPrincipal({ displayName: 'Managed integration', kind: 'integration', @@ -71,10 +55,8 @@ void test('persists managed key lifecycle without credential material and enforc principalManagementRepositoryFromTransaction(transaction), ), ), - ), - ); - const second = await runEffectTestPromise( - database.transaction((transaction) => + ); + const second = yield* database.transaction((transaction) => createNonHumanPrincipal({ displayName: 'Managed service', kind: 'service', @@ -85,10 +67,8 @@ void test('persists managed key lifecycle without credential material and enforc principalManagementRepositoryFromTransaction(transaction), ), ), - ), - ); - const binding = await runEffectTestPromise( - database.transaction((transaction) => + ); + const binding = yield* database.transaction((transaction) => bindApiKey({ managed: true, principalId: first.principalId, @@ -100,10 +80,8 @@ void test('persists managed key lifecycle without credential material and enforc principalManagementRepositoryFromTransaction(transaction), ), ), - ), - ); - const duplicate = await runEffectTestPromise( - database.transaction((transaction) => + ); + const duplicate = yield* database.transaction((transaction) => Effect.flip( bindApiKey({ managed: true, @@ -117,12 +95,10 @@ void test('persists managed key lifecycle without credential material and enforc ), ), ), - ), - ); - assert.ok(Predicate.isTagged(duplicate, 'IdentityLifecycleConflictError')); + ); + expect(Predicate.isTagged(duplicate, 'IdentityLifecycleConflictError')).toBe(true); - const missingReason = await runEffectTestPromise( - database.transaction((transaction) => + const missingReason = yield* database.transaction((transaction) => Effect.flip( setApiKeyBindingStatus({ authBindingId: binding.authBindingId, @@ -138,12 +114,10 @@ void test('persists managed key lifecycle without credential material and enforc ), ), ), - ), - ); - assert.ok(Predicate.isTagged(missingReason, 'IdentityTargetInvalidError')); + ); + expect(Predicate.isTagged(missingReason, 'IdentityTargetInvalidError')).toBe(true); - await runEffectTestPromise( - database.transaction((transaction) => + yield* database.transaction((transaction) => setApiKeyBindingStatus({ authBindingId: binding.authBindingId, expectedStatus: 'active', @@ -158,18 +132,16 @@ void test('persists managed key lifecycle without credential material and enforc principalManagementRepositoryFromTransaction(transaction), ), ), - ), - ); - const [stored] = await runEffectTestPromise( - database + ); + const [stored] = yield* database .select() .from(principalAuthBindings) - .where(eq(principalAuthBindings.principalAuthBindingId, binding.authBindingId)), - ); - assert.equal(stored?.status, 'revoked'); - assert.equal(JSON.stringify(stored).includes('secret'), false); - } finally { - await cleanup(); - await pool.end(); - } -}); + .where(eq(principalAuthBindings.principalAuthBindingId, binding.authBindingId)); + expect(stored?.status).toBe('revoked'); + expect( + (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(stored)).includes( + 'secret', + ), + ).toBe(false); + }), +); diff --git a/app/packages/core-runtime/tests/integration/principal-resolver.test.ts b/app/packages/core-runtime/tests/integration/principal-resolver.test.ts index 8b4e9a278..063873767 100644 --- a/app/packages/core-runtime/tests/integration/principal-resolver.test.ts +++ b/app/packages/core-runtime/tests/integration/principal-resolver.test.ts @@ -1,9 +1,7 @@ -import { makeEffectTestCallback } from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from 'effect-rstest'; import { and, eq } from 'drizzle-orm'; -import { DateTime, Effect } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { DateTime, Effect, Predicate } from 'effect'; import { makePrincipalResolver } from '../../src/auth/principal-resolver.ts'; import { loadDatabaseConfig } from '../../src/db/config.ts'; import { principalAuthBindings, principals, tenants } from '../../src/db/schema.ts'; @@ -15,28 +13,28 @@ const principalOne = '20000000-0000-4000-8000-000000000001'; const principalTwo = '20000000-0000-4000-8000-000000000002'; const subject = 'better-auth-integration-subject'; -test( +it.live( 'lists and selects multiple tenant-scoped principals and fails closed after access changes', - Effect.gen(function* principalResolverIntegration() { - const configuration = yield* loadDatabaseConfig(); - const { executor: database } = yield* makeCoreDatabase(configuration); - const resolver = makePrincipalResolver({ executor: database }); - const cleanup = Effect.gen(function* cleanPrincipalResolverFixtures() { - yield* database - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.providerSubjectId, subject)); - yield* database - .delete(principals) - .where(and(eq(principals.principalId, principalOne), eq(principals.tenantId, tenantOne))); - yield* database - .delete(principals) - .where(and(eq(principals.principalId, principalTwo), eq(principals.tenantId, tenantTwo))); - yield* database.delete(tenants).where(eq(tenants.tenantId, tenantOne)); - yield* database.delete(tenants).where(eq(tenants.tenantId, tenantTwo)); - }); + () => + Effect.gen(function* principalResolverIntegration() { + const configuration = yield* loadDatabaseConfig(); + const { executor: database } = yield* makeCoreDatabase(configuration); + const resolver = makePrincipalResolver({ executor: database }); + const cleanup = Effect.gen(function* cleanPrincipalResolverFixtures() { + yield* database + .delete(principalAuthBindings) + .where(eq(principalAuthBindings.providerSubjectId, subject)); + yield* database + .delete(principals) + .where(and(eq(principals.principalId, principalOne), eq(principals.tenantId, tenantOne))); + yield* database + .delete(principals) + .where(and(eq(principals.principalId, principalTwo), eq(principals.tenantId, tenantTwo))); + yield* database.delete(tenants).where(eq(tenants.tenantId, tenantOne)); + yield* database.delete(tenants).where(eq(tenants.tenantId, tenantTwo)); + }); - yield* Effect.gen(function* exercisePrincipalResolver() { - yield* cleanup; + yield* Effect.acquireRelease(cleanup, () => cleanup.pipe(Effect.orDie)); yield* database.insert(tenants).values([ { defaultLocale: 'en', @@ -88,18 +86,18 @@ test( }, ]); - assert.deepEqual(yield* resolver.listAvailableTenants(subject), [ + expect(yield* resolver.listAvailableTenants(subject)).toEqual([ { name: 'Resolver tenant one', tenantId: tenantOne }, { name: 'Resolver tenant two', tenantId: tenantTwo }, ]); const resolvedOne = yield* resolver.resolveBetterAuthUserForTenant(subject, tenantOne); const resolvedTwo = yield* resolver.resolveBetterAuthUserForTenant(subject, tenantTwo); - assert.equal(resolvedOne.principalId, principalOne); - assert.equal(resolvedTwo.principalId, principalTwo); + expect(resolvedOne.principalId).toBe(principalOne); + expect(resolvedTwo.principalId).toBe(principalTwo); const foreignResolution = yield* Effect.flip( resolver.resolveBetterAuthUserForTenant('foreign-better-auth-subject', tenantOne), ); - assert.equal(foreignResolution._tag, 'PrincipalBindingMissingError'); + expect(Predicate.isTagged(foreignResolution, 'PrincipalBindingMissingError')).toBe(true); yield* database .update(principalAuthBindings) @@ -108,13 +106,13 @@ test( status: 'revoked', }) .where(eq(principalAuthBindings.tenantId, tenantOne)); - assert.deepEqual(yield* resolver.listAvailableTenants(subject), [ + expect(yield* resolver.listAvailableTenants(subject)).toEqual([ { name: 'Resolver tenant two', tenantId: tenantTwo }, ]); const revokedResolution = yield* Effect.flip( resolver.resolveBetterAuthUserForTenant(subject, tenantOne), ); - assert.equal(revokedResolution._tag, 'PrincipalBindingInactiveError'); + expect(Predicate.isTagged(revokedResolution, 'PrincipalBindingInactiveError')).toBe(true); yield* database .update(principalAuthBindings) @@ -127,7 +125,7 @@ test( const inactivePrincipal = yield* Effect.flip( resolver.resolveBetterAuthUserForTenant(subject, tenantOne), ); - assert.equal(inactivePrincipal._tag, 'PrincipalInactiveError'); + expect(Predicate.isTagged(inactivePrincipal, 'PrincipalInactiveError')).toBe(true); yield* database .update(principals) @@ -140,7 +138,6 @@ test( const inactiveTenant = yield* Effect.flip( resolver.resolveBetterAuthUserForTenant(subject, tenantOne), ); - assert.equal(inactiveTenant._tag, 'TenantInactiveError'); - }).pipe(Effect.ensuring(cleanup.pipe(Effect.orDie))); - }).pipe(Effect.scoped, makeEffectTestCallback), + expect(Predicate.isTagged(inactiveTenant, 'TenantInactiveError')).toBe(true); + }), ); diff --git a/app/packages/core-runtime/tests/integration/read-runtime.test.ts b/app/packages/core-runtime/tests/integration/read-runtime.test.ts index d0e833fba..d04c687d7 100644 --- a/app/packages/core-runtime/tests/integration/read-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/read-runtime.test.ts @@ -1,20 +1,12 @@ -import { - makeEffectTestCallback as nativeTestCallback, - runEffectTestPromise, -} from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from 'effect-rstest'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. import { getTableConfig } from 'drizzle-orm/pg-core'; -import { Effect, Exit as NativeExit, Scope as NativeScope, Schema } from 'effect'; -import assert from 'node:assert/strict'; +import { Effect, Schema } from 'effect'; import { randomUUID } from 'node:crypto'; -import test, { after as afterNativeDatabase } from 'node:test'; -import { Pool } from 'pg'; import { makeSystemPrincipalContextResolver, registerSystemWorkload, } from '../../src/auth/system-principal-context.ts'; -import { loadDatabaseConnectionPair } from '../../src/db/config.ts'; import { coreRelations, dataAccessEvents } from '../../src/db/schema.ts'; import { defineSystemModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; import { @@ -23,70 +15,76 @@ import { } from '../../src/operations/context.ts'; import { defineRead } from '../../src/reads/definition.ts'; import { makeReadRuntime } from '../../src/reads/runtime.ts'; -import { makeTestDatabaseFromPool } from '../support/database.ts'; -import { runEffectTestSync as runNativeSync } from '../support/effect-runtime.ts'; +import { makeTestDatabaseFromPool, testDatabasePools } from '../support/database.ts'; import { openModuleEntrypointGateway } from '../support/open-module-entrypoint-gateway.ts'; -const nativeDatabaseScope = runNativeSync(NativeScope.make()); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), -); - -void test('standalone governed-read evidence permits no Action invocation and requires outcome fields', () => { +it('standalone governed-read evidence permits no Action invocation and requires outcome fields', () => { const config = getTableConfig(dataAccessEvents); const column = (name: string) => config.columns.find((candidate) => candidate.name === name); - assert.equal(column('action_invocation_id')?.notNull, false); - assert.equal(column('outcome')?.notNull, true); - assert.equal(column('outcome_stage')?.notNull, true); - assert.equal(column('outcome_code')?.notNull, true); + expect(column('action_invocation_id')?.notNull).toBe(false); + expect(column('outcome')?.notNull).toBe(true); + expect(column('outcome_stage')?.notNull).toBe(true); + expect(column('outcome_code')?.notNull).toBe(true); }); -void test('commits live allowed evidence before releasing a governed read result', async () => { - const connections = await runEffectTestPromise(loadDatabaseConnectionPair()); - const admin = new Pool({ connectionString: connections.admin.connectionString }); - const runtimePool = new Pool({ connectionString: connections.runtime.connectionString }); - const runtimeDatabase = await runEffectTestPromise( - makeTestDatabaseFromPool(runtimePool, coreRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ), - ); - const tenantId = randomUUID(); - const principalId = randomUUID(); - const readKey = `core.shell.integration.${randomUUID()}`; - const correlationId = randomUUID(); - const registration = defineRead( - { - accessKind: 'list', - entrypoint: defineSystemModuleEntrypoint({ - access: 'read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: readKey, - moduleKey: 'core.shell', - role: 'api', - }), - evidencePolicy: { captureMode: 'metadata_only', policyKey: `${readKey}.v1` }, - inputSchema: Schema.Struct({}), - legalEntityScope: 'forbidden', - owningModuleKey: 'core.shell', - permissionTarget: 'module', - policies: [], - readKey, - resultSchema: Schema.Array(Schema.String), - schemaVersion: '1', - }, - () => Effect.succeed({ evidence: { resultCount: 1 }, result: ['visible'] }), - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }), - ); +it.live('commits live allowed evidence before releasing a governed read result', () => + Effect.gen(function* readRuntime1() { + const { admin, runtimePool } = yield* testDatabasePools; + const runtimeDatabase = yield* makeTestDatabaseFromPool(runtimePool, coreRelations); + const tenantId = randomUUID(); + const principalId = randomUUID(); + const readKey = `core.shell.integration.${randomUUID()}`; + const correlationId = randomUUID(); + const registration = defineRead( + { + accessKind: 'list', + entrypoint: defineSystemModuleEntrypoint({ + access: 'read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: readKey, + moduleKey: 'core.shell', + role: 'api', + }), + evidencePolicy: { captureMode: 'metadata_only', policyKey: `${readKey}.v1` }, + inputSchema: Schema.Struct({}), + legalEntityScope: 'forbidden', + owningModuleKey: 'core.shell', + permissionTarget: 'module', + policies: [], + readKey, + resultSchema: Schema.Array(Schema.String), + schemaVersion: '1', + }, + () => Effect.succeed({ evidence: { resultCount: 1 }, result: ['visible'] }), + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); - try { - await admin.query( - `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $2, 'Read runtime tenant', 'active', 'en')`, - [tenantId, `read-runtime-${tenantId}`], + yield* Effect.addFinalizer(() => + Effect.gen(function* readRuntime2() { + yield* Effect.promise(() => + admin.query('delete from core.data_access_events where tenant_id = $1', [tenantId]), + ); + yield* Effect.promise(() => + admin.query('delete from core.principals where tenant_id = $1', [tenantId]), + ); + yield* Effect.promise(() => + admin.query('delete from core.tenants where tenant_id = $1', [tenantId]), + ); + }).pipe(Effect.orDie), ); - await admin.query( - `insert into core.principals (principal_id, tenant_id, kind, display_name, status) values ($1, $2, 'system', 'Read runtime principal', 'active')`, - [principalId, tenantId], + + yield* Effect.promise(() => + admin.query( + `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $2, 'Read runtime tenant', 'active', 'en')`, + [tenantId, `read-runtime-${tenantId}`], + ), + ); + yield* Effect.promise(() => + admin.query( + `insert into core.principals (principal_id, tenant_id, kind, display_name, status) values ($1, $2, 'system', 'Read runtime principal', 'active')`, + [principalId, tenantId], + ), ); const contextAccess = { legalEntities: () => Effect.succeed([]), @@ -94,14 +92,14 @@ void test('commits live allowed evidence before releasing a governed read result resources: () => Effect.succeed([]), tenants: () => Effect.succeed([]), }; - const principal = await runEffectTestPromise( - makeSystemPrincipalContextResolver({ executor: runtimeDatabase }).resolve({ - principalId, - registration: registerSystemWorkload({ jobKey: 'read-runtime-integration' }), - runReference: readKey, - tenantId, - }), - ); + const principal = yield* makeSystemPrincipalContextResolver({ + executor: runtimeDatabase, + }).resolve({ + principalId, + registration: registerSystemWorkload({ jobKey: 'read-runtime-integration' }), + runReference: readKey, + tenantId, + }); const runtime = makeReadRuntime( { executor: runtimeDatabase }, openModuleEntrypointGateway, @@ -111,28 +109,27 @@ void test('commits live allowed evidence before releasing a governed read result ), contextAccess, ); - assert.deepEqual( - await runEffectTestPromise( - runtime.runRead({ - input: {}, - principal, - registration, - transport: { correlationId }, - }), + expect( + yield* runtime.runRead({ + input: {}, + principal, + registration, + transport: { correlationId }, + }), + ).toEqual(['visible']); + const evidence = yield* Effect.promise(() => + admin.query<{ + action_invocation_id: null; + outcome: string; + outcome_code: string; + query_hash: null; + result_count: number; + }>( + `select action_invocation_id, outcome, outcome_code, query_hash, result_count from core.data_access_events where tenant_id = $1 and evidence_policy_key = $2`, + [tenantId, `${readKey}.v1`], ), - ['visible'], - ); - const evidence = await admin.query<{ - action_invocation_id: null; - outcome: string; - outcome_code: string; - query_hash: null; - result_count: number; - }>( - `select action_invocation_id, outcome, outcome_code, query_hash, result_count from core.data_access_events where tenant_id = $1 and evidence_policy_key = $2`, - [tenantId, `${readKey}.v1`], ); - assert.deepEqual(evidence.rows, [ + expect(evidence.rows).toEqual([ { action_invocation_id: null, outcome: 'allowed', @@ -141,11 +138,5 @@ void test('commits live allowed evidence before releasing a governed read result result_count: 1, }, ]); - } finally { - await admin.query('delete from core.data_access_events where tenant_id = $1', [tenantId]); - await admin.query('delete from core.principals where tenant_id = $1', [tenantId]); - await admin.query('delete from core.tenants where tenant_id = $1', [tenantId]); - await runtimePool.end(); - await admin.end(); - } -}); + }), +); diff --git a/app/packages/core-runtime/tests/integration/search-persistence.test.ts b/app/packages/core-runtime/tests/integration/search-persistence.test.ts index bcaa9a978..41e5c2332 100644 --- a/app/packages/core-runtime/tests/integration/search-persistence.test.ts +++ b/app/packages/core-runtime/tests/integration/search-persistence.test.ts @@ -1,25 +1,12 @@ -import { - makeEffectTestCallback as nativeTestCallback, - makeEffectTestCallback, -} from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from 'effect-rstest'; -import { Effect, Function as Fn, Exit as NativeExit, Scope as NativeScope, Schema } from 'effect'; -import assert from 'node:assert/strict'; +import { Effect, Function as Fn, Schema, Predicate } from 'effect'; import { randomUUID } from 'node:crypto'; -import test, { after as afterNativeDatabase } from 'node:test'; -import type { QueryResult, QueryResultRow } from 'pg'; -import { Pool } from 'pg'; -import { loadDatabaseConnectionPair } from '../../src/db/config.ts'; +import type { Pool, QueryResult, QueryResultRow } from 'pg'; import { coreRelations } from '../../src/db/schema.ts'; import { makePostgresCoreSearchProjectionStore } from '../../src/search/persistence.ts'; import { createCoreSearchQueryRuntime } from '../../src/search/projection.ts'; -import { makeTestDatabaseFromPool } from '../support/database.ts'; -import { runEffectTestSync as runNativeSync } from '../support/effect-runtime.ts'; - -const nativeDatabaseScope = runNativeSync(NativeScope.make()); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), -); +import { makeTestDatabaseFromPool, testDatabasePools } from '../support/database.ts'; const queryEffect = ( client: Pool, @@ -29,106 +16,100 @@ const queryEffect = ( Effect.suspend(() => Effect.promise(Fn.constant(client.query(statement, [...(parameters ?? [])]))), ); -const endPool = (pool: Pool): Effect.Effect => - Effect.suspend(() => Effect.promise(Fn.constant(pool.end()))); const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); -const effectTest = (name: string, effect: Effect.Effect): void => { - test(name, makeEffectTestCallback(effect)); -}; -effectTest( +it.live( 'durably rebuilds tenant projections with tombstones and selected-Legal-Entity filtering', - Effect.gen(function* searchPersistenceIntegration() { - const connections = yield* loadDatabaseConnectionPair(); - const admin = new Pool({ connectionString: connections.admin.connectionString }); - const runtimePool = new Pool({ connectionString: connections.runtime.connectionString }); - const tenantId = randomUUID(); - const otherTenantId = randomUUID(); - const legalEntityId = randomUUID(); - const otherLegalEntityId = randomUUID(); - const partyId = randomUUID(); - const removedPartyId = randomUUID(); - const counterpartyId = randomUUID(); - const otherCounterpartyId = randomUUID(); - const aliasRef = { - moduleId: 'party.registry', - resourceId: randomUUID(), - resourceType: 'party.registry.party', - tenantId, - }; - const store = makePostgresCoreSearchProjectionStore({ - executor: yield* makeTestDatabaseFromPool(runtimePool, coreRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ), - }); - const search = createCoreSearchQueryRuntime(store); - const partyDocument = (resourceId: string, projectionVersion: string, title: string) => ({ - aliases: [ - { - kind: 'resource', - ref: aliasRef, - searchableText: ['Former Acme'], - temporalSearchableText: [ - { - validFrom: '2026-01-01T00:00:00Z', - validTo: '2026-02-01T00:00:00Z', - value: 'alias-private@example.test', - }, - ], - }, - ], - archived: false, - facets: [], - metadata: [], - projectionVersion, - ref: { + () => + Effect.gen(function* searchPersistenceIntegration() { + const { admin, runtimePool } = yield* testDatabasePools; + const tenantId = randomUUID(); + const otherTenantId = randomUUID(); + const legalEntityId = randomUUID(); + const otherLegalEntityId = randomUUID(); + const partyId = randomUUID(); + const removedPartyId = randomUUID(); + const counterpartyId = randomUUID(); + const otherCounterpartyId = randomUUID(); + const aliasRef = { moduleId: 'party.registry', - resourceId, + resourceId: randomUUID(), resourceType: 'party.registry.party', tenantId, - }, - searchableText: [title, 'private@example.test'], - temporalSearchableText: [ - { - validFrom: '2026-02-01T00:00:00Z', - value: 'canonical-private@example.test', + }; + const store = makePostgresCoreSearchProjectionStore({ + executor: yield* makeTestDatabaseFromPool(runtimePool, coreRelations), + }); + const search = createCoreSearchQueryRuntime(store); + const partyDocument = (resourceId: string, projectionVersion: string, title: string) => ({ + aliases: [ + { + kind: 'resource', + ref: aliasRef, + searchableText: ['Former Acme'], + temporalSearchableText: [ + { + validFrom: '2026-01-01T00:00:00Z', + validTo: '2026-02-01T00:00:00Z', + value: 'alias-private@example.test', + }, + ], + }, + ], + archived: false, + facets: [], + metadata: [], + projectionVersion, + ref: { + moduleId: 'party.registry', + resourceId, + resourceType: 'party.registry.party', + tenantId, }, - ], - title, - }); - const counterpartyDocument = (resourceId: string, selectedLegalEntityId: string) => ({ - archived: false, - facets: [], - metadata: [], - projectionVersion: '1', - ref: { - moduleId: 'party.registry', - resourceId, - resourceType: 'party.registry.counterparty', - tenantId, - }, - searchableText: ['Acme counterparty'], - selectedLegalEntityId, - title: 'Acme counterparty', - }); + searchableText: [title, 'private@example.test'], + temporalSearchableText: [ + { + validFrom: '2026-02-01T00:00:00Z', + value: 'canonical-private@example.test', + }, + ], + title, + }); + const counterpartyDocument = (resourceId: string, selectedLegalEntityId: string) => ({ + archived: false, + facets: [], + metadata: [], + projectionVersion: '1', + ref: { + moduleId: 'party.registry', + resourceId, + resourceType: 'party.registry.counterparty', + tenantId, + }, + searchableText: ['Acme counterparty'], + selectedLegalEntityId, + title: 'Acme counterparty', + }); - const cleanup = Effect.gen(function* cleanSearchPersistenceFixtures() { - yield* queryEffect(admin, `delete from core.search_index_entries where tenant_id = $1`, [ - tenantId, - ]); - yield* queryEffect( - admin, - `delete from core.search_projection_rebuilds where tenant_id = $1`, - [tenantId], - ); - yield* queryEffect(admin, `delete from core.legal_entities where tenant_id = $1`, [tenantId]); - yield* queryEffect(admin, `delete from core.tenants where tenant_id in ($1, $2)`, [ - tenantId, - otherTenantId, - ]); - }).pipe(Effect.orDie); + const cleanup = Effect.gen(function* cleanSearchPersistenceFixtures() { + yield* queryEffect(admin, `delete from core.search_index_entries where tenant_id = $1`, [ + tenantId, + ]); + yield* queryEffect( + admin, + `delete from core.search_projection_rebuilds where tenant_id = $1`, + [tenantId], + ); + yield* queryEffect(admin, `delete from core.legal_entities where tenant_id = $1`, [ + tenantId, + ]); + yield* queryEffect(admin, `delete from core.tenants where tenant_id in ($1, $2)`, [ + tenantId, + otherTenantId, + ]); + }).pipe(Effect.orDie); - yield* Effect.gen(function* exerciseSearchPersistence() { + yield* Effect.addFinalizer(() => cleanup); yield* queryEffect( admin, `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $2, 'Search tenant', 'active', 'en'), ($3, $4, 'Other tenant', 'active', 'en')`, @@ -177,11 +158,8 @@ effectTest( resourceType: 'party.registry.party', tenantId, }); - assert.deepEqual( - partyHits.map(({ title }) => title), - ['Acme current'], - ); - assert.doesNotMatch(yield* encodeJson(partyHits), /private@example\.test/u); + expect(partyHits.map(({ title }) => title)).toEqual(['Acme current']); + expect(yield* encodeJson(partyHits)).not.toMatch(/private@example\.test/u); const evidenceSearch = (query: string, effectiveAt = '2026-02-01T00:00:00Z') => search.search({ effectiveAt, @@ -192,19 +170,18 @@ effectTest( tenantId, }); const aliasHits = yield* evidenceSearch('former'); - assert.equal(aliasHits.length, 1); - assert.deepEqual(aliasHits[0]?.matchedRef, aliasRef); + expect(aliasHits.length).toBe(1); + expect(aliasHits[0]?.matchedRef).toEqual(aliasRef); const canonicalHits = yield* evidenceSearch('acme'); - assert.equal(canonicalHits[0]?.matchedRef, undefined); + expect(canonicalHits[0]?.matchedRef).toBe(undefined); const historicalAliasHits = yield* evidenceSearch('alias-private', '2026-01-01T00:00:00Z'); - assert.deepEqual(historicalAliasHits[0]?.matchedRef, aliasRef); - assert.deepEqual(yield* evidenceSearch('alias-private'), []); - assert.deepEqual(yield* evidenceSearch('canonical-private', '2026-01-31T00:00:00Z'), []); + expect(historicalAliasHits[0]?.matchedRef).toEqual(aliasRef); + expect(yield* evidenceSearch('alias-private')).toEqual([]); + expect(yield* evidenceSearch('canonical-private', '2026-01-31T00:00:00Z')).toEqual([]); const temporalHits = yield* evidenceSearch('canonical-private'); - assert.equal(temporalHits.length, 1); - assert.equal(temporalHits[0]?.matchedRef, undefined); - assert.doesNotMatch( - yield* encodeJson([aliasHits, temporalHits]), + expect(temporalHits.length).toBe(1); + expect(temporalHits[0]?.matchedRef).toBe(undefined); + expect(yield* encodeJson([aliasHits, temporalHits])).not.toMatch( /private@example|searchableText|aliases/u, ); const floorRef = { ...aliasRef, resourceType: 'party.registry.floor-test' }; @@ -222,9 +199,7 @@ effectTest( yield* store.replace(emptyRebuild); // A fresh service instance must observe the durable floor, not process-local state. const restarted = makePostgresCoreSearchProjectionStore({ - executor: yield* makeTestDatabaseFromPool(runtimePool, coreRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ), + executor: yield* makeTestDatabaseFromPool(runtimePool, coreRelations), }); const floorSearch = () => createCoreSearchQueryRuntime(restarted).search({ @@ -240,25 +215,25 @@ effectTest( documents: [staleDocument], rebuildVersion: '1', }); - assert.deepEqual(yield* floorSearch(), []); + expect(yield* floorSearch()).toEqual([]); yield* restarted.replace(emptyRebuild); const divergence = yield* Effect.flip( restarted.replace({ ...emptyRebuild, documents: [staleDocument] }), ); - assert.equal(divergence._tag, 'CoreSearchProjectionInvalid'); + expect(Predicate.isTagged(divergence, 'CoreSearchProjectionInvalid')).toBe(true); yield* restarted.apply({ document: { ...staleDocument, projectionVersion: '3' }, kind: 'upsert', }); yield* restarted.replace(emptyRebuild); const rebuiltFloorHits = yield* floorSearch(); - assert.equal(rebuiltFloorHits.length, 1); + expect(rebuiltFloorHits.length).toBe(1); const rebuildRows = yield* queryEffect( runtimePool, `select rebuild_version from core.search_projection_rebuilds where tenant_id = $1`, [tenantId], ); - assert.equal(rebuildRows.rowCount, 0); + expect(rebuildRows.rowCount).toBe(0); const counterpartyHits = yield* search.search({ includeArchived: false, moduleId: 'party.registry', @@ -267,11 +242,8 @@ effectTest( selectedLegalEntityId: legalEntityId, tenantId, }); - assert.deepEqual( - counterpartyHits.map(({ ref }) => ref.resourceId), - [counterpartyId], - ); - assert.deepEqual( + expect(counterpartyHits.map(({ ref }) => ref.resourceId)).toEqual([counterpartyId]); + expect( yield* search.search({ includeArchived: false, moduleId: 'party.registry', @@ -279,28 +251,19 @@ effectTest( resourceType: 'party.registry.counterparty', tenantId, }), - [], - ); + ).toEqual([]); const runtimeRows = yield* queryEffect( runtimePool, `select source_resource_id from core.search_index_entries where tenant_id = $1`, [tenantId], ); - assert.equal(runtimeRows.rowCount, 0); + expect(runtimeRows.rowCount).toBe(0); const stored = yield* queryEffect<{ deleted: boolean; projection_version: string }>( admin, `select deleted, projection_version::text from core.search_index_entries where tenant_id = $1 and source_resource_id = $2`, [tenantId, removedPartyId], ); - assert.deepEqual(stored.rows, [{ deleted: true, projection_version: '2' }]); - }).pipe( - Effect.ensuring(cleanup), - Effect.ensuring( - Effect.all([endPool(admin), endPool(runtimePool)], { concurrency: 'unbounded' }).pipe( - Effect.orDie, - ), - ), - ); - }), + expect(stored.rows).toEqual([{ deleted: true, projection_version: '2' }]); + }), ); diff --git a/app/packages/core-runtime/tests/integration/search-worker-snapshot.test.ts b/app/packages/core-runtime/tests/integration/search-worker-snapshot.test.ts index 12052114c..5e9d13cd9 100644 --- a/app/packages/core-runtime/tests/integration/search-worker-snapshot.test.ts +++ b/app/packages/core-runtime/tests/integration/search-worker-snapshot.test.ts @@ -1,20 +1,9 @@ -import { makeEffectTestCallback as nativeTestCallback } from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from 'effect-rstest'; + import { NodeServices } from '@effect/platform-node'; import { eq, sql } from 'drizzle-orm'; -import { - Cause, - Crypto, - Deferred, - Effect, - Exit, - Fiber, - ManagedRuntime, - Exit as NativeExit, - Scope as NativeScope, -} from 'effect'; -import assert from 'node:assert/strict'; -import test, { after as afterNativeDatabase } from 'node:test'; -import type { PoolClient, QueryResult, QueryResultRow } from 'pg'; +import { Cause, Crypto, Deferred, Effect, Fiber, Option } from 'effect'; +import type { PoolClient } from 'pg'; import { Pool } from 'pg'; import { loadDatabaseConnectionPair } from '../../src/db/config.ts'; import { coreRelations, domainEvents } from '../../src/db/schema.ts'; @@ -29,23 +18,6 @@ import { makePostgresCoreSearchSnapshotBackend, } from '../../src/search/worker-snapshot.ts'; import { makeTestDatabaseFromPool } from '../support/database.ts'; -import { runEffectTestSync as runNativeSync } from '../support/effect-runtime.ts'; - -const nativeDatabaseScope = runNativeSync(NativeScope.make()); -const databaseEffect = (operation: () => PromiseLike) => - Effect.tryPromise(() => operation()); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), -); - -const workerSnapshotRuntime = ManagedRuntime.make(NodeServices.layer); -const queryPromise = ( - client: Pool | PoolClient, - statement: string, - values?: unknown[], -): PromiseLike> => client.query(statement, values); -const connectPromise = (pool: Pool): PromiseLike => pool.connect(); -const endPromise = (pool: Pool): PromiseLike => pool.end(); const readLegalEntitySettings = (executor: CoreSearchSnapshotReadExecutor, eventId: string) => executor @@ -76,10 +48,18 @@ const readSnapshotPosition = ( ); const beginTransaction = (client: PoolClient) => - databaseEffect(() => queryPromise(client, 'begin')); + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + + try: () => client.query('begin'), + }); const commitTransaction = (client: PoolClient) => - databaseEffect(() => queryPromise(client, 'commit')); + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + + try: () => client.query('commit'), + }); const insertPendingEvent = ( client: PoolClient, @@ -87,13 +67,15 @@ const insertPendingEvent = ( tenantId: string, pendingSubjectId: string, ) => - databaseEffect(() => - queryPromise( - client, - `insert into core.domain_events (domain_event_id, tenant_id, producer_module_key, event_type, subject_module_key, subject_resource_type, subject_resource_id) values ($1, $2, 'party.registry', 'party.registry.party-updated.v1', 'party.registry', 'party.registry.party', $3)`, - [pendingEventId, tenantId, pendingSubjectId], - ), - ); + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + + try: () => + client.query( + `insert into core.domain_events (domain_event_id, tenant_id, producer_module_key, event_type, subject_module_key, subject_resource_type, subject_resource_id) values ($1, $2, 'party.registry', 'party.registry.party-updated.v1', 'party.registry', 'party.registry.party', $3)`, + [pendingEventId, tenantId, pendingSubjectId], + ), + }); const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { const crypto = yield* Crypto.Crypto; @@ -110,61 +92,86 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { }); const source = makeCoreSearchWorkerSnapshot( makePostgresCoreSearchSnapshotBackend({ - executor: yield* makeTestDatabaseFromPool(runtimePool, coreRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ), + executor: yield* makeTestDatabaseFromPool(runtimePool, coreRelations), }), ); const insertEvent = (id: string) => Effect.gen(function* insertDomainEvent() { const subjectId = yield* crypto.randomUUIDv4; - const result = yield* databaseEffect(() => - queryPromise<{ tenant_sequence_no: string }>( - admin, - `insert into core.domain_events (domain_event_id, tenant_id, producer_module_key, event_type, subject_module_key, subject_resource_type, subject_resource_id) values ($1, $2, 'party.registry', 'party.registry.party-updated.v1', 'party.registry', 'party.registry.party', $3) returning tenant_sequence_no::text`, - [id, tenantId, subjectId], - ), - ); - const [row] = result.rows; - assert.ok(row); + const result = yield* Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + + try: () => + admin.query<{ tenant_sequence_no: string }>( + `insert into core.domain_events (domain_event_id, tenant_id, producer_module_key, event_type, subject_module_key, subject_resource_type, subject_resource_id) values ($1, $2, 'party.registry', 'party.registry.party-updated.v1', 'party.registry', 'party.registry.party', $3) returning tenant_sequence_no::text`, + [id, tenantId, subjectId], + ), + }); + const row = Option.getOrThrow(Option.fromNullishOr(result.rows[0])); + expect(row).toBeDefined(); return row.tenant_sequence_no; }); const cleanup = Effect.gen(function* cleanupWorkerSnapshot() { - yield* databaseEffect(() => - queryPromise(admin, 'delete from core.search_projection_generations where tenant_id = $1', [ - tenantId, - ]), - ); - yield* databaseEffect(() => - queryPromise(admin, 'delete from core.domain_events where tenant_id = $1', [tenantId]), - ); - yield* databaseEffect(() => - queryPromise(admin, 'delete from core.legal_entities where tenant_id = $1', [tenantId]), - ); - yield* databaseEffect(() => - queryPromise(admin, 'delete from core.tenants where tenant_id = $1', [tenantId]), - ); + yield* Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + + try: () => + admin.query('delete from core.search_projection_generations where tenant_id = $1', [ + tenantId, + ]), + }); + yield* Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + + try: () => admin.query('delete from core.domain_events where tenant_id = $1', [tenantId]), + }); + yield* Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + + try: () => admin.query('delete from core.legal_entities where tenant_id = $1', [tenantId]), + }); + yield* Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + + try: () => admin.query('delete from core.tenants where tenant_id = $1', [tenantId]), + }); yield* Effect.all( - [databaseEffect(() => endPromise(admin)), databaseEffect(() => endPromise(runtimePool))], + [ + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + + try: () => admin.end(), + }), + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + + try: () => runtimePool.end(), + }), + ], { concurrency: 'unbounded' }, ); }).pipe(Effect.orDie); - yield* Effect.gen(function* exerciseWorkerSnapshots() { - yield* databaseEffect(() => - queryPromise( - admin, - `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $2, 'Snapshot tenant', 'active', 'en')`, - [tenantId, `snapshot-${tenantId}`], - ), - ); - yield* databaseEffect(() => - queryPromise( - admin, - `insert into core.legal_entities (legal_entity_id, tenant_id, legal_name, registration_country, registration_number, status) values ($1::uuid, $2, 'Snapshot LE', 'CZ', $1::uuid::text, 'active')`, - [legalEntityId, tenantId], - ), - ); + yield* Effect.addFinalizer(() => cleanup); + const exercise = Effect.gen(function* exerciseWorkerSnapshots() { + yield* Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + + try: () => + admin.query( + `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $2, 'Snapshot tenant', 'active', 'en')`, + [tenantId, `snapshot-${tenantId}`], + ), + }); + yield* Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + + try: () => + admin.query( + `insert into core.legal_entities (legal_entity_id, tenant_id, legal_name, registration_country, registration_number, status) values ($1::uuid, $2, 'Snapshot LE', 'CZ', $1::uuid::text, 'active')`, + [legalEntityId, tenantId], + ), + }); const originalVersion = yield* insertEvent(eventId); const [claimId, deliveryId, messageId] = yield* Effect.all( [crypto.randomUUIDv4, crypto.randomUUIDv4, crypto.randomUUIDv4], @@ -189,8 +196,8 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { let newerVersion = ''; const result = yield* source.read(context, (snapshot) => Effect.gen(function* inspectSnapshot() { - assert.equal(snapshot.projectionVersion, '1'); - assert.equal(snapshot.eventWatermark, originalVersion); + expect(snapshot.projectionVersion).toBe('1'); + expect(snapshot.eventWatermark).toBe(originalVersion); const settings = yield* snapshot.forLegalEntity(legalEntityId, readEventSettings); const newerEventId = yield* crypto.randomUUIDv4; newerVersion = yield* insertEvent(newerEventId); @@ -198,7 +205,7 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { return { settings, version: rows[0]?.version }; }), ); - assert.deepEqual(result.settings, [ + expect(result.settings).toEqual([ { isolation: 'repeatable read', legalEntity: legalEntityId, @@ -206,13 +213,12 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { tenant: tenantId, }, ]); - assert.equal(result.version, originalVersion); - assert.equal( + expect(result.version).toBe(originalVersion); + expect( yield* source.read(context, (snapshot) => Effect.succeed(snapshot.projectionVersion)), - '2', - ); + ).toBe('2'); const nextSnapshot = yield* readSnapshotPosition(source, context); - assert.deepEqual(nextSnapshot, { eventWatermark: newerVersion, generation: '3' }); + expect(nextSnapshot).toEqual({ eventWatermark: newerVersion, generation: '3' }); // A second snapshot starts while the first owns the generation row. It must // retry its old RR snapshot after the first commits, never publish stale data @@ -241,28 +247,34 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { (blocked) => blocked ? Effect.succeed(true) - : databaseEffect(() => queryPromise(admin, 'select pg_sleep(0.01)')).pipe( + : Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + + try: () => admin.query('select pg_sleep(0.01)'), + }).pipe( Effect.andThen( - databaseEffect(() => - queryPromise<{ count: number }>( - admin, - `select count(*)::int as count from pg_stat_activity where application_name = $1 and wait_event_type = 'Lock'`, - [applicationName], - ), - ), + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + + try: () => + admin.query<{ count: number }>( + `select count(*)::int as count from pg_stat_activity where application_name = $1 and wait_event_type = 'Lock'`, + [applicationName], + ), + }), ), Effect.map((activity) => activity.rows[0]?.count === 1), ), ); - assert.equal(waiting, true, 'second snapshot must wait on first generation before retrying'); + expect(waiting, 'second snapshot must wait on first generation before retrying').toBe(true); const latestEventId = yield* crypto.randomUUIDv4; const latestEvent = yield* insertEvent(latestEventId); yield* Deferred.succeed(release, null); const [firstResult, secondResult] = yield* Effect.all([Fiber.join(first), Fiber.join(second)], { concurrency: 'unbounded', }); - assert.deepEqual(firstResult, { eventWatermark: newerVersion, generation: '4' }); - assert.deepEqual(secondResult, { eventWatermark: latestEvent, generation: '5' }); + expect(firstResult).toEqual({ eventWatermark: newerVersion, generation: '4' }); + expect(secondResult).toEqual({ eventWatermark: latestEvent, generation: '5' }); // Business transactions may commit event allocation sequences out of order. // Both snapshots below have the same event max but must get new generations. @@ -278,34 +290,36 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { const beforeLateCommit = yield* readSnapshotPosition(source, context); yield* commitTransaction(pending); const afterLateCommit = yield* readSnapshotPosition(source, context); - assert.deepEqual(beforeLateCommit, { + expect(beforeLateCommit).toEqual({ eventWatermark: higherEvent, generation: '6', }); - assert.deepEqual(afterLateCommit, { + expect(afterLateCommit).toEqual({ eventWatermark: higherEvent, generation: '7', }); }); yield* Effect.acquireUseRelease( - databaseEffect(() => connectPromise(admin)), + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + + try: () => admin.connect(), + }), lateCommitSnapshot, (pending) => - databaseEffect(() => queryPromise(pending, 'rollback')).pipe( - Effect.orDie, - Effect.ensuring(Effect.sync(() => pending.release())), - ), - ); - }).pipe(Effect.ensuring(cleanup)); -}); + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), -test('worker projection uses independent generations and one repeatable snapshot across tenant and Legal Entity scopes', (_context, done) => { - workerSnapshotRuntime.runCallback(workerSnapshotProgram, { - onExit: Exit.match({ - onFailure: (cause) => done(Cause.squash(cause)), - onSuccess: () => done(), - }), + try: () => pending.query('rollback'), + }).pipe(Effect.orDie, Effect.ensuring(Effect.sync(() => pending.release()))), + ); }); + yield* exercise; }); -test.after(workerSnapshotRuntime.dispose.bind(workerSnapshotRuntime)); +it.layer(NodeServices.layer, { excludeTestServices: true })('worker snapshots', (suite) => { + suite.effect( + 'worker projection uses independent generations and one repeatable snapshot across tenant and Legal Entity scopes', + () => workerSnapshotProgram, + ); +}); diff --git a/app/packages/core-runtime/tests/integration/tenant-isolation.test.ts b/app/packages/core-runtime/tests/integration/tenant-isolation.test.ts index 12e70640a..af969bd7d 100644 --- a/app/packages/core-runtime/tests/integration/tenant-isolation.test.ts +++ b/app/packages/core-runtime/tests/integration/tenant-isolation.test.ts @@ -1,20 +1,8 @@ -import { - makeEffectTestCallback as nativeTestCallback, - makeEffectTestCallback, -} from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from 'effect-rstest'; import { getTableConfig, pgSchema, text, uuid } from 'drizzle-orm/pg-core'; -import { - Effect, - Function as Fn, - Exit as NativeExit, - Scope as NativeScope, - Option, - Schema, -} from 'effect'; -import assert from 'node:assert/strict'; +import { Effect, Option, Schema } from 'effect'; import { randomUUID } from 'node:crypto'; -import test, { after as afterNativeDatabase } from 'node:test'; import type { PoolClient, QueryResult, QueryResultRow } from 'pg'; import { Pool } from 'pg'; import { loadDatabaseConnectionPair } from '../../src/db/config.ts'; @@ -43,14 +31,8 @@ import type { ReadHandlerContext } from '../../src/reads/context.ts'; import { defineRead } from '../../src/reads/definition.ts'; import { makeReadRuntime } from '../../src/reads/runtime.ts'; import { makeTestDatabaseFromPool } from '../support/database.ts'; -import { runEffectTestSync as runNativeSync } from '../support/effect-runtime.ts'; import { openModuleEntrypointGateway } from '../support/open-module-entrypoint-gateway.ts'; -const nativeDatabaseScope = runNativeSync(NativeScope.make()); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), -); - type DatabaseQueryFailureSelf = typeof DatabaseQueryFailureContract.Type; const DatabaseQueryFailureContract = Schema.TaggedStruct('DatabaseQueryFailure', { code: Schema.String, @@ -65,30 +47,21 @@ const queryEffect = ( statement: string, parameters?: readonly unknown[], ): Effect.Effect> => - Effect.suspend(() => - Effect.promise(Fn.constant(client.query(statement, [...(parameters ?? [])]))), - ); + Effect.promise(() => client.query(statement, [...(parameters ?? [])])); const queryTryEffect = ( client: Pool | PoolClient, statement: string, parameters?: readonly unknown[], ): Effect.Effect, DatabaseQueryFailureSelf> => - Effect.suspend(() => { - const query = client.query(statement, [...(parameters ?? [])]); - return Effect.tryPromise({ - catch: (error) => { - const decoded = Schema.decodeUnknownOption(DatabaseErrorCode)(error); - return new DatabaseQueryFailure({ - code: Option.isSome(decoded) ? decoded.value.code : 'unknown', - }); - }, - try: Fn.constant(query), - }); + Effect.tryPromise({ + catch: (error) => { + const decoded = Schema.decodeUnknownOption(DatabaseErrorCode)(error); + return new DatabaseQueryFailure({ + code: Option.isSome(decoded) ? decoded.value.code : 'unknown', + }); + }, + try: () => client.query(statement, [...(parameters ?? [])]), }); -const connectPool = (pool: Pool): Effect.Effect => - Effect.suspend(() => Effect.promise(Fn.constant(pool.connect()))); -const endPool = (pool: Pool): Effect.Effect => - Effect.suspend(() => Effect.promise(Fn.constant(pool.end()))); const effectAccessor = (effect: Effect.Effect) => @@ -98,11 +71,8 @@ const toReadResult = (rows: readonly { readonly value: string }[]) => ({ evidence: { resultCount: rows.length }, result: rows.map((row) => row.value), }); -const effectTest = (name: string, effect: Effect.Effect): void => { - test(name, makeEffectTestCallback(effect)); -}; -void test('declares the composite same-tenant parent keys used by isolation foreign keys', () => { +it('declares the composite same-tenant parent keys used by isolation foreign keys', () => { const names = new Set( [legalEntities, principals, principalAuthBindings, actionInvocations].flatMap((table) => getTableConfig(table) @@ -110,10 +80,10 @@ void test('declares the composite same-tenant parent keys used by isolation fore .map((index) => index.config.name), ), ); - assert.ok(names.has('core_legal_entities_tenant_id_uk')); - assert.ok(names.has('core_principals_tenant_id_uk')); - assert.ok(names.has('core_auth_bindings_tenant_id_uk')); - assert.ok(names.has('core_action_invocations_tenant_id_uk')); + expect(names.has('core_legal_entities_tenant_id_uk')).toBe(true); + expect(names.has('core_principals_tenant_id_uk')).toBe(true); + expect(names.has('core_auth_bindings_tenant_id_uk')).toBe(true); + expect(names.has('core_action_invocations_tenant_id_uk')).toBe(true); const tenantQualifiedChildren = [ principalAuthBindings, @@ -132,20 +102,26 @@ void test('declares the composite same-tenant parent keys used by isolation fore const businessReferences = getTableConfig(table) .foreignKeys.map((foreignKey) => foreignKey.reference().columns.map((column) => column.name)) .filter((columns) => columns.some((column) => column !== 'tenant_id')); - assert.ok(businessReferences.length > 0); - assert.equal( + expect(businessReferences.length > 0).toBe(true); + expect( businessReferences.every((columns) => columns.length === 2 && columns[0] === 'tenant_id'), - true, - ); + ).toBe(true); } }); -effectTest( - 'runtime RLS isolates tenant and legal-entity rows and never leaks transaction scope', +it.live('runtime RLS isolates tenant and legal-entity rows and never leaks transaction scope', () => Effect.gen(function* runtimeRlsIsolation() { const connections = yield* loadDatabaseConnectionPair(); - const admin = new Pool({ connectionString: connections.admin.connectionString }); - const runtime = new Pool({ connectionString: connections.runtime.connectionString, max: 1 }); + const admin = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), + (pool) => Effect.promise(() => pool.end()), + ); + const runtime = yield* Effect.acquireRelease( + Effect.sync( + () => new Pool({ connectionString: connections.runtime.connectionString, max: 1 }), + ), + (pool) => Effect.promise(() => pool.end()), + ); const schema = `isolation_${randomUUID().replaceAll('-', '')}`; const tenantA = randomUUID(); const tenantB = randomUUID(); @@ -213,15 +189,15 @@ effectTest( `, [schema], ); - assert.deepEqual(catalog.rows[0], { + expect(catalog.rows[0]).toEqual({ policy_count: 4, relforcerowsecurity: true, relrowsecurity: true, }); const unscopedRows = yield* queryEffect(runtime, `select * from ${schema}.records`); - assert.equal(unscopedRows.rowCount, 0); - const client = yield* connectPool(runtime); + expect(unscopedRows.rowCount).toBe(0); + const client = yield* Effect.promise(() => runtime.connect()); yield* Effect.gen(function* scopedRuntimeQueries() { yield* queryEffect(client, 'begin'); yield* queryEffect( @@ -233,17 +209,17 @@ effectTest( client, `select value from ${schema}.records`, ); - assert.deepEqual(entityARows.rows, [{ value: 'entity-a' }]); + expect(entityARows.rows).toEqual([{ value: 'entity-a' }]); const foreignUpdate = yield* queryEffect( client, `update ${schema}.records set value = 'hacked' where value = 'tenant-b'`, ); - assert.equal(foreignUpdate.rowCount, 0); + expect(foreignUpdate.rowCount).toBe(0); const foreignDelete = yield* queryEffect( client, `delete from ${schema}.records where value = 'entity-b'`, ); - assert.equal(foreignDelete.rowCount, 0); + expect(foreignDelete.rowCount).toBe(0); const forbiddenInsert = yield* Effect.flip( queryTryEffect( client, @@ -251,7 +227,7 @@ effectTest( [tenantB, entityC, randomUUID()], ), ); - assert.equal(forbiddenInsert.code, '42501'); + expect(forbiddenInsert.code).toBe('42501'); yield* queryEffect(client, 'rollback'); yield* queryEffect(client, 'begin'); @@ -264,41 +240,39 @@ effectTest( client, `select value from ${schema}.records`, ); - assert.deepEqual(entityBRows.rows, [{ value: 'entity-b' }]); + expect(entityBRows.rows).toEqual([{ value: 'entity-b' }]); yield* queryEffect(client, 'commit'); }).pipe(Effect.ensuring(Effect.sync(() => client.release()))); const resetRows = yield* queryEffect(runtime, `select * from ${schema}.records`); - assert.equal(resetRows.rowCount, 0); + expect(resetRows.rowCount).toBe(0); const protectedRows = yield* queryEffect<{ value: string }>( admin, `select value from ${schema}.records order by value`, ); - assert.deepEqual(protectedRows.rows, [ + expect(protectedRows.rows).toEqual([ { value: 'entity-a' }, { value: 'entity-b' }, { value: 'tenant-b' }, ]); }); - const release = endPool(runtime).pipe( - Effect.ensuring( - queryEffect(admin, `drop schema if exists ${schema} cascade`).pipe(Effect.orDie), - ), - Effect.ensuring(endPool(admin).pipe(Effect.orDie)), - ); + const release = queryEffect(admin, `drop schema if exists ${schema} cascade`); yield* exercise.pipe(Effect.ensuring(release)); }), ); -effectTest( - 'an unscoped owner repository remains isolated inside a governed read transaction', +it.live('an unscoped owner repository remains isolated inside a governed read transaction', () => Effect.gen(function* governedReadIsolation() { const connections = yield* loadDatabaseConnectionPair(); - const admin = new Pool({ connectionString: connections.admin.connectionString }); - const runtimePool = new Pool({ connectionString: connections.runtime.connectionString }); - const runtimeDatabase = yield* makeTestDatabaseFromPool(runtimePool, coreRelations).pipe( - NativeScope.provide(nativeDatabaseScope), + const admin = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), + (pool) => Effect.promise(() => pool.end()), + ); + const runtimePool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.runtime.connectionString })), + (pool) => Effect.promise(() => pool.end()), ); + const runtimeDatabase = yield* makeTestDatabaseFromPool(runtimePool, coreRelations); const schemaName = `governed_isolation_${randomUUID().replaceAll('-', '')}`; const ownerSchema = pgSchema(schemaName); const records = ownerSchema.table('records', { @@ -465,7 +439,7 @@ effectTest( [tenantA, entityA, entityB, tenantB, entityC, resourceId], ); - assert.deepEqual( + expect( yield* runForScope({ authBindingId: bindingA, authMethod: 'session', @@ -474,9 +448,8 @@ effectTest( principalId: principalA, tenantId: tenantA, }), - ['tenant-a-entity-a'], - ); - assert.deepEqual( + ).toEqual(['tenant-a-entity-a']); + expect( yield* runForScope({ authBindingId: bindingA, authMethod: 'session', @@ -485,9 +458,8 @@ effectTest( principalId: principalA, tenantId: tenantA, }), - ['tenant-a-entity-b'], - ); - assert.deepEqual( + ).toEqual(['tenant-a-entity-b']); + expect( yield* runForScope({ authBindingId: bindingB, authMethod: 'session', @@ -496,8 +468,7 @@ effectTest( principalId: principalB, tenantId: tenantB, }), - ['tenant-b-entity-c'], - ); + ).toEqual(['tenant-b-entity-c']); }); const release = Effect.gen(function* cleanGovernedReadIsolation() { yield* queryEffect(admin, 'delete from core.data_access_events where tenant_id in ($1, $2)', [ @@ -522,18 +493,19 @@ effectTest( tenantB, ]); yield* queryEffect(admin, `drop schema if exists ${schemaName} cascade`); - yield* Effect.all([endPool(runtimePool), endPool(admin)], { concurrency: 'unbounded' }); }).pipe(Effect.orDie); yield* exercise.pipe(Effect.ensuring(release)); }), ); -effectTest( - 'PostgreSQL rejects cross-tenant entity, principal, and Action references', +it.live('PostgreSQL rejects cross-tenant entity, principal, and Action references', () => Effect.gen(function* crossTenantForeignKeys() { const connections = yield* loadDatabaseConnectionPair(); - const admin = new Pool({ connectionString: connections.admin.connectionString }); - const client = yield* connectPool(admin); + const admin = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), + (pool) => Effect.promise(() => pool.end()), + ); + const client = yield* Effect.promise(() => admin.connect()); const tenantA = randomUUID(); const tenantB = randomUUID(); const entityA = randomUUID(); @@ -546,7 +518,7 @@ effectTest( Effect.gen(function* rejectCrossTenantReference() { yield* queryEffect(client, 'savepoint isolation_failure'); const failure = yield* Effect.flip(queryTryEffect(client, statement, parameters)); - assert.equal(failure.code, '23503'); + expect(failure.code).toBe('23503'); yield* queryEffect(client, 'rollback to savepoint isolation_failure'); }); @@ -589,7 +561,6 @@ effectTest( }); const release = queryEffect(client, 'rollback').pipe( Effect.ensuring(Effect.sync(() => client.release())), - Effect.ensuring(endPool(admin).pipe(Effect.orDie)), ); yield* exercise.pipe(Effect.ensuring(release)); }), diff --git a/app/packages/core-runtime/tests/integration/tenant-module-state.test.ts b/app/packages/core-runtime/tests/integration/tenant-module-state.test.ts index 6a7238977..aa4635217 100644 --- a/app/packages/core-runtime/tests/integration/tenant-module-state.test.ts +++ b/app/packages/core-runtime/tests/integration/tenant-module-state.test.ts @@ -1,11 +1,10 @@ +import { expect, it } from 'effect-rstest'; import { makeInstalledCatalogFixture as catalogFrom } from '../support/installed-catalog.ts'; import { makeModuleContractFixture } from '../../src/testing/module-contract.ts'; import { SqlError, UnknownError } from 'effect/unstable/sql/SqlError'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; + import { and, asc, eq, inArray } from 'drizzle-orm'; -import { Cause, Effect, Exit, Match, Option, Schema, flow } from 'effect'; -import assert from 'node:assert/strict'; -import test, { after, before } from 'node:test'; +import { Cause, Effect, Exit, Match, Option, Schema, Layer } from 'effect'; import { makeActionRepository } from '../../src/actions/repository.ts'; import { makeActionRuntime } from '../../src/actions/runtime.ts'; import { makeFaultInjectableCoreDatabase, TestQueryHook } from '../support/database-faults.ts'; @@ -84,13 +83,6 @@ const withDatabase = ( }), ); -const effectCallback = (effect: Effect.Effect) => - flow(() => Effect.asVoid(effect), runEffectTestPromise); - -const effectTest = (name: string, effect: Effect.Effect): void => { - test(name, effectCallback(effect)); -}; - const cleanup = withDatabase((database) => Effect.gen(function* cleanTenantModuleStateFixtures() { // Keep dependent evidence ahead of its referenced identity rows. @@ -109,69 +101,69 @@ const cleanup = withDatabase((database) => }), ); -before( - Effect.gen(function* initializeTenantModuleStateFixtures() { - yield* cleanup; - yield* withDatabase((database) => - Effect.gen(function* insertTenantModuleStateFixtures() { - yield* database.executor.insert(tenants).values([ - { - defaultLocale: 'en', - name: 'Tenant module state one', - slug: `tenant-module-state-${tenantOne}`, - status: 'active', - tenantId: tenantOne, - }, - { - defaultLocale: 'en', - name: 'Tenant module state two', - slug: `tenant-module-state-${tenantTwo}`, - status: 'active', - tenantId: tenantTwo, - }, - ]); - yield* database.executor.insert(principals).values([ - { - displayName: 'Tenant module state principal one', - kind: 'human', - principalId: principalOne, - status: 'active', - tenantId: tenantOne, - }, - { - displayName: 'Tenant module state principal two', - kind: 'human', - principalId: principalTwo, - status: 'active', - tenantId: tenantTwo, - }, - ]); - yield* database.executor.insert(principalAuthBindings).values([ - { - principalAuthBindingId: bindingOne, - principalId: principalOne, - provider: 'better_auth', - providerSubjectId: `tenant-module-state-user-${principalOne}`, - status: 'active', - subjectType: 'user', - tenantId: tenantOne, - }, - { - principalAuthBindingId: bindingTwo, - principalId: principalTwo, - provider: 'better_auth', - providerSubjectId: `tenant-module-state-user-${principalTwo}`, - status: 'active', - subjectType: 'user', - tenantId: tenantTwo, - }, - ]); - }), - ); - }).pipe(effectCallback), -); +const setup = Effect.gen(function* initializeTenantModuleStateFixtures() { + yield* cleanup; + yield* withDatabase((database) => + Effect.gen(function* insertTenantModuleStateFixtures() { + yield* database.executor.insert(tenants).values([ + { + defaultLocale: 'en', + name: 'Tenant module state one', + slug: `tenant-module-state-${tenantOne}`, + status: 'active', + tenantId: tenantOne, + }, + { + defaultLocale: 'en', + name: 'Tenant module state two', + slug: `tenant-module-state-${tenantTwo}`, + status: 'active', + tenantId: tenantTwo, + }, + ]); + yield* database.executor.insert(principals).values([ + { + displayName: 'Tenant module state principal one', + kind: 'human', + principalId: principalOne, + status: 'active', + tenantId: tenantOne, + }, + { + displayName: 'Tenant module state principal two', + kind: 'human', + principalId: principalTwo, + status: 'active', + tenantId: tenantTwo, + }, + ]); + yield* database.executor.insert(principalAuthBindings).values([ + { + principalAuthBindingId: bindingOne, + principalId: principalOne, + provider: 'better_auth', + providerSubjectId: `tenant-module-state-user-${principalOne}`, + status: 'active', + subjectType: 'user', + tenantId: tenantOne, + }, + { + principalAuthBindingId: bindingTwo, + principalId: principalTwo, + provider: 'better_auth', + providerSubjectId: `tenant-module-state-user-${principalTwo}`, + status: 'active', + subjectType: 'user', + tenantId: tenantTwo, + }, + ]); + }), + ); +}); -after(cleanup.pipe(effectCallback)); +const Fixtures = Layer.effectDiscard( + Effect.acquireRelease(setup, () => cleanup.pipe(Effect.orDie)), +); const allowedPermission = { checkActionPermission: () => Effect.succeed('allowed' as const), @@ -231,8 +223,8 @@ const verifyHistoryEvidence = ( .select() .from(actionInvocations) .where(eq(actionInvocations.actionInvocationId, row.actionInvocationId ?? '')); - assert.equal(invocation?.principalId, principalOne); - assert.equal(invocation?.status, 'succeeded'); + expect(invocation?.principalId).toBe(principalOne); + expect(invocation?.status).toBe('succeeded'); const audit = yield* database.executor .select() .from(auditEvents) @@ -241,277 +233,253 @@ const verifyHistoryEvidence = ( .select() .from(dataAccessEvents) .where(eq(dataAccessEvents.actionInvocationId, row.actionInvocationId ?? '')); - assert.ok(audit.some((event) => event.eventType === 'action.executed')); - assert.equal(access.length, 1); - assert.deepEqual( - access.map((event) => event.targetResourceType), - ['tenant-module-state'], - ); - assert.ok(access.every((event) => event.accessKind === 'read')); + expect(audit.some((event) => event.eventType === 'action.executed')).toBe(true); + expect(access.length).toBe(1); + expect(access.map((event) => event.targetResourceType)).toEqual(['tenant-module-state']); + expect(access.every((event) => event.accessKind === 'read')).toBe(true); }); -effectTest( - 'lists exact active rows and all states for one trusted tenant in module-key order', - withDatabase((database) => - Effect.gen(function* listTenantModuleStates() { - yield* database.executor.insert(tenantModuleStates).values([ - { moduleKey: 'list.zeta', state: 'active', tenantId: tenantOne }, - { moduleKey: 'list.alpha', state: 'active', tenantId: tenantOne }, - { moduleKey: 'list.inactive', state: 'inactive', tenantId: tenantOne }, - { moduleKey: 'list.alpha', state: 'active', tenantId: tenantTwo }, - ]); - - const service = makeTenantModuleStateService(database); - assert.deepEqual(yield* service.listActiveTenantModules(tenantOne), [ - { moduleKey: 'list.alpha', state: 'active' }, - { moduleKey: 'list.zeta', state: 'active' }, - ]); - assert.deepEqual(yield* service.listActiveTenantModules(tenantTwo), [ - { moduleKey: 'list.alpha', state: 'active' }, - ]); - assert.deepEqual(yield* service.listTenantModuleStates(tenantOne), [ - { moduleKey: 'list.alpha', state: 'active' }, - { moduleKey: 'list.inactive', state: 'inactive' }, - { moduleKey: 'list.zeta', state: 'active' }, - ]); - assert.deepEqual(yield* service.listTenantModuleStates(tenantTwo), [ - { moduleKey: 'list.alpha', state: 'active' }, - ]); - }), - ), +const tenantModuleStateTest1 = withDatabase((database) => + Effect.gen(function* listTenantModuleStates() { + yield* database.executor.insert(tenantModuleStates).values([ + { moduleKey: 'list.zeta', state: 'active', tenantId: tenantOne }, + { moduleKey: 'list.alpha', state: 'active', tenantId: tenantOne }, + { moduleKey: 'list.inactive', state: 'inactive', tenantId: tenantOne }, + { moduleKey: 'list.alpha', state: 'active', tenantId: tenantTwo }, + ]); + + const service = makeTenantModuleStateService(database); + expect(yield* service.listActiveTenantModules(tenantOne)).toEqual([ + { moduleKey: 'list.alpha', state: 'active' }, + { moduleKey: 'list.zeta', state: 'active' }, + ]); + expect(yield* service.listActiveTenantModules(tenantTwo)).toEqual([ + { moduleKey: 'list.alpha', state: 'active' }, + ]); + expect(yield* service.listTenantModuleStates(tenantOne)).toEqual([ + { moduleKey: 'list.alpha', state: 'active' }, + { moduleKey: 'list.inactive', state: 'inactive' }, + { moduleKey: 'list.zeta', state: 'active' }, + ]); + expect(yield* service.listTenantModuleStates(tenantTwo)).toEqual([ + { moduleKey: 'list.alpha', state: 'active' }, + ]); + }), ); - -effectTest( - 'atomically creates and transitions state with truthful Action history and evidence', - Effect.gen(function* createAndTransitionTenantModuleState() { - const moduleKey = testModuleKey('testing', tenantOne); - - yield* withDatabase((database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - return Effect.gen(function* transitionSequence() { - const created = yield* runtime.runAction(actionInput(moduleKey, 'active', 'create')); - assert.deepEqual(created, { moduleKey, newState: 'active', previousState: null }); - const suspended = yield* runtime.runAction(actionInput(moduleKey, 'suspended', 'suspend')); - assert.deepEqual(suspended, { - moduleKey, - newState: 'suspended', - previousState: 'active', - }); - const reactivated = yield* runtime.runAction( - actionInput(moduleKey, 'active', 'reactivate'), - ); - assert.deepEqual(reactivated, { - moduleKey, - newState: 'active', - previousState: 'suspended', - }); +const tenantModuleStateTest2 = Effect.gen(function* createAndTransitionTenantModuleState() { + const moduleKey = testModuleKey('testing', tenantOne); + + yield* withDatabase((database) => { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + return Effect.gen(function* transitionSequence() { + const created = yield* runtime.runAction(actionInput(moduleKey, 'active', 'create')); + expect(created).toEqual({ moduleKey, newState: 'active', previousState: null }); + const suspended = yield* runtime.runAction(actionInput(moduleKey, 'suspended', 'suspend')); + expect(suspended).toEqual({ + moduleKey, + newState: 'suspended', + previousState: 'active', + }); + const reactivated = yield* runtime.runAction(actionInput(moduleKey, 'active', 'reactivate')); + expect(reactivated).toEqual({ + moduleKey, + newState: 'active', + previousState: 'suspended', }); }); + }); - yield* withDatabase((database) => - Effect.gen(function* verifyTenantModuleStateHistory() { - const [current] = yield* database.executor - .select() - .from(tenantModuleStates) - .where( - and( - eq(tenantModuleStates.tenantId, tenantOne), - eq(tenantModuleStates.moduleKey, moduleKey), - ), - ); - const history = yield* database.executor - .select() - .from(tenantModuleStateChanges) - .where( - and( - eq(tenantModuleStateChanges.tenantId, tenantOne), - eq(tenantModuleStateChanges.moduleKey, moduleKey), - ), - ) - .orderBy(asc(tenantModuleStateChanges.occurredAt)); - assert.equal(current?.state, 'active'); - assert.equal(history.length, 3); - assert.deepEqual( - history.map(({ changeSource, newState, previousState }) => ({ - changeSource, - newState, - previousState, - })), - [ - { changeSource: 'user', newState: 'active', previousState: null }, - { changeSource: 'user', newState: 'suspended', previousState: 'active' }, - { changeSource: 'user', newState: 'active', previousState: 'suspended' }, - ], - ); - assert.equal(current?.lastChangeId, history.at(-1)?.moduleStateChangeId); - assert.ok(history.every((row) => row.changedByPrincipalId === principalOne)); - assert.ok(history.every((row) => row.actionInvocationId !== null)); - assert.ok( - history.every((row) => row.reason?.startsWith('Integration transition to ') === true), + yield* withDatabase((database) => + Effect.gen(function* verifyTenantModuleStateHistory() { + const [current] = yield* database.executor + .select() + .from(tenantModuleStates) + .where( + and( + eq(tenantModuleStates.tenantId, tenantOne), + eq(tenantModuleStates.moduleKey, moduleKey), + ), ); + const history = yield* database.executor + .select() + .from(tenantModuleStateChanges) + .where( + and( + eq(tenantModuleStateChanges.tenantId, tenantOne), + eq(tenantModuleStateChanges.moduleKey, moduleKey), + ), + ) + .orderBy(asc(tenantModuleStateChanges.occurredAt)); + expect(current?.state).toBe('active'); + expect(history.length).toBe(3); + expect( + history.map(({ changeSource, newState, previousState }) => ({ + changeSource, + newState, + previousState, + })), + ).toEqual([ + { changeSource: 'user', newState: 'active', previousState: null }, + { changeSource: 'user', newState: 'suspended', previousState: 'active' }, + { changeSource: 'user', newState: 'active', previousState: 'suspended' }, + ]); + expect(current?.lastChangeId).toBe(history.at(-1)?.moduleStateChangeId); + expect(history.every((row) => row.changedByPrincipalId === principalOne)).toBe(true); + expect(history.every((row) => row.actionInvocationId !== null)).toBe(true); + expect( + history.every((row) => row.reason?.startsWith('Integration transition to ') === true), + ).toBe(true); + + yield* Effect.forEach(history, (row) => verifyHistoryEvidence(database, row), { + concurrency: 1, + }); + }), + ); +}); +const tenantModuleStateTest3 = Effect.gen(function* allDeclaredTenantModuleStates() { + const otherModuleKey = testModuleKey('other', tenantOne); + const targetModuleKey = testModuleKey('independent', tenantOne); + const transitionCatalog = catalogFrom( + installedContract(otherModuleKey), + installedContract(targetModuleKey), + ); + yield* withDatabase((database) => + database.executor.insert(tenantModuleStates).values({ + moduleKey: otherModuleKey, + state: 'inactive', + tenantId: tenantOne, + }), + ); - yield* Effect.forEach(history, (row) => verifyHistoryEvidence(database, row), { - concurrency: 1, - }); - }), - ); - }), -); - -effectTest( - 'supports every declared state independently of other installed module states', - Effect.gen(function* allDeclaredTenantModuleStates() { - const otherModuleKey = testModuleKey('other', tenantOne); - const targetModuleKey = testModuleKey('independent', tenantOne); - const transitionCatalog = catalogFrom( - installedContract(otherModuleKey), - installedContract(targetModuleKey), + yield* withDatabase((database) => { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, ); - yield* withDatabase((database) => - database.executor.insert(tenantModuleStates).values({ - moduleKey: otherModuleKey, - state: 'inactive', - tenantId: tenantOne, - }), + const withCatalog = ( + effect: Effect.Effect, + ) => + effect.pipe( + Effect.provideService(InstalledModuleCatalogService, { + load: Effect.succeed(transitionCatalog), + }), + ); + const states = [ + 'active', + 'read_only', + 'suspended', + 'quarantined', + 'deprecated', + 'archived', + 'inactive', + ] as const; + return Effect.forEach( + states, + (state) => + withCatalog(runtime.runAction(actionInput(targetModuleKey, state, `independent-${state}`))), + { concurrency: 1, discard: true }, ); + }); - yield* withDatabase((database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - const withCatalog = ( - effect: Effect.Effect, - ) => - effect.pipe( - Effect.provideService(InstalledModuleCatalogService, { - load: Effect.succeed(transitionCatalog), - }), - ); - const states = [ + yield* withDatabase((database) => + Effect.gen(function* verifyAllDeclaredStates() { + const stateRows = yield* database.executor + .select({ moduleKey: tenantModuleStates.moduleKey, state: tenantModuleStates.state }) + .from(tenantModuleStates) + .where(inArray(tenantModuleStates.moduleKey, [otherModuleKey, targetModuleKey])); + const historyRows = yield* database.executor + .select() + .from(tenantModuleStateChanges) + .where(eq(tenantModuleStateChanges.moduleKey, targetModuleKey)); + expect(Object.fromEntries(stateRows.map((row) => [row.moduleKey, row.state]))).toEqual({ + [otherModuleKey]: 'inactive', + [targetModuleKey]: 'inactive', + }); + expect(historyRows.map(({ newState }) => newState).toSorted()).toEqual([ 'active', - 'read_only', - 'suspended', - 'quarantined', - 'deprecated', 'archived', + 'deprecated', 'inactive', - ] as const; - return Effect.forEach( - states, - (state) => - withCatalog( - runtime.runAction(actionInput(targetModuleKey, state, `independent-${state}`)), - ), - { concurrency: 1, discard: true }, - ); - }); - - yield* withDatabase((database) => - Effect.gen(function* verifyAllDeclaredStates() { - const stateRows = yield* database.executor - .select({ moduleKey: tenantModuleStates.moduleKey, state: tenantModuleStates.state }) - .from(tenantModuleStates) - .where(inArray(tenantModuleStates.moduleKey, [otherModuleKey, targetModuleKey])); - const historyRows = yield* database.executor - .select() - .from(tenantModuleStateChanges) - .where(eq(tenantModuleStateChanges.moduleKey, targetModuleKey)); - assert.deepEqual(Object.fromEntries(stateRows.map((row) => [row.moduleKey, row.state])), { - [otherModuleKey]: 'inactive', - [targetModuleKey]: 'inactive', - }); - assert.deepEqual(historyRows.map(({ newState }) => newState).toSorted(), [ - 'active', - 'archived', - 'deprecated', - 'inactive', - 'quarantined', - 'read_only', - 'suspended', - ]); - }), + 'quarantined', + 'read_only', + 'suspended', + ]); + }), + ); +}); +const tenantModuleStateTest4 = Effect.gen(function* idempotentReplayAndSameStateRejection() { + const moduleKey = testModuleKey('idempotency', tenantOne); + const input = actionInput(moduleKey, 'active', 'same-intent'); + + yield* withDatabase((database) => { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, ); - }), -); - -effectTest( - 'idempotent replay and same-state rejection create no duplicate history or evidence', - Effect.gen(function* idempotentReplayAndSameStateRejection() { - const moduleKey = testModuleKey('idempotency', tenantOne); - const input = actionInput(moduleKey, 'active', 'same-intent'); - - yield* withDatabase((database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - return runtime.runAction(input); - }); - const replay = yield* withDatabase((database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - return Effect.exit(runtime.runAction(input)); - }); - assert.equal(failureTag(replay), 'ActionAlreadyCommitted'); - - const unchanged = yield* withDatabase((database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - return Effect.exit(runtime.runAction(actionInput(moduleKey, 'active', 'same-state'))); - }); - assert.equal(failureTag(unchanged), 'TenantModuleStateUnchangedError'); - - yield* withDatabase((database) => - Effect.gen(function* verifyIdempotentEvidence() { - const history = yield* database.executor - .select() - .from(tenantModuleStateChanges) - .where(eq(tenantModuleStateChanges.moduleKey, moduleKey)); - assert.equal(history.length, 1); - const unchangedInvocation = yield* database.executor - .select() - .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, 'same-state')); - assert.equal(unchangedInvocation.length, 1); - const invocationId = unchangedInvocation[0]?.actionInvocationId ?? ''; - const unchangedAudit = yield* database.executor - .select() - .from(auditEvents) - .where(eq(auditEvents.actionInvocationId, invocationId)); - assert.equal(unchangedAudit.length, 0); - const unchangedAccess = yield* database.executor - .select() - .from(dataAccessEvents) - .where(eq(dataAccessEvents.actionInvocationId, invocationId)); - assert.equal(unchangedAccess.length, 0); - }), + return runtime.runAction(input); + }); + const replay = yield* withDatabase((database) => { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, ); - }), -); - + return Effect.exit(runtime.runAction(input)); + }); + expect(failureTag(replay)).toBe('ActionAlreadyCommitted'); + + const unchanged = yield* withDatabase((database) => { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, + ); + return Effect.exit(runtime.runAction(actionInput(moduleKey, 'active', 'same-state'))); + }); + expect(failureTag(unchanged)).toBe('TenantModuleStateUnchangedError'); + + yield* withDatabase((database) => + Effect.gen(function* verifyIdempotentEvidence() { + const history = yield* database.executor + .select() + .from(tenantModuleStateChanges) + .where(eq(tenantModuleStateChanges.moduleKey, moduleKey)); + expect(history.length).toBe(1); + const unchangedInvocation = yield* database.executor + .select() + .from(actionInvocations) + .where(eq(actionInvocations.idempotencyKey, 'same-state')); + expect(unchangedInvocation.length).toBe(1); + const invocationId = unchangedInvocation[0]?.actionInvocationId ?? ''; + const unchangedAudit = yield* database.executor + .select() + .from(auditEvents) + .where(eq(auditEvents.actionInvocationId, invocationId)); + expect(unchangedAudit.length).toBe(0); + const unchangedAccess = yield* database.executor + .select() + .from(dataAccessEvents) + .where(eq(dataAccessEvents.actionInvocationId, invocationId)); + expect(unchangedAccess.length).toBe(0); + }), + ); +}); const withTenantStateWriteFailure = (database: DatabaseService): DatabaseService => { const transaction: DatabaseService['executor']['transaction'] = (operation) => database.executor.transaction((currentTransaction) => @@ -540,57 +508,50 @@ const withTenantStateWriteFailure = (database: DatabaseService): DatabaseService ); return { executor }; }; - -effectTest( - 'rolls back history and Action evidence when current-state persistence fails', - Effect.gen(function* rollbackFailedTenantModuleStateWrite() { - const moduleKey = testModuleKey('rollback', tenantOne); - const failure = yield* withDatabase((database) => { - const runtime = makeActionRuntime( - withTenantStateWriteFailure(database), - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - return Effect.exit(runtime.runAction(actionInput(moduleKey, 'active', 'forced-failure'))); - }); - assert.equal( - failureTag(failure), - 'TenantModuleStatePersistenceUnavailableError', - Exit.isFailure(failure) ? Cause.pretty(failure.cause) : 'success', +const tenantModuleStateTest5 = Effect.gen(function* rollbackFailedTenantModuleStateWrite() { + const moduleKey = testModuleKey('rollback', tenantOne); + const failure = yield* withDatabase((database) => { + const runtime = makeActionRuntime( + withTenantStateWriteFailure(database), + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, ); - - yield* withDatabase((database) => - Effect.gen(function* verifyFailedWriteRollback() { - const states = yield* database.executor - .select() - .from(tenantModuleStates) - .where(eq(tenantModuleStates.moduleKey, moduleKey)); - assert.equal(states.length, 0); - const history = yield* database.executor - .select() - .from(tenantModuleStateChanges) - .where(eq(tenantModuleStateChanges.moduleKey, moduleKey)); - assert.equal(history.length, 0); - const [invocation] = yield* database.executor - .select() - .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, 'forced-failure')); - assert.ok(invocation); - const audits = yield* database.executor - .select() - .from(auditEvents) - .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)); - assert.equal(audits.length, 0); - }), - ); - }), -); - -effectTest( - 'serializes concurrent transitions into one truthful history chain', - Effect.gen(function* serializeConcurrentTenantModuleStateTransitions() { + return Effect.exit(runtime.runAction(actionInput(moduleKey, 'active', 'forced-failure'))); + }); + expect( + failureTag(failure), + Exit.isFailure(failure) ? Cause.pretty(failure.cause) : 'success', + ).toBe('TenantModuleStatePersistenceUnavailableError'); + + yield* withDatabase((database) => + Effect.gen(function* verifyFailedWriteRollback() { + const states = yield* database.executor + .select() + .from(tenantModuleStates) + .where(eq(tenantModuleStates.moduleKey, moduleKey)); + expect(states.length).toBe(0); + const history = yield* database.executor + .select() + .from(tenantModuleStateChanges) + .where(eq(tenantModuleStateChanges.moduleKey, moduleKey)); + expect(history.length).toBe(0); + const [invocation] = yield* database.executor + .select() + .from(actionInvocations) + .where(eq(actionInvocations.idempotencyKey, 'forced-failure')); + expect(invocation).toBeDefined(); + const audits = yield* database.executor + .select() + .from(auditEvents) + .where(eq(auditEvents.actionInvocationId, invocation?.actionInvocationId ?? '')); + expect(audits.length).toBe(0); + }), + ); +}); +const tenantModuleStateTest6 = Effect.gen( + function* serializeConcurrentTenantModuleStateTransitions() { const moduleKey = testModuleKey('concurrency', tenantOne); yield* withDatabase((database) => { const runtime = makeActionRuntime( @@ -621,7 +582,7 @@ effectTest( }), { concurrency: 'unbounded' }, ); - assert.ok(exits.every(Exit.isSuccess)); + expect(exits.every(Exit.isSuccess)).toBe(true); yield* withDatabase((database) => Effect.gen(function* verifySerializedTransitions() { @@ -633,57 +594,89 @@ effectTest( .select() .from(tenantModuleStateChanges) .where(eq(tenantModuleStateChanges.moduleKey, moduleKey)); - assert.equal(history.length, 3); + expect(history.length).toBe(3); const last = history.find((row) => row.moduleStateChangeId === current?.lastChangeId); const concurrentFirst = history.find( (row) => row.previousState === 'inactive' && row.moduleStateChangeId !== last?.moduleStateChangeId, ); - assert.ok(last); - assert.ok(concurrentFirst); - assert.equal(last.previousState, concurrentFirst.newState); - assert.equal(current?.state, last.newState); + expect(last).toBeDefined(); + expect(concurrentFirst).toBeDefined(); + expect(last?.previousState).toBe(concurrentFirst?.newState); + expect(current?.state).toBe(last?.newState); }), ); - }), + }, ); +const tenantModuleStateTest7 = Effect.gen(function* deriveTrustedTenantScope() { + const moduleKey = testModuleKey('isolation', tenantOne); + yield* withDatabase((database) => + database.executor.insert(tenantModuleStates).values({ + moduleKey, + state: 'active', + tenantId: tenantTwo, + }), + ); -effectTest( - 'derives tenant scope only from the trusted principal', - Effect.gen(function* deriveTrustedTenantScope() { - const moduleKey = testModuleKey('isolation', tenantOne); - yield* withDatabase((database) => - database.executor.insert(tenantModuleStates).values({ - moduleKey, - state: 'active', - tenantId: tenantTwo, - }), + yield* withDatabase((database) => { + const runtime = makeActionRuntime( + database, + makeActionRepository(), + allowedPermission, + testOperationalScopeResolver, + openActionRuntimeOptions, ); + return runtime.runAction(actionInput(moduleKey, 'suspended', 'tenant-isolation')); + }); - yield* withDatabase((database) => { - const runtime = makeActionRuntime( - database, - makeActionRepository(), - allowedPermission, - testOperationalScopeResolver, - openActionRuntimeOptions, - ); - return runtime.runAction(actionInput(moduleKey, 'suspended', 'tenant-isolation')); - }); + yield* withDatabase((database) => + Effect.gen(function* verifyTrustedTenantScope() { + const rows = yield* database.executor + .select({ state: tenantModuleStates.state, tenantId: tenantModuleStates.tenantId }) + .from(tenantModuleStates) + .where(eq(tenantModuleStates.moduleKey, moduleKey)) + .orderBy(asc(tenantModuleStates.tenantId)); + expect(Object.fromEntries(rows.map((row) => [row.tenantId, row.state]))).toEqual({ + [tenantOne]: 'suspended', + [tenantTwo]: 'active', + }); + }), + ); +}); +it.layer(Fixtures, { excludeTestServices: true })('tenant module state', (suite) => { + suite.effect( + 'lists exact active rows and all states for one trusted tenant in module-key order', + () => tenantModuleStateTest1, + ); - yield* withDatabase((database) => - Effect.gen(function* verifyTrustedTenantScope() { - const rows = yield* database.executor - .select({ state: tenantModuleStates.state, tenantId: tenantModuleStates.tenantId }) - .from(tenantModuleStates) - .where(eq(tenantModuleStates.moduleKey, moduleKey)) - .orderBy(asc(tenantModuleStates.tenantId)); - assert.deepEqual(Object.fromEntries(rows.map((row) => [row.tenantId, row.state])), { - [tenantOne]: 'suspended', - [tenantTwo]: 'active', - }); - }), - ); - }), -); + suite.effect( + 'atomically creates and transitions state with truthful Action history and evidence', + () => tenantModuleStateTest2, + ); + + suite.effect( + 'supports every declared state independently of other installed module states', + () => tenantModuleStateTest3, + ); + + suite.effect( + 'idempotent replay and same-state rejection create no duplicate history or evidence', + () => tenantModuleStateTest4, + ); + + suite.effect( + 'rolls back history and Action evidence when current-state persistence fails', + () => tenantModuleStateTest5, + ); + + suite.effect( + 'serializes concurrent transitions into one truthful history chain', + () => tenantModuleStateTest6, + ); + + suite.effect( + 'derives tenant scope only from the trusted principal', + () => tenantModuleStateTest7, + ); +}); diff --git a/app/packages/core-runtime/tests/support/database.ts b/app/packages/core-runtime/tests/support/database.ts index 13383c39c..0785503f8 100644 --- a/app/packages/core-runtime/tests/support/database.ts +++ b/app/packages/core-runtime/tests/support/database.ts @@ -1,4 +1,4 @@ -import type { Pool } from 'pg'; +import { Pool } from 'pg'; import { PgClient } from '@effect/sql-pg'; import type { AnyRelations } from 'drizzle-orm'; import { makeWithDefaults } from 'drizzle-orm/effect-postgres'; @@ -7,28 +7,27 @@ import { Reactivity } from 'effect/unstable/reactivity'; import { testSqlConnection } from './sql-connection.ts'; import type { SqlError } from 'effect/unstable/sql/SqlError'; import { coreRelations } from '../../src/db/schema.ts'; -import { runEffectTestSync } from './effect-runtime.ts'; +import { loadDatabaseConnectionPair } from '../../src/db/config.ts'; +import { acquirePoolResource } from '../../src/db/client.ts'; /** Native SQL connection fixture; Drizzle and Effect own query and transaction execution. */ export const makeTestDatabase = ( execute: (sql: string, params: readonly unknown[]) => Effect.Effect, ) => - runEffectTestSync( - Effect.scoped( - Effect.gen(function* makeNativeTestDatabase() { - const connection = testSqlConnection(execute); - const reactivity = yield* Reactivity.make; - const client = yield* PgClient.makeWith({ - acquirer: Effect.succeed(connection), - config: {}, - listenAcquirer: Effect.die('This fixture does not support notifications'), - transactionAcquirer: Effect.succeed(connection), - }).pipe(Effect.provideService(Reactivity.Reactivity, reactivity), Effect.orDie); - return yield* makeWithDefaults({ relations: coreRelations }).pipe( - Effect.provideService(PgClient.PgClient, client), - ); - }), - ), + Effect.scoped( + Effect.gen(function* makeNativeTestDatabase() { + const connection = testSqlConnection(execute); + const reactivity = yield* Reactivity.make; + const client = yield* PgClient.makeWith({ + acquirer: Effect.succeed(connection), + config: {}, + listenAcquirer: Effect.die('This fixture does not support notifications'), + transactionAcquirer: Effect.succeed(connection), + }).pipe(Effect.provideService(Reactivity.Reactivity, reactivity), Effect.orDie); + return yield* makeWithDefaults({ relations: coreRelations }).pipe( + Effect.provideService(PgClient.PgClient, client), + ); + }), ); /** The caller owns the pool and keeps this scope open until its tests finish. */ @@ -45,3 +44,15 @@ export const makeTestDatabaseFromPool = ( Effect.provideService(PgClient.PgClient, client), ); }); + +/** Fresh pools per execution; the caller's scope releases them after test cleanup. */ +export const testDatabasePools = Effect.gen(function* acquireTestDatabasePools() { + const connections = yield* loadDatabaseConnectionPair(); + const admin = yield* acquirePoolResource( + () => new Pool({ connectionString: connections.admin.connectionString }), + ); + const runtimePool = yield* acquirePoolResource( + () => new Pool({ connectionString: connections.runtime.connectionString }), + ); + return { admin, runtimePool }; +}); diff --git a/app/packages/core-runtime/tests/support/effect-runtime.ts b/app/packages/core-runtime/tests/support/effect-runtime.ts deleted file mode 100644 index 609402057..000000000 --- a/app/packages/core-runtime/tests/support/effect-runtime.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Cause, Exit, Layer, Logger, ManagedRuntime, References, Tracer } from 'effect'; -import type { SuiteContext, TestContext } from 'node:test'; -import type { Effect as EffectType } from 'effect'; - -const testTracer = Tracer.make({ - span: (options) => new Tracer.NativeSpan(options), -}); - -const testRuntime = ManagedRuntime.make( - Layer.mergeAll( - Logger.layer([Logger.defaultLogger]), - Layer.succeed(Tracer.Tracer, testTracer), - Layer.succeed(References.MinimumLogLevel, 'Info'), - ), -); - -export const runEffectTestPromise = testRuntime.runPromise; -export const runEffectTestSync = testRuntime.runSync; - -/** Adapts a fully provided Effect to Node's completion-callback test boundary. */ -export const makeEffectTestCallback = - (effect: EffectType.Effect) => - (_context: TestContext | SuiteContext, done: (result?: Error) => void): void => { - testRuntime.runCallback(effect, { - onExit: Exit.match({ - onFailure: (cause) => { - done(new Error(Cause.pretty(cause), { cause: Cause.squash(cause) })); - }, - onSuccess: () => { - done(); - }, - }), - }); - }; diff --git a/app/packages/core-runtime/tests/unit/action-authorization-rollout.test.ts b/app/packages/core-runtime/tests/unit/action-authorization-rollout.test.ts index 756391870..5068e5671 100644 --- a/app/packages/core-runtime/tests/unit/action-authorization-rollout.test.ts +++ b/app/packages/core-runtime/tests/unit/action-authorization-rollout.test.ts @@ -1,5 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { Effect, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; import { decideAuthorizationRollout } from '../../src/authorization/rollout-decision.ts'; import type { AuthorizationWouldDenyEvent } from '../../src/authorization/rollout-decision.ts'; @@ -23,70 +23,79 @@ const input = { surface: 'action' as const, }; -test('active, baselined report-only compatibility preserves only missing-policy behavior', () => { - const events: AuthorizationWouldDenyEvent[] = []; - assert.equal( - decideAuthorizationRollout(input, { - contract, - emit: (event) => { - events.push(event); - }, +it.effect( + 'active, baselined report-only compatibility preserves only missing-policy behavior', + () => + Effect.gen(function* authorizationRollout() { + const events: AuthorizationWouldDenyEvent[] = []; + expect( + decideAuthorizationRollout(input, { + contract, + emit: (event) => { + events.push(event); + }, + }), + ).toBe('allowed'); + expect(events).toEqual([ + { + denialReason: 'missing_policy', + entrypointKey: 'contacts.create-contact', + inventoryHash: 'inventory-hash', + policyClass: 'action_execution', + schemaVersion: 1, + sourceRevision: 'source-revision', + surface: 'action', + timestamp: '2026-09-10T00:00:00.000Z', + type: 'authorization.would_deny', + }, + ]); + expect( + (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(events)).includes( + 'principal', + ), + ).toBe(false); + expect( + (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(events)).includes( + 'tenant', + ), + ).toBe(false); }), - 'allowed', - ); - assert.deepEqual(events, [ - { - denialReason: 'missing_policy', - entrypointKey: 'contacts.create-contact', - inventoryHash: 'inventory-hash', - policyClass: 'action_execution', - schemaVersion: 1, - sourceRevision: 'source-revision', - surface: 'action', - timestamp: '2026-09-10T00:00:00.000Z', - type: 'authorization.would_deny', - }, - ]); - assert.equal(JSON.stringify(events).includes('principal'), false); - assert.equal(JSON.stringify(events).includes('tenant'), false); -}); +); -test('enforced, expired, and unbaselined entrypoints deny without evidence', () => { +it('enforced, expired, and unbaselined entrypoints deny without evidence', () => { for (const changed of [ { contract: { ...contract, mode: 'enforced' as const }, input }, { contract, input: { ...input, nowEpochMs: contract.expiresAtEpochMs } }, { contract, input: { ...input, entrypointKey: 'contacts.new-action' } }, ]) { const events: AuthorizationWouldDenyEvent[] = []; - assert.equal( + expect( decideAuthorizationRollout(changed.input, { contract: changed.contract, emit: (event) => { events.push(event); }, }), - 'denied', - ); - assert.deepEqual(events, []); + ).toBe('denied'); + expect(events).toEqual([]); } }); -test('a candidate allow never broadens a denial from the current authorization path', () => { - assert.equal( +it('a candidate allow never broadens a denial from the current authorization path', () => { + expect( decideAuthorizationRollout( { ...input, candidate: 'allowed', current: 'denied' }, { contract, emit: () => { - assert.fail(); + expect.unreachable(); }, }, ), - 'denied', - ); + ).toBe('denied'); }); -test('all protected surfaces keep credential, tenancy, module, replay, and infrastructure failures non-bypassable', () => { +it('all protected surfaces keep credential, tenancy, module, replay, and infrastructure failures non-bypassable', () => { for (const surface of ['action', 'capability_issuance', 'route', 'worker'] as const) { for (const denialReason of [ 'cross_tenant', @@ -97,18 +106,17 @@ test('all protected surfaces keep credential, tenancy, module, replay, and infra 'replayed_credential', 'wrong_audience', ] as const) { - assert.equal( + expect( decideAuthorizationRollout( { ...input, denialReason, surface }, { contract, emit: () => { - assert.fail(); + expect.unreachable(); }, }, ), - 'denied', - ); + ).toBe('denied'); } } }); diff --git a/app/packages/core-runtime/tests/unit/action-collector.test.ts b/app/packages/core-runtime/tests/unit/action-collector.test.ts index 71fa21527..b3f9cc2f0 100644 --- a/app/packages/core-runtime/tests/unit/action-collector.test.ts +++ b/app/packages/core-runtime/tests/unit/action-collector.test.ts @@ -1,8 +1,5 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import test from 'node:test'; -import { Effect, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Schema, Predicate } from 'effect'; import { createActionCollector } from '../../src/actions/collector.ts'; const event = (id: string) => @@ -39,14 +36,14 @@ const makeCollector = () => Schema.Struct({ checkpoint: Schema.String, nested: Schema.optionalKey(Schema.Json) }), ); -void test('preserves event order, multiple messages, and events without messages', async () => { - const collector = makeCollector(); - const first = await runEffectTestPromise(collector.addDomainEvent(event('first'))); - await runEffectTestPromise(collector.addDomainEvent(event('second'))); - await runEffectTestPromise(collector.addOutboxMessage(first, message('counter.project'))); - await runEffectTestPromise(collector.addOutboxMessage(first, message('counter.notify'))); - await runEffectTestPromise( - collector.recordDataAccess({ +it.effect('preserves event order, multiple messages, and events without messages', () => + Effect.gen(function* preservesEventOrderMultipleMessagesAndEventsWithout() { + const collector = makeCollector(); + const first = yield* collector.addDomainEvent(event('first')); + yield* collector.addDomainEvent(event('second')); + yield* collector.addOutboxMessage(first, message('counter.project')); + yield* collector.addOutboxMessage(first, message('counter.notify')); + yield* collector.recordDataAccess({ accessKind: 'read', queryHash: 'query-hash', resultCount: 1, @@ -54,200 +51,187 @@ void test('preserves event order, multiple messages, and events without messages targetModuleKey: 'shell.core', targetResourceId: 'first', targetResourceType: 'counter', - }), - ); + }); - const snapshot = collector.snapshot(); + const snapshot = collector.snapshot(); - assert.deepEqual( - snapshot.domainEvents.map((item) => item.subjectResourceId), - ['first', 'second'], - ); - assert.deepEqual( - snapshot.outboxMessages.map((item) => [item.domainEventIndex, item.message.topic]), - [ + expect(snapshot.domainEvents.map((item) => item.subjectResourceId)).toEqual([ + 'first', + 'second', + ]); + expect( + snapshot.outboxMessages.map((item) => [item.domainEventIndex, item.message.topic]), + ).toEqual([ [0, 'counter.project'], [0, 'counter.notify'], - ], - ); - assert.equal(snapshot.dataAccessEvents.length, 1); -}); - -void test('rejects orphan and foreign Domain Event references', async () => { - const first = makeCollector(); - const second = makeCollector(); - const foreign = await runEffectTestPromise(first.addDomainEvent(event('foreign'))); - - const foreignError = await runEffectTestPromise( - Effect.flip(second.addOutboxMessage(foreign, message('counter.project'))), - ); - const orphanError = await runEffectTestPromise( - Effect.flip(second.addOutboxMessageInput({}, message('counter.project'))), - ); - - assert.equal(foreignError._tag, 'ActionCollectorError'); - assert.equal(orphanError._tag, 'ActionCollectorError'); -}); + ]); + expect(snapshot.dataAccessEvents.length).toBe(1); + }), +); + +it.effect('rejects orphan and foreign Domain Event references', () => + Effect.gen(function* rejectsOrphanAndForeignDomainEventReferences() { + const first = makeCollector(); + const second = makeCollector(); + const foreign = yield* first.addDomainEvent(event('foreign')); + + const foreignError = yield* Effect.flip( + second.addOutboxMessage(foreign, message('counter.project')), + ); + const orphanError = yield* Effect.flip( + second.addOutboxMessageInput({}, message('counter.project')), + ); + + expect(Predicate.isTagged(foreignError, 'ActionCollectorError')).toBe(true); + expect(Predicate.isTagged(orphanError, 'ActionCollectorError')).toBe(true); + }), +); -void test('does not expose externally mutable collector arrays or captured payloads', async () => { - const collector = makeCollector(); - const mutablePayload = { value: 1 }; - await runEffectTestPromise( - collector.addDomainEvent({ +it.effect('does not expose externally mutable collector arrays or captured payloads', () => + Effect.gen(function* doesNotExposeExternallyMutableCollectorArraysOr() { + const collector = makeCollector(); + const mutablePayload = { value: 1 }; + yield* collector.addDomainEvent({ ...event('immutable'), payloadJson: { id: 'immutable', mutable: mutablePayload }, - }), - ); - mutablePayload.value = 2; - - const snapshot = collector.snapshot(); - - assert.equal(Object.isFrozen(snapshot.domainEvents), true); - assert.equal(Object.isFrozen(snapshot.domainEvents[0]), true); - assert.deepEqual(snapshot.domainEvents[0]?.payloadJson, { - id: 'immutable', - mutable: { value: 1 }, - }); - assert.throws(() => { - Object.defineProperty(snapshot.domainEvents, snapshot.domainEvents.length, { - value: event('mutated'), }); - }); -}); + mutablePayload.value = 2; -void test('captures one immutable JSON audit-evidence object and rejects invalid repeats', async () => { - const collector = makeCollector(); - const nested = { reason: 'support request' }; - await runEffectTestPromise(collector.recordAuditEvidence({ checkpoint: 'started', nested })); - nested.reason = 'mutated'; - - const snapshot = collector.snapshot(); - assert.deepEqual(snapshot.auditEvidence, { - checkpoint: 'started', - nested: { reason: 'support request' }, - }); - assert.equal(Object.isFrozen(snapshot.auditEvidence), true); - assert.equal(Object.isFrozen(snapshot.auditEvidence.nested), true); - - const repeated = await runEffectTestPromise( - Effect.flip(collector.recordAuditEvidence({ checkpoint: 'stopped' })), - ); - const invalid = await runEffectTestPromise( - Effect.flip(makeCollector().recordAuditEvidenceInput({ value: undefined })), - ); - const undeclared = await runEffectTestPromise( - Effect.flip( + const snapshot = collector.snapshot(); + + expect(Object.isFrozen(snapshot.domainEvents)).toBe(true); + expect(Object.isFrozen(snapshot.domainEvents[0])).toBe(true); + expect(snapshot.domainEvents[0]?.payloadJson).toEqual({ + id: 'immutable', + mutable: { value: 1 }, + }); + expect(() => { + Object.defineProperty(snapshot.domainEvents, snapshot.domainEvents.length, { + value: event('mutated'), + }); + }).toThrow(); + }), +); + +it.effect('captures one immutable JSON audit-evidence object and rejects invalid repeats', () => + Effect.gen(function* capturesOneImmutableJSONAuditevidenceObjectAndRejects() { + const collector = makeCollector(); + const nested = { reason: 'support request' }; + yield* collector.recordAuditEvidence({ checkpoint: 'started', nested }); + nested.reason = 'mutated'; + + const snapshot = collector.snapshot(); + expect(snapshot.auditEvidence).toEqual({ + checkpoint: 'started', + nested: { reason: 'support request' }, + }); + expect(Object.isFrozen(snapshot.auditEvidence)).toBe(true); + expect(Object.isFrozen(snapshot.auditEvidence['nested'])).toBe(true); + + const repeated = yield* Effect.flip(collector.recordAuditEvidence({ checkpoint: 'stopped' })); + const invalid = yield* Effect.flip( + makeCollector().recordAuditEvidenceInput({ value: undefined }), + ); + const undeclared = yield* Effect.flip( makeCollector().recordAuditEvidence({ checkpoint: 'started', secret: 'must-not-persist' }), - ), - ); - const missingSchema = await runEffectTestPromise( - Effect.flip( + ); + const missingSchema = yield* Effect.flip( createActionCollector(domainEventContracts, 'shell.core', { captureMode: 'metadata_only', policyKey: 'counter.read.v1', }).recordAuditEvidence({ checkpoint: 'started' }), - ), - ); - assert.equal(repeated._tag, 'ActionCollectorError'); - assert.equal(invalid._tag, 'ActionCollectorError'); - assert.equal(undeclared._tag, 'ActionCollectorError'); - assert.equal(missingSchema._tag, 'ActionCollectorError'); -}); - -void test('applies descriptor evidence policy and rejects incompatible evidence', async () => { - const collector = createActionCollector(domainEventContracts, 'shell.core', { - captureMode: 'redacted_payload', - policyKey: 'counter.read.redacted.v1', - redactionProfile: 'counter.summary.v1', - }); - const error = await runEffectTestPromise( - Effect.flip( + ); + expect(Predicate.isTagged(repeated, 'ActionCollectorError')).toBe(true); + expect(Predicate.isTagged(invalid, 'ActionCollectorError')).toBe(true); + expect(Predicate.isTagged(undeclared, 'ActionCollectorError')).toBe(true); + expect(Predicate.isTagged(missingSchema, 'ActionCollectorError')).toBe(true); + }), +); + +it.effect('applies descriptor evidence policy and rejects incompatible evidence', () => + Effect.gen(function* appliesDescriptorEvidencePolicyAndRejectsIncompatibleEvidence() { + const collector = createActionCollector(domainEventContracts, 'shell.core', { + captureMode: 'redacted_payload', + policyKey: 'counter.read.redacted.v1', + redactionProfile: 'counter.summary.v1', + }); + const error = yield* Effect.flip( collector.recordDataAccessInput({ accessKind: 'read', queryHash: 'query-hash', resultCount: 1, servingModuleKey: 'shell.core', }), - ), - ); + ); - assert.equal(error._tag, 'ActionCollectorError'); + expect(Predicate.isTagged(error, 'ActionCollectorError')).toBe(true); - const metadataCollector = makeCollector(); - await runEffectTestPromise( - metadataCollector.recordDataAccessInput({ + const metadataCollector = makeCollector(); + yield* metadataCollector.recordDataAccessInput({ accessKind: 'read', evidenceCaptureMode: 'stored_artifact', evidencePolicyKey: 'handler-controlled', queryHash: 'metadata-query', resultCount: 1, servingModuleKey: 'shell.core', - }), - ); - assert.equal( - metadataCollector.snapshot().dataAccessEvents[0]?.evidenceCaptureMode, - 'metadata_only', - ); - assert.equal( - metadataCollector.snapshot().dataAccessEvents[0]?.evidencePolicyKey, - 'counter.read.v1', - ); -}); + }); + expect(metadataCollector.snapshot().dataAccessEvents[0]?.evidenceCaptureMode).toBe( + 'metadata_only', + ); + expect(metadataCollector.snapshot().dataAccessEvents[0]?.evidencePolicyKey).toBe( + 'counter.read.v1', + ); + }), +); -void test('rejects an Outbox producer that differs from its registered Domain Event', async () => { - const collector = makeCollector(); - const reference = await runEffectTestPromise(collector.addDomainEvent(event('producer'))); - const error = await runEffectTestPromise( - Effect.flip( +it.effect('rejects an Outbox producer that differs from its registered Domain Event', () => + Effect.gen(function* rejectsAnOutboxProducerThatDiffersFromIts() { + const collector = makeCollector(); + const reference = yield* collector.addDomainEvent(event('producer')); + const error = yield* Effect.flip( collector.addOutboxMessage(reference, { payloadJson: {}, producerModuleKey: 'another.module', topic: 'counter.project', }), - ), - ); + ); - assert.equal(error._tag, 'ActionCollectorError'); -}); + expect(Predicate.isTagged(error, 'ActionCollectorError')).toBe(true); + }), +); -void test('enforces Action-declared event payloads and producer ownership', async () => { - const collector = makeCollector(); - const invalidPayload = await runEffectTestPromise( - Effect.flip( +it.effect('enforces Action-declared event payloads and producer ownership', () => + Effect.gen(function* enforcesActiondeclaredEventPayloadsAndProducerOwnership() { + const collector = makeCollector(); + const invalidPayload = yield* Effect.flip( collector.addDomainEventInput({ ...event('payload'), payloadJson: { id: 1 }, }), - ), - ); - const invalidProducer = await runEffectTestPromise( - Effect.flip( + ); + const invalidProducer = yield* Effect.flip( collector.addDomainEvent({ ...event('producer'), producerModuleKey: 'another.module', }), - ), - ); - const undeclared = await runEffectTestPromise( - Effect.flip( + ); + const undeclared = yield* Effect.flip( collector.addDomainEventInput({ ...event('undeclared'), eventType: 'counter.reset', }), - ), - ); - const inheritedName = await runEffectTestPromise( - Effect.flip( + ); + const inheritedName = yield* Effect.flip( collector.addDomainEventInput({ ...event('inherited'), eventType: 'toString', }), - ), - ); + ); - assert.equal(invalidPayload._tag, 'ActionCollectorError'); - assert.equal(invalidProducer._tag, 'ActionCollectorError'); - assert.equal(undeclared._tag, 'ActionCollectorError'); - assert.equal(inheritedName._tag, 'ActionCollectorError'); - assert.equal(collector.snapshot().domainEvents.length, 0); -}); + expect(Predicate.isTagged(invalidPayload, 'ActionCollectorError')).toBe(true); + expect(Predicate.isTagged(invalidProducer, 'ActionCollectorError')).toBe(true); + expect(Predicate.isTagged(undeclared, 'ActionCollectorError')).toBe(true); + expect(Predicate.isTagged(inheritedName, 'ActionCollectorError')).toBe(true); + expect(collector.snapshot().domainEvents.length).toBe(0); + }), +); diff --git a/app/packages/core-runtime/tests/unit/action-definition.test.ts b/app/packages/core-runtime/tests/unit/action-definition.test.ts index 34b0e42e6..d34db7632 100644 --- a/app/packages/core-runtime/tests/unit/action-definition.test.ts +++ b/app/packages/core-runtime/tests/unit/action-definition.test.ts @@ -1,7 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { DateTime, Effect, Option, Schema, Predicate } from 'effect'; import { decodeActionPayload, @@ -38,31 +35,33 @@ const counterActionDescriptor = () => schemaVersion: '1', }) as const; -void test('defines an immutable typed descriptor and decodes typed payloads and results', async () => { - const registration = defineAction( - { - ...counterActionDescriptor(), - resultSchema: Schema.Struct({ total: Schema.Finite }), - schemaVersion: '1', - }, - (payload) => Effect.succeed({ total: payload.amount }), - ); +it.effect('defines an immutable typed descriptor and decodes typed payloads and results', () => + Effect.gen(function* definesAnImmutableTypedDescriptorAndDecodesTyped() { + const registration = defineAction( + { + ...counterActionDescriptor(), + resultSchema: Schema.Struct({ total: Schema.Finite }), + schemaVersion: '1', + }, + (payload) => Effect.succeed({ total: payload.amount }), + ); - const payload = await runEffectTestPromise( - decodeActionPayload(registration.descriptor.payloadSchema, { amount: 4 }), - ); - const result = await runEffectTestPromise( - decodeActionResult(registration.descriptor.resultSchema, { total: payload.amount }), - ); + const payload = yield* decodeActionPayload(registration.descriptor.payloadSchema, { + amount: 4, + }); + const result = yield* decodeActionResult(registration.descriptor.resultSchema, { + total: payload.amount, + }); - assert.deepEqual(payload, { amount: 4 }); - assert.deepEqual(result, { total: 4 }); - assert.equal(Object.isFrozen(registration), true); - assert.equal(Object.isFrozen(registration.descriptor), true); - assert.equal(Object.isFrozen(registration.descriptor.policies), true); -}); + expect(payload).toEqual({ amount: 4 }); + expect(result).toEqual({ total: 4 }); + expect(Object.isFrozen(registration)).toBe(true); + expect(Object.isFrozen(registration.descriptor)).toBe(true); + expect(Object.isFrozen(registration.descriptor.policies)).toBe(true); + }), +); -test('keeps the Resource permission resolver private behind an immutable declaration', () => { +it('keeps the Resource permission resolver private behind an immutable declaration', () => { const permission = defineActionResourcePermission<{ readonly counterpartyId: string }>( ({ counterpartyId }) => ({ permission: 'write', @@ -74,13 +73,13 @@ test('keeps the Resource permission resolver private behind an immutable declara }), ); - assert.equal(Object.isFrozen(permission), true); - assert.deepEqual(Object.keys(permission), ['kind']); - assert.equal(permission.kind, 'resource'); - assert.equal('resolver' in permission, false); + expect(Object.isFrozen(permission)).toBe(true); + expect(Object.keys(permission)).toEqual(['kind']); + expect(permission.kind).toBe('resource'); + expect('resolver' in permission).toBe(false); }); -test('requires trusted Legal Entity scope for a Counterparty permission declaration', () => { +it('requires trusted Legal Entity scope for a Counterparty permission declaration', () => { const entrypoint = defineTenantModuleEntrypoint({ access: 'write', authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, @@ -88,7 +87,7 @@ test('requires trusted Legal Entity scope for a Counterparty permission declarat moduleKey: 'party.registry', role: 'action', }); - assert.throws(() => + expect(() => validateActionDescriptorInput({ entrypoint, legalEntityPermission: 'manage_counterparty', @@ -96,8 +95,8 @@ test('requires trusted Legal Entity scope for a Counterparty permission declarat owningModuleKey: 'party.registry', policies: [], }), - ); - assert.doesNotThrow(() => + ).toThrow(); + expect(() => validateActionDescriptorInput({ entrypoint, legalEntityPermission: 'manage_counterparty', @@ -105,48 +104,51 @@ test('requires trusted Legal Entity scope for a Counterparty permission declarat owningModuleKey: 'party.registry', policies: [], }), - ); + ).not.toThrow(); }); -test('uses Schema.Void for a no-payload Action', async () => { - const registration = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'cache.read.v1' }, - actionKey: 'shell.cache.refresh', - auditProfile: 'minimal', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineSystemModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'shell.cache.refresh', - moduleKey: 'core.shell', - role: 'action', - }), - idempotency: 'optional', - legalEntityScope: 'optional', - owningModuleKey: 'core.shell', - payloadSchema: Schema.Void, - policies: [], - resultSchema: Schema.Void, - schemaVersion: '1', - }, - () => Effect.void, - ); +it.effect('uses Schema.Void for a no-payload Action', () => + Effect.gen(function* usesSchemaVoidForANopayloadAction() { + const registration = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'cache.read.v1' }, + actionKey: 'shell.cache.refresh', + auditProfile: 'minimal', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineSystemModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'shell.cache.refresh', + moduleKey: 'core.shell', + role: 'action', + }), + idempotency: 'optional', + legalEntityScope: 'optional', + owningModuleKey: 'core.shell', + payloadSchema: Schema.Void, + policies: [], + resultSchema: Schema.Void, + schemaVersion: '1', + }, + () => Effect.void, + ); - const payload = await runEffectTestPromise( - // eslint-disable-next-line unicorn/no-useless-undefined -- Explicitly proves the Schema.Void payload contract. - decodeActionPayload(registration.descriptor.payloadSchema, undefined), - ); - const invalid = await runEffectTestPromise( - Effect.flip(decodeActionPayload(registration.descriptor.payloadSchema, {})), - ); + // oxlint-disable-next-line unicorn/no-useless-undefined -- Required argument exercises the no-payload contract. + const payload = yield* decodeActionPayload(registration.descriptor.payloadSchema, undefined); + const invalid = yield* Effect.flip( + decodeActionPayload(registration.descriptor.payloadSchema, {}), + ); - assert.equal(payload, undefined); - assert.equal(invalid._tag, 'ActionPayloadValidationError'); -}); + expect(payload).toBeUndefined(); + expect(Predicate.isTagged(invalid, 'ActionPayloadValidationError')).toBe(true); + }), +); -void test('keeps the private handler outside the public Action registration', () => { +it('keeps the private handler outside the public Action registration', () => { const registration = defineAction( { ...counterActionDescriptor(), @@ -156,35 +158,41 @@ void test('keeps the private handler outside the public Action registration', () (payload) => Effect.succeed(payload.amount), ); - assert.equal('handler' in registration, false); - assert.deepEqual(Object.keys(registration), ['descriptor']); + expect('handler' in registration).toBe(false); + expect(Object.keys(registration)).toEqual(['descriptor']); }); -void test('rejects invalid declared results through a typed error', async () => { - const error = await runEffectTestPromise( - Effect.flip(decodeActionResult(Schema.Struct({ id: Schema.String }), { id: 1 })), - ); +it.effect('rejects invalid declared results through a typed error', () => + Effect.gen(function* rejectsInvalidDeclaredResultsThroughATypedError() { + const error = yield* Effect.flip( + decodeActionResult(Schema.Struct({ id: Schema.String }), { id: 1 }), + ); - assert.equal(error._tag, 'ActionResultValidationError'); - assert.equal(error.code, 'action_result_invalid'); -}); + expect(Predicate.isTagged(error, 'ActionResultValidationError')).toBe(true); + expect(error.code).toBe('action_result_invalid'); + }), +); -void test('validates decoded DateTime and Option results through their encoded representation', async () => { - const resultSchema = Schema.Struct({ - archivedAt: Schema.OptionFromNullOr(Schema.DateTimeUtcFromString), - createdAt: Schema.DateTimeUtcFromString, - }); - const decoded = Schema.decodeUnknownSync(resultSchema)({ - archivedAt: null, - createdAt: '2026-09-07T10:30:00.000Z', - }); - const result = await runEffectTestPromise(decodeActionResult(resultSchema, decoded)); +it.effect( + 'validates decoded DateTime and Option results through their encoded representation', + () => + Effect.gen(function* validatesDecodedDateTimeAndOptionResultsThroughTheir() { + const resultSchema = Schema.Struct({ + archivedAt: Schema.OptionFromNullOr(Schema.DateTimeUtcFromString), + createdAt: Schema.DateTimeUtcFromString, + }); + const decoded = yield* Schema.decodeUnknownEffect(resultSchema)({ + archivedAt: null, + createdAt: '2026-09-07T10:30:00.000Z', + }); + const result = yield* decodeActionResult(resultSchema, decoded); - assert.equal(Option.isNone(result.archivedAt), true); - assert.equal(DateTime.formatIso(result.createdAt), '2026-09-07T10:30:00.000Z'); -}); + expect(Option.isNone(result.archivedAt)).toBe(true); + expect(DateTime.formatIso(result.createdAt)).toBe('2026-09-07T10:30:00.000Z'); + }), +); -void test('accepts global and same-owner Policy references and copies the collection', () => { +it('accepts global and same-owner Policy references and copies the collection', () => { const globalPolicy = defineGlobalPolicy<{ readonly amount: number }>({ evaluate: () => Effect.void, policyKey: 'global.tenant-active.v1', @@ -221,13 +229,13 @@ void test('accepts global and same-owner Policy references and copies the collec ); policies.pop(); - assert.deepEqual(registration.descriptor.policies, [globalPolicy, modulePolicy]); - assert.equal(Object.isFrozen(registration.descriptor.policies), true); - assert.equal(registration.descriptor.policies[0], globalPolicy); - assert.equal(registration.descriptor.policies[1], modulePolicy); + expect(registration.descriptor.policies).toEqual([globalPolicy, modulePolicy]); + expect(Object.isFrozen(registration.descriptor.policies)).toBe(true); + expect(registration.descriptor.policies[0]).toBe(globalPolicy); + expect(registration.descriptor.policies[1]).toBe(modulePolicy); }); -void test('rejects cross-owner, string, copied, and missing Policy references at definition time', () => { +it('rejects cross-owner, string, copied, and missing Policy references at definition time', () => { const foreignPolicy = defineMicroverticalPolicy({ evaluate: () => Effect.void, owningModuleKey: 'billing.invoice', @@ -276,7 +284,7 @@ void test('rejects cross-owner, string, copied, and missing Policy references at ); }; - assert.throws(() => + expect(() => defineAction( { ...descriptor, @@ -285,21 +293,21 @@ void test('rejects cross-owner, string, copied, and missing Policy references at }, () => Effect.void, ), - ); - assert.equal(Predicate.isFunction(compileOnlyInvalidReferences), true); - assert.throws(() => + ).toThrow(); + expect(Predicate.isFunction(compileOnlyInvalidReferences)).toBe(true); + expect(() => validateActionDescriptorInput({ ...descriptor, policies: ['inventory.stock.available.v1'], }), - ); - assert.throws(() => + ).toThrow(); + expect(() => validateActionDescriptorInput({ ...descriptor, policies: [{ ...foreignPolicy }] }), - ); - assert.throws(() => validateActionDescriptorInput(descriptor)); + ).toThrow(); + expect(() => validateActionDescriptorInput(descriptor)).toThrow(); }); -void test('rejects Action entrypoint owner, scope, role/access, and forged immutability mismatches', () => { +it('rejects Action entrypoint owner, scope, role/access, and forged immutability mismatches', () => { const registration = defineAction( { accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'stock.read.v1' }, @@ -324,7 +332,7 @@ void test('rejects Action entrypoint owner, scope, role/access, and forged immut }, () => Effect.void, ); - assert.throws(() => + expect(() => validateActionDescriptorInput({ ...registration.descriptor, entrypoint: defineTenantModuleEntrypoint({ @@ -335,8 +343,8 @@ void test('rejects Action entrypoint owner, scope, role/access, and forged immut role: 'action', }), }), - ); - assert.throws(() => + ).toThrow(); + expect(() => validateActionDescriptorInput({ ...registration.descriptor, entrypoint: defineSystemModuleEntrypoint({ @@ -347,8 +355,8 @@ void test('rejects Action entrypoint owner, scope, role/access, and forged immut role: 'action', }), }), - ); - assert.throws(() => + ).toThrow(); + expect(() => validateActionDescriptorInput({ ...registration.descriptor, entrypoint: defineTenantModuleEntrypoint({ @@ -360,17 +368,17 @@ void test('rejects Action entrypoint owner, scope, role/access, and forged immut }), owningModuleKey: 'core.modules', }), - ); - assert.throws(() => + ).toThrow(); + expect(() => validateActionDescriptorInput({ ...registration.descriptor, entrypoint: { ...registration.descriptor.entrypoint }, }), - ); - assert.throws(() => + ).toThrow(); + expect(() => validateActionDescriptorInput({ ...registration.descriptor, legalEntityScope: 'implicit', }), - ); + ).toThrow(); }); diff --git a/app/packages/core-runtime/tests/unit/action-errors.test.ts b/app/packages/core-runtime/tests/unit/action-errors.test.ts index e489d4952..cb725af40 100644 --- a/app/packages/core-runtime/tests/unit/action-errors.test.ts +++ b/app/packages/core-runtime/tests/unit/action-errors.test.ts @@ -1,6 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Schema, Predicate } from 'effect'; import { ACTION_CORE_ERROR_TAGS, ActionAlreadyCommitted, @@ -26,7 +25,7 @@ import { ModuleStateDeniedError, } from '../../src/modules/module-state-gate-errors.ts'; -void test('publishes the exhaustive stable Core Action error tags', () => { +it('publishes the exhaustive stable Core Action error tags', () => { const errors = [ new ActionPayloadValidationError({ code: 'action_payload_invalid', @@ -109,18 +108,18 @@ void test('publishes the exhaustive stable Core Action error tags', () => { }), ]; - assert.deepEqual( - errors.map((error) => error._tag), - ACTION_CORE_ERROR_TAGS, - ); + expect(errors.length).toBe(ACTION_CORE_ERROR_TAGS.length); + for (const [index, tag] of ACTION_CORE_ERROR_TAGS.entries()) { + expect(Predicate.isTagged(errors[index], tag)).toBe(true); + } for (const error of errors) { - assert.equal(error.reason.includes('postgresql://'), false); - assert.equal(error.reason.includes('ontos-local-development-key'), false); - assert.equal('status' in error, false); + expect(error.reason.includes('postgresql://')).toBe(false); + expect(error.reason.includes('ontos-local-development-key')).toBe(false); + expect('status' in error).toBe(false); } const denial = errors.find(Schema.is(ActionPolicyDenied)); - assert.equal(denial?.reason, 'This tenant is suspended'); - assert.equal(denial?.policyReasonCode, 'tenant_suspended'); - assert.equal('payload' in (denial ?? {}), false); - assert.equal('cause' in (denial ?? {}), false); + expect(denial?.reason).toBe('This tenant is suspended'); + expect(denial?.policyReasonCode).toBe('tenant_suspended'); + expect('payload' in (denial ?? {})).toBe(false); + expect('cause' in (denial ?? {})).toBe(false); }); diff --git a/app/packages/core-runtime/tests/unit/action-http-runner.test.ts b/app/packages/core-runtime/tests/unit/action-http-runner.test.ts index 996db1635..48bfbdbf7 100644 --- a/app/packages/core-runtime/tests/unit/action-http-runner.test.ts +++ b/app/packages/core-runtime/tests/unit/action-http-runner.test.ts @@ -1,13 +1,10 @@ -// @effect-diagnostics asyncFunction:off nodeBuiltinImport:off -- Node's test API owns the Promise boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Effect, Redacted, Schema } from 'effect'; import { defineAction } from '../../src/actions/definition.ts'; import { ActionRuntime } from '../../src/actions/runtime.ts'; import type { ActionRuntimeService } from '../../src/actions/runtime.ts'; import { runGovernedActionHttp } from '../../src/http/http-instrumentation-seam.ts'; import { defineSystemModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; -import { runEffectTestPromise } from '../../src/testing/effect-runtime.ts'; const principal = { authBindingId: '10000000-0000-4000-8000-000000000001', @@ -57,42 +54,61 @@ const invalidProblem = { _tag: 'InvalidProblem' as const }; const internalProblem = { _tag: 'InternalProblem' as const }; const authorization = (value?: string) => Redacted.make(value); -const forEachSequential = async ( - items: Iterable, - run: (item: Item) => Promise, -): Promise => { - const iterator = items[Symbol.iterator](); - const advance = async (): Promise => { - const item = iterator.next(); - if (item.done === true) { - return; - } - await run(item.value); - await advance(); - }; - await advance(); -}; +it.effect( + 'invalid correlation metadata is rejected before principal acquisition and runtime lookup', + () => + Effect.gen(function* rejectInvalidCorrelation() { + const requestHeaders = [ + {}, + { 'x-correlation-id': '' }, + { 'x-correlation-id': ' ' }, + ] as const; + let authenticationCalls = 0; + let runtimeCalls = 0; + const runtime = unusedRuntime(() => { + runtimeCalls += 1; + }); + const authenticate = () => { + authenticationCalls += 1; + return Effect.succeed(principal); + }; + + for (const headers of requestHeaders) { + const effect = runGovernedActionHttp({ + endpointHeaders: { idempotencyKey: 'not-reached', traceId: 'not-reached' }, + internalProblem: () => internalProblem, + invalidCorrelationProblem: () => invalidProblem, + mapError: () => internalProblem, + payload: {}, + principal: { authenticate }, + registration, + requestHeaders: { authorization: authorization(), ...headers }, + }).pipe(Effect.provideService(ActionRuntime, runtime)); -test('invalid correlation metadata is rejected before principal acquisition and runtime lookup', async () => { - const requestHeaders = [{}, { 'x-correlation-id': '' }, { 'x-correlation-id': ' ' }] as const; - let authenticationCalls = 0; - let runtimeCalls = 0; + expect(yield* Effect.flip(effect)).toBe(invalidProblem); + } + + expect(authenticationCalls).toBe(0); + expect(runtimeCalls).toBe(0); + }), +); - await forEachSequential(requestHeaders, async (headers) => { +it.effect('principal authentication failure prevents Action runtime execution', () => + Effect.gen(function* rejectAuthenticationFailure() { + const authenticationProblem = { _tag: 'AuthenticationProblem' as const }; + let runtimeCalls = 0; const effect = runGovernedActionHttp({ endpointHeaders: { idempotencyKey: 'not-reached', traceId: 'not-reached' }, internalProblem: () => internalProblem, invalidCorrelationProblem: () => invalidProblem, mapError: () => internalProblem, payload: {}, - principal: { - authenticate: () => { - authenticationCalls += 1; - return Effect.succeed(principal); - }, - }, + principal: { authenticate: () => Effect.fail(authenticationProblem) }, registration, - requestHeaders: { authorization: authorization(), ...headers }, + requestHeaders: { + authorization: authorization(), + 'x-correlation-id': 'correlation-test', + }, }).pipe( Effect.provideService( ActionRuntime, @@ -102,89 +118,54 @@ test('invalid correlation metadata is rejected before principal acquisition and ), ); - await assert.rejects(runEffectTestPromise(effect), (failure) => failure === invalidProblem); - }); - - assert.equal(authenticationCalls, 0); - assert.equal(runtimeCalls, 0); -}); - -test('principal authentication failure prevents Action runtime execution', async () => { - const authenticationProblem = { _tag: 'AuthenticationProblem' as const }; - let runtimeCalls = 0; - const effect = runGovernedActionHttp({ - endpointHeaders: { idempotencyKey: 'not-reached', traceId: 'not-reached' }, - internalProblem: () => internalProblem, - invalidCorrelationProblem: () => invalidProblem, - mapError: () => internalProblem, - payload: {}, - principal: { authenticate: () => Effect.fail(authenticationProblem) }, - registration, - requestHeaders: { - authorization: authorization(), - 'x-correlation-id': 'correlation-test', - }, - }).pipe( - Effect.provideService( - ActionRuntime, - unusedRuntime(() => { - runtimeCalls += 1; - }), - ), - ); - - await assert.rejects( - runEffectTestPromise(effect), - (failure) => failure === authenticationProblem, - ); - assert.equal(runtimeCalls, 0); -}); + expect(yield* Effect.flip(effect)).toBe(authenticationProblem); + expect(runtimeCalls).toBe(0); + }), +); -test('synchronous endpoint callback defects are sanitized before the Action runtime', async () => { - const callbackDefects = [ - { - invalidCorrelationProblem: () => { - throw new Error('private invalid-problem constructor defect'); - }, - principal: { authenticate: () => Effect.succeed(principal) }, - requestHeaders: { authorization: authorization('Bearer private-token') }, - }, - { - invalidCorrelationProblem: () => invalidProblem, - principal: { - authenticate: () => { - throw new Error('private principal authentication defect'); +it.effect('synchronous endpoint callback defects are sanitized before the Action runtime', () => + Effect.gen(function* sanitizeCallbackDefects() { + const callbackDefects = [ + { + invalidCorrelationProblem: () => { + throw new Error('private invalid-problem constructor defect'); }, + principal: { authenticate: () => Effect.succeed(principal) }, + requestHeaders: { authorization: authorization('Bearer private-token') }, }, - requestHeaders: { - authorization: authorization('Bearer private-token'), - 'x-correlation-id': 'correlation-auth-defect', + { + invalidCorrelationProblem: () => invalidProblem, + principal: { + authenticate: () => { + throw new Error('private principal authentication defect'); + }, + }, + requestHeaders: { + authorization: authorization('Bearer private-token'), + 'x-correlation-id': 'correlation-auth-defect', + }, }, - }, - ] as const; - let runtimeCalls = 0; + ] as const; + let runtimeCalls = 0; + const runtime = unusedRuntime(() => { + runtimeCalls += 1; + }); - await forEachSequential(callbackDefects, async (fixture) => { - const effect = runGovernedActionHttp({ - endpointHeaders: { idempotencyKey: 'not-reached', traceId: 'not-reached' }, - internalProblem: () => internalProblem, - invalidCorrelationProblem: fixture.invalidCorrelationProblem, - mapError: () => internalProblem, - payload: {}, - principal: fixture.principal, - registration, - requestHeaders: fixture.requestHeaders, - }).pipe( - Effect.provideService( - ActionRuntime, - unusedRuntime(() => { - runtimeCalls += 1; - }), - ), - ); + for (const fixture of callbackDefects) { + const effect = runGovernedActionHttp({ + endpointHeaders: { idempotencyKey: 'not-reached', traceId: 'not-reached' }, + internalProblem: () => internalProblem, + invalidCorrelationProblem: fixture.invalidCorrelationProblem, + mapError: () => internalProblem, + payload: {}, + principal: fixture.principal, + registration, + requestHeaders: fixture.requestHeaders, + }).pipe(Effect.provideService(ActionRuntime, runtime)); - await assert.rejects(runEffectTestPromise(effect), (failure) => failure === internalProblem); - }); + expect(yield* Effect.flip(effect)).toBe(internalProblem); + } - assert.equal(runtimeCalls, 0); -}); + expect(runtimeCalls).toBe(0); + }), +); diff --git a/app/packages/core-runtime/tests/unit/action-identity.test.ts b/app/packages/core-runtime/tests/unit/action-identity.test.ts index 9bcb80e3a..485dc9d7b 100644 --- a/app/packages/core-runtime/tests/unit/action-identity.test.ts +++ b/app/packages/core-runtime/tests/unit/action-identity.test.ts @@ -1,6 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Schema } from 'effect'; import { bindManagedApiKeyAction, bindSelfApiKeyAction, @@ -21,157 +20,165 @@ const registrations = [ setSelfApiKeyBindingStatusAction, ] as const; -void test('identity Actions are generated, sensitive, idempotent, and owned by Core identity', () => { +it('identity Actions are generated, sensitive, idempotent, and owned by Core identity', () => { for (const registration of registrations) { - assert.equal(registration.descriptor.actionKey.startsWith('core.identity.'), true); - assert.equal(registration.descriptor.auditProfile, 'sensitive'); - assert.equal(registration.descriptor.idempotency, 'required'); - assert.equal(registration.descriptor.owningModuleKey, 'core.identity'); - assert.equal(registration.descriptor.accessEvidencePolicy.captureMode, 'metadata_only'); - assert.equal(Object.isFrozen(registration.descriptor), true); + expect(registration.descriptor.actionKey.startsWith('core.identity.')).toBe(true); + expect(registration.descriptor.auditProfile).toBe('sensitive'); + expect(registration.descriptor.idempotency).toBe('required'); + expect(registration.descriptor.owningModuleKey).toBe('core.identity'); + expect(registration.descriptor.accessEvidencePolicy.captureMode).toBe('metadata_only'); + expect(Object.isFrozen(registration.descriptor)).toBe(true); } }); -void test('identity administration and support starts declare independent tenant permissions', () => { - const principalId = '00000000-0000-4000-8000-000000000001'; - const authBindingId = '00000000-0000-4000-8000-000000000002'; - const originalPrincipalId = '00000000-0000-4000-8000-000000000003'; - const managedPermissions = [ - bindManagedApiKeyAction.descriptor.tenantPermission?.( - Schema.decodeUnknownSync(bindManagedApiKeyAction.descriptor.payloadSchema)({ - principalId, - providerSubjectId: 'provider-key-id', - }), - ), - changePrincipalStatusAction.descriptor.tenantPermission?.( - Schema.decodeUnknownSync(changePrincipalStatusAction.descriptor.payloadSchema)({ - expectedStatus: 'active', - newStatus: 'disabled', - principalId, - reason: 'Offboarding', - }), - ), - createNonHumanPrincipalAction.descriptor.tenantPermission?.( - Schema.decodeUnknownSync(createNonHumanPrincipalAction.descriptor.payloadSchema)({ - displayName: 'Inventory service', - kind: 'service', - }), - ), - setManagedApiKeyBindingStatusAction.descriptor.tenantPermission?.( - Schema.decodeUnknownSync(setManagedApiKeyBindingStatusAction.descriptor.payloadSchema)({ - authBindingId, - expectedStatus: 'active', - newStatus: 'disabled', - principalId, - }), - ), - ]; - for (const permission of managedPermissions) { - assert.equal(permission, 'manage_identity'); - } - assert.equal(bindSelfApiKeyAction.descriptor.tenantPermission, undefined); - assert.equal(setSelfApiKeyBindingStatusAction.descriptor.tenantPermission, undefined); - const supportPayload = { - originalPrincipalId, - reason: 'Investigating a support request', - targetPrincipalId: principalId, - }; - assert.equal( - recordSupportImpersonationAction.descriptor.tenantPermission?.( - Schema.decodeUnknownSync(recordSupportImpersonationAction.descriptor.payloadSchema)({ - ...supportPayload, - checkpoint: 'requested', - }), - ), - 'impersonate', - ); - assert.equal( - recordSupportImpersonationAction.descriptor.tenantPermission?.( - Schema.decodeUnknownSync(recordSupportImpersonationAction.descriptor.payloadSchema)({ - ...supportPayload, - checkpoint: 'stopped', - sessionRef: 'better-auth-session:safe-session-reference', - }), - ), - undefined, - ); -}); +it.effect('identity administration and support starts declare independent tenant permissions', () => + Effect.gen(function* identityScenario2() { + const principalId = '00000000-0000-4000-8000-000000000001'; + const authBindingId = '00000000-0000-4000-8000-000000000002'; + const originalPrincipalId = '00000000-0000-4000-8000-000000000003'; + const managedPermissions = [ + bindManagedApiKeyAction.descriptor.tenantPermission?.( + yield* Schema.decodeUnknownEffect(bindManagedApiKeyAction.descriptor.payloadSchema)({ + principalId, + providerSubjectId: 'provider-key-id', + }), + ), + changePrincipalStatusAction.descriptor.tenantPermission?.( + yield* Schema.decodeUnknownEffect(changePrincipalStatusAction.descriptor.payloadSchema)({ + expectedStatus: 'active', + newStatus: 'disabled', + principalId, + reason: 'Offboarding', + }), + ), + createNonHumanPrincipalAction.descriptor.tenantPermission?.( + yield* Schema.decodeUnknownEffect(createNonHumanPrincipalAction.descriptor.payloadSchema)({ + displayName: 'Inventory service', + kind: 'service', + }), + ), + setManagedApiKeyBindingStatusAction.descriptor.tenantPermission?.( + yield* Schema.decodeUnknownEffect( + setManagedApiKeyBindingStatusAction.descriptor.payloadSchema, + )({ + authBindingId, + expectedStatus: 'active', + newStatus: 'disabled', + principalId, + }), + ), + ]; + for (const permission of managedPermissions) { + expect(permission).toBe('manage_identity'); + } + expect(bindSelfApiKeyAction.descriptor.tenantPermission).toBe(undefined); + expect(setSelfApiKeyBindingStatusAction.descriptor.tenantPermission).toBe(undefined); + const supportPayload = { + originalPrincipalId, + reason: 'Investigating a support request', + targetPrincipalId: principalId, + }; + expect( + recordSupportImpersonationAction.descriptor.tenantPermission?.( + yield* Schema.decodeUnknownEffect( + recordSupportImpersonationAction.descriptor.payloadSchema, + )({ + ...supportPayload, + checkpoint: 'requested', + }), + ), + ).toBe('impersonate'); + expect( + recordSupportImpersonationAction.descriptor.tenantPermission?.( + yield* Schema.decodeUnknownEffect( + recordSupportImpersonationAction.descriptor.payloadSchema, + )({ + ...supportPayload, + checkpoint: 'stopped', + sessionRef: 'better-auth-session:safe-session-reference', + }), + ), + ).toBe(undefined); + }), +); -void test('identity status schemas require reasons for disabling, archiving, and revoking', () => { +it('identity status schemas require reasons for disabling, archiving, and revoking', () => { const principalId = '00000000-0000-4000-8000-000000000001'; const authBindingId = '00000000-0000-4000-8000-000000000002'; - assert.throws(() => + expect(() => Schema.decodeUnknownSync(changePrincipalStatusAction.descriptor.payloadSchema)({ expectedStatus: 'active', newStatus: 'disabled', principalId, }), - ); - assert.throws(() => + ).toThrow(); + expect(() => Schema.decodeUnknownSync(setSelfApiKeyBindingStatusAction.descriptor.payloadSchema)({ authBindingId, expectedStatus: 'active', newStatus: 'revoked', }), - ); - assert.throws(() => + ).toThrow(); + expect(() => Schema.decodeUnknownSync(setManagedApiKeyBindingStatusAction.descriptor.payloadSchema)({ authBindingId, expectedStatus: 'active', newStatus: 'revoked', principalId, }), - ); + ).toThrow(); }); -void test('support checkpoints forbid unsafe or misplaced session references', () => { - const originalPrincipalId = '00000000-0000-4000-8000-000000000001'; - const targetPrincipalId = '00000000-0000-4000-8000-000000000002'; - const decode = Schema.decodeUnknownSync( - recordSupportImpersonationAction.descriptor.payloadSchema, - ); +it.effect('support checkpoints forbid unsafe or misplaced session references', () => + Effect.gen(function* identityScenario4() { + const originalPrincipalId = '00000000-0000-4000-8000-000000000001'; + const targetPrincipalId = '00000000-0000-4000-8000-000000000002'; + const decode = Schema.decodeUnknownEffect( + recordSupportImpersonationAction.descriptor.payloadSchema, + ); - assert.deepEqual( - decode({ - checkpoint: 'requested', - originalPrincipalId, - reason: 'Investigating a support request', - sessionRef: 'better-auth-session:must-not-exist-yet', - targetPrincipalId, - }), - { + expect( + yield* decode({ + checkpoint: 'requested', + originalPrincipalId, + reason: 'Investigating a support request', + sessionRef: 'better-auth-session:must-not-exist-yet', + targetPrincipalId, + }), + ).toEqual({ checkpoint: 'requested', originalPrincipalId, reason: 'Investigating a support request', targetPrincipalId, - }, - ); - for (const sessionRef of ['raw-session-token', 'better-auth-session:contains whitespace']) { - assert.throws(() => - decode({ + }); + for (const sessionRef of ['raw-session-token', 'better-auth-session:contains whitespace']) { + expect( + yield* Effect.flip( + decode({ + checkpoint: 'stopped', + originalPrincipalId, + reason: 'Investigating a support request', + sessionRef, + targetPrincipalId, + }), + ), + ).toBeDefined(); + } + expect( + yield* decode({ checkpoint: 'stopped', originalPrincipalId, reason: 'Investigating a support request', - sessionRef, + sessionRef: 'better-auth-session:safe-session-reference', targetPrincipalId, }), - ); - } - assert.deepEqual( - decode({ + ).toEqual({ checkpoint: 'stopped', originalPrincipalId, reason: 'Investigating a support request', sessionRef: 'better-auth-session:safe-session-reference', targetPrincipalId, - }), - { - checkpoint: 'stopped', - originalPrincipalId, - reason: 'Investigating a support request', - sessionRef: 'better-auth-session:safe-session-reference', - targetPrincipalId, - }, - ); -}); + }); + }), +); diff --git a/app/packages/core-runtime/tests/unit/action-permission.test.ts b/app/packages/core-runtime/tests/unit/action-permission.test.ts index 4a44a2ed0..4fc559e11 100644 --- a/app/packages/core-runtime/tests/unit/action-permission.test.ts +++ b/app/packages/core-runtime/tests/unit/action-permission.test.ts @@ -1,7 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off missingEffectError:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { v1 } from '@authzed/authzed-node'; import { Effect, Schema } from 'effect'; import { @@ -52,282 +49,291 @@ const makeClient = ( }; }; -test('loads the root SpiceDB environment independently of the invocation directory', async () => { - const originalDirectory = process.cwd(); - const rootExamplePath = SPICEDB_ROOT_ENV_PATH.replace(/\.env$/u, '.env.example'); +it.effect('loads the root SpiceDB environment independently of the invocation directory', () => + Effect.gen(function* loadsTheRootSpiceDBEnvironmentIndependentlyOfThe() { + const originalDirectory = process.cwd(); + const rootExamplePath = SPICEDB_ROOT_ENV_PATH.replace(/\.env$/u, '.env.example'); - try { - process.chdir('/'); - const configuration = await runEffectTestPromise( - loadSpiceDbConfig({ environment: {}, envPath: rootExamplePath }), + yield* Effect.addFinalizer(() => + Effect.sync(() => { + process.chdir(originalDirectory); + }), ); + process.chdir('/'); + const configuration = yield* loadSpiceDbConfig({ environment: {}, envPath: rootExamplePath }); - assert.equal(SPICEDB_ROOT_ENV_PATH.endsWith('/app/.env'), true); - assert.deepEqual(configuration, { + expect(SPICEDB_ROOT_ENV_PATH.endsWith('/app/.env')).toBe(true); + expect(configuration).toEqual({ endpoint: 'localhost:50051', insecureLocal: true, preSharedKey: 'ontos-local-development-key', }); - } finally { - process.chdir(originalDirectory); - } -}); + }), +); -test('requires complete configuration and explicit secure or localhost-insecure transport', async () => { - const validSecure = await runEffectTestPromise( - parseSpiceDbConfig({ - SPICEDB_ENDPOINT: 'spicedb.internal.example:443', - SPICEDB_INSECURE: 'false', - SPICEDB_PRESHARED_KEY: 'test-key', - }), - ); - const failures = await Promise.all( - [ - {}, - { - SPICEDB_ENDPOINT: 'localhost:50051', - SPICEDB_PRESHARED_KEY: 'test-key', - }, - { - SPICEDB_ENDPOINT: 'spicedb.internal.example:50051', - SPICEDB_INSECURE: 'true', - SPICEDB_PRESHARED_KEY: 'test-key', - }, - { - SPICEDB_ENDPOINT: 'https://spicedb.internal.example/path', +it.effect( + 'requires complete configuration and explicit secure or localhost-insecure transport', + () => + Effect.gen(function* requiresCompleteConfigurationAndExplicitSecureOrLocalhostinsecure() { + const validSecure = yield* parseSpiceDbConfig({ + SPICEDB_ENDPOINT: 'spicedb.internal.example:443', SPICEDB_INSECURE: 'false', SPICEDB_PRESHARED_KEY: 'test-key', - }, - { - SPICEDB_ENDPOINT: 'spicedb.internal.example:443?credential=test-key', - SPICEDB_INSECURE: 'false', - SPICEDB_PRESHARED_KEY: 'test-key', - }, - { - SPICEDB_ENDPOINT: 'localhost:50051#fragment', - SPICEDB_INSECURE: 'true', - SPICEDB_PRESHARED_KEY: 'test-key', - }, - { - SPICEDB_ENDPOINT: 'localhost:50051', - SPICEDB_INSECURE: 'true', - SPICEDB_PRESHARED_KEY: ' ', - }, - ].map( - async (environment) => - await runEffectTestPromise(Effect.flip(parseSpiceDbConfig(environment))), - ), - ); + }); + const failures = yield* Effect.all( + [ + {}, + { + SPICEDB_ENDPOINT: 'localhost:50051', + SPICEDB_PRESHARED_KEY: 'test-key', + }, + { + SPICEDB_ENDPOINT: 'spicedb.internal.example:50051', + SPICEDB_INSECURE: 'true', + SPICEDB_PRESHARED_KEY: 'test-key', + }, + { + SPICEDB_ENDPOINT: 'https://spicedb.internal.example/path', + SPICEDB_INSECURE: 'false', + SPICEDB_PRESHARED_KEY: 'test-key', + }, + { + SPICEDB_ENDPOINT: 'spicedb.internal.example:443?credential=test-key', + SPICEDB_INSECURE: 'false', + SPICEDB_PRESHARED_KEY: 'test-key', + }, + { + SPICEDB_ENDPOINT: 'localhost:50051#fragment', + SPICEDB_INSECURE: 'true', + SPICEDB_PRESHARED_KEY: 'test-key', + }, + { + SPICEDB_ENDPOINT: 'localhost:50051', + SPICEDB_INSECURE: 'true', + SPICEDB_PRESHARED_KEY: ' ', + }, + ].map((environment) => Effect.flip(parseSpiceDbConfig(environment))), + ); - assert.deepEqual(validSecure, { - endpoint: 'spicedb.internal.example:443', - insecureLocal: false, - preSharedKey: 'test-key', - }); - assert.ok(failures.every(Schema.is(SpiceDbConfigError))); - assert.equal( - failures.some((failure) => failure.reason.includes('test-key')), - false, - ); -}); + expect(validSecure).toEqual({ + endpoint: 'spicedb.internal.example:443', + insecureLocal: false, + preSharedKey: 'test-key', + }); + expect(failures.every(Schema.is(SpiceDbConfigError))).toBe(true); + expect(failures.some((failure) => failure.reason.includes('test-key'))).toBe(false); + }), +); -test('allows insecure transport only for the exact Zerops stage private endpoint', async () => { - const stage = await runEffectTestPromise( - parseSpiceDbConfig({ +it.effect('allows insecure transport only for the exact Zerops stage private endpoint', () => + Effect.gen(function* allowsInsecureTransportOnlyForTheExactZerops() { + const stage = yield* parseSpiceDbConfig({ SPICEDB_ENDPOINT: 'spicedb:50051', SPICEDB_INSECURE: 'true', SPICEDB_PRESHARED_KEY: 'test-key', ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'stage', - }), - ); - const rejected = await Promise.all( - [ - { - SPICEDB_ENDPOINT: 'spicedb:50051', - SPICEDB_INSECURE: 'true', - SPICEDB_PRESHARED_KEY: 'test-key', - }, - { - SPICEDB_ENDPOINT: 'spicedb:50052', - SPICEDB_INSECURE: 'true', - SPICEDB_PRESHARED_KEY: 'test-key', - ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'stage', - }, - { - SPICEDB_ENDPOINT: 'spicedb:50051', - SPICEDB_INSECURE: 'true', - SPICEDB_PRESHARED_KEY: 'test-key', - ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'production', - }, - ].map( - async (environment) => - await runEffectTestPromise(Effect.flip(parseSpiceDbConfig(environment))), - ), - ); + }); + const rejected = yield* Effect.all( + [ + { + SPICEDB_ENDPOINT: 'spicedb:50051', + SPICEDB_INSECURE: 'true', + SPICEDB_PRESHARED_KEY: 'test-key', + }, + { + SPICEDB_ENDPOINT: 'spicedb:50052', + SPICEDB_INSECURE: 'true', + SPICEDB_PRESHARED_KEY: 'test-key', + ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'stage', + }, + { + SPICEDB_ENDPOINT: 'spicedb:50051', + SPICEDB_INSECURE: 'true', + SPICEDB_PRESHARED_KEY: 'test-key', + ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'production', + }, + ].map((environment) => Effect.flip(parseSpiceDbConfig(environment))), + ); - assert.deepEqual(stage, { - deploymentEnvironment: 'stage', - endpoint: 'spicedb:50051', - insecureLocal: true, - preSharedKey: 'test-key', - }); - assert.ok(rejected.every(Schema.is(SpiceDbConfigError))); -}); + expect(stage).toEqual({ + deploymentEnvironment: 'stage', + endpoint: 'spicedb:50051', + insecureLocal: true, + preSharedKey: 'test-key', + }); + expect(rejected.every(Schema.is(SpiceDbConfigError))).toBe(true); + }), +); -test('losslessly maps Action keys and exact principal identities using fully consistent requests', async () => { - const requests: v1.CheckPermissionRequest[] = []; - const service = makeActionPermissionService( - makeClient([response(v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION)], requests), - ); +it.effect( + 'losslessly maps Action keys and exact principal identities using fully consistent requests', + () => + Effect.gen(function* losslesslyMapsActionKeysAndExactPrincipalIdentities() { + const requests: v1.CheckPermissionRequest[] = []; + const service = makeActionPermissionService( + makeClient([response(v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION)], requests), + ); - const decision = await runEffectTestPromise(service.checkActionPermission(input)); + const decision = yield* service.checkActionPermission(input); - assert.equal(decision, 'allowed'); - assert.equal(requests.length, 1); - assert.deepEqual(requests[0]?.resource, { - objectId: toSpiceDbActionObjectId(input.actionKey), - objectType: SPICEDB_ACTION_OBJECT_TYPE, - }); - assert.equal(toSpiceDbActionObjectId(input.actionKey), 'ak_aW52ZW50b3J5LnN0b2NrLnJlc2VydmU'); - assert.notEqual( - toSpiceDbActionObjectId('inventory.stock.reserve'), - toSpiceDbActionObjectId('inventory-stock-reserve'), - ); - assert.deepEqual(requests[0]?.subject?.object, { - objectId: input.principalId, - objectType: SPICEDB_PRINCIPAL_OBJECT_TYPE, - }); - assert.equal(requests[0]?.permission, SPICEDB_EXECUTE_PERMISSION); - for (const request of requests) { - assert.deepEqual(request.consistency?.requirement, { - fullyConsistent: true, - oneofKind: 'fullyConsistent', - }); - } -}); + expect(decision).toBe('allowed'); + expect(requests.length).toBe(1); + expect(requests[0]?.resource).toEqual({ + objectId: toSpiceDbActionObjectId(input.actionKey), + objectType: SPICEDB_ACTION_OBJECT_TYPE, + }); + expect(toSpiceDbActionObjectId(input.actionKey)).toBe('ak_aW52ZW50b3J5LnN0b2NrLnJlc2VydmU'); + expect(toSpiceDbActionObjectId('inventory.stock.reserve')).not.toBe( + toSpiceDbActionObjectId('inventory-stock-reserve'), + ); + expect(requests[0]?.subject?.object).toEqual({ + objectId: input.principalId, + objectType: SPICEDB_PRINCIPAL_OBJECT_TYPE, + }); + expect(requests[0]?.permission).toBe(SPICEDB_EXECUTE_PERMISSION); + for (const request of requests) { + expect(request.consistency?.requirement).toEqual({ + fullyConsistent: true, + oneofKind: 'fullyConsistent', + }); + } + }), +); -test('classifies fully consistent execute permission as allowed or denied with one check', async () => { - const deniedRequests: v1.CheckPermissionRequest[] = []; - const allowed = makeActionPermissionService( - makeClient([response(v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION)]), - ); - const denied = makeActionPermissionService( - makeClient([response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION)], deniedRequests), - ); +it.effect( + 'classifies fully consistent execute permission as allowed or denied with one check', + () => + Effect.gen(function* classifiesFullyConsistentExecutePermissionAsAllowedOr() { + const deniedRequests: v1.CheckPermissionRequest[] = []; + const allowed = makeActionPermissionService( + makeClient([response(v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION)]), + ); + const denied = makeActionPermissionService( + makeClient( + [response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION)], + deniedRequests, + ), + ); - assert.equal(await runEffectTestPromise(allowed.checkActionPermission(input)), 'allowed'); - assert.equal(await runEffectTestPromise(denied.checkActionPermission(input)), 'denied'); - assert.equal(deniedRequests.length, 1); -}); + expect(yield* allowed.checkActionPermission(input)).toBe('allowed'); + expect(yield* denied.checkActionPermission(input)).toBe('denied'); + expect(deniedRequests.length).toBe(1); + }), +); -test('report-only compatibility distinguishes missing policy from an explicit restriction', async () => { - const nowEpochMs = Date.parse('2026-09-10T00:00:00.000Z'); - const events: unknown[] = []; - const rollout = { - activatedAtEpochMs: nowEpochMs - 1000, - compatibilityEntrypoints: new Set([input.actionKey]), - expiresAtEpochMs: nowEpochMs + 1000, - inventoryHash: 'inventory', - mode: 'report_only' as const, - sourceRevision: 'revision', - }; - const missingRequests: v1.CheckPermissionRequest[] = []; - const missing = makeActionPermissionService( - makeClient( - [ - response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION), - response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION), - ], - missingRequests, - ), - { - emit: (event) => { - events.push(event); - }, - nowEpochMs: () => nowEpochMs, - rollout, - }, - ); - assert.equal(await runEffectTestPromise(missing.checkActionPermission(input)), 'allowed'); - assert.deepEqual( - missingRequests.map(({ permission }) => permission), - [SPICEDB_EXECUTE_PERMISSION, SPICEDB_RESTRICTION_PERMISSION], - ); - assert.equal(events.length, 1); +it.effect( + 'report-only compatibility distinguishes missing policy from an explicit restriction', + () => + Effect.gen(function* reportonlyCompatibilityDistinguishesMissingPolicyFromAnExplicit() { + const nowEpochMs = Date.parse('2026-09-10T00:00:00.000Z'); + const events: unknown[] = []; + const rollout = { + activatedAtEpochMs: nowEpochMs - 1000, + compatibilityEntrypoints: new Set([input.actionKey]), + expiresAtEpochMs: nowEpochMs + 1000, + inventoryHash: 'inventory', + mode: 'report_only' as const, + sourceRevision: 'revision', + }; + const missingRequests: v1.CheckPermissionRequest[] = []; + const missing = makeActionPermissionService( + makeClient( + [ + response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION), + response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION), + ], + missingRequests, + ), + { + emit: (event) => { + events.push(event); + }, + nowEpochMs: () => nowEpochMs, + rollout, + }, + ); + expect(yield* missing.checkActionPermission(input)).toBe('allowed'); + expect(missingRequests.map(({ permission }) => permission)).toEqual([ + SPICEDB_EXECUTE_PERMISSION, + SPICEDB_RESTRICTION_PERMISSION, + ]); + expect(events.length).toBe(1); - const restricted = makeActionPermissionService( - makeClient([ - response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION), - response(v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION), - ]), - { emit: () => assert.fail(), nowEpochMs: () => nowEpochMs, rollout }, - ); - assert.equal(await runEffectTestPromise(restricted.checkActionPermission(input)), 'denied'); -}); + const restricted = makeActionPermissionService( + makeClient([ + response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION), + response(v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION), + ]), + { emit: () => expect.unreachable(), nowEpochMs: () => nowEpochMs, rollout }, + ); + expect(yield* restricted.checkActionPermission(input)).toBe('denied'); + }), +); -test('fails closed for conditional, unspecified, malformed, and client failures', async () => { - const failures = await Promise.all( - [ - makeClient([response(v1.CheckPermissionResponse_Permissionship.CONDITIONAL_PERMISSION)]), - makeClient([response(v1.CheckPermissionResponse_Permissionship.UNSPECIFIED)]), - makeClient([Effect.fail(spiceDbPermissionClientError())]), - makeClient([ - Effect.fail( - spiceDbPermissionClientError( - new Error('ontos-local-development-key unavailable at internal host'), +it.effect('fails closed for conditional, unspecified, malformed, and client failures', () => + Effect.gen(function* failsClosedForConditionalUnspecifiedMalformedAndClient() { + const failures = yield* Effect.all( + [ + makeClient([response(v1.CheckPermissionResponse_Permissionship.CONDITIONAL_PERMISSION)]), + makeClient([response(v1.CheckPermissionResponse_Permissionship.UNSPECIFIED)]), + makeClient([Effect.fail(spiceDbPermissionClientError())]), + makeClient([ + Effect.fail( + spiceDbPermissionClientError( + new Error('ontos-local-development-key unavailable at internal host'), + ), ), - ), - ]), - ].map( - async (client) => - await runEffectTestPromise( - Effect.flip(makeActionPermissionService(client).checkActionPermission(input)), - ), - ), - ); + ]), + ].map((client) => + Effect.flip(makeActionPermissionService(client).checkActionPermission(input)), + ), + ); - for (const failure of failures) { - assert.ok(Schema.is(ActionPermissionCheckError)(failure)); - assert.equal(failure.code, 'action_permission_check_failed'); - assert.equal(failure.reason.includes('ontos-local-development-key'), false); - assert.equal(failure.reason.includes('internal host'), false); - } -}); + for (const failure of failures) { + expect(Schema.is(ActionPermissionCheckError)(failure)).toBe(true); + expect(failure.code).toBe('action_permission_check_failed'); + expect(failure.reason.includes('ontos-local-development-key')).toBe(false); + expect(failure.reason.includes('internal host')).toBe(false); + } + }), +); -test('constructs the live client with a bounded deadline and finalizes it with the scope', async () => { - let finalized = false; - let observedTimeout = 0; - const configuration = { - endpoint: 'localhost:50051', - insecureLocal: true, - preSharedKey: 'test-key', - } as const; +it.effect( + 'constructs the live client with a bounded deadline and finalizes it with the scope', + () => + Effect.gen(function* constructsTheLiveClientWithABoundedDeadline() { + let finalized = false; + let observedTimeout = 0; + const configuration = { + endpoint: 'localhost:50051', + insecureLocal: true, + preSharedKey: 'test-key', + } as const; - await runEffectTestPromise( - Effect.scoped( - makeActionPermissionLive( - (_configuration, timeoutMilliseconds) => { - observedTimeout = timeoutMilliseconds; - return { - checkPermission: () => - response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION), - close: () => { - finalized = true; - }, - }; - }, - () => Effect.succeed(configuration), - ).pipe(Effect.flatMap((service) => service.checkActionPermission(input))), - ), - ); + yield* Effect.scoped( + makeActionPermissionLive( + (_configuration, timeoutMilliseconds) => { + observedTimeout = timeoutMilliseconds; + return { + checkPermission: () => + response(v1.CheckPermissionResponse_Permissionship.NO_PERMISSION), + close: () => { + finalized = true; + }, + }; + }, + () => Effect.succeed(configuration), + ).pipe(Effect.flatMap((service) => service.checkActionPermission(input))), + ); - assert.equal(observedTimeout, SPICEDB_CHECK_TIMEOUT_MS); - assert.equal(finalized, true); -}); + expect(observedTimeout).toBe(SPICEDB_CHECK_TIMEOUT_MS); + expect(finalized).toBe(true); + }), +); -test('turns missing live configuration into a fail-closed permission service', async () => { - const failure = await runEffectTestPromise( - Effect.scoped( +it.effect('turns missing live configuration into a fail-closed permission service', () => + Effect.gen(function* turnsMissingLiveConfigurationIntoAFailclosedPermission() { + const failure = yield* Effect.scoped( makeActionPermissionLive( () => { throw new Error('the client must not be constructed'); @@ -337,17 +343,17 @@ test('turns missing live configuration into a fail-closed permission service', a Effect.flatMap((service) => service.checkActionPermission(input)), Effect.flip, ), - ), - ); + ); - assert.ok(Schema.is(ActionPermissionCheckError)(failure)); - assert.equal(failure.code, 'action_permission_check_failed'); -}); + expect(Schema.is(ActionPermissionCheckError)(failure)).toBe(true); + expect(failure.code).toBe('action_permission_check_failed'); + }), +); -test('finalizes an acquired client even when its scoped use fails', async () => { - let finalized = false; - const failure = await runEffectTestPromise( - Effect.flip( +it.effect('finalizes an acquired client even when its scoped use fails', () => + Effect.gen(function* finalizesAnAcquiredClientEvenWhenItsScoped() { + let finalized = false; + const failure = yield* Effect.flip( Effect.scoped( acquirePermissionClientResource(() => ({ checkPermission: () => @@ -357,9 +363,9 @@ test('finalizes an acquired client even when its scoped use fails', async () => }, })).pipe(Effect.flatMap(() => Effect.fail('test-failure'))), ), - ), - ); + ); - assert.equal(failure, 'test-failure'); - assert.equal(finalized, true); -}); + expect(failure).toBe('test-failure'); + expect(finalized).toBe(true); + }), +); diff --git a/app/packages/core-runtime/tests/unit/action-policy.test.ts b/app/packages/core-runtime/tests/unit/action-policy.test.ts index 9911f4319..1c9e6b557 100644 --- a/app/packages/core-runtime/tests/unit/action-policy.test.ts +++ b/app/packages/core-runtime/tests/unit/action-policy.test.ts @@ -1,8 +1,5 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import test from 'node:test'; -import { Effect } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Predicate } from 'effect'; import { defineGlobalPolicy, defineMicroverticalPolicy, @@ -31,7 +28,7 @@ const input = { transport: { correlationId: 'correlation-policy' }, } as const; -void test('defines immutable global and owner-local Policy references', () => { +it('defines immutable global and owner-local Policy references', () => { const globalPolicy = defineGlobalPolicy({ evaluate: () => Effect.void, policyKey: 'global.tenant-active.v1', @@ -42,59 +39,61 @@ void test('defines immutable global and owner-local Policy references', () => { policyKey: 'inventory.stock.available.v1', }); - assert.deepEqual( - { policyKey: globalPolicy.policyKey, scope: globalPolicy.scope }, - { policyKey: 'global.tenant-active.v1', scope: 'global' }, - ); - assert.deepEqual( - { - owningModuleKey: modulePolicy.owningModuleKey, - policyKey: modulePolicy.policyKey, - scope: modulePolicy.scope, - }, - { - owningModuleKey: 'inventory.stock', - policyKey: 'inventory.stock.available.v1', - scope: 'microvertical', - }, - ); - assert.equal(Object.isFrozen(globalPolicy), true); - assert.equal(Object.isFrozen(modulePolicy), true); - assert.equal(isActionPolicy(globalPolicy), true); - assert.equal(isActionPolicy({ ...globalPolicy }), false); -}); - -void test('evaluates typed allow and safe denial outcomes', async () => { - const observed: ActionPolicyEvaluatorInput[] = []; - const allowed = defineGlobalPolicy({ - evaluate: (evaluationInput) => { - observed.push(evaluationInput); - return Effect.void; - }, - policyKey: 'global.allowed.v1', + expect({ policyKey: globalPolicy.policyKey, scope: globalPolicy.scope }).toEqual({ + policyKey: 'global.tenant-active.v1', + scope: 'global', }); - const denied = defineMicroverticalPolicy({ - evaluate: () => - Effect.fail(denyPolicy('stock_unavailable', 'Requested stock is unavailable — retry later')), + expect({ + owningModuleKey: modulePolicy.owningModuleKey, + policyKey: modulePolicy.policyKey, + scope: modulePolicy.scope, + }).toEqual({ owningModuleKey: 'inventory.stock', policyKey: 'inventory.stock.available.v1', + scope: 'microvertical', }); + expect(Object.isFrozen(globalPolicy)).toBe(true); + expect(Object.isFrozen(modulePolicy)).toBe(true); + expect(isActionPolicy(globalPolicy)).toBe(true); + expect(isActionPolicy({ ...globalPolicy })).toBe(false); +}); + +it.effect( + 'evaluates typed allow and safe denial outcomes', + Effect.fn(function* testProgram1() { + const observed: ActionPolicyEvaluatorInput[] = []; + const allowed = defineGlobalPolicy({ + evaluate: (evaluationInput) => { + observed.push(evaluationInput); + return Effect.void; + }, + policyKey: 'global.allowed.v1', + }); + const denied = defineMicroverticalPolicy({ + evaluate: () => + Effect.fail( + denyPolicy('stock_unavailable', 'Requested stock is unavailable — retry later'), + ), + owningModuleKey: 'inventory.stock', + policyKey: 'inventory.stock.available.v1', + }); - await runEffectTestPromise(allowed.evaluate(input)); - const denial = await runEffectTestPromise(Effect.flip(denied.evaluate(input))); + yield* allowed.evaluate(input); + const denial = yield* Effect.flip(denied.evaluate(input)); - assert.deepEqual(observed, [input]); - assert.equal(denial._tag, 'PolicyDenied'); - assert.equal(denial.reasonCode, 'stock_unavailable'); - assert.equal(denial.reason, 'Requested stock is unavailable — retry later'); - assert.equal(Object.isFrozen(denial), true); -}); + expect(observed).toEqual([input]); + expect(Predicate.isTagged(denial, 'PolicyDenied')).toBe(true); + + expect(denial.reasonCode).toBe('stock_unavailable'); + expect(denial.reason).toBe('Requested stock is unavailable — retry later'); + expect(Object.isFrozen(denial)).toBe(true); + }), +); -void test('rejects empty stable identifiers and denial messages', () => { - assert.throws( - () => defineGlobalPolicy({ evaluate: () => Effect.void, policyKey: ' ' }), +it('rejects empty stable identifiers and denial messages', () => { + expect(() => defineGlobalPolicy({ evaluate: () => Effect.void, policyKey: ' ' })).toThrow( TypeError, ); - assert.throws(() => denyPolicy('', 'Safe message'), TypeError); - assert.throws(() => denyPolicy('stable_code', ''), TypeError); + expect(() => denyPolicy('', 'Safe message')).toThrow(TypeError); + expect(() => denyPolicy('stable_code', '')).toThrow(TypeError); }); diff --git a/app/packages/core-runtime/tests/unit/action-public-surface.test.ts b/app/packages/core-runtime/tests/unit/action-public-surface.test.ts index 628cd8e58..a7ced6522 100644 --- a/app/packages/core-runtime/tests/unit/action-public-surface.test.ts +++ b/app/packages/core-runtime/tests/unit/action-public-surface.test.ts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { computeActionRequestHash, computeCanonicalValueHash, @@ -13,7 +12,7 @@ const principal = { tenantId: '00000000-0000-4000-8000-000000000001', } as const; -void test('computes deterministic hashes independent of object key ordering', () => { +it('computes deterministic hashes independent of object key ordering', () => { const left = computeActionRequestHash({ actionKey: 'shell.test.hash', normalizedPayload: { @@ -45,9 +44,8 @@ void test('computes deterministic hashes independent of object key ordering', () }, }); - assert.equal(left, right); - assert.notEqual( - left, + expect(left).toBe(right); + expect(left).not.toBe( computeCanonicalValueHash({ nested: { alpha: 1, beta: 3 }, values: ['first', 'second'], @@ -55,11 +53,11 @@ void test('computes deterministic hashes independent of object key ordering', () ); }); -void test('rejects cyclic values instead of producing an unstable request hash', () => { +it('rejects cyclic values instead of producing an unstable request hash', () => { const cyclic: unknown[] = []; cyclic.push(cyclic); - assert.throws(() => + expect(() => computeActionRequestHash({ actionKey: 'shell.test.hash', normalizedPayload: cyclic, @@ -68,51 +66,50 @@ void test('rejects cyclic values instead of producing an unstable request hash', schemaVersion: '1', target: {}, }), - ); + ).toThrow(); }); -void test('canonical hashing distinguishes literal objects from internal value types', () => { - assert.notEqual(computeCanonicalValueHash(), computeCanonicalValueHash({ $undefined: true })); - assert.notEqual( - computeCanonicalValueHash(Number.NaN), +it('canonical hashing distinguishes literal objects from internal value types', () => { + expect(computeCanonicalValueHash()).not.toBe(computeCanonicalValueHash({ $undefined: true })); + expect(computeCanonicalValueHash(Number.NaN)).not.toBe( computeCanonicalValueHash({ $number: 'NaN' }), ); - assert.notEqual(computeCanonicalValueHash(-0), computeCanonicalValueHash(0)); + expect(computeCanonicalValueHash(-0)).not.toBe(computeCanonicalValueHash(0)); }); -void test('publishes only the narrow server Action surface', () => { - assert.equal('ActionRuntime' in publicSurface, true); - assert.equal('defineAction' in publicSurface, true); - assert.equal('defineGlobalPolicy' in publicSurface, true); - assert.equal('defineMicroverticalPolicy' in publicSurface, true); - assert.equal('defineActionResourcePermission' in publicSurface, true); - assert.equal('LEGAL_ENTITY_PERMISSION_KEYS' in publicSurface, true); - assert.equal('TENANT_PERMISSION_KEYS' in publicSurface, true); - assert.equal('denyPolicy' in publicSurface, true); - assert.equal('ActionPolicyDenied' in publicSurface, true); - assert.equal('ActionPolicyEvaluationError' in publicSurface, true); - assert.equal('ActionPermissionDenied' in publicSurface, true); - assert.equal('ActionPermissionCheckError' in publicSurface, true); - assert.equal('resolveActionCommit' in publicSurface, true); - assert.equal('ActionRepository' in publicSurface, false); - assert.equal('ActionRepositoryLive' in publicSurface, false); - assert.equal('createActionCollector' in publicSurface, false); - assert.equal('makeActionRepository' in publicSurface, false); - assert.equal('finalizePolicyDenial' in publicSurface, false); - assert.equal('isActionPolicy' in publicSurface, false); - assert.equal('ActionPermission' in publicSurface, false); - assert.equal('ActionPermissionLive' in publicSurface, false); - assert.equal('SpiceDbConfig' in publicSurface, false); - assert.equal('createPermissionCheckClient' in publicSurface, false); - assert.equal('makeActionPermissionService' in publicSurface, false); - assert.equal('Pool' in publicSurface, false); +it('publishes only the narrow server Action surface', () => { + expect('ActionRuntime' in publicSurface).toBe(true); + expect('defineAction' in publicSurface).toBe(true); + expect('defineGlobalPolicy' in publicSurface).toBe(true); + expect('defineMicroverticalPolicy' in publicSurface).toBe(true); + expect('defineActionResourcePermission' in publicSurface).toBe(true); + expect('LEGAL_ENTITY_PERMISSION_KEYS' in publicSurface).toBe(true); + expect('TENANT_PERMISSION_KEYS' in publicSurface).toBe(true); + expect('denyPolicy' in publicSurface).toBe(true); + expect('ActionPolicyDenied' in publicSurface).toBe(true); + expect('ActionPolicyEvaluationError' in publicSurface).toBe(true); + expect('ActionPermissionDenied' in publicSurface).toBe(true); + expect('ActionPermissionCheckError' in publicSurface).toBe(true); + expect('resolveActionCommit' in publicSurface).toBe(true); + expect('ActionRepository' in publicSurface).toBe(false); + expect('ActionRepositoryLive' in publicSurface).toBe(false); + expect('createActionCollector' in publicSurface).toBe(false); + expect('makeActionRepository' in publicSurface).toBe(false); + expect('finalizePolicyDenial' in publicSurface).toBe(false); + expect('isActionPolicy' in publicSurface).toBe(false); + expect('ActionPermission' in publicSurface).toBe(false); + expect('ActionPermissionLive' in publicSurface).toBe(false); + expect('SpiceDbConfig' in publicSurface).toBe(false); + expect('createPermissionCheckClient' in publicSurface).toBe(false); + expect('makeActionPermissionService' in publicSurface).toBe(false); + expect('Pool' in publicSurface).toBe(false); }); -void test('publishes the sanitized PostgreSQL classifier on the server surface', () => { - assert.equal('findPostgresFailure' in publicSurface, true); +it('publishes the sanitized PostgreSQL classifier on the server surface', () => { + expect('findPostgresFailure' in publicSurface).toBe(true); }); -test('publishes the typed governed Read alternative-target composition', () => { +it('publishes the typed governed Read alternative-target composition', () => { const target = { kind: 'any_of', targets: [ @@ -128,6 +125,6 @@ test('publishes the typed governed Read alternative-target composition', () => { ], } as const satisfies ResolvedReadPermissionTarget; - assert.equal(target.kind, 'any_of'); - assert.equal(target.targets[0].kind, 'resource'); + expect(target.kind).toBe('any_of'); + expect(target.targets[0].kind).toBe('resource'); }); diff --git a/app/packages/core-runtime/tests/unit/action-runtime.test.ts b/app/packages/core-runtime/tests/unit/action-runtime.test.ts index b692be9c3..3c99a6040 100644 --- a/app/packages/core-runtime/tests/unit/action-runtime.test.ts +++ b/app/packages/core-runtime/tests/unit/action-runtime.test.ts @@ -1,10 +1,17 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -/* oxlint-disable sonarjs/use-type-alias -- Existing compatibility boundary; expires: 2026-12-31. */ -// @effect-diagnostics asyncFunction:off globalDate:off globalDateInEffect:off missingEffectError:off unsafeEffectTypeAssertion:off -- Existing compatibility boundary; expires: 2026-12-31. -import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Option, Predicate, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { + Cause, + DateTime, + Deferred, + Effect, + Exit, + Fiber, + Option, + Predicate, + Schema, + Struct, +} from 'effect'; import { ConnectionError, SqlError } from 'effect/unstable/sql/SqlError'; -import assert from 'node:assert/strict'; -import test from 'node:test'; import { defineAction, defineActionResourcePermission, @@ -82,23 +89,18 @@ const transport = (idempotencyKey = 'intent-1') => ({ const CounterpartyIdSchema = Schema.String.pipe(Schema.brand('CounterpartyId')); type CounterpartyId = typeof CounterpartyIdSchema.Type; +type RetainedCauseError = ActionTransactionError | ActionInvocationPersistenceError; + +const expectSameJson = (actual: RetainedCauseError, expected: RetainedCauseError) => { + expect(JSON.stringify(actual)).toBe(JSON.stringify(expected)); +}; + const completionTime = () => DateTime.toDateUtc(DateTime.makeUnsafe(0)); -const forEachSequential = async ( +const forEachSequential = ( items: readonly Item[], - run: (item: Item) => Promise, -): Promise => { - const iterator = items.values(); - const visitNext = async (): Promise => { - const next = iterator.next(); - if (next.done === true) { - return; - } - await run(next.value); - await visitNext(); - }; - await visitNext(); -}; + run: (item: Item) => Effect.Effect, +) => Effect.forEach(items, run, { discard: true }); const unusedPrincipalManagementOperation = () => Effect.die('The ambient PrincipalManagementRepository must not be used by the Action runtime'); @@ -114,11 +116,14 @@ const providePrincipalManagementRepository = Effect.provideService( ambientPrincipalManagementRepository, ); +const PermissionDecisionSchema = Schema.Literals(['allowed', 'denied', 'unavailable']); +type PermissionDecision = typeof PermissionDecisionSchema.Type; + interface HarnessOptions { readonly commit?: Effect.Effect; readonly commitFailureCode?: string; readonly createRecord?: ActionInvocationRecord; - readonly legalEntityPermissionDecision?: 'allowed' | 'denied' | 'unavailable'; + readonly legalEntityPermissionDecision?: PermissionDecision; readonly lockedModuleState?: 'active' | 'denied' | 'unavailable'; readonly moduleState?: TenantModuleState | 'missing' | 'unavailable'; readonly permissionDecision?: ActionPermissionDecision; @@ -126,12 +131,12 @@ interface HarnessOptions { readonly policyFinalizationFailure?: boolean; readonly rejectionFailure?: boolean; readonly resolutionUnavailable?: boolean; - readonly resourcePermissionDecision?: 'allowed' | 'denied' | 'unavailable'; - readonly tenantPermissionDecision?: 'allowed' | 'denied' | 'unavailable'; + readonly resourcePermissionDecision?: PermissionDecision; + readonly tenantPermissionDecision?: PermissionDecision; readonly transactionMode?: 'commit-definite' | 'definite-failure' | 'normal' | 'uncertain'; } -const makeHarness = (options: HarnessOptions = {}) => { +const makeHarness = Effect.fn(function* makeHarness(options: HarnessOptions = {}) { const finalized: FinalizeActionPolicyDenialInput[] = []; const flushed: FlushActionSuccessInput[] = []; const legalEntityChecks: unknown[] = []; @@ -259,36 +264,35 @@ const makeHarness = (options: HarnessOptions = {}) => { } return []; }); - const query = (statement: string, values: readonly unknown[]) => - Effect.gen(function* executeQuery() { - const text = statement.toLowerCase(); - if (text.includes('set_config')) { - const [tenantId, legalEntityId] = values; - if (Predicate.isString(tenantId) && Predicate.isString(legalEntityId)) { - installedTenantId = tenantId; - installedLegalEntityId = legalEntityId; - } + const query = Effect.fn(function* executeQuery(statement: string, values: readonly unknown[]) { + const text = statement.toLowerCase(); + if (text.includes('set_config')) { + const [tenantId, legalEntityId] = values; + if (Predicate.isString(tenantId) && Predicate.isString(legalEntityId)) { + installedTenantId = tenantId; + installedLegalEntityId = legalEntityId; } - if (text === 'begin') { - transactionCount += 1; - if (options.transactionMode === 'definite-failure') { - return yield* new SqlError({ - reason: new ConnectionError({ cause: new Error('transaction unavailable') }), - }); - } - } - if (text === 'commit') { - return yield* commitTransaction(); - } - if (text.includes('current_setting')) { - return [{ legal_entity_id: installedLegalEntityId, tenant_id: installedTenantId }]; - } - if (text.startsWith('select')) { - return [{ authBindingId: principal.authBindingId }]; + } + if (text === 'begin') { + transactionCount += 1; + if (options.transactionMode === 'definite-failure') { + return yield* new SqlError({ + reason: new ConnectionError({ cause: new Error('transaction unavailable') }), + }); } - return []; - }); - const database = { executor: makeTestDatabase(query) }; + } + if (text === 'commit') { + return yield* commitTransaction(); + } + if (text.includes('current_setting')) { + return [{ legal_entity_id: installedLegalEntityId, tenant_id: installedTenantId }]; + } + if (text.startsWith('select')) { + return [{ authBindingId: principal.authBindingId }]; + } + return []; + }); + const database = { executor: yield* makeTestDatabase(query) }; const permission = { checkActionPermission: (input: CheckActionPermissionInput) => { @@ -412,7 +416,11 @@ const makeHarness = (options: HarnessOptions = {}) => { counts: () => ({ createCount, lockCount, transactionCount, transitionCount }), finalized, flushed, - gateCounts: () => ({ handlerResolutionCount, moduleStateReadCount, moduleStateRecheckCount }), + gateCounts: () => ({ + handlerResolutionCount, + moduleStateReadCount, + moduleStateRecheckCount, + }), legalEntityChecks, permissionChecks, permissionCounts: () => ({ permissionCheckCount, rejectionCount }), @@ -423,13 +431,13 @@ const makeHarness = (options: HarnessOptions = {}) => { stages, tenantChecks, }; -}; +}); -const makeRepositoryFailures = async () => { +const makeRepositoryFailures = Effect.fn(function* testProgram1() { const cause = new SqlError({ reason: new ConnectionError({ cause: new Error('private repository defect') }), }); - const executor = makeTestDatabase(() => Effect.fail(cause)); + const executor = yield* makeTestDatabase(() => Effect.fail(cause)); const repository = makeActionRepository(); const input = { actionInvocationId: 'invocation-1', @@ -438,80 +446,99 @@ const makeRepositoryFailures = async () => { principal, transport: transport('denied'), } as const; - const transactionFailure = await runEffectTestPromise( - Effect.flip(repository.rejectPermissionDenied(executor, input)), - ); - const persistenceFailure = await runEffectTestPromise( - Effect.flip( - repository.finalizePolicyDenial(executor, { - ...input, - policy: { policyKey: 'global.counter-locked.v1', scope: 'global' }, - reasonCode: 'counter_locked', - }), - ), + const transactionFailure = yield* Effect.flip(repository.rejectPermissionDenied(executor, input)); + const persistenceFailure = yield* Effect.flip( + repository.finalizePolicyDenial(executor, { + ...input, + policy: { policyKey: 'global.counter-locked.v1', scope: 'global' }, + reasonCode: 'counter_locked', + }), ); - assert.ok(Schema.is(ActionTransactionError)(transactionFailure)); - assert.ok(Schema.is(ActionInvocationPersistenceError)(persistenceFailure)); + expect(Schema.is(ActionTransactionError)(transactionFailure)).toBe(true); + if (!Schema.is(ActionTransactionError)(transactionFailure)) { + throw new Error('Expected typed test outcome'); + } + expect(Schema.is(ActionInvocationPersistenceError)(persistenceFailure)).toBe(true); + if (!Schema.is(ActionInvocationPersistenceError)(persistenceFailure)) { + throw new Error('Expected typed test outcome'); + } return { cause, persistenceFailure, transactionFailure }; -}; - -test('repository constructors retain original causes across Effect Cause propagation', async () => { - const { cause, persistenceFailure, transactionFailure } = await makeRepositoryFailures(); - const propagatedTransaction = await runEffectTestPromise( - Effect.flip(Effect.failCause(Cause.fail(transactionFailure))), - ); - const propagatedPersistence = await runEffectTestPromise( - Effect.flip(Effect.failCause(Cause.fail(persistenceFailure))), - ); - assert.equal(propagatedTransaction, transactionFailure); - assert.equal(propagatedPersistence, persistenceFailure); - assert.deepEqual(getActionTransactionFailureCause(propagatedTransaction), Cause.die(cause)); - assert.deepEqual( - getActionInvocationPersistenceFailureCause(propagatedPersistence), - Cause.die(cause), - ); }); -test('public error classes expose no retained-cause accessors', () => { +it.effect( + 'repository constructors retain original causes across Effect Cause propagation', + Effect.fn(function* testProgram2() { + const { cause, persistenceFailure, transactionFailure } = yield* makeRepositoryFailures(); + const propagatedTransaction = yield* Effect.flip( + Effect.failCause(Cause.fail(transactionFailure)), + ); + const propagatedPersistence = yield* Effect.flip( + Effect.failCause(Cause.fail(persistenceFailure)), + ); + expect(propagatedTransaction).toBe(transactionFailure); + expect(propagatedPersistence).toBe(persistenceFailure); + expect(getActionTransactionFailureCause(propagatedTransaction)).toEqual(Cause.die(cause)); + expect(getActionInvocationPersistenceFailureCause(propagatedPersistence)).toEqual( + Cause.die(cause), + ); + }), +); + +it('public error classes expose no retained-cause accessors', () => { for (const errorClass of [ActionTransactionError, ActionInvocationPersistenceError]) { - assert.equal('withCause' in errorClass, false); - assert.equal('causeOf' in errorClass, false); + expect('withCause' in errorClass).toBe(false); + expect('causeOf' in errorClass).toBe(false); } }); -test('repository causes are absent from reflection, JSON, and Schema encoding', async () => { - const { persistenceFailure, transactionFailure } = await makeRepositoryFailures(); - const publicTransaction = new ActionTransactionError({ - code: transactionFailure.code, - reason: transactionFailure.reason, - }); - const publicPersistence = new ActionInvocationPersistenceError({ - code: persistenceFailure.code, - reason: persistenceFailure.reason, - }); - assert.deepEqual(Object.keys(transactionFailure), Object.keys(publicTransaction)); - assert.deepEqual(Object.keys(persistenceFailure), Object.keys(publicPersistence)); - assert.deepEqual(Reflect.ownKeys(transactionFailure), Reflect.ownKeys(publicTransaction)); - assert.deepEqual(Reflect.ownKeys(persistenceFailure), Reflect.ownKeys(publicPersistence)); - assert.equal(JSON.stringify(transactionFailure), JSON.stringify(publicTransaction)); - assert.equal(JSON.stringify(persistenceFailure), JSON.stringify(publicPersistence)); - assert.deepEqual(Schema.encodeSync(ActionTransactionError)(transactionFailure), { - _tag: 'ActionTransactionError', - code: transactionFailure.code, - reason: transactionFailure.reason, - }); - assert.deepEqual(Schema.encodeSync(ActionInvocationPersistenceError)(persistenceFailure), { - _tag: 'ActionInvocationPersistenceError', - code: persistenceFailure.code, - reason: persistenceFailure.reason, - }); -}); +it.effect( + 'repository causes are absent from reflection, JSON, and Schema encoding', + Effect.fn(function* testProgram3() { + const { persistenceFailure, transactionFailure } = yield* makeRepositoryFailures(); + const publicTransaction = new ActionTransactionError({ + code: transactionFailure.code, + reason: transactionFailure.reason, + }); + const publicPersistence = new ActionInvocationPersistenceError({ + code: persistenceFailure.code, + reason: persistenceFailure.reason, + }); + expect(Object.keys(transactionFailure)).toEqual(Object.keys(publicTransaction)); + expect(Object.keys(persistenceFailure)).toEqual(Object.keys(publicPersistence)); + expect(Reflect.ownKeys(transactionFailure)).toEqual(Reflect.ownKeys(publicTransaction)); + expect(Reflect.ownKeys(persistenceFailure)).toEqual(Reflect.ownKeys(publicPersistence)); + expectSameJson(transactionFailure, publicTransaction); + expectSameJson(persistenceFailure, publicPersistence); + const encodedTransactionFailure = + yield* Schema.encodeEffect(ActionTransactionError)(transactionFailure); + expect(Schema.is(Schema.toEncoded(ActionTransactionError))(encodedTransactionFailure)).toBe( + true, + ); + expect(Struct.omit(encodedTransactionFailure, ['_tag'])).toEqual({ + code: transactionFailure.code, + reason: transactionFailure.reason, + }); + const encodedPersistenceFailure = yield* Schema.encodeEffect(ActionInvocationPersistenceError)( + persistenceFailure, + ); + expect( + Schema.is(Schema.toEncoded(ActionInvocationPersistenceError))(encodedPersistenceFailure), + ).toBe(true); + expect(Struct.omit(encodedPersistenceFailure, ['_tag'])).toEqual({ + code: persistenceFailure.code, + reason: persistenceFailure.reason, + }); + }), +); -test('repository cause readers reject foreign objects carrying the former cause property', () => { +it('repository cause readers reject foreign objects carrying the former cause property', () => { const formerCauseProperty = ['ontos', 'Repository', 'Failure', 'Cause'].join(''); const cause = new Error('foreign defect'); const transactionFailure = Object.assign( - new ActionTransactionError({ code: 'action_transaction_failed', reason: 'foreign failure' }), + new ActionTransactionError({ + code: 'action_transaction_failed', + reason: 'foreign failure', + }), { [formerCauseProperty]: cause }, ); const persistenceFailure = Object.assign( @@ -521,8 +548,8 @@ test('repository cause readers reject foreign objects carrying the former cause }), { [formerCauseProperty]: cause }, ); - assert.equal(getActionTransactionFailureCause(transactionFailure), undefined); - assert.equal(getActionInvocationPersistenceFailureCause(persistenceFailure), undefined); + expect(getActionTransactionFailureCause(transactionFailure)).toBe(undefined); + expect(getActionInvocationPersistenceFailureCause(persistenceFailure)).toBe(undefined); }); const registration = () => @@ -550,143 +577,147 @@ const registration = () => resultSchema: Schema.Struct({ total: Schema.Finite }), schemaVersion: '1', }, - (payload, context) => - Effect.gen(function* changeCounter() { - assert.equal(context.actionInvocationId, 'invocation-1'); - assert.equal(Object.isFrozen(context), true); - assert.equal('transaction' in context, false); - assert.deepEqual(context.services, {}); - yield* context.recordDataAccess({ - accessKind: 'read', - queryHash: `counter-${payload.amount}`, - resultCount: 1, - servingModuleKey: 'core.shell', - }); - const domainEvent = yield* context.addDomainEvent({ - eventType: 'counter.changed', - payloadJson: { amount: payload.amount }, - producerModuleKey: 'core.shell', - subjectModuleKey: 'core.shell', - subjectResourceId: 'primary', - subjectResourceType: 'counter', - }); - yield* context.addOutboxMessage(domainEvent, { - payloadJson: { amount: payload.amount }, - producerModuleKey: 'core.shell', - topic: 'counter.project', - }); - return { total: payload.amount }; - }), + Effect.fn(function* changeCounter(payload, context) { + expect(context.actionInvocationId).toBe('invocation-1'); + expect(Object.isFrozen(context)).toBe(true); + expect('transaction' in context).toBe(false); + expect(context.services).toEqual({}); + yield* context.recordDataAccess({ + accessKind: 'read', + queryHash: `counter-${payload.amount}`, + resultCount: 1, + servingModuleKey: 'core.shell', + }); + const domainEvent = yield* context.addDomainEvent({ + eventType: 'counter.changed', + payloadJson: { amount: payload.amount }, + producerModuleKey: 'core.shell', + subjectModuleKey: 'core.shell', + subjectResourceId: 'primary', + subjectResourceType: 'counter', + }); + yield* context.addOutboxMessage(domainEvent, { + payloadJson: { amount: payload.amount }, + producerModuleKey: 'core.shell', + topic: 'counter.project', + }); + return { total: payload.amount }; + }), ); -test('executes the complete stage order with transaction ownership and success evidence', async () => { - const harness = makeHarness(); - const result = await runEffectTestPromise( - harness.runtime.runAction({ +it.effect( + 'executes the complete stage order with transaction ownership and success evidence', + Effect.fn(function* testProgram4() { + const harness = yield* makeHarness(); + const result = yield* harness.runtime.runAction({ payload: { amount: 3 }, principal, registration: registration(), transport: transport(), - }), - ); + }); - assert.deepEqual(result, { total: 3 }); - assert.deepEqual(harness.stages, ACTION_RUNTIME_STAGES); - assert.deepEqual(harness.counts(), { - createCount: 1, - lockCount: 1, - transactionCount: 1, - transitionCount: 1, - }); - assert.equal(harness.flushed.length, 1); - assert.equal(harness.flushed[0]?.evidence.dataAccessEvents.length, 1); - assert.equal(harness.flushed[0]?.evidence.domainEvents.length, 1); - assert.equal(harness.flushed[0]?.evidence.outboxMessages.length, 1); - assert.deepEqual(harness.flushed[0]?.allowedPolicies, []); - assert.deepEqual(harness.permissionChecks, [ - { - actionKey: 'shell.counter.change', - correlationId: 'correlation-intent-1', - principalId: principal.principalId, - }, - ]); - assert.deepEqual(harness.gateCounts(), { - handlerResolutionCount: 1, - moduleStateReadCount: 0, - moduleStateRecheckCount: 0, - }); -}); + expect(result).toEqual({ total: 3 }); + expect(harness.stages).toEqual(ACTION_RUNTIME_STAGES); + expect(harness.counts()).toEqual({ + createCount: 1, + lockCount: 1, + transactionCount: 1, + transitionCount: 1, + }); + expect(harness.flushed.length).toBe(1); + expect(harness.flushed[0]?.evidence.dataAccessEvents.length).toBe(1); + expect(harness.flushed[0]?.evidence.domainEvents.length).toBe(1); + expect(harness.flushed[0]?.evidence.outboxMessages.length).toBe(1); + expect(harness.flushed[0]?.allowedPolicies).toEqual([]); + expect(harness.permissionChecks).toEqual([ + { + actionKey: 'shell.counter.change', + correlationId: 'correlation-intent-1', + principalId: principal.principalId, + }, + ]); + expect(harness.gateCounts()).toEqual({ + handlerResolutionCount: 1, + moduleStateReadCount: 0, + moduleStateRecheckCount: 0, + }); + }), +); -test('hashes the encoded representation of decoded DateTime and Option values', async () => { - const occurredAt = '2026-09-07T10:30:00.000Z'; - const payloadSchema = Schema.Struct({ - note: Schema.OptionFromNullOr(Schema.String), - occurredAt: Schema.DateTimeUtcFromString, - }); - const action = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'shell.temporal.v1' }, - actionKey: 'shell.temporal.change', - auditProfile: 'standard', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineSystemModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'shell.temporal.change', - moduleKey: 'core.shell', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'core.shell', - payloadSchema, - policies: [], - resultSchema: payloadSchema, - schemaVersion: '1', - }, - (payload) => { - assert.equal(DateTime.formatIso(payload.occurredAt), occurredAt); - assert.equal(Option.isNone(payload.note), true); - return Effect.succeed(payload); - }, - ); - const harness = makeHarness(); +it.effect( + 'hashes the encoded representation of decoded DateTime and Option values', + Effect.fn(function* testProgram5() { + const occurredAt = '2026-09-07T10:30:00.000Z'; + const payloadSchema = Schema.Struct({ + note: Schema.OptionFromNullOr(Schema.String), + occurredAt: Schema.DateTimeUtcFromString, + }); + const action = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'shell.temporal.v1' }, + actionKey: 'shell.temporal.change', + auditProfile: 'standard', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineSystemModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'shell.temporal.change', + moduleKey: 'core.shell', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'core.shell', + payloadSchema, + policies: [], + resultSchema: payloadSchema, + schemaVersion: '1', + }, + (payload) => { + expect(DateTime.formatIso(payload.occurredAt)).toBe(occurredAt); + expect(Option.isNone(payload.note)).toBe(true); + return Effect.succeed(payload); + }, + ); + const harness = yield* makeHarness(); - const result = await runEffectTestPromise( - harness.runtime.runAction({ + const result = yield* harness.runtime.runAction({ payload: { note: null, occurredAt }, principal, registration: action, transport: transport('temporal-payload'), - }), - ); + }); - assert.deepEqual(harness.requestHashes, [ - computeActionRequestHash({ - actionKey: 'shell.temporal.change', - normalizedPayload: { note: null, occurredAt }, - owningModuleKey: 'core.shell', - principal, - schemaVersion: '1', - target: { - targetModuleKey: 'core.shell', - targetResourceId: 'primary', - targetResourceType: 'counter', - }, - }), - ]); - assert.equal(DateTime.formatIso(result.occurredAt), occurredAt); - assert.equal(Option.isNone(result.note), true); - assert.equal( - harness.flushed[0]?.resultHash, - computeCanonicalValueHash({ note: null, occurredAt }), - ); -}); + expect(harness.requestHashes).toEqual([ + computeActionRequestHash({ + actionKey: 'shell.temporal.change', + normalizedPayload: { note: null, occurredAt }, + owningModuleKey: 'core.shell', + principal, + schemaVersion: '1', + target: { + targetModuleKey: 'core.shell', + targetResourceId: 'primary', + targetResourceType: 'counter', + }, + }), + ]); + expect(DateTime.formatIso(result.occurredAt)).toBe(occurredAt); + expect(Option.isNone(result.note)).toBe(true); + expect(harness.flushed[0]?.resultHash).toBe( + computeCanonicalValueHash({ note: null, occurredAt }), + ); + }), +); -test('uses a resolver-branded recovery only for the exact support-stop Action and still checks permission', async () => { - const recoveryPrincipal = await runEffectTestPromise( - supportRecoveryPrincipalContextResolverFromRepository({ +it.effect( + 'uses a resolver-branded recovery only for the exact support-stop Action and still checks permission', + Effect.fn(function* testProgram6() { + const recoveryPrincipal = yield* supportRecoveryPrincipalContextResolverFromRepository({ load: () => Effect.succeed( Option.some({ @@ -702,15 +733,13 @@ test('uses a resolver-branded recovery only for the exact support-stop Action an originalPrincipalId: principal.principalId, originalSessionId: 'expired-original-session', tenantId: principal.tenantId, - }), - ); - const harness = makeHarness({ - permissionDecision: 'allowed', - tenantPermissionDecision: 'denied', - }); + }); + const harness = yield* makeHarness({ + permissionDecision: 'allowed', + tenantPermissionDecision: 'denied', + }); - const result = await runEffectTestPromise( - harness.runtime + const result = yield* harness.runtime .runAction({ payload: { checkpoint: 'stopped', @@ -723,15 +752,13 @@ test('uses a resolver-branded recovery only for the exact support-stop Action an registration: recordSupportImpersonationAction, transport: transport('support-recovery'), }) - .pipe(providePrincipalManagementRepository), - ); + .pipe(providePrincipalManagementRepository); - assert.deepEqual(result, { checkpoint: 'stopped', recorded: true }); - assert.deepEqual(harness.permissionCounts(), { permissionCheckCount: 1, rejectionCount: 0 }); + expect(result).toEqual({ checkpoint: 'stopped', recorded: true }); + expect(harness.permissionCounts()).toEqual({ permissionCheckCount: 1, rejectionCount: 0 }); - const deniedHarness = makeHarness({ permissionDecision: 'denied' }); - const denied = await runEffectTestPromise( - Effect.flip( + const deniedHarness = yield* makeHarness({ permissionDecision: 'denied' }); + const denied = yield* Effect.flip( deniedHarness.runtime .runAction({ payload: { @@ -746,16 +773,15 @@ test('uses a resolver-branded recovery only for the exact support-stop Action an transport: transport('support-recovery-denied'), }) .pipe(providePrincipalManagementRepository), - ), - ); - assert.equal(denied._tag, 'ActionPermissionDenied'); - assert.deepEqual(deniedHarness.permissionCounts(), { - permissionCheckCount: 1, - rejectionCount: 1, - }); + ); + expect(Predicate.isTagged(denied, 'ActionPermissionDenied')).toBe(true); + + expect(deniedHarness.permissionCounts()).toEqual({ + permissionCheckCount: 1, + rejectionCount: 1, + }); - const wrongCheckpoint = await runEffectTestPromise( - Effect.flip( + const wrongCheckpoint = yield* Effect.flip( harness.runtime .runAction({ payload: { @@ -769,345 +795,364 @@ test('uses a resolver-branded recovery only for the exact support-stop Action an transport: transport('support-recovery-wrong-checkpoint'), }) .pipe(providePrincipalManagementRepository), - ), - ); - assert.equal(wrongCheckpoint._tag, 'ActionTrustedContextValidationError'); + ); + expect(Predicate.isTagged(wrongCheckpoint, 'ActionTrustedContextValidationError')).toBe(true); - const wrongAction = await runEffectTestPromise( - Effect.flip( + const wrongAction = yield* Effect.flip( harness.runtime.runAction({ payload: { amount: 1 }, principal: recoveryPrincipal, registration: registration(), transport: transport('support-recovery-wrong-action'), }), - ), - ); - assert.equal(wrongAction._tag, 'ActionTrustedContextValidationError'); -}); + ); + expect(Predicate.isTagged(wrongAction, 'ActionTrustedContextValidationError')).toBe(true); + }), +); -test('fails business Actions closed before invocation, permission, Policy, or handler access', async () => { - await forEachSequential( - ( - [ - 'inactive', - 'read_only', - 'suspended', - 'quarantined', - 'deprecated', - 'archived', - 'missing', - ] as const - ).map((state, index) => [index, state] as const), - async ([index, state]) => { - let handlerCalls = 0; - let policyCalls = 0; - const harness = makeHarness({ moduleState: state }); - const action = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'stock.read.v1' }, - actionKey: `inventory.stock.reserve-state-${index}`, - auditProfile: 'standard', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineTenantModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: `inventory.stock.reserve-state-${index}`, - moduleKey: 'inventory.stock', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'inventory.stock', - payloadSchema: Schema.Void, - policies: [ - defineGlobalPolicy({ - evaluate: () => Effect.sync(() => (policyCalls += 1)), - policyKey: `global.unreachable-${index}.v1`, +it.effect( + 'fails business Actions closed before invocation, permission, Policy, or handler access', + Effect.fn(function* testProgram7() { + yield* forEachSequential( + ( + [ + 'inactive', + 'read_only', + 'suspended', + 'quarantined', + 'deprecated', + 'archived', + 'missing', + ] as const + ).map((state, index) => [index, state] as const), + Effect.fn(function* testProgram8([index, state]) { + let handlerCalls = 0; + let policyCalls = 0; + const harness = yield* makeHarness({ moduleState: state }); + const action = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'stock.read.v1' }, + actionKey: `inventory.stock.reserve-state-${index}`, + auditProfile: 'standard', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineTenantModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: `inventory.stock.reserve-state-${index}`, + moduleKey: 'inventory.stock', + role: 'action', }), - ], - resultSchema: Schema.Void, - schemaVersion: '1', - }, - () => - Effect.sync(() => { - handlerCalls += 1; - }), - ); - const failure = await runEffectTestPromise( - Effect.flip( + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'inventory.stock', + payloadSchema: Schema.Void, + policies: [ + defineGlobalPolicy({ + evaluate: () => Effect.sync(() => (policyCalls += 1)), + policyKey: `global.unreachable-${index}.v1`, + }), + ], + resultSchema: Schema.Void, + schemaVersion: '1', + }, + () => + Effect.sync(() => { + handlerCalls += 1; + }), + ); + const failure = yield* Effect.flip( harness.runtime.runAction({ payload: undefined, principal, registration: action, transport: transport(`state-${state}`), }), - ), - ); - assert.equal(failure._tag, 'ModuleStateDeniedError', state); - assert.equal(handlerCalls, 0); - assert.equal(policyCalls, 0); - assert.deepEqual(harness.counts(), { - createCount: 0, - lockCount: 0, - transactionCount: 0, - transitionCount: 0, - }); - assert.deepEqual(harness.permissionCounts(), { - permissionCheckCount: 0, - rejectionCount: 0, - }); - assert.deepEqual(harness.gateCounts(), { - handlerResolutionCount: 0, - moduleStateReadCount: 1, - moduleStateRecheckCount: 0, - }); - }, - ); -}); - -test('distinguishes unavailable early checks and rolls back a denied locked recheck', async () => { - const action = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'stock.read.v1' }, - actionKey: 'inventory.stock.reserve-locked', - auditProfile: 'standard', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineTenantModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'inventory.stock.reserve-locked', - moduleKey: 'inventory.stock', - role: 'action', + ); + expect(Predicate.isTagged(failure, 'ModuleStateDeniedError'), state).toBe(true); + + expect(handlerCalls).toBe(0); + expect(policyCalls).toBe(0); + expect(harness.counts()).toEqual({ + createCount: 0, + lockCount: 0, + transactionCount: 0, + transitionCount: 0, + }); + expect(harness.permissionCounts()).toEqual({ + permissionCheckCount: 0, + rejectionCount: 0, + }); + expect(harness.gateCounts()).toEqual({ + handlerResolutionCount: 0, + moduleStateReadCount: 1, + moduleStateRecheckCount: 0, + }); }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'inventory.stock', - payloadSchema: Schema.Void, - policies: [], - resultSchema: Schema.Void, - schemaVersion: '1', - }, - () => Effect.void, - ); + ); + }), +); + +it.effect( + 'distinguishes unavailable early checks and rolls back a denied locked recheck', + Effect.fn(function* testProgram9() { + const action = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'stock.read.v1' }, + actionKey: 'inventory.stock.reserve-locked', + auditProfile: 'standard', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineTenantModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'inventory.stock.reserve-locked', + moduleKey: 'inventory.stock', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'inventory.stock', + payloadSchema: Schema.Void, + policies: [], + resultSchema: Schema.Void, + schemaVersion: '1', + }, + () => Effect.void, + ); - const unavailable = makeHarness({ moduleState: 'unavailable' }); - const unavailableFailure = await runEffectTestPromise( - Effect.flip( + const unavailable = yield* makeHarness({ moduleState: 'unavailable' }); + const unavailableFailure = yield* Effect.flip( unavailable.runtime.runAction({ payload: undefined, principal, registration: action, transport: transport('state-unavailable'), }), - ), - ); - assert.equal(unavailableFailure._tag, 'ModuleStateCheckUnavailableError'); - assert.equal(unavailable.counts().createCount, 0); + ); + expect(Predicate.isTagged(unavailableFailure, 'ModuleStateCheckUnavailableError')).toBe(true); + + expect(unavailable.counts().createCount).toBe(0); - const locked = makeHarness({ lockedModuleState: 'denied' }); - const lockedFailure = await runEffectTestPromise( - Effect.flip( + const locked = yield* makeHarness({ lockedModuleState: 'denied' }); + const lockedFailure = yield* Effect.flip( locked.runtime.runAction({ payload: undefined, principal, registration: action, transport: transport('state-locked-denied'), }), - ), - ); - assert.equal(lockedFailure._tag, 'ModuleStateDeniedError'); - assert.deepEqual(locked.gateCounts(), { - handlerResolutionCount: 0, - moduleStateReadCount: 1, - moduleStateRecheckCount: 1, - }); - assert.deepEqual(locked.counts(), { - createCount: 1, - lockCount: 1, - transactionCount: 1, - transitionCount: 1, - }); -}); + ); + expect(Predicate.isTagged(lockedFailure, 'ModuleStateDeniedError')).toBe(true); + + expect(locked.gateCounts()).toEqual({ + handlerResolutionCount: 0, + moduleStateReadCount: 1, + moduleStateRecheckCount: 1, + }); + expect(locked.counts()).toEqual({ + createCount: 1, + lockCount: 1, + transactionCount: 1, + transitionCount: 1, + }); + }), +); -test('allows an explicitly authorized Action before Policy evaluation', async () => { - const harness = makeHarness({ permissionDecision: 'allowed' }); - const result = await runEffectTestPromise( - harness.runtime.runAction({ +it.effect( + 'allows an explicitly authorized Action before Policy evaluation', + Effect.fn(function* testProgram10() { + const harness = yield* makeHarness({ permissionDecision: 'allowed' }); + const result = yield* harness.runtime.runAction({ payload: { amount: 2 }, principal, registration: registration(), transport: transport('allowed'), - }), - ); + }); - assert.deepEqual(result, { total: 2 }); - assert.ok( - harness.stages.indexOf('permission_checked') < harness.stages.indexOf('policy_boundary'), - ); - assert.equal(harness.counts().transitionCount, 1); - assert.equal(harness.counts().transactionCount, 1); -}); + expect(result).toEqual({ total: 2 }); + expect( + harness.stages.indexOf('permission_checked') < harness.stages.indexOf('policy_boundary'), + ).toBe(true); + expect(harness.counts().transitionCount).toBe(1); + expect(harness.counts().transactionCount).toBe(1); + }), +); -test('requires a declared tenant role independently from the Action executor relation', async () => { - const tenantAuthorizedRegistration = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'identity.read.v1' }, - actionKey: 'core.identity.tenant-authorized', - auditProfile: 'sensitive', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineSystemModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'core.identity.tenant-authorized', - moduleKey: 'core.identity', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'core.identity', - payloadSchema: Schema.Void, - policies: [], - resultSchema: Schema.Void, - schemaVersion: '1', - tenantPermission: () => 'manage_identity', - }, - () => Effect.void, - ); +it.effect( + 'requires a declared tenant role independently from the Action executor relation', + Effect.fn(function* testProgram11() { + const tenantAuthorizedRegistration = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'identity.read.v1' }, + actionKey: 'core.identity.tenant-authorized', + auditProfile: 'sensitive', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineSystemModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'core.identity.tenant-authorized', + moduleKey: 'core.identity', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'core.identity', + payloadSchema: Schema.Void, + policies: [], + resultSchema: Schema.Void, + schemaVersion: '1', + tenantPermission: () => 'manage_identity', + }, + () => Effect.void, + ); - await forEachSequential( - [ - ['denied', 'ActionPermissionDenied'], - ['unavailable', 'ActionPermissionCheckError'], - ] as const, - async ([decision, expectedTag]) => { - const harness = makeHarness({ - permissionDecision: 'allowed', - tenantPermissionDecision: decision, - }); - const failure = await runEffectTestPromise( - Effect.flip( + yield* forEachSequential( + [ + ['denied', 'ActionPermissionDenied'], + ['unavailable', 'ActionPermissionCheckError'], + ] as const, + Effect.fn(function* testProgram12([decision, expectedTag]) { + const harness = yield* makeHarness({ + permissionDecision: 'allowed', + tenantPermissionDecision: decision, + }); + const failure = yield* Effect.flip( harness.runtime.runAction({ payload: undefined, principal, registration: tenantAuthorizedRegistration, transport: transport(`tenant-${decision}`), }), - ), - ); - assert.equal(failure._tag, expectedTag); - assert.equal(harness.counts().transitionCount, 0); - }, - ); + ); + expect(Predicate.isTagged(failure, expectedTag)).toBe(true); - const allowed = makeHarness({ - permissionDecision: 'allowed', - tenantPermissionDecision: 'allowed', - }); - await runEffectTestPromise( - allowed.runtime.runAction({ + expect(harness.counts().transitionCount).toBe(0); + }), + ); + + const allowed = yield* makeHarness({ + permissionDecision: 'allowed', + tenantPermissionDecision: 'allowed', + }); + yield* allowed.runtime.runAction({ payload: undefined, principal, registration: tenantAuthorizedRegistration, transport: transport('tenant-allowed'), - }), - ); - assert.equal(allowed.counts().transitionCount, 1); -}); + }); + expect(allowed.counts().transitionCount).toBe(1); + }), +); -test('accepts every Party write authority as an explicit tenant permission', async () => { - const partyPermissions = [ - 'manage_party_identity', - 'manage_party_relationships', - 'merge_party_identity', - 'review_party_identity', - ] as const; - await forEachSequential( - partyPermissions.map((permission, index) => [index, permission] as const), - async ([index, permission]) => { - const actionKey = `party.registry.permission-${index}`; - const action = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'party.read.v1' }, - actionKey, - auditProfile: 'sensitive', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineTenantModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: actionKey, - moduleKey: 'party.registry', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'party.registry', - payloadSchema: Schema.Void, - policies: [], - resultSchema: Schema.Void, - schemaVersion: '1', - tenantPermission: () => permission, - }, - () => Effect.void, - ); - const harness = makeHarness(); - await runEffectTestPromise( - harness.runtime.runAction({ +it.effect( + 'accepts every Party write authority as an explicit tenant permission', + Effect.fn(function* testProgram13() { + const partyPermissions = [ + 'manage_party_identity', + 'manage_party_relationships', + 'merge_party_identity', + 'review_party_identity', + ] as const; + yield* forEachSequential( + partyPermissions.map((permission, index) => [index, permission] as const), + Effect.fn(function* testProgram14([index, permission]) { + const actionKey = `party.registry.permission-${index}`; + const action = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'party.read.v1' }, + actionKey, + auditProfile: 'sensitive', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineTenantModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: actionKey, + moduleKey: 'party.registry', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'party.registry', + payloadSchema: Schema.Void, + policies: [], + resultSchema: Schema.Void, + schemaVersion: '1', + tenantPermission: () => permission, + }, + () => Effect.void, + ); + const harness = yield* makeHarness(); + yield* harness.runtime.runAction({ payload: undefined, principal, registration: action, transport: transport(permission), - }), - ); - assert.deepEqual(harness.tenantChecks, [ - { - permission, - principalId: principal.principalId, - tenantIds: [principal.tenantId], - }, - ]); - assert.deepEqual(harness.flushed[0]?.transport, { - correlationId: `correlation-${permission}`, - idempotencyKey: permission, - }); - }, - ); -}); - -test('canonicalizes every resolved tenant permission target for hash and evidence', async () => { - const action = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'identity.read.v1' }, - actionKey: 'core.identity.rotate-managed-key', - auditProfile: 'sensitive', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineSystemModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'core.identity.rotate-managed-key', - moduleKey: 'core.identity', - role: 'action', + }); + expect(harness.tenantChecks).toEqual([ + { + permission, + principalId: principal.principalId, + tenantIds: [principal.tenantId], + }, + ]); + expect(harness.flushed[0]?.transport).toEqual({ + correlationId: `correlation-${permission}`, + idempotencyKey: permission, + }); }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'core.identity', - payloadSchema: Schema.Void, - policies: [], - resultSchema: Schema.Void, - schemaVersion: '1', - tenantPermission: () => 'manage_identity', - }, - () => Effect.void, - ); - const first = makeHarness(); - const second = makeHarness(); - await runEffectTestPromise( - first.runtime.runAction({ + ); + }), +); + +it.effect( + 'canonicalizes every resolved tenant permission target for hash and evidence', + Effect.fn(function* testProgram15() { + const action = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'identity.read.v1' }, + actionKey: 'core.identity.rotate-managed-key', + auditProfile: 'sensitive', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineSystemModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'core.identity.rotate-managed-key', + moduleKey: 'core.identity', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'core.identity', + payloadSchema: Schema.Void, + policies: [], + resultSchema: Schema.Void, + schemaVersion: '1', + tenantPermission: () => 'manage_identity', + }, + () => Effect.void, + ); + const first = yield* makeHarness(); + const second = yield* makeHarness(); + yield* first.runtime.runAction({ payload: undefined, principal, registration: action, @@ -1117,10 +1162,8 @@ test('canonicalizes every resolved tenant permission target for hash and evidenc targetResourceId: 'forged-one', targetResourceType: 'first', }, - }), - ); - await runEffectTestPromise( - second.runtime.runAction({ + }); + yield* second.runtime.runAction({ payload: undefined, principal, registration: action, @@ -1130,112 +1173,117 @@ test('canonicalizes every resolved tenant permission target for hash and evidenc targetResourceId: 'forged-two', targetResourceType: 'second', }, - }), - ); + }); - assert.deepEqual(first.requestHashes, second.requestHashes); - assert.deepEqual(first.flushed[0]?.transport, { - correlationId: 'correlation-same-idempotency-key', - idempotencyKey: 'same-idempotency-key', - }); - assert.deepEqual(second.flushed[0]?.transport, first.flushed[0]?.transport); -}); + expect(first.requestHashes).toEqual(second.requestHashes); + expect(first.flushed[0]?.transport).toEqual({ + correlationId: 'correlation-same-idempotency-key', + idempotencyKey: 'same-idempotency-key', + }); + expect(second.flushed[0]?.transport).toEqual(first.flushed[0]?.transport); + }), +); -test('authorizes Counterparty creation against the trusted Legal Entity before Policy and transaction', async () => { - let handlerCalls = 0; - let policyCalls = 0; - const action = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counterparty.read.v1' }, - actionKey: 'party.registry.create-counterparty', - auditProfile: 'sensitive', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineTenantModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'party.registry.create-counterparty', - moduleKey: 'party.registry', - role: 'action', - }), - idempotency: 'required', - legalEntityPermission: 'manage_counterparty', - legalEntityScope: 'required', - owningModuleKey: 'party.registry', - payloadSchema: Schema.Void, - policies: [ - defineGlobalPolicy({ - evaluate: (input) => { - policyCalls += 1; - assert.deepEqual(input.target, {}); - return Effect.void; +it.effect( + 'authorizes Counterparty creation against the trusted Legal Entity before Policy and transaction', + Effect.fn(function* testProgram16() { + let handlerCalls = 0; + let policyCalls = 0; + const action = defineAction( + { + accessEvidencePolicy: { + captureMode: 'metadata_only', + policyKey: 'counterparty.read.v1', + }, + actionKey: 'party.registry.create-counterparty', + auditProfile: 'sensitive', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineTenantModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', }, - policyKey: 'party.registry.counterparty-create.v1', + entrypointKey: 'party.registry.create-counterparty', + moduleKey: 'party.registry', + role: 'action', }), - ], - resultSchema: Schema.Void, - schemaVersion: '1', - }, - () => { - handlerCalls += 1; - return Effect.void; - }, - ); - const forgedTransport = { - ...transport('legal-entity-denied'), - targetModuleKey: 'forged.module', - targetResourceId: 'forged-legal-entity', - targetResourceType: 'legal_entity', - }; - - const denied = makeHarness({ legalEntityPermissionDecision: 'denied' }); - const failure = await runEffectTestPromise( - Effect.flip( + idempotency: 'required', + legalEntityPermission: 'manage_counterparty', + legalEntityScope: 'required', + owningModuleKey: 'party.registry', + payloadSchema: Schema.Void, + policies: [ + defineGlobalPolicy({ + evaluate: (input) => { + policyCalls += 1; + expect(input.target).toEqual({}); + return Effect.void; + }, + policyKey: 'party.registry.counterparty-create.v1', + }), + ], + resultSchema: Schema.Void, + schemaVersion: '1', + }, + () => { + handlerCalls += 1; + return Effect.void; + }, + ); + const forgedTransport = { + ...transport('legal-entity-denied'), + targetModuleKey: 'forged.module', + targetResourceId: 'forged-legal-entity', + targetResourceType: 'legal_entity', + }; + + const denied = yield* makeHarness({ legalEntityPermissionDecision: 'denied' }); + const failure = yield* Effect.flip( denied.runtime.runAction({ payload: undefined, principal, registration: action, transport: forgedTransport, }), - ), - ); - assert.equal(failure._tag, 'ActionPermissionDenied'); - assert.equal(policyCalls, 0); - assert.equal(handlerCalls, 0); - assert.equal(denied.counts().transactionCount, 0); - assert.deepEqual(denied.legalEntityChecks, [ - { - legalEntityIds: [principal.legalEntityId], - permission: 'manage_counterparty', - principalId: principal.principalId, - tenantId: principal.tenantId, - }, - ]); - assert.deepEqual(denied.rejections[0]?.transport, { - correlationId: 'correlation-legal-entity-denied', - idempotencyKey: 'legal-entity-denied', - }); - assert.equal(denied.stages.at(-1), 'permission_checked'); + ); + expect(Predicate.isTagged(failure, 'ActionPermissionDenied')).toBe(true); + + expect(policyCalls).toBe(0); + expect(handlerCalls).toBe(0); + expect(denied.counts().transactionCount).toBe(0); + expect(denied.legalEntityChecks).toEqual([ + { + legalEntityIds: [principal.legalEntityId], + permission: 'manage_counterparty', + principalId: principal.principalId, + tenantId: principal.tenantId, + }, + ]); + expect(denied.rejections[0]?.transport).toEqual({ + correlationId: 'correlation-legal-entity-denied', + idempotencyKey: 'legal-entity-denied', + }); + expect(denied.stages.at(-1)).toBe('permission_checked'); - const unavailable = makeHarness({ legalEntityPermissionDecision: 'unavailable' }); - const unavailableFailure = await runEffectTestPromise( - Effect.flip( + const unavailable = yield* makeHarness({ legalEntityPermissionDecision: 'unavailable' }); + const unavailableFailure = yield* Effect.flip( unavailable.runtime.runAction({ payload: undefined, principal, registration: action, transport: forgedTransport, }), - ), - ); - assert.equal(unavailableFailure._tag, 'ActionPermissionCheckError'); - assert.equal(unavailable.rejections.length, 0); - assert.equal(unavailable.counts().transactionCount, 0); - assert.equal(unavailable.stages.includes('permission_checked'), false); - - const allowed = makeHarness(); - await runEffectTestPromise( - allowed.runtime.runAction({ + ); + expect(Predicate.isTagged(unavailableFailure, 'ActionPermissionCheckError')).toBe(true); + + expect(unavailable.rejections.length).toBe(0); + expect(unavailable.counts().transactionCount).toBe(0); + expect(unavailable.stages.includes('permission_checked')).toBe(false); + + const allowed = yield* makeHarness(); + yield* allowed.runtime.runAction({ payload: undefined, principal, registration: action, @@ -1243,79 +1291,86 @@ test('authorizes Counterparty creation against the trusted Legal Entity before P ...forgedTransport, idempotencyKey: 'legal-entity-allowed', }, - }), - ); - assert.equal(policyCalls, 1); - assert.equal(handlerCalls, 1); - assert.deepEqual(allowed.flushed[0]?.transport, { - correlationId: 'correlation-legal-entity-denied', - idempotencyKey: 'legal-entity-allowed', - }); - assert.ok( - allowed.stages.indexOf('permission_checked') < allowed.stages.indexOf('policy_boundary'), - ); -}); + }); + expect(policyCalls).toBe(1); + expect(handlerCalls).toBe(1); + expect(allowed.flushed[0]?.transport).toEqual({ + correlationId: 'correlation-legal-entity-denied', + idempotencyKey: 'legal-entity-allowed', + }); + expect( + allowed.stages.indexOf('permission_checked') < allowed.stages.indexOf('policy_boundary'), + ).toBe(true); + }), +); -test('authorizes the resolved Resource target before Policy, transaction, and handler', async () => { - let handlerCalls = 0; - let policyCalls = 0; - const action = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counterparty.read.v1' }, - actionKey: 'party.registry.end-counterparty-role', - auditProfile: 'sensitive', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineTenantModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'party.registry.end-counterparty-role', - moduleKey: 'party.registry', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'required', - owningModuleKey: 'party.registry', - payloadSchema: Schema.Struct({ counterpartyId: CounterpartyIdSchema }), - policies: [ - defineGlobalPolicy<{ readonly counterpartyId: CounterpartyId }>({ - evaluate: (input) => { - policyCalls += 1; - assert.deepEqual(input.target, { - targetModuleKey: 'party.registry', - targetResourceId: 'counterparty-1', - targetResourceType: 'counterparty', - }); - return Effect.void; +it.effect( + 'authorizes the resolved Resource target before Policy, transaction, and handler', + Effect.fn(function* testProgram17() { + let handlerCalls = 0; + let policyCalls = 0; + const action = defineAction( + { + accessEvidencePolicy: { + captureMode: 'metadata_only', + policyKey: 'counterparty.read.v1', + }, + actionKey: 'party.registry.end-counterparty-role', + auditProfile: 'sensitive', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineTenantModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', }, - policyKey: 'party.registry.role-end.v1', + entrypointKey: 'party.registry.end-counterparty-role', + moduleKey: 'party.registry', + role: 'action', }), - ], - resourcePermission: defineActionResourcePermission<{ - readonly counterpartyId: CounterpartyId; - }>(({ counterpartyId }, scope) => { - assert.equal(scope.legalEntityId, principal.legalEntityId); - return { - permission: 'write', - resource: { - moduleId: 'party.registry', - resourceId: counterpartyId, - resourceType: 'counterparty', - }, - }; - }), - resultSchema: Schema.Void, - schemaVersion: '1', - }, - () => { - handlerCalls += 1; - return Effect.void; - }, - ); + idempotency: 'required', + legalEntityScope: 'required', + owningModuleKey: 'party.registry', + payloadSchema: Schema.Struct({ counterpartyId: CounterpartyIdSchema }), + policies: [ + defineGlobalPolicy<{ readonly counterpartyId: CounterpartyId }>({ + evaluate: (input) => { + policyCalls += 1; + expect(input.target).toEqual({ + targetModuleKey: 'party.registry', + targetResourceId: 'counterparty-1', + targetResourceType: 'counterparty', + }); + return Effect.void; + }, + policyKey: 'party.registry.role-end.v1', + }), + ], + resourcePermission: defineActionResourcePermission<{ + readonly counterpartyId: CounterpartyId; + }>(({ counterpartyId }, scope) => { + expect(scope.legalEntityId).toBe(principal.legalEntityId); + return { + permission: 'write', + resource: { + moduleId: 'party.registry', + resourceId: counterpartyId, + resourceType: 'counterparty', + }, + }; + }), + resultSchema: Schema.Void, + schemaVersion: '1', + }, + () => { + handlerCalls += 1; + return Effect.void; + }, + ); - const denied = makeHarness({ resourcePermissionDecision: 'denied' }); - const failure = await runEffectTestPromise( - Effect.flip( + const denied = yield* makeHarness({ resourcePermissionDecision: 'denied' }); + const failure = yield* Effect.flip( denied.runtime.runAction({ payload: { counterpartyId: 'counterparty-1' }, principal, @@ -1327,598 +1382,639 @@ test('authorizes the resolved Resource target before Policy, transaction, and ha targetResourceType: 'forged-type', }, }), - ), - ); + ); - assert.equal(failure._tag, 'ActionPermissionDenied'); - assert.equal(policyCalls, 0); - assert.equal(handlerCalls, 0); - assert.equal(denied.counts().transactionCount, 0); - assert.deepEqual(denied.resourceChecks, [ - { - legalEntityId: principal.legalEntityId, - permission: 'write', - principalId: principal.principalId, - resources: [ - { - moduleId: 'party.registry', - resourceId: 'counterparty-1', - resourceType: 'counterparty', - }, - ], - tenantId: principal.tenantId, - }, - ]); - assert.deepEqual(denied.rejections[0]?.transport, { - correlationId: 'correlation-resource-denied', - idempotencyKey: 'resource-denied', - targetModuleKey: 'party.registry', - targetResourceId: 'counterparty-1', - targetResourceType: 'counterparty', - }); + expect(Predicate.isTagged(failure, 'ActionPermissionDenied')).toBe(true); + + expect(policyCalls).toBe(0); + expect(handlerCalls).toBe(0); + expect(denied.counts().transactionCount).toBe(0); + expect(denied.resourceChecks).toEqual([ + { + legalEntityId: principal.legalEntityId, + permission: 'write', + principalId: principal.principalId, + resources: [ + { + moduleId: 'party.registry', + resourceId: 'counterparty-1', + resourceType: 'counterparty', + }, + ], + tenantId: principal.tenantId, + }, + ]); + expect(denied.rejections[0]?.transport).toEqual({ + correlationId: 'correlation-resource-denied', + idempotencyKey: 'resource-denied', + targetModuleKey: 'party.registry', + targetResourceId: 'counterparty-1', + targetResourceType: 'counterparty', + }); - const unavailable = makeHarness({ resourcePermissionDecision: 'unavailable' }); - const unavailableFailure = await runEffectTestPromise( - Effect.flip( + const unavailable = yield* makeHarness({ resourcePermissionDecision: 'unavailable' }); + const unavailableFailure = yield* Effect.flip( unavailable.runtime.runAction({ payload: { counterpartyId: 'counterparty-1' }, principal, registration: action, transport: transport('resource-unavailable'), }), - ), - ); - assert.equal(unavailableFailure._tag, 'ActionPermissionCheckError'); - assert.equal(unavailable.rejections.length, 0); - assert.equal(unavailable.counts().transactionCount, 0); -}); + ); + expect(Predicate.isTagged(unavailableFailure, 'ActionPermissionCheckError')).toBe(true); -test('persists a definite permission denial before returning it and never evaluates Policies', async () => { - let handlerCount = 0; - let policyCount = 0; - let serviceFactoryCount = 0; - const harness = makeHarness({ permissionDecision: 'denied' }); - const deniedRegistration = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, - actionKey: 'shell.counter.denied', - auditProfile: 'sensitive', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineSystemModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'shell.counter.denied', - moduleKey: 'core.shell', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'core.shell', - payloadSchema: Schema.Void, - policies: [ - defineGlobalPolicy({ - evaluate: () => { - policyCount += 1; - return Effect.void; + expect(unavailable.rejections.length).toBe(0); + expect(unavailable.counts().transactionCount).toBe(0); + }), +); + +it.effect( + 'persists a definite permission denial before returning it and never evaluates Policies', + Effect.fn(function* testProgram18() { + let handlerCount = 0; + let policyCount = 0; + let serviceFactoryCount = 0; + const harness = yield* makeHarness({ permissionDecision: 'denied' }); + const deniedRegistration = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, + actionKey: 'shell.counter.denied', + auditProfile: 'sensitive', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineSystemModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', }, - policyKey: 'global.unreachable-after-permission-denial.v1', + entrypointKey: 'shell.counter.denied', + moduleKey: 'core.shell', + role: 'action', }), - ], - resultSchema: Schema.Void, - schemaVersion: '1', - }, - () => { - handlerCount += 1; - return Effect.void; - }, - () => { - serviceFactoryCount += 1; - return Effect.succeed({}); - }, - ); + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'core.shell', + payloadSchema: Schema.Void, + policies: [ + defineGlobalPolicy({ + evaluate: () => { + policyCount += 1; + return Effect.void; + }, + policyKey: 'global.unreachable-after-permission-denial.v1', + }), + ], + resultSchema: Schema.Void, + schemaVersion: '1', + }, + () => { + handlerCount += 1; + return Effect.void; + }, + () => { + serviceFactoryCount += 1; + return Effect.succeed({}); + }, + ); - const failure = await runEffectTestPromise( - Effect.flip( + const failure = yield* Effect.flip( harness.runtime.runAction({ payload: undefined, principal, registration: deniedRegistration, transport: transport('denied'), }), - ), - ); + ); - assert.equal(failure._tag, 'ActionPermissionDenied'); - assert.equal(failure.code, 'action_permission_denied'); - assert.equal(handlerCount, 0); - assert.equal(policyCount, 0); - assert.equal(serviceFactoryCount, 0); - assert.deepEqual(harness.stages, [ - 'payload_decoded', - 'trusted_context_validated', - 'module_state_gate', - 'invocation_prepared', - 'authentication_boundary', - 'permission_checked', - ]); - assert.deepEqual(harness.permissionCounts(), { - permissionCheckCount: 1, - rejectionCount: 1, - }); - assert.deepEqual(harness.counts(), { - createCount: 1, - lockCount: 0, - transactionCount: 0, - transitionCount: 0, - }); - assert.deepEqual(harness.rejections, [ - { - actionInvocationId: 'invocation-1', - actionKey: 'shell.counter.denied', - auditProfile: 'sensitive', - principal, - transport: transport('denied'), - }, - ]); -}); + expect(Predicate.isTagged(failure, 'ActionPermissionDenied')).toBe(true); + + expect(failure.code).toBe('action_permission_denied'); + expect(handlerCount).toBe(0); + expect(policyCount).toBe(0); + expect(serviceFactoryCount).toBe(0); + expect(harness.stages).toEqual([ + 'payload_decoded', + 'trusted_context_validated', + 'module_state_gate', + 'invocation_prepared', + 'authentication_boundary', + 'permission_checked', + ]); + expect(harness.permissionCounts()).toEqual({ + permissionCheckCount: 1, + rejectionCount: 1, + }); + expect(harness.counts()).toEqual({ + createCount: 1, + lockCount: 0, + transactionCount: 0, + transitionCount: 0, + }); + expect(harness.rejections).toEqual([ + { + actionInvocationId: 'invocation-1', + actionKey: 'shell.counter.denied', + auditProfile: 'sensitive', + principal, + transport: transport('denied'), + }, + ]); + }), +); -test('fails closed before Policy evaluation when permission cannot be determined', async () => { - const harness = makeHarness({ permissionFailure: true }); - const failure = await runEffectTestPromise( - Effect.flip( +it.effect( + 'fails closed before Policy evaluation when permission cannot be determined', + Effect.fn(function* testProgram19() { + const harness = yield* makeHarness({ permissionFailure: true }); + const failure = yield* Effect.flip( harness.runtime.runAction({ payload: { amount: 1 }, principal, registration: registration(), transport: transport('unavailable'), }), - ), - ); + ); - assert.equal(failure._tag, 'ActionPermissionCheckError'); - assert.deepEqual(harness.permissionCounts(), { - permissionCheckCount: 1, - rejectionCount: 0, - }); - assert.deepEqual(harness.counts(), { - createCount: 1, - lockCount: 0, - transactionCount: 0, - transitionCount: 0, - }); - assert.deepEqual(harness.stages, [ - 'payload_decoded', - 'trusted_context_validated', - 'module_state_gate', - 'invocation_prepared', - 'authentication_boundary', - ]); -}); + expect(Predicate.isTagged(failure, 'ActionPermissionCheckError')).toBe(true); -test('does not claim permission denial when terminal evidence persistence rolls back', async () => { - const harness = makeHarness({ permissionDecision: 'denied', rejectionFailure: true }); - const failure = await runEffectTestPromise( - Effect.flip( + expect(harness.permissionCounts()).toEqual({ + permissionCheckCount: 1, + rejectionCount: 0, + }); + expect(harness.counts()).toEqual({ + createCount: 1, + lockCount: 0, + transactionCount: 0, + transitionCount: 0, + }); + expect(harness.stages).toEqual([ + 'payload_decoded', + 'trusted_context_validated', + 'module_state_gate', + 'invocation_prepared', + 'authentication_boundary', + ]); + }), +); + +it.effect( + 'does not claim permission denial when terminal evidence persistence rolls back', + Effect.fn(function* testProgram20() { + const harness = yield* makeHarness({ + permissionDecision: 'denied', + rejectionFailure: true, + }); + const failure = yield* Effect.flip( harness.runtime.runAction({ payload: { amount: 1 }, principal, registration: registration(), transport: transport('permission-denial-persistence-failure'), }), - ), - ); + ); - assert.equal(failure._tag, 'ActionTransactionError'); - assert.deepEqual(harness.permissionCounts(), { - permissionCheckCount: 1, - rejectionCount: 1, - }); - assert.equal(harness.counts().transitionCount, 0); - assert.equal(harness.counts().transactionCount, 0); -}); + expect(Predicate.isTagged(failure, 'ActionTransactionError')).toBe(true); -test('evaluates Policies in order before running and hands allowed checkpoints to success', async () => { - const observed: string[] = []; - const globalPolicy = defineGlobalPolicy<{ readonly amount: number }>({ - evaluate: () => { - observed.push('global'); - return Effect.void; - }, - policyKey: 'global.tenant-active.v1', - }); - const modulePolicy = defineMicroverticalPolicy<{ readonly amount: number }, 'inventory.stock'>({ - evaluate: (input) => { - observed.push(`module:${input.payload.amount}`); - assert.equal(input.principal.principalId, principal.principalId); - assert.equal(input.action.actionKey, 'inventory.stock.policy-allowed'); - assert.equal(input.target.targetResourceId, 'primary'); - assert.equal('idempotencyKey' in input.transport, false); - return Effect.void; - }, - owningModuleKey: 'inventory.stock', - policyKey: 'inventory.stock.allowed.v1', - }); - const action = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, - actionKey: 'inventory.stock.policy-allowed', - auditProfile: 'standard', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineTenantModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'inventory.stock.policy-allowed', - moduleKey: 'inventory.stock', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', + expect(harness.permissionCounts()).toEqual({ + permissionCheckCount: 1, + rejectionCount: 1, + }); + expect(harness.counts().transitionCount).toBe(0); + expect(harness.counts().transactionCount).toBe(0); + }), +); + +it.effect( + 'evaluates Policies in order before running and hands allowed checkpoints to success', + Effect.fn(function* testProgram21() { + const observed: string[] = []; + const globalPolicy = defineGlobalPolicy<{ readonly amount: number }>({ + evaluate: () => { + observed.push('global'); + return Effect.void; + }, + policyKey: 'global.tenant-active.v1', + }); + const modulePolicy = defineMicroverticalPolicy<{ readonly amount: number }, 'inventory.stock'>({ + evaluate: (input) => { + observed.push(`module:${input.payload.amount}`); + expect(input.principal.principalId).toBe(principal.principalId); + expect(input.action.actionKey).toBe('inventory.stock.policy-allowed'); + expect(input.target.targetResourceId).toBe('primary'); + expect('idempotencyKey' in input.transport).toBe(false); + return Effect.void; + }, owningModuleKey: 'inventory.stock', - payloadSchema: Schema.Struct({ amount: Schema.Finite }), - policies: [globalPolicy, modulePolicy], - resultSchema: Schema.Finite, - schemaVersion: '1', - }, - (payload) => { - observed.push('handler'); - return Effect.succeed(payload.amount); - }, - ); - const harness = makeHarness(); + policyKey: 'inventory.stock.allowed.v1', + }); + const action = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, + actionKey: 'inventory.stock.policy-allowed', + auditProfile: 'standard', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineTenantModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'inventory.stock.policy-allowed', + moduleKey: 'inventory.stock', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'inventory.stock', + payloadSchema: Schema.Struct({ amount: Schema.Finite }), + policies: [globalPolicy, modulePolicy], + resultSchema: Schema.Finite, + schemaVersion: '1', + }, + (payload) => { + observed.push('handler'); + return Effect.succeed(payload.amount); + }, + ); + const harness = yield* makeHarness(); - const result = await runEffectTestPromise( - harness.runtime.runAction({ + const result = yield* harness.runtime.runAction({ payload: { amount: 4 }, principal, registration: action, transport: { ...transport(), targetModuleKey: 'inventory.stock' }, - }), - ); - - assert.equal(result, 4); - assert.deepEqual(observed, ['global', 'module:4', 'handler']); - assert.deepEqual(harness.flushed[0]?.allowedPolicies, [ - { policyKey: 'global.tenant-active.v1', scope: 'global' }, - { - owningModuleKey: 'inventory.stock', - policyKey: 'inventory.stock.allowed.v1', - scope: 'microvertical', - }, - ]); -}); + }); -test('short-circuits the first Policy denial, finalizes it, and never starts execution', async () => { - const observed: string[] = []; - let handlerExecutions = 0; - const policies = [ - defineGlobalPolicy<{ readonly amount: number }>({ - evaluate: () => { - observed.push('first'); - return Effect.void; + expect(result).toBe(4); + expect(observed).toEqual(['global', 'module:4', 'handler']); + expect(harness.flushed[0]?.allowedPolicies).toEqual([ + { policyKey: 'global.tenant-active.v1', scope: 'global' }, + { + owningModuleKey: 'inventory.stock', + policyKey: 'inventory.stock.allowed.v1', + scope: 'microvertical', }, - policyKey: 'global.first.v1', - }), - defineGlobalPolicy<{ readonly amount: number }>({ - evaluate: () => { - observed.push('denied'); - return Effect.fail(denyPolicy('counter_locked', 'Counter changes are locked — try later')); + ]); + }), +); + +it.effect( + 'short-circuits the first Policy denial, finalizes it, and never starts execution', + Effect.fn(function* testProgram22() { + const observed: string[] = []; + let handlerExecutions = 0; + const policies = [ + defineGlobalPolicy<{ readonly amount: number }>({ + evaluate: () => { + observed.push('first'); + return Effect.void; + }, + policyKey: 'global.first.v1', + }), + defineGlobalPolicy<{ readonly amount: number }>({ + evaluate: () => { + observed.push('denied'); + return Effect.fail( + denyPolicy('counter_locked', 'Counter changes are locked — try later'), + ); + }, + policyKey: 'global.counter-locked.v1', + }), + defineGlobalPolicy<{ readonly amount: number }>({ + evaluate: () => { + observed.push('unreachable'); + return Effect.void; + }, + policyKey: 'global.unreachable.v1', + }), + ] as const; + const action = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, + actionKey: 'shell.counter.policy-denied', + auditProfile: 'sensitive', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineSystemModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'shell.counter.policy-denied', + moduleKey: 'core.shell', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'core.shell', + payloadSchema: Schema.Struct({ amount: Schema.Finite }), + policies, + resultSchema: Schema.Void, + schemaVersion: '1', }, - policyKey: 'global.counter-locked.v1', - }), - defineGlobalPolicy<{ readonly amount: number }>({ - evaluate: () => { - observed.push('unreachable'); + () => { + handlerExecutions += 1; return Effect.void; }, - policyKey: 'global.unreachable.v1', - }), - ] as const; - const action = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, - actionKey: 'shell.counter.policy-denied', - auditProfile: 'sensitive', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineSystemModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'shell.counter.policy-denied', - moduleKey: 'core.shell', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'core.shell', - payloadSchema: Schema.Struct({ amount: Schema.Finite }), - policies, - resultSchema: Schema.Void, - schemaVersion: '1', - }, - () => { - handlerExecutions += 1; - return Effect.void; - }, - ); - const harness = makeHarness(); + ); + const harness = yield* makeHarness(); - const denial = await runEffectTestPromise( - Effect.flip( + const denial = yield* Effect.flip( harness.runtime.runAction({ payload: { amount: 1 }, principal, registration: action, transport: transport('policy-denied'), }), - ), - ); + ); - assert.equal(denial._tag, 'ActionPolicyDenied'); - assert.equal(denial.policyReasonCode, 'counter_locked'); - assert.equal(denial.reason, 'Counter changes are locked — try later'); - assert.deepEqual(observed, ['first', 'denied']); - assert.equal(handlerExecutions, 0); - assert.deepEqual(harness.counts(), { - createCount: 1, - lockCount: 0, - transactionCount: 0, - transitionCount: 0, - }); - assert.deepEqual(harness.stages, [ - 'payload_decoded', - 'trusted_context_validated', - 'module_state_gate', - 'invocation_prepared', - 'authentication_boundary', - 'permission_checked', - 'policy_boundary', - ]); - assert.deepEqual(harness.finalized[0], { - actionInvocationId: 'invocation-1', - actionKey: 'shell.counter.policy-denied', - auditProfile: 'sensitive', - policy: { policyKey: 'global.counter-locked.v1', scope: 'global' }, - principal, - reasonCode: 'counter_locked', - transport: transport('policy-denied'), - }); - assert.equal(harness.flushed.length, 0); -}); - -test('sanitizes Policy defects and interrupts without finalizing', async () => { - const evaluators = [() => Effect.die('secret evaluator defect'), () => Effect.interrupt] as const; + expect(Predicate.isTagged(denial, 'ActionPolicyDenied')).toBe(true); + if (!Predicate.isTagged(denial, 'ActionPolicyDenied')) { + throw new Error('Expected typed test outcome'); + } + expect(denial.policyReasonCode).toBe('counter_locked'); + expect(denial.reason).toBe('Counter changes are locked — try later'); + expect(observed).toEqual(['first', 'denied']); + expect(handlerExecutions).toBe(0); + expect(harness.counts()).toEqual({ + createCount: 1, + lockCount: 0, + transactionCount: 0, + transitionCount: 0, + }); + expect(harness.stages).toEqual([ + 'payload_decoded', + 'trusted_context_validated', + 'module_state_gate', + 'invocation_prepared', + 'authentication_boundary', + 'permission_checked', + 'policy_boundary', + ]); + expect(harness.finalized[0]).toEqual({ + actionInvocationId: 'invocation-1', + actionKey: 'shell.counter.policy-denied', + auditProfile: 'sensitive', + policy: { policyKey: 'global.counter-locked.v1', scope: 'global' }, + principal, + reasonCode: 'counter_locked', + transport: transport('policy-denied'), + }); + expect(harness.flushed.length).toBe(0); + }), +); - await forEachSequential( - evaluators.map((evaluate, index) => [index, evaluate] as const), - async ([index, evaluate]) => { - let handlerExecutions = 0; - const policy = defineGlobalPolicy({ - evaluate, - policyKey: `global.failure-${index}.v1`, - }); - const action = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, - actionKey: `shell.counter.policy-failure-${index}`, - auditProfile: 'standard', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineSystemModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: `shell.counter.policy-failure-${index}`, - moduleKey: 'core.shell', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'core.shell', - payloadSchema: Schema.Void, - policies: [policy], - resultSchema: Schema.Void, - schemaVersion: '1', - }, - () => { - handlerExecutions += 1; - return Effect.void; - }, - ); - const harness = makeHarness(); - const error = await runEffectTestPromise( - Effect.flip( +it.effect( + 'sanitizes Policy defects and interrupts without finalizing', + Effect.fn(function* testProgram23() { + const evaluators = [ + () => Effect.die('secret evaluator defect'), + () => Effect.interrupt, + ] as const; + + yield* forEachSequential( + evaluators.map((evaluate, index) => [index, evaluate] as const), + Effect.fn(function* testProgram24([index, evaluate]) { + let handlerExecutions = 0; + const policy = defineGlobalPolicy({ + evaluate, + policyKey: `global.failure-${index}.v1`, + }); + const action = defineAction( + { + accessEvidencePolicy: { + captureMode: 'metadata_only', + policyKey: 'counter.read.v1', + }, + actionKey: `shell.counter.policy-failure-${index}`, + auditProfile: 'standard', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineSystemModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: `shell.counter.policy-failure-${index}`, + moduleKey: 'core.shell', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'core.shell', + payloadSchema: Schema.Void, + policies: [policy], + resultSchema: Schema.Void, + schemaVersion: '1', + }, + () => { + handlerExecutions += 1; + return Effect.void; + }, + ); + const harness = yield* makeHarness(); + const error = yield* Effect.flip( harness.runtime.runAction({ payload: undefined, principal, registration: action, transport: transport(`policy-failure-${index}`), }), - ), - ); + ); - assert.equal(error._tag, 'ActionPolicyEvaluationError'); - assert.equal(error.reason.includes('secret'), false); - assert.equal(handlerExecutions, 0); - assert.equal(harness.finalized.length, 0); - assert.deepEqual(harness.counts(), { - createCount: 1, - lockCount: 0, - transactionCount: 0, - transitionCount: 0, - }); - }, - ); -}); + expect(Predicate.isTagged(error, 'ActionPolicyEvaluationError')).toBe(true); -test('returns persistence failure when denial evidence cannot be finalized', async () => { - let handlerExecutions = 0; - const policy = defineGlobalPolicy({ - evaluate: () => Effect.fail(denyPolicy('blocked', 'This action is blocked')), - policyKey: 'global.blocked.v1', - }); - const action = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, - actionKey: 'shell.counter.policy-persistence-failure', - auditProfile: 'standard', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineSystemModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'shell.counter.policy-persistence-failure', - moduleKey: 'core.shell', - role: 'action', + expect(error.reason.includes('secret')).toBe(false); + expect(handlerExecutions).toBe(0); + expect(harness.finalized.length).toBe(0); + expect(harness.counts()).toEqual({ + createCount: 1, + lockCount: 0, + transactionCount: 0, + transitionCount: 0, + }); }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'core.shell', - payloadSchema: Schema.Void, - policies: [policy], - resultSchema: Schema.Void, - schemaVersion: '1', - }, - () => { - handlerExecutions += 1; - return Effect.void; - }, - ); - const harness = makeHarness({ policyFinalizationFailure: true }); + ); + }), +); - const error = await runEffectTestPromise( - Effect.flip( +it.effect( + 'returns persistence failure when denial evidence cannot be finalized', + Effect.fn(function* testProgram25() { + let handlerExecutions = 0; + const policy = defineGlobalPolicy({ + evaluate: () => Effect.fail(denyPolicy('blocked', 'This action is blocked')), + policyKey: 'global.blocked.v1', + }); + const action = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, + actionKey: 'shell.counter.policy-persistence-failure', + auditProfile: 'standard', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineSystemModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'shell.counter.policy-persistence-failure', + moduleKey: 'core.shell', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'core.shell', + payloadSchema: Schema.Void, + policies: [policy], + resultSchema: Schema.Void, + schemaVersion: '1', + }, + () => { + handlerExecutions += 1; + return Effect.void; + }, + ); + const harness = yield* makeHarness({ policyFinalizationFailure: true }); + + const error = yield* Effect.flip( harness.runtime.runAction({ payload: undefined, principal, registration: action, transport: transport('policy-finalization-failure'), }), - ), - ); + ); - assert.equal(error._tag, 'ActionInvocationPersistenceError'); - assert.equal(handlerExecutions, 0); - assert.equal(harness.finalized.length, 0); - assert.equal(harness.counts().transactionCount, 0); -}); + expect(Predicate.isTagged(error, 'ActionInvocationPersistenceError')).toBe(true); + + expect(handlerExecutions).toBe(0); + expect(harness.finalized.length).toBe(0); + expect(harness.counts().transactionCount).toBe(0); + }), +); -test('creates fresh collectors for every execution', async () => { - const harness = makeHarness(); - await forEachSequential( - [ - ['first', 1], - ['second', 2], - ] as const, - async ([key, amount]) => { - await runEffectTestPromise( - harness.runtime.runAction({ +it.effect( + 'creates fresh collectors for every execution', + Effect.fn(function* testProgram26() { + const harness = yield* makeHarness(); + yield* forEachSequential( + [ + ['first', 1], + ['second', 2], + ] as const, + Effect.fn(function* testProgram27([key, amount]) { + yield* harness.runtime.runAction({ payload: { amount }, principal, registration: registration(), transport: transport(key), - }), - ); - }, - ); + }); + }), + ); - assert.equal(harness.flushed.length, 2); - assert.deepEqual( - harness.flushed.map((item) => item.evidence.domainEvents.length), - [1, 1], - ); - assert.notEqual(harness.flushed[0]?.evidence, harness.flushed[1]?.evidence); -}); + expect(harness.flushed.length).toBe(2); + expect(harness.flushed.map((item) => item.evidence.domainEvents.length)).toEqual([1, 1]); + expect(harness.flushed[0]?.evidence).not.toBe(harness.flushed[1]?.evidence); + }), +); -test('evaluates Policies afresh for separate invocations', async () => { - let evaluations = 0; - const policy = defineGlobalPolicy<{ readonly amount: number }>({ - evaluate: () => { - evaluations += 1; - return Effect.void; - }, - policyKey: 'global.fresh-evaluation.v1', - }); - const action = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, - actionKey: 'shell.counter.fresh-policy', - auditProfile: 'standard', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineSystemModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'shell.counter.fresh-policy', - moduleKey: 'core.shell', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'core.shell', - payloadSchema: Schema.Struct({ amount: Schema.Finite }), - policies: [policy], - resultSchema: Schema.Void, - schemaVersion: '1', - }, - () => Effect.void, - ); - await forEachSequential(['fresh-first', 'fresh-second'], async (key) => { - const harness = makeHarness({ - createRecord: { - actionInvocationId: key, - completedAt: null, - requestHash: '', - status: 'received', +it.effect( + 'evaluates Policies afresh for separate invocations', + Effect.fn(function* testProgram28() { + let evaluations = 0; + const policy = defineGlobalPolicy<{ readonly amount: number }>({ + evaluate: () => { + evaluations += 1; + return Effect.void; }, + policyKey: 'global.fresh-evaluation.v1', }); - await runEffectTestPromise( - harness.runtime.runAction({ - payload: { amount: 1 }, - principal, - registration: action, - transport: transport(key), + const action = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, + actionKey: 'shell.counter.fresh-policy', + auditProfile: 'standard', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineSystemModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'shell.counter.fresh-policy', + moduleKey: 'core.shell', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'core.shell', + payloadSchema: Schema.Struct({ amount: Schema.Finite }), + policies: [policy], + resultSchema: Schema.Void, + schemaVersion: '1', + }, + () => Effect.void, + ); + yield* forEachSequential( + ['fresh-first', 'fresh-second'], + Effect.fn(function* testProgram29(key) { + const harness = yield* makeHarness({ + createRecord: { + actionInvocationId: key, + completedAt: null, + requestHash: '', + status: 'received', + }, + }); + yield* harness.runtime.runAction({ + payload: { amount: 1 }, + principal, + registration: action, + transport: transport(key), + }); }), ); - }); - assert.equal(evaluations, 2); -}); + expect(evaluations).toBe(2); + }), +); -test('rejects structural payloads, trusted context, and missing idempotency before invocation', async () => { - const harness = makeHarness(); - const invalidPayload = await runEffectTestPromise( - Effect.flip( +it.effect( + 'rejects structural payloads, trusted context, and missing idempotency before invocation', + Effect.fn(function* testProgram30() { + const harness = yield* makeHarness(); + const invalidPayload = yield* Effect.flip( harness.runtime.runAction({ payload: { amount: 'not-a-number' }, principal, registration: registration(), transport: transport(), }), - ), - ); - const invalidPrincipal = await runEffectTestPromise( - Effect.flip( + ); + const invalidPrincipal = yield* Effect.flip( harness.runtime.runAction({ payload: { amount: 1 }, principal: { ...principal, principalId: 'not-a-uuid' }, registration: registration(), transport: transport(), }), - ), - ); - const missingKey = await runEffectTestPromise( - Effect.flip( + ); + const missingKey = yield* Effect.flip( harness.runtime.runAction({ payload: { amount: 1 }, principal, registration: registration(), transport: { correlationId: 'correlation-missing-key' }, }), - ), - ); - const forgedSystemPrincipal = await runEffectTestPromise( - Effect.flip( + ); + const forgedSystemPrincipal = yield* Effect.flip( harness.runtime.runAction({ payload: { amount: 1 }, principal: { @@ -1930,59 +2026,67 @@ test('rejects structural payloads, trusted context, and missing idempotency befo registration: registration(), transport: transport('forged-system'), }), - ), - ); + ); - assert.equal(invalidPayload._tag, 'ActionPayloadValidationError'); - assert.equal(invalidPrincipal._tag, 'ActionTrustedContextValidationError'); - assert.equal(missingKey._tag, 'ActionIdempotencyKeyRequired'); - assert.equal(forgedSystemPrincipal._tag, 'ActionTrustedContextValidationError'); - assert.equal(harness.counts().createCount, 0); -}); + expect(Predicate.isTagged(invalidPayload, 'ActionPayloadValidationError')).toBe(true); -test('preserves declared domain rejections and rolls back collected evidence', async () => { - const DomainRejectedContract = Schema.TaggedStruct('DomainRejected', { - reason: Schema.String, - }); - type DomainRejectedSelf = typeof DomainRejectedContract.Type; - const DomainRejected = Schema.TaggedError()('DomainRejected', { - reason: Schema.String, - }); - const harness = makeHarness(); - let policyEvaluations = 0; - const allowedPolicy = defineGlobalPolicy({ - evaluate: () => { - policyEvaluations += 1; - return Effect.void; - }, - policyKey: 'global.domain-rejection-allowed.v1', - }); - const rejected = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, - actionKey: 'shell.counter.reject', - auditProfile: 'standard', - domainErrorSchema: DomainRejected, - domainEvents: { - 'counter.considered': Schema.Struct({}), + expect(Predicate.isTagged(invalidPrincipal, 'ActionTrustedContextValidationError')).toBe(true); + expect(Predicate.isTagged(missingKey, 'ActionIdempotencyKeyRequired')).toBe(true); + + expect(Predicate.isTagged(forgedSystemPrincipal, 'ActionTrustedContextValidationError')).toBe( + true, + ); + expect(harness.counts().createCount).toBe(0); + }), +); + +it.effect( + 'preserves declared domain rejections and rolls back collected evidence', + Effect.fn(function* testProgram31() { + const DomainRejectedContract = Schema.TaggedStruct('DomainRejected', { + reason: Schema.String, + }); + type DomainRejectedSelf = typeof DomainRejectedContract.Type; + const DomainRejected = Schema.TaggedError()('DomainRejected', { + reason: Schema.String, + }); + const harness = yield* makeHarness(); + let policyEvaluations = 0; + const allowedPolicy = defineGlobalPolicy({ + evaluate: () => { + policyEvaluations += 1; + return Effect.void; }, - entrypoint: defineSystemModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'shell.counter.reject', - moduleKey: 'core.shell', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'core.shell', - payloadSchema: Schema.Void, - policies: [allowedPolicy], - resultSchema: Schema.Void, - schemaVersion: '1', - }, - (_payload, context) => - Effect.gen(function* rejectCounter() { + policyKey: 'global.domain-rejection-allowed.v1', + }); + const rejected = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, + actionKey: 'shell.counter.reject', + auditProfile: 'standard', + domainErrorSchema: DomainRejected, + domainEvents: { + 'counter.considered': Schema.Struct({}), + }, + entrypoint: defineSystemModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'shell.counter.reject', + moduleKey: 'core.shell', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'core.shell', + payloadSchema: Schema.Void, + policies: [allowedPolicy], + resultSchema: Schema.Void, + schemaVersion: '1', + }, + Effect.fn(function* rejectCounter(_payload, context) { yield* context.addDomainEvent({ eventType: 'counter.considered', payloadJson: {}, @@ -1993,416 +2097,444 @@ test('preserves declared domain rejections and rolls back collected evidence', a }); return yield* new DomainRejected({ reason: 'counter_locked' }); }), - ); + ); - const error = await runEffectTestPromise( - Effect.flip( + const error = yield* Effect.flip( harness.runtime.runAction({ payload: undefined, principal, registration: rejected, transport: transport(), }), - ), - ); + ); - assert.equal(error._tag, 'DomainRejected'); - assert.equal(error.reason, 'counter_locked'); - assert.equal(policyEvaluations, 1); - assert.equal(harness.flushed.length, 0); -}); + expect(Predicate.isTagged(error, 'DomainRejected')).toBe(true); -test('sanitizes unexpected defects and rejects invalid typed results', async () => { - const defectHarness = makeHarness(); - const defective = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, - actionKey: 'shell.counter.defect', - auditProfile: 'standard', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineSystemModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'shell.counter.defect', - moduleKey: 'core.shell', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'core.shell', - payloadSchema: Schema.Void, - policies: [], - resultSchema: Schema.Void, - schemaVersion: '1', - }, - () => Effect.die('secret database detail'), - ); - const defect = await runEffectTestPromise( - Effect.flip( + expect(error.reason).toBe('counter_locked'); + expect(policyEvaluations).toBe(1); + expect(harness.flushed.length).toBe(0); + }), +); + +it.effect( + 'sanitizes unexpected defects and rejects invalid typed results', + Effect.fn(function* testProgram32() { + const defectHarness = yield* makeHarness(); + const defective = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, + actionKey: 'shell.counter.defect', + auditProfile: 'standard', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineSystemModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'shell.counter.defect', + moduleKey: 'core.shell', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'core.shell', + payloadSchema: Schema.Void, + policies: [], + resultSchema: Schema.Void, + schemaVersion: '1', + }, + () => Effect.die('secret database detail'), + ); + const defect = yield* Effect.flip( defectHarness.runtime.runAction({ payload: undefined, principal, registration: defective, transport: transport(), }), - ), - ); + ); - const resultHarness = makeHarness(); - const invalidResult = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, - actionKey: 'shell.counter.invalid-result', - auditProfile: 'standard', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineSystemModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'shell.counter.invalid-result', - moduleKey: 'core.shell', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'core.shell', - payloadSchema: Schema.Void, - policies: [], - resultSchema: Schema.Struct({ total: Schema.Finite }), - schemaVersion: '1', - }, - () => { - const result = { total: 0 }; - Object.defineProperty(result, 'total', { value: 'invalid' }); - return Effect.succeed(result); - }, - ); - const resultError = await runEffectTestPromise( - Effect.flip( + const resultHarness = yield* makeHarness(); + const invalidResult = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, + actionKey: 'shell.counter.invalid-result', + auditProfile: 'standard', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineSystemModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'shell.counter.invalid-result', + moduleKey: 'core.shell', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'core.shell', + payloadSchema: Schema.Void, + policies: [], + resultSchema: Schema.Struct({ total: Schema.Finite }), + schemaVersion: '1', + }, + () => { + const result = { total: 0 }; + Object.defineProperty(result, 'total', { value: 'invalid' }); + return Effect.succeed(result); + }, + ); + const resultError = yield* Effect.flip( resultHarness.runtime.runAction({ payload: undefined, principal, registration: invalidResult, transport: transport(), }), - ), - ); + ); - assert.equal(defect._tag, 'ActionHandlerExecutionError'); - assert.equal(defect.reason.includes('secret'), false); - assert.equal(resultError._tag, 'ActionResultValidationError'); - assert.equal(defectHarness.flushed.length, 0); - assert.equal(resultHarness.flushed.length, 0); -}); + expect(Predicate.isTagged(defect, 'ActionHandlerExecutionError')).toBe(true); -test('sanitizes undeclared handler failures instead of widening the domain error contract', async () => { - const DeclaredDomainErrorContract = Schema.TaggedStruct('DeclaredDomainError', { - reason: Schema.String, - }); - type DeclaredDomainErrorSelf = typeof DeclaredDomainErrorContract.Type; - const DeclaredDomainError = Schema.TaggedError()('DeclaredDomainError', { - reason: Schema.String, - }); - const undeclaredDomainError = new DeclaredDomainError({ - reason: 'secret undeclared failure', - }); - Object.defineProperty(undeclaredDomainError, '_tag', { - value: 'UndeclaredDomainError', - }); - const harness = makeHarness(); - const action = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, - actionKey: 'shell.counter.undeclared-error', - auditProfile: 'standard', - domainErrorSchema: DeclaredDomainError, - domainEvents: {}, - entrypoint: defineSystemModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'shell.counter.undeclared-error', - moduleKey: 'core.shell', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'core.shell', - payloadSchema: Schema.Void, - policies: [], - resultSchema: Schema.Void, - schemaVersion: '1', - }, - () => Effect.fail(undeclaredDomainError), - ); - const error = await runEffectTestPromise( - Effect.flip( + expect(defect.reason.includes('secret')).toBe(false); + expect(Predicate.isTagged(resultError, 'ActionResultValidationError')).toBe(true); + + expect(defectHarness.flushed.length).toBe(0); + expect(resultHarness.flushed.length).toBe(0); + }), +); + +it.effect( + 'sanitizes undeclared handler failures instead of widening the domain error contract', + Effect.fn(function* testProgram33() { + const DeclaredDomainErrorContract = Schema.TaggedStruct('DeclaredDomainError', { + reason: Schema.String, + }); + type DeclaredDomainErrorSelf = typeof DeclaredDomainErrorContract.Type; + const DeclaredDomainError = Schema.TaggedError()( + 'DeclaredDomainError', + { + reason: Schema.String, + }, + ); + const undeclaredDomainError = new DeclaredDomainError({ + reason: 'secret undeclared failure', + }); + Object.defineProperty(undeclaredDomainError, '_tag', { + value: 'UndeclaredDomainError', + }); + const harness = yield* makeHarness(); + const action = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, + actionKey: 'shell.counter.undeclared-error', + auditProfile: 'standard', + domainErrorSchema: DeclaredDomainError, + domainEvents: {}, + entrypoint: defineSystemModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'shell.counter.undeclared-error', + moduleKey: 'core.shell', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'core.shell', + payloadSchema: Schema.Void, + policies: [], + resultSchema: Schema.Void, + schemaVersion: '1', + }, + () => Effect.fail(undeclaredDomainError), + ); + const error = yield* Effect.flip( harness.runtime.runAction({ payload: undefined, principal, registration: action, transport: transport(), }), - ), - ); + ); - assert.equal(error._tag, 'ActionHandlerExecutionError'); - assert.equal(error.reason.includes('secret'), false); - assert.equal(harness.flushed.length, 0); -}); + expect(Predicate.isTagged(error, 'ActionHandlerExecutionError')).toBe(true); -test('handles committed, conflict, definite rollback, and indeterminate commit branches', async () => { - const committed = makeHarness({ - createRecord: { - actionInvocationId: 'committed', - completedAt: null, - requestHash: '', - status: 'succeeded', - }, - }); - const committedError = await runEffectTestPromise( - Effect.flip( + expect(error.reason.includes('secret')).toBe(false); + expect(harness.flushed.length).toBe(0); + }), +); + +it.effect( + 'handles committed, conflict, definite rollback, and indeterminate commit branches', + Effect.fn(function* testProgram34() { + const committed = yield* makeHarness({ + createRecord: { + actionInvocationId: 'committed', + completedAt: null, + requestHash: '', + status: 'succeeded', + }, + }); + const committedError = yield* Effect.flip( committed.runtime.runAction({ payload: { amount: 1 }, principal, registration: registration(), transport: transport(), }), - ), - ); + ); - const conflict = makeHarness({ - createRecord: { - actionInvocationId: 'conflict', - completedAt: null, - requestHash: 'different-request-hash', - status: 'running', - }, - }); - const conflictError = await runEffectTestPromise( - Effect.flip( + const conflict = yield* makeHarness({ + createRecord: { + actionInvocationId: 'conflict', + completedAt: null, + requestHash: 'different-request-hash', + status: 'running', + }, + }); + const conflictError = yield* Effect.flip( conflict.runtime.runAction({ payload: { amount: 1 }, principal, registration: registration(), transport: transport(), }), - ), - ); + ); - const definite = makeHarness({ transactionMode: 'definite-failure' }); - const definiteError = await runEffectTestPromise( - Effect.flip( + const definite = yield* makeHarness({ transactionMode: 'definite-failure' }); + const definiteError = yield* Effect.flip( definite.runtime.runAction({ payload: { amount: 1 }, principal, registration: registration(), transport: transport(), }), - ), - ); + ); - const uncertain = makeHarness({ transactionMode: 'uncertain' }); - const uncertainError = await runEffectTestPromise( - Effect.flip( + const uncertain = yield* makeHarness({ transactionMode: 'uncertain' }); + const uncertainError = yield* Effect.flip( uncertain.runtime.runAction({ payload: { amount: 1 }, principal, registration: registration(), transport: transport(), }), - ), - ); + ); - const definiteCommit = makeHarness({ transactionMode: 'commit-definite' }); - const definiteCommitError = await runEffectTestPromise( - Effect.flip( + const definiteCommit = yield* makeHarness({ transactionMode: 'commit-definite' }); + const definiteCommitError = yield* Effect.flip( definiteCommit.runtime.runAction({ payload: { amount: 1 }, principal, registration: registration(), transport: transport('definite-commit'), }), - ), - ); + ); - const acknowledgementFailureCodes = ['ETIMEDOUT', 'ECONNABORTED', 'ENETRESET', '08007']; - const acknowledgementErrors = await Promise.all( - acknowledgementFailureCodes.map(async (code) => { - const harness = makeHarness({ commitFailureCode: code }); - return await runEffectTestPromise( - Effect.flip( - harness.runtime.runAction({ - payload: { amount: 1 }, - principal, - registration: registration(), - transport: transport(`uncertain-${code}`), - }), - ), - ); - }), - ); + const acknowledgementFailureCodes = ['ETIMEDOUT', 'ECONNABORTED', 'ENETRESET', '08007']; + const acknowledgementErrors = yield* Effect.all( + acknowledgementFailureCodes.map( + Effect.fn(function* testProgram35(code) { + const harness = yield* makeHarness({ commitFailureCode: code }); + return yield* Effect.flip( + harness.runtime.runAction({ + payload: { amount: 1 }, + principal, + registration: registration(), + transport: transport(`uncertain-${code}`), + }), + ); + }), + ), + { concurrency: 'unbounded' }, + ); - assert.equal(committedError._tag, 'ActionAlreadyCommitted'); - assert.equal(committed.counts().transactionCount, 0); - assert.equal(committed.permissionCounts().permissionCheckCount, 0); - assert.equal(conflictError._tag, 'ActionRequestHashConflict'); - assert.equal(conflict.counts().transactionCount, 0); - assert.equal(conflict.permissionCounts().permissionCheckCount, 0); - assert.equal(definiteError._tag, 'ActionTransactionError'); - assert.equal(definiteCommitError._tag, 'ActionTransactionError'); - assert.equal(uncertainError._tag, 'ActionCommitIndeterminate'); - assert.equal(uncertain.flushed.length, 1); - assert.deepEqual( - acknowledgementErrors.map((error) => error._tag), - acknowledgementFailureCodes.map(() => 'ActionCommitIndeterminate'), - ); -}); + expect(Predicate.isTagged(committedError, 'ActionAlreadyCommitted')).toBe(true); -test('interruption during commit waits for native commit settlement', async () => { - const commitStarted = Deferred.makeUnsafe(); - const commitSettlement = Deferred.makeUnsafe(); - const harness = makeHarness({ - commit: Deferred.succeed(commitStarted, null).pipe( - Effect.andThen(Deferred.await(commitSettlement)), - ), - }); - const { exit, pendingBeforeSettlement } = await runEffectTestPromise( - Effect.gen(function* interruptCommittedAction() { - const actionFiber = yield* harness.runtime - .runAction({ - payload: { amount: 1 }, - principal, - registration: registration(), - transport: transport('interrupted-commit'), - }) - .pipe(Effect.forkChild); - yield* Deferred.await(commitStarted); - const interruption = yield* Fiber.interrupt(actionFiber).pipe(Effect.forkChild); - yield* Effect.yieldNow; - const pending = actionFiber.pollUnsafe() === undefined; - yield* Deferred.succeed(commitSettlement, []); - yield* Fiber.join(interruption); - return { exit: yield* Fiber.await(actionFiber), pendingBeforeSettlement: pending }; - }), - ); - assert.equal(pendingBeforeSettlement, true); - assert.ok(Exit.isFailure(exit)); - assert.equal(Cause.hasInterrupts(exit.cause), true); - assert.equal(harness.flushed.length, 1); -}); + expect(committed.counts().transactionCount).toBe(0); + expect(committed.permissionCounts().permissionCheckCount).toBe(0); + expect(Predicate.isTagged(conflictError, 'ActionRequestHashConflict')).toBe(true); -test('resolves commit state explicitly and keeps unavailable outcomes indeterminate', async () => { - const invocationId = '00000000-0000-4000-8000-000000000099'; - const open = makeHarness({ - createRecord: { - actionInvocationId: invocationId, - completedAt: null, - requestHash: 'request', - status: 'running', - }, - }); - const openResolution = await runEffectTestPromise( - open.runtime.resolveActionCommit({ invocationId, principal }), - ); + expect(conflict.counts().transactionCount).toBe(0); + expect(conflict.permissionCounts().permissionCheckCount).toBe(0); + expect(Predicate.isTagged(definiteError, 'ActionTransactionError')).toBe(true); - const committed = makeHarness({ - createRecord: { - actionInvocationId: invocationId, - completedAt: completionTime(), - requestHash: 'request', - status: 'succeeded', - }, - }); - const committedResolution = await runEffectTestPromise( - Effect.flip(committed.runtime.resolveActionCommit({ invocationId, principal })), - ); + expect(Predicate.isTagged(definiteCommitError, 'ActionTransactionError')).toBe(true); - const unavailable = makeHarness({ - createRecord: { - actionInvocationId: invocationId, - completedAt: null, - requestHash: 'request', - status: 'indeterminate', - }, - resolutionUnavailable: true, - }); - const unavailableResolution = await runEffectTestPromise( - Effect.flip(unavailable.runtime.resolveActionCommit({ invocationId, principal })), - ); + expect(Predicate.isTagged(uncertainError, 'ActionCommitIndeterminate')).toBe(true); - assert.deepEqual(openResolution, { - _tag: 'ActionCommitOpen', - invocationId, - }); - assert.equal(committedResolution._tag, 'ActionAlreadyCommitted'); - assert.equal(unavailableResolution._tag, 'ActionCommitIndeterminate'); - assert.equal(unavailableResolution.invocationId, invocationId); -}); + expect(uncertain.flushed.length).toBe(1); + expect(acknowledgementErrors.length).toBe(acknowledgementFailureCodes.length); + for (const error of acknowledgementErrors) { + expect(Predicate.isTagged(error, 'ActionCommitIndeterminate')).toBe(true); + } + }), +); -test('rejects terminal invocation states before handler execution', async () => { - const terminal = makeHarness({ - createRecord: { - actionInvocationId: 'terminal', - completedAt: completionTime(), - requestHash: '', - status: 'failed', - }, - }); - const error = await runEffectTestPromise( - Effect.flip( +it.effect( + 'interruption during commit waits for native commit settlement', + Effect.fn(function* testProgram36() { + const commitStarted = Deferred.makeUnsafe(); + const commitSettlement = Deferred.makeUnsafe(); + const harness = yield* makeHarness({ + commit: Deferred.succeed(commitStarted, null).pipe( + Effect.andThen(Deferred.await(commitSettlement)), + ), + }); + + const actionFiber = yield* harness.runtime + .runAction({ + payload: { amount: 1 }, + principal, + registration: registration(), + transport: transport('interrupted-commit'), + }) + .pipe(Effect.forkChild); + yield* Deferred.await(commitStarted); + const interruption = yield* Fiber.interrupt(actionFiber).pipe(Effect.forkChild); + yield* Effect.yieldNow; + const pending = actionFiber.pollUnsafe() === undefined; + yield* Deferred.succeed(commitSettlement, []); + yield* Fiber.join(interruption); + + const exit = yield* Fiber.await(actionFiber); + const pendingBeforeSettlement = pending; + expect(pendingBeforeSettlement).toBe(true); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) { + throw new Error('Expected typed test outcome'); + } + expect(Cause.hasInterrupts(exit.cause)).toBe(true); + expect(harness.flushed.length).toBe(1); + }), +); + +it.effect( + 'resolves commit state explicitly and keeps unavailable outcomes indeterminate', + Effect.fn(function* testProgram37() { + const invocationId = '00000000-0000-4000-8000-000000000099'; + const open = yield* makeHarness({ + createRecord: { + actionInvocationId: invocationId, + completedAt: null, + requestHash: 'request', + status: 'running', + }, + }); + const openResolution = yield* open.runtime.resolveActionCommit({ invocationId, principal }); + + const committed = yield* makeHarness({ + createRecord: { + actionInvocationId: invocationId, + completedAt: completionTime(), + requestHash: 'request', + status: 'succeeded', + }, + }); + const committedResolution = yield* Effect.flip( + committed.runtime.resolveActionCommit({ invocationId, principal }), + ); + + const unavailable = yield* makeHarness({ + createRecord: { + actionInvocationId: invocationId, + completedAt: null, + requestHash: 'request', + status: 'indeterminate', + }, + resolutionUnavailable: true, + }); + const unavailableResolution = yield* Effect.flip( + unavailable.runtime.resolveActionCommit({ invocationId, principal }), + ); + + expect(Predicate.isTagged(openResolution, 'ActionCommitOpen')).toBe(true); + expect(Struct.omit(openResolution, ['_tag'])).toEqual({ + invocationId, + }); + expect(Predicate.isTagged(committedResolution, 'ActionAlreadyCommitted')).toBe(true); + + expect(Predicate.isTagged(unavailableResolution, 'ActionCommitIndeterminate')).toBe(true); + if (!Predicate.isTagged(unavailableResolution, 'ActionCommitIndeterminate')) { + throw new Error('Expected typed test outcome'); + } + expect(unavailableResolution.invocationId).toBe(invocationId); + }), +); + +it.effect( + 'rejects terminal invocation states before handler execution', + Effect.fn(function* testProgram38() { + const terminal = yield* makeHarness({ + createRecord: { + actionInvocationId: 'terminal', + completedAt: completionTime(), + requestHash: '', + status: 'failed', + }, + }); + const error = yield* Effect.flip( terminal.runtime.runAction({ payload: { amount: 1 }, principal, registration: registration(), transport: transport(), }), - ), - ); + ); - assert.equal(error._tag, 'ActionInvocationStateError'); - assert.equal(terminal.counts().transitionCount, 0); - assert.equal(terminal.counts().transactionCount, 0); -}); + expect(Predicate.isTagged(error, 'ActionInvocationStateError')).toBe(true); -test('uses one runtime contract for Shell/Core and MicroVertical-shaped registrations', async () => { - const shell = makeHarness(); - const microvertical = makeHarness(); - const moduleRegistration = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'stock.read.v1' }, - actionKey: 'inventory.stock.reserve', - auditProfile: 'standard', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineTenantModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'inventory.stock.reserve', - moduleKey: 'inventory.stock', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'inventory.stock', - payloadSchema: Schema.Struct({ quantity: Schema.Finite }), - policies: [], - resultSchema: Schema.Struct({ reserved: Schema.Boolean }), - schemaVersion: '1', - }, - () => Effect.succeed({ reserved: true }), - ); + expect(terminal.counts().transitionCount).toBe(0); + expect(terminal.counts().transactionCount).toBe(0); + }), +); + +it.effect( + 'uses one runtime contract for Shell/Core and MicroVertical-shaped registrations', + Effect.fn(function* testProgram39() { + const shell = yield* makeHarness(); + const microvertical = yield* makeHarness(); + const moduleRegistration = defineAction( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'stock.read.v1' }, + actionKey: 'inventory.stock.reserve', + auditProfile: 'standard', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineTenantModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'inventory.stock.reserve', + moduleKey: 'inventory.stock', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'inventory.stock', + payloadSchema: Schema.Struct({ quantity: Schema.Finite }), + policies: [], + resultSchema: Schema.Struct({ reserved: Schema.Boolean }), + schemaVersion: '1', + }, + () => Effect.succeed({ reserved: true }), + ); - const shellResult = await runEffectTestPromise( - shell.runtime.runAction({ + const shellResult = yield* shell.runtime.runAction({ payload: { amount: 1 }, principal, registration: registration(), transport: transport('shell'), - }), - ); - const moduleResult = await runEffectTestPromise( - microvertical.runtime.runAction({ + }); + const moduleResult = yield* microvertical.runtime.runAction({ payload: { quantity: 2 }, principal, registration: moduleRegistration, @@ -2410,13 +2542,13 @@ test('uses one runtime contract for Shell/Core and MicroVertical-shaped registra ...transport('microvertical'), targetModuleKey: 'inventory.stock', }, - }), - ); + }); - assert.deepEqual(shellResult, { total: 1 }); - assert.deepEqual(moduleResult, { reserved: true }); -}); + expect(shellResult).toEqual({ total: 1 }); + expect(moduleResult).toEqual({ reserved: true }); + }), +); -test('the Core database service identity remains server-only', () => { - assert.equal(Predicate.isFunction(CoreDatabase), true); +it('the Core database service identity remains server-only', () => { + expect(Predicate.isFunction(CoreDatabase)).toBe(true); }); diff --git a/app/packages/core-runtime/tests/unit/action-testing-harness.test.ts b/app/packages/core-runtime/tests/unit/action-testing-harness.test.ts index a97b7e886..86ff33f02 100644 --- a/app/packages/core-runtime/tests/unit/action-testing-harness.test.ts +++ b/app/packages/core-runtime/tests/unit/action-testing-harness.test.ts @@ -1,8 +1,5 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Schema, Predicate } from 'effect'; import { defineAction } from '../../src/actions/definition.ts'; import { defineGlobalPolicy, denyPolicy } from '../../src/actions/policy.ts'; import { ACTION_RUNTIME_STAGES } from '../../src/actions/runtime.ts'; @@ -40,23 +37,22 @@ const lifecycleAction = defineAction( schemaVersion: '1', tenantPermission: () => 'manage_party_identity', }, - (payload, context) => - Effect.gen(function* lifecycleHandler() { - const event = yield* context.addDomainEvent({ - eventType: 'test.counter.incremented.v1', - payloadJson: { amount: payload.amount }, - producerModuleKey: 'test.counter', - subjectModuleKey: 'test.counter', - subjectResourceId: 'primary', - subjectResourceType: 'counter', - }); - yield* context.addOutboxMessage(event, { - payloadJson: { amount: payload.amount }, - producerModuleKey: 'test.counter', - topic: 'test.counter.incremented.v1', - }); - return { total: payload.amount }; - }), + Effect.fn(function* lifecycleHandler(payload, context) { + const event = yield* context.addDomainEvent({ + eventType: 'test.counter.incremented.v1', + payloadJson: { amount: payload.amount }, + producerModuleKey: 'test.counter', + subjectModuleKey: 'test.counter', + subjectResourceId: 'primary', + subjectResourceType: 'counter', + }); + yield* context.addOutboxMessage(event, { + payloadJson: { amount: payload.amount }, + producerModuleKey: 'test.counter', + topic: 'test.counter.incremented.v1', + }); + return { total: payload.amount }; + }), ); const request = { @@ -66,121 +62,139 @@ const request = { transport: { correlationId: 'action-harness-test', idempotencyKey: 'increment-once' }, } as const; -test('runs the real Action lifecycle and preserves committed replay semantics', async () => { - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - tenantPermission: 'allowed', - }); - - assert.deepEqual(await runEffectTestPromise(harness.runtime.runAction(request)), { total: 2 }); - const replay = await runEffectTestPromise(harness.runtime.runAction(request).pipe(Effect.flip)); - const snapshot = harness.snapshot(); - - assert.equal(replay._tag, 'ActionAlreadyCommitted'); - assert.deepEqual(snapshot.stages.slice(0, ACTION_RUNTIME_STAGES.length), ACTION_RUNTIME_STAGES); - assert.equal(snapshot.invocations.length, 1); - assert.equal(snapshot.invocations[0]?.status, 'succeeded'); - assert.equal(snapshot.transactionCount, 1); - assert.equal(snapshot.committed.length, 1); - assert.equal(snapshot.committed[0]?.evidence.domainEvents.length, 1); - assert.equal(snapshot.committed[0]?.evidence.outboxMessages.length, 1); -}); - -test('defaults authorization closed and never starts a transaction for a denial', async () => { - const harness = makeActionTestHarness(); - const denied = await runEffectTestPromise(harness.runtime.runAction(request).pipe(Effect.flip)); - const snapshot = harness.snapshot(); - - assert.equal(denied._tag, 'ActionPermissionDenied'); - assert.equal(snapshot.invocations.length, 1); - assert.equal(snapshot.invocations[0]?.status, 'rejected'); - assert.equal(snapshot.permissionDenials.length, 1); - assert.equal(snapshot.policyDenials.length, 0); - assert.equal(snapshot.invocations[0]?.completedAt?.getTime(), 0); - assert.equal(snapshot.transactionCount, 0); - assert.equal(snapshot.stages.includes('handler_executed'), false); -}); - -test('substitutes typed owner services without replacing the private handler', async () => { - interface CounterServices { - readonly increment: (amount: number) => Effect.Effect; - } - const serviceAction = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'test.service.read.v1' }, - actionKey: 'test.service.increment', - auditProfile: 'minimal', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineTenantModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'test.service.increment', - moduleKey: 'test.service', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'test.service', - payloadSchema: Schema.Struct({ amount: Schema.Finite }), - policies: [], - resultSchema: Schema.Finite, - schemaVersion: '1', - }, - (payload, context) => context.services.increment(payload.amount), - (): Effect.Effect => - Effect.die('production owner services must not run in this test'), - ); - let calls = 0; - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - services: [ - bindActionTestServices(serviceAction, { - increment: (amount) => - Effect.sync(() => { - calls += 1; - return amount + 1; - }), - } satisfies CounterServices), - ], - }); - - const result = await runEffectTestPromise( - harness.runtime.runAction({ +it.effect( + 'runs the real Action lifecycle and preserves committed replay semantics', + Effect.fn(function* testProgram1() { + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'allowed', + }); + + expect(yield* harness.runtime.runAction(request)).toEqual({ total: 2 }); + const replay = yield* harness.runtime.runAction(request).pipe(Effect.flip); + const snapshot = harness.snapshot(); + + expect(Predicate.isTagged(replay, 'ActionAlreadyCommitted')).toBe(true); + + expect(snapshot.stages.slice(0, ACTION_RUNTIME_STAGES.length)).toEqual(ACTION_RUNTIME_STAGES); + expect(snapshot.invocations.length).toBe(1); + expect(snapshot.invocations[0]?.status).toBe('succeeded'); + expect(snapshot.transactionCount).toBe(1); + expect(snapshot.committed.length).toBe(1); + expect(snapshot.committed[0]?.evidence.domainEvents.length).toBe(1); + expect(snapshot.committed[0]?.evidence.outboxMessages.length).toBe(1); + }), +); + +it.effect( + 'defaults authorization closed and never starts a transaction for a denial', + Effect.fn(function* testProgram2() { + const harness = yield* makeActionTestHarness(); + const denied = yield* harness.runtime.runAction(request).pipe(Effect.flip); + const snapshot = harness.snapshot(); + + expect(Predicate.isTagged(denied, 'ActionPermissionDenied')).toBe(true); + + expect(snapshot.invocations.length).toBe(1); + expect(snapshot.invocations[0]?.status).toBe('rejected'); + expect(snapshot.permissionDenials.length).toBe(1); + expect(snapshot.policyDenials.length).toBe(0); + expect(snapshot.invocations[0]?.completedAt?.getTime()).toBe(0); + expect(snapshot.transactionCount).toBe(0); + expect(snapshot.stages.includes('handler_executed')).toBe(false); + }), +); + +it.effect( + 'substitutes typed owner services without replacing the private handler', + Effect.fn(function* testProgram3() { + interface CounterServices { + readonly increment: (amount: number) => Effect.Effect; + } + const serviceAction = defineAction( + { + accessEvidencePolicy: { + captureMode: 'metadata_only', + policyKey: 'test.service.read.v1', + }, + actionKey: 'test.service.increment', + auditProfile: 'minimal', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineTenantModuleEntrypoint({ + access: 'write', + authorization: { + kind: 'action_execution', + provisioning: 'tenant_membership_default', + }, + entrypointKey: 'test.service.increment', + moduleKey: 'test.service', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'test.service', + payloadSchema: Schema.Struct({ amount: Schema.Finite }), + policies: [], + resultSchema: Schema.Finite, + schemaVersion: '1', + }, + (payload, context) => context.services.increment(payload.amount), + (): Effect.Effect => + Effect.die('production owner services must not run in this test'), + ); + let calls = 0; + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + services: [ + bindActionTestServices(serviceAction, { + increment: (amount) => + Effect.sync(() => { + calls += 1; + return amount + 1; + }), + } satisfies CounterServices), + ], + }); + + const result = yield* harness.runtime.runAction({ payload: { amount: 4 }, principal, registration: serviceAction, transport: { correlationId: 'service-test', idempotencyKey: 'service-once' }, - }), - ); - - assert.equal(result, 5); - assert.equal(calls, 1); - assert.equal(harness.snapshot().committed.length, 1); -}); - -test('rejects missing idempotency before creating an invocation', async () => { - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - tenantPermission: 'allowed', - }); - const failure = await runEffectTestPromise( - harness.runtime + }); + + expect(result).toBe(5); + expect(calls).toBe(1); + expect(harness.snapshot().committed.length).toBe(1); + }), +); + +it.effect( + 'rejects missing idempotency before creating an invocation', + Effect.fn(function* testProgram4() { + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'allowed', + }); + const failure = yield* harness.runtime .runAction({ payload: { amount: 2 }, principal, registration: lifecycleAction, transport: { correlationId: 'missing-idempotency' }, }) - .pipe(Effect.flip), - ); + .pipe(Effect.flip); - assert.equal(failure._tag, 'ActionIdempotencyKeyRequired'); - assert.equal(harness.snapshot().invocations.length, 0); -}); + expect(Predicate.isTagged(failure, 'ActionIdempotencyKeyRequired')).toBe(true); -test('persists policy denials separately from permission denials before handler execution', async () => - await runEffectTestPromise( + expect(harness.snapshot().invocations.length).toBe(0); + }), +); + +it.effect( + 'persists policy denials separately from permission denials before handler execution', + () => Effect.gen(function* policyDenialSnapshot() { const registration = defineAction( { @@ -194,22 +208,21 @@ test('persists policy denials separately from permission denials before handler }, () => Effect.die('A denied policy must not execute the handler'), ); - const harness = makeActionTestHarness({ + const harness = yield* makeActionTestHarness({ actionPermission: 'allowed', tenantPermission: 'allowed', }); yield* harness.runtime.runAction({ ...request, registration }).pipe(Effect.flip); const snapshot = harness.snapshot(); - assert.equal(snapshot.policyDenials.length, 1); - assert.equal(snapshot.permissionDenials.length, 0); - assert.equal(snapshot.invocations[0]?.status, 'rejected'); - assert.equal(snapshot.invocations[0]?.completedAt?.getTime(), 0); - assert.equal( - snapshot.policyDenials[0]?.actionInvocationId, + expect(snapshot.policyDenials.length).toBe(1); + expect(snapshot.permissionDenials.length).toBe(0); + expect(snapshot.invocations[0]?.status).toBe('rejected'); + expect(snapshot.invocations[0]?.completedAt?.getTime()).toBe(0); + expect(snapshot.policyDenials[0]?.actionInvocationId).toBe( snapshot.invocations[0]?.actionInvocationId, ); - assert.equal(snapshot.transactionCount, 0); - assert.equal(snapshot.committed.length, 0); - assert.equal(snapshot.stages.includes('handler_executed'), false); + expect(snapshot.transactionCount).toBe(0); + expect(snapshot.committed.length).toBe(0); + expect(snapshot.stages.includes('handler_executed')).toBe(false); }), - )); +); diff --git a/app/packages/core-runtime/tests/unit/application-composition.test.ts b/app/packages/core-runtime/tests/unit/application-composition.test.ts index d2eb410ae..acc4e235d 100644 --- a/app/packages/core-runtime/tests/unit/application-composition.test.ts +++ b/app/packages/core-runtime/tests/unit/application-composition.test.ts @@ -1,7 +1,5 @@ -import { runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Schema, Struct } from 'effect'; import { canonicalizeApplicationComposition, ApplicationCompositionSchema, @@ -11,10 +9,13 @@ import { const sha256 = (character: string) => character.repeat(64); -const assertInvalid = ( +const assertInvalid = Effect.fn(function* testProgram1( effect: Effect.Effect, reason: RegExp, -): void => assert.match(runEffectTestSync(Effect.flip(effect)).reason, reason); +) { + const error = yield* Effect.flip(effect); + expect(error.reason).toMatch(reason); +}); type Candidate = ReturnType; type Evidence = ReturnType; @@ -98,7 +99,10 @@ const evidence = () => ({ }); const required = (value: Value | undefined): Value => { - assert.ok(value !== undefined, 'invalid test fixture'); + expect(value !== undefined, 'invalid test fixture').toBe(true); + if (value === undefined) { + throw new Error('invalid test fixture'); + } return value; }; @@ -107,14 +111,19 @@ const onlyModule = (input: Candidate) => required(input.modules[0]); const federationManifest = (observations: Evidence) => required(observations.federationManifests['https://contacts.example/mf-manifest.json']); -test('defaults the validation error code without changing its encoded contract', () => { - const error = new ApplicationCompositionValidationError({ reason: 'Invalid candidate' }); - assert.deepEqual(Schema.encodeSync(ApplicationCompositionValidationError)(error), { - _tag: 'ApplicationCompositionValidationError', - code: 'application_composition_invalid', - reason: 'Invalid candidate', - }); -}); +it.effect('defaults the validation error code without changing its encoded contract', () => + Effect.gen(function* encodeValidationError() { + const error = new ApplicationCompositionValidationError({ reason: 'Invalid candidate' }); + const encodedError = yield* Schema.encodeEffect(ApplicationCompositionValidationError)(error); + expect(Schema.is(Schema.toEncoded(ApplicationCompositionValidationError))(encodedError)).toBe( + true, + ); + expect(Struct.omit(encodedError, ['_tag'])).toEqual({ + code: 'application_composition_invalid', + reason: 'Invalid candidate', + }); + }), +); const addModuleCopy = ( input: Candidate, @@ -140,232 +149,238 @@ const addModuleCopy = ( }); }; -test('accepts one provider-neutral composition and produces deterministic canonical JSON', () => { - const input = candidate(); - const composition = runEffectTestSync(validateApplicationCompositionCandidate(input, evidence())); +it.effect( + 'accepts one provider-neutral composition and produces deterministic canonical JSON', + Effect.fn(function* testProgram2() { + const input = candidate(); + const composition = yield* validateApplicationCompositionCandidate(input, evidence()); - assert.deepEqual(composition, input); - assert.equal(Object.isFrozen(composition), true); - assert.equal(Object.isFrozen(required(composition.modules[0]).federation.exposes), true); - assert.equal(Object.isFrozen(input), false); - assertInvalid( - validateApplicationCompositionCandidate({ ...input, provider: 'zephyr' }, evidence()), - /supported .* schema/u, - ); + expect(composition).toEqual(input); + expect(Object.isFrozen(composition)).toBe(true); + expect(Object.isFrozen(required(composition.modules[0]).federation.exposes)).toBe(true); + expect(Object.isFrozen(input)).toBe(false); + yield* assertInvalid( + validateApplicationCompositionCandidate({ ...input, provider: 'zephyr' }, evidence()), + /supported .* schema/u, + ); - const reordered = structuredClone(composition); - const reorderedModule = required(reordered.modules[0]); - reorderedModule.allowedContributions.reverse(); - reorderedModule.federation.exposes.reverse(); - reorderedModule.requiredCoreCapabilities.reverse(); - reorderedModule.sharedSingletons.reverse(); - reordered.shell.coreCapabilities.reverse(); - reordered.shell.sharedSingletons.reverse(); - /* oxlint-disable perfectionist/sort-objects -- Deliberately reorder nested fields to test canonical encoding. expires: 2026-12-31. */ - reordered.shell.coreCapabilities = reordered.shell.coreCapabilities.map(({ id, version }) => ({ - version, - id, - })); - reorderedModule.sharedSingletons = reorderedModule.sharedSingletons.map( - ({ packageName, version }) => ({ version, packageName }), - ); - reorderedModule.contract = { - url: reorderedModule.contract.url, - sha256: reorderedModule.contract.sha256, - }; - /* oxlint-enable perfectionist/sort-objects */ - assert.equal( - canonicalizeApplicationComposition(reordered), - canonicalizeApplicationComposition(composition), - ); - assert.deepEqual( - Schema.decodeSync(Schema.fromJsonString(ApplicationCompositionSchema))( - canonicalizeApplicationComposition(composition), - ), - composition, - ); -}); + const reordered = structuredClone(input); + const reorderedModule = required(reordered.modules[0]); + reorderedModule.allowedContributions.reverse(); + reorderedModule.federation.exposes.reverse(); + reorderedModule.requiredCoreCapabilities.reverse(); + reorderedModule.sharedSingletons.reverse(); + reordered.shell.coreCapabilities.reverse(); + reordered.shell.sharedSingletons.reverse(); + /* oxlint-disable perfectionist/sort-objects -- Deliberately reorder nested fields to test canonical encoding. expires: 2026-12-31. */ + reordered.shell.coreCapabilities = reordered.shell.coreCapabilities.map(({ id, version }) => ({ + version, + id, + })); + reorderedModule.sharedSingletons = reorderedModule.sharedSingletons.map( + ({ packageName, version }) => ({ version, packageName }), + ); + reorderedModule.contract = { + url: reorderedModule.contract.url, + sha256: reorderedModule.contract.sha256, + }; + /* oxlint-enable perfectionist/sort-objects */ + expect( + canonicalizeApplicationComposition( + yield* validateApplicationCompositionCandidate(reordered, evidence()), + ), + ).toBe(canonicalizeApplicationComposition(composition)); + expect( + yield* Schema.decodeEffect(Schema.fromJsonString(ApplicationCompositionSchema))( + canonicalizeApplicationComposition(composition), + ), + ).toEqual(composition); + }), +); -test('allows loopback HTTP only with trusted development evidence', () => { - for (const host of ['localhost', '127.0.0.1', '[::1]', 'contacts.localhost']) { - for (const artifact of ['contract', 'federation']) { - const input = candidate(); - const observations = evidence(); - const module = onlyModule(input); - if (artifact === 'contract') { - module.contract.url = `http://${host}/ontos-module-manifest.json`; - observations.contracts.contacts.contractUrl = module.contract.url; - } else { - module.federation.manifest.url = `http://${host}/mf-manifest.json`; - } - const observed = { - ...observations, - federationManifests: { - [module.federation.manifest.url]: federationManifest(observations), - }, - }; - for (const environment of [{}, { environment: 'stage' }, { environment: 'production' }]) { - assertInvalid( - validateApplicationCompositionCandidate(input, { ...observed, ...environment }), - /HTTPS outside development/u, - ); - } - assert.deepEqual( - runEffectTestSync( - validateApplicationCompositionCandidate(input, { +it.effect( + 'allows loopback HTTP only with trusted development evidence', + Effect.fn(function* testProgram3() { + for (const host of ['localhost', '127.0.0.1', '[::1]', 'contacts.localhost']) { + for (const artifact of ['contract', 'federation']) { + const input = candidate(); + const observations = evidence(); + const module = onlyModule(input); + if (artifact === 'contract') { + module.contract.url = `http://${host}/ontos-module-manifest.json`; + observations.contracts.contacts.contractUrl = module.contract.url; + } else { + module.federation.manifest.url = `http://${host}/mf-manifest.json`; + } + const observed = { + ...observations, + federationManifests: { + [module.federation.manifest.url]: federationManifest(observations), + }, + }; + for (const environment of [{}, { environment: 'stage' }, { environment: 'production' }]) { + yield* assertInvalid( + validateApplicationCompositionCandidate(input, { ...observed, ...environment }), + /HTTPS outside development/u, + ); + } + expect( + yield* validateApplicationCompositionCandidate(input, { ...observed, environment: 'development', }), - ), - input, - ); + ).toEqual(input); + } } - } - const input = candidate(); - onlyModule(input).contract.url = 'http://contacts.example/manifest.json'; - assertInvalid( - validateApplicationCompositionCandidate(input, { ...evidence(), environment: 'development' }), - /supported .* schema/u, - ); -}); + const input = candidate(); + onlyModule(input).contract.url = 'http://contacts.example/manifest.json'; + yield* assertInvalid( + validateApplicationCompositionCandidate(input, { ...evidence(), environment: 'development' }), + /supported .* schema/u, + ); + }), +); -test('rejects candidate-wide ownership and compatibility contradictions', () => { - const cases: readonly [ - mutate: (input: Candidate, observations: Evidence) => number | readonly string[] | string, - reason: RegExp, - ][] = [ - [ - (input) => (onlyModule(input).federation.remoteName = 'anotherRemote'), - /observed deployment contract/u, - ], - [ - (_input, observations) => (observations.contracts.contacts.mfBoundaryId = 'anotherRemote'), - /observed deployment contract/u, - ], - [ - (_input, observations) => (federationManifest(observations).remoteName = 'anotherRemote'), - /Module Federation manifest/u, - ], - [ - (_input, observations) => (observations.contracts.contacts.contractUrl = 'invalid-url'), - /observation schema/u, - ], - [(input) => onlyModule(input).dependencies.push('billing.core'), /dependency billing\.core/u], - [(input) => onlyModule(input).dependencies.push('contacts.core'), /dependency cycle/u], - [ - (input) => onlyModule(input).dependencies.push('contacts.core', 'contacts.core'), - /duplicate dependency/u, - ], - [ - (input) => addModuleCopy(input, { moduleId: 'inventory.stock' }), - /duplicate Shell contribution/u, - ], - [ - (input) => addModuleCopy(input, { allowedContributions: [] }), - /duplicate module ID contacts\.core/u, - ], - [ - (input) => (onlyModule(input).allowedContributions = ['contacts.core.page.contacts']), - /observed deployment contract/u, - ], - [ - (input) => (onlyModule(input).federation.exposes = ['./Navigation']), - /observed deployment contract/u, - ], - [ - (input) => { - const module = onlyModule(input); - return addModuleCopy(input, { - allowedContributions: [], - contract: { - ...module.contract, - url: 'https://contacts.example:443/.well-known/ontos-module-manifest.json', - }, - moduleId: 'inventory.stock', - publicContract: { ...module.publicContract, id: 'inventory.stock' }, - }); - }, - /duplicate artifact URL/u, - ], - [ - (input) => { - const module = onlyModule(input); - return addModuleCopy(input, { - allowedContributions: [], - contract: { - ...module.contract, - url: 'https://inventory.example/.well-known/ontos-module-manifest.json', - }, - federation: { - ...module.federation, - manifest: { - ...module.federation.manifest, - url: 'https://contacts.example/artifacts/../mf-manifest.json', +it.effect( + 'rejects candidate-wide ownership and compatibility contradictions', + Effect.fn(function* testProgram4() { + const cases: readonly [ + mutate: (input: Candidate, observations: Evidence) => number | readonly string[] | string, + reason: RegExp, + ][] = [ + [ + (input) => (onlyModule(input).federation.remoteName = 'anotherRemote'), + /observed deployment contract/u, + ], + [ + (_input, observations) => (observations.contracts.contacts.mfBoundaryId = 'anotherRemote'), + /observed deployment contract/u, + ], + [ + (_input, observations) => (federationManifest(observations).remoteName = 'anotherRemote'), + /Module Federation manifest/u, + ], + [ + (_input, observations) => (observations.contracts.contacts.contractUrl = 'invalid-url'), + /observation schema/u, + ], + [(input) => onlyModule(input).dependencies.push('billing.core'), /dependency billing\.core/u], + [(input) => onlyModule(input).dependencies.push('contacts.core'), /dependency cycle/u], + [ + (input) => onlyModule(input).dependencies.push('contacts.core', 'contacts.core'), + /duplicate dependency/u, + ], + [ + (input) => addModuleCopy(input, { moduleId: 'inventory.stock' }), + /duplicate Shell contribution/u, + ], + [ + (input) => addModuleCopy(input, { allowedContributions: [] }), + /duplicate module ID contacts\.core/u, + ], + [ + (input) => (onlyModule(input).allowedContributions = ['contacts.core.page.contacts']), + /observed deployment contract/u, + ], + [ + (input) => (onlyModule(input).federation.exposes = ['./Navigation']), + /observed deployment contract/u, + ], + [ + (input) => { + const module = onlyModule(input); + return addModuleCopy(input, { + allowedContributions: [], + contract: { + ...module.contract, + url: 'https://contacts.example:443/.well-known/ontos-module-manifest.json', }, - remoteName: 'inventory', - }, - moduleId: 'inventory.stock', - publicContract: { ...module.publicContract, id: 'inventory.stock' }, - }); - }, - /duplicate artifact URL/u, - ], - [ - (input) => { - onlyModule(input).requiredShellAbi.version = '2'; - input.shell.contributionAbi.version = '2'; - return input.shell.contributionAbi.version; - }, - /observed runtime contract/u, - ], - [ - (input, observations) => { - const moduleSingleton = required(onlyModule(input).sharedSingletons[0]); - const runtimeSingleton = required(observations.runtime.sharedSingletons[0]); - const shellSingleton = required(input.shell.sharedSingletons[0]); - shellSingleton.packageName = 'foo'; - shellSingleton.version = 'bar@baz'; - runtimeSingleton.packageName = 'foo'; - runtimeSingleton.version = 'bar@baz'; - moduleSingleton.packageName = 'foo@bar'; - moduleSingleton.version = 'baz'; - return moduleSingleton.version; - }, - /incompatible shared singleton foo@bar/u, - ], - [(input) => (onlyModule(input).requiredShellAbi.version = '2'), /Shell contribution ABI/u], - [ - (input) => (required(onlyModule(input).requiredCoreCapabilities[0]).version = '2'), - /Core capability core\.authorization/u, - ], - [ - (input) => input.shell.sharedSingletons.push({ packageName: 'react', version: '18.3.1' }), - /shared singleton react/u, - ], - [(input) => (onlyModule(input).federation.execution = 'server'), /supported .* schema/u], - [ - (input) => - (onlyModule(input).contract.url = 'https://contacts.example/manifest.json?tag=live'), - /supported .* schema/u, - ], - [ - (_input, observations) => (federationManifest(observations).exposes = []), - /Module Federation manifest/u, - ], - [ - (_input, observations) => { - const singleton = required(federationManifest(observations).sharedSingletons[0]); - singleton.version = '18.3.1'; - return singleton.version; - }, - /Module Federation manifest/u, - ], - ]; + moduleId: 'inventory.stock', + publicContract: { ...module.publicContract, id: 'inventory.stock' }, + }); + }, + /duplicate artifact URL/u, + ], + [ + (input) => { + const module = onlyModule(input); + return addModuleCopy(input, { + allowedContributions: [], + contract: { + ...module.contract, + url: 'https://inventory.example/.well-known/ontos-module-manifest.json', + }, + federation: { + ...module.federation, + manifest: { + ...module.federation.manifest, + url: 'https://contacts.example/artifacts/../mf-manifest.json', + }, + remoteName: 'inventory', + }, + moduleId: 'inventory.stock', + publicContract: { ...module.publicContract, id: 'inventory.stock' }, + }); + }, + /duplicate artifact URL/u, + ], + [ + (input) => { + onlyModule(input).requiredShellAbi.version = '2'; + input.shell.contributionAbi.version = '2'; + return input.shell.contributionAbi.version; + }, + /observed runtime contract/u, + ], + [ + (input, observations) => { + const moduleSingleton = required(onlyModule(input).sharedSingletons[0]); + const runtimeSingleton = required(observations.runtime.sharedSingletons[0]); + const shellSingleton = required(input.shell.sharedSingletons[0]); + shellSingleton.packageName = 'foo'; + shellSingleton.version = 'bar@baz'; + runtimeSingleton.packageName = 'foo'; + runtimeSingleton.version = 'bar@baz'; + moduleSingleton.packageName = 'foo@bar'; + moduleSingleton.version = 'baz'; + return moduleSingleton.version; + }, + /incompatible shared singleton foo@bar/u, + ], + [(input) => (onlyModule(input).requiredShellAbi.version = '2'), /Shell contribution ABI/u], + [ + (input) => (required(onlyModule(input).requiredCoreCapabilities[0]).version = '2'), + /Core capability core\.authorization/u, + ], + [ + (input) => input.shell.sharedSingletons.push({ packageName: 'react', version: '18.3.1' }), + /shared singleton react/u, + ], + [(input) => (onlyModule(input).federation.execution = 'server'), /supported .* schema/u], + [ + (input) => + (onlyModule(input).contract.url = 'https://contacts.example/manifest.json?tag=live'), + /supported .* schema/u, + ], + [ + (_input, observations) => (federationManifest(observations).exposes = []), + /Module Federation manifest/u, + ], + [ + (_input, observations) => { + const singleton = required(federationManifest(observations).sharedSingletons[0]); + singleton.version = '18.3.1'; + return singleton.version; + }, + /Module Federation manifest/u, + ], + ]; - for (const [mutate, reason] of cases) { - const input = candidate(); - const observations = evidence(); - mutate(input, observations); - assertInvalid(validateApplicationCompositionCandidate(input, observations), reason); - } -}); + for (const [mutate, reason] of cases) { + const input = candidate(); + const observations = evidence(); + mutate(input, observations); + yield* assertInvalid(validateApplicationCompositionCandidate(input, observations), reason); + } + }), +); diff --git a/app/packages/core-runtime/tests/unit/catalog-contract.test.ts b/app/packages/core-runtime/tests/unit/catalog-contract.test.ts index 2659f88dd..adf9786d1 100644 --- a/app/packages/core-runtime/tests/unit/catalog-contract.test.ts +++ b/app/packages/core-runtime/tests/unit/catalog-contract.test.ts @@ -1,12 +1,11 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { compareApplicationCatalog, expectedCoreTableCatalog } from '../../src/db/catalog.ts'; import type { CatalogEntry } from '../../src/db/catalog.ts'; const exactCatalog = expectedCoreTableCatalog.map((qualifiedName) => { const [schemaName, tableName] = qualifiedName.split('.'); - assert.equal((schemaName?.length ?? 0) > 0, true); - assert.equal((tableName?.length ?? 0) > 0, true); + expect((schemaName?.length ?? 0) > 0).toBe(true); + expect((tableName?.length ?? 0) > 0).toBe(true); if (schemaName === undefined || tableName === undefined) { throw new TypeError('Core catalog entries must be schema-qualified'); } @@ -18,14 +17,14 @@ const exactCatalog = expectedCoreTableCatalog.map((qualifiedName) }; }); -void test('reports one missing expected Core table', () => { +it('reports one missing expected Core table', () => { const difference = compareApplicationCatalog(exactCatalog.slice(1)); - assert.deepEqual(difference.missing, [expectedCoreTableCatalog[0]]); - assert.deepEqual(difference.unexpected, []); + expect(difference.missing).toEqual([expectedCoreTableCatalog[0]]); + expect(difference.unexpected).toEqual([]); }); -void test('reports unexpected application tables and schemas', () => { +it('reports unexpected application tables and schemas', () => { const difference = compareApplicationCatalog([ ...exactCatalog, { @@ -40,6 +39,6 @@ void test('reports unexpected application tables and schemas', () => { }, ]); - assert.deepEqual(difference.missing, []); - assert.deepEqual(difference.unexpected, ['auth.*', 'public.unexpected_table']); + expect(difference.missing).toEqual([]); + expect(difference.unexpected).toEqual(['auth.*', 'public.unexpected_table']); }); diff --git a/app/packages/core-runtime/tests/unit/commit-recovery-metadata.test.ts b/app/packages/core-runtime/tests/unit/commit-recovery-metadata.test.ts index a4a6b4929..b2b347905 100644 --- a/app/packages/core-runtime/tests/unit/commit-recovery-metadata.test.ts +++ b/app/packages/core-runtime/tests/unit/commit-recovery-metadata.test.ts @@ -1,9 +1,8 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -/* oxlint-disable sonarjs/no-undefined-assignment, typescript/strict-boolean-expressions -- Existing compatibility boundary; expires: 2026-12-31. */ -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; + +/* oxlint-disable sonarjs/no-undefined-assignment -- Existing compatibility boundary; expires: 2026-12-31. */ + +import { Effect, Schema, Predicate, Struct } from 'effect'; import { defineAction } from '../../src/actions/definition.ts'; import { ActionAlreadyCommitted } from '../../src/actions/errors.ts'; import { defineTenantModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; @@ -17,161 +16,175 @@ const principal = { tenantId: '30000000-0000-4000-8000-000000000001', } as const; -test('committed retry and explicit recovery return the same invocation without rerunning or replaying the result', async () => { - let executions = 0; - const registration = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'test.recovery.read.v1' }, - actionKey: 'test.recovery.execute', - auditProfile: 'minimal', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineTenantModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'test.recovery.execute', - moduleKey: 'test.recovery', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'test.recovery', - payloadSchema: Schema.Struct({ amount: Schema.Finite }), - policies: [], - resultSchema: Schema.Struct({ total: Schema.Finite }), - schemaVersion: '1', - }, - (payload) => - Effect.sync(() => { - executions += 1; - return { total: payload.amount * executions }; - }), - ); - const harness = makeActionTestHarness({ actionPermission: 'allowed' }); - const request = { - payload: { amount: 2 }, - principal, - registration, - transport: { correlationId: 'commit-recovery-test', idempotencyKey: 'commit-once' }, - } as const; +it.effect( + 'committed retry and explicit recovery return the same invocation without rerunning or replaying the result', + () => + Effect.gen(function* migratedTest() { + let executions = 0; + const registration = defineAction( + { + accessEvidencePolicy: { + captureMode: 'metadata_only', + policyKey: 'test.recovery.read.v1', + }, + actionKey: 'test.recovery.execute', + auditProfile: 'minimal', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineTenantModuleEntrypoint({ + access: 'write', + authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, + entrypointKey: 'test.recovery.execute', + moduleKey: 'test.recovery', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'test.recovery', + payloadSchema: Schema.Struct({ amount: Schema.Finite }), + policies: [], + resultSchema: Schema.Struct({ total: Schema.Finite }), + schemaVersion: '1', + }, + (payload) => + Effect.sync(() => { + executions += 1; + return { total: payload.amount * executions }; + }), + ); + const harness = yield* makeActionTestHarness({ actionPermission: 'allowed' }); + const request = { + payload: { amount: 2 }, + principal, + registration, + transport: { correlationId: 'commit-recovery-test', idempotencyKey: 'commit-once' }, + } as const; - assert.deepEqual(await runEffectTestPromise(harness.runtime.runAction(request)), { total: 2 }); - const invocationId = harness.snapshot().invocations[0]?.actionInvocationId; - assert.ok(invocationId); - const replay = await runEffectTestPromise(harness.runtime.runAction(request).pipe(Effect.flip)); - const recovered = await runEffectTestPromise( - harness.runtime.resolveActionCommit({ invocationId, principal }).pipe(Effect.flip), - ); + expect(yield* harness.runtime.runAction(request)).toEqual({ total: 2 }); + const invocationId = harness.snapshot().invocations[0]?.actionInvocationId; + expect(invocationId).toBeDefined(); + if (invocationId === undefined) { + throw new Error('Missing invocationId'); + } + const replay = yield* harness.runtime.runAction(request).pipe(Effect.flip); + const recovered = yield* harness.runtime + .resolveActionCommit({ invocationId, principal }) + .pipe(Effect.flip); - for (const outcome of [replay, recovered]) { - assert.equal(outcome._tag, 'ActionAlreadyCommitted'); - assert.equal('invocationId' in outcome ? outcome.invocationId : undefined, invocationId); - assert.equal('total' in outcome, false); - assert.equal('result' in outcome, false); - } - assert.equal(executions, 1); - assert.equal(harness.snapshot().committed.length, 1); - assert.equal(harness.snapshot().transactionCount, 1); -}); + for (const outcome of [replay, recovered]) { + expect(Predicate.isTagged(outcome, 'ActionAlreadyCommitted')).toBe(true); + expect('invocationId' in outcome ? outcome.invocationId : undefined).toBe(invocationId); + expect('total' in outcome).toBe(false); + expect('result' in outcome).toBe(false); + } + expect(executions).toBe(1); + expect(harness.snapshot().committed.length).toBe(1); + expect(harness.snapshot().transactionCount).toBe(1); + }), +); -test('committed error schema requires and preserves the recovery invocation identifier', async () => { - const encoded = { - _tag: 'ActionAlreadyCommitted', - code: 'action_already_committed', - invocationId: '40000000-0000-4000-8000-000000000001', - reason: 'This idempotency key already committed successfully', - } as const; - const decoded = await runEffectTestPromise( - Schema.decodeUnknownEffect(ActionAlreadyCommitted)(encoded), - ); - assert.deepEqual( - await runEffectTestPromise(decoded.pipe(Schema.encodeEffect(ActionAlreadyCommitted))), - encoded, - ); - assert.equal( - Schema.is(ActionAlreadyCommitted)({ +it.effect('committed error schema requires and preserves the recovery invocation identifier', () => + Effect.gen(function* migratedTest() { + const encoded = { _tag: 'ActionAlreadyCommitted', code: 'action_already_committed', + invocationId: '40000000-0000-4000-8000-000000000001', reason: 'This idempotency key already committed successfully', - }), - false, - ); - assert.equal('result' in decoded, false); - assert.equal('status' in decoded, false); -}); - -test('lost commit acknowledgement recovers the committed invocation and faults only once', async () => { - let executions = 0; - const registration = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'test.recovery.read.v1' }, - actionKey: 'test.recovery.acknowledgement', - auditProfile: 'minimal', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineTenantModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'test.recovery.acknowledgement', - moduleKey: 'test.recovery', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'test.recovery', - payloadSchema: Schema.Void, - policies: [], - resultSchema: Schema.Finite, - schemaVersion: '1', - }, - () => - Effect.sync(() => { - executions += 1; - return executions; + } as const; + const decoded = yield* Schema.decodeUnknownEffect(ActionAlreadyCommitted)(encoded); + const reencoded = yield* decoded.pipe(Schema.encodeEffect(ActionAlreadyCommitted)); + expect(Schema.is(Schema.toEncoded(ActionAlreadyCommitted))(reencoded)).toBe(true); + expect(Struct.omit(reencoded, ['_tag'])).toEqual(Struct.omit(encoded, ['_tag'])); + expect( + Schema.is(ActionAlreadyCommitted)({ + _tag: 'ActionAlreadyCommitted', + code: 'action_already_committed', + reason: 'This idempotency key already committed successfully', }), - ); - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - commitAcknowledgement: 'indeterminate-once', - }); - const request = { - payload: undefined, - principal, - registration, - transport: { correlationId: 'lost-acknowledgement', idempotencyKey: 'commit-once' }, - } as const; + ).toBe(false); + expect('result' in decoded).toBe(false); + expect('status' in decoded).toBe(false); + }), +); - const uncertain = await runEffectTestPromise( - harness.runtime.runAction(request).pipe(Effect.flip), - ); - assert.equal(uncertain._tag, 'ActionCommitIndeterminate'); - assert.ok('invocationId' in uncertain); - assert.equal(uncertain.invocationId, harness.snapshot().invocations[0]?.actionInvocationId); - assert.equal(harness.snapshot().invocations[0]?.status, 'succeeded'); +it.effect( + 'lost commit acknowledgement recovers the committed invocation and faults only once', + () => + Effect.gen(function* migratedTest() { + let executions = 0; + const registration = defineAction( + { + accessEvidencePolicy: { + captureMode: 'metadata_only', + policyKey: 'test.recovery.read.v1', + }, + actionKey: 'test.recovery.acknowledgement', + auditProfile: 'minimal', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineTenantModuleEntrypoint({ + access: 'write', + authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, + entrypointKey: 'test.recovery.acknowledgement', + moduleKey: 'test.recovery', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'test.recovery', + payloadSchema: Schema.Void, + policies: [], + resultSchema: Schema.Finite, + schemaVersion: '1', + }, + () => + Effect.sync(() => { + executions += 1; + return executions; + }), + ); + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + commitAcknowledgement: 'indeterminate-once', + }); + const request = { + payload: undefined, + principal, + registration, + transport: { correlationId: 'lost-acknowledgement', idempotencyKey: 'commit-once' }, + } as const; - const recovered = await runEffectTestPromise( - harness.runtime - .resolveActionCommit({ invocationId: uncertain.invocationId, principal }) - .pipe(Effect.flip), - ); - const replay = await runEffectTestPromise(harness.runtime.runAction(request).pipe(Effect.flip)); - for (const outcome of [recovered, replay]) { - assert.equal(outcome._tag, 'ActionAlreadyCommitted'); - assert.ok('invocationId' in outcome); - assert.equal(outcome.invocationId, uncertain.invocationId); - } - assert.equal(executions, 1); - assert.equal(harness.snapshot().committed.length, 1); - assert.equal(harness.snapshot().transactionCount, 1); + const uncertain = yield* harness.runtime.runAction(request).pipe(Effect.flip); + expect(Predicate.isTagged(uncertain, 'ActionCommitIndeterminate')).toBe(true); + expect('invocationId' in uncertain).toBe(true); + if (!('invocationId' in uncertain)) { + throw new Error('Missing invocation identifier'); + } + expect(uncertain.invocationId).toBe(harness.snapshot().invocations[0]?.actionInvocationId); + expect(harness.snapshot().invocations[0]?.status).toBe('succeeded'); - assert.equal( - await runEffectTestPromise( - harness.runtime.runAction({ - ...request, - transport: { correlationId: 'acknowledged-next', idempotencyKey: 'next-invocation' }, - }), - ), - 2, - ); - assert.equal(harness.snapshot().committed.length, 2); -}); + const recovered = yield* harness.runtime + .resolveActionCommit({ invocationId: uncertain.invocationId, principal }) + .pipe(Effect.flip); + const replay = yield* harness.runtime.runAction(request).pipe(Effect.flip); + for (const outcome of [recovered, replay]) { + expect(Predicate.isTagged(outcome, 'ActionAlreadyCommitted')).toBe(true); + expect('invocationId' in outcome).toBe(true); + if (!('invocationId' in outcome)) { + throw new Error('Missing invocation identifier'); + } + expect(outcome.invocationId).toBe(uncertain.invocationId); + } + expect(executions).toBe(1); + expect(harness.snapshot().committed.length).toBe(1); + expect(harness.snapshot().transactionCount).toBe(1); + + expect( + yield* harness.runtime.runAction({ + ...request, + transport: { correlationId: 'acknowledged-next', idempotencyKey: 'next-invocation' }, + }), + ).toBe(2); + expect(harness.snapshot().committed.length).toBe(2); + }), +); diff --git a/app/packages/core-runtime/tests/unit/config.test.ts b/app/packages/core-runtime/tests/unit/config.test.ts index 6dd59706c..0465cd1ef 100644 --- a/app/packages/core-runtime/tests/unit/config.test.ts +++ b/app/packages/core-runtime/tests/unit/config.test.ts @@ -1,8 +1,6 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import test from 'node:test'; -import { Effect } from 'effect'; +import { expect, it } from 'effect-rstest'; + +import { Effect, Predicate } from 'effect'; import { acquirePoolResource } from '../../src/db/client.ts'; import { ROOT_ENV_PATH, @@ -11,126 +9,117 @@ import { parseDatabaseConnectionPair, } from '../../src/db/config.ts'; -void test('loads the root environment independently of the invocation directory', async () => { - const originalDirectory = process.cwd(); - const rootExamplePath = ROOT_ENV_PATH.replace(/\.env$/u, '.env.example'); +it.effect('loads the root environment independently of the invocation directory', () => + Effect.gen(function* migratedTest() { + const originalDirectory = process.cwd(); + const rootExamplePath = ROOT_ENV_PATH.replace(/\.env$/u, '.env.example'); - try { - process.chdir('/'); - const configuration = await runEffectTestPromise( - loadDatabaseConfig({ - environment: {}, - envPath: rootExamplePath, + yield* Effect.addFinalizer(() => + Effect.sync(() => { + process.chdir(originalDirectory); }), ); + process.chdir('/'); + const configuration = yield* loadDatabaseConfig({ + environment: {}, + envPath: rootExamplePath, + }); - assert.equal(ROOT_ENV_PATH.endsWith('/app/.env'), true); - assert.equal( - configuration.connectionString, + expect(ROOT_ENV_PATH.endsWith('/app/.env')).toBe(true); + expect(configuration.connectionString).toBe( 'postgresql://ontos_runtime:ontos_runtime@localhost:5433/ontos', ); - } finally { - process.chdir(originalDirectory); - } -}); + }), +); -void test('parses valid local PostgreSQL connection settings', async () => { - const configuration = await runEffectTestPromise( - parseDatabaseConfig({ +it.effect('parses valid local PostgreSQL connection settings', () => + Effect.gen(function* migratedTest() { + const configuration = yield* parseDatabaseConfig({ DATABASE_URL: 'postgresql://ontos:ontos@localhost:5433/ontos', - }), - ); + }); - assert.deepEqual(configuration, { - connectionString: 'postgresql://ontos:ontos@localhost:5433/ontos', - database: 'ontos', - host: 'localhost', - port: 5433, - user: 'ontos', - }); -}); + expect(configuration).toEqual({ + connectionString: 'postgresql://ontos:ontos@localhost:5433/ontos', + database: 'ontos', + host: 'localhost', + port: 5433, + user: 'ontos', + }); + }), +); -void test('keeps missing and malformed configuration in the typed error channel', async () => { - const missing = await runEffectTestPromise(Effect.flip(parseDatabaseConfig({}))); - const malformed = await runEffectTestPromise( - Effect.flip( +it.effect('keeps missing and malformed configuration in the typed error channel', () => + Effect.gen(function* migratedTest() { + const missing = yield* Effect.flip(parseDatabaseConfig({})); + const malformed = yield* Effect.flip( parseDatabaseConfig({ DATABASE_URL: 'https://localhost/not-postgres', }), - ), - ); + ); - assert.equal(missing._tag, 'DatabaseConfigError'); - assert.equal(malformed._tag, 'DatabaseConfigError'); -}); + expect(Predicate.isTagged(missing, 'DatabaseConfigError')).toBe(true); + expect(Predicate.isTagged(malformed, 'DatabaseConfigError')).toBe(true); + }), +); -void test('requires distinct administrative and least-privilege runtime identities', async () => { - const valid = await runEffectTestPromise( - parseDatabaseConnectionPair({ +it.effect('requires distinct administrative and least-privilege runtime identities', () => + Effect.gen(function* migratedTest() { + const valid = yield* parseDatabaseConnectionPair({ DATABASE_ADMIN_URL: 'postgresql://ontos_admin:admin@localhost:5433/ontos', DATABASE_URL: 'postgresql://ontos_runtime:runtime@localhost:5433/ontos', - }), - ); - const missing = await runEffectTestPromise( - Effect.flip( + }); + const missing = yield* Effect.flip( parseDatabaseConnectionPair({ DATABASE_URL: 'postgresql://ontos_runtime:runtime@localhost:5433/ontos', }), - ), - ); - const identical = await runEffectTestPromise( - Effect.flip( + ); + const identical = yield* Effect.flip( parseDatabaseConnectionPair({ DATABASE_ADMIN_URL: 'postgresql://ontos:secret@localhost:5433/ontos', DATABASE_URL: 'postgresql://ontos:secret@localhost:5433/ontos', }), - ), - ); - const superuserCompatible = await runEffectTestPromise( - Effect.flip( + ); + const superuserCompatible = yield* Effect.flip( parseDatabaseConnectionPair({ DATABASE_ADMIN_URL: 'postgresql://ontos_admin:admin@localhost:5433/ontos', DATABASE_URL: 'postgresql://postgres:secret@localhost:5433/ontos', }), - ), - ); - const queryParameterIdentities = await runEffectTestPromise( - parseDatabaseConnectionPair({ + ); + const queryParameterIdentities = yield* parseDatabaseConnectionPair({ DATABASE_ADMIN_URL: 'postgresql://connection-proxy@localhost:5433/ontos?user=ontos_admin', DATABASE_URL: 'postgresql://connection-proxy@localhost:5433/ontos?user=ontos_runtime', - }), - ); - const queryParameterCollision = await runEffectTestPromise( - Effect.flip( + }); + const queryParameterCollision = yield* Effect.flip( parseDatabaseConnectionPair({ DATABASE_ADMIN_URL: 'postgresql://admin-authority@localhost:5433/ontos?user=effective_role', DATABASE_URL: 'postgresql://runtime-authority@localhost:5433/ontos?user=effective_role', }), - ), - ); + ); - assert.equal(valid.admin.user, 'ontos_admin'); - assert.equal(valid.runtime.user, 'ontos_runtime'); - assert.equal(queryParameterIdentities.admin.user, 'ontos_admin'); - assert.equal(queryParameterIdentities.runtime.user, 'ontos_runtime'); - assert.equal(missing._tag, 'DatabaseConfigError'); - assert.equal(identical._tag, 'DatabaseConfigError'); - assert.equal(queryParameterCollision._tag, 'DatabaseConfigError'); - assert.equal(superuserCompatible._tag, 'DatabaseConfigError'); -}); + expect(valid.admin.user).toBe('ontos_admin'); + expect(valid.runtime.user).toBe('ontos_runtime'); + expect(queryParameterIdentities.admin.user).toBe('ontos_admin'); + expect(queryParameterIdentities.runtime.user).toBe('ontos_runtime'); + expect(Predicate.isTagged(missing, 'DatabaseConfigError')).toBe(true); + expect(Predicate.isTagged(identical, 'DatabaseConfigError')).toBe(true); + expect(Predicate.isTagged(queryParameterCollision, 'DatabaseConfigError')).toBe(true); + expect(Predicate.isTagged(superuserCompatible, 'DatabaseConfigError')).toBe(true); + }), +); -void test('finalizes the pool resource when its Effect scope closes', async () => { - let finalized = false; +it.effect('finalizes the pool resource when its Effect scope closes', () => + Effect.gen(function* migratedTest() { + let finalized = false; - await runEffectTestPromise( - Effect.scoped( + yield* Effect.scoped( acquirePoolResource(() => ({ - end: async () => { + end: () => { finalized = true; + return Promise.resolve(); }, })), - ), - ); + ); - assert.equal(finalized, true); -}); + expect(finalized).toBe(true); + }), +); diff --git a/app/packages/core-runtime/tests/unit/context-access.test.ts b/app/packages/core-runtime/tests/unit/context-access.test.ts index 183c6114f..e74cbc15e 100644 --- a/app/packages/core-runtime/tests/unit/context-access.test.ts +++ b/app/packages/core-runtime/tests/unit/context-access.test.ts @@ -1,8 +1,7 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; + import { v1 } from '@authzed/authzed-node'; -import { Effect, flow } from 'effect'; +import { Effect } from 'effect'; import { LEGAL_ENTITY_PERMISSION_KEYS, TENANT_PERMISSION_KEYS, @@ -17,12 +16,6 @@ import type { SpiceDbPermissionClient } from '../../src/permissions/client.ts'; const tenantId = '10000000-0000-4000-8000-000000000001'; const legalEntityId = '20000000-0000-4000-8000-000000000001'; const principalId = '30000000-0000-4000-8000-000000000001'; -const effectTest = (name: string, effect: Effect.Effect): void => { - test( - name, - flow(() => Effect.asVoid(effect), runEffectTestPromise), - ); -}; const responseFor = ( request: v1.CheckBulkPermissionsRequest, @@ -51,45 +44,45 @@ const makeClient = ( close: () => {}, }); -effectTest( +it.effect( 'uses one fully consistent batch and correlates allowed and denied module decisions', - Effect.gen(function* correlatesModuleDecisions() { - const requests: v1.CheckBulkPermissionsRequest[] = []; - const access = makeContextAccess( - makeClient((request) => - Effect.sync(() => { - requests.push(request); - return responseFor(request, [ - v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION, - v1.CheckPermissionResponse_Permissionship.NO_PERMISSION, - ]); - }), - ), - ); + () => + Effect.gen(function* correlatesModuleDecisions() { + const requests: v1.CheckBulkPermissionsRequest[] = []; + const access = makeContextAccess( + makeClient((request) => + Effect.sync(() => { + requests.push(request); + return responseFor(request, [ + v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION, + v1.CheckPermissionResponse_Permissionship.NO_PERMISSION, + ]); + }), + ), + ); - const result = yield* access.modules({ - legalEntityId, - moduleIds: ['property.registry', 'billing.core'], - principalId, - tenantId, - }); - assert.deepEqual(result, [ - { decision: 'allowed', key: 'property.registry' }, - { decision: 'denied', key: 'billing.core' }, - ]); - assert.equal(requests.length, 1); - assert.deepEqual(requests[0]?.consistency?.requirement, { - fullyConsistent: true, - oneofKind: 'fullyConsistent', - }); - assert.equal(requests[0]?.items[0]?.resource?.objectType, 'module_access'); - assert.equal(requests[0]?.items[0]?.permission, 'access'); - assert.equal(requests[0]?.items[0]?.subject?.object?.objectId, principalId); - }), + const result = yield* access.modules({ + legalEntityId, + moduleIds: ['property.registry', 'billing.core'], + principalId, + tenantId, + }); + expect(result).toEqual([ + { decision: 'allowed', key: 'property.registry' }, + { decision: 'denied', key: 'billing.core' }, + ]); + expect(requests.length).toBe(1); + expect(requests[0]?.consistency?.requirement).toEqual({ + fullyConsistent: true, + oneofKind: 'fullyConsistent', + }); + expect(requests[0]?.items[0]?.resource?.objectType).toBe('module_access'); + expect(requests[0]?.items[0]?.permission).toBe('access'); + expect(requests[0]?.items[0]?.subject?.object?.objectId).toBe(principalId); + }), ); -effectTest( - 'checks resource writes independently from resource reads', +it.effect('checks resource writes independently from resource reads', () => Effect.gen(function* checksResourceWrites() { const permissions: string[] = []; const service = makeContextAccess( @@ -100,7 +93,11 @@ effectTest( }), ), ); - const target = { moduleId: 'property.registry', resourceId: 'unit-1', resourceType: 'unit' }; + const target = { + moduleId: 'property.registry', + resourceId: 'unit-1', + resourceType: 'unit', + }; const result = yield* service.resources({ legalEntityId: 'entity-1', permission: 'write', @@ -108,8 +105,8 @@ effectTest( resources: [target], tenantId: 'tenant-1', }); - assert.deepEqual(result, [{ decision: 'denied', key: 'property.registry:unit:unit-1' }]); - assert.deepEqual(permissions, ['write']); + expect(result).toEqual([{ decision: 'denied', key: 'property.registry:unit:unit-1' }]); + expect(permissions).toEqual(['write']); }), ); @@ -129,8 +126,7 @@ const makeAllowedPermissionRecorder = () => { return { observed, service }; }; -effectTest( - 'forwards every closed tenant permission key without widening it', +it.effect('forwards every closed tenant permission key without widening it', () => Effect.gen(function* forwardsTenantPermissionKeys() { const { observed, service } = makeAllowedPermissionRecorder(); @@ -140,17 +136,16 @@ effectTest( .tenants({ permission, principalId, tenantIds: [tenantId] }) .pipe( Effect.map((result) => - assert.deepEqual(result, [{ decision: 'allowed', key: tenantId }]), + expect(result).toEqual([{ decision: 'allowed', key: tenantId }]), ), ), ), ); - assert.deepEqual(observed, TENANT_PERMISSION_KEYS); + expect(observed).toEqual(TENANT_PERMISSION_KEYS); }), ); -effectTest( - 'forwards every closed Legal Entity permission key without widening it', +it.effect('forwards every closed Legal Entity permission key without widening it', () => Effect.gen(function* forwardsLegalEntityPermissionKeys() { const { observed, service } = makeAllowedPermissionRecorder(); @@ -160,31 +155,28 @@ effectTest( .legalEntities({ legalEntityIds: [legalEntityId], permission, principalId, tenantId }) .pipe( Effect.map((result) => - assert.deepEqual(result, [{ decision: 'allowed', key: legalEntityId }]), + expect(result).toEqual([{ decision: 'allowed', key: legalEntityId }]), ), ), ), ); - assert.deepEqual(observed, LEGAL_ENTITY_PERMISSION_KEYS); + expect(observed).toEqual(LEGAL_ENTITY_PERMISSION_KEYS); }), ); -test('creates lossless tenant and legal-entity-qualified object identities', () => { +it('creates lossless tenant and legal-entity-qualified object identities', () => { const resource = { moduleId: 'property.registry', resourceId: 'unit:with/slashes', resourceType: 'property.unit', }; - assert.notEqual( - toLegalEntityAccessObjectId(tenantId, legalEntityId), + expect(toLegalEntityAccessObjectId(tenantId, legalEntityId)).not.toBe( toLegalEntityAccessObjectId('10000000-0000-4000-8000-000000000002', legalEntityId), ); - assert.notEqual( - toModuleAccessObjectId(tenantId, legalEntityId, 'property.registry'), + expect(toModuleAccessObjectId(tenantId, legalEntityId, 'property.registry')).not.toBe( toModuleAccessObjectId(tenantId, legalEntityId, 'property-registry'), ); - assert.notEqual( - toResourceAccessObjectId(tenantId, legalEntityId, resource), + expect(toResourceAccessObjectId(tenantId, legalEntityId, resource)).not.toBe( toResourceAccessObjectId(tenantId, legalEntityId, { ...resource, resourceId: 'unit-with/slashes', @@ -192,8 +184,7 @@ test('creates lossless tenant and legal-entity-qualified object identities', () ); }); -effectTest( - 'supports empty batches and exact resource filtering', +it.effect('supports empty batches and exact resource filtering', () => Effect.gen(function* supportsEmptyBatches() { let requests = 0; const access = makeContextAccess( @@ -207,77 +198,86 @@ effectTest( }), ), ); - assert.deepEqual( - yield* access.legalEntities({ legalEntityIds: [], principalId, tenantId }), - [], - ); - assert.deepEqual( + expect(yield* access.legalEntities({ legalEntityIds: [], principalId, tenantId })).toEqual([]); + expect( yield* access.resources({ legalEntityId, principalId, resources: [ - { moduleId: 'property.registry', resourceId: 'unit-1', resourceType: 'property.unit' }, - { moduleId: 'property.registry', resourceId: 'unit-2', resourceType: 'property.unit' }, + { + moduleId: 'property.registry', + resourceId: 'unit-1', + resourceType: 'property.unit', + }, + { + moduleId: 'property.registry', + resourceId: 'unit-2', + resourceType: 'property.unit', + }, ], tenantId, }), - [ - { decision: 'allowed', key: 'property.registry:property.unit:unit-1' }, - { decision: 'denied', key: 'property.registry:property.unit:unit-2' }, - ], - ); - assert.equal(requests, 1); + ).toEqual([ + { decision: 'allowed', key: 'property.registry:property.unit:unit-1' }, + { decision: 'denied', key: 'property.registry:property.unit:unit-2' }, + ]); + expect(requests).toBe(1); }), ); -effectTest( +it.effect( 'classifies client, partial, duplicate, malformed, and conditional results as unavailable', - Effect.gen(function* classifiesUnavailableResults() { - const input = { legalEntityIds: [legalEntityId], principalId, tenantId }; - const failures = [ - makeClient(() => - Effect.fail(spiceDbPermissionClientError(new Error('secret SpiceDB diagnostic'))), - ), - makeClient(() => Effect.succeed(v1.CheckBulkPermissionsResponse.create({ pairs: [] }))), - makeClient((request) => - Effect.succeed( - responseFor(request, [v1.CheckPermissionResponse_Permissionship.CONDITIONAL_PERMISSION]), + () => + Effect.gen(function* classifiesUnavailableResults() { + const input = { legalEntityIds: [legalEntityId], principalId, tenantId }; + const failures = [ + makeClient(() => + Effect.fail(spiceDbPermissionClientError(new Error('secret SpiceDB diagnostic'))), ), - ), - makeClient((request) => - Effect.sync(() => { - const response = responseFor(request, [ - v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION, - ]); - const [pair] = response.pairs; - return v1.CheckBulkPermissionsResponse.create({ - pairs: pair === undefined ? [] : [{ response: pair.response }], - }); - }), - ), - ]; - const [failingClient] = failures; - assert.ok(failingClient); - yield* Effect.all( - failures.map((client) => - makeContextAccess(client) - .legalEntities(input) - .pipe( - Effect.map((result) => - assert.deepEqual(result, [{ decision: 'unavailable', key: legalEntityId }]), - ), + makeClient(() => Effect.succeed(v1.CheckBulkPermissionsResponse.create({ pairs: [] }))), + makeClient((request) => + Effect.succeed( + responseFor(request, [ + v1.CheckPermissionResponse_Permissionship.CONDITIONAL_PERMISSION, + ]), ), - ), - ); - assert.deepEqual( - yield* makeContextAccess(failingClient).legalEntities({ - ...input, - legalEntityIds: [legalEntityId, legalEntityId], - }), - [ + ), + makeClient((request) => + Effect.sync(() => { + const response = responseFor(request, [ + v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION, + ]); + const [pair] = response.pairs; + return v1.CheckBulkPermissionsResponse.create({ + pairs: pair === undefined ? [] : [{ response: pair.response }], + }); + }), + ), + ]; + const [failingClient] = failures; + expect(failingClient).toBeDefined(); + if (failingClient === undefined) { + throw new Error('Missing failingClient'); + } + yield* Effect.all( + failures.map((client) => + makeContextAccess(client) + .legalEntities(input) + .pipe( + Effect.map((result) => + expect(result).toEqual([{ decision: 'unavailable', key: legalEntityId }]), + ), + ), + ), + ); + expect( + yield* makeContextAccess(failingClient).legalEntities({ + ...input, + legalEntityIds: [legalEntityId, legalEntityId], + }), + ).toEqual([ { decision: 'unavailable', key: legalEntityId }, { decision: 'unavailable', key: legalEntityId }, - ], - ); - }), + ]); + }), ); diff --git a/app/packages/core-runtime/tests/unit/database-driver-failure.test.ts b/app/packages/core-runtime/tests/unit/database-driver-failure.test.ts index 81b8194ff..057ac470e 100644 --- a/app/packages/core-runtime/tests/unit/database-driver-failure.test.ts +++ b/app/packages/core-runtime/tests/unit/database-driver-failure.test.ts @@ -1,8 +1,8 @@ +import { expect, it } from 'effect-rstest'; import { EffectDrizzleQueryError } from 'drizzle-orm/effect-core'; -import { Cause, Option, Schema } from 'effect'; +import { Cause, Option, Schema, Predicate } from 'effect'; import { SqlError, UniqueViolation } from 'effect/unstable/sql/SqlError'; -import assert from 'node:assert/strict'; -import test from 'node:test'; + import { DatabaseDriverFailureSchema, decodeDatabaseDriverFailure, @@ -15,7 +15,7 @@ interface MutableCause { cause?: unknown; } -void test('finds sanitized PostgreSQL metadata on the root failure', () => { +it('finds sanitized PostgreSQL metadata on the root failure', () => { const failure = findPostgresFailure({ code: '23505', constraint: 'principals_tenant_provider_subject_uk', @@ -24,14 +24,14 @@ void test('finds sanitized PostgreSQL metadata on the root failure', () => { }); const metadata = Option.getOrThrow(failure); - assert.deepEqual(metadata, { + expect(metadata).toEqual({ code: '23505', constraint: 'principals_tenant_provider_subject_uk', }); - assert.equal(Object.isFrozen(metadata), true); + expect(Object.isFrozen(metadata)).toBe(true); }); -void test('finds PostgreSQL metadata through Error and plain-object cause wrappers', () => { +it('finds PostgreSQL metadata through Error and plain-object cause wrappers', () => { const failure = new Error('outer wrapper', { cause: { cause: { @@ -40,22 +40,21 @@ void test('finds PostgreSQL metadata through Error and plain-object cause wrappe }, }); - assert.deepEqual(Option.getOrThrow(findPostgresFailure(failure)), { + expect(Option.getOrThrow(findPostgresFailure(failure))).toEqual({ code: '23505', constraint: 'principal_auth_bindings_provider_subject_uk', }); }); -void test('ignores a non-string constraint while retaining a valid code', () => { - assert.deepEqual( +it('ignores a non-string constraint while retaining a valid code', () => { + expect( Option.getOrThrow( findPostgresFailure({ code: '23505', constraint: { private: 'diagnostic object' } }), ), - { code: '23505' }, - ); + ).toEqual({ code: '23505' }); }); -void test('returns no PostgreSQL metadata for non-objects and unrelated objects', () => { +it('returns no PostgreSQL metadata for non-objects and unrelated objects', () => { for (const failure of [ null, undefined, @@ -67,22 +66,22 @@ void test('returns no PostgreSQL metadata for non-objects and unrelated objects' () => ({ code: '23505' }), new Error('unrelated'), ]) { - assert.equal(Option.isNone(findPostgresFailure(failure)), true); + expect(Option.isNone(findPostgresFailure(failure))).toBe(true); } }); -void test('requires a string code and treats the string constraint as optional', () => { +it('requires a string code and treats the string constraint as optional', () => { for (const failure of [{}, { code: 23_505 }, { cause: { code: false } }]) { - assert.equal(Option.isNone(findPostgresFailure(failure)), true); + expect(Option.isNone(findPostgresFailure(failure))).toBe(true); } - assert.deepEqual(Option.getOrThrow(findPostgresFailure({ code: '23505' })), { + expect(Option.getOrThrow(findPostgresFailure({ code: '23505' }))).toEqual({ code: '23505', }); }); -void test('returns the first recognizable PostgreSQL metadata in root-to-cause order', () => { - assert.deepEqual( +it('returns the first recognizable PostgreSQL metadata in root-to-cause order', () => { + expect( Option.getOrThrow( findPostgresFailure({ cause: { code: '23505', constraint: 'nested_constraint' }, @@ -90,31 +89,29 @@ void test('returns the first recognizable PostgreSQL metadata in root-to-cause o constraint: 'root_constraint', }), ), - { code: '40001', constraint: 'root_constraint' }, - ); + ).toEqual({ code: '40001', constraint: 'root_constraint' }); }); -void test('supports owner-local matching without changing default root precedence', () => { +it('supports owner-local matching without changing default root precedence', () => { const failure = { cause: { code: '23505', constraint: 'owner_constraint' }, code: 'ERR_QUERY_FAILED', }; - assert.deepEqual(Option.getOrThrow(findPostgresFailure(failure)), { + expect(Option.getOrThrow(findPostgresFailure(failure))).toEqual({ code: 'ERR_QUERY_FAILED', }); - assert.deepEqual( + expect( Option.getOrThrow( findPostgresFailure( failure, ({ code, constraint }) => code === '23505' && constraint === 'owner_constraint', ), ), - { code: '23505', constraint: 'owner_constraint' }, - ); + ).toEqual({ code: '23505', constraint: 'owner_constraint' }); }); -void test('terminates on cyclic cause graphs with a first match or no match', () => { +it('terminates on cyclic cause graphs with a first match or no match', () => { const matched: MutableCause & { readonly code: string } = { code: '23505' }; matched.cause = matched; @@ -123,45 +120,45 @@ void test('terminates on cyclic cause graphs with a first match or no match', () first.cause = second; second.cause = first; - assert.deepEqual(Option.getOrThrow(findPostgresFailure(matched)), { code: '23505' }); - assert.equal(Option.isNone(findPostgresFailure(first)), true); + expect(Option.getOrThrow(findPostgresFailure(matched))).toEqual({ code: '23505' }); + expect(Option.isNone(findPostgresFailure(first))).toBe(true); }); -void test('classifies unavailable PostgreSQL SQLSTATE classes', () => { +it('classifies unavailable PostgreSQL SQLSTATE classes', () => { for (const code of ['08006', '40001', '53100', '55P03', '57P01', '58030']) { const decoded = decodeDatabaseDriverFailure({ code }); - assert.equal(Option.isSome(decoded), true); + expect(Option.isSome(decoded)).toBe(true); if (Option.isSome(decoded)) { - assert.equal(decoded.value.kind, 'sqlstate'); - assert.equal(decoded.value.code, code); - assert.equal(Schema.is(DatabaseDriverFailureSchema)(decoded.value), true); + expect(decoded.value.kind).toBe('sqlstate'); + expect(decoded.value.code).toBe(code); + expect(Schema.is(DatabaseDriverFailureSchema)(decoded.value)).toBe(true); } - assert.equal(isDatabaseUnavailableFailure({ code }), true); + expect(isDatabaseUnavailableFailure({ code })).toBe(true); } }); -void test('distinguishes commit ambiguity from definite transaction failures', () => { +it('distinguishes commit ambiguity from definite transaction failures', () => { const connectionFailure = decodeDatabaseDriverFailure({ code: '08006' }); const administrativeShutdown = decodeDatabaseDriverFailure({ code: '57P01' }); const serializationFailure = decodeDatabaseDriverFailure({ code: '40001' }); - assert.equal( - Option.isSome(connectionFailure) && connectionFailure.value._tag, - 'DatabaseCommitAcknowledgementAmbiguous', - ); - assert.equal( - Option.isSome(administrativeShutdown) && administrativeShutdown.value._tag, - 'DatabaseCommitAcknowledgementAmbiguous', - ); - assert.equal( - Option.isSome(serializationFailure) && serializationFailure.value._tag, - 'DatabaseTransactionFailure', - ); - assert.equal(isDatabaseCommitAcknowledgementAmbiguous({ code: '40001' }), false); - assert.equal(isDatabaseCommitAcknowledgementAmbiguous({ code: '57014' }), false); -}); - -void test('classifies the exact commit-acknowledgement socket vocabulary', () => { + expect( + Option.isSome(connectionFailure) && + Predicate.isTagged(connectionFailure.value, 'DatabaseCommitAcknowledgementAmbiguous'), + ).toBe(true); + expect( + Option.isSome(administrativeShutdown) && + Predicate.isTagged(administrativeShutdown.value, 'DatabaseCommitAcknowledgementAmbiguous'), + ).toBe(true); + expect( + Option.isSome(serializationFailure) && + Predicate.isTagged(serializationFailure.value, 'DatabaseTransactionFailure'), + ).toBe(true); + expect(isDatabaseCommitAcknowledgementAmbiguous({ code: '40001' })).toBe(false); + expect(isDatabaseCommitAcknowledgementAmbiguous({ code: '57014' })).toBe(false); +}); + +it('classifies the exact commit-acknowledgement socket vocabulary', () => { const commitCodes = [ 'ECONNABORTED', 'ECONNRESET', @@ -174,41 +171,41 @@ void test('classifies the exact commit-acknowledgement socket vocabulary', () => 'ETIMEDOUT', ]; for (const code of commitCodes) { - assert.equal(isDatabaseCommitAcknowledgementAmbiguous({ code }), true); + expect(isDatabaseCommitAcknowledgementAmbiguous({ code })).toBe(true); } - assert.equal(isDatabaseCommitAcknowledgementAmbiguous({ code: 'ECONNREFUSED' }), false); + expect(isDatabaseCommitAcknowledgementAmbiguous({ code: 'ECONNREFUSED' })).toBe(false); }); -void test('preserves the auth-facing unavailable socket vocabulary', () => { +it('preserves the auth-facing unavailable socket vocabulary', () => { for (const code of ['ECONNREFUSED', 'ECONNRESET', 'EPIPE', 'ETIMEDOUT']) { - assert.equal(isDatabaseUnavailableFailure({ code }), true); + expect(isDatabaseUnavailableFailure({ code })).toBe(true); } for (const code of ['ECONNABORTED', 'EHOSTDOWN', 'ENETRESET']) { - assert.equal(isDatabaseUnavailableFailure({ code }), false); + expect(isDatabaseUnavailableFailure({ code })).toBe(false); } }); -void test('classifies unavailable driver metadata found through the shared cause-chain seam', () => { +it('classifies unavailable driver metadata found through the shared cause-chain seam', () => { const nestedFailure = { cause: { cause: { cause: { cause: { code: 'ECONNRESET' } } } }, }; - assert.equal(isDatabaseUnavailableFailure(nestedFailure), true); - assert.equal(isDatabaseCommitAcknowledgementAmbiguous(nestedFailure), true); + expect(isDatabaseUnavailableFailure(nestedFailure)).toBe(true); + expect(isDatabaseCommitAcknowledgementAmbiguous(nestedFailure)).toBe(true); }); -void test('continues past an unrelated wrapper code when classifying a nested driver failure', () => { +it('continues past an unrelated wrapper code when classifying a nested driver failure', () => { const nestedFailure = { cause: { code: 'ECONNRESET' }, code: 'ERR_QUERY_FAILED', }; - assert.equal(isDatabaseUnavailableFailure(nestedFailure), true); - assert.equal(isDatabaseCommitAcknowledgementAmbiguous(nestedFailure), true); + expect(isDatabaseUnavailableFailure(nestedFailure)).toBe(true); + expect(isDatabaseCommitAcknowledgementAmbiguous(nestedFailure)).toBe(true); }); -void test('does not classify unrelated or malformed failures as unavailable', () => { +it('does not classify unrelated or malformed failures as unavailable', () => { for (const failure of [ null, 'ECONNRESET', @@ -217,19 +214,19 @@ void test('does not classify unrelated or malformed failures as unavailable', () { code: 'ENOTFOUND' }, { cause: { code: 'not-a-driver-code' } }, ]) { - assert.equal(isDatabaseUnavailableFailure(failure), false); - assert.equal(Option.isNone(decodeDatabaseDriverFailure(failure)), true); + expect(isDatabaseUnavailableFailure(failure)).toBe(false); + expect(Option.isNone(decodeDatabaseDriverFailure(failure))).toBe(true); } }); -void test('terminates safely when a cause chain contains a cycle', () => { +it('terminates safely when a cause chain contains a cycle', () => { const cyclic: MutableCause = {}; cyclic.cause = cyclic; - assert.equal(isDatabaseUnavailableFailure(cyclic), false); + expect(isDatabaseUnavailableFailure(cyclic)).toBe(false); }); -void test('decodes native Drizzle and Effect SQL causes without exposing query data', () => { +it('decodes native Drizzle and Effect SQL causes without exposing query data', () => { const constraint = 'principal_auth_bindings_provider_subject_uk'; const driver = { code: '23505', constraint, detail: 'private detail' }; const sqlError = new SqlError({ reason: new UniqueViolation({ cause: driver, constraint }) }); @@ -238,23 +235,20 @@ void test('decodes native Drizzle and Effect SQL causes without exposing query d params: ['private parameter'], query: 'private SQL', }); - assert.deepEqual(Option.getOrThrow(findPostgresFailure(failure)), { code: '23505', constraint }); - assert.deepEqual(Option.getOrThrow(findPostgresFailure(Cause.die(sqlError))), { + expect(Option.getOrThrow(findPostgresFailure(failure))).toEqual({ code: '23505', constraint }); + expect(Option.getOrThrow(findPostgresFailure(Cause.die(sqlError)))).toEqual({ code: '23505', constraint, }); }); -void test('walks native mixed Causes in order and skips unrelated failures', () => { +it('walks native mixed Causes in order and skips unrelated failures', () => { const failure = Cause.combine( Cause.fail({ code: '40001' }), Cause.die({ code: '23505', constraint: 'owned_unique' }), ); - assert.deepEqual( - Option.getOrThrow(findPostgresFailure(failure, ({ code }) => code === '23505')), - { - code: '23505', - constraint: 'owned_unique', - }, - ); + expect(Option.getOrThrow(findPostgresFailure(failure, ({ code }) => code === '23505'))).toEqual({ + code: '23505', + constraint: 'owned_unique', + }); }); diff --git a/app/packages/core-runtime/tests/unit/entrypoint-classification.test.ts b/app/packages/core-runtime/tests/unit/entrypoint-classification.test.ts index 55efca3a3..dbea342eb 100644 --- a/app/packages/core-runtime/tests/unit/entrypoint-classification.test.ts +++ b/app/packages/core-runtime/tests/unit/entrypoint-classification.test.ts @@ -1,5 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; + import { Schema } from 'effect'; import { EntrypointAuthorizationSchema, @@ -10,7 +10,7 @@ import { defineTenantModuleEntrypoint, } from '../../src/modules/module-entrypoint.ts'; -test('decodes every closed authorization classification', () => { +it('decodes every closed authorization classification', () => { const classifications = [ { kind: 'public' }, { kind: 'authenticated_principal' }, @@ -23,11 +23,11 @@ test('decodes every closed authorization classification', () => { ] as const; for (const classification of classifications) { - assert.deepEqual(decodeEntrypointAuthorization(classification), classification); + expect(decodeEntrypointAuthorization(classification)).toEqual(classification); } }); -test('rejects omitted, unknown, excessive, and incompatible authorization fields', () => { +it('rejects omitted, unknown, excessive, and incompatible authorization fields', () => { const invalid = [ undefined, { kind: 'unknown' }, @@ -39,15 +39,15 @@ test('rejects omitted, unknown, excessive, and incompatible authorization fields ]; for (const value of invalid) { - assert.throws(() => + expect(() => Schema.decodeUnknownSync(EntrypointAuthorizationSchema, { onExcessProperty: 'error', })(value), - ); + ).toThrow(); } }); -test('requires role-compatible authorization and freezes nested classification', () => { +it('requires role-compatible authorization and freezes nested classification', () => { const action = defineTenantModuleEntrypoint({ access: 'write', authorization: { @@ -66,9 +66,9 @@ test('requires role-compatible authorization and freezes nested classification', role: 'page', }); - assert.equal(Object.isFrozen(action.authorization), true); - assert.equal(Object.isFrozen(route.authorization), true); - assert.throws(() => + expect(Object.isFrozen(action.authorization)).toBe(true); + expect(Object.isFrozen(route.authorization)).toBe(true); + expect(() => defineTenantModuleEntrypoint({ access: 'write', authorization: { kind: 'authenticated_principal' }, @@ -76,8 +76,8 @@ test('requires role-compatible authorization and freezes nested classification', moduleKey: 'inventory.stock', role: 'action', }), - ); - assert.throws(() => + ).toThrow(); + expect(() => defineTenantModuleEntrypoint({ access: 'background', authorization: { kind: 'action_execution', provisioning: 'explicit' }, @@ -85,10 +85,10 @@ test('requires role-compatible authorization and freezes nested classification', moduleKey: 'inventory.stock', role: 'worker', }), - ); + ).toThrow(); }); -test('keeps discovery metadata independent from authorization', () => { +it('keeps discovery metadata independent from authorization', () => { const route = { entrypoint: defineSystemModuleEntrypoint({ access: 'read', @@ -101,7 +101,7 @@ test('keeps discovery metadata independent from authorization', () => { public: false, } as const; - assert.equal(route.entrypoint.authorization.kind, 'public'); - assert.equal(route.public, false); - assert.equal(route.indexable, false); + expect(route.entrypoint.authorization.kind).toBe('public'); + expect(route.public).toBe(false); + expect(route.indexable).toBe(false); }); diff --git a/app/packages/core-runtime/tests/unit/fixture-cleanup.test.ts b/app/packages/core-runtime/tests/unit/fixture-cleanup.test.ts index a5ba0586f..edc88a584 100644 --- a/app/packages/core-runtime/tests/unit/fixture-cleanup.test.ts +++ b/app/packages/core-runtime/tests/unit/fixture-cleanup.test.ts @@ -1,7 +1,5 @@ -import assert from 'node:assert/strict'; -import { test } from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema } from 'effect'; -import { makeEffectTestCallback } from '../support/effect-runtime.ts'; import { purgeFixtureRows } from '../support/fixture-cleanup.ts'; class FixtureDeletionError extends Schema.TaggedError()( @@ -9,56 +7,47 @@ class FixtureDeletionError extends Schema.TaggedError()( {}, ) {} -void test( - 'purges fixture rows sequentially in child-before-parent order', - makeEffectTestCallback( - Effect.gen(function* verifyDeletionOrder() { - const deleted: string[] = []; - yield* purgeFixtureRows([ - Effect.yieldNow.pipe( - Effect.andThen( - Effect.sync(() => { - deleted.push('child'); - }), - ), +it.effect('purges fixture rows sequentially in child-before-parent order', () => + Effect.gen(function* verifyDeletionOrder() { + const deleted: string[] = []; + yield* purgeFixtureRows([ + Effect.yieldNow.pipe( + Effect.andThen( + Effect.sync(() => { + deleted.push('child'); + }), ), - Effect.sync(() => { - assert.deepEqual(deleted, ['child']); - deleted.push('parent'); - }), - ]); - assert.deepEqual(deleted, ['child', 'parent']); - }), - ), + ), + Effect.sync(() => { + expect(deleted).toEqual(['child']); + deleted.push('parent'); + }), + ]); + expect(deleted).toEqual(['child', 'parent']); + }), ); -void test( - 'stops fixture cleanup at the first failed deletion', - makeEffectTestCallback( - Effect.gen(function* verifyFirstFailure() { - const deleted: string[] = []; - const failure = new FixtureDeletionError(); - const error = yield* purgeFixtureRows([ - Effect.sync(() => { - deleted.push('child'); - }), - Effect.fail(failure), - Effect.sync(() => { - deleted.push('parent'); - }), - ]).pipe(Effect.flip); - assert.equal(error, failure); - assert.deepEqual(deleted, ['child']); - }), - ), +it.effect('stops fixture cleanup at the first failed deletion', () => + Effect.gen(function* verifyFirstFailure() { + const deleted: string[] = []; + const failure = new FixtureDeletionError(); + const error = yield* purgeFixtureRows([ + Effect.sync(() => { + deleted.push('child'); + }), + Effect.fail(failure), + Effect.sync(() => { + deleted.push('parent'); + }), + ]).pipe(Effect.flip); + expect(error).toBe(failure); + expect(deleted).toEqual(['child']); + }), ); -void test( - 'accepts an empty fixture cleanup', - makeEffectTestCallback( - Effect.gen(function* verifyEmptyCleanup() { - const result = yield* purgeFixtureRows([]); - assert.equal(result, undefined); - }), - ), +it.effect('accepts an empty fixture cleanup', () => + Effect.gen(function* verifyEmptyCleanup() { + const result = yield* purgeFixtureRows([]); + expect(result).toBe(undefined); + }), ); diff --git a/app/packages/core-runtime/tests/unit/governed-read-http.test.ts b/app/packages/core-runtime/tests/unit/governed-read-http.test.ts index 04b409b28..4f2dc05cc 100644 --- a/app/packages/core-runtime/tests/unit/governed-read-http.test.ts +++ b/app/packages/core-runtime/tests/unit/governed-read-http.test.ts @@ -1,7 +1,5 @@ -// @effect-diagnostics asyncFunction:off strictEffectProvide:off -- Node test and logger capture entrypoints; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { runEffectTestPromise } from '../../src/testing/effect-runtime.ts'; +// @effect-diagnostics strictEffectProvide:off -- Test-owned logger capture entrypoint; expires: 2026-12-31. +import { expect, it } from 'effect-rstest'; import { TrustedPrincipalContextSchema } from '../../src/actions/principal-context.ts'; import { defineSystemModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; import { ModuleStateCheckUnavailableError } from '../../src/modules/module-state-check-unavailable-error.ts'; @@ -105,15 +103,15 @@ const coreFailures: readonly [ [new ReadResultValidationError({ code: 'read_result_invalid', reason }), problems.internal()], ]; -test('classifies every Core governed-read failure through the endpoint problem set', () => { +it('classifies every Core governed-read failure through the endpoint problem set', () => { for (const [failure, expected] of coreFailures) { const actual = classifyReadCoreError(failure, problems); - assert.equal(actual.kind, expected.kind, failure._tag); - assert.equal(actual.status, expected.status, failure._tag); + expect(actual.kind, JSON.stringify(failure)).toBe(expected.kind); + expect(actual.status, JSON.stringify(failure)).toBe(expected.status); } }); -test('preserves the explicit Policy denial HTTP status', () => { +it('preserves the explicit Policy denial HTTP status', () => { for (const [httpStatus, expected] of [ [409, 'policy-conflict'], [422, 'policy-ineligible'], @@ -125,8 +123,8 @@ test('preserves the explicit Policy denial HTTP status', () => { reason, }); const actual = classifyReadCoreError(failure, problems); - assert.equal(actual.kind, expected); - assert.equal(actual.status, httpStatus); + expect(actual.kind).toBe(expected); + expect(actual.status).toBe(httpStatus); } }); @@ -178,18 +176,18 @@ const readRuntime = { } as ReadRuntimeService; const requestService = HttpServerRequest.fromWeb(new Request('https://ontos.test/reads/fixture')); -test('validates correlation before authentication or ReadRuntime acquisition', async () => { - let authenticationCalls = 0; - const handler = makeGovernedReadHttpHandler({ - authenticatePrincipal: () => { - authenticationCalls += 1; - return Effect.succeed(principal); - }, - problems, - registration, - }); - const exit = await runEffectTestPromise( - Effect.exit( +it.effect('validates correlation before authentication or ReadRuntime acquisition', () => + Effect.gen(function* validateCorrelationFirst() { + let authenticationCalls = 0; + const handler = makeGovernedReadHttpHandler({ + authenticatePrincipal: () => { + authenticationCalls += 1; + return Effect.succeed(principal); + }, + problems, + registration, + }); + const exit = yield* Effect.exit( handler({ payload: { query: 'fixture' }, request: { @@ -202,116 +200,120 @@ test('validates correlation before authentication or ReadRuntime acquisition', a ).pipe( Effect.provideService(ReadRuntime, readRuntime), Effect.provideService(HttpServerRequest.HttpServerRequest, requestService), - ), - ); - assert.equal(authenticationCalls, 0); - assert.equal(Exit.isFailure(exit), true); - if (Exit.isFailure(exit)) { - assert.deepEqual(Cause.squash(exit.cause), problems.invalid()); - } -}); + ); + expect(authenticationCalls).toBe(0); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(Cause.squash(exit.cause)).toEqual(problems.invalid()); + } + }), +); -test('sanitizes synchronous defects across correlation validation and authentication', async () => { - const cases = [ - { - handler: makeGovernedReadHttpHandler({ - authenticatePrincipal: () => Effect.succeed(principal), - problems: { - ...problems, - invalid: () => { - throw new Error('private invalid-problem factory detail'); +it.effect('sanitizes synchronous defects across correlation validation and authentication', () => + Effect.gen(function* sanitizeSynchronousDefects() { + const cases = [ + { + handler: makeGovernedReadHttpHandler({ + authenticatePrincipal: () => Effect.succeed(principal), + problems: { + ...problems, + invalid: () => { + throw new Error('private invalid-problem factory detail'); + }, }, - }, - registration, - }), - headers: Headers.empty, - }, - { - handler: makeGovernedReadHttpHandler({ - authenticatePrincipal: (): Effect.Effect => { - throw new Error('private authentication adapter detail'); + registration, + }), + headers: Headers.empty, + }, + { + handler: makeGovernedReadHttpHandler({ + authenticatePrincipal: (): Effect.Effect => { + throw new Error('private authentication adapter detail'); + }, + problems, + registration, + }), + headers: Headers.fromInput({ 'x-correlation-id': 'synchronous-defect' }), + }, + ]; + const exits = yield* Effect.forEach( + cases, + ({ handler, headers }) => + Effect.exit(handler({ payload: { query: 'fixture' }, request: { headers } })).pipe( + Effect.provideService(ReadRuntime, readRuntime), + Effect.provideService(HttpServerRequest.HttpServerRequest, requestService), + ), + { concurrency: 'unbounded' }, + ); + for (const exit of exits) { + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const publicFailure = Cause.squash(exit.cause); + expect(publicFailure).toEqual(problems.internal()); + expect( + yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(publicFailure), + ).not.toMatch(/private/u); + } + } + }), +); + +it.effect( + 'passes only payload, trusted principal, registration, and correlation to ReadRuntime', + () => + Effect.gen(function* forwardTrustedReadInputs() { + const payload = { query: 'fixture' }; + observed.length = 0; + const handler = makeGovernedReadHttpHandler({ + authenticatePrincipal: (authorization) => { + expect(Redacted.value(authorization)).toBe('Bearer private'); + return Effect.succeed(principal); }, problems, registration, - }), - headers: Headers.fromInput({ 'x-correlation-id': 'synchronous-defect' }), - }, - ]; - const exits = await Promise.all( - cases.map( - async ({ handler, headers }) => - await runEffectTestPromise( - Effect.exit(handler({ payload: { query: 'fixture' }, request: { headers } })).pipe( - Effect.provideService(ReadRuntime, readRuntime), - Effect.provideService(HttpServerRequest.HttpServerRequest, requestService), - ), - ), - ), - ); - for (const exit of exits) { - assert.equal(Exit.isFailure(exit), true); - if (Exit.isFailure(exit)) { - const publicFailure = Cause.squash(exit.cause); - assert.deepEqual(publicFailure, problems.internal()); - assert.doesNotMatch(JSON.stringify(publicFailure), /private/u); - } - } -}); + }); + const assertDecodedPayloadInput = () => + // @ts-expect-error The HTTP framework must pass the schema-decoded payload shape. + handler({ payload: { query: 123 }, request: { headers: Headers.empty } }); + void assertDecodedPayloadInput; + const result = yield* handler({ + payload, + request: { + headers: Headers.fromInput({ + authorization: 'Bearer private', + 'x-correlation-id': 'correlation-test', + }), + }, + }).pipe( + Effect.provideService(ReadRuntime, readRuntime), + Effect.provideService(HttpServerRequest.HttpServerRequest, requestService), + ); + expect(result).toEqual({ ok: true }); + expect(observed).toEqual([ + { + input: payload, + principal, + registration, + transport: { correlationId: 'correlation-test' }, + }, + ]); + }), +); -test('passes only payload, trusted principal, registration, and correlation to ReadRuntime', async () => { - const payload = { query: 'fixture' }; - observed.length = 0; - const handler = makeGovernedReadHttpHandler({ - authenticatePrincipal: (authorization) => { - assert.equal(Redacted.value(authorization), 'Bearer private'); - return Effect.succeed(principal); - }, - problems, - registration, - }); - const assertDecodedPayloadInput = () => - // @ts-expect-error The HTTP framework must pass the schema-decoded payload shape. - handler({ payload: { query: 123 }, request: { headers: Headers.empty } }); - void assertDecodedPayloadInput; - const result = await runEffectTestPromise( - handler({ - payload, - request: { - headers: Headers.fromInput({ - authorization: 'Bearer private', - 'x-correlation-id': 'correlation-test', - }), - }, - }).pipe( - Effect.provideService(ReadRuntime, readRuntime), - Effect.provideService(HttpServerRequest.HttpServerRequest, requestService), - ), - ); - assert.deepEqual(result, { ok: true }); - assert.deepEqual(observed, [ - { - input: payload, - principal, +it.effect('sanitizes unexpected defects at the complete governed handler boundary', () => + Effect.gen(function* sanitizeHandlerDefects() { + // SAFETY: This test double exercises only the handler's runRead call and deliberately omits no + // other ReadRuntimeService member; remove when the generic runtime interface exposes a test port. + const defectRuntime = { + runRead: () => Effect.die(new Error('private database connection detail')), + } as ReadRuntimeService; + const handler = makeGovernedReadHttpHandler({ + authenticatePrincipal: () => Effect.succeed(principal), + problems, registration, - transport: { correlationId: 'correlation-test' }, - }, - ]); -}); - -test('sanitizes unexpected defects at the complete governed handler boundary', async () => { - // SAFETY: This test double exercises only the handler's runRead call and deliberately omits no - // other ReadRuntimeService member; remove when the generic runtime interface exposes a test port. - const defectRuntime = { - runRead: () => Effect.die(new Error('private database connection detail')), - } as ReadRuntimeService; - const handler = makeGovernedReadHttpHandler({ - authenticatePrincipal: () => Effect.succeed(principal), - problems, - registration, - }); - const logEntries: string[] = []; - const exit = await runEffectTestPromise( - Effect.exit( + }); + const logEntries: string[] = []; + const exit = yield* Effect.exit( handler({ payload: { query: 'fixture' }, request: { @@ -322,15 +324,17 @@ test('sanitizes unexpected defects at the complete governed handler boundary', a Effect.provideService(ReadRuntime, defectRuntime), Effect.provideService(HttpServerRequest.HttpServerRequest, requestService), Effect.provide(capturedLoggerLayer(logEntries)), - ), - ); - assert.equal(Exit.isFailure(exit), true); - if (Exit.isFailure(exit)) { - const publicFailure = Cause.squash(exit.cause); - assert.deepEqual(publicFailure, problems.internal()); - assert.doesNotMatch(JSON.stringify(publicFailure), /private database connection detail/u); - } - assert.equal(logEntries.length, 1); - assert.doesNotMatch(logEntries.join('\n'), /private database connection detail/u); - assert.match(logEntries[0] ?? '', /correlation-defect/u); -}); + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + const publicFailure = Cause.squash(exit.cause); + expect(publicFailure).toEqual(problems.internal()); + expect( + yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(publicFailure), + ).not.toMatch(/private database connection detail/u); + } + expect(logEntries.length).toBe(1); + expect(logEntries.join('\n')).not.toMatch(/private database connection detail/u); + expect(logEntries[0] ?? '').toMatch(/correlation-defect/u); + }), +); diff --git a/app/packages/core-runtime/tests/unit/http-principal-authentication.test.ts b/app/packages/core-runtime/tests/unit/http-principal-authentication.test.ts index dd5fec255..aa524e367 100644 --- a/app/packages/core-runtime/tests/unit/http-principal-authentication.test.ts +++ b/app/packages/core-runtime/tests/unit/http-principal-authentication.test.ts @@ -1,9 +1,7 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from 'effect-rstest'; // @effect-diagnostics strictEffectProvide:off -- Test-owned HTTP application entrypoint; expires: 2026-12-31. import { NodeHttpServer } from '@effect/platform-node'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Match, Redacted, Schema } from 'effect'; +import { Effect, Match, Redacted, Schema, Predicate } from 'effect'; import { FetchHttpClient, HttpClient, @@ -27,10 +25,10 @@ const principal = { const verificationFailure = ( _tag: (typeof OperationPrincipalVerificationErrorSchema.Type)['_tag'], ) => - Schema.decodeUnknownSync(OperationPrincipalVerificationErrorSchema)({ + Schema.decodeUnknownEffect(OperationPrincipalVerificationErrorSchema)({ _tag, reason: 'Private verifier diagnostic', - }); + }).pipe(Effect.orDie); const authenticationProblem = () => ({ _tag: 'FixtureAuthenticationProblem' as const, @@ -63,109 +61,101 @@ const respondWithProblem = ( error: ReturnType, ) => Effect.succeed(problemResponse(error)); -// oxlint-disable-next-line typescript/promise-function-async -- Effect is the test's async control flow; expires: 2026-12-31. -test('mounted HTTP authentication maps verifier classes, challenges unusable credentials, and stops before private logic', () => { - let privateOperationReached = 0; - const failureByCredential = new Map< - string | undefined, - (typeof OperationPrincipalVerificationErrorSchema.Type)['_tag'] - >([ - [undefined, 'ActionPrincipalMissingError'], - ['Bearer malformed', 'ActionPrincipalInvalidError'], - ['Bearer expired', 'ActionPrincipalExpiredError'], - ['Bearer wrong-scope', 'ActionPrincipalScopeError'], - ['Bearer misconfigured', 'ActionPrincipalConfigurationError'], - ['Bearer unavailable', 'ActionPrincipalUnavailableError'], - ]); - const authenticate = makeMicroverticalHttpPrincipalAuthentication((authorization) => { - const raw = Redacted.value(authorization); - const failure = failureByCredential.get(raw); - return failure === undefined - ? Effect.succeed(principal) - : Effect.fail(verificationFailure(failure)); - }); +it.live( + 'mounted HTTP authentication maps verifier classes, challenges unusable credentials, and stops before private logic', + () => { + let privateOperationReached = 0; + const failureByCredential = new Map< + string | undefined, + (typeof OperationPrincipalVerificationErrorSchema.Type)['_tag'] + >([ + [undefined, 'ActionPrincipalMissingError'], + ['Bearer malformed', 'ActionPrincipalInvalidError'], + ['Bearer expired', 'ActionPrincipalExpiredError'], + ['Bearer wrong-scope', 'ActionPrincipalScopeError'], + ['Bearer misconfigured', 'ActionPrincipalConfigurationError'], + ['Bearer unavailable', 'ActionPrincipalUnavailableError'], + ]); + const authenticate = makeMicroverticalHttpPrincipalAuthentication((authorization) => { + const raw = Redacted.value(authorization); + const failure = failureByCredential.get(raw); + return failure === undefined + ? Effect.succeed(principal) + : verificationFailure(failure).pipe(Effect.flatMap(Effect.fail)); + }); - return runEffectTestPromise( - Effect.scoped( - Effect.gen(function* mountedAuthenticationHandler() { - const server = yield* NodeHttpServer.make( - () => process.getBuiltinModule('http').createServer(), - { host: '127.0.0.1', port: 0 }, - ); - const application = HttpServerRequest.HttpServerRequest.use((request) => - authenticate(Redacted.make(request.headers['authorization']), { - authentication: authenticationProblem, - unavailable: unavailableProblem, - }).pipe( - Effect.flatMap((trustedPrincipal) => - Effect.sync(() => { - privateOperationReached += 1; - return HttpServerResponse.jsonUnsafe({ principal: trustedPrincipal }); - }), - ), - Effect.catch(respondWithProblem), + return Effect.gen(function* mountedAuthenticationHandler() { + const server = yield* NodeHttpServer.make( + () => process.getBuiltinModule('http').createServer(), + { host: '127.0.0.1', port: 0 }, + ); + const application = HttpServerRequest.HttpServerRequest.use((request) => + authenticate(Redacted.make(request.headers['authorization']), { + authentication: authenticationProblem, + unavailable: unavailableProblem, + }).pipe( + Effect.flatMap((trustedPrincipal) => + Effect.sync(() => { + privateOperationReached += 1; + return HttpServerResponse.jsonUnsafe({ principal: trustedPrincipal }); + }), ), + Effect.catch(respondWithProblem), + ), + ); + yield* server.serve(application); + const address = yield* Match.value(server.address).pipe( + Match.tag('TcpAddress', (tcpAddress) => Effect.succeed(tcpAddress)), + Match.orElse(() => Effect.die('HTTP authentication fixture did not bind to TCP')), + ); + const client = yield* HttpClient.HttpClient; + const url = `http://127.0.0.1:${address.port}/operation`; + for (const [authorization, expectedStatus] of [ + [undefined, 401], + ['Bearer malformed', 401], + ['Bearer expired', 401], + ['Bearer wrong-scope', 401], + ['Bearer misconfigured', 503], + ['Bearer unavailable', 503], + ] as const) { + const request = + authorization === undefined + ? HttpClientRequest.get(url) + : HttpClientRequest.get(url).pipe( + HttpClientRequest.setHeader('authorization', authorization), + ); + const response = yield* client.execute(request); + expect(response.status).toBe(expectedStatus); + expect(response.headers['content-type']).toBe('application/problem+json'); + expect(response.headers['www-authenticate']).toBe( + expectedStatus === 401 ? 'Bearer' : undefined, ); - yield* server.serve(application); - const address = yield* Match.value(server.address).pipe( - Match.tag('TcpAddress', (tcpAddress) => Effect.succeed(tcpAddress)), - Match.orElse(() => Effect.die('HTTP authentication fixture did not bind to TCP')), + const rawBody = yield* response.json; + expect(rawBody).toEqual( + expectedStatus === 401 ? authenticationProblem() : unavailableProblem(), ); - const client = yield* HttpClient.HttpClient; - const url = `http://127.0.0.1:${address.port}/operation`; - for (const [authorization, expectedStatus] of [ - [undefined, 401], - ['Bearer malformed', 401], - ['Bearer expired', 401], - ['Bearer wrong-scope', 401], - ['Bearer misconfigured', 503], - ['Bearer unavailable', 503], - ] as const) { - const request = - authorization === undefined - ? HttpClientRequest.get(url) - : HttpClientRequest.get(url).pipe( - HttpClientRequest.setHeader('authorization', authorization), - ); - const response = yield* client.execute(request); - assert.equal(response.status, expectedStatus); - assert.equal(response.headers['content-type'], 'application/problem+json'); - assert.equal( - response.headers['www-authenticate'], - expectedStatus === 401 ? 'Bearer' : undefined, - ); - const rawBody = yield* response.json; - assert.deepEqual( - rawBody, - expectedStatus === 401 ? authenticationProblem() : unavailableProblem(), - ); - const body = yield* Schema.decodeUnknownEffect(ProblemResponseSchema)(rawBody); - assert.ok( - Schema.is( - Schema.TaggedStruct( - expectedStatus === 401 - ? 'FixtureAuthenticationProblem' - : 'FixtureUnavailableProblem', - {}, - ), - )(body), - ); - assert.equal(body.status, expectedStatus); - } - assert.equal(privateOperationReached, 0); - - const success = yield* client.execute( - HttpClientRequest.get(url).pipe( - HttpClientRequest.setHeader('authorization', 'Bearer valid'), + const body = yield* Schema.decodeUnknownEffect(ProblemResponseSchema)(rawBody); + expect( + Predicate.isTagged( + body, + expectedStatus === 401 ? 'FixtureAuthenticationProblem' : 'FixtureUnavailableProblem', ), - ); - assert.equal(success.status, 200); - const successBody = yield* success.json.pipe( - Effect.flatMap(Schema.decodeUnknownEffect(SuccessResponseSchema)), - ); - assert.deepEqual(successBody.principal, principal); - assert.equal(privateOperationReached, 1); - }), - ).pipe(Effect.provide(FetchHttpClient.layer)), - ); -}); + ).toBe(true); + expect(body.status).toBe(expectedStatus); + } + expect(privateOperationReached).toBe(0); + + const success = yield* client.execute( + HttpClientRequest.get(url).pipe( + HttpClientRequest.setHeader('authorization', 'Bearer valid'), + ), + ); + expect(success.status).toBe(200); + const successBody = yield* success.json.pipe( + Effect.flatMap(Schema.decodeUnknownEffect(SuccessResponseSchema)), + ); + expect(successBody.principal).toEqual(principal); + expect(privateOperationReached).toBe(1); + }).pipe(Effect.provide(FetchHttpClient.layer)); + }, +); diff --git a/app/packages/core-runtime/tests/unit/legal-entity-context.test.ts b/app/packages/core-runtime/tests/unit/legal-entity-context.test.ts index 6dc088ac0..165fbf01c 100644 --- a/app/packages/core-runtime/tests/unit/legal-entity-context.test.ts +++ b/app/packages/core-runtime/tests/unit/legal-entity-context.test.ts @@ -1,12 +1,9 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { Effect, flow } from 'effect'; +import { expect, it } from 'effect-rstest'; + +import { Effect, Predicate } from 'effect'; import { ConnectionError, SqlError } from 'effect/unstable/sql/SqlError'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import type { - LegalEntityContextError, - LegalEntityContextRecord, -} from '../../src/auth/legal-entity-context.ts'; + +import type { LegalEntityContextRecord } from '../../src/auth/legal-entity-context.ts'; import { classifyActiveLegalEntities, classifySelectedLegalEntity, @@ -15,12 +12,7 @@ import { import { makeTestDatabase } from '../support/database.ts'; const tenantId = '10000000-0000-4000-8000-000000000001'; -const effectTest = (name: string, effect: Effect.Effect): void => { - test( - name, - flow(() => Effect.asVoid(effect), runEffectTestPromise), - ); -}; + const activeRecord: LegalEntityContextRecord = { legalEntityId: '20000000-0000-4000-8000-000000000001', legalName: 'Zeta s.r.o.', @@ -28,22 +20,13 @@ const activeRecord: LegalEntityContextRecord = { tenantId, }; -const failureTag = (effect: Effect.Effect) => - effect.pipe( - Effect.match({ - onFailure: (error) => error._tag, - onSuccess: () => assert.fail('Expected legal-entity context classification to fail'), - }), - ); - -effectTest( - 'lists zero, one, and many active legal entities in deterministic safe order', +it.effect('lists zero, one, and many active legal entities in deterministic safe order', () => Effect.gen(function* listsActiveLegalEntities() { - assert.deepEqual(yield* classifyActiveLegalEntities([], tenantId), []); - assert.deepEqual(yield* classifyActiveLegalEntities([activeRecord], tenantId), [ + expect(yield* classifyActiveLegalEntities([], tenantId)).toEqual([]); + expect(yield* classifyActiveLegalEntities([activeRecord], tenantId)).toEqual([ { legalEntityId: activeRecord.legalEntityId, legalName: 'Zeta s.r.o.' }, ]); - assert.deepEqual( + expect( yield* classifyActiveLegalEntities( [ activeRecord, @@ -72,81 +55,88 @@ effectTest( ], tenantId, ), - [ - { - legalEntityId: '20000000-0000-4000-8000-000000000002', - legalName: 'Alpha s.r.o.', - }, - { - legalEntityId: '20000000-0000-4000-8000-000000000003', - legalName: 'Alpha s.r.o.', - }, - { legalEntityId: activeRecord.legalEntityId, legalName: 'Zeta s.r.o.' }, - ], - ); + ).toEqual([ + { + legalEntityId: '20000000-0000-4000-8000-000000000002', + legalName: 'Alpha s.r.o.', + }, + { + legalEntityId: '20000000-0000-4000-8000-000000000003', + legalName: 'Alpha s.r.o.', + }, + { legalEntityId: activeRecord.legalEntityId, legalName: 'Zeta s.r.o.' }, + ]); }), ); -effectTest( - 'validates exactly one active selection and rejects missing or inactive selections', +it.effect('validates exactly one active selection and rejects missing or inactive selections', () => Effect.gen(function* validatesLegalEntitySelection() { - assert.deepEqual( + expect( yield* classifySelectedLegalEntity([activeRecord], tenantId, activeRecord.legalEntityId), - { legalEntityId: activeRecord.legalEntityId, legalName: activeRecord.legalName }, - ); - assert.equal( - yield* failureTag( - classifySelectedLegalEntity( - [activeRecord], - tenantId, - '20000000-0000-4000-8000-000000000099', + ).toEqual({ legalEntityId: activeRecord.legalEntityId, legalName: activeRecord.legalName }); + expect( + Predicate.isTagged( + yield* Effect.flip( + classifySelectedLegalEntity( + [activeRecord], + tenantId, + '20000000-0000-4000-8000-000000000099', + ), ), + 'LegalEntityContextMissingError', ), - 'LegalEntityContextMissingError', - ); - assert.equal( - yield* failureTag( - classifySelectedLegalEntity( - [{ ...activeRecord, status: 'suspended' }], - tenantId, - activeRecord.legalEntityId, + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip( + classifySelectedLegalEntity( + [{ ...activeRecord, status: 'suspended' }], + tenantId, + activeRecord.legalEntityId, + ), ), + 'LegalEntityContextInactiveError', ), - 'LegalEntityContextInactiveError', - ); + ).toBe(true); }), ); -effectTest( - 'rejects cross-tenant, malformed, and duplicate records', +it.effect('rejects cross-tenant, malformed, and duplicate records', () => Effect.gen(function* rejectsInvalidLegalEntityRecords() { - assert.equal( - yield* failureTag( - classifyActiveLegalEntities( - [{ ...activeRecord, tenantId: '10000000-0000-4000-8000-000000000002' }], - tenantId, + expect( + Predicate.isTagged( + yield* Effect.flip( + classifyActiveLegalEntities( + [{ ...activeRecord, tenantId: '10000000-0000-4000-8000-000000000002' }], + tenantId, + ), ), + 'LegalEntityContextInvalidError', ), - 'LegalEntityContextInvalidError', - ); - assert.equal( - yield* failureTag( - classifyActiveLegalEntities([{ ...activeRecord, legalName: '' }], tenantId), + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip( + classifyActiveLegalEntities([{ ...activeRecord, legalName: '' }], tenantId), + ), + 'LegalEntityContextInvalidError', + ), + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip( + classifyActiveLegalEntities([activeRecord, { ...activeRecord }], tenantId), + ), + 'LegalEntityContextAmbiguousError', ), - 'LegalEntityContextInvalidError', - ); - assert.equal( - yield* failureTag(classifyActiveLegalEntities([activeRecord, { ...activeRecord }], tenantId)), - 'LegalEntityContextAmbiguousError', - ); + ).toBe(true); }), ); -effectTest( - 'types database failures as sanitized legal-entity context unavailability', +it.effect('types database failures as sanitized legal-entity context unavailability', () => Effect.gen(function* sanitizesLegalEntityDatabaseFailure() { const context = makeLegalEntityContext({ - executor: makeTestDatabase(() => + executor: yield* makeTestDatabase(() => Effect.fail( new SqlError({ reason: new ConnectionError({ cause: new Error('secret database diagnostic') }), @@ -155,7 +145,9 @@ effectTest( ), }); const error = yield* Effect.flip(context.listActiveForTenant(tenantId)); - assert.equal(error._tag, 'LegalEntityContextUnavailableError'); - assert.doesNotMatch(error.reason, /secret database diagnostic/u); + expect(Predicate.isTagged(error, 'LegalEntityContextUnavailableError')).toBe(true); + expect( + Predicate.isTagged(error, 'LegalEntityContextUnavailableError') ? error.reason : undefined, + ).not.toMatch(/secret database diagnostic/u); }), ); diff --git a/app/packages/core-runtime/tests/unit/module-catalog.test.ts b/app/packages/core-runtime/tests/unit/module-catalog.test.ts index a35dae6a7..18bf00ff2 100644 --- a/app/packages/core-runtime/tests/unit/module-catalog.test.ts +++ b/app/packages/core-runtime/tests/unit/module-catalog.test.ts @@ -1,6 +1,5 @@ import { makeModuleContractFixture } from '../../src/testing/module-contract.ts'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { buildInstalledModuleCatalog, resolveInstalledModuleCatalog, @@ -19,7 +18,7 @@ const contract = ( outboxSubscriptions, }); -void test('builds immutable deterministic dual indexes for distinct deployment and module IDs', () => { +it('builds immutable deterministic dual indexes for distinct deployment and module IDs', () => { const catalog = buildInstalledModuleCatalog([ { contract: contract('property-registry', 'property.registry'), @@ -31,19 +30,18 @@ void test('builds immutable deterministic dual indexes for distinct deployment a }, ]); - assert.deepEqual(catalog.deploymentAppIds, ['documents-center', 'property-registry']); - assert.deepEqual(catalog.moduleIds, ['documents.center', 'property.registry']); - assert.equal( - catalog.getByDeploymentAppId('property-registry')?.manifest.module.id, + expect(catalog.deploymentAppIds).toEqual(['documents-center', 'property-registry']); + expect(catalog.moduleIds).toEqual(['documents.center', 'property.registry']); + expect(catalog.getByDeploymentAppId('property-registry')?.manifest.module.id).toBe( 'property.registry', ); - assert.equal(catalog.getByModuleId('property.registry')?.deployment.appId, 'property-registry'); - assert.equal(Object.isFrozen(catalog), true); - assert.equal(Object.isFrozen(catalog.contracts), true); - assert.equal(Object.isFrozen(catalog.outboxSubscriptions), true); + expect(catalog.getByModuleId('property.registry')?.deployment.appId).toBe('property-registry'); + expect(Object.isFrozen(catalog)).toBe(true); + expect(Object.isFrozen(catalog.contracts)).toBe(true); + expect(Object.isFrozen(catalog.outboxSubscriptions)).toBe(true); }); -void test('accepts a valid owner-local subscription whose producer is not installed', () => { +it('accepts a valid owner-local subscription whose producer is not installed', () => { const subscription = { consumerModuleKey: 'property.registry', entrypoint: { @@ -64,10 +62,10 @@ void test('accepts a valid owner-local subscription whose producer is not instal expectedAppId: 'property-registry', }, ]); - assert.deepEqual(catalog.outboxSubscriptions, [subscription]); + expect(catalog.outboxSubscriptions).toEqual([subscription]); }); -void test('rejects contradictory or incomplete Outbox subscription snapshots', () => { +it('rejects contradictory or incomplete Outbox subscription snapshots', () => { const invalidSubscription = { consumerModuleKey: 'other.module', entrypoint: { @@ -82,15 +80,15 @@ void test('rejects contradictory or incomplete Outbox subscription snapshots', ( topic: 'missing.producer.created', workerKey: 'property.registry.projector', } as const; - assert.throws(() => + expect(() => buildInstalledModuleCatalog([ { contract: contract('property-registry', 'property.registry', [invalidSubscription]), expectedAppId: 'property-registry', }, ]), - ); - assert.throws(() => + ).toThrow(); + expect(() => buildInstalledModuleCatalog([ { contract: contract('property-registry', 'property.registry', [ @@ -102,10 +100,10 @@ void test('rejects contradictory or incomplete Outbox subscription snapshots', ( expectedAppId: 'property-registry', }, ]), - ); + ).toThrow(); const duplicateWorkerKey = 'shared.projector'; - assert.throws(() => + expect(() => buildInstalledModuleCatalog([ { contract: contract('property-registry', 'property.registry', [ @@ -146,19 +144,19 @@ void test('rejects contradictory or incomplete Outbox subscription snapshots', ( expectedAppId: 'documents-center', }, ]), - ); + ).toThrow(); }); -void test('rejects deployment mismatch, duplicate deployment IDs, and duplicate module claims', () => { - assert.throws(() => +it('rejects deployment mismatch, duplicate deployment IDs, and duplicate module claims', () => { + expect(() => buildInstalledModuleCatalog([ { contract: contract('property-registry', 'property.registry'), expectedAppId: 'different-app', }, ]), - ); - assert.throws(() => + ).toThrow(); + expect(() => buildInstalledModuleCatalog([ { contract: contract('property-registry', 'property.registry'), @@ -169,8 +167,8 @@ void test('rejects deployment mismatch, duplicate deployment IDs, and duplicate expectedAppId: 'property-registry', }, ]), - ); - assert.throws(() => + ).toThrow(); + expect(() => buildInstalledModuleCatalog([ { contract: contract('property-registry', 'property.registry'), @@ -181,21 +179,21 @@ void test('rejects deployment mismatch, duplicate deployment IDs, and duplicate expectedAppId: 'property-other', }, ]), - ); + ).toThrow(); }); -void test('rejects unsupported contract versions without weakening catalog safety', () => { - assert.throws(() => +it('rejects unsupported contract versions without weakening catalog safety', () => { + expect(() => buildInstalledModuleCatalog([ { contract: { ...contract('property-registry', 'property.registry'), schemaVersion: '0' }, expectedAppId: 'property-registry', }, ]), - ); + ).toThrow(); }); -void test('resolves healthy, incompatible, and unreachable deployments independently', () => { +it('resolves healthy, incompatible, and unreachable deployments independently', () => { const catalog = resolveInstalledModuleCatalog([ { contract: contract('documents-center', 'documents.center'), @@ -216,8 +214,8 @@ void test('resolves healthy, incompatible, and unreachable deployments independe { expectedAppId: 'revoked-center', outcome: 'revoked' }, ]); - assert.deepEqual(catalog.moduleIds, ['documents.center']); - assert.deepEqual(catalog.deploymentStatuses, [ + expect(catalog.moduleIds).toEqual(['documents.center']); + expect(catalog.deploymentStatuses).toEqual([ { appId: 'disabled-center', status: 'disabled' }, { appId: 'documents-center', moduleId: 'documents.center', status: 'available' }, { appId: 'property-registry', reason: 'incompatible', status: 'unavailable' }, @@ -255,7 +253,7 @@ for (const scenario of [ ], }, ] as const) { - void test(scenario.name, () => { + it(scenario.name, () => { const catalog = resolveInstalledModuleCatalog( scenario.identities.map(([appId, moduleId]) => ({ contract: contract(appId, moduleId), @@ -263,12 +261,12 @@ for (const scenario of [ outcome: 'fetched', })), ); - assert.deepEqual(catalog.moduleIds, scenario.moduleIds); - assert.deepEqual(catalog.deploymentStatuses, scenario.statuses); + expect(catalog.moduleIds).toEqual(scenario.moduleIds); + expect(catalog.deploymentStatuses).toEqual(scenario.statuses); }); } -void test('keeps authoritative revocation ahead of a stale fetched candidate', () => { +it('keeps authoritative revocation ahead of a stale fetched candidate', () => { const catalog = resolveInstalledModuleCatalog([ { contract: contract('property-registry', 'property.registry'), @@ -284,8 +282,8 @@ void test('keeps authoritative revocation ahead of a stale fetched candidate', ( }, ]); - assert.deepEqual(catalog.moduleIds, ['documents.center']); - assert.deepEqual(catalog.deploymentStatuses, [ + expect(catalog.moduleIds).toEqual(['documents.center']); + expect(catalog.deploymentStatuses).toEqual([ { appId: 'documents-center', moduleId: 'documents.center', status: 'available' }, { appId: 'property-registry', status: 'revoked' }, ]); diff --git a/app/packages/core-runtime/tests/unit/module-manifest.test.ts b/app/packages/core-runtime/tests/unit/module-manifest.test.ts index 1d3a829ce..19f0446eb 100644 --- a/app/packages/core-runtime/tests/unit/module-manifest.test.ts +++ b/app/packages/core-runtime/tests/unit/module-manifest.test.ts @@ -1,7 +1,5 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Schema, flow } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Schema } from 'effect'; import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/unstable/httpapi'; import { defineAction } from '../../src/actions/definition.ts'; import { defineTenantModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; @@ -91,160 +89,160 @@ const emptyManifestInput = () => ({ }, }); -void test('defines a valid empty manifest, preserves literals, and freezes its public shape', () => { +it('defines a valid empty manifest, preserves literals, and freezes its public shape', () => { const manifest = defineOntosModuleManifest(emptyManifestInput()); const literal: 'property.registry' = manifest.module.id; - assert.equal(literal, 'property.registry'); - assert.deepEqual(Object.keys(manifest), ['activation', 'module', 'publicSurface']); - assert.equal(Object.isFrozen(manifest), true); - assert.equal(Object.isFrozen(manifest.activation.supportedStates), true); - assert.equal(Object.isFrozen(manifest.publicSurface.actions), true); - assert.throws(() => + expect(literal).toBe('property.registry'); + expect(Object.keys(manifest)).toEqual(['activation', 'module', 'publicSurface']); + expect(Object.isFrozen(manifest)).toBe(true); + expect(Object.isFrozen(manifest.activation.supportedStates)).toBe(true); + expect(Object.isFrozen(manifest.publicSurface.actions)).toBe(true); + expect(() => Object.defineProperty(manifest.publicSurface.actions, 0, { value: 'private', }), - ); + ).toThrow(); }); -test( +it.effect( 'accepts populated typed surfaces and keeps executable values out of safe descriptors', - flow( - () => - Effect.gen(function* verifySafeDescriptors() { - const action = createAction(); - const apiValue = HttpApi.make('PropertyApi').add( - HttpApiGroup.make('property').add(HttpApiEndpoint.get('listUnits', '/units')), - ); - const parameterizedApiValue = HttpApi.make('PropertyDetailApi').add( - HttpApiGroup.make('propertyDetail').add( - HttpApiEndpoint.get('getUnit', '/units/:unitId', { - headers: {}, - params: { unitId: UnitId }, - query: {}, - }), - ), - ); - const manifest = defineOntosModuleManifest({ - ...emptyManifestInput(), - publicSurface: { - actions: [action], - api: { PropertyClient: apiValue, PropertyDetail: parameterizedApiValue }, - components: { PropertyUnitCard: componentValue }, - events: [ - { - key: 'property.unit-created', - owningModuleId: 'property.registry', - payloadSchema: Schema.Struct({ unitId: UnitId }), - referencesResourceTypes: ['property.unit'], - tense: 'past', - visibility: 'public_module_event', - }, - ], - reports: [ - { - accessFiltering: 'legal_entity_scope', - dimensions: ['legal_entity'], - key: 'property.unit-inventory', - label: 'Unit inventory', - owningModuleId: 'property.registry', - resourceTypes: ['property.unit'], - }, - ], - resourceTypes: [ - { - capabilities: { - graphVisible: true, - linkable: true, - mediaAttachable: true, - searchable: true, - timelineVisible: true, - }, - description: 'A physical unit', - key: 'property.unit', - label: 'Unit', - owningModuleId: 'property.registry', - }, - ], - search: [ - { - accessFiltering: 'legal_entity_scope', - key: 'property.unit-search', - owningModuleId: 'property.registry', - resourceType: 'property.unit', - }, - ], - shellContributions: emptyManifestInput().publicSurface.shellContributions, - }, - }); - const registration = defineVerticalRuntimeRegistration({ + () => + Effect.gen(function* verifySafeDescriptors() { + const action = createAction(); + const apiValue = HttpApi.make('PropertyApi').add( + HttpApiGroup.make('property').add(HttpApiEndpoint.get('listUnits', '/units')), + ); + const parameterizedApiValue = HttpApi.make('PropertyDetailApi').add( + HttpApiGroup.make('propertyDetail').add( + HttpApiEndpoint.get('getUnit', '/units/:unitId', { + headers: {}, + params: { unitId: UnitId }, + query: {}, + }), + ), + ); + const manifest = defineOntosModuleManifest({ + ...emptyManifestInput(), + publicSurface: { actions: [action], - entrypoints: { - api: { resource: flow(() => Effect.succeed(apiValue), runEffectTestPromise) }, - components: { - dashboard: flow(() => Effect.succeed(componentValue), runEffectTestPromise), + api: { PropertyClient: apiValue, PropertyDetail: parameterizedApiValue }, + components: { PropertyUnitCard: componentValue }, + events: [ + { + key: 'property.unit-created', + owningModuleId: 'property.registry', + payloadSchema: Schema.Struct({ unitId: UnitId }), + referencesResourceTypes: ['property.unit'], + tense: 'past', + visibility: 'public_module_event', }, - pages: {}, - reports: {}, - search: {}, - }, - manifest, - outboxWorkers: [], - }); - const descriptors = extractVerticalRuntimeSafeDescriptors(registration); - - assert.equal(manifest.publicSurface.actions[0], action); - assert.equal(manifest.publicSurface.api.PropertyClient, apiValue); - assert.equal(manifest.publicSurface.api.PropertyDetail, parameterizedApiValue); - assert.equal(manifest.publicSurface.components.PropertyUnitCard, componentValue); - assert.deepEqual(Object.keys(registration), ['moduleId']); - assert.equal(getVerticalRuntimeActions(registration)[0], action); - const loadDashboard = getVerticalRuntimeEntrypoints(registration).components['dashboard']; - assert.ok(loadDashboard); - assert.equal(yield* Effect.promise(loadDashboard), componentValue); - assert.deepEqual(descriptors, { - actions: [ + ], + reports: [ { - actionKey: 'property.registry.create-unit', - auditProfile: 'standard', - entrypoint: action.descriptor.entrypoint, - idempotency: 'required', - legalEntityScope: 'optional', + accessFiltering: 'legal_entity_scope', + dimensions: ['legal_entity'], + key: 'property.unit-inventory', + label: 'Unit inventory', owningModuleId: 'property.registry', - schemaVersion: '1', + resourceTypes: ['property.unit'], + }, + ], + resourceTypes: [ + { + capabilities: { + graphVisible: true, + linkable: true, + mediaAttachable: true, + searchable: true, + timelineVisible: true, + }, + description: 'A physical unit', + key: 'property.unit', + label: 'Unit', + owningModuleId: 'property.registry', + }, + ], + search: [ + { + accessFiltering: 'legal_entity_scope', + key: 'property.unit-search', + owningModuleId: 'property.registry', + resourceType: 'property.unit', }, ], - moduleId: 'property.registry', - outboxSubscriptions: [], shellContributions: emptyManifestInput().publicSurface.shellContributions, - }); - }), - runEffectTestPromise, - ), + }, + }); + const registration = defineVerticalRuntimeRegistration({ + actions: [action], + entrypoints: { + api: { resource: () => Promise.resolve(apiValue) }, + components: { + dashboard: () => Promise.resolve(componentValue), + }, + pages: {}, + reports: {}, + search: {}, + }, + manifest, + outboxWorkers: [], + }); + const descriptors = extractVerticalRuntimeSafeDescriptors(registration); + + expect(manifest.publicSurface.actions[0]).toBe(action); + expect(manifest.publicSurface.api.PropertyClient).toBe(apiValue); + expect(manifest.publicSurface.api.PropertyDetail).toBe(parameterizedApiValue); + expect(manifest.publicSurface.components.PropertyUnitCard).toBe(componentValue); + expect(Object.keys(registration)).toEqual(['moduleId']); + expect(getVerticalRuntimeActions(registration)[0]).toBe(action); + const loadDashboard = getVerticalRuntimeEntrypoints(registration).components['dashboard']; + expect(loadDashboard).toBeDefined(); + if (loadDashboard === undefined) { + throw new Error('Expected assertion to hold'); + } + expect(yield* Effect.promise(loadDashboard)).toBe(componentValue); + expect(descriptors).toEqual({ + actions: [ + { + actionKey: 'property.registry.create-unit', + auditProfile: 'standard', + entrypoint: action.descriptor.entrypoint, + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleId: 'property.registry', + schemaVersion: '1', + }, + ], + moduleId: 'property.registry', + outboxSubscriptions: [], + shellContributions: emptyManifestInput().publicSurface.shellContributions, + }); + }), ); -void test('rejects invalid identities, private fields, duplicates, cross-owner values, and undeclared references', () => { - assert.throws(() => +it('rejects invalid identities, private fields, duplicates, cross-owner values, and undeclared references', () => { + expect(() => defineOntosModuleManifest({ ...emptyManifestInput(), module: { ...emptyManifestInput().module, id: 'property-registry' }, }), - ); + ).toThrow(); const privateRoutesInput = { ...emptyManifestInput(), privateRoutes: [], }; - assert.throws(() => + expect(() => validateOntosModuleManifestFields(privateRoutesInput, privateRoutesInput.publicSurface), - ); + ).toThrow(); const dependenciesInput = { ...emptyManifestInput(), dependencies: { core: [], externalSystems: [], modules: [] }, }; - assert.throws(() => + expect(() => validateOntosModuleManifestFields(dependenciesInput, dependenciesInput.publicSurface), - ); - assert.throws(() => + ).toThrow(); + expect(() => defineOntosModuleManifest({ ...emptyManifestInput(), activation: { @@ -252,8 +250,8 @@ void test('rejects invalid identities, private fields, duplicates, cross-owner v supportedStates: ['inactive', 'inactive'], }, }), - ); - assert.throws(() => + ).toThrow(); + expect(() => defineOntosModuleManifest({ ...emptyManifestInput(), publicSurface: { @@ -261,8 +259,8 @@ void test('rejects invalid identities, private fields, duplicates, cross-owner v actions: [createAction('billing.invoice')], }, }), - ); - assert.throws(() => + ).toThrow(); + expect(() => defineOntosModuleManifest({ ...emptyManifestInput(), publicSurface: { @@ -277,45 +275,39 @@ void test('rejects invalid identities, private fields, duplicates, cross-owner v ], }, }), - ); - assert.throws( - () => - validateOntosModuleExecutableReferences( - [ - { - descriptor: { - actionKey: 'property.registry.fake', - auditProfile: 'minimal', - idempotency: 'optional', - legalEntityScope: 'optional', - owningModuleKey: 'property.registry', - schemaVersion: '1', - }, + ).toThrow(); + expect(() => + validateOntosModuleExecutableReferences( + [ + { + descriptor: { + actionKey: 'property.registry.fake', + auditProfile: 'minimal', + idempotency: 'optional', + legalEntityScope: 'optional', + owningModuleKey: 'property.registry', + schemaVersion: '1', }, - ], - [], - [], - [], - 'property.registry', - ), - /real values created by defineAction/u, - ); - assert.throws( - () => validateOntosModuleExecutableReferences([], [42], [], [], 'property.registry'), - /real Effect HttpApi/u, - ); - assert.throws( - () => - validateOntosModuleExecutableReferences([], [], ['not-a-component'], [], 'property.registry'), - /callable component/u, - ); - assert.throws( - () => validateOntosModuleExecutableReferences([], [], [], [{}], 'property.registry'), - /Effect Schema value/u, - ); + }, + ], + [], + [], + [], + 'property.registry', + ), + ).toThrow(/real values created by defineAction/u); + expect(() => + validateOntosModuleExecutableReferences([], [42], [], [], 'property.registry'), + ).toThrow(/real Effect HttpApi/u); + expect(() => + validateOntosModuleExecutableReferences([], [], ['not-a-component'], [], 'property.registry'), + ).toThrow(/callable component/u); + expect(() => + validateOntosModuleExecutableReferences([], [], [], [{}], 'property.registry'), + ).toThrow(/Effect Schema value/u); }); -void test('deployment contract decoding is exact and versioned', () => { +it('deployment contract decoding is exact and versioned', () => { const contract = { deployment: { appId: 'property-registry', buildMarker: 'build-1' }, manifest: { @@ -335,12 +327,12 @@ void test('deployment contract decoding is exact and versioned', () => { schemaVersion: ONTOS_MODULE_CONTRACT_SCHEMA_VERSION, }; - assert.deepEqual(decodeOntosModuleDeploymentContract(contract), contract); - assert.equal(contract.schemaVersion, '2'); - assert.throws(() => + expect(decodeOntosModuleDeploymentContract(contract)).toEqual(contract); + expect(contract.schemaVersion).toBe('2'); + expect(() => decodeOntosModuleDeploymentContract({ ...contract, sourcePath: './private.ts' }), - ); - assert.throws(() => + ).toThrow(); + expect(() => decodeOntosModuleDeploymentContract({ ...contract, manifest: { @@ -348,7 +340,9 @@ void test('deployment contract decoding is exact and versioned', () => { dependencies: { core: [], externalSystems: [], modules: [] }, }, }), - ); - assert.throws(() => decodeOntosModuleDeploymentContract({ ...contract, schemaVersion: '0' })); - assert.throws(() => decodeOntosModuleDeploymentContract({ ...contract, schemaVersion: '999' })); + ).toThrow(); + expect(() => decodeOntosModuleDeploymentContract({ ...contract, schemaVersion: '0' })).toThrow(); + expect(() => + decodeOntosModuleDeploymentContract({ ...contract, schemaVersion: '999' }), + ).toThrow(); }); diff --git a/app/packages/core-runtime/tests/unit/module-state-gate.test.ts b/app/packages/core-runtime/tests/unit/module-state-gate.test.ts index 1a8508555..0bbca04bf 100644 --- a/app/packages/core-runtime/tests/unit/module-state-gate.test.ts +++ b/app/packages/core-runtime/tests/unit/module-state-gate.test.ts @@ -1,7 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off lazyEffect:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Effect, Tracer, Predicate } from 'effect'; import { MODULE_ENTRYPOINT_ACCESSES, @@ -86,26 +83,23 @@ const expectedAllowed = { suspended: accessSet('historical_read'), } satisfies Readonly>>; -void test('encodes the exhaustive state/access matrix once, including missing state', () => { +it('encodes the exhaustive state/access matrix once, including missing state', () => { for (const state of TENANT_MODULE_STATES) { for (const access of MODULE_ENTRYPOINT_ACCESSES) { - assert.equal( - decideModuleStateAccess(state, access), + expect(decideModuleStateAccess(state, access), `${state}/${access}`).toBe( expectedAllowed[state].has(access) ? 'allow' : 'deny', - `${state}/${access}`, ); } } for (const access of MODULE_ENTRYPOINT_ACCESSES) { - assert.equal(decideModuleStateAccess(null, access), 'deny'); - assert.deepEqual( - tenantStatesAllowingAccess(access), + expect(decideModuleStateAccess(null, access)).toBe('deny'); + expect(tenantStatesAllowingAccess(access)).toEqual( TENANT_MODULE_STATES.filter((state) => expectedAllowed[state].has(access)).toSorted(), ); } }); -void test('constructs frozen tenant and explicit system entrypoints and rejects forged combinations', () => { +it('constructs frozen tenant and explicit system entrypoints and rejects forged combinations', () => { const tenant = defineTenantModuleEntrypoint({ access: 'write', authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, @@ -120,18 +114,18 @@ void test('constructs frozen tenant and explicit system entrypoints and rejects moduleKey: 'core.modules', role: 'action', }); - assert.equal(Object.isFrozen(tenant), true); - assert.equal(tenant.scope, 'tenant'); - assert.equal(system.scope, 'system'); - assert.throws(() => + expect(Object.isFrozen(tenant)).toBe(true); + expect(tenant.scope).toBe('tenant'); + expect(system.scope).toBe('system'); + expect(() => decodeTenantModuleEntrypoint({ access: 'read', entrypointKey: 'inventory.stock.reserve', moduleKey: 'inventory.stock', role: 'action', }), - ); - assert.throws(() => + ).toThrow(); + expect(() => defineSystemModuleEntrypoint({ access: 'write', authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, @@ -139,7 +133,7 @@ void test('constructs frozen tenant and explicit system entrypoints and rejects moduleKey: 'inventory.stock', role: 'action', }), - ); + ).toThrow(); for (const [role, access] of [ ['page', 'read'], ['public_component', 'historical_read'], @@ -148,7 +142,7 @@ void test('constructs frozen tenant and explicit system entrypoints and rejects ['report', 'read'], ['worker', 'background'], ] as const) { - assert.equal( + expect( defineTenantModuleEntrypoint({ access, authorization: @@ -159,353 +153,354 @@ void test('constructs frozen tenant and explicit system entrypoints and rejects moduleKey: 'inventory.stock', role, }).role, - role, - ); + ).toBe(role); } }); -void test('deduplicates one batch, reuses an immutable snapshot, and fails undeclared keys closed', async () => { - let reads = 0; - let observedKeys: readonly string[] = []; - const service: TenantModuleStateServiceContract = { - getTenantModuleStates: (_tenantId, moduleKeys) => { - reads += 1; - observedKeys = moduleKeys; - return Effect.succeed( - moduleKeys.map((moduleKey) => ({ - moduleKey, - state: moduleKey === 'billing.invoice' ? ('read_only' as const) : ('active' as const), - })), +it.effect( + 'deduplicates one batch, reuses an immutable snapshot, and fails undeclared keys closed', + () => + Effect.gen(function* reuseSnapshot() { + let reads = 0; + let observedKeys: readonly string[] = []; + const service: TenantModuleStateServiceContract = { + getTenantModuleStates: (_tenantId, moduleKeys) => { + reads += 1; + observedKeys = moduleKeys; + return Effect.succeed( + moduleKeys.map((moduleKey) => ({ + moduleKey, + state: moduleKey === 'billing.invoice' ? ('read_only' as const) : ('active' as const), + })), + ); + }, + listActiveTenantModules: () => Effect.succeed([]), + listTenantModuleStates: () => Effect.succeed([]), + }; + const descriptors = [ + defineTenantModuleEntrypoint({ + access: 'read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'inventory.stock.page', + moduleKey: 'inventory.stock', + role: 'page', + }), + defineTenantModuleEntrypoint({ + access: 'historical_read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'inventory.stock.report', + moduleKey: 'inventory.stock', + role: 'report', + }), + defineTenantModuleEntrypoint({ + access: 'read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'billing.invoice.search', + moduleKey: 'billing.invoice', + role: 'search', + }), + ] as const; + const snapshot = yield* prepareModuleStateSnapshot(service, 'tenant-1', descriptors); + expect(observedKeys).toEqual(['billing.invoice', 'inventory.stock']); + expect(reads).toBe(1); + expect(Object.isFrozen(snapshot)).toBe(true); + expect(Object.isFrozen(snapshot.entrypointKeys)).toBe(true); + expect(Object.isFrozen(snapshot.moduleKeys)).toBe(true); + yield* checkModuleEntrypoint(snapshot, descriptors[0]); + yield* checkModuleEntrypoint(snapshot, descriptors[0]); + expect(reads).toBe(1); + + const undeclared = defineTenantModuleEntrypoint({ + access: 'read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'people.directory.page', + moduleKey: 'people.directory', + role: 'page', + }); + const failure = yield* Effect.flip(checkModuleEntrypoint(snapshot, undeclared)); + expect(Predicate.isTagged(failure, 'ModuleStateCheckUnavailableError')).toBe(true); + const undeclaredSameModule = defineTenantModuleEntrypoint({ + access: 'write', + authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, + entrypointKey: 'inventory.stock.undeclared-action', + moduleKey: 'inventory.stock', + role: 'action', + }); + const sameModuleFailure = yield* Effect.flip( + checkModuleEntrypoint(snapshot, undeclaredSameModule), ); - }, - listActiveTenantModules: () => Effect.succeed([]), - listTenantModuleStates: () => Effect.succeed([]), - }; - const descriptors = [ - defineTenantModuleEntrypoint({ + expect(Predicate.isTagged(sameModuleFailure, 'ModuleStateCheckUnavailableError')).toBe(true); + expect(reads).toBe(1); + }), +); + +it.effect('records safe acquisition and evaluation telemetry including snapshot reuse', () => + Effect.gen(function* recordTelemetry() { + const spans: Tracer.Span[] = []; + const tracer = makeRecordingTracer(spans); + const descriptor = defineTenantModuleEntrypoint({ access: 'read', authorization: { kind: 'context_permission', permission: 'module.access' }, entrypointKey: 'inventory.stock.page', moduleKey: 'inventory.stock', role: 'page', - }), - defineTenantModuleEntrypoint({ - access: 'historical_read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'inventory.stock.report', - moduleKey: 'inventory.stock', - role: 'report', - }), - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'billing.invoice.search', - moduleKey: 'billing.invoice', - role: 'search', - }), - ] as const; - const snapshot = await runEffectTestPromise( - prepareModuleStateSnapshot(service, 'tenant-1', descriptors), - ); - assert.deepEqual(observedKeys, ['billing.invoice', 'inventory.stock']); - assert.equal(reads, 1); - assert.equal(Object.isFrozen(snapshot), true); - assert.equal(Object.isFrozen(snapshot.entrypointKeys), true); - assert.equal(Object.isFrozen(snapshot.moduleKeys), true); - await runEffectTestPromise(checkModuleEntrypoint(snapshot, descriptors[0])); - await runEffectTestPromise(checkModuleEntrypoint(snapshot, descriptors[0])); - assert.equal(reads, 1); - - const undeclared = defineTenantModuleEntrypoint({ - access: 'read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'people.directory.page', - moduleKey: 'people.directory', - role: 'page', - }); - const failure = await runEffectTestPromise( - Effect.flip(checkModuleEntrypoint(snapshot, undeclared)), - ); - assert.equal(failure._tag, 'ModuleStateCheckUnavailableError'); - const undeclaredSameModule = defineTenantModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'inventory.stock.undeclared-action', - moduleKey: 'inventory.stock', - role: 'action', - }); - const sameModuleFailure = await runEffectTestPromise( - Effect.flip(checkModuleEntrypoint(snapshot, undeclaredSameModule)), - ); - assert.equal(sameModuleFailure._tag, 'ModuleStateCheckUnavailableError'); - assert.equal(reads, 1); -}); - -void test('records safe acquisition and evaluation telemetry including snapshot reuse', async () => { - const spans: Tracer.Span[] = []; - const tracer = makeRecordingTracer(spans); - const descriptor = defineTenantModuleEntrypoint({ - access: 'read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'inventory.stock.page', - moduleKey: 'inventory.stock', - role: 'page', - }); - const service: TenantModuleStateServiceContract = { - getTenantModuleStates: () => - Effect.succeed([{ moduleKey: 'inventory.stock', state: 'active' }]), - listActiveTenantModules: () => Effect.succeed([]), - listTenantModuleStates: () => Effect.succeed([]), - }; + }); + const service: TenantModuleStateServiceContract = { + getTenantModuleStates: () => + Effect.succeed([{ moduleKey: 'inventory.stock', state: 'active' }]), + listActiveTenantModules: () => Effect.succeed([]), + listTenantModuleStates: () => Effect.succeed([]), + }; - await runEffectTestPromise( - Effect.gen(function* telemetryEffect() { + yield* Effect.gen(function* telemetryEffect() { const snapshot = yield* prepareModuleStateSnapshot(service, 'tenant-1', [descriptor]); yield* checkModuleEntrypoint(snapshot, descriptor); yield* checkModuleEntrypoint(snapshot, descriptor); yield* Effect.exit(prepareModuleStateSnapshot(service, '', [descriptor])); - }).pipe(Effect.provideService(Tracer.Tracer, tracer)), - ); + }).pipe(Effect.provideService(Tracer.Tracer, tracer)); - const acquisitions = spans.filter((span) => span.name === 'ModuleStateGate.acquire'); - const evaluations = spans.filter((span) => span.name === 'ModuleStateGate.evaluate'); - assert.equal(acquisitions.length, 2); - assert.equal(evaluations.length, 2); - assert.equal(acquisitions[0]?.attributes.get('batchSize'), 1); - assert.equal(acquisitions[0]?.attributes.get('outcome'), 'available'); - assert.equal(Predicate.isNumber(acquisitions[0]?.attributes.get('elapsedMs')), true); - assert.equal(acquisitions[1]?.attributes.get('outcome'), 'unavailable'); - assert.equal(evaluations[0]?.attributes.get('access'), 'read'); - assert.equal(evaluations[0]?.attributes.get('outcome'), 'allow'); - assert.equal(evaluations[0]?.attributes.get('scope'), 'tenant'); - assert.equal(evaluations[0]?.attributes.get('snapshotReuse'), false); - assert.equal(evaluations[1]?.attributes.get('snapshotReuse'), true); + const acquisitions = spans.filter((span) => span.name === 'ModuleStateGate.acquire'); + const evaluations = spans.filter((span) => span.name === 'ModuleStateGate.evaluate'); + expect(acquisitions.length).toBe(2); + expect(evaluations.length).toBe(2); + expect(acquisitions[0]?.attributes.get('batchSize')).toBe(1); + expect(acquisitions[0]?.attributes.get('outcome')).toBe('available'); + expect(Predicate.isNumber(acquisitions[0]?.attributes.get('elapsedMs'))).toBe(true); + expect(acquisitions[1]?.attributes.get('outcome')).toBe('unavailable'); + expect(evaluations[0]?.attributes.get('access')).toBe('read'); + expect(evaluations[0]?.attributes.get('outcome')).toBe('allow'); + expect(evaluations[0]?.attributes.get('scope')).toBe('tenant'); + expect(evaluations[0]?.attributes.get('snapshotReuse')).toBe(false); + expect(evaluations[1]?.attributes.get('snapshotReuse')).toBe(true); - for (const span of spans) { - for (const key of span.attributes.keys()) { - assert.doesNotMatch(key, /entrypoint|module|payload|principal|tenant/u); + for (const span of spans) { + for (const key of span.attributes.keys()) { + expect(key).not.toMatch(/entrypoint|module|payload|principal|tenant/u); + } } - } -}); - -void test('empty and system-only compositions perform zero reads', async () => { - let reads = 0; - const service: TenantModuleStateServiceContract = { - getTenantModuleStates: () => { - reads += 1; - return Effect.succeed([]); - }, - listActiveTenantModules: () => Effect.succeed([]), - listTenantModuleStates: () => Effect.succeed([]), - }; - const system = defineSystemModuleEntrypoint({ - access: 'read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'core.audit.page', - moduleKey: 'core.audit', - role: 'page', - }); - const empty = await runEffectTestPromise(prepareModuleStateSnapshot(service, 'tenant-1', [])); - const systemOnly = await runEffectTestPromise( - prepareModuleStateSnapshot(service, 'tenant-1', [system]), - ); - await runEffectTestPromise(checkModuleEntrypoint(systemOnly, system)); - assert.deepEqual(empty.moduleKeys, []); - assert.equal(reads, 0); -}); + }), +); -void test('the gateway rejects missing trusted principal context before state acquisition', async () => { - let reads = 0; - const descriptor = defineTenantModuleEntrypoint({ - access: 'read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'inventory.stock.page', - moduleKey: 'inventory.stock', - role: 'page', - }); - const gate = makeModuleStateGate({ - getTenantModuleStates: () => { - reads += 1; - return Effect.succeed([]); - }, - listActiveTenantModules: () => Effect.succeed([]), - listTenantModuleStates: () => Effect.succeed([]), - }); - const failure = await runEffectTestPromise( - Effect.flip(makeModuleEntrypointGateway(gate).prepareSnapshotInput({}, [descriptor])), - ); - assert.equal(failure._tag, 'ModuleStateCheckUnavailableError'); - assert.equal(reads, 0); -}); - -void test('gates every future entrypoint category before its fake implementation load', async () => { - let reads = 0; - let authorizationCalls = 0; - let loadCalls = 0; - const records = [ - { moduleKey: 'module.active', state: 'active' }, - { moduleKey: 'module.archived', state: 'archived' }, - { moduleKey: 'module.deprecated', state: 'deprecated' }, - { moduleKey: 'module.inactive', state: 'inactive' }, - { moduleKey: 'module.read-only', state: 'read_only' }, - { moduleKey: 'module.suspended', state: 'suspended' }, - ] as const; - const gateway = makeModuleEntrypointGateway( - makeModuleStateGate({ - getTenantModuleStates: (_tenantId, moduleKeys) => { +it.effect('empty and system-only compositions perform zero reads', () => + Effect.gen(function* skipEmptyReads() { + let reads = 0; + const service: TenantModuleStateServiceContract = { + getTenantModuleStates: () => { reads += 1; - return Effect.succeed(records.filter((record) => moduleKeys.includes(record.moduleKey))); + return Effect.succeed([]); }, listActiveTenantModules: () => Effect.succeed([]), listTenantModuleStates: () => Effect.succeed([]), - }), - ); - const allowed = [ - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'module.active.page', - moduleKey: 'module.active', - role: 'page', - }), - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'module.active.component', - moduleKey: 'module.active', - role: 'public_component', - }), - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'module.read-only.api', - moduleKey: 'module.read-only', - role: 'api', - }), - defineTenantModuleEntrypoint({ - access: 'historical_read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'module.suspended.api-history', - moduleKey: 'module.suspended', - role: 'api', - }), - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'module.deprecated.search', - moduleKey: 'module.deprecated', - role: 'search', - }), - defineTenantModuleEntrypoint({ - access: 'historical_read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'module.inactive.search-history', - moduleKey: 'module.inactive', - role: 'search', - }), - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'module.deprecated.report', - moduleKey: 'module.deprecated', - role: 'report', - }), - defineTenantModuleEntrypoint({ - access: 'historical_read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'module.archived.report-history', - moduleKey: 'module.archived', - role: 'report', - }), - defineSystemModuleEntrypoint({ + }; + const system = defineSystemModuleEntrypoint({ access: 'read', authorization: { kind: 'context_permission', permission: 'module.access' }, entrypointKey: 'core.audit.page', moduleKey: 'core.audit', role: 'page', - }), - ] as const; - const denied = [ - defineTenantModuleEntrypoint({ - access: 'write', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'module.read-only.api-write', - moduleKey: 'module.read-only', - role: 'api', - }), - defineTenantModuleEntrypoint({ - access: 'read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'module.inactive.search', - moduleKey: 'module.inactive', - role: 'search', - }), - defineTenantModuleEntrypoint({ + }); + const empty = yield* prepareModuleStateSnapshot(service, 'tenant-1', []); + const systemOnly = yield* prepareModuleStateSnapshot(service, 'tenant-1', [system]); + yield* checkModuleEntrypoint(systemOnly, system); + expect(empty.moduleKeys).toEqual([]); + expect(reads).toBe(0); + }), +); + +it.effect('the gateway rejects missing trusted principal context before state acquisition', () => + Effect.gen(function* rejectMissingPrincipal() { + let reads = 0; + const descriptor = defineTenantModuleEntrypoint({ access: 'read', authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'module.archived.report', - moduleKey: 'module.archived', - role: 'report', - }), - defineTenantModuleEntrypoint({ - access: 'historical_read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'module.missing.report-history', - moduleKey: 'module.missing', - role: 'report', - }), - ] as const; - const snapshot = await runEffectTestPromise( - gateway.prepareSnapshot(trustedContext(), [...allowed, ...denied]), - ); - assert.equal(reads, 1); - assert.equal(snapshot.moduleKeys.includes('core.audit'), false); - const run = (entrypoint: (typeof allowed)[number] | (typeof denied)[number]) => - gateway.run({ - authorize: Effect.sync(() => { - authorizationCalls += 1; + entrypointKey: 'inventory.stock.page', + moduleKey: 'inventory.stock', + role: 'page', + }); + const gate = makeModuleStateGate({ + getTenantModuleStates: () => { + reads += 1; + return Effect.succeed([]); + }, + listActiveTenantModules: () => Effect.succeed([]), + listTenantModuleStates: () => Effect.succeed([]), + }); + const failure = yield* Effect.flip( + makeModuleEntrypointGateway(gate).prepareSnapshotInput({}, [descriptor]), + ); + expect(Predicate.isTagged(failure, 'ModuleStateCheckUnavailableError')).toBe(true); + expect(reads).toBe(0); + }), +); + +it.effect('gates every future entrypoint category before its fake implementation load', () => + Effect.gen(function* gateEntrypoints() { + let reads = 0; + let authorizationCalls = 0; + let loadCalls = 0; + const records = [ + { moduleKey: 'module.active', state: 'active' }, + { moduleKey: 'module.archived', state: 'archived' }, + { moduleKey: 'module.deprecated', state: 'deprecated' }, + { moduleKey: 'module.inactive', state: 'inactive' }, + { moduleKey: 'module.read-only', state: 'read_only' }, + { moduleKey: 'module.suspended', state: 'suspended' }, + ] as const; + const gateway = makeModuleEntrypointGateway( + makeModuleStateGate({ + getTenantModuleStates: (_tenantId, moduleKeys) => { + reads += 1; + return Effect.succeed(records.filter((record) => moduleKeys.includes(record.moduleKey))); + }, + listActiveTenantModules: () => Effect.succeed([]), + listTenantModuleStates: () => Effect.succeed([]), }), - entrypoint, - load: Effect.sync(() => { - loadCalls += 1; + ); + const allowed = [ + defineTenantModuleEntrypoint({ + access: 'read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'module.active.page', + moduleKey: 'module.active', + role: 'page', }), - snapshot, - }); - await Promise.all(allowed.map(async (entrypoint) => await runEffectTestPromise(run(entrypoint)))); - const deniedFailures = await Promise.all( - denied.map(async (entrypoint) => await runEffectTestPromise(Effect.flip(run(entrypoint)))), - ); - for (const failure of deniedFailures) { - assert.equal(failure._tag, 'ModuleStateDeniedError'); - } - assert.equal(authorizationCalls, allowed.length); - assert.equal(loadCalls, allowed.length); - assert.equal(reads, 1); -}); + defineTenantModuleEntrypoint({ + access: 'read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'module.active.component', + moduleKey: 'module.active', + role: 'public_component', + }), + defineTenantModuleEntrypoint({ + access: 'read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'module.read-only.api', + moduleKey: 'module.read-only', + role: 'api', + }), + defineTenantModuleEntrypoint({ + access: 'historical_read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'module.suspended.api-history', + moduleKey: 'module.suspended', + role: 'api', + }), + defineTenantModuleEntrypoint({ + access: 'read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'module.deprecated.search', + moduleKey: 'module.deprecated', + role: 'search', + }), + defineTenantModuleEntrypoint({ + access: 'historical_read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'module.inactive.search-history', + moduleKey: 'module.inactive', + role: 'search', + }), + defineTenantModuleEntrypoint({ + access: 'read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'module.deprecated.report', + moduleKey: 'module.deprecated', + role: 'report', + }), + defineTenantModuleEntrypoint({ + access: 'historical_read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'module.archived.report-history', + moduleKey: 'module.archived', + role: 'report', + }), + defineSystemModuleEntrypoint({ + access: 'read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'core.audit.page', + moduleKey: 'core.audit', + role: 'page', + }), + ] as const; + const denied = [ + defineTenantModuleEntrypoint({ + access: 'write', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'module.read-only.api-write', + moduleKey: 'module.read-only', + role: 'api', + }), + defineTenantModuleEntrypoint({ + access: 'read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'module.inactive.search', + moduleKey: 'module.inactive', + role: 'search', + }), + defineTenantModuleEntrypoint({ + access: 'read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'module.archived.report', + moduleKey: 'module.archived', + role: 'report', + }), + defineTenantModuleEntrypoint({ + access: 'historical_read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'module.missing.report-history', + moduleKey: 'module.missing', + role: 'report', + }), + ] as const; + const snapshot = yield* gateway.prepareSnapshot(trustedContext(), [...allowed, ...denied]); + expect(reads).toBe(1); + expect(snapshot.moduleKeys.includes('core.audit')).toBe(false); + const run = (entrypoint: (typeof allowed)[number] | (typeof denied)[number]) => + gateway.run({ + authorize: Effect.sync(() => { + authorizationCalls += 1; + }), + entrypoint, + load: Effect.sync(() => { + loadCalls += 1; + }), + snapshot, + }); + yield* Effect.forEach(allowed, run, { concurrency: 'unbounded' }); + const deniedFailures = yield* Effect.forEach( + denied, + (entrypoint) => Effect.flip(run(entrypoint)), + { concurrency: 'unbounded' }, + ); + for (const failure of deniedFailures) { + expect(Predicate.isTagged(failure, 'ModuleStateDeniedError')).toBe(true); + } + expect(authorizationCalls).toBe(allowed.length); + expect(loadCalls).toBe(allowed.length); + expect(reads).toBe(1); + }), +); -void test('the gateway never evaluates authorization or lazy implementation on denial', async () => { - const gate = makeModuleStateGate({ - getTenantModuleStates: () => - Effect.succeed([{ moduleKey: 'inventory.stock', state: 'read_only' }]), - listActiveTenantModules: () => Effect.succeed([]), - listTenantModuleStates: () => Effect.succeed([]), - }); - const gateway = makeModuleEntrypointGateway(gate); - const descriptor = defineTenantModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'inventory.stock.reserve', - moduleKey: 'inventory.stock', - role: 'action', - }); - const snapshot = await runEffectTestPromise( - gateway.prepareSnapshot(trustedContext(), [descriptor]), - ); - let authorizationCalls = 0; - let loadFactoryCalls = 0; - let loadCalls = 0; - const failure = await runEffectTestPromise( - Effect.flip( +it.effect('the gateway never evaluates authorization or lazy implementation on denial', () => + Effect.gen(function* denyBeforeLoading() { + const gate = makeModuleStateGate({ + getTenantModuleStates: () => + Effect.succeed([{ moduleKey: 'inventory.stock', state: 'read_only' }]), + listActiveTenantModules: () => Effect.succeed([]), + listTenantModuleStates: () => Effect.succeed([]), + }); + const gateway = makeModuleEntrypointGateway(gate); + const descriptor = defineTenantModuleEntrypoint({ + access: 'write', + authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, + entrypointKey: 'inventory.stock.reserve', + moduleKey: 'inventory.stock', + role: 'action', + }); + const snapshot = yield* gateway.prepareSnapshot(trustedContext(), [descriptor]); + let authorizationCalls = 0; + let loadFactoryCalls = 0; + let loadCalls = 0; + const failure = yield* Effect.flip( gateway.run({ authorize: Effect.sync(() => { authorizationCalls += 1; @@ -520,25 +515,25 @@ void test('the gateway never evaluates authorization or lazy implementation on d }), snapshot, }), - ), - ); - assert.equal(failure._tag, 'ModuleStateDeniedError'); - assert.equal(authorizationCalls, 0); - assert.equal(loadFactoryCalls, 0); - assert.equal(loadCalls, 0); -}); + ); + expect(Predicate.isTagged(failure, 'ModuleStateDeniedError')).toBe(true); + expect(authorizationCalls).toBe(0); + expect(loadFactoryCalls).toBe(0); + expect(loadCalls).toBe(0); + }), +); -void test('a missing row is a definite denial rather than an unavailable read', async () => { - const descriptor = defineTenantModuleEntrypoint({ - access: 'read', - authorization: { kind: 'context_permission', permission: 'module.access' }, - entrypointKey: 'inventory.stock.page', - moduleKey: 'inventory.stock', - role: 'page', - }); - const snapshot = makeModuleStateSnapshot('tenant-1', [descriptor], []); - const failure = await runEffectTestPromise( - Effect.flip(checkModuleEntrypoint(snapshot, descriptor)), - ); - assert.equal(failure._tag, 'ModuleStateDeniedError'); -}); +it.effect('a missing row is a definite denial rather than an unavailable read', () => + Effect.gen(function* denyMissingRow() { + const descriptor = defineTenantModuleEntrypoint({ + access: 'read', + authorization: { kind: 'context_permission', permission: 'module.access' }, + entrypointKey: 'inventory.stock.page', + moduleKey: 'inventory.stock', + role: 'page', + }); + const snapshot = makeModuleStateSnapshot('tenant-1', [descriptor], []); + const failure = yield* Effect.flip(checkModuleEntrypoint(snapshot, descriptor)); + expect(Predicate.isTagged(failure, 'ModuleStateDeniedError')).toBe(true); + }), +); diff --git a/app/packages/core-runtime/tests/unit/native-transaction-context.test.ts b/app/packages/core-runtime/tests/unit/native-transaction-context.test.ts index ebc7deb3c..1bb0ca200 100644 --- a/app/packages/core-runtime/tests/unit/native-transaction-context.test.ts +++ b/app/packages/core-runtime/tests/unit/native-transaction-context.test.ts @@ -1,112 +1,142 @@ -// @effect-diagnostics asyncFunction:off -- Node test runner and foreign driver fixtures require Promises; expires: 2026-12-31. -import { Clock, Config, ConfigProvider, Context, Effect, Logger, Option, References } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { + Clock, + Config, + ConfigProvider, + Context, + Effect, + Layer, + Logger, + Option, + References, + Tracer, +} from 'effect'; import { TestClock } from 'effect/testing'; -import assert from 'node:assert/strict'; -import test from 'node:test'; import { makeTestDatabase } from '../support/database.ts'; -import { runEffectTestPromise } from '../support/effect-runtime.ts'; -const executor = makeTestDatabase(() => Effect.succeed([])); +it.effect('preserves a caller Context.Reference override instead of its default', () => + Effect.gen(function* preserveReference() { + const executor = yield* makeTestDatabase(() => Effect.succeed([])); + const fallback = { source: 'default' }; + const override = { source: 'caller' }; + const reference = Context.Reference('native-transaction-context/reference', { + defaultValue: () => fallback, + }); + expect(yield* reference).toBe(fallback); + const actual = yield* executor + .transaction(() => reference) + .pipe(Effect.provideService(reference, override)); + expect(actual).toBe(override); + expect(yield* reference).toBe(fallback); + }), +); -test('preserves a caller Context.Reference override instead of its default', async () => { - const fallback = { source: 'default' }; - const override = { source: 'caller' }; - const reference = Context.Reference('native-transaction-context/reference', { - defaultValue: () => fallback, - }); - assert.equal(await runEffectTestPromise(reference), fallback); - const actual = await runEffectTestPromise( - executor.transaction(() => reference).pipe(Effect.provideService(reference, override)), - ); - assert.equal(actual, override); - assert.equal(await runEffectTestPromise(reference), fallback); -}); - -test('uses caller Clock operations inside the transaction', async () => { - let sleeps = 0; - const clock: Clock.Clock = { - currentTimeMillis: Effect.succeed(1234), - currentTimeMillisUnsafe: () => 1234, - currentTimeNanos: Effect.succeed(1_234_000_000n), - currentTimeNanosUnsafe: () => 1_234_000_000n, - monotonicTimeNanos: Effect.succeed(5_678_000_000n), - monotonicTimeNanosUnsafe: () => 5_678_000_000n, - sleep: () => - Effect.sync(() => { - sleeps += 1; - }), - }; - const actual = await runEffectTestPromise( - executor +it.effect('uses caller Clock operations inside the transaction', () => + Effect.gen(function* preserveClock() { + const executor = yield* makeTestDatabase(() => Effect.succeed([])); + let sleeps = 0; + const clock: Clock.Clock = { + currentTimeMillis: Effect.succeed(1234), + currentTimeMillisUnsafe: () => 1234, + currentTimeNanos: Effect.succeed(1_234_000_000n), + currentTimeNanosUnsafe: () => 1_234_000_000n, + monotonicTimeNanos: Effect.succeed(5_678_000_000n), + monotonicTimeNanosUnsafe: () => 5_678_000_000n, + sleep: () => + Effect.sync(() => { + sleeps += 1; + }), + }; + const actual = yield* executor .transaction(() => Effect.gen(function* callerProgram() { yield* Effect.sleep('1 millis'); return yield* Clock.currentTimeMillis; }), ) - .pipe(Effect.provideService(Clock.Clock, clock)), - ); - assert.equal(actual, 1234); - assert.equal(sleeps, 1); -}); + .pipe(Effect.provideService(Clock.Clock, clock)); + expect(actual).toBe(1234); + expect(sleeps).toBe(1); + }), +); -test('preserves a caller TestClock inside the transaction', async () => { - const actual = await runEffectTestPromise( - Effect.gen(function* virtualClockProgram() { +it.effect('preserves a caller TestClock inside the transaction', () => + Effect.gen(function* preserveTestClock() { + const executor = yield* makeTestDatabase(() => Effect.succeed([])); + const actual = yield* Effect.gen(function* virtualClockProgram() { const clock = yield* TestClock.make(); yield* clock.setTime(1234); return yield* executor .transaction(() => Clock.currentTimeMillis) .pipe(Effect.provideService(Clock.Clock, clock)); - }).pipe(Effect.scoped), - ); - assert.equal(actual, 1234); -}); + }).pipe(Effect.scoped); + expect(actual).toBe(1234); + }), +); -test('loads configuration from the caller provider inside the transaction', async () => { - const provider = ConfigProvider.fromUnknown({ NATIVE_CONTEXT_TEST_VALUE: 'caller-config' }); - const actual = await runEffectTestPromise( - executor +it.effect('loads configuration from the caller provider inside the transaction', () => + Effect.gen(function* preserveConfig() { + const executor = yield* makeTestDatabase(() => Effect.succeed([])); + const provider = ConfigProvider.fromUnknown({ NATIVE_CONTEXT_TEST_VALUE: 'caller-config' }); + const actual = yield* executor .transaction(() => Config.string('NATIVE_CONTEXT_TEST_VALUE')) - .pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)), - ); - assert.equal(actual, 'caller-config'); + .pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)); + expect(actual).toBe('caller-config'); + }), +); + +const spanPreservation = Effect.gen(function* preserveSpans() { + const executor = yield* makeTestDatabase(() => Effect.succeed([])); + const actual = yield* Effect.gen(function* callerProgram() { + const caller = yield* Effect.currentSpan; + const inner = yield* executor.transaction(() => + Effect.gen(function* transactionProgram() { + const parent = yield* Effect.currentParentSpan; + const child = yield* Effect.currentSpan.pipe(Effect.withSpan('transaction-child')); + return { child, parent }; + }), + ); + return { caller, ...inner }; + }).pipe(Effect.withSpan('transaction-caller')); + expect('name' in actual.parent).toBe(true); + if (!('name' in actual.parent)) { + throw new Error('Expected assertion to hold'); + } + expect(actual.parent.name).toBe('sql.transaction'); + expect(Option.isSome(actual.parent.parent)).toBe(true); + if (!Option.isSome(actual.parent.parent)) { + throw new Error('Expected assertion to hold'); + } + expect(actual.parent.parent.value).toBe(actual.caller); + expect(Option.isSome(actual.child.parent)).toBe(true); + if (!Option.isSome(actual.child.parent)) { + throw new Error('Expected assertion to hold'); + } + expect(actual.child.parent.value).toBe(actual.parent); }); -test('preserves the caller span and parents transaction child spans to it', async () => { - const actual = await runEffectTestPromise( - Effect.gen(function* callerProgram() { - const caller = yield* Effect.currentSpan; - const inner = yield* executor.transaction(() => - Effect.gen(function* transactionProgram() { - const parent = yield* Effect.currentParentSpan; - const child = yield* Effect.currentSpan.pipe(Effect.withSpan('transaction-child')); - return { child, parent }; - }), - ); - return { caller, ...inner }; - }).pipe(Effect.withSpan('transaction-caller')), +it.layer( + Layer.succeed(Tracer.Tracer, Tracer.make({ span: (options) => new Tracer.NativeSpan(options) })), +)('native transaction tracing', (tracingIt) => { + tracingIt.effect( + 'preserves the caller span and parents transaction child spans to it', + () => spanPreservation, ); - assert.ok('name' in actual.parent); - assert.equal(actual.parent.name, 'sql.transaction'); - assert.ok(Option.isSome(actual.parent.parent)); - assert.equal(actual.parent.parent.value, actual.caller); - assert.ok(Option.isSome(actual.child.parent)); - assert.equal(actual.child.parent.value, actual.parent); }); -test('emits transaction logs with caller annotations and logger', async () => { - const records: Effect.Success[] = []; - const logger = Logger.make(({ fiber }) => { - records.push(fiber.getRef(References.CurrentLogAnnotations)); - }); - await runEffectTestPromise( - executor +it.effect('emits transaction logs with caller annotations and logger', () => + Effect.gen(function* preserveLogging() { + const executor = yield* makeTestDatabase(() => Effect.succeed([])); + const records: Effect.Success[] = []; + const logger = Logger.make(({ fiber }) => { + records.push(fiber.getRef(References.CurrentLogAnnotations)); + }); + yield* executor .transaction(() => Effect.logInfo('transaction-body')) .pipe( Effect.annotateLogs({ operation: 'context-test', requestId: 'native-request' }), Effect.provideService(Logger.CurrentLoggers, new Set([logger])), - ), - ); - assert.deepEqual(records, [{ operation: 'context-test', requestId: 'native-request' }]); -}); + ); + expect(records).toEqual([{ operation: 'context-test', requestId: 'native-request' }]); + }), +); diff --git a/app/packages/core-runtime/tests/unit/native-transaction.test.ts b/app/packages/core-runtime/tests/unit/native-transaction.test.ts index d66e6c2cc..5dda7011b 100644 --- a/app/packages/core-runtime/tests/unit/native-transaction.test.ts +++ b/app/packages/core-runtime/tests/unit/native-transaction.test.ts @@ -1,17 +1,14 @@ -// @effect-diagnostics asyncFunction:off -- Node's test callback is the Effect execution boundary; expires: 2026-12-31. +import { expect, it } from 'effect-rstest'; import { sql } from 'drizzle-orm'; import { Cause, Context, Deferred, Effect, Exit, Fiber } from 'effect'; import { ConnectionError, SqlError } from 'effect/unstable/sql/SqlError'; -import assert from 'node:assert/strict'; -import test from 'node:test'; import { makeTestDatabase } from '../support/database.ts'; -import { runEffectTestPromise } from '../support/effect-runtime.ts'; -const harness = ( +const harness = Effect.fn(function* makeHarness( settle: (statement: string) => Effect.Effect = () => Effect.void, -) => { +) { const events: string[] = []; - const executor = makeTestDatabase((statement) => + const executor = yield* makeTestDatabase((statement) => Effect.gen(function* execute() { events.push(statement); yield* settle(statement); @@ -19,32 +16,32 @@ const harness = ( }), ); return { events, executor }; -}; +}); -test('native transactions preserve caller services and execute the body once', async () => { - class Service extends Context.Service()( - '@app/core-runtime/tests/unit/native-transaction.test/Service', - ) {} - const service = { value: {} }; - const h = harness(); - let calls = 0; - const value = await runEffectTestPromise( - h.executor +it.effect('native transactions preserve caller services and execute the body once', () => + Effect.gen(function* preserveCallerServices() { + class Service extends Context.Service()( + '@app/core-runtime/tests/unit/native-transaction.test/Service', + ) {} + const service = { value: {} }; + const h = yield* harness(); + let calls = 0; + const value = yield* h.executor .transaction(() => { calls += 1; return Service.pipe(Effect.map((current) => current.value)); }) - .pipe(Effect.provideService(Service, service)), - ); - assert.equal(value, service.value); - assert.equal(calls, 1); - assert.deepEqual(h.events, ['BEGIN', 'COMMIT']); -}); + .pipe(Effect.provideService(Service, service)); + expect(value).toBe(service.value); + expect(calls).toBe(1); + expect(h.events).toEqual(['BEGIN', 'COMMIT']); + }), +); -test('native isolation configuration precedes transaction queries', async () => { - const h = harness(); - await runEffectTestPromise( - h.executor.transaction((transaction) => +it.effect('native isolation configuration precedes transaction queries', () => + Effect.gen(function* configureIsolation() { + const h = yield* harness(); + yield* h.executor.transaction((transaction) => Effect.gen(function* snapshot() { yield* transaction.setTransaction({ accessMode: 'read only', @@ -52,155 +49,168 @@ test('native isolation configuration precedes transaction queries', async () => }); yield* transaction.execute(sql`select 1`, 'objects'); }), - ), - ); - assert.deepEqual(h.events, [ - 'BEGIN', - 'set transaction isolation level repeatable read read only', - 'select 1', - 'COMMIT', - ]); -}); + ); + expect(h.events).toEqual([ + 'BEGIN', + 'set transaction isolation level repeatable read read only', + 'select 1', + 'COMMIT', + ]); + }), +); for (const [name, cause] of [ ['typed failure', Cause.fail({ _tag: 'ExpectedFailure', identity: {} })], ['defect', Cause.die(new Error('body defect'))], ] as const) { - test(`native ${name} rolls back with the original cause`, async () => { - const h = harness(); - const exit = await runEffectTestPromise( - Effect.exit(h.executor.transaction(() => Effect.failCause(cause))), - ); - assert.ok(Exit.isFailure(exit)); - assert.deepEqual(exit.cause, cause); - assert.deepEqual(h.events, ['BEGIN', 'ROLLBACK']); - }); + it.effect(`native ${name} rolls back with the original cause`, () => + Effect.gen(function* rollbackOriginalCause() { + const h = yield* harness(); + const exit = yield* Effect.exit(h.executor.transaction(() => Effect.failCause(cause))); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) { + throw new Error('Expected assertion to hold'); + } + expect(exit.cause).toEqual(cause); + expect(h.events).toEqual(['BEGIN', 'ROLLBACK']); + }), + ); } -test('a synchronous body construction throw rolls back', async () => { - const h = harness(); - const defect = new Error('construction defect'); - const exit = await runEffectTestPromise( - Effect.exit( +it.effect('a synchronous body construction throw rolls back', () => + Effect.gen(function* rollbackConstructionThrow() { + const h = yield* harness(); + const defect = new Error('construction defect'); + const exit = yield* Effect.exit( h.executor.transaction((): Effect.Effect => { throw defect; }), - ), - ); - assert.ok(Exit.isFailure(exit)); - assert.deepEqual(exit.cause, Cause.die(defect)); - assert.deepEqual(h.events, ['BEGIN', 'ROLLBACK']); -}); + ); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) { + throw new Error('Expected assertion to hold'); + } + expect(exit.cause).toEqual(Cause.die(defect)); + expect(h.events).toEqual(['BEGIN', 'ROLLBACK']); + }), +); for (const phase of ['COMMIT', 'ROLLBACK']) { - test(`native ${phase} failure surfaces the SQL defect`, async () => { - const failure = new SqlError({ - reason: new ConnectionError({ cause: new Error(`${phase} failed`) }), - }); - const h = harness((statement) => (statement === phase ? Effect.fail(failure) : Effect.void)); - const exit = await runEffectTestPromise( - Effect.exit( + it.effect(`native ${phase} failure surfaces the SQL defect`, () => + Effect.gen(function* surfaceSettlementFailure() { + const failure = new SqlError({ + reason: new ConnectionError({ cause: new Error(`${phase} failed`) }), + }); + const h = yield* harness((statement) => + statement === phase ? Effect.fail(failure) : Effect.void, + ); + const exit = yield* Effect.exit( h.executor.transaction(() => phase === 'COMMIT' ? Effect.succeed(42) : Effect.fail('body failure'), ), - ), - ); - assert.ok(Exit.isFailure(exit)); - assert.ok( - exit.cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect === failure), - ); - assert.deepEqual(h.events, ['BEGIN', phase]); - }); + ); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) { + throw new Error('Expected assertion to hold'); + } + expect( + exit.cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect === failure), + ).toBe(true); + expect(h.events).toEqual(['BEGIN', phase]); + }), + ); } -test( +it.effect( 'interruption waits for body finalizers and native rollback settlement', - { timeout: 2000 }, - async () => { - const result = await runEffectTestPromise( - Effect.gen(function* interruptTransaction() { + () => + Effect.gen(function* waitForRollback() { + const started = yield* Deferred.make(); + const finalizing = yield* Deferred.make(); + const releaseFinalizer = yield* Deferred.make(); + const rollingBack = yield* Deferred.make(); + const releaseRollback = yield* Deferred.make(); + const h = yield* harness((statement) => + statement === 'ROLLBACK' + ? Deferred.succeed(rollingBack, null).pipe( + Effect.andThen(Deferred.await(releaseRollback)), + ) + : Effect.void, + ); + const fiber = yield* h.executor + .transaction(() => + Deferred.succeed(started, null).pipe( + Effect.andThen(Effect.never), + Effect.ensuring( + Deferred.succeed(finalizing, null).pipe( + Effect.andThen(Deferred.await(releaseFinalizer)), + ), + ), + ), + ) + .pipe(Effect.forkChild); + yield* Deferred.await(started); + const interrupt = yield* Fiber.interrupt(fiber).pipe(Effect.forkChild); + yield* Deferred.await(finalizing); + expect(fiber.pollUnsafe()).toBe(undefined); + expect(h.events).toEqual(['BEGIN']); + yield* Deferred.succeed(releaseFinalizer, null); + yield* Deferred.await(rollingBack); + expect(fiber.pollUnsafe()).toBe(undefined); + yield* Deferred.succeed(releaseRollback, null); + yield* Fiber.join(interrupt); + const result = { events: h.events, exit: yield* Fiber.await(fiber) }; + expect(Exit.isFailure(result.exit)).toBe(true); + if (!Exit.isFailure(result.exit)) { + throw new Error('Expected assertion to hold'); + } + expect(Cause.hasInterrupts(result.exit.cause)).toBe(true); + expect(result.events).toEqual(['BEGIN', 'ROLLBACK']); + }), + 2000, +); + +for (const phase of ['COMMIT', 'ROLLBACK']) { + it.effect( + `interruption waits for native ${phase} and preserves a later SQL failure`, + () => + Effect.gen(function* preserveSettlementFailure() { + const failure = new SqlError({ + reason: new ConnectionError({ cause: new Error(`${phase} rejected`) }), + }); const started = yield* Deferred.make(); - const finalizing = yield* Deferred.make(); - const releaseFinalizer = yield* Deferred.make(); - const rollingBack = yield* Deferred.make(); - const releaseRollback = yield* Deferred.make(); - const h = harness((statement) => - statement === 'ROLLBACK' - ? Deferred.succeed(rollingBack, null).pipe( - Effect.andThen(Deferred.await(releaseRollback)), + const release = yield* Deferred.make(); + const h = yield* harness((statement) => + statement === phase + ? Deferred.succeed(started, null).pipe( + Effect.andThen(Deferred.await(release)), + Effect.andThen(Effect.fail(failure)), ) : Effect.void, ); const fiber = yield* h.executor .transaction(() => - Deferred.succeed(started, null).pipe( - Effect.andThen(Effect.never), - Effect.ensuring( - Deferred.succeed(finalizing, null).pipe( - Effect.andThen(Deferred.await(releaseFinalizer)), - ), - ), - ), + phase === 'COMMIT' ? Effect.succeed(42) : Effect.fail('domain failure'), ) .pipe(Effect.forkChild); yield* Deferred.await(started); const interrupt = yield* Fiber.interrupt(fiber).pipe(Effect.forkChild); - yield* Deferred.await(finalizing); - assert.equal(fiber.pollUnsafe(), undefined); - assert.deepEqual(h.events, ['BEGIN']); - yield* Deferred.succeed(releaseFinalizer, null); - yield* Deferred.await(rollingBack); - assert.equal(fiber.pollUnsafe(), undefined); - yield* Deferred.succeed(releaseRollback, null); + yield* Effect.yieldNow; + expect(fiber.pollUnsafe()).toBe(undefined); + yield* Deferred.succeed(release, null); yield* Fiber.join(interrupt); - return { events: h.events, exit: yield* Fiber.await(fiber) }; + const exit = yield* Fiber.await(fiber); + expect(Exit.isFailure(exit)).toBe(true); + if (!Exit.isFailure(exit)) { + throw new Error('Expected assertion to hold'); + } + // Native settlement defects take precedence over pending interruption. + expect( + exit.cause.reasons.some( + (reason) => Cause.isDieReason(reason) && reason.defect === failure, + ), + ).toBe(true); }), - ); - assert.ok(Exit.isFailure(result.exit)); - assert.equal(Cause.hasInterrupts(result.exit.cause), true); - assert.deepEqual(result.events, ['BEGIN', 'ROLLBACK']); - }, -); - -for (const phase of ['COMMIT', 'ROLLBACK']) { - test( - `interruption waits for native ${phase} and preserves a later SQL failure`, - { timeout: 2000 }, - async () => { - const failure = new SqlError({ - reason: new ConnectionError({ cause: new Error(`${phase} rejected`) }), - }); - const exit = await runEffectTestPromise( - Effect.gen(function* interruptSettlement() { - const started = yield* Deferred.make(); - const release = yield* Deferred.make(); - const h = harness((statement) => - statement === phase - ? Deferred.succeed(started, null).pipe( - Effect.andThen(Deferred.await(release)), - Effect.andThen(Effect.fail(failure)), - ) - : Effect.void, - ); - const fiber = yield* h.executor - .transaction(() => - phase === 'COMMIT' ? Effect.succeed(42) : Effect.fail('domain failure'), - ) - .pipe(Effect.forkChild); - yield* Deferred.await(started); - const interrupt = yield* Fiber.interrupt(fiber).pipe(Effect.forkChild); - yield* Effect.yieldNow; - assert.equal(fiber.pollUnsafe(), undefined); - yield* Deferred.succeed(release, null); - yield* Fiber.join(interrupt); - return yield* Fiber.await(fiber); - }), - ); - assert.ok(Exit.isFailure(exit)); - // Native settlement defects take precedence over pending interruption. - assert.ok( - exit.cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect === failure), - ); - }, + 2000, ); } diff --git a/app/packages/core-runtime/tests/unit/operation-context.test.ts b/app/packages/core-runtime/tests/unit/operation-context.test.ts index decae9358..1fc57eae3 100644 --- a/app/packages/core-runtime/tests/unit/operation-context.test.ts +++ b/app/packages/core-runtime/tests/unit/operation-context.test.ts @@ -1,7 +1,5 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { DateTime, Effect, Exit, Option, Schema, flow } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; +import { DateTime, Effect, Exit, Option, Schema } from 'effect'; import { supportRecoveryPrincipalContextResolverFromRepository } from '../../src/auth/support-recovery-principal-context.ts'; import { decodeTrustedPrincipalContext, @@ -49,91 +47,86 @@ const InactiveContextError = Schema.Union([ OperationAuthenticationRequired, OperationContextDenied, ]); -const effectTest = (name: string, effect: Effect.Effect): void => { - test( - name, - flow(() => Effect.asVoid(effect), runEffectTestPromise), - ); -}; -effectTest( +it.effect( 'classifies required, optional, forbidden, denied, unavailable, and valid scope before handlers', - Effect.gen(function* scopeClassification() { - const repository = { load: () => Effect.succeed(active) }; - const allowed = makeOperationalScopeResolver(repository, access('allowed')); - const valid = yield* allowed.resolve({ - correlationId: 'c-1', - legalEntityScope: 'required', - principal, - }); - const { legalEntityId: _legalEntityId, ...principalWithoutLegalEntity } = principal; - const missing = yield* Effect.flip( - allowed.resolve({ + () => + Effect.gen(function* scopeClassification() { + const repository = { load: () => Effect.succeed(active) }; + const allowed = makeOperationalScopeResolver(repository, access('allowed')); + const valid = yield* allowed.resolve({ correlationId: 'c-1', legalEntityScope: 'required', - principal: principalWithoutLegalEntity, - }), - ); - const forbidden = yield* Effect.flip( - allowed.resolve({ correlationId: 'c-1', legalEntityScope: 'forbidden', principal }), - ); - const denied = yield* Effect.flip( - makeOperationalScopeResolver(repository, access('denied')).resolve({ - correlationId: 'c-1', - legalEntityScope: 'optional', principal, - }), - ); - const unavailable = yield* Effect.flip( - makeOperationalScopeResolver(repository, access('unavailable')).resolve({ - correlationId: 'c-1', - legalEntityScope: 'optional', - principal, - }), - ); + }); + const { legalEntityId: _legalEntityId, ...principalWithoutLegalEntity } = principal; + const missing = yield* Effect.flip( + allowed.resolve({ + correlationId: 'c-1', + legalEntityScope: 'required', + principal: principalWithoutLegalEntity, + }), + ); + const forbidden = yield* Effect.flip( + allowed.resolve({ correlationId: 'c-1', legalEntityScope: 'forbidden', principal }), + ); + const denied = yield* Effect.flip( + makeOperationalScopeResolver(repository, access('denied')).resolve({ + correlationId: 'c-1', + legalEntityScope: 'optional', + principal, + }), + ); + const unavailable = yield* Effect.flip( + makeOperationalScopeResolver(repository, access('unavailable')).resolve({ + correlationId: 'c-1', + legalEntityScope: 'optional', + principal, + }), + ); - assert.equal(Object.isFrozen(valid), true); - assert.equal(Schema.is(OperationContextDenied)(missing), true); - assert.equal(Schema.is(OperationContextInvalid)(forbidden), true); - assert.equal(Schema.is(OperationContextDenied)(denied), true); - assert.equal(Schema.is(OperationContextUnavailable)(unavailable), true); - }), + expect(Object.isFrozen(valid)).toBe(true); + expect(Schema.is(OperationContextDenied)(missing)).toBe(true); + expect(Schema.is(OperationContextInvalid)(forbidden)).toBe(true); + expect(Schema.is(OperationContextDenied)(denied)).toBe(true); + expect(Schema.is(OperationContextUnavailable)(unavailable)).toBe(true); + }), ); -effectTest( +it.effect( 'rejects stale tenant, principal, revoked auth binding, and cross-tenant entity records', - Effect.gen(function* staleContextRecords() { - const records = [ - { ...active, tenantStatus: 'suspended' }, - { ...active, principalStatus: 'disabled' }, - { - ...active, - bindingRevokedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), - }, - { ...active, bindingTenantId: '00000000-0000-4000-8000-000000000099' }, - { ...active, legalEntityTenantId: '00000000-0000-4000-8000-000000000099' }, - ]; - const errors = yield* Effect.forEach( - records, - (record) => { - const resolver = makeOperationalScopeResolver( - { load: () => Effect.succeed(record) }, - access('allowed'), - ); - return Effect.flip( - resolver.resolve({ correlationId: 'c-1', legalEntityScope: 'required', principal }), - ); - }, - { concurrency: 1 }, - ); - for (const error of errors) { - assert.equal(Schema.is(InactiveContextError)(error), true); - } - }), + () => + Effect.gen(function* staleContextRecords() { + const records = [ + { ...active, tenantStatus: 'suspended' }, + { ...active, principalStatus: 'disabled' }, + { + ...active, + bindingRevokedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), + }, + { ...active, bindingTenantId: '00000000-0000-4000-8000-000000000099' }, + { ...active, legalEntityTenantId: '00000000-0000-4000-8000-000000000099' }, + ]; + const errors = yield* Effect.forEach( + records, + (record) => { + const resolver = makeOperationalScopeResolver( + { load: () => Effect.succeed(record) }, + access('allowed'), + ); + return Effect.flip( + resolver.resolve({ correlationId: 'c-1', legalEntityScope: 'required', principal }), + ); + }, + { concurrency: 1 }, + ); + for (const error of errors) { + expect(Schema.is(InactiveContextError)(error)).toBe(true); + } + }), ); -effectTest( - 'preserves resolver-issued system provenance across operational scope construction', +it.effect('preserves resolver-issued system provenance across operational scope construction', () => Effect.gen(function* systemProvenance() { const systemContext = yield* systemPrincipalContextResolverFromRepository({ load: () => @@ -169,59 +162,59 @@ effectTest( principal: systemContext, }); - assert.equal(scope.authMethod, 'system'); - assert.equal(scope.correlationId, 'system-correlation'); + expect(scope.authMethod).toBe('system'); + expect(scope.correlationId).toBe('system-correlation'); const decoded = yield* decodeTrustedPrincipalContext(scope); - assert.equal(decoded.authMethod, 'system'); - assert.equal(decoded.principalId, scope.principalId); + expect(decoded.authMethod).toBe('system'); + expect(decoded.principalId).toBe(scope.principalId); const untrusted = yield* Effect.exit(decodeTrustedPrincipalContext({ ...scope })); - assert.equal(Exit.isFailure(untrusted), true); + expect(Exit.isFailure(untrusted)).toBe(true); }), ); -effectTest( +it.effect( 'permits only a resolver-branded support-stop recovery through inactive historical scope', - Effect.gen(function* supportRecovery() { - const recoveryPrincipal = yield* supportRecoveryPrincipalContextResolverFromRepository({ - load: () => - Effect.succeed({ - bindingPrincipalId: principal.principalId, - bindingTenantId: principal.tenantId, - principalKind: 'human' as const, - principalTenantId: principal.tenantId, - tenantId: principal.tenantId, - }).pipe(Effect.map(Option.some)), - }).resolveStoppedImpersonation({ - originalAuthBindingId: principal.authBindingId, - originalPrincipalId: principal.principalId, - originalSessionId: 'expired-original-session', - tenantId: principal.tenantId, - }); - const resolver = makeOperationalScopeResolver( - { + () => + Effect.gen(function* supportRecovery() { + const recoveryPrincipal = yield* supportRecoveryPrincipalContextResolverFromRepository({ load: () => Effect.succeed({ - ...active, - bindingRevokedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-08-09T00:00:00.000Z')), - bindingStatus: 'revoked', - principalStatus: 'disabled', - tenantStatus: 'suspended', - }), - }, - access('allowed'), - ); + bindingPrincipalId: principal.principalId, + bindingTenantId: principal.tenantId, + principalKind: 'human' as const, + principalTenantId: principal.tenantId, + tenantId: principal.tenantId, + }).pipe(Effect.map(Option.some)), + }).resolveStoppedImpersonation({ + originalAuthBindingId: principal.authBindingId, + originalPrincipalId: principal.principalId, + originalSessionId: 'expired-original-session', + tenantId: principal.tenantId, + }); + const resolver = makeOperationalScopeResolver( + { + load: () => + Effect.succeed({ + ...active, + bindingRevokedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-08-09T00:00:00.000Z')), + bindingStatus: 'revoked', + principalStatus: 'disabled', + tenantStatus: 'suspended', + }), + }, + access('allowed'), + ); - const scope = yield* resolver.resolve({ - correlationId: 'support-recovery', - legalEntityScope: 'optional', - principal: recoveryPrincipal, - }); + const scope = yield* resolver.resolve({ + correlationId: 'support-recovery', + legalEntityScope: 'optional', + principal: recoveryPrincipal, + }); - assert.equal( - isTrustedSupportRecoveryPrincipalContext(scope, recordSupportImpersonationAction), - true, - ); - assert.equal(isTrustedSupportRecoveryPrincipalContext(scope, {}), false); - assert.equal(isTrustedSupportRecoveryPrincipalContext({ ...scope }), false); - }), + expect( + isTrustedSupportRecoveryPrincipalContext(scope, recordSupportImpersonationAction), + ).toBe(true); + expect(isTrustedSupportRecoveryPrincipalContext(scope, {})).toBe(false); + expect(isTrustedSupportRecoveryPrincipalContext({ ...scope })).toBe(false); + }), ); diff --git a/app/packages/core-runtime/tests/unit/outbox-definition.test.ts b/app/packages/core-runtime/tests/unit/outbox-definition.test.ts index eb441725f..316698d5f 100644 --- a/app/packages/core-runtime/tests/unit/outbox-definition.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-definition.test.ts @@ -1,6 +1,4 @@ -import { makeEffectTestCallback } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema, Predicate } from 'effect'; import { defineOutboxWorker, @@ -14,7 +12,6 @@ import { OutboxWorkerDescriptorError } from '../../src/outbox/errors.ts'; const MessageKey = Schema.String.pipe(Schema.brand('MessageKey')); const payloadSchema = Schema.Struct({ messageKey: MessageKey }); - const makeWorker = (workerKey = 'consumer.message-logger') => defineOutboxWorker( { @@ -38,61 +35,54 @@ const makeWorker = (workerKey = 'consumer.message-logger') => topic: 'producer.message-created', workerKey, }, - (payload) => Effect.sync(() => assert.equal(Predicate.isString(payload.messageKey), true)), + (payload) => Effect.sync(() => expect(Predicate.isString(payload.messageKey)).toBe(true)), ); - -void test( - 'defines an exact immutable registration while keeping the handler opaque', - makeEffectTestCallback( - Effect.gen(function* immutableRegistration() { - const worker = makeWorker(); - - assert.deepEqual(worker.descriptor, { - consumerModuleKey: 'consumer', - entrypoint: { - access: 'background', - authorization: { kind: 'owner_local_background' }, - entrypointKey: 'consumer.message-logger', - moduleKey: 'consumer', - role: 'worker', - scope: 'tenant', - }, - leaseDurationMs: 30_000, - payloadSchema, - producerModuleKey: 'producer', - retryPolicy: { - initialBackoffMs: 1000, - maxAttempts: 5, - maxBackoffMs: 10_000, - multiplier: 2, - }, - topic: 'producer.message-created', - workerKey: 'consumer.message-logger', - }); - assert.equal(Object.isFrozen(worker), true); - assert.equal(Object.isFrozen(worker.descriptor), true); - assert.equal(Object.isFrozen(worker.descriptor.retryPolicy), true); - assert.equal('handler' in worker, false); - assert.deepEqual(Object.keys(worker), ['descriptor']); - - const payload = yield* Schema.decodeUnknownEffect(payloadSchema)({ messageKey: 'message-1' }); - yield* getOutboxWorkerHandler(worker)(payload, { - attemptNumber: 1, - claimId: 'claim-1', - deliveryId: 'delivery-1', - domainEventId: 'event-1', - messageId: 'message-1', - producerModuleKey: 'producer', - tenantId: 'tenant-1', - tenantSequenceNo: 1n, - topic: 'producer.message-created', - workerKey: 'consumer.message-logger', - }); - }), - ), +it.effect('defines an exact immutable registration while keeping the handler opaque', () => + Effect.gen(function* immutableRegistration() { + const worker = makeWorker(); + expect(worker.descriptor).toEqual({ + consumerModuleKey: 'consumer', + entrypoint: { + access: 'background', + authorization: { kind: 'owner_local_background' }, + entrypointKey: 'consumer.message-logger', + moduleKey: 'consumer', + role: 'worker', + scope: 'tenant', + }, + leaseDurationMs: 30_000, + payloadSchema, + producerModuleKey: 'producer', + retryPolicy: { + initialBackoffMs: 1000, + maxAttempts: 5, + maxBackoffMs: 10_000, + multiplier: 2, + }, + topic: 'producer.message-created', + workerKey: 'consumer.message-logger', + }); + expect(Object.isFrozen(worker)).toBe(true); + expect(Object.isFrozen(worker.descriptor)).toBe(true); + expect(Object.isFrozen(worker.descriptor.retryPolicy)).toBe(true); + expect('handler' in worker).toBe(false); + expect(Object.keys(worker)).toEqual(['descriptor']); + const payload = yield* Schema.decodeUnknownEffect(payloadSchema)({ messageKey: 'message-1' }); + yield* getOutboxWorkerHandler(worker)(payload, { + attemptNumber: 1, + claimId: 'claim-1', + deliveryId: 'delivery-1', + domainEventId: 'event-1', + messageId: 'message-1', + producerModuleKey: 'producer', + tenantId: 'tenant-1', + tenantSequenceNo: 1n, + topic: 'producer.message-created', + workerKey: 'consumer.message-logger', + }); + }), ); - -void test('preserves schema inference for a typed handler payload', () => { +it('preserves schema inference for a typed handler payload', () => { defineOutboxWorker( { consumerModuleKey: 'consumer', @@ -117,12 +107,11 @@ void test('preserves schema inference for a typed handler payload', () => { }, (payload) => { const key: string = payload.messageKey; - return Effect.sync(() => assert.equal(key, payload.messageKey)); + return Effect.sync(() => expect(key).toBe(payload.messageKey)); }, ); }); - -void test('rejects invalid identities, retry policies, and lease policies', () => { +it('rejects invalid identities, retry policies, and lease policies', () => { const valid = makeWorker().descriptor; const invalidDescriptors = [ { ...valid, workerKey: 'producer.foreign-worker' }, @@ -145,28 +134,26 @@ void test('rejects invalid identities, retry policies, and lease policies', () = }, { ...valid, retryPolicy: { ...valid.retryPolicy, multiplier: 0 } }, ]; - for (const descriptor of invalidDescriptors) { - assert.throws( - () => defineOutboxWorker(descriptor, () => Effect.void), - Schema.is(OutboxWorkerDescriptorError), + expect(() => defineOutboxWorker(descriptor, () => Effect.void)).toThrow( + OutboxWorkerDescriptorError, ); } }); - -void test('rejects duplicate worker keys and calculates bounded exponential backoff', () => { +it('rejects duplicate worker keys and calculates bounded exponential backoff', () => { const worker = makeWorker(); - assert.throws(() => validateOutboxWorkerRegistrations([worker, worker]), { - name: 'OutboxWorkerDescriptorError', - reason: /duplicate Outbox Worker key/u, - }); - assert.deepEqual(validateOutboxWorkerRegistrations([worker]), [worker]); - assert.equal(retryBackoffMs(worker.descriptor.retryPolicy, 1), 1000); - assert.equal(retryBackoffMs(worker.descriptor.retryPolicy, 3), 4000); - assert.equal(retryBackoffMs(worker.descriptor.retryPolicy, 10), 10_000); + expect(() => validateOutboxWorkerRegistrations([worker, worker])).toThrow( + expect.objectContaining({ + name: 'OutboxWorkerDescriptorError', + reason: expect.stringMatching(/duplicate Outbox Worker key/u), + }), + ); + expect(validateOutboxWorkerRegistrations([worker])).toEqual([worker]); + expect(retryBackoffMs(worker.descriptor.retryPolicy, 1)).toBe(1000); + expect(retryBackoffMs(worker.descriptor.retryPolicy, 3)).toBe(4000); + expect(retryBackoffMs(worker.descriptor.retryPolicy, 10)).toBe(10_000); }); - -void test('validates and freezes the schema-free installed subscription catalog', () => { +it('validates and freezes the schema-free installed subscription catalog', () => { const worker = makeWorker(); const subscription = { consumerModuleKey: worker.descriptor.consumerModuleKey, @@ -176,11 +163,13 @@ void test('validates and freezes the schema-free installed subscription catalog' workerKey: worker.descriptor.workerKey, }; const validated = validateOutboxWorkerSubscriptions([subscription]); - assert.deepEqual(validated, [subscription]); - assert.equal(Object.isFrozen(validated), true); - assert.equal(Object.isFrozen(validated[0]), true); - assert.throws(() => validateOutboxWorkerSubscriptions([subscription, subscription]), { - name: 'OutboxWorkerDescriptorError', - reason: /duplicate Outbox Worker key/u, - }); + expect(validated).toEqual([subscription]); + expect(Object.isFrozen(validated)).toBe(true); + expect(Object.isFrozen(validated[0])).toBe(true); + expect(() => validateOutboxWorkerSubscriptions([subscription, subscription])).toThrow( + expect.objectContaining({ + name: 'OutboxWorkerDescriptorError', + reason: expect.stringMatching(/duplicate Outbox Worker key/u), + }), + ); }); diff --git a/app/packages/core-runtime/tests/unit/outbox-errors.test.ts b/app/packages/core-runtime/tests/unit/outbox-errors.test.ts index d38f6d482..f6caf1a7b 100644 --- a/app/packages/core-runtime/tests/unit/outbox-errors.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-errors.test.ts @@ -1,7 +1,5 @@ -import { makeEffectTestCallback } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Predicate, Schema, Struct } from 'effect'; import type { Cause } from 'effect'; import { OutboxClaimLostError, @@ -31,47 +29,47 @@ const checkErrorContract = ( failure: Failure, encoded: { readonly _tag: string; readonly code: string; readonly reason: string }, ): void => { - void test( - `${encoded._tag} preserves its schema and yieldable failure contract`, - makeEffectTestCallback( - Effect.gen(function* errorContract() { - assert.ok(Schema.is(schema)(failure)); - assert.deepEqual(yield* Schema.encodeEffect(schema)(failure), encoded); - const decoded = yield* Schema.decodeUnknownEffect(schema)(encoded); - assert.ok(Schema.is(schema)(decoded)); - assert.deepEqual(yield* Schema.encodeEffect(schema)(decoded), encoded); - for (const otherSchema of errorSchemas) { - assert.equal(Schema.is(otherSchema)(failure), Object.is(otherSchema, schema)); - assert.equal(Schema.is(otherSchema)(decoded), Object.is(otherSchema, schema)); - } - assert.throws(() => Schema.decodeUnknownSync(schema)({ ...encoded, _tag: 'WrongError' })); - assert.throws(() => Schema.decodeUnknownSync(schema)({ ...encoded, code: 'wrong_code' })); - assert.throws(() => Schema.decodeUnknownSync(schema)({ ...encoded, reason: 42 })); - assert.throws(() => - Schema.decodeUnknownSync(schema)({ _tag: encoded._tag, code: encoded.code }), - ); - const yielded = yield* Effect.flip( - Effect.gen(function* yieldFailure() { - assert.ok(Schema.is(schema)(failure)); - return yield* failure; - }), - ); - assert.equal(yielded, failure); - const decodedFailure = yield* Effect.flip( - Effect.gen(function* yieldDecodedFailure() { - assert.ok(Schema.is(schema)(decoded)); - return yield* decoded; - }), - ); - assert.equal(decodedFailure, decoded); - }), - ), + it.effect(`${encoded._tag} preserves its schema and yieldable failure contract`, () => + Effect.gen(function* errorContract() { + expect(Schema.is(schema)(failure)).toBeTruthy(); + expect(yield* Schema.encodeEffect(schema)(failure)).toEqual(encoded); + const decoded = yield* Schema.decodeUnknownEffect(schema)(encoded); + expect(Schema.is(schema)(decoded)).toBeTruthy(); + expect(yield* Schema.encodeEffect(schema)(decoded)).toEqual(encoded); + for (const otherSchema of errorSchemas) { + expect(Schema.is(otherSchema)(failure)).toBe(Object.is(otherSchema, schema)); + expect(Schema.is(otherSchema)(decoded)).toBe(Object.is(otherSchema, schema)); + } + expect(() => Schema.decodeUnknownSync(schema)({ ...encoded, _tag: 'WrongError' })).toThrow(); + expect(() => Schema.decodeUnknownSync(schema)({ ...encoded, code: 'wrong_code' })).toThrow(); + expect(() => Schema.decodeUnknownSync(schema)({ ...encoded, reason: 42 })).toThrow(); + expect(() => + Schema.decodeUnknownSync(schema)({ _tag: encoded._tag, code: encoded.code }), + ).toThrow(); + const yielded = yield* Effect.flip( + Effect.gen(function* yieldFailure() { + expect(Schema.is(schema)(failure)).toBeTruthy(); + return yield* failure; + }), + ); + expect(yielded).toBe(failure); + const decodedFailure = yield* Effect.flip( + Effect.gen(function* yieldDecodedFailure() { + expect(Schema.is(schema)(decoded)).toBeTruthy(); + return yield* decoded; + }), + ); + expect(decodedFailure).toBe(decoded); + }), ); }; checkErrorContract( OutboxWorkerDescriptorError, - new OutboxWorkerDescriptorError({ code: 'outbox_worker_descriptor_invalid', reason: 'detail' }), + new OutboxWorkerDescriptorError({ + code: 'outbox_worker_descriptor_invalid', + reason: 'detail', + }), { _tag: 'OutboxWorkerDescriptorError', code: 'outbox_worker_descriptor_invalid', @@ -95,7 +93,10 @@ checkErrorContract( ); checkErrorContract( OutboxHandlerExecutionError, - new OutboxHandlerExecutionError({ code: 'outbox_handler_execution_failed', reason: 'detail' }), + new OutboxHandlerExecutionError({ + code: 'outbox_handler_execution_failed', + reason: 'detail', + }), { _tag: 'OutboxHandlerExecutionError', code: 'outbox_handler_execution_failed', @@ -108,44 +109,41 @@ checkErrorContract( { _tag: 'OutboxPollerConfigError', code: 'outbox_poller_config_invalid', reason: 'detail' }, ); -void test('persistence errors keep the original cause private and immutable', () => { +it('persistence errors keep the original cause private and immutable', () => { const cause = { secret: 'database credential' }; const failure = outboxPersistenceError(cause); - assert.ok(Schema.is(OutboxPersistenceError)(failure)); - assert.deepEqual(Object.getOwnPropertyDescriptor(failure, 'ontosOutboxPersistenceCause'), { + expect(Schema.is(OutboxPersistenceError)(failure)).toBeTruthy(); + expect(Object.getOwnPropertyDescriptor(failure, 'ontosOutboxPersistenceCause')).toEqual({ configurable: false, enumerable: false, value: cause, writable: false, }); - assert.equal( - Object.getOwnPropertyDescriptor(failure, 'ontosOutboxPersistenceCause')?.value, + expect(Object.getOwnPropertyDescriptor(failure, 'ontosOutboxPersistenceCause')?.value).toBe( cause, ); - assert.equal(Object.keys(failure).includes('ontosOutboxPersistenceCause'), false); - assert.equal(JSON.stringify(failure).includes('database credential'), false); + expect(Object.keys(failure).includes('ontosOutboxPersistenceCause')).toBe(false); + expect(JSON.stringify(failure).includes('database credential')).toBe(false); const encoded = Schema.encodeSync(OutboxPersistenceError)(failure); - assert.deepEqual(encoded, { - _tag: 'OutboxPersistenceError', + expect(Predicate.isTagged(encoded, 'OutboxPersistenceError')).toBe(true); + expect(Struct.omit(encoded, ['_tag'])).toEqual({ code: 'outbox_persistence_failed', reason: 'The Outbox Worker persistence operation failed', }); - assert.equal( + expect( Object.hasOwn( Schema.decodeUnknownSync(OutboxPersistenceError)(encoded), 'ontosOutboxPersistenceCause', ), - false, - ); + ).toBe(false); }); -void test('sanitizer normalizes control whitespace, trims, truncates and falls back', () => { - assert.equal( - sanitizeOutboxErrorMessage(' \r\nfirst\r\n\tsecond\t third \n'), +it('sanitizer normalizes control whitespace, trims, truncates and falls back', () => { + expect(sanitizeOutboxErrorMessage(' \r\nfirst\r\n\tsecond\t third \n')).toBe( 'first second third', ); - assert.equal(sanitizeOutboxErrorMessage(' plain detail '), 'plain detail'); - assert.equal(sanitizeOutboxErrorMessage(` ${'x'.repeat(501)} `), 'x'.repeat(500)); - assert.equal(sanitizeOutboxErrorMessage(' \r\n\t '), 'Outbox Worker processing failed'); - assert.equal(sanitizeOutboxErrorMessage(''), 'Outbox Worker processing failed'); + expect(sanitizeOutboxErrorMessage(' plain detail ')).toBe('plain detail'); + expect(sanitizeOutboxErrorMessage(` ${'x'.repeat(501)} `)).toBe('x'.repeat(500)); + expect(sanitizeOutboxErrorMessage(' \r\n\t ')).toBe('Outbox Worker processing failed'); + expect(sanitizeOutboxErrorMessage('')).toBe('Outbox Worker processing failed'); }); diff --git a/app/packages/core-runtime/tests/unit/outbox-health.test.ts b/app/packages/core-runtime/tests/unit/outbox-health.test.ts index 1c9bce66f..b79f2c098 100644 --- a/app/packages/core-runtime/tests/unit/outbox-health.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-health.test.ts @@ -1,91 +1,81 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -/* oxlint-disable typescript/return-await -- Existing compatibility boundary; expires: 2026-12-31. */ -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { ConfigProvider, Effect, Layer, Result } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { ConfigProvider, Effect, Layer, Predicate } from 'effect'; import { FetchHttpClient, HttpClient } from 'effect/unstable/http'; import { createOutboxWorkerHealth, serveOutboxWorkerHealth } from '../../src/outbox/health.ts'; import { runOutboxWorkerProcess } from '../../src/outbox/process.ts'; import { OutboxRuntime } from '../../src/outbox/runtime.ts'; -test('production health binds all IPv4 interfaces for external-container probes', async () => - runEffectTestPromise( - Effect.scoped( - Effect.gen(function* externallyReachableHealth() { - const health = yield* createOutboxWorkerHealth({ staleAfterMs: 5000 }); - const server = yield* serveOutboxWorkerHealth(health, { port: 0 }); - assert.equal(server.hostname, '0.0.0.0'); - }), - ), - )); +it.live('production health binds all IPv4 interfaces for external-container probes', () => + Effect.gen(function* externallyReachableHealth() { + const health = yield* createOutboxWorkerHealth({ staleAfterMs: 5000 }); + const server = yield* serveOutboxWorkerHealth(health, { port: 0 }); + expect(server.hostname).toBe('0.0.0.0'); + }), +); -test('readiness starts false, follows successful/failing cycles, expires, and closes on shutdown', async () => { - let now = 1000; - return runEffectTestPromise( - Effect.scoped( - Effect.gen(function* healthLifecycle() { - const services = yield* Layer.build(FetchHttpClient.layer); - const client = yield* Effect.provide(HttpClient.HttpClient, services); - const health = yield* createOutboxWorkerHealth({ - now: Effect.sync(() => now), - staleAfterMs: 100, - }); - const server = yield* serveOutboxWorkerHealth(health, { port: 0 }); - const ready = client.get(`http://127.0.0.1:${server.port}/ready`); - const startingResponse = yield* ready; - assert.equal(startingResponse.status, 503); - assert.deepEqual(yield* startingResponse.json, { ready: false }); - assert.equal((yield* client.get(`http://127.0.0.1:${server.port}/unknown`)).status, 404); - yield* health.cycleSucceeded; - const readyResponse = yield* ready; - assert.equal(readyResponse.status, 200); - assert.deepEqual(yield* readyResponse.json, { ready: true }); - now = 1101; - assert.equal((yield* ready).status, 503); - yield* health.cycleSucceeded; - yield* health.cycleFailed; - assert.equal((yield* ready).status, 503); - yield* health.cycleSucceeded; - yield* health.shuttingDown; - assert.equal((yield* ready).status, 503); - }), - ), - ); -}); +it.live( + 'readiness starts false, follows successful/failing cycles, expires, and closes on shutdown', + () => { + let now = 1000; + return Effect.gen(function* healthLifecycle() { + const services = yield* Layer.build(FetchHttpClient.layer); + const client = yield* Effect.provide(HttpClient.HttpClient, services); + const health = yield* createOutboxWorkerHealth({ + now: Effect.sync(() => now), + staleAfterMs: 100, + }); + const server = yield* serveOutboxWorkerHealth(health, { port: 0 }); + const ready = client.get(`http://127.0.0.1:${server.port}/ready`); + const startingResponse = yield* ready; + expect(startingResponse.status).toBe(503); + expect(yield* startingResponse.json).toEqual({ ready: false }); + expect((yield* client.get(`http://127.0.0.1:${server.port}/unknown`)).status).toBe(404); + yield* health.cycleSucceeded; + const readyResponse = yield* ready; + expect(readyResponse.status).toBe(200); + expect(yield* readyResponse.json).toEqual({ ready: true }); + now = 1101; + expect((yield* ready).status).toBe(503); + yield* health.cycleSucceeded; + yield* health.cycleFailed; + expect((yield* ready).status).toBe(503); + yield* health.cycleSucceeded; + yield* health.shuttingDown; + expect((yield* ready).status).toBe(503); + }); + }, +); -test('closing the health scope marks it unavailable and releases its dynamically allocated port', async () => - runEffectTestPromise( +it.live( + 'closing the health scope marks it unavailable and releases its dynamically allocated port', + () => Effect.gen(function* releasedPort() { const health = yield* createOutboxWorkerHealth({ staleAfterMs: 5000 }); yield* health.cycleSucceeded; const server = yield* Effect.scoped(serveOutboxWorkerHealth(health, { port: 0 })); - assert.equal(yield* health.isReady, false); + expect(yield* health.isReady).toBe(false); const rebound = yield* Effect.scoped(serveOutboxWorkerHealth(health, { port: server.port })); - assert.equal(rebound.port, server.port); + expect(rebound.port).toBe(server.port); }), - )); +); -test('a health port already in use produces a typed server startup failure', async () => - runEffectTestPromise( - Effect.scoped( - Effect.gen(function* occupiedPort() { - const health = yield* createOutboxWorkerHealth({ staleAfterMs: 5000 }); - const server = yield* serveOutboxWorkerHealth(health, { port: 0 }); - const result = yield* Effect.result( - Effect.scoped(serveOutboxWorkerHealth(health, { port: server.port })), - ); - assert.ok(Result.isFailure(result)); - assert.equal(result.failure._tag, 'ServeError'); - }), - ), - )); +it.live('a health port already in use produces a typed server startup failure', () => + Effect.gen(function* occupiedPort() { + const health = yield* createOutboxWorkerHealth({ staleAfterMs: 5000 }); + const server = yield* serveOutboxWorkerHealth(health, { port: 0 }); + const failure = yield* Effect.flip( + Effect.scoped(serveOutboxWorkerHealth(health, { port: server.port })), + ); + expect(Predicate.isTagged(failure, 'ServeError')).toBe(true); + }), +); -test('invalid configured health ports fail startup with a typed configuration error before polling', async () => - runEffectTestPromise( +it.effect( + 'invalid configured health ports fail startup with a typed configuration error before polling', + () => Effect.gen(function* invalidPortConfiguration() { for (const port of ['0', '65536', '4102.5', 'invalid']) { - const result = yield* Effect.result( + const failure = yield* Effect.flip( runOutboxWorkerProcess({ claimOwnerPrefix: 'health-config-test', health: true, @@ -102,8 +92,7 @@ test('invalid configured health ports fail startup with a typed configuration er }), ), ); - assert.ok(Result.isFailure(result)); - assert.equal(result.failure._tag, 'ConfigError'); + expect(Predicate.isTagged(failure, 'ConfigError')).toBe(true); } }), - )); +); diff --git a/app/packages/core-runtime/tests/unit/outbox-poller.test.ts b/app/packages/core-runtime/tests/unit/outbox-poller.test.ts index 65a3beaa4..6b70235a8 100644 --- a/app/packages/core-runtime/tests/unit/outbox-poller.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-poller.test.ts @@ -1,7 +1,5 @@ -import { makeEffectTestCallback } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Fiber, Layer, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Fiber, Schema } from 'effect'; import { TestClock } from 'effect/testing'; import { defineOutboxWorker } from '../../src/outbox/definition.ts'; import { defineTenantModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; @@ -46,94 +44,80 @@ const emptyResult = { succeeded: 0, } as const; -void test( - 'uses safe one-second defaults and accepts bounded scalar overrides', - makeEffectTestCallback( - Effect.gen(function* validPollingConfiguration() { - assert.deepEqual( - yield* parseOutboxPollingConfig({ - defaultClaimOwner: 'consumer:default', - environment: {}, - }), - { - claimOwner: 'consumer:default', - maxDeliveries: 100, - pollIntervalMs: 1000, - }, - ); +it.effect('uses safe one-second defaults and accepts bounded scalar overrides', () => + Effect.gen(function* validPollingConfiguration() { + expect( + yield* parseOutboxPollingConfig({ + defaultClaimOwner: 'consumer:default', + environment: {}, + }), + ).toEqual({ + claimOwner: 'consumer:default', + maxDeliveries: 100, + pollIntervalMs: 1000, + }); - assert.deepEqual( - yield* parseOutboxPollingConfig({ - defaultClaimOwner: 'consumer:default', - environment: { - OUTBOX_WORKER_CLAIM_OWNER: 'consumer:configured', - OUTBOX_WORKER_MAX_DELIVERIES: '25', - OUTBOX_WORKER_POLL_INTERVAL_MS: '250', - }, - }), - { - claimOwner: 'consumer:configured', - maxDeliveries: 25, - pollIntervalMs: 250, + expect( + yield* parseOutboxPollingConfig({ + defaultClaimOwner: 'consumer:default', + environment: { + OUTBOX_WORKER_CLAIM_OWNER: 'consumer:configured', + OUTBOX_WORKER_MAX_DELIVERIES: '25', + OUTBOX_WORKER_POLL_INTERVAL_MS: '250', }, - ); - }), - ), + }), + ).toEqual({ + claimOwner: 'consumer:configured', + maxDeliveries: 25, + pollIntervalMs: 250, + }); + }), ); -void test( - 'rejects invalid polling values instead of falling back to a busy loop', - makeEffectTestCallback( - Effect.gen(function* invalidPollingConfiguration() { - const error = yield* Effect.flip( - parseOutboxPollingConfig({ - defaultClaimOwner: 'consumer:default', - environment: { OUTBOX_WORKER_POLL_INTERVAL_MS: '0' }, - }), - ); - assert.equal(Schema.is(OutboxPollerConfigError)(error), true); - }), - ), +it.effect('rejects invalid polling values instead of falling back to a busy loop', () => + Effect.gen(function* invalidPollingConfiguration() { + const error = yield* Effect.flip( + parseOutboxPollingConfig({ + defaultClaimOwner: 'consumer:default', + environment: { OUTBOX_WORKER_POLL_INTERVAL_MS: '0' }, + }), + ); + expect(Schema.is(OutboxPollerConfigError)(error)).toBe(true); + }), ); -void test( - 'runs immediately, survives a typed cycle failure, and continues polling', - makeEffectTestCallback( - Effect.gen(function* pollingWithTestClock() { - const testClockServices = yield* Layer.build(TestClock.layer()); - return yield* Effect.gen(function* pollingContinuesAfterFailure() { - let calls = 0; - const healthTransitions: string[] = []; - const runCycle: OutboxCycleRunner = () => - Effect.suspend(() => { - calls += 1; - return calls === 1 - ? Effect.fail( - new OutboxPersistenceError({ - code: 'outbox_persistence_failed', - reason: 'controlled test failure', - }), - ) - : Effect.succeed(emptyResult); - }); - const running = yield* runOutboxPollingLoop( - { - config: { claimOwner: 'consumer:test', maxDeliveries: 10, pollIntervalMs: 10 }, - health: { - cycleFailed: Effect.sync(() => healthTransitions.push('failed')), - cycleSucceeded: Effect.sync(() => healthTransitions.push('ready')), - }, - registrations: [registration], - subscriptions: [registration.descriptor], - }, - runCycle, - ).pipe(Effect.forkChild); +it.effect('runs immediately, survives a typed cycle failure, and continues polling', () => + Effect.gen(function* pollingContinuesAfterFailure() { + let calls = 0; + const healthTransitions: string[] = []; + const runCycle: OutboxCycleRunner = () => + Effect.suspend(() => { + calls += 1; + return calls === 1 + ? Effect.fail( + new OutboxPersistenceError({ + code: 'outbox_persistence_failed', + reason: 'controlled test failure', + }), + ) + : Effect.succeed(emptyResult); + }); + const running = yield* runOutboxPollingLoop( + { + config: { claimOwner: 'consumer:test', maxDeliveries: 10, pollIntervalMs: 10 }, + health: { + cycleFailed: Effect.sync(() => healthTransitions.push('failed')), + cycleSucceeded: Effect.sync(() => healthTransitions.push('ready')), + }, + registrations: [registration], + subscriptions: [registration.descriptor], + }, + runCycle, + ).pipe(Effect.forkChild); - yield* TestClock.adjust('20 millis'); - yield* Fiber.interrupt(running); - assert.equal(calls, 3, 'polling loop did not continue'); - assert.deepEqual(healthTransitions, ['failed', 'ready', 'ready']); - }).pipe(Effect.provide(testClockServices)); - }).pipe(Effect.scoped), - ), + yield* TestClock.adjust('20 millis'); + yield* Fiber.interrupt(running); + expect(calls, 'polling loop did not continue').toBe(3); + expect(healthTransitions).toEqual(['failed', 'ready', 'ready']); + }).pipe(Effect.scoped), ); diff --git a/app/packages/core-runtime/tests/unit/outbox-process.test.ts b/app/packages/core-runtime/tests/unit/outbox-process.test.ts index f97824fc6..d9e77ddc4 100644 --- a/app/packages/core-runtime/tests/unit/outbox-process.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-process.test.ts @@ -1,7 +1,5 @@ -import { makeEffectTestCallback } from '@app/core-runtime/testing/effect-runtime'; import { NodeServices } from '@effect/platform-node'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Deferred, Effect, Fiber, Layer, Stream } from 'effect'; import { ChildProcess } from 'effect/unstable/process'; @@ -47,9 +45,9 @@ const gracefulShutdown = (signal: 'SIGINT' | 'SIGTERM') => ); const output = outputLines.join('\n'); - assert.equal(Number(code), 0, `${errors}\n${output}`); - assert.match(output, /cycle:1/u); - assert.match(output, /disposed/u); + expect(Number(code), `${errors}\n${output}`).toBe(0); + expect(output).toMatch(/cycle:1/u); + expect(output).toMatch(/disposed/u); }); const assertGracefulShutdown = (signal: 'SIGINT' | 'SIGTERM') => @@ -59,9 +57,9 @@ const assertGracefulShutdown = (signal: 'SIGINT' | 'SIGTERM') => ); for (const signal of ['SIGINT', 'SIGTERM'] as const) { - void test( + it.live( `${signal} interrupts polling and disposes the managed worker runtime`, - { timeout: 5000 }, - makeEffectTestCallback(assertGracefulShutdown(signal)), + () => assertGracefulShutdown(signal), + 5000, ); } diff --git a/app/packages/core-runtime/tests/unit/outbox-runtime.test.ts b/app/packages/core-runtime/tests/unit/outbox-runtime.test.ts index 86c8f19d9..542d12af3 100644 --- a/app/packages/core-runtime/tests/unit/outbox-runtime.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-runtime.test.ts @@ -1,6 +1,4 @@ -import { makeEffectTestCallback } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Context, Effect, Option, Schema } from 'effect'; import { defineOutboxWorker } from '../../src/outbox/definition.ts'; import { defineTenantModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; @@ -144,243 +142,214 @@ const run = ( subscriptions: [registration.descriptor], }); -void test( - 'owner-local cycles do not perform global matching', - makeEffectTestCallback( - Effect.gen(function* ownerLocalCycle() { - const controlled = repository({ match: { deliveriesCreated: 0, messagesMatched: 2 } }); - - assert.deepEqual(yield* run(controlled.service), { - claimed: 0, - dead: 0, - deliveriesCreated: 0, - failed: 0, - messagesMatched: 0, - retried: 0, - succeeded: 0, - }); - assert.deepEqual(controlled.probe.completed, []); - assert.deepEqual(controlled.probe.failed, []); - }), - ), +it.effect('owner-local cycles do not perform global matching', () => + Effect.gen(function* ownerLocalCycle() { + const controlled = repository({ match: { deliveriesCreated: 0, messagesMatched: 2 } }); + + expect(yield* run(controlled.service)).toEqual({ + claimed: 0, + dead: 0, + deliveriesCreated: 0, + failed: 0, + messagesMatched: 0, + retried: 0, + succeeded: 0, + }); + expect(controlled.probe.completed).toEqual([]); + expect(controlled.probe.failed).toEqual([]); + }), ); -void test( - 'matches messages only through the explicit Core matcher snapshot', - makeEffectTestCallback( - Effect.gen(function* explicitMatcherSnapshot() { - const controlled = repository({ match: { deliveriesCreated: 3, messagesMatched: 2 } }); - const registration = worker(() => Effect.void); - const result = yield* makeOutboxRuntime(controlled.service).matchMessages({ - subscriptions: [registration.descriptor], - }); +it.effect('matches messages only through the explicit Core matcher snapshot', () => + Effect.gen(function* explicitMatcherSnapshot() { + const controlled = repository({ match: { deliveriesCreated: 3, messagesMatched: 2 } }); + const registration = worker(() => Effect.void); + const result = yield* makeOutboxRuntime(controlled.service).matchMessages({ + subscriptions: [registration.descriptor], + }); - assert.deepEqual(result, { deliveriesCreated: 3, messagesMatched: 2 }); - }), - ), + expect(result).toEqual({ deliveriesCreated: 3, messagesMatched: 2 }); + }), ); -void test( - 'rejects an owner-local worker missing from the installed subscription catalog', - makeEffectTestCallback( - Effect.gen(function* missingInstalledSubscription() { - const controlled = repository(); - const registration = worker(() => Effect.void); - const error = yield* Effect.flip( - makeOutboxRuntime(controlled.service).runCycle({ - claimOwner: 'unit-runtime', - registrations: [registration], - subscriptions: [], - }), - ); +it.effect('rejects an owner-local worker missing from the installed subscription catalog', () => + Effect.gen(function* missingInstalledSubscription() { + const controlled = repository(); + const registration = worker(() => Effect.void); + const error = yield* Effect.flip( + makeOutboxRuntime(controlled.service).runCycle({ + claimOwner: 'unit-runtime', + registrations: [registration], + subscriptions: [], + }), + ); - assert.equal(Schema.is(OutboxWorkerDescriptorError)(error), true); - assert.match(error.reason, /absent from the installed subscription catalog/u); - }), - ), + expect(Schema.is(OutboxWorkerDescriptorError)(error)).toBe(true); + expect(error.reason).toMatch(/absent from the installed subscription catalog/u); + }), ); -void test( - 'rejects deployed owner descriptors without a matching local worker registration', - makeEffectTestCallback( - Effect.gen(function* missingLocalRegistration() { - const controlled = repository(); - const registration = worker(() => Effect.void); - const error = yield* Effect.flip( - makeOutboxRuntime(controlled.service).runCycle({ - claimOwner: 'unit-runtime', - registrations: [registration], - subscriptions: [ - registration.descriptor, - { - ...registration.descriptor, - entrypoint: defineTenantModuleEntrypoint({ - access: 'background', - authorization: { kind: 'owner_local_background' }, - entrypointKey: 'consumer.second-worker', - moduleKey: registration.descriptor.consumerModuleKey, - role: 'worker', - }), - workerKey: 'consumer.second-worker', - }, - ], - }), - ); +it.effect('rejects deployed owner descriptors without a matching local worker registration', () => + Effect.gen(function* missingLocalRegistration() { + const controlled = repository(); + const registration = worker(() => Effect.void); + const error = yield* Effect.flip( + makeOutboxRuntime(controlled.service).runCycle({ + claimOwner: 'unit-runtime', + registrations: [registration], + subscriptions: [ + registration.descriptor, + { + ...registration.descriptor, + entrypoint: defineTenantModuleEntrypoint({ + access: 'background', + authorization: { kind: 'owner_local_background' }, + entrypointKey: 'consumer.second-worker', + moduleKey: registration.descriptor.consumerModuleKey, + role: 'worker', + }), + workerKey: 'consumer.second-worker', + }, + ], + }), + ); - assert.equal(Schema.is(OutboxWorkerDescriptorError)(error), true); - assert.match(error.reason, /contradicts its deployed descriptor snapshot/u); - }), - ), + expect(Schema.is(OutboxWorkerDescriptorError)(error)).toBe(true); + expect(error.reason).toMatch(/contradicts its deployed descriptor snapshot/u); + }), ); -void test( - 'decodes a published payload, supplies exact context, and completes success', - makeEffectTestCallback( - Effect.gen(function* successfulDelivery() { - const selected = claim(); - const controlled = repository({ claims: [selected] }); - let observed: WorkerInvocation | undefined; - const registration = worker((payload, context) => - Effect.sync(() => { - observed = { context, payload }; - }), - ); - - const result = yield* run(controlled.service, registration); - - assert.equal(result.succeeded, 1); - assert.deepEqual(controlled.probe.completed, [selected]); - assert.deepEqual(controlled.probe.failed, []); - assert.deepEqual(observed, { - context: { - attemptNumber: 1, - claimId: 'runtime:claim-1', - correlationId: 'correlation-1', - deliveryId: 'delivery-1', - domainEventId: 'event-1', - messageId: 'message-1', - producerModuleKey: 'producer', - tenantId: 'tenant-1', - tenantSequenceNo: 7n, - topic: 'producer.message-created', - workerKey: 'consumer.logger', - }, - payload: { messageKey: 'message-1' }, - }); - }), - ), +it.effect('decodes a published payload, supplies exact context, and completes success', () => + Effect.gen(function* successfulDelivery() { + const selected = claim(); + const controlled = repository({ claims: [selected] }); + let observed: WorkerInvocation | undefined; + const registration = worker((payload, context) => + Effect.sync(() => { + observed = { context, payload }; + }), + ); + + const result = yield* run(controlled.service, registration); + + expect(result.succeeded).toBe(1); + expect(controlled.probe.completed).toEqual([selected]); + expect(controlled.probe.failed).toEqual([]); + expect(observed).toEqual({ + context: { + attemptNumber: 1, + claimId: 'runtime:claim-1', + correlationId: 'correlation-1', + deliveryId: 'delivery-1', + domainEventId: 'event-1', + messageId: 'message-1', + producerModuleKey: 'producer', + tenantId: 'tenant-1', + tenantSequenceNo: 7n, + topic: 'producer.message-created', + workerKey: 'consumer.logger', + }, + payload: { messageKey: 'message-1' }, + }); + }), ); -void test( - 'runs a worker with Effect services provided by its owning MicroVertical host', - makeEffectTestCallback( - Effect.gen(function* ownerProvidedServices() { - const selected = { ...claim(), workerKey: 'consumer.layered-logger' }; - const controlled = repository({ claims: [selected] }); - const observed: string[] = []; - const registration = defineOutboxWorker( - { - consumerModuleKey: 'consumer', - entrypoint: defineTenantModuleEntrypoint({ - access: 'background', - authorization: { kind: 'owner_local_background' }, - entrypointKey: 'consumer.layered-logger', - moduleKey: 'consumer', - role: 'worker', - }), - leaseDurationMs: 30_000, - payloadSchema: Schema.Struct({ messageKey: MessageKey }), - producerModuleKey: 'producer', - retryPolicy, - topic: 'producer.message-created', - workerKey: 'consumer.layered-logger', - }, - (_payload, context) => - TestWorkerDependency.pipe( - Effect.flatMap(({ record }) => Effect.sync(() => record(context.messageId))), - ), +it.effect('runs a worker with Effect services provided by its owning MicroVertical host', () => + Effect.gen(function* ownerProvidedServices() { + const selected = { ...claim(), workerKey: 'consumer.layered-logger' }; + const controlled = repository({ claims: [selected] }); + const observed: string[] = []; + const registration = defineOutboxWorker( + { + consumerModuleKey: 'consumer', + entrypoint: defineTenantModuleEntrypoint({ + access: 'background', + authorization: { kind: 'owner_local_background' }, + entrypointKey: 'consumer.layered-logger', + moduleKey: 'consumer', + role: 'worker', + }), + leaseDurationMs: 30_000, + payloadSchema: Schema.Struct({ messageKey: MessageKey }), + producerModuleKey: 'producer', + retryPolicy, + topic: 'producer.message-created', + workerKey: 'consumer.layered-logger', + }, + (_payload, context) => + TestWorkerDependency.pipe( + Effect.flatMap(({ record }) => Effect.sync(() => record(context.messageId))), + ), + ); + + const result = yield* makeOutboxRuntime(controlled.service) + .runCycle({ + claimOwner: 'unit-runtime', + registrations: [registration], + subscriptions: [registration.descriptor], + }) + .pipe( + Effect.provideService(TestWorkerDependency, { + record: (messageId) => observed.push(messageId), + }), ); - const result = yield* makeOutboxRuntime(controlled.service) - .runCycle({ - claimOwner: 'unit-runtime', - registrations: [registration], - subscriptions: [registration.descriptor], - }) - .pipe( - Effect.provideService(TestWorkerDependency, { - record: (messageId) => observed.push(messageId), - }), - ); - - assert.equal(result.succeeded, 1); - assert.deepEqual(observed, ['message-1']); - }), - ), + expect(result.succeeded).toBe(1); + expect(observed).toEqual(['message-1']); + }), ); -void test( - 'records decode failures as retries without calling the handler or completion', - makeEffectTestCallback( - Effect.gen(function* decodeFailure() { - const controlled = repository({ - claims: [claim(1, { messageKey: 42 })], - failureStatuses: ['pending'], - }); - let calls = 0; - - const result = yield* run( - controlled.service, - worker(() => Effect.sync(() => (calls += 1))), - ); - - assert.equal(calls, 0); - assert.equal(result.failed, 1); - assert.equal(result.retried, 1); - assert.deepEqual(controlled.probe.completed, []); - assert.equal( - controlled.probe.failed[0]?.message, - 'The Outbox Message payload does not match its published schema', - ); - }), - ), +it.effect('records decode failures as retries without calling the handler or completion', () => + Effect.gen(function* decodeFailure() { + const controlled = repository({ + claims: [claim(1, { messageKey: 42 })], + failureStatuses: ['pending'], + }); + let calls = 0; + + const result = yield* run( + controlled.service, + worker(() => Effect.sync(() => (calls += 1))), + ); + + expect(calls).toBe(0); + expect(result.failed).toBe(1); + expect(result.retried).toBe(1); + expect(controlled.probe.completed).toEqual([]); + expect(controlled.probe.failed[0]?.message).toBe( + 'The Outbox Message payload does not match its published schema', + ); + }), ); -void test( - 'classifies declared failures, defects, retry exhaustion, and never completes them', - makeEffectTestCallback( - Effect.gen(function* failureClassification() { - const declared = repository({ claims: [claim()], failureStatuses: ['pending'] }); - const declaredResult = yield* run( - declared.service, - worker(() => Effect.fail(new TestHandlerFailure({ reason: 'secret typed detail' }))), - ); - assert.equal(declaredResult.retried, 1); - assert.equal( - declared.probe.failed[0]?.message, - 'The Outbox Worker handler returned a declared failure', - ); - - const defect = repository({ claims: [claim(2)], failureStatuses: ['dead'] }); - const defectResult = yield* run( - defect.service, - worker(() => Effect.die(new Error('database password must not be stored'))), - ); - assert.equal(defectResult.dead, 1); - assert.equal( - defect.probe.failed[0]?.message, - 'The Outbox Worker handler failed unexpectedly', - ); - assert.doesNotMatch(defect.probe.failed[0]?.message ?? '', /password/u); - assert.deepEqual(declared.probe.completed, []); - assert.deepEqual(defect.probe.completed, []); - }), - ), +it.effect('classifies declared failures, defects, retry exhaustion, and never completes them', () => + Effect.gen(function* failureClassification() { + const declared = repository({ claims: [claim()], failureStatuses: ['pending'] }); + const declaredResult = yield* run( + declared.service, + worker(() => Effect.fail(new TestHandlerFailure({ reason: 'secret typed detail' }))), + ); + expect(declaredResult.retried).toBe(1); + expect(declared.probe.failed[0]?.message).toBe( + 'The Outbox Worker handler returned a declared failure', + ); + + const defect = repository({ claims: [claim(2)], failureStatuses: ['dead'] }); + const defectResult = yield* run( + defect.service, + worker(() => Effect.die(new Error('database password must not be stored'))), + ); + expect(defectResult.dead).toBe(1); + expect(defect.probe.failed[0]?.message).toBe('The Outbox Worker handler failed unexpectedly'); + expect(defect.probe.failed[0]?.message ?? '').not.toMatch(/password/u); + expect(declared.probe.completed).toEqual([]); + expect(defect.probe.completed).toEqual([]); + }), ); -void test( +it.effect( 'surfaces stale-claim finalization and leaves checkpoint responsibility with the repository', - makeEffectTestCallback( + () => Effect.gen(function* staleClaimFinalization() { const controlled = repository({ claims: [claim()], @@ -391,8 +360,7 @@ void test( }); const error = yield* Effect.flip(run(controlled.service)); - assert.equal(Schema.is(OutboxClaimLostError)(error), true); - assert.deepEqual(controlled.probe.failed, []); + expect(Schema.is(OutboxClaimLostError)(error)).toBe(true); + expect(controlled.probe.failed).toEqual([]); }), - ), ); diff --git a/app/packages/core-runtime/tests/unit/permission-client.test.ts b/app/packages/core-runtime/tests/unit/permission-client.test.ts index ea820d5c9..8bdd41864 100644 --- a/app/packages/core-runtime/tests/unit/permission-client.test.ts +++ b/app/packages/core-runtime/tests/unit/permission-client.test.ts @@ -1,107 +1,112 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it, rstest } from 'effect-rstest'; import { v1 } from '@authzed/authzed-node'; -import { Cause, Effect, flow, Schema } from 'effect'; +import { Cause, Effect, Fiber, Predicate, Schema } from 'effect'; +import { TestClock } from 'effect/testing'; import { SpiceDbPermissionClientError, createSpiceDbPermissionClient, SPICEDB_CHECK_TIMEOUT_MS, } from '../../src/permissions/client.ts'; +type PermissionRpcError = NonNullable< + Parameters[3]>>[0] +>; + const configuration = { endpoint: 'localhost:50051', insecureLocal: true, preSharedKey: 'test-key', } as const; -test( - 'permission RPCs are lazy and execute again on each Effect run', - flow( - (context) => - Effect.gen(function* checksLazyRpcExecution() { - const check = context.mock.method( - v1.PermissionsServiceClient.prototype, - 'checkPermission', - ( - _request: v1.CheckPermissionRequest, - // oxlint-disable-next-line promise/prefer-await-to-callbacks -- Implements the Authzed SDK callback protocol; remove-when: SDK exposes native Effect. - callback: (error: null, response: v1.CheckPermissionResponse) => void, - // oxlint-disable-next-line promise/prefer-await-to-callbacks -- Implements the Authzed SDK callback protocol; remove-when: SDK exposes native Effect. - ) => callback(null, v1.CheckPermissionResponse.create({})), - ); - const bulk = context.mock.method( - v1.PermissionsServiceClient.prototype, - 'checkBulkPermissions', - ( - _request: v1.CheckBulkPermissionsRequest, - // oxlint-disable-next-line promise/prefer-await-to-callbacks -- Implements the Authzed SDK callback protocol; remove-when: SDK exposes native Effect. - callback: (error: null, response: v1.CheckBulkPermissionsResponse) => void, - // oxlint-disable-next-line promise/prefer-await-to-callbacks -- Implements the Authzed SDK callback protocol; remove-when: SDK exposes native Effect. - ) => callback(null, v1.CheckBulkPermissionsResponse.create({})), - ); - const client = createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS); - context.after(() => client.close()); - const request = v1.CheckPermissionRequest.create({}); - const bulkRequest = v1.CheckBulkPermissionsRequest.create({}); - const operation = client.checkPermission(request); - const bulkOperation = client.checkBulkPermissions(bulkRequest); - assert.equal(check.mock.callCount(), 0); - assert.equal(bulk.mock.callCount(), 0); - yield* operation; - yield* operation; - yield* bulkOperation; - yield* bulkOperation; - assert.equal(check.mock.callCount(), 2); - assert.equal(bulk.mock.callCount(), 2); - assert.equal(check.mock.calls[0]?.arguments[0], request); - assert.equal(bulk.mock.calls[0]?.arguments[0], bulkRequest); - }), - runEffectTestPromise, - ), +it.effect('permission RPCs are lazy and execute again on each Effect run', () => + Effect.gen(function* checksLazyRpcExecution() { + yield* Effect.addFinalizer(() => Effect.sync(() => rstest.restoreAllMocks())); + const check = rstest + .spyOn(v1.PermissionsServiceClient.prototype, 'checkPermission') + .mockImplementation((request, metadata) => { + if (Predicate.isFunction(metadata)) { + metadata(null, v1.CheckPermissionResponse.create({})); + } + return rstest.fn()(request, metadata); + }); + const bulk = rstest + .spyOn(v1.PermissionsServiceClient.prototype, 'checkBulkPermissions') + .mockImplementation((request, metadata) => { + if (Predicate.isFunction(metadata)) { + metadata(null, v1.CheckBulkPermissionsResponse.create({})); + } + return rstest.fn()(request, metadata); + }); + const client = yield* Effect.acquireRelease( + Effect.sync(() => createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS)), + (acquiredClient) => Effect.sync(() => acquiredClient.close()), + ); + const request = v1.CheckPermissionRequest.create({}); + const bulkRequest = v1.CheckBulkPermissionsRequest.create({}); + const operation = client.checkPermission(request); + const bulkOperation = client.checkBulkPermissions(bulkRequest); + expect(check.mock.calls.length).toBe(0); + expect(bulk.mock.calls.length).toBe(0); + yield* operation; + yield* operation; + yield* bulkOperation; + yield* bulkOperation; + expect(check.mock.calls.length).toBe(2); + expect(bulk.mock.calls.length).toBe(2); + expect(check.mock.calls[0]?.[0]).toBe(request); + expect(bulk.mock.calls[0]?.[0]).toBe(bulkRequest); + }), ); -test( - 'SDK rejections become typed permission failures without leaking diagnostics', - flow( - (context) => - Effect.gen(function* checksTypedSdkFailure() { - const cause = new Error('private transport diagnostic'); - context.mock.method( - v1.PermissionsServiceClient.prototype, - 'checkPermission', - // oxlint-disable-next-line promise/prefer-await-to-callbacks -- Implements the Authzed SDK callback protocol; remove-when: SDK exposes native Effect. - (_request: v1.CheckPermissionRequest, callback: (error: Error) => void) => - // oxlint-disable-next-line promise/prefer-await-to-callbacks -- Implements the Authzed SDK callback protocol; remove-when: SDK exposes native Effect. - callback(cause), - ); - const client = createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS); - context.after(() => client.close()); - const failure = yield* Effect.flip( - client.checkPermission(v1.CheckPermissionRequest.create({})), - ); - assert.ok(Schema.is(SpiceDbPermissionClientError)(failure)); - assert.equal(failure.reason.includes(cause.message), false); - assert.equal(Object.getOwnPropertyDescriptor(failure, 'cause')?.value, cause); - }), - runEffectTestPromise, - ), +it.effect('SDK rejections become typed permission failures without leaking diagnostics', () => + Effect.gen(function* checksTypedSdkFailure() { + yield* Effect.addFinalizer(() => Effect.sync(() => rstest.restoreAllMocks())); + const cause = Object.assign(new Error('private transport diagnostic'), { + code: 13, + details: 'private transport diagnostic', + metadata: rstest.fn<() => PermissionRpcError['metadata']>()(), + }); + rstest + .spyOn(v1.PermissionsServiceClient.prototype, 'checkPermission') + .mockImplementation((request, metadata) => { + if (Predicate.isFunction(metadata)) { + metadata(cause); + } + return rstest.fn()(request, metadata); + }); + const client = yield* Effect.acquireRelease( + Effect.sync(() => createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS)), + (acquiredClient) => Effect.sync(() => acquiredClient.close()), + ); + const failure = yield* Effect.flip( + client.checkPermission(v1.CheckPermissionRequest.create({})), + ); + expect(Schema.is(SpiceDbPermissionClientError)(failure)).toBe(true); + expect(failure.reason.includes(cause.message)).toBe(false); + expect(Object.getOwnPropertyDescriptor(failure, 'cause')?.value).toBe(cause); + }), ); -test( - 'an SDK call that never replies is bounded by the permission deadline', - flow( - (context) => - Effect.gen(function* checksPermissionDeadline() { - context.mock.method(v1.PermissionsServiceClient.prototype, 'checkPermission', () => {}); - const client = createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS); - context.after(() => client.close()); - const failure = yield* Effect.flip( - client.checkPermission(v1.CheckPermissionRequest.create({})), - ); - assert.ok(Schema.is(SpiceDbPermissionClientError)(failure)); - assert.ok(Cause.isTimeoutError(Object.getOwnPropertyDescriptor(failure, 'cause')?.value)); - }), - runEffectTestPromise, - ), +it.effect('an SDK call that never replies is bounded by the permission deadline', () => + Effect.gen(function* checksPermissionDeadline() { + yield* Effect.addFinalizer(() => Effect.sync(() => rstest.restoreAllMocks())); + rstest + .spyOn(v1.PermissionsServiceClient.prototype, 'checkPermission') + .mockImplementation((request, metadata) => + rstest.fn()(request, metadata), + ); + const client = yield* Effect.acquireRelease( + Effect.sync(() => createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS)), + (acquiredClient) => Effect.sync(() => acquiredClient.close()), + ); + const fiber = yield* Effect.flip( + client.checkPermission(v1.CheckPermissionRequest.create({})), + ).pipe(Effect.forkChild); + yield* TestClock.adjust(SPICEDB_CHECK_TIMEOUT_MS); + const failure = yield* Fiber.join(fiber); + expect(Schema.is(SpiceDbPermissionClientError)(failure)).toBe(true); + expect(Cause.isTimeoutError(Object.getOwnPropertyDescriptor(failure, 'cause')?.value)).toBe( + true, + ); + }), ); diff --git a/app/packages/core-runtime/tests/unit/pool-configuration.test.ts b/app/packages/core-runtime/tests/unit/pool-configuration.test.ts index 1248760f5..ce4281ab5 100644 --- a/app/packages/core-runtime/tests/unit/pool-configuration.test.ts +++ b/app/packages/core-runtime/tests/unit/pool-configuration.test.ts @@ -1,7 +1,5 @@ -import { makeEffectTestCallback } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Redacted } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Redacted, Predicate } from 'effect'; import { DEFAULT_DATABASE_POOL_DEADLINES, configureDatabasePool, @@ -9,82 +7,64 @@ import { const runtimeUrl = 'postgresql://runtime:secret@localhost:5432/ontos'; -test( - 'uses acquisition and statement deadlines without opting into a lock deadline', - makeEffectTestCallback( - Effect.gen(function* verifyDefaults() { - const connectionString = Redacted.make(`${runtimeUrl}?sslmode=require`); - const configuration = yield* configureDatabasePool(connectionString); +it.effect('uses acquisition and statement deadlines without opting into a lock deadline', () => + Effect.gen(function* verifyDefaults() { + const connectionString = Redacted.make(`${runtimeUrl}?sslmode=require`); + const configuration = yield* configureDatabasePool(connectionString); - assert.equal( - configuration.connectionTimeoutMillis, - DEFAULT_DATABASE_POOL_DEADLINES.connectionTimeoutMillis, - ); - assert.equal( - configuration.statement_timeout, - DEFAULT_DATABASE_POOL_DEADLINES.statement_timeout, - ); - assert.equal(Object.hasOwn(configuration, 'lock_timeout'), false); - assert.equal(configuration.connectionString, `${runtimeUrl}?sslmode=require`); - }), - ), + expect(configuration.connectionTimeoutMillis).toBe( + DEFAULT_DATABASE_POOL_DEADLINES.connectionTimeoutMillis, + ); + expect(configuration.statement_timeout).toBe(DEFAULT_DATABASE_POOL_DEADLINES.statement_timeout); + expect(Object.hasOwn(configuration, 'lock_timeout')).toBe(false); + expect(configuration.connectionString).toBe(`${runtimeUrl}?sslmode=require`); + }), ); -test( - 'includes an explicitly opted-in lock deadline', - makeEffectTestCallback( - Effect.gen(function* verifyLockDeadline() { - const connectionString = Redacted.make(runtimeUrl); - const configuration = yield* configureDatabasePool(connectionString, { - lock_timeout: 250, - }); +it.effect('includes an explicitly opted-in lock deadline', () => + Effect.gen(function* verifyLockDeadline() { + const connectionString = Redacted.make(runtimeUrl); + const configuration = yield* configureDatabasePool(connectionString, { + lock_timeout: 250, + }); - assert.equal(configuration.lock_timeout, 250); - }), - ), + expect(configuration.lock_timeout).toBe(250); + }), ); -test( - 'rejects URL deadline overrides with a typed configuration failure', - makeEffectTestCallback( - Effect.forEach( - [ - 'connectionTimeoutMillis=1', - 'connect_timeout=1', - 'lock_timeout=1', - 'statement_timeout=1', - 'query_timeout=1', - 'options=-c%20statement_timeout%3D1', - ], - (parameter) => - Effect.gen(function* verifyParameter() { - const connectionString = Redacted.make(`${runtimeUrl}?${parameter}`); - const error = yield* Effect.flip(configureDatabasePool(connectionString)); - assert.equal(error._tag, 'DatabaseConnectionError'); - assert.equal( - error.reason, - 'Database URL deadline parameters and startup options are unsupported; use poolDeadlines', - ); - }), - { concurrency: 'unbounded' }, - ), +it.effect('rejects URL deadline overrides with a typed configuration failure', () => + Effect.forEach( + [ + 'connectionTimeoutMillis=1', + 'connect_timeout=1', + 'lock_timeout=1', + 'statement_timeout=1', + 'query_timeout=1', + 'options=-c%20statement_timeout%3D1', + ], + (parameter) => + Effect.gen(function* verifyParameter() { + const connectionString = Redacted.make(`${runtimeUrl}?${parameter}`); + const error = yield* Effect.flip(configureDatabasePool(connectionString)); + expect(Predicate.isTagged(error, 'DatabaseConnectionError')).toBe(true); + expect(error.reason).toBe( + 'Database URL deadline parameters and startup options are unsupported; use poolDeadlines', + ); + }), + { concurrency: 'unbounded' }, ), ); -test( - 'rejects invalid deadline values with a typed configuration failure', - makeEffectTestCallback( - Effect.gen(function* verifyInvalidDeadline() { - const connectionString = Redacted.make(runtimeUrl); - const error = yield* Effect.flip( - configureDatabasePool(connectionString, { statement_timeout: 0 }), - ); +it.effect('rejects invalid deadline values with a typed configuration failure', () => + Effect.gen(function* verifyInvalidDeadline() { + const connectionString = Redacted.make(runtimeUrl); + const error = yield* Effect.flip( + configureDatabasePool(connectionString, { statement_timeout: 0 }), + ); - assert.equal(error._tag, 'DatabaseConnectionError'); - assert.equal( - error.reason, - 'Database pool deadlines must be positive 32-bit millisecond integers', - ); - }), - ), + expect(Predicate.isTagged(error, 'DatabaseConnectionError')).toBe(true); + expect(error.reason).toBe( + 'Database pool deadlines must be positive 32-bit millisecond integers', + ); + }), ); diff --git a/app/packages/core-runtime/tests/unit/principal-management.test.ts b/app/packages/core-runtime/tests/unit/principal-management.test.ts index 962c4df8f..a043aa5d4 100644 --- a/app/packages/core-runtime/tests/unit/principal-management.test.ts +++ b/app/packages/core-runtime/tests/unit/principal-management.test.ts @@ -1,7 +1,6 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Option, flow } from 'effect'; +import { expect, it } from 'effect-rstest'; + +import { Effect, Option, Predicate } from 'effect'; import type { PrincipalManagementPersistence, PrincipalManagementRepositoryService, @@ -18,12 +17,6 @@ import { const tenantId = '10000000-0000-4000-8000-000000000001'; const principalId = '20000000-0000-4000-8000-000000000001'; const authBindingId = '30000000-0000-4000-8000-000000000001'; -const effectTest = (name: string, effect: Effect.Effect): void => { - test( - name, - flow(() => Effect.asVoid(effect), runEffectTestPromise), - ); -}; const unconfigured = (operation: string) => Effect.die(`${operation} is not configured in this test`); @@ -40,7 +33,6 @@ const repository = ( overrides: Partial, ): PrincipalManagementRepositoryService => principalManagementRepositoryFromPersistence({ ...repositoryDefaults, ...overrides }); - type OptionValue = Outcome extends Option.Option ? Value : never; type PrincipalRecord = OptionValue< Effect.Success> @@ -48,7 +40,6 @@ type PrincipalRecord = OptionValue< type ApiKeyBindingRecord = OptionValue< Effect.Success> >; - const selectingPrincipal = (record: PrincipalRecord | undefined) => repository({ loadPrincipal: () => Effect.succeed(Option.fromNullishOr(record)) }); const selectingBinding = (record: ApiKeyBindingRecord | undefined) => @@ -66,12 +57,9 @@ const repositoryForSupportParticipants = ( }), }); }; - const provideRepository = (service: PrincipalManagementRepositoryService) => Effect.provideService(PrincipalManagementRepository, service); - -effectTest( - 'rejects human principal administration and managed keys targeting humans', +it.effect('rejects human principal administration and managed keys targeting humans', () => Effect.gen(function* rejectsHumanPrincipalAdministration() { const transaction = selectingPrincipal({ kind: 'human', status: 'active' }); const principalError = yield* Effect.flip( @@ -92,13 +80,11 @@ effectTest( }).pipe(provideRepository(transaction)), ); - assert.equal(principalError._tag, 'IdentityTargetInvalidError'); - assert.equal(bindingError._tag, 'IdentityTargetInvalidError'); + expect(Predicate.isTagged(principalError, 'IdentityTargetInvalidError')).toBe(true); + expect(Predicate.isTagged(bindingError, 'IdentityTargetInvalidError')).toBe(true); }), ); - -effectTest( - 'enforces expected state, terminal revocation, and revocation reasons', +it.effect('enforces expected state, terminal revocation, and revocation reasons', () => Effect.gen(function* enforcesBindingLifecycle() { const conflictError = yield* Effect.flip( setApiKeyBindingStatus({ @@ -157,14 +143,12 @@ effectTest( ), ); - assert.equal(conflictError._tag, 'IdentityLifecycleConflictError'); - assert.equal(terminalError._tag, 'IdentityLifecycleConflictError'); - assert.equal(reasonError._tag, 'IdentityTargetInvalidError'); + expect(Predicate.isTagged(conflictError, 'IdentityLifecycleConflictError')).toBe(true); + expect(Predicate.isTagged(terminalError, 'IdentityLifecycleConflictError')).toBe(true); + expect(Predicate.isTagged(reasonError, 'IdentityTargetInvalidError')).toBe(true); }), ); - -effectTest( - 'rejects managed binding transitions for human or inactive targets', +it.effect('rejects managed binding transitions for human or inactive targets', () => Effect.gen(function* rejectsIneligibleBindingTargets() { const records = [ { bindingStatus: 'active', principalKind: 'human', principalStatus: 'active' }, @@ -183,42 +167,42 @@ effectTest( tenantId, }).pipe(provideRepository(selectingBinding(record))), ); - assert.equal(error._tag, 'IdentityTargetInvalidError'); + expect(Predicate.isTagged(error, 'IdentityTargetInvalidError')).toBe(true); }), ), ); }), ); - -effectTest( +it.effect( 'binds only eligible active self and managed principal kinds without secret material', - Effect.gen(function* bindsEligiblePrincipal() { - let inserted: Parameters[0] | undefined; - const transaction = repository({ - insertApiKeyBinding: (value) => - Effect.sync(() => { - inserted = value; - return Option.some({ authBindingId }); - }), - loadPrincipal: () => Effect.succeed(Option.some({ kind: 'service', status: 'active' })), - }); - const result = yield* bindApiKey({ - managed: true, - principalId, - providerSubjectId: 'provider-key-id', - tenantId, - }).pipe(provideRepository(transaction)); - - assert.deepEqual(result, { authBindingId, status: 'active' }); - assert.equal(inserted?.providerSubjectId, 'provider-key-id'); - assert.equal('key' in (inserted ?? {}), false); - assert.equal('secret' in (inserted ?? {}), false); - assert.equal('hash' in (inserted ?? {}), false); - }), + () => + Effect.gen(function* bindsEligiblePrincipal() { + let inserted: + | Parameters[0] + | undefined; + const transaction = repository({ + insertApiKeyBinding: (value) => + Effect.sync(() => { + inserted = value; + return Option.some({ authBindingId }); + }), + loadPrincipal: () => Effect.succeed(Option.some({ kind: 'service', status: 'active' })), + }); + const result = yield* bindApiKey({ + managed: true, + principalId, + providerSubjectId: 'provider-key-id', + tenantId, + }).pipe(provideRepository(transaction)); + + expect(result).toEqual({ authBindingId, status: 'active' }); + expect(inserted?.providerSubjectId).toBe('provider-key-id'); + expect('key' in (inserted ?? {})).toBe(false); + expect('secret' in (inserted ?? {})).toBe(false); + expect('hash' in (inserted ?? {})).toBe(false); + }), ); - -effectTest( - 'maps an existing API key binding to a lifecycle conflict', +it.effect('maps an existing API key binding to a lifecycle conflict', () => Effect.gen(function* mapsExistingBindingToConflict() { const transaction = repository({ insertApiKeyBinding: () => Effect.succeed(Option.none()), @@ -234,37 +218,37 @@ effectTest( }).pipe(provideRepository(transaction)), ); - assert.equal(error._tag, 'IdentityLifecycleConflictError'); + expect(Predicate.isTagged(error, 'IdentityLifecycleConflictError')).toBe(true); }), ); - -effectTest( +it.effect( 'requires exactly one active tenant-local user binding for both impersonation participants', - Effect.gen(function* validatesSupportParticipants() { - const original = [{ authBindingId }]; - const target = [{ authBindingId: '30000000-0000-4000-8000-000000000002' }]; - const input: Parameters[0] = { - checkpoint: 'requested', - originalAuthBindingId: authBindingId, - originalPrincipalId: principalId, - targetPrincipalId: '20000000-0000-4000-8000-000000000002', - tenantId, - }; - - yield* validateSupportImpersonation(input).pipe( - provideRepository(repositoryForSupportParticipants([original, target])), - ); - const error = yield* Effect.flip( - validateSupportImpersonation(input).pipe( - provideRepository(repositoryForSupportParticipants([original, []])), - ), - ); + () => + Effect.gen(function* validatesSupportParticipants() { + const original = [{ authBindingId }]; + const target = [{ authBindingId: '30000000-0000-4000-8000-000000000002' }]; + const input: Parameters[0] = { + checkpoint: 'requested', + originalAuthBindingId: authBindingId, + originalPrincipalId: principalId, + targetPrincipalId: '20000000-0000-4000-8000-000000000002', + tenantId, + }; + + yield* validateSupportImpersonation(input).pipe( + provideRepository(repositoryForSupportParticipants([original, target])), + ); + const error = yield* Effect.flip( + validateSupportImpersonation(input).pipe( + provideRepository(repositoryForSupportParticipants([original, []])), + ), + ); - assert.equal(error._tag, 'IdentityTargetInvalidError'); + expect(Predicate.isTagged(error, 'IdentityTargetInvalidError')).toBe(true); - yield* validateSupportImpersonation({ - ...input, - checkpoint: 'stopped', - }).pipe(provideRepository(repositoryForSupportParticipants([original, target]))); - }), + yield* validateSupportImpersonation({ + ...input, + checkpoint: 'stopped', + }).pipe(provideRepository(repositoryForSupportParticipants([original, target]))); + }), ); diff --git a/app/packages/core-runtime/tests/unit/principal-resolver.test.ts b/app/packages/core-runtime/tests/unit/principal-resolver.test.ts index 9a0e5ecb9..5e77e0780 100644 --- a/app/packages/core-runtime/tests/unit/principal-resolver.test.ts +++ b/app/packages/core-runtime/tests/unit/principal-resolver.test.ts @@ -1,9 +1,8 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { DateTime, Effect, flow } from 'effect'; +import { expect, it } from 'effect-rstest'; + +import { DateTime, Effect, Predicate } from 'effect'; import { ConnectionError, SqlError } from 'effect/unstable/sql/SqlError'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import type { PrincipalResolutionError } from '../../src/auth/principal-resolver-errors.ts'; + import type { PrincipalResolutionRecord } from '../../src/auth/principal-resolver.ts'; import { classifyApiKeyPrincipal, @@ -14,13 +13,6 @@ import { } from '../../src/auth/principal-resolver.ts'; import { makeTestDatabase } from '../support/database.ts'; -const effectTest = (name: string, effect: Effect.Effect): void => { - test( - name, - flow(() => Effect.asVoid(effect), runEffectTestPromise), - ); -}; - const activeRecord: PrincipalResolutionRecord = { authBindingId: 'binding-1', bindingCreatedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), @@ -34,17 +26,7 @@ const activeRecord: PrincipalResolutionRecord = { tenantName: 'Zeta tenant', tenantStatus: 'active', }; - -const failureTag = (effect: Effect.Effect) => - effect.pipe( - Effect.match({ - onFailure: (error) => error._tag, - onSuccess: () => assert.fail('Expected principal resolution to fail'), - }), - ); - -effectTest( - 'lists safe eligible tenants by name and tenant ID', +it.effect('lists safe eligible tenants by name and tenant ID', () => Effect.gen(function* listsEligibleTenants() { const records = [ activeRecord, @@ -86,28 +68,26 @@ effectTest( ]; const result = yield* classifyAvailableTenants(records); - assert.deepEqual(result, [ + expect(result).toEqual([ { name: 'Alpha tenant', tenantId: 'tenant-2' }, { name: 'Alpha tenant', tenantId: 'tenant-3' }, { name: 'Zeta tenant', tenantId: 'tenant-1' }, ]); }), ); - -effectTest( - 'lists and resolves one active tenant binding', +it.effect('lists and resolves one active tenant binding', () => Effect.gen(function* listsAndResolvesActiveTenant() { - assert.deepEqual(yield* classifyAvailableTenants([activeRecord]), [ + expect(yield* classifyAvailableTenants([activeRecord])).toEqual([ { name: 'Zeta tenant', tenantId: 'tenant-1' }, ]); - assert.deepEqual(yield* classifyDefaultPrincipal([activeRecord]), { + expect(yield* classifyDefaultPrincipal([activeRecord])).toEqual({ authBindingId: 'binding-1', displayName: 'Ada Lovelace', principalId: 'principal-1', principalKind: 'human', tenantId: 'tenant-1', }); - assert.deepEqual(yield* classifySelectedPrincipal([activeRecord], 'tenant-1'), { + expect(yield* classifySelectedPrincipal([activeRecord], 'tenant-1')).toEqual({ authBindingId: 'binding-1', displayName: 'Ada Lovelace', principalId: 'principal-1', @@ -116,9 +96,7 @@ effectTest( }); }), ); - -effectTest( - 'chooses the oldest eligible binding and breaks creation ties by tenant ID', +it.effect('chooses the oldest eligible binding and breaks creation ties by tenant ID', () => Effect.gen(function* choosesOldestBinding() { const result = yield* classifyDefaultPrincipal([ activeRecord, @@ -136,7 +114,7 @@ effectTest( }, ]); - assert.deepEqual(result, { + expect(result).toEqual({ authBindingId: 'binding-1', displayName: 'Tie winner', principalId: 'principal-0', @@ -145,9 +123,7 @@ effectTest( }); }), ); - -effectTest( - 'resolves only the exact eligible selected tenant', +it.effect('resolves only the exact eligible selected tenant', () => Effect.gen(function* resolvesExactTenant() { const selected = { ...activeRecord, @@ -155,111 +131,134 @@ effectTest( principalId: 'principal-2', tenantId: 'tenant-2', }; - assert.deepEqual(yield* classifySelectedPrincipal([activeRecord, selected], 'tenant-2'), { + expect(yield* classifySelectedPrincipal([activeRecord, selected], 'tenant-2')).toEqual({ authBindingId: 'binding-1', displayName: 'Grace Hopper', principalId: 'principal-2', principalKind: 'human', tenantId: 'tenant-2', }); - assert.equal( - yield* failureTag(classifySelectedPrincipal([activeRecord, selected], 'foreign-tenant')), - 'PrincipalBindingMissingError', - ); + expect( + Predicate.isTagged( + yield* Effect.flip(classifySelectedPrincipal([activeRecord, selected], 'foreign-tenant')), + 'PrincipalBindingMissingError', + ), + ).toBe(true); }), ); - -effectTest( - 'rejects Better Auth user bindings to non-human principals', +it.effect('rejects Better Auth user bindings to non-human principals', () => Effect.all( (['service', 'integration', 'agent', 'system'] as const).map((principalKind) => Effect.gen(function* rejectsNonHumanPrincipal() { const record = { ...activeRecord, principalKind }; - assert.equal( - yield* failureTag(classifyDefaultPrincipal([record])), - 'PrincipalInactiveError', - ); - assert.equal( - yield* failureTag(classifySelectedPrincipal([record], record.tenantId)), - 'PrincipalInactiveError', - ); - assert.equal( - yield* failureTag(classifyAvailableTenants([record])), - 'PrincipalInactiveError', - ); + expect( + Predicate.isTagged( + yield* Effect.flip(classifyDefaultPrincipal([record])), + 'PrincipalInactiveError', + ), + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip(classifySelectedPrincipal([record], record.tenantId)), + 'PrincipalInactiveError', + ), + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip(classifyAvailableTenants([record])), + 'PrincipalInactiveError', + ), + ).toBe(true); }), ), ), ); - -effectTest( +it.effect( 'resolves exactly one API-key subject for human, service, or integration principals', - Effect.gen(function* resolvesApiKeySubject() { - yield* Effect.all( - (['human', 'service', 'integration'] as const).map((principalKind) => - Effect.gen(function* resolvesPrincipalKind() { - const resolved = yield* classifyApiKeyPrincipal([{ ...activeRecord, principalKind }]); - assert.equal(resolved.principalKind, principalKind); - assert.equal(resolved.authBindingId, activeRecord.authBindingId); - }), - ), - ); - assert.equal( - yield* failureTag( - classifyApiKeyPrincipal([activeRecord, { ...activeRecord, tenantId: 't-2' }]), - ), - 'PrincipalBindingAmbiguousError', - ); - }), + () => + Effect.gen(function* resolvesApiKeySubject() { + yield* Effect.all( + (['human', 'service', 'integration'] as const).map((principalKind) => + Effect.gen(function* resolvesPrincipalKind() { + const resolved = yield* classifyApiKeyPrincipal([{ ...activeRecord, principalKind }]); + expect(resolved.principalKind).toBe(principalKind); + expect(resolved.authBindingId).toBe(activeRecord.authBindingId); + }), + ), + ); + expect( + Predicate.isTagged( + yield* Effect.flip( + classifyApiKeyPrincipal([activeRecord, { ...activeRecord, tenantId: 't-2' }]), + ), + 'PrincipalBindingAmbiguousError', + ), + ).toBe(true); + }), ); - -effectTest( - 'fails closed for empty, inactive, and duplicate eligible resolver states', +it.effect('fails closed for empty, inactive, and duplicate eligible resolver states', () => Effect.gen(function* rejectsInvalidResolverStates() { - assert.equal(yield* failureTag(classifyAvailableTenants([])), 'PrincipalBindingMissingError'); - assert.equal( - yield* failureTag(classifyAvailableTenants([{ ...activeRecord, bindingStatus: 'revoked' }])), - 'PrincipalBindingInactiveError', - ); - assert.equal( - yield* failureTag( - classifyAvailableTenants([ - { - ...activeRecord, - bindingRevokedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-03-01T00:00:00.000Z')), - }, - ]), + expect( + Predicate.isTagged( + yield* Effect.flip(classifyAvailableTenants([])), + 'PrincipalBindingMissingError', ), - 'PrincipalBindingInactiveError', - ); - assert.equal( - yield* failureTag( - classifyAvailableTenants([{ ...activeRecord, principalStatus: 'disabled' }]), + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip( + classifyAvailableTenants([{ ...activeRecord, bindingStatus: 'revoked' }]), + ), + 'PrincipalBindingInactiveError', ), - 'PrincipalInactiveError', - ); - assert.equal( - yield* failureTag(classifyAvailableTenants([{ ...activeRecord, tenantStatus: 'suspended' }])), - 'TenantInactiveError', - ); - assert.equal( - yield* failureTag( - classifyAvailableTenants([ - activeRecord, - { ...activeRecord, principalId: 'duplicate-principal' }, - ]), + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip( + classifyAvailableTenants([ + { + ...activeRecord, + bindingRevokedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-03-01T00:00:00.000Z')), + }, + ]), + ), + 'PrincipalBindingInactiveError', ), - 'PrincipalBindingAmbiguousError', - ); + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip( + classifyAvailableTenants([{ ...activeRecord, principalStatus: 'disabled' }]), + ), + 'PrincipalInactiveError', + ), + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip( + classifyAvailableTenants([{ ...activeRecord, tenantStatus: 'suspended' }]), + ), + 'TenantInactiveError', + ), + ).toBe(true); + expect( + Predicate.isTagged( + yield* Effect.flip( + classifyAvailableTenants([ + activeRecord, + { ...activeRecord, principalId: 'duplicate-principal' }, + ]), + ), + 'PrincipalBindingAmbiguousError', + ), + ).toBe(true); }), ); - -effectTest( - 'types database failures as resolver unavailability', +it.effect('types database failures as resolver unavailability', () => Effect.gen(function* sanitizesResolverDatabaseFailure() { const error = yield* Effect.flip( makePrincipalResolver({ - executor: makeTestDatabase(() => + executor: yield* makeTestDatabase(() => Effect.fail( new SqlError({ reason: new ConnectionError({ cause: new Error('secret database error') }), @@ -268,7 +267,9 @@ effectTest( ), }).listAvailableTenants('subject'), ); - assert.equal(error._tag, 'PrincipalResolverUnavailableError'); - assert.doesNotMatch(error.reason, /secret database error/u); + if (!Predicate.isTagged(error, 'PrincipalResolverUnavailableError')) { + expect.unreachable('Expected resolver unavailability'); + } + expect(error.reason).not.toMatch(/secret database error/u); }), ); diff --git a/app/packages/core-runtime/tests/unit/read-definition.test.ts b/app/packages/core-runtime/tests/unit/read-definition.test.ts index bf816cca0..e14fca1e1 100644 --- a/app/packages/core-runtime/tests/unit/read-definition.test.ts +++ b/app/packages/core-runtime/tests/unit/read-definition.test.ts @@ -1,14 +1,14 @@ -// @effect-diagnostics asyncFunction:off nodeBuiltinImport:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; +// @effect-diagnostics nodeBuiltinImport:off -- Verifies package source files via the Node filesystem boundary; expires: 2026-12-31. +import { expect, it } from 'effect-rstest'; + import { readFile } from 'node:fs/promises'; -import test from 'node:test'; + import { Effect, Schema } from 'effect'; import { defineRead, validateReadDescriptorInput } from '../../src/reads/definition.ts'; import { defineSystemModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; const modulePermissionTarget = () => ({ kind: 'module', moduleId: 'core.shell' }) as const; - -void test('defines immutable read metadata while keeping handler and service factory private', () => { +it('defines immutable read metadata while keeping handler and service factory private', () => { const registration = defineRead( { accessKind: 'list', @@ -33,13 +33,12 @@ void test('defines immutable read metadata while keeping handler and service fac () => Effect.succeed(Object.freeze({})), modulePermissionTarget, ); - assert.deepEqual(Object.keys(registration), ['descriptor']); - assert.equal(Object.isFrozen(registration.descriptor), true); - assert.equal(Object.isFrozen(registration.descriptor.policies), true); + expect(Object.keys(registration)).toEqual(['descriptor']); + expect(Object.isFrozen(registration.descriptor)).toBe(true); + expect(Object.isFrozen(registration.descriptor.policies)).toBe(true); }); - -void test('requires an explicit valid owner-scoped read entrypoint', () => { - assert.throws(() => +it('requires an explicit valid owner-scoped read entrypoint', () => { + expect(() => validateReadDescriptorInput({ entrypoint: defineSystemModuleEntrypoint({ access: 'read', @@ -51,12 +50,11 @@ void test('requires an explicit valid owner-scoped read entrypoint', () => { legalEntityScope: 'forbidden', owningModuleKey: 'core.shell', }), - ); + ).toThrow(); }); - -void test('supports every governed access kind and rejects forged scope metadata', () => { +it('supports every governed access kind and rejects forged scope metadata', () => { for (const accessKind of ['detail', 'download', 'export', 'list', 'report', 'search'] as const) { - assert.doesNotThrow(() => + expect(() => defineRead( { accessKind, @@ -85,9 +83,9 @@ void test('supports every governed access kind and rejects forged scope metadata modulePermissionTarget, accessKind === 'search' ? () => [] : undefined, ), - ); + ).not.toThrow(); } - assert.throws(() => + expect(() => validateReadDescriptorInput({ entrypoint: defineSystemModuleEntrypoint({ access: 'read', @@ -99,14 +97,18 @@ void test('supports every governed access kind and rejects forged scope metadata legalEntityScope: 'implicit', owningModuleKey: 'core.shell', }), - ); -}); - -void test('keeps low-level read runtime construction and Core schema out of package exports', async () => { - const [indexSource, packageSource] = await Promise.all([ - readFile(new URL('../../src/index.ts', import.meta.url), 'utf-8'), - readFile(new URL('../../package.json', import.meta.url), 'utf-8'), - ]); - assert.doesNotMatch(indexSource, /\bmakeReadRuntime,?$/mu); - assert.doesNotMatch(packageSource, /"\.\/db\/schema"/u); + ).toThrow(); }); +it.effect('keeps low-level read runtime construction and Core schema out of package exports', () => + Effect.gen(function* migratedTest1() { + const [indexSource, packageSource] = yield* Effect.all( + [ + Effect.promise(() => readFile(new URL('../../src/index.ts', import.meta.url), 'utf-8')), + Effect.promise(() => readFile(new URL('../../package.json', import.meta.url), 'utf-8')), + ], + { concurrency: 'unbounded' }, + ); + expect(indexSource).not.toMatch(/\bmakeReadRuntime,?$/mu); + expect(packageSource).not.toMatch(/"\.\/db\/schema"/u); + }), +); diff --git a/app/packages/core-runtime/tests/unit/read-runtime.test.ts b/app/packages/core-runtime/tests/unit/read-runtime.test.ts index e4fca1acb..65f538cf4 100644 --- a/app/packages/core-runtime/tests/unit/read-runtime.test.ts +++ b/app/packages/core-runtime/tests/unit/read-runtime.test.ts @@ -1,10 +1,9 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; /* oxlint-disable sonarjs/use-type-alias, typescript/no-unsafe-type-assertion -- Existing compatibility boundary; expires: 2026-12-31. */ -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. +import { expect, it } from 'effect-rstest'; + import { Cause, Deferred, Effect, Exit, Fiber, Option, Predicate, Schema } from 'effect'; import { ConnectionError, SqlError } from 'effect/unstable/sql/SqlError'; -import assert from 'node:assert/strict'; -import test from 'node:test'; + import { defineGlobalPolicy, denyPolicy } from '../../src/actions/policy.ts'; import { defineSystemModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; import { OperationContextUnavailable } from '../../src/operations/errors.ts'; @@ -27,12 +26,10 @@ const scope = Object.freeze({ principalId: '00000000-0000-4000-8000-000000000003', tenantId: '00000000-0000-4000-8000-000000000001', }); - const EvidenceRowSchema = Schema.Struct({ queryHash: Schema.optionalKey(Schema.String), }); type EvidenceRow = Schema.Schema.Type; - const ModuleIdSchema = Schema.String.pipe(Schema.brand('ModuleId')); const ResourceIdSchema = Schema.String.pipe(Schema.brand('ResourceId')); const ResourceTargetSchema = Schema.Struct({ @@ -40,8 +37,7 @@ const ResourceTargetSchema = Schema.Struct({ resourceId: ResourceIdSchema, resourceType: Schema.String, }); - -const makeHarness = ( +const makeHarness = Effect.fn(function* makeHarness( options: { readonly failEvidence?: boolean; readonly onLegalEntityPermission?: (permission: string | undefined) => void; @@ -58,7 +54,7 @@ const makeHarness = ( readonly tenantPermissionDecision?: 'allowed' | 'denied' | 'unavailable'; readonly transactionEvents?: string[]; } = {}, -) => { +) { let evidence = 0; let tenantPermissionChecks = 0; const evidenceRows: EvidenceRow[] = []; @@ -85,7 +81,7 @@ const makeHarness = ( ] : []; }); - const database = { executor: makeTestDatabase(query) }; + const database = { executor: yield* makeTestDatabase(query) }; const transact = database.executor.transaction.bind(database.executor); const transaction: typeof database.executor.transaction = (body) => transact(body).pipe( @@ -155,8 +151,7 @@ const makeHarness = ( { onStage: (stage) => stages.push(stage) }, ); return { evidence: () => evidence, evidenceRows: () => evidenceRows, runtime, stages }; -}; - +}); const registration = (items: readonly string[] = []) => defineRead( { @@ -186,109 +181,110 @@ const registration = (items: readonly string[] = []) => () => Effect.succeed({ items }), () => ({ kind: 'module', moduleId: 'core.shell' }), ); - -void test('runs every gate before the handler and persists evidence before releasing zero results', async () => { - const harness = makeHarness(); - const result = await runEffectTestPromise( - harness.runtime.runRead({ - input: {}, - principal: scope, - registration: registration(), - transport: { correlationId: scope.correlationId }, +it.effect( + 'runs every gate before the handler and persists evidence before releasing zero results', + () => + Effect.gen(function* migratedTest1() { + const harness = yield* makeHarness(); + const result = yield* harness.runtime.runRead({ + input: {}, + principal: scope, + registration: registration(), + transport: { correlationId: scope.correlationId }, + }); + expect(result).toEqual([]); + expect(harness.evidence()).toBe(1); + expect(harness.stages).toEqual(READ_RUNTIME_STAGES); }), - ); - assert.deepEqual(result, []); - assert.equal(harness.evidence(), 1); - assert.deepEqual(harness.stages, READ_RUNTIME_STAGES); -}); - -void test('validates decoded transformed results and preserves their nullable JSON encoding', async () => { - const harness = makeHarness(); - const ResultSchema = Schema.Struct({ value: Schema.OptionFromNullOr(Schema.String) }); - const transformedRegistration = defineRead( - { ...registration().descriptor, resultSchema: ResultSchema }, - () => Effect.succeed({ evidence: { resultCount: 1 }, result: { value: Option.none() } }), - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }), - ); - const result = await runEffectTestPromise( - harness.runtime.runRead({ +); +it.effect('validates decoded transformed results and preserves their nullable JSON encoding', () => + Effect.gen(function* migratedTest2() { + const harness = yield* makeHarness(); + const ResultSchema = Schema.Struct({ value: Schema.OptionFromNullOr(Schema.String) }); + const transformedRegistration = defineRead( + { ...registration().descriptor, resultSchema: ResultSchema }, + () => Effect.succeed({ evidence: { resultCount: 1 }, result: { value: Option.none() } }), + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); + const result = yield* harness.runtime.runRead({ input: {}, principal: scope, registration: transformedRegistration, transport: { correlationId: scope.correlationId }, - }), - ); - const encoded = await runEffectTestPromise( - Schema.encodeUnknownEffect(Schema.toCodecJson(ResultSchema))(result), - ); - - assert.ok(Option.isNone(result.value)); - assert.deepEqual(encoded, { value: null }); -}); - -void test('uses each denying Policy reference own declared HTTP status', async () => { - await Promise.all( - ([409, 422] as const).map(async (denialStatus) => { - const harness = makeHarness(); - const policy = defineGlobalPolicy>>({ - evaluate: () => Effect.fail(denyPolicy(`policy-${denialStatus}`, 'Denied by test Policy')), - policyKey: `global.read-policy-${denialStatus}.v1`, - }); - const governed = defineRead( - { - ...registration().descriptor, - policies: [{ denialStatus, policyKey: policy.policyKey }], - }, - () => Effect.succeed({ evidence: { resultCount: 0 }, result: [] }), - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }), - undefined, - [policy], - ); - const error = await runEffectTestPromise( - Effect.flip( - harness.runtime.runRead({ - input: {}, - principal: scope, - registration: governed, - transport: { correlationId: scope.correlationId }, - }), - ), - ); - assert.equal(error._tag, 'ReadPolicyDenied'); - assert.equal(Schema.decodeUnknownSync(ReadPolicyDenied)(error).httpStatus, denialStatus); - }), - ); -}); + }); + const encoded = yield* Schema.encodeUnknownEffect(Schema.toCodecJson(ResultSchema))(result); -void test('executes every governed access kind and computes hash-only query evidence inside Core', async () => { - await Promise.all( - (['detail', 'download', 'export', 'list', 'report', 'search'] as const).map( - async (accessKind) => { - const legalEntityId = '00000000-0000-4000-8000-000000000004'; - const harness = makeHarness({ - permissionDecision: 'allowed', - resolvedScope: { ...scope, legalEntityId }, + expect(Option.isNone(result.value)).toBe(true); + expect(encoded).toEqual({ value: null }); + }), +); +it.effect('uses each denying Policy reference own declared HTTP status', () => + Effect.all( + ([409, 422] as const).map((denialStatus) => + Effect.gen(function* migratedTest4() { + const harness = yield* makeHarness(); + const policy = defineGlobalPolicy>>({ + evaluate: () => + Effect.fail(denyPolicy(`policy-${denialStatus}`, 'Denied by test Policy')), + policyKey: `global.read-policy-${denialStatus}.v1`, }); const governed = defineRead( { ...registration().descriptor, - accessKind, - evidencePolicy: { - captureMode: 'hash_only', - policyKey: `core.shell.${accessKind}.hash.v1`, - }, - legalEntityScope: 'required', + policies: [{ denialStatus, policyKey: policy.policyKey }], }, () => Effect.succeed({ evidence: { resultCount: 0 }, result: [] }), () => Effect.succeed({}), () => ({ kind: 'module', moduleId: 'core.shell' }), - accessKind === 'search' ? () => [] : undefined, + undefined, + [policy], ); - assert.deepEqual( - await runEffectTestPromise( - harness.runtime.runRead({ + const error = yield* Effect.flip( + harness.runtime.runRead({ + input: {}, + principal: scope, + registration: governed, + transport: { correlationId: scope.correlationId }, + }), + ); + expect(Predicate.isTagged(error, 'ReadPolicyDenied')).toBe(true); + expect((yield* Schema.decodeUnknownEffect(ReadPolicyDenied)(error)).httpStatus).toBe( + denialStatus, + ); + }), + ), + { concurrency: 'unbounded' }, + ), +); +it.effect( + 'executes every governed access kind and computes hash-only query evidence inside Core', + () => + Effect.all( + (['detail', 'download', 'export', 'list', 'report', 'search'] as const).map((accessKind) => + Effect.gen(function* migratedTest6() { + const legalEntityId = '00000000-0000-4000-8000-000000000004'; + const harness = yield* makeHarness({ + permissionDecision: 'allowed', + resolvedScope: { ...scope, legalEntityId }, + }); + const governed = defineRead( + { + ...registration().descriptor, + accessKind, + evidencePolicy: { + captureMode: 'hash_only', + policyKey: `core.shell.${accessKind}.hash.v1`, + }, + legalEntityScope: 'required', + }, + () => Effect.succeed({ evidence: { resultCount: 0 }, result: [] }), + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + accessKind === 'search' ? () => [] : undefined, + ); + expect( + yield* harness.runtime.runRead({ input: {}, principal: { authBindingId: '00000000-0000-4000-8000-000000000005', @@ -301,46 +297,43 @@ void test('executes every governed access kind and computes hash-only query evid registration: governed, transport: { correlationId: scope.correlationId }, }), - ), - [], - ); - assert.match(String(harness.evidenceRows()[0]?.queryHash), /^[\da-f]{64}$/u); - }, + ).toEqual([]); + expect(String(harness.evidenceRows()[0]?.queryHash)).toMatch(/^[\da-f]{64}$/u); + }), + ), + { concurrency: 'unbounded' }, ), - ); -}); - -void test('rejects invalid input before opening a transaction or executing a handler', async () => { - const harness = makeHarness(); - const error = await runEffectTestPromise( - Effect.flip( +); +it.effect('rejects invalid input before opening a transaction or executing a handler', () => + Effect.gen(function* migratedTest7() { + const harness = yield* makeHarness(); + const error = yield* Effect.flip( harness.runtime.runRead({ input: { unexpected: Symbol('invalid') }, principal: {}, registration: registration(), transport: {}, }), - ), - ); - assert.equal(error._tag, 'ReadInputValidationError'); - assert.equal(harness.evidence(), 0); -}); - -void test('preserves typed result-validation failure across transaction rollback', async () => { - const harness = makeHarness(); - const invalidRegistration = defineRead( - registration().descriptor, - () => { - const result: string[] = []; - const handlerResult = { evidence: { resultCount: 1 }, result }; - Object.defineProperty(handlerResult, 'result', { value: 42 }); - return Effect.succeed(handlerResult); - }, - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }), - ); - const error = await runEffectTestPromise( - Effect.flip( + ); + expect(Predicate.isTagged(error, 'ReadInputValidationError')).toBe(true); + expect(harness.evidence()).toBe(0); + }), +); +it.effect('preserves typed result-validation failure across transaction rollback', () => + Effect.gen(function* migratedTest8() { + const harness = yield* makeHarness(); + const invalidRegistration = defineRead( + registration().descriptor, + () => { + const result: string[] = []; + const handlerResult = { evidence: { resultCount: 1 }, result }; + Object.defineProperty(handlerResult, 'result', { value: 42 }); + return Effect.succeed(handlerResult); + }, + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); + const error = yield* Effect.flip( harness.runtime.runRead({ input: {}, principal: { @@ -353,61 +346,58 @@ void test('preserves typed result-validation failure across transaction rollback registration: invalidRegistration, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.equal(error._tag, 'ReadResultValidationError'); - assert.equal(harness.evidence(), 0); -}); - -void test('never releases an allowed result when required evidence persistence fails', async () => { - const harness = makeHarness({ failEvidence: true }); - const error = await runEffectTestPromise( - Effect.flip( + ); + expect(Predicate.isTagged(error, 'ReadResultValidationError')).toBe(true); + expect(harness.evidence()).toBe(0); + }), +); +it.effect('never releases an allowed result when required evidence persistence fails', () => + Effect.gen(function* migratedTest9() { + const harness = yield* makeHarness({ failEvidence: true }); + const error = yield* Effect.flip( harness.runtime.runRead({ input: {}, principal: scope, registration: registration(), transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.equal(error._tag, 'ReadEvidencePersistenceError'); - assert.equal(harness.evidence(), 0); -}); - -void test('preserves scoped service-factory unavailability and never invokes the handler', async () => { - const harness = makeHarness(); - let handlerCalls = 0; - const unavailableRegistration = defineRead( - registration().descriptor, - () => { - handlerCalls += 1; - return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); - }, - () => - Effect.fail( - new OperationContextUnavailable({ - code: 'operation_context_unavailable', - reason: 'The owner repository scope is temporarily unavailable', - }), - ), - () => ({ kind: 'module', moduleId: 'core.shell' }), - ); - const error = await runEffectTestPromise( - Effect.flip( + ); + expect(Predicate.isTagged(error, 'ReadEvidencePersistenceError')).toBe(true); + expect(harness.evidence()).toBe(0); + }), +); +it.effect('preserves scoped service-factory unavailability and never invokes the handler', () => + Effect.gen(function* migratedTest10() { + const harness = yield* makeHarness(); + let handlerCalls = 0; + const unavailableRegistration = defineRead( + registration().descriptor, + () => { + handlerCalls += 1; + return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); + }, + () => + Effect.fail( + new OperationContextUnavailable({ + code: 'operation_context_unavailable', + reason: 'The owner repository scope is temporarily unavailable', + }), + ), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); + const error = yield* Effect.flip( harness.runtime.runRead({ input: {}, principal: scope, registration: unavailableRegistration, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.equal(error._tag, 'OperationContextUnavailable'); - assert.equal(handlerCalls, 0); - assert.equal(harness.evidence(), 0); -}); - + ); + expect(Predicate.isTagged(error, 'OperationContextUnavailable')).toBe(true); + expect(handlerCalls).toBe(0); + expect(harness.evidence()).toBe(0); + }), +); const counterpartyReadRegistration = ( legalEntityScope: 'required' | 'optional', onHandler: () => void, @@ -431,274 +421,268 @@ const counterpartyReadPrincipal = (legalEntityId: string) => ({ tenantId: scope.tenantId, }); -void test('persists sanitized permission denial and never invokes the private handler', async () => { - const legalEntityId = '00000000-0000-4000-8000-000000000004'; - const legalEntityPermissions: (string | undefined)[] = []; - const harness = makeHarness({ - onLegalEntityPermission: (permission) => legalEntityPermissions.push(permission), - permissionDecision: 'denied', - resolvedScope: { ...scope, legalEntityId }, - }); - let handlerCalls = 0; - const deniedRegistration = counterpartyReadRegistration('required', () => { - handlerCalls += 1; - }); - const error = await runEffectTestPromise( - Effect.flip( +it.effect('persists sanitized permission denial and never invokes the private handler', () => + Effect.gen(function* migratedTest11() { + const legalEntityId = '00000000-0000-4000-8000-000000000004'; + const legalEntityPermissions: (string | undefined)[] = []; + const harness = yield* makeHarness({ + onLegalEntityPermission: (permission) => legalEntityPermissions.push(permission), + permissionDecision: 'denied', + resolvedScope: { ...scope, legalEntityId }, + }); + let handlerCalls = 0; + const deniedRegistration = counterpartyReadRegistration('required', () => { + handlerCalls += 1; + }); + const error = yield* Effect.flip( harness.runtime.runRead({ input: {}, principal: counterpartyReadPrincipal(legalEntityId), registration: deniedRegistration, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.equal(error._tag, 'ReadPermissionDenied'); - assert.deepEqual(legalEntityPermissions, ['read_counterparty']); - assert.equal(handlerCalls, 0); - assert.equal(harness.evidence(), 1); -}); - -test('fails closed when explicit Counterparty read authority is unavailable', async () => { - const legalEntityId = '00000000-0000-4000-8000-000000000004'; - let handlerCalls = 0; - const harness = makeHarness({ - permissionDecision: 'unavailable', - resolvedScope: { ...scope, legalEntityId }, - }); - const counterpartyRead = counterpartyReadRegistration('optional', () => { - handlerCalls += 1; - }); - const error = await runEffectTestPromise( - Effect.flip( + ); + expect(Predicate.isTagged(error, 'ReadPermissionDenied')).toBe(true); + expect(legalEntityPermissions).toEqual(['read_counterparty']); + expect(handlerCalls).toBe(0); + expect(harness.evidence()).toBe(1); + }), +); +it.effect('fails closed when explicit Counterparty read authority is unavailable', () => + Effect.gen(function* migratedTest12() { + const legalEntityId = '00000000-0000-4000-8000-000000000004'; + let handlerCalls = 0; + const harness = yield* makeHarness({ + permissionDecision: 'unavailable', + resolvedScope: { ...scope, legalEntityId }, + }); + const counterpartyRead = counterpartyReadRegistration('optional', () => { + handlerCalls += 1; + }); + const error = yield* Effect.flip( harness.runtime.runRead({ input: {}, principal: counterpartyReadPrincipal(legalEntityId), registration: counterpartyRead, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.equal(error._tag, 'ReadPermissionUnavailable'); - assert.equal(handlerCalls, 0); - assert.equal(harness.evidence(), 0); -}); - -test('derives the authorized resource from decoded input and ignores conflicting transport hints', async () => { - const legalEntityId = '00000000-0000-4000-8000-000000000004'; - let authorizedTarget: { moduleId: string; resourceId: string; resourceType: string } | undefined; - const harness = makeHarness({ - onResourceTarget: (target) => { - authorizedTarget = target; - }, - permissionDecision: 'allowed', - resolvedScope: { ...scope, legalEntityId }, - }); - const target = { - moduleId: 'inventory.stock', - resourceId: 'stock-1', - resourceType: 'inventory.stock.item', - }; - const targetRegistration = defineRead( - { - ...registration().descriptor, - inputSchema: ResourceTargetSchema, - legalEntityScope: 'required', - permissionTarget: 'resource', - resultSchema: Schema.String, - }, - () => Effect.succeed({ evidence: { resultCount: 1 }, result: 'visible' }), - () => Effect.succeed({}), - (input) => ({ kind: 'resource', resource: input }), - ); - const result = await runEffectTestPromise( - harness.runtime.runRead({ - input: target, - principal: { - ...scope, - authBindingId: '00000000-0000-4000-8000-000000000005', - authContextRef: 'better-auth-session:read-runtime', - authMethod: 'session', - legalEntityId, - }, - registration: targetRegistration, - transport: { - correlationId: scope.correlationId, - targetModuleKey: 'forged.module', - targetResourceId: 'forged-resource', - targetResourceType: 'forged.type', - }, - }), - ); - assert.equal(result, 'visible'); - assert.deepEqual(authorizedTarget, target); -}); - -test('authorizes a canonical Resource through explicit tenant Party administration alternatives', async () => { - const legalEntityId = '00000000-0000-4000-8000-000000000004'; - const target = { - moduleId: 'party.registry', - resourceId: 'counterparty-1', - resourceType: 'counterparty', - }; - const policyTargets: unknown[] = []; - const policy = defineGlobalPolicy({ - evaluate: ({ target: policyTarget }) => { - policyTargets.push(policyTarget); - return Effect.void; - }, - policyKey: 'party.registry.counterparty-read.v1', - }); - let handlerCalls = 0; - const counterpartyRead = defineRead( - { - ...registration().descriptor, - inputSchema: ResourceTargetSchema, - legalEntityScope: 'required', - permissionTarget: 'resource', - policies: [{ denialStatus: 422, policyKey: policy.policyKey }], - resultSchema: Schema.String, - }, - () => { - handlerCalls += 1; - return Effect.succeed({ evidence: { resultCount: 1 }, result: 'visible' }); - }, - () => Effect.succeed({}), - (input) => ({ - kind: 'any_of', - targets: [ - { kind: 'resource', resource: input }, - { kind: 'tenant', permission: 'manage_party_identity' }, - ], - }), - undefined, - [policy], - ); - const principal = { - ...scope, - authBindingId: '00000000-0000-4000-8000-000000000005', - authContextRef: 'better-auth-session:read-runtime', - authMethod: 'session' as const, - legalEntityId, - }; - - const tenantAdmin = makeHarness({ - permissionDecision: 'denied', - tenantPermissionDecision: 'allowed', - }); - assert.equal( - await runEffectTestPromise( - tenantAdmin.runtime.runRead({ + ); + expect(Predicate.isTagged(error, 'ReadPermissionUnavailable')).toBe(true); + expect(handlerCalls).toBe(0); + expect(harness.evidence()).toBe(0); + }), +); +it.effect( + 'derives the authorized resource from decoded input and ignores conflicting transport hints', + () => + Effect.gen(function* migratedTest13() { + const legalEntityId = '00000000-0000-4000-8000-000000000004'; + let authorizedTarget: + | { moduleId: string; resourceId: string; resourceType: string } + | undefined; + const harness = yield* makeHarness({ + onResourceTarget: (target) => { + authorizedTarget = target; + }, + permissionDecision: 'allowed', + resolvedScope: { ...scope, legalEntityId }, + }); + const target = { + moduleId: 'inventory.stock', + resourceId: 'stock-1', + resourceType: 'inventory.stock.item', + }; + const targetRegistration = defineRead( + { + ...registration().descriptor, + inputSchema: ResourceTargetSchema, + legalEntityScope: 'required', + permissionTarget: 'resource', + resultSchema: Schema.String, + }, + () => Effect.succeed({ evidence: { resultCount: 1 }, result: 'visible' }), + () => Effect.succeed({}), + (input) => ({ kind: 'resource', resource: input }), + ); + const result = yield* harness.runtime.runRead({ input: target, - principal: scope, - registration: counterpartyRead, + principal: { + ...scope, + authBindingId: '00000000-0000-4000-8000-000000000005', + authContextRef: 'better-auth-session:read-runtime', + authMethod: 'session', + legalEntityId, + }, + registration: targetRegistration, transport: { correlationId: scope.correlationId, targetModuleKey: 'forged.module', targetResourceId: 'forged-resource', targetResourceType: 'forged.type', }, - }), - ), - 'visible', - ); - assert.deepEqual(policyTargets, [ - { - targetModuleKey: 'party.registry', - targetResourceId: 'counterparty-1', - targetResourceType: 'counterparty', - }, - ]); + }); + expect(result).toBe('visible'); + expect(authorizedTarget).toEqual(target); + }), +); +it.effect( + 'authorizes a canonical Resource through explicit tenant Party administration alternatives', + () => + Effect.gen(function* migratedTest14() { + const legalEntityId = '00000000-0000-4000-8000-000000000004'; + const target = { + moduleId: 'party.registry', + resourceId: 'counterparty-1', + resourceType: 'counterparty', + }; + const policyTargets: unknown[] = []; + const policy = defineGlobalPolicy({ + evaluate: ({ target: policyTarget }) => { + policyTargets.push(policyTarget); + return Effect.void; + }, + policyKey: 'party.registry.counterparty-read.v1', + }); + let handlerCalls = 0; + const counterpartyRead = defineRead( + { + ...registration().descriptor, + inputSchema: ResourceTargetSchema, + legalEntityScope: 'required', + permissionTarget: 'resource', + policies: [{ denialStatus: 422, policyKey: policy.policyKey }], + resultSchema: Schema.String, + }, + () => { + handlerCalls += 1; + return Effect.succeed({ evidence: { resultCount: 1 }, result: 'visible' }); + }, + () => Effect.succeed({}), + (input) => ({ + kind: 'any_of', + targets: [ + { kind: 'resource', resource: input }, + { kind: 'tenant', permission: 'manage_party_identity' }, + ], + }), + undefined, + [policy], + ); + const principal = { + ...scope, + authBindingId: '00000000-0000-4000-8000-000000000005', + authContextRef: 'better-auth-session:read-runtime', + authMethod: 'session' as const, + legalEntityId, + }; - const resourceAuthority = makeHarness({ - permissionDecision: 'allowed', - resolvedScope: { ...scope, legalEntityId }, - tenantPermissionDecision: 'denied', - }); - assert.equal( - await runEffectTestPromise( - resourceAuthority.runtime.runRead({ - input: target, - principal, - registration: counterpartyRead, - transport: { correlationId: scope.correlationId }, - }), - ), - 'visible', - ); + const tenantAdmin = yield* makeHarness({ + permissionDecision: 'denied', + tenantPermissionDecision: 'allowed', + }); + expect( + yield* tenantAdmin.runtime.runRead({ + input: target, + principal: scope, + registration: counterpartyRead, + transport: { + correlationId: scope.correlationId, + targetModuleKey: 'forged.module', + targetResourceId: 'forged-resource', + targetResourceType: 'forged.type', + }, + }), + ).toBe('visible'); + expect(policyTargets).toEqual([ + { + targetModuleKey: 'party.registry', + targetResourceId: 'counterparty-1', + targetResourceType: 'counterparty', + }, + ]); - const indeterminate = makeHarness({ - permissionDecision: 'denied', - resolvedScope: { ...scope, legalEntityId }, - tenantPermissionDecision: 'unavailable', - }); - const unavailable = await runEffectTestPromise( - Effect.flip( - indeterminate.runtime.runRead({ - input: target, - principal, - registration: counterpartyRead, - transport: { correlationId: scope.correlationId }, - }), - ), - ); - assert.equal(unavailable._tag, 'ReadPermissionUnavailable'); - assert.equal(indeterminate.evidence(), 0); + const resourceAuthority = yield* makeHarness({ + permissionDecision: 'allowed', + resolvedScope: { ...scope, legalEntityId }, + tenantPermissionDecision: 'denied', + }); + expect( + yield* resourceAuthority.runtime.runRead({ + input: target, + principal, + registration: counterpartyRead, + transport: { correlationId: scope.correlationId }, + }), + ).toBe('visible'); - const denied = makeHarness({ - permissionDecision: 'denied', - resolvedScope: { ...scope, legalEntityId }, - tenantPermissionDecision: 'denied', - }); - const denial = await runEffectTestPromise( - Effect.flip( - denied.runtime.runRead({ - input: target, - principal, - registration: counterpartyRead, - transport: { correlationId: scope.correlationId }, - }), - ), - ); - assert.equal(denial._tag, 'ReadPermissionDenied'); - assert.equal(denied.evidence(), 1); - assert.equal(handlerCalls, 2); -}); + const indeterminate = yield* makeHarness({ + permissionDecision: 'denied', + resolvedScope: { ...scope, legalEntityId }, + tenantPermissionDecision: 'unavailable', + }); + const unavailable = yield* Effect.flip( + indeterminate.runtime.runRead({ + input: target, + principal, + registration: counterpartyRead, + transport: { correlationId: scope.correlationId }, + }), + ); + expect(Predicate.isTagged(unavailable, 'ReadPermissionUnavailable')).toBe(true); + expect(indeterminate.evidence()).toBe(0); -test('rejects generic tenant access as an alternative permission target', async () => { - const legalEntityId = '00000000-0000-4000-8000-000000000004'; - let handlerCalls = 0; - const invalid = defineRead( - { - ...registration().descriptor, - legalEntityScope: 'required', - permissionTarget: 'resource', - }, - () => { - handlerCalls += 1; - return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); - }, - () => Effect.succeed({}), - () => - // SAFETY: This intentionally forges a runtime-invalid alternative to prove validation fails closed. - ({ - kind: 'any_of', - targets: [ - { - kind: 'resource', - resource: { - moduleId: 'party.registry', - resourceId: 'counterparty-1', - resourceType: 'counterparty', + const denied = yield* makeHarness({ + permissionDecision: 'denied', + resolvedScope: { ...scope, legalEntityId }, + tenantPermissionDecision: 'denied', + }); + const denial = yield* Effect.flip( + denied.runtime.runRead({ + input: target, + principal, + registration: counterpartyRead, + transport: { correlationId: scope.correlationId }, + }), + ); + expect(Predicate.isTagged(denial, 'ReadPermissionDenied')).toBe(true); + expect(denied.evidence()).toBe(1); + expect(handlerCalls).toBe(2); + }), +); +it.effect('rejects generic tenant access as an alternative permission target', () => + Effect.gen(function* migratedTest15() { + const legalEntityId = '00000000-0000-4000-8000-000000000004'; + let handlerCalls = 0; + const invalid = defineRead( + { + ...registration().descriptor, + legalEntityScope: 'required', + permissionTarget: 'resource', + }, + () => { + handlerCalls += 1; + return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); + }, + () => Effect.succeed({}), + () => + // SAFETY: This intentionally forges a runtime-invalid alternative to prove validation fails closed. + ({ + kind: 'any_of', + targets: [ + { + kind: 'resource', + resource: { + moduleId: 'party.registry', + resourceId: 'counterparty-1', + resourceType: 'counterparty', + }, }, - }, - { kind: 'tenant', permission: 'access' }, - ], - }) as never, - ); - const failure = await runEffectTestPromise( - Effect.flip( - makeHarness({ resolvedScope: { ...scope, legalEntityId } }).runtime.runRead({ + { kind: 'tenant', permission: 'access' }, + ], + }) as never, + ); + const failure = yield* Effect.flip( + (yield* makeHarness({ resolvedScope: { ...scope, legalEntityId } })).runtime.runRead({ input: {}, principal: { ...scope, @@ -710,12 +694,11 @@ test('rejects generic tenant access as an alternative permission target', async registration: invalid, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.equal(failure._tag, 'ReadHandlerExecutionError'); - assert.equal(handlerCalls, 0); -}); - + ); + expect(Predicate.isTagged(failure, 'ReadHandlerExecutionError')).toBe(true); + expect(handlerCalls).toBe(0); + }), +); for (const scenario of [ { expectedFailure: 'ReadPermissionUnavailable', @@ -732,39 +715,39 @@ for (const scenario of [ tenantPermissionDecision: 'allowed', }, ] as const) { - test(scenario.name, async () => { - let handlerCalls = 0; - const alternativeRead = defineRead( - { ...registration().descriptor, permissionTarget: 'tenant' }, - () => { - handlerCalls += 1; - return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); - }, - () => Effect.succeed({}), - () => ({ - kind: 'any_of', - targets: [ - { kind: 'tenant', permission: scenario.permission }, - { kind: 'module', moduleId: 'party.registry' }, - ], - }), - scenario.resultTargets ?? undefined, - ); - const failure = await runEffectTestPromise( - Effect.flip( - makeHarness({ + it.effect(scenario.name, () => + Effect.gen(function* tableScenario() { + let handlerCalls = 0; + const alternativeRead = defineRead( + { ...registration().descriptor, permissionTarget: 'tenant' }, + () => { + handlerCalls += 1; + return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); + }, + () => Effect.succeed({}), + () => ({ + kind: 'any_of', + targets: [ + { kind: 'tenant', permission: scenario.permission }, + { kind: 'module', moduleId: 'party.registry' }, + ], + }), + scenario.resultTargets ?? undefined, + ); + const failure = yield* Effect.flip( + (yield* makeHarness({ tenantPermissionDecision: scenario.tenantPermissionDecision, - }).runtime.runRead({ + })).runtime.runRead({ input: {}, principal: scope, registration: alternativeRead, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.equal(failure._tag, scenario.expectedFailure); - assert.equal(handlerCalls, 0); - }); + ); + expect(Predicate.isTagged(failure, scenario.expectedFailure)).toBe(true); + expect(handlerCalls).toBe(0); + }), + ); } for (const scenario of [ @@ -789,54 +772,54 @@ for (const scenario of [ ), }, ]) { - test(scenario.name, async () => { - const harness = makeHarness(); - const failingRead = defineRead( - registration().descriptor, - () => scenario.outcome, - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }), - ); - const error = await runEffectTestPromise( - Effect.flip( + it.effect(scenario.name, () => + Effect.gen(function* tableScenario() { + const harness = yield* makeHarness(); + const failingRead = defineRead( + registration().descriptor, + () => scenario.outcome, + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); + const error = yield* Effect.flip( harness.runtime.runRead({ input: {}, principal: scope, registration: failingRead, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.equal(error._tag, scenario.expectedFailure); - assert.equal(harness.evidence(), scenario.expectedEvidence); - }); + ); + expect(Predicate.isTagged(error, scenario.expectedFailure)).toBe(true); + expect(harness.evidence()).toBe(scenario.expectedEvidence); + }), + ); } -void test('does not release generated search candidates denied by result-level authorization', async () => { - const legalEntityId = '00000000-0000-4000-8000-000000000004'; - const candidate = Schema.decodeUnknownSync(ResourceTargetSchema)({ - moduleId: 'inventory.stock', - resourceId: 'stock-1', - resourceType: 'inventory.stock.item', - }); - const harness = makeHarness({ - permissionDecision: 'allowed', - resolvedScope: { ...scope, legalEntityId }, - resultPermissionDecision: 'denied', - }); - const searchRegistration = defineRead( - { - ...registration().descriptor, - legalEntityScope: 'required', - resultSchema: Schema.Array(ResourceTargetSchema), - }, - () => Effect.succeed({ evidence: { resultCount: 1 }, result: [candidate] }), - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }), - (result) => result, - ); - const error = await runEffectTestPromise( - Effect.flip( +it.effect('does not release generated search candidates denied by result-level authorization', () => + Effect.gen(function* migratedTest20() { + const legalEntityId = '00000000-0000-4000-8000-000000000004'; + const candidate = yield* Schema.decodeUnknownEffect(ResourceTargetSchema)({ + moduleId: 'inventory.stock', + resourceId: 'stock-1', + resourceType: 'inventory.stock.item', + }); + const harness = yield* makeHarness({ + permissionDecision: 'allowed', + resolvedScope: { ...scope, legalEntityId }, + resultPermissionDecision: 'denied', + }); + const searchRegistration = defineRead( + { + ...registration().descriptor, + legalEntityScope: 'required', + resultSchema: Schema.Array(ResourceTargetSchema), + }, + () => Effect.succeed({ evidence: { resultCount: 1 }, result: [candidate] }), + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + (result) => result, + ); + const error = yield* Effect.flip( harness.runtime.runRead({ input: {}, principal: { @@ -849,199 +832,205 @@ void test('does not release generated search candidates denied by result-level a registration: searchRegistration, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.equal(error._tag, 'ReadPermissionDenied'); - assert.equal(harness.evidence(), 1); -}); - -test('authorizes tenant-scoped Party search results without fabricating a Legal Entity', async () => { - const candidate = Schema.decodeUnknownSync(ResourceTargetSchema)({ - moduleId: 'party.registry', - resourceId: 'party-1', - resourceType: 'party.registry.party', - }); - let resourceChecks = 0; - const tenantPermissions: string[] = []; - const harness = makeHarness({ - onResourceTarget: () => { - resourceChecks += 1; - }, - onTenantPermission: (permission) => tenantPermissions.push(permission), - permissionDecision: 'allowed', - }); - const searchRegistration = defineRead( - { - ...registration().descriptor, - accessKind: 'search', - legalEntityScope: 'optional', - permissionTarget: 'tenant', - resultSchema: Schema.Array(ResourceTargetSchema), - }, - () => Effect.succeed({ evidence: { resultCount: 1 }, result: [candidate] }), - () => Effect.succeed({}), - () => ({ kind: 'tenant', permission: 'read_party_identity' }), - (result) => result, - ); + ); + expect(Predicate.isTagged(error, 'ReadPermissionDenied')).toBe(true); + expect(harness.evidence()).toBe(1); + }), +); +it.effect('authorizes tenant-scoped Party search results without fabricating a Legal Entity', () => + Effect.gen(function* migratedTest21() { + const candidate = yield* Schema.decodeUnknownEffect(ResourceTargetSchema)({ + moduleId: 'party.registry', + resourceId: 'party-1', + resourceType: 'party.registry.party', + }); + let resourceChecks = 0; + const tenantPermissions: string[] = []; + const harness = yield* makeHarness({ + onResourceTarget: () => { + resourceChecks += 1; + }, + onTenantPermission: (permission) => tenantPermissions.push(permission), + permissionDecision: 'allowed', + }); + const searchRegistration = defineRead( + { + ...registration().descriptor, + accessKind: 'search', + legalEntityScope: 'optional', + permissionTarget: 'tenant', + resultSchema: Schema.Array(ResourceTargetSchema), + }, + () => Effect.succeed({ evidence: { resultCount: 1 }, result: [candidate] }), + () => Effect.succeed({}), + () => ({ kind: 'tenant', permission: 'read_party_identity' }), + (result) => result, + ); - assert.deepEqual( - await runEffectTestPromise( - harness.runtime.runRead({ + expect( + yield* harness.runtime.runRead({ input: {}, principal: scope, registration: searchRegistration, transport: { correlationId: scope.correlationId }, }), - ), - [candidate], - ); - assert.deepEqual(tenantPermissions, ['read_party_identity', 'read_party_identity']); - assert.equal(resourceChecks, 0); -}); - -test('fails closed when tenant-scoped Party result authorization becomes unavailable', async () => { - const candidate = Schema.decodeUnknownSync(ResourceTargetSchema)({ - moduleId: 'party.registry', - resourceId: 'party-1', - resourceType: 'party.registry.party', - }); - const harness = makeHarness({ - permissionDecision: 'allowed', - resultTenantPermissionDecision: 'unavailable', - }); - const searchRegistration = defineRead( - { - ...registration().descriptor, - accessKind: 'search', - legalEntityScope: 'optional', - permissionTarget: 'tenant', - resultSchema: Schema.Array(ResourceTargetSchema), - }, - () => Effect.succeed({ evidence: { resultCount: 1 }, result: [candidate] }), - () => Effect.succeed({}), - () => ({ kind: 'tenant', permission: 'read_party_identity' }), - (result) => result, - ); - const failure = await runEffectTestPromise( - Effect.flip( + ).toEqual([candidate]); + expect(tenantPermissions).toEqual(['read_party_identity', 'read_party_identity']); + expect(resourceChecks).toBe(0); + }), +); +it.effect('fails closed when tenant-scoped Party result authorization becomes unavailable', () => + Effect.gen(function* migratedTest22() { + const candidate = yield* Schema.decodeUnknownEffect(ResourceTargetSchema)({ + moduleId: 'party.registry', + resourceId: 'party-1', + resourceType: 'party.registry.party', + }); + const harness = yield* makeHarness({ + permissionDecision: 'allowed', + resultTenantPermissionDecision: 'unavailable', + }); + const searchRegistration = defineRead( + { + ...registration().descriptor, + accessKind: 'search', + legalEntityScope: 'optional', + permissionTarget: 'tenant', + resultSchema: Schema.Array(ResourceTargetSchema), + }, + () => Effect.succeed({ evidence: { resultCount: 1 }, result: [candidate] }), + () => Effect.succeed({}), + () => ({ kind: 'tenant', permission: 'read_party_identity' }), + (result) => result, + ); + const failure = yield* Effect.flip( harness.runtime.runRead({ input: {}, principal: scope, registration: searchRegistration, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.equal(failure._tag, 'ReadPermissionUnavailable'); -}); - -test('preserves declared owner read availability and not-found failures but sanitizes defects', async () => { - const failures = [ - new ReadHandlerUnavailable({ - code: 'read_handler_unavailable', - reason: 'A provider is temporarily unavailable', - }), - new ReadHandlerNotFound({ - code: 'read_handler_not_found', - reason: 'The resource does not exist', - }), - new Error('secret owner defect'), - ] as const; - const expectedTags = [ - 'ReadHandlerUnavailable', - 'ReadHandlerNotFound', - 'ReadHandlerExecutionError', - ]; - await Promise.all( - failures.map(async (failure, index) => { - const harness = makeHarness(); - const failingRegistration = defineRead( - registration().descriptor, - () => Effect.fail(failure), - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }), - ); - const error = await runEffectTestPromise( - Effect.flip( - harness.runtime.runRead({ - input: {}, - principal: scope, - registration: failingRegistration, - transport: { correlationId: scope.correlationId }, + ); + expect(Predicate.isTagged(failure, 'ReadPermissionUnavailable')).toBe(true); + }), +); +it.effect( + 'preserves declared owner read availability and not-found failures but sanitizes defects', + () => + Effect.gen(function* migratedTest23() { + const failures = [ + new ReadHandlerUnavailable({ + code: 'read_handler_unavailable', + reason: 'A provider is temporarily unavailable', + }), + new ReadHandlerNotFound({ + code: 'read_handler_not_found', + reason: 'The resource does not exist', + }), + new Error('secret owner defect'), + ] as const; + const expectedTags = [ + 'ReadHandlerUnavailable', + 'ReadHandlerNotFound', + 'ReadHandlerExecutionError', + ]; + yield* Effect.all( + failures.map((failure, index) => + Effect.gen(function* migratedTest24() { + const harness = yield* makeHarness(); + const failingRegistration = defineRead( + registration().descriptor, + () => Effect.fail(failure), + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); + const error = yield* Effect.flip( + harness.runtime.runRead({ + input: {}, + principal: scope, + registration: failingRegistration, + transport: { correlationId: scope.correlationId }, + }), + ); + const expectedTag = expectedTags[index]; + if (expectedTag === undefined) { + expect.unreachable('Expected value to be present'); + } + expect(Predicate.isTagged(error, expectedTag)).toBe(true); + expect(error.reason).not.toMatch(/secret/u); + expect(harness.evidence()).toBe(0); }), ), + { concurrency: 'unbounded' }, ); - assert.equal(error._tag, expectedTags[index]); - assert.doesNotMatch(error.reason, /secret/u); - assert.equal(harness.evidence(), 0); }), - ); -}); +); +it.effect('keeps read interruption and waits for transaction settlement', () => + Effect.gen(function* migratedTest25() { + const events: string[] = []; + const harness = yield* makeHarness({ transactionEvents: events }); -void test('keeps read interruption and waits for transaction settlement', async () => { - const events: string[] = []; - const harness = makeHarness({ transactionEvents: events }); - const exit = await runEffectTestPromise( - Effect.gen(function* interruptReadTest() { - const entered = yield* Deferred.make(); - const blocked = yield* Deferred.make(); - const governed = defineRead( - registration().descriptor, - () => - Effect.gen(function* blockedReadHandler() { - yield* Deferred.succeed(entered, true); - yield* Deferred.await(blocked); - return { evidence: { resultCount: 0 }, result: [] }; - }), - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }), - ); - const fiber = yield* harness.runtime - .runRead({ - input: {}, - principal: scope, - registration: governed, - transport: { correlationId: scope.correlationId }, - }) - .pipe(Effect.forkChild); - yield* Deferred.await(entered); - yield* Fiber.interrupt(fiber); - events.push('read_completed'); - return yield* Fiber.await(fiber); - }), - ); - assert.ok(Exit.isFailure(exit)); - assert.ok(Cause.hasInterruptsOnly(exit.cause)); - assert.deepEqual(events, ['transaction_settled', 'read_completed']); - assert.equal(harness.evidence(), 0); -}); - -void test('prioritizes failed denial evidence while retaining permission denial in the cause', async () => { - const harness = makeHarness({ failEvidence: true }); - const denied = new ReadPermissionDenied({ - code: 'read_permission_denied', - reason: 'Denied by read handler', - }); - const governed = defineRead( - registration().descriptor, - () => Effect.fail(denied), - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }), - ); - const exit = await runEffectTestPromise( - Effect.exit( + const entered = yield* Deferred.make(); + const blocked = yield* Deferred.make(); + const governed = defineRead( + registration().descriptor, + () => + Effect.gen(function* blockedReadHandler() { + yield* Deferred.succeed(entered, true); + yield* Deferred.await(blocked); + return { evidence: { resultCount: 0 }, result: [] }; + }), + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); + const fiber = yield* harness.runtime + .runRead({ + input: {}, + principal: scope, + registration: governed, + transport: { correlationId: scope.correlationId }, + }) + .pipe(Effect.forkChild); + yield* Deferred.await(entered); + yield* Fiber.interrupt(fiber); + events.push('read_completed'); + const exit = yield* Fiber.await(fiber); + if (!Exit.isFailure(exit)) { + expect.unreachable('Expected value to be present'); + } + expect(Cause.hasInterruptsOnly(exit.cause)).toBe(true); + expect(events).toEqual(['transaction_settled', 'read_completed']); + expect(harness.evidence()).toBe(0); + }), +); +it.effect('prioritizes failed denial evidence while retaining permission denial in the cause', () => + Effect.gen(function* migratedTest26() { + const harness = yield* makeHarness({ failEvidence: true }); + const denied = new ReadPermissionDenied({ + code: 'read_permission_denied', + reason: 'Denied by read handler', + }); + const governed = defineRead( + registration().descriptor, + () => Effect.fail(denied), + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); + const exit = yield* Effect.exit( harness.runtime.runRead({ input: {}, principal: scope, registration: governed, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.ok(Exit.isFailure(exit)); - const failures = exit.cause.reasons.filter(Cause.isFailReason).map((reason) => reason.error); - assert.equal(failures.length, 2); - assert.equal(failures[0]?._tag, 'ReadEvidencePersistenceError'); - assert.equal(failures[1], denied); - assert.equal(failures[1]._tag, 'ReadPermissionDenied'); -}); + ); + if (!Exit.isFailure(exit)) { + expect.unreachable('Expected value to be present'); + } + const failures = exit.cause.reasons.filter(Cause.isFailReason).map((reason) => reason.error); + expect(failures.length).toBe(2); + expect(Predicate.isTagged(failures[0], 'ReadEvidencePersistenceError')).toBe(true); + expect(failures[1]).toBe(denied); + expect(Predicate.isTagged(failures[1], 'ReadPermissionDenied')).toBe(true); + }), +); diff --git a/app/packages/core-runtime/tests/unit/schema-contract.test.ts b/app/packages/core-runtime/tests/unit/schema-contract.test.ts index 7a3e330c9..47f3ae73e 100644 --- a/app/packages/core-runtime/tests/unit/schema-contract.test.ts +++ b/app/packages/core-runtime/tests/unit/schema-contract.test.ts @@ -1,5 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; + import { getTableName, isTable } from 'drizzle-orm'; import { getTableConfig, PgDialect } from 'drizzle-orm/pg-core'; import type { PgTable } from 'drizzle-orm/pg-core'; @@ -17,14 +17,14 @@ const actionConfig = getTableConfig(actionInvocations); const dialect = new PgDialect(); type SchemaExport = (typeof schemaExports)[keyof typeof schemaExports]; const isPgTable = (value: SchemaExport): value is Extract => isTable(value); - const getColumn = (name: string) => { const column = actionConfig.columns.find((candidate) => candidate.name === name); - assert.ok(column, `Expected action_invocations.${name}`); + if (column === undefined) { + expect.unreachable(`Expected action_invocations.${name}`); + } return column; }; - -void test('exports exactly the 18 Core tables in PostgreSQL schema core', () => { +it('exports exactly the 18 Core tables in PostgreSQL schema core', () => { const exportedTables: PgTable[] = []; for (const value of Object.values(schemaExports)) { if (isPgTable(value)) { @@ -41,29 +41,24 @@ void test('exports exactly the 18 Core tables in PostgreSQL schema core', () => (tableName) => `${CORE_SCHEMA_NAME}.${tableName}`, ).toSorted(); - assert.deepEqual(qualifiedNames, expectedQualifiedNames); - assert.equal(new Set(qualifiedNames).size, CORE_TABLE_INVENTORY.length); - assert.equal( - qualifiedNames.some((name) => name.startsWith('public.')), - false, - ); - assert.equal( + expect(qualifiedNames).toEqual(expectedQualifiedNames); + expect(new Set(qualifiedNames).size).toBe(CORE_TABLE_INVENTORY.length); + expect(qualifiedNames.some((name) => name.startsWith('public.'))).toBe(false); + expect( qualifiedNames.some((name) => /^(?:auth|ticketing|properties|property|accounting)\./u.test(name), ), - false, - ); + ).toBe(false); }); +it('supports pre-authentication Action Invocation rows and indeterminate outcomes', () => { + expect(getColumn('principal_id').notNull).toBe(false); + expect(getColumn('auth_binding_id').notNull).toBe(false); + expect(getColumn('auth_context_ref').notNull).toBe(false); + expect(getColumn('auth_method').notNull).toBe(false); + expect(getColumn('anonymous_session_ref').notNull).toBe(false); + expect(getColumn('correlation_id').notNull).toBe(false); -void test('supports pre-authentication Action Invocation rows and indeterminate outcomes', () => { - assert.equal(getColumn('principal_id').notNull, false); - assert.equal(getColumn('auth_binding_id').notNull, false); - assert.equal(getColumn('auth_context_ref').notNull, false); - assert.equal(getColumn('auth_method').notNull, false); - assert.equal(getColumn('anonymous_session_ref').notNull, false); - assert.equal(getColumn('correlation_id').notNull, false); - - assert.deepEqual(ACTION_INVOCATION_STATUSES, [ + expect(ACTION_INVOCATION_STATUSES).toEqual([ 'received', 'rejected', 'running', @@ -76,66 +71,68 @@ void test('supports pre-authentication Action Invocation rows and indeterminate const statusCheck = actionConfig.checks.find( (candidate) => candidate.name === 'core_action_invocations_status_ck', ); - assert.ok(statusCheck); + if (statusCheck === undefined) { + expect.unreachable('Expected value to be present'); + } const statusSql = dialect.sqlToQuery(statusCheck.value).sql; for (const status of ACTION_INVOCATION_STATUSES) { - assert.match(statusSql, new RegExp(`'${status}'`, 'u')); + expect(statusSql).toMatch(new RegExp(`'${status}'`, 'u')); } }); - -void test('preserves critical Action foreign keys and unique idempotency index', () => { +it('preserves critical Action foreign keys and unique idempotency index', () => { const principalForeignKey = actionConfig.foreignKeys.find((foreignKey) => foreignKey.reference().columns.some((column) => column.name === 'principal_id'), ); - assert.ok(principalForeignKey); - assert.equal( - getTableName(principalForeignKey.reference().foreignTable), - getTableName(principals), - ); - assert.equal(principalForeignKey.onDelete, 'restrict'); - assert.deepEqual( - principalForeignKey.reference().columns.map((column) => column.name), - ['tenant_id', 'principal_id'], - ); + if (principalForeignKey === undefined) { + expect.unreachable('Expected value to be present'); + } + expect(getTableName(principalForeignKey.reference().foreignTable)).toBe(getTableName(principals)); + expect(principalForeignKey.onDelete).toBe('restrict'); + expect(principalForeignKey.reference().columns.map((column) => column.name)).toEqual([ + 'tenant_id', + 'principal_id', + ]); const idempotencyIndex = actionConfig.indexes.find( (candidate) => candidate.config.name === 'core_action_invocations_idempotency_uk', ); - assert.ok(idempotencyIndex); - assert.equal(idempotencyIndex.config.unique, true); - assert.ok(idempotencyIndex.config.where); - assert.deepEqual( + if (idempotencyIndex === undefined) { + expect.unreachable('Expected value to be present'); + } + expect(idempotencyIndex.config.unique).toBe(true); + expect(idempotencyIndex.config.where).toBeDefined(); + expect( idempotencyIndex.config.columns.map((column) => ('name' in column ? column.name : false)), - ['tenant_id', 'action_key', 'principal_id', 'idempotency_key'], - ); + ).toEqual(['tenant_id', 'action_key', 'principal_id', 'idempotency_key']); }); - -void test('allocates Domain Event order through a database-owned monotonic sequence', () => { +it('allocates Domain Event order through a database-owned monotonic sequence', () => { const domainEventConfig = getTableConfig(domainEvents); const sequenceColumn = domainEventConfig.columns.find( (candidate) => candidate.name === 'tenant_sequence_no', ); - assert.ok(sequenceColumn); - assert.equal(sequenceColumn.notNull, true); - assert.equal(sequenceColumn.hasDefault, true); - assert.equal(sequenceColumn.getSQLType(), 'bigint'); + if (sequenceColumn === undefined) { + expect.unreachable('Expected value to be present'); + } + expect(sequenceColumn.notNull).toBe(true); + expect(sequenceColumn.hasDefault).toBe(true); + expect(sequenceColumn.getSQLType()).toBe('bigint'); const sequenceIndex = domainEventConfig.indexes.find( (candidate) => candidate.config.name === 'core_domain_events_tenant_sequence_uk', ); - assert.ok(sequenceIndex); - assert.equal(sequenceIndex.config.unique, true); - assert.deepEqual( + if (sequenceIndex === undefined) { + expect.unreachable('Expected value to be present'); + } + expect(sequenceIndex.config.unique).toBe(true); + expect( sequenceIndex.config.columns.map((column) => ('name' in column ? column.name : false)), - ['tenant_id', 'tenant_sequence_no'], - ); + ).toEqual(['tenant_id', 'tenant_sequence_no']); }); - -void test('keeps the inferred Action status type aligned with the lifecycle union', () => { +it('keeps the inferred Action status type aligned with the lifecycle union', () => { type ActionInsert = typeof actionInvocations.$inferInsert; const status: ActionInsert['status'] = 'indeterminate'; - assert.equal(status, 'indeterminate'); + expect(status).toBe('indeterminate'); }); diff --git a/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts b/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts index 833729e17..f508d3963 100644 --- a/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts +++ b/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts @@ -1,8 +1,6 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Option } from 'effect'; +import { expect, it } from 'effect-rstest'; + +import { Effect, Option, Predicate } from 'effect'; import { OperationalScopeTransaction, installOperationalScopeFromTransactionService, @@ -26,43 +24,41 @@ const transactionService = ( update: unusedOperation, verify, }); - -void test('installs and verifies transaction-local scope and exposes no transaction controls', async () => { - let calls = 0; - const transaction = transactionService( - () => +it.effect('installs and verifies transaction-local scope and exposes no transaction controls', () => + Effect.gen(function* migratedTest1() { + let calls = 0; + const transaction = transactionService( + () => + Effect.sync(() => { + calls += 1; + }), Effect.sync(() => { calls += 1; + return Option.some({ legal_entity_id: 'entity', tenant_id: 'tenant' }); }), - Effect.sync(() => { - calls += 1; - return Option.some({ legal_entity_id: 'entity', tenant_id: 'tenant' }); - }), - ); - const capability = await runEffectTestPromise( - installOperationalScopeFromTransactionService({ + ); + const capability = yield* installOperationalScopeFromTransactionService({ authContextRef: 'job:test:run:scoped-transaction', authMethod: 'system', correlationId: 'c-1', legalEntityId: 'entity', principalId: 'principal', tenantId: 'tenant', - }).pipe(Effect.provideService(OperationalScopeTransaction, transaction)), - ); - assert.equal(calls, 2); - assert.equal('commit' in capability, false); - assert.equal('query' in capability, false); - assert.equal('rollback' in capability, false); - assert.equal('transaction' in capability, false); -}); - -void test('fails closed when transaction settings do not match', async () => { - const transaction = transactionService( - () => Effect.void, - Effect.succeed(Option.some({ legal_entity_id: '', tenant_id: 'foreign' })), - ); - const error = await runEffectTestPromise( - Effect.flip( + }).pipe(Effect.provideService(OperationalScopeTransaction, transaction)); + expect(calls).toBe(2); + expect('commit' in capability).toBe(false); + expect('query' in capability).toBe(false); + expect('rollback' in capability).toBe(false); + expect('transaction' in capability).toBe(false); + }), +); +it.effect('fails closed when transaction settings do not match', () => + Effect.gen(function* migratedTest2() { + const transaction = transactionService( + () => Effect.void, + Effect.succeed(Option.some({ legal_entity_id: '', tenant_id: 'foreign' })), + ); + const error = yield* Effect.flip( installOperationalScopeFromTransactionService({ authContextRef: 'job:test:run:scoped-transaction', authMethod: 'system', @@ -70,26 +66,22 @@ void test('fails closed when transaction settings do not match', async () => { principalId: 'principal', tenantId: 'tenant', }).pipe(Effect.provideService(OperationalScopeTransaction, transaction)), - ), - ); - assert.equal(error._tag, 'OperationContextUnavailable'); -}); - -void test('creates complete CRUD RLS policies with update using and with-check predicates', () => { + ); + expect(Predicate.isTagged(error, 'OperationContextUnavailable')).toBe(true); + }), +); +it('creates complete CRUD RLS policies with update using and with-check predicates', () => { const fixture = pgTable.withRLS('fixture', { legalEntityId: uuid('legal_entity_id').notNull(), tenantId: uuid('tenant_id').notNull(), }); - assert.equal(getTableConfig(fixture).enableRLS, true); + expect(getTableConfig(fixture).enableRLS).toBe(true); for (const policies of [ tenantRlsPolicies('tenant_fixture', fixture.tenantId), tenantLegalEntityRlsPolicies('entity_fixture', fixture.tenantId, fixture.legalEntityId), ]) { - assert.deepEqual( - policies.map((policy) => policy.for), - ['select', 'insert', 'update', 'delete'], - ); - assert.ok(policies[2].using); - assert.ok(policies[2].withCheck); + expect(policies.map((policy) => policy.for)).toEqual(['select', 'insert', 'update', 'delete']); + expect(policies[2].using).toBeDefined(); + expect(policies[2].withCheck).toBeDefined(); } }); diff --git a/app/packages/core-runtime/tests/unit/search-ingestion.test.ts b/app/packages/core-runtime/tests/unit/search-ingestion.test.ts index 213b84191..9126101da 100644 --- a/app/packages/core-runtime/tests/unit/search-ingestion.test.ts +++ b/app/packages/core-runtime/tests/unit/search-ingestion.test.ts @@ -1,7 +1,5 @@ -import { runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Predicate } from 'effect'; import { CORE_SEARCH_INGESTION_REGISTRATIONS, CORE_SEARCH_PARTY_LIFECYCLE_TOPICS, @@ -12,12 +10,6 @@ import { makeInMemoryCoreSearchProjectionStore, } from '../../src/search/projection.ts'; -const effectTest = (name: string, body: () => Effect.Effect): void => { - void test(name, () => { - void runEffectTestSync(body()); - }); -}; - const tenantId = '10000000-0000-4000-8000-000000000001'; const ref = { moduleId: 'party.registry', @@ -46,24 +38,22 @@ const observation = (projectionVersion: string, title: string) => ({ workerKey: 'party.registry.project-party-updated-to-search', }); -void test('declares one immutable Core registration for every closed Party lifecycle topic', () => { - assert.deepEqual( - CORE_SEARCH_INGESTION_REGISTRATIONS.map(({ topic }) => topic), +it('declares one immutable Core registration for every closed Party lifecycle topic', () => { + expect(CORE_SEARCH_INGESTION_REGISTRATIONS.map(({ topic }) => topic)).toEqual( CORE_SEARCH_PARTY_LIFECYCLE_TOPICS, ); - assert.equal(Object.isFrozen(CORE_SEARCH_INGESTION_REGISTRATIONS), true); - assert.equal( + expect(Object.isFrozen(CORE_SEARCH_INGESTION_REGISTRATIONS)).toBe(true); + expect( CORE_SEARCH_INGESTION_REGISTRATIONS.every( (registration) => Object.isFrozen(registration) && registration.consumerModuleKey === 'party.registry' && registration.producerModuleKey === 'party.registry', ), - true, - ); + ).toBe(true); }); -effectTest('ingests duplicate and out-of-order post-commit observations idempotently', () => { +it.effect('ingests duplicate and out-of-order post-commit observations idempotently', () => { const store = makeInMemoryCoreSearchProjectionStore(); const ingestion = makeCoreSearchIngestion(store); const runtime = createCoreSearchQueryRuntime(store); @@ -80,14 +70,11 @@ effectTest('ingests duplicate and out-of-order post-commit observations idempote resourceType: 'party.registry.party', tenantId, }); - assert.deepEqual( - hits.map(({ title }) => title), - ['Current title'], - ); + expect(hits.map(({ title }) => title)).toEqual(['Current title']); }); }); -effectTest('identifier updates accept only their generated self-consumer worker', () => { +it.effect('identifier updates accept only their generated self-consumer worker', () => { const store = makeInMemoryCoreSearchProjectionStore(); const ingestion = makeCoreSearchIngestion(store); const update = { @@ -104,11 +91,11 @@ effectTest('identifier updates accept only their generated self-consumer worker' workerKey: 'party.registry.project-official-identifier-added-to-search', }), ); - assert.equal(denied._tag, 'CoreSearchProjectionInvalid'); + expect(Predicate.isTagged(denied, 'CoreSearchProjectionInvalid')).toBe(true); }); }); -effectTest('rejects undeclared topics and sequence/document identity mismatches', () => { +it.effect('rejects undeclared topics and sequence/document identity mismatches', () => { const ingestion = makeCoreSearchIngestion(makeInMemoryCoreSearchProjectionStore()); const invalidObservations = [ { ...observation('1', 'Party'), topic: 'party.registry.undeclared.v1' }, @@ -129,18 +116,15 @@ effectTest('rejects undeclared topics and sequence/document identity mismatches' }, }, ]; - return Effect.all( - invalidObservations.map((invalidObservation) => - Effect.flip(ingestion.ingest(invalidObservation)), - ), - { concurrency: 'unbounded' }, - ).pipe( - Effect.tap((failures) => - Effect.sync(() => { - for (const failure of failures) { - assert.equal(failure._tag, 'CoreSearchProjectionInvalid'); - } - }), - ), - ); + return Effect.gen(function* testInvalidObservations() { + const failures = yield* Effect.all( + invalidObservations.map((invalidObservation) => + Effect.flip(ingestion.ingest(invalidObservation)), + ), + { concurrency: 'unbounded' }, + ); + for (const failure of failures) { + expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe(true); + } + }); }); diff --git a/app/packages/core-runtime/tests/unit/search-projection.test.ts b/app/packages/core-runtime/tests/unit/search-projection.test.ts index f3c19cb56..259ed373b 100644 --- a/app/packages/core-runtime/tests/unit/search-projection.test.ts +++ b/app/packages/core-runtime/tests/unit/search-projection.test.ts @@ -1,18 +1,11 @@ -import { runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Schema, Predicate } from 'effect'; +import { TestClock } from 'effect/testing'; import { createCoreSearchQueryRuntime, makeInMemoryCoreSearchProjectionStore, } from '../../src/search/projection.ts'; -const effectTest = (name: string, body: () => Effect.Effect): void => { - void test(name, () => { - void runEffectTestSync(body()); - }); -}; - const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Any)); const tenantId = '10000000-0000-4000-8000-000000000001'; @@ -59,7 +52,7 @@ const party = (overrides: PartyOverrides = {}) => ({ ...overrides, }); -effectTest( +it.effect( 'a projection rebuild floor prevents unseen stale resources and rejects divergent equal-version rebuilds', () => { const store = makeInMemoryCoreSearchProjectionStore(); @@ -75,7 +68,7 @@ effectTest( yield* store.replace(rebuild); yield* store.apply({ document: party(), kind: 'upsert' }); yield* store.replace({ ...rebuild, documents: [party()], rebuildVersion: '1' }); - assert.deepEqual( + expect( yield* runtime.search({ includeArchived: false, moduleId: partyRef.moduleId, @@ -83,11 +76,10 @@ effectTest( resourceType: partyRef.resourceType, tenantId, }), - [], - ); + ).toEqual([]); yield* store.replace(rebuild); const divergent = yield* Effect.flip(store.replace({ ...rebuild, documents: [party()] })); - assert.equal(divergent._tag, 'CoreSearchProjectionInvalid'); + expect(Predicate.isTagged(divergent, 'CoreSearchProjectionInvalid')).toBe(true); yield* store.apply({ document: party({ projectionVersion: '3' }), kind: 'upsert' }); yield* store.replace(rebuild); const searchResults = yield* runtime.search({ @@ -97,12 +89,12 @@ effectTest( resourceType: partyRef.resourceType, tenantId, }); - assert.equal(searchResults.length, 1); + expect(searchResults.length).toBe(1); }); }, ); -effectTest( +it.effect( 'Core Search identifies alias-only matches while canonical evidence takes precedence', () => { const store = makeInMemoryCoreSearchProjectionStore(); @@ -126,18 +118,18 @@ effectTest( tenantId, }); const aliasHits = yield* search('former'); - assert.equal(aliasHits.length, 1); - assert.deepEqual(aliasHits[0]?.ref, partyRef); - assert.deepEqual(aliasHits[0]?.matchedRef, aliasRef); - assert.equal(aliasHits[0]?.matchedSubjectRef, undefined); - assert.doesNotMatch(encodeJson(aliasHits), /Former Company|searchableText|aliases/u); + expect(aliasHits.length).toBe(1); + expect(aliasHits[0]?.ref).toEqual(partyRef); + expect(aliasHits[0]?.matchedRef).toEqual(aliasRef); + expect(aliasHits[0]?.matchedSubjectRef).toBe(undefined); + expect(encodeJson(aliasHits)).not.toMatch(/Former Company|searchableText|aliases/u); const canonicalHits = yield* search('acme'); - assert.equal(canonicalHits[0]?.matchedRef, undefined); + expect(canonicalHits[0]?.matchedRef).toBe(undefined); }); }, ); -effectTest( +it.effect( 'Core Search rejects cross-tenant aliases and malformed or oversized temporal evidence', () => { const store = makeInMemoryCoreSearchProjectionStore(); @@ -183,81 +175,76 @@ effectTest( })), }, ]; - return Effect.all( - invalidEvidence.map((evidence) => - Effect.flip(store.apply({ document: party(evidence), kind: 'upsert' })), - ), - { concurrency: 'unbounded' }, - ).pipe( - Effect.tap((failures) => - Effect.sync(() => { - for (const failure of failures) { - assert.equal(failure._tag, 'CoreSearchProjectionInvalid'); - } - }), - ), - ); + return Effect.gen(function* testInvalidEvidence() { + const failures = yield* Effect.all( + invalidEvidence.map((evidence) => + Effect.flip(store.apply({ document: party(evidence), kind: 'upsert' })), + ), + { concurrency: 'unbounded' }, + ); + for (const failure of failures) { + expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe(true); + } + }); }, ); -effectTest( - 'Core Search honors half-open evidence periods for canonical and subject aliases', - () => { - const store = makeInMemoryCoreSearchProjectionStore(); - const runtime = createCoreSearchQueryRuntime(store); - return Effect.gen(function* testHalfOpenEvidencePeriods() { - yield* store.apply({ - document: party({ - aliases: [ - { - kind: 'subject', - ref: aliasRef, - searchableText: [], - temporalSearchableText: [ - { - validFrom: '2026-01-01T00:00:00Z', - validTo: '2026-02-01T00:00:00Z', - value: 'old-private@example.test', - }, - ], - }, - ], - temporalSearchableText: [ - { validFrom: '2026-02-01T00:00:00Z', value: 'current-private@example.test' }, - { - validFrom: '2000-01-01T00:00:00Z', - validTo: '2100-01-01T00:00:00Z', - value: 'long-lived@example.test', - }, - ], - }), - kind: 'upsert', - }); - const search = (query: string, effectiveAt?: string) => { - const request = { - includeArchived: false, - moduleId: partyRef.moduleId, - query, - resourceType: partyRef.resourceType, - tenantId, - }; - return runtime.search(effectiveAt === undefined ? request : { ...request, effectiveAt }); - }; - const historicalHits = yield* search('old-private', '2026-01-01T00:00:00Z'); - assert.deepEqual(historicalHits[0]?.matchedSubjectRef, aliasRef); - assert.deepEqual(yield* search('old-private', '2026-02-01T00:00:00Z'), []); - assert.deepEqual(yield* search('current-private', '2026-01-31T23:59:59Z'), []); - const current = yield* search('current-private', '2026-02-01T00:00:00Z'); - assert.equal(current.length, 1); - assert.equal(current[0]?.matchedSubjectRef, undefined); - assert.doesNotMatch(encodeJson(current), /private@example|temporalSearchableText/u); - const longLivedHits = yield* search('long-lived'); - assert.equal(longLivedHits.length, 1); +it.effect('Core Search honors half-open evidence periods for canonical and subject aliases', () => { + const store = makeInMemoryCoreSearchProjectionStore(); + const runtime = createCoreSearchQueryRuntime(store); + return Effect.gen(function* testHalfOpenEvidencePeriods() { + yield* TestClock.setTime(Date.parse('2026-09-03T00:00:00Z')); + yield* store.apply({ + document: party({ + aliases: [ + { + kind: 'subject', + ref: aliasRef, + searchableText: [], + temporalSearchableText: [ + { + validFrom: '2026-01-01T00:00:00Z', + validTo: '2026-02-01T00:00:00Z', + value: 'old-private@example.test', + }, + ], + }, + ], + temporalSearchableText: [ + { validFrom: '2026-02-01T00:00:00Z', value: 'current-private@example.test' }, + { + validFrom: '2000-01-01T00:00:00Z', + validTo: '2100-01-01T00:00:00Z', + value: 'long-lived@example.test', + }, + ], + }), + kind: 'upsert', }); - }, -); + const search = (query: string, effectiveAt?: string) => { + const request = { + includeArchived: false, + moduleId: partyRef.moduleId, + query, + resourceType: partyRef.resourceType, + tenantId, + }; + return runtime.search(effectiveAt === undefined ? request : { ...request, effectiveAt }); + }; + const historicalHits = yield* search('old-private', '2026-01-01T00:00:00Z'); + expect(historicalHits[0]?.matchedSubjectRef).toEqual(aliasRef); + expect(yield* search('old-private', '2026-02-01T00:00:00Z')).toEqual([]); + expect(yield* search('current-private', '2026-01-31T23:59:59Z')).toEqual([]); + const current = yield* search('current-private', '2026-02-01T00:00:00Z'); + expect(current.length).toBe(1); + expect(current[0]?.matchedSubjectRef).toBe(undefined); + expect(encodeJson(current)).not.toMatch(/private@example|temporalSearchableText/u); + const longLivedHits = yield* search('long-lived'); + expect(longLivedHits.length).toBe(1); + }); +}); -effectTest('Core Search rebuilds one owned projection atomically and isolates tenants', () => { +it.effect('Core Search rebuilds one owned projection atomically and isolates tenants', () => { const store = makeInMemoryCoreSearchProjectionStore(); const runtime = createCoreSearchQueryRuntime(store); @@ -289,7 +276,7 @@ effectTest('Core Search rebuilds one owned projection atomically and isolates te resourceType: 'party.registry.party', tenantId, }); - assert.deepEqual(result, [ + expect(result).toEqual([ { archived: false, facets: [], @@ -298,7 +285,7 @@ effectTest('Core Search rebuilds one owned projection atomically and isolates te title: 'Acme, s.r.o.', }, ]); - assert.doesNotMatch(encodeJson(result), /private@example\.test/u); + expect(encodeJson(result)).not.toMatch(/private@example\.test/u); yield* store.replace({ documents: [party({ archived: true, projectionVersion: '2', title: 'Replacement' })], @@ -314,11 +301,11 @@ effectTest('Core Search rebuilds one owned projection atomically and isolates te resourceType: 'party.registry.party', tenantId, }); - assert.deepEqual(hits, []); + expect(hits).toEqual([]); }); }); -effectTest( +it.effect( 'Core Search applies typed Legal Entity and role facets without returning match evidence', () => { const store = makeInMemoryCoreSearchProjectionStore(); @@ -373,7 +360,7 @@ effectTest( selectedLegalEntityId: legalEntityId, tenantId, }); - assert.deepEqual(result, [ + expect(result).toEqual([ { archived: false, facets: [], @@ -397,8 +384,8 @@ effectTest( title: 'Acme', }, ]); - assert.doesNotMatch(encodeJson(result), /private@example\.test/u); - assert.deepEqual( + expect(encodeJson(result)).not.toMatch(/private@example\.test/u); + expect( yield* runtime.search({ includeArchived: false, moduleId: 'party.registry', @@ -406,13 +393,12 @@ effectTest( resourceType: 'party.registry.counterparty', tenantId, }), - [], - ); + ).toEqual([]); }); }, ); -effectTest( +it.effect( 'Core Search rejects malformed or cross-owner rebuild documents without partial replacement', () => { const store = makeInMemoryCoreSearchProjectionStore(); @@ -435,7 +421,7 @@ effectTest( tenantId, }), ); - assert.equal(failure._tag, 'CoreSearchProjectionInvalid'); + expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe(true); const result = yield* runtime.search({ includeArchived: false, moduleId: 'party.registry', @@ -443,12 +429,12 @@ effectTest( resourceType: 'party.registry.party', tenantId, }); - assert.equal(result.length, 1); + expect(result.length).toBe(1); }); }, ); -effectTest('Core Search makes duplicate and out-of-order lifecycle observations harmless', () => { +it.effect('Core Search makes duplicate and out-of-order lifecycle observations harmless', () => { const store = makeInMemoryCoreSearchProjectionStore(); const runtime = createCoreSearchQueryRuntime(store); const versionTwo = party({ projectionVersion: '2', title: 'Current title' }); @@ -462,7 +448,7 @@ effectTest('Core Search makes duplicate and out-of-order lifecycle observations yield* store.apply({ kind: 'delete', projectionVersion: '3', ref: partyRef }); yield* store.apply({ document: versionTwo, kind: 'upsert' }); - assert.deepEqual( + expect( yield* runtime.search({ includeArchived: true, moduleId: 'party.registry', @@ -470,7 +456,6 @@ effectTest('Core Search makes duplicate and out-of-order lifecycle observations resourceType: 'party.registry.party', tenantId, }), - [], - ); + ).toEqual([]); }); }); diff --git a/app/packages/core-runtime/tests/unit/search-schema.test.ts b/app/packages/core-runtime/tests/unit/search-schema.test.ts index 09d4ec8f6..147d48bd5 100644 --- a/app/packages/core-runtime/tests/unit/search-schema.test.ts +++ b/app/packages/core-runtime/tests/unit/search-schema.test.ts @@ -1,6 +1,4 @@ -/* oxlint-disable typescript/strict-boolean-expressions -- Existing compatibility boundary; expires: 2026-12-31. */ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { getTableConfig, PgDialect } from 'drizzle-orm/pg-core'; import { searchIndexEntries, @@ -10,99 +8,97 @@ import { const config = getTableConfig(searchIndexEntries); -test('Core Search rebuild floors are tenant/resource-scoped and cannot be deleted by runtime', () => { +it('Core Search rebuild floors are tenant/resource-scoped and cannot be deleted by runtime', () => { const rebuilds = getTableConfig(searchProjectionRebuilds); - assert.equal(rebuilds.enableRLS, true); - assert.deepEqual( - rebuilds.primaryKeys[0]?.columns.map(({ name }) => name), - ['tenant_id', 'source_module_key', 'source_resource_type'], - ); - assert.deepEqual( - rebuilds.policies.map(({ for: operation }) => operation), - ['select', 'insert', 'update'], - ); - assert.deepEqual( - rebuilds.checks.map(({ name }) => name), - [ - 'core_search_projection_rebuilds_version_ck', - 'core_search_projection_rebuilds_fingerprint_ck', - ], - ); + expect(rebuilds.enableRLS).toBe(true); + expect(rebuilds.primaryKeys[0]?.columns.map(({ name }) => name)).toEqual([ + 'tenant_id', + 'source_module_key', + 'source_resource_type', + ]); + expect(rebuilds.policies.map(({ for: operation }) => operation)).toEqual([ + 'select', + 'insert', + 'update', + ]); + expect(rebuilds.checks.map(({ name }) => name)).toEqual([ + 'core_search_projection_rebuilds_version_ck', + 'core_search_projection_rebuilds_fingerprint_ck', + ]); }); -test('Core Search snapshot generations are independent tenant/source-scoped infrastructure', () => { +it('Core Search snapshot generations are independent tenant/source-scoped infrastructure', () => { const generations = getTableConfig(searchProjectionGenerations); - assert.equal(generations.enableRLS, true); - assert.deepEqual( - generations.primaryKeys[0]?.columns.map(({ name }) => name), - ['tenant_id', 'source_module_key'], - ); - assert.deepEqual( - generations.policies.map(({ for: operation }) => operation), - ['select', 'insert', 'update'], - ); - assert.ok(generations.columns.some(({ name }) => name === 'generation')); - assert.ok(generations.columns.some(({ name }) => name === 'event_watermark')); + expect(generations.enableRLS).toBe(true); + expect(generations.primaryKeys[0]?.columns.map(({ name }) => name)).toEqual([ + 'tenant_id', + 'source_module_key', + ]); + expect(generations.policies.map(({ for: operation }) => operation)).toEqual([ + 'select', + 'insert', + 'update', + ]); + expect(generations.columns.some(({ name }) => name === 'generation')).toBe(true); + expect(generations.columns.some(({ name }) => name === 'event_watermark')).toBe(true); }); -test('Core Search physical projection has versioned tenant-qualified lookup keys', () => { - assert.equal(config.enableRLS, true); - assert.deepEqual( +it('Core Search physical projection has versioned tenant-qualified lookup keys', () => { + expect(config.enableRLS).toBe(true); + expect( config.columns .filter(({ name }) => ['deleted', 'projection_version'].includes(name)) .map(({ name, notNull }) => ({ name, notNull })), - [ - { name: 'deleted', notNull: true }, - { name: 'projection_version', notNull: true }, - ], - ); + ).toEqual([ + { name: 'deleted', notNull: true }, + { name: 'projection_version', notNull: true }, + ]); const source = config.indexes.find( ({ config: index }) => index.name === 'core_search_index_entries_source_uk', ); - assert.ok(source?.config.unique); - assert.deepEqual( - source.config.columns.map((column) => 'name' in column && column.name), - ['tenant_id', 'source_module_key', 'source_resource_type', 'source_resource_id'], - ); + expect(source?.config.unique).toBe(true); + expect(source?.config.columns.map((column) => 'name' in column && column.name)).toEqual([ + 'tenant_id', + 'source_module_key', + 'source_resource_type', + 'source_resource_id', + ]); const query = config.indexes.find( ({ config: index }) => index.name === 'core_search_index_entries_query_idx', ); - assert.ok(query); - assert.deepEqual( - query.config.columns.map((column) => 'name' in column && column.name), - ['tenant_id', 'source_module_key', 'source_resource_type', 'legal_entity_id', 'deleted'], - ); + expect(query).toBeDefined(); + expect(query?.config.columns.map((column) => 'name' in column && column.name)).toEqual([ + 'tenant_id', + 'source_module_key', + 'source_resource_type', + 'legal_entity_id', + 'deleted', + ]); }); -test('Core Search projection declares complete tenant RLS and bounded document checks', () => { - assert.deepEqual( - config.policies.map(({ name }) => name), - [ - 'core_search_index_entries_tenant_select', - 'core_search_index_entries_tenant_insert', - 'core_search_index_entries_tenant_update', - 'core_search_index_entries_tenant_delete', - ], - ); - assert.deepEqual( - config.policies.map((policy) => policy.for), - ['select', 'insert', 'update', 'delete'], - ); - assert.equal( - config.policies.every(({ to }) => to === 'ontos_runtime'), - true, - ); +it('Core Search projection declares complete tenant RLS and bounded document checks', () => { + expect(config.policies.map(({ name }) => name)).toEqual([ + 'core_search_index_entries_tenant_select', + 'core_search_index_entries_tenant_insert', + 'core_search_index_entries_tenant_update', + 'core_search_index_entries_tenant_delete', + ]); + expect(config.policies.map((policy) => policy.for)).toEqual([ + 'select', + 'insert', + 'update', + 'delete', + ]); + expect(config.policies.every(({ to }) => to === 'ontos_runtime')).toBe(true); const dialect = new PgDialect(); const checks = config.checks.map(({ name, value }) => ({ name, sql: dialect.sqlToQuery(value).sql, })); - assert.match( + expect( checks.find(({ name }) => name === 'core_search_index_entries_document_ck')?.sql ?? '', - /body_text/u, - ); - assert.match( + ).toMatch(/body_text/u); + expect( checks.find(({ name }) => name === 'core_search_index_entries_version_ck')?.sql ?? '', - /projection_version/u, - ); + ).toMatch(/projection_version/u); }); diff --git a/app/packages/core-runtime/tests/unit/search-worker-snapshot.test.ts b/app/packages/core-runtime/tests/unit/search-worker-snapshot.test.ts index 3b693ac47..186c1c198 100644 --- a/app/packages/core-runtime/tests/unit/search-worker-snapshot.test.ts +++ b/app/packages/core-runtime/tests/unit/search-worker-snapshot.test.ts @@ -1,7 +1,5 @@ -import { runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Schema, Predicate } from 'effect'; import { attestOutboxWorkerHandlerContext } from '../../src/outbox/definition.ts'; import { CoreSearchProjectionUnavailable } from '../../src/search/projection.ts'; import { @@ -45,29 +43,20 @@ class SnapshotRetryFailure extends Schema.TaggedError()( }, ) {} -const effectTest = ( - name: string, - body: () => Effect.Effect, -): void => { - void test(name, () => { - runEffectTestSync(body()); - }); -}; - const readParty = (readExecutor: CoreSearchSnapshotReadExecutor) => { - assert.deepEqual(Object.keys(readExecutor), ['select']); + expect(Object.keys(readExecutor)).toEqual(['select']); return Effect.succeed('party'); }; const readCounterparty = (readExecutor: CoreSearchSnapshotReadExecutor) => { - assert.deepEqual(Object.keys(readExecutor), ['select']); + expect(Object.keys(readExecutor)).toEqual(['select']); return Effect.succeed('counterparty'); }; const readInvalid = () => Effect.succeed('invalid'); const readStillInvalid = () => Effect.succeed('still invalid'); -effectTest( +it.effect( 'worker snapshot rejects caller-created and unregistered contexts before opening persistence', () => { let calls = 0; @@ -91,20 +80,16 @@ effectTest( (candidate) => Effect.flip(snapshot.read(candidate, () => Effect.succeed('unreachable'))), { concurrency: 'unbounded' }, ); - assert.deepEqual( - failures.map(({ _tag }) => _tag), - [ - 'CoreSearchProjectionInvalid', - 'CoreSearchProjectionInvalid', - 'CoreSearchProjectionInvalid', - ], - ); - assert.equal(calls, 0); + expect(failures.length).toBe(3); + for (const failure of failures) { + expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe(true); + } + expect(calls).toBe(0); }); }, ); -effectTest('identifier-update worker receives the verified Core snapshot capability', () => { +it.effect('identifier-update worker receives the verified Core snapshot capability', () => { const reader = makeCoreSearchWorkerSnapshot({ run: (_context, readSnapshot) => readSnapshot( @@ -127,11 +112,11 @@ effectTest('identifier-update worker receives the verified Core snapshot capabil }), (snapshot) => Effect.succeed(snapshot.projectionVersion), ); - assert.equal(version, '1'); + expect(version).toBe('1'); }); }); -effectTest( +it.effect( 'worker snapshot exposes select-only owner reads at one current watermark and restores scope', () => { const installedScopes: (string | undefined)[] = []; @@ -155,26 +140,26 @@ effectTest( return Effect.gen(function* readCurrentOwnerSnapshot() { const result = yield* snapshot.read(attestOutboxWorkerHandlerContext(context), (view) => Effect.gen(function* readOwnerProjection() { - assert.equal(view.projectionVersion, '42'); - assert.equal(view.eventWatermark, '100'); - assert.equal(view.tenantId, tenantId); - assert.deepEqual(view.legalEntityIds, [legalEntityId]); + expect(view.projectionVersion).toBe('42'); + expect(view.eventWatermark).toBe('100'); + expect(view.tenantId).toBe(tenantId); + expect(view.legalEntityIds).toEqual([legalEntityId]); const party = yield* view.tenant(readParty); const counterparty = yield* view.forLegalEntity(legalEntityId, readCounterparty); return { counterparty, party, projectionVersion: view.projectionVersion }; }), ); - assert.deepEqual(result, { + expect(result).toEqual({ counterparty: 'counterparty', party: 'party', projectionVersion: '42', }); - assert.deepEqual(installedScopes, [undefined, undefined, legalEntityId, undefined]); + expect(installedScopes).toEqual([undefined, undefined, legalEntityId, undefined]); }); }, ); -effectTest( +it.effect( 'worker snapshot rejects a Legal Entity outside its tenant enumeration and preserves owner failures', () => { const installedScopes: (string | undefined)[] = []; @@ -202,20 +187,20 @@ effectTest( view.forLegalEntity('20000000-0000-4000-8000-000000000002', () => Effect.succeed('no')), ), ); - assert.equal(invalidScope._tag, 'CoreSearchProjectionInvalid'); - assert.deepEqual(installedScopes, []); + expect(Predicate.isTagged(invalidScope, 'CoreSearchProjectionInvalid')).toBe(true); + expect(installedScopes).toEqual([]); const failure = yield* Effect.flip( snapshot.read(verified, (view) => view.forLegalEntity(legalEntityId, () => Effect.fail('owner-unavailable')), ), ); - assert.equal(failure, 'owner-unavailable'); - assert.deepEqual(installedScopes, [legalEntityId, undefined]); + expect(failure).toBe('owner-unavailable'); + expect(installedScopes).toEqual([legalEntityId, undefined]); }); }, ); -effectTest('worker snapshot maps persistence failure to a sanitized unavailable error', () => { +it.effect('worker snapshot maps persistence failure to a sanitized unavailable error', () => { const snapshot = makeCoreSearchWorkerSnapshot({ run: () => Effect.fail( @@ -230,12 +215,12 @@ effectTest('worker snapshot maps persistence failure to a sanitized unavailable const failure = yield* Effect.flip( snapshot.read(attestOutboxWorkerHandlerContext(context), () => Effect.succeed('no')), ); - assert.equal(failure._tag, 'CoreSearchProjectionUnavailable'); - assert.doesNotMatch(failure.reason, /private database/u); + expect(Predicate.isTagged(failure, 'CoreSearchProjectionUnavailable')).toBe(true); + expect(failure.reason).not.toMatch(/private database/u); }); }); -effectTest( +it.effect( 'snapshot generation retries serialization conflicts only and bounds repeated contention', () => { let attempts = 0; @@ -251,8 +236,8 @@ effectTest( ) : Effect.succeed('fresh snapshot'); }).pipe(retryCoreSearchSnapshot); - assert.equal(snapshot, 'fresh snapshot'); - assert.equal(attempts, 3); + expect(snapshot).toBe('fresh snapshot'); + expect(attempts).toBe(3); attempts = 0; const contention = yield* Effect.flip( @@ -261,8 +246,8 @@ effectTest( return Effect.fail(new SnapshotRetryFailure({ code: '40001', message: 'contention' })); }).pipe(retryCoreSearchSnapshot), ); - assert.match(contention.message, /contention/u); - assert.equal(attempts, 4); + expect(contention.message).toMatch(/contention/u); + expect(attempts).toBe(4); attempts = 0; const nonSerialization = yield* Effect.flip( @@ -271,13 +256,13 @@ effectTest( return Effect.fail(new SnapshotRetryFailure({ message: 'not serialization' })); }).pipe(retryCoreSearchSnapshot), ); - assert.match(nonSerialization.message, /not serialization/u); - assert.equal(attempts, 1); + expect(nonSerialization.message).toMatch(/not serialization/u); + expect(attempts).toBe(1); }); }, ); -effectTest('snapshot revokes escaped scope capabilities when the owner callback finishes', () => { +it.effect('snapshot revokes escaped scope capabilities when the owner callback finishes', () => { const reader = makeCoreSearchWorkerSnapshot({ run: (_context, readSnapshot) => readSnapshot( @@ -292,11 +277,11 @@ effectTest('snapshot revokes escaped scope capabilities when the owner callback Effect.succeed, ); const failure = yield* Effect.flip(escaped.tenant(() => Effect.succeed('stale'))); - assert.equal(failure._tag, 'CoreSearchProjectionInvalid'); + expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe(true); }); }); -effectTest('nested scope rejection does not unlock the active owner read', () => { +it.effect('nested scope rejection does not unlock the active owner read', () => { const reader = makeCoreSearchWorkerSnapshot({ run: (_context, readSnapshot) => readSnapshot( @@ -310,8 +295,8 @@ effectTest('nested scope rejection does not unlock the active owner read', () => Effect.gen(function* nestedReads() { const first = yield* Effect.flip(snapshot.forLegalEntity(legalEntityId, readInvalid)); const second = yield* Effect.flip(snapshot.tenant(readStillInvalid)); - assert.equal(first._tag, 'CoreSearchProjectionInvalid'); - assert.equal(second._tag, 'CoreSearchProjectionInvalid'); + expect(Predicate.isTagged(first, 'CoreSearchProjectionInvalid')).toBe(true); + expect(Predicate.isTagged(second, 'CoreSearchProjectionInvalid')).toBe(true); }), ), ); diff --git a/app/packages/core-runtime/tests/unit/service-public-surface.test.ts b/app/packages/core-runtime/tests/unit/service-public-surface.test.ts index 8b3c55d54..24dcda527 100644 --- a/app/packages/core-runtime/tests/unit/service-public-surface.test.ts +++ b/app/packages/core-runtime/tests/unit/service-public-surface.test.ts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import type { ContextAccessService, InstalledModuleCatalogServiceContract, @@ -27,6 +26,6 @@ const preservePublicServiceContract = ( service: Service, ): Service => service; -void test('exports the anti-slop-compliant Core service contracts', () => { - assert.equal(preservePublicServiceContract.length, 1); +it('exports the anti-slop-compliant Core service contracts', () => { + expect(preservePublicServiceContract.length).toBe(1); }); diff --git a/app/packages/core-runtime/tests/unit/shell-contribution.test.ts b/app/packages/core-runtime/tests/unit/shell-contribution.test.ts index 1a28b802c..5e9f5db2a 100644 --- a/app/packages/core-runtime/tests/unit/shell-contribution.test.ts +++ b/app/packages/core-runtime/tests/unit/shell-contribution.test.ts @@ -1,12 +1,14 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; import { validateShellContributions } from '../../src/modules/shell-contribution.ts'; +const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Any)); + const moduleId = 'property.registry'; const first = (values: readonly Value[]): Value => { const [value] = values; if (value === undefined) { - assert.fail('Expected a fixture item'); + throw new Error('Expected a fixture item'); } return value; }; @@ -94,7 +96,7 @@ const full = () => ({ ], }); -test('accepts exact empty and full Shell contribution contracts with deterministic JSON data', () => { +it('accepts exact empty and full Shell contribution contracts with deterministic JSON data', () => { const empty = { mediaAttachments: [], navigation: [], @@ -105,46 +107,46 @@ test('accepts exact empty and full Shell contribution contracts with determinist search: [], timelines: [], }; - assert.deepEqual(validateShellContributions(empty, references), empty); + expect(validateShellContributions(empty, references)).toEqual(empty); const decoded = validateShellContributions(full(), references); - assert.deepEqual(structuredClone(decoded), decoded); - assert.doesNotMatch(JSON.stringify(decoded), /handler|sourcePath|remote|import/iu); + expect(structuredClone(decoded)).toEqual(decoded); + expect(encodeJson(decoded)).not.toMatch(/handler|sourcePath|remote|import/iu); }); -test('accepts safe dynamic page templates as plain serialized data', () => { +it('accepts safe dynamic page templates as plain serialized data', () => { const dynamic = full(); dynamic.pages[0] = { ...first(dynamic.pages), routePath: '/contacts/customers/:id/edit', }; const decoded = validateShellContributions(dynamic, references); - assert.equal(decoded.pages[0]?.routePath, '/contacts/customers/:id/edit'); - assert.deepEqual(structuredClone(decoded), decoded); - assert.doesNotMatch(JSON.stringify(decoded), /handler|loader|sourcePath|remote|import/iu); + expect(decoded.pages[0]?.routePath).toBe('/contacts/customers/:id/edit'); + expect(structuredClone(decoded)).toEqual(decoded); + expect(encodeJson(decoded)).not.toMatch(/handler|loader|sourcePath|remote|import/iu); }); -test('rejects extra keys, duplicates, cross-owner entrypoints, and missing references', () => { - assert.throws(() => validateShellContributions({ ...full(), route: '/private' }, references)); +it('rejects extra keys, duplicates, cross-owner entrypoints, and missing references', () => { + expect(() => validateShellContributions({ ...full(), route: '/private' }, references)).toThrow(); const duplicate = full(); duplicate.publicComponents[0] = { ...first(duplicate.publicComponents), contributionKey: first(duplicate.pages).contributionKey, }; - assert.throws(() => validateShellContributions(duplicate, references), /duplicate/u); + expect(() => validateShellContributions(duplicate, references)).toThrow(/duplicate/u); const crossOwner = full(); crossOwner.pages[0] = { ...first(crossOwner.pages), entrypoint: { ...first(crossOwner.pages).entrypoint, moduleKey: 'billing.core' }, }; - assert.throws(() => validateShellContributions(crossOwner, references), /owner/u); - assert.throws(() => + expect(() => validateShellContributions(crossOwner, references)).toThrow(/owner/u); + expect(() => validateShellContributions(full(), { ...references, componentKeys: new Set() }), - ); + ).toThrow(); }); -test('rejects incompatible entrypoint roles and arbitrary transport metadata', () => { +it('rejects incompatible entrypoint roles and arbitrary transport metadata', () => { const baseline = full(); - assert.throws(() => + expect(() => validateShellContributions( { ...baseline, @@ -152,8 +154,8 @@ test('rejects incompatible entrypoint roles and arbitrary transport metadata', ( }, references, ), - ); - assert.throws(() => + ).toThrow(); + expect(() => validateShellContributions( { ...baseline, @@ -166,8 +168,8 @@ test('rejects incompatible entrypoint roles and arbitrary transport metadata', ( }, references, ), - ); - assert.throws(() => + ).toThrow(); + expect(() => validateShellContributions( { ...baseline, @@ -183,8 +185,8 @@ test('rejects incompatible entrypoint roles and arbitrary transport metadata', ( }, references, ), - ); - assert.throws(() => + ).toThrow(); + expect(() => validateShellContributions( { ...baseline, @@ -192,13 +194,13 @@ test('rejects incompatible entrypoint roles and arbitrary transport metadata', ( }, references, ), - ); + ).toThrow(); const withUnsafeRoute = full(); withUnsafeRoute.pages[0] = { ...first(withUnsafeRoute.pages), routePath: '/modules/:module-id', }; - assert.throws(() => validateShellContributions(withUnsafeRoute, references)); + expect(() => validateShellContributions(withUnsafeRoute, references)).toThrow(); }); for (const routePath of [ @@ -219,9 +221,9 @@ for (const routePath of [ '/contacts/customers/:1id', '/contacts/customers/:id/edit/:id', ] as const) { - test(`rejects unsafe or ambiguous page route template ${routePath}`, () => { + it(`rejects unsafe or ambiguous page route template ${routePath}`, () => { const candidate = full(); candidate.pages[0] = { ...first(candidate.pages), routePath }; - assert.throws(() => validateShellContributions(candidate, references)); + expect(() => validateShellContributions(candidate, references)).toThrow(); }); } diff --git a/app/packages/core-runtime/tests/unit/spicedb-client.test.ts b/app/packages/core-runtime/tests/unit/spicedb-client.test.ts index e9989588d..f07a2dc87 100644 --- a/app/packages/core-runtime/tests/unit/spicedb-client.test.ts +++ b/app/packages/core-runtime/tests/unit/spicedb-client.test.ts @@ -1,38 +1,34 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { v1 } from '@authzed/authzed-node'; import { spiceDbClientSecurity } from '../../src/permissions/client.ts'; import { SpiceDbConfigError } from '../../src/permissions/config-error.ts'; -void test('uses authenticated plaintext credentials for an explicitly insecure transport', () => { - assert.equal( +it('uses authenticated plaintext credentials for an explicitly insecure transport', () => { + expect( spiceDbClientSecurity({ endpoint: 'localhost:50051', insecureLocal: true, }), - v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS, - ); - assert.equal( + ).toBe(v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS); + expect( spiceDbClientSecurity({ deploymentEnvironment: 'stage', endpoint: 'spicedb:50051', insecureLocal: true, }), - v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS, - ); + ).toBe(v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS); }); -void test('uses TLS credentials for a secure transport', () => { - assert.equal( +it('uses TLS credentials for a secure transport', () => { + expect( spiceDbClientSecurity({ endpoint: 'spicedb.internal.example:443', insecureLocal: false, }), - v1.ClientSecurity.SECURE, - ); + ).toBe(v1.ClientSecurity.SECURE); }); -void test('rejects plaintext credentials for an arbitrary or non-stage endpoint', () => { +it('rejects plaintext credentials for an arbitrary or non-stage endpoint', () => { for (const configuration of [ { endpoint: 'spicedb.internal.example:50051', insecureLocal: true }, { endpoint: 'spicedb:50051', insecureLocal: true }, @@ -42,6 +38,6 @@ void test('rejects plaintext credentials for an arbitrary or non-stage endpoint' insecureLocal: true, }, ] as const) { - assert.throws(() => spiceDbClientSecurity(configuration), SpiceDbConfigError); + expect(() => spiceDbClientSecurity(configuration)).toThrow(SpiceDbConfigError); } }); diff --git a/app/packages/core-runtime/tests/unit/spicedb-database-bootstrap.test.ts b/app/packages/core-runtime/tests/unit/spicedb-database-bootstrap.test.ts index 6a39bb93e..3a0e6c670 100644 --- a/app/packages/core-runtime/tests/unit/spicedb-database-bootstrap.test.ts +++ b/app/packages/core-runtime/tests/unit/spicedb-database-bootstrap.test.ts @@ -1,7 +1,7 @@ -// @effect-diagnostics asyncFunction:off nodeBuiltinImport:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; +// @effect-diagnostics nodeBuiltinImport:off -- Reads repository fixture files through the Node promise API; expires: 2026-12-31. +import { Effect } from 'effect'; +import { expect, it } from 'effect-rstest'; import { readFile } from 'node:fs/promises'; -import test from 'node:test'; import { parseSpiceDbDatabaseBootstrapConfig } from '../../src/install/spicedb-database-config.ts'; import { toModuleAccessObjectId } from '../../src/permissions/context-access.ts'; import { ONTOS_SPICEDB_SCHEMA } from '../../src/permissions/schema.ts'; @@ -19,22 +19,21 @@ const extractSchema = (source: string): string => .map((line) => line.replace(/^ {2}/u, '')) .join('\n'); -test('accepts a distinct SpiceDB role and database on the administrative server', () => { - assert.deepEqual( +it('accepts a distinct SpiceDB role and database on the administrative server', () => { + expect( parseSpiceDbDatabaseBootstrapConfig({ DATABASE_ADMIN_URL: 'postgresql://db:admin@db:5432/db', SPICEDB_DATABASE_URL: 'postgresql://spicedb:p%40ssword@db:5432/spicedb', }), - { - adminUrl: 'postgresql://db:admin@db:5432/db', - database: 'spicedb', - password: 'p@ssword', - user: 'spicedb', - }, - ); + ).toEqual({ + adminUrl: 'postgresql://db:admin@db:5432/db', + database: 'spicedb', + password: 'p@ssword', + user: 'spicedb', + }); }); -test('rejects unsafe SpiceDB database bootstrap targets', () => { +it('rejects unsafe SpiceDB database bootstrap targets', () => { for (const environment of [ {}, { @@ -50,65 +49,69 @@ test('rejects unsafe SpiceDB database bootstrap targets', () => { SPICEDB_DATABASE_URL: 'postgresql://spicedb:secret@db:5432/ontos', }, ]) { - assert.throws(() => parseSpiceDbDatabaseBootstrapConfig(environment)); + expect(() => parseSpiceDbDatabaseBootstrapConfig(environment)).toThrow(); } }); -test('keeps the stage bootstrap schema aligned without development relationships', async () => { - const development = await readFile( - new URL('../../spicedb/bootstrap.yaml', import.meta.url), - 'utf-8', - ); - const stage = await readFile( - new URL('../../spicedb/stage-bootstrap.yaml', import.meta.url), - 'utf-8', - ); - assert.equal(extractSchema(development), ONTOS_SPICEDB_SCHEMA); - assert.equal(extractSchema(stage), ONTOS_SPICEDB_SCHEMA); - assert.doesNotMatch(stage, /relationships:|assertions:/u); - assert.match(development, /#executor@tenant:test-tenant#member/u); - assert.match(development, /#executor@principal:allowed-principal/u); -}); +it.effect('keeps the stage bootstrap schema aligned without development relationships', () => + Effect.gen(function* testScenario1() { + const development = yield* Effect.promise(() => + readFile(new URL('../../spicedb/bootstrap.yaml', import.meta.url), 'utf-8'), + ); + const stage = yield* Effect.promise(() => + readFile(new URL('../../spicedb/stage-bootstrap.yaml', import.meta.url), 'utf-8'), + ); + expect(extractSchema(development)).toBe(ONTOS_SPICEDB_SCHEMA); + expect(extractSchema(stage)).toBe(ONTOS_SPICEDB_SCHEMA); + expect(stage).not.toMatch(/relationships:|assertions:/u); + expect(development).toMatch(/#executor@tenant:test-tenant#member/u); + expect(development).toMatch(/#executor@principal:allowed-principal/u); + }), +); -test('grants fresh development module access only to Contacts', async () => { - const development = await readFile( - new URL('../../spicedb/bootstrap.yaml', import.meta.url), - 'utf-8', - ); - const tenantId = '50000000-0000-4000-8000-000000000001'; - const legalEntityId = '55000000-0000-4000-8000-000000000001'; - const contactsObjectId = toModuleAccessObjectId(tenantId, legalEntityId, 'contacts.core'); - assert.ok(contactsObjectId !== undefined && contactsObjectId.length > 0); - assert.deepEqual( - development.match( - /^ {2}module_access:\S+#accessor@principal:60000000-0000-4000-8000-000000000001$/gmu, - ), - [` module_access:${contactsObjectId}#accessor@principal:60000000-0000-4000-8000-000000000001`], - ); -}); +it.effect('grants fresh development module access only to Contacts', () => + Effect.gen(function* testScenario2() { + const development = yield* Effect.promise(() => + readFile(new URL('../../spicedb/bootstrap.yaml', import.meta.url), 'utf-8'), + ); + const tenantId = '50000000-0000-4000-8000-000000000001'; + const legalEntityId = '55000000-0000-4000-8000-000000000001'; + const contactsObjectId = toModuleAccessObjectId(tenantId, legalEntityId, 'contacts.core'); + expect(contactsObjectId !== undefined && contactsObjectId.length > 0).toBe(true); + expect( + development.match( + /^ {2}module_access:\S+#accessor@principal:60000000-0000-4000-8000-000000000001$/gmu, + ), + ).toEqual([ + ` module_access:${contactsObjectId}#accessor@principal:60000000-0000-4000-8000-000000000001`, + ]); + }), +); -test('declares the complete Party tenant permission vocabulary', async () => { - const development = await readFile( - new URL('../../spicedb/bootstrap.yaml', import.meta.url), - 'utf-8', - ); - for (const permission of [ - 'manage_party_identity', - 'manage_party_relationships', - 'merge_party_identity', - 'read_party_identity', - 'review_party_identity', - ]) { - assert.match(development, new RegExp(`permission ${permission} =`, 'u')); - } -}); +it.effect('declares the complete Party tenant permission vocabulary', () => + Effect.gen(function* testScenario3() { + const development = yield* Effect.promise(() => + readFile(new URL('../../spicedb/bootstrap.yaml', import.meta.url), 'utf-8'), + ); + for (const permission of [ + 'manage_party_identity', + 'manage_party_relationships', + 'merge_party_identity', + 'read_party_identity', + 'review_party_identity', + ]) { + expect(development).toMatch(new RegExp(`permission ${permission} =`, 'u')); + } + }), +); -test('declares the Counterparty Legal Entity permission vocabulary', async () => { - const development = await readFile( - new URL('../../spicedb/bootstrap.yaml', import.meta.url), - 'utf-8', - ); - for (const permission of ['manage_counterparty', 'read_counterparty']) { - assert.match(development, new RegExp(`permission ${permission} =`, 'u')); - } -}); +it.effect('declares the Counterparty Legal Entity permission vocabulary', () => + Effect.gen(function* testScenario4() { + const development = yield* Effect.promise(() => + readFile(new URL('../../spicedb/bootstrap.yaml', import.meta.url), 'utf-8'), + ); + for (const permission of ['manage_counterparty', 'read_counterparty']) { + expect(development).toMatch(new RegExp(`permission ${permission} =`, 'u')); + } + }), +); diff --git a/app/packages/core-runtime/tests/unit/stage-context-bootstrap.test.ts b/app/packages/core-runtime/tests/unit/stage-context-bootstrap.test.ts index 3fb79619f..b76aaf6c3 100644 --- a/app/packages/core-runtime/tests/unit/stage-context-bootstrap.test.ts +++ b/app/packages/core-runtime/tests/unit/stage-context-bootstrap.test.ts @@ -1,9 +1,8 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { STAGE_CONTEXTS } from '../../src/install/stage-context-bootstrap.ts'; -void test('defines the exact Techsio and Siampark stage contexts', () => { - assert.deepEqual(STAGE_CONTEXTS, { +it('defines the exact Techsio and Siampark stage contexts', () => { + expect(STAGE_CONTEXTS).toEqual({ siampark: { authBindingId: '73000000-0000-4000-8000-000000000002', defaultLocale: 'cs', diff --git a/app/packages/core-runtime/tests/unit/system-principal-context.test.ts b/app/packages/core-runtime/tests/unit/system-principal-context.test.ts index 69e05a995..4f0ed0cee 100644 --- a/app/packages/core-runtime/tests/unit/system-principal-context.test.ts +++ b/app/packages/core-runtime/tests/unit/system-principal-context.test.ts @@ -1,8 +1,5 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics anyUnknownInErrorContext:off asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import { Effect, Option, Schema } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; +import { Effect, Option, Schema, Predicate } from 'effect'; import { TrustedPrincipalContextSchema } from '../../src/actions/principal-context.ts'; import { decodeTrustedPrincipalContext } from '../../src/auth/system-principal-context-provenance.ts'; import { @@ -22,95 +19,99 @@ const resolverFor = (record: { load: () => Effect.succeed(Option.some(record)), }); -void test('constructs one immutable trusted system context from a branded registration', async () => { - const registration = registerSystemWorkload({ jobKey: 'inventory-reconcile' }); - const context = await runEffectTestPromise( - resolverFor({ kind: 'system', principalStatus: 'active', tenantStatus: 'active' }).resolve({ +it.effect('constructs one immutable trusted system context from a branded registration', () => + Effect.gen(function* testScenario1() { + const registration = registerSystemWorkload({ jobKey: 'inventory-reconcile' }); + const context = yield* resolverFor({ + kind: 'system', + principalStatus: 'active', + tenantStatus: 'active', + }).resolve({ principalId, registration, runReference: 'run-42', tenantId, - }), - ); + }); - assert.equal(Object.isFrozen(registration), true); - assert.equal(Object.isFrozen(context), true); - assert.deepEqual(context, { - authContextRef: 'job:inventory-reconcile:run:run-42', - authMethod: 'system', - principalId, - tenantId, - }); - assert.deepEqual(Schema.decodeUnknownSync(TrustedPrincipalContextSchema)(context), context); - assert.deepEqual(await runEffectTestPromise(decodeTrustedPrincipalContext(context)), context); - await assert.rejects(runEffectTestPromise(decodeTrustedPrincipalContext({ ...context }))); -}); + expect(Object.isFrozen(registration)).toBe(true); + expect(Object.isFrozen(context)).toBe(true); + expect(context).toEqual({ + authContextRef: 'job:inventory-reconcile:run:run-42', + authMethod: 'system', + principalId, + tenantId, + }); + expect(yield* Schema.decodeUnknownEffect(TrustedPrincipalContextSchema)(context)).toEqual( + context, + ); + expect(yield* decodeTrustedPrincipalContext(context)).toEqual(context); + expect(yield* Effect.flip(decodeTrustedPrincipalContext({ ...context }))).toBeDefined(); + }), +); -void test('rejects forged registrations, unsafe refs, wrong kinds, and inactive state', async () => { - const registration = registerSystemWorkload({ jobKey: 'inventory-reconcile' }); - const forged = { ...registration }; - const invalid = await runEffectTestPromise( - Effect.flip( +it.effect('rejects forged registrations, unsafe refs, wrong kinds, and inactive state', () => + Effect.gen(function* testScenario2() { + const registration = registerSystemWorkload({ jobKey: 'inventory-reconcile' }); + const forged = { ...registration }; + const invalid = yield* Effect.flip( resolverFor({ kind: 'system', principalStatus: 'active', tenantStatus: 'active' }).resolve({ principalId, registration: forged, runReference: 'run-42', tenantId, }), - ), - ); - const wrongKind = await runEffectTestPromise( - Effect.flip( + ); + const wrongKind = yield* Effect.flip( resolverFor({ kind: 'human', principalStatus: 'active', tenantStatus: 'active' }).resolve({ principalId, registration, runReference: 'run-42', tenantId, }), - ), - ); - const inactive = await runEffectTestPromise( - Effect.flip( + ); + const inactive = yield* Effect.flip( resolverFor({ kind: 'system', principalStatus: 'disabled', tenantStatus: 'active' }).resolve({ principalId, registration, runReference: 'run-42', tenantId, }), - ), - ); + ); - assert.equal(invalid._tag, 'SystemPrincipalContextInvalidError'); - assert.equal(wrongKind._tag, 'SystemPrincipalContextDeniedError'); - assert.equal(inactive._tag, 'SystemPrincipalContextDeniedError'); - assert.throws(() => registerSystemWorkload({ jobKey: 'unsafe:key' }), TypeError); -}); + expect(Predicate.isTagged(invalid, 'SystemPrincipalContextInvalidError')).toBe(true); + expect(Predicate.isTagged(wrongKind, 'SystemPrincipalContextDeniedError')).toBe(true); + expect(Predicate.isTagged(inactive, 'SystemPrincipalContextDeniedError')).toBe(true); + expect(() => registerSystemWorkload({ jobKey: 'unsafe:key' })).toThrow(TypeError); + }), +); -void test('permits service principals only when the trusted registration opts in', async () => { - const denied = await runEffectTestPromise( - Effect.flip( +it.effect('permits service principals only when the trusted registration opts in', () => + Effect.gen(function* testScenario3() { + const denied = yield* Effect.flip( resolverFor({ kind: 'service', principalStatus: 'active', tenantStatus: 'active' }).resolve({ principalId, registration: registerSystemWorkload({ jobKey: 'service-job' }), runReference: 'run-1', tenantId, }), - ), - ); - const allowed = await runEffectTestPromise( - resolverFor({ kind: 'service', principalStatus: 'active', tenantStatus: 'active' }).resolve({ + ); + const allowed = yield* resolverFor({ + kind: 'service', + principalStatus: 'active', + tenantStatus: 'active', + }).resolve({ principalId, registration: registerSystemWorkload({ allowServicePrincipal: true, jobKey: 'service-job' }), runReference: 'run-1', tenantId, - }), - ); + }); - assert.equal(denied._tag, 'SystemPrincipalContextDeniedError'); - assert.equal(allowed.authMethod, 'system'); -}); + expect(Predicate.isTagged(denied, 'SystemPrincipalContextDeniedError')).toBe(true); + expect(allowed.authMethod).toBe('system'); + }), +); -void test('enforces mode-specific trusted context cross-field invariants', () => { +it('enforces mode-specific trusted context cross-field invariants', () => { const binding = '30000000-0000-4000-8000-000000000001'; const original = '40000000-0000-4000-8000-000000000001'; const valid = [ @@ -138,17 +139,17 @@ void test('enforces mode-specific trusted context cross-field invariants', () => }, ]; for (const context of valid) { - assert.doesNotThrow(() => Schema.decodeUnknownSync(TrustedPrincipalContextSchema)(context)); + expect(() => Schema.decodeUnknownSync(TrustedPrincipalContextSchema)(context)).not.toThrow(); } - assert.throws(() => + expect(() => Schema.decodeUnknownSync(TrustedPrincipalContextSchema)({ authContextRef: 'better-auth-api-key:key-id', authMethod: 'api_key', principalId, tenantId, }), - ); - assert.throws(() => + ).toThrow(); + expect(() => Schema.decodeUnknownSync(TrustedPrincipalContextSchema)({ authBindingId: binding, authContextRef: 'better-auth-session:nested', @@ -157,5 +158,5 @@ void test('enforces mode-specific trusted context cross-field invariants', () => principalId, tenantId, }), - ); + ).toThrow(); }); diff --git a/app/packages/core-runtime/tests/unit/tenant-module-state.test.ts b/app/packages/core-runtime/tests/unit/tenant-module-state.test.ts index 9cc4ba77c..6a0be7051 100644 --- a/app/packages/core-runtime/tests/unit/tenant-module-state.test.ts +++ b/app/packages/core-runtime/tests/unit/tenant-module-state.test.ts @@ -1,10 +1,8 @@ import { makeInstalledCatalogFixture as catalog } from '../support/installed-catalog.ts'; import { makeModuleContractFixture } from '../../src/testing/module-contract.ts'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import test from 'node:test'; -import { Effect, Schema } from 'effect'; +// @effect-diagnostics preferSchemaOverJson:off -- Verifies native JSON serialization of errors and schema AST metadata; expires: 2026-12-31. +import { expect, it } from 'effect-rstest'; +import { Effect, Schema, Predicate } from 'effect'; import { changeTenantModuleStateAction } from '../../src/modules/actions/change-tenant-module-state.action.ts'; import type { OntosModuleDeploymentContract } from '../../src/index.ts'; import { @@ -39,51 +37,46 @@ const contract = ( supportedStates, }); -void test('uses one canonical tenant module state schema', async () => { - const decodedStates = await Promise.all( - TENANT_MODULE_STATES.map( - async (state) => - await runEffectTestPromise(Schema.decodeUnknownEffect(TenantModuleStateSchema)(state)), - ), - ); - assert.deepEqual(decodedStates, TENANT_MODULE_STATES); +it.effect('uses one canonical tenant module state schema', () => + Effect.gen(function* testScenario1() { + const decodedStates = yield* Effect.forEach((state: (typeof TENANT_MODULE_STATES)[number]) => + Schema.decodeUnknownEffect(TenantModuleStateSchema)(state), + )(TENANT_MODULE_STATES); + expect(decodedStates).toEqual(TENANT_MODULE_STATES); - const failure = await runEffectTestPromise( - Effect.flip(Schema.decodeUnknownEffect(TenantModuleStateSchema)('enabled')), - ); - assert.equal(failure._tag, 'SchemaError'); -}); + const failure = yield* Effect.flip( + Schema.decodeUnknownEffect(TenantModuleStateSchema)('enabled'), + ); + expect(Predicate.isTagged(failure, 'SchemaError')).toBe(true); + }), +); -void test('maps only trusted supported authentication methods to history sources', async () => { - assert.equal(await runEffectTestPromise(resolveTenantModuleStateChangeSource('session')), 'user'); - assert.equal( - await runEffectTestPromise(resolveTenantModuleStateChangeSource('support_impersonation')), - 'support', - ); - assert.equal( - await runEffectTestPromise(resolveTenantModuleStateChangeSource('system')), - 'system', - ); +it.effect('maps only trusted supported authentication methods to history sources', () => + Effect.gen(function* testScenario2() { + expect(yield* resolveTenantModuleStateChangeSource('session')).toBe('user'); + expect(yield* resolveTenantModuleStateChangeSource('support_impersonation')).toBe('support'); + expect(yield* resolveTenantModuleStateChangeSource('system')).toBe('system'); - const unsupported = await runEffectTestPromise( - Effect.flip(resolveTenantModuleStateChangeSource('api_key')), - ); - assert.equal(unsupported._tag, 'TenantModuleStateUnsupportedChangeSourceError'); - assert.equal(unsupported.code, 'tenant_module_state_change_source_unsupported'); -}); + const unsupported = yield* Effect.flip(resolveTenantModuleStateChangeSource('api_key')); + expect(Predicate.isTagged(unsupported, 'TenantModuleStateUnsupportedChangeSourceError')).toBe( + true, + ); + expect(unsupported.code).toBe('tenant_module_state_change_source_unsupported'); + }), +); -void test('rejects a no-op transition without changing first-state semantics', async () => { - await runEffectTestPromise(rejectUnchangedTenantModuleState(null, 'active')); - await runEffectTestPromise(rejectUnchangedTenantModuleState('inactive', 'active')); +it.effect('rejects a no-op transition without changing first-state semantics', () => + Effect.gen(function* testScenario3() { + yield* rejectUnchangedTenantModuleState(null, 'active'); + yield* rejectUnchangedTenantModuleState('inactive', 'active'); - const unchanged = await runEffectTestPromise( - Effect.flip(rejectUnchangedTenantModuleState('active', 'active')), - ); - assert.equal(unchanged._tag, 'TenantModuleStateUnchangedError'); - assert.equal(unchanged.code, 'tenant_module_state_unchanged'); -}); + const unchanged = yield* Effect.flip(rejectUnchangedTenantModuleState('active', 'active')); + expect(Predicate.isTagged(unchanged, 'TenantModuleStateUnchangedError')).toBe(true); + expect(unchanged.code).toBe('tenant_module_state_unchanged'); + }), +); -void test('keeps Core module-state errors stable and sanitized', () => { +it('keeps Core module-state errors stable and sanitized', () => { const errors = [ new TenantModuleStateConcurrentChangeError({ code: 'tenant_module_state_changed_concurrently', @@ -125,73 +118,70 @@ void test('keeps Core module-state errors stable and sanitized', () => { for (const error of errors) { const serialized = JSON.stringify(error); - assert.doesNotMatch(serialized, /postgres|select |insert |tenant-[0-9]|principal-[0-9]/iu); + expect(serialized).not.toMatch(/postgres|select |insert |tenant-[0-9]|principal-[0-9]/iu); } }); -void test('validates only installed membership and the target module supported states', async () => { - const other = contract('documents.center', ['inactive', 'active']); - const target = contract('property.registry', ['inactive', 'active', 'read_only']); - const installed = catalog(other, target); +it.effect('validates only installed membership and the target module supported states', () => + Effect.gen(function* testScenario4() { + const other = contract('documents.center', ['inactive', 'active']); + const target = contract('property.registry', ['inactive', 'active', 'read_only']); + const installed = catalog(other, target); - const unknown = await runEffectTestPromise( - Effect.flip(validateTenantModuleStateTransition(installed, 'unknown.module', 'active')), - ); - assert.equal(unknown._tag, 'TenantModuleStateUnknownModuleError'); - const unsupported = await runEffectTestPromise( - Effect.flip(validateTenantModuleStateTransition(installed, 'property.registry', 'archived')), - ); - assert.equal(unsupported._tag, 'TenantModuleStateUnsupportedStateError'); - await runEffectTestPromise( - validateTenantModuleStateTransition(installed, 'property.registry', 'active'), - ); - await runEffectTestPromise( - validateTenantModuleStateTransition(installed, 'stale.module', 'inactive'), - ); -}); + const unknown = yield* Effect.flip( + validateTenantModuleStateTransition(installed, 'unknown.module', 'active'), + ); + expect(Predicate.isTagged(unknown, 'TenantModuleStateUnknownModuleError')).toBe(true); + const unsupported = yield* Effect.flip( + validateTenantModuleStateTransition(installed, 'property.registry', 'archived'), + ); + expect(Predicate.isTagged(unsupported, 'TenantModuleStateUnsupportedStateError')).toBe(true); + yield* validateTenantModuleStateTransition(installed, 'property.registry', 'active'); + yield* validateTenantModuleStateTransition(installed, 'stale.module', 'inactive'); + }), +); -void test('declares the generated Core Action contract and bounded business payload', async () => { - const { descriptor } = changeTenantModuleStateAction; - assert.equal(descriptor.actionKey, 'core.modules.change-tenant-module-state'); - assert.equal(descriptor.owningModuleKey, 'core.modules'); - assert.equal(descriptor.auditProfile, 'sensitive'); - assert.equal(descriptor.idempotency, 'required'); - assert.deepEqual(descriptor.policies, []); - assert.equal(Object.isFrozen(descriptor), true); - assert.doesNotMatch(JSON.stringify(descriptor.domainErrorSchema.ast), /dependency/iu); +it.effect('declares the generated Core Action contract and bounded business payload', () => + Effect.gen(function* testScenario5() { + const { descriptor } = changeTenantModuleStateAction; + expect(descriptor.actionKey).toBe('core.modules.change-tenant-module-state'); + expect(descriptor.owningModuleKey).toBe('core.modules'); + expect(descriptor.auditProfile).toBe('sensitive'); + expect(descriptor.idempotency).toBe('required'); + expect(descriptor.policies).toEqual([]); + expect(Object.isFrozen(descriptor)).toBe(true); + expect(JSON.stringify(descriptor.domainErrorSchema.ast)).not.toMatch(/dependency/iu); - assert.deepEqual( - await runEffectTestPromise( - Schema.decodeUnknownEffect(descriptor.payloadSchema)({ + expect( + yield* Schema.decodeUnknownEffect(descriptor.payloadSchema)({ expectedState: 'inactive', moduleKey: 'testing.module', newState: 'active', reason: 'Tenant administrator enabled the module', }), - ), - { + ).toEqual({ expectedState: 'inactive', moduleKey: 'testing.module', newState: 'active', reason: 'Tenant administrator enabled the module', - }, - ); - await assert.rejects( - runEffectTestPromise( - Schema.decodeUnknownEffect(descriptor.payloadSchema)({ - moduleKey: 'testing.module', - newState: 'active', - reason: 'x'.repeat(501), - }), - ), - ); - await assert.rejects( - runEffectTestPromise( - Schema.decodeUnknownEffect(descriptor.payloadSchema)({ - moduleKey: 'testing.module', - newState: 'enabled', - tenantId: 'browser-supplied', - }), - ), - ); -}); + }); + expect( + yield* Effect.flip( + Schema.decodeUnknownEffect(descriptor.payloadSchema)({ + moduleKey: 'testing.module', + newState: 'active', + reason: 'x'.repeat(501), + }), + ), + ).toBeDefined(); + expect( + yield* Effect.flip( + Schema.decodeUnknownEffect(descriptor.payloadSchema)({ + moduleKey: 'testing.module', + newState: 'enabled', + tenantId: 'browser-supplied', + }), + ), + ).toBeDefined(); + }), +); diff --git a/app/packages/gateway-principal-verifier/package.json b/app/packages/gateway-principal-verifier/package.json index d2b6b20c1..68e0dab99 100644 --- a/app/packages/gateway-principal-verifier/package.json +++ b/app/packages/gateway-principal-verifier/package.json @@ -8,7 +8,7 @@ "./server": "./src/server.ts" }, "scripts": { - "test:unit": "node --test tests/unit/*.test.ts", + "test:unit": "rstest --project unit", "typecheck": "node ../../scripts/ultramodern-typecheck.mts --project tsconfig.json" }, "dependencies": { @@ -18,6 +18,8 @@ "jose": "6.2.5" }, "devDependencies": { - "@types/node": "20.19.43" + "@types/node": "20.19.43", + "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc", + "@rstest/core": "0.11.11" } } diff --git a/app/packages/gateway-principal-verifier/rstest.config.ts b/app/packages/gateway-principal-verifier/rstest.config.ts new file mode 100644 index 000000000..324494aa4 --- /dev/null +++ b/app/packages/gateway-principal-verifier/rstest.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + projects: [{ include: ['tests/unit/**/*.test.ts'], name: 'unit', testEnvironment: 'node' }], +}); diff --git a/app/packages/gateway-principal-verifier/tests/unit/gateway-principal-verifier.test.ts b/app/packages/gateway-principal-verifier/tests/unit/gateway-principal-verifier.test.ts index 65512d847..e2e8ec197 100644 --- a/app/packages/gateway-principal-verifier/tests/unit/gateway-principal-verifier.test.ts +++ b/app/packages/gateway-principal-verifier/tests/unit/gateway-principal-verifier.test.ts @@ -1,11 +1,8 @@ -// @effect-diagnostics asyncFunction:off -- Node test callbacks and JOSE fixture creation are Promise APIs. remove-when: Effect test adapters support async Node callbacks. -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from 'effect-rstest'; import { GatewayAssertionRedemptionUnavailableError, GatewayAssertionReplayError, } from '@app/core-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; import { Effect, Redacted, Schema } from 'effect'; import { SignJWT, exportJWK, generateKeyPair } from 'jose'; import type { JWK, LocalJWKSet } from 'jose'; @@ -17,7 +14,6 @@ import { GatewayPrincipalVerifierConfiguration, bindGatewayPrincipalVerifier, } from '../../src/server.ts'; -import type { ActionPrincipalError } from '../../src/server.ts'; const currentTimeSeconds = 1_700_000_001; const issuer = 'https://shell.ontos.test'; @@ -29,112 +25,123 @@ const principal = { tenantId: '50000000-0000-4000-8000-000000000001', }; -const makeFixture = async (audience: string, version = 1) => { - const { privateKey, publicKey } = await generateKeyPair('Ed25519'); - const publicJwk = { - ...(await exportJWK(publicKey)), - alg: 'EdDSA', - kid: 'shared-verifier-test', - use: 'sig', - }; - const token = await new SignJWT({ principal, ver: version }) - .setProtectedHeader({ alg: 'EdDSA', kid: 'shared-verifier-test', typ: 'JWT' }) - .setIssuer(issuer) - .setAudience(audience) - .setSubject(principal.principalId) - .setIssuedAt(1_700_000_000) - .setExpirationTime(1_700_000_300) - .setJti('60000000-0000-4000-8000-000000000001') - .sign(privateKey); - return { - environment: { - ONTOS_GATEWAY_ISSUER: issuer, - ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify({ keys: [publicJwk] }), - }, - publicJwk, - token, - }; -}; +const makeFixture = (audience: string, version = 1) => + Effect.gen(function* createFixture() { + const { privateKey, publicKey } = yield* Effect.promise(() => generateKeyPair('Ed25519')); + const publicJwk = { + ...(yield* Effect.promise(() => exportJWK(publicKey))), + alg: 'EdDSA', + kid: 'shared-verifier-test', + use: 'sig', + }; + const token = yield* Effect.promise(() => + new SignJWT({ principal, ver: version }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'shared-verifier-test', typ: 'JWT' }) + .setIssuer(issuer) + .setAudience(audience) + .setSubject(principal.principalId) + .setIssuedAt(1_700_000_000) + .setExpirationTime(1_700_000_300) + .setJti('60000000-0000-4000-8000-000000000001') + .sign(privateKey), + ); + return { + environment: { + ONTOS_GATEWAY_ISSUER: issuer, + ONTOS_GATEWAY_PUBLIC_JWKS: yield* Schema.encodeEffect( + Schema.fromJsonString(Schema.Unknown), + )({ + keys: [publicJwk], + }), + }, + publicJwk, + token, + }; + }); const isConfigurationError = Schema.is(ActionPrincipalConfigurationErrorSchema); const isInvalidError = Schema.is(ActionPrincipalInvalidErrorSchema); const isScopeError = Schema.is(ActionPrincipalScopeErrorSchema); const isUnavailableError = Schema.is(ActionPrincipalUnavailableErrorSchema); const failingKeySet = Object.assign( - async () => { - throw new Error('fixture verifier details must be discarded'); - }, + () => Promise.reject(new Error('fixture verifier details must be discarded')), { jwks: () => ({ keys: [] }) }, ) satisfies LocalJWKSet; -test('an audience-bound verifier accepts only its exact topology app ID', async () => { - const partyFixture = await makeFixture('party-registry'); - const billingFixture = await makeFixture('billing'); - const verifier = bindGatewayPrincipalVerifier('party-registry'); - const verify = async (token: string, environment: typeof partyFixture.environment) => - await runEffectTestPromise( +it.effect('an audience-bound verifier accepts only its exact topology app ID', () => + Effect.gen(function* verifyAudienceBinding() { + const partyFixture = yield* makeFixture('party-registry'); + const billingFixture = yield* makeFixture('billing'); + const verifier = bindGatewayPrincipalVerifier('party-registry'); + const verify = (token: string, environment: typeof partyFixture.environment) => verifier.verify(Redacted.make(`Bearer ${token}`), { currentTimeSeconds: Effect.succeed(currentTimeSeconds), environment, - }), - ); + }); - assert.deepEqual(await verify(partyFixture.token, partyFixture.environment), principal); - await assert.rejects(verify(billingFixture.token, billingFixture.environment), isScopeError); -}); + expect(yield* verify(partyFixture.token, partyFixture.environment)).toEqual(principal); + expect( + isScopeError(yield* Effect.flip(verify(billingFixture.token, billingFixture.environment))), + ).toBe(true); + }), +); -test('Bearer scheme matching is case insensitive without changing the signed token', async () => { - const fixture = await makeFixture('party-registry'); - const verifier = bindGatewayPrincipalVerifier('party-registry'); - - await Promise.all( - ['Bearer', 'bearer', 'BEARER', 'bEaReR'].map(async (scheme) => { - const verified = await runEffectTestPromise( - verifier.verify(Redacted.make(`${scheme} ${fixture.token}`), { - currentTimeSeconds: Effect.succeed(currentTimeSeconds), - environment: fixture.environment, +it.effect('Bearer scheme matching is case insensitive without changing the signed token', () => + Effect.gen(function* verifyBearerCaseVariants() { + const fixture = yield* makeFixture('party-registry'); + const verifier = bindGatewayPrincipalVerifier('party-registry'); + yield* Effect.forEach( + ['Bearer', 'bearer', 'BEARER', 'bEaReR'], + (scheme) => + Effect.gen(function* verifyBearerScheme() { + const verified = yield* verifier.verify(Redacted.make(`${scheme} ${fixture.token}`), { + currentTimeSeconds: Effect.succeed(currentTimeSeconds), + environment: fixture.environment, + }); + expect(verified).toEqual(principal); }), - ); - assert.deepEqual(verified, principal); - }), - ); -}); - -test('case insensitive Bearer matching still rejects malformed authorization headers', async () => { - const fixture = await makeFixture('party-registry'); - const verifier = bindGatewayPrincipalVerifier('party-registry'); + { concurrency: 'unbounded' }, + ); + }), +); - await Promise.all( - [ - ` bearer ${fixture.token}`, - `bearer ${fixture.token}`, - `bearer\t${fixture.token}`, - `bearer ${fixture.token} `, - `bearer ${fixture.token} extra`, - 'bearer ', - `Basic ${fixture.token}`, - ].map( - async (authorization) => - await assert.rejects( - runEffectTestPromise( +it.effect('case insensitive Bearer matching still rejects malformed authorization headers', () => + Effect.gen(function* rejectMalformedBearerHeaders() { + const fixture = yield* makeFixture('party-registry'); + const verifier = bindGatewayPrincipalVerifier('party-registry'); + yield* Effect.forEach( + [ + ` bearer ${fixture.token}`, + `bearer ${fixture.token}`, + `bearer\t${fixture.token}`, + `bearer ${fixture.token} `, + `bearer ${fixture.token} extra`, + 'bearer ', + `Basic ${fixture.token}`, + ], + (authorization) => + Effect.gen(function* rejectMalformedBearerHeader() { + const failure = yield* Effect.flip( verifier.verify(Redacted.make(authorization), { currentTimeSeconds: Effect.succeed(currentTimeSeconds), environment: fixture.environment, }), - ), - isInvalidError, - ), - ), - ); -}); + ); + expect(isInvalidError(failure)).toBe(true); + }), + { concurrency: 'unbounded' }, + ); + }), +); -test('empty and malformed audience bindings fail closed as configuration errors', async () => { - const fixture = await makeFixture('party-registry'); - await Promise.all( - ['', 'Party Registry', 'party/registry'].map( - async (audience) => - await assert.rejects( - runEffectTestPromise( +it.effect('empty and malformed audience bindings fail closed as configuration errors', () => + Effect.gen(function* rejectMalformedBindings() { + const fixture = yield* makeFixture('party-registry'); + yield* Effect.forEach( + ['', 'Party Registry', 'party/registry'], + (audience) => + Effect.gen(function* checkMalformedBinding() { + const failure = yield* Effect.flip( bindGatewayPrincipalVerifier(audience).verify( Redacted.make(`Bearer ${fixture.token}`), { @@ -142,71 +149,68 @@ test('empty and malformed audience bindings fail closed as configuration errors' environment: fixture.environment, }, ), - ), - isConfigurationError, - ), - ), - ); -}); + ); + expect(isConfigurationError(failure)).toBe(true); + }), + { concurrency: 'unbounded' }, + ); + }), +); -test('redemption failures remain sanitized and distinguish replay from unavailability', async () => { - const fixture = await makeFixture('party-registry'); - const verifier = bindGatewayPrincipalVerifier('party-registry'); - const verify = async (redemption: Parameters[1]['redemption']) => - await runEffectTestPromise( +it.effect('redemption failures remain sanitized and distinguish replay from unavailability', () => + Effect.gen(function* verifyRedemptionFailures() { + const fixture = yield* makeFixture('party-registry'); + const verifier = bindGatewayPrincipalVerifier('party-registry'); + const verify = (redemption: Parameters[1]['redemption']) => verifier.verifyAndRedeem(Redacted.make(`Bearer ${fixture.token}`), { currentTimeSeconds: Effect.succeed(currentTimeSeconds), environment: fixture.environment, redemption, + }); + + const replayFailure = yield* Effect.flip( + verify({ + consume: () => + Effect.fail( + new GatewayAssertionReplayError({ reason: 'fixture replay details must be discarded' }), + ), }), ); + expect(isInvalidError(replayFailure)).toBe(true); + expect( + yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(replayFailure), + ).not.toMatch(/fixture|eyJ/u); + const unavailableFailure = yield* Effect.flip( + verify({ + consume: () => + Effect.fail( + new GatewayAssertionRedemptionUnavailableError({ + reason: 'fixture storage details must be discarded', + }), + ), + }), + ); + expect(isUnavailableError(unavailableFailure)).toBe(true); + expect( + yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(unavailableFailure), + ).not.toMatch(/fixture|eyJ/u); + }), +); - await assert.rejects( - verify({ - consume: () => - Effect.fail( - new GatewayAssertionReplayError({ reason: 'fixture replay details must be discarded' }), - ), - }), - (failure: ActionPrincipalError) => { - assert.equal(isInvalidError(failure), true); - assert.doesNotMatch(JSON.stringify(failure), /fixture|eyJ/u); - return true; - }, - ); - await assert.rejects( - verify({ - consume: () => - Effect.fail( - new GatewayAssertionRedemptionUnavailableError({ - reason: 'fixture storage details must be discarded', - }), - ), - }), - (failure: ActionPrincipalError) => { - assert.equal(isUnavailableError(failure), true); - assert.doesNotMatch(JSON.stringify(failure), /fixture|eyJ/u); - return true; - }, - ); -}); - -test('unsupported assertion versions and unexpected verifier failures fail closed', async () => { - const unsupportedVersion = await makeFixture('party-registry', 2); - const verifier = bindGatewayPrincipalVerifier('party-registry'); - await assert.rejects( - runEffectTestPromise( +it.effect('unsupported assertion versions and unexpected verifier failures fail closed', () => + Effect.gen(function* rejectUnsupportedAndUnexpectedFailures() { + const unsupportedVersion = yield* makeFixture('party-registry', 2); + const verifier = bindGatewayPrincipalVerifier('party-registry'); + const versionFailure = yield* Effect.flip( verifier.verify(Redacted.make(`Bearer ${unsupportedVersion.token}`), { currentTimeSeconds: Effect.succeed(currentTimeSeconds), environment: unsupportedVersion.environment, }), - ), - isInvalidError, - ); + ); + expect(isInvalidError(versionFailure)).toBe(true); - const fixture = await makeFixture('party-registry'); - await assert.rejects( - runEffectTestPromise( + const fixture = yield* makeFixture('party-registry'); + const failure = yield* Effect.flip( verifier .verify(Redacted.make(`Bearer ${fixture.token}`), { currentTimeSeconds: Effect.succeed(currentTimeSeconds), @@ -216,31 +220,42 @@ test('unsupported assertion versions and unexpected verifier failures fail close configuration: Effect.succeed({ issuer, keySet: failingKeySet }), }), ), - ), - (failure: ActionPrincipalError) => { - assert.equal(isUnavailableError(failure), true); - assert.doesNotMatch(JSON.stringify(failure), /fixture|eyJ/u); - return true; - }, - ); -}); - -test('malformed Ed25519 public keys fail during configuration acquisition', async () => { - const fixture = await makeFixture('party-registry'); - const verifier = bindGatewayPrincipalVerifier('party-registry'); - const verifyWithKey = async (key: JWK) => - await runEffectTestPromise( - verifier.verify(Redacted.make(`Bearer ${fixture.token}`), { - currentTimeSeconds: Effect.succeed(currentTimeSeconds), - environment: { - ONTOS_GATEWAY_ISSUER: issuer, - ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify({ keys: [key] }), - }, - }), ); + expect(isUnavailableError(failure)).toBe(true); + expect(yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(failure)).not.toMatch( + /fixture|eyJ/u, + ); + }), +); - await Promise.all([ - assert.rejects(verifyWithKey({ ...fixture.publicJwk, key_ops: [] }), isConfigurationError), - assert.rejects(verifyWithKey({ ...fixture.publicJwk, x: '!!!' }), isConfigurationError), - ]); -}); +it.effect('malformed Ed25519 public keys fail during configuration acquisition', () => + Effect.gen(function* rejectMalformedPublicKeys() { + const fixture = yield* makeFixture('party-registry'); + const verifier = bindGatewayPrincipalVerifier('party-registry'); + const verifyWithKey = (key: JWK) => + Effect.gen(function* verifyPublicKey() { + const jwks = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ + keys: [key], + }); + return yield* verifier.verify(Redacted.make(`Bearer ${fixture.token}`), { + currentTimeSeconds: Effect.succeed(currentTimeSeconds), + environment: { + ONTOS_GATEWAY_ISSUER: issuer, + ONTOS_GATEWAY_PUBLIC_JWKS: jwks, + }, + }); + }); + + yield* Effect.forEach( + [ + { ...fixture.publicJwk, key_ops: [] }, + { ...fixture.publicJwk, x: '!!!' }, + ], + (key) => + Effect.gen(function* checkMalformedPublicKey() { + expect(isConfigurationError(yield* Effect.flip(verifyWithKey(key)))).toBe(true); + }), + { concurrency: 'unbounded' }, + ); + }), +); diff --git a/app/packages/shared-contracts/package.json b/app/packages/shared-contracts/package.json index f7430c9dc..020e6bcae 100644 --- a/app/packages/shared-contracts/package.json +++ b/app/packages/shared-contracts/package.json @@ -13,7 +13,7 @@ }, "scripts": { "test:types": "node ../../scripts/ultramodern-typecheck.mts --project tests/tsconfig.json && node ../../scripts/ultramodern-typecheck.mts --project tests/client-runtime-types.tsconfig.json", - "test:unit": "node --test tests/unit/*.test.ts && pnpm test:types", + "test:unit": "rstest --project unit && pnpm test:types", "typecheck": "node ../../scripts/ultramodern-typecheck.mts --project tsconfig.json" }, "dependencies": { @@ -22,6 +22,8 @@ "effect": "4.0.0-beta.107" }, "devDependencies": { - "@types/node": "20.19.43" + "@types/node": "20.19.43", + "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc", + "@rstest/core": "0.11.11" } } diff --git a/app/packages/shared-contracts/rstest.config.ts b/app/packages/shared-contracts/rstest.config.ts new file mode 100644 index 000000000..324494aa4 --- /dev/null +++ b/app/packages/shared-contracts/rstest.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + projects: [{ include: ['tests/unit/**/*.test.ts'], name: 'unit', testEnvironment: 'node' }], +}); diff --git a/app/packages/shared-contracts/src/operation-gateway.type-test.ts b/app/packages/shared-contracts/src/operation-gateway.type-test.ts index b29cf5b37..c75446133 100644 --- a/app/packages/shared-contracts/src/operation-gateway.type-test.ts +++ b/app/packages/shared-contracts/src/operation-gateway.type-test.ts @@ -7,7 +7,6 @@ type Equal = (() => Value extends Left ? 1 : 2) extends () => Value extends Right ? 1 : 2 ? true : false; -type Expect = Value; type EffectChannels = Value extends Effect.Effect ? readonly [Success, Failure, Requirements] @@ -28,13 +27,16 @@ const successfulInvocation = makeOperationGateway(inventoryAudience, () => type InventoryIssuer = OperationGatewayIssuer; type InventoryAudience = Parameters[0]['audience']; -export type OperationGatewayAudienceTypeTest = Expect>; -export type OperationGatewayFailureChannelsTypeTest = Expect< - Equal< - EffectChannels, - readonly [never, typeof acquisitionFailure | typeof attemptFailure, never] - > ->; -export type OperationGatewaySuccessChannelsTypeTest = Expect< - Equal, readonly ['completed', never, never]> ->; +const audienceIsExact: Equal = true; +const failureChannelsAreExact: Equal< + EffectChannels, + readonly [never, typeof acquisitionFailure | typeof attemptFailure, never] +> = true; +const successChannelsAreExact: Equal< + EffectChannels, + readonly ['completed', never, never] +> = true; + +void audienceIsExact; +void failureChannelsAreExact; +void successChannelsAreExact; diff --git a/app/packages/shared-contracts/tests/unit/client-runtime.test.ts b/app/packages/shared-contracts/tests/unit/client-runtime.test.ts index cea5c9ad8..72f6c0c0d 100644 --- a/app/packages/shared-contracts/tests/unit/client-runtime.test.ts +++ b/app/packages/shared-contracts/tests/unit/client-runtime.test.ts @@ -1,6 +1,4 @@ -// @effect-diagnostics asyncFunction:off -- Node's test runner owns this compatibility edge; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { makeEffectBffClient } from '@app/shared-contracts/client-runtime'; import { @@ -11,7 +9,7 @@ import { HttpApiSchema, Schema, } from '@modern-js/plugin-bff/effect-client'; -import { Result } from 'effect'; +import { Predicate, Struct } from 'effect'; import { FetchHttpClient } from 'effect/unstable/http'; const RepresentativeConflictSchema = Schema.TaggedStruct('RepresentativeConflict', { @@ -33,10 +31,9 @@ const RepresentativeApi = HttpApi.make('RepresentativeApi').add( ), ); -const controlledTransportFailureFetch: typeof fetch = async () => { - throw new TypeError('controlled transport failure'); -}; -const invalidResponseFetch: typeof fetch = async () => Response.json({ value: 358 }); +const controlledTransportFailureFetch: typeof fetch = () => + Promise.reject(new TypeError('controlled transport failure')); +const invalidResponseFetch: typeof fetch = () => Promise.resolve(Response.json({ value: 358 })); const representativeClientEffect = makeEffectBffClient({ api: RepresentativeApi, @@ -63,98 +60,97 @@ void preserveRepresentativeReadType; void preserveRepresentativeSuccessType; void preserveRepresentativeErrorType; -test('constructs fresh typed clients lazily as Effect values', async () => { - const clientEffect = makeEffectBffClient({ - api: RepresentativeApi, - defaultApiPrefix: 'https://owner.example/representative-api', - }); - assert.equal(Effect.isEffect(clientEffect), true); - - const first = await Effect.runPromise(clientEffect); - const second = await Effect.runPromise(clientEffect); +it.effect('constructs fresh typed clients lazily as Effect values', () => + Effect.gen(function* testScenario1() { + const clientEffect = makeEffectBffClient({ + api: RepresentativeApi, + defaultApiPrefix: 'https://owner.example/representative-api', + }); + expect(Effect.isEffect(clientEffect)).toBe(true); - assert.notEqual(first, second); - assert.equal(Effect.isEffect(first.representative.read({})), true); -}); + const first = yield* clientEffect; + const second = yield* clientEffect; -test('uses the owner-supplied API prefix by default', async () => { - const requests: Request[] = []; - const fakeFetch: typeof fetch = async (input, init) => { - requests.push(new Request(input, init)); - return Response.json({ value: 'default-prefix' }); - }; - const location = Object.getOwnPropertyDescriptor(globalThis, 'location'); - Object.defineProperty(globalThis, 'location', { - configurable: true, - value: { origin: 'https://shell.example', pathname: '/en' }, - }); + expect(first).not.toBe(second); + expect(Effect.isEffect(first.representative.read({}))).toBe(true); + }), +); - try { - const result = await Effect.runPromise( - makeEffectBffClient({ - api: RepresentativeApi, - defaultApiPrefix: '/representative-api', - }).pipe( - Effect.flatMap((client) => client.representative.read({})), - Effect.provideService(FetchHttpClient.Fetch, fakeFetch), - ), +it.effect('uses the owner-supplied API prefix by default', () => + Effect.gen(function* testScenario2() { + const requests: Request[] = []; + const fakeFetch: typeof fetch = (input, init) => { + requests.push(new Request(input, init)); + return Promise.resolve(Response.json({ value: 'default-prefix' })); + }; + const location = Object.getOwnPropertyDescriptor(globalThis, 'location'); + yield* Effect.addFinalizer(() => + Effect.sync(() => { + if (location === undefined) { + Reflect.deleteProperty(globalThis, 'location'); + } else { + Object.defineProperty(globalThis, 'location', location); + } + }), ); + Object.defineProperty(globalThis, 'location', { + configurable: true, + value: { origin: 'https://shell.example', pathname: '/en' }, + }); - assert.deepEqual(result, { value: 'default-prefix' }); - assert.deepEqual( - requests.map(({ url }) => url), - ['https://shell.example/representative-api/read'], + const result = yield* makeEffectBffClient({ + api: RepresentativeApi, + defaultApiPrefix: '/representative-api', + }).pipe( + Effect.flatMap((client) => client.representative.read({})), + Effect.provideService(FetchHttpClient.Fetch, fakeFetch), ); - } finally { - if (location === undefined) { - Reflect.deleteProperty(globalThis, 'location'); - } else { - Object.defineProperty(globalThis, 'location', location); - } - } -}); -test('uses an explicit caller base URL instead of the owner prefix', async () => { - const requests: Request[] = []; - const fakeFetch: typeof fetch = async (input, init) => { - requests.push(new Request(input, init)); - return Response.json({ value: 'override' }); - }; + expect(result).toEqual({ value: 'default-prefix' }); + expect(requests.map(({ url }) => url)).toEqual([ + 'https://shell.example/representative-api/read', + ]); + }), +); + +it.effect('uses an explicit caller base URL instead of the owner prefix', () => + Effect.gen(function* testScenario3() { + const requests: Request[] = []; + const fakeFetch: typeof fetch = (input, init) => { + requests.push(new Request(input, init)); + return Promise.resolve(Response.json({ value: 'override' })); + }; - const result = await Effect.runPromise( - makeEffectBffClient({ + const result = yield* makeEffectBffClient({ api: RepresentativeApi, baseUrl: new URL('https://owner.example/custom-api'), defaultApiPrefix: '/representative-api', }).pipe( Effect.flatMap((client) => client.representative.read({})), Effect.provideService(FetchHttpClient.Fetch, fakeFetch), - ), - ); + ); - assert.deepEqual(result, { value: 'override' }); - assert.deepEqual( - requests.map(({ url }) => url), - ['https://owner.example/custom-api/read'], - ); -}); + expect(result).toEqual({ value: 'override' }); + expect(requests.map(({ url }) => url)).toEqual(['https://owner.example/custom-api/read']); + }), +); -test('propagates supported request context and resolved transport headers', async () => { - const requests: Request[] = []; - const fakeFetch: typeof fetch = async (input, init) => { - requests.push(new Request(input, init)); - return Response.json({ value: 'context' }); - }; - const operationContext = { - method: 'GET', - operationId: 'RepresentativeApi:/read', - routePath: '/read', - source: 'generated-client' as const, - }; - const traceparent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'; +it.effect('propagates supported request context and resolved transport headers', () => + Effect.gen(function* testScenario4() { + const requests: Request[] = []; + const fakeFetch: typeof fetch = (input, init) => { + requests.push(new Request(input, init)); + return Promise.resolve(Response.json({ value: 'context' })); + }; + const operationContext = { + method: 'GET', + operationId: 'RepresentativeApi:/read', + routePath: '/read', + source: 'generated-client' as const, + }; + const traceparent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'; - await Effect.runPromise( - makeEffectBffClient({ + yield* makeEffectBffClient({ api: RepresentativeApi, baseUrl: 'https://owner.example/representative-api', defaultApiPrefix: '/representative-api', @@ -170,31 +166,35 @@ test('propagates supported request context and resolved transport headers', asyn }).pipe( Effect.flatMap((client) => client.representative.read({})), Effect.provideService(FetchHttpClient.Fetch, fakeFetch), - ), - ); + ); - const [request] = requests; - assert.ok(request); - assert.equal(request.headers.get('accept-language'), 'cs'); - assert.equal(request.headers.get('traceparent'), traceparent); - assert.equal(request.headers.get('x-operation-id'), operationContext.operationId); - assert.deepEqual( - JSON.parse(request.headers.get('x-modernjs-bff-operation-context') ?? ''), - operationContext, - ); - assert.equal(request.headers.get('authorization'), 'Bearer owner-resolved-assertion'); - assert.equal(request.headers.get('x-correlation-id'), 'correlation-358'); -}); + const [request] = requests; + expect(request).toBeDefined(); + if (request === undefined) { + throw new Error('Expected captured request'); + } + expect(request.headers.get('accept-language')).toBe('cs'); + expect(request.headers.get('traceparent')).toBe(traceparent); + expect(request.headers.get('x-operation-id')).toBe(operationContext.operationId); + expect( + yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))( + request.headers.get('x-modernjs-bff-operation-context') ?? '', + ), + ).toEqual(operationContext); + expect(request.headers.get('authorization')).toBe('Bearer owner-resolved-assertion'); + expect(request.headers.get('x-correlation-id')).toBe('correlation-358'); + }), +); -test('omits absent optional request context and transport header values', async () => { - const requests: Request[] = []; - const fakeFetch: typeof fetch = async (input, init) => { - requests.push(new Request(input, init)); - return Response.json({ value: 'omitted' }); - }; +it.effect('omits absent optional request context and transport header values', () => + Effect.gen(function* testScenario5() { + const requests: Request[] = []; + const fakeFetch: typeof fetch = (input, init) => { + requests.push(new Request(input, init)); + return Promise.resolve(Response.json({ value: 'omitted' })); + }; - await Effect.runPromise( - makeEffectBffClient({ + yield* makeEffectBffClient({ api: RepresentativeApi, baseUrl: 'https://owner.example/representative-api', defaultApiPrefix: '/representative-api', @@ -203,67 +203,70 @@ test('omits absent optional request context and transport header values', async }).pipe( Effect.flatMap((client) => client.representative.read({})), Effect.provideService(FetchHttpClient.Fetch, fakeFetch), - ), - ); + ); - const [request] = requests; - assert.ok(request); - for (const header of [ - 'accept-language', - 'traceparent', - 'x-modernjs-bff-operation-context', - 'x-operation-id', - ]) { - assert.equal(request.headers.has(header), false, header); - } -}); + const [request] = requests; + expect(request).toBeDefined(); + if (request === undefined) { + throw new Error('Expected captured request'); + } + for (const header of [ + 'accept-language', + 'traceparent', + 'x-modernjs-bff-operation-context', + 'x-operation-id', + ]) { + expect(request.headers.has(header), header).toBe(false); + } + }), +); -test('keeps declared backend failures in the typed Effect error channel', async () => { - const problem = { - _tag: 'RepresentativeConflict' as const, - detail: 'The representative value changed.', - status: 409 as const, - title: 'Representative conflict', - type: 'urn:ontos:test:representative-conflict', - }; - const fakeFetch: typeof fetch = async () => - Response.json(problem, { - headers: { 'content-type': 'application/problem+json' }, - status: 409, - }); +it.effect('keeps declared backend failures in the typed Effect error channel', () => + Effect.gen(function* testScenario6() { + const problem = { + _tag: 'RepresentativeConflict' as const, + detail: 'The representative value changed.', + status: 409 as const, + title: 'Representative conflict', + type: 'urn:ontos:test:representative-conflict', + }; + const fakeFetch: typeof fetch = () => + Promise.resolve( + Response.json(problem, { + headers: { 'content-type': 'application/problem+json' }, + status: 409, + }), + ); - const outcome = await Effect.runPromise( - makeEffectBffClient({ + const outcome = yield* makeEffectBffClient({ api: RepresentativeApi, defaultApiPrefix: 'https://owner.example/representative-api', }).pipe( Effect.flatMap((client) => client.representative.read({})), - Effect.result, + Effect.flip, Effect.provideService(FetchHttpClient.Fetch, fakeFetch), - ), - ); + ); - assert.ok(Result.isFailure(outcome)); - assert.deepEqual(outcome.failure, problem); -}); + expect(Schema.is(RepresentativeConflictSchema)(outcome)).toBe(true); + expect(Struct.omit(outcome, ['_tag'])).toEqual(Struct.omit(problem, ['_tag'])); + }), +); for (const [failureKind, transport, expectedTag] of [ ['transport', controlledTransportFailureFetch, 'HttpClientError'], ['response decoding', invalidResponseFetch, 'SchemaError'], ] as const) { - test(`keeps ${failureKind} failures in the typed Effect error channel`, async () => { - const outcome = await Effect.runPromise( - makeEffectBffClient({ + it.effect(`keeps ${failureKind} failures in the typed Effect error channel`, () => + Effect.gen(function* typedClientFailure() { + const outcome = yield* makeEffectBffClient({ api: RepresentativeApi, defaultApiPrefix: 'https://owner.example/representative-api', }).pipe( Effect.flatMap((client) => client.representative.read({})), - Effect.result, + Effect.flip, Effect.provideService(FetchHttpClient.Fetch, transport), - ), - ); - - assert.ok(Result.isFailure(outcome)); - assert.equal(outcome.failure._tag, expectedTag); - }); + ); + expect(Predicate.isTagged(outcome, expectedTag)).toBe(true); + }), + ); } diff --git a/app/packages/shared-contracts/tests/unit/effect-bff-runtime.test.ts b/app/packages/shared-contracts/tests/unit/effect-bff-runtime.test.ts index 9f1fdd27f..cff53fb25 100644 --- a/app/packages/shared-contracts/tests/unit/effect-bff-runtime.test.ts +++ b/app/packages/shared-contracts/tests/unit/effect-bff-runtime.test.ts @@ -1,6 +1,4 @@ -// @effect-diagnostics asyncFunction:off -- Node test callbacks exercise the generated Web handler boundary; expires: 2027-03-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Effect, @@ -61,55 +59,67 @@ const inferredRuntime: EffectBffDefinition & EffectBffRuntime { - const server = makeRuntime('substitute runtime').createHandler(); - try { - const response = await server.handler(new Request('http://localhost/greet')); - assert.equal(response.status, 200); - assert.deepEqual(await response.json(), { greeting: 'substitute runtime' }); - } finally { - await server.dispose(); - } -}); +it.live('assembles a concrete API with caller-provided handler dependencies', () => + Effect.gen(function* assembleRuntimeEffect() { + const server = yield* Effect.acquireRelease( + Effect.sync(() => makeRuntime('substitute runtime').createHandler()), + (runtimeServer) => Effect.promise(() => runtimeServer.dispose()), + ); + const response = yield* Effect.promise(() => + server.handler(new Request('http://localhost/greet')), + ); + expect(response.status).toBe(200); + expect(yield* Effect.promise(() => response.json())).toEqual({ + greeting: 'substitute runtime', + }); + }), +); -void test('keeps an optional caller-owned CORS layer in the assembled runtime', async () => { - const server = makeCorsRuntime('with cors').createHandler(); - try { - const response = await server.handler( - new Request('http://localhost/greet', { - headers: { - 'access-control-request-method': 'GET', - origin: 'https://shell.example.test', - }, - method: 'OPTIONS', - }), +it.live('keeps an optional caller-owned CORS layer in the assembled runtime', () => + Effect.gen(function* corsRuntimeEffect() { + const server = yield* Effect.acquireRelease( + Effect.sync(() => makeCorsRuntime('with cors').createHandler()), + (runtimeServer) => Effect.promise(() => runtimeServer.dispose()), ); - assert.equal(response.status, 204); - assert.equal(response.headers.get('access-control-allow-origin'), 'https://shell.example.test'); - assert.equal(response.headers.get('access-control-max-age'), '600'); - } finally { - await server.dispose(); - } -}); + const response = yield* Effect.promise(() => + server.handler( + new Request('http://localhost/greet', { + headers: { + 'access-control-request-method': 'GET', + origin: 'https://shell.example.test', + }, + method: 'OPTIONS', + }), + ), + ); + expect(response.status).toBe(204); + expect(response.headers.get('access-control-allow-origin')).toBe('https://shell.example.test'); + expect(response.headers.get('access-control-max-age')).toBe('600'); + }), +); -void test('keeps strict runtime defect handling at the generated HTTP boundary', async () => { - const server = makeRuntime('unused').createHandler(); - try { - const response = await server.handler(new Request('http://localhost/fail')); - assert.equal(response.status, 500); - } finally { - await server.dispose(); - } -}); +it.live('keeps strict runtime defect handling at the generated HTTP boundary', () => + Effect.gen(function* runtimeDefectEffect() { + const server = yield* Effect.acquireRelease( + Effect.sync(() => makeRuntime('unused').createHandler()), + (runtimeServer) => Effect.promise(() => runtimeServer.dispose()), + ); + const response = yield* Effect.promise(() => + server.handler(new Request('http://localhost/fail')), + ); + expect(response.status).toBe(500); + }), +); -void test('preserves caller-owned Layer startup defects', async () => { - const server = failingStartupRuntime.createHandler(); - try { - await assert.rejects( - server.handler(new Request('http://localhost/greet')), - /fixture layer startup defect/u, +it.live('preserves caller-owned Layer startup defects', () => + Effect.gen(function* startupDefectEffect() { + const server = yield* Effect.acquireRelease( + Effect.sync(() => failingStartupRuntime.createHandler()), + (runtimeServer) => Effect.promise(() => runtimeServer.dispose()), ); - } finally { - await server.dispose(); - } -}); + const error = yield* Effect.tryPromise(() => + server.handler(new Request('http://localhost/greet')), + ).pipe(Effect.flip); + expect(String(error.cause)).toMatch(/fixture layer startup defect/u); + }), +); diff --git a/app/packages/shared-contracts/tests/unit/gateway-context.test.ts b/app/packages/shared-contracts/tests/unit/gateway-context.test.ts index 7411d6915..9e46b9072 100644 --- a/app/packages/shared-contracts/tests/unit/gateway-context.test.ts +++ b/app/packages/shared-contracts/tests/unit/gateway-context.test.ts @@ -1,9 +1,6 @@ -// @effect-diagnostics asyncFunction:off -- Node test callbacks bridge the Effect contracts under test; expires: 2027-03-31. -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { TrustedPrincipalContextSchema } from '@app/core-runtime/actions/principal-context'; -import { Schema, SchemaAST } from 'effect'; +import { Effect, Schema, SchemaAST, Struct } from 'effect'; import { ApiKeyGatewayHeadersSchema, GatewayContextApiGroup, @@ -24,11 +21,13 @@ const endpointStatuses = ( .map((schema) => schema.ast.annotations?.['httpApiStatus']) .toSorted((left, right) => Number(left) - Number(right)); -const problemTag = (schema: Schema.Top): SchemaAST.LiteralValue => { - assert.ok(SchemaAST.isObjects(schema.ast)); - const tag = schema.ast.propertySignatures.find(({ name }) => name === '_tag')?.type; - assert.ok(tag !== undefined && SchemaAST.isLiteral(tag)); - return tag.literal; +const problemTag = (schema: Schema.Top) => { + expect(SchemaAST.isObjects(schema.ast)).toBe(true); + const tag = SchemaAST.isObjects(schema.ast) + ? schema.ast.propertySignatures.find(({ name }) => name === '_tag')?.type + : undefined; + expect(tag !== undefined && SchemaAST.isLiteral(tag)).toBe(true); + return tag !== undefined && SchemaAST.isLiteral(tag) ? tag.literal : undefined; }; const principal = { @@ -50,93 +49,93 @@ const claims = { ver: 1 as const, }; -void test('decodes the exact versioned public assertion contract', async () => { - assert.deepEqual(await runEffectTestPromise(decodeGatewayContextClaims(claims)), claims); - assert.deepEqual( - Schema.decodeUnknownSync(GatewayContextProtectedHeaderSchema)({ - alg: 'EdDSA', - kid: 'current-2026-08', - typ: 'JWT', - }), - { +it.effect('decodes the exact versioned public assertion contract', () => + Effect.gen(function* testScenario1() { + expect(yield* decodeGatewayContextClaims(claims)).toEqual(claims); + expect( + yield* Schema.decodeUnknownEffect(GatewayContextProtectedHeaderSchema)({ + alg: 'EdDSA', + kid: 'current-2026-08', + typ: 'JWT', + }), + ).toEqual({ alg: 'EdDSA', kid: 'current-2026-08', typ: 'JWT', - }, - ); - assert.deepEqual( - Schema.decodeUnknownSync(GatewayContextRequestSchema)({ - audience: 'inventory-stock', - }), - { audience: 'inventory-stock' }, - ); - assert.deepEqual( - Schema.decodeUnknownSync(GatewayContextResponseSchema)({ - expiresAt: claims.exp, - token: 'header.payload.signature', - }), - { expiresAt: claims.exp, token: 'header.payload.signature' }, - ); -}); - -void test('rejects malformed audiences, invalid ordering, and subject mismatch', async () => { - await assert.rejects( - runEffectTestPromise(Schema.decodeUnknownEffect(GatewayContextRequestSchema)({ audience: '' })), - ); - await assert.rejects( - runEffectTestPromise(decodeGatewayContextClaims({ ...claims, exp: claims.iat })), - ); - await assert.rejects( - runEffectTestPromise(decodeGatewayContextClaims({ ...claims, exp: claims.iat + 301 })), - ); - await assert.rejects( - runEffectTestPromise( - decodeGatewayContextClaims({ - ...claims, - sub: '60000000-0000-4000-8000-000000000001', + }); + expect( + yield* Schema.decodeUnknownEffect(GatewayContextRequestSchema)({ + audience: 'inventory-stock', }), - ), - ); -}); + ).toEqual({ audience: 'inventory-stock' }); + expect( + yield* Schema.decodeUnknownEffect(GatewayContextResponseSchema)({ + expiresAt: claims.exp, + token: 'header.payload.signature', + }), + ).toEqual({ expiresAt: claims.exp, token: 'header.payload.signature' }); + }), +); -void test('rejects credential, display, authorization, Action, and business claim expansion', async () => { - const forbiddenFields = [ - 'email', - 'displayName', - 'credential', - 'rawApiKey', - 'providerKeyId', - 'keyId', - 'cookie', - 'sessionToken', - 'actionKey', - 'permission', - 'policyDecision', - 'businessPayload', - ] as const; +it.effect('rejects malformed audiences, invalid ordering, and subject mismatch', () => + Effect.gen(function* testScenario2() { + expect( + yield* Effect.flip(Schema.decodeUnknownEffect(GatewayContextRequestSchema)({ audience: '' })), + ).toBeDefined(); + expect( + yield* Effect.flip(decodeGatewayContextClaims({ ...claims, exp: claims.iat })), + ).toBeDefined(); + expect( + yield* Effect.flip(decodeGatewayContextClaims({ ...claims, exp: claims.iat + 301 })), + ).toBeDefined(); + expect( + yield* Effect.flip( + decodeGatewayContextClaims({ + ...claims, + sub: '60000000-0000-4000-8000-000000000001', + }), + ), + ).toBeDefined(); + }), +); - await Promise.all( - forbiddenFields.map( - async (field) => - await assert.rejects( - runEffectTestPromise(decodeGatewayContextClaims({ ...claims, [field]: 'must-not-pass' })), - field, - ), - ), - ); - await assert.rejects( - runEffectTestPromise( - decodeGatewayContextClaims({ - ...claims, - principal: { ...principal, email: 'must-not-pass@example.test' }, - }), - ), - ); -}); +it.effect('rejects credential, display, authorization, Action, and business claim expansion', () => + Effect.gen(function* testScenario3() { + const forbiddenFields = [ + 'email', + 'displayName', + 'credential', + 'rawApiKey', + 'providerKeyId', + 'keyId', + 'cookie', + 'sessionToken', + 'actionKey', + 'permission', + 'policyDecision', + 'businessPayload', + ] as const; + + for (const field of forbiddenFields) { + expect( + yield* Effect.flip(decodeGatewayContextClaims({ ...claims, [field]: 'must-not-pass' })), + field, + ).toBeDefined(); + } + expect( + yield* Effect.flip( + decodeGatewayContextClaims({ + ...claims, + principal: { ...principal, email: 'must-not-pass@example.test' }, + }), + ), + ).toBeDefined(); + }), +); -void test('schemas publish only the required public field names', () => { - assert.equal(GatewayTrustedPrincipalContextSchema, TrustedPrincipalContextSchema); - assert.deepEqual(Object.keys(GatewayContextClaimsSchema.fields).toSorted(), [ +it('schemas publish only the required public field names', () => { + expect(GatewayTrustedPrincipalContextSchema).toBe(TrustedPrincipalContextSchema); + expect(Object.keys(GatewayContextClaimsSchema.fields).toSorted()).toEqual([ 'aud', 'exp', 'iat', @@ -146,22 +145,21 @@ void test('schemas publish only the required public field names', () => { 'sub', 'ver', ]); - assert.deepEqual(Object.keys(GatewayContextProtectedHeaderSchema.fields).toSorted(), [ + expect(Object.keys(GatewayContextProtectedHeaderSchema.fields).toSorted()).toEqual([ 'alg', 'kid', 'typ', ]); }); -void test('publishes the exact API-key credential boundary and failure statuses', () => { - assert.deepEqual(Object.keys(ApiKeyGatewayHeadersSchema.fields), ['x-api-key']); - assert.deepEqual( - endpointStatuses(GatewayContextApiGroup.endpoints.issueApiKeyGatewayContext), - [400, 401, 403, 429, 500, 503], - ); +it('publishes the exact API-key credential boundary and failure statuses', () => { + expect(Object.keys(ApiKeyGatewayHeadersSchema.fields)).toEqual(['x-api-key']); + expect(endpointStatuses(GatewayContextApiGroup.endpoints.issueApiKeyGatewayContext)).toEqual([ + 400, 401, 403, 429, 500, 503, + ]); }); -void test('preserves migrated gateway Problem Details shapes and ordered endpoint membership', () => { +it('preserves migrated gateway Problem Details shapes and ordered endpoint membership', () => { const rateLimited = { _tag: 'GatewayRateLimitedProblem', detail: 'Retry after the published delay.', @@ -178,22 +176,20 @@ void test('preserves migrated gateway Problem Details shapes and ordered endpoin title: 'Gateway unavailable', type: 'https://ontos.dev/problems/gateway-unavailable', } as const; - assert.deepEqual( - Schema.decodeUnknownSync(GatewayRateLimitedProblemSchema)(rateLimited), - rateLimited, - ); - assert.deepEqual( - Schema.decodeUnknownSync(GatewayUnavailableProblemSchema)(unavailable), - unavailable, - ); - assert.throws(() => + const decodedRateLimited = Schema.decodeUnknownSync(GatewayRateLimitedProblemSchema)(rateLimited); + expect(Schema.is(GatewayRateLimitedProblemSchema)(decodedRateLimited)).toBe(true); + expect(Struct.omit(decodedRateLimited, ['_tag'])).toEqual(Struct.omit(rateLimited, ['_tag'])); + const decodedUnavailable = Schema.decodeUnknownSync(GatewayUnavailableProblemSchema)(unavailable); + expect(Schema.is(GatewayUnavailableProblemSchema)(decodedUnavailable)).toBe(true); + expect(Struct.omit(decodedUnavailable, ['_tag'])).toEqual(Struct.omit(unavailable, ['_tag'])); + expect(() => Schema.decodeUnknownSync(GatewayRateLimitedProblemSchema, { onExcessProperty: 'error' })({ ...rateLimited, internalDiagnostic: 'must-not-pass', }), - ); + ).toThrow(); const actual = [...GatewayContextApiGroup.endpoints.issueApiKeyGatewayContext.error]; - assert.deepEqual(actual.map(problemTag), [ + expect(actual.map(problemTag)).toEqual([ 'GatewayAuthenticationRequiredProblem', 'GatewayAudienceInvalidProblem', 'GatewayForbiddenProblem', diff --git a/app/packages/shared-contracts/tests/unit/governed-runtime.test.ts b/app/packages/shared-contracts/tests/unit/governed-runtime.test.ts index e1eead8b6..ca20ceebc 100644 --- a/app/packages/shared-contracts/tests/unit/governed-runtime.test.ts +++ b/app/packages/shared-contracts/tests/unit/governed-runtime.test.ts @@ -1,12 +1,7 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Redacted, Schema, Result, flow } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Redacted, Schema } from 'effect'; import { HttpApi, HttpApiEndpoint, HttpApiGroup } from '@modern-js/plugin-bff/effect-client'; import { FetchHttpClient } from 'effect/unstable/http'; -import { - makeEffectTestCallback, - runEffectTestPromise, -} from '../../../core-runtime/src/testing/effect-runtime.ts'; import { makeGovernedReadProblems } from '../../src/effect-bff-runtime.ts'; import { makeGovernedEffectBffClient } from '../../src/client-runtime.ts'; import { @@ -36,7 +31,7 @@ const statuses = { unavailable: 503, } as const; -test('shared problem factories preserve concrete schemas, statuses, retryability and sanitized values', () => { +it('shared problem factories preserve concrete schemas, statuses, retryability and sanitized values', () => { for (const key of Object.keys(problems)) { // Decode the key rather than casting away the concrete schema/factory contract. const kind = Schema.decodeUnknownSync( @@ -52,16 +47,15 @@ test('shared problem factories preserve concrete schemas, statuses, retryability ]), )(key); const problem = problems[kind](); - assert.equal(Schema.is(schemas[kind])(problem), true); - assert.equal(problem.status, statuses[kind]); - assert.equal('retryable' in problem, kind === 'unavailable'); - assert.equal(problem.type.startsWith('https://ontos.dev/problems/'), true); - assert.notEqual(problems[kind](), problem); + expect(Schema.is(schemas[kind])(problem)).toBe(true); + expect(problem.status).toBe(statuses[kind]); + expect('retryable' in problem).toBe(kind === 'unavailable'); + expect(problem.type.startsWith('https://ontos.dev/problems/')).toBe(true); + expect(problems[kind]()).not.toBe(problem); } const unavailable: typeof schemas.unavailable.Type = problems.unavailable(); - assert.equal(unavailable.retryable, true); - assert.deepEqual( - problems.authentication(), + expect(unavailable.retryable).toBe(true); + expect(problems.authentication()).toEqual( schemas.authentication.make({ detail: 'A valid audience-scoped Bearer assertion is required.', status: 401, @@ -69,8 +63,7 @@ test('shared problem factories preserve concrete schemas, statuses, retryability type: 'https://ontos.dev/problems/operation-authentication-required', }), ); - assert.deepEqual( - problems.internal(), + expect(problems.internal()).toEqual( schemas.internal.make({ detail: 'The governed read could not be completed.', status: 500, @@ -82,7 +75,10 @@ test('shared problem factories preserve concrete schemas, statuses, retryability const api = HttpApi.make('GovernedTransportTest').add( HttpApiGroup.make('read').add( - HttpApiEndpoint.get('execute', '/read', { error: schemas.unavailable, success: Schema.String }), + HttpApiEndpoint.get('execute', '/read', { + error: schemas.unavailable, + success: Schema.String, + }), ), ); const makeClient = (credential: string, requestCorrelation: string, baseUrl: string | URL) => @@ -96,19 +92,15 @@ const makeClient = (credential: string, requestCorrelation: string, baseUrl: str { baseUrl }, ); -test( +it.effect( 'shared transport is lazy and keeps each invocation credential, correlation and trusted URL', - makeEffectTestCallback( + () => Effect.gen(function* checkTransport() { const requests: Request[] = []; - const fetch: typeof globalThis.fetch = flow( - (input: RequestInfo | URL, init?: RequestInit) => - Effect.sync(() => { - requests.push(new Request(input, init)); - return Response.json('ok'); - }), - runEffectTestPromise, - ); + const fetch: typeof globalThis.fetch = (input, init) => { + requests.push(new Request(input, init)); + return Promise.resolve(Response.json('ok')); + }; const url = new URL('https://owner.example/custom'); const first = makeClient('Bearer first', 'first-correlation', url); url.protocol = 'ftp:'; @@ -118,58 +110,51 @@ test( 'second-correlation', 'https://owner.example/custom', ); - assert.equal(requests.length, 0); + expect(requests.length).toBe(0); for (const client of [first, second]) { const result = yield* client.pipe( Effect.flatMap((value) => value.read.execute({})), Effect.provideService(FetchHttpClient.Fetch, fetch), ); - assert.equal(result, 'ok'); + expect(result).toBe('ok'); } - assert.deepEqual( + expect( requests.map((request) => [ request.url, request.headers.get('authorization'), request.headers.get('x-correlation-id'), ]), - [ - ['https://owner.example/custom/read', 'Bearer first', 'first-correlation'], - ['https://owner.example/custom/read', 'Bearer second', 'second-correlation'], - ], - ); + ).toEqual([ + ['https://owner.example/custom/read', 'Bearer first', 'first-correlation'], + ['https://owner.example/custom/read', 'Bearer second', 'second-correlation'], + ]); }), - ), ); -test( - 'shared transport retains the concrete retryable backend error union', - makeEffectTestCallback( - Effect.gen(function* checkTypedFailure() { - const fetch: typeof globalThis.fetch = flow( - () => - Effect.sync(() => - Response.json(problems.unavailable(), { - headers: { 'content-type': 'application/problem+json' }, - status: 503, - }), - ), - runEffectTestPromise, +it.effect('shared transport retains the concrete retryable backend error union', () => + Effect.gen(function* checkTypedFailure() { + const fetch: typeof globalThis.fetch = () => + Promise.resolve( + Response.json(problems.unavailable(), { + headers: { 'content-type': 'application/problem+json' }, + status: 503, + }), ); - const result = yield* makeClient( - 'Bearer proof', - 'correlation', - 'https://owner.example/api', - ).pipe( - Effect.flatMap((client) => client.read.execute({})), - Effect.provideService(FetchHttpClient.Fetch, fetch), - Effect.result, - ); - assert.equal(Result.isFailure(result), true); - if (Result.isFailure(result)) { - assert.equal(Schema.is(schemas.unavailable)(result.failure), true); - } - }), - ), + const result = yield* makeClient( + 'Bearer proof', + 'correlation', + 'https://owner.example/api', + ).pipe( + Effect.flatMap((client) => client.read.execute({})), + Effect.provideService(FetchHttpClient.Fetch, fetch), + Effect.flip, + ); + expect(Schema.is(schemas.unavailable)(result)).toBe(true); + const problem = yield* Schema.decodeUnknownEffect(schemas.unavailable)(result); + expect(yield* Schema.encodeEffect(schemas.unavailable)(problem)).toEqual( + yield* Schema.encodeEffect(schemas.unavailable)(problems.unavailable()), + ); + }), ); for (const baseUrl of [ @@ -177,33 +162,23 @@ for (const baseUrl of [ 'https://user:password@owner.example/api', '//attacker.example/api', ]) { - test( - `shared transport rejects unsafe URL ${baseUrl} before fetch`, - makeEffectTestCallback( - Effect.gen(function* checkUnsafeUrl() { - let calls = 0; - const fetch: typeof globalThis.fetch = flow( - () => - Effect.sync(() => { - calls += 1; - return Response.json('unsafe'); - }), - runEffectTestPromise, - ); - const result = yield* makeClient('Bearer secret', 'correlation', baseUrl).pipe( - Effect.flatMap((client) => client.read.execute({})), - Effect.provideService(FetchHttpClient.Fetch, fetch), - Effect.result, - ); - assert.equal(Result.isFailure(result), true); - if (Result.isFailure(result)) { - assert.equal(Schema.isSchemaError(result.failure), true); - if (Schema.isSchemaError(result.failure)) { - assert.doesNotMatch(result.failure.message, /password|Bearer secret|owner\.example/u); - } - } - assert.equal(calls, 0); - }), - ), + it.effect(`shared transport rejects unsafe URL ${baseUrl} before fetch`, () => + Effect.gen(function* checkUnsafeUrl() { + let calls = 0; + const fetch: typeof globalThis.fetch = () => { + calls += 1; + return Promise.resolve(Response.json('unsafe')); + }; + const result = yield* makeClient('Bearer secret', 'correlation', baseUrl).pipe( + Effect.flatMap((client) => client.read.execute({})), + Effect.provideService(FetchHttpClient.Fetch, fetch), + Effect.flip, + ); + expect(Schema.isSchemaError(result)).toBe(true); + if (Schema.isSchemaError(result)) { + expect(result.message).not.toMatch(/password|Bearer secret|owner\.example/u); + } + expect(calls).toBe(0); + }), ); } diff --git a/app/packages/shared-contracts/tests/unit/microvertical-api-baseline.test.ts b/app/packages/shared-contracts/tests/unit/microvertical-api-baseline.test.ts index 41a25df43..b53711d47 100644 --- a/app/packages/shared-contracts/tests/unit/microvertical-api-baseline.test.ts +++ b/app/packages/shared-contracts/tests/unit/microvertical-api-baseline.test.ts @@ -1,7 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; - -import { Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Schema } from 'effect'; import { MicroVerticalBuildMarkerSchema, @@ -36,29 +34,32 @@ const marker = { version: generatedBuildMetadata.version, }; -test('marker and readiness schemas preserve the generated wire representation', () => { - const readiness = { - checks: { - api: 'ready' as const, - moduleFederation: 'ready' as const, - ssr: 'ready' as const, - translations: 'ready' as const, - }, - marker, - status: 'ready' as const, - versionSkew: 'none' as const, - }; +it.effect('marker and readiness schemas preserve the generated wire representation', () => + Effect.gen(function* wireRepresentationEffect() { + const readiness = { + checks: { + api: 'ready' as const, + moduleFederation: 'ready' as const, + ssr: 'ready' as const, + translations: 'ready' as const, + }, + marker, + status: 'ready' as const, + versionSkew: 'none' as const, + }; - assert.deepEqual( - Schema.decodeUnknownSync(MicroVerticalBuildMarkerSchema)(generatedBuildMetadata), - marker, - ); - assert.deepEqual(Schema.encodeSync(MicroVerticalBuildMarkerSchema)(marker), marker); - assert.deepEqual(Schema.decodeUnknownSync(MicroVerticalReadinessSchema)(readiness), readiness); - assert.deepEqual(Schema.encodeSync(MicroVerticalReadinessSchema)(readiness), readiness); -}); + expect( + yield* Schema.decodeUnknownEffect(MicroVerticalBuildMarkerSchema)(generatedBuildMetadata), + ).toEqual(marker); + expect(yield* Schema.encodeEffect(MicroVerticalBuildMarkerSchema)(marker)).toEqual(marker); + expect(yield* Schema.decodeUnknownEffect(MicroVerticalReadinessSchema)(readiness)).toEqual( + readiness, + ); + expect(yield* Schema.encodeEffect(MicroVerticalReadinessSchema)(readiness)).toEqual(readiness); + }), +); -test('constructs generated-client operation metadata with and without trace identity', () => { +it('constructs generated-client operation metadata with and without trace identity', () => { const inputWithSensitiveExtras = { credential: 'must-not-pass', method: 'GET', @@ -68,30 +69,29 @@ test('constructs generated-client operation metadata with and without trace iden routePath: '/inventory/readiness', tenantId: 'must-not-pass', }; - assert.deepEqual(createMicroVerticalOperationContext(inputWithSensitiveExtras), { + expect(createMicroVerticalOperationContext(inputWithSensitiveExtras)).toEqual({ method: 'GET', operationId: 'InventoryApi:inventory:readiness', routePath: '/inventory/readiness', source: 'generated-client', }); - assert.deepEqual( + expect( createMicroVerticalOperationContext({ method: 'POST', operationId: 'InventoryApi:inventory:create', routePath: '/inventory', traceId: 'trace-123', }), - { - method: 'POST', - operationId: 'InventoryApi:inventory:create', - routePath: '/inventory', - source: 'generated-client', - traceId: 'trace-123', - }, - ); + ).toEqual({ + method: 'POST', + operationId: 'InventoryApi:inventory:create', + routePath: '/inventory', + source: 'generated-client', + traceId: 'trace-123', + }); }); -test('projects only standard operation telemetry attributes', () => { +it('projects only standard operation telemetry attributes', () => { const operationContext = { ...createMicroVerticalOperationContext({ method: 'POST', @@ -107,14 +107,14 @@ test('projects only standard operation telemetry attributes', () => { tenantId: 'must-not-pass', }; - assert.deepEqual(microVerticalOperationAttributes(operationContext), { + expect(microVerticalOperationAttributes(operationContext)).toEqual({ 'modernjs.operation.id': 'InventoryApi:inventory:create', 'modernjs.operation.method': 'POST', 'modernjs.operation.route': '/inventory', 'modernjs.operation.source': 'generated-client', 'modernjs.trace.id': 'trace-123', }); - assert.deepEqual( + expect( microVerticalOperationAttributes( createMicroVerticalOperationContext({ method: 'GET', @@ -122,11 +122,10 @@ test('projects only standard operation telemetry attributes', () => { routePath: '/inventory', }), ), - { - 'modernjs.operation.id': 'InventoryApi:inventory:list', - 'modernjs.operation.method': 'GET', - 'modernjs.operation.route': '/inventory', - 'modernjs.operation.source': 'generated-client', - }, - ); + ).toEqual({ + 'modernjs.operation.id': 'InventoryApi:inventory:list', + 'modernjs.operation.method': 'GET', + 'modernjs.operation.route': '/inventory', + 'modernjs.operation.source': 'generated-client', + }); }); diff --git a/app/packages/shared-contracts/tests/unit/operation-gateway.test.ts b/app/packages/shared-contracts/tests/unit/operation-gateway.test.ts index 1227b9e67..0ee9d9e1d 100644 --- a/app/packages/shared-contracts/tests/unit/operation-gateway.test.ts +++ b/app/packages/shared-contracts/tests/unit/operation-gateway.test.ts @@ -1,6 +1,4 @@ -import { makeEffectTestCallback } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Effect } from 'effect'; import { makeOperationGateway } from '../../src/operation-gateway.ts'; @@ -21,116 +19,101 @@ const options = { const expectType = (value: Expected): Expected => value; -void test( - 'preserves the literal audience and success and failure inference', - makeEffectTestCallback( - Effect.gen(function* inferenceEffect() { - const gateway = makeOperationGateway(audience, ({ audience: receivedAudience }) => { - expectType(receivedAudience); - return Effect.fail(gatewayAcquisitionFailure); - }); - - yield* expectType< - Effect.Effect - >(gateway.invoke(() => Effect.fail(operationAttemptFailure))).pipe(Effect.flip); - yield* expectType>( - makeOperationGateway(audience, () => - Effect.succeed({ expiresAt: 1_700_000_300, token: 'test-token' }), - ).invoke(() => Effect.succeed('completed' as const)), - ); - }), - ), +it.effect('preserves the literal audience and success and failure inference', () => + Effect.gen(function* inferenceEffect() { + const gateway = makeOperationGateway(audience, ({ audience: receivedAudience }) => { + expectType(receivedAudience); + return Effect.fail(gatewayAcquisitionFailure); + }); + + yield* expectType< + Effect.Effect + >(gateway.invoke(() => Effect.fail(operationAttemptFailure))).pipe(Effect.flip); + yield* expectType>( + makeOperationGateway(audience, () => + Effect.succeed({ expiresAt: 1_700_000_300, token: 'test-token' }), + ).invoke(() => Effect.succeed('completed' as const)), + ); + }), ); -void test( - 'acquires one audience-scoped assertion and forwards options unchanged', - makeEffectTestCallback( - Effect.gen(function* audienceAssertionEffect() { - const issuerCalls: { - readonly audience: typeof audience; - readonly options: typeof options; - }[] = []; - const authorizations: string[] = []; - const gateway = makeOperationGateway(audience, (payload, receivedOptions) => - Effect.sync(() => { - assert.equal(receivedOptions, options); - issuerCalls.push({ audience: payload.audience, options }); - return { expiresAt: 1_700_000_300, token: 'header.payload.signature' }; - }), - ); - - const result = yield* gateway.invoke((authorization) => { - authorizations.push(authorization); - return Effect.succeed('completed' as const); - }, options); - - assert.equal(result, 'completed'); - assert.deepEqual(issuerCalls, [{ audience, options }]); - assert.deepEqual(authorizations, ['Bearer header.payload.signature']); - }), - ), +it.effect('acquires one audience-scoped assertion and forwards options unchanged', () => + Effect.gen(function* audienceAssertionEffect() { + const issuerCalls: { + readonly audience: typeof audience; + readonly options: typeof options; + }[] = []; + const authorizations: string[] = []; + const gateway = makeOperationGateway(audience, (payload, receivedOptions) => + Effect.sync(() => { + expect(receivedOptions).toBe(options); + issuerCalls.push({ audience: payload.audience, options }); + return { expiresAt: 1_700_000_300, token: 'header.payload.signature' }; + }), + ); + + const result = yield* gateway.invoke((authorization) => { + authorizations.push(authorization); + return Effect.succeed('completed' as const); + }, options); + + expect(result).toBe('completed'); + expect(issuerCalls).toEqual([{ audience, options }]); + expect(authorizations).toEqual(['Bearer header.payload.signature']); + }), ); -void test( - 'acquires a fresh assertion whenever an invocation Effect is executed', - makeEffectTestCallback( - Effect.gen(function* freshAssertionEffect() { - let acquisitions = 0; - const gateway = makeOperationGateway(audience, () => - Effect.sync(() => { - acquisitions += 1; - return { expiresAt: 1_700_000_300, token: `attempt-${acquisitions}` }; - }), - ); - const authorizations: string[] = []; - const invocation = gateway.invoke((authorization) => - Effect.sync(() => { - authorizations.push(authorization); - }), - ); - - yield* invocation; - yield* invocation; - - assert.equal(acquisitions, 2); - assert.deepEqual(authorizations, ['Bearer attempt-1', 'Bearer attempt-2']); - }), - ), +it.effect('acquires a fresh assertion whenever an invocation Effect is executed', () => + Effect.gen(function* freshAssertionEffect() { + let acquisitions = 0; + const gateway = makeOperationGateway(audience, () => + Effect.sync(() => { + acquisitions += 1; + return { expiresAt: 1_700_000_300, token: `attempt-${acquisitions}` }; + }), + ); + const authorizations: string[] = []; + const invocation = gateway.invoke((authorization) => + Effect.sync(() => { + authorizations.push(authorization); + }), + ); + + yield* invocation; + yield* invocation; + + expect(acquisitions).toBe(2); + expect(authorizations).toEqual(['Bearer attempt-1', 'Bearer attempt-2']); + }), ); -void test( - 'preserves issuer failure and does not construct the attempted Effect', - makeEffectTestCallback( - Effect.gen(function* issuerFailureEffect() { - let attemptCalls = 0; - const gateway = makeOperationGateway(audience, () => Effect.fail(gatewayAcquisitionFailure)); - - const received = yield* gateway - .invoke(() => { - attemptCalls += 1; - return Effect.succeed('must not run'); - }) - .pipe(Effect.flip); - - assert.equal(received, gatewayAcquisitionFailure); - assert.equal(attemptCalls, 0); - }), - ), +it.effect('preserves issuer failure and does not construct the attempted Effect', () => + Effect.gen(function* issuerFailureEffect() { + let attemptCalls = 0; + const gateway = makeOperationGateway(audience, () => Effect.fail(gatewayAcquisitionFailure)); + + const received = yield* gateway + .invoke(() => { + attemptCalls += 1; + return Effect.succeed('must not run'); + }) + .pipe(Effect.flip); + + expect(received).toBe(gatewayAcquisitionFailure); + expect(attemptCalls).toBe(0); + }), ); -void test( - 'preserves the attempted-operation failure without translation', - makeEffectTestCallback( - Effect.gen(function* attemptFailureEffect() { - const gateway = makeOperationGateway(audience, () => - Effect.succeed({ expiresAt: 1_700_000_300, token: 'test-token' }), - ); +it.effect('preserves the attempted-operation failure without translation', () => + Effect.gen(function* attemptFailureEffect() { + const gateway = makeOperationGateway(audience, () => + Effect.succeed({ expiresAt: 1_700_000_300, token: 'test-token' }), + ); - const received = yield* gateway - .invoke(() => Effect.fail(operationAttemptFailure)) - .pipe(Effect.flip); + const received = yield* gateway + .invoke(() => Effect.fail(operationAttemptFailure)) + .pipe(Effect.flip); - assert.equal(received, operationAttemptFailure); - }), - ), + expect(received).toBe(operationAttemptFailure); + }), ); diff --git a/app/packages/shared-contracts/tests/unit/problem-details.test.ts b/app/packages/shared-contracts/tests/unit/problem-details.test.ts index 4ed89f52c..4d50c95cb 100644 --- a/app/packages/shared-contracts/tests/unit/problem-details.test.ts +++ b/app/packages/shared-contracts/tests/unit/problem-details.test.ts @@ -1,6 +1,4 @@ -// @effect-diagnostics asyncFunction:off -- Node test callbacks and Web handlers bridge the Effect contracts under test; expires: 2027-03-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { makeEffectHttpApiClient } from '@modern-js/plugin-bff/effect-client'; import { HttpApi, @@ -10,8 +8,7 @@ import { HttpRouter, HttpServer, } from '@modern-js/plugin-bff/effect-edge'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { Context, Effect, Layer, Result, Schema, SchemaAST } from 'effect'; +import { Context, Effect, Layer, Predicate, Schema, SchemaAST, Struct } from 'effect'; import { FetchHttpClient } from 'effect/unstable/http'; import { makeProblemDetailsSchema, @@ -22,7 +19,7 @@ import { ImportedUnconstrainedExtensionSchema } from '../fixtures/unconstrained- const statuses = [400, 401, 403, 404, 409, 422, 428, 429, 500, 503, 504] as const; for (const status of statuses) { - void test(`couples the ${status} body, schema, and HttpApi status`, () => { + it(`couples the ${status} body, schema, and HttpApi status`, () => { const schema = makeProblemDetailsSchema(`FixtureProblem${status}`, status, { recoveryCode: Schema.Literal(`RECOVER_${status}`), }); @@ -35,24 +32,32 @@ for (const status of statuses) { type: `urn:ontos:test:problem:${status}`, } as const; - assert.deepEqual(Schema.decodeUnknownSync(schema)(problem), problem); - assert.deepEqual(Schema.encodeUnknownSync(schema)(problem), problem); - assert.equal(schema.ast.annotations?.['httpApiStatus'], status); - assert.deepEqual(schema.ast.annotations?.['~httpApiEncoding'], { - _tag: 'Json', + const decodedProblem = Schema.decodeUnknownSync(schema)(problem); + expect(Schema.is(schema)(decodedProblem)).toBe(true); + expect(Struct.omit(decodedProblem, ['_tag'])).toEqual(Struct.omit(problem, ['_tag'])); + const encodedProblem = Schema.encodeUnknownSync(schema)(problem); + expect(Schema.is(Schema.toEncoded(schema))(encodedProblem)).toBe(true); + expect(Struct.omit(encodedProblem, ['_tag'])).toEqual(Struct.omit(problem, ['_tag'])); + expect(schema.ast.annotations?.['httpApiStatus']).toBe(status); + const encoding = schema.ast.annotations?.['~httpApiEncoding']; + expect(Predicate.isTagged(encoding, 'Json')).toBe(true); + if (!Predicate.isTagged(encoding, 'Json')) { + throw new Error('Expected JSON HTTP API encoding'); + } + expect(Struct.omit(encoding, ['_tag'])).toEqual({ contentType: 'application/problem+json', }); - assert.throws(() => Schema.decodeUnknownSync(schema)({ ...problem, status: 418 })); - assert.throws(() => + expect(() => Schema.decodeUnknownSync(schema)({ ...problem, status: 418 })).toThrow(); + expect(() => Schema.decodeUnknownSync(schema, { onExcessProperty: 'error' })({ ...problem, internalDiagnostic: 'must-not-pass', }), - ); + ).toThrow(); }); } -void test('adds only the deliberate retryable literal marker', () => { +it('adds only the deliberate retryable literal marker', () => { const schema = makeRetryableProblemDetailsSchema('RetryableFixtureProblem', 503, { retryAfterSeconds: Schema.Finite, }); @@ -66,113 +71,103 @@ void test('adds only the deliberate retryable literal marker', () => { type: 'urn:ontos:test:retryable', } as const; - assert.deepEqual(Schema.decodeUnknownSync(schema)(problem), problem); - assert.throws(() => Schema.decodeUnknownSync(schema)({ ...problem, retryable: false })); + const decodedProblem = Schema.decodeUnknownSync(schema)(problem); + expect(Schema.is(schema)(decodedProblem)).toBe(true); + expect(Struct.omit(decodedProblem, ['_tag'])).toEqual(Struct.omit(problem, ['_tag'])); + expect(() => Schema.decodeUnknownSync(schema)({ ...problem, retryable: false })).toThrow(); }); -void test('drives real HttpApi responses and generated client decoding', async () => { - const schema = makeRetryableProblemDetailsSchema('FixtureGatewayTimeoutProblem', 504, { - operation: Schema.Literal('fixture-read'), - }); - const problem = schema.make({ - detail: 'The fixture operation timed out.', - operation: 'fixture-read', - retryable: true, - status: 504, - title: 'Fixture timeout', - type: 'urn:ontos:test:fixture-timeout', - }); - const group = HttpApiGroup.make('problemFixture').add( - HttpApiEndpoint.post('execute', '/problem-fixture', { - error: [schema], - payload: Schema.Struct({}), - success: Schema.Struct({ ok: Schema.Literal(true) }), - }), - ); - const api = HttpApi.make('ProblemFixtureApi').add(group); - const handlers = HttpApiBuilder.group(api, 'problemFixture', (builder) => - builder.handle('execute', () => Effect.fail(problem)), - ); - const server = HttpRouter.toWebHandler( - HttpApiBuilder.layer(api).pipe( - Layer.provide(handlers), - Layer.provide(HttpServer.layerServices), - ), - { disableLogger: true }, - ); +it.live('drives real HttpApi responses and generated client decoding', () => + Effect.gen(function* problemDetailsHttpScenario() { + const schema = makeRetryableProblemDetailsSchema('FixtureGatewayTimeoutProblem', 504, { + operation: Schema.Literal('fixture-read'), + }); + const problem = schema.make({ + detail: 'The fixture operation timed out.', + operation: 'fixture-read', + retryable: true, + status: 504, + title: 'Fixture timeout', + type: 'urn:ontos:test:fixture-timeout', + }); + const group = HttpApiGroup.make('problemFixture').add( + HttpApiEndpoint.post('execute', '/problem-fixture', { + error: [schema], + payload: Schema.Struct({}), + success: Schema.Struct({ ok: Schema.Literal(true) }), + }), + ); + const api = HttpApi.make('ProblemFixtureApi').add(group); + const handlers = HttpApiBuilder.group(api, 'problemFixture', (builder) => + builder.handle('execute', () => Effect.fail(problem)), + ); + const server = yield* Effect.acquireRelease( + Effect.sync(() => + HttpRouter.toWebHandler( + HttpApiBuilder.layer(api).pipe( + Layer.provide(handlers), + Layer.provide(HttpServer.layerServices), + ), + { disableLogger: true }, + ), + ), + (webHandler) => Effect.promise(() => webHandler.dispose()), + ); - try { const request = new Request('https://fixture.ontos.test/problem-fixture', { body: '{}', headers: { 'content-type': 'application/json' }, method: 'POST', }); - const response = await server.handler(request, Context.empty()); - assert.equal(response.status, problem.status); - assert.match(response.headers.get('content-type') ?? '', /^application\/problem\+json\b/u); - assert.deepEqual(await response.json(), problem); + const response = yield* Effect.promise(() => server.handler(request, Context.empty())); + expect(response.status).toBe(problem.status); + expect(response.headers.get('content-type') ?? '').toMatch(/^application\/problem\+json\b/u); + expect(yield* Effect.promise(() => response.json())).toEqual(problem); const client = makeEffectHttpApiClient(api, { baseUrl: 'https://fixture.ontos.test' }); - const clientResult = await runEffectTestPromise( + const clientError = yield* Effect.flip( client.pipe( Effect.flatMap((generated) => generated.problemFixture.execute({ payload: {} })), - Effect.result, - Effect.provideService( - FetchHttpClient.Fetch, - async (input, init) => await server.handler(new Request(input, init), Context.empty()), + Effect.provideService(FetchHttpClient.Fetch, (input, init) => + server.handler(new Request(input, init), Context.empty()), ), ), ); - assert.ok(Result.isFailure(clientResult)); - assert.deepEqual(clientResult.failure, problem); - } finally { - await server.dispose(); - } -}); + expect(clientError).toEqual(problem); + }), +); -void test('rejects reserved and unconstrained extension schemas at construction', () => { +it('rejects reserved and unconstrained extension schemas at construction', () => { const uniqueSymbol = Symbol('problem-detail-extension'); const nestedUniqueSymbol = Symbol('nested-problem-detail-extension'); - assert.throws( - () => - makeProblemDetailsSchema('ReservedFixtureProblem', 400, { - status: Schema.Finite, - }), - /reserved/u, - ); - assert.throws( - () => - makeProblemDetailsSchema('PrototypeSyntaxFixtureProblem', 400, { __proto__: Schema.String }), - /plain object/u, - ); - assert.throws( - () => - makeProblemDetailsSchema('SymbolKeyFixtureProblem', 400, { - [uniqueSymbol]: Schema.Unknown, - }), - /names must be strings/u, - ); - assert.throws( - () => - makeProblemDetailsSchema('PrototypeKeyFixtureProblem', 400, { - ['__proto__']: Schema.String, - }), - /reserved/u, - ); - assert.throws( - () => - makeProblemDetailsSchema('UnknownFixtureProblem', 400, { - unsafe: Schema.Unknown, - }), - /concrete/u, - ); - assert.throws( - () => - makeProblemDetailsSchema('AnyFixtureProblem', 400, { - unsafe: Schema.Any, - }), - /concrete/u, - ); + expect(() => + makeProblemDetailsSchema('ReservedFixtureProblem', 400, { + status: Schema.Finite, + }), + ).toThrow(/reserved/u); + expect(() => + makeProblemDetailsSchema('PrototypeSyntaxFixtureProblem', 400, { __proto__: Schema.String }), + ).toThrow(/plain object/u); + expect(() => + makeProblemDetailsSchema('SymbolKeyFixtureProblem', 400, { + [uniqueSymbol]: Schema.Unknown, + }), + ).toThrow(/names must be strings/u); + expect(() => + makeProblemDetailsSchema('PrototypeKeyFixtureProblem', 400, { + ['__proto__']: Schema.String, + }), + ).toThrow(/reserved/u); + expect(() => + makeProblemDetailsSchema('UnknownFixtureProblem', 400, { + unsafe: Schema.Unknown, + }), + ).toThrow(/concrete/u); + expect(() => + makeProblemDetailsSchema('AnyFixtureProblem', 400, { + unsafe: Schema.Any, + }), + ).toThrow(/concrete/u); for (const unsafe of [ Schema.Array(Schema.Unknown), Schema.BigInt, @@ -191,22 +186,20 @@ void test('rejects reserved and unconstrained extension schemas at construction' Schema.Undefined, ImportedUnconstrainedExtensionSchema, ]) { - assert.throws( - () => makeProblemDetailsSchema('NestedUnknownFixtureProblem', 400, { unsafe }), + expect(() => makeProblemDetailsSchema('NestedUnknownFixtureProblem', 400, { unsafe })).toThrow( /concrete/u, ); } for (const literal of [undefined, Symbol('non-json-literal')]) { // @ts-expect-error JavaScript callers can provide unsupported literal values, so the runtime factory must still reject them. const unsafe = Schema.Literal(literal); - assert.throws( - () => makeProblemDetailsSchema('NonJsonLiteralFixtureProblem', 400, { unsafe }), + expect(() => makeProblemDetailsSchema('NonJsonLiteralFixtureProblem', 400, { unsafe })).toThrow( /concrete/u, ); } }); -void test('rejects accessor-backed extension fields before reading them', () => { +it('rejects accessor-backed extension fields before reading them', () => { let reads = 0; const extensions = Object.defineProperty({}, 'diagnostics', { enumerable: true, @@ -216,14 +209,13 @@ void test('rejects accessor-backed extension fields before reading them', () => }, }); - assert.throws( - () => makeProblemDetailsSchema('AccessorFixtureProblem', 400, extensions), + expect(() => makeProblemDetailsSchema('AccessorFixtureProblem', 400, extensions)).toThrow( /enumerable data property/u, ); - assert.equal(reads, 0); + expect(reads).toBe(0); }); -void test('uses one descriptor snapshot for extension keys and schema ASTs', () => { +it('uses one descriptor snapshot for extension keys and schema ASTs', () => { let ownKeyReads = 0; const changingFields = new Proxy( {}, @@ -241,8 +233,8 @@ void test('uses one descriptor snapshot for extension keys and schema ASTs', () }, ); const stableSchema = makeProblemDetailsSchema('StableSnapshotProblem', 400, changingFields); - assert.equal(ownKeyReads, 1); - assert.throws(() => + expect(ownKeyReads).toBe(1); + expect(() => Schema.decodeUnknownSync(stableSchema)({ _tag: 'StableSnapshotProblem', detail: 'Wrong status.', @@ -250,7 +242,7 @@ void test('uses one descriptor snapshot for extension keys and schema ASTs', () title: 'Wrong status', type: 'urn:ontos:test:wrong-status', }), - ); + ).toThrow(); let astReads = 0; const changingSchema = new Proxy(Schema.Unknown, { @@ -263,23 +255,24 @@ void test('uses one descriptor snapshot for extension keys and schema ASTs', () return Reflect.get(target, key, receiver); }, }); - assert.throws( - () => - makeProblemDetailsSchema('StableAstProblem', 400, { - diagnostics: changingSchema, - }), - /concrete/u, - ); + expect(() => + makeProblemDetailsSchema('StableAstProblem', 400, { + diagnostics: changingSchema, + }), + ).toThrow(/concrete/u); const mutableExtension = Schema.Struct({ note: Schema.String }); const immutableProblem = makeProblemDetailsSchema('ImmutableAstProblem', 400, { metadata: mutableExtension, }); - assert.ok(SchemaAST.isObjects(mutableExtension.ast)); + expect(SchemaAST.isObjects(mutableExtension.ast)).toBe(true); const [note] = mutableExtension.ast.propertySignatures; - assert.ok(note !== undefined); + expect(note).toBeDefined(); + if (note === undefined) { + return; + } Reflect.set(note, 'type', Schema.Unknown.ast); - assert.throws(() => + expect(() => Schema.decodeUnknownSync(immutableProblem)({ _tag: 'ImmutableAstProblem', detail: 'Must stay concrete.', @@ -288,10 +281,10 @@ void test('uses one descriptor snapshot for extension keys and schema ASTs', () title: 'Immutable AST', type: 'urn:ontos:test:immutable-ast', }), - ); + ).toThrow(); }); -void test('accepts concrete JSON literals and schemas with JSON-safe encodings', () => { +it('accepts concrete JSON literals and schemas with JSON-safe encodings', () => { const schema = makeProblemDetailsSchema('EncodedExtensionsProblem', 422, { amount: Schema.FiniteFromString, attachment: Schema.Uint8ArrayFromBase64, @@ -313,11 +306,13 @@ void test('accepts concrete JSON literals and schemas with JSON-safe encodings', type: 'urn:ontos:test:encoded-extensions', } as const; const decoded = Schema.decodeUnknownSync(schema)(encoded); - assert.equal(decoded.amount, 12.5); - assert.deepEqual(decoded.attachment, new Uint8Array([1, 2])); - assert.equal(decoded.count, 7n); - assert.equal(decoded.occurredAt.toISOString(), encoded.occurredAt); - assert.deepEqual(Schema.encodeUnknownSync(schema)(decoded), encoded); + expect(decoded.amount).toBe(12.5); + expect(decoded.attachment).toEqual(new Uint8Array([1, 2])); + expect(decoded.count).toBe(7n); + expect(decoded.occurredAt.toISOString()).toBe(encoded.occurredAt); + const reencoded = Schema.encodeUnknownSync(schema)(decoded); + expect(Schema.is(Schema.toEncoded(schema))(reencoded)).toBe(true); + expect(Struct.omit(reencoded, ['_tag'])).toEqual(Struct.omit(encoded, ['_tag'])); }); const narrowSchema = makeProblemDetailsSchema('NarrowFixtureProblem', 409, { diff --git a/app/packages/shared-contracts/tests/unit/ultramodern-build.test.ts b/app/packages/shared-contracts/tests/unit/ultramodern-build.test.ts index 5dfd726b8..4ea124d9a 100644 --- a/app/packages/shared-contracts/tests/unit/ultramodern-build.test.ts +++ b/app/packages/shared-contracts/tests/unit/ultramodern-build.test.ts @@ -1,8 +1,7 @@ -import assert from 'node:assert/strict'; -import { test } from 'node:test'; +import { expect, it } from 'effect-rstest'; import { withUltramodernBuildIdentity } from '@app/shared-contracts/ultramodern-build'; -test('injected build identity updates all surfaces without mutating generated metadata', () => { +it('injected build identity updates all surfaces without mutating generated metadata', () => { const deliveryUnit = { appId: 'build-identity-test', build: 'generated-build', @@ -25,7 +24,7 @@ test('injected build identity updates all surfaces without mutating generated me buildMarker: 'injected-build', sourceRevision: 'source-revision', }; - assert.deepEqual(result, { + expect(result).toEqual({ ...artifact, deliveryUnit: expectedIdentity, surfaces: { @@ -33,7 +32,7 @@ test('injected build identity updates all surfaces without mutating generated me ui: { ...expectedIdentity, surface: 'ui' }, }, }); - assert.equal(artifact.deliveryUnit.build, 'generated-build'); - assert.equal(artifact.surfaces.api.sourceRevision, 'workspace'); - assert.equal(artifact.surfaces.ui.buildMarker, 'generated-build'); + expect(artifact.deliveryUnit.build).toBe('generated-build'); + expect(artifact.surfaces.api.sourceRevision).toBe('workspace'); + expect(artifact.surfaces.ui.buildMarker).toBe('generated-build'); }); diff --git a/app/patches/@bleedingdev__modern-js-app-tools@3.8.2-ultramodern.12.patch b/app/patches/@bleedingdev__modern-js-app-tools@3.8.2-ultramodern.12.patch index f5747b16e..2f60ad8f7 100644 --- a/app/patches/@bleedingdev__modern-js-app-tools@3.8.2-ultramodern.12.patch +++ b/app/patches/@bleedingdev__modern-js-app-tools@3.8.2-ultramodern.12.patch @@ -367,3 +367,18 @@ index 5a5b418c5d28a5ebec5c11c272f537c58fb59bb4..3e233f2812fbb5247a9aebf1a035631b }).join(';\n'); const getPluginsCode = (plugins)=>`[${plugins.map(([, options], index)=>`plugin_${index}(${options ? JSON.stringify(options) : ''})`).join(',')}]`; const getServerConfigPath = (meta)=>`path.join(__dirname, "${SERVER_DIR}", "${meta}.server")`; +diff --git a/package.json b/package.json +--- a/package.json ++++ b/package.json +@@ -72,6 +72,11 @@ + "./bundle-docs": { + "types": "./dist/types/bundleDocs.d.ts", + "default": "./dist/esm-node/bundleDocs.mjs" ++ }, ++ "./release-envelope": { ++ "types": "./dist/types/ultramodern-release-envelope/framework-output.d.ts", ++ "import": "./dist/esm-node/ultramodern-release-envelope/framework-output.mjs", ++ "require": "./dist/cjs/ultramodern-release-envelope/framework-output.js" + } + }, + "engines": { diff --git a/app/patches/@bleedingdev__modern-js-create@3.8.2-ultramodern.12.patch b/app/patches/@bleedingdev__modern-js-create@3.8.2-ultramodern.12.patch index 631214bcb..f1c9f448d 100644 --- a/app/patches/@bleedingdev__modern-js-create@3.8.2-ultramodern.12.patch +++ b/app/patches/@bleedingdev__modern-js-create@3.8.2-ultramodern.12.patch @@ -3200,7 +3200,6 @@ index 04820fd4264e7df2d2fa5c803df181e3e1401a34..3eb4b2387a43ba4a163f93b3dc309df3 - }, + }), diff --git a/templates/workspace-scripts/check-ultramodern-api-boundaries.mts b/templates/workspace-scripts/check-ultramodern-api-boundaries.mts -index d606a2265cb438d32a612cfdd6ebb9561529d4f5..624bf77fa44f720c9ee394f138273b2461e906ba 100644 --- a/templates/workspace-scripts/check-ultramodern-api-boundaries.mts +++ b/templates/workspace-scripts/check-ultramodern-api-boundaries.mts @@ -1,6 +1,10 @@ @@ -3222,10 +3221,14 @@ index d606a2265cb438d32a612cfdd6ebb9561529d4f5..624bf77fa44f720c9ee394f138273b24 'node_modules', 'repos', ]); -@@ -182,11 +187,1184 @@ for (const file of textFiles) { +@@ -181,12 +186,1187 @@ for (const file of textFiles) { + ); } - const verticalDirectories = listDirectories('verticals'); +-const verticalDirectories = listDirectories('verticals'); ++const verticalDirectories = listDirectories('verticals').filter(verticalPath => ++ exists(`${verticalPath}/package.json`), ++); +const topology = exists('topology/reference-topology.json') + ? JSON.parse(readText('topology/reference-topology.json')) + : { verticals: [] }; @@ -4407,7 +4410,7 @@ index d606a2265cb438d32a612cfdd6ebb9561529d4f5..624bf77fa44f720c9ee394f138273b24 function assertApiSurface(appPath) { const apiEntry = `${appPath}/api/index.ts`; const backendEffectExpose = `${appPath}/api/effect-api.ts`; -@@ -211,30 +1389,28 @@ function assertApiSurface(appPath) { +@@ -211,30 +1391,28 @@ function assertApiSurface(appPath) { if (exists(apiEntry)) { const entry = readText(apiEntry); @@ -4452,7 +4455,7 @@ index d606a2265cb438d32a612cfdd6ebb9561529d4f5..624bf77fa44f720c9ee394f138273b24 } if (exists(backendEffectExpose)) { const backendExpose = readText(backendEffectExpose); -@@ -300,6 +1476,62 @@ function assertApiSurface(appPath) { +@@ -300,6 +1478,62 @@ function assertApiSurface(appPath) { /\bSchema\./u, 'must use Schema for request, response and error shapes.', ); @@ -4515,7 +4518,7 @@ index d606a2265cb438d32a612cfdd6ebb9561529d4f5..624bf77fa44f720c9ee394f138273b24 } if (exists(modernConfig)) { -@@ -373,7 +1605,6 @@ if (exists('package.json')) { +@@ -373,7 +1607,6 @@ if (exists('package.json')) { } if (exists('topology/reference-topology.json')) { diff --git a/app/patches/effect-rstest@0.1.0.patch b/app/patches/effect-rstest@0.1.0.patch new file mode 100644 index 000000000..7a2467fa3 --- /dev/null +++ b/app/patches/effect-rstest@0.1.0.patch @@ -0,0 +1,214 @@ +diff --git a/dist/index.mjs b/dist/index.mjs +index c2ba4e3cc43376c79da7e95810678a5e28b10bee..3d5f0118e9e913efeee08975142dc0a4a1f2f7af 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -2,7 +2,9 @@ import * as Rs from "@rstest/core"; + import * as Cause from "effect/Cause"; + import * as Duration from "effect/Duration"; + import * as Effect from "effect/Effect"; ++import * as Equal from "effect/Equal"; + import * as Exit from "effect/Exit"; ++import * as Fiber from "effect/Fiber"; + import { flow, pipe } from "effect/Function"; + import * as Layer from "effect/Layer"; + import { isObject } from "effect/Predicate"; +@@ -27,11 +29,17 @@ const runPromise = Effect.fnUntraced(function* (effect, _ctx) { + return yield* exit; + }, (effect, _, ctx) => Effect.runPromise(effect, { signal: ctx?.signal })); + /** @internal */ +-const runTest = (ctx) => (effect) => runPromise(effect, ctx); ++const runTest = (ctx) => (effect) => { ++ let settlement; ++ ctx?.onTestFinished(() => settlement, 0); ++ const result = runPromise(effect, ctx); ++ settlement = result.then(() => {}, () => {}); ++ return result; ++}; + const TestEnv = Layer.mergeAll(TestConsole.layer, TestClock.layer()); + /** @internal */ + const addEqualityTesters$1 = () => { +- Rs.expect.addEqualityTesters([]); ++ Rs.expect.addEqualityTesters([(a, b) => Equal.isEqual(a) && Equal.isEqual(b) ? Equal.equals(a, b) : void 0]); + }; + /** @internal */ + const testOptions = (timeout) => typeof timeout === "number" ? { timeout } : timeout ?? {}; +@@ -47,7 +55,7 @@ const makeItProxy = (it, overrides) => new Proxy(it, { + }); + /** @internal */ + const makeTester = (mapEffect, it = Rs.it) => { +- const run = (ctx, args, self) => pipe(Effect.suspend(() => self(...args)), mapEffect, runTest(ctx)); ++ const run = (ctx, args, self) => pipe(Effect.suspend(() => self(...args)), mapEffect, Effect.asVoid, runTest(ctx)); + const f = (name, self, timeout) => it(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)); + const skip = (name, self, timeout) => it.skip(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)); + const skipIf = (condition) => (name, self, timeout) => it.skipIf(Boolean(condition))(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)); +@@ -84,15 +92,14 @@ const makeTester = (mapEffect, it = Rs.it) => { + const prop$1 = (name, arbitraries, self, timeout) => { + if (Array.isArray(arbitraries)) { + const arbs = arbitraries.map((arbitrary) => { +- if (Schema.isSchema(arbitrary)) throw new Error("Schemas are not supported yet"); ++ if (Schema.isSchema(arbitrary)) return Schema.toArbitrary(arbitrary)(fc); + return arbitrary; + }); + return Rs.it(name, testOptions(timeout), (ctx) => fc.assert(fc.property(...arbs, (...as) => self(as, ctx)), isObject(timeout) ? timeout?.fastCheck : {})); + } + const arbs = fc.record(Object.keys(arbitraries).reduce(function(result, key) { + const arb = arbitraries[key]; +- if (Schema.isSchema(arb)) throw new Error("Schemas are not supported yet"); +- Rec.assignProperty(result, key, arb); ++ Rec.assignProperty(result, key, Schema.isSchema(arb) ? Schema.toArbitrary(arb)(fc) : arb); + return result; + }, {})); + return Rs.it(name, testOptions(timeout), (ctx) => fc.assert(fc.property(arbs, (as) => self(as, ctx)), isObject(timeout) ? timeout?.fastCheck : {})); +@@ -104,11 +111,16 @@ const layer$1 = (layer_, options) => (...args) => { + const memoMap = options?.memoMap ?? Effect.runSync(Layer.makeMemoMap); + const scope = Effect.runSync(Scope.make()); + const contextEffect = Layer.buildWithMemoMap(withTestEnv, memoMap, scope).pipe(Effect.orDie, Effect.cached, Effect.runSync); ++ let setupFiber; ++ const buildContext = () => runPromise(Effect.withFiber((fiber) => { ++ setupFiber = fiber; ++ return Effect.asVoid(contextEffect); ++ })); + let closed = false; + const closeScope = (ctx) => { + if (closed) return Promise.resolve(); + closed = true; +- return runPromise(Scope.close(scope, Exit.void), ctx); ++ return runPromise(Effect.andThen(setupFiber !== void 0 ? Fiber.interrupt(setupFiber) : Effect.void, Scope.close(scope, Exit.void)), ctx); + }; + const makeIt = (it) => makeItProxy(it, { + effect: makeTester((effect) => Effect.flatMap(contextEffect, (context) => effect.pipe(Effect.scoped, Effect.provide(context))), it), +@@ -124,12 +136,12 @@ const layer$1 = (layer_, options) => (...args) => { + } + }); + if (args.length === 1) return Rs.describe("", () => { +- Rs.beforeAll(() => runPromise(Effect.asVoid(contextEffect)), hookTimeout(options?.timeout)); ++ Rs.beforeAll(buildContext, hookTimeout(options?.timeout)); + Rs.afterAll(() => closeScope(), hookTimeout(options?.timeout)); + return args[0](makeIt(Rs.it)); + }); + return Rs.describe(args[0], () => { +- Rs.beforeAll(() => runPromise(Effect.asVoid(contextEffect)), hookTimeout(options?.timeout)); ++ Rs.beforeAll(buildContext, hookTimeout(options?.timeout)); + Rs.afterAll(() => closeScope(), hookTimeout(options?.timeout)); + return args[1](makeIt(Rs.it)); + }); +diff --git a/src/internal/internal.ts b/src/internal/internal.ts +index b5a654301d060003daf34cbe7e4ec4cd80bf6f24..874f2919fd36e9efabb49f1e5c97fce2ad37d79e 100644 +--- a/src/internal/internal.ts ++++ b/src/internal/internal.ts +@@ -5,7 +5,9 @@ + import * as Cause from "effect/Cause" + import * as Duration from "effect/Duration" + import * as Effect from "effect/Effect" ++import * as Equal from "effect/Equal" + import * as Exit from "effect/Exit" ++import * as Fiber from "effect/Fiber" + import { flow, pipe } from "effect/Function" + import * as Layer from "effect/Layer" + import { isObject } from "effect/Predicate" +@@ -34,7 +36,17 @@ const runPromise: ( + }, (effect, _, ctx) => Effect.runPromise(effect, { signal: ctx?.signal })) + + /** @internal */ +-const runTest = (ctx?: Rs.TestContext) => (effect: Effect.Effect) => runPromise(effect, ctx) ++const runTest = (ctx?: Rs.TestContext) => (effect: Effect.Effect) => { ++ let settlement: Promise | undefined ++ // Rstest does not await timed-out callbacks. Await finalizers before the next ++ // test or suite teardown, without imposing a second cleanup deadline. ++ // Native afterEach hooks run before onTestFinished and are not covered. ++ ctx?.onTestFinished(() => settlement, 0) ++ const result = runPromise(effect, ctx) ++ // Preserve the original result without rethrowing already-handled failures. ++ settlement = result.then(() => {}, () => {}) ++ return result ++} + + /** @internal */ + export type TestContext = TestConsole.TestConsole | TestClock.TestClock +@@ -43,7 +55,9 @@ const TestEnv = Layer.mergeAll(TestConsole.layer, TestClock.layer()) + + /** @internal */ + export const addEqualityTesters = () => { +- Rs.expect.addEqualityTesters([]) ++ Rs.expect.addEqualityTesters([ ++ (a, b) => Equal.isEqual(a) && Equal.isEqual(b) ? Equal.equals(a, b) : undefined ++ ]) + } + + /** @internal */ +@@ -81,7 +95,7 @@ const makeTester = ( + ctx: Rs.TestContext & object, + args: TestArgs, + self: EffectRstest.Vitest.TestFunction +- ): Promise => pipe(Effect.suspend(() => self(...args)), mapEffect, runTest(ctx)) ++ ): Promise => pipe(Effect.suspend(() => self(...args)), mapEffect, Effect.asVoid, runTest(ctx)) + + const f: EffectRstest.Vitest.Test = (name, self, timeout) => + it(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)) +@@ -161,7 +175,7 @@ export const prop: EffectRstest.Vitest.Methods["prop"] = (name, arbitraries, sel + if (Array.isArray(arbitraries)) { + const arbs = arbitraries.map((arbitrary) => { + if (Schema.isSchema(arbitrary)) { +- throw new Error("Schemas are not supported yet") ++ return Schema.toArbitrary(arbitrary)(fc) + } + return arbitrary + }) +@@ -176,10 +190,7 @@ export const prop: EffectRstest.Vitest.Methods["prop"] = (name, arbitraries, sel + const arbs = fc.record( + Object.keys(arbitraries).reduce(function(result, key) { + const arb: any = arbitraries[key] +- if (Schema.isSchema(arb)) { +- throw new Error("Schemas are not supported yet") +- } +- Rec.assignProperty(result, key, arb) ++ Rec.assignProperty(result, key, Schema.isSchema(arb) ? Schema.toArbitrary(arb)(fc) : arb) + return result + }, {} as Record>) + ) +@@ -228,13 +239,26 @@ export const layer = ( + Effect.cached, + Effect.runSync + ) ++ let setupFiber: Fiber.Fiber | undefined ++ const buildContext = () => runPromise(Effect.withFiber((fiber) => { ++ setupFiber = fiber ++ return Effect.asVoid(contextEffect) ++ })) + let closed = false + const closeScope = (ctx?: Rs.TestContext) => { + if (closed) { + return Promise.resolve() + } + closed = true +- return runPromise(Scope.close(scope, Exit.void), ctx) ++ // SuiteContext has no AbortSignal, so timed-out setup may still be running. ++ // Interrupt and await it before releasing resources it may still be using. ++ return runPromise( ++ Effect.andThen( ++ setupFiber !== undefined ? Fiber.interrupt(setupFiber) : Effect.void, ++ Scope.close(scope, Exit.void) ++ ), ++ ctx ++ ) + } + + const makeIt = (it: Rs.TestAPIs): EffectRstest.Vitest.MethodsNonLive => +@@ -269,7 +293,7 @@ export const layer = ( + // scope closes before later tests in the enclosing suite run. + return Rs.describe("", () => { + Rs.beforeAll( +- () => runPromise(Effect.asVoid(contextEffect)), ++ buildContext, + hookTimeout(options?.timeout) + ) + Rs.afterAll( +@@ -282,7 +306,7 @@ export const layer = ( + + return Rs.describe(args[0], () => { + Rs.beforeAll( +- () => runPromise(Effect.asVoid(contextEffect)), ++ buildContext, + hookTimeout(options?.timeout) + ) + Rs.afterAll( diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 8942877cc..4cdf46c4a 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -9,16 +9,15 @@ overrides: '@tanstack/react-router': 1.170.25 '@tanstack/router-core': 1.171.21 '@effect/opentelemetry': 4.0.0-beta.107 - '@effect/vitest': 4.0.0-beta.107 effect: 4.0.0-beta.107 node-fetch: ^3.3.2 patchedDependencies: '@better-fetch/fetch@1.3.1': 9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747 - '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12': 92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c + '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12': 8f49154e0dd132ce88a4583fc0a0b726c1bf3ba4f3862ad65f8b77971fc0242b '@bleedingdev/modern-js-builder@3.8.2-ultramodern.12': c5bdbbf89a17e39cb43c17f66a1904ed29a1462b09fd19aaf05c7301cbe4601c '@bleedingdev/modern-js-code-tools@3.8.2-ultramodern.12': 227ad960c0ce793da1dee90eeaba8edc310b14a58334be185c7cce71ae59a110 - '@bleedingdev/modern-js-create@3.8.2-ultramodern.12': 2e3af68a4da1baca903853057cd804d197a52bfe2c35e864cf12bad79878670b + '@bleedingdev/modern-js-create@3.8.2-ultramodern.12': fe09c7875888067ae4ab161d302024d95f45d52844f6fb9268db1214a35f84f5 '@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12': e96021e5be6d0ee85e6656b9110807d08e4b8dda295d17933606ff0472b61ed0 '@bleedingdev/modern-js-server-utils@3.8.2-ultramodern.12': 254be68a353f0c3a57aed91447a2de7b86afe26d1badeb9786945fd466fe760d '@module-federation/bridge-react@2.8.0': 54bfc79e097473222f83cbfa6d717792e3026bf16b5097c2ed91715b7da126be @@ -26,6 +25,7 @@ patchedDependencies: '@tanstack/router-core@1.171.21': 413c2453d06aa521ed65ab7fcfb16bac8700e58e97693c2ba4d40727d7c9790d '@vercel/nft@0.29.2': c0ed4897b98e9055716031187bb8ea16739f6ae0843d35e4873f1a177472cac7 drizzle-orm@1.0.0-rc.5-ab785fc: b99614c9074d41f359743addc0f2a131dbd9c9c479d57cc0f0bb2bb1344bf5fe + effect-rstest@0.1.0: aa7e505a2b575757ce3951715189b3cd30d90b7a57c17669f9f89340e751f138 effect@4.0.0-beta.107: 88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98 importers: @@ -54,6 +54,9 @@ importers: '@effect/tsgo': specifier: 0.19.0 version: 0.19.0 + '@modern-js/app-tools': + specifier: npm:@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12 + version: '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=8f49154e0dd132ce88a4583fc0a0b726c1bf3ba4f3862ad65f8b77971fc0242b)(1de7912af751fa74e815801433c09f46)' '@modern-js/code-tools': specifier: npm:@bleedingdev/modern-js-code-tools@3.8.2-ultramodern.12 version: '@bleedingdev/modern-js-code-tools@3.8.2-ultramodern.12(patch_hash=227ad960c0ce793da1dee90eeaba8edc310b14a58334be185c7cce71ae59a110)(oxlint-tsgolint@7.0.2001)' @@ -62,7 +65,7 @@ importers: version: 2.6.9(supports-color@10.2.2) '@modern-js/create': specifier: npm:@bleedingdev/modern-js-create@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-create@3.8.2-ultramodern.12(patch_hash=2e3af68a4da1baca903853057cd804d197a52bfe2c35e864cf12bad79878670b)(oxlint@1.79.0(oxlint-tsgolint@7.0.2001))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)' + version: '@bleedingdev/modern-js-create@3.8.2-ultramodern.12(patch_hash=fe09c7875888067ae4ab161d302024d95f45d52844f6fb9268db1214a35f84f5)(oxlint@1.79.0(oxlint-tsgolint@7.0.2001))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)' '@modern-js/plugin-bff': specifier: npm:@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12 version: '@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12(patch_hash=e96021e5be6d0ee85e6656b9110807d08e4b8dda295d17933606ff0472b61ed0)(3a233a8c5baa0ff66c13c170d030459d)' @@ -75,6 +78,9 @@ importers: '@oxlint/plugins': specifier: 1.79.0 version: 1.79.0 + '@rstest/core': + specifier: 0.11.11 + version: 0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3) '@types/node': specifier: 20.19.43 version: 20.19.43 @@ -87,9 +93,15 @@ importers: '@typescript/native-preview': specifier: npm:typescript@7.0.2 version: typescript@7.0.2 + '@vercel/nft': + specifier: 0.29.2 + version: 0.29.2(patch_hash=c0ed4897b98e9055716031187bb8ea16739f6ae0843d35e4873f1a177472cac7)(supports-color@10.2.2) effect: specifier: 4.0.0-beta.107 version: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) + effect-rstest: + specifier: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc + version: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc(patch_hash=aa7e505a2b575757ce3951715189b3cd30d90b7a57c17669f9f89340e751f138)(@rstest/core@0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3))(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)) esbuild: specifier: 0.28.1 version: 0.28.1 @@ -176,7 +188,7 @@ importers: version: '@bleedingdev/modern-js-plugin-i18n@3.8.2-ultramodern.12(@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(i18next@26.3.6(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@modern-js/plugin-tanstack': specifier: npm:@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12(@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893))(@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' + version: '@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12(@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=8f49154e0dd132ce88a4583fc0a0b726c1bf3ba4f3862ad65f8b77971fc0242b)(c95492ca4d79fdd0565fb79cc4972893))(@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' '@modern-js/runtime': specifier: npm:@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12 version: '@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' @@ -222,10 +234,10 @@ importers: version: 5.20260810.1 '@modern-js/adapter-rstest': specifier: npm:@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12(914587a8ba3a134cde2c523e045244cd)' + version: '@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12(2ed8dc7978631e6ad66c75a70c01a221)' '@modern-js/app-tools': specifier: npm:@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893)' + version: '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=8f49154e0dd132ce88a4583fc0a0b726c1bf3ba4f3862ad65f8b77971fc0242b)(c95492ca4d79fdd0565fb79cc4972893)' '@playwright/test': specifier: 1.61.0 version: 1.61.0 @@ -233,8 +245,8 @@ importers: specifier: ^2.0.3 version: 2.0.3(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) '@rstest/core': - specifier: 0.11.10 - version: 0.11.10(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3) + specifier: 0.11.11 + version: 0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3) '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 @@ -262,6 +274,9 @@ importers: drizzle-kit: specifier: 1.0.0-rc.5-ab785fc version: 1.0.0-rc.5-ab785fc + effect-rstest: + specifier: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc + version: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc(patch_hash=aa7e505a2b575757ce3951715189b3cd30d90b7a57c17669f9f89340e751f138)(@rstest/core@0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3))(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)) happy-dom: specifier: 20.8.3 version: 20.8.3 @@ -299,6 +314,9 @@ importers: specifier: 8.22.0 version: 8.22.0 devDependencies: + '@rstest/core': + specifier: 0.11.11 + version: 0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3) '@types/node': specifier: ^20.19.43 version: 20.19.43 @@ -308,6 +326,9 @@ importers: drizzle-kit: specifier: 1.0.0-rc.5-ab785fc version: 1.0.0-rc.5-ab785fc + effect-rstest: + specifier: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc + version: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc(patch_hash=aa7e505a2b575757ce3951715189b3cd30d90b7a57c17669f9f89340e751f138)(@rstest/core@0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3))(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)) packages/gateway-principal-verifier: dependencies: @@ -324,9 +345,15 @@ importers: specifier: 6.2.5 version: 6.2.5 devDependencies: + '@rstest/core': + specifier: 0.11.11 + version: 0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3) '@types/node': specifier: 20.19.43 version: 20.19.43 + effect-rstest: + specifier: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc + version: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc(patch_hash=aa7e505a2b575757ce3951715189b3cd30d90b7a57c17669f9f89340e751f138)(@rstest/core@0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3))(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)) packages/shared-contracts: dependencies: @@ -340,9 +367,15 @@ importers: specifier: 4.0.0-beta.107 version: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) devDependencies: + '@rstest/core': + specifier: 0.11.11 + version: 0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3) '@types/node': specifier: 20.19.43 version: 20.19.43 + effect-rstest: + specifier: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc + version: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc(patch_hash=aa7e505a2b575757ce3951715189b3cd30d90b7a57c17669f9f89340e751f138)(@rstest/core@0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3))(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)) packages/shared-design-tokens: {} @@ -374,7 +407,7 @@ importers: version: '@bleedingdev/modern-js-plugin-i18n@3.8.2-ultramodern.12(@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(i18next@26.3.6(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@modern-js/plugin-tanstack': specifier: npm:@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12(@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893))(@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' + version: '@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12(@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=8f49154e0dd132ce88a4583fc0a0b726c1bf3ba4f3862ad65f8b77971fc0242b)(c95492ca4d79fdd0565fb79cc4972893))(@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' '@modern-js/runtime': specifier: npm:@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12 version: '@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' @@ -414,16 +447,16 @@ importers: version: 0.19.0 '@modern-js/adapter-rstest': specifier: npm:@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12(914587a8ba3a134cde2c523e045244cd)' + version: '@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12(2ed8dc7978631e6ad66c75a70c01a221)' '@modern-js/app-tools': specifier: npm:@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12 - version: '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893)' + version: '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=8f49154e0dd132ce88a4583fc0a0b726c1bf3ba4f3862ad65f8b77971fc0242b)(c95492ca4d79fdd0565fb79cc4972893)' '@rsbuild/plugin-tailwindcss': specifier: ^2.0.3 version: 2.0.3(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) '@rstest/core': - specifier: 0.11.10 - version: 0.11.10(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3) + specifier: 0.11.11 + version: 0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3) '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 @@ -448,6 +481,9 @@ importers: drizzle-kit: specifier: 1.0.0-rc.5-ab785fc version: 1.0.0-rc.5-ab785fc + effect-rstest: + specifier: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc + version: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc(patch_hash=aa7e505a2b575757ce3951715189b3cd30d90b7a57c17669f9f89340e751f138)(@rstest/core@0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3))(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)) fast-check: specifier: 4.9.0 version: 4.9.0 @@ -3602,8 +3638,8 @@ packages: '@rsbuild/core': ^1.0.0 || ^2.0.0 '@rstest/core': ^0.11.0 - '@rstest/core@0.11.10': - resolution: {integrity: sha512-x/PNGPdyKQWbiVhpoOQco8xXevh4P/QamuyJ0/YdTrdEiUcUglT6tGSnxaudwyrQr8e/5gwWuOt6zykZKWmSUg==} + '@rstest/core@0.11.11': + resolution: {integrity: sha512-7Ii2/2/ex1Oa0Kg5Qk5RKx2+e0I83BHLwH0/gn1B7IDwXCh5msFCF7yTyVGFuMltlm3E8Cf4QD7jadOEaBfXfw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -5439,6 +5475,13 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + effect-rstest@https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc: + resolution: {integrity: sha512-vAoaIALD1PEKZDHdX8RdFKzJQtV5FTTzmGt9+xSbjbCJyPX/yKXvL9kJlR9QjSXRxGwFcxy+v+QxEK2ja7JKpA==, tarball: https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc} + version: 0.1.0 + peerDependencies: + '@rstest/core': ^0.11.11 + effect: 4.0.0-beta.107 + effect@4.0.0-beta.107: resolution: {integrity: sha512-OoBAv8eF+yanc+C6xhgEUnWeXUSHA6ynnscYqpkAY9GSnzZWystsIjBowVqCkLpHGlnRtdIqYT3wHwpOY6JDnQ==} @@ -7454,10 +7497,6 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} - engines: {node: '>=12'} - picomatch@4.0.7: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} @@ -8913,18 +8952,6 @@ packages: utf-8-validate: optional: true - ws@8.21.1: - resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.21.3: resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} engines: {node: '>=10.0.0'} @@ -9237,10 +9264,10 @@ snapshots: '@better-fetch/fetch@1.3.1(patch_hash=9276628e25c79910215b343c608abe336b3636f093565a04953069224d890747)': {} - '@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12(914587a8ba3a134cde2c523e045244cd)': + '@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12(2ed8dc7978631e6ad66c75a70c01a221)': dependencies: - '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893)' - '@rstest/adapter-rsbuild': 0.11.9(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rstest/core@0.11.10(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3)) + '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=8f49154e0dd132ce88a4583fc0a0b726c1bf3ba4f3862ad65f8b77971fc0242b)(c95492ca4d79fdd0565fb79cc4972893)' + '@rstest/adapter-rsbuild': 0.11.9(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rstest/core@0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3)) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -9271,7 +9298,63 @@ snapshots: - utf-8-validate - webpack - '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893)': + '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=8f49154e0dd132ce88a4583fc0a0b726c1bf3ba4f3862ad65f8b77971fc0242b)(1de7912af751fa74e815801433c09f46)': + dependencies: + '@babel/parser': 8.0.4 + '@babel/traverse': 8.0.4 + '@babel/types': 8.0.4 + '@loadable/component': 5.16.7(react@19.2.8) + '@modern-js/builder': '@bleedingdev/modern-js-builder@3.8.2-ultramodern.12(patch_hash=c5bdbbf89a17e39cb43c17f66a1904ed29a1462b09fd19aaf05c7301cbe4601c)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@module-federation/runtime-tools@2.8.0)(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rsbuild-plugin-rsc@0.1.1(@rsbuild/core@2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)))(supports-color@10.2.2)(tslib@2.8.1)(typescript@7.0.2)(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26))' + '@modern-js/i18n-utils': '@bleedingdev/modern-js-i18n-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/plugin': '@bleedingdev/modern-js-plugin@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/plugin-data-loader': '@bleedingdev/modern-js-plugin-data-loader@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/prod-server': '@bleedingdev/modern-js-prod-server@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server': '@bleedingdev/modern-js-server@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(debug@4.3.7(supports-color@10.2.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)(tsconfig-paths@4.2.0)(typescript@7.0.2)' + '@modern-js/server-core': '@bleedingdev/modern-js-server-core@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@modern-js/server-utils': '@bleedingdev/modern-js-server-utils@3.8.2-ultramodern.12(patch_hash=254be68a353f0c3a57aed91447a2de7b86afe26d1badeb9786945fd466fe760d)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)' + '@modern-js/types': '@bleedingdev/modern-js-types@3.8.2-ultramodern.12' + '@modern-js/utils': '@bleedingdev/modern-js-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + '@rsbuild/core': 2.2.0-rc.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) + '@swc/core': 1.15.43(@swc/helpers@0.5.23) + '@swc/helpers': 0.5.23 + compression-webpack-plugin: 12.0.0(webpack@5.108.4(@swc/core@1.15.43(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@8.0.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)) + es-module-lexer: 2.3.1 + esbuild: 0.28.1 + flatted: 3.4.4 + import-meta-resolve: 4.2.0 + mlly: 1.8.2 + ndepe: 0.1.13(supports-color@10.2.2) + pkg-types: 2.3.1 + std-env: 4.2.0 + optionalDependencies: + tsconfig-paths: 4.2.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + - '@module-federation/runtime-tools' + - '@parcel/css' + - '@rspack/core' + - '@swc/css' + - '@typescript/native-preview' + - bufferutil + - clean-css + - core-js + - csso + - debug + - devcert + - lightningcss + - react + - react-dom + - react-server-dom-rspack + - rollup + - rsbuild-plugin-rsc + - supports-color + - tslib + - typescript + - utf-8-validate + - webpack + + '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=8f49154e0dd132ce88a4583fc0a0b726c1bf3ba4f3862ad65f8b77971fc0242b)(c95492ca4d79fdd0565fb79cc4972893)': dependencies: '@babel/parser': 8.0.4 '@babel/traverse': 8.0.4 @@ -9476,7 +9559,7 @@ snapshots: - react - react-dom - '@bleedingdev/modern-js-create@3.8.2-ultramodern.12(patch_hash=2e3af68a4da1baca903853057cd804d197a52bfe2c35e864cf12bad79878670b)(oxlint@1.79.0(oxlint-tsgolint@7.0.2001))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)': + '@bleedingdev/modern-js-create@3.8.2-ultramodern.12(patch_hash=fe09c7875888067ae4ab161d302024d95f45d52844f6fb9268db1214a35f84f5)(oxlint@1.79.0(oxlint-tsgolint@7.0.2001))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@10.2.2)': dependencies: '@modern-js/codesmith': 2.6.9(supports-color@10.2.2) '@modern-js/i18n-utils': '@bleedingdev/modern-js-i18n-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' @@ -9635,9 +9718,9 @@ snapshots: - '@module-federation/runtime-tools' - core-js - '@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12(@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893))(@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)': + '@bleedingdev/modern-js-plugin-tanstack@3.8.2-ultramodern.12(@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=8f49154e0dd132ce88a4583fc0a0b726c1bf3ba4f3862ad65f8b77971fc0242b)(c95492ca4d79fdd0565fb79cc4972893))(@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)': dependencies: - '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c)(c95492ca4d79fdd0565fb79cc4972893)' + '@modern-js/app-tools': '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12(patch_hash=8f49154e0dd132ce88a4583fc0a0b726c1bf3ba4f3862ad65f8b77971fc0242b)(c95492ca4d79fdd0565fb79cc4972893)' '@modern-js/plugin': '@bleedingdev/modern-js-plugin@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' '@modern-js/runtime': '@bleedingdev/modern-js-runtime@3.8.2-ultramodern.12(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(react-dom@19.2.8(react@19.2.8))(react-server-dom-rspack@0.1.0(@rspack/core@2.2.0(@module-federation/runtime-tools@2.8.0)(@swc/helpers@0.5.23))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)' '@modern-js/runtime-utils': '@bleedingdev/modern-js-runtime-utils@3.8.2-ultramodern.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' @@ -11527,7 +11610,7 @@ snapshots: dependencies: '@types/estree': 1.0.9 estree-walker: 2.0.2 - picomatch: 4.0.5 + picomatch: 4.0.7 '@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)': dependencies: @@ -11976,12 +12059,12 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - '@rstest/adapter-rsbuild@0.11.9(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rstest/core@0.11.10(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3))': + '@rstest/adapter-rsbuild@0.11.9(@rsbuild/core@2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0))(@rstest/core@0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3))': dependencies: '@rsbuild/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) - '@rstest/core': 0.11.10(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3) + '@rstest/core': 0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3) - '@rstest/core@0.11.10(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3)': + '@rstest/core@0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3)': dependencies: '@rsbuild/core': 2.2.0(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0) '@types/chai': 5.2.3 @@ -12658,7 +12741,7 @@ snapshots: glob: 10.5.0 graceful-fs: 4.2.11 node-gyp-build: 4.8.4 - picomatch: 4.0.5 + picomatch: 4.0.7 resolve-from: 5.0.0 transitivePeerDependencies: - rollup @@ -13847,6 +13930,11 @@ snapshots: eastasianwidth@0.2.0: {} + effect-rstest@https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc(patch_hash=aa7e505a2b575757ce3951715189b3cd30d90b7a57c17669f9f89340e751f138)(@rstest/core@0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3))(effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98)): + dependencies: + '@rstest/core': 0.11.11(@module-federation/runtime-tools@2.8.0)(core-js@3.49.0)(happy-dom@20.8.3) + effect: 4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98) + effect@4.0.0-beta.107(patch_hash=88f3b14039aa2f5b39430af1b0034f5dfed1b4d4191bef99e8dfc58fc9ab3e98): dependencies: '@standard-schema/spec': 1.1.0 @@ -14424,10 +14512,6 @@ snapshots: dependencies: walk-up-path: 4.0.0 - fdir@6.5.0(picomatch@4.0.5): - optionalDependencies: - picomatch: 4.0.5 - fdir@6.5.0(picomatch@4.0.7): optionalDependencies: picomatch: 4.0.7 @@ -14660,12 +14744,12 @@ snapshots: happy-dom@20.8.3: dependencies: - '@types/node': 26.4.1 + '@types/node': 20.19.43 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 7.0.1 whatwg-mimetype: 3.0.0 - ws: 8.21.1 + ws: 8.21.3 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -16094,8 +16178,6 @@ snapshots: picomatch@2.3.2: {} - picomatch@4.0.5: {} - picomatch@4.0.7: {} pkg-types@1.3.1: @@ -17253,8 +17335,8 @@ snapshots: tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 tinypool@2.1.0: {} @@ -17638,8 +17720,6 @@ snapshots: ws@8.21.0: {} - ws@8.21.1: {} - ws@8.21.3: {} wsl-utils@0.1.0: diff --git a/app/pnpm-workspace.yaml b/app/pnpm-workspace.yaml index 87d92b794..0d1ef9889 100644 --- a/app/pnpm-workspace.yaml +++ b/app/pnpm-workspace.yaml @@ -103,13 +103,11 @@ strictDepBuilds: true peerDependencyRules: allowedVersions: react: '>=19.0.0' - '@effect/vitest>effect': 4.0.0-beta.107 overrides: react-server-dom-rspack: 0.1.0 '@tanstack/react-router': 1.170.25 '@tanstack/router-core': 1.171.21 '@effect/opentelemetry': 4.0.0-beta.107 - '@effect/vitest': 4.0.0-beta.107 effect: 4.0.0-beta.107 node-fetch: ^3.3.2 allowBuilds: @@ -136,3 +134,4 @@ patchedDependencies: '@better-fetch/fetch@1.3.1': patches/@better-fetch__fetch@1.3.1.patch '@bleedingdev/modern-js-code-tools@3.8.2-ultramodern.12': patches/@bleedingdev__modern-js-code-tools@3.8.2-ultramodern.12.patch drizzle-orm@1.0.0-rc.5-ab785fc: patches/drizzle-orm-rc5-declarations.patch + effect-rstest@0.1.0: patches/effect-rstest@0.1.0.patch diff --git a/app/quality-audit/knip-model.mts b/app/quality-audit/knip-model.mts index f3ac2a355..64dc1c751 100644 --- a/app/quality-audit/knip-model.mts +++ b/app/quality-audit/knip-model.mts @@ -191,6 +191,39 @@ const objectValue = (object: ObjectExpression | undefined, name: string): Node | return property?.type === 'Property' ? property.value : undefined; }; +const rstestEnvironmentEvidence = (facts: SourceFacts, workspace: string): KnipModelEvidence[] => { + if (!/rstest\.config\.[cm]?[jt]s$/u.test(facts.file)) { + return []; + } + const config = exportedObject(facts); + const projects = unwrap(objectValue(config, 'projects'), facts.variables); + const configurations = [config]; + if (projects?.type === 'ArrayExpression') { + for (const element of projects.elements) { + const project = unwrap(element ?? undefined, facts.variables); + if (project?.type === 'ObjectExpression') { + configurations.push(project); + } + } + } + return configurations.flatMap((object) => { + const environment = objectValue(object, 'testEnvironment'); + const target = staticString(environment, facts.variables); + return target === undefined || target === 'node' + ? [] + : [ + evidenceAt( + facts, + workspace, + 'dependency', + target, + environment?.start ?? 0, + 'Rstest testEnvironment consumer', + ), + ]; + }); +}; + const exportLeaves = (value: typeof ExportsSchema.Type | undefined): string[] => { if (isString(value)) { return [value]; @@ -567,6 +600,19 @@ const isChildProcessMake = (node: Node): boolean => node.object.name === 'ChildProcess' && propertyName(node.property) === 'make'; +const isJoinedSourceSpecifier = ( + node: Node | undefined, + directory: string, + file: string, + variables: ReadonlyMap, +): boolean => + node?.type === 'CallExpression' && + node.callee.type === 'MemberExpression' && + propertyName(node.callee.property) === 'join' && + node.arguments[0]?.type === 'Identifier' && + node.arguments[0].name === directory && + staticString(node.arguments[1], variables) === file; + const sourceEvidence = ( facts: SourceFacts, workspace: string, @@ -656,54 +702,75 @@ const sourceEvidence = ( } }; const recordLintConfig = (node: Extract) => { + const consumers = new Map([ + ['tools/oxlint/effect-native/report.mts', 'report.config.ts'], + [ + 'tools/oxlint/effect-native/tests/repository-policy.test.mts', + 'repository-policy.config.ts', + ], + ]); + const config = consumers.get(facts.file); + const [joined] = node.arguments; if ( - facts.file === 'tools/oxlint/effect-native/report.mts' && - node.callee.type === 'Identifier' && - node.callee.name === 'runOxlint' + config === undefined || + node.callee.type !== 'Identifier' || + node.callee.name !== 'runOxlint' || + joined?.type !== 'CallExpression' || + staticString(joined.arguments[1], facts.variables) !== config ) { - const [joined] = node.arguments; - if ( - joined?.type === 'CallExpression' && - staticString(joined.arguments[1], facts.variables) === 'report.config.ts' - ) { - result.push( - evidenceAt( - facts, - workspace, - 'file', - 'tools/oxlint/effect-native/report.config.ts', - node.start, - 'Lint report subprocess configuration', - ), - evidenceAt( - facts, - workspace, - 'export', - 'tools/oxlint/effect-native/report.config.ts#default', - node.start, - 'Oxlint loader consumes the configuration default export', - ), - ); - } + return; } + const target = `tools/oxlint/effect-native/${config}`; + result.push( + evidenceAt(facts, workspace, 'file', target, node.start, 'Lint subprocess configuration'), + evidenceAt( + facts, + workspace, + 'export', + `${target}#default`, + node.start, + 'Oxlint loader consumes the configuration default export', + ), + ); + }; + const recordLintPlugin = (node: ObjectExpression) => { + const specifier = objectValue(node, 'specifier'); + if ( + facts.file !== 'tools/oxlint/effect-native/tests/shared-helpers.test.mts' || + staticString(objectValue(node, 'name'), facts.variables) !== 'shared-helpers-probe' || + !isJoinedSourceSpecifier( + specifier, + 'testsDirectory', + 'shared-helpers-probe.ts', + facts.variables, + ) + ) { + return; + } + const target = 'tools/oxlint/effect-native/tests/shared-helpers-probe.ts'; + result.push( + evidenceAt(facts, workspace, 'file', target, node.start, 'Oxlint jsPlugins source specifier'), + evidenceAt( + facts, + workspace, + 'export', + `${target}#default`, + node.start, + 'Oxlint jsPlugins loader consumes only the plugin default export', + ), + ); }; const recordCall = (node: Extract) => { recordSubprocess(node); recordConfiguredFile(node); recordLintConfig(node); }; - new Visitor({ CallExpression: recordCall, NewExpression: recordUrl }).visit(facts.program); - if (/rstest\.config\.[cm]?[jt]s$/u.test(facts.file)) { - const target = staticString( - objectValue(exportedObject(facts), 'testEnvironment'), - facts.variables, - ); - if (target !== undefined && target !== 'node') { - result.push( - evidenceAt(facts, workspace, 'dependency', target, 0, 'Rstest testEnvironment consumer'), - ); - } - } + new Visitor({ + CallExpression: recordCall, + NewExpression: recordUrl, + ObjectExpression: recordLintPlugin, + }).visit(facts.program); + result.push(...rstestEnvironmentEvidence(facts, workspace)); if (facts.file === 'scripts/quality-audit.mts') { const recordAuditStep = (node: ObjectExpression) => { const tool = staticString(objectValue(node, 'tool'), facts.variables); diff --git a/app/quality-audit/knip-runtime-model.mts b/app/quality-audit/knip-runtime-model.mts index 97ffadf48..9040f1110 100644 --- a/app/quality-audit/knip-runtime-model.mts +++ b/app/quality-audit/knip-runtime-model.mts @@ -33,6 +33,9 @@ const Tsconfig = Schema.Struct({ types: Schema.optional(Schema.Array(Schema.String)), }), ), + exclude: Schema.optional(Schema.Array(Schema.String)), + files: Schema.optional(Schema.Array(Schema.String)), + include: Schema.optional(Schema.Array(Schema.String)), }); const parseTsconfig = Effect.fn('QualityAudit.parseTsconfig')(function* parseTsconfigEffect( file: string, @@ -204,6 +207,76 @@ export const buildKnipRuntimeEvidence = Effect.fn('QualityAudit.buildKnipRuntime } } }); + const testConsumerEvidence = Effect.fn('QualityAudit.testConsumerEvidence')( + function* testConsumerEvidence( + manifestFile: string, + manifestText: string, + commands: readonly string[], + prefix: string, + workspace: string, + ) { + // Rstest's default-config loader consumes default, not every named export. + const config = `${prefix}rstest.config.ts`; + if ( + commands.some( + (command) => /^rstest(?:\s|$)/u.test(command) && !/--config(?:\s|=)/u.test(command), + ) && + (yield* read(config)) !== undefined + ) { + evidence.push( + at( + manifestFile, + manifestText, + 0, + workspace, + 'file', + 'rstest.config.ts', + 'Package script invokes Rstest default configuration discovery', + ), + at( + manifestFile, + manifestText, + 0, + workspace, + 'export', + `${config}#default`, + 'Rstest default-config loader consumes the default export', + ), + ); + } + const tsconfigFile = `${prefix}tsconfig.json`; + const tsconfigText = yield* read(tsconfigFile); + if (tsconfigText === undefined) { + return; + } + const tsconfig = yield* parseTsconfig(tsconfigFile, tsconfigText); + // Only the explicit directory inclusion contract is modeled here. Do not + // turn all compiler inputs into roots or mark their exports as consumed. + if ( + tsconfig.include?.includes('src') !== true || + tsconfig.exclude !== undefined || + tsconfig.files !== undefined || + !(yield* fs.exists(path.join(appRoot, prefix, 'src'))) + ) { + return; + } + for (const name of yield* fs.readDirectory(path.join(appRoot, prefix, 'src'))) { + if (name.endsWith('.type-test.ts')) { + evidence.push( + at( + tsconfigFile, + tsconfigText, + 0, + workspace, + 'file', + `src/${name}`, + 'TypeScript include src compiles this type-test module; exports remain audited', + ), + ); + } + } + }, + ); const cssEvidence = Effect.fn('QualityAudit.cssEvidence')(function* cssEvidence( extension: string, prefix: string, @@ -466,7 +539,9 @@ export const buildKnipRuntimeEvidence = Effect.fn('QualityAudit.buildKnipRuntime continue; } const manifest = yield* Schema.decodeUnknownEffect(Manifest)(manifestText); - for (const command of Object.values(manifest.scripts ?? {})) { + const commands = Object.values(manifest.scripts ?? {}); + yield* testConsumerEvidence(manifestFile, manifestText, commands, prefix, workspace); + for (const command of commands) { yield* shellEvidence(command, prefix, workspace); } for (const extension of ['tsx', 'ts', 'jsx', 'js']) { diff --git a/app/rstest.config.ts b/app/rstest.config.ts new file mode 100644 index 000000000..3e0e143ca --- /dev/null +++ b/app/rstest.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from '@rstest/core'; + +// SWC rejects every generic arrow function in `.mts` files (even `(...)`) under its +// default mts/cts parser mode, and Rstest bundles the imported scripts through SWC. +const swc = { jsc: { parser: { disallowAmbiguousJsxLike: false, syntax: 'typescript' } } } as const; + +const shared = { testEnvironment: 'node', testTimeout: 120_000, tools: { swc } } as const; + +export default defineConfig({ + projects: [ + { + ...shared, + exclude: ['scripts/scaffolding/**'], + include: ['scripts/**/*.test.mts'], + name: 'scripts', + }, + { + ...shared, + include: ['scripts/scaffolding/tests/**/*.test.mts'], + name: 'generation', + }, + { + ...shared, + include: ['tools/oxlint/effect-native/tests/*.test.mts'], + name: 'lint-rules', + }, + ], +}); diff --git a/app/scripts/audit-database-trust-boundaries.mts b/app/scripts/audit-database-trust-boundaries.mts index b4a7358ea..7730fa04c 100644 --- a/app/scripts/audit-database-trust-boundaries.mts +++ b/app/scripts/audit-database-trust-boundaries.mts @@ -69,8 +69,8 @@ export const auditDatabaseTrustBoundaries = (): Effect.Effect< (error) => new DatabaseTrustBoundaryAuditError({ reason: - error instanceof DatabaseTargetMismatchError || - error instanceof DatabaseSessionIdentityError + Schema.is(DatabaseTargetMismatchError)(error) || + Schema.is(DatabaseSessionIdentityError)(error) ? error.message : 'Database trust-boundary evidence could not be collected', }), diff --git a/app/scripts/check-database-access-boundaries.mts b/app/scripts/check-database-access-boundaries.mts index a019935fd..35dbe7a7e 100644 --- a/app/scripts/check-database-access-boundaries.mts +++ b/app/scripts/check-database-access-boundaries.mts @@ -5,7 +5,6 @@ import { Effect, Exit, FileSystem, - flow, ManagedRuntime, Order, Path, @@ -376,7 +375,7 @@ const compareViolations = ( const ViolationOrder = Order.make(compareViolations); -const checkDatabaseAccessBoundariesEffect = (root: string) => +export const checkDatabaseAccessBoundaries = (root: string) => Effect.gen(function* checkDatabaseAccessBoundariesProgram() { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -422,15 +421,6 @@ const checkDatabaseAccessBoundariesEffect = (root: string) => return EffectArray.sort(violations, ViolationOrder); }); -const databaseAccessBoundaryRuntime = ManagedRuntime.make(NodeServices.layer); - -export const checkDatabaseAccessBoundaries: ( - root: string, -) => Promise = flow( - checkDatabaseAccessBoundariesEffect, - databaseAccessBoundaryRuntime.runPromise, -); - class DatabaseAccessBoundaryCheckFailed extends Schema.TaggedError()( 'DatabaseAccessBoundaryCheckFailed', { violationCount: Schema.Number }, @@ -438,7 +428,7 @@ class DatabaseAccessBoundaryCheckFailed extends Schema.TaggedError 0) { yield* Effect.all( violations.map((violation) => @@ -455,6 +445,7 @@ const main = Effect.gen(function* databaseAccessBoundaryMain() { const [, invokedPath] = process.argv; if (invokedPath === import.meta.filename) { + const databaseAccessBoundaryRuntime = ManagedRuntime.make(NodeServices.layer); const exit = await databaseAccessBoundaryRuntime.runPromiseExit(main); process.exitCode = Exit.isSuccess(exit) ? 0 : 1; } diff --git a/app/scripts/check-ontos-module-contracts.mts b/app/scripts/check-ontos-module-contracts.mts index 6b67a1dbd..9119b5a9b 100644 --- a/app/scripts/check-ontos-module-contracts.mts +++ b/app/scripts/check-ontos-module-contracts.mts @@ -1,7 +1,7 @@ #!/usr/bin/env node import path from 'node:path'; import { pathToFileURL } from 'node:url'; -import { NodeFileSystem, NodeRuntime } from '@effect/platform-node'; +import { NodeServices, NodeRuntime } from '@effect/platform-node'; import { Effect, Equal, FileSystem, Layer, Schema } from 'effect'; import { ONTOS_MODULE_CONTRACT_MAX_BYTES, @@ -369,14 +369,12 @@ const checkVertical = (workspaceRoot: string, vertical: TopologyVertical, contra if (!contractUrl.endsWith(ONTOS_MODULE_CONTRACT_PATH)) { return yield* failure(`${appId} development module-contract URL is invalid`); } - const derived = yield* Effect.tryPromise({ - catch: () => failure(`${appId} authored module contract could not be derived`), - try: async () => - await deriveOntosModuleDeploymentContract({ - vertical: verticalName, - workspaceRoot, - }), - }); + const derived = yield* deriveOntosModuleDeploymentContract({ + vertical: verticalName, + workspaceRoot, + }).pipe( + Effect.mapError(() => failure(`${appId} authored module contract could not be derived`)), + ); if (derived.deployment.appId !== appId || derived.manifest.module.id !== manifestModuleId) { return yield* failure( `${appId} authored module contract disagrees with generated owner metadata`, @@ -443,6 +441,6 @@ if ( checkOntosModuleContracts().pipe( Effect.tap(() => Effect.logInfo('OntOS module contracts validated')), ), - ).pipe(Layer.provide(NodeFileSystem.layer)); + ).pipe(Layer.provide(NodeServices.layer)); NodeRuntime.runMain(Effect.scoped(Layer.build(programLayer))); } diff --git a/app/scripts/database-trust-audit/collect-snapshot.mts b/app/scripts/database-trust-audit/collect-snapshot.mts index 48ac14572..0598cd443 100644 --- a/app/scripts/database-trust-audit/collect-snapshot.mts +++ b/app/scripts/database-trust-audit/collect-snapshot.mts @@ -235,7 +235,7 @@ export const collectSnapshot = Effect.fn('collectSnapshot')(function* collectSna const runtimeEndpoint = getEffectiveDatabaseEndpoint(runtime); yield* Effect.try({ catch: (cause) => - cause instanceof DatabaseTargetMismatchError + Schema.is(DatabaseTargetMismatchError)(cause) ? cause : new DatabaseTrustBoundarySnapshotError({ code: 'database_target_identity_unavailable', @@ -259,7 +259,7 @@ export const collectSnapshot = Effect.fn('collectSnapshot')(function* collectSna }); yield* Effect.try({ catch: (cause) => - cause instanceof DatabaseSessionIdentityError + Schema.is(DatabaseSessionIdentityError)(cause) ? cause : new DatabaseTrustBoundarySnapshotError({ code: 'database_session_identity_unavailable', diff --git a/app/scripts/database-trust-audit/report.mts b/app/scripts/database-trust-audit/report.mts index a1473b3ba..068c76988 100644 --- a/app/scripts/database-trust-audit/report.mts +++ b/app/scripts/database-trust-audit/report.mts @@ -1,3 +1,4 @@ +// oxlint-disable-next-line max-classes-per-file -- This report owns the three related tagged audit failures. import type { Client } from 'pg'; import { Cause, Option, Schema } from 'effect'; @@ -198,15 +199,15 @@ export class DatabaseTrustBoundaryAuditError extends Schema.TaggedError()( +export class DatabaseTargetMismatchError extends Schema.TaggedError()( 'DatabaseTargetMismatchError', { message: Schema.String }, -); +) {} -export const DatabaseSessionIdentityError = Schema.TaggedError()( +export class DatabaseSessionIdentityError extends Schema.TaggedError()( 'DatabaseSessionIdentityError', { message: Schema.String }, -); +) {} export const genericAuditFailureMessage = 'Database trust-boundary audit failed'; diff --git a/app/scripts/generate-ontos-module-contract.mts b/app/scripts/generate-ontos-module-contract.mts index 2ada412f6..87ed20987 100644 --- a/app/scripts/generate-ontos-module-contract.mts +++ b/app/scripts/generate-ontos-module-contract.mts @@ -4,17 +4,7 @@ import { createRequire } from 'node:module'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { NodeServices } from '@effect/platform-node'; -import { - Effect, - Exit, - FileSystem, - flow, - ManagedRuntime, - Path, - Predicate, - Schema, - Stream, -} from 'effect'; +import { Effect, Exit, FileSystem, ManagedRuntime, Path, Predicate, Schema, Stream } from 'effect'; import { Command, Flag } from 'effect/unstable/cli'; import { HttpApi } from 'effect/unstable/httpapi'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; @@ -28,7 +18,6 @@ import { extractVerticalRuntimeSafeDescriptors, } from '../packages/core-runtime/src/index.ts'; import type { - OntosModuleDeploymentContract, OntosModuleManifest, VerticalRuntimeRegistration, } from '../packages/core-runtime/src/index.ts'; @@ -134,7 +123,6 @@ const outputRootByTarget: Readonly> = const sha256 = (value: string): string => createHash('sha256').update(value).digest('hex'); const require = createRequire(import.meta.url); -const moduleContractRuntime = ManagedRuntime.make(NodeServices.layer); const failure = (message: string, cause?: unknown): OntosModuleContractGenerationError => new OntosModuleContractGenerationError({ cause, message }); @@ -478,7 +466,7 @@ const deriveContract = (workspaceRoot: string, vertical: string, owner: LoadedOw ); }); -const deriveOntosModuleDeploymentContractEffect = (input: DeriveOntosModuleContractInput) => +export const deriveOntosModuleDeploymentContract = (input: DeriveOntosModuleContractInput) => Effect.gen(function* deriveDeploymentContractProgram() { const platformPath = yield* Path.Path; const workspaceRoot = platformPath.resolve( @@ -492,14 +480,8 @@ const deriveOntosModuleDeploymentContractEffect = (input: DeriveOntosModuleContr }); /** Derives and validates one contract without writing deployment output. */ -export const deriveOntosModuleDeploymentContract: ( - input: DeriveOntosModuleContractInput, -) => Promise = flow( - deriveOntosModuleDeploymentContractEffect, - moduleContractRuntime.runPromise, -); -const generateOntosModuleContractEffect = (input: GenerateInput) => +export const generateOntosModuleContract = (input: GenerateInput) => Effect.gen(function* generateContractProgram() { const fileSystem = yield* FileSystem.FileSystem; const platformPath = yield* Path.Path; @@ -511,7 +493,7 @@ const generateOntosModuleContractEffect = (input: GenerateInput) => input.target, ).pipe(Effect.mapError(() => failure('target must be dist or cloudflare-dist'))); const verticalDirectory = platformPath.join(workspaceRoot, 'verticals', vertical); - const contract = yield* deriveOntosModuleDeploymentContractEffect({ vertical, workspaceRoot }); + const contract = yield* deriveOntosModuleDeploymentContract({ vertical, workspaceRoot }); const encodedContract = yield* Schema.encodeEffect(ContractJsonTextSchema)(contract).pipe( Effect.mapError((cause) => failure('unable to encode the OntOS module contract', cause)), ); @@ -564,20 +546,13 @@ const generateOntosModuleContractEffect = (input: GenerateInput) => return { bytes, etag, path: outputPath }; }); -export const generateOntosModuleContract: ( - input: GenerateInput, -) => Promise<{ readonly bytes: number; readonly etag: string; readonly path: string }> = flow( - generateOntosModuleContractEffect, - moduleContractRuntime.runPromise, -); - const verticalFlag = Flag.string('vertical'); const targetFlag = Flag.choice('target', ['cloudflare-dist', 'dist']); const cli = Command.make( 'generate-ontos-module-contract', { target: targetFlag, vertical: verticalFlag }, ({ target, vertical }) => - generateOntosModuleContractEffect({ target, vertical }).pipe( + generateOntosModuleContract({ target, vertical }).pipe( Effect.flatMap((result) => Effect.logInfo(`Generated ${result.path} (${result.bytes} bytes, ETag ${result.etag})`), ), @@ -588,6 +563,7 @@ if ( process.argv[1] !== undefined && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href ) { + const moduleContractRuntime = ManagedRuntime.make(NodeServices.layer); const exit = await moduleContractRuntime.runPromiseExit( Command.run({ version: '1.0.0' })(cli).pipe(Effect.tapError((error) => Effect.logError(error))), ); diff --git a/app/scripts/initialize-local-development.mts b/app/scripts/initialize-local-development.mts index dc26abe3d..449238359 100644 --- a/app/scripts/initialize-local-development.mts +++ b/app/scripts/initialize-local-development.mts @@ -8,7 +8,7 @@ import { import { createHash } from 'node:crypto'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; -import { NodeFileSystem, NodePath } from '@effect/platform-node'; +import { NodeServices } from '@effect/platform-node'; import { v1 } from '@authzed/authzed-node'; import { betterAuth } from 'better-auth'; import { verifyPassword } from 'better-auth/crypto'; @@ -336,14 +336,14 @@ export const deriveActivatedModuleIds = ( const contracts = yield* Effect.forEach( activatedVerticals, (vertical) => - Effect.tryPromise({ - catch: () => + deriveContract({ vertical, workspaceRoot }).pipe( + Effect.mapError(() => failure( 'local_contract_invalid', `The ${vertical} deployment contract could not be derived`, ), - try: async () => await deriveContract({ vertical, workspaceRoot }), - }), + ), + ), { concurrency: 'unbounded' }, ); const moduleIds = contracts.map((contract) => contract.manifest.module.id); @@ -756,7 +756,7 @@ export const initializeLocalDevelopment = ( ): Effect.Effect< LocalDevelopmentInitializationResult, LocalDevelopmentInitializationError, - FileSystem.FileSystem | Path.Path + NodeServices.NodeServices > => Effect.gen(function* initialize() { const configuration = @@ -833,9 +833,7 @@ if ( import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href ) { const succeeded = await Effect.runPromise( - runLocalDevelopmentInitialization.pipe( - Effect.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)), - ), + runLocalDevelopmentInitialization.pipe(Effect.provide(NodeServices.layer)), ); if (!succeeded) { process.exitCode = 1; diff --git a/app/scripts/local-environment-values.test.mts b/app/scripts/local-environment-values.test.mts index 867a83a71..e9fd8c7c0 100644 --- a/app/scripts/local-environment-values.test.mts +++ b/app/scripts/local-environment-values.test.mts @@ -1,12 +1,11 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { localPublicClientValues, localSpiceDbValues } from './local-environment-values.mts'; const spiceDbGrpcPort = '50052'; const spiceDbHttpPort = '8444'; const spiceDbEndpoint = `localhost:${spiceDbGrpcPort}`; -await test('preserves canonical SpiceDB values when no local override is supplied', () => { +it('preserves canonical SpiceDB values when no local override is supplied', () => { const values = localSpiceDbValues( [ `SPICEDB_ENDPOINT=${spiceDbEndpoint}`, @@ -18,7 +17,7 @@ await test('preserves canonical SpiceDB values when no local override is supplie {}, ); - assert.deepEqual(values, { + expect(values).toEqual({ SPICEDB_ENDPOINT: spiceDbEndpoint, SPICEDB_GRPC_PORT: spiceDbGrpcPort, SPICEDB_HTTP_PORT: spiceDbHttpPort, @@ -27,33 +26,32 @@ await test('preserves canonical SpiceDB values when no local override is supplie }); }); -await test('applies explicit local port overrides as one consistent endpoint', () => { +it('applies explicit local port overrides as one consistent endpoint', () => { const values = localSpiceDbValues( ['SPICEDB_ENDPOINT=localhost:50051', 'SPICEDB_GRPC_PORT=50051'], { grpcPort: spiceDbGrpcPort, httpPort: spiceDbHttpPort }, ); - assert.equal(values.SPICEDB_ENDPOINT, spiceDbEndpoint); - assert.equal(values.SPICEDB_GRPC_PORT, spiceDbGrpcPort); - assert.equal(values.SPICEDB_HTTP_PORT, spiceDbHttpPort); + expect(values.SPICEDB_ENDPOINT).toBe(spiceDbEndpoint); + expect(values.SPICEDB_GRPC_PORT).toBe(spiceDbGrpcPort); + expect(values.SPICEDB_HTTP_PORT).toBe(spiceDbHttpPort); }); -await test('derives local public-client URLs from configured Shell identity/port and Party API URL', () => { - assert.deepEqual( +it('derives local public-client URLs from configured Shell identity/port and Party API URL', () => { + expect( localPublicClientValues([], { partyRegistryApiBaseUrl: 'http://localhost:4199/party-api', shellId: 'staff-shell', shellPort: 3099, }), - { - ONTOS_PARTY_REGISTRY_API_BASE_URL: 'http://localhost:4199/party-api', - ONTOS_SHELL_GATEWAY_BASE_URL: 'http://localhost:3099/staff-shell-api', - }, - ); + ).toEqual({ + ONTOS_PARTY_REGISTRY_API_BASE_URL: 'http://localhost:4199/party-api', + ONTOS_SHELL_GATEWAY_BASE_URL: 'http://localhost:3099/staff-shell-api', + }); }); -await test('preserves explicitly configured public-client URLs', () => { - assert.deepEqual( +it('preserves explicitly configured public-client URLs', () => { + expect( localPublicClientValues( [ 'ONTOS_SHELL_GATEWAY_BASE_URL=https://gateway.example.test/shell-super-app-api', @@ -65,9 +63,8 @@ await test('preserves explicitly configured public-client URLs', () => { shellPort: 3020, }, ), - { - ONTOS_PARTY_REGISTRY_API_BASE_URL: 'https://party.example.test/party-registry-api', - ONTOS_SHELL_GATEWAY_BASE_URL: 'https://gateway.example.test/shell-super-app-api', - }, - ); + ).toEqual({ + ONTOS_PARTY_REGISTRY_API_BASE_URL: 'https://party.example.test/party-registry-api', + ONTOS_SHELL_GATEWAY_BASE_URL: 'https://gateway.example.test/shell-super-app-api', + }); }); diff --git a/app/scripts/migrate-contacts-authorization.mts b/app/scripts/migrate-contacts-authorization.mts index 3f173e6b0..31ec5c6d8 100644 --- a/app/scripts/migrate-contacts-authorization.mts +++ b/app/scripts/migrate-contacts-authorization.mts @@ -505,7 +505,7 @@ const acquireSpiceDbClient = (configuration: SpiceDbConfigValue) => (client) => Effect.sync(() => client.close()), ); -const migrateContactsAuthorizationEffect = ( +const migrateContactsAuthorization = ( mode: ContactsAuthorizationMigrationMode, ): Effect.Effect => Effect.gen(function* migrateContactsAuthorizationProgram() { @@ -534,14 +534,12 @@ const migrateContactsAuthorizationEffect = ( }; }).pipe(Effect.scoped); -const migrationRuntime = ManagedRuntime.make(NodeServices.layer); - const command = Command.make( 'migrate-contacts-authorization', { mode: Argument.choice('mode', ['prepare', 'verify', 'finalize']) }, ({ mode }) => Effect.gen(function* migrateContactsAuthorizationCommand() { - const result = yield* migrateContactsAuthorizationEffect(mode).pipe( + const result = yield* migrateContactsAuthorization(mode).pipe( Effect.tapError((error) => Console.error(error.message)), ); yield* Console.log( @@ -552,6 +550,7 @@ const command = Command.make( const [, invokedPath] = process.argv; if (invokedPath !== undefined && import.meta.url === pathToFileURL(invokedPath).href) { + const migrationRuntime = ManagedRuntime.make(NodeServices.layer); const exit = await migrationRuntime.runPromiseExit(Command.run(command, { version: '1.0.0' })); process.exitCode = Exit.isSuccess(exit) ? 0 : 1; } diff --git a/app/scripts/prepare-dev-module-contract.mts b/app/scripts/prepare-dev-module-contract.mts index a4c37049a..525f347c4 100644 --- a/app/scripts/prepare-dev-module-contract.mts +++ b/app/scripts/prepare-dev-module-contract.mts @@ -31,19 +31,19 @@ const prepareDevModuleContractCommand = Command.make( const fileSystem = yield* FileSystem.FileSystem; const pathService = yield* Path.Path; const workspaceRoot = yield* pathService.fromFileUrl(new URL('..', import.meta.url)); - const generated = yield* Effect.tryPromise({ - catch: (cause) => - new ModuleContractPreparationError({ - cause, - reason: `Unable to generate the ${vertical} development module contract`, - }), - try: async () => - await generateOntosModuleContract({ - target: 'dist', - vertical, - workspaceRoot, - }), - }); + const generated = yield* generateOntosModuleContract({ + target: 'dist', + vertical, + workspaceRoot, + }).pipe( + Effect.mapError( + (cause) => + new ModuleContractPreparationError({ + cause, + reason: `Unable to generate the ${vertical} development module contract`, + }), + ), + ); const publicDirectory = pathService.join(workspaceRoot, 'verticals', vertical, '.dev-public'); const contractDirectory = pathService.join(publicDirectory, '.well-known'); yield* fileSystem.makeDirectory(contractDirectory, { recursive: true }); diff --git a/app/scripts/proof-workerd-ssr.mts b/app/scripts/proof-workerd-ssr.mts index 930fad823..f268fecbe 100644 --- a/app/scripts/proof-workerd-ssr.mts +++ b/app/scripts/proof-workerd-ssr.mts @@ -154,6 +154,7 @@ type JsonObject = typeof FragmentPropsSchema.Type; type ProofFailure = PlatformError | WorkerdProofError; type ProofEffect = Effect.Effect; type ScopedProofEffect = Effect.Effect; +// oxlint-disable-next-line effect-native/no-promise-shaped-port -- Miniflare requires this foreign SDK service-binding callback. type ServiceBindingHandler = ( request: MiniflareRequest, miniflare: Miniflare, diff --git a/app/scripts/provision-current-action-authorization.mts b/app/scripts/provision-current-action-authorization.mts index 3ffae2a37..dda9a6a07 100644 --- a/app/scripts/provision-current-action-authorization.mts +++ b/app/scripts/provision-current-action-authorization.mts @@ -9,9 +9,7 @@ import { Duration, Effect, FileSystem, - flow, Layer, - ManagedRuntime, Option, Order, Path, @@ -157,13 +155,13 @@ const decodeRepositoryInventory = (workspaceRoot: string) => return { ownership, topology }; }); -const discoverCurrentActionsEffect = ( +export const discoverCurrentActions = ( workspaceRoot: string, deriveContract: DeriveContract = deriveOntosModuleDeploymentContract, ): Effect.Effect< readonly ActionAuthorizationProvisioningAction[], ActionAuthorizationProvisioningError, - FileSystem.FileSystem | Path.Path + NodeServices.NodeServices > => Effect.gen(function* discoverCurrentActionsEffectGenerator() { const path = yield* Path.Path; @@ -192,10 +190,10 @@ const discoverCurrentActionsEffect = ( const contracts = yield* Effect.forEach( verticals, ({ id }) => - Effect.tryPromise({ - catch: discoveryFailure, - try: async () => await deriveContract({ vertical: id, workspaceRoot }), - }).pipe(Effect.map((contract) => ({ contract, id }))), + deriveContract({ vertical: id, workspaceRoot }).pipe( + Effect.mapError(discoveryFailure), + Effect.map((contract) => ({ contract, id })), + ), { concurrency: 'unbounded' }, ); const collectVerticalActions = Effect.gen(function* collectVerticalActionsEffect() { @@ -237,39 +235,18 @@ const discoverCurrentActionsEffect = ( return actions; }).pipe(Effect.catchDefect(discoveryFailure)); -const repositoryRuntime = ManagedRuntime.make(NodeServices.layer); - -const discoverCurrentActionsProgram = ( - workspaceRoot: string, - deriveContract: DeriveContract = deriveOntosModuleDeploymentContract, -): Effect.Effect< - readonly ActionAuthorizationProvisioningAction[], - ActionAuthorizationProvisioningError, - FileSystem.FileSystem | Path.Path -> => discoverCurrentActionsEffect(workspaceRoot, deriveContract); - -export const discoverCurrentActions = flow( - discoverCurrentActionsProgram, - repositoryRuntime.runPromise, -); - -const discoverCurrentActionKeysProgram = ( +export const discoverCurrentActionKeys = ( workspaceRoot: string, deriveContract: DeriveContract = deriveOntosModuleDeploymentContract, ): Effect.Effect< readonly string[], ActionAuthorizationProvisioningError, - FileSystem.FileSystem | Path.Path + NodeServices.NodeServices > => - discoverCurrentActionsEffect(workspaceRoot, deriveContract).pipe( + discoverCurrentActions(workspaceRoot, deriveContract).pipe( Effect.map((actions) => actions.map(({ actionKey }) => actionKey)), ); -export const discoverCurrentActionKeys = flow( - discoverCurrentActionKeysProgram, - repositoryRuntime.runPromise, -); - interface CloseableProvisioningClient extends ActionAuthorizationProvisioningClient { readonly close: () => void; } @@ -334,7 +311,7 @@ const runCurrentActionAuthorizationProvisioningWithServices = ( ): Effect.Effect< ActionAuthorizationProvisioningResult & { readonly environment: 'development' | 'stage' }, ActionAuthorizationProvisioningError, - FileSystem.FileSystem | Path.Path + NodeServices.NodeServices > => Effect.gen(function* runCurrentActionAuthorizationProvisioningEffect() { if (commandArguments.length > 0) { @@ -352,7 +329,7 @@ const runCurrentActionAuthorizationProvisioningWithServices = ( ), ); const target = yield* selectActionAuthorizationProvisioningTarget(configuration); - const actions = yield* discoverCurrentActionsEffect(workspaceRoot); + const actions = yield* discoverCurrentActions(workspaceRoot); const client = yield* acquireProvisioningClient(target.configuration); const result = yield* provisionActionAuthorization(client, { actions, @@ -373,7 +350,7 @@ export function runCurrentActionAuthorizationProvisioning( ): Effect.Effect< ActionAuthorizationProvisioningResult & { readonly environment: 'development' | 'stage' }, ActionAuthorizationProvisioningError, - FileSystem.FileSystem | Path.Path + NodeServices.NodeServices >; export function runCurrentActionAuthorizationProvisioning( workspaceRoot: string, @@ -381,7 +358,7 @@ export function runCurrentActionAuthorizationProvisioning( ): Effect.Effect< ActionAuthorizationProvisioningResult & { readonly environment: 'development' | 'stage' }, ActionAuthorizationProvisioningError, - FileSystem.FileSystem | Path.Path + NodeServices.NodeServices > { return runCurrentActionAuthorizationProvisioningWithServices(workspaceRoot, commandArguments); } diff --git a/app/scripts/quality-audit.mts b/app/scripts/quality-audit.mts index fea41bc8b..162227c3f 100644 --- a/app/scripts/quality-audit.mts +++ b/app/scripts/quality-audit.mts @@ -717,7 +717,9 @@ const executeStep = Effect.fn('qualityAudit.executeStep')(function* executeStepE const processHandle = yield* spawner.spawn( ChildProcess.make(process.execPath, [binary, ...step.args], { cwd: root, - env: { NO_COLOR: '1' }, + // Inherited FORCE_COLOR overrides NO_COLOR and makes Node emit a warning. + // Disable forcing at the subprocess boundary; keep real diagnostics intact. + env: { FORCE_COLOR: '0', NO_COLOR: '1' }, extendEnv: true, stderr: 'pipe', stdin: 'ignore', diff --git a/app/scripts/scaffolding/cli.mts b/app/scripts/scaffolding/cli.mts index eb530051e..0a46b111c 100644 --- a/app/scripts/scaffolding/cli.mts +++ b/app/scripts/scaffolding/cli.mts @@ -4,7 +4,7 @@ import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { CodeSmith, FsMaterial, GeneratorCore } from '@modern-js/codesmith'; import type { GeneratorContext } from '@modern-js/codesmith'; -import { Console, Effect, flow, Option, Predicate, Schema } from 'effect'; +import { Console, Effect, Option, Predicate, Schema } from 'effect'; import { Argument, CliConfig, Command, Flag, GlobalFlag } from 'effect/unstable/cli'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; import actionGenerator from './action/scaffold.mts'; @@ -23,7 +23,6 @@ import resourceGenerator from './resource/scaffold.mts'; import retireContributionGenerator from './retire-contribution/scaffold.mts'; import searchProviderGenerator from './search-provider/scaffold.mts'; import searchProviderAccessGenerator from './search-provider-access/generator.mts'; -import { scaffoldingRuntime } from '../scaffolding-runtime.mts'; import type { ActionScaffoldConfig, ActionScaffoldResult, @@ -122,14 +121,16 @@ type TypedGeneratorContext = Omit & { type LocalGenerator = ( context: TypedGeneratorContext, core: GeneratorCore, -) => Promise; +) => Effect.Effect; export interface RouteRefreshInput { readonly appId: string; readonly workspaceRoot: string; } -export type RouteRefreshExecutor = (input: RouteRefreshInput) => void | Promise; +export type RouteRefreshExecutor = ( + input: RouteRefreshInput, +) => Effect.Effect; export interface RunScaffoldOptions { readonly routeRefresh?: RouteRefreshExecutor; @@ -174,7 +175,7 @@ interface CommandDefinition { readonly generate: ( flags: ParsedScaffoldFlags, workspaceRoot: string, - ) => Effect.Effect; + ) => Effect.Effect; readonly help: string; readonly requiredFlags: readonly string[]; } @@ -210,7 +211,7 @@ const runCodesmithGenerator = Effect.fn('runCodesmithGenerator')( generator: LocalGenerator, workspaceRoot: string, config: Config, - ): Effect.fn.Return { + ): Effect.fn.Return { const prepared = yield* Effect.try({ catch: (cause) => new ScaffoldingError({ cause, message: 'failed to prepare the Codesmith generator' }), @@ -231,14 +232,24 @@ const runCodesmithGenerator = Effect.fn('runCodesmithGenerator')( return { core, generatorContext }; }, }); - const result = yield* Effect.tryPromise({ - catch: (cause) => - new ScaffoldingError({ - cause, - message: cause instanceof Error ? cause.message : 'Codesmith generation failed', - }), - try: async () => await generator(prepared.generatorContext, prepared.core), - }).pipe(Effect.ensuring(Effect.sync(() => (prepared.core._context.current = null)))); + const result = yield* generator(prepared.generatorContext, prepared.core).pipe( + Effect.catchDefect((cause) => + Effect.fail( + new ScaffoldingError({ + cause, + message: Predicate.isError(cause) ? cause.message : 'Codesmith generation failed', + }), + ), + ), + Effect.mapError( + (cause) => + new ScaffoldingError({ + cause, + message: Predicate.isError(cause) ? cause.message : 'Codesmith generation failed', + }), + ), + Effect.ensuring(Effect.sync(() => (prepared.core._context.current = null))), + ); return result; }, ); @@ -477,14 +488,7 @@ Options: if (options.routeRefresh === undefined) { return defaultRouteRefresh(input); } - return Effect.tryPromise({ - catch: (cause) => - new ScaffoldingError({ - cause, - message: Predicate.isError(cause) ? cause.message : 'route refresh failed', - }), - try: async () => await options.routeRefresh?.(input), - }); + return options.routeRefresh(input); }; yield* refresh({ appId: result.appId, workspaceRoot }); yield* refresh({ appId: 'shell-super-app', workspaceRoot }); @@ -939,11 +943,11 @@ const parseFlags = ( }; }); -const runScaffoldEffect = Effect.fn('runScaffold')(function* runScaffoldEffectGenerator( +export const runScaffoldEffect = Effect.fn('runScaffold')(function* runScaffoldEffectGenerator( command: ScaffoldCommand, rawArguments: readonly string[], options: RunScaffoldOptions = {}, -): Effect.fn.Return { +): Effect.fn.Return { const argumentsList = normalizeForwardedArguments(rawArguments); if (argumentsList.length === 1 && argumentsList[0] === '--help') { return { help: getHelpText(command), kind: 'help' }; @@ -958,12 +962,6 @@ const runScaffoldEffect = Effect.fn('runScaffold')(function* runScaffoldEffectGe return { kind: 'generated', result }; }); -export const runScaffold: ( - command: ScaffoldCommand, - rawArguments: readonly string[], - options?: RunScaffoldOptions, -) => Promise = flow(runScaffoldEffect, scaffoldingRuntime.runPromise); - const optionalTextFlag = (name: string) => Flag.string(name).pipe(Flag.optional); const forwardedArguments = Argument.variadic(Argument.string('forwarded flags')); const cliFlags = { @@ -1053,6 +1051,6 @@ if (entryPath !== undefined && import.meta.url === pathToFileURL(path.resolve(en }), CliConfig.CliConfig, () => CliConfig.make({ builtIns: [customHelp] }), - ).pipe(Effect.ensuring(scaffoldingRuntime.disposeEffect)); + ); await Effect.runPromise(cliProgram.pipe(Effect.provide(NodeServices.layer))); } diff --git a/app/scripts/scaffolding/generator-adapter.mts b/app/scripts/scaffolding/generator-adapter.mts index bd015f52c..5927d461c 100644 --- a/app/scripts/scaffolding/generator-adapter.mts +++ b/app/scripts/scaffolding/generator-adapter.mts @@ -1,7 +1,6 @@ import type { NodeServices } from '@effect/platform-node'; import type { GeneratorContext, GeneratorCore } from '@modern-js/codesmith'; -import { Effect, flow, Schema } from 'effect'; -import { scaffoldingRuntime } from '../scaffolding-runtime.mts'; +import { Effect } from 'effect'; import { applyMutationPlanEffect } from './shared.mts'; import type { ScaffoldPlan } from './shared.mts'; @@ -14,56 +13,18 @@ type EffectScaffoldPlanner = ( config: Config, ) => Effect.Effect, PlannerError, Services>; -type PromiseScaffoldPlanner = ( - workspaceRoot: string, - config: Config, -) => Promise>; - -type CodesmithGenerator = ( - context: TypedGeneratorContext, - core: GeneratorCore, -) => Promise; - -class GeneratorAdapterFailure extends Schema.TaggedError()( - 'GeneratorAdapterFailure', - { - cause: Schema.Unknown, - message: Schema.String, - }, -) {} - -export function createCodesmithGenerator< +export const createCodesmithGenerator = < Config, Result, PlannerError, Services extends NodeServices.NodeServices, >( planner: EffectScaffoldPlanner, -): CodesmithGenerator; -export function createCodesmithGenerator( - planner: PromiseScaffoldPlanner, -): CodesmithGenerator; -export function createCodesmithGenerator< - Config, - Result, - PlannerError, - Services extends NodeServices.NodeServices, ->( - planner: - | EffectScaffoldPlanner - | PromiseScaffoldPlanner, -): CodesmithGenerator { - const codesmithGeneratorEffect = (context: TypedGeneratorContext, core: GeneratorCore) => - Effect.gen(function* planAndApplyScaffold() { - const planned = planner(core.outputPath, context.config); - const plan = Effect.isEffect(planned) - ? yield* planned - : yield* Effect.tryPromise({ - catch: (cause) => - new GeneratorAdapterFailure({ cause, message: 'The scaffold planner failed' }), - try: async () => await planned, - }); - return yield* applyMutationPlanEffect(core, plan); - }); - return flow(codesmithGeneratorEffect, scaffoldingRuntime.runPromise); -} +) => + Effect.fn('planAndApplyScaffold')(function* planAndApplyScaffold( + context: TypedGeneratorContext, + core: GeneratorCore, + ) { + const plan = yield* planner(core.outputPath, context.config); + return yield* applyMutationPlanEffect(core, plan); + }); diff --git a/app/scripts/scaffolding/microvertical-page/scaffold.mts b/app/scripts/scaffolding/microvertical-page/scaffold.mts index e15e909fb..8083a2941 100644 --- a/app/scripts/scaffolding/microvertical-page/scaffold.mts +++ b/app/scripts/scaffolding/microvertical-page/scaffold.mts @@ -1,4 +1,4 @@ -import { Effect, Equal, FileSystem, Schema } from 'effect'; +import { Effect, Equal, FileSystem, Schema, Predicate } from 'effect'; import { createCodesmithGenerator } from '../generator-adapter.mts'; import { tailwindPrefixForNamespace } from '../tailwind-prefix.mts'; import { @@ -13,8 +13,8 @@ import { MODULE_REGISTRATION_PAGE_SLOT_END, MODULE_REGISTRATION_PAGE_SLOT_START, asJsonObject, - createMutation, - discoverOntosModule, + createMutationEffect as createSharedMutation, + discoverOntosModuleEffect as discoverSharedModule, ensureUniqueMutationPaths, generatedSlotContainsExactEntry, insertModuleFederationExposure, @@ -22,7 +22,7 @@ import { isModuleManifestImport, moduleFederationExposureSource, patchJsonObjectProperty, - readJson, + readJsonEffect as readSharedJson, readGeneratedSlotEntries, requireCanonicalSlug, requiredString, @@ -80,42 +80,29 @@ const pageScaffoldFailure = (message: string, cause?: unknown): PageScaffoldErro new PageScaffoldError(cause === undefined ? { message } : { cause, message }); const pageScaffoldFailureFromUnknown = (cause: unknown, fallback: string): PageScaffoldError => - cause instanceof PageScaffoldError + Schema.is(PageScaffoldError)(cause) ? cause - : pageScaffoldFailure(cause instanceof Error ? cause.message : fallback, cause); + : pageScaffoldFailure(Predicate.isError(cause) ? cause.message : fallback, cause); -const discoverOntosModuleEffect = ( - workspaceRoot: string, - requestedVertical: string, -): Effect.Effect => - Effect.tryPromise({ - catch: (cause) => +const discoverOntosModuleEffect = (workspaceRoot: string, requestedVertical: string) => + discoverSharedModule(workspaceRoot, requestedVertical).pipe( + Effect.mapError((cause) => pageScaffoldFailureFromUnknown( cause, `vertical ${requestedVertical} could not be discovered`, ), - try: async () => await discoverOntosModule(workspaceRoot, requestedVertical), - }); + ), + ); -const readJsonEffect = ( - filePath: string, - description: string, - fallback: string, -): Effect.Effect>, PageScaffoldError> => - Effect.tryPromise({ - catch: (cause) => pageScaffoldFailureFromUnknown(cause, fallback), - try: async () => await readJson(filePath, description), - }); +const readJsonEffect = (filePath: string, description: string, fallback: string) => + readSharedJson(filePath, description).pipe( + Effect.mapError((cause) => pageScaffoldFailureFromUnknown(cause, fallback)), + ); -const createMutationEffect = ( - filePath: string, - content: string, - fallback: string, -): Effect.Effect => - Effect.tryPromise({ - catch: (cause) => pageScaffoldFailureFromUnknown(cause, fallback), - try: async () => await createMutation(filePath, content), - }); +const createMutationEffect = (filePath: string, content: string, fallback: string) => + createSharedMutation(filePath, content).pipe( + Effect.mapError((cause) => pageScaffoldFailureFromUnknown(cause, fallback)), + ); const mapFileSystemError = ( operation: Effect.Effect, @@ -233,7 +220,7 @@ const validateLocale = ( namespace: string, packageExports: JsonObject, locale: string, -): Effect.Effect => +): Effect.Effect => Effect.gen(function* validateLocaleEffect() { const expectedExport = `./locales/${locale}/${namespace}.json`; if (packageExports[`./locales/${locale}`] !== expectedExport) { @@ -421,7 +408,7 @@ const renderReadAuthorization = ( const validateReadAuthorization = ( config: Pick, -): Effect.Effect => +): Effect.Effect => Effect.gen(function* validateReadAuthorizationEffect() { if ( config.authorization === 'context_permission' && @@ -709,7 +696,7 @@ const patchLocale = ( vertical: PageVerticalMetadata, locale: string, page: string, -): Effect.Effect => +): Effect.Effect => Effect.gen(function* patchLocaleEffect() { const localePath = resolveContainedPath( workspaceRoot, @@ -1074,7 +1061,7 @@ const generatedLocaleState = ( vertical: PageVerticalMetadata, locale: string, pageKey: string, -): Effect.Effect => +): Effect.Effect => Effect.gen(function* generatedLocaleStateEffect() { const localePath = resolveContainedPath( workspaceRoot, diff --git a/app/scripts/scaffolding/policy/scaffold.mts b/app/scripts/scaffolding/policy/scaffold.mts index 04ca1b258..f067e78d5 100644 --- a/app/scripts/scaffolding/policy/scaffold.mts +++ b/app/scripts/scaffolding/policy/scaffold.mts @@ -3,7 +3,7 @@ import { createCodesmithGenerator } from '../generator-adapter.mts'; import { CORE_POLICY_SLOT_END, CORE_POLICY_SLOT_START, - discoverOntosModule, + discoverOntosModuleEffect, ensureUniqueMutationPaths, insertSortedSlot, requireCanonicalSlug, @@ -36,11 +36,6 @@ const fromLegacySync = ( ): Effect.Effect => Effect.try({ catch: planningFailure, try: operation }); -const fromLegacyPromise = ( - operation: () => Promise, -): Effect.Effect => - Effect.tryPromise({ catch: planningFailure, try: operation }); - const createPolicyMutation = ( filePath: string, content: string, @@ -167,8 +162,8 @@ const planPolicyScaffold = Effect.fn('PolicyScaffold.planPolicyScaffold')( ); } const requestedVertical = config.vertical; - const vertical = yield* fromLegacyPromise( - discoverOntosModule.bind(undefined, workspaceRoot, requestedVertical), + const vertical = yield* discoverOntosModuleEffect(workspaceRoot, requestedVertical).pipe( + Effect.mapError(planningFailure), ); const policyPath = yield* fromLegacySync(() => resolveContainedPath( diff --git a/app/scripts/scaffolding/shared.mts b/app/scripts/scaffolding/shared.mts index 68592d3b8..c59beefad 100644 --- a/app/scripts/scaffolding/shared.mts +++ b/app/scripts/scaffolding/shared.mts @@ -1,9 +1,9 @@ import { NodePath } from '@effect/platform-node'; +import { scaffoldingRuntime } from '../scaffolding-runtime.mts'; import type { GeneratorCore } from '@modern-js/codesmith'; -import { Effect, FileSystem, flow, Option, Path, Predicate, Result, Schema } from 'effect'; +import { Effect, FileSystem, Option, Path, Predicate, Result, Schema } from 'effect'; import { format } from 'oxfmt'; import ultraciteOxfmt from 'ultracite/oxfmt'; -import { scaffoldingRuntime } from '../scaffolding-runtime.mts'; import { ONTOS_MODULE_CONTRACT_SCHEMA_VERSION } from '../../packages/core-runtime/src/index.ts'; /* eslint-disable unicorn/prefer-number-coercion -- The schema version is parsed as a base-10 integer by contract. expires: 2026-12-31. */ @@ -835,14 +835,6 @@ export const readJsonEffect = (filePath: string, label: string) => return { content, value: asJsonObject(parsed.success, label) }; }); -export const readJson: ( - filePath: string, - label: string, -) => Promise<{ content: string; value: JsonObject }> = flow( - readJsonEffect, - scaffoldingRuntime.runPromise, -); - interface JsonPropertySpan { readonly key: string; readonly keyStart: number; @@ -1263,14 +1255,6 @@ export const discoverOntosModuleEffect = ( }; }); -export const discoverOntosModule: ( - workspaceRoot: string, - requestedVertical: string, -) => Promise = flow( - discoverOntosModuleEffect, - scaffoldingRuntime.runPromise, -); - const formatGeneratedMutationContent = ( filePath: string, content: string, @@ -1336,11 +1320,6 @@ export const createOrAcceptGeneratedMutationEffect = ( return yield* scaffoldFailure(`refusing to overwrite existing business file: ${filePath}`); }); -export const createMutation: (filePath: string, content: string) => Promise = flow( - createMutationEffect, - scaffoldingRuntime.runPromise, -); - export const updateMutation = ( filePath: string, previous: string, @@ -1469,7 +1448,7 @@ export const applyMutationPlanEffect = ( mutation.content, 'utf-8', ), - }), + }).pipe(Effect.uninterruptible), { concurrency: 'unbounded', discard: true }, ); const fileSystem = yield* FileSystem.FileSystem; diff --git a/app/scripts/scaffolding/tests/fixture-files.mts b/app/scripts/scaffolding/tests/fixture-files.mts index c537daea9..7451b305a 100644 --- a/app/scripts/scaffolding/tests/fixture-files.mts +++ b/app/scripts/scaffolding/tests/fixture-files.mts @@ -1,30 +1,48 @@ +import { Effect } from 'effect'; import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'; import path from 'node:path'; -export const write = async (root: string, relativePath: string, content: string): Promise => { +export const write = Effect.fn(function* writeFixture( + root: string, + relativePath: string, + content: string, +) { const target = path.join(root, relativePath); - await mkdir(path.dirname(target), { recursive: true }); - await writeFile(target, content, 'utf-8'); -}; + yield* Effect.promise(async () => await mkdir(path.dirname(target), { recursive: true })); + yield* Effect.promise(async () => await writeFile(target, content, 'utf-8')); +}); -export const snapshotTree = async ( +const visitTree = ( root: string, - excludedDirectories: readonly string[] = [], -): Promise>> => { - const snapshot: Record = {}; - const visit = async (directory: string): Promise => { - const entries = await readdir(directory, { withFileTypes: true }); - await Promise.all( - entries.map(async (entry) => { + directory: string, + excludedDirectories: readonly string[], + snapshot: Record, +): Effect.Effect => + Effect.gen(function* visitFixtureTree() { + const entries = yield* Effect.promise( + async () => await readdir(directory, { withFileTypes: true }), + ); + yield* Effect.forEach( + entries.toSorted((left, right) => left.name.localeCompare(right.name)), + Effect.fn(function* visitFixtureEntry(entry) { const target = path.join(directory, entry.name); if (entry.isDirectory() && !excludedDirectories.includes(entry.name)) { - await visit(target); + yield* visitTree(root, target, excludedDirectories, snapshot); } else if (entry.isFile()) { - snapshot[path.relative(root, target)] = await readFile(target, 'utf-8'); + snapshot[path.relative(root, target)] = yield* Effect.promise( + async () => await readFile(target, 'utf-8'), + ); } }), + { concurrency: 'unbounded', discard: true }, ); - }; - await visit(root); + }); + +export const snapshotTree = Effect.fn(function* snapshotFixtureTree( + root: string, + excludedDirectories: readonly string[] = [], +) { + const snapshot: Record = {}; + yield* visitTree(root, root, excludedDirectories, snapshot); return snapshot; -}; +}); diff --git a/app/scripts/scaffolding/tests/fixture-ownership.mts b/app/scripts/scaffolding/tests/fixture-ownership.mts new file mode 100644 index 000000000..d958ccb97 --- /dev/null +++ b/app/scripts/scaffolding/tests/fixture-ownership.mts @@ -0,0 +1,24 @@ +import { Effect } from 'effect'; +import { mkdir, rm, symlink } from 'node:fs/promises'; +import path from 'node:path'; + +/** Link only the dependencies a generated subprocess fixture actually needs. */ +export const linkFixtureDependencies = Effect.fn(function* linkFixtureDependencies( + root: string, + appRoot: string, + dependencies: Readonly>, +) { + for (const [name, source] of Object.entries(dependencies)) { + const target = path.join(root, 'node_modules', name); + yield* Effect.promise(async () => await mkdir(path.dirname(target), { recursive: true })); + yield* Effect.promise(async () => await symlink(path.join(appRoot, source), target, 'dir')); + } +}); + +/** Keep fixture ownership local, including cleanup when the scenario fails. */ +export const withCreatedFixture = + (create: Effect.Effect) => + (run: (root: string) => Effect.Effect): Effect.Effect => + Effect.acquireUseRelease(create, run, (root) => + Effect.promise(async () => await rm(root, { force: true, recursive: true })), + ); diff --git a/app/scripts/scaffolding/tests/module-contract-generator.test.mts b/app/scripts/scaffolding/tests/module-contract-generator.test.mts index 17934cf7c..c9826daef 100644 --- a/app/scripts/scaffolding/tests/module-contract-generator.test.mts +++ b/app/scripts/scaffolding/tests/module-contract-generator.test.mts @@ -1,20 +1,39 @@ -import assert from 'node:assert/strict'; -import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; +import { write } from './fixture-files.mts'; +import { linkFixtureDependencies, withCreatedFixture } from './fixture-ownership.mts'; +import { Cause, Effect, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { NodeServices } from '@effect/platform-node'; + +import { mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import test from 'node:test'; -import { NodeFileSystem } from '@effect/platform-node'; -import { Effect, Schema } from 'effect'; -import { runEffectTestPromise } from '../../../packages/core-runtime/src/testing/effect-runtime.ts'; -import { checkOntosModuleContracts } from '../../check-ontos-module-contracts.mts'; import { privateOwnerImportViolation, unconstrainedHttpApiContractSchemaViolation, } from '../../ultramodern-api-boundary-rules.mts'; -import { generateOntosModuleContract } from '../../generate-ontos-module-contract.mts'; -import { getHelpText, runScaffold } from '../cli.mts'; +// Match the native module identity used by the generated fixture's owner bundle. + +import { getHelpText, runScaffoldEffect } from '../cli.mts'; import type { JsonValue } from '../shared.mts'; +const { checkOntosModuleContracts } = await import( + /* webpackIgnore: true */ + '../../check-ontos-module-contracts.mts' +); +const { generateOntosModuleContract } = await import( + /* webpackIgnore: true */ + '../../generate-ontos-module-contract.mts' +); + +const expectFailure = (self: Effect.Effect, check: (cause: unknown) => void) => + Effect.matchCauseEffect(self, { + onFailure: (cause) => Effect.sync(() => check(Cause.squash(cause))), + onSuccess: () => + Effect.sync(() => { + throw new Error('Expected operation to fail'); + }), + }); + const APP_ID = 'property-registry'; const AUTHORIZATION_FLAG = '--authorization'; const DOCUMENTS_APP_ID = 'documents-center'; @@ -55,564 +74,621 @@ const ModuleContractDocumentSchema = Schema.Struct({ schemaVersion: Schema.String, }); const decodeModulePackage = (source: string) => - Schema.decodeUnknownSync(ModulePackageSchema, { onExcessProperty: 'preserve' })( + Schema.decodeUnknownEffect(ModulePackageSchema, { onExcessProperty: 'preserve' })( JSON.parse(source), ); const decodeModuleContract = (source: string) => - Schema.decodeUnknownSync(ModuleContractDocumentSchema, { onExcessProperty: 'preserve' })( + Schema.decodeUnknownEffect(ModuleContractDocumentSchema, { onExcessProperty: 'preserve' })( JSON.parse(source), ); const appRoot = path.resolve(import.meta.dirname, '..', '..', '..'); const json = (value: JsonValue): string => `${JSON.stringify(value, null, 2)}\n`; -const write = async (root: string, relative: string, content: string): Promise => { - const target = path.join(root, relative); - await mkdir(path.dirname(target), { recursive: true }); - await writeFile(target, content, 'utf-8'); -}; - -const writePinnedEffectApi = async (root: string, slug: string): Promise => { - await write( - root, - `verticals/${slug}/shared/api.ts`, - `export const fixtureApi = HttpApi.make('FixtureApi').add(HttpApiGroup.make('fixture'));\n`, - ); - await write( - root, - `verticals/${slug}/api/index.ts`, - `const fixtureLayer = HttpApiBuilder.group(fixtureApi, 'fixture', handlers => handlers); +const writePinnedEffectApi = (root: string, slug: string): Effect.Effect => + Effect.gen(function* mergedScenario1() { + yield* write( + root, + `verticals/${slug}/shared/api.ts`, + `export const fixtureApi = HttpApi.make('FixtureApi').add(HttpApiGroup.make('fixture'));\n`, + ); + yield* write( + root, + `verticals/${slug}/api/index.ts`, + `const fixtureLayer = HttpApiBuilder.group(fixtureApi, 'fixture', handlers => handlers); const layer = HttpApiBuilder.layer(fixtureApi).pipe( Layer.provide(fixtureLayer), ) satisfies EffectRuntimeLayer; export default defineEffectBff({ api: fixtureApi, layer }); `, - ); -}; - -const createFixture = async (): Promise => { - const root = await mkdtemp(path.join(tmpdir(), 'ontos-module-contract-')); - await write(root, 'package.json', json({ name: 'fixture', private: true, type: 'module' })); - await write( - root, - PROPERTY_PACKAGE_PATH, - json({ - dependencies: { zeta: '1.0.0' }, - exports: { '.': './src/index.ts' }, - modernjs: { - apiRuntime: 'effect', - appId: APP_ID, - preset: 'presetUltramodern', - role: 'module-federation-remote', - topology: '../../topology/reference-topology.json', - }, - name: '@app/property-registry', - private: true, - scripts: { - build: 'modern build && MODERNJS_DEPLOY=node modern deploy --skip-build', - 'cloudflare:build': - 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy --skip-build', - existing: 'preserve-me', - }, - type: 'module', - version: '0.1.0', - }), - ); - await write( - root, - 'verticals/property-registry/tsconfig.json', - json({ compilerOptions: { composite: true }, include: ['src', 'shared'], references: [] }), - ); - await write( - root, - 'verticals/property-registry/module-federation.config.ts', - `export default { exposes: {} };\n`, - ); - await write( - root, - 'verticals/documents-center/package.json', - json({ - dependencies: {}, - modernjs: { - appId: DOCUMENTS_APP_ID, - role: 'module-federation-remote', - topology: '../../topology/reference-topology.json', - }, - name: '@app/documents-center', - private: true, - scripts: { - build: 'modern build', - 'cloudflare:build': 'MODERNJS_DEPLOY=cloudflare modern build', - }, - type: 'module', - version: '0.1.0', - }), - ); - await write( - root, - 'verticals/documents-center/tsconfig.json', - json({ compilerOptions: { composite: true }, include: ['src'], references: [] }), - ); - await write( - root, - 'verticals/documents-center/module-federation.config.ts', - 'export default {};\n', - ); - await Promise.all([ - writePinnedEffectApi(root, APP_ID), - writePinnedEffectApi(root, DOCUMENTS_APP_ID), - ]); - await write( - root, - 'topology/reference-topology.json', - json({ - schemaVersion: 1, - verticals: [ - { - deliveryUnit: { buildMarker: 'property-build' }, - domain: 'property', - id: APP_ID, - kind: 'vertical', - moduleFederation: { name: 'verticalPropertyRegistry', role: 'remote' }, - package: '@app/property-registry', - path: 'verticals/property-registry', - }, - { - deliveryUnit: { buildMarker: 'documents-build' }, - domain: 'documents', - id: DOCUMENTS_APP_ID, - kind: 'vertical', - moduleFederation: { name: 'verticalDocumentsCenter', role: 'remote' }, - package: '@app/documents-center', - path: 'verticals/documents-center', - }, - ], - }), - ); - await write( - root, - 'topology/local-overlays/development.json', - json({ - environment: 'development', - ontosModuleManifests: Object.fromEntries([ - [DOCUMENTS_APP_ID, 'http://localhost:4102/.well-known/ontos-module-manifest.json'], - [APP_ID, 'http://localhost:4101/.well-known/ontos-module-manifest.json'], - ]), - schemaVersion: 1, - }), - ); - await mkdir(path.join(root, 'node_modules', '@app'), { recursive: true }); - await symlink( - path.join(appRoot, 'packages/core-runtime'), - path.join(root, 'node_modules/@app/core-runtime'), - 'dir', - ); - await symlink(path.join(appRoot, 'node_modules/effect'), path.join(root, 'node_modules/effect')); - return root; -}; - -const withFixture = async (run: (root: string) => Promise): Promise => { - const root = await createFixture(); - try { - await run(root); - } finally { - await rm(root, { force: true, recursive: true }); - } -}; - -const scaffold = async (root: string, vertical = APP_ID, module = MODULE_ID) => - await runScaffold(MODULE_CONTRACT_COMMAND, [VERTICAL_FLAG, vertical, '--module', module], { - workspaceRoot: root, - }); - -void test('module-contract help is exact and write-free', async () => { - const missingRoot = path.join(tmpdir(), 'module-contract-help-does-not-exist'); - const result = await runScaffold(MODULE_CONTRACT_COMMAND, ['--help'], { - workspaceRoot: missingRoot, - }); - assert.deepEqual(result, { help: getHelpText(MODULE_CONTRACT_COMMAND), kind: 'help' }); - assert.match(result.help, /--vertical --module /u); -}); - -void test('business generators fail closed before the mandatory module contract exists', async () => { - await withFixture(async (root) => { - const commands = [ - [ - 'action', - [ - VERTICAL_FLAG, - APP_ID, - '--action', - 'create-property', - '--legal-entity-scope', - 'optional', - AUTHORIZATION_FLAG, - 'action_execution', - '--provisioning', - 'tenant_membership_default', - ], - ], - ['microvertical-action-boundary', [VERTICAL_FLAG, APP_ID]], - [ - 'microvertical-page', - [ - VERTICAL_FLAG, - APP_ID, - '--page', - 'properties', - AUTHORIZATION_FLAG, - 'context_permission', - '--permission', - 'module.access', - ], - ], - [ - 'outbox-message', - [VERTICAL_FLAG, APP_ID, '--action', 'create-property', '--topic', 'property.created'], - ], - [ - 'outbox-worker', - [ - VERTICAL_FLAG, - APP_ID, - '--worker', - 'property-projector', - '--producer', - DOCUMENTS_APP_ID, - '--topic', - 'document.created', - AUTHORIZATION_FLAG, - 'owner_local_background', - ], - ], - [ - 'policy', - ['--scope', 'microvertical', VERTICAL_FLAG, APP_ID, '--policy', 'property-visible'], - ], - ] as const; - await Promise.all( - commands.map( - async ([command, flags]) => - await assert.rejects( - runScaffold(command, flags, { workspaceRoot: root }), - /requires scaffold:module-contract/u, - ), - ), ); }); -}); -void test('rejects malformed, traversing, duplicate, and overwrite requests without partial writes', async () => { - await withFixture(async (root) => { - await assert.rejects(scaffold(root, '../property', MODULE_ID), /lower-kebab-case/u); - await assert.rejects(scaffold(root, APP_ID, APP_ID), /dotted/u); - await assert.rejects(scaffold(root, APP_ID, 'core.modules'), /non-core/u); - await scaffold(root); - const packageAfterFirst = await readFile(path.join(root, PROPERTY_PACKAGE_PATH), 'utf-8'); - await assert.rejects(scaffold(root), /refusing to overwrite/u); - assert.equal( - await readFile(path.join(root, PROPERTY_PACKAGE_PATH), 'utf-8'), - packageAfterFirst, +const createFixture = (): Effect.Effect => + Effect.gen(function* mergedScenario2() { + const root = yield* Effect.promise(() => + mkdtemp(path.join(tmpdir(), 'ontos-module-contract-')), ); - await assert.rejects(scaffold(root, DOCUMENTS_APP_ID, MODULE_ID), /duplicate OntOS module ID/u); - }); -}); - -void test('generates conservative owner files and patches only package and tsconfig owner metadata', async () => { - await withFixture(async (root) => { - const result = await scaffold(root); - assert.equal(result.kind, 'generated'); - const manifest = await readFile(path.join(root, PROPERTY_MANIFEST_PATH), 'utf-8'); - const registration = await readFile( - path.join(root, 'verticals/property-registry/vertical.registration.ts'), - 'utf-8', + yield* write(root, 'package.json', json({ name: 'fixture', private: true, type: 'module' })); + yield* write( + root, + PROPERTY_PACKAGE_PATH, + json({ + dependencies: { zeta: '1.0.0' }, + exports: { '.': './src/index.ts' }, + modernjs: { + apiRuntime: 'effect', + appId: APP_ID, + preset: 'presetUltramodern', + role: 'module-federation-remote', + topology: '../../topology/reference-topology.json', + }, + name: '@app/property-registry', + private: true, + scripts: { + build: 'modern build && MODERNJS_DEPLOY=node modern deploy --skip-build', + 'cloudflare:build': + 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy --skip-build', + existing: 'preserve-me', + }, + type: 'module', + version: '0.1.0', + }), ); - assert.match(manifest, /@ontos-deployment-app-id property-registry/u); - assert.match(manifest, /@ontos-module-id property\.registry/u); - assert.match(manifest, /defaultState: 'inactive'/u); - assert.doesNotMatch(manifest, /dependencies:|core\.identity|externalSystems/u); - const retiredLifecycleMarkers = [ - ['must', 'be', 'active', 'first'].join('_'), - ['enable', 'together', 'when', 'available'].join('_'), - ['optional', 'enhancement'].join('_'), - ['integration', 'required', 'for', 'api'].join('_'), - ]; - for (const marker of retiredLifecycleMarkers) { - assert.equal(manifest.includes(marker), false); - } - assert.match(manifest, /actions: \[/u); - assert.match(registration, /defineVerticalRuntimeRegistration/u); - assert.match(registration, /generated-module-registration-workers/u); - assert.doesNotMatch(registration, /handler|migration|route/u); - const packageJson = decodeModulePackage( - await readFile(path.join(root, PROPERTY_PACKAGE_PATH), 'utf-8'), + yield* write( + root, + 'verticals/property-registry/tsconfig.json', + json({ compilerOptions: { composite: true }, include: ['src', 'shared'], references: [] }), ); - assert.deepEqual(packageJson.dependencies, { - '@app/core-runtime': 'workspace:*', - zeta: '1.0.0', - }); - assert.deepEqual(packageJson.exports, { '.': './src/index.ts' }); - assert.equal(packageJson.scripts['existing'], 'preserve-me'); - assert.match(packageJson.scripts['build'] ?? '', /--vertical property-registry --target dist/u); - assert.match( - packageJson.scripts['cloudflare:build'] ?? '', - /--vertical property-registry --target cloudflare-dist/u, + yield* write( + root, + 'verticals/property-registry/module-federation.config.ts', + `export default { exposes: {} };\n`, ); - assert.deepEqual(packageJson.modernjs.ontosModule, { - contractPath: '/.well-known/ontos-module-manifest.json', - manifest: './vertical.manifest.ts', - moduleId: MODULE_ID, - registration: './vertical.registration.ts', - schemaVersion: 2, - }); - const tsconfig = Schema.decodeUnknownSync(ModuleTsconfigSchema)( - JSON.parse( - await readFile(path.join(root, 'verticals/property-registry/tsconfig.json'), 'utf-8'), - ), + yield* write( + root, + 'verticals/documents-center/package.json', + json({ + dependencies: {}, + modernjs: { + appId: DOCUMENTS_APP_ID, + role: 'module-federation-remote', + topology: '../../topology/reference-topology.json', + }, + name: '@app/documents-center', + private: true, + scripts: { + build: 'modern build', + 'cloudflare:build': 'MODERNJS_DEPLOY=cloudflare modern build', + }, + type: 'module', + version: '0.1.0', + }), ); - assert.deepEqual(tsconfig.include, [ - 'src', - 'shared', - 'vertical.manifest.ts', - 'vertical.registration.ts', - ]); - }); -}); - -void test('emits deterministic deployment-safe JSON and rejects damaged owner slots', async () => { - await withFixture(async (root) => { - await scaffold(root); - await scaffold(root, DOCUMENTS_APP_ID, DOCUMENTS_MODULE_ID); - const authoredManifestPath = path.join(root, PROPERTY_MANIFEST_PATH); - const authoredManifest = await readFile(authoredManifestPath, 'utf-8'); - await writeFile( - authoredManifestPath, - authoredManifest - .replace( - '// ', - "import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/unstable/httpapi';\n\nconst PropertyApi = HttpApi.make('PropertyApi').add(\n HttpApiGroup.make('property').add(HttpApiEndpoint.get('listUnits', '/units')),\n);\n// ", - ) - .replace( - ' // \n // ', - ' // \n PropertyApi,\n // ', - ), - 'utf-8', + yield* write( + root, + 'verticals/documents-center/tsconfig.json', + json({ compilerOptions: { composite: true }, include: ['src'], references: [] }), ); - const first = await generateOntosModuleContract({ - target: 'dist', - vertical: APP_ID, - workspaceRoot: root, - }); - const firstContent = await readFile(first.path, 'utf-8'); - const packagePath = path.join(root, PROPERTY_PACKAGE_PATH); - const packageContent = await readFile(packagePath, 'utf-8'); - const decodedPackage = decodeModulePackage(packageContent); - const incompatiblePackage = { - ...decodedPackage, - modernjs: { - ...decodedPackage.modernjs, - ontosModule: { ...decodedPackage.modernjs.ontosModule, schemaVersion: 0 }, - }, - }; - await writeFile(packagePath, json(incompatiblePackage), 'utf-8'); - await assert.rejects( - generateOntosModuleContract({ - target: 'dist', - vertical: APP_ID, - workspaceRoot: root, - }), - /module marker does not match/u, + yield* write( + root, + 'verticals/documents-center/module-federation.config.ts', + 'export default {};\n', ); - assert.equal(await readFile(first.path, 'utf-8'), firstContent); - await writeFile(packagePath, packageContent, 'utf-8'); - const second = await generateOntosModuleContract({ - target: 'dist', - vertical: APP_ID, - workspaceRoot: root, - }); - assert.equal(await readFile(second.path, 'utf-8'), firstContent); - assert.equal(second.etag, first.etag); - const document = decodeModuleContract(firstContent); - assert.equal(document.deployment.appId, APP_ID); - assert.equal(document.manifest.module.id, MODULE_ID); - assert.equal(document.schemaVersion, '2'); - assert.equal(Object.hasOwn(document.manifest, 'dependencies'), false); - assert.deepEqual(document.manifest.publicSurface.api[0]?.operationKeys, ['property.listUnits']); - assert.doesNotMatch(firstContent, /vertical\.registration|function|handler|sourcePath/u); - const headers = await readFile( - path.join(root, 'verticals/property-registry/dist/public/_headers'), - 'utf-8', + yield* Effect.all( + [writePinnedEffectApi(root, APP_ID), writePinnedEffectApi(root, DOCUMENTS_APP_ID)], + { concurrency: 'unbounded' }, ); - assert.match(headers, /Cache-Control: no-cache/u); - assert.match(headers, /Content-Type: application\/json/u); - assert.match(headers, /ETag: "[a-f0-9]{64}"/u); - const secondDeployment = await generateOntosModuleContract({ - target: 'dist', - vertical: DOCUMENTS_APP_ID, - workspaceRoot: root, - }); - const secondDocument = decodeModuleContract(await readFile(secondDeployment.path, 'utf-8')); - assert.equal(secondDocument.deployment.appId, DOCUMENTS_APP_ID); - assert.equal(secondDocument.manifest.module.id, DOCUMENTS_MODULE_ID); - - const manifestPath = path.join(root, PROPERTY_MANIFEST_PATH); - const manifest = await readFile(manifestPath, 'utf-8'); - await writeFile( - manifestPath, - manifest.replace('// ', ''), - 'utf-8', + yield* write( + root, + 'topology/reference-topology.json', + json({ + schemaVersion: 1, + verticals: [ + { + deliveryUnit: { buildMarker: 'property-build' }, + domain: 'property', + id: APP_ID, + kind: 'vertical', + moduleFederation: { name: 'verticalPropertyRegistry', role: 'remote' }, + package: '@app/property-registry', + path: 'verticals/property-registry', + }, + { + deliveryUnit: { buildMarker: 'documents-build' }, + domain: 'documents', + id: DOCUMENTS_APP_ID, + kind: 'vertical', + moduleFederation: { name: 'verticalDocumentsCenter', role: 'remote' }, + package: '@app/documents-center', + path: 'verticals/documents-center', + }, + ], + }), ); - await assert.rejects( - generateOntosModuleContract({ - target: 'dist', - vertical: APP_ID, - workspaceRoot: root, + yield* write( + root, + 'topology/local-overlays/development.json', + json({ + environment: 'development', + ontosModuleManifests: Object.fromEntries([ + [DOCUMENTS_APP_ID, 'http://localhost:4102/.well-known/ontos-module-manifest.json'], + [APP_ID, 'http://localhost:4101/.well-known/ontos-module-manifest.json'], + ]), + schemaVersion: 1, }), - /exactly one.*slot/u, ); + yield* linkFixtureDependencies(root, appRoot, { + '@app/core-runtime': 'packages/core-runtime', + effect: 'node_modules/effect', + }); + return root; }); -}); -void test('maps Cloudflare emission to the Modern output root and validates authored contracts', async () => { - await withFixture(async (root) => { - await scaffold(root); - await scaffold(root, DOCUMENTS_APP_ID, DOCUMENTS_MODULE_ID); - const emitted = await generateOntosModuleContract({ - target: 'cloudflare-dist', - vertical: APP_ID, +const withFixture = withCreatedFixture(createFixture()); + +const scaffold = Effect.fn(function* scenario4( + root: string, + vertical = APP_ID, + module = MODULE_ID, +) { + return yield* runScaffoldEffect( + MODULE_CONTRACT_COMMAND, + [VERTICAL_FLAG, vertical, '--module', module], + { workspaceRoot: root, - }); - assert.match( - emitted.path, - /verticals\/property-registry\/dist-cloudflare\/public\/\.well-known\/ontos-module-manifest\.json$/u, + }, + ).pipe(Effect.provide(NodeServices.layer)); +}); + +it.live( + 'module-contract help is exact and write-free', + Effect.fn(function* scenario5() { + const missingRoot = path.join(tmpdir(), 'module-contract-help-does-not-exist'); + const result = yield* runScaffoldEffect(MODULE_CONTRACT_COMMAND, ['--help'], { + workspaceRoot: missingRoot, + }).pipe(Effect.provide(NodeServices.layer)); + expect(result).toEqual({ help: getHelpText(MODULE_CONTRACT_COMMAND), kind: 'help' }); + if (result.kind !== 'help') { + throw new Error('Expected help result'); + } + expect(result.help).toMatch(/--vertical --module /u); + }), +); + +it.live( + 'business generators fail closed before the mandatory module contract exists', + Effect.fn(function* scenario6() { + yield* withFixture( + Effect.fn(function* scenario7(root) { + const commands = [ + [ + 'action', + [ + VERTICAL_FLAG, + APP_ID, + '--action', + 'create-property', + '--legal-entity-scope', + 'optional', + AUTHORIZATION_FLAG, + 'action_execution', + '--provisioning', + 'tenant_membership_default', + ], + ], + ['microvertical-action-boundary', [VERTICAL_FLAG, APP_ID]], + [ + 'microvertical-page', + [ + VERTICAL_FLAG, + APP_ID, + '--page', + 'properties', + AUTHORIZATION_FLAG, + 'context_permission', + '--permission', + 'module.access', + ], + ], + [ + 'outbox-message', + [VERTICAL_FLAG, APP_ID, '--action', 'create-property', '--topic', 'property.created'], + ], + [ + 'outbox-worker', + [ + VERTICAL_FLAG, + APP_ID, + '--worker', + 'property-projector', + '--producer', + DOCUMENTS_APP_ID, + '--topic', + 'document.created', + AUTHORIZATION_FLAG, + 'owner_local_background', + ], + ], + [ + 'policy', + ['--scope', 'microvertical', VERTICAL_FLAG, APP_ID, '--policy', 'property-visible'], + ], + ] as const; + yield* Effect.all( + commands.map( + Effect.fn(function* scenario8([command, flags]) { + return yield* expectFailure( + runScaffoldEffect(command, flags, { workspaceRoot: root }).pipe( + Effect.provide(NodeServices.layer), + ), + (error) => expect(String(error)).toMatch(/requires scaffold:module-contract/u), + ); + }), + ), + { concurrency: 'unbounded' }, + ); + }), + ); + }), +); + +it.live( + 'rejects malformed, traversing, duplicate, and overwrite requests without partial writes', + Effect.fn(function* scenario9() { + yield* withFixture( + Effect.fn(function* scenario10(root) { + yield* expectFailure(scaffold(root, '../property', MODULE_ID), (error) => + expect(String(error)).toMatch(/lower-kebab-case/u), + ); + yield* expectFailure(scaffold(root, APP_ID, APP_ID), (error) => + expect(String(error)).toMatch(/dotted/u), + ); + yield* expectFailure(scaffold(root, APP_ID, 'core.modules'), (error) => + expect(String(error)).toMatch(/non-core/u), + ); + yield* scaffold(root); + const packageAfterFirst = yield* Effect.promise(() => + readFile(path.join(root, PROPERTY_PACKAGE_PATH), 'utf-8'), + ); + yield* expectFailure(scaffold(root), (error) => + expect(String(error)).toMatch(/refusing to overwrite/u), + ); + expect( + yield* Effect.promise(() => readFile(path.join(root, PROPERTY_PACKAGE_PATH), 'utf-8')), + ).toBe(packageAfterFirst); + yield* expectFailure(scaffold(root, DOCUMENTS_APP_ID, MODULE_ID), (error) => + expect(String(error)).toMatch(/duplicate OntOS module ID/u), + ); + }), ); - await runEffectTestPromise( - checkOntosModuleContracts(root).pipe(Effect.provide(NodeFileSystem.layer)), + }), +); + +it.live( + 'generates conservative owner files and patches only package and tsconfig owner metadata', + Effect.fn(function* scenario11() { + yield* withFixture( + Effect.fn(function* scenario12(root) { + const result = yield* scaffold(root); + expect(result.kind).toBe('generated'); + const manifest = yield* Effect.promise(() => + readFile(path.join(root, PROPERTY_MANIFEST_PATH), 'utf-8'), + ); + const registration = yield* Effect.promise(() => + readFile( + path.join(root, 'verticals/property-registry/vertical.registration.ts'), + 'utf-8', + ), + ); + expect(manifest).toMatch(/@ontos-deployment-app-id property-registry/u); + expect(manifest).toMatch(/@ontos-module-id property\.registry/u); + expect(manifest).toMatch(/defaultState: 'inactive'/u); + expect(manifest).not.toMatch(/dependencies:|core\.identity|externalSystems/u); + const retiredLifecycleMarkers = [ + ['must', 'be', 'active', 'first'].join('_'), + ['enable', 'together', 'when', 'available'].join('_'), + ['optional', 'enhancement'].join('_'), + ['integration', 'required', 'for', 'api'].join('_'), + ]; + for (const marker of retiredLifecycleMarkers) { + expect(manifest.includes(marker)).toBe(false); + } + expect(manifest).toMatch(/actions: \[/u); + expect(registration).toMatch(/defineVerticalRuntimeRegistration/u); + expect(registration).toMatch(/generated-module-registration-workers/u); + expect(registration).not.toMatch(/handler|migration|route/u); + const packageJson = yield* decodeModulePackage( + yield* Effect.promise(() => readFile(path.join(root, PROPERTY_PACKAGE_PATH), 'utf-8')), + ); + expect(packageJson.dependencies).toEqual({ + '@app/core-runtime': 'workspace:*', + zeta: '1.0.0', + }); + expect(packageJson.exports).toEqual({ '.': './src/index.ts' }); + expect(packageJson.scripts['existing']).toBe('preserve-me'); + expect(packageJson.scripts['build'] ?? '').toMatch( + /--vertical property-registry --target dist/u, + ); + expect(packageJson.scripts['cloudflare:build'] ?? '').toMatch( + /--vertical property-registry --target cloudflare-dist/u, + ); + expect(packageJson.modernjs.ontosModule).toEqual({ + contractPath: '/.well-known/ontos-module-manifest.json', + manifest: './vertical.manifest.ts', + moduleId: MODULE_ID, + registration: './vertical.registration.ts', + schemaVersion: 2, + }); + const tsconfig = yield* Schema.decodeUnknownEffect(ModuleTsconfigSchema)( + JSON.parse( + yield* Effect.promise(() => + readFile(path.join(root, 'verticals/property-registry/tsconfig.json'), 'utf-8'), + ), + ), + ); + expect(tsconfig.include).toEqual([ + 'src', + 'shared', + 'vertical.manifest.ts', + 'vertical.registration.ts', + ]); + }), ); - }); -}); + }), +); -void test('permits owner-local registration imports but rejects cross-deployment owner imports', () => { +it.live( + 'emits deterministic deployment-safe JSON and rejects damaged owner slots', + Effect.fn(function* scenario13() { + yield* withFixture( + Effect.fn(function* scenario14(root) { + yield* scaffold(root); + yield* scaffold(root, DOCUMENTS_APP_ID, DOCUMENTS_MODULE_ID); + const authoredManifestPath = path.join(root, PROPERTY_MANIFEST_PATH); + const authoredManifest = yield* Effect.promise(() => + readFile(authoredManifestPath, 'utf-8'), + ); + yield* Effect.promise(() => + writeFile( + authoredManifestPath, + authoredManifest + .replace( + '// ', + "import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/unstable/httpapi';\n\nconst PropertyApi = HttpApi.make('PropertyApi').add(\n HttpApiGroup.make('property').add(HttpApiEndpoint.get('listUnits', '/units')),\n);\n// ", + ) + .replace( + ' // \n // ', + ' // \n PropertyApi,\n // ', + ), + 'utf-8', + ), + ); + const first = yield* generateOntosModuleContract({ + target: 'dist', + vertical: APP_ID, + workspaceRoot: root, + }).pipe(Effect.provide(NodeServices.layer)); + const firstContent = yield* Effect.promise(() => readFile(first.path, 'utf-8')); + const packagePath = path.join(root, PROPERTY_PACKAGE_PATH); + const packageContent = yield* Effect.promise(() => readFile(packagePath, 'utf-8')); + const decodedPackage = yield* decodeModulePackage(packageContent); + const incompatiblePackage = { + ...decodedPackage, + modernjs: { + ...decodedPackage.modernjs, + ontosModule: { ...decodedPackage.modernjs.ontosModule, schemaVersion: 0 }, + }, + }; + yield* Effect.promise(() => writeFile(packagePath, json(incompatiblePackage), 'utf-8')); + yield* expectFailure( + generateOntosModuleContract({ + target: 'dist', + vertical: APP_ID, + workspaceRoot: root, + }).pipe(Effect.provide(NodeServices.layer)), + (error) => expect(String(error)).toMatch(/module marker does not match/u), + ); + expect(yield* Effect.promise(() => readFile(first.path, 'utf-8'))).toBe(firstContent); + yield* Effect.promise(() => writeFile(packagePath, packageContent, 'utf-8')); + const second = yield* generateOntosModuleContract({ + target: 'dist', + vertical: APP_ID, + workspaceRoot: root, + }).pipe(Effect.provide(NodeServices.layer)); + expect(yield* Effect.promise(() => readFile(second.path, 'utf-8'))).toBe(firstContent); + expect(second.etag).toBe(first.etag); + const document = yield* decodeModuleContract(firstContent); + expect(document.deployment.appId).toBe(APP_ID); + expect(document.manifest.module.id).toBe(MODULE_ID); + expect(document.schemaVersion).toBe('2'); + expect(Object.hasOwn(document.manifest, 'dependencies')).toBe(false); + expect(document.manifest.publicSurface.api[0]?.operationKeys).toEqual([ + 'property.listUnits', + ]); + expect(firstContent).not.toMatch(/vertical\.registration|function|handler|sourcePath/u); + const headers = yield* Effect.promise(() => + readFile(path.join(root, 'verticals/property-registry/dist/public/_headers'), 'utf-8'), + ); + expect(headers).toMatch(/Cache-Control: no-cache/u); + expect(headers).toMatch(/Content-Type: application\/json/u); + expect(headers).toMatch(/ETag: "[a-f0-9]{64}"/u); + const secondDeployment = yield* generateOntosModuleContract({ + target: 'dist', + vertical: DOCUMENTS_APP_ID, + workspaceRoot: root, + }).pipe(Effect.provide(NodeServices.layer)); + const secondDocument = yield* decodeModuleContract( + yield* Effect.promise(() => readFile(secondDeployment.path, 'utf-8')), + ); + expect(secondDocument.deployment.appId).toBe(DOCUMENTS_APP_ID); + expect(secondDocument.manifest.module.id).toBe(DOCUMENTS_MODULE_ID); + + const manifestPath = path.join(root, PROPERTY_MANIFEST_PATH); + const manifest = yield* Effect.promise(() => readFile(manifestPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + manifestPath, + manifest.replace('// ', ''), + 'utf-8', + ), + ); + yield* expectFailure( + generateOntosModuleContract({ + target: 'dist', + vertical: APP_ID, + workspaceRoot: root, + }).pipe(Effect.provide(NodeServices.layer)), + (error) => expect(String(error)).toMatch(/exactly one.*slot/u), + ); + }), + ); + }), +); + +it.live( + 'maps Cloudflare emission to the Modern output root and validates authored contracts', + Effect.fn(function* scenario15() { + yield* withFixture( + Effect.fn(function* scenario16(root) { + yield* scaffold(root); + yield* scaffold(root, DOCUMENTS_APP_ID, DOCUMENTS_MODULE_ID); + const emitted = yield* generateOntosModuleContract({ + target: 'cloudflare-dist', + vertical: APP_ID, + workspaceRoot: root, + }).pipe(Effect.provide(NodeServices.layer)); + expect(emitted.path).toMatch( + /verticals\/property-registry\/dist-cloudflare\/public\/\.well-known\/ontos-module-manifest\.json$/u, + ); + yield* checkOntosModuleContracts(root).pipe(Effect.provide(NodeServices.layer)); + }), + ); + }), +); + +it('permits owner-local registration imports but rejects cross-deployment owner imports', () => { const root = '/workspace/app'; - assert.equal( + expect( privateOwnerImportViolation( root, 'verticals/billing/src/worker-host/main.ts', '../../vertical.registration.ts', ), - undefined, - ); - assert.match( + ).toBe(undefined); + expect( privateOwnerImportViolation( root, 'verticals/billing/src/worker-host/main.ts', '../../../inventory-stock/vertical.registration.ts', ) ?? '', - /only its own/u, - ); - assert.match( + ).toMatch(/only its own/u); + expect( privateOwnerImportViolation( root, 'verticals/billing/vertical.registration.ts', '../inventory-stock/vertical.registration.ts', ) ?? '', - /only its own/u, - ); - assert.match( + ).toMatch(/only its own/u); + expect( privateOwnerImportViolation( root, 'apps/shell-super-app/api/index.ts', '../../../verticals/billing/vertical.registration.ts', ) ?? '', - /may not import/u, - ); + ).toMatch(/may not import/u); }); -void test('module-contract ignores non-code roots and nested semicolons when inserting API slots', async () => { - await withFixture(async (root) => { - const source = `// export const commentApi = HttpApi.make('Comment'); +it.live( + 'module-contract ignores non-code roots and nested semicolons when inserting API slots', + Effect.fn(function* mergedScenario5() { + yield* withFixture( + Effect.fn(function* mergedScenario4(root) { + const source = `// export const commentApi = HttpApi.make('Comment'); const example = "export const stringApi = HttpApi.make('String');"; export const fixtureApi = HttpApi.make('Fixture;Api') /* semicolon ; before the end of the expression */ .pipe((api) => { const label = ';'; return api; }); export const untouched = true; `; - await write(root, 'verticals/property-registry/shared/api.ts', source); - await scaffold(root); - const generated = await readFile( - path.join(root, 'verticals/property-registry/shared/api.ts'), - 'utf-8', + yield* write(root, 'verticals/property-registry/shared/api.ts', source); + yield* scaffold(root); + const generated = yield* Effect.promise(() => + readFile(path.join(root, 'verticals/property-registry/shared/api.ts'), 'utf-8'), + ); + expect(generated).toMatch(/HttpApi\.make\('Fixture;Api'\)/u); + expect(generated).toMatch( + /return api; \}\)\s*\/\/ /u, + ); + expect(generated).toMatch(/export const governedHttpApi = fixtureApi;/u); + expect(generated).toMatch(/export const untouched = true;/u); + }), ); - assert.match(generated, /HttpApi\.make\('Fixture;Api'\)/u); - assert.match(generated, /return api; \}\)\s*\/\/ /u); - assert.match(generated, /export const governedHttpApi = fixtureApi;/u); - assert.match(generated, /export const untouched = true;/u); - }); -}); + }), +); -void test('module-contract injects only its own Layer binding into pinned handler roots', async () => { - await withFixture(async (root) => { - await write( - root, - 'verticals/property-registry/api/index.ts', - `const layer = HttpApiBuilder.layer(fixtureApi).pipe( +it.live( + 'module-contract injects only its own Layer binding into pinned handler roots', + Effect.fn(function* mergedScenario8() { + yield* withFixture( + Effect.fn(function* mergedScenario7(root) { + yield* write( + root, + 'verticals/property-registry/api/index.ts', + `const layer = HttpApiBuilder.layer(fixtureApi).pipe( identity, ) satisfies EffectRuntimeLayer; export default defineEffectBff({ api: fixtureApi, layer }); `, + ); + yield* scaffold(root); + const generated = yield* Effect.promise(() => + readFile(path.join(root, 'verticals/property-registry/api/index.ts'), 'utf-8'), + ); + expect(generated).toMatch(/GovernedReadLayer\.provide\(governedReadApiHandlersLive\)/u); + expect(generated).toMatch(/GovernedReadLayer\.orDie/u); + expect(generated).not.toMatch(/\bLayer\./u); + }), ); - await scaffold(root); - const generated = await readFile( - path.join(root, 'verticals/property-registry/api/index.ts'), - 'utf-8', - ); - assert.match(generated, /GovernedReadLayer\.provide\(governedReadApiHandlersLive\)/u); - assert.match(generated, /GovernedReadLayer\.orDie/u); - assert.doesNotMatch(generated, /\bLayer\./u); - }); -}); + }), +); -void test('requires concrete HttpApi contract schemas through Problem Details helpers', () => { - assert.equal( +it('requires concrete HttpApi contract schemas through Problem Details helpers', () => { + expect( unconstrainedHttpApiContractSchemaViolation(` const InvalidProblem = makeProblemDetailsSchema('InvalidProblem', 400, { field: Schema.String, }); `), - undefined, - ); - assert.match( + ).toBe(undefined); + expect( unconstrainedHttpApiContractSchemaViolation(` const InvalidProblem = makeProblemDetailsSchema('InvalidProblem', 400, { field: Schema.Unknown, }); `) ?? '', - /must use concrete/u, - ); - assert.match( + ).toMatch(/must use concrete/u); + expect( unconstrainedHttpApiContractSchemaViolation(` HttpApiEndpoint.post('execute', '/reads/example', { success: Schema.Any, }); `) ?? '', - /must use concrete/u, - ); - assert.match( + ).toMatch(/must use concrete/u); + expect( unconstrainedHttpApiContractSchemaViolation(` const InvalidProblem = makeProblemDetailsSchema('InvalidProblem', 400, { diagnostics: Schema.Record(Schema.String, Schema.String), }); `) ?? '', - /must use concrete/u, - ); - assert.match( + ).toMatch(/must use concrete/u); + expect( unconstrainedHttpApiContractSchemaViolation(` const InvalidProblem = makeProblemDetailsSchema('InvalidProblem', 400, { diagnostics: Schema.Json, }); `) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); }); const contractApiFixturePath = 'contracts/api.ts'; @@ -623,7 +699,7 @@ const packageSafeFixturePath = 'packages/example/src/safe.ts'; const packageUnsafeFixturePath = 'packages/example/src/unsafe.ts'; const packageSafeSchemaFixture = `export const UnsafeSchema = Schema.String;`; -void test('follows imported payload, query, parameter, success, and error schemas', () => { +it('follows imported payload, query, parameter, success, and error schemas', () => { for (const member of ['payload', 'query', 'urlParams', 'success', 'error']) { const sources = new Map([ [ @@ -638,35 +714,32 @@ void test('follows imported payload, query, parameter, success, and error schema `export const UnsafeSchema = Schema.Struct({ nested: Schema.Unknown });`, ], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, member, - ); + ).toMatch(/must use concrete/u); } }); -void test('covers every supported endpoint constructor through direct and aliased paths', () => { +it('covers every supported endpoint constructor through direct and aliased paths', () => { for (const method of ['delete', 'head', 'options']) { - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(` HttpApiEndpoint.${method}('execute', '/reads/example', { success: Schema.Any }); `) ?? '', - /must use concrete/u, method, - ); + ).toMatch(/must use concrete/u); } - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(` const inspectHeaders = HttpApiEndpoint.head; inspectHeaders('execute', '/reads/example', { success: Schema.Unknown }); `) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); const sources = new Map([ [ @@ -681,16 +754,15 @@ void test('covers every supported endpoint constructor through direct and aliase `export { HttpApiEndpoint as ReexportedEndpoint } from 'effect/unstable/httpapi';`, ], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); }); -void test('follows HttpApiEndpoint.make factories through local and imported helpers', () => { +it('follows HttpApiEndpoint.make factories through local and imported helpers', () => { for (const source of [ ` HttpApiEndpoint.make('GET')('execute', '/reads/example', { success: Schema.Any }); @@ -704,7 +776,7 @@ void test('follows HttpApiEndpoint.make factories through local and imported hel makeEndpoint()('execute', '/reads/example', { success: Schema.Any }); `, ]) { - assert.match(unconstrainedHttpApiContractSchemaViolation(source) ?? '', /must use concrete/u); + expect(unconstrainedHttpApiContractSchemaViolation(source) ?? '').toMatch(/must use concrete/u); } const sources = new Map([ @@ -725,30 +797,28 @@ void test('follows HttpApiEndpoint.make factories through local and imported hel `, ], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); }); -void test('follows the direct HttpApiEndpoint provider through verbs, make, and re-exports', () => { +it('follows the direct HttpApiEndpoint provider through verbs, make, and re-exports', () => { const provider = 'effect/unstable/httpapi/HttpApiEndpoint'; for (const method of ['delete', 'get', 'head', 'options', 'patch', 'post', 'put']) { const entry = ` import * as Endpoint from '${provider}'; Endpoint.${method}('execute', '/reads/example', { success: Schema.Any }); `; - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources: new Map([[contractApiFixturePath, entry]]), }) ?? '', - /must use concrete/u, method, - ); + ).toMatch(/must use concrete/u); } const makeEntry = ` @@ -759,13 +829,12 @@ void test('follows the direct HttpApiEndpoint provider through verbs, make, and [contractApiFixturePath, makeEntry], [contractBarrelFixturePath, `export * from '${provider}';`], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(makeEntry, { file: contractApiFixturePath, sources: makeSources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); const namespaceEntry = ` import { Endpoint } from './barrel'; @@ -775,25 +844,23 @@ void test('follows the direct HttpApiEndpoint provider through verbs, make, and [contractApiFixturePath, namespaceEntry], [contractBarrelFixturePath, `export * as Endpoint from '${provider}';`], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(namespaceEntry, { file: contractApiFixturePath, sources: namespaceSources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); }); -void test('follows local and imported function helpers used as public schemas', () => { - assert.match( +it('follows local and imported function helpers used as public schemas', () => { + expect( unconstrainedHttpApiContractSchemaViolation(` function unsafeResponse() { return Schema.Any; } HttpApiEndpoint.get('read', '/reads/example', { success: unsafeResponse() }); `) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); const sources = new Map([ [ @@ -812,16 +879,15 @@ void test('follows local and imported function helpers used as public schemas', `, ], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); }); -void test('follows imported arbitrary Problem Details extension records', () => { +it('follows imported arbitrary Problem Details extension records', () => { const sources = new Map([ [ contractApiFixturePath, @@ -836,16 +902,15 @@ void test('follows imported arbitrary Problem Details extension records', () => ], [contractBarrelFixturePath, `export { ExtensionFields as UnsafeExtensions } from './unsafe';`], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); }); -void test('follows transitive imported schema aliases without rejecting unused unsafe exports', () => { +it('follows transitive imported schema aliases without rejecting unused unsafe exports', () => { const sources = new Map([ [ contractApiFixturePath, @@ -864,13 +929,12 @@ void test('follows transitive imported schema aliases without rejecting unused u ], ['contracts/internal-response.ts', `export const InternalResponse = Schema.Json;`], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); const safeSources = new Map([ ...sources, @@ -879,16 +943,15 @@ void test('follows transitive imported schema aliases without rejecting unused u `export const InternalResponse = Schema.Struct({ value: Schema.String });`, ] as const, ]); - assert.equal( + expect( unconstrainedHttpApiContractSchemaViolation(safeSources.get(contractApiFixturePath) ?? '', { file: contractApiFixturePath, sources: safeSources, }), - undefined, - ); + ).toBe(undefined); }); -void test('rejects Effect Schema namespace aliases and destructured unsafe members', () => { +it('rejects Effect Schema namespace aliases and destructured unsafe members', () => { for (const unsafeSource of [ ` import { Schema as S } from 'effect'; @@ -914,17 +977,16 @@ void test('rejects Effect Schema namespace aliases and destructured unsafe membe ], [unsafeContractFixturePath, unsafeSource], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); } }); -void test('follows barrel re-exports and relative namespace imports', () => { +it('follows barrel re-exports and relative namespace imports', () => { for (const entrySource of [ ` import { UnsafeSchema } from './barrel'; @@ -940,17 +1002,16 @@ void test('follows barrel re-exports and relative namespace imports', () => { [contractBarrelFixturePath, `export { UnsafeSchema } from './unsafe';`], [unsafeContractFixturePath, `export const UnsafeSchema = Schema.Any;`], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); } }); -void test('follows local re-exports of imported schemas', () => { +it('follows local re-exports of imported schemas', () => { const sources = new Map([ [ contractApiFixturePath, @@ -968,25 +1029,24 @@ void test('follows local re-exports of imported schemas', () => { ], [unsafeContractFixturePath, `export const UnsafeSchema = Schema.Any;`], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); }); -void test('rejects direct Effect schema imports used through an aliased endpoint factory', () => { +it('rejects direct Effect schema imports used through an aliased endpoint factory', () => { const content = ` import { HttpApiEndpoint as Endpoint } from '@modern-js/plugin-bff/effect-client'; import { Any as UnsafeSchema } from 'effect'; Endpoint.get('read', '/reads/example', { success: UnsafeSchema }); `; - assert.match(unconstrainedHttpApiContractSchemaViolation(content) ?? '', /must use concrete/u); + expect(unconstrainedHttpApiContractSchemaViolation(content) ?? '').toMatch(/must use concrete/u); }); -void test('follows schemas imported through @app package subpaths', () => { +it('follows schemas imported through @app package subpaths', () => { const sources = new Map([ [ contractApiFixturePath, @@ -999,16 +1059,15 @@ void test('follows schemas imported through @app package subpaths', () => { ], [packageUnsafeFixturePath, `export const UnsafeSchema = Schema.Any;`], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); }); -void test('follows star barrels, default imports, and package export maps', () => { +it('follows star barrels, default imports, and package export maps', () => { const fixtures: readonly { readonly entry: string; readonly extraSources: readonly (readonly [string, string])[]; @@ -1046,17 +1105,16 @@ void test('follows star barrels, default imports, and package export maps', () = [contractApiFixturePath, fixture.entry], ...fixture.extraSources, ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); } }); -void test('covers ordinary endpoint aliases and TypeScript module forms', () => { +it('covers ordinary endpoint aliases and TypeScript module forms', () => { for (const content of [ ` const Endpoint = HttpApiEndpoint; @@ -1071,7 +1129,9 @@ void test('covers ordinary endpoint aliases and TypeScript module forms', () => HttpApiEndpoint.get('read', '/reads/example', { success: S.Any }); `, ]) { - assert.match(unconstrainedHttpApiContractSchemaViolation(content) ?? '', /must use concrete/u); + expect(unconstrainedHttpApiContractSchemaViolation(content) ?? '').toMatch( + /must use concrete/u, + ); } const fixtures: readonly (readonly [string, ReadonlyMap])[] = [ @@ -1123,17 +1183,16 @@ void test('covers ordinary endpoint aliases and TypeScript module forms', () => ]; for (const [entry, extraSources] of fixtures) { const sources = new Map([[contractApiFixturePath, entry], ...extraSources]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); } }); -void test('covers destructured, computed, and provenance-safe aliases', () => { +it('covers destructured, computed, and provenance-safe aliases', () => { for (const content of [ ` const { get } = HttpApiEndpoint; @@ -1151,10 +1210,12 @@ void test('covers destructured, computed, and provenance-safe aliases', () => { factory('InvalidProblem', 400, { values: Schema.Record(Schema.String, Schema.String) }); `, ]) { - assert.match(unconstrainedHttpApiContractSchemaViolation(content) ?? '', /must use concrete/u); + expect(unconstrainedHttpApiContractSchemaViolation(content) ?? '').toMatch( + /must use concrete/u, + ); } - assert.equal( + expect( unconstrainedHttpApiContractSchemaViolation(` const Schema = { Any: 'not an Effect schema' }; const HttpApiEndpoint = { get: () => undefined }; @@ -1162,19 +1223,17 @@ void test('covers destructured, computed, and provenance-safe aliases', () => { const makeProblemDetailsSchema = () => undefined; makeProblemDetailsSchema('SafeLocalCall', 400, { value: Schema.Any }); `), - undefined, - ); - assert.equal( + ).toBe(undefined); + expect( unconstrainedHttpApiContractSchemaViolation(` HttpApiEndpoint.get('read', '/reads/example', { success: Schema.Record(Schema.String, Schema.String), }); `), - undefined, - ); + ).toBe(undefined); }); -void test('resolves recursive namespace exports for endpoints, factories, and schemas', () => { +it('resolves recursive namespace exports for endpoints, factories, and schemas', () => { const commonSources = new Map([ [ contractBarrelFixturePath, @@ -1218,36 +1277,33 @@ void test('resolves recursive namespace exports for endpoints, factories, and sc [contractApiFixturePath, entry] as const, ['contracts/renamed.ts', `export { Problems as Renamed } from './barrel';`] as const, ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); } }); -void test('resolves lexical shadows without inspecting unused inner bindings', () => { - assert.match( +it('resolves lexical shadows without inspecting unused inner bindings', () => { + expect( unconstrainedHttpApiContractSchemaViolation(` const ResponseSchema = Schema.Any; { const ResponseSchema = Schema.String; void ResponseSchema; } HttpApiEndpoint.get('read', '/reads/example', { success: ResponseSchema }); `) ?? '', - /must use concrete/u, - ); - assert.equal( + ).toMatch(/must use concrete/u); + expect( unconstrainedHttpApiContractSchemaViolation(` const ResponseSchema = Schema.String; { const ResponseSchema = Schema.Any; void ResponseSchema; } HttpApiEndpoint.get('read', '/reads/example', { success: ResponseSchema }); `), - undefined, - ); + ).toBe(undefined); }); -void test('evaluates package export conditions, wildcard specificity, and null exclusions', () => { +it('evaluates package export conditions, wildcard specificity, and null exclusions', () => { const fixtures: readonly (readonly [string, ReadonlyMap])[] = [ [ '@app/example/api', @@ -1282,13 +1338,12 @@ void test('evaluates package export conditions, wildcard specificity, and null e HttpApiEndpoint.get('read', '/reads/example', { success: UnsafeSchema }); `; const sources = new Map([[contractApiFixturePath, entry], ...extraSources]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); } const safeEntry = ` @@ -1301,13 +1356,12 @@ void test('evaluates package export conditions, wildcard specificity, and null e [packageSafeFixturePath, `export const ResponseSchema = Schema.String;`], ['packages/example/src/api.ts', `export const ResponseSchema = Schema.Any;`], ]); - assert.equal( + expect( unconstrainedHttpApiContractSchemaViolation(safeEntry, { file: contractApiFixturePath, sources: safeSources, }), - undefined, - ); + ).toBe(undefined); const specificEntry = ` import { ResponseSchema } from '@app/example/foo/bar'; @@ -1319,16 +1373,15 @@ void test('evaluates package export conditions, wildcard specificity, and null e [packageSafeFixturePath, `export const ResponseSchema = Schema.String;`], [packageUnsafeFixturePath, `export const ResponseSchema = Schema.Any;`], ]); - assert.equal( + expect( unconstrainedHttpApiContractSchemaViolation(specificEntry, { file: contractApiFixturePath, sources: specificSources, }), - undefined, - ); + ).toBe(undefined); }); -void test('terminates on safe and unsafe cyclic re-exports', () => { +it('terminates on safe and unsafe cyclic re-exports', () => { const entry = ` import { ResponseSchema } from './cycle-a'; HttpApiEndpoint.get('read', '/reads/example', { success: ResponseSchema }); @@ -1341,13 +1394,12 @@ void test('terminates on safe and unsafe cyclic re-exports', () => { `export * from './cycle-a'; export const ResponseSchema = Schema.String;`, ], ]); - assert.equal( + expect( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources: safeSources, }), - undefined, - ); + ).toBe(undefined); const unsafeSources = new Map([ ...safeSources, [ @@ -1355,16 +1407,15 @@ void test('terminates on safe and unsafe cyclic re-exports', () => { `export * from './cycle-a'; export const ResponseSchema = Schema.Any;`, ] as const, ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources: unsafeSources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); }); -void test('honors explicit export precedence and star-export binding identity', () => { +it('honors explicit export precedence and star-export binding identity', () => { const entry = ` import { ResponseSchema } from './barrel'; HttpApiEndpoint.get('read', '/reads/example', { success: ResponseSchema }); @@ -1378,13 +1429,12 @@ void test('honors explicit export precedence and star-export binding identity', ['contracts/safe.ts', `export const SafeSchema = Schema.String;`], [unsafeContractFixturePath, `export const ResponseSchema = Schema.Any;`], ]); - assert.equal( + expect( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources: explicitSources, }), - undefined, - ); + ).toBe(undefined); const diamondSources = new Map([ [contractApiFixturePath, entry], @@ -1393,29 +1443,27 @@ void test('honors explicit export precedence and star-export binding identity', ['contracts/right.ts', `export * from './origin';`], ['contracts/origin.ts', `export const ResponseSchema = Schema.Unknown;`], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources: diamondSources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); const ambiguousSources = new Map([ ...diamondSources, ['contracts/left.ts', `export const ResponseSchema = Schema.String;`] as const, ['contracts/right.ts', `export const ResponseSchema = Schema.Any;`] as const, ]); - assert.equal( + expect( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources: ambiguousSources, }), - undefined, - ); + ).toBe(undefined); }); -void test('uses TypeScript source and relative-file resolution precedence', () => { +it('uses TypeScript source and relative-file resolution precedence', () => { const entry = ` import { ResponseSchema } from './foo'; HttpApiEndpoint.get('read', '/reads/example', { success: ResponseSchema }); @@ -1425,13 +1473,12 @@ void test('uses TypeScript source and relative-file resolution precedence', () = ['contracts/foo.ts', `export const ResponseSchema = Schema.String;`], ['contracts/foo/index.ts', `export const ResponseSchema = Schema.Any;`], ]); - assert.equal( + expect( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources, }), - undefined, - ); + ).toBe(undefined); const nodeNextEntry = ` import { ResponseSchema } from './foo.js'; @@ -1442,16 +1489,15 @@ void test('uses TypeScript source and relative-file resolution precedence', () = [contractApiFixturePath, nodeNextEntry] as const, ['contracts/foo.ts', `export const ResponseSchema = Schema.Json;`] as const, ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(nodeNextEntry, { file: contractApiFixturePath, sources: nodeNextSources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); }); -void test('tracks object, mutable, rest, var, enum, and namespace provenance', () => { +it('tracks object, mutable, rest, var, enum, and namespace provenance', () => { for (const content of [ ` const Endpoints = { endpoint: HttpApiEndpoint }; @@ -1499,7 +1545,9 @@ void test('tracks object, mutable, rest, var, enum, and namespace provenance', ( HttpApiEndpoint.get('read', '/reads/example', { success: ResponseSchema }); `, ]) { - assert.match(unconstrainedHttpApiContractSchemaViolation(content) ?? '', /must use concrete/u); + expect(unconstrainedHttpApiContractSchemaViolation(content) ?? '').toMatch( + /must use concrete/u, + ); } for (const content of [ @@ -1520,18 +1568,17 @@ void test('tracks object, mutable, rest, var, enum, and namespace provenance', ( HttpApiEndpoint.get('read', '/reads/example', { success: Schema.Any }); `, ]) { - assert.equal(unconstrainedHttpApiContractSchemaViolation(content), undefined); + expect(unconstrainedHttpApiContractSchemaViolation(content)).toBe(undefined); } - assert.equal( + expect( unconstrainedHttpApiContractSchemaViolation(` const { String, ...S } = Schema; HttpApiEndpoint.get('read', '/reads/example', { success: S.String }); `), - undefined, - ); + ).toBe(undefined); }); -void test('follows external schema and endpoint provider re-exports', () => { +it('follows external schema and endpoint provider re-exports', () => { const fixtures: readonly (readonly [string, string])[] = [ [`export * from 'effect/Schema';`, `import { Any as UnsafeSchema } from './barrel';`], [ @@ -1550,13 +1597,12 @@ void test('follows external schema and endpoint provider re-exports', () => { [contractApiFixturePath, entry], [contractBarrelFixturePath, barrel], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); } for (const barrel of [ @@ -1571,31 +1617,28 @@ void test('follows external schema and endpoint provider re-exports', () => { [contractApiFixturePath, entry], [contractBarrelFixturePath, barrel], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); } }); -void test('follows local and imported aliases of Problem Details factories', () => { - assert.match( +it('follows local and imported aliases of Problem Details factories', () => { + expect( unconstrainedHttpApiContractSchemaViolation(` const factory = makeProblemDetailsSchema; const InvalidProblem = factory('InvalidProblem', 400, { field: Schema.Unknown }); `) ?? '', - /must use concrete/u, - ); - assert.equal( + ).toMatch(/must use concrete/u); + expect( unconstrainedHttpApiContractSchemaViolation(` const factory = makeProblemDetailsSchema; const ValidProblem = factory('ValidProblem', 400, { field: Schema.String }); `), - undefined, - ); + ).toBe(undefined); const sources = new Map([ [ @@ -1612,13 +1655,12 @@ void test('follows local and imported aliases of Problem Details factories', () `export const UnsafeExtensions = { values: Schema.Record(Schema.String, Schema.String) };`, ], ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(sources.get(contractApiFixturePath) ?? '', { file: contractApiFixturePath, sources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); for (const extensions of [ `{ field: Schema.Unknown }`, `{ values: Schema.Record(Schema.String, Schema.String) }`, @@ -1633,13 +1675,12 @@ void test('follows local and imported aliases of Problem Details factories', () `, ] as const, ]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(inlineSources.get(contractApiFixturePath) ?? '', { file: contractApiFixturePath, sources: inlineSources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); } for (const factoryName of ['makeProblemDetailsSchema', 'makeRetryableProblemDetailsSchema']) { const factorySources = new Map([ @@ -1670,13 +1711,12 @@ void test('follows local and imported aliases of Problem Details factories', () `, ]) { const aliasedSources = new Map([...factorySources, [contractApiFixturePath, entry] as const]); - assert.match( + expect( unconstrainedHttpApiContractSchemaViolation(entry, { file: contractApiFixturePath, sources: aliasedSources, }) ?? '', - /must use concrete/u, - ); + ).toMatch(/must use concrete/u); } } }); diff --git a/app/scripts/scaffolding/tests/resource-generator.test.mts b/app/scripts/scaffolding/tests/resource-generator.test.mts index a4a340716..2f87528ca 100644 --- a/app/scripts/scaffolding/tests/resource-generator.test.mts +++ b/app/scripts/scaffolding/tests/resource-generator.test.mts @@ -1,20 +1,31 @@ +import { linkFixtureDependencies, withCreatedFixture } from './fixture-ownership.mts'; import { snapshotTree, write } from './fixture-files.mts'; -import assert from 'node:assert/strict'; +import { Cause, Effect, Fiber, FileSystem, Schema } from 'effect'; +import { afterEach, expect, it, rs } from 'effect-rstest'; + +import { CodeSmith, GeneratorCore } from '@modern-js/codesmith'; +import { applyMutationPlanEffect } from '../shared.mts'; +import { NodeServices } from '@effect/platform-node'; + import { spawnSync } from 'node:child_process'; import { randomUUID } from 'node:crypto'; -import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import test from 'node:test'; + import { pathToFileURL } from 'node:url'; -import { Schema } from 'effect'; -import { getHelpText, runScaffold } from '../cli.mts'; +import { getHelpText, runScaffoldEffect, ScaffoldingError } from '../cli.mts'; + +afterEach(() => { + rs.restoreAllMocks(); +}); const appRoot = path.resolve(import.meta.dirname, '..', '..', '..'); const tscPath = path.join(appRoot, 'node_modules', '.bin', 'tsc'); const verticalName = 'property-registry'; const moduleId = 'property.registry'; const resourceName = 'rental-unit'; +const verticalFlag = '--vertical'; const resourceType = `${moduleId}.${resourceName}`; const tenantId = '00000000-0000-4000-8000-000000000001'; const verticalRoot = `verticals/${verticalName}`; @@ -37,59 +48,62 @@ type JsonValue = const json = (value: JsonValue): string => `${JSON.stringify(value, null, 2)}\n`; -const createFixture = async (): Promise => { - const root = await mkdtemp(path.join(tmpdir(), 'ontos-resource-scaffold-')); - await write(root, 'package.json', json({ name: 'fixture', private: true, type: 'module' })); - await write( - root, - verticalPackagePath, - json({ - dependencies: { effect: '4.0.0-beta.107' }, - exports: { '.': './src/index.ts' }, - modernjs: { - apiRuntime: 'effect', - appId: verticalName, - preset: 'presetUltramodern', - role: 'module-federation-remote', - topology: '../../topology/reference-topology.json', - }, - name: '@app/property-registry', - private: true, - scripts: { - build: 'modern build && MODERNJS_DEPLOY=node modern deploy --skip-build', - 'cloudflare:build': - 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy --skip-build', - }, - type: 'module', - version: '0.1.0', - }), - ); - await write( - root, - 'verticals/property-registry/tsconfig.json', - json({ - compilerOptions: { composite: true }, - include: ['src', 'shared'], - references: [], - }), - ); - await write( - root, - 'verticals/property-registry/module-federation.config.ts', - 'export default { exposes: {} };\n', - ); - await write( - root, - 'verticals/property-registry/shared/api.ts', - `import { HttpApi } from 'effect/unstable/httpapi'; +const createFixture = (): Effect.Effect => + Effect.gen(function* mergedScenario9() { + const root = yield* Effect.promise(() => + mkdtemp(path.join(tmpdir(), 'ontos-resource-scaffold-')), + ); + yield* write(root, 'package.json', json({ name: 'fixture', private: true, type: 'module' })); + yield* write( + root, + verticalPackagePath, + json({ + dependencies: { effect: '4.0.0-beta.107' }, + exports: { '.': './src/index.ts' }, + modernjs: { + apiRuntime: 'effect', + appId: verticalName, + preset: 'presetUltramodern', + role: 'module-federation-remote', + topology: '../../topology/reference-topology.json', + }, + name: '@app/property-registry', + private: true, + scripts: { + build: 'modern build && MODERNJS_DEPLOY=node modern deploy --skip-build', + 'cloudflare:build': + 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy --skip-build', + }, + type: 'module', + version: '0.1.0', + }), + ); + yield* write( + root, + 'verticals/property-registry/tsconfig.json', + json({ + compilerOptions: { composite: true }, + include: ['src', 'shared'], + references: [], + }), + ); + yield* write( + root, + 'verticals/property-registry/module-federation.config.ts', + 'export default { exposes: {} };\n', + ); + yield* write( + root, + 'verticals/property-registry/shared/api.ts', + `import { HttpApi } from 'effect/unstable/httpapi'; export const propertyRegistryApi = HttpApi.make('PropertyRegistryApi'); `, - ); - await write( - root, - 'verticals/property-registry/api/index.ts', - `import { defineEffectBff, HttpApiBuilder, Layer } from '@modern-js/plugin-bff/effect-edge'; + ); + yield* write( + root, + 'verticals/property-registry/api/index.ts', + `import { defineEffectBff, HttpApiBuilder, Layer } from '@modern-js/plugin-bff/effect-edge'; import type { EffectRuntimeLayer } from '@modern-js/plugin-bff/effect-edge'; import { propertyRegistryApi } from '../shared/api.ts'; @@ -99,31 +113,31 @@ const layer = HttpApiBuilder.layer(propertyRegistryApi).pipe( export default defineEffectBff({ api: propertyRegistryApi, layer }); `, - ); - await write( - root, - 'topology/reference-topology.json', - json({ - schemaVersion: 1, - verticals: [ - { - domain: 'property', - id: verticalName, - kind: 'vertical', - moduleFederation: { - name: 'verticalPropertyRegistry', - role: 'remote', + ); + yield* write( + root, + 'topology/reference-topology.json', + json({ + schemaVersion: 1, + verticals: [ + { + domain: 'property', + id: verticalName, + kind: 'vertical', + moduleFederation: { + name: 'verticalPropertyRegistry', + role: 'remote', + }, + package: '@app/property-registry', + path: 'verticals/property-registry', }, - package: '@app/property-registry', - path: 'verticals/property-registry', - }, - ], - }), - ); - await write( - root, - 'types/core-runtime.d.ts', - `import type { Schema } from 'effect'; + ], + }), + ); + yield* write( + root, + 'types/core-runtime.d.ts', + `import type { Schema } from 'effect'; export interface OntosResourceType { readonly capabilities: { @@ -146,214 +160,334 @@ export declare const ShellPublicComponentContributionSchema: Schema.Codec; export declare const ShellSearchContributionSchema: Schema.Codec; `, - ); - await mkdir(path.join(root, 'node_modules', '@app'), { recursive: true }); - await symlink( - path.join(appRoot, 'packages/core-runtime'), - path.join(root, 'node_modules/@app/core-runtime'), - 'dir', - ); - await symlink(path.join(appRoot, 'node_modules/effect'), path.join(root, 'node_modules/effect')); - await runScaffold('module-contract', ['--vertical', verticalName, '--module', moduleId], { - workspaceRoot: root, + ); + yield* linkFixtureDependencies(root, appRoot, { + '@app/core-runtime': 'packages/core-runtime', + effect: 'node_modules/effect', + }); + yield* runScaffoldEffect( + 'module-contract', + [verticalFlag, verticalName, '--module', moduleId], + { + workspaceRoot: root, + }, + ).pipe(Effect.provide(NodeServices.layer)); + return root; }); - return root; -}; -const withFixture = async (run: (root: string) => Promise): Promise => { - const root = await createFixture(); - try { - await run(root); - } finally { - await rm(root, { force: true, recursive: true }); - } -}; +const withFixture = withCreatedFixture(createFixture()); -const scaffoldResource = async (root: string, resource = resourceName) => - await runScaffold('resource', ['--vertical', verticalName, '--resource', resource], { - workspaceRoot: root, - }); +const scaffoldResource = Effect.fn(function* scenario7(root: string, resource = resourceName) { + return yield* runScaffoldEffect( + 'resource', + [verticalFlag, verticalName, '--resource', resource], + { + workspaceRoot: root, + }, + ).pipe(Effect.provide(NodeServices.layer)); +}); -/** - * A refused resource scaffold must leave the fixture tree byte-identical, so each guard proves - * its own rejection message against a snapshot taken immediately before the run. - */ -const assertResourceScaffoldRefused = async ( +/** Refusal must preserve the fixture byte-for-byte. */ +const assertResourceScaffoldRefused = Effect.fn(function* assertResourceScaffoldRefused( root: string, expected: RegExp, ignored: readonly string[] = ['node_modules'], -): Promise => { - const before = await snapshotTree(root, ignored); - await assert.rejects(scaffoldResource(root), expected); - assert.deepEqual(await snapshotTree(root, ignored), before); -}; - -await test('resource help documents the public command and writes nothing', async () => { - const missingRoot = path.join(tmpdir(), 'resource-help-does-not-exist'); - const result = await runScaffold('resource', ['--help'], { - workspaceRoot: missingRoot, - }); - assert.deepEqual(result, { help: getHelpText('resource'), kind: 'help' }); - assert.match(result.help, /scaffold:resource -- --vertical --resource /u); - assert.match(result.help, /lower-kebab-case/u); +) { + const before = yield* snapshotTree(root, ignored); + const cause = yield* scaffoldResource(root).pipe(Effect.sandbox, Effect.flip); + expect(String(Cause.squash(cause))).toMatch(expected); + expect(yield* snapshotTree(root, ignored)).toEqual(before); }); -await test('resource scaffold publishes a typed ResourceRef and registers its descriptor', async () => { - await withFixture(async (root) => { - const result = await scaffoldResource(root); - assert.equal(result.kind, 'generated'); - - const resourcePath = path.join( - root, - 'verticals/property-registry/shared/resources/rental-unit.ts', - ); - const [resource, manifest, packageSource] = await Promise.all([ - readFile(resourcePath, 'utf-8'), - readFile(path.join(root, verticalManifestPath), 'utf-8'), - readFile(path.join(root, verticalPackagePath), 'utf-8'), - ]); - assert.match(resource, /import type \{ OntosResourceType \} from '@app\/core-runtime';/u); - assert.match(resource, /import \{ Schema \} from 'effect';/u); - assert.match(resource, /export const RentalUnitRefSchema = Schema\.Struct/u); - assert.match(resource, /Schema\.isMaxLength\(300\)/u); - assert.match(resource, /const TenantIdSchema = Schema\.String\.check\(Schema\.isUUID\(\)\)/u); - assert.match(resource, /moduleId: Schema\.Literal\('property\.registry'\)/u); - assert.match(resource, /resourceType: Schema\.Literal\('property\.registry\.rental-unit'\)/u); - assert.match(resource, /resourceId: ResourceIdSchema/u); - assert.match(resource, /tenantId: TenantIdSchema/u); - assert.match(resource, /export type RentalUnitRef = typeof RentalUnitRefSchema\.Type;/u); - assert.match( - resource, - /export const rentalUnitResourceDescriptor = \{[\s\S]*key: 'property\.registry\.rental-unit'/u, +it.live( + 'resource help documents the public command and writes nothing', + Effect.fn(function* scenario8() { + const missingRoot = path.join(tmpdir(), 'resource-help-does-not-exist'); + const result = yield* runScaffoldEffect('resource', ['--help'], { + workspaceRoot: missingRoot, + }).pipe(Effect.provide(NodeServices.layer)); + expect(result).toEqual({ help: getHelpText('resource'), kind: 'help' }); + if (result.kind !== 'help') { + throw new Error('Expected help result'); + } + expect(result.help).toMatch( + /scaffold:resource -- --vertical --resource /u, ); - assert.match(resource, /satisfies OntosResourceType/u); + expect(result.help).toMatch(/lower-kebab-case/u); + }), +); - assert.match( - manifest, - /import \{ rentalUnitResourceDescriptor \} from '\.\/shared\/resources\/rental-unit\.ts';/u, - ); - assert.match(manifest, /resourceTypes: \[[\s\S]*rentalUnitResourceDescriptor,/u); - const modulePackage = Schema.decodeUnknownSync(packageJsonSchema, { - onExcessProperty: 'preserve', - })(JSON.parse(packageSource)); - assert.equal( - modulePackage.exports['./resources/rental-unit'], - './shared/resources/rental-unit.ts', - ); +it.live( + 'resource scaffold publishes a typed ResourceRef and registers its descriptor', + Effect.fn(function* scenario9() { + yield* withFixture( + Effect.fn(function* scenario10(root) { + const result = yield* scaffoldResource(root); + expect(result.kind).toBe('generated'); - const generatedModule = Schema.decodeUnknownSync(generatedResourceModuleSchema)( - await import(`${pathToFileURL(resourcePath).href}?test=${randomUUID()}`), - ); - const rentalUnitRefSchema = Schema.make>( - generatedModule.RentalUnitRefSchema.ast, - ); - const reference = Schema.decodeUnknownSync(rentalUnitRefSchema)({ - moduleId, - resourceId: 'unit-42', - resourceType, - tenantId, - }); - assert.deepEqual(reference, { - moduleId, - resourceId: 'unit-42', - resourceType, - tenantId, - }); - assert.throws( - () => - Schema.decodeUnknownSync(rentalUnitRefSchema)({ + const resourcePath = path.join( + root, + 'verticals/property-registry/shared/resources/rental-unit.ts', + ); + const [resource, manifest, packageSource] = yield* Effect.all( + [ + Effect.promise(() => readFile(resourcePath, 'utf-8')), + Effect.promise(() => readFile(path.join(root, verticalManifestPath), 'utf-8')), + Effect.promise(() => readFile(path.join(root, verticalPackagePath), 'utf-8')), + ], + { concurrency: 'unbounded' }, + ); + expect(resource).toMatch(/import type \{ OntosResourceType \} from '@app\/core-runtime';/u); + expect(resource).toMatch(/import \{ Schema \} from 'effect';/u); + expect(resource).toMatch(/export const RentalUnitRefSchema = Schema\.Struct/u); + expect(resource).toMatch(/Schema\.isMaxLength\(300\)/u); + expect(resource).toMatch( + /const TenantIdSchema = Schema\.String\.check\(Schema\.isUUID\(\)\)/u, + ); + expect(resource).toMatch(/moduleId: Schema\.Literal\('property\.registry'\)/u); + expect(resource).toMatch( + /resourceType: Schema\.Literal\('property\.registry\.rental-unit'\)/u, + ); + expect(resource).toMatch(/resourceId: ResourceIdSchema/u); + expect(resource).toMatch(/tenantId: TenantIdSchema/u); + expect(resource).toMatch(/export type RentalUnitRef = typeof RentalUnitRefSchema\.Type;/u); + expect(resource).toMatch( + /export const rentalUnitResourceDescriptor = \{[\s\S]*key: 'property\.registry\.rental-unit'/u, + ); + expect(resource).toMatch(/satisfies OntosResourceType/u); + + expect(manifest).toMatch( + /import \{ rentalUnitResourceDescriptor \} from '\.\/shared\/resources\/rental-unit\.ts';/u, + ); + expect(manifest).toMatch(/resourceTypes: \[[\s\S]*rentalUnitResourceDescriptor,/u); + const modulePackage = yield* Schema.decodeUnknownEffect(packageJsonSchema, { + onExcessProperty: 'preserve', + })(JSON.parse(packageSource)); + expect(modulePackage.exports['./resources/rental-unit']).toBe( + './shared/resources/rental-unit.ts', + ); + + const generatedModule = yield* Schema.decodeUnknownEffect(generatedResourceModuleSchema)( + yield* Effect.promise( + () => import(`${pathToFileURL(resourcePath).href}?test=${randomUUID()}`), + ), + ); + const rentalUnitRefSchema = Schema.make>( + generatedModule.RentalUnitRefSchema.ast, + ); + const reference = yield* Schema.decodeUnknownEffect(rentalUnitRefSchema)({ + moduleId, + resourceId: 'unit-42', + resourceType, + tenantId, + }); + expect(reference).toEqual({ + moduleId, + resourceId: 'unit-42', + resourceType, + tenantId, + }); + const invalidReference = yield* Schema.decodeUnknownEffect(rentalUnitRefSchema)({ moduleId, resourceId: '', resourceType, tenantId, - }), - /length of at least 1/u, - ); + }).pipe(Effect.flip); + expect(String(invalidReference)).toMatch(/length of at least 1/u); - const fixtureTsconfig = path.join(root, 'tsconfig.generated.json'); - await write( - root, - 'tsconfig.generated.json', - json({ - compilerOptions: { - allowImportingTsExtensions: true, - module: 'preserve', - moduleResolution: 'Bundler', - noEmit: true, - paths: { - '@app/core-runtime': ['./types/core-runtime.d.ts'], - }, - skipLibCheck: true, - strict: true, - target: 'ESNext', - }, - include: ['verticals/property-registry/shared/resources/**/*.ts', verticalManifestPath], + const fixtureTsconfig = path.join(root, 'tsconfig.generated.json'); + yield* write( + root, + 'tsconfig.generated.json', + json({ + compilerOptions: { + allowImportingTsExtensions: true, + module: 'preserve', + moduleResolution: 'Bundler', + noEmit: true, + paths: { + '@app/core-runtime': ['./types/core-runtime.d.ts'], + }, + skipLibCheck: true, + strict: true, + target: 'ESNext', + }, + include: ['verticals/property-registry/shared/resources/**/*.ts', verticalManifestPath], + }), + ); + const compilation = spawnSync(tscPath, ['-p', fixtureTsconfig], { + cwd: root, + encoding: 'utf-8', + }); + expect(compilation.status, `${compilation.stdout}${compilation.stderr}`).toBe(0); }), ); - const compilation = spawnSync(tscPath, ['-p', fixtureTsconfig], { - cwd: root, - encoding: 'utf-8', - }); - assert.equal(compilation.status, 0, `${compilation.stdout}${compilation.stderr}`); - }); -}); + }), +); -await test('resource scaffold rejects traversal and reruns without partial writes', async () => { - await withFixture(async (root) => { - const beforeTraversal = await snapshotTree(root, ['node_modules']); - await assert.rejects(scaffoldResource(root, '../unsafe'), /lower-kebab-case/u); - assert.deepEqual(await snapshotTree(root, ['node_modules']), beforeTraversal); +it.live( + 'resource scaffold rejects traversal and reruns without partial writes', + Effect.fn(function* scenario11() { + yield* withFixture( + Effect.fn(function* scenario12(root) { + const beforeTraversal = yield* snapshotTree(root, ['node_modules']); + const failureCause1 = yield* Effect.flip( + Effect.sandbox(scaffoldResource(root, '../unsafe')), + ); + expect(String(Cause.squash(failureCause1))).toMatch(/lower-kebab-case/u); + expect(yield* snapshotTree(root, ['node_modules'])).toEqual(beforeTraversal); - await scaffoldResource(root); - await assertResourceScaffoldRefused(root, /refusing to overwrite existing business file/u); - }); -}); + yield* scaffoldResource(root); + yield* assertResourceScaffoldRefused(root, /refusing to overwrite existing business file/u); + }), + ); + }), +); -await test('resource scaffold leaves no artifact when generated owner slots or exports are invalid', async () => { - await withFixture(async (root) => { - const manifestPath = path.join(root, verticalManifestPath); - const manifest = await readFile(manifestPath, 'utf-8'); - await writeFile( - manifestPath, - manifest.replace('// ', '// invalid-resource-slot'), - 'utf-8', +it.live( + 'resource scaffold leaves no artifact when generated owner slots or exports are invalid', + Effect.fn(function* scenario13() { + yield* withFixture( + Effect.fn(function* scenario14(root) { + const manifestPath = path.join(root, verticalManifestPath); + const manifest = yield* Effect.promise(() => readFile(manifestPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + manifestPath, + manifest.replace( + '// ', + '// invalid-resource-slot', + ), + 'utf-8', + ), + ); + yield* assertResourceScaffoldRefused(root, /generated owner file/u); + }), ); - await assertResourceScaffoldRefused(root, /generated owner file/u); - }); - await withFixture(async (root) => { - const packagePath = path.join(root, verticalPackagePath); - const packageValue = Schema.decodeUnknownSync(packageJsonSchema, { - onExcessProperty: 'preserve', - })(JSON.parse(await readFile(packagePath, 'utf-8'))); - const packageWithExportCollision = { - ...packageValue, - exports: { - ...packageValue.exports, - './resources/rental-unit': './someone-elses-contract.ts', - }, - }; - await writeFile(packagePath, json(packageWithExportCollision), 'utf-8'); - await assertResourceScaffoldRefused(root, /resource contract export .* already exists/u); - }); -}); + yield* withFixture( + Effect.fn(function* scenario15(root) { + const packagePath = path.join(root, verticalPackagePath); + const packageValue = yield* Schema.decodeUnknownEffect(packageJsonSchema, { + onExcessProperty: 'preserve', + })(JSON.parse(yield* Effect.promise(() => readFile(packagePath, 'utf-8')))); + const packageWithExportCollision = { + ...packageValue, + exports: { + ...packageValue.exports, + './resources/rental-unit': './someone-elses-contract.ts', + }, + }; + yield* Effect.promise(() => + writeFile(packagePath, json(packageWithExportCollision), 'utf-8'), + ); + yield* assertResourceScaffoldRefused(root, /resource contract export .* already exists/u); + }), + ); + }), +); -await test('resource scaffold rejects a manifest without the governed resource slot', async () => { - await withFixture(async (root) => { - const manifestPath = path.join(root, verticalManifestPath); - const manifest = await readFile(manifestPath, 'utf-8'); - await writeFile( - manifestPath, - manifest.replace( - ` resourceTypes: [ +it.live( + 'resource scaffold rejects a manifest without the governed resource slot', + Effect.fn(function* mergedScenario12() { + yield* withFixture( + Effect.fn(function* mergedScenario11(root) { + const manifestPath = path.join(root, verticalManifestPath); + const manifest = yield* Effect.promise(() => readFile(manifestPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + manifestPath, + manifest.replace( + ` resourceTypes: [ // // ],`, - ' resourceTypes: [],', - ), - 'utf-8', + ' resourceTypes: [],', + ), + 'utf-8', + ), + ); + + yield* assertResourceScaffoldRefused(root, /slot/u, []); + }), ); + }), +); - await assertResourceScaffoldRefused(root, /slot/u, []); - }); -}); +it.live( + 'waits for an interrupted Codesmith write before removing its scoped output', + Effect.fn(function* scenario18() { + const root = yield* Effect.acquireRelease( + Effect.promise(() => mkdtemp(path.join(tmpdir(), 'ontos-scaffold-cancellation-'))), + (dir) => Effect.promise(() => rm(dir, { force: true, recursive: true })), + ); + const smith = new CodeSmith({ namespace: 'ontos-scaffolding-test' }); + const core = new GeneratorCore({ + logger: smith.logger, + materialsManager: smith.materialsManager, + outputPath: root, + }); + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); + const events: string[] = []; + rs.spyOn(core.output, 'fs').mockImplementation(async () => { + started.resolve(null); + // Codesmith owns this Promise-returning output callback. + await release.promise; + await mkdir(root, { recursive: true }); + await writeFile(path.join(root, 'generated.ts'), 'export {};'); + events.push('write'); + }); + const recordCleanup = () => events.push('cleanup'); + yield* Effect.gen(function* verifyWriteCleanupOrder() { + const fileSystem = yield* FileSystem.FileSystem; + const worker = yield* Effect.forkChild( + Effect.scoped( + Effect.gen(function* writeScopedOutput() { + yield* Effect.addFinalizer(() => + fileSystem + .remove(root, { force: true, recursive: true }) + .pipe(Effect.orDie, Effect.andThen(Effect.sync(recordCleanup))), + ); + yield* applyMutationPlanEffect(core, { + mutations: [ + { content: 'export {};', kind: 'create', path: path.join(root, 'generated.ts') }, + ], + result: null, + }); + }), + ), + ); + yield* Effect.promise(() => started.promise); + const interruption = yield* Effect.forkChild(Fiber.interrupt(worker)); + yield* Effect.yieldNow; + expect(events).toEqual([]); + release.resolve(null); + yield* Fiber.join(interruption); + expect(events).toEqual(['write', 'cleanup']); + expect(yield* fileSystem.exists(root)).toBe(false); + }).pipe(Effect.provide(NodeServices.layer)); + }), +); + +it.live( + 'reports synchronous malformed-owner validation through the typed command channel', + Effect.fn(function* scenario19() { + yield* withFixture( + Effect.fn(function* scenario20(root) { + yield* Effect.promise(() => writeFile(path.join(root, verticalPackagePath), '[]')); + const before = yield* snapshotTree(root, ['node_modules']); + const failure = yield* runScaffoldEffect( + 'resource', + [verticalFlag, verticalName, '--resource', resourceName], + { + workspaceRoot: root, + }, + ).pipe(Effect.flip, Effect.provide(NodeServices.layer)); + expect(Schema.is(ScaffoldingError)(failure)).toBe(true); + expect(failure.message).toMatch(/JSON object/u); + expect(yield* snapshotTree(root, ['node_modules'])).toEqual(before); + }), + ); + }), +); diff --git a/app/scripts/scaffolding/tests/retire-contribution.test.mts b/app/scripts/scaffolding/tests/retire-contribution.test.mts index 5e6e8e209..55cf10fd2 100644 --- a/app/scripts/scaffolding/tests/retire-contribution.test.mts +++ b/app/scripts/scaffolding/tests/retire-contribution.test.mts @@ -1,5 +1,4 @@ -import { NodeServices } from '@effect/platform-node'; -import { Effect, ManagedRuntime } from 'effect'; +import { withCreatedFixture } from './fixture-ownership.mts'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; import { fileURLToPath } from 'node:url'; import { @@ -8,18 +7,30 @@ import { removeGeneratedSlotEntry, } from '../shared.mts'; import { snapshotTree, write } from './fixture-files.mts'; -import assert from 'node:assert/strict'; -import { access, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { Cause, Effect } from 'effect'; +import { expect, it } from 'effect-rstest'; + +import { NodeServices } from '@effect/platform-node'; + +import { access, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import test from 'node:test'; -import { getHelpText, runScaffold } from '../cli.mts'; + +import { getHelpText, runScaffoldEffect } from '../cli.mts'; import type { JsonValue } from '../shared.mts'; +const expectFailure = (self: Effect.Effect, check: (cause: unknown) => void) => + Effect.matchCauseEffect(self, { + onFailure: (cause) => Effect.sync(() => check(Cause.squash(cause))), + onSuccess: () => + Effect.sync(() => { + throw new Error('Expected operation to fail'); + }), + }); + const json = (value: JsonValue): string => `${JSON.stringify(value, null, 2)}\n`; const VERTICAL_FLAG = '--vertical'; - const RETIRE_CONTRIBUTION_COMMAND = 'retire-contribution'; const ARCHIVE_ITEM = 'archive-item'; const ITEM_DETAIL = 'item-detail'; @@ -93,183 +104,232 @@ export const inventoryRegistration = { }; `; -const createFixture = async (): Promise => { - const root = await mkdtemp(path.join(tmpdir(), 'ontos-retire-contribution-')); - await write( - root, - 'verticals/inventory/package.json', - json({ - dependencies: { '@app/core-runtime': 'workspace:*' }, - exports: {}, - modernjs: { - appId: 'inventory', - ontosModule: { - contractPath: '/.well-known/ontos-module-manifest.json', - manifest: './vertical.manifest.ts', - moduleId: 'inventory.core', - registration: './vertical.registration.ts', - schemaVersion: 2, - }, - role: 'module-federation-remote', - topology: '../../topology/reference-topology.json', - }, - name: '@app/inventory', - scripts: {}, - }), - ); - await write( - root, - 'topology/reference-topology.json', - json({ - verticals: [ - { - id: 'inventory', - kind: 'vertical', - moduleFederation: { name: 'verticalInventory', role: 'remote' }, - package: '@app/inventory', - path: 'verticals/inventory', +const createFixture = (): Effect.Effect => + Effect.gen(function* scenario5() { + const root = yield* Effect.promise(() => + mkdtemp(path.join(tmpdir(), 'ontos-retire-contribution-')), + ); + yield* write( + root, + 'verticals/inventory/package.json', + json({ + dependencies: { '@app/core-runtime': 'workspace:*' }, + exports: {}, + modernjs: { + appId: 'inventory', + ontosModule: { + contractPath: '/.well-known/ontos-module-manifest.json', + manifest: './vertical.manifest.ts', + moduleId: 'inventory.core', + registration: './vertical.registration.ts', + schemaVersion: 2, + }, + role: 'module-federation-remote', + topology: '../../topology/reference-topology.json', }, - ], - }), - ); - await write(root, 'verticals/inventory/vertical.manifest.ts', manifest); - await write(root, 'verticals/inventory/vertical.registration.ts', registration); - await write( - root, - ARCHIVE_ITEM_ACTION_PATH, - `// @generated by OntOS Codesmith Action v1 + name: '@app/inventory', + scripts: {}, + }), + ); + yield* write( + root, + 'topology/reference-topology.json', + json({ + verticals: [ + { + id: 'inventory', + kind: 'vertical', + moduleFederation: { name: 'verticalInventory', role: 'remote' }, + package: '@app/inventory', + path: 'verticals/inventory', + }, + ], + }), + ); + yield* write(root, 'verticals/inventory/vertical.manifest.ts', manifest); + yield* write(root, 'verticals/inventory/vertical.registration.ts', registration); + yield* write( + root, + ARCHIVE_ITEM_ACTION_PATH, + `// @generated by OntOS Codesmith Action v1 // @ontos-action-owner inventory.core // @ontos-action-slug archive-item // // export const archiveItemAction = {}; `, - ); - const apiHeader = '// @generated by OntOS Codesmith module-api v1\n'; - await write( - root, - 'verticals/inventory/shared/apis/item-detail.ts', - `${apiHeader}export const ItemDetailApi = {};\n`, - ); - await write( - root, - 'verticals/inventory/src/api/item-detail.read.ts', - `${apiHeader}export const itemDetailRead = {};\n`, - ); - await write( - root, - 'verticals/inventory/src/api/item-detail-client.ts', - `${apiHeader}export const executeItemDetailWithAuthorization = () => {};\n`, - ); - await write( - root, - 'verticals/inventory/api/item-detail-read-server.ts', - `${apiHeader}export const itemDetailReadApiLive = {};\n`, - ); - await write( - root, - 'verticals/inventory/src/routes/[lang]/inventory/items/[id]/page.tsx', - 'export const ItemDetailPage = () => null;\n', - ); - return root; -}; + ); + const apiHeader = '// @generated by OntOS Codesmith module-api v1\n'; + yield* write( + root, + 'verticals/inventory/shared/apis/item-detail.ts', + `${apiHeader}export const ItemDetailApi = {};\n`, + ); + yield* write( + root, + 'verticals/inventory/src/api/item-detail.read.ts', + `${apiHeader}export const itemDetailRead = {};\n`, + ); + yield* write( + root, + 'verticals/inventory/src/api/item-detail-client.ts', + `${apiHeader}export const executeItemDetailWithAuthorization = () => {};\n`, + ); + yield* write( + root, + 'verticals/inventory/api/item-detail-read-server.ts', + `${apiHeader}export const itemDetailReadApiLive = {};\n`, + ); + yield* write( + root, + 'verticals/inventory/src/routes/[lang]/inventory/items/[id]/page.tsx', + 'export const ItemDetailPage = () => null;\n', + ); + return root; + }); -const withFixture = async (run: (root: string) => Promise): Promise => { - const root = await createFixture(); - try { - await run(root); - } finally { - await rm(root, { force: true, recursive: true }); - } -}; +const withFixture = withCreatedFixture(createFixture()); -const retire = async (root: string, kind: 'action' | 'api' | 'page', name: string) => - await runScaffold( +const retire = Effect.fn(function* scenario7( + root: string, + kind: 'action' | 'api' | 'page', + name: string, +) { + return yield* runScaffoldEffect( RETIRE_CONTRIBUTION_COMMAND, [VERTICAL_FLAG, 'inventory', '--kind', kind, '--name', name], { workspaceRoot: root }, - ); - -await test('retire-contribution help is write-free and documents the narrow kinds', async () => { - const result = await runScaffold(RETIRE_CONTRIBUTION_COMMAND, ['--help'], { - workspaceRoot: path.join(tmpdir(), 'retire-help-missing'), - }); - assert.deepEqual(result, { help: getHelpText(RETIRE_CONTRIBUTION_COMMAND), kind: 'help' }); - assert.match(result.help, /--kind /u); + ).pipe(Effect.provide(NodeServices.layer)); }); -await test('retires generated Actions, APIs, and page exposure while preserving page source', async () => { - await withFixture(async (root) => { - await retire(root, 'action', ARCHIVE_ITEM); - await assert.rejects(access(path.join(root, ARCHIVE_ITEM_ACTION_PATH))); +it.live( + 'retire-contribution help is write-free and documents the narrow kinds', + Effect.fn(function* scenario8() { + const result = yield* runScaffoldEffect(RETIRE_CONTRIBUTION_COMMAND, ['--help'], { + workspaceRoot: path.join(tmpdir(), 'retire-help-missing'), + }).pipe(Effect.provide(NodeServices.layer)); + expect(result).toEqual({ help: getHelpText(RETIRE_CONTRIBUTION_COMMAND), kind: 'help' }); + if (result.kind !== 'help') { + throw new Error('Expected help result'); + } + expect(result.help).toMatch(/--kind /u); + }), +); - await retire(root, 'api', ITEM_DETAIL); - await Promise.all( - [ - 'shared/apis/item-detail.ts', - 'src/api/item-detail.read.ts', - 'src/api/item-detail-client.ts', - 'api/item-detail-read-server.ts', - ].map( - async (relative) => - await assert.rejects(access(path.join(root, 'verticals/inventory', relative))), - ), - ); +it.live( + 'retires generated Actions, APIs, and page exposure while preserving page source', + Effect.fn(function* scenario9() { + yield* withFixture( + Effect.fn(function* scenario10(root) { + yield* retire(root, 'action', ARCHIVE_ITEM); + yield* expectFailure( + Effect.promise(() => access(path.join(root, ARCHIVE_ITEM_ACTION_PATH))), + (error) => expect(error).toBeDefined(), + ); - await retire(root, 'page', ITEM_DETAIL); - await access( - path.join(root, 'verticals/inventory/src/routes/[lang]/inventory/items/[id]/page.tsx'), - ); - const [nextManifest, nextRegistration] = await Promise.all([ - readFile(path.join(root, 'verticals/inventory/vertical.manifest.ts'), 'utf-8'), - readFile(path.join(root, 'verticals/inventory/vertical.registration.ts'), 'utf-8'), - ]); - assert.doesNotMatch( - nextManifest, - /archiveItemAction|ItemDetailApi|ItemDetailPage|item-detail/u, + yield* retire(root, 'api', ITEM_DETAIL); + yield* Effect.all( + [ + 'shared/apis/item-detail.ts', + 'src/api/item-detail.read.ts', + 'src/api/item-detail-client.ts', + 'api/item-detail-read-server.ts', + ].map( + Effect.fn(function* scenario11(relative) { + return yield* expectFailure( + Effect.promise(() => access(path.join(root, 'verticals/inventory', relative))), + (error) => expect(error).toBeDefined(), + ); + }), + ), + { concurrency: 'unbounded' }, + ); + + yield* retire(root, 'page', ITEM_DETAIL); + yield* Effect.promise(() => + access( + path.join(root, 'verticals/inventory/src/routes/[lang]/inventory/items/[id]/page.tsx'), + ), + ); + const [nextManifest, nextRegistration] = yield* Effect.all( + [ + Effect.promise(() => + readFile(path.join(root, 'verticals/inventory/vertical.manifest.ts'), 'utf-8'), + ), + Effect.promise(() => + readFile(path.join(root, 'verticals/inventory/vertical.registration.ts'), 'utf-8'), + ), + ], + { concurrency: 'unbounded' }, + ); + expect(nextManifest).not.toMatch( + /archiveItemAction|ItemDetailApi|ItemDetailPage|item-detail/u, + ); + expect(nextRegistration).not.toMatch(/archiveItemAction|item-detail/u); + }), ); - assert.doesNotMatch(nextRegistration, /archiveItemAction|item-detail/u); - }); -}); + }), +); -await test('refuses traversal, reruns, customized artifacts, and dependent Actions atomically', async () => { - await withFixture(async (root) => { - const beforeTraversal = await snapshotTree(root); - await assert.rejects(retire(root, 'action', '../archive-item'), /lower-kebab-case/u); - assert.deepEqual(await snapshotTree(root), beforeTraversal); +it.live( + 'refuses traversal, reruns, customized artifacts, and dependent Actions atomically', + Effect.fn(function* scenario12() { + yield* withFixture( + Effect.fn(function* scenario13(root) { + const beforeTraversal = yield* snapshotTree(root); + yield* expectFailure(retire(root, 'action', '../archive-item'), (error) => + expect(String(error)).toMatch(/lower-kebab-case/u), + ); + expect(yield* snapshotTree(root)).toEqual(beforeTraversal); - const actionPath = path.join(root, ARCHIVE_ITEM_ACTION_PATH); - await writeFile(actionPath, 'export const archiveItemAction = {};\n', 'utf-8'); - const beforeCustomized = await snapshotTree(root); - await assert.rejects(retire(root, 'action', ARCHIVE_ITEM), /matching generated Action/u); - assert.deepEqual(await snapshotTree(root), beforeCustomized); - }); + const actionPath = path.join(root, ARCHIVE_ITEM_ACTION_PATH); + yield* Effect.promise(() => + writeFile(actionPath, 'export const archiveItemAction = {};\n', 'utf-8'), + ); + const beforeCustomized = yield* snapshotTree(root); + yield* expectFailure(retire(root, 'action', ARCHIVE_ITEM), (error) => + expect(String(error)).toMatch(/matching generated Action/u), + ); + expect(yield* snapshotTree(root)).toEqual(beforeCustomized); + }), + ); - await withFixture(async (root) => { - const actionPath = path.join(root, ARCHIVE_ITEM_ACTION_PATH); - const action = await readFile(actionPath, 'utf-8'); - await writeFile( - actionPath, - action.replace( - '// ', - 'export const createMessage = () => ({});\n// ', - ), - 'utf-8', + yield* withFixture( + Effect.fn(function* scenario14(root) { + const actionPath = path.join(root, ARCHIVE_ITEM_ACTION_PATH); + const action = yield* Effect.promise(() => readFile(actionPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + actionPath, + action.replace( + '// ', + 'export const createMessage = () => ({});\n// ', + ), + 'utf-8', + ), + ); + const beforeDependent = yield* snapshotTree(root); + yield* expectFailure(retire(root, 'action', ARCHIVE_ITEM), (error) => + expect(String(error)).toMatch(/published Outbox dependents/u), + ); + expect(yield* snapshotTree(root)).toEqual(beforeDependent); + }), ); - const beforeDependent = await snapshotTree(root); - await assert.rejects(retire(root, 'action', ARCHIVE_ITEM), /published Outbox dependents/u); - assert.deepEqual(await snapshotTree(root), beforeDependent); - }); - await withFixture(async (root) => { - await retire(root, 'page', ITEM_DETAIL); - const retired = await snapshotTree(root); - await assert.rejects(retire(root, 'page', ITEM_DETAIL), /exactly one generated/u); - assert.deepEqual(await snapshotTree(root), retired); - }); -}); + yield* withFixture( + Effect.fn(function* scenario15(root) { + yield* retire(root, 'page', ITEM_DETAIL); + const retired = yield* snapshotTree(root); + yield* expectFailure(retire(root, 'page', ITEM_DETAIL), (error) => + expect(String(error)).toMatch(/exactly one generated/u), + ); + expect(yield* snapshotTree(root)).toEqual(retired); + }), + ); + }), +); -await test('slot insertion and retirement share multiline indentation and preserve the owner suffix', () => { +it('slot insertion and retirement share multiline indentation and preserve the owner suffix', () => { const start = '// '; const end = '// '; const content = `before @@ -280,17 +340,14 @@ after`; key: "example", }),`; const inserted = insertSortedSlot(content, start, end, [entry], () => true); - assert.equal( - inserted, - `before + expect(inserted).toBe(`before ${start} item({ key: "example", }), ${end} -after`, - ); - assert.deepEqual(readGeneratedSlotEntries(inserted, start, end), [entry]); +after`); + expect(readGeneratedSlotEntries(inserted, start, end)).toEqual([entry]); const removed = removeGeneratedSlotEntry( inserted, start, @@ -298,72 +355,67 @@ after`, (candidate) => candidate === entry, 'item', ); - assert.equal( - removed, - `before + expect(removed).toBe(`before ${start} ${end} -after`, - ); - assert.throws( - () => removeGeneratedSlotEntry(removed, start, end, () => true, 'item'), +after`); + expect(() => removeGeneratedSlotEntry(removed, start, end, () => true, 'item')).toThrow( /found 0/u, ); }); -await test('native scaffold CLI maps kebab-case flags and forwards trailing arguments', async (context) => { - const cliRuntime = ManagedRuntime.make(NodeServices.layer); - context.after(async () => await cliRuntime.dispose()); - const cases = [ - { - args: [ - 'action', - '--action', - 'archive-item', - VERTICAL_FLAG, - 'inventory', - '--legal-entity-scope', - 'optional', - '--authorization', - 'action_execution', - '--provisioning', - 'invalid', - ], - message: '--provisioning must be tenant_membership_default or explicit', - }, - { - args: [ - 'search-provider-access', - VERTICAL_FLAG, - 'inventory', - '--name', - 'items', - '--legal-entity-scope', - 'required', - '--access-filtering', - 'tenant_scope', - '--request-filters', - 'role', - ], - message: 'search provider access flags are internally inconsistent', - }, - { - args: [ - 'retire-contribution', - '--', - VERTICAL_FLAG, - 'inventory', - '--name', - 'item', - '--kind', - 'invalid', - ], - message: '--kind must be action, api, or page', - }, - ]; - await cliRuntime.runPromise( - Effect.gen(function* nativeCliFlags() { +it.live( + 'native scaffold CLI maps kebab-case flags and forwards trailing arguments', + Effect.fn(function* nativeCliFlagsTest() { + const cases = [ + { + args: [ + 'action', + '--action', + 'archive-item', + VERTICAL_FLAG, + 'inventory', + '--legal-entity-scope', + 'optional', + '--authorization', + 'action_execution', + '--provisioning', + 'invalid', + ], + message: '--provisioning must be tenant_membership_default or explicit', + }, + { + args: [ + 'search-provider-access', + VERTICAL_FLAG, + 'inventory', + '--name', + 'items', + '--legal-entity-scope', + 'required', + '--access-filtering', + 'tenant_scope', + '--request-filters', + 'role', + ], + message: 'search provider access flags are internally inconsistent', + }, + { + args: [ + 'retire-contribution', + '--', + VERTICAL_FLAG, + 'inventory', + '--name', + 'item', + '--kind', + 'invalid', + ], + message: '--kind must be action, api, or page', + }, + ]; + yield* Effect.gen(function* nativeCliFlags() { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; for (const { args, message } of cases) { const output = yield* spawner.string( @@ -372,8 +424,8 @@ await test('native scaffold CLI maps kebab-case flags and forwards trailing argu ...args, ]), ); - assert.ok(output.includes(message), output); + expect(output.includes(message), output).toBeTruthy(); } - }), - ); -}); + }).pipe(Effect.provide(NodeServices.layer)); + }), +); diff --git a/app/scripts/scaffolding/tests/scaffold-generators.test.mts b/app/scripts/scaffolding/tests/scaffold-generators.test.mts index a44c8d9a2..69243e9be 100644 --- a/app/scripts/scaffolding/tests/scaffold-generators.test.mts +++ b/app/scripts/scaffolding/tests/scaffold-generators.test.mts @@ -1,21 +1,19 @@ -import { runEffectTestPromise } from '../../../packages/core-runtime/src/testing/effect-runtime.ts'; -import assert from 'node:assert/strict'; +import { linkFixtureDependencies } from './fixture-ownership.mts'; +import { snapshotTree, write } from './fixture-files.mts'; +import { Cause, Clock, ConfigProvider, Predicate, Redacted } from 'effect'; +import type { Scope } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { NodeServices } from '@effect/platform-node'; + import { spawnSync } from 'node:child_process'; import { mkdtemp, mkdir, readFile, readdir, rm, stat, symlink, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; -import nodeTest from 'node:test'; -import { Clock, ConfigProvider, Predicate, Redacted } from 'effect'; -import { defineAction } from '../../../packages/core-runtime/src/actions/definition.ts'; -import { GatewayAssertionRedemptionService } from '../../../packages/core-runtime/src/auth/gateway-assertion-redemption.ts'; -import { defineSystemModuleEntrypoint } from '../../../packages/core-runtime/src/modules/module-entrypoint.ts'; -import { makeActionTestHarness } from '../../../packages/core-runtime/src/testing/actions.ts'; + import { TrustedPrincipalContextSchema } from '../../../packages/core-runtime/src/actions/principal-context.ts'; import type { TrustedPrincipalContext } from '../../../packages/core-runtime/src/actions/principal-context.ts'; -import type { GatewayPrincipalVerifierLive } from '../../../packages/gateway-principal-verifier/src/server.ts'; -import type { bindActionHttpRunner as ActionHttpRunnerBinding } from '../../../verticals/party-registry/api/action-http-runner.ts'; import { defineEffectBff, Effect, @@ -40,9 +38,20 @@ import { makeGatewayIssuerLayer, } from '../../../apps/shell-super-app/api/auth/gateway-issuer.ts'; import type { GatewayIssuerConfigValue } from '../../../apps/shell-super-app/api/auth/gateway-issuer-config.ts'; -import { getHelpText, runScaffold } from '../cli.mts'; -import { hasValidGovernedHttpCompositionRoot } from '../../generated-governed-http-boundary.mts'; +import { getHelpText, runScaffoldEffect, ScaffoldingError } from '../cli.mts'; import type { ScaffoldCommand } from '../cli.mts'; +import type { JsonValue } from '../shared.mts'; +import { + assertPublishedOutboxDependencyUsage, + publishedOutboxContractExports, +} from '../../published-outbox-contracts.mts'; +import { defineAction } from '../../../packages/core-runtime/src/actions/definition.ts'; +import { GatewayAssertionRedemptionService } from '../../../packages/core-runtime/src/auth/gateway-assertion-redemption.ts'; +import { defineSystemModuleEntrypoint } from '../../../packages/core-runtime/src/modules/module-entrypoint.ts'; +import { makeActionTestHarness } from '../../../packages/core-runtime/src/testing/actions.ts'; +import type { GatewayPrincipalVerifierLive } from '../../../packages/gateway-principal-verifier/src/server.ts'; +import type { bindActionHttpRunner as ActionHttpRunnerBinding } from '../../../verticals/party-registry/api/action-http-runner.ts'; +import { hasValidGovernedHttpCompositionRoot } from '../../generated-governed-http-boundary.mts'; import { GOVERNED_HTTP_API_ADDITION_SLOT_END, GOVERNED_HTTP_API_ADDITION_SLOT_START, @@ -57,29 +66,15 @@ import { insertSortedSlot, readGeneratedSlotEntries, } from '../shared.mts'; -import type { JsonValue } from '../shared.mts'; -import { - assertPublishedOutboxDependencyUsage, - publishedOutboxContractExports, -} from '../../published-outbox-contracts.mts'; - -interface Fixture { - readonly root: string; -} - -const GeneratedPrincipalErrorTagSchema = Schema.Literals([ - 'ActionPrincipalConfigurationError', - 'ActionPrincipalExpiredError', - 'ActionPrincipalInvalidError', - 'ActionPrincipalMissingError', - 'ActionPrincipalScopeError', - 'ActionPrincipalUnavailableError', -]); -type GeneratedPrincipalErrorTag = typeof GeneratedPrincipalErrorTagSchema.Type; -const test = (name: string, handler: () => void | Promise): void => { - void nodeTest(name, handler); -}; +const expectFailure = (self: Effect.Effect, check: (cause: unknown) => void) => + Effect.matchCauseEffect(self, { + onFailure: (cause) => Effect.sync(() => check(Cause.squash(cause))), + onSuccess: () => + Effect.sync(() => { + throw new Error('Expected operation to fail'); + }), + }); class FirstScaffoldTestError extends Schema.TaggedError()( 'FirstScaffoldTestError', @@ -97,57 +92,58 @@ const secondScaffoldErrors = createScaffoldErrorTools( 'second update failed', ); -test('scaffold error tools preserve success and own failure identity', async () => { - const value = { unchanged: true }; - assert.equal(await runEffectTestPromise(firstScaffoldErrors.trySync(() => value)), value); - const own = firstScaffoldErrors.scaffoldError('own failure'); - const failure = await runEffectTestPromise( - firstScaffoldErrors +it.effect( + 'scaffold error tools preserve success and own failure identity', + Effect.fn(function* ownScaffoldFailure() { + const value = { unchanged: true }; + expect(yield* firstScaffoldErrors.trySync(() => value)).toBe(value); + const own = firstScaffoldErrors.scaffoldError('own failure'); + const failure = yield* firstScaffoldErrors .trySync(() => { throw own; }) - .pipe(Effect.flip), - ); - assert.equal(failure, own); -}); + .pipe(Effect.flip); + expect(failure).toBe(own); + }), +); -test('scaffold error tools omit undefined causes and retain defined causes', () => { - assert.equal(Object.hasOwn(firstScaffoldErrors.scaffoldError('absent'), 'cause'), false); +it('scaffold error tools omit undefined causes and retain defined causes', () => { + expect(Object.hasOwn(firstScaffoldErrors.scaffoldError('absent'), 'cause')).toBe(false); const absentCause = firstScaffoldErrors.scaffoldError('absent').cause; - assert.equal( - Object.hasOwn(firstScaffoldErrors.scaffoldError('undefined', absentCause), 'cause'), + expect(Object.hasOwn(firstScaffoldErrors.scaffoldError('undefined', absentCause), 'cause')).toBe( false, ); for (const cause of [null, false, 0, '', { detail: 'retained' }]) { const failure = firstScaffoldErrors.scaffoldError('defined', cause); - assert.equal(Object.hasOwn(failure, 'cause'), true); - assert.equal(failure.cause, cause); + expect(Object.hasOwn(failure, 'cause')).toBe(true); + expect(failure.cause).toBe(cause); } }); -test('scaffold error tools normalize foreign errors without accepting another owner', async () => { - const foreign = secondScaffoldErrors.scaffoldError('foreign owner'); - assert.equal(Schema.is(FirstScaffoldTestError)(foreign), false); - assert.equal(Schema.is(ScaffoldFailure)(foreign), true); - const emptyMessageError = new Error('initial'); - emptyMessageError.message = ''; - await runEffectTestPromise( - Effect.gen(function* foreignScaffoldErrors() { +it.effect( + 'scaffold error tools normalize foreign errors without accepting another owner', + Effect.fn(function* foreignScaffoldFailure() { + const foreign = secondScaffoldErrors.scaffoldError('foreign owner'); + expect(Schema.is(FirstScaffoldTestError)(foreign)).toBe(false); + expect(Schema.is(ScaffoldFailure)(foreign)).toBe(true); + const emptyMessageError = new Error('initial'); + emptyMessageError.message = ''; + yield* Effect.gen(function* foreignScaffoldErrors() { for (const cause of [new Error('foreign error'), emptyMessageError, foreign]) { const failure = yield* firstScaffoldErrors .trySync(() => { throw cause; }) .pipe(Effect.flip); - assert.notEqual(failure, cause); - assert.equal(Schema.is(FirstScaffoldTestError)(failure), true); - assert.equal(Schema.is(ScaffoldFailure)(failure), false); - assert.equal(failure.message, cause.message); - assert.equal(failure.cause, cause); + expect(failure).not.toBe(cause); + expect(Schema.is(FirstScaffoldTestError)(failure)).toBe(true); + expect(Schema.is(ScaffoldFailure)(failure)).toBe(false); + expect(failure.message).toBe(cause.message); + expect(failure.cause).toBe(cause); } - }), - ); -}); + }); + }), +); for (const [index, cause] of [ undefined, @@ -155,34 +151,49 @@ for (const [index, cause] of [ 'thrown string', { message: 'not an Error' }, ].entries()) { - test(`scaffold error tools use owner fallback for non-error ${index}`, async () => { - const operation = () => { - const iterator = (function* thrownValue() { - yield cause; - })(); - iterator.next(); - return iterator.throw(cause); - }; - const first = await runEffectTestPromise( - firstScaffoldErrors.trySync(operation).pipe(Effect.flip), - ); - const second = await runEffectTestPromise( - secondScaffoldErrors.trySync(operation).pipe(Effect.flip), - ); - assert.equal(first.message, 'first update failed'); - assert.equal(second.message, 'second update failed'); - for (const failure of [first, second]) { - assert.equal(failure.cause, cause); - assert.equal(Object.hasOwn(failure, 'cause'), cause !== undefined); - } - }); + it.effect( + `scaffold error tools use owner fallback for non-error ${index}`, + Effect.fn(function* nonErrorScaffoldFailure() { + const operation = () => { + const iterator = (function* thrownValue() { + yield cause; + })(); + iterator.next(); + return iterator.throw(cause); + }; + const first = yield* firstScaffoldErrors.trySync(operation).pipe(Effect.flip); + const second = yield* secondScaffoldErrors.trySync(operation).pipe(Effect.flip); + expect(first.message).toBe('first update failed'); + expect(second.message).toBe('second update failed'); + for (const failure of [first, second]) { + expect(failure.cause).toBe(cause); + expect(Object.hasOwn(failure, 'cause')).toBe(cause !== undefined); + } + }), + ); +} + +interface Fixture { + readonly root: string; } +const GeneratedPrincipalErrorTagSchema = Schema.Literals([ + 'ActionPrincipalConfigurationError', + 'ActionPrincipalExpiredError', + 'ActionPrincipalInvalidError', + 'ActionPrincipalMissingError', + 'ActionPrincipalScopeError', + 'ActionPrincipalUnavailableError', +]); +type GeneratedPrincipalErrorTag = typeof GeneratedPrincipalErrorTagSchema.Type; + const isGeneratedPrincipalError = (tag: GeneratedPrincipalErrorTag) => Schema.is(Schema.Struct({ _tag: Schema.Literal(tag) })); const sharedContractsPackagePath = 'packages/shared-contracts'; + const sharedContractsNodeModulePath = 'node_modules/@app/shared-contracts'; + const partyGovernedContractPath = 'verticals/party-registry/shared/api.ts'; interface FixtureVertical { @@ -235,12 +246,14 @@ const GeneratedPrincipalModuleSchema = Schema.Struct({ Predicate.isFunction(value), ), }); + const GeneratedActionHttpRunnerModuleSchema = Schema.Struct({ bindActionHttpRunner: Schema.declare( (value): value is GeneratedActionHttpRunnerModule['bindActionHttpRunner'] => Predicate.isFunction(value), ), }); + const GeneratedOperationGatewayModuleSchema = Schema.Struct({ makeOperationGateway: Schema.declare( (value): value is GeneratedOperationGatewayModule['makeOperationGateway'] => @@ -287,6 +300,7 @@ type EndpointProblem = const bearerChallenge = HttpEffect.appendPreResponseHandler((_request, response) => Effect.succeed(HttpServerResponse.setHeader(response, 'www-authenticate', 'Bearer')), ); + const actionAuthenticationProblem = (): typeof ActionAuthenticationProblemSchema.Type => ({ _tag: 'ActionAuthenticationProblem', detail: 'A valid Bearer assertion is required.', @@ -294,6 +308,7 @@ const actionAuthenticationProblem = (): typeof ActionAuthenticationProblemSchema title: 'Action authentication required', type: 'https://ontos.dev/problems/action-authentication-required', }); + const actionVerificationUnavailableProblem = (): typeof ActionVerificationUnavailableProblemSchema.Type => ({ _tag: 'ActionVerificationUnavailableProblem', @@ -303,6 +318,7 @@ const actionVerificationUnavailableProblem = title: 'Action verification unavailable', type: 'https://ontos.dev/problems/action-verification-unavailable', }); + const failActionAuthentication = () => bearerChallenge.pipe(Effect.andThen(Effect.fail(actionAuthenticationProblem()))); const failActionVerificationUnavailable = () => @@ -315,7 +331,9 @@ const generatedPrincipalErrorHandlers = { ActionPrincipalScopeError: failActionAuthentication, ActionPrincipalUnavailableError: failActionVerificationUnavailable, }; + const GeneratedBindingResultSchema = Schema.Struct({ accepted: Schema.Literal(true) }); + const generatedBindingAction = defineAction( { accessEvidencePolicy: { @@ -343,6 +361,7 @@ const generatedBindingAction = defineAction( }, () => Effect.succeed({ accepted: true as const }), ); + const FixtureTsconfigSchema = Schema.Struct({ references: Schema.Array(Schema.Struct({ path: Schema.String })), }); @@ -354,11 +373,11 @@ const InventoryLocaleSchema = Schema.Struct({ }); const decodeFixturePackage = (source: string) => - Schema.decodeUnknownSync(FixturePackageSchema, { onExcessProperty: 'preserve' })( + Schema.decodeUnknownEffect(FixturePackageSchema, { onExcessProperty: 'preserve' })( JSON.parse(source), ); const decodeInventoryLocale = (source: string) => - Schema.decodeUnknownSync(InventoryLocaleSchema, { onExcessProperty: 'preserve' })( + Schema.decodeUnknownEffect(InventoryLocaleSchema, { onExcessProperty: 'preserve' })( JSON.parse(source), ); @@ -427,20 +446,33 @@ const inventorySearchProviderFile = 'verticals/inventory-stock/src/search/inventory-items.provider.ts'; const inventorySearchContractFile = 'verticals/inventory-stock/shared/apis/inventory-items-search.ts'; + const inventoryModuleApiContractFile = 'verticals/inventory-stock/shared/apis/resource-detail.ts'; + const inventoryModuleApiReadFile = 'verticals/inventory-stock/src/api/resource-detail.read.ts'; + const inventoryModuleApiClientFile = 'verticals/inventory-stock/src/api/resource-detail-client.ts'; + const inventoryModuleApiServerFile = 'verticals/inventory-stock/api/resource-detail-read-server.ts'; + const inventorySearchClientFile = 'verticals/inventory-stock/src/api/inventory-items-search-client.ts'; + const inventorySearchServerFile = 'verticals/inventory-stock/api/inventory-items-search-server.ts'; + const inventoryReportProviderFile = 'verticals/inventory-stock/src/reports/stock-levels.provider.ts'; + const inventoryReportContractFile = 'verticals/inventory-stock/shared/apis/stock-levels-report.ts'; + const inventoryReportClientFile = 'verticals/inventory-stock/src/api/stock-levels-report-client.ts'; + const inventoryReportServerFile = 'verticals/inventory-stock/api/stock-levels-report-server.ts'; + const inventoryActionPrincipalFile = 'verticals/inventory-stock/api/auth/action-principal.ts'; + const inventoryActionHttpRunnerFile = 'verticals/inventory-stock/api/action-http-runner.ts'; + const inventoryActionGatewayFile = 'verticals/inventory-stock/src/api/action-gateway.ts'; const inventoryPackageFile = 'verticals/inventory-stock/package.json'; const inventoryActionFile = 'verticals/inventory-stock/src/actions/create-order.action.ts'; @@ -486,7 +518,9 @@ const contactsVertical: FixtureVertical = { }; const json = (value: JsonValue): string => `${JSON.stringify(value, null, 2)}\n`; + const inventoryHandlerRootFile = 'verticals/inventory-stock/api/index.ts'; + const appRoot = path.resolve(import.meta.dirname, '..', '..', '..'); const require = createRequire(import.meta.url); const createEntry = require.resolve('@modern-js/create'); @@ -496,88 +530,85 @@ const esbuildPath = require.resolve('esbuild/bin/esbuild', { const oxfmtPath = path.join(appRoot, 'node_modules', '.bin', 'oxfmt'); const tscPath = path.join(appRoot, 'node_modules', '.bin', 'tsc'); -const makeGatewayKey = async ( +const makeGatewayKey = ( kid: string, -): Promise<{ - configuration: GatewayIssuerConfigValue; - publicJwk: JWK; -}> => { - const pair = await generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true }); - const privateJwk = await exportJWK(pair.privateKey); - const publicJwk = await exportJWK(pair.publicKey); - return { - configuration: { - issuer: fixtureGatewayIssuer, - privateJwk: { - alg: 'EdDSA', - crv: 'Ed25519', - d: privateJwk.d ?? '', - kid, - kty: 'OKP', - use: 'sig', - x: privateJwk.x ?? '', +): Effect.Effect< + { + configuration: GatewayIssuerConfigValue; + publicJwk: JWK; + }, + unknown +> => + Effect.gen(function* scenario1() { + const pair = yield* Effect.promise(() => + generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true }), + ); + const privateJwk = yield* Effect.promise(() => exportJWK(pair.privateKey)); + const publicJwk = yield* Effect.promise(() => exportJWK(pair.publicKey)); + return { + configuration: { + issuer: fixtureGatewayIssuer, + privateJwk: { + alg: 'EdDSA', + crv: 'Ed25519', + d: privateJwk.d ?? '', + kid, + kty: 'OKP', + use: 'sig', + x: privateJwk.x ?? '', + }, }, - }, - publicJwk: { ...publicJwk, alg: 'EdDSA', kid, use: 'sig' }, - }; -}; - -const writeFixtureFile = async ( - root: string, - relativePath: string, - content: string, -): Promise => { - const filePath = path.join(root, relativePath); - await mkdir(path.dirname(filePath), { recursive: true }); - await writeFile(filePath, content, 'utf-8'); -}; + publicJwk: { ...publicJwk, alg: 'EdDSA', kid, use: 'sig' }, + }; + }); -const createVertical = async (root: string, vertical: FixtureVertical): Promise => { - await writeFixtureFile( - root, - `verticals/${vertical.slug}/module-federation.config.ts`, - 'export default { exposes: {} };\n', - ); - await writeFixtureFile( - root, - `verticals/${vertical.slug}/tsconfig.json`, - json({ - compilerOptions: { composite: true }, - include: ['src', 'shared'], - references: [], - }), - ); - await writeFixtureFile( - root, - `verticals/${vertical.slug}/package.json`, - json({ - dependencies: { zeta: '1.0.0' }, - exports: { - './locales/cs': `./locales/cs/${vertical.namespace}.json`, - './locales/en': `./locales/en/${vertical.namespace}.json`, - }, - modernjs: { - apiRuntime: 'effect', - appId: vertical.appId, - preset: 'presetUltramodern', - role: 'module-federation-remote', - topology: '../../topology/reference-topology.json', - }, - name: `@app/${vertical.slug}`, - private: true, - scripts: { - build: 'modern build && MODERNJS_DEPLOY=node modern deploy --skip-build', - 'cloudflare:build': - 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy --skip-build', - existing: preservedFixtureValue, - }, - version: '0.1.0', - }), - ); - await writeFixtureFile( - root, - `verticals/${vertical.slug}/api/index.ts`, - `import { defineEffectBff, Effect, HttpApiBuilder, Layer } from '@modern-js/plugin-bff/effect-edge'; +const createVertical = (root: string, vertical: FixtureVertical): Effect.Effect => + Effect.gen(function* mergedScenario15() { + yield* write( + root, + `verticals/${vertical.slug}/module-federation.config.ts`, + 'export default { exposes: {} };\n', + ); + yield* write( + root, + `verticals/${vertical.slug}/tsconfig.json`, + json({ + compilerOptions: { composite: true }, + include: ['src', 'shared'], + references: [], + }), + ); + yield* write( + root, + `verticals/${vertical.slug}/package.json`, + json({ + dependencies: { zeta: '1.0.0' }, + exports: { + './locales/cs': `./locales/cs/${vertical.namespace}.json`, + './locales/en': `./locales/en/${vertical.namespace}.json`, + }, + modernjs: { + apiRuntime: 'effect', + appId: vertical.appId, + preset: 'presetUltramodern', + role: 'module-federation-remote', + topology: '../../topology/reference-topology.json', + }, + name: `@app/${vertical.slug}`, + private: true, + scripts: { + build: 'modern build && MODERNJS_DEPLOY=node modern deploy --skip-build', + 'cloudflare:build': + 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy --skip-build', + existing: preservedFixtureValue, + }, + version: '0.1.0', + }), + ); + yield* write( + root, + `verticals/${vertical.slug}/api/index.ts`, + `import { defineEffectBff, Effect, HttpApiBuilder, Layer } from '@modern-js/plugin-bff/effect-edge'; import type { EffectRuntimeLayer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; @@ -590,11 +621,11 @@ const layer = HttpApiBuilder.layer(fixtureApi).pipe( export default defineEffectBff({ api: fixtureApi, layer }); `, - ); - await writeFixtureFile( - root, - `verticals/${vertical.slug}/shared/api.ts`, - `import { Schema } from 'effect'; + ); + yield* write( + root, + `verticals/${vertical.slug}/shared/api.ts`, + `import { Schema } from 'effect'; import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/unstable/httpapi'; export const fixtureApi = HttpApi.make('FixtureApi').add( @@ -605,31 +636,33 @@ export const fixtureApi = HttpApi.make('FixtureApi').add( ), ); `, - ); - await Promise.all( - ['cs', 'en'].map( - async (locale) => - await writeFixtureFile( - root, - `verticals/${vertical.slug}/locales/${locale}/${vertical.namespace}.json`, - json({ - [vertical.namespace]: { - existing: `${locale}-preserved`, - }, - }), - ), - ), - ); - const resourcesName = `${vertical.slug - .split('-') - .map((segment, index) => - index === 0 ? segment : `${segment[0]?.toUpperCase() ?? ''}${segment.slice(1)}`, - ) - .join('')}I18nResources`; - await writeFixtureFile( - root, - `verticals/${vertical.slug}/src/i18n/resources.ts`, - `import csResource from '../../locales/cs/${vertical.namespace}.json'; + ); + yield* Effect.all( + ['cs', 'en'].map( + Effect.fn(function* mergedScenario14(locale) { + return yield* write( + root, + `verticals/${vertical.slug}/locales/${locale}/${vertical.namespace}.json`, + json({ + [vertical.namespace]: { + existing: `${locale}-preserved`, + }, + }), + ); + }), + ), + { concurrency: 'unbounded' }, + ); + const resourcesName = `${vertical.slug + .split('-') + .map((segment, index) => + index === 0 ? segment : `${segment[0]?.toUpperCase() ?? ''}${segment.slice(1)}`, + ) + .join('')}I18nResources`; + yield* write( + root, + `verticals/${vertical.slug}/src/i18n/resources.ts`, + `import csResource from '../../locales/cs/${vertical.namespace}.json'; import enResource from '../../locales/en/${vertical.namespace}.json'; type LocaleResource = string | { readonly [key: string]: LocaleResource }; @@ -654,26 +687,27 @@ export const ${resourcesName} = { en: { ${vertical.namespace}: flattenLocaleResource(enResource) }, } as const; `, - ); - await writeFixtureFile( - root, - `verticals/${vertical.slug}/src/routes/ultramodern-route-head.tsx`, - 'export const UltramodernRouteHead = () => null;\n', - ); -}; + ); + yield* write( + root, + `verticals/${vertical.slug}/src/routes/ultramodern-route-head.tsx`, + 'export const UltramodernRouteHead = () => null;\n', + ); + }); -const createFixture = async (): Promise => { - const root = await mkdtemp(path.join(tmpdir(), 'ontos-scaffolding-')); - await writeFixtureFile(root, rootPackageFile, json({ name: 'fixture', private: true })); - await writeFixtureFile( - root, - coreRuntimeIndexFile, - `export const existingCoreSurface = true;\n\n// \n// \n\n// \n// \n`, - ); - await writeFixtureFile( - root, - coreActionCatalogFile, - `export const existingCatalogSurface = true; +const createFixture = (): Effect.Effect => + Effect.gen(function* scenario5() { + const root = yield* Effect.promise(() => mkdtemp(path.join(tmpdir(), 'ontos-scaffolding-'))); + yield* write(root, rootPackageFile, json({ name: 'fixture', private: true })); + yield* write( + root, + coreRuntimeIndexFile, + `export const existingCoreSurface = true;\n\n// \n// \n\n// \n// \n`, + ); + yield* write( + root, + coreActionCatalogFile, + `export const existingCatalogSurface = true; // // @@ -683,88 +717,72 @@ export const coreActionCatalog = [ // ]; `, - ); - await writeFixtureFile(root, shellSentinelFile, 'export const shell = true;\n'); - await writeFixtureFile( - root, - shellVerticalClientsFile, - `export const ultramodernVerticalClients = [ + ); + yield* write(root, shellSentinelFile, 'export const shell = true;\n'); + yield* write( + root, + shellVerticalClientsFile, + `export const ultramodernVerticalClients = [ // @ontos-codegen-start shell-page-clients // @ontos-codegen-end shell-page-clients ] as const; `, - ); - await createVertical(root, inventoryVertical); - await createVertical(root, billingVertical); - await createVertical(root, hrVertical); - await createVertical(root, contactsVertical); - await writeFixtureFile( - root, - topologyFile, - json({ - schemaVersion: 1, - verticals: [inventoryVertical, billingVertical, hrVertical, contactsVertical].map( - (vertical) => ({ - domain: vertical.namespace, - id: vertical.appId, - kind: 'vertical', - moduleFederation: { - name: vertical.mfBoundaryId, - role: 'remote', - }, - package: `@app/${vertical.slug}`, - path: `verticals/${vertical.slug}`, + ); + yield* createVertical(root, inventoryVertical); + yield* createVertical(root, billingVertical); + yield* createVertical(root, hrVertical); + yield* createVertical(root, contactsVertical); + yield* write( + root, + topologyFile, + json({ + schemaVersion: 1, + verticals: [inventoryVertical, billingVertical, hrVertical, contactsVertical].map( + (vertical) => ({ + domain: vertical.namespace, + id: vertical.appId, + kind: 'vertical', + moduleFederation: { + name: vertical.mfBoundaryId, + role: 'remote', + }, + package: `@app/${vertical.slug}`, + path: `verticals/${vertical.slug}`, + }), + ), + }), + ); + yield* Effect.all( + [inventoryVertical, billingVertical, hrVertical, contactsVertical].map( + Effect.fn(function* scenario6(vertical) { + return yield* runScaffoldEffect( + 'module-contract', + [scaffoldFlag.vertical, vertical.slug, '--module', vertical.moduleId], + { + workspaceRoot: root, + }, + ).pipe(Effect.provide(NodeServices.layer)); }), ), - }), - ); - await Promise.all( - [inventoryVertical, billingVertical, hrVertical, contactsVertical].map( - async (vertical) => - await runScaffold( - 'module-contract', - [scaffoldFlag.vertical, vertical.slug, '--module', vertical.moduleId], - { - workspaceRoot: root, - }, - ), - ), - ); - return { root }; -}; - -const withFixture = async (run: (fixture: Fixture) => Promise): Promise => { - const fixture = await createFixture(); - try { - await run(fixture); - } finally { - await rm(fixture.root, { force: true, recursive: true }); - } -}; + { concurrency: 'unbounded' }, + ); + return { root }; + }); -const snapshotTree = async (root: string): Promise>> => { - const snapshot: Record = {}; - const visit = async (directory: string): Promise => { - const entries = await readdir(directory, { withFileTypes: true }); - await Promise.all( - entries - .toSorted((left, right) => left.name.localeCompare(right.name)) - .map(async (entry) => { - const entryPath = path.join(directory, entry.name); - if (entry.isDirectory()) { - await visit(entryPath); - } else if (entry.isFile()) { - snapshot[path.relative(root, entryPath)] = await readFile(entryPath, 'utf-8'); - } - }), +const withFixture = ( + run: (fixture: Fixture) => Effect.Effect, +): Effect.Effect => + Effect.gen(function* scenario7() { + const fixture = yield* createFixture(); + yield* Effect.scoped(run(fixture)).pipe( + Effect.ensuring(Effect.promise(() => rm(fixture.root, { force: true, recursive: true }))), ); - }; - await visit(root); - return snapshot; -}; + }); -const readFixtureFile = async (root: string, relativePath: string): Promise => - await readFile(path.join(root, relativePath), 'utf-8'); +const readFixtureFile = (root: string, relativePath: string): Effect.Effect => + Effect.gen(function* scenario11() { + return yield* Effect.promise(() => readFile(path.join(root, relativePath), 'utf-8')); + }); const contextPermissionCommands = new Set([ scaffoldCommand.microverticalPage, @@ -774,13 +792,13 @@ const contextPermissionCommands = new Set([ scaffoldCommand.searchProvider, ]); -const run = async ( +const run = Effect.fn(function* scenario12( fixture: Fixture, command: ScaffoldCommand, scaffoldArguments: readonly string[], routeRefresh?: (appId: string) => void, -) => - await runScaffold( +) { + return yield* runScaffoldEffect( command, (() => { let flags = [...scaffoldArguments]; @@ -815,34 +833,36 @@ const run = async ( return flags; })(), { - routeRefresh: ({ appId }) => routeRefresh?.(appId), + routeRefresh: ({ appId }) => Effect.sync(() => routeRefresh?.(appId)), workspaceRoot: fixture.root, }, - ); + ).pipe(Effect.provide(NodeServices.layer)); +}); -/** - * A refused scaffold must leave the workspace byte-identical: the generator either completes or - * writes nothing at all, so every guard proves its own message against an unchanged fixture tree. - */ -const assertScaffoldRefused = async ( +/** Refusal must preserve the fixture byte-for-byte. */ +const assertScaffoldRefused = Effect.fn(function* assertScaffoldRefused( fixture: Fixture, command: ScaffoldCommand, commandArguments: readonly string[], expected: RegExp, -): Promise => { - const before = await snapshotTree(fixture.root); - await assert.rejects(run(fixture, command, commandArguments), expected); - assert.deepEqual(await snapshotTree(fixture.root), before); -}; +) { + const before = yield* snapshotTree(fixture.root); + yield* expectFailure(run(fixture, command, commandArguments), (error) => + expect(String(error)).toMatch(expected), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); +}); -const addInventoryItemResourceType = async (fixture: Fixture): Promise => { - const manifestPath = path.join(fixture.root, inventoryManifestFile); - const manifest = await readFile(manifestPath, 'utf-8'); - await writeFile( - manifestPath, - manifest.replace( - ' resourceTypes: [],', - ` resourceTypes: [ +const addInventoryItemResourceType = (fixture: Fixture): Effect.Effect => + Effect.gen(function* scenario13() { + const manifestPath = path.join(fixture.root, inventoryManifestFile); + const manifest = yield* Effect.promise(() => readFile(manifestPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + manifestPath, + manifest.replace( + ' resourceTypes: [],', + ` resourceTypes: [ { capabilities: { graphVisible: false, @@ -857,266 +877,291 @@ const addInventoryItemResourceType = async (fixture: Fixture): Promise => owningModuleId: 'inventory.stock', }, ],`, - ), - 'utf-8', - ); -}; - -test('documents every command and treats --help as a write-free operation', async () => { - await Promise.all( - ( - [ - 'action', - scaffoldCommand.actionService, - scaffoldCommand.externalHttpAdapter, - scaffoldCommand.microverticalActionBoundary, - scaffoldCommand.microverticalPage, - 'module-contract', - scaffoldCommand.moduleApi, - scaffoldCommand.outboxMessage, - scaffoldCommand.outboxWorker, - 'policy', - scaffoldCommand.publicComponent, - 'report', - scaffoldCommand.searchProviderAccess, - scaffoldCommand.searchProvider, - ] as const - ).map(async (command) => { - const result = await runScaffold(command, ['--', '--help'], { - workspaceRoot: path.join(tmpdir(), 'does-not-need-to-exist'), - }); - assert.deepEqual(result, { help: getHelpText(command), kind: 'help' }); - assert.match(result.help, new RegExp(`scaffold:${command}`, 'u')); - }), - ); - assert.match(getHelpText('action'), /--vertical /u); - assert.match(getHelpText('action'), /--scope core --module /u); - assert.match(getHelpText(scaffoldCommand.microverticalPage), /--url /u); - assert.match(getHelpText(scaffoldCommand.microverticalPage), /defaults to \/\//u); - assert.match(getHelpText(scaffoldCommand.microverticalPage), /:parameter/u); - assert.match(getHelpText(scaffoldCommand.microverticalPage), /\/contacts\/customers\/:id\/edit/u); - assert.match( - getHelpText(scaffoldCommand.externalHttpAdapter), - /scaffold:external-http-adapter -- --vertical --provider --operation /u, - ); - assert.match( - getHelpText(scaffoldCommand.externalHttpAdapter), - /--vertical contacts --provider ares --operation subject/u, - ); - assert.match( - getHelpText(scaffoldCommand.searchProviderAccess), - /--tenant-permission read_party_identity/u, - ); -}); - -test('search-provider access updates only generated access metadata and fails atomically on drift', async () => { - await withFixture(async (fixture) => { - await mkdir(path.join(fixture.root, 'verticals/retired/node_modules'), { recursive: true }); - await addInventoryItemResourceType(fixture); - await run(fixture, scaffoldCommand.searchProvider, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.inventoryItems, - scaffoldFlag.resource, - 'item', - ]); - await run(fixture, scaffoldCommand.searchProviderAccess, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.inventoryItems, - scaffoldFlag.legalEntityScope, - 'optional', - scaffoldFlag.accessFiltering, - 'tenant_scope', - scaffoldFlag.requestFilters, - 'includeArchived', - '--tenant-permission', - 'read_party_identity', - ]); - - const [manifest, provider, contract] = await Promise.all([ - readFixtureFile(fixture.root, inventoryManifestFile), - readFixtureFile(fixture.root, inventorySearchProviderFile), - readFixtureFile(fixture.root, inventorySearchContractFile), - ]); - assert.match( - manifest, - /accessFiltering: 'tenant_scope'.*requestFilters: \['includeArchived'\].*tenantPermission: 'read_party_identity'/u, + ), + 'utf-8', + ), ); - assert.match(provider, /legalEntityScope: 'optional'/u); - assert.match(provider, /permissionTarget: 'tenant'/u); - assert.match(provider, /kind: 'tenant', permission: 'read_party_identity'/u); - assert.match(contract, /includeArchived: Schema\.optionalKey\(Schema\.Boolean\)/u); - - const beforeProviderRerun = await snapshotTree(fixture.root); - await run(fixture, scaffoldCommand.searchProvider, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.inventoryItems, - scaffoldFlag.resource, - 'item', - ]); - assert.deepEqual(await snapshotTree(fixture.root), beforeProviderRerun); + }); - const providerPath = path.join(fixture.root, inventorySearchProviderFile); - await writeFile( - providerPath, - `${provider}\n// Owner-customized searchable semantics remain untouched.\n`, +it.live( + 'documents every command and treats --help as a write-free operation', + Effect.fn(function* scenario14() { + yield* Effect.all( + ( + [ + 'action', + scaffoldCommand.actionService, + scaffoldCommand.externalHttpAdapter, + scaffoldCommand.microverticalActionBoundary, + scaffoldCommand.microverticalPage, + 'module-contract', + scaffoldCommand.moduleApi, + scaffoldCommand.outboxMessage, + scaffoldCommand.outboxWorker, + 'policy', + scaffoldCommand.publicComponent, + 'report', + scaffoldCommand.searchProviderAccess, + scaffoldCommand.searchProvider, + ] as const + ).map( + Effect.fn(function* scenario15(command) { + const result = yield* runScaffoldEffect(command, ['--', '--help'], { + workspaceRoot: path.join(tmpdir(), 'does-not-need-to-exist'), + }).pipe(Effect.provide(NodeServices.layer)); + expect(result).toEqual({ help: getHelpText(command), kind: 'help' }); + if (result.kind !== 'help') { + throw new Error('Expected help result'); + } + expect(result.help).toMatch(new RegExp(`scaffold:${command}`, 'u')); + }), + ), + { concurrency: 'unbounded' }, ); - const beforeIdempotentUpdate = await snapshotTree(fixture.root); - await run(fixture, scaffoldCommand.searchProviderAccess, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.inventoryItems, - scaffoldFlag.legalEntityScope, - 'optional', - scaffoldFlag.accessFiltering, - 'tenant_scope', - scaffoldFlag.requestFilters, - 'includeArchived', - '--tenant-permission', - 'read_party_identity', - ]); - assert.deepEqual(await snapshotTree(fixture.root), beforeIdempotentUpdate); - await writeFile( - providerPath, - provider.replace('// @generated by OntOS Codesmith ', '// custom '), + expect(getHelpText('action')).toMatch(/--vertical /u); + expect(getHelpText('action')).toMatch(/--scope core --module /u); + expect(getHelpText(scaffoldCommand.microverticalPage)).toMatch(/--url /u); + expect(getHelpText(scaffoldCommand.microverticalPage)).toMatch( + /defaults to \/\//u, ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.searchProviderAccess, - [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.inventoryItems, - scaffoldFlag.legalEntityScope, - 'required', - scaffoldFlag.accessFiltering, - 'resource_permission', - scaffoldFlag.requestFilters, - 'includeArchived,role', - ], - /Codesmith-owned provider/u, + expect(getHelpText(scaffoldCommand.microverticalPage)).toMatch(/:parameter/u); + expect(getHelpText(scaffoldCommand.microverticalPage)).toMatch( + /\/contacts\/customers\/:id\/edit/u, ); - }); -}); + expect(getHelpText(scaffoldCommand.externalHttpAdapter)).toMatch( + /scaffold:external-http-adapter -- --vertical --provider --operation /u, + ); + expect(getHelpText(scaffoldCommand.externalHttpAdapter)).toMatch( + /--vertical contacts --provider ares --operation subject/u, + ); + expect(getHelpText(scaffoldCommand.searchProviderAccess)).toMatch( + /--tenant-permission read_party_identity/u, + ); + }), +); -test('generated API owner slots sort property keys before suffix variants', async () => { - await withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.moduleApi, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - 'party-match-decision', - ]); - await run(fixture, scaffoldCommand.moduleApi, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - 'party-match', - ]); - const sources = await Promise.all( - [inventoryManifestFile, inventoryRegistrationFile].map( - async (owner) => await readFixtureFile(fixture.root, owner), - ), - ); - for (const source of sources) { - assert.ok(source.indexOf("'party-match':") < source.indexOf("'party-match-decision':")); - } - }); -}); +it.live( + 'search-provider access updates only generated access metadata and fails atomically on drift', + Effect.fn(function* mergedScenario18() { + yield* withFixture( + Effect.fn(function* mergedScenario17(fixture) { + yield* Effect.promise(() => + mkdir(path.join(fixture.root, 'verticals/retired/node_modules'), { recursive: true }), + ); + yield* addInventoryItemResourceType(fixture); + yield* run(fixture, scaffoldCommand.searchProvider, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.inventoryItems, + scaffoldFlag.resource, + 'item', + ]); + yield* run(fixture, scaffoldCommand.searchProviderAccess, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.inventoryItems, + scaffoldFlag.legalEntityScope, + 'optional', + scaffoldFlag.accessFiltering, + 'tenant_scope', + scaffoldFlag.requestFilters, + 'includeArchived', + '--tenant-permission', + 'read_party_identity', + ]); + + const [manifest, provider, contract] = yield* Effect.all( + [ + readFixtureFile(fixture.root, inventoryManifestFile), + readFixtureFile(fixture.root, inventorySearchProviderFile), + readFixtureFile(fixture.root, inventorySearchContractFile), + ], + { concurrency: 'unbounded' }, + ); + expect(manifest).toMatch( + /accessFiltering: 'tenant_scope'.*requestFilters: \['includeArchived'\].*tenantPermission: 'read_party_identity'/u, + ); + expect(provider).toMatch(/legalEntityScope: 'optional'/u); + expect(provider).toMatch(/permissionTarget: 'tenant'/u); + expect(provider).toMatch(/kind: 'tenant', permission: 'read_party_identity'/u); + expect(contract).toMatch(/includeArchived: Schema\.optionalKey\(Schema\.Boolean\)/u); -test('generated read clients fetch mounted owner URLs and support separately deployed hosts', async () => { - await withFixture(async (fixture) => { - await addInventoryItemResourceType(fixture); - await run(fixture, scaffoldCommand.moduleApi, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.resourceDetail, - ]); - await run(fixture, scaffoldCommand.searchProvider, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.inventoryItems, - scaffoldFlag.resource, - 'item', - ]); - await run(fixture, 'report', [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.stockLevels, - scaffoldFlag.resource, - 'item', - ]); - await mkdir(path.join(fixture.root, 'node_modules/@app'), { recursive: true }); - await mkdir(path.join(fixture.root, 'node_modules/@modern-js'), { recursive: true }); - await symlink( - path.join(appRoot, sharedContractsPackagePath), - path.join(fixture.root, sharedContractsNodeModulePath), - 'dir', - ); - await symlink( - path.join(appRoot, effectNodeModulePath), - path.join(fixture.root, effectNodeModulePath), - 'dir', + const beforeProviderRerun = yield* snapshotTree(fixture.root); + yield* run(fixture, scaffoldCommand.searchProvider, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.inventoryItems, + scaffoldFlag.resource, + 'item', + ]); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeProviderRerun); + + const providerPath = path.join(fixture.root, inventorySearchProviderFile); + yield* Effect.promise(() => + writeFile( + providerPath, + `${provider}\n// Owner-customized searchable semantics remain untouched.\n`, + ), + ); + const beforeIdempotentUpdate = yield* snapshotTree(fixture.root); + yield* run(fixture, scaffoldCommand.searchProviderAccess, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.inventoryItems, + scaffoldFlag.legalEntityScope, + 'optional', + scaffoldFlag.accessFiltering, + 'tenant_scope', + scaffoldFlag.requestFilters, + 'includeArchived', + '--tenant-permission', + 'read_party_identity', + ]); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeIdempotentUpdate); + yield* Effect.promise(() => + writeFile( + providerPath, + provider.replace('// @generated by OntOS Codesmith ', '// custom '), + ), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.searchProviderAccess, + [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.inventoryItems, + scaffoldFlag.legalEntityScope, + 'required', + scaffoldFlag.accessFiltering, + 'resource_permission', + scaffoldFlag.requestFilters, + 'includeArchived,role', + ], + /Codesmith-owned provider/u, + ); + }), ); - await symlink( - path.join(appRoot, pluginBffNodeModulePath), - path.join(fixture.root, pluginBffNodeModulePath), - 'dir', + }), +); + +it.live( + 'generated API owner slots sort property keys before suffix variants', + Effect.fn(function* scenario18() { + yield* withFixture( + Effect.fn(function* scenario19(fixture) { + yield* run(fixture, scaffoldCommand.moduleApi, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + 'party-match-decision', + ]); + yield* run(fixture, scaffoldCommand.moduleApi, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + 'party-match', + ]); + const sources = yield* Effect.all( + [inventoryManifestFile, inventoryRegistrationFile].map( + Effect.fn(function* scenario20(owner) { + return yield* readFixtureFile(fixture.root, owner); + }), + ), + { concurrency: 'unbounded' }, + ); + for (const source of sources) { + expect(source.indexOf("'party-match':") < source.indexOf("'party-match-decision':")).toBe( + true, + ); + } + }), ); - const result = spawnSync( - process.execPath, - [ - '--input-type=module', - '--eval', - ` - import { Effect, Match, Result } from 'effect'; - import { FetchHttpClient } from 'effect/unstable/http'; - import { runEffectTestPromise } from '${pathToFileURL(path.join(appRoot, 'packages/core-runtime/src/testing/effect-runtime.ts')).href}'; - import { executeResourceDetail, executeResourceDetailWithAuthorization } from './verticals/inventory-stock/src/api/resource-detail-client.ts'; - import { loadInventoryItemsClient, loadInventoryItemsClientWithAuthorization } from './verticals/inventory-stock/src/api/inventory-items-search-client.ts'; - import { loadStockLevelsClient, loadStockLevelsClientWithAuthorization } from './verticals/inventory-stock/src/api/stock-levels-report-client.ts'; - const calls = []; - let gatewayAttempts = 0; - const cases = [ + }), +); + +it.live( + 'generated read clients fetch mounted owner URLs and support separately deployed hosts', + Effect.fn(function* mergedScenario21() { + yield* withFixture( + Effect.fn(function* mergedScenario20(fixture) { + yield* addInventoryItemResourceType(fixture); + yield* run(fixture, scaffoldCommand.moduleApi, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.resourceDetail, + ]); + yield* run(fixture, scaffoldCommand.searchProvider, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.inventoryItems, + scaffoldFlag.resource, + 'item', + ]); + yield* run(fixture, 'report', [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.stockLevels, + scaffoldFlag.resource, + 'item', + ]); + yield* linkFixtureDependencies(fixture.root, appRoot, { + '@app/shared-contracts': sharedContractsPackagePath, + '@modern-js/plugin-bff': pluginBffNodeModulePath, + effect: effectNodeModulePath, + }); + const result = spawnSync( + process.execPath, + [ + '--input-type=module', + '--eval', + `import { Effect, Match, Result } from 'effect'; +import { FetchHttpClient } from 'effect/unstable/http'; +import { executeResourceDetail, executeResourceDetailWithAuthorization } from './verticals/inventory-stock/src/api/resource-detail-client.ts'; +import { loadInventoryItemsClient, loadInventoryItemsClientWithAuthorization } from './verticals/inventory-stock/src/api/inventory-items-search-client.ts'; +import { loadStockLevelsClient, loadStockLevelsClientWithAuthorization } from './verticals/inventory-stock/src/api/stock-levels-report-client.ts'; +import { NodeRuntime } from '${pathToFileURL(require.resolve('@effect/platform-node')).href}'; +Effect.gen(function* generatedHttpProof() { +const calls = []; +let gatewayAttempts = 0; +const cases = [ [executeResourceDetailWithAuthorization, {}, { ok: true }, executeResourceDetail], [loadInventoryItemsClientWithAuthorization, { query: 'chair' }, [], loadInventoryItemsClient], [loadStockLevelsClientWithAuthorization, { parameters: {} }, { rows: [] }, loadStockLevelsClient], ]; - for (const [invoke, payload, response] of cases) { - const fetch = async (url, init) => { +for (const [invoke, payload, response] of cases) { + const fetch = (url, init) => { calls.push({ url: String(url), method: init.method, authorization: new Headers(init.headers).get('authorization'), correlationId: new Headers(init.headers).get('x-correlation-id') }); - return Response.json(response); + return Promise.resolve(Response.json(response)); }; - await runEffectTestPromise(invoke(payload, 'Bearer proof', 'correlation-proof', { baseUrl: new URL('https://inventory.example.test/custom/inventory-stock-api') }).pipe(Effect.provideService(FetchHttpClient.Fetch, fetch))); + (yield* invoke(payload, 'Bearer proof', 'correlation-proof', { baseUrl: new URL('https://inventory.example.test/custom/inventory-stock-api') }).pipe(Effect.provideService(FetchHttpClient.Fetch, fetch))); } - globalThis.location = { origin: 'https://shell.example.test', pathname: '/cs/inventory' }; - for (const [invoke, payload, response] of cases) { - await runEffectTestPromise(invoke(payload, 'Bearer proof', 'correlation-proof').pipe(Effect.provideService(FetchHttpClient.Fetch, async (url, init) => { +globalThis.location = { origin: 'https://shell.example.test', pathname: '/cs/inventory' }; +for (const [invoke, payload, response] of cases) { + (yield* invoke(payload, 'Bearer proof', 'correlation-proof').pipe(Effect.provideService(FetchHttpClient.Fetch, (url, init) => { calls.push({ url: String(url), method: init.method, authorization: new Headers(init.headers).get('authorization'), correlationId: new Headers(init.headers).get('x-correlation-id') }); - return Response.json(response); + return Promise.resolve(Response.json(response)); }))); } - for (const [, payload, response, invoke] of cases) { - await runEffectTestPromise(invoke(payload, 'correlation-proof', { baseUrl: 'https://inventory.example.test/custom/inventory-stock-api' }).pipe(Effect.provideService(FetchHttpClient.Fetch, async (url, init) => { +for (const [, payload, response, invoke] of cases) { + (yield* invoke(payload, 'correlation-proof', { baseUrl: 'https://inventory.example.test/custom/inventory-stock-api' }).pipe(Effect.provideService(FetchHttpClient.Fetch, (url, init) => { if (String(url) === 'https://shell.example.test/shell-super-app-api/auth/gateway-context') { gatewayAttempts += 1; - return Response.json({ expiresAt: 2_000_000_000, token: 'proof' }); + return Promise.resolve(Response.json({ expiresAt: 2_000_000_000, token: 'proof' })); } calls.push({ url: String(url), method: init.method, authorization: new Headers(init.headers).get('authorization'), correlationId: new Headers(init.headers).get('x-correlation-id') }); - return Response.json(response); + return Promise.resolve(Response.json(response)); }))); } - const generatedProblem = { +const generatedProblem = { _tag: 'ResourceDetailUnavailableProblem', detail: 'Temporarily unavailable.', retryable: true, @@ -1124,41 +1169,37 @@ test('generated read clients fetch mounted owner URLs and support separately dep title: 'Unavailable', type: 'urn:ontos:test:generated-problem', }; - const generatedFailure = await runEffectTestPromise( - executeResourceDetailWithAuthorization( +const generatedFailure = (yield* executeResourceDetailWithAuthorization( {}, 'Bearer proof', 'correlation-proof', { baseUrl: 'https://inventory.example.test/custom/inventory-stock-api' }, ).pipe( Effect.result, - Effect.provideService(FetchHttpClient.Fetch, async () => - Response.json(generatedProblem, { + Effect.provideService(FetchHttpClient.Fetch, () => Promise.resolve(Response.json(generatedProblem, { headers: { 'content-type': 'application/problem+json' }, status: 503, - }), + })), ), - ), - ); - if (!Result.isFailure(generatedFailure)) throw new Error('Expected generated client failure'); - const preservesProblemDetails = Match.value(generatedFailure.failure).pipe( + )); +if (!Result.isFailure(generatedFailure)) throw new Error('Expected generated client failure'); +const preservesProblemDetails = Match.value(generatedFailure.failure).pipe( Match.tag('ResourceDetailUnavailableProblem', ({ status, retryable }) => status === generatedProblem.status && retryable === true, ), Match.orElse(() => false), ); - if (!preservesProblemDetails) { +if (!preservesProblemDetails) { throw new Error('Generated client did not preserve the concrete Problem Details error'); } - let endpointRequestsAfterGatewayFailure = 0; - const gatewayFailure = await runEffectTestPromise( - executeResourceDetail({}, 'failed-gateway-correlation', { +let endpointRequestsAfterGatewayFailure = 0; +const gatewayFailure = (yield* executeResourceDetail({}, 'failed-gateway-correlation', { baseUrl: 'https://inventory.example.test/custom/inventory-stock-api', }).pipe( - Effect.provideService(FetchHttpClient.Fetch, async (url) => { + Effect.provideService(FetchHttpClient.Fetch, (url) => { if (String(url) === 'https://shell.example.test/shell-super-app-api/auth/gateway-context') { gatewayAttempts += 1; - return Response.json( + return Promise.resolve(Response.json( { _tag: 'GatewayUnavailableProblem', detail: 'Gateway unavailable for generated-client proof.', @@ -1168,147 +1209,172 @@ test('generated read clients fetch mounted owner URLs and support separately dep type: 'https://ontos.dev/problems/gateway-unavailable', }, { status: 503 }, - ); + )); } endpointRequestsAfterGatewayFailure += 1; - return Response.json({ ok: true }); + return Promise.resolve(Response.json({ ok: true })); }), Effect.flip, - ), - ); - const gatewayUnavailable = Match.value(gatewayFailure).pipe( + )); +const gatewayUnavailable = Match.value(gatewayFailure).pipe( Match.tag('GatewayUnavailableProblem', () => true), Match.orElse(() => false), ); - console.log(JSON.stringify({ calls, endpointRequestsAfterGatewayFailure, gatewayAttempts, gatewayUnavailable })); - `, - ], - { cwd: fixture.root, encoding: 'utf-8' }, - ); - assert.equal(result.status, 0, result.stderr || result.error?.message); - const proof = Schema.decodeUnknownSync( - Schema.fromJsonString( - Schema.Struct({ - calls: Schema.Array(Schema.Record(Schema.String, Schema.String)), - endpointRequestsAfterGatewayFailure: Schema.Number, - gatewayAttempts: Schema.Number, - gatewayUnavailable: Schema.Boolean, - }), - ), - )(result.stdout); - assert.deepEqual( - proof.calls, - [ - 'https://inventory.example.test/custom/inventory-stock-api/reads/resource-detail', - 'https://inventory.example.test/custom/inventory-stock-api/inventory.stock/search/inventory-items', - 'https://inventory.example.test/custom/inventory-stock-api/inventory.stock/reports/stock-levels', - 'https://shell.example.test/inventory-stock-api/reads/resource-detail', - 'https://shell.example.test/inventory-stock-api/inventory.stock/search/inventory-items', - 'https://shell.example.test/inventory-stock-api/inventory.stock/reports/stock-levels', - 'https://inventory.example.test/custom/inventory-stock-api/reads/resource-detail', - 'https://inventory.example.test/custom/inventory-stock-api/inventory.stock/search/inventory-items', - 'https://inventory.example.test/custom/inventory-stock-api/inventory.stock/reports/stock-levels', - ].map((url) => ({ - authorization: 'Bearer proof', - correlationId: 'correlation-proof', - method: 'POST', - url, - })), +console.log(JSON.stringify({ calls, endpointRequestsAfterGatewayFailure, gatewayAttempts, gatewayUnavailable })); +}).pipe(Effect.scoped, NodeRuntime.runMain); +`, + ], + { cwd: fixture.root, encoding: 'utf-8' }, + ); + expect(result.status, result.stderr || result.error?.message).toBe(0); + const proof = yield* Schema.decodeUnknownEffect( + Schema.fromJsonString( + Schema.Struct({ + calls: Schema.Array(Schema.Record(Schema.String, Schema.String)), + endpointRequestsAfterGatewayFailure: Schema.Number, + gatewayAttempts: Schema.Number, + gatewayUnavailable: Schema.Boolean, + }), + ), + )(result.stdout); + expect(proof.calls).toEqual( + [ + 'https://inventory.example.test/custom/inventory-stock-api/reads/resource-detail', + 'https://inventory.example.test/custom/inventory-stock-api/inventory.stock/search/inventory-items', + 'https://inventory.example.test/custom/inventory-stock-api/inventory.stock/reports/stock-levels', + 'https://shell.example.test/inventory-stock-api/reads/resource-detail', + 'https://shell.example.test/inventory-stock-api/inventory.stock/search/inventory-items', + 'https://shell.example.test/inventory-stock-api/inventory.stock/reports/stock-levels', + 'https://inventory.example.test/custom/inventory-stock-api/reads/resource-detail', + 'https://inventory.example.test/custom/inventory-stock-api/inventory.stock/search/inventory-items', + 'https://inventory.example.test/custom/inventory-stock-api/inventory.stock/reports/stock-levels', + ].map((url) => ({ + authorization: 'Bearer proof', + correlationId: 'correlation-proof', + method: 'POST', + url, + })), + ); + expect(proof.gatewayAttempts).toBe(4); + expect(proof.gatewayUnavailable).toBe(true); + expect(proof.endpointRequestsAfterGatewayFailure).toBe(0); + }), ); - assert.equal(proof.gatewayAttempts, 4); - assert.equal(proof.gatewayUnavailable, true); - assert.equal(proof.endpointRequestsAfterGatewayFailure, 0); - }); -}); + }), +); const compactGovernedSource = (source: string): string => source.replaceAll(/\s+/gu, '').replaceAll(/,(?=[)}\]])/gu, ''); const inventorySharedApiFile = 'verticals/inventory-stock/shared/api.ts'; -test('all live Party read and search transports match actual scaffold output', async () => { - await withFixture(async (fixture) => { - await addInventoryItemResourceType(fixture); - await run(fixture, scaffoldCommand.moduleApi, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.resourceDetail, - ]); - await run(fixture, scaffoldCommand.searchProvider, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.inventoryItems, - scaffoldFlag.resource, - 'item', - ]); - const owner = path.join(appRoot, 'verticals/party-registry'); - const ownerFiles = await readdir(path.join(owner, 'api')); - const serverNames = ownerFiles.filter((name) => /-(?:read|search)-server\.ts$/u.test(name)); - assert.equal(serverNames.length, 18); - await Promise.all( - serverNames.map(async (serverName) => { - const search = serverName.endsWith('-search-server.ts'); - const suffix = search ? 'search' : 'read'; - const name = serverName.slice(0, -`-${suffix}-server.ts`.length); - const camel = name.replaceAll(/-(?[a-z])/gu, (_, letter: string) => - letter.toUpperCase(), - ); - const pascal = `${camel.charAt(0).toUpperCase()}${camel.slice(1)}`; - const fixtureNameValue = search ? 'inventory-items' : 'resource-detail'; - const fixtureCamel = search ? 'inventoryItems' : 'resourceDetail'; - const fixturePascal = search ? 'InventoryItems' : 'ResourceDetail'; - const clientName = `${name}${search ? '-search' : ''}-client.ts`; - const normalize = (source: string): string => - compactGovernedSource( - source - .replaceAll(`/${name}`, `/${fixtureNameValue}`) - .replaceAll(pascal, fixturePascal) - .replaceAll(camel, fixtureCamel) - .replaceAll(`${fixturePascal}SearchClientOptions`, `${fixturePascal}ClientOptions`) - .replaceAll('partyRegistryApi', 'fixtureApi') - .replaceAll('/party-registry-api', '/inventory-stock-api'), - ); - const expectedServer = await readFixtureFile( - fixture.root, - `verticals/inventory-stock/api/${fixtureNameValue}-${suffix}-server.ts`, - ); - const expectedClient = await readFixtureFile( - fixture.root, - `verticals/inventory-stock/src/api/${fixtureNameValue}${search ? '-search' : ''}-client.ts`, - ); - assert.equal( - normalize(await readFile(path.join(owner, 'api', serverName), 'utf-8')), - compactGovernedSource(expectedServer), - serverName, - ); - assert.equal( - normalize(await readFile(path.join(owner, 'src/api', clientName), 'utf-8')), - compactGovernedSource(expectedClient), - clientName, +it.live( + 'all live Party read and search transports match actual scaffold output', + Effect.fn(function* livePartyTransports() { + yield* withFixture( + Effect.fn(function* generatedTransportFixture(fixture) { + yield* addInventoryItemResourceType(fixture); + yield* run(fixture, scaffoldCommand.moduleApi, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.resourceDetail, + ]); + yield* run(fixture, scaffoldCommand.searchProvider, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.inventoryItems, + scaffoldFlag.resource, + 'item', + ]); + const owner = path.join(appRoot, 'verticals/party-registry'); + const ownerFiles = yield* Effect.promise(() => readdir(path.join(owner, 'api'))); + const serverNames = ownerFiles.filter((name) => /-(?:read|search)-server\.ts$/u.test(name)); + expect(serverNames.length).toBe(18); + yield* Effect.all( + serverNames.map( + Effect.fn(function* compareLiveTransport(serverName) { + const search = serverName.endsWith('-search-server.ts'); + const suffix = search ? 'search' : 'read'; + const name = serverName.slice(0, -`-${suffix}-server.ts`.length); + const camel = name.replaceAll(/-(?[a-z])/gu, (_, letter: string) => + letter.toUpperCase(), + ); + const pascal = `${camel.charAt(0).toUpperCase()}${camel.slice(1)}`; + const fixtureNameValue = search ? 'inventory-items' : 'resource-detail'; + const fixtureCamel = search ? 'inventoryItems' : 'resourceDetail'; + const fixturePascal = search ? 'InventoryItems' : 'ResourceDetail'; + const clientName = `${name}${search ? '-search' : ''}-client.ts`; + const normalize = (source: string): string => + compactGovernedSource( + source + .replaceAll(`/${name}`, `/${fixtureNameValue}`) + .replaceAll(pascal, fixturePascal) + .replaceAll(camel, fixtureCamel) + .replaceAll( + `${fixturePascal}SearchClientOptions`, + `${fixturePascal}ClientOptions`, + ) + .replaceAll('partyRegistryApi', 'fixtureApi') + .replaceAll('/party-registry-api', '/inventory-stock-api'), + ); + const expectedServer = yield* readFixtureFile( + fixture.root, + `verticals/inventory-stock/api/${fixtureNameValue}-${suffix}-server.ts`, + ); + const expectedClient = yield* readFixtureFile( + fixture.root, + `verticals/inventory-stock/src/api/${fixtureNameValue}${search ? '-search' : ''}-client.ts`, + ); + expect( + normalize( + yield* Effect.promise(() => + readFile(path.join(owner, 'api', serverName), 'utf-8'), + ), + ), + serverName, + ).toBe(compactGovernedSource(expectedServer)); + expect( + normalize( + yield* Effect.promise(() => + readFile(path.join(owner, 'src/api', clientName), 'utf-8'), + ), + ), + clientName, + ).toBe(compactGovernedSource(expectedClient)); + }), + ), + { concurrency: 'unbounded' }, ); + const sharedApi = yield* readFixtureFile(fixture.root, inventorySharedApiFile); + expect(sharedApi).not.toMatch(/governedHttpApi/u); }), ); - const sharedApi = await readFixtureFile(fixture.root, inventorySharedApiFile); - assert.doesNotMatch(sharedApi, /governedHttpApi/u); - }); -}); + }), +); -test('the migrated Party governed API slot accepts future generated additions', async () => { - const source = await readFile(path.join(appRoot, partyGovernedContractPath), 'utf-8'); - const next = insertSortedSlot( - source, - GOVERNED_HTTP_API_ADDITION_SLOT_START, - GOVERNED_HTTP_API_ADDITION_SLOT_END, - ['.addHttpApi(FutureReadApi)'], - (candidate) => candidate.startsWith('.addHttpApi(') && candidate.endsWith(')'), - ); - assert.match(next, /\.addHttpApi\(FutureReadApi\)/u); -}); +it.live( + 'the migrated Party governed API slot accepts future generated additions', + Effect.fn(function* mergedScenario22() { + const source = yield* Effect.promise(() => + readFile(path.join(appRoot, partyGovernedContractPath), 'utf-8'), + ); + const next = insertSortedSlot( + source, + GOVERNED_HTTP_API_ADDITION_SLOT_START, + GOVERNED_HTTP_API_ADDITION_SLOT_END, + ['.addHttpApi(FutureReadApi)'], + (candidate) => candidate.startsWith('.addHttpApi(') && candidate.endsWith(')'), + ); + expect(next).toMatch(/\.addHttpApi\(FutureReadApi\)/u); + }), +); const requiredGeneratedSlot = (source: string, start: string, end: string): string => { const slot = new RegExp(`${start}[\\s\\S]*?${end}`, 'u').exec(source)?.[0]; - assert.ok(slot !== undefined, `expected the generated slot between ${start} and ${end}`); + if (slot === undefined) { + expect.unreachable(`expected the generated slot between ${start} and ${end}`); + } return slot; }; @@ -1316,58 +1382,58 @@ const requiredGeneratedSlot = (source: string, start: string, end: string): stri * Moving a generated composition slot into a string literal leaves the real binding missing, so * the generator must refuse rather than accept the relocated copy as the composition point. */ -const assertRelocatedSlotRefused = async ( +const assertRelocatedSlotRefused = Effect.fn(function* assertRelocatedSlotRefused( fixture: Fixture, file: string, validSource: string, [slotStart, slotEnd]: readonly [string, string], -): Promise => { +) { const slot = requiredGeneratedSlot(validSource, slotStart, slotEnd); - await writeFile( - file, - `${validSource.replace(slot, '')}\nconst relocatedSlot = String.raw\`${slot}\`;\n`, - 'utf-8', + yield* Effect.promise(() => + writeFile( + file, + `${validSource.replace(slot, '')}\nconst relocatedSlot = String.raw\`${slot}\`;\n`, + 'utf-8', + ), ); - await assertScaffoldRefused( + yield* assertScaffoldRefused( fixture, scaffoldCommand.moduleApi, [scaffoldFlag.vertical, inventorySlug, '--name', fixtureName.resourceDetail], /composition slots are not bound/u, ); - await writeFile(file, validSource, 'utf-8'); -}; + yield* Effect.promise(() => writeFile(file, validSource, 'utf-8')); +}); const assertGovernedReadClients = (clients: readonly string[]): void => { for (const client of clients) { - assert.match(client, /from '@app\/shared-contracts\/client-runtime'/u); - assert.match(client, /makeGovernedEffectBffClient\(/u); - assert.match(client, /defaultApiPrefix: '\/inventory-stock-api'/u); - assert.match(client, /operationGateway\.invoke\(\(credential\) =>/u); - assert.match(client, /WithAuthorization/u); - assert.match( - client, + expect(client).toMatch(/from '@app\/shared-contracts\/client-runtime'/u); + expect(client).toMatch(/makeGovernedEffectBffClient\(/u); + expect(client).toMatch(/defaultApiPrefix: '\/inventory-stock-api'/u); + expect(client).toMatch(/operationGateway\.invoke\(\(credential\) =>/u); + expect(client).toMatch(/WithAuthorization/u); + expect(client).toMatch( /credential,\s+defaultApiPrefix: '\/inventory-stock-api',\s+requestCorrelation,/u, ); - assert.doesNotMatch( - client, + expect(client).not.toMatch( /makeEffectHttpApiClient|Context\.Reference|HttpClientRequest|HttpClient\.mapRequest/u, ); } }; const assertGovernedReadProviders = (providers: readonly string[]): void => { for (const provider of providers) { - assert.match(provider, /defineRead\(/u); - assert.match(provider, /legalEntityScope: 'required'/u); - assert.match(provider, /permissionTarget: 'module'/u); - assert.doesNotMatch(provider, /CoreDatabase|ScopedTransactionExecutor|from 'pg'/u); + expect(provider).toMatch(/defineRead\(/u); + expect(provider).toMatch(/legalEntityScope: 'required'/u); + expect(provider).toMatch(/permissionTarget: 'module'/u); + expect(provider).not.toMatch(/CoreDatabase|ScopedTransactionExecutor|from 'pg'/u); } }; const assertGovernedReadServers = (servers: readonly string[]): void => { for (const server of servers) { - assert.match(server, /makeGovernedReadHttpHandler\(\{/u); - assert.match(server, /authenticatePrincipal: authenticateOperationPrincipal/u); - assert.match(server, /registration: \w+Read/u); - assert.doesNotMatch(server, /ReadRuntime|Match\.tags|catchTags|bearerChallenge/u); - assert.doesNotMatch(server, /tenantId|legalEntityId|principalId|CoreDatabase|from 'pg'/u); + expect(server).toMatch(/makeGovernedReadHttpHandler\(\{/u); + expect(server).toMatch(/authenticatePrincipal: authenticateOperationPrincipal/u); + expect(server).toMatch(/registration: \w+Read/u); + expect(server).not.toMatch(/ReadRuntime|Match\.tags|catchTags|bearerChallenge/u); + expect(server).not.toMatch(/tenantId|legalEntityId|principalId|CoreDatabase|from 'pg'/u); } }; const assertComposedGovernedReads = (composedApi: string, composedHandlers: string): void => { @@ -1376,11 +1442,10 @@ const assertComposedGovernedReads = (composedApi: string, composedHandlers: stri ['ResourceDetailApi', 'resourceDetailReadApiLive'], ['StockLevelsReportApi', 'stockLevelsReadApiLive'], ] as const) { - assert.match(composedApi, new RegExp(`import \\{ ${contract} \\}`, 'u')); - assert.match(composedApi, new RegExp(`\\.addHttpApi\\(${contract}\\)`, 'u')); - assert.match(composedHandlers, new RegExp(`import \\{ ${layer} \\}`, 'u')); - assert.match( - composedHandlers, + expect(composedApi).toMatch(new RegExp(`import \\{ ${contract} \\}`, 'u')); + expect(composedApi).toMatch(new RegExp(`\\.addHttpApi\\(${contract}\\)`, 'u')); + expect(composedHandlers).toMatch(new RegExp(`import \\{ ${layer} \\}`, 'u')); + expect(composedHandlers).toMatch( new RegExp( `${layer}\\.pipe\\([\\s\\S]*?GovernedReadLayer\\.provide\\(governedReadRuntimeLive\\)`, 'u', @@ -1390,201 +1455,195 @@ const assertComposedGovernedReads = (composedApi: string, composedHandlers: stri }; const assertGovernedProblemDetailsContracts = (contracts: readonly string[]): void => { for (const contract of contracts) { - assert.match( - contract, + expect(contract).toMatch( /import \{\s*makeProblemDetailsSchema,\s*makeRetryableProblemDetailsSchema,?\s*\} from '@app\/shared-contracts\/problem-details';/u, ); - assert.match(contract, /makeProblemDetailsSchema\([^)]*,\s*409,?\s*\)/u); - assert.match(contract, /makeRetryableProblemDetailsSchema\([^)]*,\s*503,?\s*\)/u); - assert.doesNotMatch(contract, /application\/problem\+json|HttpApiSchema/u); + expect(contract).toMatch(/makeProblemDetailsSchema\([^)]*,\s*409,?\s*\)/u); + expect(contract).toMatch(/makeRetryableProblemDetailsSchema\([^)]*,\s*503,?\s*\)/u); + expect(contract).not.toMatch(/application\/problem\+json|HttpApiSchema/u); } }; -test('governed contribution generators patch owner contracts and lazy adapters atomically', async () => { - await withFixture(async (fixture) => { - const manifestPath = path.join(fixture.root, inventoryManifestFile); - await addInventoryItemResourceType(fixture); - - await run(fixture, scaffoldCommand.moduleApi, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.resourceDetail, - ]); - await run(fixture, scaffoldCommand.moduleApi, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - 'resource-history', - ]); - await run(fixture, scaffoldCommand.publicComponent, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - 'inventory-summary', - ]); - await run(fixture, scaffoldCommand.publicComponent, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - 'inventory-alerts', - ]); - await run(fixture, scaffoldCommand.searchProvider, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.inventoryItems, - scaffoldFlag.resource, - 'item', - ]); - await run(fixture, scaffoldCommand.searchProvider, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - 'inventory-suppliers', - scaffoldFlag.resource, - 'item', - ]); - await run(fixture, 'report', [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.stockLevels, - scaffoldFlag.resource, - 'item', - ]); - - const [nextManifest, registration, federation] = await Promise.all([ - readFile(manifestPath, 'utf-8'), - readFixtureFile(fixture.root, inventoryRegistrationFile), - readFixtureFile(fixture.root, inventoryFederationConfigFile), - ]); - assert.match(nextManifest, /inventory\.stock\.component\.inventory-summary/u); - assert.match(nextManifest, /inventory\.stock\.search\.inventory-items/u); - assert.match(nextManifest, /inventory\.stock\.report\.stock-levels/u); - assert.match(registration, /import\('\.\/src\/api\/resource-detail-client\.ts'\)/u); - assert.match(registration, /import\('\.\/src\/api\/inventory-items-search-client\.ts'\)/u); - assert.match(registration, /import\('\.\/src\/api\/stock-levels-report-client\.ts'\)/u); - assert.match(federation, /\.\/InventoryAlerts/u); - assert.match(federation, /\.\/InventorySummary/u); - assert.doesNotMatch(nextManifest, /import\('/u); - const searchClient = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/api/inventory-items-search-client.ts', - ); - const reportClient = await readFixtureFile(fixture.root, inventoryReportClientFile); - assert.match(searchClient, /export interface InventoryItemsClientOptions/u); - assert.doesNotMatch(searchClient, /export interface InventoryItemsSearchClientOptions/u); - assert.match(reportClient, /export interface StockLevelsClientOptions/u); - assert.doesNotMatch(reportClient, /export interface StockLevelsReportClientOptions/u); - const moduleApiClient = await readFixtureFile(fixture.root, inventoryModuleApiClientFile); - const moduleApiContract = await readFixtureFile(fixture.root, inventoryModuleApiContractFile); - const secondModuleApiContract = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/shared/apis/resource-history.ts', - ); - const secondModuleApiClient = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/api/resource-history-client.ts', - ); - const searchProvider = await readFixtureFile(fixture.root, inventorySearchProviderFile); - const reportProvider = await readFixtureFile(fixture.root, inventoryReportProviderFile); - const moduleApiRead = await readFixtureFile(fixture.root, inventoryModuleApiReadFile); - const searchServer = await readFixtureFile(fixture.root, inventorySearchServerFile); - const reportServer = await readFixtureFile(fixture.root, inventoryReportServerFile); - const moduleApiServer = await readFixtureFile(fixture.root, inventoryModuleApiServerFile); - const operationBoundary = await readFixtureFile(fixture.root, inventoryActionPrincipalFile); - const composedApi = await readFixtureFile(fixture.root, inventorySharedApiFile); - const composedHandlers = await readFixtureFile(fixture.root, inventoryHandlerRootFile); - assert.match(searchClient, /api: InventoryItemsSearchApi,/u); - assert.match(reportClient, /api: StockLevelsReportApi,/u); - assert.match( - moduleApiContract, - /headers: \{\},\s+params: \{\},\s+payload: ResourceDetailRequestSchema,\s+query: \{\}/u, - ); - assert.match( - moduleApiClient, - /client\.resourceDetail\.execute\(\{\s+headers: \{\},\s+params: \{\},\s+payload,\s+query: \{\},?\s+\}\)/u, - ); - assert.match(moduleApiContract, /HttpApiGroup\.make\('resourceDetail'\)/u); - assert.match(secondModuleApiContract, /HttpApiGroup\.make\('resourceHistory'\)/u); - assert.match(secondModuleApiClient, /client\.resourceHistory\.execute\(/u); - assertGovernedReadClients([moduleApiClient, searchClient, reportClient]); - assert.doesNotMatch(searchClient, /\.provider\.ts|import\(/u); - assert.doesNotMatch(reportClient, /\.provider\.ts|import\(/u); - assertGovernedReadProviders([searchProvider, reportProvider]); - assert.match(searchProvider, /result\.map\(\(\{ ref \}\) => ref\)/u); - assert.match(moduleApiRead, /defineRead\(/u); - assert.match(moduleApiRead, /legalEntityScope: 'required'/u); - assertGovernedReadServers([moduleApiServer, searchServer, reportServer]); - assert.match(operationBoundary, /export const authenticateOperationPrincipal/u); - assertComposedGovernedReads(composedApi, composedHandlers); - const searchContract = await readFixtureFile(fixture.root, inventorySearchContractFile); - const reportContract = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/shared/apis/stock-levels-report.ts', - ); - assertGovernedProblemDetailsContracts([moduleApiContract, searchContract, reportContract]); - assert.match( - searchContract, - /HttpApiEndpoint\.post\('execute', '\/inventory\.stock\/search\/inventory-items'/u, - ); - assert.doesNotMatch(searchContract, /tenantId|legalEntityId|principalId/u); - assert.match(searchContract, /PolicyConflictProblem/u); - assert.match( - searchContract, - /makeProblemDetailsSchema\(\s*'InventoryItemsProviderPolicyConflictProblem',\s*409,?\s*\)/u, - ); - assert.match(searchContract, /HttpApiGroup\.make\('inventoryItemsSearch'\)/u); - - await mkdir(path.join(fixture.root, 'node_modules', '@app'), { recursive: true }); - await mkdir(path.dirname(path.join(fixture.root, pluginBffNodeModulePath)), { - recursive: true, - }); - await symlink( - path.join(appRoot, 'packages/core-runtime'), - path.join(fixture.root, 'node_modules/@app/core-runtime'), - 'dir', - ); - await symlink( - path.join(appRoot, sharedContractsPackagePath), - path.join(fixture.root, sharedContractsNodeModulePath), - 'dir', - ); - await symlink( - path.join(appRoot, effectNodeModulePath), - path.join(fixture.root, effectNodeModulePath), - 'dir', - ); - await symlink( - path.join(appRoot, pluginBffNodeModulePath), - path.join(fixture.root, pluginBffNodeModulePath), - 'dir', - ); - await writeFixtureFile( - fixture.root, - inventoryActionPrincipalFile, - `import { Effect, Layer, Redacted } from 'effect'; - -const principal = { - authContextRef: 'job:generated-fixture:run:governed-read', - authMethod: 'system', - principalId: '00000000-0000-4000-8000-000000000001', - tenantId: '00000000-0000-4000-8000-000000000002', -}; +it.live( + 'governed contribution generators patch owner contracts and lazy adapters atomically', + Effect.fn(function* mergedScenario29() { + yield* withFixture( + Effect.fn(function* mergedScenario28(fixture) { + const manifestPath = path.join(fixture.root, inventoryManifestFile); + yield* addInventoryItemResourceType(fixture); -export const ActionPrincipalVerifierLive = Layer.empty; + yield* run(fixture, scaffoldCommand.moduleApi, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.resourceDetail, + ]); + yield* run(fixture, scaffoldCommand.moduleApi, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + 'resource-history', + ]); + yield* run(fixture, scaffoldCommand.publicComponent, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + 'inventory-summary', + ]); + yield* run(fixture, scaffoldCommand.publicComponent, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + 'inventory-alerts', + ]); + yield* run(fixture, scaffoldCommand.searchProvider, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.inventoryItems, + scaffoldFlag.resource, + 'item', + ]); + yield* run(fixture, scaffoldCommand.searchProvider, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + 'inventory-suppliers', + scaffoldFlag.resource, + 'item', + ]); + yield* run(fixture, 'report', [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.stockLevels, + scaffoldFlag.resource, + 'item', + ]); -export const authenticateOperationPrincipal = (authorization, problems) => - Redacted.value(authorization) === 'Bearer proof' - ? Effect.succeed(principal) - : Effect.fail(problems.authentication()); + const [nextManifest, registration, federation] = yield* Effect.all( + [ + Effect.promise(() => readFile(manifestPath, 'utf-8')), + readFixtureFile(fixture.root, inventoryRegistrationFile), + readFixtureFile(fixture.root, inventoryFederationConfigFile), + ], + { concurrency: 'unbounded' }, + ); + expect(nextManifest).toMatch(/inventory\.stock\.component\.inventory-summary/u); + expect(nextManifest).toMatch(/inventory\.stock\.search\.inventory-items/u); + expect(nextManifest).toMatch(/inventory\.stock\.report\.stock-levels/u); + expect(registration).toMatch(/import\('\.\/src\/api\/resource-detail-client\.ts'\)/u); + expect(registration).toMatch( + /import\('\.\/src\/api\/inventory-items-search-client\.ts'\)/u, + ); + expect(registration).toMatch(/import\('\.\/src\/api\/stock-levels-report-client\.ts'\)/u); + expect(federation).toMatch(/\.\/InventoryAlerts/u); + expect(federation).toMatch(/\.\/InventorySummary/u); + expect(nextManifest).not.toMatch(/import\('/u); + const searchClient = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/api/inventory-items-search-client.ts', + ); + const reportClient = yield* readFixtureFile(fixture.root, inventoryReportClientFile); + expect(searchClient).toMatch(/export interface InventoryItemsClientOptions/u); + expect(searchClient).not.toMatch(/export interface InventoryItemsSearchClientOptions/u); + expect(reportClient).toMatch(/export interface StockLevelsClientOptions/u); + expect(reportClient).not.toMatch(/export interface StockLevelsReportClientOptions/u); + const moduleApiClient = yield* readFixtureFile(fixture.root, inventoryModuleApiClientFile); + const moduleApiContract = yield* readFixtureFile( + fixture.root, + inventoryModuleApiContractFile, + ); + const secondModuleApiContract = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/shared/apis/resource-history.ts', + ); + const secondModuleApiClient = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/api/resource-history-client.ts', + ); + const searchProvider = yield* readFixtureFile(fixture.root, inventorySearchProviderFile); + const reportProvider = yield* readFixtureFile(fixture.root, inventoryReportProviderFile); + const moduleApiRead = yield* readFixtureFile(fixture.root, inventoryModuleApiReadFile); + const searchServer = yield* readFixtureFile(fixture.root, inventorySearchServerFile); + const reportServer = yield* readFixtureFile(fixture.root, inventoryReportServerFile); + const moduleApiServer = yield* readFixtureFile(fixture.root, inventoryModuleApiServerFile); + const operationBoundary = yield* readFixtureFile( + fixture.root, + inventoryActionPrincipalFile, + ); + const composedApi = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/shared/api.ts', + ); + const composedHandlers = yield* readFixtureFile(fixture.root, inventoryHandlerRootFile); + expect(searchClient).toMatch(/api: InventoryItemsSearchApi,/u); + expect(reportClient).toMatch(/api: StockLevelsReportApi,/u); + expect(moduleApiContract).toMatch( + /headers: \{\},\s+params: \{\},\s+payload: ResourceDetailRequestSchema,\s+query: \{\}/u, + ); + expect(moduleApiClient).toMatch( + /client\.resourceDetail\.execute\(\{\s+headers: \{\},\s+params: \{\},\s+payload,\s+query: \{\},?\s+\}\)/u, + ); + expect(moduleApiContract).toMatch(/HttpApiGroup\.make\('resourceDetail'\)/u); + expect(secondModuleApiContract).toMatch(/HttpApiGroup\.make\('resourceHistory'\)/u); + expect(secondModuleApiClient).toMatch(/client\.resourceHistory\.execute\(/u); + assertGovernedReadClients([moduleApiClient, searchClient, reportClient]); + expect(searchClient).not.toMatch(/\.provider\.ts|import\(/u); + expect(reportClient).not.toMatch(/\.provider\.ts|import\(/u); + assertGovernedReadProviders([searchProvider, reportProvider]); + expect(searchProvider).toMatch(/result\.map\(\(\{ ref \}\) => ref\)/u); + expect(moduleApiRead).toMatch(/defineRead\(/u); + expect(moduleApiRead).toMatch(/legalEntityScope: 'required'/u); + assertGovernedReadServers([moduleApiServer, searchServer, reportServer]); + expect(operationBoundary).toMatch(/export const authenticateOperationPrincipal/u); + assertComposedGovernedReads(composedApi, composedHandlers); + const searchContract = yield* readFixtureFile(fixture.root, inventorySearchContractFile); + const reportContract = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/shared/apis/stock-levels-report.ts', + ); + assertGovernedProblemDetailsContracts([moduleApiContract, searchContract, reportContract]); + expect(searchContract).toMatch( + /HttpApiEndpoint\.post\('execute', '\/inventory\.stock\/search\/inventory-items'/u, + ); + expect(searchContract).not.toMatch(/tenantId|legalEntityId|principalId/u); + expect(searchContract).toMatch(/PolicyConflictProblem/u); + expect(searchContract).toMatch( + /makeProblemDetailsSchema\(\s*'InventoryItemsProviderPolicyConflictProblem',\s*409,?\s*\)/u, + ); + expect(searchContract).toMatch(/HttpApiGroup\.make\('inventoryItemsSearch'\)/u); + + yield* linkFixtureDependencies(fixture.root, appRoot, { + '@app/core-runtime': 'packages/core-runtime', + '@app/shared-contracts': sharedContractsPackagePath, + '@modern-js/plugin-bff': pluginBffNodeModulePath, + effect: effectNodeModulePath, + }); + yield* write( + fixture.root, + inventoryActionPrincipalFile, + `import { Effect, Layer, Redacted } from 'effect'; + +const principal = { + authContextRef: 'job:generated-fixture:run:governed-read', + authMethod: 'system', + principalId: '00000000-0000-4000-8000-000000000001', + tenantId: '00000000-0000-4000-8000-000000000002', +}; + +export const ActionPrincipalVerifierLive = Layer.empty; + +export const authenticateOperationPrincipal = (authorization, problems) => + Redacted.value(authorization) === 'Bearer proof' + ? Effect.succeed(principal) + : Effect.fail(problems.authentication()); `, - ); - await writeFixtureFile( - fixture.root, - 'execute-generated-governed-reads.mts', - `import { ReadRuntime } from '@app/core-runtime'; + ); + yield* write( + fixture.root, + 'execute-generated-governed-reads.mts', + `import { ReadRuntime } from '@app/core-runtime'; import { defineEffectBff, Effect, @@ -1598,7 +1657,8 @@ import { inventorySuppliersReadApiLive } from './verticals/inventory-stock/api/i import { stockLevelsReadApiLive } from './verticals/inventory-stock/api/stock-levels-report-server.ts'; import generatedRuntime from './verticals/inventory-stock/api/index.ts'; import { fixtureApi } from './verticals/inventory-stock/shared/api.ts'; - +import { NodeRuntime } from '${pathToFileURL(require.resolve('@effect/platform-node')).href}'; +Effect.gen(function* generatedHttpProof() { const calls = []; const readRuntime = { runRead: ({ input, principal, registration, transport }) => @@ -1633,10 +1693,13 @@ const requests = [ ['/inventory.stock/search/inventory-suppliers', { query: 'supplier' }], ['/inventory.stock/reports/stock-levels', { parameters: {} }], ]; -try { - const successes = []; - for (const [route, payload] of requests) { - const response = await server.handler( +yield* Effect.addFinalizer(() => Effect.gen(function* disposeGeneratedServers() { + (yield* Effect.promise(() => generatedServer.dispose())); + (yield* Effect.promise(() => server.dispose())); +})); +const successes = []; +for (const [route, payload] of requests) { + const response = (yield* Effect.promise(() => server.handler( new Request('http://fixture.test' + route, { body: JSON.stringify(payload), headers: { @@ -1646,29 +1709,29 @@ try { }, method: 'POST', }), - ); - successes.push({ body: await response.json(), status: response.status }); + ))); + successes.push({ body: (yield* Effect.promise(() => response.json())), status: response.status }); } - const callsAfterSuccess = calls.length; - const missingCorrelationStatuses = []; - for (const [route, payload] of requests) { - const response = await server.handler( +const callsAfterSuccess = calls.length; +const missingCorrelationStatuses = []; +for (const [route, payload] of requests) { + const response = (yield* Effect.promise(() => server.handler( new Request('http://fixture.test' + route, { body: JSON.stringify(payload), headers: { authorization: 'Bearer proof', 'content-type': 'application/json' }, method: 'POST', }), - ); + ))); missingCorrelationStatuses.push(response.status); } - const generatedRootResponse = await generatedServer.handler( +const generatedRootResponse = (yield* Effect.promise(() => generatedServer.handler( new Request('http://fixture.test/reads/resource-detail', { body: JSON.stringify({}), headers: { authorization: 'Bearer proof', 'content-type': 'application/json' }, method: 'POST', }), - ); - console.log( + ))); +console.log( JSON.stringify({ calls, callsAfterSuccess, @@ -1677,303 +1740,308 @@ try { successes, }), ); -} finally { - await generatedServer.dispose(); - await server.dispose(); -} +}).pipe(Effect.scoped, NodeRuntime.runMain); `, - ); - const execution = spawnSync( - process.execPath, - ['--experimental-strip-types', 'execute-generated-governed-reads.mts'], - { - cwd: fixture.root, - encoding: 'utf-8', - env: { - DATABASE_ADMIN_URL: 'postgresql://ontos_admin:admin@localhost:5433/ontos', - DATABASE_URL: 'postgresql://ontos_runtime:runtime@localhost:5433/ontos', - }, - }, - ); - assert.equal(execution.status, 0, execution.stderr); - const expectedGeneratedPrincipal = { - authContextRef: 'job:generated-fixture:run:governed-read', - authMethod: 'system', - principalId: '00000000-0000-4000-8000-000000000001', - tenantId: '00000000-0000-4000-8000-000000000002', - }; - const expectedGeneratedTransport = { correlationId: 'generated-correlation' }; - const lastGeneratedLine = execution.stdout.trim().split('\n').at(-1); - assert.ok(lastGeneratedLine !== undefined); - assert.deepEqual(JSON.parse(lastGeneratedLine), { - calls: [ - { - input: {}, - principal: expectedGeneratedPrincipal, - readKey: 'inventory.stock.api.resource-detail', - transport: expectedGeneratedTransport, - }, - { - input: { query: 'chair' }, - principal: expectedGeneratedPrincipal, - readKey: 'inventory.stock.search.inventory-items', - transport: expectedGeneratedTransport, - }, - { - input: { query: 'supplier' }, - principal: expectedGeneratedPrincipal, - readKey: 'inventory.stock.search.inventory-suppliers', - transport: expectedGeneratedTransport, - }, - { - input: { parameters: {} }, - principal: expectedGeneratedPrincipal, - readKey: 'inventory.stock.report.stock-levels', - transport: expectedGeneratedTransport, - }, - ], - callsAfterSuccess: 4, - generatedRootStatus: 400, - missingCorrelationStatuses: [400, 400, 400, 400], - successes: [ - { body: { ok: true }, status: 200 }, - { body: [], status: 200 }, - { body: [], status: 200 }, - { body: { rows: [] }, status: 200 }, - ], - }); + ); + const execution = spawnSync( + process.execPath, + ['--experimental-strip-types', 'execute-generated-governed-reads.mts'], + { + cwd: fixture.root, + encoding: 'utf-8', + env: { + DATABASE_ADMIN_URL: 'postgresql://ontos_admin:admin@localhost:5433/ontos', + DATABASE_URL: 'postgresql://ontos_runtime:runtime@localhost:5433/ontos', + }, + }, + ); + expect(execution.status, execution.stderr).toBe(0); + const expectedGeneratedPrincipal = { + authContextRef: 'job:generated-fixture:run:governed-read', + authMethod: 'system', + principalId: '00000000-0000-4000-8000-000000000001', + tenantId: '00000000-0000-4000-8000-000000000002', + }; + const expectedGeneratedTransport = { correlationId: 'generated-correlation' }; + const lastGeneratedLine = execution.stdout.trim().split('\n').at(-1); + if (lastGeneratedLine === undefined) { + expect.unreachable('expected generated runtime output'); + } + expect(JSON.parse(lastGeneratedLine)).toEqual({ + calls: [ + { + input: {}, + principal: expectedGeneratedPrincipal, + readKey: 'inventory.stock.api.resource-detail', + transport: expectedGeneratedTransport, + }, + { + input: { query: 'chair' }, + principal: expectedGeneratedPrincipal, + readKey: 'inventory.stock.search.inventory-items', + transport: expectedGeneratedTransport, + }, + { + input: { query: 'supplier' }, + principal: expectedGeneratedPrincipal, + readKey: 'inventory.stock.search.inventory-suppliers', + transport: expectedGeneratedTransport, + }, + { + input: { parameters: {} }, + principal: expectedGeneratedPrincipal, + readKey: 'inventory.stock.report.stock-levels', + transport: expectedGeneratedTransport, + }, + ], + callsAfterSuccess: 4, + generatedRootStatus: 400, + missingCorrelationStatuses: [400, 400, 400, 400], + successes: [ + { body: { ok: true }, status: 200 }, + { body: [], status: 200 }, + { body: [], status: 200 }, + { body: { rows: [] }, status: 200 }, + ], + }); - // Restore the generated contract after the execution-only authentication stub. Reruns - // must validate the real owned boundary, not silently accept handwritten fixture code. - await writeFixtureFile(fixture.root, inventoryActionPrincipalFile, operationBoundary); - const packageJson = decodeFixturePackage( - await readFixtureFile(fixture.root, inventoryPackageFile), - ); - assert.equal(packageJson.dependencies['@app/shared-contracts'], workspaceVersion); + // Restore the generated contract after the execution-only authentication stub. Reruns + // must validate the real owned boundary, not silently accept handwritten fixture code. + yield* write(fixture.root, inventoryActionPrincipalFile, operationBoundary); + const packageJson = yield* decodeFixturePackage( + yield* readFixtureFile(fixture.root, inventoryPackageFile), + ); + expect(packageJson.dependencies['@app/shared-contracts']).toBe(workspaceVersion); - // Owner contracts, reads, and clients remain adaptable; thin HTTP adapters stay generator-owned. - const adaptedGeneratedArtifacts = [ - [ - inventoryModuleApiContractFile, - 'export const ResourceDetailOwnerExtensionSchema = Schema.Struct({ note: Schema.String });', - ], - [ - inventoryModuleApiReadFile, - 'export const resourceDetailOwnerProjection = (value: string) => value;', - ], - [inventoryModuleApiClientFile, '// Owner-maintained client documentation.'], - [ - inventorySearchProviderFile, - 'export const inventoryItemsOwnerRanking = (score: number) => score;', - ], - [ - inventorySearchContractFile, - 'export const InventoryItemsOwnerFilterSchema = Schema.Struct({ tag: Schema.String });', - ], - [inventorySearchClientFile, '// Owner-maintained search client documentation.'], - [ - inventoryReportProviderFile, - 'export const stockLevelsOwnerProjection = (column: string) => column;', - ], - [ - inventoryReportContractFile, - 'export const StockLevelsOwnerColumnSchema = Schema.Struct({ column: Schema.String });', - ], - [inventoryReportClientFile, '// Owner-maintained report client documentation.'], - ] as const; - await Promise.all( - adaptedGeneratedArtifacts.map(async ([relativePath, ownerAddition]) => { - const generated = await readFixtureFile(fixture.root, relativePath); - await writeFixtureFile(fixture.root, relativePath, `${generated}\n${ownerAddition}\n`); - }), - ); + // Owner contracts, reads, and clients remain adaptable; thin HTTP adapters stay generator-owned. + const adaptedGeneratedArtifacts = [ + [ + inventoryModuleApiContractFile, + 'export const ResourceDetailOwnerExtensionSchema = Schema.Struct({ note: Schema.String });', + ], + [ + inventoryModuleApiReadFile, + 'export const resourceDetailOwnerProjection = (value: string) => value;', + ], + [inventoryModuleApiClientFile, '// Owner-maintained client documentation.'], + [ + inventorySearchProviderFile, + 'export const inventoryItemsOwnerRanking = (score: number) => score;', + ], + [ + inventorySearchContractFile, + 'export const InventoryItemsOwnerFilterSchema = Schema.Struct({ tag: Schema.String });', + ], + [inventorySearchClientFile, '// Owner-maintained search client documentation.'], + [ + inventoryReportProviderFile, + 'export const stockLevelsOwnerProjection = (column: string) => column;', + ], + [ + inventoryReportContractFile, + 'export const StockLevelsOwnerColumnSchema = Schema.Struct({ column: Schema.String });', + ], + [inventoryReportClientFile, '// Owner-maintained report client documentation.'], + ] as const; + yield* Effect.all( + adaptedGeneratedArtifacts.map( + Effect.fn(function* mergedScenario27([relativePath, ownerAddition]) { + const generated = yield* readFixtureFile(fixture.root, relativePath); + yield* write(fixture.root, relativePath, `${generated}\n${ownerAddition}\n`); + }), + ), + { concurrency: 'unbounded' }, + ); - const adaptedManifest = await readFixtureFile(fixture.root, inventoryManifestFile); - assert.match(adaptedManifest, /dimensions: \[\]/u); - assert.match(adaptedManifest, /label: 'Stock Levels'/u); - await writeFixtureFile( - fixture.root, - inventoryManifestFile, - adaptedManifest - .replace('dimensions: []', "dimensions: ['warehouse']") - .replace("label: 'Stock Levels'", "label: 'Warehouse stock'"), - ); + const adaptedManifest = yield* readFixtureFile(fixture.root, inventoryManifestFile); + expect(adaptedManifest).toMatch(/dimensions: \[\]/u); + expect(adaptedManifest).toMatch(/label: 'Stock Levels'/u); + yield* write( + fixture.root, + inventoryManifestFile, + adaptedManifest + .replace('dimensions: []', "dimensions: ['warehouse']") + .replace("label: 'Stock Levels'", "label: 'Warehouse stock'"), + ); - const beforeRepeat = await snapshotTree(fixture.root); - await run(fixture, scaffoldCommand.moduleApi, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.resourceDetail, - ]); - await run(fixture, scaffoldCommand.searchProvider, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.inventoryItems, - scaffoldFlag.resource, - 'item', - ]); - await run(fixture, 'report', [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.stockLevels, - scaffoldFlag.resource, - 'item', - ]); - assert.deepEqual(await snapshotTree(fixture.root), beforeRepeat); - await assert.rejects( - run(fixture, scaffoldCommand.publicComponent, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - 'inventory-summary', - ]), - /refusing to overwrite/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), beforeRepeat); - for (const generated of [ - { - command: scaffoldCommand.moduleApi, - name: fixtureName.resourceDetail, - resource: false, - server: 'resource-detail-read-server.ts', - }, - { - command: scaffoldCommand.searchProvider, - name: fixtureName.inventoryItems, - resource: true, - server: 'inventory-items-search-server.ts', - }, - { - command: 'report', - name: fixtureName.stockLevels, - resource: true, - server: 'stock-levels-report-server.ts', - }, - ] as const) { - // Byte-identical owned output is a deterministic no-op for every governed read kind. - // eslint-disable-next-line no-await-in-loop - await run(fixture, generated.command, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - generated.name, - ...(generated.resource ? [scaffoldFlag.resource, 'item'] : []), - ]); - // eslint-disable-next-line no-await-in-loop - assert.deepEqual(await snapshotTree(fixture.root), beforeRepeat); - const serverPath = path.join(fixture.root, 'verticals/inventory-stock/api', generated.server); - // eslint-disable-next-line no-await-in-loop - const ownedServer = await readFile(serverPath, 'utf-8'); - // eslint-disable-next-line no-await-in-loop - await writeFile(serverPath, `${ownedServer}// owner customization\n`, 'utf-8'); - // eslint-disable-next-line no-await-in-loop - await assert.rejects( - run(fixture, generated.command, [ + const beforeRepeat = yield* snapshotTree(fixture.root); + yield* run(fixture, scaffoldCommand.moduleApi, [ scaffoldFlag.vertical, inventorySlug, '--name', - generated.name, - ...(generated.resource ? [scaffoldFlag.resource, 'item'] : []), - ]), - /refusing to overwrite/u, - ); - // eslint-disable-next-line no-await-in-loop - await writeFile(serverPath, ownedServer, 'utf-8'); - } - const sharedApiPath = path.join(fixture.root, inventorySharedApiFile); - const validSharedApi = await readFile(sharedApiPath, 'utf-8'); - await writeFile( - sharedApiPath, - validSharedApi.replace( - '// ', - 'ownerCustomLayer()\n // ', - ), - 'utf-8', - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.moduleApi, - [scaffoldFlag.vertical, inventorySlug, '--name', fixtureName.resourceDetail], - /composition slots are not bound|unsupported developer content/u, - ); - await writeFile(sharedApiPath, validSharedApi, 'utf-8'); + fixtureName.resourceDetail, + ]); + yield* run(fixture, scaffoldCommand.searchProvider, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.inventoryItems, + scaffoldFlag.resource, + 'item', + ]); + yield* run(fixture, 'report', [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.stockLevels, + scaffoldFlag.resource, + 'item', + ]); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeRepeat); + yield* expectFailure( + run(fixture, scaffoldCommand.publicComponent, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + 'inventory-summary', + ]), + (error) => expect(String(error)).toMatch(/refusing to overwrite/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeRepeat); + for (const generated of [ + { + command: scaffoldCommand.moduleApi, + name: fixtureName.resourceDetail, + resource: false, + server: 'resource-detail-read-server.ts', + }, + { + command: scaffoldCommand.searchProvider, + name: fixtureName.inventoryItems, + resource: true, + server: 'inventory-items-search-server.ts', + }, + { + command: 'report', + name: fixtureName.stockLevels, + resource: true, + server: 'stock-levels-report-server.ts', + }, + ] as const) { + // Byte-identical owned output is a deterministic no-op for every governed read kind. + yield* run(fixture, generated.command, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + generated.name, + ...(generated.resource ? [scaffoldFlag.resource, 'item'] : []), + ]); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeRepeat); + const serverPath = path.join( + fixture.root, + 'verticals/inventory-stock/api', + generated.server, + ); + const ownedServer = yield* Effect.promise(() => readFile(serverPath, 'utf-8')); + yield* Effect.promise(() => + writeFile(serverPath, `${ownedServer}// owner customization\n`, 'utf-8'), + ); + yield* expectFailure( + run(fixture, generated.command, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + generated.name, + ...(generated.resource ? [scaffoldFlag.resource, 'item'] : []), + ]), + (error) => expect(String(error)).toMatch(/refusing to overwrite/u), + ); + yield* Effect.promise(() => writeFile(serverPath, ownedServer, 'utf-8')); + } + const sharedApiPath = path.join(fixture.root, inventorySharedApiFile); + const validSharedApi = yield* Effect.promise(() => readFile(sharedApiPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + sharedApiPath, + validSharedApi.replace( + '// ', + 'ownerCustomLayer()\n // ', + ), + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.moduleApi, + [scaffoldFlag.vertical, inventorySlug, '--name', fixtureName.resourceDetail], + /composition slots are not bound|unsupported developer content/u, + ); + yield* Effect.promise(() => writeFile(sharedApiPath, validSharedApi, 'utf-8')); - await assertRelocatedSlotRefused(fixture, sharedApiPath, validSharedApi, [ - GOVERNED_HTTP_API_ADDITION_SLOT_START, - GOVERNED_HTTP_API_ADDITION_SLOT_END, - ]); + yield* assertRelocatedSlotRefused(fixture, sharedApiPath, validSharedApi, [ + GOVERNED_HTTP_API_ADDITION_SLOT_START, + GOVERNED_HTTP_API_ADDITION_SLOT_END, + ]); - const registrationPath = path.join( - fixture.root, - 'verticals/inventory-stock/vertical.registration.ts', - ); - const validRegistration = await readFile(registrationPath, 'utf-8'); - const resourceDetailRegistration = - " 'resource-detail': () => import('./src/api/resource-detail-client.ts'),\n"; - const wrongCategoryRegistration = validRegistration.replace( - '// ', - `${resourceDetailRegistration} // `, - ); - for (const invalidRegistration of [ - wrongCategoryRegistration, - wrongCategoryRegistration.replace( - /^\s*'resource-detail': \(\) => import\('\.\/src\/api\/resource-detail-client\.ts'\),\n/mu, - '', - ), - ]) { - // eslint-disable-next-line no-await-in-loop - await writeFile(registrationPath, invalidRegistration, 'utf-8'); - // eslint-disable-next-line no-await-in-loop - await assertScaffoldRefused( - fixture, - scaffoldCommand.moduleApi, - [scaffoldFlag.vertical, inventorySlug, '--name', fixtureName.resourceDetail], - /wrong contribution category/u, - ); - } - await writeFile(registrationPath, validRegistration, 'utf-8'); - - const handlerRootPath = path.join(fixture.root, inventoryHandlerRootFile); - const validHandlerRoot = await readFile(handlerRootPath, 'utf-8'); - await writeFile( - handlerRootPath, - validHandlerRoot.replace( - /resourceDetailReadApiLive\.pipe\(\s*GovernedReadLayer\.provide\(governedReadRuntimeLive\),?\s*\),/u, - 'resourceDetailReadApiLive.pipe(\n GovernedReadLayer.provide(governedReadRuntimeLive),\n GovernedReadLayer.provide(ownerCustomizedRuntime),\n ),', - ), - 'utf-8', - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.moduleApi, - [scaffoldFlag.vertical, inventorySlug, '--name', fixtureName.resourceDetail], - /contains drift/u, - ); - await writeFile(handlerRootPath, validHandlerRoot, 'utf-8'); - await assertRelocatedSlotRefused(fixture, handlerRootPath, validHandlerRoot, [ - GOVERNED_HTTP_HANDLER_LAYER_SLOT_START, - GOVERNED_HTTP_HANDLER_LAYER_SLOT_END, - ]); - await assert.rejects( - run(fixture, scaffoldCommand.moduleApi, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - '../unsafe', - ]), - /lower-kebab-case/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), beforeRepeat); - const billingFederationPath = path.join( - fixture.root, - 'verticals/billing/module-federation.config.ts', - ); - await writeFile( - billingFederationPath, - `const ignored = /exposes: \\{\\}/u; + const registrationPath = path.join( + fixture.root, + 'verticals/inventory-stock/vertical.registration.ts', + ); + const validRegistration = yield* Effect.promise(() => readFile(registrationPath, 'utf-8')); + const resourceDetailRegistration = + " 'resource-detail': () => import('./src/api/resource-detail-client.ts'),\n"; + const wrongCategoryRegistration = validRegistration.replace( + '// ', + `${resourceDetailRegistration} // `, + ); + for (const invalidRegistration of [ + wrongCategoryRegistration, + wrongCategoryRegistration.replace( + /^\s*'resource-detail': \(\) => import\('\.\/src\/api\/resource-detail-client\.ts'\),\n/mu, + '', + ), + ]) { + yield* Effect.promise(() => writeFile(registrationPath, invalidRegistration, 'utf-8')); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.moduleApi, + [scaffoldFlag.vertical, inventorySlug, '--name', fixtureName.resourceDetail], + /wrong contribution category/u, + ); + } + yield* Effect.promise(() => writeFile(registrationPath, validRegistration, 'utf-8')); + + const handlerRootPath = path.join(fixture.root, inventoryHandlerRootFile); + const validHandlerRoot = yield* Effect.promise(() => readFile(handlerRootPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + handlerRootPath, + validHandlerRoot.replace( + /resourceDetailReadApiLive\.pipe\(\s*GovernedReadLayer\.provide\(governedReadRuntimeLive\),?\s*\),/u, + 'resourceDetailReadApiLive.pipe(\n GovernedReadLayer.provide(governedReadRuntimeLive),\n GovernedReadLayer.provide(ownerCustomizedRuntime),\n ),', + ), + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.moduleApi, + [scaffoldFlag.vertical, inventorySlug, '--name', fixtureName.resourceDetail], + /contains drift/u, + ); + yield* Effect.promise(() => writeFile(handlerRootPath, validHandlerRoot, 'utf-8')); + yield* assertRelocatedSlotRefused(fixture, handlerRootPath, validHandlerRoot, [ + GOVERNED_HTTP_HANDLER_LAYER_SLOT_START, + GOVERNED_HTTP_HANDLER_LAYER_SLOT_END, + ]); + yield* expectFailure( + run(fixture, scaffoldCommand.moduleApi, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + '../unsafe', + ]), + (error) => expect(String(error)).toMatch(/lower-kebab-case/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeRepeat); + const billingFederationPath = path.join( + fixture.root, + 'verticals/billing/module-federation.config.ts', + ); + yield* Effect.promise(() => + writeFile( + billingFederationPath, + `const ignored = /exposes: \\{\\}/u; // exposes: {} export default { exposes: {}, @@ -1983,309 +2051,358 @@ export default { }; void ignored; `, - 'utf-8', - ); - await run(fixture, scaffoldCommand.publicComponent, [ - scaffoldFlag.vertical, - 'billing', - '--name', - 'billing-summary', - ]); - const commentSafeFederation = await readFile(billingFederationPath, 'utf-8'); - assert.match(commentSafeFederation, /\/exposes: \\\{\\\}\/u/u); - assert.match(commentSafeFederation, /\.\/BillingSummary/u); - await writeFile(billingFederationPath, 'export default {};\n', 'utf-8'); - await assertScaffoldRefused( - fixture, - scaffoldCommand.publicComponent, - [scaffoldFlag.vertical, 'billing', '--name', 'billing-details'], - /exposes object is missing/u, + 'utf-8', + ), + ); + yield* run(fixture, scaffoldCommand.publicComponent, [ + scaffoldFlag.vertical, + 'billing', + '--name', + 'billing-summary', + ]); + const commentSafeFederation = yield* Effect.promise(() => + readFile(billingFederationPath, 'utf-8'), + ); + expect(commentSafeFederation).toMatch(/\/exposes: \\\{\\\}\/u/u); + expect(commentSafeFederation).toMatch(/\.\/BillingSummary/u); + yield* Effect.promise(() => + writeFile(billingFederationPath, 'export default {};\n', 'utf-8'), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.publicComponent, + [scaffoldFlag.vertical, 'billing', '--name', 'billing-details'], + /exposes object is missing/u, + ); + }), ); - }); -}); + }), +); -test('governed contribution reruns cannot be spoofed by comments or corrupt owner slots', async () => { - await withFixture(async (fixture) => { - const scaffoldArguments = [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.resourceDetail, - ] as const; - await run(fixture, scaffoldCommand.moduleApi, scaffoldArguments); - const apiContract = await readFixtureFile(fixture.root, inventoryModuleApiContractFile); - const assertInvalidApiContractRerunRejected = async ( - invalidApiContract: string, - ): Promise => { - await writeFixtureFile(fixture.root, inventoryModuleApiContractFile, invalidApiContract); - await assertScaffoldRefused( - fixture, - scaffoldCommand.moduleApi, - scaffoldArguments, - /refusing to overwrite existing business file/u, - ); - }; - await assertInvalidApiContractRerunRejected( - apiContract.replace('/reads/resource-detail', '/reads/wrong'), - ); - await assertInvalidApiContractRerunRejected( - apiContract.replace( - "HttpApiEndpoint.post('execute', '/reads/resource-detail', {", - "HttpApiEndpoint.post('wrong', '/reads/resource-detail', {", - ), - ); - await assertInvalidApiContractRerunRejected( - apiContract.replace( - /\.add\(\n {2}HttpApiGroup\.make\('resourceDetail'\)\.add\([\s\S]*?\n {2}\),\n\);\n$/u, - ".add(HttpApiGroup.make('resourceDetail'));\n", - ), - ); - await writeFixtureFile(fixture.root, inventoryModuleApiContractFile, apiContract); - const manifest = await readFixtureFile(fixture.root, inventoryManifestFile); - const ownerImport = "import { ResourceDetailApi } from './shared/apis/resource-detail.ts';"; - await writeFixtureFile( - fixture.root, - inventoryManifestFile, - `${manifest.replace(ownerImport, '')}\n/* ${ownerImport} */\n`, - ); - await run(fixture, scaffoldCommand.moduleApi, scaffoldArguments); - const repairedManifest = await readFixtureFile(fixture.root, inventoryManifestFile); - assert.equal(repairedManifest.split(/\r?\n/u).filter((line) => line === ownerImport).length, 1); - - const registration = await readFixtureFile(fixture.root, inventoryRegistrationFile); - const entry = "'resource-detail': () => import('./src/api/resource-detail-client.ts'),"; - const corrupted = registration.replace(entry, `${entry}\n${entry}`); - await writeFixtureFile(fixture.root, inventoryRegistrationFile, corrupted); - await assertScaffoldRefused( - fixture, - scaffoldCommand.moduleApi, - scaffoldArguments, - /generated export already exists|generated owner slot/u, - ); +it.live( + 'governed contribution reruns cannot be spoofed by comments or corrupt owner slots', + Effect.fn(function* mergedScenario34() { + yield* withFixture( + Effect.fn(function* mergedScenario33(fixture) { + const scaffoldArguments = [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.resourceDetail, + ] as const; + yield* run(fixture, scaffoldCommand.moduleApi, scaffoldArguments); + const apiContract = yield* readFixtureFile(fixture.root, inventoryModuleApiContractFile); + const assertInvalidApiContractRerunRejected = ( + invalidApiContract: string, + ): Effect.Effect => + Effect.gen(function* mergedScenario32() { + yield* write(fixture.root, inventoryModuleApiContractFile, invalidApiContract); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.moduleApi, + scaffoldArguments, + /refusing to overwrite existing business file/u, + ); + }); + yield* assertInvalidApiContractRerunRejected( + apiContract.replace('/reads/resource-detail', '/reads/wrong'), + ); + yield* assertInvalidApiContractRerunRejected( + apiContract.replace( + "HttpApiEndpoint.post('execute', '/reads/resource-detail', {", + "HttpApiEndpoint.post('wrong', '/reads/resource-detail', {", + ), + ); + yield* assertInvalidApiContractRerunRejected( + apiContract.replace( + /\.add\(\n {2}HttpApiGroup\.make\('resourceDetail'\)\.add\([\s\S]*?\n {2}\),\n\);\n$/u, + ".add(HttpApiGroup.make('resourceDetail'));\n", + ), + ); + yield* write(fixture.root, inventoryModuleApiContractFile, apiContract); + const manifest = yield* readFixtureFile(fixture.root, inventoryManifestFile); + const ownerImport = "import { ResourceDetailApi } from './shared/apis/resource-detail.ts';"; + yield* write( + fixture.root, + inventoryManifestFile, + `${manifest.replace(ownerImport, '')}\n/* ${ownerImport} */\n`, + ); + yield* run(fixture, scaffoldCommand.moduleApi, scaffoldArguments); + const repairedManifest = yield* readFixtureFile(fixture.root, inventoryManifestFile); + expect(repairedManifest.split(/\r?\n/u).filter((line) => line === ownerImport).length).toBe( + 1, + ); - await writeFixtureFile(fixture.root, inventoryRegistrationFile, registration); - await writeFixtureFile( - fixture.root, - inventoryRegistrationFile, - registration.replace(entry, "'resource-detail': () => import('./src/api/evil-client.ts'),"), - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.moduleApi, - scaffoldArguments, - /generated owner slot contains mismatched identity/u, - ); + const registration = yield* readFixtureFile(fixture.root, inventoryRegistrationFile); + const entry = "'resource-detail': () => import('./src/api/resource-detail-client.ts'),"; + const corrupted = registration.replace(entry, `${entry}\n${entry}`); + yield* write(fixture.root, inventoryRegistrationFile, corrupted); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.moduleApi, + scaffoldArguments, + /generated export already exists|generated owner slot/u, + ); - const wrongSlotRegistration = registration - .replace(`${entry}\n`, '') - .replace( - ' // ', - ` ${entry}\n // `, - ); - await writeFixtureFile(fixture.root, inventoryRegistrationFile, wrongSlotRegistration); - await assertScaffoldRefused( - fixture, - scaffoldCommand.moduleApi, - scaffoldArguments, - /generated owner slot contains mismatched identity/u, - ); + yield* write(fixture.root, inventoryRegistrationFile, registration); + yield* write( + fixture.root, + inventoryRegistrationFile, + registration.replace( + entry, + "'resource-detail': () => import('./src/api/evil-client.ts'),", + ), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.moduleApi, + scaffoldArguments, + /generated owner slot contains mismatched identity/u, + ); - await writeFixtureFile( - fixture.root, - inventoryRegistrationFile, - registration.replace( - entry, - "'unrelated': () => import('./src/api/unrelated-client.ts') /* 'resource-detail': spoof */,", - ), - ); - await run(fixture, scaffoldCommand.moduleApi, scaffoldArguments); - const commentSafeRegistration = await readFixtureFile(fixture.root, inventoryRegistrationFile); - assert.equal(commentSafeRegistration.split(entry).length - 1, 1); - - await writeFixtureFile( - fixture.root, - inventoryManifestFile, - repairedManifest.replace( - ownerImport, - "import { ResourceDetailApi } from './shared/apis/evil.ts';", - ), - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.moduleApi, - scaffoldArguments, - /generated owner import binding conflicts/u, - ); - }); -}); + const wrongSlotRegistration = registration + .replace(`${entry}\n`, '') + .replace( + ' // ', + ` ${entry}\n // `, + ); + yield* write(fixture.root, inventoryRegistrationFile, wrongSlotRegistration); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.moduleApi, + scaffoldArguments, + /generated owner slot contains mismatched identity/u, + ); -test('adapted governed artifacts require executable owner identity instead of comments or strings', async () => { - await withFixture(async (fixture) => { - const assertSpoofsRejected = async ( - spoofs: readonly (readonly [string, string])[], - command: Parameters[1], - commandArguments: readonly string[], - ): Promise => { - const [spoof, ...remaining] = spoofs; - if (spoof === undefined) { - return; - } - const [file, identity] = spoof; - const current = await readFixtureFile(fixture.root, file); - const removedIdentity = 'const removedIdentity = undefined;'; - await writeFixtureFile( - fixture.root, - file, - `${current.replace(identity, removedIdentity)}\n/* ${identity} */\nconst identitySpoof = ${JSON.stringify(identity)};\n`, - ); - await assertScaffoldRefused( - fixture, - command, - commandArguments, - /refusing to overwrite existing business file/u, - ); - await writeFixtureFile(fixture.root, file, current); - await assertSpoofsRejected(remaining, command, commandArguments); - }; - await addInventoryItemResourceType(fixture); - const moduleArguments = [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.resourceDetail, - ] as const; - await run(fixture, scaffoldCommand.moduleApi, moduleArguments); - const moduleSpoofs = [ - [ - inventoryModuleApiContractFile, - "export const ResourceDetailApi = HttpApi.make('ResourceDetailApi')", - ], - [inventoryModuleApiReadFile, 'export const resourceDetailRead = defineRead('], - [ - inventoryModuleApiServerFile, - 'export const resourceDetailReadApiLive = HttpApiBuilder.group(', - ], - ] as const; - await assertSpoofsRejected(moduleSpoofs, scaffoldCommand.moduleApi, moduleArguments); - - const assertAdaptationRejected = async ( - file: string, - adapt: (source: string) => string, - ): Promise => { - const current = await readFixtureFile(fixture.root, file); - await writeFixtureFile(fixture.root, file, adapt(current)); - await assertScaffoldRefused( - fixture, - scaffoldCommand.moduleApi, - moduleArguments, - /refusing to overwrite existing business file/u, - ); - await writeFixtureFile(fixture.root, file, current); - }; - await assertAdaptationRejected( - inventoryModuleApiContractFile, - (source) => - `${source.replace( - "export const ResourceDetailApi = HttpApi.make('ResourceDetailApi')", - "namespace Decoy { export const ResourceDetailApi = HttpApi.make('ResourceDetailApi')", - )}\n}`, - ); - await assertAdaptationRejected(inventoryActionGatewayFile, (source) => - source.replace( - 'export const operationGateway = makeOperationGateway();', - "namespace Decoy { export const operationGateway = makeOperationGateway(); }\nconst spoof = 'export const operationGateway = actionGateway';", - ), - ); - await assertAdaptationRejected( - inventoryModuleApiServerFile, - (source) => - `${source.replace( - 'authenticatePrincipal: authenticateOperationPrincipal', - 'authenticatePrincipal: unverifiedPrincipal', - )}\nconst unverifiedPrincipal = authenticateOperationPrincipal;`, - ); - await assertAdaptationRejected( - inventoryModuleApiServerFile, - (source) => - `${source.replace('registration: resourceDetailRead', 'registration: otherRead')}\nvoid ReadRuntime;`, - ); - await assertAdaptationRejected( - inventoryModuleApiServerFile, - (source) => - `${source.replace('makeGovernedReadHttpHandler({', 'unsafeReadHandler({')}\nconst spoof = '.runRead({';`, + yield* write( + fixture.root, + inventoryRegistrationFile, + registration.replace( + entry, + "'unrelated': () => import('./src/api/unrelated-client.ts') /* 'resource-detail': spoof */,", + ), + ); + yield* run(fixture, scaffoldCommand.moduleApi, scaffoldArguments); + const commentSafeRegistration = yield* readFixtureFile( + fixture.root, + inventoryRegistrationFile, + ); + expect(commentSafeRegistration.split(entry).length - 1).toBe(1); + + yield* write( + fixture.root, + inventoryManifestFile, + repairedManifest.replace( + ownerImport, + "import { ResourceDetailApi } from './shared/apis/evil.ts';", + ), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.moduleApi, + scaffoldArguments, + /generated owner import binding conflicts/u, + ); + }), ); + }), +); - const searchArguments = [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.inventoryItems, - scaffoldFlag.resource, - 'item', - ] as const; - await run(fixture, scaffoldCommand.searchProvider, searchArguments); - const providerSpoofs = [ - [inventorySearchProviderFile, 'export const inventoryItemsRead = defineRead('], - [ - inventorySearchContractFile, - "export const InventoryItemsSearchApi = HttpApi.make('InventoryItemsSearchApi')", - ], - [inventorySearchServerFile, 'export const inventoryItemsReadApiLive = HttpApiBuilder.group('], - ] as const; - await assertSpoofsRejected(providerSpoofs, scaffoldCommand.searchProvider, searchArguments); - }); -}); +it.live( + 'adapted governed artifacts require executable owner identity instead of comments or strings', + Effect.fn(function* mergedScenario41() { + yield* withFixture( + Effect.fn(function* mergedScenario40(fixture) { + const assertSpoofsRejected = ( + spoofs: readonly (readonly [string, string])[], + command: Parameters[1], + commandArguments: readonly string[], + ): Effect.Effect => + Effect.gen(function* mergedScenario38() { + const [spoof, ...remaining] = spoofs; + if (spoof === undefined) { + return; + } + const [file, identity] = spoof; + const current = yield* readFixtureFile(fixture.root, file); + const removedIdentity = 'const removedIdentity = undefined;'; + yield* write( + fixture.root, + file, + `${current.replace(identity, removedIdentity)}\n/* ${identity} */\nconst identitySpoof = ${JSON.stringify(identity)};\n`, + ); + yield* assertScaffoldRefused( + fixture, + command, + commandArguments, + /refusing to overwrite existing business file/u, + ); + yield* write(fixture.root, file, current); + yield* assertSpoofsRejected(remaining, command, commandArguments); + }); + yield* addInventoryItemResourceType(fixture); + const moduleArguments = [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.resourceDetail, + ] as const; + yield* run(fixture, scaffoldCommand.moduleApi, moduleArguments); + const moduleSpoofs = [ + [ + inventoryModuleApiContractFile, + "export const ResourceDetailApi = HttpApi.make('ResourceDetailApi')", + ], + [inventoryModuleApiReadFile, 'export const resourceDetailRead = defineRead('], + [ + inventoryModuleApiServerFile, + 'export const resourceDetailReadApiLive = HttpApiBuilder.group(', + ], + ] as const; + yield* assertSpoofsRejected(moduleSpoofs, scaffoldCommand.moduleApi, moduleArguments); + + const assertAdaptationRejected = ( + file: string, + adapt: (source: string) => string, + ): Effect.Effect => + Effect.gen(function* mergedScenario39() { + const current = yield* readFixtureFile(fixture.root, file); + yield* write(fixture.root, file, adapt(current)); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.moduleApi, + moduleArguments, + /refusing to overwrite existing business file/u, + ); + yield* write(fixture.root, file, current); + }); + yield* assertAdaptationRejected( + inventoryModuleApiContractFile, + (source) => + `${source.replace( + "export const ResourceDetailApi = HttpApi.make('ResourceDetailApi')", + "namespace Decoy { export const ResourceDetailApi = HttpApi.make('ResourceDetailApi')", + )}\n}`, + ); + yield* assertAdaptationRejected(inventoryActionGatewayFile, (source) => + source.replace( + 'export const operationGateway = makeOperationGateway();', + "namespace Decoy { export const operationGateway = makeOperationGateway(); }\nconst spoof = 'export const operationGateway = actionGateway';", + ), + ); + yield* assertAdaptationRejected( + inventoryModuleApiServerFile, + (source) => + `${source.replace( + 'authenticatePrincipal: authenticateOperationPrincipal', + 'authenticatePrincipal: unverifiedPrincipal', + )}\nconst unverifiedPrincipal = authenticateOperationPrincipal;`, + ); + yield* assertAdaptationRejected( + inventoryModuleApiServerFile, + (source) => + `${source.replace('registration: resourceDetailRead', 'registration: otherRead')}\nvoid ReadRuntime;`, + ); + yield* assertAdaptationRejected( + inventoryModuleApiServerFile, + (source) => + `${source.replace('makeGovernedReadHttpHandler({', 'unsafeReadHandler({')}\nconst spoof = '.runRead({';`, + ); -test('governed client generation rejects an incompatible shared runtime dependency atomically', async () => { - await withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalActionBoundary, [ - scaffoldFlag.vertical, - inventorySlug, - ]); - const packagePath = path.join(fixture.root, inventoryPackageFile); - const packageJson = decodeFixturePackage(await readFile(packagePath, 'utf-8')); - await writeFile( - packagePath, - json({ - ...packageJson, - dependencies: { - ...packageJson.dependencies, - '@app/shared-contracts': '^1.0.0', - }, + const searchArguments = [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.inventoryItems, + scaffoldFlag.resource, + 'item', + ] as const; + yield* run(fixture, scaffoldCommand.searchProvider, searchArguments); + const providerSpoofs = [ + [inventorySearchProviderFile, 'export const inventoryItemsRead = defineRead('], + [ + inventorySearchContractFile, + "export const InventoryItemsSearchApi = HttpApi.make('InventoryItemsSearchApi')", + ], + [ + inventorySearchServerFile, + 'export const inventoryItemsReadApiLive = HttpApiBuilder.group(', + ], + ] as const; + yield* assertSpoofsRejected( + providerSpoofs, + scaffoldCommand.searchProvider, + searchArguments, + ); }), - 'utf-8', ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.moduleApi, - [scaffoldFlag.vertical, inventorySlug, '--name', fixtureName.resourceDetail], - /incompatible @app\/shared-contracts dependency/u, + }), +); + +it.live( + 'governed client generation rejects an incompatible shared runtime dependency atomically', + Effect.fn(function* mergedScenario44() { + yield* withFixture( + Effect.fn(function* mergedScenario43(fixture) { + yield* run(fixture, scaffoldCommand.microverticalActionBoundary, [ + scaffoldFlag.vertical, + inventorySlug, + ]); + const packagePath = path.join(fixture.root, inventoryPackageFile); + const packageJson = yield* decodeFixturePackage( + yield* Effect.promise(() => readFile(packagePath, 'utf-8')), + ); + yield* Effect.promise(() => + writeFile( + packagePath, + json({ + ...packageJson, + dependencies: { + ...packageJson.dependencies, + '@app/shared-contracts': '^1.0.0', + }, + }), + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.moduleApi, + [scaffoldFlag.vertical, inventorySlug, '--name', fixtureName.resourceDetail], + /incompatible @app\/shared-contracts dependency/u, + ); + }), ); - }); -}); + }), +); -test('governed client generation restores its missing owner-local operation gateway', async () => { - await withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalActionBoundary, [ - scaffoldFlag.vertical, - inventorySlug, - ]); - await rm(path.join(fixture.root, inventoryActionGatewayFile)); +it.live( + 'governed client generation restores its missing owner-local operation gateway', + Effect.fn(function* mergedScenario47() { + yield* withFixture( + Effect.fn(function* mergedScenario46(fixture) { + yield* run(fixture, scaffoldCommand.microverticalActionBoundary, [ + scaffoldFlag.vertical, + inventorySlug, + ]); + yield* Effect.promise(() => rm(path.join(fixture.root, inventoryActionGatewayFile))); - await run(fixture, scaffoldCommand.moduleApi, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.resourceDetail, - ]); + yield* run(fixture, scaffoldCommand.moduleApi, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.resourceDetail, + ]); - const gateway = await readFixtureFile(fixture.root, inventoryActionGatewayFile); - assert.match(gateway, /@ontos-action-boundary-owner inventory-stock/u); - assert.match(gateway, /export const operationGateway = makeOperationGateway\(\)/u); - }); -}); + const gateway = yield* readFixtureFile(fixture.root, inventoryActionGatewayFile); + expect(gateway).toMatch(/@ontos-action-boundary-owner inventory-stock/u); + expect(gateway).toMatch(/export const operationGateway = makeOperationGateway\(\)/u); + }), + ); + }), +); -test('recognizes only exact schema-only Outbox package subpaths as cross-vertical contracts', () => { +it('recognizes only exact schema-only Outbox package subpaths as cross-vertical contracts', () => { const producerPackage = { exports: { '.': './src/index.ts', @@ -2293,822 +2410,938 @@ test('recognizes only exact schema-only Outbox package subpaths as cross-vertica './workers': './src/workers/index.ts', }, }; - assert.deepEqual(publishedOutboxContractExports(producerPackage), ['./outbox/orders-created']); - assert.doesNotThrow(() => + expect(publishedOutboxContractExports(producerPackage)).toEqual(['./outbox/orders-created']); + expect(() => assertPublishedOutboxDependencyUsage({ dependencyPackageJson: producerPackage, dependencyPackageName: inventoryPackageName, moduleSpecifiers: ['@app/inventory-stock/outbox/orders-created'], }), - ); - assert.throws( - () => - assertPublishedOutboxDependencyUsage({ - dependencyPackageJson: producerPackage, - dependencyPackageName: inventoryPackageName, - moduleSpecifiers: ['@app/inventory-stock/workers'], - }), - /not a published schema-only Outbox contract subpath/u, - ); - assert.throws( - () => - assertPublishedOutboxDependencyUsage({ - dependencyPackageJson: { exports: { '.': './src/index.ts' } }, - dependencyPackageName: inventoryPackageName, - moduleSpecifiers: [inventoryPackageName], - }), - /not a published schema-only Outbox contract dependency/u, - ); -}); - -test('rejects malformed command contracts and leaves the fixture unchanged', async () => { - await withFixture(async (fixture) => { - const before = await snapshotTree(fixture.root); - await assert.rejects( - runScaffold( - 'action', - [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - scaffoldFlag.authorization, - 'action_execution', - '--provisioning', - 'tenant_membership_default', - ], - { workspaceRoot: fixture.root }, - ), - /missing required flag --legal-entity-scope/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - const invalidCalls: readonly [ScaffoldCommand, readonly string[], RegExp][] = [ - ['action', [scaffoldFlag.vertical, inventorySlug], /missing required flag --action/u], - [ - 'action', - [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - scaffoldFlag.legalEntityScope, - 'invalid', - ], - /must be required, optional, or forbidden/u, - ], - [ - 'action', - [scaffoldFlag.vertical, inventorySlug, '--action', fixtureName.action, '--unknown', 'x'], - /unknown flag --unknown/u, - ], - [ - 'action', - [scaffoldFlag.vertical, inventorySlug, '--action', fixtureName.action, '--action', 'again'], - /only once/u, - ], - ['action', [scaffoldFlag.vertical, '', '--action', fixtureName.action], /non-empty value/u], - [ - 'action', - [scaffoldFlag.vertical, '../billing', '--action', fixtureName.action], - /lower-kebab-case/u, - ], - [ - 'action', - [scaffoldFlag.vertical, '/absolute/billing', '--action', fixtureName.action], - /lower-kebab-case/u, - ], - [ - 'action', - [ - scaffoldFlag.vertical, - inventorySlug, - '--scope', - 'core', - '--module', - fixtureName.actionModule, - '--action', - fixtureName.action, - ], - /mutually exclusive/u, - ], - ['action', ['--scope', 'core', '--action', fixtureName.action], /--module is required/u], - [ - 'action', - ['--scope', 'other', '--module', fixtureName.actionModule, '--action', fixtureName.action], - /--scope core is required/u, - ], - [ - 'action', - ['--scope', 'core', '--module', 'billing.modules', '--action', fixtureName.action], - /stable lowercase core/u, - ], - [ - 'action', - ['--scope', 'core', '--module', 'core.../modules', '--action', fixtureName.action], - /stable lowercase core/u, - ], - [ - 'action', - [scaffoldFlag.vertical, 'missing', '--action', fixtureName.action], - /package metadata is missing/u, - ], - [ - scaffoldCommand.microverticalActionBoundary, - [scaffoldFlag.vertical, inventorySlug, '--unknown', 'x'], - /unknown flag --unknown/u, - ], - [ - scaffoldCommand.microverticalActionBoundary, - [scaffoldFlag.vertical, '../billing'], - /lower-kebab-case/u, - ], - [ - 'policy', - ['--scope', 'global', '--policy', fixtureName.policy, scaffoldFlag.vertical, inventorySlug], - /forbidden/u, - ], - ['policy', ['--scope', 'microvertical', '--policy', fixtureName.policy], /required/u], - ['policy', ['--scope', 'other', '--policy', fixtureName.policy], /global or microvertical/u], - [ - scaffoldCommand.outboxMessage, - [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - '--topic', - 'Not.Safe', - ], - /dot-separated/u, - ], - [ - scaffoldCommand.outboxWorker, - [ - scaffoldFlag.vertical, - 'billing', - '--worker', - fixtureName.ordersLogger, - scaffoldFlag.producer, - inventorySlug, - '--topic', - '../orders.created', - ], - /dot-separated/u, - ], - ]; - await Promise.all( - invalidCalls.map(async ([command, generatorArguments, expected]) => { - await assert.rejects(run(fixture, command, generatorArguments), expected); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - ); - }); -}); - -test('generates one immutable Action identity boundary and exact direct dependencies', async () => { - await withFixture(async (fixture) => { - const shellBefore = await readFixtureFile(fixture.root, shellSentinelFile); - const topologyBefore = await readFixtureFile(fixture.root, topologyFile); - const result = await run(fixture, scaffoldCommand.microverticalActionBoundary, [ - scaffoldFlag.vertical, - inventorySlug, - ]); - assert.equal(result.kind, 'generated'); - const server = await readFixtureFile(fixture.root, inventoryActionPrincipalFile); - const actionHttpRunner = await readFixtureFile(fixture.root, inventoryActionHttpRunnerFile); - const client = await readFixtureFile(fixture.root, inventoryActionGatewayFile); - const redemption = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/api/auth/gateway-assertion-redemption.ts', - ); - for (const source of [server, client]) { - assert.match(source, /@ontos-action-boundary-owner inventory-stock/u); - assert.match(source, /@ontos-action-boundary-audience inventory-stock/u); - assert.match(source, /ACTION_GATEWAY_AUDIENCE = 'inventory-stock'/u); - } - assert.match(server, /@app\/gateway-principal-verifier\/server/u); - assert.match(server, /bindGatewayPrincipalVerifier\(ACTION_GATEWAY_AUDIENCE\)/u); - assert.doesNotMatch( - server, - /createLocalJWKSet|decodeProtectedHeader|jwtVerify|PublicVerificationKeySchema/u, - ); - assert.match(client, /makeOperationGateway as makeSharedOperationGateway/u); - assert.match(client, /makeSharedOperationGateway\(ACTION_GATEWAY_AUDIENCE, acquire\)/u); - assert.match(client, /export const operationGateway = makeOperationGateway\(\)/u); - assert.doesNotMatch( - client, - /ActionGatewayIssuer|ActionGatewayAttempt|makeActionGateway|\bactionGateway\b/u, - ); - assert.doesNotMatch(client, /Effect\.flatMap|Bearer \$\{|acquire\(\{ audience/u); - assert.doesNotMatch( - client, - /api\/auth\/action-principal|gateway-assertion-redemption|GatewayContextProtectedHeader|verticals\//u, - ); - assert.doesNotMatch(client, /localStorage|sessionStorage/u); - assert.match(server, /verifyAndRedeem/u); - assert.match(actionHttpRunner, /bindGovernedActionHttp/u); - assert.match(actionHttpRunner, /bindActionHttpRunner/u); - assert.match(actionHttpRunner, /authenticateOperationPrincipal/u); - assert.doesNotMatch(actionHttpRunner, /ActionRuntime|ActionCoreError|HttpApiEndpoint/u); - assert.match(redemption, /GatewayAssertionRedemptionUnavailableError/u); - const packageJson = decodeFixturePackage( - await readFixtureFile(fixture.root, inventoryPackageFile), - ); - assert.deepEqual(packageJson.dependencies, { - '@app/core-runtime': workspaceVersion, - '@app/gateway-principal-verifier': workspaceVersion, - '@app/shared-contracts': workspaceVersion, - effect: '4.0.0-beta.107', - zeta: '1.0.0', - }); - assert.equal(packageJson.scripts['existing'], preservedFixtureValue); - assert.equal(await readFixtureFile(fixture.root, shellSentinelFile), shellBefore); - assert.equal(await readFixtureFile(fixture.root, topologyFile), topologyBefore); - }); -}); - -test('Action identity boundary preflight refuses unsafe writes', async () => { - await withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalActionBoundary, [ - scaffoldFlag.vertical, - inventorySlug, - ]); - const afterFirstRun = await snapshotTree(fixture.root); - await run(fixture, scaffoldCommand.microverticalActionBoundary, [ - scaffoldFlag.vertical, - inventorySlug, - ]); - assert.deepEqual(await snapshotTree(fixture.root), afterFirstRun); - }); - await withFixture(async (fixture) => { - await writeFixtureFile( - fixture.root, - inventoryActionPrincipalFile, - `// Owner-authored identity adapter -export const ownerCode = true; -`, - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalActionBoundary, - [scaffoldFlag.vertical, inventorySlug], - /refusing to overwrite existing business file/u, - ); - }); + ).not.toThrow(); + expect(() => + assertPublishedOutboxDependencyUsage({ + dependencyPackageJson: producerPackage, + dependencyPackageName: inventoryPackageName, + moduleSpecifiers: ['@app/inventory-stock/workers'], + }), + ).toThrow(/not a published schema-only Outbox contract subpath/u); + expect(() => + assertPublishedOutboxDependencyUsage({ + dependencyPackageJson: { exports: { '.': './src/index.ts' } }, + dependencyPackageName: inventoryPackageName, + moduleSpecifiers: [inventoryPackageName], + }), + ).toThrow(/not a published schema-only Outbox contract dependency/u); }); -test('governed generators reject legacy principal boundaries before writing files', async () => { - await withFixture(async (fixture) => { - await addInventoryItemResourceType(fixture); - await run(fixture, scaffoldCommand.microverticalActionBoundary, [ - scaffoldFlag.vertical, - inventorySlug, - ]); - const generated = await readFixtureFile(fixture.root, inventoryActionPrincipalFile); - const legacy = generated.replace( - /const verifyOperationPrincipal =[\s\S]*$/u, - 'export const verifyOperationPrincipal = verifyActionPrincipal;\n', - ); - assert.doesNotMatch(legacy, /export const authenticateOperationPrincipal/u); - await writeFixtureFile(fixture.root, inventoryActionPrincipalFile, legacy); - const before = await snapshotTree(fixture.root); - const calls: readonly [ScaffoldCommand, readonly string[]][] = [ - [scaffoldCommand.microverticalActionBoundary, []], - [scaffoldCommand.moduleApi, ['--name', fixtureName.resourceDetail]], - [ - scaffoldCommand.searchProvider, - ['--name', fixtureName.inventoryItems, scaffoldFlag.resource, 'item'], - ], - ['report', ['--name', fixtureName.stockLevels, scaffoldFlag.resource, 'item']], - ]; - await Promise.all( - calls.map(async ([command, args]) => { - await assert.rejects( - run(fixture, command, [scaffoldFlag.vertical, inventorySlug, ...args]), - /incompatible generated Action boundary:.*export authenticateOperationPrincipal.*provide ActionPrincipalVerifierLive|refusing to overwrite existing business file: operation boundary/u, +it.live( + 'rejects malformed command contracts and leaves the fixture unchanged', + Effect.fn(function* scenario25() { + yield* withFixture( + Effect.fn(function* scenario26(fixture) { + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + runScaffoldEffect( + 'action', + [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + scaffoldFlag.authorization, + 'action_execution', + '--provisioning', + 'tenant_membership_default', + ], + { workspaceRoot: fixture.root }, + ).pipe(Effect.provide(NodeServices.layer)), + (error) => expect(String(error)).toMatch(/missing required flag --legal-entity-scope/u), ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - ); - }); -}); - -test('governed generation preserves compatible owner principal adaptations', async () => { - await withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalActionBoundary, [ - scaffoldFlag.vertical, - inventorySlug, - ]); - const adapted = `${await readFixtureFile(fixture.root, inventoryActionPrincipalFile)}\n// Owner-specific diagnostics remain private to this adapter.\n`; - await writeFixtureFile(fixture.root, inventoryActionPrincipalFile, adapted); - await run(fixture, scaffoldCommand.microverticalActionBoundary, [ - scaffoldFlag.vertical, - inventorySlug, - ]); - await run(fixture, scaffoldCommand.moduleApi, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.resourceDetail, - ]); - assert.equal(await readFixtureFile(fixture.root, inventoryActionPrincipalFile), adapted); - }); -}); - -test('generated verifier executes real Shell assertions and overlapping Ed25519 rotation', async () => { - await withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalActionBoundary, [ - scaffoldFlag.vertical, - inventorySlug, - ]); - await run(fixture, scaffoldCommand.microverticalActionBoundary, [ - scaffoldFlag.vertical, - 'billing', - ]); - await mkdir(path.join(fixture.root, 'node_modules', '@app'), { recursive: true }); - await symlink( - path.join(appRoot, 'packages/core-runtime'), - path.join(fixture.root, 'node_modules/@app/core-runtime'), - 'dir', - ); - await symlink( - path.join(appRoot, sharedContractsPackagePath), - path.join(fixture.root, sharedContractsNodeModulePath), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/gateway-principal-verifier'), - path.join(fixture.root, 'node_modules/@app/gateway-principal-verifier'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/node_modules/effect'), - path.join(fixture.root, effectNodeModulePath), - 'dir', - ); - await symlink( - path.join(appRoot, 'apps/shell-super-app/node_modules/jose'), - path.join(fixture.root, 'node_modules/jose'), - 'dir', - ); - const edgeBundleDirectory = path.join(fixture.root, 'edge-bundle'); - await mkdir(edgeBundleDirectory, { recursive: true }); - const edgeMetafile = path.join(edgeBundleDirectory, 'meta.json'); - const edgeBundle = spawnSync( - esbuildPath, - [ - path.join(fixture.root, inventoryActionPrincipalFile), - '--bundle', - '--format=esm', - `--metafile=${edgeMetafile}`, - `--outfile=${path.join(edgeBundleDirectory, 'action-principal.mjs')}`, - '--platform=browser', - ], - { encoding: 'utf-8' }, - ); - const edgeBundleErrorMessage = edgeBundle.error?.message; - let edgeBundleFailureMessage = 'edge bundle command did not start'; - if (edgeBundle.stderr.length > 0) { - edgeBundleFailureMessage = edgeBundle.stderr; - } else if (edgeBundleErrorMessage !== undefined && edgeBundleErrorMessage.length > 0) { - edgeBundleFailureMessage = edgeBundleErrorMessage; - } - assert.equal(edgeBundle.status, 0, edgeBundleFailureMessage); - const edgeInputs = Object.keys( - Schema.decodeUnknownSync(EsbuildMetafileSchema)( - JSON.parse(await readFile(edgeMetafile, 'utf-8')), - ).inputs, - ).join('\n'); - assert.match(edgeInputs, /core-runtime\/src\/auth\/gateway-assertion-redemption\.ts/u); - assert.doesNotMatch(edgeInputs, /core-runtime\/src\/db|node:(?:crypto|path)|\/pg\//u); - const generatedModule = Schema.decodeUnknownSync(GeneratedPrincipalModuleSchema)( - await import(pathToFileURL(path.join(fixture.root, inventoryActionPrincipalFile)).href), - ); - const billingGeneratedModule = Schema.decodeUnknownSync(GeneratedPrincipalModuleSchema)( - await import( - pathToFileURL(path.join(fixture.root, 'verticals/billing/api/auth/action-principal.ts')) - .href - ), - ); - const generatedClientModule = Schema.decodeUnknownSync(GeneratedOperationGatewayModuleSchema)( - await import(pathToFileURL(path.join(fixture.root, inventoryActionGatewayFile)).href), - ); - const generatedActionHttpRunnerModule = Schema.decodeUnknownSync( - GeneratedActionHttpRunnerModuleSchema, - )(await import(pathToFileURL(path.join(fixture.root, inventoryActionHttpRunnerFile)).href)); - const current = await makeGatewayKey('current'); - const retiring = await makeGatewayKey('retiring'); - const principal = { - authBindingId: '30000000-0000-4000-8000-000000000001', - authContextRef: 'better-auth-session:scaffold-test', - authMethod: 'session' as const, - principalId: '40000000-0000-4000-8000-000000000001', - tenantId: '50000000-0000-4000-8000-000000000001', - }; - const issue = async ( - configuration: GatewayIssuerConfigValue, - issuedAt: number, - audience = inventorySlug, - ) => - await runEffectTestPromise( - issueGatewayContextAssertion({ audience, principal }).pipe( - Effect.provide( - makeGatewayIssuerLayer({ - currentTimeSeconds: Effect.succeed(issuedAt), - generateJti: Effect.succeed(fixtureGatewayJti), - loadAudiences: Effect.succeed(new Set([audience])), - loadConfig: Effect.succeed(configuration), - }), - ), - ), - ); - const environment = { - ONTOS_GATEWAY_ISSUER: fixtureGatewayIssuer, - ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify({ - keys: [current.publicJwk, retiring.publicJwk], - }), - }; - const currentAssertion = await issue(current.configuration, 1_700_000_000); - const billingAssertion = await issue(current.configuration, 1_700_000_000, 'billing'); - const retiringAssertion = await issue(retiring.configuration, 1_700_000_000); - const testRedemption = { consume: () => Effect.void }; - const verify = async (token: string, override = environment, now = 1_700_000_001) => - await runEffectTestPromise( - generatedModule.verifyActionPrincipal(`Bearer ${token}`, { - currentTimeSeconds: Effect.succeed(now), - environment: override, - redemption: testRedemption, - }), - ); - - assert.deepEqual(await verify(currentAssertion.token), principal); - assert.deepEqual( - await runEffectTestPromise( - billingGeneratedModule.verifyActionPrincipal(`Bearer ${billingAssertion.token}`, { - currentTimeSeconds: Effect.succeed(1_700_000_001), - environment, - redemption: testRedemption, - }), - ), - principal, - ); - await assert.rejects( - runEffectTestPromise( - generatedModule.verifyActionPrincipal(`Bearer ${billingAssertion.token}`, { - currentTimeSeconds: Effect.succeed(1_700_000_001), - environment, - redemption: testRedemption, - }), - ), - isGeneratedPrincipalError('ActionPrincipalScopeError'), - ); - await assert.rejects( - runEffectTestPromise( - billingGeneratedModule.verifyActionPrincipal(`Bearer ${currentAssertion.token}`, { - currentTimeSeconds: Effect.succeed(1_700_000_001), - environment, - redemption: testRedemption, - }), - ), - isGeneratedPrincipalError('ActionPrincipalScopeError'), - ); - assert.deepEqual(await verify(retiringAssertion.token), principal); - await assert.rejects( - verify('not-a-jwt'), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), - ); - await Promise.all( - [ - { keys: [] }, - { keys: [current.publicJwk, current.publicJwk] }, - { keys: [{ ...current.publicJwk, d: 'private-material' }] }, - { keys: [{ ...current.publicJwk, key_ops: ['sign'] }] }, - { keys: [{ ...current.publicJwk, alg: 'HS256' }] }, - { keys: [{ ...current.publicJwk, x: '' }] }, - ].map( - async (jwks) => - await assert.rejects( - verify(currentAssertion.token, { - ...environment, - ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify(jwks), + expect(yield* snapshotTree(fixture.root)).toEqual(before); + const invalidCalls: readonly [ScaffoldCommand, readonly string[], RegExp][] = [ + ['action', [scaffoldFlag.vertical, inventorySlug], /missing required flag --action/u], + [ + 'action', + [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + scaffoldFlag.legalEntityScope, + 'invalid', + ], + /must be required, optional, or forbidden/u, + ], + [ + 'action', + [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--unknown', + 'x', + ], + /unknown flag --unknown/u, + ], + [ + 'action', + [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--action', + 'again', + ], + /only once/u, + ], + [ + 'action', + [scaffoldFlag.vertical, '', '--action', fixtureName.action], + /non-empty value/u, + ], + [ + 'action', + [scaffoldFlag.vertical, '../billing', '--action', fixtureName.action], + /lower-kebab-case/u, + ], + [ + 'action', + [scaffoldFlag.vertical, '/absolute/billing', '--action', fixtureName.action], + /lower-kebab-case/u, + ], + [ + 'action', + [ + scaffoldFlag.vertical, + inventorySlug, + '--scope', + 'core', + '--module', + fixtureName.actionModule, + '--action', + fixtureName.action, + ], + /mutually exclusive/u, + ], + ['action', ['--scope', 'core', '--action', fixtureName.action], /--module is required/u], + [ + 'action', + [ + '--scope', + 'other', + '--module', + fixtureName.actionModule, + '--action', + fixtureName.action, + ], + /--scope core is required/u, + ], + [ + 'action', + ['--scope', 'core', '--module', 'billing.modules', '--action', fixtureName.action], + /stable lowercase core/u, + ], + [ + 'action', + ['--scope', 'core', '--module', 'core.../modules', '--action', fixtureName.action], + /stable lowercase core/u, + ], + [ + 'action', + [scaffoldFlag.vertical, 'missing', '--action', fixtureName.action], + /package metadata is missing/u, + ], + [ + scaffoldCommand.microverticalActionBoundary, + [scaffoldFlag.vertical, inventorySlug, '--unknown', 'x'], + /unknown flag --unknown/u, + ], + [ + scaffoldCommand.microverticalActionBoundary, + [scaffoldFlag.vertical, '../billing'], + /lower-kebab-case/u, + ], + [ + 'policy', + [ + '--scope', + 'global', + '--policy', + fixtureName.policy, + scaffoldFlag.vertical, + inventorySlug, + ], + /forbidden/u, + ], + ['policy', ['--scope', 'microvertical', '--policy', fixtureName.policy], /required/u], + [ + 'policy', + ['--scope', 'other', '--policy', fixtureName.policy], + /global or microvertical/u, + ], + [ + scaffoldCommand.outboxMessage, + [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--topic', + 'Not.Safe', + ], + /dot-separated/u, + ], + [ + scaffoldCommand.outboxWorker, + [ + scaffoldFlag.vertical, + 'billing', + '--worker', + fixtureName.ordersLogger, + scaffoldFlag.producer, + inventorySlug, + '--topic', + '../orders.created', + ], + /dot-separated/u, + ], + ]; + yield* Effect.all( + invalidCalls.map( + Effect.fn(function* scenario27([command, generatorArguments, expected]) { + yield* expectFailure(run(fixture, command, generatorArguments), (error) => + expect(String(error)).toMatch(expected), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); }), - isGeneratedPrincipalError('ActionPrincipalConfigurationError'), ), - ), - ); - await assert.rejects( - verify(currentAssertion.token, { - ...environment, - ONTOS_GATEWAY_ISSUER: 'file:///not-an-http-issuer', + { concurrency: 'unbounded' }, + ); }), - isGeneratedPrincipalError('ActionPrincipalConfigurationError'), - ); - await assert.rejects( - verify( - retiringAssertion.token, - { - ...environment, - ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify({ keys: [current.publicJwk] }), - }, - 1_700_000_000 + GATEWAY_ASSERTION_TTL_SECONDS + GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS + 1, - ), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), ); - const wrongAudience = await issue(current.configuration, 1_700_000_000, 'billing'); - await assert.rejects( - verify(wrongAudience.token), - isGeneratedPrincipalError('ActionPrincipalScopeError'), - ); - const wrongIssuer = await issue( - { ...current.configuration, issuer: 'https://other.example.test' }, - 1_700_000_000, - ); - await assert.rejects( - verify(wrongIssuer.token), - isGeneratedPrincipalError('ActionPrincipalScopeError'), - ); - const unknownKid = await issue( - { - ...current.configuration, - privateJwk: { ...current.configuration.privateJwk, kid: 'unknown' }, - }, - 1_700_000_000, - ); - await assert.rejects( - verify(unknownKid.token), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), - ); - const expired = await issue(current.configuration, 1_699_999_000); - await assert.rejects( - verify(expired.token), - isGeneratedPrincipalError('ActionPrincipalExpiredError'), - ); - const future = await issue(current.configuration, 1_700_000_032); - await assert.rejects( - verify(future.token), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), - ); - const signingKey = await importJWK(current.configuration.privateJwk, 'EdDSA'); - const mismatchedSubject = await new SignJWT({ principal, ver: 1 }) - .setProtectedHeader({ alg: 'EdDSA', kid: 'current', typ: 'JWT' }) - .setIssuer(fixtureGatewayIssuer) - .setAudience(inventorySlug) - .setSubject('70000000-0000-4000-8000-000000000001') - .setIssuedAt(1_700_000_000) - .setExpirationTime(1_700_000_300) - .setJti(fixtureGatewayJti) - .sign(signingKey); - await assert.rejects( - verify(mismatchedSubject), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), - ); - const invalidContext = await new SignJWT({ - principal: { ...principal, principalId: 'not-a-uuid' }, - ver: 1, - }) - .setProtectedHeader({ alg: 'EdDSA', kid: 'current', typ: 'JWT' }) - .setIssuer(fixtureGatewayIssuer) - .setAudience(inventorySlug) - .setSubject('not-a-uuid') - .setIssuedAt(1_700_000_000) - .setExpirationTime(1_700_000_300) - .setJti(fixtureGatewayJti) - .sign(signingKey); - await assert.rejects( - verify(invalidContext), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), - ); - const hmacToken = await new SignJWT({ principal, ver: 1 }) - .setProtectedHeader({ alg: 'HS256', kid: 'current', typ: 'JWT' }) - .setIssuer(fixtureGatewayIssuer) - .setAudience(inventorySlug) - .setSubject(principal.principalId) - .setIssuedAt(1_700_000_000) - .setExpirationTime(1_700_000_300) - .setJti(fixtureGatewayJti) - .sign(await generateSecret('HS256')); - await assert.rejects( - verify(hmacToken), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), - ); - const tokenParts = currentAssertion.token.split('.'); - const encodedPayload = tokenParts[1] ?? ''; - const tampered = `${tokenParts[0]}.${encodedPayload.startsWith('a') ? 'b' : 'a'}${encodedPayload.slice(1)}.${tokenParts[2]}`; - await assert.rejects( - verify(tampered), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), - ); - await assert.rejects( - runEffectTestPromise( - generatedModule.verifyActionPrincipal(undefined, { - currentTimeSeconds: Effect.succeed(1_700_000_001), - environment, - redemption: testRedemption, - }), - ), - isGeneratedPrincipalError('ActionPrincipalMissingError'), - ); - await assert.rejects( - runEffectTestPromise( - generatedModule.verifyActionPrincipal('bearer malformed', { - currentTimeSeconds: Effect.succeed(1_700_000_001), - environment, - redemption: testRedemption, - }), - ), - isGeneratedPrincipalError('ActionPrincipalInvalidError'), + }), +); + +it.live( + 'generates one immutable Action identity boundary and exact direct dependencies', + Effect.fn(function* mergedScenario50() { + yield* withFixture( + Effect.fn(function* mergedScenario49(fixture) { + const shellBefore = yield* readFixtureFile(fixture.root, shellSentinelFile); + const topologyBefore = yield* readFixtureFile(fixture.root, topologyFile); + const result = yield* run(fixture, scaffoldCommand.microverticalActionBoundary, [ + scaffoldFlag.vertical, + inventorySlug, + ]); + expect(result.kind).toBe('generated'); + const server = yield* readFixtureFile(fixture.root, inventoryActionPrincipalFile); + const actionHttpRunner = yield* readFixtureFile( + fixture.root, + inventoryActionHttpRunnerFile, + ); + const client = yield* readFixtureFile(fixture.root, inventoryActionGatewayFile); + const redemption = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/api/auth/gateway-assertion-redemption.ts', + ); + for (const source of [server, client]) { + expect(source).toMatch(/@ontos-action-boundary-owner inventory-stock/u); + expect(source).toMatch(/@ontos-action-boundary-audience inventory-stock/u); + expect(source).toMatch(/ACTION_GATEWAY_AUDIENCE = 'inventory-stock'/u); + } + expect(server).toMatch(/@app\/gateway-principal-verifier\/server/u); + expect(server).toMatch(/bindGatewayPrincipalVerifier\(ACTION_GATEWAY_AUDIENCE\)/u); + expect(server).not.toMatch( + /createLocalJWKSet|decodeProtectedHeader|jwtVerify|PublicVerificationKeySchema/u, + ); + expect(client).toMatch(/makeOperationGateway as makeSharedOperationGateway/u); + expect(client).toMatch(/makeSharedOperationGateway\(ACTION_GATEWAY_AUDIENCE, acquire\)/u); + expect(client).toMatch(/export const operationGateway = makeOperationGateway\(\)/u); + expect(client).not.toMatch( + /ActionGatewayIssuer|ActionGatewayAttempt|makeActionGateway|\bactionGateway\b/u, + ); + expect(client).not.toMatch(/Effect\.flatMap|Bearer \$\{|acquire\(\{ audience/u); + expect(client).not.toMatch( + /api\/auth\/action-principal|gateway-assertion-redemption|GatewayContextProtectedHeader|verticals\//u, + ); + expect(client).not.toMatch(/localStorage|sessionStorage/u); + expect(server).toMatch(/verifyAndRedeem/u); + expect(actionHttpRunner).toMatch(/bindGovernedActionHttp/u); + expect(actionHttpRunner).toMatch(/bindActionHttpRunner/u); + expect(actionHttpRunner).toMatch(/authenticateOperationPrincipal/u); + expect(actionHttpRunner).not.toMatch(/ActionRuntime|ActionCoreError|HttpApiEndpoint/u); + expect(redemption).toMatch(/GatewayAssertionRedemptionUnavailableError/u); + const packageJson = yield* decodeFixturePackage( + yield* readFixtureFile(fixture.root, inventoryPackageFile), + ); + expect(packageJson.dependencies).toEqual({ + '@app/core-runtime': workspaceVersion, + '@app/gateway-principal-verifier': workspaceVersion, + '@app/shared-contracts': workspaceVersion, + effect: '4.0.0-beta.107', + zeta: '1.0.0', + }); + expect(packageJson.scripts['existing']).toBe(preservedFixtureValue); + expect(yield* readFixtureFile(fixture.root, shellSentinelFile)).toBe(shellBefore); + expect(yield* readFixtureFile(fixture.root, topologyFile)).toBe(topologyBefore); + }), ); - await assert.rejects( - runEffectTestPromise( - generatedModule.verifyActionPrincipal(`Bearer ${currentAssertion.token}`, { - currentTimeSeconds: Effect.succeed(1_700_000_001), - environment: {}, - redemption: testRedemption, - }), - ), - isGeneratedPrincipalError('ActionPrincipalConfigurationError'), + }), +); + +it.live( + 'Action identity boundary preflight refuses unsafe writes', + Effect.fn(function* scenario30() { + yield* withFixture( + Effect.fn(function* scenario31(fixture) { + yield* run(fixture, scaffoldCommand.microverticalActionBoundary, [ + scaffoldFlag.vertical, + inventorySlug, + ]); + const afterFirstRun = yield* snapshotTree(fixture.root); + yield* run(fixture, scaffoldCommand.microverticalActionBoundary, [ + scaffoldFlag.vertical, + inventorySlug, + ]); + expect(yield* snapshotTree(fixture.root)).toEqual(afterFirstRun); + }), ); - let acquisitions = 0; - const authorizations: string[] = []; - const idempotencyKey = 'caller-owned-idempotency-key'; - const operationGateway = generatedClientModule.makeOperationGateway(({ audience }) => { - acquisitions += 1; - assert.equal(audience, inventorySlug); - return Effect.succeed({ token: `attempt-${acquisitions}` }); - }); - const attempt = (authorization: string) => { - authorizations.push(authorization); - return Effect.succeed(idempotencyKey); - }; - assert.equal(await runEffectTestPromise(operationGateway.invoke(attempt)), idempotencyKey); - assert.equal(await runEffectTestPromise(operationGateway.invoke(attempt)), idempotencyKey); - assert.deepEqual(authorizations, ['Bearer attempt-1', 'Bearer attempt-2']); - - const actionApi = HttpApi.make('generatedActionIdentityFixture').add( - HttpApiGroup.make('action').add( - HttpApiEndpoint.post('invoke', '/actions/invoke', { - error: [ActionAuthenticationProblemSchema, ActionVerificationUnavailableProblemSchema], - success: TrustedPrincipalContextSchema, - }), - ), + yield* withFixture( + Effect.fn(function* scenario32(fixture) { + yield* write( + fixture.root, + inventoryActionPrincipalFile, + `// Owner-authored identity adapter +export const ownerCode = true; +`, + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalActionBoundary, + [scaffoldFlag.vertical, inventorySlug], + /refusing to overwrite existing business file/u, + ); + }), ); - let actionReached = false; - let endpointEnvironment: GeneratedPrincipalEnvironment = environment; - const markActionReached = Effect.sync(() => { - actionReached = true; - }); - const actionGroupLive = HttpApiBuilder.group(actionApi, 'action', (handlers) => - handlers.handle('invoke', ({ request }) => - generatedModule - .verifyActionPrincipal(request.headers['authorization'], { - currentTimeSeconds: Effect.succeed(1_700_000_001), - environment: endpointEnvironment, - redemption: testRedemption, - }) - .pipe(Effect.tap(markActionReached), Effect.catchTags(generatedPrincipalErrorHandlers)), - ), + }), +); + +it.live( + 'governed generators reject legacy principal boundaries before writing files', + Effect.fn(function* mergedScenario57() { + yield* withFixture( + Effect.fn(function* mergedScenario56(fixture) { + yield* addInventoryItemResourceType(fixture); + yield* run(fixture, scaffoldCommand.microverticalActionBoundary, [ + scaffoldFlag.vertical, + inventorySlug, + ]); + const generated = yield* readFixtureFile(fixture.root, inventoryActionPrincipalFile); + const legacy = generated.replace( + /const verifyOperationPrincipal =[\s\S]*$/u, + 'export const verifyOperationPrincipal = verifyActionPrincipal;\n', + ); + expect(legacy).not.toMatch(/export const authenticateOperationPrincipal/u); + yield* write(fixture.root, inventoryActionPrincipalFile, legacy); + const before = yield* snapshotTree(fixture.root); + const calls: readonly [ScaffoldCommand, readonly string[]][] = [ + [scaffoldCommand.microverticalActionBoundary, []], + [scaffoldCommand.moduleApi, ['--name', fixtureName.resourceDetail]], + [ + scaffoldCommand.searchProvider, + ['--name', fixtureName.inventoryItems, scaffoldFlag.resource, 'item'], + ], + ['report', ['--name', fixtureName.stockLevels, scaffoldFlag.resource, 'item']], + ]; + yield* Effect.all( + calls.map( + Effect.fn(function* mergedScenario55([command, args]) { + yield* expectFailure( + run(fixture, command, [scaffoldFlag.vertical, inventorySlug, ...args]), + (error) => + expect(String(error)).toMatch( + /incompatible generated Action boundary:.*export authenticateOperationPrincipal.*provide ActionPrincipalVerifierLive|refusing to overwrite existing business file: operation boundary/u, + ), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ), + { concurrency: 'unbounded' }, + ); + }), ); - const actionRuntime = defineEffectBff({ - api: actionApi, - layer: HttpApiBuilder.layer(actionApi).pipe(Layer.provide(actionGroupLive)), - }); - const actionHandler = actionRuntime.createHandler(); - try { - const missingResponse = await actionHandler.handler( - new Request(actionInvokeUrl, { method: 'POST' }), - ); - assert.equal(missingResponse.status, 401); - assert.equal(missingResponse.headers.get('www-authenticate'), 'Bearer'); - assert.match( - missingResponse.headers.get('content-type') ?? '', - /application\/problem\+json/u, - ); - assert.equal(actionReached, false); + }), +); - endpointEnvironment = {}; - const unavailableResponse = await actionHandler.handler( - new Request(actionInvokeUrl, { - headers: { authorization: `Bearer ${currentAssertion.token}` }, - method: 'POST', - }), - ); - assert.equal(unavailableResponse.status, 503); - assert.equal( - Schema.decodeUnknownSync(RetryableProblemSchema)(await unavailableResponse.json()) - .retryable, - true, - ); - assert.equal(actionReached, false); +it.live( + 'governed generation preserves compatible owner principal adaptations', + Effect.fn(function* preserveOwnerPrincipalAdaptations() { + yield* withFixture( + Effect.fn(function* preserveOwnerPrincipalFixture(fixture) { + yield* run(fixture, scaffoldCommand.microverticalActionBoundary, [ + scaffoldFlag.vertical, + inventorySlug, + ]); + const adapted = `${yield* readFixtureFile(fixture.root, inventoryActionPrincipalFile)}\n// Owner-specific diagnostics remain private to this adapter.\n`; + yield* write(fixture.root, inventoryActionPrincipalFile, adapted); + yield* run(fixture, scaffoldCommand.microverticalActionBoundary, [ + scaffoldFlag.vertical, + inventorySlug, + ]); + yield* run(fixture, scaffoldCommand.moduleApi, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.resourceDetail, + ]); + expect(yield* readFixtureFile(fixture.root, inventoryActionPrincipalFile)).toBe(adapted); + }), + ); + }), +); - endpointEnvironment = environment; - const successResponse = await actionHandler.handler( - new Request(actionInvokeUrl, { - headers: { authorization: `Bearer ${currentAssertion.token}` }, - method: 'POST', - }), - ); - assert.equal(successResponse.status, 200); - assert.deepEqual(await successResponse.json(), principal); - assert.equal(actionReached, true); - } finally { - await actionHandler.dispose(); - } +it.live( + 'generated verifier executes real Shell assertions and overlapping Ed25519 rotation', + Effect.fn(function* mergedScenario68() { + yield* withFixture( + Effect.fn(function* mergedScenario67(fixture) { + yield* run(fixture, scaffoldCommand.microverticalActionBoundary, [ + scaffoldFlag.vertical, + inventorySlug, + ]); + yield* run(fixture, scaffoldCommand.microverticalActionBoundary, [ + scaffoldFlag.vertical, + 'billing', + ]); + yield* Effect.promise(() => + mkdir(path.join(fixture.root, 'node_modules', '@app'), { recursive: true }), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime'), + path.join(fixture.root, 'node_modules/@app/core-runtime'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, sharedContractsPackagePath), + path.join(fixture.root, sharedContractsNodeModulePath), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/gateway-principal-verifier'), + path.join(fixture.root, 'node_modules/@app/gateway-principal-verifier'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/node_modules/effect'), + path.join(fixture.root, effectNodeModulePath), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'apps/shell-super-app/node_modules/jose'), + path.join(fixture.root, 'node_modules/jose'), + 'dir', + ), + ); + const edgeBundleDirectory = path.join(fixture.root, 'edge-bundle'); + yield* Effect.promise(() => mkdir(edgeBundleDirectory, { recursive: true })); + const edgeMetafile = path.join(edgeBundleDirectory, 'meta.json'); + const edgeBundle = spawnSync( + esbuildPath, + [ + path.join(fixture.root, inventoryActionPrincipalFile), + '--bundle', + '--format=esm', + `--metafile=${edgeMetafile}`, + `--outfile=${path.join(edgeBundleDirectory, 'action-principal.mjs')}`, + '--platform=browser', + ], + { encoding: 'utf-8' }, + ); + const edgeBundleErrorMessage = edgeBundle.error?.message; + let edgeBundleFailureMessage = 'edge bundle command did not start'; + if (edgeBundle.stderr.length > 0) { + edgeBundleFailureMessage = edgeBundle.stderr; + } else if (edgeBundleErrorMessage !== undefined && edgeBundleErrorMessage.length > 0) { + edgeBundleFailureMessage = edgeBundleErrorMessage; + } + expect(edgeBundle.status, edgeBundleFailureMessage).toBe(0); + const edgeInputs = Object.keys( + (yield* Schema.decodeUnknownEffect(EsbuildMetafileSchema)( + JSON.parse(yield* Effect.promise(() => readFile(edgeMetafile, 'utf-8'))), + )).inputs, + ).join('\n'); + expect(edgeInputs).toMatch(/core-runtime\/src\/auth\/gateway-assertion-redemption\.ts/u); + expect(edgeInputs).not.toMatch(/core-runtime\/src\/db|node:(?:crypto|path)|\/pg\//u); + const generatedModule = yield* Schema.decodeUnknownEffect(GeneratedPrincipalModuleSchema)( + yield* Effect.promise( + () => import(pathToFileURL(path.join(fixture.root, inventoryActionPrincipalFile)).href), + ), + ); + const billingGeneratedModule = yield* Schema.decodeUnknownEffect( + GeneratedPrincipalModuleSchema, + )( + yield* Effect.promise( + () => + import( + pathToFileURL( + path.join(fixture.root, 'verticals/billing/api/auth/action-principal.ts'), + ).href + ), + ), + ); + const generatedClientModule = yield* Schema.decodeUnknownEffect( + GeneratedOperationGatewayModuleSchema, + )( + yield* Effect.promise( + () => import(pathToFileURL(path.join(fixture.root, inventoryActionGatewayFile)).href), + ), + ); + const generatedActionHttpRunnerModule = yield* Schema.decodeUnknownEffect( + GeneratedActionHttpRunnerModuleSchema, + )( + yield* Effect.promise( + () => + import(pathToFileURL(path.join(fixture.root, inventoryActionHttpRunnerFile)).href), + ), + ); + const current = yield* makeGatewayKey('current'); + const retiring = yield* makeGatewayKey('retiring'); + const principal = { + authBindingId: '30000000-0000-4000-8000-000000000001', + authContextRef: 'better-auth-session:scaffold-test', + authMethod: 'session' as const, + principalId: '40000000-0000-4000-8000-000000000001', + tenantId: '50000000-0000-4000-8000-000000000001', + }; + const issue = Effect.fn(function* mergedScenario63( + configuration: GatewayIssuerConfigValue, + issuedAt: number, + audience: string = inventorySlug, + ) { + return yield* issueGatewayContextAssertion({ audience, principal }).pipe( + Effect.provide( + makeGatewayIssuerLayer({ + currentTimeSeconds: Effect.succeed(issuedAt), + generateJti: Effect.succeed(fixtureGatewayJti), + loadAudiences: Effect.succeed(new Set([audience])), + loadConfig: Effect.succeed(configuration), + }), + ), + ); + }); + const environment = { + ONTOS_GATEWAY_ISSUER: fixtureGatewayIssuer, + ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify({ + keys: [current.publicJwk, retiring.publicJwk], + }), + }; + const currentAssertion = yield* issue(current.configuration, 1_700_000_000); + const billingAssertion = yield* issue(current.configuration, 1_700_000_000, 'billing'); + const retiringAssertion = yield* issue(retiring.configuration, 1_700_000_000); + const testRedemption = { consume: () => Effect.void }; + const verify = ( + token: string, + override: GeneratedPrincipalEnvironment = environment, + now = 1_700_000_001, + ) => + generatedModule.verifyActionPrincipal(`Bearer ${token}`, { + currentTimeSeconds: Effect.succeed(now), + environment: override, + redemption: testRedemption, + }); - const generatedBindingApi = HttpApi.make('generatedActionRunnerFixture').add( - HttpApiGroup.make('action').add( - HttpApiEndpoint.post('invoke', '/actions/generated-runner', { - error: [ActionAuthenticationProblemSchema, ActionVerificationUnavailableProblemSchema], - success: GeneratedBindingResultSchema, - }), - ), - ); - const runGeneratedActionHttp = generatedActionHttpRunnerModule.bindActionHttpRunner({ - authentication: actionAuthenticationProblem, - unavailable: actionVerificationUnavailableProblem, - }); - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - tenantPermission: 'allowed', - }); - const generatedBindingGroupLive = HttpApiBuilder.group( - generatedBindingApi, - 'action', - (handlers) => - handlers.handle('invoke', ({ request }) => - runGeneratedActionHttp({ - endpointHeaders: { - idempotencyKey: request.headers['idempotency-key'], - traceId: 'generated-trace', - }, - internalProblem: actionVerificationUnavailableProblem, - invalidCorrelationProblem: actionAuthenticationProblem, - mapError: actionVerificationUnavailableProblem, - payload: {}, - registration: generatedBindingAction, - requestHeaders: { - authorization: Redacted.make(request.headers['authorization']), - 'x-correlation-id': request.headers['x-correlation-id'], + expect(yield* verify(currentAssertion.token)).toEqual(principal); + expect( + yield* billingGeneratedModule.verifyActionPrincipal(`Bearer ${billingAssertion.token}`, { + currentTimeSeconds: Effect.succeed(1_700_000_001), + environment, + redemption: testRedemption, + }), + ).toEqual(principal); + yield* expectFailure( + generatedModule.verifyActionPrincipal(`Bearer ${billingAssertion.token}`, { + currentTimeSeconds: Effect.succeed(1_700_000_001), + environment, + redemption: testRedemption, + }), + (error) => + expect(isGeneratedPrincipalError('ActionPrincipalScopeError')(error)).toBe(true), + ); + yield* expectFailure( + billingGeneratedModule.verifyActionPrincipal(`Bearer ${currentAssertion.token}`, { + currentTimeSeconds: Effect.succeed(1_700_000_001), + environment, + redemption: testRedemption, + }), + (error) => + expect(isGeneratedPrincipalError('ActionPrincipalScopeError')(error)).toBe(true), + ); + expect(yield* verify(retiringAssertion.token)).toEqual(principal); + yield* expectFailure(verify('not-a-jwt'), (error) => + expect(isGeneratedPrincipalError('ActionPrincipalInvalidError')(error)).toBe(true), + ); + yield* Effect.all( + [ + { keys: [] }, + { keys: [current.publicJwk, current.publicJwk] }, + { keys: [{ ...current.publicJwk, d: 'private-material' }] }, + { keys: [{ ...current.publicJwk, key_ops: ['sign'] }] }, + { keys: [{ ...current.publicJwk, alg: 'HS256' }] }, + { keys: [{ ...current.publicJwk, x: '' }] }, + ].map( + Effect.fn(function* mergedScenario66(jwks) { + return yield* expectFailure( + verify(currentAssertion.token, { + ...environment, + ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify(jwks), + }), + (error) => + expect( + isGeneratedPrincipalError('ActionPrincipalConfigurationError')(error), + ).toBe(true), + ); + }), + ), + { concurrency: 'unbounded' }, + ); + yield* expectFailure( + verify(currentAssertion.token, { + ...environment, + ONTOS_GATEWAY_ISSUER: 'file:///not-an-http-issuer', + }), + (error) => + expect(isGeneratedPrincipalError('ActionPrincipalConfigurationError')(error)).toBe( + true, + ), + ); + yield* expectFailure( + verify( + retiringAssertion.token, + { + ...environment, + ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify({ keys: [current.publicJwk] }), }, + 1_700_000_000 + + GATEWAY_ASSERTION_TTL_SECONDS + + GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS + + 1, + ), + (error) => + expect(isGeneratedPrincipalError('ActionPrincipalInvalidError')(error)).toBe(true), + ); + const wrongAudience = yield* issue(current.configuration, 1_700_000_000, 'billing'); + yield* expectFailure(verify(wrongAudience.token), (error) => + expect(isGeneratedPrincipalError('ActionPrincipalScopeError')(error)).toBe(true), + ); + const wrongIssuer = yield* issue( + { ...current.configuration, issuer: 'https://other.example.test' }, + 1_700_000_000, + ); + yield* expectFailure(verify(wrongIssuer.token), (error) => + expect(isGeneratedPrincipalError('ActionPrincipalScopeError')(error)).toBe(true), + ); + const unknownKid = yield* issue( + { + ...current.configuration, + privateJwk: { ...current.configuration.privateJwk, kid: 'unknown' }, + }, + 1_700_000_000, + ); + yield* expectFailure(verify(unknownKid.token), (error) => + expect(isGeneratedPrincipalError('ActionPrincipalInvalidError')(error)).toBe(true), + ); + const expired = yield* issue(current.configuration, 1_699_999_000); + yield* expectFailure(verify(expired.token), (error) => + expect(isGeneratedPrincipalError('ActionPrincipalExpiredError')(error)).toBe(true), + ); + const future = yield* issue(current.configuration, 1_700_000_032); + yield* expectFailure(verify(future.token), (error) => + expect(isGeneratedPrincipalError('ActionPrincipalInvalidError')(error)).toBe(true), + ); + const signingKey = yield* Effect.promise(() => + importJWK(current.configuration.privateJwk, 'EdDSA'), + ); + const mismatchedSubject = yield* Effect.promise(() => + new SignJWT({ principal, ver: 1 }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'current', typ: 'JWT' }) + .setIssuer(fixtureGatewayIssuer) + .setAudience(inventorySlug) + .setSubject('70000000-0000-4000-8000-000000000001') + .setIssuedAt(1_700_000_000) + .setExpirationTime(1_700_000_300) + .setJti(fixtureGatewayJti) + .sign(signingKey), + ); + yield* expectFailure(verify(mismatchedSubject), (error) => + expect(isGeneratedPrincipalError('ActionPrincipalInvalidError')(error)).toBe(true), + ); + const invalidContext = yield* Effect.promise(() => + new SignJWT({ + principal: { ...principal, principalId: 'not-a-uuid' }, + ver: 1, + }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'current', typ: 'JWT' }) + .setIssuer(fixtureGatewayIssuer) + .setAudience(inventorySlug) + .setSubject('not-a-uuid') + .setIssuedAt(1_700_000_000) + .setExpirationTime(1_700_000_300) + .setJti(fixtureGatewayJti) + .sign(signingKey), + ); + yield* expectFailure(verify(invalidContext), (error) => + expect(isGeneratedPrincipalError('ActionPrincipalInvalidError')(error)).toBe(true), + ); + const hmacSecret = yield* Effect.promise(() => generateSecret('HS256')); + const hmacToken = yield* Effect.promise(() => + new SignJWT({ principal, ver: 1 }) + .setProtectedHeader({ alg: 'HS256', kid: 'current', typ: 'JWT' }) + .setIssuer(fixtureGatewayIssuer) + .setAudience(inventorySlug) + .setSubject(principal.principalId) + .setIssuedAt(1_700_000_000) + .setExpirationTime(1_700_000_300) + .setJti(fixtureGatewayJti) + .sign(hmacSecret), + ); + yield* expectFailure(verify(hmacToken), (error) => + expect(isGeneratedPrincipalError('ActionPrincipalInvalidError')(error)).toBe(true), + ); + const tokenParts = currentAssertion.token.split('.'); + const encodedPayload = tokenParts[1] ?? ''; + const tampered = `${tokenParts[0]}.${encodedPayload.startsWith('a') ? 'b' : 'a'}${encodedPayload.slice(1)}.${tokenParts[2]}`; + yield* expectFailure(verify(tampered), (error) => + expect(isGeneratedPrincipalError('ActionPrincipalInvalidError')(error)).toBe(true), + ); + yield* expectFailure( + generatedModule.verifyActionPrincipal(undefined, { + currentTimeSeconds: Effect.succeed(1_700_000_001), + environment, + redemption: testRedemption, }), - ), - ).pipe( - Layer.provide(generatedModule.ActionPrincipalVerifierLive), - Layer.provide(Layer.succeed(GatewayAssertionRedemptionService, testRedemption)), - Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(environment))), - Layer.provide(harness.layer), + (error) => + expect(isGeneratedPrincipalError('ActionPrincipalMissingError')(error)).toBe(true), + ); + yield* expectFailure( + generatedModule.verifyActionPrincipal('bearer malformed', { + currentTimeSeconds: Effect.succeed(1_700_000_001), + environment, + redemption: testRedemption, + }), + (error) => + expect(isGeneratedPrincipalError('ActionPrincipalInvalidError')(error)).toBe(true), + ); + yield* expectFailure( + generatedModule.verifyActionPrincipal(`Bearer ${currentAssertion.token}`, { + currentTimeSeconds: Effect.succeed(1_700_000_001), + environment: {}, + redemption: testRedemption, + }), + (error) => + expect(isGeneratedPrincipalError('ActionPrincipalConfigurationError')(error)).toBe( + true, + ), + ); + let acquisitions = 0; + const authorizations: string[] = []; + const idempotencyKey = 'caller-owned-idempotency-key'; + const operationGateway = generatedClientModule.makeOperationGateway(({ audience }) => { + acquisitions += 1; + expect(audience).toBe(inventorySlug); + return Effect.succeed({ token: `attempt-${acquisitions}` }); + }); + const attempt = (authorization: string) => { + authorizations.push(authorization); + return Effect.succeed(idempotencyKey); + }; + expect(yield* operationGateway.invoke(attempt)).toBe(idempotencyKey); + expect(yield* operationGateway.invoke(attempt)).toBe(idempotencyKey); + expect(authorizations).toEqual(['Bearer attempt-1', 'Bearer attempt-2']); + + const actionApi = HttpApi.make('generatedActionIdentityFixture').add( + HttpApiGroup.make('action').add( + HttpApiEndpoint.post('invoke', '/actions/invoke', { + error: [ + ActionAuthenticationProblemSchema, + ActionVerificationUnavailableProblemSchema, + ], + success: TrustedPrincipalContextSchema, + }), + ), + ); + let actionReached = false; + let endpointEnvironment: GeneratedPrincipalEnvironment = environment; + const markActionReached = Effect.sync(() => { + actionReached = true; + }); + const actionGroupLive = HttpApiBuilder.group(actionApi, 'action', (handlers) => + handlers.handle('invoke', ({ request }) => + generatedModule + .verifyActionPrincipal(request.headers['authorization'], { + currentTimeSeconds: Effect.succeed(1_700_000_001), + environment: endpointEnvironment, + redemption: testRedemption, + }) + .pipe( + Effect.tap(markActionReached), + Effect.catchTags(generatedPrincipalErrorHandlers), + ), + ), + ); + const actionRuntime = defineEffectBff({ + api: actionApi, + layer: HttpApiBuilder.layer(actionApi).pipe(Layer.provide(actionGroupLive)), + }); + const actionHandler = actionRuntime.createHandler(); + yield* Effect.addFinalizer(() => Effect.promise(() => actionHandler.dispose())); + const missingResponse = yield* Effect.promise(() => + actionHandler.handler(new Request(actionInvokeUrl, { method: 'POST' })), + ); + expect(missingResponse.status).toBe(401); + expect(missingResponse.headers.get('www-authenticate')).toBe('Bearer'); + expect(missingResponse.headers.get('content-type') ?? '').toMatch( + /application\/problem\+json/u, + ); + expect(actionReached).toBe(false); + endpointEnvironment = {}; + const unavailableResponse = yield* Effect.promise(() => + actionHandler.handler( + new Request(actionInvokeUrl, { + headers: { authorization: `Bearer ${currentAssertion.token}` }, + method: 'POST', + }), + ), + ); + expect(unavailableResponse.status).toBe(503); + expect( + (yield* Schema.decodeUnknownEffect(RetryableProblemSchema)( + yield* Effect.promise(() => unavailableResponse.json()), + )).retryable, + ).toBe(true); + expect(actionReached).toBe(false); + endpointEnvironment = environment; + const successResponse = yield* Effect.promise(() => + actionHandler.handler( + new Request(actionInvokeUrl, { + headers: { authorization: `Bearer ${currentAssertion.token}` }, + method: 'POST', + }), + ), + ); + expect(successResponse.status).toBe(200); + expect(yield* Effect.promise(() => successResponse.json())).toEqual(principal); + expect(actionReached).toBe(true); + + const generatedBindingApi = HttpApi.make('generatedActionRunnerFixture').add( + HttpApiGroup.make('action').add( + HttpApiEndpoint.post('invoke', '/actions/generated-runner', { + error: [ + ActionAuthenticationProblemSchema, + ActionVerificationUnavailableProblemSchema, + ], + success: GeneratedBindingResultSchema, + }), + ), + ); + const runGeneratedActionHttp = generatedActionHttpRunnerModule.bindActionHttpRunner({ + authentication: actionAuthenticationProblem, + unavailable: actionVerificationUnavailableProblem, + }); + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'allowed', + }); + const generatedBindingGroupLive = HttpApiBuilder.group( + generatedBindingApi, + 'action', + (handlers) => + handlers.handle('invoke', ({ request }) => + runGeneratedActionHttp({ + endpointHeaders: { + idempotencyKey: request.headers['idempotency-key'], + traceId: 'generated-trace', + }, + internalProblem: actionVerificationUnavailableProblem, + invalidCorrelationProblem: actionAuthenticationProblem, + mapError: actionVerificationUnavailableProblem, + payload: {}, + registration: generatedBindingAction, + requestHeaders: { + authorization: Redacted.make(request.headers['authorization']), + 'x-correlation-id': request.headers['x-correlation-id'], + }, + }), + ), + ).pipe( + Layer.provide(generatedModule.ActionPrincipalVerifierLive), + Layer.provide(Layer.succeed(GatewayAssertionRedemptionService, testRedemption)), + Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(environment))), + Layer.provide(harness.layer), + ); + const generatedBindingRuntime = defineEffectBff({ + api: generatedBindingApi, + layer: HttpApiBuilder.layer(generatedBindingApi).pipe( + Layer.provide(generatedBindingGroupLive), + Layer.provideMerge(harness.layer), + ), + }); + const generatedBindingHandler = generatedBindingRuntime.createHandler(); + yield* Effect.addFinalizer(() => Effect.promise(() => generatedBindingHandler.dispose())); + const liveIssuedAt = Math.floor((yield* Clock.currentTimeMillis) / 1000); + const liveAssertion = yield* issue(current.configuration, liveIssuedAt); + const generatedBindingResponse = yield* Effect.promise(() => + generatedBindingHandler.handler( + new Request('https://inventory.example.test/actions/generated-runner', { + headers: { + authorization: `Bearer ${liveAssertion.token}`, + 'x-correlation-id': 'generated-runner-correlation', + }, + method: 'POST', + }), + ), + ); + const generatedBindingBody = yield* Schema.decodeUnknownEffect( + GeneratedBindingResultSchema, + )(yield* Effect.promise(() => generatedBindingResponse.json())); + expect( + generatedBindingResponse.status, + JSON.stringify({ body: generatedBindingBody, snapshot: harness.snapshot() }), + ).toBe(200); + expect(generatedBindingBody).toEqual({ accepted: true }); + expect(harness.snapshot().invocations.length).toBe(1); + expect(harness.snapshot().transactionCount).toBe(1); + }), ); - const generatedBindingRuntime = defineEffectBff({ - api: generatedBindingApi, - layer: HttpApiBuilder.layer(generatedBindingApi).pipe( - Layer.provide(generatedBindingGroupLive), - Layer.provideMerge(harness.layer), - ), - }); - const generatedBindingHandler = generatedBindingRuntime.createHandler(); - try { - const liveIssuedAt = Math.floor((await runEffectTestPromise(Clock.currentTimeMillis)) / 1000); - const liveAssertion = await issue(current.configuration, liveIssuedAt); - const generatedBindingResponse = await generatedBindingHandler.handler( - new Request('https://inventory.example.test/actions/generated-runner', { - headers: { - authorization: `Bearer ${liveAssertion.token}`, - 'x-correlation-id': 'generated-runner-correlation', - }, - method: 'POST', - }), - ); - const generatedBindingBody = Schema.decodeUnknownSync(GeneratedBindingResultSchema)( - await generatedBindingResponse.json(), - ); - assert.equal( - generatedBindingResponse.status, - 200, - JSON.stringify({ body: generatedBindingBody, snapshot: harness.snapshot() }), - ); - assert.deepEqual(generatedBindingBody, { accepted: true }); - assert.equal(harness.snapshot().invocations.length, 1); - assert.equal(harness.snapshot().transactionCount, 1); - } finally { - await generatedBindingHandler.dispose(); - } - }); -}); + }), +); -test('generates one self-contained typed fail-closed Action and preserves package metadata', async () => { - await withFixture(async (fixture) => { - await run(fixture, 'action', [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - 'create-order2', - ]); - const action = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/actions/create-order2.action.ts', - ); - assert.equal( - action, - `// @generated by OntOS Codesmith Action v1 +it.live( + 'generates one self-contained typed fail-closed Action and preserves package metadata', + Effect.fn(function* scenario38() { + yield* withFixture( + Effect.fn(function* scenario39(fixture) { + yield* run(fixture, 'action', [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + 'create-order2', + ]); + const action = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/actions/create-order2.action.ts', + ); + expect(action).toBe(`// @generated by OntOS Codesmith Action v1 // @ontos-action-owner inventory.stock // @ontos-action-slug create-order2 import { Effect, Schema } from 'effect'; @@ -3166,85 +3399,89 @@ export const createOrder2Action = defineAction( // // -`, - ); - const packageJson = decodeFixturePackage( - await readFixtureFile(fixture.root, inventoryPackageFile), - ); - assert.deepEqual(packageJson.dependencies, { - '@app/core-runtime': workspaceVersion, - zeta: '1.0.0', - }); - assert.equal(packageJson.scripts['existing'], preservedFixtureValue); - await assertScaffoldRefused( - fixture, - 'action', - [scaffoldFlag.vertical, inventorySlug, '--action', 'create-order2'], - /refusing to overwrite/u, - ); - }); -}); - -test('generates an owner-local Action service without overwriting business logic', async () => { - await withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.actionService, [ - scaffoldFlag.vertical, - inventorySlug, - '--service', - 'inventory-persistence', - ]); - const service = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/services/inventory-persistence.service.ts', - ); - assert.equal( - service, - `// @generated by OntOS Codesmith Action Service v1 -import { Effect } from 'effect'; - -export const inventoryPersistenceService = () => Effect.succeed({}); -`, - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.actionService, - [scaffoldFlag.vertical, inventorySlug, '--service', 'inventory-persistence'], - /refusing to overwrite/u, +`); + const packageJson = yield* decodeFixturePackage( + yield* readFixtureFile(fixture.root, inventoryPackageFile), + ); + expect(packageJson.dependencies).toEqual({ + '@app/core-runtime': workspaceVersion, + zeta: '1.0.0', + }); + expect(packageJson.scripts['existing']).toBe(preservedFixtureValue); + yield* assertScaffoldRefused( + fixture, + 'action', + [scaffoldFlag.vertical, inventorySlug, '--action', 'create-order2'], + /refusing to overwrite/u, + ); + }), ); - }); -}); + }), +); -test('generates exactly one private owner-local external HTTP adapter', async () => { - await withFixture(async (fixture) => { - const before = await snapshotTree(fixture.root); - const result = await run(fixture, scaffoldCommand.externalHttpAdapter, [ - scaffoldFlag.vertical, - 'contacts', - scaffoldFlag.provider, - 'ares', - scaffoldFlag.operation, - 'subject', - ]); - const adapterPath = path.join( - fixture.root, - 'verticals/contacts/src/integrations/ares/ares-subject.service.ts', - ); - assert.deepEqual(result, { - kind: 'generated', - result: { adapterPath }, - }); - const after = await snapshotTree(fixture.root); - const changedPaths = new Set([ - ...Object.keys(before).filter((file) => before[file] !== after[file]), - ...Object.keys(after).filter((file) => before[file] !== after[file]), - ]); - assert.deepEqual( - [...changedPaths], - ['verticals/contacts/src/integrations/ares/ares-subject.service.ts'], +it.live( + 'generates an owner-local Action service without overwriting business logic', + Effect.fn(function* scenario40() { + yield* withFixture( + Effect.fn(function* scenario41(fixture) { + yield* run(fixture, scaffoldCommand.actionService, [ + scaffoldFlag.vertical, + inventorySlug, + '--service', + 'inventory-persistence', + ]); + const service = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/services/inventory-persistence.service.ts', + ); + expect(service).toBe(`// @generated by OntOS Codesmith Action Service v1 +import { Effect } from 'effect'; + +export const inventoryPersistenceService = () => Effect.succeed({}); +`); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.actionService, + [scaffoldFlag.vertical, inventorySlug, '--service', 'inventory-persistence'], + /refusing to overwrite/u, + ); + }), ); - assert.equal( - after['verticals/contacts/src/integrations/ares/ares-subject.service.ts'], - `// @generated by OntOS Codesmith External HTTP Adapter v1 + }), +); + +it.live( + 'generates exactly one private owner-local external HTTP adapter', + Effect.fn(function* scenario42() { + yield* withFixture( + Effect.fn(function* scenario43(fixture) { + const before = yield* snapshotTree(fixture.root); + const result = yield* run(fixture, scaffoldCommand.externalHttpAdapter, [ + scaffoldFlag.vertical, + 'contacts', + scaffoldFlag.provider, + 'ares', + scaffoldFlag.operation, + 'subject', + ]); + const adapterPath = path.join( + fixture.root, + 'verticals/contacts/src/integrations/ares/ares-subject.service.ts', + ); + expect(result).toEqual({ + kind: 'generated', + result: { adapterPath }, + }); + const after = yield* snapshotTree(fixture.root); + const changedPaths = new Set([ + ...Object.keys(before).filter((file) => before[file] !== after[file]), + ...Object.keys(after).filter((file) => before[file] !== after[file]), + ]); + expect([...changedPaths]).toEqual([ + 'verticals/contacts/src/integrations/ares/ares-subject.service.ts', + ]); + expect(after['verticals/contacts/src/integrations/ares/ares-subject.service.ts']) + .toBe(`// @generated by OntOS Codesmith External HTTP Adapter v1 import { Context, Effect, Layer, Schema } from 'effect'; import { HttpClient } from 'effect/unstable/http'; @@ -3281,480 +3518,549 @@ const makeAresSubjectService = Effect.gen(function* () { }); export const AresSubjectServiceLive = Layer.effect(AresSubjectService, makeAresSubjectService); -`, - ); - const source = after['verticals/contacts/src/integrations/ares/ares-subject.service.ts'] ?? ''; - assert.match(source, /HttpClient\.HttpClient/u); - assert.match(source, /Layer\.effect/u); - assert.doesNotMatch( - source, - /fetch\(|httpClient\.(?:execute|get|head|post|patch|put|del|options)\(|https?:\/\//u, - ); +`); + const source = + after['verticals/contacts/src/integrations/ares/ares-subject.service.ts'] ?? ''; + expect(source).toMatch(/HttpClient\.HttpClient/u); + expect(source).toMatch(/Layer\.effect/u); + expect(source).not.toMatch( + /fetch\(|httpClient\.(?:execute|get|head|post|patch|put|del|options)\(|https?:\/\//u, + ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.externalHttpAdapter, - [ - scaffoldFlag.vertical, - 'contacts', - scaffoldFlag.provider, - 'ares', - scaffoldFlag.operation, - 'subject', - ], - /refusing to overwrite/u, + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.externalHttpAdapter, + [ + scaffoldFlag.vertical, + 'contacts', + scaffoldFlag.provider, + 'ares', + scaffoldFlag.operation, + 'subject', + ], + /refusing to overwrite/u, + ); + }), ); - }); -}); - -test('rejects unsafe external HTTP adapter command input without writing', async () => { - await withFixture(async (fixture) => { - const before = await snapshotTree(fixture.root); - const invalidCalls: readonly [readonly string[], RegExp][] = [ - [ - [scaffoldFlag.vertical, 'contacts', scaffoldFlag.operation, 'subject'], - /missing required flag --provider/u, - ], - [ - [scaffoldFlag.vertical, 'contacts', scaffoldFlag.provider, 'ares'], - /missing required flag --operation/u, - ], - [ - [scaffoldFlag.provider, 'ares', scaffoldFlag.operation, 'subject'], - /missing required flag --vertical/u, - ], - [ - [ - scaffoldFlag.vertical, - 'contacts', - scaffoldFlag.provider, - 'ares', - scaffoldFlag.operation, - 'subject', - '--unknown', - 'x', - ], - /unknown flag --unknown/u, - ], - [ - [ - scaffoldFlag.vertical, - 'contacts', - scaffoldFlag.provider, - 'ares', - scaffoldFlag.provider, - 'other', - scaffoldFlag.operation, - 'subject', - ], - /only once/u, - ], - [ - [ - scaffoldFlag.vertical, - 'contacts', - scaffoldFlag.provider, - 'Ares', - scaffoldFlag.operation, - 'subject', - ], - /provider must be canonical lower-kebab-case/u, - ], - [ - [ - scaffoldFlag.vertical, - 'contacts', - scaffoldFlag.provider, - 'ares', - scaffoldFlag.operation, - 'Subject', - ], - /operation must be canonical lower-kebab-case/u, - ], - [ - [ - scaffoldFlag.vertical, - 'contacts', - scaffoldFlag.provider, - 'src', - scaffoldFlag.operation, - 'subject', - ], - /provider must be canonical lower-kebab-case/u, - ], - [ - [ - scaffoldFlag.vertical, - 'contacts', - scaffoldFlag.provider, - 'ares', - scaffoldFlag.operation, - 'node_modules', - ], - /operation must be canonical lower-kebab-case/u, - ], - [ - [ - scaffoldFlag.vertical, - 'contacts', - scaffoldFlag.provider, - '../ares', - scaffoldFlag.operation, - 'subject', - ], - /provider must be canonical lower-kebab-case/u, - ], - [ - [ - scaffoldFlag.vertical, - 'contacts', - scaffoldFlag.provider, - 'ares', - scaffoldFlag.operation, - '../subject', - ], - /operation must be canonical lower-kebab-case/u, - ], - [ - [ - scaffoldFlag.vertical, - 'missing', - scaffoldFlag.provider, - 'ares', - scaffoldFlag.operation, - 'subject', - ], - /package metadata is missing/u, - ], - ]; - const assertInvalidCall = async (index = 0): Promise => { - const invalidCall = invalidCalls[index]; - if (invalidCall === undefined) { - return; - } - const [generatorArguments, expected] = invalidCall; - await assert.rejects( - run(fixture, scaffoldCommand.externalHttpAdapter, generatorArguments), - expected, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - await assertInvalidCall(index + 1); - }; - await assertInvalidCall(); - }); -}); + }), +); -test('external HTTP adapter planner rejects malformed OntOS ownership atomically', async () => { - await withFixture(async (fixture) => { - const manifestPath = path.join(fixture.root, 'verticals/contacts/vertical.manifest.ts'); - const manifest = await readFile(manifestPath, 'utf-8'); - await writeFile( - manifestPath, - manifest.replace( - '// @generated by OntOS Codesmith Module Contract v1', - '// developer-owned manifest', - ), - 'utf-8', - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.externalHttpAdapter, - [ - scaffoldFlag.vertical, - 'contacts', - scaffoldFlag.provider, - 'ares', - scaffoldFlag.operation, - 'subject', - ], - /is not a generated module owner/u, +it.live( + 'rejects unsafe external HTTP adapter command input without writing', + Effect.fn(function* scenario44() { + yield* withFixture( + Effect.fn(function* scenario45(fixture) { + const before = yield* snapshotTree(fixture.root); + const invalidCalls: readonly [readonly string[], RegExp][] = [ + [ + [scaffoldFlag.vertical, 'contacts', scaffoldFlag.operation, 'subject'], + /missing required flag --provider/u, + ], + [ + [scaffoldFlag.vertical, 'contacts', scaffoldFlag.provider, 'ares'], + /missing required flag --operation/u, + ], + [ + [scaffoldFlag.provider, 'ares', scaffoldFlag.operation, 'subject'], + /missing required flag --vertical/u, + ], + [ + [ + scaffoldFlag.vertical, + 'contacts', + scaffoldFlag.provider, + 'ares', + scaffoldFlag.operation, + 'subject', + '--unknown', + 'x', + ], + /unknown flag --unknown/u, + ], + [ + [ + scaffoldFlag.vertical, + 'contacts', + scaffoldFlag.provider, + 'ares', + scaffoldFlag.provider, + 'other', + scaffoldFlag.operation, + 'subject', + ], + /only once/u, + ], + [ + [ + scaffoldFlag.vertical, + 'contacts', + scaffoldFlag.provider, + 'Ares', + scaffoldFlag.operation, + 'subject', + ], + /provider must be canonical lower-kebab-case/u, + ], + [ + [ + scaffoldFlag.vertical, + 'contacts', + scaffoldFlag.provider, + 'ares', + scaffoldFlag.operation, + 'Subject', + ], + /operation must be canonical lower-kebab-case/u, + ], + [ + [ + scaffoldFlag.vertical, + 'contacts', + scaffoldFlag.provider, + 'src', + scaffoldFlag.operation, + 'subject', + ], + /provider must be canonical lower-kebab-case/u, + ], + [ + [ + scaffoldFlag.vertical, + 'contacts', + scaffoldFlag.provider, + 'ares', + scaffoldFlag.operation, + 'node_modules', + ], + /operation must be canonical lower-kebab-case/u, + ], + [ + [ + scaffoldFlag.vertical, + 'contacts', + scaffoldFlag.provider, + '../ares', + scaffoldFlag.operation, + 'subject', + ], + /provider must be canonical lower-kebab-case/u, + ], + [ + [ + scaffoldFlag.vertical, + 'contacts', + scaffoldFlag.provider, + 'ares', + scaffoldFlag.operation, + '../subject', + ], + /operation must be canonical lower-kebab-case/u, + ], + [ + [ + scaffoldFlag.vertical, + 'missing', + scaffoldFlag.provider, + 'ares', + scaffoldFlag.operation, + 'subject', + ], + /package metadata is missing/u, + ], + ]; + for (const [generatorArguments, expected] of invalidCalls) { + yield* expectFailure( + run(fixture, scaffoldCommand.externalHttpAdapter, generatorArguments), + (error) => expect(String(error)).toMatch(expected), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + } + }), ); - }); + }), +); - await withFixture(async (fixture) => { - await writeFixtureFile( - fixture.root, - 'verticals/contacts/src/integrations', - 'planner fixture blocks the required directory\n', - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.externalHttpAdapter, - [ - scaffoldFlag.vertical, - 'contacts', - scaffoldFlag.provider, - 'ares', - scaffoldFlag.operation, - 'subject', - ], - /ENOTDIR|not a directory/u, +it.live( + 'external HTTP adapter planner rejects malformed OntOS ownership atomically', + Effect.fn(function* scenario47() { + yield* withFixture( + Effect.fn(function* scenario48(fixture) { + const manifestPath = path.join(fixture.root, 'verticals/contacts/vertical.manifest.ts'); + const manifest = yield* Effect.promise(() => readFile(manifestPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + manifestPath, + manifest.replace( + '// @generated by OntOS Codesmith Module Contract v1', + '// developer-owned manifest', + ), + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.externalHttpAdapter, + [ + scaffoldFlag.vertical, + 'contacts', + scaffoldFlag.provider, + 'ares', + scaffoldFlag.operation, + 'subject', + ], + /is not a generated module owner/u, + ); + }), ); - }); -}); -test('Action generation rejects unrelated imports in its governed owner slots', async () => { - await withFixture(async (fixture) => { - const manifestPath = path.join(fixture.root, inventoryManifestFile); - const manifest = await readFile(manifestPath, 'utf-8'); - await writeFile( - manifestPath, - manifest.replace( - '// ', - `// -import { fakeRead } from './src/api/fake.read.ts';`, - ), - 'utf-8', - ); - await assertScaffoldRefused( - fixture, - 'action', - [scaffoldFlag.vertical, inventorySlug, '--action', 'create-order3'], - /generated owner slot contains unsupported developer content/u, + yield* withFixture( + Effect.fn(function* scenario49(fixture) { + yield* write( + fixture.root, + 'verticals/contacts/src/integrations', + 'planner fixture blocks the required directory\n', + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.externalHttpAdapter, + [ + scaffoldFlag.vertical, + 'contacts', + scaffoldFlag.provider, + 'ares', + scaffoldFlag.operation, + 'subject', + ], + /ENOTDIR|not a directory/u, + ); + }), ); - }); -}); - -test('generates Core-owned Actions only through the Core owner slot with atomic preflight', async () => { - await withFixture(async (fixture) => { - await run(fixture, 'action', [ - '--scope', - 'core', - '--module', - fixtureName.actionModule, - '--action', - 'z-last-change', - ]); - await run(fixture, 'action', [ - '--scope', - 'core', - '--module', - fixtureName.actionModule, - '--action', - 'account-change', - ]); + }), +); - const action = await readFixtureFile( - fixture.root, - 'packages/core-runtime/src/modules/actions/account-change.action.ts', - ); - assert.match(action, /@ontos-action-owner core\.modules/u); - assert.match(action, /actionKey: 'core\.modules\.account-change'/u); - assert.match(action, /entrypoint: defineSystemModuleEntrypoint\(\{/u); - assert.match(action, /access: 'write'/u); - assert.match(action, /role: 'action'/u); - assert.match(action, /from '\.\.\/\.\.\/actions\/definition\.ts'/u); - assert.doesNotMatch(action, /verticals|fetch\(/u); - - const coreIndex = await readFixtureFile(fixture.root, coreRuntimeIndexFile); - const accountExport = - "export { accountChangeAction } from './modules/actions/account-change.action.ts';"; - const zExport = - "export { zLastChangeAction } from './modules/actions/z-last-change.action.ts';"; - assert.ok(coreIndex.includes(accountExport)); - assert.ok(coreIndex.includes(zExport)); - assert.ok(coreIndex.indexOf(accountExport) < coreIndex.indexOf(zExport)); - assert.match(coreIndex, /export const existingCoreSurface = true/u); - - const coreCatalog = await readFixtureFile(fixture.root, coreActionCatalogFile); - const accountImport = "import { accountChangeAction } from './account-change.action.ts';"; - const zImport = "import { zLastChangeAction } from './z-last-change.action.ts';"; - assert.ok(coreCatalog.includes(accountImport)); - assert.ok(coreCatalog.includes(zImport)); - assert.ok(coreCatalog.includes('accountChangeAction.descriptor,')); - assert.ok(coreCatalog.includes('zLastChangeAction.descriptor,')); - assert.ok(coreCatalog.indexOf(accountImport) < coreCatalog.indexOf(zImport)); - assert.ok( - coreCatalog.indexOf('accountChangeAction.descriptor,') < - coreCatalog.indexOf('zLastChangeAction.descriptor,'), +it.live( + 'Action generation rejects unrelated imports in its governed owner slots', + Effect.fn(function* scenario50() { + yield* withFixture( + Effect.fn(function* scenario51(fixture) { + const manifestPath = path.join(fixture.root, inventoryManifestFile); + const manifest = yield* Effect.promise(() => readFile(manifestPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + manifestPath, + manifest.replace( + '// ', + `// +import { fakeRead } from './src/api/fake.read.ts';`, + ), + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + 'action', + [scaffoldFlag.vertical, inventorySlug, '--action', 'create-order3'], + /generated owner slot contains unsupported developer content/u, + ); + }), ); - assert.match(coreCatalog, /export const existingCatalogSurface = true/u); + }), +); - await assertScaffoldRefused( - fixture, - 'action', - ['--scope', 'core', '--module', fixtureName.actionModule, '--action', 'account-change'], - /refusing to overwrite/u, - ); - }); +it.live( + 'generates Core-owned Actions only through the Core owner slot with atomic preflight', + Effect.fn(function* scenario52() { + yield* withFixture( + Effect.fn(function* scenario53(fixture) { + yield* run(fixture, 'action', [ + '--scope', + 'core', + '--module', + fixtureName.actionModule, + '--action', + 'z-last-change', + ]); + yield* run(fixture, 'action', [ + '--scope', + 'core', + '--module', + fixtureName.actionModule, + '--action', + 'account-change', + ]); - await withFixture(async (fixture) => { - const indexPath = path.join(fixture.root, coreRuntimeIndexFile); - await writeFile( - indexPath, - `export const existingCoreSurface = true;\n\n// \n// \n`, - 'utf-8', - ); - await assertScaffoldRefused( - fixture, - 'action', - ['--scope', 'core', '--module', fixtureName.actionModule, '--action', fixtureName.action], - /generated owner file does not contain one valid/u, + const action = yield* readFixtureFile( + fixture.root, + 'packages/core-runtime/src/modules/actions/account-change.action.ts', + ); + expect(action).toMatch(/@ontos-action-owner core\.modules/u); + expect(action).toMatch(/actionKey: 'core\.modules\.account-change'/u); + expect(action).toMatch(/entrypoint: defineSystemModuleEntrypoint\(\{/u); + expect(action).toMatch(/access: 'write'/u); + expect(action).toMatch(/role: 'action'/u); + expect(action).toMatch(/from '\.\.\/\.\.\/actions\/definition\.ts'/u); + expect(action).not.toMatch(/verticals|fetch\(/u); + + const coreIndex = yield* readFixtureFile(fixture.root, coreRuntimeIndexFile); + const accountExport = + "export { accountChangeAction } from './modules/actions/account-change.action.ts';"; + const zExport = + "export { zLastChangeAction } from './modules/actions/z-last-change.action.ts';"; + expect(coreIndex.includes(accountExport)).toBe(true); + expect(coreIndex.includes(zExport)).toBe(true); + expect(coreIndex.indexOf(accountExport) < coreIndex.indexOf(zExport)).toBe(true); + expect(coreIndex).toMatch(/export const existingCoreSurface = true/u); + + const coreCatalog = yield* readFixtureFile(fixture.root, coreActionCatalogFile); + const accountImport = "import { accountChangeAction } from './account-change.action.ts';"; + const zImport = "import { zLastChangeAction } from './z-last-change.action.ts';"; + expect(coreCatalog.includes(accountImport)).toBe(true); + expect(coreCatalog.includes(zImport)).toBe(true); + expect(coreCatalog.includes('accountChangeAction.descriptor,')).toBe(true); + expect(coreCatalog.includes('zLastChangeAction.descriptor,')).toBe(true); + expect(coreCatalog.indexOf(accountImport) < coreCatalog.indexOf(zImport)).toBe(true); + expect( + coreCatalog.indexOf('accountChangeAction.descriptor,') < + coreCatalog.indexOf('zLastChangeAction.descriptor,'), + ).toBe(true); + expect(coreCatalog).toMatch(/export const existingCatalogSurface = true/u); + + yield* assertScaffoldRefused( + fixture, + 'action', + ['--scope', 'core', '--module', fixtureName.actionModule, '--action', 'account-change'], + /refusing to overwrite/u, + ); + }), ); - }); - await withFixture(async (fixture) => { - const indexPath = path.join(fixture.root, coreRuntimeIndexFile); - const index = await readFile(indexPath, 'utf-8'); - await writeFile( - indexPath, - index.replace( - '// \n', - '// \nexport const developerOwned = true;\n', - ), - 'utf-8', - ); - await assertScaffoldRefused( - fixture, - 'action', - ['--scope', 'core', '--module', fixtureName.actionModule, '--action', fixtureName.action], - /unsupported developer content/u, + yield* withFixture( + Effect.fn(function* scenario54(fixture) { + const indexPath = path.join(fixture.root, coreRuntimeIndexFile); + yield* Effect.promise(() => + writeFile( + indexPath, + `export const existingCoreSurface = true;\n\n// \n// \n`, + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + 'action', + ['--scope', 'core', '--module', fixtureName.actionModule, '--action', fixtureName.action], + /generated owner file does not contain one valid/u, + ); + }), ); - }); - await withFixture(async (fixture) => { - const catalogPath = path.join(fixture.root, coreActionCatalogFile); - const catalog = await readFile(catalogPath, 'utf-8'); - await writeFile( - catalogPath, - catalog.replace( - '// \n', - '// \n developerOwned.descriptor,\n', - ), - 'utf-8', - ); - await assertScaffoldRefused( - fixture, - 'action', - ['--scope', 'core', '--module', fixtureName.actionModule, '--action', fixtureName.action], - /unsupported developer content/u, + yield* withFixture( + Effect.fn(function* scenario55(fixture) { + const indexPath = path.join(fixture.root, coreRuntimeIndexFile); + const index = yield* Effect.promise(() => readFile(indexPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + indexPath, + index.replace( + '// \n', + '// \nexport const developerOwned = true;\n', + ), + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + 'action', + ['--scope', 'core', '--module', fixtureName.actionModule, '--action', fixtureName.action], + /unsupported developer content/u, + ); + }), ); - }); -}); -test('preflights the Action dependency patch before creating a file', async () => { - await withFixture(async (fixture) => { - const packagePath = path.join(fixture.root, inventoryPackageFile); - const packageJson = decodeFixturePackage(await readFile(packagePath, 'utf-8')); - await writeFile( - packagePath, - json({ - ...packageJson, - dependencies: { '@app/core-runtime': '^1.0.0', zeta: '1.0.0' }, + yield* withFixture( + Effect.fn(function* scenario56(fixture) { + const catalogPath = path.join(fixture.root, coreActionCatalogFile); + const catalog = yield* Effect.promise(() => readFile(catalogPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + catalogPath, + catalog.replace( + '// \n', + '// \n developerOwned.descriptor,\n', + ), + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + 'action', + ['--scope', 'core', '--module', fixtureName.actionModule, '--action', fixtureName.action], + /unsupported developer content/u, + ); }), - 'utf-8', - ); - await assertScaffoldRefused( - fixture, - 'action', - [scaffoldFlag.vertical, inventorySlug, '--action', fixtureName.action], - /incompatible/u, ); - }); -}); + }), +); -test('rejects Action generation when a vertical app identity is duplicated', async () => { - await withFixture(async (fixture) => { - const billingPackagePath = path.join(fixture.root, 'verticals/billing/package.json'); - const billingPackage = decodeFixturePackage(await readFile(billingPackagePath, 'utf-8')); - await writeFile( - billingPackagePath, - json({ - ...billingPackage, - modernjs: { ...billingPackage.modernjs, appId: inventoryVertical.appId }, +it.live( + 'preflights the Action dependency patch before creating a file', + Effect.fn(function* scenario57() { + yield* withFixture( + Effect.fn(function* scenario58(fixture) { + const packagePath = path.join(fixture.root, inventoryPackageFile); + const packageJson = yield* decodeFixturePackage( + yield* Effect.promise(() => readFile(packagePath, 'utf-8')), + ); + yield* Effect.promise(() => + writeFile( + packagePath, + json({ + ...packageJson, + dependencies: { '@app/core-runtime': '^1.0.0', zeta: '1.0.0' }, + }), + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + 'action', + [scaffoldFlag.vertical, inventorySlug, '--action', fixtureName.action], + /incompatible/u, + ); }), - 'utf-8', ); - await assertScaffoldRefused( - fixture, - 'action', - [scaffoldFlag.vertical, inventorySlug, '--action', fixtureName.action], - /duplicate generated appId inventory-stock/u, - ); - }); -}); + }), +); -test('rejects Action generation when the target identity is absent from topology', async () => { - await withFixture(async (fixture) => { - const packagePath = path.join(fixture.root, inventoryPackageFile); - const packageJson = decodeFixturePackage(await readFile(packagePath, 'utf-8')); - await writeFile( - packagePath, - json({ - ...packageJson, - modernjs: { ...packageJson.modernjs, appId: 'inventory-shadow' }, +it.live( + 'rejects Action generation when a vertical app identity is duplicated', + Effect.fn(function* scenario59() { + yield* withFixture( + Effect.fn(function* scenario60(fixture) { + const billingPackagePath = path.join(fixture.root, 'verticals/billing/package.json'); + const billingPackage = yield* decodeFixturePackage( + yield* Effect.promise(() => readFile(billingPackagePath, 'utf-8')), + ); + yield* Effect.promise(() => + writeFile( + billingPackagePath, + json({ + ...billingPackage, + modernjs: { ...billingPackage.modernjs, appId: inventoryVertical.appId }, + }), + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + 'action', + [scaffoldFlag.vertical, inventorySlug, '--action', fixtureName.action], + /duplicate generated appId inventory-stock/u, + ); }), - 'utf-8', - ); - await assertScaffoldRefused( - fixture, - 'action', - [scaffoldFlag.vertical, inventorySlug, '--action', fixtureName.action], - /must have exactly one matching generated topology entry/u, ); - }); -}); + }), +); -test('preserves owner JSON document style while patching the Core dependency', async () => { - await withFixture(async (fixture) => { - const packagePath = path.join(fixture.root, inventoryPackageFile); - const packageJson = decodeFixturePackage(await readFile(packagePath, 'utf-8')); - const styledPackage = JSON.stringify(packageJson, null, 4).replaceAll('\n', '\r\n'); - await writeFile(packagePath, styledPackage, 'utf-8'); +it.live( + 'rejects Action generation when the target identity is absent from topology', + Effect.fn(function* scenario61() { + yield* withFixture( + Effect.fn(function* scenario62(fixture) { + const packagePath = path.join(fixture.root, inventoryPackageFile); + const packageJson = yield* decodeFixturePackage( + yield* Effect.promise(() => readFile(packagePath, 'utf-8')), + ); + yield* Effect.promise(() => + writeFile( + packagePath, + json({ + ...packageJson, + modernjs: { ...packageJson.modernjs, appId: 'inventory-shadow' }, + }), + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + 'action', + [scaffoldFlag.vertical, inventorySlug, '--action', fixtureName.action], + /must have exactly one matching generated topology entry/u, + ); + }), + ); + }), +); - await run(fixture, 'action', [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - ]); +it.live( + 'preserves owner JSON document style while patching the Core dependency', + Effect.fn(function* scenario63() { + yield* withFixture( + Effect.fn(function* scenario64(fixture) { + const packagePath = path.join(fixture.root, inventoryPackageFile); + const packageJson = yield* decodeFixturePackage( + yield* Effect.promise(() => readFile(packagePath, 'utf-8')), + ); + const styledPackage = JSON.stringify(packageJson, null, 4).replaceAll('\n', '\r\n'); + yield* Effect.promise(() => writeFile(packagePath, styledPackage, 'utf-8')); - const patched = await readFile(packagePath, 'utf-8'); - assert.match(patched, /\r\n {4}"dependencies": \{\r\n/u); - assert.match(patched, /\r\n {8}"existing": "preserve-me"/u); - assert.doesNotMatch(patched, /(? { - await withFixture(async (fixture) => { - await run(fixture, 'action', [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - ]); - const actionPath = path.join(fixture.root, inventoryActionFile); - const generatedAction = await readFile(actionPath, 'utf-8'); - await writeFile( - actionPath, - `${generatedAction}\nexport const developerOwned = true;\n`, - 'utf-8', - ); - await run(fixture, scaffoldCommand.outboxMessage, [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - '--topic', - fixtureName.ordersShipped, - ]); - await run(fixture, scaffoldCommand.outboxMessage, [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - '--topic', - fixtureName.ordersCreated, - ]); - const message = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/actions/create-order.orders-created.outbox-message.ts', + const patched = yield* Effect.promise(() => readFile(packagePath, 'utf-8')); + expect(patched).toMatch(/\r\n {4}"dependencies": \{\r\n/u); + expect(patched).toMatch(/\r\n {8}"existing": "preserve-me"/u); + expect(patched).not.toMatch(/(? readFile(actionPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + actionPath, + `${generatedAction}\nexport const developerOwned = true;\n`, + 'utf-8', + ), + ); + yield* run(fixture, scaffoldCommand.outboxMessage, [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--topic', + fixtureName.ordersShipped, + ]); + yield* run(fixture, scaffoldCommand.outboxMessage, [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--topic', + fixtureName.ordersCreated, + ]); + const message = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/actions/create-order.orders-created.outbox-message.ts', + ); + expect(message).toBe(`import type { OutboxMessage } from '@app/core-runtime'; import { OutboxPayloadSchema, outboxProducerModuleKey, @@ -3774,11 +4080,9 @@ export const createCreateOrderOrdersCreatedOutboxMessage = ( producerModuleKey: CreateOrderOrdersCreatedOutboxProducerModuleKey, topic: CreateOrderOrdersCreatedOutboxTopic, }); -`, - ); - assert.equal( - await readFixtureFile(fixture.root, inventoryOutboxContractFile), - `// @generated by OntOS Codesmith Outbox Message Contract v1 +`); + expect(yield* readFixtureFile(fixture.root, inventoryOutboxContractFile)) + .toBe(`// @generated by OntOS Codesmith Outbox Message Contract v1 // @ontos-outbox-producer inventory.stock // @ontos-outbox-topic orders.created import { Schema } from 'effect'; @@ -3790,178 +4094,189 @@ export type OutboxPayload = Schema.Schema.Type; export const outboxTopic = 'orders.created' as const; export const outboxProducerModuleKey = 'inventory.stock' as const; -`, - ); - const producerPackage = decodeFixturePackage( - await readFixtureFile(fixture.root, inventoryPackageFile), - ); - assert.equal(producerPackage.exports['./outbox/orders-created'], generatedOutboxContractPath); - const action = await readFile(actionPath, 'utf-8'); - const createdExport = - "export { CreateOrderOrdersCreatedOutboxPayloadSchema } from './create-order.orders-created.outbox-message.ts';"; - const shippedExport = - "export { CreateOrderOrdersShippedOutboxPayloadSchema } from './create-order.orders-shipped.outbox-message.ts';"; - assert.ok(action.indexOf(createdExport) < action.indexOf(shippedExport)); - assert.match(action, /export const developerOwned = true;/u); - assert.doesNotMatch( - message, - /addDomainEvent|addOutboxMessage|subjectResource|transport|worker/u, - ); +`); + const producerPackage = yield* decodeFixturePackage( + yield* readFixtureFile(fixture.root, inventoryPackageFile), + ); + expect(producerPackage.exports['./outbox/orders-created']).toBe( + generatedOutboxContractPath, + ); + const action = yield* Effect.promise(() => readFile(actionPath, 'utf-8')); + const createdExport = + "export { CreateOrderOrdersCreatedOutboxPayloadSchema } from './create-order.orders-created.outbox-message.ts';"; + const shippedExport = + "export { CreateOrderOrdersShippedOutboxPayloadSchema } from './create-order.orders-shipped.outbox-message.ts';"; + expect(action.indexOf(createdExport) < action.indexOf(shippedExport)).toBe(true); + expect(action).toMatch(/export const developerOwned = true;/u); + expect(message).not.toMatch( + /addDomainEvent|addOutboxMessage|subjectResource|transport|worker/u, + ); - await run(fixture, scaffoldCommand.outboxMessage, [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - '--topic', - 'events.foo-1-bar', - ]); - await assertScaffoldRefused( - fixture, - scaffoldCommand.outboxMessage, - [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - '--topic', - 'events.foo1-bar', - ], - /Outbox identifier CreateOrderEventsFoo1BarOutbox already exists/u, + yield* run(fixture, scaffoldCommand.outboxMessage, [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--topic', + 'events.foo-1-bar', + ]); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.outboxMessage, + [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--topic', + 'events.foo1-bar', + ], + /Outbox identifier CreateOrderEventsFoo1BarOutbox already exists/u, + ); + }), ); - }); -}); + }), +); -test('rejects missing, handwritten, duplicate, and normalized-collision Outbox targets without partial writes', async () => { - await withFixture(async (fixture) => { - await assertScaffoldRefused( - fixture, - scaffoldCommand.outboxMessage, - [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - 'missing-action', - '--topic', - fixtureName.ordersCreated, - ], - /requires the generated Action/u, - ); +it.live( + 'rejects missing, handwritten, duplicate, and normalized-collision Outbox targets without partial writes', + Effect.fn(function* scenario67() { + yield* withFixture( + Effect.fn(function* scenario68(fixture) { + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.outboxMessage, + [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + 'missing-action', + '--topic', + fixtureName.ordersCreated, + ], + /requires the generated Action/u, + ); - await writeFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/actions/handwritten.action.ts', - `// \n// \n`, - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.outboxMessage, - [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - 'handwritten', - '--topic', - fixtureName.ordersCreated, - ], - /only the matching generated Action/u, - ); + yield* write( + fixture.root, + 'verticals/inventory-stock/src/actions/handwritten.action.ts', + `// \n// \n`, + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.outboxMessage, + [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + 'handwritten', + '--topic', + fixtureName.ordersCreated, + ], + /only the matching generated Action/u, + ); - await run(fixture, 'action', [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - ]); - const governedActionPath = inventoryActionFile; - const governedAction = await readFixtureFile(fixture.root, governedActionPath); - await writeFixtureFile( - fixture.root, - governedActionPath, - governedAction.replace(" access: 'write',", " access: 'read',"), - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.outboxMessage, - [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - '--topic', - fixtureName.ordersCreated, - ], - /matching generated Action with its governed write entrypoint/u, - ); - await writeFixtureFile(fixture.root, governedActionPath, governedAction); - await run(fixture, scaffoldCommand.outboxMessage, [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - '--topic', - 'orders.created-v2', - ]); - const beforeCollision = await snapshotTree(fixture.root); - await Promise.all( - ['orders.created-v2', 'orders-created.v2'].map(async (topic) => { - await assert.rejects( - run(fixture, scaffoldCommand.outboxMessage, [ + yield* run(fixture, 'action', [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + ]); + const governedActionPath = inventoryActionFile; + const governedAction = yield* readFixtureFile(fixture.root, governedActionPath); + yield* write( + fixture.root, + governedActionPath, + governedAction.replace(" access: 'write',", " access: 'read',"), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.outboxMessage, + [ scaffoldFlag.vertical, inventorySlug, '--action', fixtureName.action, '--topic', - topic, - ]), - /already exists/u, + fixtureName.ordersCreated, + ], + /matching generated Action with its governed write entrypoint/u, + ); + yield* write(fixture.root, governedActionPath, governedAction); + yield* run(fixture, scaffoldCommand.outboxMessage, [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--topic', + 'orders.created-v2', + ]); + const beforeCollision = yield* snapshotTree(fixture.root); + yield* Effect.all( + ['orders.created-v2', 'orders-created.v2'].map( + Effect.fn(function* scenario69(topic) { + yield* expectFailure( + run(fixture, scaffoldCommand.outboxMessage, [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--topic', + topic, + ]), + (error) => expect(String(error)).toMatch(/already exists/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeCollision); + }), + ), + { concurrency: 'unbounded' }, ); - assert.deepEqual(await snapshotTree(fixture.root), beforeCollision); }), ); - }); -}); + }), +); -test('generates isolated Outbox Workers from published contracts and composes a stable registry', async () => { - await withFixture(async (fixture) => { - const billingApiBefore = await readFixtureFile(fixture.root, billingApiIndexFile); - await run(fixture, 'action', [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - ]); - await run(fixture, scaffoldCommand.outboxMessage, [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - '--topic', - fixtureName.ordersCreated, - ]); - const producerBefore = Object.fromEntries( - Object.entries(await snapshotTree(fixture.root)).filter(([file]) => - file.startsWith('verticals/inventory-stock/'), - ), - ); +it.live( + 'generates isolated Outbox Workers from published contracts and composes a stable registry', + Effect.fn(function* mergedScenario71() { + yield* withFixture( + Effect.fn(function* mergedScenario70(fixture) { + const billingApiBefore = yield* readFixtureFile(fixture.root, billingApiIndexFile); + yield* run(fixture, 'action', [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + ]); + yield* run(fixture, scaffoldCommand.outboxMessage, [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--topic', + fixtureName.ordersCreated, + ]); + const producerBefore = Object.fromEntries( + Object.entries(yield* snapshotTree(fixture.root)).filter(([file]) => + file.startsWith('verticals/inventory-stock/'), + ), + ); - await run(fixture, scaffoldCommand.outboxWorker, [ - scaffoldFlag.vertical, - 'billing', - '--worker', - fixtureName.ordersCreatedLogger, - scaffoldFlag.producer, - inventorySlug, - '--topic', - fixtureName.ordersCreated, - ]); - const worker = await readFixtureFile( - fixture.root, - 'verticals/billing/src/workers/orders-created-logger.worker.ts', - ); - assert.equal( - worker, - `// @generated by OntOS Codesmith Outbox Worker v1 + yield* run(fixture, scaffoldCommand.outboxWorker, [ + scaffoldFlag.vertical, + 'billing', + '--worker', + fixtureName.ordersCreatedLogger, + scaffoldFlag.producer, + inventorySlug, + '--topic', + fixtureName.ordersCreated, + ]); + const worker = yield* readFixtureFile( + fixture.root, + 'verticals/billing/src/workers/orders-created-logger.worker.ts', + ); + expect(worker).toBe(`// @generated by OntOS Codesmith Outbox Worker v1 // @ontos-outbox-worker-key billing.core.orders-created-logger // @ontos-outbox-worker-owner billing.core // @ontos-outbox-worker-producer inventory.stock @@ -4014,11 +4329,9 @@ export const ordersCreatedLoggerWorker = defineOutboxWorker( }, handleOrdersCreatedLogger, ); -`, - ); - assert.equal( - await readFixtureFile(fixture.root, billingWorkersIndexFile), - `import type { AnyOutboxWorkerRegistration } from '@app/core-runtime'; +`); + expect(yield* readFixtureFile(fixture.root, billingWorkersIndexFile)) + .toBe(`import type { AnyOutboxWorkerRegistration } from '@app/core-runtime'; // import { ordersCreatedLoggerWorker } from './orders-created-logger.worker.ts'; @@ -4029,11 +4342,9 @@ export const outboxWorkers = Object.freeze([ ordersCreatedLoggerWorker, // ]) satisfies readonly AnyOutboxWorkerRegistration[]; -`, - ); - assert.equal( - await readFixtureFile(fixture.root, 'verticals/billing/src/worker-host/layer.ts'), - `// @generated by scaffold:outbox-worker worker-host +`); + expect(yield* readFixtureFile(fixture.root, 'verticals/billing/src/worker-host/layer.ts')) + .toBe(`// @generated by scaffold:outbox-worker worker-host // @ontos-outbox-worker-host-owner billing.core import { Layer } from 'effect'; import { OutboxWorkerInfrastructureLive } from '@app/core-runtime/outbox/worker'; @@ -4049,20 +4360,16 @@ export const outboxWorkerLayer = Layer.merge( OutboxWorkerInfrastructureLive, outboxWorkerHandlerLayer, ); -`, - ); - assert.equal( - await readFixtureFile(fixture.root, 'verticals/billing/src/worker-host/main.ts'), - `// @generated by scaffold:outbox-worker worker-host +`); + expect(yield* readFixtureFile(fixture.root, 'verticals/billing/src/worker-host/main.ts')) + .toBe(`// @generated by scaffold:outbox-worker worker-host // @ontos-outbox-worker-host-owner billing.core import { startBillingOutboxWorker } from '../../scripts/outbox-worker.ts'; startBillingOutboxWorker(); -`, - ); - assert.equal( - await readFixtureFile(fixture.root, 'verticals/billing/scripts/outbox-worker.ts'), - `// @generated by scaffold:outbox-worker worker-host +`); + expect(yield* readFixtureFile(fixture.root, 'verticals/billing/scripts/outbox-worker.ts')) + .toBe(`// @generated by scaffold:outbox-worker worker-host // @ontos-outbox-worker-host-owner billing.core import { Layer } from 'effect'; import { @@ -4092,256 +4399,275 @@ export const startBillingOutboxWorker = (): void => registrations: outboxWorkers, subscriptions: outboxSubscriptions, }); -`, - ); - assert.equal(await readFixtureFile(fixture.root, billingApiIndexFile), billingApiBefore); - const consumerPackage = decodeFixturePackage( - await readFixtureFile(fixture.root, 'verticals/billing/package.json'), - ); - assert.equal(consumerPackage.dependencies['@app/core-runtime'], workspaceVersion); - assert.equal(consumerPackage.dependencies[inventoryPackageName], workspaceVersion); - assert.equal(consumerPackage.exports['./workers'], undefined); - assert.equal(consumerPackage.scripts['dev:worker'], workerStartScript); - assert.equal(consumerPackage.scripts['worker:start'], workerStartScript); - const consumerTsconfig = Schema.decodeUnknownSync(FixtureTsconfigSchema)( - JSON.parse(await readFixtureFile(fixture.root, 'verticals/billing/tsconfig.json')), - ); - assert.deepEqual(consumerTsconfig.references, [{ path: '../inventory-stock' }]); - const producerAfter = Object.fromEntries( - Object.entries(await snapshotTree(fixture.root)).filter(([file]) => - file.startsWith('verticals/inventory-stock/'), - ), - ); - assert.deepEqual(producerAfter, producerBefore); +`); + expect(yield* readFixtureFile(fixture.root, billingApiIndexFile)).toBe(billingApiBefore); + const consumerPackage = yield* decodeFixturePackage( + yield* readFixtureFile(fixture.root, 'verticals/billing/package.json'), + ); + expect(consumerPackage.dependencies['@app/core-runtime']).toBe(workspaceVersion); + expect(consumerPackage.dependencies[inventoryPackageName]).toBe(workspaceVersion); + expect(consumerPackage.exports['./workers']).toBe(undefined); + expect(consumerPackage.scripts['dev:worker']).toBe(workerStartScript); + expect(consumerPackage.scripts['worker:start']).toBe(workerStartScript); + const consumerTsconfig = yield* Schema.decodeUnknownEffect(FixtureTsconfigSchema)( + JSON.parse(yield* readFixtureFile(fixture.root, 'verticals/billing/tsconfig.json')), + ); + expect(consumerTsconfig.references).toEqual([{ path: '../inventory-stock' }]); + const producerAfter = Object.fromEntries( + Object.entries(yield* snapshotTree(fixture.root)).filter(([file]) => + file.startsWith('verticals/inventory-stock/'), + ), + ); + expect(producerAfter).toEqual(producerBefore); - await run(fixture, scaffoldCommand.outboxMessage, [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - '--topic', - fixtureName.ordersShipped, - ]); - await run(fixture, scaffoldCommand.outboxWorker, [ - scaffoldFlag.vertical, - 'billing', - '--worker', - 'orders-shipped-projector', - scaffoldFlag.producer, - inventorySlug, - '--topic', - fixtureName.ordersShipped, - ]); - const registry = await readFixtureFile(fixture.root, billingWorkersIndexFile); - assert.ok( - registry.indexOf('ordersCreatedLoggerWorker') < - registry.indexOf('ordersShippedProjectorWorker'), - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.outboxWorker, - [ - scaffoldFlag.vertical, - 'billing', - '--worker', - fixtureName.ordersCreatedLogger, - scaffoldFlag.producer, - inventorySlug, - '--topic', - fixtureName.ordersCreated, - ], - /refusing to overwrite/u, + yield* run(fixture, scaffoldCommand.outboxMessage, [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--topic', + fixtureName.ordersShipped, + ]); + yield* run(fixture, scaffoldCommand.outboxWorker, [ + scaffoldFlag.vertical, + 'billing', + '--worker', + 'orders-shipped-projector', + scaffoldFlag.producer, + inventorySlug, + '--topic', + fixtureName.ordersShipped, + ]); + const registry = yield* readFixtureFile(fixture.root, billingWorkersIndexFile); + expect( + registry.indexOf('ordersCreatedLoggerWorker') < + registry.indexOf('ordersShippedProjectorWorker'), + ).toBeTruthy(); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.outboxWorker, + [ + scaffoldFlag.vertical, + 'billing', + '--worker', + fixtureName.ordersCreatedLogger, + scaffoldFlag.producer, + inventorySlug, + '--topic', + fixtureName.ordersCreated, + ], + /refusing to overwrite/u, + ); + }), ); - }); -}); + }), +); -test('generates self-consuming Outbox Workers without circular project or package dependencies', async () => { - await withFixture(async (fixture) => { - await run(fixture, 'action', [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - ]); - await run(fixture, scaffoldCommand.outboxMessage, [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - '--topic', - fixtureName.ordersCreated, - ]); - const manifestBefore = await readFixtureFile(fixture.root, inventoryManifestFile); - const tsconfigBefore = await readFixtureFile(fixture.root, inventoryTsconfigFile); - const args = [ - scaffoldFlag.vertical, - inventorySlug, - '--worker', - 'orders-created-projector', - scaffoldFlag.producer, - inventorySlug, - '--topic', - fixtureName.ordersCreated, - ]; - await run(fixture, scaffoldCommand.outboxWorker, args); - const worker = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/workers/orders-created-projector.worker.ts', - ); - assert.ok(worker.includes('// @ontos-outbox-worker-owner inventory.stock')); - assert.ok(worker.includes('// @ontos-outbox-worker-producer inventory.stock')); - assert.ok(worker.includes("from '@app/inventory-stock/outbox/orders-created'")); - const registry = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/workers/index.ts', - ); - const hostLayer = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/worker-host/layer.ts', - ); - const hostMain = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/worker-host/main.ts', - ); - const hostScript = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/scripts/outbox-worker.ts', - ); - assert.ok(registry.includes(workerRegistryEntry)); - assert.ok(hostLayer.includes('OutboxWorkerInfrastructureLive')); - assert.ok(hostMain.includes('startInventoryStockOutboxWorker();')); - assert.ok(hostScript.includes('startOutboxWorkerProcess({')); - const registration = await readFixtureFile(fixture.root, inventoryRegistrationFile); - assert.ok(registration.includes('createOrderAction,')); - assert.ok(registration.includes(workerRegistryEntry)); - assert.equal(await readFixtureFile(fixture.root, inventoryManifestFile), manifestBefore); - assert.equal(await readFixtureFile(fixture.root, inventoryTsconfigFile), tsconfigBefore); - const ownerPackage = decodeFixturePackage( - await readFixtureFile(fixture.root, inventoryPackageFile), - ); - assert.equal(ownerPackage.dependencies['@app/core-runtime'], workspaceVersion); - assert.equal(ownerPackage.dependencies[inventoryPackageName], undefined); - assert.equal(ownerPackage.exports['./outbox/orders-created'], generatedOutboxContractPath); - for (const script of ['dev:worker', 'worker:start']) { - assert.equal(ownerPackage.scripts[script], workerStartScript); - } - await assertScaffoldRefused( - fixture, - scaffoldCommand.outboxWorker, - args, - /refusing to overwrite/u, - ); - await run(fixture, 'action', [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - 'request-rebuild', - ]); - const registrationAfterAction = await readFixtureFile(fixture.root, inventoryRegistrationFile); - assert.ok(registrationAfterAction.includes('requestRebuildAction,')); - assert.ok(registrationAfterAction.includes(workerRegistryEntry)); - await writeFixtureFile( - fixture.root, - inventoryTsconfigFile, - JSON.stringify({ references: [{ path: '../inventory-stock' }] }), - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.outboxWorker, - [ - scaffoldFlag.vertical, - inventorySlug, - '--worker', - 'orders-created-audit', - scaffoldFlag.producer, - inventorySlug, - '--topic', - fixtureName.ordersCreated, - ], - /circular self project reference/u, +it.live( + 'generates self-consuming Outbox Workers without circular project or package dependencies', + Effect.fn(function* scenario72() { + yield* withFixture( + Effect.fn(function* scenario73(fixture) { + yield* run(fixture, 'action', [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + ]); + yield* run(fixture, scaffoldCommand.outboxMessage, [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--topic', + fixtureName.ordersCreated, + ]); + const manifestBefore = yield* readFixtureFile(fixture.root, inventoryManifestFile); + const tsconfigBefore = yield* readFixtureFile(fixture.root, inventoryTsconfigFile); + const args = [ + scaffoldFlag.vertical, + inventorySlug, + '--worker', + 'orders-created-projector', + scaffoldFlag.producer, + inventorySlug, + '--topic', + fixtureName.ordersCreated, + ]; + yield* run(fixture, scaffoldCommand.outboxWorker, args); + const worker = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/workers/orders-created-projector.worker.ts', + ); + expect(worker.includes('// @ontos-outbox-worker-owner inventory.stock')).toBe(true); + expect(worker.includes('// @ontos-outbox-worker-producer inventory.stock')).toBe(true); + expect(worker.includes("from '@app/inventory-stock/outbox/orders-created'")).toBe(true); + const registry = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/workers/index.ts', + ); + const hostLayer = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/worker-host/layer.ts', + ); + const hostMain = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/worker-host/main.ts', + ); + const hostScript = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/scripts/outbox-worker.ts', + ); + expect(registry.includes(workerRegistryEntry)).toBe(true); + expect(hostLayer.includes('OutboxWorkerInfrastructureLive')).toBe(true); + expect(hostMain.includes('startInventoryStockOutboxWorker();')).toBe(true); + expect(hostScript.includes('startOutboxWorkerProcess({')).toBe(true); + const registration = yield* readFixtureFile(fixture.root, inventoryRegistrationFile); + expect(registration.includes('createOrderAction,')).toBe(true); + expect(registration.includes(workerRegistryEntry)).toBe(true); + expect(yield* readFixtureFile(fixture.root, inventoryManifestFile)).toBe(manifestBefore); + expect(yield* readFixtureFile(fixture.root, inventoryTsconfigFile)).toBe(tsconfigBefore); + const ownerPackage = yield* decodeFixturePackage( + yield* readFixtureFile(fixture.root, inventoryPackageFile), + ); + expect(ownerPackage.dependencies['@app/core-runtime']).toBe(workspaceVersion); + expect(ownerPackage.dependencies[inventoryPackageName]).toBe(undefined); + expect(ownerPackage.exports['./outbox/orders-created']).toBe(generatedOutboxContractPath); + for (const script of ['dev:worker', 'worker:start']) { + expect(ownerPackage.scripts[script]).toBe(workerStartScript); + } + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.outboxWorker, + args, + /refusing to overwrite/u, + ); + yield* run(fixture, 'action', [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + 'request-rebuild', + ]); + const registrationAfterAction = yield* readFixtureFile( + fixture.root, + inventoryRegistrationFile, + ); + expect(registrationAfterAction.includes('requestRebuildAction,')).toBe(true); + expect(registrationAfterAction.includes(workerRegistryEntry)).toBe(true); + yield* write( + fixture.root, + inventoryTsconfigFile, + JSON.stringify({ references: [{ path: '../inventory-stock' }] }), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.outboxWorker, + [ + scaffoldFlag.vertical, + inventorySlug, + '--worker', + 'orders-created-audit', + scaffoldFlag.producer, + inventorySlug, + '--topic', + fixtureName.ordersCreated, + ], + /circular self project reference/u, + ); + }), ); - }); -}); + }), +); -test('refuses unpublished or malformed Outbox contracts without partial consumer writes', async () => { - await withFixture(async (fixture) => { - await assertScaffoldRefused( - fixture, - scaffoldCommand.outboxWorker, - [ - scaffoldFlag.vertical, - 'billing', - '--worker', - fixtureName.ordersLogger, - scaffoldFlag.producer, - inventorySlug, - '--topic', - 'orders.missing', - ], - /published producer Outbox contract is missing/u, - ); +it.live( + 'refuses unpublished or malformed Outbox contracts without partial consumer writes', + Effect.fn(function* scenario74() { + yield* withFixture( + Effect.fn(function* scenario75(fixture) { + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.outboxWorker, + [ + scaffoldFlag.vertical, + 'billing', + '--worker', + fixtureName.ordersLogger, + scaffoldFlag.producer, + inventorySlug, + '--topic', + 'orders.missing', + ], + /published producer Outbox contract is missing/u, + ); - await run(fixture, 'action', [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - ]); - await run(fixture, scaffoldCommand.outboxMessage, [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - '--topic', - fixtureName.ordersCreated, - ]); - const contractPath = path.join(fixture.root, inventoryOutboxContractFile); - const validContract = await readFile(contractPath, 'utf-8'); - await writeFile( - contractPath, - validContract.replace( - '// @ontos-outbox-producer inventory.stock', - '// @ontos-outbox-producer billing', - ), - 'utf-8', - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.outboxWorker, - [ - scaffoldFlag.vertical, - 'billing', - '--worker', - fixtureName.ordersLogger, - scaffoldFlag.producer, - inventorySlug, - '--topic', - fixtureName.ordersCreated, - ], - /owner\/topic\/schema mismatch/u, + yield* run(fixture, 'action', [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + ]); + yield* run(fixture, scaffoldCommand.outboxMessage, [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--topic', + fixtureName.ordersCreated, + ]); + const contractPath = path.join(fixture.root, inventoryOutboxContractFile); + const validContract = yield* Effect.promise(() => readFile(contractPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + contractPath, + validContract.replace( + '// @ontos-outbox-producer inventory.stock', + '// @ontos-outbox-producer billing', + ), + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.outboxWorker, + [ + scaffoldFlag.vertical, + 'billing', + '--worker', + fixtureName.ordersLogger, + scaffoldFlag.producer, + inventorySlug, + '--topic', + fixtureName.ordersCreated, + ], + /owner\/topic\/schema mismatch/u, + ); + }), ); - }); -}); + }), +); -test('generates fail-closed global and owner-local Policies with narrow exports', async () => { - await withFixture(async (fixture) => { - await run(fixture, 'policy', ['--scope', 'global', '--policy', fixtureName.policy]); - await run(fixture, 'policy', ['--scope', 'global', '--policy', 'account-open']); - await run(fixture, 'policy', [ - '--scope', - 'microvertical', - '--policy', - 'stock-available', - scaffoldFlag.vertical, - inventorySlug, - ]); +it.live( + 'generates fail-closed global and owner-local Policies with narrow exports', + Effect.fn(function* scenario76() { + yield* withFixture( + Effect.fn(function* scenario77(fixture) { + yield* run(fixture, 'policy', ['--scope', 'global', '--policy', fixtureName.policy]); + yield* run(fixture, 'policy', ['--scope', 'global', '--policy', 'account-open']); + yield* run(fixture, 'policy', [ + '--scope', + 'microvertical', + '--policy', + 'stock-available', + scaffoldFlag.vertical, + inventorySlug, + ]); - assert.equal( - await readFixtureFile( - fixture.root, - 'packages/core-runtime/src/policies/tenant-active.policy.ts', - ), - `import { Effect } from 'effect'; + expect( + yield* readFixtureFile( + fixture.root, + 'packages/core-runtime/src/policies/tenant-active.policy.ts', + ), + ).toBe(`import { Effect } from 'effect'; import { defineGlobalPolicy, denyPolicy } from '../actions/policy.ts'; export const tenantActivePolicy = defineGlobalPolicy({ @@ -4351,14 +4677,13 @@ export const tenantActivePolicy = defineGlobalPolicy({ ), policyKey: 'global.tenant-active.v1', }); -`, - ); - assert.equal( - await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/policies/stock-available.policy.ts', - ), - `import { Effect } from 'effect'; +`); + expect( + yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/policies/stock-available.policy.ts', + ), + ).toBe(`import { Effect } from 'effect'; import { defineMicroverticalPolicy, denyPolicy } from '@app/core-runtime'; export const stockAvailablePolicy = defineMicroverticalPolicy({ @@ -4369,12 +4694,9 @@ export const stockAvailablePolicy = defineMicroverticalPolicy // @@ -4383,58 +4705,60 @@ export const stockAvailablePolicy = defineMicroverticalPolicy -`, - ); - assert.doesNotMatch(coreIndex, /stockAvailablePolicy/u); - assert.equal( - decodeFixturePackage(await readFixtureFile(fixture.root, inventoryPackageFile)).dependencies[ - '@app/core-runtime' - ], - workspaceVersion, - ); - await assertScaffoldRefused( - fixture, - 'policy', - ['--scope', 'global', '--policy', fixtureName.policy], - /refusing to overwrite/u, - ); +`); + expect(coreIndex).not.toMatch(/stockAvailablePolicy/u); + expect( + (yield* decodeFixturePackage(yield* readFixtureFile(fixture.root, inventoryPackageFile))) + .dependencies['@app/core-runtime'], + ).toBe(workspaceVersion); + yield* assertScaffoldRefused( + fixture, + 'policy', + ['--scope', 'global', '--policy', fixtureName.policy], + /refusing to overwrite/u, + ); - await run(fixture, 'policy', ['--scope', 'global', '--policy', 'foo-1-bar']); - await assertScaffoldRefused( - fixture, - 'policy', - ['--scope', 'global', '--policy', 'foo1-bar'], - /Policy identifier foo1BarPolicy already exists/u, + yield* run(fixture, 'policy', ['--scope', 'global', '--policy', 'foo-1-bar']); + yield* assertScaffoldRefused( + fixture, + 'policy', + ['--scope', 'global', '--policy', 'foo1-bar'], + /Policy identifier foo1BarPolicy already exists/u, + ); + }), ); - }); -}); + }), +); -test('generates a title-only authenticated page at the default MicroVertical URL', async () => { - await withFixture(async (fixture) => { - const shellBefore = await readFixtureFile(fixture.root, shellSentinelFile); - const englishLocalePath = path.join(fixture.root, inventoryEnglishLocaleFile); - await writeFile( - englishLocalePath, - '{\r\n "inventory": {"existing":"en-preserved"}\r\n}', - 'utf-8', - ); - const refreshes: string[] = []; - await run( - fixture, - scaffoldCommand.microverticalPage, - [scaffoldFlag.vertical, inventorySlug, '--page', fixtureName.purchaseOrdersPage], - (appId) => { - refreshes.push(appId); - }, - ); - assert.deepEqual(refreshes, [inventorySlug, shellAppId]); - const page = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/purchase-orders/page.tsx', - ); - assert.equal( - page, - `import { useModernI18n } from '@modern-js/plugin-i18n/runtime'; +it.live( + 'generates a title-only authenticated page at the default MicroVertical URL', + Effect.fn(function* scenario78() { + yield* withFixture( + Effect.fn(function* scenario79(fixture) { + const shellBefore = yield* readFixtureFile(fixture.root, shellSentinelFile); + const englishLocalePath = path.join(fixture.root, inventoryEnglishLocaleFile); + yield* Effect.promise(() => + writeFile( + englishLocalePath, + '{\r\n "inventory": {"existing":"en-preserved"}\r\n}', + 'utf-8', + ), + ); + const refreshes: string[] = []; + yield* run( + fixture, + scaffoldCommand.microverticalPage, + [scaffoldFlag.vertical, inventorySlug, '--page', fixtureName.purchaseOrdersPage], + (appId) => { + refreshes.push(appId); + }, + ); + expect(refreshes).toEqual([inventorySlug, shellAppId]); + const page = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/purchase-orders/page.tsx', + ); + expect(page).toBe(`import { useModernI18n } from '@modern-js/plugin-i18n/runtime'; import { UltramodernRouteHead } from '../../../ultramodern-route-head'; export const PurchaseOrdersPage = () => { @@ -4454,64 +4778,58 @@ export const PurchaseOrdersPage = () => { > {t('inventory.pages.purchaseOrders.title')} - - - ); -}; - -export default PurchaseOrdersPage; -`, - ); - const manifest = await readFixtureFile(fixture.root, inventoryManifestFile); - const registration = await readFixtureFile(fixture.root, inventoryRegistrationFile); - const federation = await readFixtureFile(fixture.root, inventoryFederationConfigFile); - const federatedPage = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/federation/page-purchase-orders.tsx', - ); - const shellClients = await readFixtureFile(fixture.root, shellVerticalClientsFile); - assert.match(manifest, /inventory\.stock\.navigation\.purchase-orders/u); - assert.match(manifest, /inventory\.stock\.page\.purchase-orders/u); - assert.match(manifest, /routePath: '\/inventory-stock\/purchase-orders'/u); - assert.match(registration, /page-purchase-orders/u); - assert.match( - federation, - /'\.\/PagePurchaseOrders': '\.\/src\/federation\/page-purchase-orders\.tsx'/u, - ); - assert.match(federatedPage, / import\('inventoryStock\/PagePurchaseOrders'\)/u, - ); - assert.equal( - await readFixtureFile( - fixture.root, - 'apps/shell-super-app/src/routes/[lang]/inventory-stock/purchase-orders/page.tsx', - ), - `export { default } from '../../modules/[moduleId]/page.tsx'; -`, - ); - assert.match( - await readFixtureFile( - fixture.root, - 'apps/shell-super-app/src/routes/[lang]/inventory-stock/purchase-orders/page.data.ts', - ), - /entrypointKey: 'inventory\.stock\.page\.purchase-orders'/u, - ); - assert.match( - await readFixtureFile( - fixture.root, - 'apps/shell-super-app/src/routes/[lang]/inventory-stock/purchase-orders/route.meta.ts', - ), - /canonicalPath: '\/inventory-stock\/purchase-orders'/u, - ); - assert.equal( - await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/purchase-orders/route.meta.ts', - ), - `import { defineTenantModuleEntrypoint } from '@app/core-runtime'; + + + ); +}; + +export default PurchaseOrdersPage; +`); + const manifest = yield* readFixtureFile(fixture.root, inventoryManifestFile); + const registration = yield* readFixtureFile(fixture.root, inventoryRegistrationFile); + const federation = yield* readFixtureFile(fixture.root, inventoryFederationConfigFile); + const federatedPage = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/federation/page-purchase-orders.tsx', + ); + const shellClients = yield* readFixtureFile(fixture.root, shellVerticalClientsFile); + expect(manifest).toMatch(/inventory\.stock\.navigation\.purchase-orders/u); + expect(manifest).toMatch(/inventory\.stock\.page\.purchase-orders/u); + expect(manifest).toMatch(/routePath: '\/inventory-stock\/purchase-orders'/u); + expect(registration).toMatch(/page-purchase-orders/u); + expect(federation).toMatch( + /'\.\/PagePurchaseOrders': '\.\/src\/federation\/page-purchase-orders\.tsx'/u, + ); + expect(federatedPage).toMatch(/ import\('inventoryStock\/PagePurchaseOrders'\)/u, + ); + expect( + yield* readFixtureFile( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/inventory-stock/purchase-orders/page.tsx', + ), + ).toBe(`export { default } from '../../modules/[moduleId]/page.tsx'; +`); + expect( + yield* readFixtureFile( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/inventory-stock/purchase-orders/page.data.ts', + ), + ).toMatch(/entrypointKey: 'inventory\.stock\.page\.purchase-orders'/u); + expect( + yield* readFixtureFile( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/inventory-stock/purchase-orders/route.meta.ts', + ), + ).toMatch(/canonicalPath: '\/inventory-stock\/purchase-orders'/u); + expect( + yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/purchase-orders/route.meta.ts', + ), + ).toBe(`import { defineTenantModuleEntrypoint } from '@app/core-runtime'; const routeMeta = { canonicalPath: '/inventory-stock/purchase-orders', @@ -4540,78 +4858,97 @@ const routeMeta = { export default routeMeta; export { routeMeta }; -`, - ); - const englishContent = await readFile(englishLocalePath, 'utf-8'); - const english = decodeInventoryLocale(englishContent); - const czech = decodeInventoryLocale( - await readFixtureFile(fixture.root, 'verticals/inventory-stock/locales/cs/inventory.json'), +`); + const englishContent = yield* Effect.promise(() => readFile(englishLocalePath, 'utf-8')); + const english = yield* decodeInventoryLocale(englishContent); + const czech = yield* decodeInventoryLocale( + yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/locales/cs/inventory.json', + ), + ); + expect(english.inventory.existing).toBe('en-preserved'); + expect(englishContent).toMatch(/"inventory": \{"existing":"en-preserved", "pages":/u); + expect(englishContent).not.toMatch(/(? { - await withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - 'hr', - '--page', - 'people', - ]); - await stat(path.join(fixture.root, 'verticals/hr/src/routes/[lang]/hr/people/page.tsx')); - assert.match( - await readFixtureFile(fixture.root, 'verticals/hr/vertical.manifest.ts'), - /routePath: '\/hr\/people'/u, +it.live( + 'allows a two-letter MicroVertical slug in a derived default page URL', + Effect.fn(function* scenario80() { + yield* withFixture( + Effect.fn(function* scenario81(fixture) { + yield* run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + 'hr', + '--page', + 'people', + ]); + yield* Effect.promise(() => + stat(path.join(fixture.root, 'verticals/hr/src/routes/[lang]/hr/people/page.tsx')), + ); + expect(yield* readFixtureFile(fixture.root, 'verticals/hr/vertical.manifest.ts')).toMatch( + /routePath: '\/hr\/people'/u, + ); + }), ); - }); -}); + }), +); -test('renders a newly generated federated page with English and Czech owner resources', async () => { - await withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'customers', - ]); - await mkdir(path.join(fixture.root, 'node_modules', '@modern-js'), { recursive: true }); - await Promise.all( - ['react', 'react-dom'].map( - async (packageName) => - await symlink( - path.join(appRoot, 'apps', shellAppId, 'node_modules', packageName), - path.join(fixture.root, 'node_modules', packageName), - 'dir', +it.live( + 'renders a newly generated federated page with English and Czech owner resources', + Effect.fn(function* scenario82() { + yield* withFixture( + Effect.fn(function* scenario83(fixture) { + yield* run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'customers', + ]); + yield* Effect.promise(() => + mkdir(path.join(fixture.root, 'node_modules', '@modern-js'), { recursive: true }), + ); + yield* Effect.all( + ['react', 'react-dom'].map( + Effect.fn(function* scenario84(packageName) { + return yield* Effect.promise(() => + symlink( + path.join(appRoot, 'apps', shellAppId, 'node_modules', packageName), + path.join(fixture.root, 'node_modules', packageName), + 'dir', + ), + ); + }), ), - ), - ); - await writeFixtureFile( - fixture.root, - 'node_modules/@modern-js/plugin-i18n/package.json', - json({ - exports: { './runtime': './runtime.tsx' }, - name: '@modern-js/plugin-i18n', - type: 'module', - }), - ); - await writeFixtureFile( - fixture.root, - 'node_modules/@modern-js/plugin-i18n/runtime.tsx', - `import { createContext, useContext } from 'react'; + { concurrency: 'unbounded' }, + ); + yield* write( + fixture.root, + 'node_modules/@modern-js/plugin-i18n/package.json', + json({ + exports: { './runtime': './runtime.tsx' }, + name: '@modern-js/plugin-i18n', + type: 'module', + }), + ); + yield* write( + fixture.root, + 'node_modules/@modern-js/plugin-i18n/runtime.tsx', + `import { createContext, useContext } from 'react'; import type { ReactNode } from 'react'; interface BoundaryValue { @@ -4640,216 +4977,240 @@ export const useModernI18n = () => { }; }; `, - ); - const runnerPath = path.join(fixture.root, 'render-generated-page.tsx'); - await writeFile( - runnerPath, - `import { renderToStaticMarkup } from 'react-dom/server'; + ); + const runnerPath = path.join(fixture.root, 'render-generated-page.tsx'); + yield* Effect.promise(() => + writeFile( + runnerPath, + `import { renderToStaticMarkup } from 'react-dom/server'; import Page from './verticals/inventory-stock/src/federation/page-customers.tsx'; process.stdout.write(renderToStaticMarkup()); `, - 'utf-8', - ); - const bundlePath = path.join(fixture.root, 'render-generated-page.cjs'); - const bundle = spawnSync( - esbuildPath, - [ - runnerPath, - '--bundle', - '--format=cjs', - '--jsx=automatic', - '--platform=node', - `--outfile=${bundlePath}`, - ], - { cwd: fixture.root, encoding: 'utf-8' }, - ); - assert.equal(bundle.status, 0, bundle.stderr || bundle.error?.message); - const renderLanguage = (language: 'cs' | 'en') => - spawnSync(process.execPath, [bundlePath], { - cwd: fixture.root, - encoding: 'utf-8', - env: { PAGE_LANGUAGE: language }, - }); - const english = renderLanguage('en'); - const czech = renderLanguage('cs'); - assert.equal(english.status, 0, english.stderr); - assert.equal(czech.status, 0, czech.stderr); - assert.match(english.stdout, />New Page<\/h1>/u); - assert.match(czech.stdout, />Nová stránka<\/h1>/u); - }); -}); - -test('adds further pages after generated owner files have been formatted', async () => { - await withFixture(async (fixture) => { - const formattedOwnerPaths = [ - inventoryManifestFile, - inventoryRegistrationFile, - shellVerticalClientsFile, - ] as const; - const formatOwners = async (): Promise => { - await Promise.all( - formattedOwnerPaths.map(async (relativePath) => { - const filePath = path.join(fixture.root, relativePath); - const formatted = spawnSync(oxfmtPath, [`--stdin-filepath=${relativePath}`], { - cwd: appRoot, + 'utf-8', + ), + ); + const bundlePath = path.join(fixture.root, 'render-generated-page.cjs'); + const bundle = spawnSync( + esbuildPath, + [ + runnerPath, + '--bundle', + '--format=cjs', + '--jsx=automatic', + '--platform=node', + `--outfile=${bundlePath}`, + ], + { cwd: fixture.root, encoding: 'utf-8' }, + ); + expect(bundle.status, bundle.stderr || bundle.error?.message).toBe(0); + const renderLanguage = (language: 'cs' | 'en') => + spawnSync(process.execPath, [bundlePath], { + cwd: fixture.root, encoding: 'utf-8', - input: await readFile(filePath, 'utf-8'), + env: { PAGE_LANGUAGE: language }, }); - assert.equal(formatted.status, 0, formatted.stderr); - await writeFile(filePath, formatted.stdout, 'utf-8'); - }), - ); - }; + const english = renderLanguage('en'); + const czech = renderLanguage('cs'); + expect(english.status, english.stderr).toBe(0); + expect(czech.status, czech.stderr).toBe(0); + expect(english.stdout).toMatch(/>New Page<\/h1>/u); + expect(czech.stdout).toMatch(/>Nová stránka<\/h1>/u); + }), + ); + }), +); - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - fixtureName.purchaseOrdersPage, - ]); - await formatOwners(); +it.live( + 'adds further pages after generated owner files have been formatted', + Effect.fn(function* scenario85() { + yield* withFixture( + Effect.fn(function* scenario86(fixture) { + const formattedOwnerPaths = [ + inventoryManifestFile, + inventoryRegistrationFile, + shellVerticalClientsFile, + ] as const; + const formatOwners = Effect.fn(function* scenario87() { + for (const relativePath of formattedOwnerPaths) { + const filePath = path.join(fixture.root, relativePath); + const formatted = spawnSync(oxfmtPath, [`--stdin-filepath=${relativePath}`], { + cwd: appRoot, + encoding: 'utf-8', + input: yield* Effect.promise(() => readFile(filePath, 'utf-8')), + }); + expect(formatted.status, formatted.stderr).toBe(0); + yield* Effect.promise(() => writeFile(filePath, formatted.stdout, 'utf-8')); + } + }); - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'customers', - ]); - await formatOwners(); + yield* run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.purchaseOrdersPage, + ]); + yield* formatOwners(); - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'customer-notes', - ]); + yield* run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'customers', + ]); + yield* formatOwners(); - const manifest = await readFixtureFile(fixture.root, inventoryManifestFile); - const registration = await readFixtureFile(fixture.root, inventoryRegistrationFile); - const shellClients = await readFixtureFile(fixture.root, shellVerticalClientsFile); - await Promise.all( - ['customer-notes', 'customers', fixtureName.purchaseOrdersPage].map(async (page) => { - assert.match(manifest, new RegExp(`inventory\\.stock\\.page\\.${page}`, 'u')); - assert.match(registration, new RegExp(`'page-${page}'`, 'u')); - assert.match(shellClients, new RegExp(`inventory\\.stock\\.page-${page}`, 'u')); - await stat( - path.join( - fixture.root, - `verticals/inventory-stock/src/routes/[lang]/inventory-stock/${page}/page.tsx`, + yield* run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'customer-notes', + ]); + + const manifest = yield* readFixtureFile(fixture.root, inventoryManifestFile); + const registration = yield* readFixtureFile(fixture.root, inventoryRegistrationFile); + const shellClients = yield* readFixtureFile(fixture.root, shellVerticalClientsFile); + yield* Effect.all( + ['customer-notes', 'customers', fixtureName.purchaseOrdersPage].map( + Effect.fn(function* scenario89(page) { + expect(manifest).toMatch(new RegExp(`inventory\\.stock\\.page\\.${page}`, 'u')); + expect(registration).toMatch(new RegExp(`'page-${page}'`, 'u')); + expect(shellClients).toMatch(new RegExp(`inventory\\.stock\\.page-${page}`, 'u')); + yield* Effect.promise(() => + stat( + path.join( + fixture.root, + `verticals/inventory-stock/src/routes/[lang]/inventory-stock/${page}/page.tsx`, + ), + ), + ); + }), ), + { concurrency: 'unbounded' }, ); }), ); - }); -}); + }), +); -test('supports an explicit nested page URL and rejects unsafe URL inputs atomically', async () => { - await withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - fixtureName.purchaseOrdersPage, - '--url', - purchasingOrdersUrl, - ]); - const page = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/purchasing/orders/page.tsx', - ); - assert.match(page, /from '\.\.\/\.\.\/\.\.\/ultramodern-route-head'/u); - const manifest = await readFixtureFile(fixture.root, inventoryManifestFile); - assert.match(manifest, /routePath: '\/purchasing\/orders'/u); - assert.match( - await readFixtureFile( - fixture.root, - 'apps/shell-super-app/src/routes/[lang]/purchasing/orders/page.data.ts', - ), - /entrypointKey: 'inventory\.stock\.page\.purchase-orders'/u, - ); - const beforeRerun = await snapshotTree(fixture.root); - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - fixtureName.purchaseOrdersPage, - '--url', - purchasingOrdersUrl, - ]); - assert.deepEqual(await snapshotTree(fixture.root), beforeRerun); - await assert.rejects( - run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - fixtureName.purchaseOrdersPage, - '--url', - '/different/orders', - ]), - /already exists at another URL/u, - ); - await assert.rejects( - run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'different-page', - '--url', - purchasingOrdersUrl, - ]), - /already exists|collides/u, +it.live( + 'supports an explicit nested page URL and rejects unsafe URL inputs atomically', + Effect.fn(function* scenario90() { + yield* withFixture( + Effect.fn(function* scenario91(fixture) { + yield* run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.purchaseOrdersPage, + '--url', + purchasingOrdersUrl, + ]); + const page = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/routes/[lang]/purchasing/orders/page.tsx', + ); + expect(page).toMatch(/from '\.\.\/\.\.\/\.\.\/ultramodern-route-head'/u); + const manifest = yield* readFixtureFile(fixture.root, inventoryManifestFile); + expect(manifest).toMatch(/routePath: '\/purchasing\/orders'/u); + expect( + yield* readFixtureFile( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/purchasing/orders/page.data.ts', + ), + ).toMatch(/entrypointKey: 'inventory\.stock\.page\.purchase-orders'/u); + const beforeRerun = yield* snapshotTree(fixture.root); + yield* run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.purchaseOrdersPage, + '--url', + purchasingOrdersUrl, + ]); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeRerun); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.purchaseOrdersPage, + '--url', + '/different/orders', + ]), + (error) => expect(String(error)).toMatch(/already exists at another URL/u), + ); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'different-page', + '--url', + purchasingOrdersUrl, + ]), + (error) => expect(String(error)).toMatch(/already exists|collides/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeRerun); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), beforeRerun); - }); - await withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'orders', - '--url', - '/orders', - ]); - await stat(path.join(fixture.root, inventoryOrdersRouteFile)); - assert.match( - await readFixtureFile(fixture.root, inventoryManifestFile), - /routePath: '\/orders'/u, + yield* withFixture( + Effect.fn(function* scenario92(fixture) { + yield* run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'orders', + '--url', + '/orders', + ]); + yield* Effect.promise(() => stat(path.join(fixture.root, inventoryOrdersRouteFile))); + expect(yield* readFixtureFile(fixture.root, inventoryManifestFile)).toMatch( + /routePath: '\/orders'/u, + ); + }), ); - }); - await Promise.all( - [ - '/cs/orders', - '/de/orders', - '/en-us/orders', - '/orders/', - '/Orders', - '/orders?state=open', - '/orders#open', - '/%2e%2e/orders', - 'https://example.test/orders', - ].map( - async (url) => - await withFixture(async (fixture) => { - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - [scaffoldFlag.vertical, inventorySlug, '--page', 'orders', '--url', url], - /--url/u, + yield* Effect.all( + [ + '/cs/orders', + '/de/orders', + '/en-us/orders', + '/orders/', + '/Orders', + '/orders?state=open', + '/orders#open', + '/%2e%2e/orders', + 'https://example.test/orders', + ].map( + Effect.fn(function* scenario93(url) { + return yield* withFixture( + Effect.fn(function* scenario94(fixture) { + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + [scaffoldFlag.vertical, inventorySlug, '--page', 'orders', '--url', url], + /--url/u, + ); + }), ); }), - ), - ); -}); + ), + { concurrency: 'unbounded' }, + ); + }), +); -test('generates a non-navigational dynamic page with canonical parameters and router directories', async () => { - await withFixture(async (fixture) => { - await writeFixtureFile( - fixture.root, - inventoryFederationConfigFile, - `export default { +it.live( + 'generates a non-navigational dynamic page with canonical parameters and router directories', + Effect.fn(function* scenario95() { + yield* withFixture( + Effect.fn(function* scenario96(fixture) { + yield* write( + fixture.root, + inventoryFederationConfigFile, + `export default { exposes: { './PageInventoryStock': './src/federation-entry.tsx', }, @@ -4858,539 +5219,630 @@ test('generates a non-navigational dynamic page with canonical parameters and ro }, }; `, + ); + const generatorArguments = [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.customerEditPage, + '--url', + '/contacts/customers/:id/edit', + ]; + yield* run(fixture, scaffoldCommand.microverticalPage, generatorArguments); + + const ownerRoute = + 'verticals/inventory-stock/src/routes/[lang]/contacts/customers/[id]/edit'; + const shellRoute = 'apps/shell-super-app/src/routes/[lang]/contacts/customers/[id]/edit'; + const page = yield* readFixtureFile(fixture.root, `${ownerRoute}/page.tsx`); + const ownerMetadata = yield* readFixtureFile(fixture.root, `${ownerRoute}/route.meta.ts`); + const shellLoader = yield* readFixtureFile(fixture.root, `${shellRoute}/page.data.ts`); + const shellMetadata = yield* readFixtureFile(fixture.root, `${shellRoute}/route.meta.ts`); + const manifest = yield* readFixtureFile(fixture.root, inventoryManifestFile); + const registration = yield* readFixtureFile(fixture.root, inventoryRegistrationFile); + const federation = yield* readFixtureFile(fixture.root, inventoryFederationConfigFile); + const federatedPage = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/federation/page-customer-edit.tsx', + ); + const shellClients = yield* readFixtureFile(fixture.root, shellVerticalClientsFile); + + expect(page).toMatch(/export const CustomerEditPageRouteParams = Schema\.Struct/u); + expect(page).toMatch( + /id: Schema\.String\.pipe\(Schema\.brand\('CustomerEditPageIdRouteParameter'\)\)/u, + ); + expect(page).toMatch( + /export type CustomerEditPageRouteParams = typeof CustomerEditPageRouteParams\.Type/u, + ); + expect(page).toMatch(/Schema\.toStandardSchemaV1\(\s*CustomerEditPageRouteParams,?\s*\)/u); + expect(page).toMatch(/CustomerEditPage = \(\{ routeParams \}/u); + expect(page).toMatch(/void routeParams;/u); + expect(ownerMetadata).toMatch(/canonicalPath: '\/contacts\/customers\/:id\/edit'/u); + expect(ownerMetadata).toMatch(/en: '\/contacts\/customers\/:id\/edit'/u); + expect(shellMetadata).toMatch(/canonicalPath: '\/contacts\/customers\/:id\/edit'/u); + expect(manifest).toMatch(/routePath: '\/contacts\/customers\/:id\/edit'/u); + expect(manifest).toMatch(/inventory\.stock\.page\.customer-edit/u); + expect(manifest).not.toMatch(/inventory\.stock\.navigation\.customer-edit/u); + expect(registration).toMatch(/'page-customer-edit'/u); + expect(federation).toMatch(/'\.\/PageCustomerEdit'/u); + expect(federatedPage).toMatch(/type CustomerEditPageRouteParams/u); + expect(federatedPage).not.toMatch(/Schema\.Struct/u); + expect(federatedPage).toMatch(//u); + expect(shellClients).toMatch(/inventory\.stock\.page-customer-edit/u); + expect(shellLoader).toMatch(/selectRouteParams/u); + expect(shellLoader).toMatch(/const routeParameterNames = \['id'\] as const;/u); + expect(shellLoader).toMatch( + /routeParams: selectRouteParams\(params, routeParameterNames\)/u, + ); + expect(yield* readFixtureFile(fixture.root, inventoryEnglishLocaleFile)).toMatch( + /"customerEdit"/u, + ); + expect( + yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/locales/cs/inventory.json', + ), + ).toMatch(/"customerEdit"/u); + + const afterFirstRun = yield* snapshotTree(fixture.root); + yield* run(fixture, scaffoldCommand.microverticalPage, generatorArguments); + expect(yield* snapshotTree(fixture.root)).toEqual(afterFirstRun); + }), ); + }), +); + +it.live( + 'generates the Contacts Contact-detail two-parameter page atomically and safely reruns it', + Effect.fn(function* scenario97() { const generatorArguments = [ scaffoldFlag.vertical, inventorySlug, '--page', - fixtureName.customerEditPage, + 'contact-detail', '--url', - '/contacts/customers/:id/edit', + '/contacts/customers/:id/contacts/:contactId', ]; - await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); - - const ownerRoute = 'verticals/inventory-stock/src/routes/[lang]/contacts/customers/[id]/edit'; - const shellRoute = 'apps/shell-super-app/src/routes/[lang]/contacts/customers/[id]/edit'; - const page = await readFixtureFile(fixture.root, `${ownerRoute}/page.tsx`); - const ownerMetadata = await readFixtureFile(fixture.root, `${ownerRoute}/route.meta.ts`); - const shellLoader = await readFixtureFile(fixture.root, `${shellRoute}/page.data.ts`); - const shellMetadata = await readFixtureFile(fixture.root, `${shellRoute}/route.meta.ts`); - const manifest = await readFixtureFile(fixture.root, inventoryManifestFile); - const registration = await readFixtureFile(fixture.root, inventoryRegistrationFile); - const federation = await readFixtureFile(fixture.root, inventoryFederationConfigFile); - const federatedPage = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/federation/page-customer-edit.tsx', - ); - const shellClients = await readFixtureFile(fixture.root, shellVerticalClientsFile); - assert.match(page, /export const CustomerEditPageRouteParams = Schema\.Struct/u); - assert.match( - page, - /id: Schema\.String\.pipe\(Schema\.brand\('CustomerEditPageIdRouteParameter'\)\)/u, - ); - assert.match( - page, - /export type CustomerEditPageRouteParams = typeof CustomerEditPageRouteParams\.Type/u, - ); - assert.match(page, /Schema\.toStandardSchemaV1\(\s*CustomerEditPageRouteParams,?\s*\)/u); - assert.match(page, /CustomerEditPage = \(\{ routeParams \}/u); - assert.match(page, /void routeParams;/u); - assert.match(ownerMetadata, /canonicalPath: '\/contacts\/customers\/:id\/edit'/u); - assert.match(ownerMetadata, /en: '\/contacts\/customers\/:id\/edit'/u); - assert.match(shellMetadata, /canonicalPath: '\/contacts\/customers\/:id\/edit'/u); - assert.match(manifest, /routePath: '\/contacts\/customers\/:id\/edit'/u); - assert.match(manifest, /inventory\.stock\.page\.customer-edit/u); - assert.doesNotMatch(manifest, /inventory\.stock\.navigation\.customer-edit/u); - assert.match(registration, /'page-customer-edit'/u); - assert.match(federation, /'\.\/PageCustomerEdit'/u); - assert.match(federatedPage, /type CustomerEditPageRouteParams/u); - assert.doesNotMatch(federatedPage, /Schema\.Struct/u); - assert.match(federatedPage, //u); - assert.match(shellClients, /inventory\.stock\.page-customer-edit/u); - assert.match(shellLoader, /selectRouteParams/u); - assert.match(shellLoader, /const routeParameterNames = \['id'\] as const;/u); - assert.match(shellLoader, /routeParams: selectRouteParams\(params, routeParameterNames\)/u); - assert.match( - await readFixtureFile(fixture.root, inventoryEnglishLocaleFile), - /"customerEdit"/u, - ); - assert.match( - await readFixtureFile(fixture.root, 'verticals/inventory-stock/locales/cs/inventory.json'), - /"customerEdit"/u, - ); + yield* withFixture( + Effect.fn(function* scenario98(fixture) { + const ownerRoute = + 'verticals/inventory-stock/src/routes/[lang]/contacts/customers/[id]/contacts/[contactId]'; + const shellRoute = + 'apps/shell-super-app/src/routes/[lang]/contacts/customers/[id]/contacts/[contactId]'; - const afterFirstRun = await snapshotTree(fixture.root); - await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); - assert.deepEqual(await snapshotTree(fixture.root), afterFirstRun); - }); -}); + yield* run(fixture, scaffoldCommand.microverticalPage, generatorArguments); -test('generates the Contacts Contact-detail two-parameter page atomically and safely reruns it', async () => { - const generatorArguments = [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'contact-detail', - '--url', - '/contacts/customers/:id/contacts/:contactId', - ]; - - await withFixture(async (fixture) => { - const ownerRoute = - 'verticals/inventory-stock/src/routes/[lang]/contacts/customers/[id]/contacts/[contactId]'; - const shellRoute = - 'apps/shell-super-app/src/routes/[lang]/contacts/customers/[id]/contacts/[contactId]'; - - await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); - - const page = await readFixtureFile(fixture.root, `${ownerRoute}/page.tsx`); - const ownerMetadata = await readFixtureFile(fixture.root, `${ownerRoute}/route.meta.ts`); - const shellLoader = await readFixtureFile(fixture.root, `${shellRoute}/page.data.ts`); - const shellMetadata = await readFixtureFile(fixture.root, `${shellRoute}/route.meta.ts`); - const manifest = await readFixtureFile(fixture.root, inventoryManifestFile); - - assert.match(page, /export const ContactDetailPageRouteParams = Schema\.Struct/u); - assert.match( - page, - /id: Schema\.String\.pipe\(Schema\.brand\('ContactDetailPageIdRouteParameter'\)\)/u, - ); - assert.match( - page, - /contactId: Schema\.String\.pipe\(Schema\.brand\('ContactDetailPageContactIdRouteParameter'\)\)/u, - ); - assert.match( - page, - /export type ContactDetailPageRouteParams = typeof ContactDetailPageRouteParams\.Type/u, - ); - assert.match(page, /Schema\.toStandardSchemaV1\(\s*ContactDetailPageRouteParams,?\s*\)/u); - assert.match( - ownerMetadata, - /canonicalPath: '\/contacts\/customers\/:id\/contacts\/:contactId'/u, - ); - assert.match( - shellMetadata, - /canonicalPath: '\/contacts\/customers\/:id\/contacts\/:contactId'/u, + const page = yield* readFixtureFile(fixture.root, `${ownerRoute}/page.tsx`); + const ownerMetadata = yield* readFixtureFile(fixture.root, `${ownerRoute}/route.meta.ts`); + const shellLoader = yield* readFixtureFile(fixture.root, `${shellRoute}/page.data.ts`); + const shellMetadata = yield* readFixtureFile(fixture.root, `${shellRoute}/route.meta.ts`); + const manifest = yield* readFixtureFile(fixture.root, inventoryManifestFile); + + expect(page).toMatch(/export const ContactDetailPageRouteParams = Schema\.Struct/u); + expect(page).toMatch( + /id: Schema\.String\.pipe\(Schema\.brand\('ContactDetailPageIdRouteParameter'\)\)/u, + ); + expect(page).toMatch( + /contactId: Schema\.String\.pipe\(Schema\.brand\('ContactDetailPageContactIdRouteParameter'\)\)/u, + ); + expect(page).toMatch( + /export type ContactDetailPageRouteParams = typeof ContactDetailPageRouteParams\.Type/u, + ); + expect(page).toMatch(/Schema\.toStandardSchemaV1\(\s*ContactDetailPageRouteParams,?\s*\)/u); + expect(ownerMetadata).toMatch( + /canonicalPath: '\/contacts\/customers\/:id\/contacts\/:contactId'/u, + ); + expect(shellMetadata).toMatch( + /canonicalPath: '\/contacts\/customers\/:id\/contacts\/:contactId'/u, + ); + expect(manifest).toMatch(/routePath: '\/contacts\/customers\/:id\/contacts\/:contactId'/u); + expect(manifest).toMatch(/inventory\.stock\.page\.contact-detail/u); + expect(manifest).not.toMatch(/inventory\.stock\.navigation\.contact-detail/u); + expect(shellLoader).toMatch(/const routeParameterNames = \['id', 'contactId'\] as const;/u); + expect(shellLoader).toMatch( + /routeParams: selectRouteParams\(params, routeParameterNames\)/u, + ); + yield* Effect.promise(() => stat(path.join(fixture.root, ownerRoute))); + yield* Effect.promise(() => stat(path.join(fixture.root, shellRoute))); + + const afterFirstRun = yield* snapshotTree(fixture.root); + yield* run(fixture, scaffoldCommand.microverticalPage, generatorArguments); + expect(yield* snapshotTree(fixture.root)).toEqual(afterFirstRun); + }), ); - assert.match(manifest, /routePath: '\/contacts\/customers\/:id\/contacts\/:contactId'/u); - assert.match(manifest, /inventory\.stock\.page\.contact-detail/u); - assert.doesNotMatch(manifest, /inventory\.stock\.navigation\.contact-detail/u); - assert.match(shellLoader, /const routeParameterNames = \['id', 'contactId'\] as const;/u); - assert.match(shellLoader, /routeParams: selectRouteParams\(params, routeParameterNames\)/u); - await stat(path.join(fixture.root, ownerRoute)); - await stat(path.join(fixture.root, shellRoute)); - - const afterFirstRun = await snapshotTree(fixture.root); - await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); - assert.deepEqual(await snapshotTree(fixture.root), afterFirstRun); - }); - await withFixture(async (fixture) => { - await writeFixtureFile( - fixture.root, - 'apps/shell-super-app/src/routes/[lang]/contacts/customers/[id]/contacts/[contactId]/page.tsx', - 'export default function DeveloperOwnedPage() { return null; }\n', + yield* withFixture( + Effect.fn(function* scenario99(fixture) { + yield* write( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/contacts/customers/[id]/contacts/[contactId]/page.tsx', + 'export default function DeveloperOwnedPage() { return null; }\n', + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + generatorArguments, + /refusing to overwrite|already exists/u, + ); + }), ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - generatorArguments, - /refusing to overwrite|already exists/u, + }), +); + +it.live( + 'rejects unsafe dynamic parameters and dynamic route collisions without writing', + Effect.fn(function* scenario100() { + yield* Effect.all( + [ + '/inventory/customers/:1id/edit', + '/inventory/customers/:customer-id/edit', + '/inventory/customers/:id?/edit', + '/inventory/customers/:id*/edit', + '/inventory/customers/*id/edit', + '/inventory/customers/[id]/edit', + '/inventory/customers/:id/edit/:id', + '/inventory/customers/%2e%2e/:id', + '/cs/inventory/customers/:id', + ].map( + Effect.fn(function* scenario101(url) { + return yield* withFixture( + Effect.fn(function* scenario102(fixture) { + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.customerEditPage, + '--url', + url, + ], + /--url/u, + ); + }), + ); + }), + ), + { concurrency: 'unbounded' }, ); - }); -}); -test('rejects unsafe dynamic parameters and dynamic route collisions without writing', async () => { - await Promise.all( - [ - '/inventory/customers/:1id/edit', - '/inventory/customers/:customer-id/edit', - '/inventory/customers/:id?/edit', - '/inventory/customers/:id*/edit', - '/inventory/customers/*id/edit', - '/inventory/customers/[id]/edit', - '/inventory/customers/:id/edit/:id', - '/inventory/customers/%2e%2e/:id', - '/cs/inventory/customers/:id', - ].map( - async (url) => - await withFixture(async (fixture) => { - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - [ + yield* Effect.all( + [ + withFixture( + Effect.fn(function* scenario103(fixture) { + yield* run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'customer-detail', + '--url', + customerDetailUrl, + ]); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.customerEditPage, + '--url', + '/inventory/customers/:customerId', + ], + /routing collision|already registered|collides/u, + ); + }), + ), + withFixture( + Effect.fn(function* scenario104(fixture) { + const generatorArguments = [ scaffoldFlag.vertical, inventorySlug, '--page', fixtureName.customerEditPage, '--url', - url, - ], - /--url/u, - ); - }), - ), - ); + customerEditUrl, + ]; + yield* run(fixture, scaffoldCommand.microverticalPage, generatorArguments); + const pagePath = path.join( + fixture.root, + 'verticals/inventory-stock/src/routes/[lang]/inventory/customers/[id]/edit/page.tsx', + ); + const pageSource = yield* Effect.promise(() => readFile(pagePath, 'utf-8')); + yield* Effect.promise(() => + writeFile(pagePath, `${pageSource}\n// developer edit\n`, 'utf-8'), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + generatorArguments, + /collides/u, + ); + }), + ), + withFixture( + Effect.fn(function* scenario105(fixture) { + yield* write( + fixture.root, + 'verticals/inventory-stock/src/routes/[lang]/inventory/customers/[id]/edit/page.tsx', + 'export default function PartialPage() { return null; }\n', + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.customerEditPage, + '--url', + customerEditUrl, + ], + /collides with nested content/u, + ); + }), + ), + withFixture( + Effect.fn(function* scenario106(fixture) { + yield* run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'customer-new', + '--url', + '/inventory/customers/new', + ]); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.customerEditPage, + '--url', + customerDetailUrl, + ], + /static route segment|collides/u, + ); + }), + ), + withFixture( + Effect.fn(function* scenario107(fixture) { + yield* run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + 'billing', + '--page', + fixtureName.customerEditPage, + '--url', + '/shared/customers/:id/edit', + ]); + yield* Effect.promise(() => + rm( + path.join( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/shared/customers/[id]/edit', + ), + { recursive: true }, + ), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.customerEditPage, + '--url', + '/shared/customers/:id/edit', + ], + /already registered by billing/u, + ); + }), + ), + ], + { concurrency: 'unbounded' }, + ); + }), +); - await Promise.all([ - withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'customer-detail', - '--url', - customerDetailUrl, - ]); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - fixtureName.customerEditPage, - '--url', - '/inventory/customers/:customerId', - ], - /routing collision|already registered|collides/u, - ); - }), - withFixture(async (fixture) => { - const generatorArguments = [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - fixtureName.customerEditPage, - '--url', - customerEditUrl, - ]; - await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); - const pagePath = path.join( - fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/inventory/customers/[id]/edit/page.tsx', - ); - await writeFile( - pagePath, - `${await readFile(pagePath, 'utf-8')}\n// developer edit\n`, - 'utf-8', - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - generatorArguments, - /collides/u, - ); - }), - withFixture(async (fixture) => { - await writeFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/inventory/customers/[id]/edit/page.tsx', - 'export default function PartialPage() { return null; }\n', - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - fixtureName.customerEditPage, - '--url', - customerEditUrl, - ], - /collides with nested content/u, - ); - }), - withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'customer-new', - '--url', - '/inventory/customers/new', - ]); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - [ +it.live( + 'extends an existing dynamic route branch without reclassifying an existing static sibling', + Effect.fn(function* scenario108() { + yield* withFixture( + Effect.fn(function* scenario109(fixture) { + yield* run(fixture, scaffoldCommand.microverticalPage, [ scaffoldFlag.vertical, inventorySlug, '--page', - fixtureName.customerEditPage, + 'customer-detail', '--url', customerDetailUrl, - ], - /static route segment|collides/u, - ); - }), - withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - 'billing', - '--page', - fixtureName.customerEditPage, - '--url', - '/shared/customers/:id/edit', - ]); - await rm( - path.join( + ]); + yield* write( fixture.root, - 'apps/shell-super-app/src/routes/[lang]/shared/customers/[id]/edit', - ), - { recursive: true }, - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - [ + 'apps/shell-super-app/src/routes/[lang]/inventory/customers/new/page.tsx', + 'export default function ExistingStaticSibling() { return null; }\n', + ); + + yield* run(fixture, scaffoldCommand.microverticalPage, [ scaffoldFlag.vertical, inventorySlug, '--page', fixtureName.customerEditPage, '--url', - '/shared/customers/:id/edit', - ], - /already registered by billing/u, - ); - }), - ]); -}); - -test('extends an existing dynamic route branch without reclassifying an existing static sibling', async () => { - await withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'customer-detail', - '--url', - customerDetailUrl, - ]); - await writeFixtureFile( - fixture.root, - 'apps/shell-super-app/src/routes/[lang]/inventory/customers/new/page.tsx', - 'export default function ExistingStaticSibling() { return null; }\n', + customerEditUrl, + ]); + + yield* Effect.promise(() => + stat( + path.join( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/inventory/customers/[id]/edit/page.tsx', + ), + ), + ); + }), ); + }), +); - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - fixtureName.customerEditPage, - '--url', - customerEditUrl, - ]); - - await stat( - path.join( - fixture.root, - 'apps/shell-super-app/src/routes/[lang]/inventory/customers/[id]/edit/page.tsx', - ), +it.live( + 'rejects reserved, dynamic, and cross-owner page URLs before writing', + Effect.fn(function* scenario110() { + yield* Effect.all( + [ + withFixture( + Effect.fn(function* scenario111(fixture) { + yield* write( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/modules/[moduleId]/page.tsx', + 'export default function ModulePage() { return null; }\n', + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'customers', + '--url', + '/modules/customers', + ], + /collides with dynamic route segment \[moduleId\]/u, + ); + }), + ), + withFixture( + Effect.fn(function* scenario112(fixture) { + yield* write( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/login/page.tsx', + 'export default function LoginPage() { return null; }\n', + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'customers', + '--url', + '/login/customers', + ], + /reserved route prefix \/login/u, + ); + }), + ), + withFixture( + Effect.fn(function* scenario113(fixture) { + yield* run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + 'billing', + '--page', + 'customers', + '--url', + '/shared/customers', + ]); + yield* Effect.promise(() => + rm( + path.join(fixture.root, 'apps/shell-super-app/src/routes/[lang]/shared/customers'), + { + recursive: true, + }, + ), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'customer-list', + '--url', + '/shared/customers', + ], + /already registered by billing/u, + ); + }), + ), + ], + { concurrency: 'unbounded' }, ); - }); -}); + }), +); -test('rejects reserved, dynamic, and cross-owner page URLs before writing', async () => { - await Promise.all([ - withFixture(async (fixture) => { - await writeFixtureFile( - fixture.root, - 'apps/shell-super-app/src/routes/[lang]/modules/[moduleId]/page.tsx', - 'export default function ModulePage() { return null; }\n', - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - [ +it.live( + 'uses exact page identities and rejects edited generated wiring', + Effect.fn(function* scenario114() { + yield* withFixture( + Effect.fn(function* scenario115(fixture) { + yield* run(fixture, scaffoldCommand.microverticalPage, [ scaffoldFlag.vertical, inventorySlug, '--page', - 'customers', - '--url', - '/modules/customers', - ], - /collides with dynamic route segment \[moduleId\]/u, - ); - }), - withFixture(async (fixture) => { - await writeFixtureFile( - fixture.root, - 'apps/shell-super-app/src/routes/[lang]/login/page.tsx', - 'export default function LoginPage() { return null; }\n', - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - [scaffoldFlag.vertical, inventorySlug, '--page', 'customers', '--url', '/login/customers'], - /reserved route prefix \/login/u, - ); - }), - withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - 'billing', - '--page', - 'customers', - '--url', - '/shared/customers', - ]); - await rm(path.join(fixture.root, 'apps/shell-super-app/src/routes/[lang]/shared/customers'), { - recursive: true, - }); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - [ + 'order-lines', + ]); + yield* run(fixture, scaffoldCommand.microverticalPage, [ scaffoldFlag.vertical, inventorySlug, '--page', - 'customer-list', - '--url', - '/shared/customers', - ], - /already registered by billing/u, - ); - }), - ]); -}); - -test('uses exact page identities and rejects edited generated wiring', async () => { - await withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'order-lines', - ]); - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'order', - ]); - await stat( - path.join( - fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/order/page.tsx', - ), + 'order', + ]); + yield* Effect.promise(() => + stat( + path.join( + fixture.root, + 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/order/page.tsx', + ), + ), + ); + }), ); - }); - await Promise.all([ - withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'orders', - '--url', - '/first/orders', - ]); - const manifestPath = path.join(fixture.root, inventoryManifestFile); - const manifest = await readFile(manifestPath, 'utf-8'); - await writeFile( - manifestPath, - manifest - .replaceAll("'page-orders'", '"page-orders"') - .replaceAll("'inventory.stock.page.orders'", '"inventory.stock.page.orders"'), - 'utf-8', - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - [scaffoldFlag.vertical, inventorySlug, '--page', 'orders', '--url', '/second/orders'], - /page identity inventory\.stock\.page\.orders already exists/u, - ); - }), - withFixture(async (fixture) => { - const generatorArguments = [scaffoldFlag.vertical, inventorySlug, '--page', 'orders']; - await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); - const manifestPath = path.join(fixture.root, inventoryManifestFile); - const manifest = await readFile(manifestPath, 'utf-8'); - await writeFile(manifestPath, manifest.replace('order: 100', 'order: 101'), 'utf-8'); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - generatorArguments, - /already exists|collides/u, - ); - }), - withFixture(async (fixture) => { - const generatorArguments = [scaffoldFlag.vertical, inventorySlug, '--page', 'orders']; - await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); - const manifestPath = path.join(fixture.root, inventoryManifestFile); - const manifest = await readFile(manifestPath, 'utf-8'); - await writeFile( - manifestPath, - manifest.replace( - '// ', - `{ contributionKey : "inventory.stock.navigation.orders", entrypoint: { access: 'read', entrypointKey: 'inventory.stock.page.orders', moduleKey: 'inventory.stock', role: 'page', scope: 'tenant' }, groupKey: 'shell.navigation.modules', order: 101, pageKey: 'inventory.stock.page.orders' }, + yield* Effect.all( + [ + withFixture( + Effect.fn(function* scenario116(fixture) { + yield* run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'orders', + '--url', + '/first/orders', + ]); + const manifestPath = path.join(fixture.root, inventoryManifestFile); + const manifest = yield* Effect.promise(() => readFile(manifestPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + manifestPath, + manifest + .replaceAll("'page-orders'", '"page-orders"') + .replaceAll("'inventory.stock.page.orders'", '"inventory.stock.page.orders"'), + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + [scaffoldFlag.vertical, inventorySlug, '--page', 'orders', '--url', '/second/orders'], + /page identity inventory\.stock\.page\.orders already exists/u, + ); + }), + ), + withFixture( + Effect.fn(function* scenario117(fixture) { + const generatorArguments = [scaffoldFlag.vertical, inventorySlug, '--page', 'orders']; + yield* run(fixture, scaffoldCommand.microverticalPage, generatorArguments); + const manifestPath = path.join(fixture.root, inventoryManifestFile); + const manifest = yield* Effect.promise(() => readFile(manifestPath, 'utf-8')); + yield* Effect.promise(() => + writeFile(manifestPath, manifest.replace('order: 100', 'order: 101'), 'utf-8'), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + generatorArguments, + /already exists|collides/u, + ); + }), + ), + withFixture( + Effect.fn(function* scenario118(fixture) { + const generatorArguments = [scaffoldFlag.vertical, inventorySlug, '--page', 'orders']; + yield* run(fixture, scaffoldCommand.microverticalPage, generatorArguments); + const manifestPath = path.join(fixture.root, inventoryManifestFile); + const manifest = yield* Effect.promise(() => readFile(manifestPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + manifestPath, + manifest.replace( + '// ', + `{ contributionKey : "inventory.stock.navigation.orders", entrypoint: { access: 'read', entrypointKey: 'inventory.stock.page.orders', moduleKey: 'inventory.stock', role: 'page', scope: 'tenant' }, groupKey: 'shell.navigation.modules', order: 101, pageKey: 'inventory.stock.page.orders' }, // `, + ), + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + generatorArguments, + /already exists|collides/u, + ); + }), ), - 'utf-8', - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - generatorArguments, - /already exists|collides/u, - ); - }), - withFixture(async (fixture) => { - const generatorArguments = [scaffoldFlag.vertical, inventorySlug, '--page', 'orders']; - await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); - const federationPath = path.join(fixture.root, inventoryFederationConfigFile); - const federation = await readFile(federationPath, 'utf-8'); - await writeFile( - federationPath, - federation.replace( - "'./src/federation/page-orders.tsx'", - "'./src/federation/page-other.tsx'", + withFixture( + Effect.fn(function* scenario119(fixture) { + const generatorArguments = [scaffoldFlag.vertical, inventorySlug, '--page', 'orders']; + yield* run(fixture, scaffoldCommand.microverticalPage, generatorArguments); + const federationPath = path.join(fixture.root, inventoryFederationConfigFile); + const federation = yield* Effect.promise(() => readFile(federationPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + federationPath, + federation.replace( + "'./src/federation/page-orders.tsx'", + "'./src/federation/page-other.tsx'", + ), + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + generatorArguments, + /already exists|collides/u, + ); + }), ), - 'utf-8', - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - generatorArguments, - /already exists|collides/u, - ); - }), - withFixture(async (fixture) => { - const generatorArguments = [scaffoldFlag.vertical, inventorySlug, '--page', 'orders']; - await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); - await writeFixtureFile( - fixture.root, - 'apps/shell-super-app/src/routes/[lang]/inventory-stock/orders/developer-note.ts', - 'export const developerNote = true;\n', - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - generatorArguments, - /already exists|collides/u, - ); - }), - ]); -}); + withFixture( + Effect.fn(function* scenario120(fixture) { + const generatorArguments = [scaffoldFlag.vertical, inventorySlug, '--page', 'orders']; + yield* run(fixture, scaffoldCommand.microverticalPage, generatorArguments); + yield* write( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/inventory-stock/orders/developer-note.ts', + 'export const developerNote = true;\n', + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + generatorArguments, + /already exists|collides/u, + ); + }), + ), + ], + { concurrency: 'unbounded' }, + ); + }), +); -test('rejects obsolete generated page output without changing files', async () => { - await withFixture(async (fixture) => { - const generatorArguments = [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'orders', - '--url', - '/orders', - ]; - await run(fixture, scaffoldCommand.microverticalPage, generatorArguments); - await writeFixtureFile( - fixture.root, - inventoryOrdersRouteFile, - `import { useModernI18n } from '@modern-js/plugin-i18n/runtime'; +it.live( + 'rejects obsolete generated page output without changing files', + Effect.fn(function* mergedScenario78() { + yield* withFixture( + Effect.fn(function* mergedScenario77(fixture) { + const generatorArguments = [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'orders', + '--url', + '/orders', + ]; + yield* run(fixture, scaffoldCommand.microverticalPage, generatorArguments); + yield* write( + fixture.root, + inventoryOrdersRouteFile, + `import { useModernI18n } from '@modern-js/plugin-i18n/runtime'; import { UltramodernRouteHead } from '../../ultramodern-route-head'; export const OrdersPage = () => { @@ -5427,11 +5879,11 @@ export const OrdersPage = () => { export default OrdersPage; `, - ); - await writeFixtureFile( - fixture.root, - 'apps/shell-super-app/src/routes/[lang]/orders/page.data.ts', - `import { loader as loadModuleTarget } from '../modules/[moduleId]/page.data.ts'; + ); + yield* write( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/orders/page.data.ts', + `import { loader as loadModuleTarget } from '../modules/[moduleId]/page.data.ts'; interface ShellPageLoaderArguments { readonly request: Request; @@ -5440,501 +5892,600 @@ interface ShellPageLoaderArguments { export const loader = ({ request }: ShellPageLoaderArguments) => loadModuleTarget({ params: { moduleId: 'inventory.stock' }, request }); `, - ); - await Promise.all( - ['cs', 'en'].map(async (locale) => { - const localePath = path.join( - fixture.root, - `verticals/inventory-stock/locales/${locale}/inventory.json`, - ); - const catalog = decodeInventoryLocale(await readFile(localePath, 'utf-8')); - const ordersPage = - locale === 'cs' - ? { - description: 'Tato stránka je připravena k implementaci.', - empty: 'Zatím zde není žádný obsah.', - title: 'Nová stránka', - } - : { - description: pagePlaceholder, - empty: 'No content has been added yet.', - title: 'New Page', - }; - const nextCatalog = Schema.decodeUnknownSync(Schema.Json)({ - ...catalog, - inventory: { - ...catalog.inventory, - pages: { ...catalog.inventory.pages, orders: ordersPage }, - }, - }); - await writeFile(localePath, json(nextCatalog), 'utf-8'); - }), - ); - - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - generatorArguments, - /page route already exists or collides/u, - ); - }); -}); + ); + yield* Effect.all( + ['cs', 'en'].map( + Effect.fn(function* mergedScenario76(locale) { + const localePath = path.join( + fixture.root, + `verticals/inventory-stock/locales/${locale}/inventory.json`, + ); + const catalog = yield* decodeInventoryLocale( + yield* Effect.promise(() => readFile(localePath, 'utf-8')), + ); + const ordersPage = + locale === 'cs' + ? { + description: 'Tato stránka je připravena k implementaci.', + empty: 'Zatím zde není žádný obsah.', + title: 'Nová stránka', + } + : { + description: pagePlaceholder, + empty: 'No content has been added yet.', + title: 'New Page', + }; + const nextCatalog = yield* Schema.decodeUnknownEffect(Schema.Json)({ + ...catalog, + inventory: { + ...catalog.inventory, + pages: { ...catalog.inventory.pages, orders: ordersPage }, + }, + }); + yield* Effect.promise(() => writeFile(localePath, json(nextCatalog), 'utf-8')); + }), + ), + { concurrency: 'unbounded' }, + ); -test('rejects page generation when an owning locale has no truthful starter translation', async () => { - await withFixture(async (fixture) => { - const packagePath = path.join(fixture.root, inventoryPackageFile); - const packageJson = decodeFixturePackage(await readFile(packagePath, 'utf-8')); - await writeFile( - packagePath, - json({ - ...packageJson, - exports: { ...packageJson.exports, './locales/de': './locales/de/inventory.json' }, + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + generatorArguments, + /page route already exists or collides/u, + ); }), - 'utf-8', - ); - await writeFixtureFile( - fixture.root, - 'verticals/inventory-stock/locales/de/inventory.json', - json({ inventory: { existing: 'de-preserved' } }), - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - [scaffoldFlag.vertical, inventorySlug, '--page', fixtureName.purchaseOrdersPage], - /no starter translation for locale de/u, - ); - }); -}); - -test('page prerequisite and nested-route failures are preflighted, while refresh failure is safely rerunnable', async () => { - await withFixture(async (fixture) => { - await rm( - path.join(fixture.root, 'verticals/inventory-stock/src/routes/ultramodern-route-head.tsx'), - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - [scaffoldFlag.vertical, inventorySlug, '--page', 'orders'], - /UltramodernRouteHead is missing/u, ); - }); + }), +); - await withFixture(async (fixture) => { - await writeFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/orders/nested.ts', - 'export {};\n', - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalPage, - [scaffoldFlag.vertical, inventorySlug, '--page', 'orders'], - /collides with nested content/u, +it.live( + 'rejects page generation when an owning locale has no truthful starter translation', + Effect.fn(function* scenario124() { + yield* withFixture( + Effect.fn(function* scenario125(fixture) { + const packagePath = path.join(fixture.root, inventoryPackageFile); + const packageJson = yield* decodeFixturePackage( + yield* Effect.promise(() => readFile(packagePath, 'utf-8')), + ); + yield* Effect.promise(() => + writeFile( + packagePath, + json({ + ...packageJson, + exports: { ...packageJson.exports, './locales/de': './locales/de/inventory.json' }, + }), + 'utf-8', + ), + ); + yield* write( + fixture.root, + 'verticals/inventory-stock/locales/de/inventory.json', + json({ inventory: { existing: 'de-preserved' } }), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + [scaffoldFlag.vertical, inventorySlug, '--page', fixtureName.purchaseOrdersPage], + /no starter translation for locale de/u, + ); + }), ); - }); + }), +); - await withFixture(async (fixture) => { - await assert.rejects( - runScaffold( - scaffoldCommand.microverticalPage, - [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'orders', - scaffoldFlag.authorization, - 'context_permission', - '--permission', - 'module.access', - ], - { - routeRefresh: () => { - throw new Error('route refresh fixture failure'); - }, - workspaceRoot: fixture.root, - }, - ), - /route refresh fixture failure/u, - ); - await stat( - path.join( - fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/orders/page.tsx', - ), - ); - const afterRefreshFailure = await snapshotTree(fixture.root); - const refreshes: string[] = []; - await run( - fixture, - scaffoldCommand.microverticalPage, - [scaffoldFlag.vertical, inventorySlug, '--page', 'orders'], - (appId) => { - refreshes.push(appId); - }, +it.live( + 'page prerequisite and nested-route failures are preflighted, while refresh failure is safely rerunnable', + Effect.fn(function* scenario126() { + yield* withFixture( + Effect.fn(function* scenario127(fixture) { + yield* Effect.promise(() => + rm( + path.join( + fixture.root, + 'verticals/inventory-stock/src/routes/ultramodern-route-head.tsx', + ), + ), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + [scaffoldFlag.vertical, inventorySlug, '--page', 'orders'], + /UltramodernRouteHead is missing/u, + ); + }), ); - assert.deepEqual(refreshes, [inventorySlug, shellAppId]); - assert.deepEqual(await snapshotTree(fixture.root), afterRefreshFailure); - }); -}); - -const runCombinedScenario = async (fixture: Fixture): Promise>> => { - await addInventoryItemResourceType(fixture); - await run(fixture, scaffoldCommand.microverticalActionBoundary, [ - scaffoldFlag.vertical, - inventorySlug, - ]); - await run(fixture, scaffoldCommand.externalHttpAdapter, [ - scaffoldFlag.vertical, - inventorySlug, - scaffoldFlag.provider, - 'warehouse-api', - scaffoldFlag.operation, - 'stock-level', - ]); - await run(fixture, 'action', [ - '--scope', - 'core', - '--module', - fixtureName.actionModule, - '--action', - 'change-tenant-state', - ]); - await run(fixture, 'action', [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - ]); - await run(fixture, scaffoldCommand.outboxMessage, [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - '--topic', - fixtureName.ordersCreated, - ]); - await run(fixture, 'policy', ['--scope', 'global', '--policy', fixtureName.policy]); - await run(fixture, 'policy', [ - '--scope', - 'microvertical', - '--policy', - 'stock-available', - scaffoldFlag.vertical, - inventorySlug, - ]); - await run(fixture, scaffoldCommand.moduleApi, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.resourceDetail, - ]); - await run(fixture, scaffoldCommand.searchProvider, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.inventoryItems, - scaffoldFlag.resource, - 'item', - ]); - await run(fixture, 'report', [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.stockLevels, - scaffoldFlag.resource, - 'item', - ]); - await run( - fixture, - scaffoldCommand.microverticalPage, - [scaffoldFlag.vertical, inventorySlug, '--page', 'orders'], - (appId) => assert.ok([inventorySlug, shellAppId].includes(appId)), - ); - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - fixtureName.customerEditPage, - '--url', - '/contacts/customers/:id/edit', - ]); - return await snapshotTree(fixture.root); -}; -test('all generators compose deterministically without crossing owner boundaries', async () => { - const first = await createFixture(); - const second = await createFixture(); - try { - const billingBefore = Object.fromEntries( - Object.entries(await snapshotTree(first.root)).filter(([file]) => - file.startsWith('verticals/billing/'), - ), + yield* withFixture( + Effect.fn(function* scenario128(fixture) { + yield* write( + fixture.root, + 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/orders/nested.ts', + 'export {};\n', + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalPage, + [scaffoldFlag.vertical, inventorySlug, '--page', 'orders'], + /collides with nested content/u, + ); + }), ); - const shellBefore = await readFixtureFile(first.root, shellSentinelFile); - const topologyBefore = await readFixtureFile(first.root, topologyFile); - const firstTree = await runCombinedScenario(first); - const secondTree = await runCombinedScenario(second); - assert.deepEqual(firstTree, secondTree); - const billingAfter = Object.fromEntries( - Object.entries(firstTree).filter(([file]) => file.startsWith('verticals/billing/')), + + yield* withFixture( + Effect.fn(function* scenario129(fixture) { + yield* expectFailure( + runScaffoldEffect( + scaffoldCommand.microverticalPage, + [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'orders', + scaffoldFlag.authorization, + 'context_permission', + '--permission', + 'module.access', + ], + { + routeRefresh: () => + Effect.fail(new ScaffoldingError({ message: 'route refresh fixture failure' })), + workspaceRoot: fixture.root, + }, + ).pipe(Effect.provide(NodeServices.layer)), + (error) => expect(String(error)).toMatch(/route refresh fixture failure/u), + ); + yield* Effect.promise(() => + stat( + path.join( + fixture.root, + 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/orders/page.tsx', + ), + ), + ); + const afterRefreshFailure = yield* snapshotTree(fixture.root); + const refreshes: string[] = []; + yield* run( + fixture, + scaffoldCommand.microverticalPage, + [scaffoldFlag.vertical, inventorySlug, '--page', 'orders'], + (appId) => { + refreshes.push(appId); + }, + ); + expect(refreshes).toEqual([inventorySlug, shellAppId]); + expect(yield* snapshotTree(fixture.root)).toEqual(afterRefreshFailure); + }), ); - assert.deepEqual(billingAfter, billingBefore); - assert.equal(await readFixtureFile(first.root, shellSentinelFile), shellBefore); - assert.equal(await readFixtureFile(first.root, topologyFile), topologyBefore); - const combinedSource = Object.values(firstTree).join('\n'); - assert.doesNotMatch(combinedSource, /from ['"]\.\.\/\.\.\/billing|fetch\(/u); - } finally { - await rm(first.root, { force: true, recursive: true }); - await rm(second.root, { force: true, recursive: true }); - } -}); + }), +); -test('every generated TypeScript file is already formatter-stable', async () => { - await withFixture(async (fixture) => { - await runCombinedScenario(fixture); - await run(fixture, scaffoldCommand.outboxWorker, [ +const runCombinedScenario = ( + fixture: Fixture, +): Effect.Effect>, unknown> => + Effect.gen(function* scenario130() { + yield* addInventoryItemResourceType(fixture); + yield* run(fixture, scaffoldCommand.microverticalActionBoundary, [ scaffoldFlag.vertical, - 'billing', - '--worker', - fixtureName.ordersCreatedLogger, - scaffoldFlag.producer, inventorySlug, - '--topic', - fixtureName.ordersCreated, ]); - const generatedFiles = [ - 'packages/core-runtime/src/modules/actions/change-tenant-state.action.ts', - 'packages/core-runtime/src/policies/tenant-active.policy.ts', - inventoryActionFile, - 'verticals/inventory-stock/src/integrations/warehouse-api/warehouse-api-stock-level.service.ts', - 'verticals/inventory-stock/src/actions/create-order.orders-created.outbox-message.ts', - inventoryOutboxContractFile, - billingWorkersIndexFile, - billingApiIndexFile, - 'verticals/billing/src/workers/orders-created-logger.worker.ts', - 'verticals/billing/src/worker-host/layer.ts', - 'verticals/billing/src/worker-host/main.ts', - 'verticals/billing/scripts/outbox-worker.ts', - 'verticals/inventory-stock/src/policies/stock-available.policy.ts', - 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/orders/page.tsx', - 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/orders/route.meta.ts', - 'verticals/inventory-stock/src/federation/page-orders.tsx', - inventoryActionPrincipalFile, - inventoryActionHttpRunnerFile, - inventoryActionGatewayFile, - inventoryModuleApiContractFile, - inventoryModuleApiReadFile, - inventoryModuleApiClientFile, - inventoryModuleApiServerFile, - inventorySearchContractFile, - inventorySearchProviderFile, - inventorySearchClientFile, - inventorySearchServerFile, - inventoryReportContractFile, - inventoryReportProviderFile, - inventoryReportClientFile, - inventoryReportServerFile, - ]; - - await Promise.all( - generatedFiles.map(async (relativePath) => { - const source = await readFixtureFile(fixture.root, relativePath); - const formatted = spawnSync(oxfmtPath, [`--stdin-filepath=${relativePath}`], { - cwd: appRoot, - encoding: 'utf-8', - input: source, - }); - assert.equal(formatted.status, 0, formatted.stderr); - assert.equal(formatted.stdout, source, `${relativePath} must be formatter-stable`); - }), - ); - }); -}); - -test('all generated files typecheck against the real workspace contracts', async () => { - await withFixture(async (fixture) => { - await runCombinedScenario(fixture); - await run(fixture, scaffoldCommand.outboxWorker, [ + yield* run(fixture, scaffoldCommand.externalHttpAdapter, [ + scaffoldFlag.vertical, + inventorySlug, + scaffoldFlag.provider, + 'warehouse-api', + scaffoldFlag.operation, + 'stock-level', + ]); + yield* run(fixture, 'action', [ + '--scope', + 'core', + '--module', + fixtureName.actionModule, + '--action', + 'change-tenant-state', + ]); + yield* run(fixture, 'action', [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + ]); + yield* run(fixture, scaffoldCommand.outboxMessage, [ scaffoldFlag.vertical, - 'billing', - '--worker', - fixtureName.ordersCreatedLogger, - scaffoldFlag.producer, inventorySlug, + '--action', + fixtureName.action, '--topic', fixtureName.ordersCreated, ]); - await mkdir(path.join(fixture.root, 'node_modules', '@authzed'), { recursive: true }); - await mkdir(path.join(fixture.root, 'node_modules', '@effect'), { recursive: true }); - await mkdir(path.join(fixture.root, 'node_modules', '@modern-js'), { recursive: true }); - await mkdir(path.join(fixture.root, 'node_modules', '@types'), { recursive: true }); - // Every generated-runtime dependency is linked from the real workspace so the fixture - // typechecks and runs against the same modules the shipped verticals resolve. - await Promise.all( - ( - [ - ['packages/core-runtime/node_modules/effect', effectNodeModulePath, 'dir'], - [ - 'packages/core-runtime/node_modules/@effect/sql-pg', - 'node_modules/@effect/sql-pg', - 'dir', - ], - [ - 'packages/core-runtime/node_modules/@effect/platform-node', - 'node_modules/@effect/platform-node', - 'dir', - ], - ['apps/shell-super-app/node_modules/jose', 'node_modules/jose', 'dir'], - ['packages/core-runtime/node_modules/drizzle-orm', 'node_modules/drizzle-orm', 'dir'], - ['packages/core-runtime/node_modules/dotenv', 'node_modules/dotenv', 'dir'], - ['packages/core-runtime/node_modules/pg', 'node_modules/pg', 'dir'], - [ - 'packages/core-runtime/node_modules/@authzed/authzed-node', - 'node_modules/@authzed/authzed-node', - 'dir', - ], - [ - 'apps/shell-super-app/node_modules/@modern-js/plugin-i18n', - 'node_modules/@modern-js/plugin-i18n', - 'dir', - ], - [ - 'apps/shell-super-app/node_modules/@modern-js/plugin-bff', - pluginBffNodeModulePath, - 'dir', - ], - ['apps/shell-super-app/node_modules/@types/react', 'node_modules/@types/react', 'dir'], - ['packages/core-runtime/node_modules/@types/pg', 'node_modules/@types/pg', 'dir'], - ['node_modules/@types/node', 'node_modules/@types/node', 'dir'], - ['packages/core-runtime/src/actions', 'packages/core-runtime/src/actions', 'dir'], - ['packages/core-runtime/src/db', 'packages/core-runtime/src/db', 'dir'], - ['packages/core-runtime/src/operations', 'packages/core-runtime/src/operations', 'dir'], - ['packages/core-runtime/src/database', 'packages/core-runtime/src/database', 'dir'], - ['packages/core-runtime/src/environment', 'packages/core-runtime/src/environment', 'dir'], - ['packages/core-runtime/src/permissions', 'packages/core-runtime/src/permissions', 'dir'], - ['packages/core-runtime/src/auth', 'packages/core-runtime/src/auth', 'dir'], - [ - 'packages/core-runtime/src/authorization', - 'packages/core-runtime/src/authorization', - 'dir', - ], - [ - 'packages/core-runtime/src/modules/module-entrypoint.ts', - 'packages/core-runtime/src/modules/module-entrypoint.ts', - 'file', - ], - ] as const - ).map( - async ([source, target, kind]) => - await symlink(path.join(appRoot, source), path.join(fixture.root, target), kind), - ), + yield* run(fixture, 'policy', ['--scope', 'global', '--policy', fixtureName.policy]); + yield* run(fixture, 'policy', [ + '--scope', + 'microvertical', + '--policy', + 'stock-available', + scaffoldFlag.vertical, + inventorySlug, + ]); + yield* run(fixture, scaffoldCommand.moduleApi, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.resourceDetail, + ]); + yield* run(fixture, scaffoldCommand.searchProvider, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.inventoryItems, + scaffoldFlag.resource, + 'item', + ]); + yield* run(fixture, 'report', [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.stockLevels, + scaffoldFlag.resource, + 'item', + ]); + yield* run( + fixture, + scaffoldCommand.microverticalPage, + [scaffoldFlag.vertical, inventorySlug, '--page', 'orders'], + (appId) => expect([inventorySlug, shellAppId].includes(appId)).toBe(true), ); - await Promise.all( - [ - 'module-entrypoint-gateway.ts', - 'module-state-check-unavailable-error.ts', - 'module-state-denied-error.ts', - 'module-state-gate-errors.ts', - 'module-state-gate.ts', - 'tenant-module-state-errors.ts', - 'tenant-module-state-service.ts', - ].map( - async (moduleFile) => - await symlink( - path.join(appRoot, 'packages/core-runtime/src/modules', moduleFile), - path.join(fixture.root, 'packages/core-runtime/src/modules', moduleFile), - 'file', - ), + yield* run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.customerEditPage, + '--url', + '/contacts/customers/:id/edit', + ]); + return yield* snapshotTree(fixture.root); + }); + +it.live( + 'all generators compose deterministically without crossing owner boundaries', + Effect.fn(function* scenario131() { + const first = yield* createFixture(); + const second = yield* createFixture(); + yield* Effect.gen(function* useResource3() { + const billingBefore = Object.fromEntries( + Object.entries(yield* snapshotTree(first.root)).filter(([file]) => + file.startsWith('verticals/billing/'), + ), + ); + const shellBefore = yield* readFixtureFile(first.root, shellSentinelFile); + const topologyBefore = yield* readFixtureFile(first.root, topologyFile); + const firstTree = yield* runCombinedScenario(first); + const secondTree = yield* runCombinedScenario(second); + expect(firstTree).toEqual(secondTree); + const billingAfter = Object.fromEntries( + Object.entries(firstTree).filter(([file]) => file.startsWith('verticals/billing/')), + ); + expect(billingAfter).toEqual(billingBefore); + expect(yield* readFixtureFile(first.root, shellSentinelFile)).toBe(shellBefore); + expect(yield* readFixtureFile(first.root, topologyFile)).toBe(topologyBefore); + const combinedSource = Object.values(firstTree).join('\n'); + expect(combinedSource).not.toMatch(/from ['"]\.\.\/\.\.\/billing|fetch\(/u); + }).pipe( + Effect.ensuring( + Effect.gen(function* releaseResources3() { + yield* Effect.promise(() => rm(first.root, { force: true, recursive: true })); + yield* Effect.promise(() => rm(second.root, { force: true, recursive: true })); + }), ), ); - const fixtureTsconfig = path.join(fixture.root, 'tsconfig.generated.json'); - await writeFile( - fixtureTsconfig, - json({ - compilerOptions: { - allowImportingTsExtensions: true, - jsx: 'preserve', - module: 'preserve', - moduleResolution: 'Bundler', - noEmit: true, - paths: { - '@app/core-runtime': [path.join(appRoot, coreRuntimeIndexFile)], - '@app/core-runtime/actions/principal-context': [ - path.join(appRoot, 'packages/core-runtime/src/actions/principal-context.ts'), - ], - '@app/core-runtime/actions/runtime-wiring': [ - path.join(appRoot, 'packages/core-runtime/src/actions/runtime-wiring.ts'), - ], - '@app/core-runtime/auth/gateway-assertion-redemption': [ - path.join(appRoot, 'packages/core-runtime/src/auth/gateway-assertion-redemption.ts'), - ], - '@app/core-runtime/http/action-runner': [ - path.join(appRoot, 'packages/core-runtime/src/http/http-instrumentation-seam.ts'), - ], - '@app/core-runtime/http/governed-read': [ - path.join(appRoot, 'packages/core-runtime/src/http/governed-read.ts'), - ], - '@app/core-runtime/http/principal-authentication': [ - path.join(appRoot, 'packages/core-runtime/src/http/principal-authentication.ts'), - ], - '@app/core-runtime/outbox/worker': [ - path.join(appRoot, 'packages/core-runtime/src/outbox/worker-entrypoint.ts'), - ], - '@app/gateway-principal-verifier/server': [ - path.join(appRoot, 'packages/gateway-principal-verifier/src/server.ts'), - ], - '@app/inventory-stock/outbox/*': ['./verticals/inventory-stock/shared/outbox/*.ts'], - '@app/shared-contracts': [path.join(appRoot, 'packages/shared-contracts/src/index.ts')], - '@app/shared-contracts/client-runtime': [ - path.join(appRoot, 'packages/shared-contracts/src/client-runtime.ts'), - ], - '@app/shared-contracts/problem-details': [ - path.join(appRoot, 'packages/shared-contracts/src/problem-details.ts'), - ], - '@app/shared-contracts/server/effect-bff-runtime': [ - path.join(appRoot, 'packages/shared-contracts/src/effect-bff-runtime.ts'), - ], - }, - resolveJsonModule: true, - skipLibCheck: true, - strict: true, - target: 'ESNext', - types: ['node', 'react'], - }, - include: [ - 'packages/core-runtime/src/modules/actions/**/*.ts', - 'packages/core-runtime/src/policies/**/*.ts', - inventoryManifestFile, - 'verticals/inventory-stock/src/actions/**/*.ts', - 'verticals/inventory-stock/src/integrations/**/*.ts', - 'verticals/inventory-stock/shared/outbox/**/*.ts', - 'verticals/billing/src/workers/**/*.ts', - 'verticals/billing/src/worker-host/**/*.ts', + }), +); + +it.live( + 'every generated TypeScript file is already formatter-stable', + Effect.fn(function* mergedScenario85() { + yield* withFixture( + Effect.fn(function* mergedScenario84(fixture) { + yield* runCombinedScenario(fixture); + yield* run(fixture, scaffoldCommand.outboxWorker, [ + scaffoldFlag.vertical, + 'billing', + '--worker', + fixtureName.ordersCreatedLogger, + scaffoldFlag.producer, + inventorySlug, + '--topic', + fixtureName.ordersCreated, + ]); + const generatedFiles = [ + 'packages/core-runtime/src/modules/actions/change-tenant-state.action.ts', + 'packages/core-runtime/src/policies/tenant-active.policy.ts', + inventoryActionFile, + 'verticals/inventory-stock/src/integrations/warehouse-api/warehouse-api-stock-level.service.ts', + 'verticals/inventory-stock/src/actions/create-order.orders-created.outbox-message.ts', + inventoryOutboxContractFile, + billingWorkersIndexFile, billingApiIndexFile, - 'verticals/inventory-stock/src/policies/**/*.ts', - 'verticals/inventory-stock/src/routes/**/*.ts', - 'verticals/inventory-stock/src/routes/**/*.tsx', - 'verticals/inventory-stock/src/federation/**/*.tsx', - 'verticals/inventory-stock/src/i18n/**/*.ts', - 'verticals/inventory-stock/api/**/*.ts', - 'verticals/inventory-stock/src/api/**/*.ts', - 'verticals/inventory-stock/shared/apis/**/*.ts', - 'verticals/inventory-stock/src/search/**/*.ts', - 'verticals/inventory-stock/src/reports/**/*.ts', - ], + 'verticals/billing/src/workers/orders-created-logger.worker.ts', + 'verticals/billing/src/worker-host/layer.ts', + 'verticals/billing/src/worker-host/main.ts', + 'verticals/billing/scripts/outbox-worker.ts', + 'verticals/inventory-stock/src/policies/stock-available.policy.ts', + 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/orders/page.tsx', + 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/orders/route.meta.ts', + 'verticals/inventory-stock/src/federation/page-orders.tsx', + inventoryActionPrincipalFile, + inventoryActionHttpRunnerFile, + inventoryActionGatewayFile, + inventoryModuleApiContractFile, + inventoryModuleApiReadFile, + inventoryModuleApiClientFile, + inventoryModuleApiServerFile, + inventorySearchContractFile, + inventorySearchProviderFile, + inventorySearchClientFile, + inventorySearchServerFile, + inventoryReportContractFile, + inventoryReportProviderFile, + inventoryReportClientFile, + inventoryReportServerFile, + ]; + + yield* Effect.all( + generatedFiles.map( + Effect.fn(function* mergedScenario83(relativePath) { + const source = yield* readFixtureFile(fixture.root, relativePath); + const formatted = spawnSync(oxfmtPath, [`--stdin-filepath=${relativePath}`], { + cwd: appRoot, + encoding: 'utf-8', + input: source, + }); + expect(formatted.status, formatted.stderr).toBe(0); + expect(formatted.stdout, `${relativePath} must be formatter-stable`).toBe(source); + }), + ), + { concurrency: 'unbounded' }, + ); }), - 'utf-8', ); + }), +); - const result = spawnSync(tscPath, ['-p', fixtureTsconfig], { - cwd: fixture.root, - encoding: 'utf-8', - }); - assert.equal(result.status, 0, `${result.stdout}${result.stderr}`); - }); -}); +it.live( + 'all generated files typecheck against the real workspace contracts', + Effect.fn(function* mergedScenario92() { + yield* withFixture( + Effect.fn(function* mergedScenario91(fixture) { + yield* runCombinedScenario(fixture); + yield* run(fixture, scaffoldCommand.outboxWorker, [ + scaffoldFlag.vertical, + 'billing', + '--worker', + fixtureName.ordersCreatedLogger, + scaffoldFlag.producer, + inventorySlug, + '--topic', + fixtureName.ordersCreated, + ]); + yield* Effect.promise(() => + mkdir(path.join(fixture.root, 'node_modules', '@authzed'), { recursive: true }), + ); + yield* Effect.promise(() => + mkdir(path.join(fixture.root, 'node_modules', '@effect'), { recursive: true }), + ); + yield* Effect.promise(() => + mkdir(path.join(fixture.root, 'node_modules', '@modern-js'), { recursive: true }), + ); + yield* Effect.promise(() => + mkdir(path.join(fixture.root, 'node_modules', '@types'), { recursive: true }), + ); + // Every generated-runtime dependency is linked from the real workspace so the fixture + // typechecks and runs against the same modules the shipped verticals resolve. + yield* Effect.all( + ( + [ + ['packages/core-runtime/node_modules/effect', effectNodeModulePath, 'dir'], + [ + 'packages/core-runtime/node_modules/@effect/sql-pg', + 'node_modules/@effect/sql-pg', + 'dir', + ], + [ + 'packages/core-runtime/node_modules/@effect/platform-node', + 'node_modules/@effect/platform-node', + 'dir', + ], + ['apps/shell-super-app/node_modules/jose', 'node_modules/jose', 'dir'], + ['packages/core-runtime/node_modules/drizzle-orm', 'node_modules/drizzle-orm', 'dir'], + ['packages/core-runtime/node_modules/dotenv', 'node_modules/dotenv', 'dir'], + ['packages/core-runtime/node_modules/pg', 'node_modules/pg', 'dir'], + [ + 'packages/core-runtime/node_modules/@authzed/authzed-node', + 'node_modules/@authzed/authzed-node', + 'dir', + ], + [ + 'apps/shell-super-app/node_modules/@modern-js/plugin-i18n', + 'node_modules/@modern-js/plugin-i18n', + 'dir', + ], + [ + 'apps/shell-super-app/node_modules/@modern-js/plugin-bff', + pluginBffNodeModulePath, + 'dir', + ], + [ + 'apps/shell-super-app/node_modules/@types/react', + 'node_modules/@types/react', + 'dir', + ], + ['packages/core-runtime/node_modules/@types/pg', 'node_modules/@types/pg', 'dir'], + ['node_modules/@types/node', 'node_modules/@types/node', 'dir'], + ['packages/core-runtime/src/actions', 'packages/core-runtime/src/actions', 'dir'], + ['packages/core-runtime/src/db', 'packages/core-runtime/src/db', 'dir'], + [ + 'packages/core-runtime/src/operations', + 'packages/core-runtime/src/operations', + 'dir', + ], + ['packages/core-runtime/src/database', 'packages/core-runtime/src/database', 'dir'], + [ + 'packages/core-runtime/src/environment', + 'packages/core-runtime/src/environment', + 'dir', + ], + [ + 'packages/core-runtime/src/permissions', + 'packages/core-runtime/src/permissions', + 'dir', + ], + ['packages/core-runtime/src/auth', 'packages/core-runtime/src/auth', 'dir'], + [ + 'packages/core-runtime/src/authorization', + 'packages/core-runtime/src/authorization', + 'dir', + ], + [ + 'packages/core-runtime/src/modules/module-entrypoint.ts', + 'packages/core-runtime/src/modules/module-entrypoint.ts', + 'file', + ], + ] as const + ).map(([source, target, kind]) => + Effect.promise(() => + symlink(path.join(appRoot, source), path.join(fixture.root, target), kind), + ), + ), + { concurrency: 'unbounded' }, + ); + yield* Effect.all( + [ + 'module-entrypoint-gateway.ts', + 'module-state-check-unavailable-error.ts', + 'module-state-denied-error.ts', + 'module-state-gate-errors.ts', + 'module-state-gate.ts', + 'tenant-module-state-errors.ts', + 'tenant-module-state-service.ts', + ].map( + Effect.fn(function* mergedScenario90(moduleFile) { + return yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/src/modules', moduleFile), + path.join(fixture.root, 'packages/core-runtime/src/modules', moduleFile), + 'file', + ), + ); + }), + ), + { concurrency: 'unbounded' }, + ); + const fixtureTsconfig = path.join(fixture.root, 'tsconfig.generated.json'); + yield* Effect.promise(() => + writeFile( + fixtureTsconfig, + json({ + compilerOptions: { + allowImportingTsExtensions: true, + jsx: 'preserve', + module: 'preserve', + moduleResolution: 'Bundler', + noEmit: true, + paths: { + '@app/core-runtime': [path.join(appRoot, coreRuntimeIndexFile)], + '@app/core-runtime/actions/principal-context': [ + path.join(appRoot, 'packages/core-runtime/src/actions/principal-context.ts'), + ], + '@app/core-runtime/actions/runtime-wiring': [ + path.join(appRoot, 'packages/core-runtime/src/actions/runtime-wiring.ts'), + ], + '@app/core-runtime/auth/gateway-assertion-redemption': [ + path.join( + appRoot, + 'packages/core-runtime/src/auth/gateway-assertion-redemption.ts', + ), + ], + '@app/core-runtime/http/action-runner': [ + path.join( + appRoot, + 'packages/core-runtime/src/http/http-instrumentation-seam.ts', + ), + ], + '@app/core-runtime/http/governed-read': [ + path.join(appRoot, 'packages/core-runtime/src/http/governed-read.ts'), + ], + '@app/core-runtime/http/principal-authentication': [ + path.join( + appRoot, + 'packages/core-runtime/src/http/principal-authentication.ts', + ), + ], + '@app/core-runtime/outbox/worker': [ + path.join(appRoot, 'packages/core-runtime/src/outbox/worker-entrypoint.ts'), + ], + '@app/gateway-principal-verifier/server': [ + path.join(appRoot, 'packages/gateway-principal-verifier/src/server.ts'), + ], + '@app/inventory-stock/outbox/*': [ + './verticals/inventory-stock/shared/outbox/*.ts', + ], + '@app/shared-contracts': [ + path.join(appRoot, 'packages/shared-contracts/src/index.ts'), + ], + '@app/shared-contracts/client-runtime': [ + path.join(appRoot, 'packages/shared-contracts/src/client-runtime.ts'), + ], + '@app/shared-contracts/problem-details': [ + path.join(appRoot, 'packages/shared-contracts/src/problem-details.ts'), + ], + '@app/shared-contracts/server/effect-bff-runtime': [ + path.join(appRoot, 'packages/shared-contracts/src/effect-bff-runtime.ts'), + ], + }, + resolveJsonModule: true, + skipLibCheck: true, + strict: true, + target: 'ESNext', + types: ['node', 'react'], + }, + include: [ + 'packages/core-runtime/src/modules/actions/**/*.ts', + 'packages/core-runtime/src/policies/**/*.ts', + inventoryManifestFile, + 'verticals/inventory-stock/src/actions/**/*.ts', + 'verticals/inventory-stock/src/integrations/**/*.ts', + 'verticals/inventory-stock/shared/outbox/**/*.ts', + 'verticals/billing/src/workers/**/*.ts', + 'verticals/billing/src/worker-host/**/*.ts', + billingApiIndexFile, + 'verticals/inventory-stock/src/policies/**/*.ts', + 'verticals/inventory-stock/src/routes/**/*.ts', + 'verticals/inventory-stock/src/routes/**/*.tsx', + 'verticals/inventory-stock/src/federation/**/*.tsx', + 'verticals/inventory-stock/src/i18n/**/*.ts', + 'verticals/inventory-stock/api/**/*.ts', + 'verticals/inventory-stock/src/api/**/*.ts', + 'verticals/inventory-stock/shared/apis/**/*.ts', + 'verticals/inventory-stock/src/search/**/*.ts', + 'verticals/inventory-stock/src/reports/**/*.ts', + ], + }), + 'utf-8', + ), + ); + + const result = spawnSync(tscPath, ['-p', fixtureTsconfig], { + cwd: fixture.root, + encoding: 'utf-8', + }); + expect(result.status, `${result.stdout}${result.stderr}`).toBe(0); + }), + ); + }), +); -test('generated fluent slots preserve nested chains beside independent top-level entries', () => { +it('generated fluent slots preserve nested chains beside independent top-level entries', () => { const nestedEntry = `.addHttpApi( FirstApi, (api) => api @@ -5947,14 +6498,13 @@ test('generated fluent slots preserve nested chains beside independent top-level ${nestedEntry} ${neighborEntry} ${GOVERNED_HTTP_API_ADDITION_SLOT_END}`; - assert.deepEqual( + expect( readGeneratedSlotEntries( source, GOVERNED_HTTP_API_ADDITION_SLOT_START, GOVERNED_HTTP_API_ADDITION_SLOT_END, ), - [nestedEntry, neighborEntry], - ); + ).toEqual([nestedEntry, neighborEntry]); const next = insertSortedSlot( source, GOVERNED_HTTP_API_ADDITION_SLOT_START, @@ -5962,14 +6512,13 @@ ${GOVERNED_HTTP_API_ADDITION_SLOT_END}`; [addedEntry], (entry) => entry === nestedEntry || entry === neighborEntry || entry === addedEntry, ); - assert.deepEqual( + expect( readGeneratedSlotEntries( next, GOVERNED_HTTP_API_ADDITION_SLOT_START, GOVERNED_HTTP_API_ADDITION_SLOT_END, ), - [nestedEntry, neighborEntry, addedEntry], - ); + ).toEqual([nestedEntry, neighborEntry, addedEntry]); }); for (const protectedEntry of [ @@ -5978,52 +6527,49 @@ for (const protectedEntry of [ '.addHttpApi(`text )\n.addHttpApi(StringOnly)\n(`)', '.addHttpApi("text )", \'text ]\', /* ) ] }\n.addGroup(CommentOnly)\n*/ FirstApi)', ]) { - test(`generated fluent slots shield protected text in ${protectedEntry}`, () => { + it(`generated fluent slots shield protected text in ${protectedEntry}`, () => { const source = `${GOVERNED_HTTP_API_ADDITION_SLOT_START} ${protectedEntry} .addHttpApi(SecondApi) ${GOVERNED_HTTP_API_ADDITION_SLOT_END}`; - assert.deepEqual( + expect( readGeneratedSlotEntries( source, GOVERNED_HTTP_API_ADDITION_SLOT_START, GOVERNED_HTTP_API_ADDITION_SLOT_END, ), - [protectedEntry, '.addHttpApi(SecondApi)'], - ); + ).toEqual([protectedEntry, '.addHttpApi(SecondApi)']); }); } -test('generated fluent slots preserve terminated statements and reset tail boundaries', () => { +it('generated fluent slots preserve terminated statements and reset tail boundaries', () => { const statement = '.addHttpApi(StatementApi)\n.addHttpApi(StatementNeighbor);'; const tailEntries = ['.addHttpApi(TailApi)', '.addHttpApi(TailNeighbor)']; const source = `${GOVERNED_HTTP_API_ADDITION_SLOT_START} ${statement} ${tailEntries.join('\n')} ${GOVERNED_HTTP_API_ADDITION_SLOT_END}`; - assert.deepEqual( + expect( readGeneratedSlotEntries( source, GOVERNED_HTTP_API_ADDITION_SLOT_START, GOVERNED_HTTP_API_ADDITION_SLOT_END, ), - [statement, ...tailEntries], - ); + ).toEqual([statement, ...tailEntries]); }); -test('generated fluent slots preserve nonfluent multiline statement continuations', () => { +it('generated fluent slots preserve nonfluent multiline statement continuations', () => { const entries = ['const api = FirstApi\n .addGroup(FirstGroup);', 'SecondApi,']; const source = `${GOVERNED_HTTP_API_ADDITION_SLOT_START} ${entries.join('\n')} ${GOVERNED_HTTP_API_ADDITION_SLOT_END}`; - assert.deepEqual( + expect( readGeneratedSlotEntries( source, GOVERNED_HTTP_API_ADDITION_SLOT_START, GOVERNED_HTTP_API_ADDITION_SLOT_END, ), - entries, - ); + ).toEqual(entries); }); for (const incompleteEntry of [ @@ -6034,23 +6580,21 @@ for (const incompleteEntry of [ '.addHttpApi(FirstApi) /* unclosed', 'const api = FirstApi\n.addGroup(FirstGroup)', ]) { - test(`generated fluent slots reject incomplete or unbalanced syntax in ${incompleteEntry}`, () => { + it(`generated fluent slots reject incomplete or unbalanced syntax in ${incompleteEntry}`, () => { const source = `${GOVERNED_HTTP_API_ADDITION_SLOT_START} ${incompleteEntry} ${GOVERNED_HTTP_API_ADDITION_SLOT_END}`; - assert.throws( - () => - readGeneratedSlotEntries( - source, - GOVERNED_HTTP_API_ADDITION_SLOT_START, - GOVERNED_HTTP_API_ADDITION_SLOT_END, - ), - /generated owner slot contains unsupported developer content/u, - ); + expect(() => + readGeneratedSlotEntries( + source, + GOVERNED_HTTP_API_ADDITION_SLOT_START, + GOVERNED_HTTP_API_ADDITION_SLOT_END, + ), + ).toThrow(/generated owner slot contains unsupported developer content/u); }); } -test('generated fluent slots preserve multiline call entries', () => { +it('generated fluent slots preserve multiline call entries', () => { const source = `${GOVERNED_HTTP_API_ADDITION_SLOT_START} .addHttpApi( FirstApi, @@ -6069,112 +6613,129 @@ test('generated fluent slots preserve multiline call entries', () => { GOVERNED_HTTP_API_ADDITION_SLOT_START, GOVERNED_HTTP_API_ADDITION_SLOT_END, ); - assert.equal(entries.length, 3); - assert.match(entries[0] ?? '', /FirstApi/u); + expect(entries.length).toBe(3); + expect(entries[0] ?? '').toMatch(/FirstApi/u); }); for (const [start, end] of [ [GOVERNED_HTTP_HANDLER_SUPPORT_IMPORT_SLOT_START, GOVERNED_HTTP_HANDLER_SUPPORT_IMPORT_SLOT_END], [GOVERNED_HTTP_HANDLER_SUPPORT_LAYER_SLOT_START, GOVERNED_HTTP_HANDLER_SUPPORT_LAYER_SLOT_END], ] as const) { - test(`governed generation accepts the independent support slot without ${start}`, async () => { - await withFixture(async (fixture) => { - const rootPath = path.join(fixture.root, inventoryHandlerRootFile); - const source = await readFile(rootPath, 'utf-8'); - assert.ok(source.includes(start)); - await writeFile(rootPath, source.replace(start, '').replace(end, ''), 'utf-8'); - await run(fixture, scaffoldCommand.moduleApi, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.resourceDetail, - ]); - const generated = await readFile(rootPath, 'utf-8'); - assert.match(generated, /resourceDetailReadApiLive/u); - }); - }); + it.live( + `governed generation accepts the independent support slot without ${start}`, + Effect.fn(function* mergedScenario95() { + yield* withFixture( + Effect.fn(function* mergedScenario94(fixture) { + const rootPath = path.join(fixture.root, inventoryHandlerRootFile); + const source = yield* Effect.promise(() => readFile(rootPath, 'utf-8')); + expect(source.includes(start)).toBeTruthy(); + yield* Effect.promise(() => + writeFile(rootPath, source.replace(start, '').replace(end, ''), 'utf-8'), + ); + yield* run(fixture, scaffoldCommand.moduleApi, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.resourceDetail, + ]); + const generated = yield* Effect.promise(() => readFile(rootPath, 'utf-8')); + expect(generated).toMatch(/resourceDetailReadApiLive/u); + }), + ); + }), + ); } -test('Action identity boundary rejects an owned file without the authentication adapter', async () => { - await withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalActionBoundary, [ - scaffoldFlag.vertical, - inventorySlug, - ]); - const serverPath = path.join(fixture.root, inventoryActionPrincipalFile); - const source = await readFile(serverPath, 'utf-8'); - await writeFile( - serverPath, - source.replaceAll('authenticateOperationPrincipal', 'removedAuthenticationAdapter'), - 'utf-8', - ); - await assertScaffoldRefused( - fixture, - scaffoldCommand.microverticalActionBoundary, - [scaffoldFlag.vertical, inventorySlug], - /refusing|owned|boundary/u, +it.live( + 'Action identity boundary rejects an owned file without the authentication adapter', + Effect.fn(function* mergedScenario98() { + yield* withFixture( + Effect.fn(function* mergedScenario97(fixture) { + yield* run(fixture, scaffoldCommand.microverticalActionBoundary, [ + scaffoldFlag.vertical, + inventorySlug, + ]); + const serverPath = path.join(fixture.root, inventoryActionPrincipalFile); + const source = yield* Effect.promise(() => readFile(serverPath, 'utf-8')); + yield* Effect.promise(() => + writeFile( + serverPath, + source.replaceAll('authenticateOperationPrincipal', 'removedAuthenticationAdapter'), + 'utf-8', + ), + ); + yield* assertScaffoldRefused( + fixture, + scaffoldCommand.microverticalActionBoundary, + [scaffoldFlag.vertical, inventorySlug], + /refusing|owned|boundary/u, + ); + }), ); - }); -}); + }), +); -test('typed injected governed runtime stays bound to the exported owner composition', async () => { - const shared = await readFile(path.join(appRoot, partyGovernedContractPath), 'utf-8'); - const handler = await readFile( - path.join(appRoot, 'verticals/party-registry/api/index.ts'), - 'utf-8', - ); - assert.equal(hasValidGovernedHttpCompositionRoot(shared, handler), true); - assert.equal( - hasValidGovernedHttpCompositionRoot( - shared, - handler.replace( - 'readRuntime: Layer.Layer + readFile(path.join(appRoot, partyGovernedContractPath), 'utf-8'), + ); + const handler = yield* Effect.promise(() => + readFile(path.join(appRoot, 'verticals/party-registry/api/index.ts'), 'utf-8'), + ); + expect(hasValidGovernedHttpCompositionRoot(shared, handler)).toBe(true); + expect( + hasValidGovernedHttpCompositionRoot( + shared, + handler.replace( + 'readRuntime: Layer.Layer { - const shared = await readFile(path.join(appRoot, partyGovernedContractPath), 'utf-8'); - const handler = await readFile( - path.join(appRoot, 'verticals/party-registry/api/index.ts'), - 'utf-8', - ); - for (const [before, after] of [ - [ - 'const resolvedApiHandlersLive = apiHandlersLive.pipe(', - 'const resolvedApiHandlersLive = unrelatedHandlers.pipe(', - ], - ["'@app/shared-contracts/server/effect-bff-runtime'", "'./counterfeit-assembler.ts'"], - ['handlers: resolvedApiHandlersLive,', 'handlers: unrelatedHandlers,'], - ] as const) { - assert.ok(handler.includes(before)); - assert.equal( - hasValidGovernedHttpCompositionRoot(shared, handler.replace(before, after)), - false, +it.live( + 'assembled governed runtime rejects disconnected handler pipelines and counterfeit assemblers', + Effect.fn(function* mergedScenario100() { + const shared = yield* Effect.promise(() => + readFile(path.join(appRoot, partyGovernedContractPath), 'utf-8'), ); - } -}); + const handler = yield* Effect.promise(() => + readFile(path.join(appRoot, 'verticals/party-registry/api/index.ts'), 'utf-8'), + ); + for (const [before, after] of [ + [ + 'const resolvedApiHandlersLive = apiHandlersLive.pipe(', + 'const resolvedApiHandlersLive = unrelatedHandlers.pipe(', + ], + ["'@app/shared-contracts/server/effect-bff-runtime'", "'./counterfeit-assembler.ts'"], + ['handlers: resolvedApiHandlersLive,', 'handlers: unrelatedHandlers,'], + ] as const) { + expect(handler.includes(before)).toBeTruthy(); + expect(hasValidGovernedHttpCompositionRoot(shared, handler.replace(before, after))).toBe( + false, + ); + } + }), +); diff --git a/app/scripts/tests/api-only-tooling.test.mts b/app/scripts/tests/api-only-tooling.test.mts index b0afde6e0..2003e78cc 100644 --- a/app/scripts/tests/api-only-tooling.test.mts +++ b/app/scripts/tests/api-only-tooling.test.mts @@ -1,48 +1,81 @@ -/// +import type { + emitFrameworkMicroVerticalReleaseEnvelope, + emitNodeStagedReleaseEnvelope, + verifyBuildOutputReleaseEnvelope, + verifyNodeReleaseEnvelopeStaging, +} from '@modern-js/app-tools/release-envelope'; +import type { defineEffectBff } from '@modern-js/plugin-bff/effect-edge'; + +import { Cause, Effect, Predicate, Schema } from 'effect'; + +import { afterEach, expect, it, rs } from 'effect-rstest'; -import assert from 'node:assert/strict'; import { execFileSync, spawnSync } from 'node:child_process'; -import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; + import type { ExecFileSyncOptionsWithStringEncoding } from 'node:child_process'; + import { mkdtemp, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises'; + import { createRequire } from 'node:module'; + import os from 'node:os'; + import path from 'node:path'; -import test from 'node:test'; -import type { TestContext } from 'node:test'; + import { fileURLToPath, pathToFileURL } from 'node:url'; + import { promisify } from 'node:util'; -import { Predicate, Schema } from 'effect'; -import type { Effect as EffectType } from 'effect/Effect'; + import { build as bundleSource, transform } from 'esbuild'; + import { format } from 'oxfmt'; + import { MicroVerticalReadinessSchema } from '../../packages/shared-contracts/src/microvertical-api-baseline.ts'; + import { configuredMicroVerticalApiStem, microVerticalApiBaselineViolation as microVerticalApiBaselineViolationForFile, } from '../microvertical-api-baseline-boundary.mts'; + import type { MicroVerticalApiBaselineExpectation } from '../microvertical-api-baseline-boundary.mts'; import { strictEffectRuntimeTopologyViolation } from '../ultramodern-api-boundary-rules.mts'; +const EXPECTED_PROOF_VALUE = 'Expected a defined proof value'; + const workspaceRoot = fileURLToPath(new URL('../..', import.meta.url)); + const partyId = 'party-registry'; + const partyDirectory = 'verticals/party-registry'; + const generatedFixtureId = 'inventory-stock'; + const generatedApiPrefix = '/inventory-stock-api'; + const generatedServiceModuleName = 'api/service'; + const generatedApiServiceModule = 'dist/esm-node/ultramodern-workspace/api/service.js'; + const generatedSharedApiImport = '../shared/api.ts'; + const generatedSharedRpcImport = '../shared/rpc.ts'; + const apiIndexFile = 'api/index.ts'; + const sharedApiFile = 'shared/api.ts'; + const buildMarkerFile = 'shared/ultramodern-build.ts'; + const tsconfigFile = 'tsconfig.json'; + const fixtureApiModuleSource = 'export const fixtureApi = {};'; + const governedApiModuleSource = `${fixtureApiModuleSource} export const governedHttpApi = fixtureApi; export const unusedApi = {}; `; + const governedLayerAliasFixture = ` import { assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; import { HttpApiBuilder, Layer } from '@modern-js/plugin-bff/effect-edge'; @@ -52,6 +85,7 @@ const group = HttpApiBuilder.group(governedHttpApi, 'fixture', (handlers) => han const handlers = Layer.mergeAll(group.pipe(GovernedReadLayer.provide(Layer.empty))); export default assembleEffectBffRuntime({ api: fixtureApi, handlers: handlers }); `; + const governedLayerAliasMutations = [ ["from 'effect'", "from './counterfeit-layer.ts'"], ['fixtureApi, governedHttpApi', 'fixtureApi, unusedApi as governedHttpApi'], @@ -59,62 +93,95 @@ const governedLayerAliasMutations = [ ] as const; const mfManifestPath = '/mf-manifest.json'; + const readinessPath = '/party-registry-api/party-registry/readiness'; + const localePath = '/locales/en/party-registry.json'; + const apiSmokePath = '/api-smoke'; + const compiledUiAssetPath = 'static/js/index.js'; + const ssrBundlePath = 'bundles/index.js'; + const apiBundlePath = 'api/index.js'; + const routesManifestFile = 'routes-manifest.json'; + const mfManifestFile = 'mf-manifest.json'; + const generatedProofScope = 'generated-proof'; + const generatedClientContractImport = '../../shared/api.ts'; + const generatedSharedApiModule = 'api/shared'; + const generatedSharedContractsPackage = '@generated-proof/shared-contracts'; + const effectClientPackage = '@modern-js/plugin-bff/effect-client'; + const generatedBaselineTemplateEntry = 'src/index.ts'; + const apiBoundaryCheckerPath = 'scripts/check-ultramodern-api-boundaries.mts'; + const topologyReferencePath = 'topology/reference-topology.json'; + const checkoutId = 'checkout'; + const checkoutApiPrefix = '/checkout-api'; + const inventoryStockId = 'inventory-stock'; + const warehouseItemsApiStem = 'warehouse-items'; + const warehouseApiPrefix = '/warehouse-api'; + const partyReadinessMetadataLine = " readinessPath: '/party-registry-api/party-registry/readiness',"; -const microVerticalApiBaselineViolation = ( +const microVerticalApiBaselineViolation = Effect.fn(function* inspectMicroVerticalBaseline( stem: string, source: string, expectation?: Partial, -): string | undefined => { - const fixture = mkdtempSync(path.join(os.tmpdir(), 'ontos-microvertical-api-test-')); +) { + const fixture = yield* Effect.acquireRelease( + Effect.promise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-microvertical-api-test-'))), + (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), + ); const contractPath = path.join(fixture, 'api.ts'); - try { - writeFileSync(contractPath, source); - return microVerticalApiBaselineViolationForFile(stem, contractPath, { - additionalPaths: {}, - apiPrefix: `/${stem}-api`, - basePath: `/${stem}-api/${stem}`, - effectClientPackage, - ownerId: stem, - readinessPath: `/${stem}-api/${stem}/readiness`, - sharedContractsPackage: '@app/shared-contracts', - ...expectation, - }); - } finally { - rmSync(fixture, { force: true, recursive: true }); - } -}; + yield* Effect.promise(() => writeFile(contractPath, source)); + return microVerticalApiBaselineViolationForFile(stem, contractPath, { + additionalPaths: {}, + apiPrefix: `/${stem}-api`, + basePath: `/${stem}-api/${stem}`, + effectClientPackage, + ownerId: stem, + readinessPath: `/${stem}-api/${stem}/readiness`, + sharedContractsPackage: '@app/shared-contracts', + ...expectation, + }); +}); const unexpectedTopologyImport = (specifier: string): never => { throw new Error(`Unexpected strict-topology import: ${specifier}`); }; const generatorRoot = await realpath(path.join(workspaceRoot, 'node_modules/@modern-js/create')); + const require = createRequire(import.meta.url); +const publishedGeneratorModulePath = + (moduleFormat: string) => + (name: string): string => + path.join( + generatorRoot, + `dist/${moduleFormat}/ultramodern-workspace/${name}.${moduleFormat === 'cjs' ? 'cjs' : 'js'}`, + ); + +const violationText = (violation: string | undefined): string => violation ?? ''; + const AppIdSchema = Schema.String.pipe(Schema.brand('AppId')); + const IdentitySchema = Schema.Struct({ appId: AppIdSchema, build: Schema.String, @@ -127,6 +194,7 @@ const IdentitySchema = Schema.Struct({ unitId: Schema.String.pipe(Schema.brand('UnitId')), version: Schema.String, }); + const BuildArtifactSchema = Schema.Struct({ deliveryUnit: IdentitySchema, kind: Schema.String, @@ -136,28 +204,12 @@ const BuildArtifactSchema = Schema.Struct({ ui: Schema.Struct({ ...IdentitySchema.fields, surface: Schema.Literal('ui') }), }), }); -interface ReleaseEnvelope { - readonly surfaces: { - readonly apiBackend: readonly string[]; - readonly ssr: readonly string[]; - readonly uiClient: readonly string[]; - }; -} interface ReleaseFramework { - readonly emitFrameworkMicroVerticalReleaseEnvelope: (input: { - readonly apiOnly: boolean; - readonly distDirectory: string; - readonly target: string; - }) => Promise; - readonly emitNodeStagedReleaseEnvelope: (input: { - readonly distDirectory: string; - readonly outputDirectory: string; - }) => Promise; - readonly verifyBuildOutputReleaseEnvelope: (root: string, target: string) => Promise; - readonly verifyNodeReleaseEnvelopeStaging: (input: { - readonly outputDirectory: string; - }) => Promise; + readonly emitFrameworkMicroVerticalReleaseEnvelope: typeof emitFrameworkMicroVerticalReleaseEnvelope; + readonly emitNodeStagedReleaseEnvelope: typeof emitNodeStagedReleaseEnvelope; + readonly verifyBuildOutputReleaseEnvelope: typeof verifyBuildOutputReleaseEnvelope; + readonly verifyNodeReleaseEnvelopeStaging: typeof verifyNodeReleaseEnvelopeStaging; } interface WorkspaceAppFixture { @@ -169,6 +221,7 @@ interface WorkspaceAppFixture { readonly exposes: Readonly>; readonly id: typeof AppIdSchema.Type; } + type CreateSharedPackage = ( scope: string, id: string, @@ -181,9 +234,11 @@ type CreateSharedPackage = ( readonly dependencies: Readonly>; readonly exports: Readonly>; }; + interface StrictEffectApiBoundaryNode { readonly type?: string; } + interface StrictEffectApiBoundaryContext { readonly filename: string; readonly getSourceCode: () => { @@ -192,16 +247,23 @@ interface StrictEffectApiBoundaryContext { }; readonly report: (finding: { readonly message: string }) => void; } + type StrictEffectApiBoundaryRuleFactory = () => { readonly create: (context: StrictEffectApiBoundaryContext) => { readonly Program: (node: StrictEffectApiBoundaryNode) => void; }; }; + type CreateVerticalDescriptor = (appId: string, port: number) => WorkspaceAppFixture; + type CreateLayout = (appId: typeof AppIdSchema.Type) => string; + type CreateAppModernConfig = (applicationRoot: string, app: WorkspaceAppFixture) => string; + type CreateBackendModuleFederationConfig = (app: WorkspaceAppFixture) => string; + type CreateUltramodernBuildModule = (applicationRoot: string, app: WorkspaceAppFixture) => string; + interface ApiGeneratorFixture { readonly api?: { readonly consumedBy?: readonly string[]; @@ -212,48 +274,52 @@ interface ApiGeneratorFixture { readonly exposes?: Readonly>; readonly id: string; } + type CreateSharedApi = (scope: string, app: ApiGeneratorFixture) => string; + type CreateApiClient = (app: WorkspaceAppFixture, contractImportPath: string) => string; + type CreateApiServiceEntry = ( scope: string, app: ApiGeneratorFixture, contractImportPath: string, ) => string; + interface GeneratedWorkspaceScriptArtifact { readonly content: string; readonly relativePath: string; } + type MigratedWorkspaceScriptArtifacts = (options: { readonly hasBackendSurface: boolean; readonly shellOnly: boolean; }) => readonly GeneratedWorkspaceScriptArtifact[]; -interface GeneratedHttpHandler { - readonly dispose: () => Promise; - readonly handler: (request: Request) => Promise; -} + type CreateGeneratedHttpHandler = () => GeneratedHttpHandler; + type GeneratedReadiness = typeof MicroVerticalReadinessSchema.Type; -type GeneratedReadinessEffect = EffectType; + +type GeneratedReadinessEffect = Effect.Effect; + type GetGeneratedReadiness = (options?: { readonly baseUrl?: string | URL; }) => GeneratedReadinessEffect; -type RunGeneratedEffect = (effect: GeneratedReadinessEffect) => Promise; -type ValidateCloudflareApp = ( - app: ApiOnlyAppFixture, - applicationPublicUrl: string, -) => Promise; + type ValidateModuleFederationTypes = (input: { readonly appDirs: readonly string[]; readonly workspaceRoot: string; }) => typeof ModuleFederationValidationResultSchema.Type; + type InspectModuleFederationConfigSource = ( source: string, appDirectory: string, configFile: string, ) => typeof ModuleFederationInspectionSchema.Type; + interface RspackPluginFixture { readonly apply: (...argumentsList: never[]) => void; } + interface RspackConfiguration { readonly entry: string; readonly externals: Readonly>; @@ -277,18 +343,23 @@ interface RspackConfiguration { readonly plugins: readonly RspackPluginFixture[]; readonly target: 'node'; } + type RspackFactory = (configuration: RspackConfiguration) => CompilerFixture; + type DefinePluginConstructor = new ( definitions: Readonly>, ) => RspackPluginFixture; + type RspackModuleFixture = RspackFactory & { readonly DefinePlugin: DefinePluginConstructor; readonly rspack: RspackFactory; }; + interface CompilerStatsFixture { readonly hasErrors: () => boolean; readonly toString: (options: { readonly all: boolean; readonly errors: boolean }) => string; } + interface CompilerFixture { readonly close: (onComplete: (error?: Error | null) => void) => void; readonly run: ( @@ -298,6 +369,7 @@ interface CompilerFixture { const callable = void>() => Schema.Opaque()(Schema.Unknown.pipe(Schema.refine(Predicate.isFunction))); + const ReleaseEnvelopeSchema = Schema.Struct({ surfaces: Schema.Struct({ apiBackend: Schema.Array(Schema.String), @@ -305,6 +377,7 @@ const ReleaseEnvelopeSchema = Schema.Struct({ uiClient: Schema.Array(Schema.String), }), }); + const ReleaseFrameworkModuleSchema = Schema.Struct({ emitFrameworkMicroVerticalReleaseEnvelope: callable(), @@ -314,6 +387,7 @@ const ReleaseFrameworkModuleSchema = Schema.Struct({ verifyNodeReleaseEnvelopeStaging: callable(), }); + const WorkspaceAppFixtureSchema = Schema.Struct({ api: Schema.optionalKey( Schema.Struct({ @@ -325,69 +399,90 @@ const WorkspaceAppFixtureSchema = Schema.Struct({ exposes: Schema.Record(Schema.String, Schema.String), id: AppIdSchema, }); + const DescriptorModuleSchema = Schema.Struct({ createVerticalDescriptor: callable(), }); + const ApiServiceGeneratorModuleSchema = Schema.Struct({ createApiServiceEntry: callable(), }); + const PackageGeneratorModuleSchema = Schema.Struct({ createSharedPackage: callable(), }); + const SharedApiGeneratorModuleSchema = Schema.Struct({ createSharedApi: callable(), }); + const StrictEffectApiBoundaryRuleModuleSchema = Schema.Struct({ createStrictEffectApiBoundariesRule: callable(), }); + const ComponentModuleSchema = Schema.Struct({ createLayout: callable() }); + const FederationConfigModuleSchema = Schema.Struct({ createAppModernConfig: callable(), createBackendModuleFederationConfig: callable(), }); + const BuildModuleGeneratorSchema = Schema.Struct({ createUltramodernBuildModule: callable(), }); + const SharedApiGeneratorSchema = Schema.Struct({ createSharedApi: callable() }); + const ApiClientGeneratorSchema = Schema.Struct({ createApiClient: callable() }); + const ApiServiceGeneratorSchema = Schema.Struct({ createApiServiceEntry: callable(), }); + const WorkspaceScriptsGeneratorSchema = Schema.Struct({ migratedWorkspaceScriptArtifacts: callable(), }); + const GeneratedApiRuntimeModuleSchema = Schema.Struct({ default: Schema.Struct({ createHandler: callable() }), }); + const CloudflareEvidenceSchema = Schema.Struct({ assertions: Schema.Array(Schema.Struct({ status: Schema.String, type: Schema.String })), }); -const CloudflareProofModuleSchema = Schema.Struct({ - validateApp: callable(), -}); + const ModuleFederationValidationModuleSchema = Schema.Struct({ validateModuleFederationTypes: callable(), }); + const ModuleFederationValidationResultSchema = Schema.Struct({ hostOnlyAppCount: Schema.Number }); + const ModuleFederationInspectionModuleSchema = Schema.Struct({ inspectModuleFederationConfigSource: callable(), }); + const ModuleFederationInspectionSchema = Schema.Struct({ dts: Schema.Record(Schema.String, Schema.Json), }); + const RspackModuleFixtureSchema = callable(); + const CompilerFixtureSchema = Schema.Struct({ close: callable(), run: callable(), }); + const CompilerStatsFixtureSchema = Schema.Struct({ hasErrors: callable(), toString: callable(), }); + const CompiledReaderSchema = Schema.Struct({ allowedOrigins: Schema.Array(Schema.String) }); + const PackageJsonSchema = Schema.Struct({ scripts: Schema.Record(Schema.String, Schema.String), }); + const TopologySchema = Schema.Struct({ verticals: Schema.Array( Schema.Struct({ @@ -410,10 +505,12 @@ const TopologySchema = Schema.Struct({ }), ), }); + const OverlaySchema = Schema.Struct({ apis: Schema.Record(Schema.String, Schema.String), ports: Schema.Record(Schema.String, Schema.Number), }); + const CloudflareReportSchema = Schema.Struct({ results: Schema.Array( Schema.Struct({ @@ -424,51 +521,58 @@ const CloudflareReportSchema = Schema.Struct({ status: Schema.String, }); -const loadReleaseFramework = async (modulePath: string): Promise => { - const source: unknown = await import(pathToFileURL(modulePath).href); - const framework = Schema.decodeUnknownSync(ReleaseFrameworkModuleSchema)(source); - const emitFrameworkMicroVerticalReleaseEnvelope = - framework.emitFrameworkMicroVerticalReleaseEnvelope.bind(source); - const emitNodeStagedReleaseEnvelope = framework.emitNodeStagedReleaseEnvelope.bind(source); - const verifyBuildOutputReleaseEnvelope = framework.verifyBuildOutputReleaseEnvelope.bind(source); - const verifyNodeReleaseEnvelopeStaging = framework.verifyNodeReleaseEnvelopeStaging.bind(source); - return { - emitFrameworkMicroVerticalReleaseEnvelope: async (input) => { - const output: unknown = await emitFrameworkMicroVerticalReleaseEnvelope(input); - return Schema.decodeUnknownSync(ReleaseEnvelopeSchema)(output); - }, - emitNodeStagedReleaseEnvelope: async (input) => { - const output: unknown = await emitNodeStagedReleaseEnvelope(input); - return Schema.decodeUnknownSync(ReleaseEnvelopeSchema)(output); - }, - verifyBuildOutputReleaseEnvelope: async (root, target) => { - await verifyBuildOutputReleaseEnvelope(root, target); - }, - verifyNodeReleaseEnvelopeStaging: async (input) => { - await verifyNodeReleaseEnvelopeStaging(input); - }, - }; -}; +const loadReleaseFramework = (modulePath: string): Effect.Effect => + Effect.gen(function* scenario1() { + const source: unknown = yield* Effect.promise(() => import(pathToFileURL(modulePath).href)); + const framework = yield* Schema.decodeUnknownEffect(ReleaseFrameworkModuleSchema)(source); + const emitFrameworkMicroVerticalReleaseEnvelope = + framework.emitFrameworkMicroVerticalReleaseEnvelope.bind(source); + const emitNodeStagedReleaseEnvelope = framework.emitNodeStagedReleaseEnvelope.bind(source); + const verifyBuildOutputReleaseEnvelope = + framework.verifyBuildOutputReleaseEnvelope.bind(source); + const verifyNodeReleaseEnvelopeStaging = + framework.verifyNodeReleaseEnvelopeStaging.bind(source); + return { + emitFrameworkMicroVerticalReleaseEnvelope, + emitNodeStagedReleaseEnvelope, + verifyBuildOutputReleaseEnvelope, + verifyNodeReleaseEnvelopeStaging, + }; + }); -const readJson = async >( +const readJson = >( schema: JsonSchema, filePath: string, -): Promise => - Schema.decodeUnknownSync(schema)(JSON.parse(await readFile(filePath, 'utf-8'))); +): Effect.Effect => + Effect.gen(function* scenario2() { + return yield* Schema.decodeUnknownEffect(schema)( + JSON.parse(yield* Effect.promise(() => readFile(filePath, 'utf-8'))), + ); + }); -const writeJson = async ( +const writeJson = ( root: string, logicalPath: string, value: Value, -): Promise => { - await mkdir(path.dirname(path.join(root, logicalPath)), { recursive: true }); - await writeFile(path.join(root, logicalPath), JSON.stringify(value)); -}; +): Effect.Effect => + Effect.gen(function* scenario3() { + yield* Effect.promise(() => + mkdir(path.dirname(path.join(root, logicalPath)), { recursive: true }), + ); + yield* Effect.promise(() => writeFile(path.join(root, logicalPath), JSON.stringify(value))); + }); -const writeText = async (root: string, logicalPath: string, value: string): Promise => { - await mkdir(path.dirname(path.join(root, logicalPath)), { recursive: true }); - await writeFile(path.join(root, logicalPath), value); -}; +const writeText = ( + root: string, + logicalPath: string, + value: string, +): Effect.Effect => + Effect.gen(function* scenario4() { + yield* Effect.promise(() => + mkdir(path.dirname(path.join(root, logicalPath)), { recursive: true }), + ); + yield* Effect.promise(() => writeFile(path.join(root, logicalPath), value)); + }); const runNode = ( argumentsList: readonly string[], @@ -484,63 +588,72 @@ const releaseFrameworkRoot = path.join( workspaceRoot, 'verticals/party-registry/node_modules/@modern-js/app-tools/dist', ); -const releaseFramework = await loadReleaseFramework( - path.join(releaseFrameworkRoot, 'esm-node/ultramodern-release-envelope/framework-output.mjs'), -); -void test('MicroVertical templates use the shared strict Effect BFF assembly primitive', async () => { - const apiServiceModule: unknown = await import( - pathToFileURL(path.join(generatorRoot, generatedApiServiceModule)).href - ); - const apiServiceGenerator = Schema.decodeUnknownSync(ApiServiceGeneratorModuleSchema)( - apiServiceModule, - ); - const packageModule: unknown = await import( - pathToFileURL(path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/package-json.js')) - .href - ); - const packageGenerator = Schema.decodeUnknownSync(PackageGeneratorModuleSchema)(packageModule); - const source = apiServiceGenerator.createApiServiceEntry( - 'fixture', - { - api: { consumedBy: [], prefix: generatedApiPrefix, stem: generatedFixtureId }, - id: generatedFixtureId, - }, - generatedSharedApiImport, - ); +it.live( + 'MicroVertical templates use the shared strict Effect BFF assembly primitive', + Effect.fn(function* governanceScenario1() { + const apiServiceModule: unknown = yield* Effect.promise( + () => import(pathToFileURL(path.join(generatorRoot, generatedApiServiceModule)).href), + ); + const apiServiceGenerator = yield* Schema.decodeUnknownEffect(ApiServiceGeneratorModuleSchema)( + apiServiceModule, + ); + const packageModule: unknown = yield* Effect.promise( + () => + import( + pathToFileURL( + path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/package-json.js'), + ).href + ), + ); + const packageGenerator = yield* Schema.decodeUnknownEffect(PackageGeneratorModuleSchema)( + packageModule, + ); + const source = apiServiceGenerator.createApiServiceEntry( + 'fixture', + { + api: { consumedBy: [], prefix: generatedApiPrefix, stem: generatedFixtureId }, + id: generatedFixtureId, + }, + generatedSharedApiImport, + ); - assert.match( - source, - /import \{ assembleEffectBffRuntime \} from '@fixture\/shared-contracts\/server\/effect-bff-runtime';/u, - ); - assert.match(source, /const apiHandlersLive = Layer\.mergeAll\(/u); - assert.match(source, /assembleEffectBffRuntime\(\{[\s\S]*handlers: apiHandlersLive/u); - assert.doesNotMatch(source, /\bdefineEffectBff\b/u); - - const sharedContractsPackage = packageGenerator.createSharedPackage( - 'fixture', - 'shared-contracts', - 'fixture contracts', - { modernPackageVersion: '3.8.2', strategy: 'install' }, - ); - assert.equal( - sharedContractsPackage.exports['./server/effect-bff-runtime'], - './src/effect-bff-runtime.ts', - ); - assert.equal(sharedContractsPackage.dependencies['@modern-js/plugin-bff'], '3.8.2'); - assert.equal(sharedContractsPackage.dependencies.effect, '4.0.0-beta.107'); - assert.match( - await readFile(path.join(generatorRoot, 'templates/packages/effect-bff-runtime.ts'), 'utf-8'), - /export const assembleEffectBffRuntime/u, - ); - assert.match( - await readFile( - path.join(generatorRoot, 'templates/workspace-scripts/check-ultramodern-api-boundaries.mts'), - 'utf-8', - ), - /strictEffectRuntimeTopologyViolation/u, - ); -}); + expect(source).toMatch( + /import \{ assembleEffectBffRuntime \} from '@fixture\/shared-contracts\/server\/effect-bff-runtime';/u, + ); + expect(source).toMatch(/const apiHandlersLive = Layer\.mergeAll\(/u); + expect(source).toMatch(/assembleEffectBffRuntime\(\{[\s\S]*handlers: apiHandlersLive/u); + expect(source).not.toMatch(/\bdefineEffectBff\b/u); + + const sharedContractsPackage = packageGenerator.createSharedPackage( + 'fixture', + 'shared-contracts', + 'fixture contracts', + { modernPackageVersion: '3.8.2', strategy: 'install' }, + ); + expect(sharedContractsPackage.exports['./server/effect-bff-runtime']).toBe( + './src/effect-bff-runtime.ts', + ); + expect(sharedContractsPackage.dependencies['@modern-js/plugin-bff']).toBe('3.8.2'); + expect(sharedContractsPackage.dependencies.effect).toBe('4.0.0-beta.107'); + expect( + yield* Effect.promise(() => + readFile(path.join(generatorRoot, 'templates/packages/effect-bff-runtime.ts'), 'utf-8'), + ), + ).toMatch(/export const assembleEffectBffRuntime/u); + expect( + yield* Effect.promise(() => + readFile( + path.join( + generatorRoot, + 'templates/workspace-scripts/check-ultramodern-api-boundaries.mts', + ), + 'utf-8', + ), + ), + ).toMatch(/strictEffectRuntimeTopologyViolation/u); + }), +); const adversarialStrictRuntimeSources = [ ` @@ -936,7 +1049,7 @@ const validAdversarialStrictRuntimeSources = [ `, ] as const; -void test('static API validation proves the imported helper call topology', () => { +it('static API validation proves the imported helper call topology', () => { const valid = ` import { assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; import { HttpApiBuilder, Layer } from '@modern-js/plugin-bff/effect-edge'; @@ -949,7 +1062,7 @@ void test('static API validation proves the imported helper call topology', () = const fixtureHandlers = Layer.mergeAll(groupLayer); export default assembleEffectBffRuntime({ api: fixtureApi, handlers: fixtureHandlers }); `; - assert.equal(strictEffectRuntimeTopologyViolation(valid), undefined); + expect(strictEffectRuntimeTopologyViolation(valid)).toBe(undefined); const groupModule = ` import { HttpApiBuilder } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; @@ -1004,8 +1117,7 @@ void test('static API validation proves the imported helper call topology', () = source: aggregateModule, }; }; - assert.equal( - strictEffectRuntimeTopologyViolation(importedHandlers, resolveImportedHandlers), + expect(strictEffectRuntimeTopologyViolation(importedHandlers, resolveImportedHandlers)).toBe( undefined, ); const foreignGroupModule = groupModule.replace( @@ -1043,11 +1155,10 @@ void test('static API validation proves the imported helper call topology', () = source: aggregateModule, }; }; - assert.match( + expect( strictEffectRuntimeTopologyViolation(importedHandlers, resolveForeignHandlers) ?? '', - /explicitly composed Layer/u, - ); - assert.equal( + ).toMatch(/explicitly composed Layer/u); + expect( strictEffectRuntimeTopologyViolation(` import { assembleEffectBffRuntime as assemble } from '@fixture/shared-contracts/server/effect-bff-runtime'; import { HttpApiBuilder, Layer } from '@modern-js/plugin-bff/effect-edge'; @@ -1061,17 +1172,15 @@ void test('static API validation proves the imported helper call topology', () = const handlers = Layer.mergeAll(groupLayer); export default assemble({ api: fixtureApi, handlers: handlers }); `), - undefined, - ); + ).toBe(undefined); for (const [index, source] of validAdversarialStrictRuntimeSources.entries()) { - assert.equal( + expect( strictEffectRuntimeTopologyViolation(source), - undefined, `valid adversarial source ${index + 1}`, - ); + ).toBe(undefined); } for (const source of adversarialStrictRuntimeSources) { - assert.notEqual(strictEffectRuntimeTopologyViolation(source), undefined); + expect(strictEffectRuntimeTopologyViolation(source)).not.toBe(undefined); } for (const [label, source, expected] of [ [ @@ -1342,11 +1451,10 @@ void test('static API validation proves the imported helper call topology', () = /explicitly composed Layer/u, ], ] as const) { - assert.match(strictEffectRuntimeTopologyViolation(source) ?? '', expected, label); + expect(strictEffectRuntimeTopologyViolation(source) ?? '', label).toMatch(expected); } }); -/** Every published rule format must report the same violations for the same candidate root. */ const strictBoundaryReports = ( module: typeof StrictEffectApiBoundaryRuleModuleSchema.Type, filename: string, @@ -1365,6 +1473,7 @@ const strictBoundaryReports = ( .Program({}); return messages; }; + const reportsAssemblyViolation = (messages: readonly string[]): boolean => messages.some((message) => /server-only shared Effect BFF assembly helper|explicitly composed handler Layer/u.test( @@ -1372,84 +1481,92 @@ const reportsAssemblyViolation = (messages: readonly string[]): boolean => ), ); -void test('published lint validators reject comment, string, and local strict-root spoofs', async (context) => { - const codeToolsRoot = await realpath( - path.join(workspaceRoot, 'node_modules/@modern-js/code-tools'), - ); - const formats = [ - 'dist/cjs/oxlint-plugin/rules/strict-effect-api-boundaries.cjs', - 'dist/esm/oxlint-plugin/rules/strict-effect-api-boundaries.js', - 'dist/esm-node/oxlint-plugin/rules/strict-effect-api-boundaries.js', - ] as const; - const apiServiceSource: unknown = await import( - pathToFileURL(path.join(generatorRoot, generatedApiServiceModule)).href - ); - const apiServiceGenerator = Schema.decodeUnknownSync(ApiServiceGeneratorModuleSchema)( - apiServiceSource, - ); - const generatedSource = apiServiceGenerator.createApiServiceEntry( - 'app', - { - api: { consumedBy: [], prefix: generatedApiPrefix, stem: generatedFixtureId }, - id: generatedFixtureId, - }, - generatedSharedApiImport, - ); - const generatedRpcSource = apiServiceGenerator.createApiServiceEntry( - 'app', - { - api: { - consumedBy: [], - prefix: generatedApiPrefix, - protocol: 'rpc', - stem: generatedFixtureId, +it.live( + 'published lint validators reject comment, string, and local strict-root spoofs', + Effect.fn(function* mergedScenario2() { + const codeToolsRoot = yield* Effect.promise(() => + realpath(path.join(workspaceRoot, 'node_modules/@modern-js/code-tools')), + ); + const formats = [ + 'dist/cjs/oxlint-plugin/rules/strict-effect-api-boundaries.cjs', + 'dist/esm/oxlint-plugin/rules/strict-effect-api-boundaries.js', + 'dist/esm-node/oxlint-plugin/rules/strict-effect-api-boundaries.js', + ] as const; + const apiServiceSource: unknown = yield* Effect.promise( + () => import(pathToFileURL(path.join(generatorRoot, generatedApiServiceModule)).href), + ); + const apiServiceGenerator = Schema.decodeUnknownSync(ApiServiceGeneratorModuleSchema)( + apiServiceSource, + ); + const generatedSource = apiServiceGenerator.createApiServiceEntry( + 'app', + { + api: { consumedBy: [], prefix: generatedApiPrefix, stem: generatedFixtureId }, + id: generatedFixtureId, }, - id: generatedFixtureId, - }, - generatedSharedRpcImport, - ); - const generatedRpcContractSource = ` + generatedSharedApiImport, + ); + const generatedRpcSource = apiServiceGenerator.createApiServiceEntry( + 'app', + { + api: { + consumedBy: [], + prefix: generatedApiPrefix, + protocol: 'rpc', + stem: generatedFixtureId, + }, + id: generatedFixtureId, + }, + generatedSharedRpcImport, + ); + const generatedRpcContractSource = ` import { RpcGroup } from 'effect/unstable/rpc'; export const inventoryStockRpcGroup = RpcGroup.make(); `; - assert.equal( - strictEffectRuntimeTopologyViolation(generatedRpcSource, (specifier) => - specifier === generatedSharedRpcImport - ? { - id: 'inventory-stock/shared/rpc.ts', - resolveImport: unexpectedTopologyImport, - source: generatedRpcContractSource, - } - : unexpectedTopologyImport(specifier), - ), - undefined, - ); - assert.match( - strictEffectRuntimeTopologyViolation(generatedRpcSource, (specifier) => - specifier === generatedSharedRpcImport - ? { - id: 'inventory-stock/shared/rpc.ts', - resolveImport: unexpectedTopologyImport, - source: 'export const inventoryStockRpcGroup = { toLayer: () => undefined };', - } - : unexpectedTopologyImport(specifier), - ) ?? '', - /server-only shared Effect BFF assembly helper/u, - ); - const lintRoot = await mkdtemp(path.join(os.tmpdir(), 'ontos-strict-api-lint-')); - context.after(async () => await rm(lintRoot, { force: true, recursive: true })); - const fixtureRoot = path.join(lintRoot, 'verticals/fixture'); - const fixtureApiEntryPath = path.join(fixtureRoot, apiIndexFile); - await mkdir(path.join(fixtureRoot, 'api'), { recursive: true }); - await mkdir(path.join(fixtureRoot, 'shared'), { recursive: true }); - await writeFile(path.join(fixtureRoot, sharedApiFile), governedApiModuleSource); - await writeFile( - path.join(fixtureRoot, 'shared/rpc.ts'), - 'export const fixtureRpcGroup = { toLayer: () => undefined };\n', - ); - await writeFile( - path.join(fixtureRoot, 'api/shadowed-group.ts'), - ` + expect( + strictEffectRuntimeTopologyViolation(generatedRpcSource, (specifier) => + specifier === generatedSharedRpcImport + ? { + id: 'inventory-stock/shared/rpc.ts', + resolveImport: unexpectedTopologyImport, + source: generatedRpcContractSource, + } + : unexpectedTopologyImport(specifier), + ), + ).toBe(undefined); + expect( + strictEffectRuntimeTopologyViolation(generatedRpcSource, (specifier) => + specifier === generatedSharedRpcImport + ? { + id: 'inventory-stock/shared/rpc.ts', + resolveImport: unexpectedTopologyImport, + source: 'export const inventoryStockRpcGroup = { toLayer: () => undefined };', + } + : unexpectedTopologyImport(specifier), + ) ?? '', + ).toMatch(/server-only shared Effect BFF assembly helper/u); + const lintRoot = yield* Effect.acquireRelease( + Effect.promise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-strict-api-lint-'))), + (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), + ); + + const fixtureRoot = path.join(lintRoot, 'verticals/fixture'); + const fixtureApiEntryPath = path.join(fixtureRoot, apiIndexFile); + yield* Effect.promise(() => mkdir(path.join(fixtureRoot, 'api'), { recursive: true })); + yield* Effect.promise(() => mkdir(path.join(fixtureRoot, 'shared'), { recursive: true })); + yield* Effect.promise(() => + writeFile(path.join(fixtureRoot, sharedApiFile), governedApiModuleSource), + ); + yield* Effect.promise(() => + writeFile( + path.join(fixtureRoot, 'shared/rpc.ts'), + 'export const fixtureRpcGroup = { toLayer: () => undefined };\n', + ), + ); + yield* Effect.promise(() => + writeFile( + path.join(fixtureRoot, 'api/shadowed-group.ts'), + ` export { HttpApiBuilder } from '@modern-js/plugin-bff/effect-edge'; import { Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; @@ -1462,14 +1579,18 @@ void test('published lint validators reject comment, string, and local strict-ro (handlers) => handlers.handle('reachable', () => undefined), ); `, - ); - const foreignRoot = path.join(lintRoot, 'verticals/foreign'); - await mkdir(path.join(foreignRoot, 'shared'), { recursive: true }); - await mkdir(path.join(foreignRoot, 'api'), { recursive: true }); - await writeFile(path.join(foreignRoot, sharedApiFile), `${fixtureApiModuleSource}\n`); - await writeFile( - path.join(foreignRoot, 'api/group.ts'), - ` + ), + ); + const foreignRoot = path.join(lintRoot, 'verticals/foreign'); + yield* Effect.promise(() => mkdir(path.join(foreignRoot, 'shared'), { recursive: true })); + yield* Effect.promise(() => mkdir(path.join(foreignRoot, 'api'), { recursive: true })); + yield* Effect.promise(() => + writeFile(path.join(foreignRoot, sharedApiFile), `${fixtureApiModuleSource}\n`), + ); + yield* Effect.promise(() => + writeFile( + path.join(foreignRoot, 'api/group.ts'), + ` import { HttpApiBuilder } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; export const foreignGroup = HttpApiBuilder.group( @@ -1478,20 +1599,22 @@ void test('published lint validators reject comment, string, and local strict-ro (handlers) => handlers.handle('reachable', () => undefined), ); `, - ); - const generatedRoot = path.join(lintRoot, 'verticals/inventory-stock'); - await mkdir(path.join(generatedRoot, 'shared'), { recursive: true }); - await writeFile( - path.join(generatedRoot, sharedApiFile), - 'export const inventoryStockApi = {};\n', - ); - await writeFile(path.join(generatedRoot, 'shared/rpc.ts'), generatedRpcContractSource); - const invalidSources = [ - ...adversarialStrictRuntimeSources, - ...governedLayerAliasMutations.map(([before, after]) => - governedLayerAliasFixture.replace(before, after), - ), - ` + ), + ); + const generatedRoot = path.join(lintRoot, 'verticals/inventory-stock'); + yield* Effect.promise(() => mkdir(path.join(generatedRoot, 'shared'), { recursive: true })); + yield* Effect.promise(() => + writeFile(path.join(generatedRoot, sharedApiFile), 'export const inventoryStockApi = {};\n'), + ); + yield* Effect.promise(() => + writeFile(path.join(generatedRoot, 'shared/rpc.ts'), generatedRpcContractSource), + ); + const invalidSources = [ + ...adversarialStrictRuntimeSources, + ...governedLayerAliasMutations.map(([before, after]) => + governedLayerAliasFixture.replace(before, after), + ), + ` import { assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; import { Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; @@ -1499,7 +1622,7 @@ void test('published lint validators reject comment, string, and local strict-ro const handlers = Layer.mergeAll(shadowedGroup); export default assembleEffectBffRuntime({ api: fixtureApi, handlers: handlers }); `, - ` + ` import { assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; import { Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; @@ -1507,39 +1630,39 @@ void test('published lint validators reject comment, string, and local strict-ro const handlers = Layer.mergeAll(foreignGroup); export default assembleEffectBffRuntime({ api: fixtureApi, handlers: handlers }); `, - ` + ` import { fake as defineEffectBff, HttpApiBuilder, Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; const fixtureLayer = HttpApiBuilder.layer(fixtureApi).pipe(Layer.provide(fixtureHandlers)); defineEffectBff({ api: fixtureApi, layer: fixtureLayer }); `, - ` + ` import { defineEffectBff, Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; const fixtureLayer = Layer.empty; defineEffectBff({ api: fixtureApi, layer: fixtureLayer }); `, - ` + ` import { Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; const defineEffectBff = () => undefined; const fixtureLayer = Layer.empty; defineEffectBff({ api: fixtureApi, layer: fixtureLayer }); `, - ` + ` import { Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; const decoy = "defineEffectBff({ api: fixtureApi, layer: Layer.empty })"; // defineEffectBff({ api: fixtureApi, layer: Layer.empty }); `, - ` + ` import { assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; import { Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; const fakeHandlers = "Layer.mergeAll(groupLayer)"; assembleEffectBffRuntime({ api: fixtureApi, handlers: fakeHandlers }); `, - ` + ` import { assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; import { Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; @@ -1548,7 +1671,7 @@ void test('published lint validators reject comment, string, and local strict-ro return assembleEffectBffRuntime({ api: fixtureApi, handlers: handlers }); } `, - ` + ` import { assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; import { Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; @@ -1557,14 +1680,14 @@ void test('published lint validators reject comment, string, and local strict-ro return assembleEffectBffRuntime({ api: fixtureApi, handlers: handlers }); } `, - ` + ` import { assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; import { Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; const handlers = Layer.thisDoesNotExist(groupLayer); assembleEffectBffRuntime({ api: fixtureApi, handlers: handlers }); `, - ` + ` import { defineEffectBff, HttpApiBuilder, Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; const fixtureLayer = HttpApiBuilder.layer(fixtureApi).pipe( @@ -1573,14 +1696,14 @@ void test('published lint validators reject comment, string, and local strict-ro ); defineEffectBff({ api: fixtureApi, layer: fixtureLayer }); `, - ` + ` import { assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; import { Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; const handlers = Layer.mergeAll(groupLayer).pipe(() => Layer.empty); assembleEffectBffRuntime({ api: fixtureApi, handlers: handlers }); `, - ` + ` import { assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; import { Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; @@ -1588,7 +1711,7 @@ void test('published lint validators reject comment, string, and local strict-ro const transport = Layer.mergeAll(transportLayer).pipe(() => Layer.empty); assembleEffectBffRuntime({ api: fixtureApi, handlers: handlers, transport: transport }); `, - ` + ` import { assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; import { Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; @@ -1598,7 +1721,7 @@ void test('published lint validators reject comment, string, and local strict-ro assembleEffectBffRuntime({ api: fixtureApi, handlers: handlers }); } `, - ` + ` import { assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; import { Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; @@ -1609,7 +1732,7 @@ void test('published lint validators reject comment, string, and local strict-ro assembleEffectBffRuntime({ api: fixtureApi, handlers: handlers }); } `, - ` + ` import { assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; import { Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; @@ -1620,14 +1743,14 @@ void test('published lint validators reject comment, string, and local strict-ro }, }; `, - ` + ` import { fake as assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; import { Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; const handlers = Layer.mergeAll(groupLayer); assembleEffectBffRuntime({ api: fixtureApi, handlers: handlers }); `, - ` + ` import { Layer } from '@modern-js/plugin-bff/effect-edge'; const fakeImports = \` import { assembleEffectBffRuntime } from '@fixture/shared-contracts/server/effect-bff-runtime'; @@ -1638,27 +1761,30 @@ void test('published lint validators reject comment, string, and local strict-ro const assembleEffectBffRuntime = (input) => input; assembleEffectBffRuntime({ api: fixtureApi, handlers: handlers }); `, - ] as const; + ] as const; - const modules = await Promise.all( - formats.map(async (moduleFormat) => { - const imported: unknown = await import( - pathToFileURL(path.join(codeToolsRoot, moduleFormat)).href - ); - return { - module: Schema.decodeUnknownSync(StrictEffectApiBoundaryRuleModuleSchema)(imported), - moduleFormat, - }; - }), - ); - for (const { module, moduleFormat } of modules) { - for (const source of invalidSources) { - assert.ok( - reportsAssemblyViolation(strictBoundaryReports(module, fixtureApiEntryPath, source)), - `${moduleFormat} accepted a fake strict runtime root`, - ); - } - const legacySource = ` + const modules = yield* Effect.all( + formats.map( + Effect.fn(function* mergedScenario1(moduleFormat) { + const imported: unknown = yield* Effect.promise( + () => import(pathToFileURL(path.join(codeToolsRoot, moduleFormat)).href), + ); + return { + module: Schema.decodeUnknownSync(StrictEffectApiBoundaryRuleModuleSchema)(imported), + moduleFormat, + }; + }), + ), + { concurrency: 'unbounded' }, + ); + for (const { module, moduleFormat } of modules) { + for (const source of invalidSources) { + expect( + reportsAssemblyViolation(strictBoundaryReports(module, fixtureApiEntryPath, source)), + `${moduleFormat} accepted a fake strict runtime root`, + ).toBeTruthy(); + } + const legacySource = ` import { defineEffectBff, HttpApiBuilder, Layer } from '@modern-js/plugin-bff/effect-edge'; import { fixtureApi } from '../shared/api.ts'; const fixtureLayer: Layer.Layer = HttpApiBuilder.layer(fixtureApi).pipe( @@ -1666,75 +1792,80 @@ void test('published lint validators reject comment, string, and local strict-ro ); export default defineEffectBff({ api: fixtureApi, layer: fixtureLayer }); `; - assert.equal( - reportsAssemblyViolation(strictBoundaryReports(module, fixtureApiEntryPath, legacySource)), - true, - `${moduleFormat} accepted a legacy runtime root without the shared assembly helper`, - ); - const generatedMessages = strictBoundaryReports( - module, - path.join(generatedRoot, apiIndexFile), - generatedSource, - ); - assert.equal( - reportsAssemblyViolation(generatedMessages), - false, - `${moduleFormat} rejected exact generated helper output: ${generatedMessages.join(' | ')}`, - ); - const generatedRpcMessages = strictBoundaryReports( - module, - path.join(generatedRoot, apiIndexFile), - generatedRpcSource, - ); - assert.equal( - generatedRpcMessages.some((message) => - /server-only shared Effect BFF assembly helper|explicitly composed handler Layer|\.\.\/shared\/api\.ts/u.test( - message, - ), - ), - false, - `${moduleFormat} rejected exact generated RPC output: ${generatedRpcMessages.join(' | ')}`, - ); - for (const source of [...validAdversarialStrictRuntimeSources, governedLayerAliasFixture]) { - const messages = strictBoundaryReports(module, fixtureApiEntryPath, source); - assert.equal( - reportsAssemblyViolation(messages), - false, - `${moduleFormat} rejected a valid strict runtime root: ${messages.join(' | ')}`, + expect( + reportsAssemblyViolation(strictBoundaryReports(module, fixtureApiEntryPath, legacySource)), + `${moduleFormat} accepted a legacy runtime root without the shared assembly helper`, + ).toBe(true); + const generatedMessages = strictBoundaryReports( + module, + path.join(generatedRoot, apiIndexFile), + generatedSource, + ); + expect( + reportsAssemblyViolation(generatedMessages), + `${moduleFormat} rejected exact generated helper output: ${generatedMessages.join(' | ')}`, + ).toBe(false); + const generatedRpcMessages = strictBoundaryReports( + module, + path.join(generatedRoot, apiIndexFile), + generatedRpcSource, ); + expect( + generatedRpcMessages.some((message) => + /server-only shared Effect BFF assembly helper|explicitly composed handler Layer|\.\.\/shared\/api\.ts/u.test( + message, + ), + ), + `${moduleFormat} rejected exact generated RPC output: ${generatedRpcMessages.join(' | ')}`, + ).toBe(false); + for (const source of [...validAdversarialStrictRuntimeSources, governedLayerAliasFixture]) { + const messages = strictBoundaryReports(module, fixtureApiEntryPath, source); + expect( + reportsAssemblyViolation(messages), + `${moduleFormat} rejected a valid strict runtime root: ${messages.join(' | ')}`, + ).toBe(false); + } } - } -}); + }), +); -void test('a minimal generated MicroVertical typechecks and serves its runtime', async () => { - const apiServiceSource: unknown = await import( - pathToFileURL(path.join(generatorRoot, generatedApiServiceModule)).href - ); - const apiServiceGenerator = Schema.decodeUnknownSync(ApiServiceGeneratorModuleSchema)( - apiServiceSource, - ); - const sharedApiSource: unknown = await import( - pathToFileURL(path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/api/shared.js')) - .href - ); - const sharedApiGenerator = Schema.decodeUnknownSync(SharedApiGeneratorModuleSchema)( - sharedApiSource, - ); - const descriptor = { - api: { consumedBy: [], prefix: generatedApiPrefix, stem: generatedFixtureId }, - id: generatedFixtureId, - } as const; - const fixture = await mkdtemp( - path.join(workspaceRoot, 'verticals/party-registry/.generated-runtime-'), - ); - try { - await writeText( +it.live( + 'a minimal generated MicroVertical typechecks and serves its runtime', + Effect.fn(function* governanceScenario4() { + const apiServiceSource: unknown = yield* Effect.promise( + () => import(pathToFileURL(path.join(generatorRoot, generatedApiServiceModule)).href), + ); + const apiServiceGenerator = yield* Schema.decodeUnknownEffect(ApiServiceGeneratorModuleSchema)( + apiServiceSource, + ); + const sharedApiSource: unknown = yield* Effect.promise( + () => + import( + pathToFileURL( + path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/api/shared.js'), + ).href + ), + ); + const sharedApiGenerator = yield* Schema.decodeUnknownEffect(SharedApiGeneratorModuleSchema)( + sharedApiSource, + ); + const descriptor = { + api: { consumedBy: [], prefix: generatedApiPrefix, stem: generatedFixtureId }, + id: generatedFixtureId, + } as const; + const fixture = yield* Effect.acquireRelease( + Effect.promise(() => + mkdtemp(path.join(workspaceRoot, 'verticals/party-registry/.generated-runtime-')), + ), + (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), + ); + yield* writeText( fixture, apiIndexFile, apiServiceGenerator.createApiServiceEntry('app', descriptor, generatedSharedApiImport), ); - await writeText(fixture, sharedApiFile, sharedApiGenerator.createSharedApi('app', descriptor)); - await writeText( + yield* writeText(fixture, sharedApiFile, sharedApiGenerator.createSharedApi('app', descriptor)); + yield* writeText( fixture, buildMarkerFile, `export const ultramodernApiMarker = { @@ -1749,7 +1880,7 @@ void test('a minimal generated MicroVertical typechecks and serves its runtime', version: '0.1.0', } as const;\n`, ); - await writeJson(fixture, tsconfigFile, { + yield* writeJson(fixture, tsconfigFile, { compilerOptions: { composite: false, noEmit: true, types: ['node'] }, extends: '../../../tsconfig.base.json', include: ['api/**/*.ts', 'shared/**/*.ts'], @@ -1758,7 +1889,7 @@ void test('a minimal generated MicroVertical typechecks and serves its runtime', cwd: fixture, encoding: 'utf-8', }); - await writeText( + yield* writeText( fixture, 'runtime-proof.mjs', `import { makeInventoryStockApiRuntime } from './api/index.ts'; @@ -1776,17 +1907,25 @@ void test('a minimal generated MicroVertical typechecks and serves its runtime', }\n`, ); runNode([path.join(fixture, 'runtime-proof.mjs')], { cwd: fixture }); - } finally { - await rm(fixture, { force: true, recursive: true }); - } -}); + }), +); -const releaseFixture = async (context: TestContext) => { - const root = await mkdtemp(path.join(os.tmpdir(), 'ontos-empty-producer-')); - context.after(async (): Promise => { - await rm(root, { force: true, recursive: true }); - }); - const baseArtifact = await readJson( +const releaseFixture = Effect.fn(function* scenario5( + moduleFormat: 'cjs' | 'esm' | 'esm-node' = 'esm-node', +) { + const extension = moduleFormat === 'cjs' ? 'js' : 'mjs'; + const releaseFramework = yield* loadReleaseFramework( + path.join( + releaseFrameworkRoot, + moduleFormat, + `ultramodern-release-envelope/framework-output.${extension}`, + ), + ); + const root = yield* Effect.acquireRelease( + Effect.promise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-empty-producer-'))), + (dir) => Effect.promise(() => rm(dir, { force: true, recursive: true })), + ); + const baseArtifact = yield* readJson( BuildArtifactSchema, path.join(workspaceRoot, 'verticals/party-registry/shared/ultramodern-build.json'), ); @@ -1807,200 +1946,247 @@ const releaseFixture = async (context: TestContext) => { }, remotes: [], }; - const putJson = async (logicalPath: string, value: Value): Promise => - await writeJson(root, logicalPath, value); - const putText = async (logicalPath: string, value: string): Promise => - await writeText(root, logicalPath, value); - await putJson('ultramodern-build.json', artifact); - await putJson('backend-mf-manifest.json', { + const putJson = ( + logicalPath: string, + value: Value, + ): Effect.Effect => + Effect.gen(function* scenario6() { + return yield* writeJson(root, logicalPath, value); + }); + const putText = (logicalPath: string, value: string): Effect.Effect => + Effect.gen(function* scenario7() { + return yield* writeText(root, logicalPath, value); + }); + yield* putJson('ultramodern-build.json', artifact); + yield* putJson('backend-mf-manifest.json', { backendFederation: { deliveryUnit: artifact.deliveryUnit, versionBoundary: { deliveryUnit: artifact.deliveryUnit }, }, }); - await putJson(mfManifestFile, manifest); - await putJson(routesManifestFile, { - routeAssets: { index: { assets: [`https://assets.example.test/app/${compiledUiAssetPath}`] } }, + yield* putJson(mfManifestFile, manifest); + yield* putJson(routesManifestFile, { + routeAssets: { + index: { assets: [`https://assets.example.test/app/${compiledUiAssetPath}`] }, + }, }); - await putJson('route.json', { routes: [{ bundle: ssrBundlePath }] }); - await putJson('package.json', { type: 'module' }); - await Promise.all( + yield* putJson('route.json', { routes: [{ bundle: ssrBundlePath }] }); + yield* putJson('package.json', { type: 'module' }); + yield* Effect.all( [compiledUiAssetPath, ssrBundlePath, apiBundlePath, 'index.js', 'backendRemoteEntry.cjs'].map( - async (file) => await putText(file, 'console.log("compiled fixture");'), + Effect.fn(function* scenario8(file) { + return yield* putText(file, 'console.log("compiled fixture");'); + }), ), + { concurrency: 'unbounded' }, ); - const emit = async () => - await releaseFramework.emitFrameworkMicroVerticalReleaseEnvelope({ - apiOnly: false, - distDirectory: root, - target: 'node', - }); - return { artifact, emit, manifest, putJson, putText, root }; -}; - -void test('empty MF producers retain complete build and Node staged release evidence in every framework format', async (context) => { - await Promise.all( - ['cjs', 'esm', 'esm-node'].map(async (moduleFormat) => { - const fixture = await releaseFixture(context); - const extension = moduleFormat === 'cjs' ? 'js' : 'mjs'; - const framework = await loadReleaseFramework( - path.join( - releaseFrameworkRoot, - moduleFormat, - `ultramodern-release-envelope/framework-output.${extension}`, - ), - ); - const envelope = await framework.emitFrameworkMicroVerticalReleaseEnvelope({ - apiOnly: false, - distDirectory: fixture.root, - target: 'node', - }); - assert.ok(envelope.surfaces.uiClient.includes(compiledUiAssetPath)); - assert.deepEqual(envelope.surfaces.ssr, [ssrBundlePath]); - assert.deepEqual(envelope.surfaces.apiBackend, [apiBundlePath]); - await framework.verifyBuildOutputReleaseEnvelope(fixture.root, 'node'); - const staged = await framework.emitNodeStagedReleaseEnvelope({ - distDirectory: fixture.root, - outputDirectory: fixture.root, - }); - assert.ok(staged.surfaces.uiClient.includes(compiledUiAssetPath)); - await framework.verifyNodeReleaseEnvelopeStaging({ outputDirectory: fixture.root }); - }), - ); - const fixture = await releaseFixture(context); - await fixture.emit(); - await fixture.putText(compiledUiAssetPath, 'console.log("tampered");'); - await assert.rejects( - async () => await releaseFramework.verifyBuildOutputReleaseEnvelope(fixture.root, 'node'), - /digest|hash|size/iu, - ); + const emit = Effect.fn(function* scenario9() { + return yield* Schema.decodeUnknownEffect(ReleaseEnvelopeSchema)( + yield* Effect.promise( + async () => + await releaseFramework.emitFrameworkMicroVerticalReleaseEnvelope({ + apiOnly: false, + distDirectory: root, + target: 'node', + }), + ), + ); + }); + return { artifact, emit, framework: releaseFramework, manifest, putJson, putText, root }; }); -void test('empty MF producers bind root-relative route assets when publicPath is auto', async (context) => { - await Promise.all( - ['cjs', 'esm', 'esm-node'].map(async (moduleFormat) => { - const fixture = await releaseFixture(context); - fixture.manifest.metaData.publicPath = 'auto'; - await fixture.putJson(mfManifestFile, fixture.manifest); - await fixture.putJson(routesManifestFile, { - routeAssets: { index: { assets: [`/${compiledUiAssetPath}`] } }, - }); - const extension = moduleFormat === 'cjs' ? 'js' : 'mjs'; - const framework = await loadReleaseFramework( - path.join( - releaseFrameworkRoot, - moduleFormat, - `ultramodern-release-envelope/framework-output.${extension}`, - ), - ); - const envelope = await framework.emitFrameworkMicroVerticalReleaseEnvelope({ - apiOnly: false, - distDirectory: fixture.root, - target: 'node', - }); - assert.ok(envelope.surfaces.uiClient.includes(compiledUiAssetPath)); - }), - ); -}); +it.live( + 'empty MF producers retain complete build and Node staged release evidence in every framework format', + Effect.fn(function* scenario10() { + yield* Effect.all( + (['cjs', 'esm', 'esm-node'] as const).map( + Effect.fn(function* scenario11(moduleFormat) { + const fixture = yield* releaseFixture(moduleFormat); + const envelope = yield* fixture.emit(); + expect(envelope.surfaces.uiClient.includes(compiledUiAssetPath)).toBe(true); + expect(envelope.surfaces.ssr).toEqual([ssrBundlePath]); + expect(envelope.surfaces.apiBackend).toEqual([apiBundlePath]); + yield* Effect.promise( + async () => + await fixture.framework.verifyBuildOutputReleaseEnvelope(fixture.root, 'node'), + ); + const staged = yield* Schema.decodeUnknownEffect(ReleaseEnvelopeSchema)( + yield* Effect.promise( + async () => + await fixture.framework.emitNodeStagedReleaseEnvelope({ + distDirectory: fixture.root, + outputDirectory: fixture.root, + }), + ), + ); + expect(staged.surfaces.uiClient.includes(compiledUiAssetPath)).toBe(true); + yield* Effect.promise( + async () => + await fixture.framework.verifyNodeReleaseEnvelopeStaging({ + outputDirectory: fixture.root, + }), + ); + }), + ), + { concurrency: 'unbounded' }, + ); + const fixture = yield* releaseFixture(); + yield* fixture.emit(); + yield* fixture.putText(compiledUiAssetPath, 'console.log("tampered");'); + const failureCause1 = yield* Effect.flip( + Effect.sandbox( + Effect.fn(function* scenario12() { + return yield* Effect.promise( + async () => + await fixture.framework.verifyBuildOutputReleaseEnvelope(fixture.root, 'node'), + ); + })(), + ), + ); + expect(String(Cause.squash(failureCause1))).toMatch(/digest|hash|size/iu); + }), +); -void test('empty-producer fallback rejects undeclared, foreign, traversing, missing, and nonbrowser assets', async (context) => { - const references = [ - `https://foreign.example.test/app/${compiledUiAssetPath}`, - `https://assets.example.test/app/../app/${compiledUiAssetPath}`, - `https://assets.example.test/app/%2e%2e/app/${compiledUiAssetPath}`, - String.raw`https://assets.example.test/app/static\js/index.js`, - 'https://assets.example.test/app/static%5cjs/index.js', - 'https://assets.example.test/app/static/js/missing.js', - `https://assets.example.test/app/${apiBundlePath}`, - `https://assets.example.test/app/${ssrBundlePath}`, - `https://assets.example.test/app/${compiledUiAssetPath}?forged=true`, - ]; - await Promise.all( - references.map(async (reference) => { - const fixture = await releaseFixture(context); - await fixture.putJson(routesManifestFile, { - routeAssets: { index: { assets: [reference] } }, - }); - await assert.rejects( - fixture.emit, - /UI\/client manifest references no compiled execution module/u, - reference, - ); - }), - ); - const baseline = await releaseFixture(context); - const invalidManifests = [ - { ...baseline.manifest, exposes: [{ name: './Page' }] }, - { ...baseline.manifest, remotes: [{ name: 'shell' }] }, - { metaData: baseline.manifest.metaData, remotes: baseline.manifest.remotes }, - { exposes: baseline.manifest.exposes, metaData: baseline.manifest.metaData }, - { - ...baseline.manifest, - metaData: { ...baseline.manifest.metaData, remoteEntry: { name: '', path: '' } }, - }, - ]; - await Promise.all( - invalidManifests.map(async (manifest) => { - const fixture = await releaseFixture(context); - await fixture.putJson(mfManifestFile, manifest); - await assert.rejects( - fixture.emit, - /UI\/client manifest references no compiled execution module/u, - ); - }), - ); - const fixture = await releaseFixture(context); - await rm(path.join(fixture.root, routesManifestFile)); - await assert.rejects(fixture.emit, /ENOENT/u); -}); +it.live( + 'empty MF producers bind root-relative route assets when publicPath is auto', + Effect.fn(function* scenario13() { + yield* Effect.all( + (['cjs', 'esm', 'esm-node'] as const).map( + Effect.fn(function* scenario14(moduleFormat) { + const fixture = yield* releaseFixture(moduleFormat); + fixture.manifest.metaData.publicPath = 'auto'; + yield* fixture.putJson(mfManifestFile, fixture.manifest); + yield* fixture.putJson(routesManifestFile, { + routeAssets: { index: { assets: [`/${compiledUiAssetPath}`] } }, + }); + const envelope = yield* fixture.emit(); + expect(envelope.surfaces.uiClient.includes(compiledUiAssetPath)).toBe(true); + }), + ), + { concurrency: 'unbounded' }, + ); + }), +); -void test('empty MF producers cannot bypass backend, SSR, revision, or identity proof', async (context) => { - await Promise.all( - [apiBundlePath, ssrBundlePath, 'backendRemoteEntry.cjs'].map(async (file) => { - const fixture = await releaseFixture(context); - await rm(path.join(fixture.root, file)); - await assert.rejects( - fixture.emit, - /compiled Node Effect API|SSR artifacts|emitted together/u, - ); - }), - ); - const fixture = await releaseFixture(context); - await fixture.putJson('backend-mf-manifest.json', { - backendFederation: { - deliveryUnit: { ...fixture.artifact.deliveryUnit, sourceRevision: 'b'.repeat(40) }, - }, - }); - await assert.rejects(fixture.emit, /must match/u); - const workspaceArtifact = { - ...fixture.artifact, - deliveryUnit: { ...fixture.artifact.deliveryUnit, sourceRevision: 'workspace' }, - surfaces: { - api: { ...fixture.artifact.surfaces.api, sourceRevision: 'workspace' }, - ui: { ...fixture.artifact.surfaces.ui, sourceRevision: 'workspace' }, - }, - }; - await fixture.putJson('ultramodern-build.json', workspaceArtifact); - await assert.rejects(fixture.emit, /workspace/u); -}); +it.live( + 'empty-producer fallback rejects undeclared, foreign, traversing, missing, and nonbrowser assets', + Effect.fn(function* scenario15() { + const references = [ + `https://foreign.example.test/app/${compiledUiAssetPath}`, + `https://assets.example.test/app/../app/${compiledUiAssetPath}`, + `https://assets.example.test/app/%2e%2e/app/${compiledUiAssetPath}`, + String.raw`https://assets.example.test/app/static\js/index.js`, + 'https://assets.example.test/app/static%5cjs/index.js', + 'https://assets.example.test/app/static/js/missing.js', + `https://assets.example.test/app/${apiBundlePath}`, + `https://assets.example.test/app/${ssrBundlePath}`, + `https://assets.example.test/app/${compiledUiAssetPath}?forged=true`, + ]; + yield* Effect.all( + references.map( + Effect.fn(function* scenario16(reference) { + const fixture = yield* releaseFixture(); + yield* fixture.putJson(routesManifestFile, { + routeAssets: { index: { assets: [reference] } }, + }); + const failureCause2 = yield* Effect.flip(Effect.sandbox(fixture.emit())); + expect(String(Cause.squash(failureCause2)), reference).toMatch( + /UI\/client manifest references no compiled execution module/u, + ); + }), + ), + { concurrency: 'unbounded' }, + ); + const baseline = yield* releaseFixture(); + const invalidManifests = [ + { ...baseline.manifest, exposes: [{ name: './Page' }] }, + { ...baseline.manifest, remotes: [{ name: 'shell' }] }, + { metaData: baseline.manifest.metaData, remotes: baseline.manifest.remotes }, + { exposes: baseline.manifest.exposes, metaData: baseline.manifest.metaData }, + { + ...baseline.manifest, + metaData: { ...baseline.manifest.metaData, remoteEntry: { name: '', path: '' } }, + }, + ]; + yield* Effect.all( + invalidManifests.map( + Effect.fn(function* scenario17(manifest) { + const fixture = yield* releaseFixture(); + yield* fixture.putJson(mfManifestFile, manifest); + const failureCause3 = yield* Effect.flip(Effect.sandbox(fixture.emit())); + expect(String(Cause.squash(failureCause3))).toMatch( + /UI\/client manifest references no compiled execution module/u, + ); + }), + ), + { concurrency: 'unbounded' }, + ); + const fixture = yield* releaseFixture(); + yield* Effect.promise(() => rm(path.join(fixture.root, routesManifestFile))); + const failureCause4 = yield* Effect.flip(Effect.sandbox(fixture.emit())); + expect(String(Cause.squash(failureCause4))).toMatch(/ENOENT/u); + }), +); + +it.live( + 'empty MF producers cannot bypass backend, SSR, revision, or identity proof', + Effect.fn(function* scenario18() { + yield* Effect.all( + [apiBundlePath, ssrBundlePath, 'backendRemoteEntry.cjs'].map( + Effect.fn(function* scenario19(file) { + const fixture = yield* releaseFixture(); + yield* Effect.promise(() => rm(path.join(fixture.root, file))); + const failureCause5 = yield* Effect.flip(Effect.sandbox(fixture.emit())); + expect(String(Cause.squash(failureCause5))).toMatch( + /compiled Node Effect API|SSR artifacts|emitted together/u, + ); + }), + ), + { concurrency: 'unbounded' }, + ); + const fixture = yield* releaseFixture(); + yield* fixture.putJson('backend-mf-manifest.json', { + backendFederation: { + deliveryUnit: { ...fixture.artifact.deliveryUnit, sourceRevision: 'b'.repeat(40) }, + }, + }); + const failureCause6 = yield* Effect.flip(Effect.sandbox(fixture.emit())); + expect(String(Cause.squash(failureCause6))).toMatch(/must match/u); + const workspaceArtifact = { + ...fixture.artifact, + deliveryUnit: { ...fixture.artifact.deliveryUnit, sourceRevision: 'workspace' }, + surfaces: { + api: { ...fixture.artifact.surfaces.api, sourceRevision: 'workspace' }, + ui: { ...fixture.artifact.surfaces.ui, sourceRevision: 'workspace' }, + }, + }; + yield* fixture.putJson('ultramodern-build.json', workspaceArtifact); + const failureCause7 = yield* Effect.flip(Effect.sandbox(fixture.emit())); + expect(String(Cause.squash(failureCause7))).toMatch(/workspace/u); + }), +); const GlobalVarsSchema = Schema.Struct({ ULTRAMODERN_SHELL_ORIGIN: Schema.String }); -const evaluatePartyBuildGlobalVars = async (shellOrigin: string) => { - const temporaryRoot = await mkdtemp(path.join(os.tmpdir(), 'ontos-party-config-')); - try { +const evaluatePartyBuildGlobalVars = Effect.fn(function* scenario20(shellOrigin: string) { + const temporaryRoot = yield* Effect.promise(() => + mkdtemp(path.join(os.tmpdir(), 'ontos-party-config-')), + ); + return yield* Effect.gen(function* useResource1() { const harnessPath = path.join(temporaryRoot, 'read-config.mjs'); - const configSource = await readFile( - path.join(workspaceRoot, 'verticals/party-registry/modern.config.ts'), - 'utf-8', + const configSource = yield* Effect.promise(() => + readFile(path.join(workspaceRoot, 'verticals/party-registry/modern.config.ts'), 'utf-8'), ); const effectModuleUrl = pathToFileURL( require.resolve('effect', { paths: [workspaceRoot] }), ).href; - const { code } = await transform(configSource, { format: 'cjs', loader: 'ts' }); - await writeFile( - harnessPath, - `import * as effect from ${JSON.stringify(effectModuleUrl)}; + const { code } = yield* Effect.promise(() => + transform(configSource, { format: 'cjs', loader: 'ts' }), + ); + yield* Effect.promise(() => + writeFile( + harnessPath, + `import * as effect from ${JSON.stringify(effectModuleUrl)}; import * as sharedBuild from ${JSON.stringify(pathToFileURL(path.join(workspaceRoot, 'packages/shared-contracts/tooling/modern-config.ts')).href)}; import { runInNewContext } from 'node:vm'; const framework = { @@ -2025,124 +2211,149 @@ runInNewContext(${JSON.stringify(code)}, { }); process.stdout.write(JSON.stringify(module.exports.default.source.globalVars)); `, + ), ); const output = runNode([harnessPath]); - return Schema.decodeUnknownSync(Schema.fromJsonString(GlobalVarsSchema))(output); - } finally { - await rm(temporaryRoot, { force: true, recursive: true }); - } -}; - -void test('Party build configuration injects the exact nonlocal Shell origin into the API runtime', async () => { - const shellOrigin = 'https://operations.example.test'; - const globalVars = await evaluatePartyBuildGlobalVars(shellOrigin); - assert.equal(globalVars.ULTRAMODERN_SHELL_ORIGIN, shellOrigin); + return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(GlobalVarsSchema))(output); + }).pipe( + Effect.ensuring(Effect.promise(() => rm(temporaryRoot, { force: true, recursive: true }))), + ); }); -void test('compiled Party CORS reader uses the nonlocal DefinePlugin origin without a runtime global', async () => { - const shellOrigin = 'https://operations.example.test'; - const globalVars = await evaluatePartyBuildGlobalVars(shellOrigin); - const partyRoot = path.join(workspaceRoot, partyDirectory); - const source = await readFile(path.join(partyRoot, apiIndexFile), 'utf-8'); - const reader = - /(?declare const ULTRAMODERN_SHELL_ORIGIN[\s\S]+?const shellOrigin = readShellOrigin\(\);)/u.exec( - source, - )?.groups?.reader; - assert.notEqual(reader, undefined, 'compile the actual API origin-reader boundary'); - const appToolsPath = require.resolve('@modern-js/app-tools/config', { paths: [partyRoot] }); - const rsbuildPath = require.resolve('@rsbuild/core', { paths: [appToolsPath] }); - const rspackModule: unknown = require(require.resolve('@rspack/core', { paths: [rsbuildPath] })); - const rspackFixture = Schema.decodeUnknownSync(RspackModuleFixtureSchema)(rspackModule); - const temporaryRoot = await mkdtemp(path.join(partyRoot, 'node_modules/.ontos-compiled-cors-')); - try { - const entry = path.join(temporaryRoot, 'reader.ts'); - await writeFile( - entry, - `import { Schema } from 'effect';\nimport { resolvePartyRegistryShellOrigin, partyRegistryCorsAllowedOrigins } from ${JSON.stringify(path.join(partyRoot, 'api/read-server-support.ts'))};\n${reader}\nexport const allowedOrigins = partyRegistryCorsAllowedOrigins(shellOrigin);\n`, - ); - const definePlugin = new rspackFixture.DefinePlugin( - Object.fromEntries( - Object.entries(globalVars).map(([key, value]) => [key, JSON.stringify(value)]), - ), +it.live( + 'Party build configuration injects the exact nonlocal Shell origin into the API runtime', + Effect.fn(function* scenario21() { + const shellOrigin = 'https://operations.example.test'; + const globalVars = yield* evaluatePartyBuildGlobalVars(shellOrigin); + expect(globalVars.ULTRAMODERN_SHELL_ORIGIN).toBe(shellOrigin); + }), +); + +it.live( + 'compiled Party CORS reader uses the nonlocal DefinePlugin origin without a runtime global', + Effect.fn(function* scenario22() { + const shellOrigin = 'https://operations.example.test'; + const globalVars = yield* evaluatePartyBuildGlobalVars(shellOrigin); + const partyRoot = path.join(workspaceRoot, partyDirectory); + const source = yield* Effect.promise(() => + readFile(path.join(partyRoot, 'api/index.ts'), 'utf-8'), ); - const createCompiler = rspackFixture.rspack.bind(rspackModule); - const compilerSource = createCompiler({ - entry, - externals: { effect: 'commonjs effect' }, - mode: 'none', - module: { - rules: [ - { - test: /\.ts$/u, - use: { - loader: 'builtin:swc-loader', - options: { jsc: { parser: { syntax: 'typescript' } } }, + const reader = + /(?declare const ULTRAMODERN_SHELL_ORIGIN[\s\S]+?const shellOrigin = readShellOrigin\(\);)/u.exec( + source, + )?.groups?.reader; + expect(reader, 'compile the actual API origin-reader boundary').not.toBe(undefined); + const appToolsPath = require.resolve('@modern-js/app-tools/config', { paths: [partyRoot] }); + const rsbuildPath = require.resolve('@rsbuild/core', { paths: [appToolsPath] }); + const rspackModule: unknown = require( + require.resolve('@rspack/core', { paths: [rsbuildPath] }), + ); + const rspackFixture = + yield* Schema.decodeUnknownEffect(RspackModuleFixtureSchema)(rspackModule); + const temporaryRoot = yield* Effect.promise(() => + mkdtemp(path.join(partyRoot, 'node_modules/.ontos-compiled-cors-')), + ); + yield* Effect.gen(function* useResource3() { + const entry = path.join(temporaryRoot, 'reader.ts'); + yield* Effect.promise(() => + writeFile( + entry, + `import { Schema } from 'effect';\nimport { resolvePartyRegistryShellOrigin, partyRegistryCorsAllowedOrigins } from ${JSON.stringify(path.join(partyRoot, 'api/read-server-support.ts'))};\n${reader}\nexport const allowedOrigins = partyRegistryCorsAllowedOrigins(shellOrigin);\n`, + ), + ); + const definePlugin = new rspackFixture.DefinePlugin( + Object.fromEntries( + Object.entries(globalVars).map(([key, value]) => [key, JSON.stringify(value)]), + ), + ); + const createCompiler = rspackFixture.rspack.bind(rspackModule); + const compilerSource = createCompiler({ + entry, + externals: { effect: 'commonjs effect' }, + mode: 'none', + module: { + rules: [ + { + test: /\.ts$/u, + use: { + loader: 'builtin:swc-loader', + options: { jsc: { parser: { syntax: 'typescript' } } }, + }, }, - }, - ], - }, - output: { filename: 'reader.cjs', library: { type: 'commonjs2' }, path: temporaryRoot }, - plugins: [definePlugin], - target: 'node', - }); - const compiler = Schema.decodeUnknownSync(CompilerFixtureSchema)({ - close: compilerSource.close, - run: compilerSource.run, - }); - const runCompiler = promisify(compiler.run.bind(compilerSource)); - const closeCompiler = promisify(compiler.close.bind(compilerSource)); - try { - const statsSource = await runCompiler(); - assert.ok(statsSource); - const stats = Schema.decodeUnknownSync(CompilerStatsFixtureSchema)({ - hasErrors: statsSource.hasErrors, - toString: statsSource.toString, + ], + }, + output: { filename: 'reader.cjs', library: { type: 'commonjs2' }, path: temporaryRoot }, + plugins: [definePlugin], + target: 'node', }); - const hasErrors = stats.hasErrors.bind(statsSource)(); - const errorText = stats.toString.bind(statsSource)({ all: false, errors: true }); - assert.equal(hasErrors, false, errorText); - } finally { - await closeCompiler(); - } - const compiledReaderModule: unknown = require(path.join(temporaryRoot, 'reader.cjs')); - const compiledReader = Schema.decodeUnknownSync(CompiledReaderSchema)(compiledReaderModule); - assert.deepEqual([...compiledReader.allowedOrigins], [shellOrigin]); - } finally { - await rm(temporaryRoot, { force: true, recursive: true }); - } -}); + const compiler = yield* Schema.decodeUnknownEffect(CompilerFixtureSchema)({ + close: compilerSource.close, + run: compilerSource.run, + }); + const runCompiler = promisify(compiler.run.bind(compilerSource)); + const closeCompiler = promisify(compiler.close.bind(compilerSource)); + yield* Effect.gen(function* useResource2() { + const statsSource = yield* Effect.promise(() => runCompiler()); + expect(statsSource).toBeDefined(); + if (!statsSource) { + throw new Error('Compiler stats are missing'); + } + const stats = yield* Schema.decodeUnknownEffect(CompilerStatsFixtureSchema)({ + hasErrors: statsSource.hasErrors, + toString: statsSource.toString, + }); + const hasErrors = stats.hasErrors.bind(statsSource)(); + const errorText = stats.toString.bind(statsSource)({ all: false, errors: true }); + expect(hasErrors, errorText).toBe(false); + }).pipe(Effect.ensuring(Effect.promise(() => closeCompiler()))); + const compiledReaderModule: unknown = require(path.join(temporaryRoot, 'reader.cjs')); + const compiledReader = + yield* Schema.decodeUnknownEffect(CompiledReaderSchema)(compiledReaderModule); + expect([...compiledReader.allowedOrigins]).toEqual([shellOrigin]); + }).pipe( + Effect.ensuring(Effect.promise(() => rm(temporaryRoot, { force: true, recursive: true }))), + ); + }), +); -const normalizedGeneratedSource = async (fileName: string, source: string) => { - const result = await format(fileName, source, { singleQuote: true, sortImports: true }); - assert.deepEqual(result.errors, []); +const normalizedGeneratedSource = Effect.fn(function* scenario23(fileName: string, source: string) { + const result = yield* Effect.promise(() => + format(fileName, source, { singleQuote: true, sortImports: true }), + ); + expect(result.errors).toEqual([]); return result.code.replaceAll(/^\s*\n/gmu, ''); -}; +}); -const evaluatedInfrastructureSource = async ( +const evaluatedInfrastructureSource = Effect.fn(function* mergedScenario1( fileName: string, source: string, cloudflare: boolean, -): Promise => { +) { const partyRoot = path.join(workspaceRoot, partyDirectory); - const result = await bundleSource({ - bundle: true, - define: { - 'import.meta.resolve': '__resolve', - 'import.meta.url': JSON.stringify(pathToFileURL(path.join(partyRoot, fileName)).href), - }, - external: ['./src/routes/ultramodern-route-metadata'], - format: 'cjs', - packages: 'external', - platform: 'node', - stdin: { - contents: source, - loader: 'ts', - resolveDir: path.dirname(path.join(partyRoot, fileName)), - }, - write: false, - }); + const result = yield* Effect.promise(() => + bundleSource({ + bundle: true, + define: { + 'import.meta.resolve': '__resolve', + 'import.meta.url': JSON.stringify(pathToFileURL(path.join(partyRoot, fileName)).href), + }, + external: ['./src/routes/ultramodern-route-metadata'], + format: 'cjs', + packages: 'external', + platform: 'node', + stdin: { + contents: source, + loader: 'ts', + resolveDir: path.dirname(path.join(partyRoot, fileName)), + }, + write: false, + }), + ); const code = result.outputFiles[0]?.text; - assert.ok(code); + expect(code).toBeTruthy(); + if (!code) { + throw new Error(EXPECTED_PROOF_VALUE); + } const effectUrl = pathToFileURL(require.resolve('effect')).href; const buildIdentityUrl = pathToFileURL( createRequire(path.join(partyRoot, fileName)).resolve( @@ -2219,487 +2430,572 @@ const normalize = (_key, value) => { process.stdout.write(JSON.stringify({ exported: module.exports, observations }, normalize)); `, ]); -}; +}); -void test('all published scaffold formats retain Party infrastructure behavior and source parity', async () => { - await Promise.all( - ['esm', 'esm-node', 'cjs'].map(async (moduleFormat) => { - const extension = moduleFormat === 'cjs' ? 'cjs' : 'js'; - const generatorModulePath = (name: string): string => - path.join(generatorRoot, `dist/${moduleFormat}/ultramodern-workspace/${name}.${extension}`); - const descriptorPath = generatorModulePath('descriptors'); - const descriptorSource: unknown = - moduleFormat === 'cjs' - ? require(descriptorPath) - : await import(pathToFileURL(descriptorPath).href); - const descriptorModule = Schema.decodeUnknownSync(DescriptorModuleSchema)(descriptorSource); - const componentPath = generatorModulePath('demo-components'); - const componentSource: unknown = - moduleFormat === 'cjs' - ? require(componentPath) - : await import(pathToFileURL(componentPath).href); - const componentModule = Schema.decodeUnknownSync(ComponentModuleSchema)(componentSource); - const federationPath = generatorModulePath('module-federation/config'); - const federationSource: unknown = - moduleFormat === 'cjs' - ? require(federationPath) - : await import(pathToFileURL(federationPath).href); - const federationModule = Schema.decodeUnknownSync(FederationConfigModuleSchema)( - federationSource, - ); - const buildModulePath = generatorModulePath('module-federation/reexport-module'); - const buildModuleSource: unknown = - moduleFormat === 'cjs' - ? require(buildModulePath) - : await import(pathToFileURL(buildModulePath).href); - const buildModule = Schema.decodeUnknownSync(BuildModuleGeneratorSchema)(buildModuleSource); - const createVerticalDescriptor = - descriptorModule.createVerticalDescriptor.bind(descriptorSource); - const createLayout = componentModule.createLayout.bind(componentSource); - const createAppModernConfig = federationModule.createAppModernConfig.bind(federationSource); - const createBackendModuleFederationConfig = - federationModule.createBackendModuleFederationConfig.bind(federationSource); - const createUltramodernBuildModule = - buildModule.createUltramodernBuildModule.bind(buildModuleSource); - const descriptor: unknown = createVerticalDescriptor(partyId, 4102); - Schema.asserts(WorkspaceAppFixtureSchema, descriptor); - const app = { ...descriptor, exposes: {} }; - const generated = { - 'backend-federation.config.ts': Schema.decodeUnknownSync(Schema.String)( - createBackendModuleFederationConfig(app), - ), - [buildMarkerFile]: Schema.decodeUnknownSync(Schema.String)( - createUltramodernBuildModule('app', app), - ), - 'modern.config.ts': Schema.decodeUnknownSync(Schema.String)( - createAppModernConfig('app', app), - ), - 'src/routes/layout.tsx': Schema.decodeUnknownSync(Schema.String)(createLayout(app.id)), - }; - await Promise.all( - Object.entries(generated).map(async ([fileName, source]) => { - const actual = await readFile( - path.join(workspaceRoot, partyDirectory, fileName), - 'utf-8', +it.live( + 'all published scaffold formats retain Party infrastructure behavior and source parity', + Effect.fn(function* mergedScenario4() { + yield* Effect.all( + ['esm', 'esm-node', 'cjs'].map( + Effect.fn(function* mergedScenario3(moduleFormat) { + const descriptorPath = publishedGeneratorModulePath(moduleFormat)('descriptors'); + const descriptorSource: unknown = + moduleFormat === 'cjs' + ? require(descriptorPath) + : yield* Effect.promise(() => import(pathToFileURL(descriptorPath).href)); + const descriptorModule = + Schema.decodeUnknownSync(DescriptorModuleSchema)(descriptorSource); + const componentPath = publishedGeneratorModulePath(moduleFormat)('demo-components'); + const componentSource: unknown = + moduleFormat === 'cjs' + ? require(componentPath) + : yield* Effect.promise(() => import(pathToFileURL(componentPath).href)); + const componentModule = Schema.decodeUnknownSync(ComponentModuleSchema)(componentSource); + const federationPath = publishedGeneratorModulePath(moduleFormat)( + 'module-federation/config', + ); + const federationSource: unknown = + moduleFormat === 'cjs' + ? require(federationPath) + : yield* Effect.promise(() => import(pathToFileURL(federationPath).href)); + const federationModule = Schema.decodeUnknownSync(FederationConfigModuleSchema)( + federationSource, + ); + const buildModulePath = publishedGeneratorModulePath(moduleFormat)( + 'module-federation/reexport-module', ); - if (fileName === 'modern.config.ts' || fileName === 'shared/ultramodern-build.ts') { - await Promise.all( - [false, true].map(async (cloudflare) => { - const [expected, evaluated] = await Promise.all([ - evaluatedInfrastructureSource(fileName, source, cloudflare), - evaluatedInfrastructureSource(fileName, actual, cloudflare), - ]); - const decode = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Json)); - assert.deepEqual( - decode(expected), - decode(evaluated), - `${moduleFormat}: ${fileName} must preserve evaluated configuration, build identity and plugin behavior`, + const buildModuleSource: unknown = + moduleFormat === 'cjs' + ? require(buildModulePath) + : yield* Effect.promise(() => import(pathToFileURL(buildModulePath).href)); + const buildModule = Schema.decodeUnknownSync(BuildModuleGeneratorSchema)( + buildModuleSource, + ); + const createVerticalDescriptor = + descriptorModule.createVerticalDescriptor.bind(descriptorSource); + const createLayout = componentModule.createLayout.bind(componentSource); + const createAppModernConfig = + federationModule.createAppModernConfig.bind(federationSource); + const createBackendModuleFederationConfig = + federationModule.createBackendModuleFederationConfig.bind(federationSource); + const createUltramodernBuildModule = + buildModule.createUltramodernBuildModule.bind(buildModuleSource); + const descriptor: unknown = createVerticalDescriptor(partyId, 4102); + Schema.asserts(WorkspaceAppFixtureSchema, descriptor); + const app = { ...descriptor, exposes: {} }; + const generated = { + 'backend-federation.config.ts': Schema.decodeUnknownSync(Schema.String)( + createBackendModuleFederationConfig(app), + ), + [buildMarkerFile]: Schema.decodeUnknownSync(Schema.String)( + createUltramodernBuildModule('app', app), + ), + 'modern.config.ts': Schema.decodeUnknownSync(Schema.String)( + createAppModernConfig('app', app), + ), + 'src/routes/layout.tsx': Schema.decodeUnknownSync(Schema.String)(createLayout(app.id)), + }; + yield* Effect.all( + Object.entries(generated).map( + Effect.fn(function* mergedScenario2([fileName, source]) { + const actual = yield* Effect.promise(() => + readFile(path.join(workspaceRoot, partyDirectory, fileName), 'utf-8'), ); + if (fileName === 'modern.config.ts' || fileName === 'shared/ultramodern-build.ts') { + yield* Effect.all( + [false, true].map( + Effect.fn(function* mergedScenario1(cloudflare) { + const [expected, evaluated] = yield* Effect.all( + [ + evaluatedInfrastructureSource(fileName, source, cloudflare), + evaluatedInfrastructureSource(fileName, actual, cloudflare), + ], + { concurrency: 'unbounded' }, + ); + const decode = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Json)); + expect( + decode(expected), + `${moduleFormat}: ${fileName} must preserve evaluated configuration, build identity and plugin behavior`, + ).toEqual(decode(evaluated)); + }), + ), + { concurrency: 'unbounded' }, + ); + return; + } + expect( + yield* normalizedGeneratedSource(fileName, source), + `${moduleFormat}: ${fileName} must match the controlled scaffold`, + ).toBe(yield* normalizedGeneratedSource(fileName, actual)); }), - ); - return; - } - assert.equal( - await normalizedGeneratedSource(fileName, source), - await normalizedGeneratedSource(fileName, actual), - `${moduleFormat}: ${fileName} must match the controlled scaffold`, + ), + { concurrency: 'unbounded' }, ); }), - ); - }), - ); -}); + ), + { concurrency: 'unbounded' }, + ); + }), +); -void test('all published scaffold formats generate the shared MicroVertical API baseline', async () => { - await Promise.all( - ['esm', 'esm-node', 'cjs'].map(async (moduleFormat) => { - const extension = moduleFormat === 'cjs' ? 'cjs' : 'js'; - const generatorModulePath = (name: string): string => - path.join(generatorRoot, `dist/${moduleFormat}/ultramodern-workspace/${name}.${extension}`); - const descriptorPath = generatorModulePath('descriptors'); - const sharedApiPath = generatorModulePath(generatedSharedApiModule); - const clientPath = generatorModulePath('api/client'); - const servicePath = generatorModulePath(generatedServiceModuleName); - const descriptorSource: unknown = - moduleFormat === 'cjs' - ? require(descriptorPath) - : await import(pathToFileURL(descriptorPath).href); - const sharedApiSource: unknown = - moduleFormat === 'cjs' - ? require(sharedApiPath) - : await import(pathToFileURL(sharedApiPath).href); - const clientSource: unknown = - moduleFormat === 'cjs' ? require(clientPath) : await import(pathToFileURL(clientPath).href); - const serviceSource: unknown = - moduleFormat === 'cjs' - ? require(servicePath) - : await import(pathToFileURL(servicePath).href); - const descriptorModule = Schema.decodeUnknownSync(DescriptorModuleSchema)(descriptorSource); - const sharedApiModule = Schema.decodeUnknownSync(SharedApiGeneratorSchema)(sharedApiSource); - const clientModule = Schema.decodeUnknownSync(ApiClientGeneratorSchema)(clientSource); - const serviceModule = Schema.decodeUnknownSync(ApiServiceGeneratorSchema)(serviceSource); - const descriptor: unknown = descriptorModule.createVerticalDescriptor(inventoryStockId, 4103); - Schema.asserts(WorkspaceAppFixtureSchema, descriptor); - const app = { ...descriptor, exposes: {} }; - const contract = sharedApiModule.createSharedApi(generatedProofScope, app); - const client = clientModule.createApiClient(app, generatedClientContractImport); - const service = serviceModule.createApiServiceEntry( - generatedProofScope, - app, - generatedSharedApiImport, - ); +it.live( + 'all published scaffold formats generate the shared MicroVertical API baseline', + Effect.fn(function* governanceScenario8() { + yield* Effect.all( + ['esm', 'esm-node', 'cjs'].map( + Effect.fn(function* governanceScenario9(moduleFormat) { + const descriptorPath = publishedGeneratorModulePath(moduleFormat)('descriptors'); + const sharedApiPath = + publishedGeneratorModulePath(moduleFormat)(generatedSharedApiModule); + const clientPath = publishedGeneratorModulePath(moduleFormat)('api/client'); + const servicePath = publishedGeneratorModulePath(moduleFormat)( + generatedServiceModuleName, + ); + const descriptorSource: unknown = + moduleFormat === 'cjs' + ? require(descriptorPath) + : yield* Effect.promise(() => import(pathToFileURL(descriptorPath).href)); + const sharedApiSource: unknown = + moduleFormat === 'cjs' + ? require(sharedApiPath) + : yield* Effect.promise(() => import(pathToFileURL(sharedApiPath).href)); + const clientSource: unknown = + moduleFormat === 'cjs' + ? require(clientPath) + : yield* Effect.promise(() => import(pathToFileURL(clientPath).href)); + const serviceSource: unknown = + moduleFormat === 'cjs' + ? require(servicePath) + : yield* Effect.promise(() => import(pathToFileURL(servicePath).href)); + const descriptorModule = + yield* Schema.decodeUnknownEffect(DescriptorModuleSchema)(descriptorSource); + const sharedApiModule = + yield* Schema.decodeUnknownEffect(SharedApiGeneratorSchema)(sharedApiSource); + const clientModule = + yield* Schema.decodeUnknownEffect(ApiClientGeneratorSchema)(clientSource); + const serviceModule = + yield* Schema.decodeUnknownEffect(ApiServiceGeneratorSchema)(serviceSource); + const descriptor = yield* Schema.decodeUnknownEffect(WorkspaceAppFixtureSchema, { + onExcessProperty: 'preserve', + })(descriptorModule.createVerticalDescriptor(inventoryStockId, 4103), { + onExcessProperty: 'preserve', + }); + const app = { ...descriptor, exposes: {} }; + const contract = sharedApiModule.createSharedApi(generatedProofScope, app); + const client = clientModule.createApiClient(app, generatedClientContractImport); + const service = serviceModule.createApiServiceEntry( + generatedProofScope, + app, + generatedSharedApiImport, + ); - assert.match(contract, /MicroVerticalBuildMarkerSchema/u, moduleFormat); - assert.match(contract, /MicroVerticalReadinessSchema/u, moduleFormat); - assert.match(contract, /createMicroVerticalOperationContext/u, moduleFormat); - assert.match(contract, /@generated-proof\/shared-contracts/u, moduleFormat); - assert.doesNotMatch(contract, /export interface OperationContext/u, moduleFormat); - assert.match(client, /client\.foundation\.readiness\(\{\}\)/u, moduleFormat); - assert.doesNotMatch(client, /client\.inventoryStock\.readiness/u, moduleFormat); - assert.match(service, /microVerticalOperationAttributes/u, moduleFormat); - assert.match(service, /@generated-proof\/shared-contracts/u, moduleFormat); - assert.doesNotMatch(service, /const operationAttributes/u, moduleFormat); - const generatedBaselineExpectation = { - additionalPaths: {}, - apiPrefix: `/${inventoryStockId}-api`, - basePath: `/${inventoryStockId}-api/${inventoryStockId}`, - effectClientPackage, - ownerId: inventoryStockId, - readinessPath: `/${inventoryStockId}-api/${inventoryStockId}/readiness`, - sharedContractsPackage: generatedSharedContractsPackage, - } as const; - assert.equal( - microVerticalApiBaselineViolation(inventoryStockId, contract, generatedBaselineExpectation), - undefined, - ); + expect(contract, moduleFormat).toMatch(/MicroVerticalBuildMarkerSchema/u); + expect(contract, moduleFormat).toMatch(/MicroVerticalReadinessSchema/u); + expect(contract, moduleFormat).toMatch(/createMicroVerticalOperationContext/u); + expect(contract, moduleFormat).toMatch(/@generated-proof\/shared-contracts/u); + expect(contract, moduleFormat).not.toMatch(/export interface OperationContext/u); + expect(client, moduleFormat).toMatch(/client\.foundation\.readiness\(\{\}\)/u); + expect(client, moduleFormat).not.toMatch(/client\.inventoryStock\.readiness/u); + expect(service, moduleFormat).toMatch(/microVerticalOperationAttributes/u); + expect(service, moduleFormat).toMatch(/@generated-proof\/shared-contracts/u); + expect(service, moduleFormat).not.toMatch(/const operationAttributes/u); + const generatedBaselineExpectation = { + additionalPaths: {}, + apiPrefix: `/${inventoryStockId}-api`, + basePath: `/${inventoryStockId}-api/${inventoryStockId}`, + effectClientPackage, + ownerId: inventoryStockId, + readinessPath: `/${inventoryStockId}-api/${inventoryStockId}/readiness`, + sharedContractsPackage: generatedSharedContractsPackage, + } as const; + expect( + yield* microVerticalApiBaselineViolation( + inventoryStockId, + contract, + generatedBaselineExpectation, + ), + ).toBe(undefined); - const customStemApp = { - ...app, - api: { ...app.api, prefix: warehouseApiPrefix, stem: warehouseItemsApiStem }, - }; - const customStemContract = sharedApiModule.createSharedApi( - generatedProofScope, - customStemApp, - ); - assert.equal( - microVerticalApiBaselineViolation(warehouseItemsApiStem, customStemContract, { - ...generatedBaselineExpectation, - apiPrefix: warehouseApiPrefix, - basePath: `/warehouse-api/${warehouseItemsApiStem}`, - readinessPath: `/warehouse-api/${warehouseItemsApiStem}/readiness`, - }), - undefined, - ); + const customStemApp = { + ...app, + api: { ...app.api, prefix: warehouseApiPrefix, stem: warehouseItemsApiStem }, + }; + const customStemContract = sharedApiModule.createSharedApi( + generatedProofScope, + customStemApp, + ); + expect( + yield* microVerticalApiBaselineViolation(warehouseItemsApiStem, customStemContract, { + ...generatedBaselineExpectation, + apiPrefix: warehouseApiPrefix, + basePath: `/warehouse-api/${warehouseItemsApiStem}`, + readinessPath: `/warehouse-api/${warehouseItemsApiStem}/readiness`, + }), + ).toBe(undefined); - const checkoutDescriptor: unknown = descriptorModule.createVerticalDescriptor( - checkoutId, - 4105, - ); - Schema.asserts(WorkspaceAppFixtureSchema, checkoutDescriptor); - const checkoutContract = sharedApiModule.createSharedApi(generatedProofScope, { - ...checkoutDescriptor, - exposes: {}, - }); - const checkoutClient = clientModule.createApiClient( - checkoutDescriptor, - generatedClientContractImport, - ); - assert.equal( - microVerticalApiBaselineViolation(checkoutId, checkoutContract, { - additionalPaths: { - checkoutCartPath: '/checkout-api/checkout/cart', - }, - ownerId: checkoutId, - sharedContractsPackage: generatedSharedContractsPackage, + const checkoutDescriptor = yield* Schema.decodeUnknownEffect(WorkspaceAppFixtureSchema, { + onExcessProperty: 'preserve', + })(descriptorModule.createVerticalDescriptor(checkoutId, 4105), { + onExcessProperty: 'preserve', + }); + const checkoutContract = sharedApiModule.createSharedApi(generatedProofScope, { + ...checkoutDescriptor, + exposes: {}, + }); + const checkoutClient = clientModule.createApiClient( + checkoutDescriptor, + generatedClientContractImport, + ); + expect( + yield* microVerticalApiBaselineViolation(checkoutId, checkoutContract, { + additionalPaths: { + checkoutCartPath: '/checkout-api/checkout/cart', + }, + ownerId: checkoutId, + sharedContractsPackage: generatedSharedContractsPackage, + }), + `${moduleFormat} checkout cart operations must retain baseline validation`, + ).toBe(undefined); + expect(checkoutClient, moduleFormat).toMatch(/client\.foundation\.readiness\(\{\}\)/u); + expect(checkoutClient, moduleFormat).not.toMatch(/client\.checkout\.readiness/u); }), - undefined, - `${moduleFormat} checkout cart operations must retain baseline validation`, - ); - assert.match(checkoutClient, /client\.foundation\.readiness\(\{\}\)/u, moduleFormat); - assert.doesNotMatch(checkoutClient, /client\.checkout\.readiness/u, moduleFormat); - }), - ); -}); + ), + { concurrency: 'unbounded' }, + ); + }), +); -/** - * Generator proofs compile real output, so each one gets its own scratch root inside the - * shared-contracts package and drops it when the owning test finishes. - */ -const makeProofRoot = async (context: TestContext, prefix: string): Promise => { +const makeProofRoot = Effect.fn(function* mergedScenario1(prefix: string) { const scratchRoot = path.join(workspaceRoot, 'packages/shared-contracts/.scratch'); - await mkdir(scratchRoot, { recursive: true }); - const proofRoot = await mkdtemp(path.join(scratchRoot, `${prefix}-`)); - context.after(async (): Promise => { - await rm(proofRoot, { force: true, recursive: true }); - }); + yield* Effect.promise(() => mkdir(scratchRoot, { recursive: true })); + const proofRoot = yield* Effect.acquireRelease( + Effect.promise(() => mkdtemp(path.join(scratchRoot, `${prefix}-`))), + (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), + ); + return proofRoot; -}; +}); -void test('all published scaffold formats emit the executable AST baseline validator', async (context) => { - const proofRoot = await makeProofRoot(context, 'generated-baseline-validator-proof'); - const expectedHelper = await readFile( - path.join(workspaceRoot, 'scripts/microvertical-api-baseline-boundary.mts'), - 'utf-8', - ); +it.live( + 'all published scaffold formats emit the executable AST baseline validator', + Effect.fn(function* mergedScenario2() { + const proofRoot = yield* makeProofRoot('generated-baseline-validator-proof'); + const expectedHelper = yield* Effect.promise(() => + readFile( + path.join(workspaceRoot, 'scripts/microvertical-api-baseline-boundary.mts'), + 'utf-8', + ), + ); - await Promise.all( - ['esm', 'esm-node', 'cjs'].map(async (moduleFormat) => { - const extension = moduleFormat === 'cjs' ? 'cjs' : 'js'; - const modulePath = path.join( - generatorRoot, - `dist/${moduleFormat}/ultramodern-workspace/workspace-scripts.${extension}`, - ); - const moduleSource: unknown = - moduleFormat === 'cjs' ? require(modulePath) : await import(pathToFileURL(modulePath).href); - const generator = Schema.decodeUnknownSync(WorkspaceScriptsGeneratorSchema)(moduleSource); - const artifacts = generator.migratedWorkspaceScriptArtifacts({ - hasBackendSurface: true, - shellOnly: false, - }); - const helper = artifacts.find( - ({ relativePath }) => relativePath === 'scripts/microvertical-api-baseline-boundary.mts', - ); - const checker = artifacts.find(({ relativePath }) => relativePath === apiBoundaryCheckerPath); - assert.ok(helper, `${moduleFormat} must emit the AST baseline helper`); - assert.ok(checker, `${moduleFormat} must emit the API checker`); - assert.equal( - helper.content, - expectedHelper, - `${moduleFormat} must emit byte-exact baseline source`, - ); - assert.equal( - await normalizedGeneratedSource('microvertical-api-baseline-boundary.mts', helper.content), - await normalizedGeneratedSource('microvertical-api-baseline-boundary.mts', expectedHelper), - `${moduleFormat} must emit the exact repository validator`, - ); - const formatRoot = path.join(proofRoot, moduleFormat); - await writeText(formatRoot, helper.relativePath, helper.content); - await writeText(formatRoot, checker.relativePath, checker.content); - assert.match(checker.content, /microVerticalApiBaselineViolation/u); - assert.match( - runNode([path.join(formatRoot, checker.relativePath)], { - env: { ULTRAMODERN_WORKSPACE_ROOT: workspaceRoot }, - }), - /UltraModern API boundary check passed/u, - moduleFormat, - ); + yield* Effect.all( + ['esm', 'esm-node', 'cjs'].map( + Effect.fn(function* mergedScenario1(moduleFormat) { + const extension = moduleFormat === 'cjs' ? 'cjs' : 'js'; + const modulePath = path.join( + generatorRoot, + `dist/${moduleFormat}/ultramodern-workspace/workspace-scripts.${extension}`, + ); + const moduleSource: unknown = + moduleFormat === 'cjs' + ? require(modulePath) + : yield* Effect.promise(() => import(pathToFileURL(modulePath).href)); + const generator = Schema.decodeUnknownSync(WorkspaceScriptsGeneratorSchema)(moduleSource); + const artifacts = generator.migratedWorkspaceScriptArtifacts({ + hasBackendSurface: true, + shellOnly: false, + }); + const helper = artifacts.find( + ({ relativePath }) => + relativePath === 'scripts/microvertical-api-baseline-boundary.mts', + ); + const checker = artifacts.find( + ({ relativePath }) => relativePath === apiBoundaryCheckerPath, + ); + expect(helper, `${moduleFormat} must emit the AST baseline helper`).toBeTruthy(); + if (!helper) { + throw new Error(EXPECTED_PROOF_VALUE); + } + expect(checker, `${moduleFormat} must emit the API checker`).toBeTruthy(); + if (!checker) { + throw new Error(EXPECTED_PROOF_VALUE); + } + expect(helper.content, `${moduleFormat} must emit byte-exact baseline source`).toBe( + expectedHelper, + ); + expect( + yield* normalizedGeneratedSource( + 'microvertical-api-baseline-boundary.mts', + helper.content, + ), + `${moduleFormat} must emit the exact repository validator`, + ).toBe( + yield* normalizedGeneratedSource( + 'microvertical-api-baseline-boundary.mts', + expectedHelper, + ), + ); + const formatRoot = path.join(proofRoot, moduleFormat); + yield* writeText(formatRoot, helper.relativePath, helper.content); + yield* writeText(formatRoot, checker.relativePath, checker.content); + expect(checker.content).toMatch(/microVerticalApiBaselineViolation/u); + expect( + runNode([path.join(formatRoot, checker.relativePath)], { + env: { ULTRAMODERN_WORKSPACE_ROOT: workspaceRoot }, + }), + moduleFormat, + ).toMatch(/UltraModern API boundary check passed/u); - const generatorModulePath = (name: string): string => - path.join(generatorRoot, `dist/${moduleFormat}/ultramodern-workspace/${name}.${extension}`); - const descriptorSource: unknown = - moduleFormat === 'cjs' - ? require(generatorModulePath('descriptors')) - : await import(pathToFileURL(generatorModulePath('descriptors')).href); - const sharedApiSource: unknown = - moduleFormat === 'cjs' - ? require(generatorModulePath(generatedSharedApiModule)) - : await import(pathToFileURL(generatorModulePath(generatedSharedApiModule)).href); - const descriptorModule = Schema.decodeUnknownSync(DescriptorModuleSchema)(descriptorSource); - const sharedApiModule = Schema.decodeUnknownSync(SharedApiGeneratorSchema)(sharedApiSource); - const checkoutDescriptor = descriptorModule.createVerticalDescriptor('shopping', 4105); - const checkoutStemDescriptor = { - ...checkoutDescriptor, - api: { prefix: checkoutApiPrefix, stem: checkoutId }, - exposes: {}, - }; - const servicePath = generatorModulePath(generatedServiceModuleName); - const serviceSource: unknown = - moduleFormat === 'cjs' - ? require(servicePath) - : await import(pathToFileURL(servicePath).href); - const serviceModule = Schema.decodeUnknownSync(ApiServiceGeneratorSchema)(serviceSource); - const checkoutWorkspace = path.join(formatRoot, 'checkout-workspace'); - await writeText( - checkoutWorkspace, - 'verticals/shopping/shared/api.ts', - sharedApiModule.createSharedApi(generatedProofScope, checkoutStemDescriptor), - ); - await writeText( - checkoutWorkspace, - 'verticals/shopping/api/index.ts', - serviceModule.createApiServiceEntry( - generatedProofScope, - checkoutStemDescriptor, - generatedSharedApiImport, - ), - ); - await writeText( - checkoutWorkspace, - 'verticals/shopping/src/api/checkout-client.ts', - 'export const checkoutClient = true;\n', - ); - await writeText( - checkoutWorkspace, - 'verticals/shopping/modern.config.ts', - `export default { - bff: { + const descriptorSource: unknown = + moduleFormat === 'cjs' + ? require(publishedGeneratorModulePath(moduleFormat)('descriptors')) + : yield* Effect.promise( + () => + import( + pathToFileURL(publishedGeneratorModulePath(moduleFormat)('descriptors')).href + ), + ); + const sharedApiSource: unknown = + moduleFormat === 'cjs' + ? require(publishedGeneratorModulePath(moduleFormat)(generatedSharedApiModule)) + : yield* Effect.promise( + () => + import( + pathToFileURL( + publishedGeneratorModulePath(moduleFormat)(generatedSharedApiModule), + ).href + ), + ); + const descriptorModule = + Schema.decodeUnknownSync(DescriptorModuleSchema)(descriptorSource); + const sharedApiModule = + Schema.decodeUnknownSync(SharedApiGeneratorSchema)(sharedApiSource); + const checkoutDescriptor = descriptorModule.createVerticalDescriptor('shopping', 4105); + const checkoutStemDescriptor = { + ...checkoutDescriptor, + api: { prefix: checkoutApiPrefix, stem: checkoutId }, + exposes: {}, + }; + const servicePath = publishedGeneratorModulePath(moduleFormat)( + generatedServiceModuleName, + ); + const serviceSource: unknown = + moduleFormat === 'cjs' + ? require(servicePath) + : yield* Effect.promise(() => import(pathToFileURL(servicePath).href)); + const serviceModule = Schema.decodeUnknownSync(ApiServiceGeneratorSchema)(serviceSource); + const checkoutWorkspace = path.join(formatRoot, 'checkout-workspace'); + yield* writeText( + checkoutWorkspace, + 'verticals/shopping/shared/api.ts', + sharedApiModule.createSharedApi(generatedProofScope, checkoutStemDescriptor), + ); + yield* writeText( + checkoutWorkspace, + 'verticals/shopping/api/index.ts', + serviceModule.createApiServiceEntry( + generatedProofScope, + checkoutStemDescriptor, + generatedSharedApiImport, + ), + ); + yield* writeText( + checkoutWorkspace, + 'verticals/shopping/src/api/checkout-client.ts', + 'export const checkoutClient = true;\n', + ); + yield* writeText( + checkoutWorkspace, + 'verticals/shopping/modern.config.ts', + `export default { + bff: { runtimeFramework: 'effect', effect: { entry: './api/index', strictEffectApproach: true }, }, }; `, - ); - await writeJson(checkoutWorkspace, 'verticals/shopping/package.json', { - exports: { - './api': './shared/api.ts', - './api/client': './src/api/checkout-client.ts', - }, - }); - await writeJson(checkoutWorkspace, 'packages/shared-contracts/package.json', { - name: generatedSharedContractsPackage, - }); - await writeJson(checkoutWorkspace, topologyReferencePath, { - verticals: [ - { - api: { - basePath: '/checkout-api/checkout', - bff: { prefix: checkoutApiPrefix, strictEffectApproach: true }, - readiness: { endpoint: '/checkout/readiness' }, - runtime: 'effect', - serverEntry: 'verticals/shopping/api/index.ts', + ); + yield* writeJson(checkoutWorkspace, 'verticals/shopping/package.json', { + exports: { + './api': './shared/api.ts', + './api/client': './src/api/checkout-client.ts', }, - id: 'shopping', - path: 'verticals/shopping', - }, - ], - }); - await writeText( - checkoutWorkspace, - 'apps/shell-super-app/src/api/vertical-clients.ts', - 'export const verticalClients = {};\n', - ); - const invalidApiSource = `import { Schema } from 'effect'; + }); + yield* writeJson(checkoutWorkspace, 'packages/shared-contracts/package.json', { + name: generatedSharedContractsPackage, + }); + yield* writeJson(checkoutWorkspace, topologyReferencePath, { + verticals: [ + { + api: { + basePath: '/checkout-api/checkout', + bff: { prefix: checkoutApiPrefix, strictEffectApproach: true }, + readiness: { endpoint: '/checkout/readiness' }, + runtime: 'effect', + serverEntry: 'verticals/shopping/api/index.ts', + }, + id: 'shopping', + path: 'verticals/shopping', + }, + ], + }); + yield* writeText( + checkoutWorkspace, + 'apps/shell-super-app/src/api/vertical-clients.ts', + 'export const verticalClients = {};\n', + ); + const invalidApiSource = `import { Schema } from 'effect'; export const response = new Response('generated'); export const responseSchema = Schema.Unknown; `; - await writeText( - checkoutWorkspace, - 'verticals/shopping/dist-cloudflare/api/index.js', - invalidApiSource, - ); - await writeText( - checkoutWorkspace, - 'apps/shell-super-app/dist-cloudflare/api/index.js', - invalidApiSource, - ); - assert.match( - runNode([path.join(formatRoot, checker.relativePath)], { - env: { ULTRAMODERN_WORKSPACE_ROOT: checkoutWorkspace }, + yield* writeText( + checkoutWorkspace, + 'verticals/shopping/dist-cloudflare/api/index.js', + invalidApiSource, + ); + yield* writeText( + checkoutWorkspace, + 'apps/shell-super-app/dist-cloudflare/api/index.js', + invalidApiSource, + ); + expect( + runNode([path.join(formatRoot, checker.relativePath)], { + env: { ULTRAMODERN_WORKSPACE_ROOT: checkoutWorkspace }, + }), + `${moduleFormat} checkout workspace`, + ).toMatch(/UltraModern API boundary check passed/u); + const authoredApiPath = 'verticals/shopping/api/invalid.ts'; + yield* writeText(checkoutWorkspace, authoredApiPath, invalidApiSource); + const authoredResult = spawnSync( + process.execPath, + [path.join(formatRoot, checker.relativePath)], + { + encoding: 'utf-8', + env: { ULTRAMODERN_WORKSPACE_ROOT: checkoutWorkspace }, + }, + ); + expect(authoredResult.status, moduleFormat).toBe(1); + expect(authoredResult.stderr, moduleFormat).toMatch( + /verticals\/shopping\/api\/invalid\.ts: API modules must not hand-build Response objects/u, + ); + expect(authoredResult.stderr, moduleFormat).toMatch( + /verticals\/shopping\/api\/invalid\.ts: API modules must use concrete request, response and error schemas/u, + ); + expect(authoredResult.stderr, moduleFormat).not.toMatch(/dist-cloudflare/u); }), - /UltraModern API boundary check passed/u, - `${moduleFormat} checkout workspace`, - ); - const authoredApiPath = 'verticals/shopping/api/invalid.ts'; - await writeText(checkoutWorkspace, authoredApiPath, invalidApiSource); - const authoredResult = spawnSync( - process.execPath, - [path.join(formatRoot, checker.relativePath)], - { - encoding: 'utf-8', - env: { ULTRAMODERN_WORKSPACE_ROOT: checkoutWorkspace }, - }, - ); - assert.equal(authoredResult.status, 1, moduleFormat); - assert.match( - authoredResult.stderr, - /verticals\/shopping\/api\/invalid\.ts: API modules must not hand-build Response objects/u, - moduleFormat, - ); - assert.match( - authoredResult.stderr, - /verticals\/shopping\/api\/invalid\.ts: API modules must use concrete request, response and error schemas/u, - moduleFormat, - ); - assert.doesNotMatch(authoredResult.stderr, /dist-cloudflare/u, moduleFormat); - }), - ); -}); + ), + { concurrency: 'unbounded' }, + ); + }), +); -void test('two generated MicroVertical root contracts execute invariant readiness endpoints', async (context) => { - const proofRoot = await mkdtemp( - path.join(workspaceRoot, `verticals/${partyId}/.generated-api-baseline-`), - ); - context.after(async (): Promise => { - await rm(proofRoot, { force: true, recursive: true }); - }); - const descriptorSource: unknown = await import( - pathToFileURL(path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/descriptors.js')) - .href - ); - const sharedApiSource: unknown = await import( - pathToFileURL(path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/api/shared.js')) - .href - ); - const apiServiceSource: unknown = await import( - pathToFileURL(path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/api/service.js')) - .href - ); - const apiClientSource: unknown = await import( - pathToFileURL(path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/api/client.js')) - .href - ); - const descriptorModule = Schema.decodeUnknownSync(DescriptorModuleSchema)(descriptorSource); - const sharedApiModule = Schema.decodeUnknownSync(SharedApiGeneratorSchema)(sharedApiSource); - const apiServiceModule = Schema.decodeUnknownSync(ApiServiceGeneratorSchema)(apiServiceSource); - const apiClientModule = Schema.decodeUnknownSync(ApiClientGeneratorSchema)(apiClientSource); - const fixtures = [ - { - id: inventoryStockId, - port: 4103, - prefix: '/inventory-stock-api', - readinessExport: 'getInventoryStockReadiness', - stem: inventoryStockId, - }, - { - id: 'order-management', - port: 4104, - prefix: '/order-management-api', - readinessExport: 'getOrderManagementReadiness', - stem: 'order-management', - }, - { - id: 'shopping', - port: 4105, - prefix: checkoutApiPrefix, - readinessExport: 'getCheckoutReadiness', - stem: checkoutId, - }, - ] as const; +it.live( + 'two generated MicroVertical root contracts execute invariant readiness endpoints', + Effect.fn(function* governanceScenario12() { + const proofRoot = yield* Effect.acquireRelease( + Effect.promise(() => + mkdtemp(path.join(workspaceRoot, `verticals/${partyId}/.generated-api-baseline-`)), + ), + (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), + ); - const generatedProofs = await Promise.all( - fixtures.map(async (fixture) => { - const descriptor: unknown = descriptorModule.createVerticalDescriptor( - fixture.id, - fixture.port, - ); - Schema.asserts(WorkspaceAppFixtureSchema, descriptor); - assert.ok(descriptor.api); - const generatedDescriptor = { - ...descriptor, - api: { ...descriptor.api, prefix: fixture.prefix, stem: fixture.stem }, - exposes: {}, - }; - const contract = sharedApiModule.createSharedApi('app', generatedDescriptor); - const basePath = `${fixture.prefix}/${fixture.stem}`; - assert.equal( - microVerticalApiBaselineViolation(fixture.stem, contract, { - additionalPaths: - fixture.stem === checkoutId ? { checkoutCartPath: `${basePath}/cart` } : {}, - apiPrefix: fixture.prefix, - basePath, - effectClientPackage, - ownerId: fixture.id, - readinessPath: `${basePath}/readiness`, - sharedContractsPackage: '@app/shared-contracts', - }), - undefined, - ); - const ownerRoot = path.join(proofRoot, fixture.id); - await writeText(ownerRoot, 'shared/api.ts', contract); - await writeText( - ownerRoot, - buildMarkerFile, - `export const ultramodernApiMarker = { + const descriptorSource: unknown = yield* Effect.promise( + () => + import( + pathToFileURL( + path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/descriptors.js'), + ).href + ), + ); + const sharedApiSource: unknown = yield* Effect.promise( + () => + import( + pathToFileURL( + path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/api/shared.js'), + ).href + ), + ); + const apiServiceSource: unknown = yield* Effect.promise( + () => + import( + pathToFileURL( + path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/api/service.js'), + ).href + ), + ); + const apiClientSource: unknown = yield* Effect.promise( + () => + import( + pathToFileURL( + path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/api/client.js'), + ).href + ), + ); + const descriptorModule = + yield* Schema.decodeUnknownEffect(DescriptorModuleSchema)(descriptorSource); + const sharedApiModule = + yield* Schema.decodeUnknownEffect(SharedApiGeneratorSchema)(sharedApiSource); + const apiServiceModule = + yield* Schema.decodeUnknownEffect(ApiServiceGeneratorSchema)(apiServiceSource); + const apiClientModule = + yield* Schema.decodeUnknownEffect(ApiClientGeneratorSchema)(apiClientSource); + const fixtures = [ + { + id: inventoryStockId, + port: 4103, + prefix: '/inventory-stock-api', + readinessExport: 'getInventoryStockReadiness', + stem: inventoryStockId, + }, + { + id: 'order-management', + port: 4104, + prefix: '/order-management-api', + readinessExport: 'getOrderManagementReadiness', + stem: 'order-management', + }, + { + id: 'shopping', + port: 4105, + prefix: checkoutApiPrefix, + readinessExport: 'getCheckoutReadiness', + stem: checkoutId, + }, + ] as const; + + const generatedProofs = yield* Effect.all( + fixtures.map( + Effect.fn(function* governanceScenario13(fixture) { + const descriptor = yield* Schema.decodeUnknownEffect(WorkspaceAppFixtureSchema, { + onExcessProperty: 'preserve', + })(descriptorModule.createVerticalDescriptor(fixture.id, fixture.port), { + onExcessProperty: 'preserve', + }); + expect(descriptor.api).toBeTruthy(); + const generatedDescriptor = { + ...descriptor, + api: { ...descriptor.api, prefix: fixture.prefix, stem: fixture.stem }, + exposes: {}, + }; + const contract = sharedApiModule.createSharedApi('app', generatedDescriptor); + const basePath = `${fixture.prefix}/${fixture.stem}`; + expect( + yield* microVerticalApiBaselineViolation(fixture.stem, contract, { + additionalPaths: + fixture.stem === checkoutId ? { checkoutCartPath: `${basePath}/cart` } : {}, + apiPrefix: fixture.prefix, + basePath, + effectClientPackage, + ownerId: fixture.id, + readinessPath: `${basePath}/readiness`, + sharedContractsPackage: '@app/shared-contracts', + }), + ).toBe(undefined); + const ownerRoot = path.join(proofRoot, fixture.id); + yield* writeText(ownerRoot, 'shared/api.ts', contract); + yield* writeText( + ownerRoot, + buildMarkerFile, + `export const ultramodernApiMarker = { appId: '${fixture.id}', build: '${fixture.id}-build', buildMarker: '${fixture.id}-build', @@ -2713,607 +3009,636 @@ void test('two generated MicroVertical root contracts execute invariant readines version: '0.1.0', } as const; `, - ); - await writeText( - ownerRoot, - apiIndexFile, - apiServiceModule.createApiServiceEntry( - 'app', - generatedDescriptor, - generatedSharedApiImport, + ); + yield* writeText( + ownerRoot, + apiIndexFile, + apiServiceModule.createApiServiceEntry( + 'app', + generatedDescriptor, + generatedSharedApiImport, + ), + ); + const clientEntryPath = `src/api/${fixture.id}-client.ts`; + yield* writeText( + ownerRoot, + clientEntryPath, + apiClientModule.createApiClient(generatedDescriptor, generatedClientContractImport), + ); + yield* writeJson(ownerRoot, 'package.json', { type: 'module' }); + yield* writeJson(ownerRoot, tsconfigFile, { + compilerOptions: { + allowImportingTsExtensions: true, + module: 'NodeNext', + moduleResolution: 'NodeNext', + noEmit: true, + skipLibCheck: true, + strict: true, + target: 'ES2023', + }, + include: ['api/**/*.ts', 'shared/**/*.ts', 'src/**/*.ts'], + }); + runNode( + [ + path.join(workspaceRoot, 'node_modules/@typescript/native-preview/bin/tsc'), + '-p', + ownerRoot, + ], + { cwd: workspaceRoot }, + ); + const generatedModuleSource: unknown = yield* Effect.promise( + () => import(pathToFileURL(path.join(ownerRoot, apiIndexFile)).href), + ); + const generatedClientSource: unknown = yield* Effect.promise( + () => import(pathToFileURL(path.join(ownerRoot, clientEntryPath)).href), + ); + const generatedModule = yield* Schema.decodeUnknownEffect( + GeneratedApiRuntimeModuleSchema, + )(generatedModuleSource); + const generatedClient = yield* Schema.decodeUnknownEffect( + Schema.Record(Schema.String, Schema.Unknown), + )(generatedClientSource); + const getReadiness = yield* Schema.decodeUnknownEffect(callable())( + generatedClient[fixture.readinessExport], + ); + return { fixture, generatedModule, getReadiness }; + }), + ), + { concurrency: 'unbounded' }, + ); + const handlers = new Map( + yield* Effect.all( + generatedProofs.map(({ fixture, generatedModule }) => + Effect.acquireRelease( + Effect.sync(() => generatedModule.default.createHandler()), + (handler) => Effect.promise(() => handler.dispose()), + ).pipe(Effect.map((handler) => [fixture.stem, handler] as const)), ), - ); - const clientEntryPath = `src/api/${fixture.id}-client.ts`; - await writeText( - ownerRoot, - clientEntryPath, - apiClientModule.createApiClient(generatedDescriptor, generatedClientContractImport), - ); - await writeJson(ownerRoot, 'package.json', { type: 'module' }); - await writeJson(ownerRoot, tsconfigFile, { - compilerOptions: { - allowImportingTsExtensions: true, - module: 'NodeNext', - moduleResolution: 'NodeNext', - noEmit: true, - skipLibCheck: true, - strict: true, - target: 'ES2023', - }, - include: ['api/**/*.ts', 'shared/**/*.ts', 'src/**/*.ts'], - }); - runNode( - [ - path.join(workspaceRoot, 'node_modules/@typescript/native-preview/bin/tsc'), - '-p', - ownerRoot, - ], - { cwd: workspaceRoot }, - ); - const generatedModuleSource: unknown = await import( - pathToFileURL(path.join(ownerRoot, apiIndexFile)).href - ); - const generatedClientSource: unknown = await import( - pathToFileURL(path.join(ownerRoot, clientEntryPath)).href - ); - const generatedModule = Schema.decodeUnknownSync(GeneratedApiRuntimeModuleSchema)( - generatedModuleSource, - ); - const generatedClient = Schema.decodeUnknownSync( - Schema.Record(Schema.String, Schema.Unknown), - )(generatedClientSource); - const getReadiness = Schema.decodeUnknownSync(callable())( - generatedClient[fixture.readinessExport], - ); - const runEffectRequest = Schema.decodeUnknownSync(callable())( - generatedClient.runEffectRequest, - ); - return { fixture, generatedModule, getReadiness, runEffectRequest }; - }), - ); - const handlers = new Map( - generatedProofs.map(({ fixture, generatedModule }) => [ - fixture.stem, - generatedModule.default.createHandler(), - ]), - ); - const originalFetch = globalThis.fetch; - globalThis.fetch = async (input, init) => { - const request = new Request(input, init); - const stem = new URL(request.url).pathname.split('/').find((segment) => segment.length > 0); - const handler = stem === undefined ? undefined : handlers.get(stem); - return handler === undefined - ? new Response(undefined, { status: 503 }) - : await handler.handler(request); - }; - let readinessValues: (typeof MicroVerticalReadinessSchema.Type)[]; - try { - readinessValues = await Promise.all( - generatedProofs.map(async ({ fixture, getReadiness, runEffectRequest }) => { - const handler = handlers.get(fixture.stem); - assert.ok(handler); - const directResponse = await handler.handler( - new Request(`http://localhost/${fixture.stem}/readiness`), - ); - assert.equal(directResponse.status, 200); - const directReadiness = Schema.decodeUnknownSync(MicroVerticalReadinessSchema)( - await directResponse.json(), - ); - const clientReadiness = Schema.decodeUnknownSync(MicroVerticalReadinessSchema)( - await runEffectRequest(getReadiness({ baseUrl: 'http://localhost' })), - ); - assert.deepEqual(clientReadiness, directReadiness); - return clientReadiness; - }), + ), + ); + rs.spyOn(globalThis, 'fetch').mockImplementation((input, init) => { + const request = new Request(input, init); + const stem = new URL(request.url).pathname.split('/').find((segment) => segment.length > 0); + const handler = stem === undefined ? undefined : handlers.get(stem); + return handler === undefined + ? Promise.resolve(new Response(undefined, { status: 503 })) + : handler.handler(request); + }); + const readinessValues = yield* Effect.all( + generatedProofs.map( + Effect.fn(function* verifyGeneratedReadiness({ fixture, getReadiness }) { + const handler = handlers.get(fixture.stem); + expect(handler).toBeDefined(); + if (handler === undefined) { + return yield* Effect.die(new Error(`Missing generated handler for ${fixture.stem}`)); + } + const directResponse = yield* Effect.promise(() => + handler.handler(new Request(`http://localhost/${fixture.stem}/readiness`)), + ); + expect(directResponse.status).toBe(200); + const directReadiness = yield* Schema.decodeUnknownEffect(MicroVerticalReadinessSchema)( + yield* Effect.promise(() => directResponse.json()), + ); + const clientReadiness = yield* Schema.decodeUnknownEffect(MicroVerticalReadinessSchema)( + yield* getReadiness({ baseUrl: 'http://localhost' }), + ); + expect(clientReadiness).toEqual(directReadiness); + return clientReadiness; + }), + ), + { concurrency: 'unbounded' }, ); - } finally { - globalThis.fetch = originalFetch; - await Promise.all([...handlers.values()].map(async (handler) => await handler.dispose())); - } - const [firstReadiness, secondReadiness] = readinessValues; - assert.ok(firstReadiness !== undefined); - assert.ok(secondReadiness !== undefined); - assert.notEqual(firstReadiness.marker.appId, secondReadiness.marker.appId); - assert.equal('kind' in firstReadiness.marker, false); - assert.equal('schemaVersion' in firstReadiness.marker, false); - assert.deepEqual(firstReadiness.checks, secondReadiness.checks); - assert.equal(firstReadiness.status, secondReadiness.status); - assert.equal(firstReadiness.versionSkew, secondReadiness.versionSkew); -}); + const [firstReadiness, secondReadiness] = readinessValues; + expect(firstReadiness).toBeDefined(); + expect(secondReadiness).toBeDefined(); + if (firstReadiness === undefined || secondReadiness === undefined) { + throw new Error('Expected both generated readiness results'); + } + expect(firstReadiness.marker.appId).not.toBe(secondReadiness.marker.appId); + expect('kind' in firstReadiness.marker).toBe(false); + expect('schemaVersion' in firstReadiness.marker).toBe(false); + expect(firstReadiness.checks).toEqual(secondReadiness.checks); + expect(firstReadiness.status).toBe(secondReadiness.status); + expect(firstReadiness.versionSkew).toBe(secondReadiness.versionSkew); + }), +); -void test('generated shared-contracts baseline template is lint-clean and type-safe', async (context) => { - const proofRoot = await makeProofRoot(context, 'generated-baseline-template-proof'); - const templateSource = await readFile( - path.join(generatorRoot, 'templates/packages/shared-contracts-index.ts'), - 'utf-8', - ); - const baselineEnd = templateSource.indexOf('export type UltramodernPublicSitemapChangeFrequency'); - assert.notEqual(baselineEnd, -1); - const generatedSource = templateSource.slice(0, baselineEnd); - await writeText(proofRoot, generatedBaselineTemplateEntry, generatedSource); - await writeJson(proofRoot, tsconfigFile, { - compilerOptions: { - module: 'NodeNext', - moduleResolution: 'NodeNext', - noEmit: true, - skipLibCheck: true, - strict: true, - target: 'ES2022', - }, - include: [generatedBaselineTemplateEntry], - }); - const generatedFile = path.join(proofRoot, generatedBaselineTemplateEntry); - runNode([path.join(workspaceRoot, 'node_modules/oxlint/bin/oxlint'), generatedFile], { - cwd: workspaceRoot, - }); - runNode( - [path.join(workspaceRoot, 'node_modules/@typescript/native-preview/bin/tsc'), '-p', proofRoot], - { cwd: workspaceRoot }, - ); -}); +it.live( + 'generated shared-contracts baseline template is lint-clean and type-safe', + Effect.fn(function* mergedScenario1() { + const proofRoot = yield* makeProofRoot('generated-baseline-template-proof'); + const templateSource = yield* Effect.promise(() => + readFile(path.join(generatorRoot, 'templates/packages/shared-contracts-index.ts'), 'utf-8'), + ); + const baselineEnd = templateSource.indexOf( + 'export type UltramodernPublicSitemapChangeFrequency', + ); + expect(baselineEnd).not.toBe(-1); + const generatedSource = templateSource.slice(0, baselineEnd); + yield* writeText(proofRoot, generatedBaselineTemplateEntry, generatedSource); + yield* writeJson(proofRoot, tsconfigFile, { + compilerOptions: { + module: 'NodeNext', + moduleResolution: 'NodeNext', + noEmit: true, + skipLibCheck: true, + strict: true, + target: 'ES2022', + }, + include: [generatedBaselineTemplateEntry], + }); + const generatedFile = path.join(proofRoot, generatedBaselineTemplateEntry); + runNode([path.join(workspaceRoot, 'node_modules/oxlint/bin/oxlint'), generatedFile], { + cwd: workspaceRoot, + }); + runNode( + [ + path.join(workspaceRoot, 'node_modules/@typescript/native-preview/bin/tsc'), + '-p', + proofRoot, + ], + { cwd: workspaceRoot }, + ); + }), +); -void test('baseline imports require values even with comments after the type keyword', async () => { - const contract = await readFile( - path.join(workspaceRoot, `verticals/${partyId}/shared/api.ts`), - 'utf-8', - ); - assert.equal(microVerticalApiBaselineViolation(partyId, contract), undefined); - for (const declaration of [ - 'import type ', - 'import type/* comment */ ', - 'import /* comment */ ', - ]) { - const mutated = contract.replace( - 'import {\n MicroVerticalBuildMarkerSchema,', - `${declaration}{\n MicroVerticalBuildMarkerSchema,`, - ); - const violation = microVerticalApiBaselineViolation(partyId, mutated); - if (declaration.startsWith('import type')) { - assert.match(violation ?? '', /import exact baseline primitives/u); - } else { - assert.equal(violation, undefined); +it.live( + 'baseline imports require values even with comments after the type keyword', + Effect.fn(function* governanceScenario18() { + const contract = yield* Effect.promise(() => + readFile(path.join(workspaceRoot, `verticals/${partyId}/shared/api.ts`), 'utf-8'), + ); + expect(yield* microVerticalApiBaselineViolation(partyId, contract)).toBe(undefined); + for (const declaration of [ + 'import type ', + 'import type/* comment */ ', + 'import /* comment */ ', + ]) { + const mutated = contract.replace( + 'import {\n MicroVerticalBuildMarkerSchema,', + `${declaration}{\n MicroVerticalBuildMarkerSchema,`, + ); + const violation = yield* microVerticalApiBaselineViolation(partyId, mutated); + if (declaration.startsWith('import type')) { + expect(violation ?? '').toMatch(/import exact baseline primitives/u); + } else { + expect(violation).toBe(undefined); + } } - } -}); + }), +); -void test('repository checker respects custom readiness prefixes and diagnoses missing topology', async (context) => { - const fixture = await mkdtemp(path.join(os.tmpdir(), 'ontos-baseline-topology-')); - context.after(async (): Promise => { - await rm(fixture, { force: true, recursive: true }); - }); - const generatorModulePath = (name: string): string => - path.join(generatorRoot, `dist/esm-node/ultramodern-workspace/${name}.js`); - const descriptors = Schema.decodeUnknownSync(DescriptorModuleSchema)( - await import(pathToFileURL(generatorModulePath('descriptors')).href), - ); - const shared = Schema.decodeUnknownSync(SharedApiGeneratorSchema)( - await import(pathToFileURL(generatorModulePath(generatedSharedApiModule)).href), - ); - const service = Schema.decodeUnknownSync(ApiServiceGeneratorSchema)( - await import(pathToFileURL(generatorModulePath(generatedServiceModuleName)).href), - ); - const app = { - ...descriptors.createVerticalDescriptor(inventoryStockId, 4103), - api: { prefix: warehouseApiPrefix, stem: warehouseItemsApiStem }, - exposes: {}, - }; - const ownerPath = `verticals/${inventoryStockId}`; - const readinessContract = shared - .createSharedApi('app', app) - .replace( - /(?\.addHttpApi\(warehouseItemsFoundationApi\))[\s\S]*?(?=export const warehouseItemsOperationContexts)/u, - '$;\n\n', - ) - .replace( - /(?export const warehouseItemsOperationContexts = \{)[\s\S]*?(?= {2}readiness:)/u, - '$\n', - ); - await writeText(fixture, `${ownerPath}/shared/api.ts`, readinessContract); - await writeText( - fixture, - `${ownerPath}/api/index.ts`, - service.createApiServiceEntry('app', app, generatedSharedApiImport), - ); - await writeText( - fixture, - `${ownerPath}/src/api/warehouse-client.ts`, - 'export const client = true;\n', - ); - await writeJson(fixture, `${ownerPath}/package.json`, { exports: {} }); - const api = { - basePath: '/warehouse-api/warehouse-items', - bff: { prefix: warehouseApiPrefix, strictEffectApproach: true }, - readiness: { endpoint: '/warehouse-items/readiness' }, - runtime: 'effect', - serverEntry: `${ownerPath}/api/index.ts`, - }; - const vertical = { api, id: inventoryStockId, path: ownerPath }; - await writeJson(fixture, topologyReferencePath, { verticals: [vertical] }); - const check = (): string => - runNode([path.join(workspaceRoot, apiBoundaryCheckerPath)], { - env: { ULTRAMODERN_WORKSPACE_ROOT: fixture }, - }); - assert.match(check(), /UltraModern API boundary check passed/u); - const cases = [ - { expected: /topology must declare this MicroVertical owner/u, verticals: [] }, - { - expected: /topology must declare api\.basePath/u, - verticals: [ - { - ...vertical, - api: { - bff: api.bff, - readiness: api.readiness, - runtime: api.runtime, - serverEntry: api.serverEntry, +it.live( + 'repository checker respects custom readiness prefixes and diagnoses missing topology', + Effect.fn(function* governanceScenario19() { + const fixture = yield* Effect.acquireRelease( + Effect.promise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-baseline-topology-'))), + (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), + ); + + const generatorModulePath = (name: string): string => + path.join(generatorRoot, `dist/esm-node/ultramodern-workspace/${name}.js`); + const descriptors = yield* Schema.decodeUnknownEffect(DescriptorModuleSchema)( + yield* Effect.promise(() => import(pathToFileURL(generatorModulePath('descriptors')).href)), + ); + const shared = yield* Schema.decodeUnknownEffect(SharedApiGeneratorSchema)( + yield* Effect.promise( + () => import(pathToFileURL(generatorModulePath(generatedSharedApiModule)).href), + ), + ); + const service = yield* Schema.decodeUnknownEffect(ApiServiceGeneratorSchema)( + yield* Effect.promise( + () => import(pathToFileURL(generatorModulePath(generatedServiceModuleName)).href), + ), + ); + const app = { + ...descriptors.createVerticalDescriptor(inventoryStockId, 4103), + api: { prefix: warehouseApiPrefix, stem: warehouseItemsApiStem }, + exposes: {}, + }; + const ownerPath = `verticals/${inventoryStockId}`; + const readinessContract = shared + .createSharedApi('app', app) + .replace( + /(?\.addHttpApi\(warehouseItemsFoundationApi\))[\s\S]*?(?=export const warehouseItemsOperationContexts)/u, + '$;\n\n', + ) + .replace( + /(?export const warehouseItemsOperationContexts = \{)[\s\S]*?(?= {2}readiness:)/u, + '$\n', + ); + yield* writeText(fixture, `${ownerPath}/shared/api.ts`, readinessContract); + yield* writeText( + fixture, + `${ownerPath}/api/index.ts`, + service.createApiServiceEntry('app', app, generatedSharedApiImport), + ); + yield* writeText( + fixture, + `${ownerPath}/src/api/warehouse-client.ts`, + 'export const client = true;\n', + ); + yield* writeJson(fixture, `${ownerPath}/package.json`, { exports: {} }); + const api = { + basePath: '/warehouse-api/warehouse-items', + bff: { prefix: warehouseApiPrefix, strictEffectApproach: true }, + readiness: { endpoint: '/warehouse-items/readiness' }, + runtime: 'effect', + serverEntry: `${ownerPath}/api/index.ts`, + }; + const vertical = { api, id: inventoryStockId, path: ownerPath }; + yield* writeJson(fixture, topologyReferencePath, { verticals: [vertical] }); + const check = (): string => + runNode([path.join(workspaceRoot, apiBoundaryCheckerPath)], { + env: { ULTRAMODERN_WORKSPACE_ROOT: fixture }, + }); + expect(check()).toMatch(/UltraModern API boundary check passed/u); + const cases = [ + { expected: /topology must declare this MicroVertical owner/u, verticals: [] }, + { + expected: /topology must declare api\.basePath/u, + verticals: [ + { + ...vertical, + api: { + bff: api.bff, + readiness: api.readiness, + runtime: api.runtime, + serverEntry: api.serverEntry, + }, }, + ], + }, + { + expected: /topology must declare api\.bff\.prefix/u, + verticals: [{ ...vertical, api: { ...api, bff: { strictEffectApproach: true } } }], + }, + ]; + for (const entry of cases) { + yield* writeJson(fixture, topologyReferencePath, { verticals: entry.verticals }); + const result = spawnSync( + process.execPath, + [path.join(workspaceRoot, apiBoundaryCheckerPath)], + { + encoding: 'utf-8', + env: { ULTRAMODERN_WORKSPACE_ROOT: fixture }, }, - ], - }, - { - expected: /topology must declare api\.bff\.prefix/u, - verticals: [{ ...vertical, api: { ...api, bff: { strictEffectApproach: true } } }], - }, - ]; - for (const entry of cases) { - writeFileSync( - path.join(fixture, topologyReferencePath), - JSON.stringify({ verticals: entry.verticals }), - ); - const result = spawnSync(process.execPath, [path.join(workspaceRoot, apiBoundaryCheckerPath)], { - encoding: 'utf-8', - env: { ULTRAMODERN_WORKSPACE_ROOT: fixture }, - }); - assert.equal(result.status, 1); - assert.match(result.stderr, entry.expected); - assert.doesNotMatch(result.stderr, /exact owner and API path metadata/u); - } -}); + ); + expect(result.status).toBe(1); + expect(result.stderr).toMatch(entry.expected); + expect(result.stderr).not.toMatch(/exact owner and API path metadata/u); + } + }), +); -void test('static validation rejects a MicroVertical root contract without readiness baseline', async () => { - const contract = await readFile( - path.join(workspaceRoot, `verticals/${partyId}/shared/api.ts`), - 'utf-8', - ); - assert.equal(microVerticalApiBaselineViolation(partyId, contract), undefined); - const readinessEndpointDecoy = - "HttpApiEndpoint.get('readiness', '/party-registry/readiness', { success: partyRegistryReadinessSchema })"; - const foundationComposition = '.addHttpApi(partyRegistryFoundationApi)'; - const sharedBaselineImport = `import { +it.live( + 'static validation rejects a MicroVertical root contract without readiness baseline', + Effect.fn(function* mergedScenario1() { + const contract = yield* Effect.promise(() => + readFile(path.join(workspaceRoot, `verticals/${partyId}/shared/api.ts`), 'utf-8'), + ); + expect(yield* microVerticalApiBaselineViolation(partyId, contract)).toBe(undefined); + const readinessEndpointDecoy = + "HttpApiEndpoint.get('readiness', '/party-registry/readiness', { success: partyRegistryReadinessSchema })"; + const foundationComposition = '.addHttpApi(partyRegistryFoundationApi)'; + const sharedBaselineImport = `import { MicroVerticalBuildMarkerSchema, MicroVerticalReadinessSchema, createMicroVerticalOperationContext, } from '@app/shared-contracts';`; - for (const [label, mutated, expected] of [ - [ - 'renamed readiness endpoint', - contract.replace("HttpApiEndpoint.get('readiness'", "HttpApiEndpoint.get('health'"), - /exact readiness endpoint/u, - ], - [ - 'foreign readiness schema fields', - contract.replace('...MicroVerticalReadinessSchema.fields,', '...Schema.Unknown.fields,'), - /shared readiness schema/u, - ], - [ - 'readiness success schema commented out', - contract.replace( - 'success: partyRegistryReadinessSchema', - 'success: Schema.String /* success: partyRegistryReadinessSchema */', - ), - /exact readiness endpoint/u, - ], - [ - 'baseline primitives imported from a copied package', - contract.replace("from '@app/shared-contracts';", "from '@app/copied-contracts';"), - /import exact baseline primitives from the shared contracts package/u, - ], - [ - 'Effect API primitives imported from a foreign client', - contract.replace( - "from '@modern-js/plugin-bff/effect-client';", - "from '@evil/fake-effect-client';", - ), - /import exact Effect API primitives from the framework client package/u, - ], - [ - 'baseline primitives redefined locally', - contract.replace( - sharedBaselineImport, - `const MicroVerticalBuildMarkerSchema = Schema.Struct({ copied: Schema.String }); + for (const [label, mutated, expected] of [ + [ + 'renamed readiness endpoint', + contract.replace("HttpApiEndpoint.get('readiness'", "HttpApiEndpoint.get('health'"), + /exact readiness endpoint/u, + ], + [ + 'foreign readiness schema fields', + contract.replace('...MicroVerticalReadinessSchema.fields,', '...Schema.Unknown.fields,'), + /shared readiness schema/u, + ], + [ + 'readiness success schema commented out', + contract.replace( + 'success: partyRegistryReadinessSchema', + 'success: Schema.String /* success: partyRegistryReadinessSchema */', + ), + /exact readiness endpoint/u, + ], + [ + 'baseline primitives imported from a copied package', + contract.replace("from '@app/shared-contracts';", "from '@app/copied-contracts';"), + /import exact baseline primitives from the shared contracts package/u, + ], + [ + 'Effect API primitives imported from a foreign client', + contract.replace( + "from '@modern-js/plugin-bff/effect-client';", + "from '@evil/fake-effect-client';", + ), + /import exact Effect API primitives from the framework client package/u, + ], + [ + 'baseline primitives redefined locally', + contract.replace( + sharedBaselineImport, + `const MicroVerticalBuildMarkerSchema = Schema.Struct({ copied: Schema.String }); const MicroVerticalReadinessSchema = Schema.Struct({ copied: Schema.String }); const createMicroVerticalOperationContext = (value: Value): Value => value;`, - ), - /import exact baseline primitives from the shared contracts package/u, - ], - [ - 'renamed readiness endpoint with a decoy API name', - contract - .replace("HttpApiEndpoint.get('readiness'", "HttpApiEndpoint.get('health'") - .replace( - "HttpApi.make('PartyRegistryFoundationApi')", - `HttpApi.make("${readinessEndpointDecoy}")`, ), - /exact readiness endpoint/u, - ], - [ - 'missing foundation API composition', - contract.replace(foundationComposition, ''), - /explicitly compose its readiness foundation API/u, - ], - [ - 'foundation composition discarded by a pipe', - contract.replace( - foundationComposition, - `${foundationComposition} + /import exact baseline primitives from the shared contracts package/u, + ], + [ + 'renamed readiness endpoint with a decoy API name', + contract + .replace("HttpApiEndpoint.get('readiness'", "HttpApiEndpoint.get('health'") + .replace( + "HttpApi.make('PartyRegistryFoundationApi')", + `HttpApi.make("${readinessEndpointDecoy}")`, + ), + /exact readiness endpoint/u, + ], + [ + 'missing foundation API composition', + contract.replace(foundationComposition, ''), + /explicitly compose its readiness foundation API/u, + ], + [ + 'foundation composition discarded by a pipe', + contract.replace( + foundationComposition, + `${foundationComposition} .pipe(() => HttpApi.make('DiscardedPartyRegistryApi'))`, - ), - /explicitly compose its readiness foundation API/u, - ], - [ - 'foundation endpoint discarded by a pipe', - contract.replace( - ` ), + ), + /explicitly compose its readiness foundation API/u, + ], + [ + 'foundation endpoint discarded by a pipe', + contract.replace( + ` ), );`, - ` ), + ` ), ).pipe(() => HttpApi.make('DiscardedPartyRegistryFoundationApi'));`, - ), - /directly compose its exact readiness endpoint/u, - ], - [ - 'missing foundation composition with a decoy API name', - contract - .replace(foundationComposition, '') - .replace( - "HttpApi.make('PartyRegistryApi')", - "HttpApi.make('.addHttpApi(partyRegistryFoundationApi)')", ), - /explicitly compose its readiness foundation API/u, - ], - [ - 'renamed foundation API without composition', - contract - .replaceAll('partyRegistryFoundationApi', 'renamedFoundationApi') - .replace('.addHttpApi(renamedFoundationApi)', ''), - /directly compose its exact readiness endpoint/u, - ], - [ - 'hand-forked build marker fields', - contract.replace('...MicroVerticalBuildMarkerSchema.fields,', 'build: Schema.String,'), - /shared build marker schema/u, - ], - [ - 'readiness schema built by a sequence expression', - contract.replace( - `export const partyRegistryReadinessSchema = Schema.Struct({ + /directly compose its exact readiness endpoint/u, + ], + [ + 'missing foundation composition with a decoy API name', + contract + .replace(foundationComposition, '') + .replace( + "HttpApi.make('PartyRegistryApi')", + "HttpApi.make('.addHttpApi(partyRegistryFoundationApi)')", + ), + /explicitly compose its readiness foundation API/u, + ], + [ + 'renamed foundation API without composition', + contract + .replaceAll('partyRegistryFoundationApi', 'renamedFoundationApi') + .replace('.addHttpApi(renamedFoundationApi)', ''), + /directly compose its exact readiness endpoint/u, + ], + [ + 'hand-forked build marker fields', + contract.replace('...MicroVerticalBuildMarkerSchema.fields,', 'build: Schema.String,'), + /shared build marker schema/u, + ], + [ + 'readiness schema built by a sequence expression', + contract.replace( + `export const partyRegistryReadinessSchema = Schema.Struct({ ...MicroVerticalReadinessSchema.fields, marker: partyRegistryMarkerSchema, });`, - `export const partyRegistryReadinessSchema = ( + `export const partyRegistryReadinessSchema = ( MicroVerticalReadinessSchema, Schema.Struct({ marker: partyRegistryMarkerSchema, status: Schema.String }) );`, - ), - /consume the shared readiness schema/u, - ], - [ - 'readiness schema aliased to the shared schema', - contract.replace( - `export const partyRegistryReadinessSchema = Schema.Struct({ + ), + /consume the shared readiness schema/u, + ], + [ + 'readiness schema aliased to the shared schema', + contract.replace( + `export const partyRegistryReadinessSchema = Schema.Struct({ ...MicroVerticalReadinessSchema.fields, marker: partyRegistryMarkerSchema, });`, - 'export const partyRegistryReadinessSchema = MicroVerticalReadinessSchema;', - ), - /consume the shared readiness schema/u, - ], - [ - 'foundation composed inside a pipe callback', - contract.replace( - `export const partyRegistryApi = HttpApi.make('PartyRegistryApi') + 'export const partyRegistryReadinessSchema = MicroVerticalReadinessSchema;', + ), + /consume the shared readiness schema/u, + ], + [ + 'foundation composed inside a pipe callback', + contract.replace( + `export const partyRegistryApi = HttpApi.make('PartyRegistryApi') .addHttpApi(partyRegistryFoundationApi)`, - `export const partyRegistryApi = HttpApi.make('PartyRegistryApi').pipe( + `export const partyRegistryApi = HttpApi.make('PartyRegistryApi').pipe( (api) => (api.addHttpApi(partyRegistryFoundationApi), api), )`, - ), - /explicitly compose its readiness foundation API/u, - ], - [ - 'decoy API declaration shadowed by an uncomposed API', - `${contract.replace( - 'export const partyRegistryApi =', - 'export const partyRegistryApiDecoy =', - )}\nexport const partyRegistryApi = HttpApi.make('PartyRegistryApi');\n`, - /explicitly compose its readiness foundation API/u, - ], - [ - 'decoy foundation API declaration without a readiness endpoint', - `${contract.replace( - 'export const partyRegistryFoundationApi =', - 'export const partyRegistryFoundationApiDecoy =', - )}\nexport const partyRegistryFoundationApi = HttpApi.make('PartyRegistryFoundationApi');\n`, - /exact readiness endpoint/u, - ], - [ - 'decoy contract declaration without path metadata', - `${contract.replace( - 'export const partyRegistryApiContract =', - 'export const partyRegistryApiContractDecoy =', - )}\nexport const partyRegistryApiContract = { ownerId: 'party-registry' };\n`, - /exact owner and API path metadata/u, - ], - [ - 'build marker overriding a shared field', - contract.replace( - '...MicroVerticalBuildMarkerSchema.fields,', - '...MicroVerticalBuildMarkerSchema.fields,\n build: Schema.Number,', - ), - /without overriding shared fields/u, - ], - [ - 'foreign AppId schema', - contract.replace( - "const AppIdSchema = Schema.String.pipe(Schema.brand('AppId'));", - 'const AppIdSchema = Schema.Number;', - ), - /shared build marker schema/u, - ], - [ - 'readiness schema overriding a shared field', - contract.replace( - '...MicroVerticalReadinessSchema.fields,', - '...MicroVerticalReadinessSchema.fields,\n status: Schema.String,', - ), - /without overriding shared fields/u, - ], - [ - 'renamed foundation group', - contract.replace("HttpApiGroup.make('foundation')", "HttpApiGroup.make('not-foundation')"), - /exact readiness endpoint and foundation identity/u, - ], - [ - 'renamed root API', - contract.replace("HttpApi.make('PartyRegistryApi')", "HttpApi.make('WrongApi')"), - /explicitly compose its readiness foundation API/u, - ], - [ - 'renamed operation contexts', - contract.replace( - 'export const partyRegistryOperationContexts =', - 'export const renamedOperationContexts =', - ), - /construct every operation with the shared context constructor/u, - ], - [ - 'foreign operation id', - contract.replace( - "operationId: 'PartyRegistryApi:/reads/ares-lookup'", - "operationId: 'WrongApi:unrelated'", - ), - /construct every operation with the shared context constructor/u, - ], - [ - 'foreign route path', - contract.replace("routePath: '/reads/ares-lookup'", "routePath: '/not-an-endpoint'"), - /construct every operation with the shared context constructor/u, - ], - [ - 'readiness context built without the shared constructor', - `${contract.replace( - `readiness: createMicroVerticalOperationContext({ + ), + /explicitly compose its readiness foundation API/u, + ], + [ + 'decoy API declaration shadowed by an uncomposed API', + `${contract.replace( + 'export const partyRegistryApi =', + 'export const partyRegistryApiDecoy =', + )}\nexport const partyRegistryApi = HttpApi.make('PartyRegistryApi');\n`, + /explicitly compose its readiness foundation API/u, + ], + [ + 'decoy foundation API declaration without a readiness endpoint', + `${contract.replace( + 'export const partyRegistryFoundationApi =', + 'export const partyRegistryFoundationApiDecoy =', + )}\nexport const partyRegistryFoundationApi = HttpApi.make('PartyRegistryFoundationApi');\n`, + /exact readiness endpoint/u, + ], + [ + 'decoy contract declaration without path metadata', + `${contract.replace( + 'export const partyRegistryApiContract =', + 'export const partyRegistryApiContractDecoy =', + )}\nexport const partyRegistryApiContract = { ownerId: 'party-registry' };\n`, + /exact owner and API path metadata/u, + ], + [ + 'build marker overriding a shared field', + contract.replace( + '...MicroVerticalBuildMarkerSchema.fields,', + '...MicroVerticalBuildMarkerSchema.fields,\n build: Schema.Number,', + ), + /without overriding shared fields/u, + ], + [ + 'foreign AppId schema', + contract.replace( + "const AppIdSchema = Schema.String.pipe(Schema.brand('AppId'));", + 'const AppIdSchema = Schema.Number;', + ), + /shared build marker schema/u, + ], + [ + 'readiness schema overriding a shared field', + contract.replace( + '...MicroVerticalReadinessSchema.fields,', + '...MicroVerticalReadinessSchema.fields,\n status: Schema.String,', + ), + /without overriding shared fields/u, + ], + [ + 'renamed foundation group', + contract.replace("HttpApiGroup.make('foundation')", "HttpApiGroup.make('not-foundation')"), + /exact readiness endpoint and foundation identity/u, + ], + [ + 'renamed root API', + contract.replace("HttpApi.make('PartyRegistryApi')", "HttpApi.make('WrongApi')"), + /explicitly compose its readiness foundation API/u, + ], + [ + 'renamed operation contexts', + contract.replace( + 'export const partyRegistryOperationContexts =', + 'export const renamedOperationContexts =', + ), + /construct every operation with the shared context constructor/u, + ], + [ + 'foreign operation id', + contract.replace( + "operationId: 'PartyRegistryApi:/reads/ares-lookup'", + "operationId: 'WrongApi:unrelated'", + ), + /construct every operation with the shared context constructor/u, + ], + [ + 'foreign route path', + contract.replace("routePath: '/reads/ares-lookup'", "routePath: '/not-an-endpoint'"), + /construct every operation with the shared context constructor/u, + ], + [ + 'readiness context built without the shared constructor', + `${contract.replace( + `readiness: createMicroVerticalOperationContext({ method: 'GET', operationId: 'PartyRegistryApi:/party-registry/readiness', routePath: '/party-registry/readiness', }),`, - `readiness: { + `readiness: { method: 'GET', operationId: 'PartyRegistryApi:/party-registry/readiness', routePath: '/party-registry/readiness', },`, - )} + )} createMicroVerticalOperationContext({ method: 'GET', operationId: 'PartyRegistryApi:/party-registry/readiness', routePath: '/party-registry/readiness', }); `, - /construct every operation with the shared context constructor/u, - ], - [ - 'missing apiPrefix', - contract.replace(" apiPrefix: '/party-registry-api',\n", ''), - /exact owner and API path metadata/u, - ], - [ - 'missing basePath', - contract.replace(" basePath: '/party-registry-api/party-registry',\n", ''), - /exact owner and API path metadata/u, - ], - [ - 'missing ownerId', - contract.replace(" ownerId: 'party-registry',\n", ''), - /exact owner and API path metadata/u, - ], - [ - 'wrong apiPrefix', - contract.replace("apiPrefix: '/party-registry-api'", "apiPrefix: '/evil-api'"), - /exact owner and API path metadata/u, - ], - [ - 'wrong basePath', - contract.replace( - "basePath: '/party-registry-api/party-registry'", - "basePath: '/party-registry-api/evil'", - ), - /exact owner and API path metadata/u, - ], - [ - 'wrong ownerId', - contract.replace("ownerId: 'party-registry'", "ownerId: 'evil-owner'"), - /exact owner and API path metadata/u, - ], - [ - 'wrong readinessPath', - contract.replace( - "readinessPath: '/party-registry-api/party-registry/readiness'", - "readinessPath: '/evil-prefix/party-registry/readiness'", - ), - /exact owner and API path metadata/u, - ], - [ - 'coordinated topology drift', - contract - .replace("apiPrefix: '/party-registry-api'", "apiPrefix: '/evil-api'") - .replace( + /construct every operation with the shared context constructor/u, + ], + [ + 'missing apiPrefix', + contract.replace(" apiPrefix: '/party-registry-api',\n", ''), + /exact owner and API path metadata/u, + ], + [ + 'missing basePath', + contract.replace(" basePath: '/party-registry-api/party-registry',\n", ''), + /exact owner and API path metadata/u, + ], + [ + 'missing ownerId', + contract.replace(" ownerId: 'party-registry',\n", ''), + /exact owner and API path metadata/u, + ], + [ + 'wrong apiPrefix', + contract.replace("apiPrefix: '/party-registry-api'", "apiPrefix: '/evil-api'"), + /exact owner and API path metadata/u, + ], + [ + 'wrong basePath', + contract.replace( "basePath: '/party-registry-api/party-registry'", - "basePath: '/evil-api/party-registry'", - ) - .replace( + "basePath: '/party-registry-api/evil'", + ), + /exact owner and API path metadata/u, + ], + [ + 'wrong ownerId', + contract.replace("ownerId: 'party-registry'", "ownerId: 'evil-owner'"), + /exact owner and API path metadata/u, + ], + [ + 'wrong readinessPath', + contract.replace( "readinessPath: '/party-registry-api/party-registry/readiness'", - "readinessPath: '/evil-api/party-registry/readiness'", + "readinessPath: '/evil-prefix/party-registry/readiness'", ), - /exact owner and API path metadata/u, - ], - [ - 'forbidden credential metadata', - contract.replace( - partyReadinessMetadataLine, - `${partyReadinessMetadataLine}\n credential: 'secret',`, - ), - /exact owner and API path metadata/u, - ], - [ - 'forbidden credential path metadata', - contract.replace( - partyReadinessMetadataLine, - `${partyReadinessMetadataLine}\n credentialPath: '/party-registry-api/party-registry/secret',`, - ), - /exact owner and API path metadata/u, - ], - [ - 'unknown path metadata', - contract.replace( - partyReadinessMetadataLine, - `${partyReadinessMetadataLine}\n unknownPath: '/party-registry-api/party-registry/unknown',`, - ), - /exact owner and API path metadata/u, - ], - [ - 'spread metadata', - contract.replace( - 'export const partyRegistryApiContract = {', - 'const copiedMetadata = {};\nexport const partyRegistryApiContract = {\n ...copiedMetadata,', - ), - /exact owner and API path metadata/u, - ], - ] as const) { - assert.match(microVerticalApiBaselineViolation(partyId, mutated) ?? '', expected, label); - } -}); + /exact owner and API path metadata/u, + ], + [ + 'coordinated topology drift', + contract + .replace("apiPrefix: '/party-registry-api'", "apiPrefix: '/evil-api'") + .replace( + "basePath: '/party-registry-api/party-registry'", + "basePath: '/evil-api/party-registry'", + ) + .replace( + "readinessPath: '/party-registry-api/party-registry/readiness'", + "readinessPath: '/evil-api/party-registry/readiness'", + ), + /exact owner and API path metadata/u, + ], + [ + 'forbidden credential metadata', + contract.replace( + partyReadinessMetadataLine, + `${partyReadinessMetadataLine}\n credential: 'secret',`, + ), + /exact owner and API path metadata/u, + ], + [ + 'forbidden credential path metadata', + contract.replace( + partyReadinessMetadataLine, + `${partyReadinessMetadataLine}\n credentialPath: '/party-registry-api/party-registry/secret',`, + ), + /exact owner and API path metadata/u, + ], + [ + 'unknown path metadata', + contract.replace( + partyReadinessMetadataLine, + `${partyReadinessMetadataLine}\n unknownPath: '/party-registry-api/party-registry/unknown',`, + ), + /exact owner and API path metadata/u, + ], + [ + 'spread metadata', + contract.replace( + 'export const partyRegistryApiContract = {', + 'const copiedMetadata = {};\nexport const partyRegistryApiContract = {\n ...copiedMetadata,', + ), + /exact owner and API path metadata/u, + ], + ] as const) { + expect((yield* microVerticalApiBaselineViolation(partyId, mutated)) ?? '', label).toMatch( + expected, + ); + } + }), +); -void test('MicroVertical baseline validation resolves an API stem independently from its directory', () => { - assert.equal( +it('MicroVertical baseline validation resolves an API stem independently from its directory', () => { + expect( configuredMicroVerticalApiStem('verticals/inventory', [ { api: { readiness: { endpoint: `/${warehouseItemsApiStem}/readiness` } }, @@ -3321,49 +3646,68 @@ void test('MicroVertical baseline validation resolves an API stem independently path: 'verticals/inventory', }, ]), - warehouseItemsApiStem, - ); + ).toBe(warehouseItemsApiStem); }); -void test('full-stack Party Registry keeps backend and Contacts component tests executable', async () => { - const packageJson = await readJson( - PackageJsonSchema, - path.join(workspaceRoot, 'verticals/party-registry/package.json'), - ); - assert.equal(packageJson.scripts['test:component'], 'rstest --config rstest.config.ts'); - assert.equal(packageJson.scripts['test:unit'], 'node --test tests/unit/*.test.ts'); - assert.equal(packageJson.scripts['test:integration'], 'node --test tests/integration/*.test.ts'); - assert.match( - await readFile(path.join(workspaceRoot, 'verticals/party-registry/rstest.config.ts'), 'utf-8'), - /tests\/components/u, - ); -}); +it.live( + 'full-stack Party Registry keeps backend and Contacts component tests executable', + Effect.fn(function* scenario27() { + const packageJson = yield* readJson( + PackageJsonSchema, + path.join(workspaceRoot, 'verticals/party-registry/package.json'), + ); + expect(packageJson.scripts['test:component']).toBe('rstest --project component'); + expect(packageJson.scripts['test:unit']).toBe('rstest --project unit'); + expect(packageJson.scripts['test:integration']).toBe('rstest --project integration'); + expect( + yield* Effect.promise(() => + readFile(path.join(workspaceRoot, 'verticals/party-registry/rstest.config.ts'), 'utf-8'), + ), + ).toMatch(/tests\/components/u); + }), +); + const cloudflareProofModule: unknown = await import( pathToFileURL( path.join(generatorRoot, 'templates/workspace-scripts/ultramodern-cloudflare-proof.mjs'), ).href ); -const cloudflareProof = Schema.decodeUnknownSync(CloudflareProofModuleSchema)( - cloudflareProofModule, -); -const validateCloudflareApp = cloudflareProof.validateApp.bind(cloudflareProofModule); -const validateApp = async ( + +const validateApp = ( app: ApiOnlyAppFixture, applicationPublicUrl: string, -): Promise => { - const output: unknown = await validateCloudflareApp(app, applicationPublicUrl); - return Schema.decodeUnknownSync(CloudflareEvidenceSchema)(output); -}; +): Effect.Effect => + Effect.gen(function* validateGeneratedCloudflareProof() { + const output: unknown = yield* Effect.promise(() => { + const framework = Schema.decodeUnknownSync( + Schema.Struct({ + validateApp: + callable< + ( + fixture: ApiOnlyAppFixture, + publicUrl: string, + ) => Promise + >(), + }), + )(cloudflareProofModule); + return framework.validateApp(app, applicationPublicUrl); + }); + return yield* Schema.decodeUnknownEffect(CloudflareEvidenceSchema)(output); + }); + const federationValidationModule: unknown = await import( pathToFileURL( path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/mf-validation/validate.js'), ).href ); + const federationValidation = Schema.decodeUnknownSync(ModuleFederationValidationModuleSchema)( federationValidationModule, ); + const validateInstalledModuleFederationTypes = federationValidation.validateModuleFederationTypes.bind(federationValidationModule); + const validateModuleFederationTypes = (input: { readonly appDirs: readonly string[]; readonly workspaceRoot: string; @@ -3373,7 +3717,9 @@ const validateModuleFederationTypes = (input: { }; const publicUrl = 'https://party.example.test'; + const buildMarker = 'party-build'; + interface ApiOnlyAppFixture { readonly deliveryUnit: { readonly buildMarker: string; readonly unitId: string }; readonly deploy: { @@ -3412,41 +3758,55 @@ const apiOnlyApp = (): ApiOnlyAppFixture => ({ marker: { build: buildMarker }, }); -const mockPublicResponses = (context: TestContext, failedPath?: string) => { +const mockPublicResponses = (failedPath?: string) => { const requested: string[] = []; - context.mock.method(globalThis, 'fetch', async (input: string | URL) => { - const route = new URL(String(input)).pathname; + rs.spyOn(globalThis, 'fetch').mockImplementation((input) => { + let address: string; + if (Schema.is(Schema.String)(input)) { + address = input; + } else if ('url' in input) { + address = input.url; + } else { + address = input.href; + } + const route = new URL(address).pathname; requested.push(route); if (route === failedPath) { - return new Response('unavailable', { status: 503 }); + return Promise.resolve(new Response('unavailable', { status: 503 })); } const body = route === mfManifestPath ? { metaData: { publicPath: `${publicUrl}/` } } : { marker: { build: buildMarker }, status: 'ready' }; - return Response.json(body, { headers: { 'access-control-allow-origin': '*' } }); + return Promise.resolve( + Response.json(body, { headers: { 'access-control-allow-origin': '*' } }), + ); }); return requested; }; -void test('API-only proof keeps manifest, readiness, service-binding and JSON proofs without invented pages/locales', async (context) => { - const requested = mockPublicResponses(context); - const evidence = await validateApp(apiOnlyApp(), publicUrl); - assert.deepEqual(requested, [mfManifestPath, readinessPath, '/binding', apiSmokePath]); - for (const proof of [ - 'mf-manifest', - 'api-marker', - 'delivery-unit-api-marker', - 'service-binding-api-marker', - 'json-smoke-value', - ]) { - assert.ok(evidence.assertions.some((entry) => entry.type === proof && entry.status === 'pass')); - } - assert.equal( - evidence.assertions.some((entry) => entry.type === 'ssr' || entry.type === 'i18n-marker'), - false, - ); -}); +it.live( + 'API-only proof keeps manifest, readiness, service-binding and JSON proofs without invented pages/locales', + Effect.fn(function* scenario29() { + const requested = mockPublicResponses(); + const evidence = yield* validateApp(apiOnlyApp(), publicUrl); + expect(requested).toEqual([mfManifestPath, readinessPath, '/binding', apiSmokePath]); + for (const proof of [ + 'mf-manifest', + 'api-marker', + 'delivery-unit-api-marker', + 'service-binding-api-marker', + 'json-smoke-value', + ]) { + expect( + evidence.assertions.some((entry) => entry.type === proof && entry.status === 'pass'), + ).toBe(true); + } + expect( + evidence.assertions.some((entry) => entry.type === 'ssr' || entry.type === 'i18n-marker'), + ).toBe(false); + }), +); for (const [route, error] of [ [mfManifestPath, /MF manifest returned HTTP 503/u], @@ -3454,161 +3814,214 @@ for (const [route, error] of [ ['/binding', /service binding PARTY_WORKER returned HTTP 503/u], [apiSmokePath, /JSON smoke api returned HTTP 503/u], ] as const) { - void test(`API-only proof still fails closed for ${route}`, async (context) => { - mockPublicResponses(context, route); - await assert.rejects(validateApp(apiOnlyApp(), publicUrl), error); - }); + it.live( + `API-only proof still fails closed for ${route}`, + Effect.fn(function* scenario30() { + mockPublicResponses(route); + const failureCause8 = yield* Effect.flip( + Effect.sandbox(validateApp(apiOnlyApp(), publicUrl)), + ); + expect(String(Cause.squash(failureCause8))).toMatch(error); + }), + ); } -void test('full-stack declared SSR remains mandatory', async (context) => { - const requested = mockPublicResponses(context, '/en'); - const app = apiOnlyApp(); - Object.assign(app.deploy.cloudflare.routes, { - locale: localePath, - ssr: '/en', - }); - await assert.rejects(validateApp(app, publicUrl), /SSR route returned HTTP 503/u); - assert.deepEqual(requested, ['/en']); -}); +it.live( + 'full-stack declared SSR remains mandatory', + Effect.fn(function* scenario31() { + const requested = mockPublicResponses('/en'); + const app = apiOnlyApp(); + Object.assign(app.deploy.cloudflare.routes, { + locale: localePath, + ssr: '/en', + }); + const failureCause9 = yield* Effect.flip(Effect.sandbox(validateApp(app, publicUrl))); + expect(String(Cause.squash(failureCause9))).toMatch(/SSR route returned HTTP 503/u); + expect(requested).toEqual(['/en']); + }), +); -void test('declared namespace locale remains mandatory independently of SSR', async (context) => { - const requested = mockPublicResponses(context, localePath); - const app = apiOnlyApp(); - Object.assign(app.deploy.cloudflare.routes, { locale: localePath }); - await assert.rejects(validateApp(app, publicUrl), /locale JSON returned HTTP 503/u); - assert.deepEqual(requested, [mfManifestPath, localePath]); -}); +it.live( + 'declared namespace locale remains mandatory independently of SSR', + Effect.fn(function* scenario32() { + const requested = mockPublicResponses(localePath); + const app = apiOnlyApp(); + Object.assign(app.deploy.cloudflare.routes, { locale: localePath }); + const failureCause10 = yield* Effect.flip(Effect.sandbox(validateApp(app, publicUrl))); + expect(String(Cause.squash(failureCause10))).toMatch(/locale JSON returned HTTP 503/u); + expect(requested).toEqual([mfManifestPath, localePath]); + }), +); for (const field of ['ssr', 'locale']) { - void test(`an invalid declared ${field} route cannot disable its proof`, async (context) => { - mockPublicResponses(context); - const app = apiOnlyApp(); - Object.assign(app.deploy.cloudflare.routes, { [field]: '' }); - await assert.rejects( - validateApp(app, publicUrl), - /declared .* route must be a root-relative path/u, - ); - }); + it.live( + `an invalid declared ${field} route cannot disable its proof`, + Effect.fn(function* scenario33() { + mockPublicResponses(); + const app = apiOnlyApp(); + Object.assign(app.deploy.cloudflare.routes, { [field]: '' }); + const failureCause11 = yield* Effect.flip(Effect.sandbox(validateApp(app, publicUrl))); + expect(String(Cause.squash(failureCause11))).toMatch( + /declared .* route must be a root-relative path/u, + ); + }), + ); } for (const variant of ['cjs', 'esm', 'esm-node']) { - void test(`${variant} inspector permits dts:false only with zero frontend exposes`, async () => { - const extension = variant === 'cjs' ? 'cjs' : 'js'; - const inspectionModule: unknown = await import( - pathToFileURL( - path.join( - generatorRoot, - `dist/${variant}/ultramodern-workspace/mf-validation/inspect.${extension}`, - ), - ).href + it.live( + `${variant} inspector permits dts:false only with zero frontend exposes`, + Effect.fn(function* scenario34() { + const extension = variant === 'cjs' ? 'cjs' : 'js'; + const inspectionModule: unknown = yield* Effect.promise( + () => + import( + pathToFileURL( + path.join( + generatorRoot, + `dist/${variant}/ultramodern-workspace/mf-validation/inspect.${extension}`, + ), + ).href + ), + ); + const inspection = yield* Schema.decodeUnknownEffect(ModuleFederationInspectionModuleSchema)( + inspectionModule, + ); + const inspectInstalledModuleFederationConfig = + inspection.inspectModuleFederationConfigSource.bind(inspectionModule); + const inspect = (source: string): typeof ModuleFederationInspectionSchema.Type => { + const output: unknown = inspectInstalledModuleFederationConfig( + source, + 'verticals/api', + 'module-federation.config.ts', + ); + return Schema.decodeUnknownSync(ModuleFederationInspectionSchema)(output); + }; + expect( + inspect('// @ultramodern-mf no-exposes\nexport default { dts: false, exposes: {} };').dts, + ).toEqual({}); + expect(() => + inspect('export default { dts: false, exposes: { "./Page": "./page.tsx" } };'), + ).toThrow(/DTS cannot be disabled for exposed app/u); + }), + ); +} + +it.live( + 'MF proof accepts explicit API-only intent but keeps exposed-app archives mandatory', + Effect.fn(function* scenario35() { + const fixture = yield* Effect.acquireRelease( + Effect.promise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-api-only-mf-'))), + (dir) => Effect.promise(() => rm(dir, { force: true, recursive: true })), ); - const inspection = Schema.decodeUnknownSync(ModuleFederationInspectionModuleSchema)( - inspectionModule, + const appDir = 'verticals/api'; + yield* Effect.promise(() => mkdir(path.join(fixture, appDir), { recursive: true })); + const configPath = path.join(fixture, appDir, 'module-federation.config.ts'); + const validate = () => + validateModuleFederationTypes({ appDirs: [appDir], workspaceRoot: fixture }); + yield* Effect.promise(() => + writeFile( + configPath, + '// @ultramodern-mf no-exposes\nexport default { dts: false, exposes: {} };', + ), ); - const inspectInstalledModuleFederationConfig = - inspection.inspectModuleFederationConfigSource.bind(inspectionModule); - const inspect = (source: string): typeof ModuleFederationInspectionSchema.Type => { - const output: unknown = inspectInstalledModuleFederationConfig( - source, - 'verticals/api', - 'module-federation.config.ts', - ); - return Schema.decodeUnknownSync(ModuleFederationInspectionSchema)(output); - }; - assert.deepEqual( - inspect('// @ultramodern-mf no-exposes\nexport default { dts: false, exposes: {} };').dts, - {}, + expect(validate().hostOnlyAppCount).toBe(1); + yield* Effect.promise(() => + writeFile(configPath, 'export default { dts: false, exposes: {} };'), ); - assert.throws( - () => inspect('export default { dts: false, exposes: { "./Page": "./page.tsx" } };'), - /DTS cannot be disabled for exposed app/u, + expect(validate).toThrow(/without an explicit host-only\/no-exposes declaration/u); + yield* Effect.promise(() => + writeFile( + configPath, + 'export default { dts: { tsConfigPath: "./tsconfig.mf-types.json", generateTypes: { compilerInstance: "effect-tsgo" } }, exposes: { "./Page": "./page.tsx" } };', + ), ); - }); -} - -void test('MF proof accepts explicit API-only intent but keeps exposed-app archives mandatory', async (context) => { - const fixture = await mkdtemp(path.join(os.tmpdir(), 'ontos-api-only-mf-')); - context.after(async (): Promise => { - await rm(fixture, { force: true, recursive: true }); - }); - const appDir = 'verticals/api'; - await mkdir(path.join(fixture, appDir), { recursive: true }); - const configPath = path.join(fixture, appDir, 'module-federation.config.ts'); - const validate = () => - validateModuleFederationTypes({ appDirs: [appDir], workspaceRoot: fixture }); - await writeFile( - configPath, - '// @ultramodern-mf no-exposes\nexport default { dts: false, exposes: {} };', - ); - assert.equal(validate().hostOnlyAppCount, 1); - await writeFile(configPath, 'export default { dts: false, exposes: {} };'); - assert.throws(validate, /without an explicit host-only\/no-exposes declaration/u); - await writeFile( - configPath, - 'export default { dts: { tsConfigPath: "./tsconfig.mf-types.json", generateTypes: { compilerInstance: "effect-tsgo" } }, exposes: { "./Page": "./page.tsx" } };', - ); - assert.throws(validate, /Missing Module Federation DTS archive/u); -}); + expect(validate).toThrow(/Missing Module Federation DTS archive/u); + }), +); -void test('Party deployment declares no fake SSR/locale URL while retaining backend contracts', async () => { - const topology = await readJson(TopologySchema, path.join(workspaceRoot, topologyReferencePath)); - const party = topology.verticals.find((entry) => entry.id === partyId); - assert.ok(party); - assert.equal(party.cloudflare.routes.ssr, undefined); - assert.equal(party.cloudflare.routes.locale, undefined); - assert.equal(party.cloudflare.routes.mfManifest, mfManifestPath); - assert.equal(party.cloudflare.routes.apiReadiness, readinessPath); - assert.equal( - party.backendFederation.exposes['./effect-api'].contract, - 'verticals/party-registry/shared/api.ts', - ); - assert.equal( - party.backendFederation.exposes['./effect-api'].openapi, - '/party-registry-api/openapi.json', - ); -}); +it.live( + 'Party deployment declares no fake SSR/locale URL while retaining backend contracts', + Effect.fn(function* scenario36() { + const topology = yield* readJson( + TopologySchema, + path.join(workspaceRoot, topologyReferencePath), + ); + const party = topology.verticals.find((entry) => entry.id === partyId); + expect(party).toBeDefined(); + if (!party) { + throw new Error('Party deployment is missing'); + } + expect(party.cloudflare.routes.ssr).toBe(undefined); + expect(party.cloudflare.routes.locale).toBe(undefined); + expect(party.cloudflare.routes.mfManifest).toBe(mfManifestPath); + expect(party.cloudflare.routes.apiReadiness).toBe(readinessPath); + expect(party.backendFederation.exposes['./effect-api'].contract).toBe( + 'verticals/party-registry/shared/api.ts', + ); + expect(party.backendFederation.exposes['./effect-api'].openapi).toBe( + '/party-registry-api/openapi.json', + ); + }), +); -void test('Party Registry is the sole deployment owner for Contacts capabilities', async () => { - const topology = await readJson(TopologySchema, path.join(workspaceRoot, topologyReferencePath)); - const overlay = await readJson( - OverlaySchema, - path.join(workspaceRoot, 'topology/local-overlays/development.json'), - ); - const zerops = await readFile(path.join(workspaceRoot, 'zerops.yaml'), 'utf-8'); - const partySetup = zerops.split(` - setup: '${partyId}'`)[1]?.split(' - setup:')[0]; - assert.ok(partySetup); - assert.equal(zerops.includes(" - setup: 'contacts'"), false); - assert.equal( - topology.verticals.some((entry) => entry.id === 'contacts'), - false, - ); - const party = topology.verticals.find((entry) => entry.id === partyId); - assert.ok(party); - assert.equal(overlay.ports[party.id], 4102); - assert.equal(overlay.apis[party.id], 'http://localhost:4102/party-registry-api'); - assert.ok(partySetup.includes('ULTRAMODERN_ZEROPS_SERVICE: party-registry')); - assert.ok(party.moduleFederation.exposes.includes('./PageContacts')); -}); +it.live( + 'Party Registry is the sole deployment owner for Contacts capabilities', + Effect.fn(function* scenario37() { + const topology = yield* readJson( + TopologySchema, + path.join(workspaceRoot, topologyReferencePath), + ); + const overlay = yield* readJson( + OverlaySchema, + path.join(workspaceRoot, 'topology/local-overlays/development.json'), + ); + const zerops = yield* Effect.promise(() => + readFile(path.join(workspaceRoot, 'zerops.yaml'), 'utf-8'), + ); + const partySetup = zerops.split(` - setup: '${partyId}'`)[1]?.split(' - setup:')[0]; + expect(partySetup).toBeDefined(); + if (!partySetup) { + throw new Error('Party setup is missing'); + } + expect(zerops.includes(" - setup: 'contacts'")).toBe(false); + expect(topology.verticals.some((entry) => entry.id === 'contacts')).toBe(false); + const party = topology.verticals.find((entry) => entry.id === partyId); + expect(party).toBeDefined(); + if (!party) { + throw new Error('Party deployment is missing'); + } + expect(overlay.ports[party.id]).toBe(4102); + expect(overlay.apis[party.id]).toBe('http://localhost:4102/party-registry-api'); + expect(partySetup.includes('ULTRAMODERN_ZEROPS_SERVICE: party-registry')).toBe(true); + expect(party.moduleFederation.exposes.includes('./PageContacts')).toBe(true); + }), +); -void test('installed Cloudflare CLI preserves API-only routes when synthesizing the real Party contract', async (context) => { - const fixture = await mkdtemp(path.join(os.tmpdir(), 'ontos-api-only-proof-')); - context.after(async (): Promise => { - await rm(fixture, { force: true, recursive: true }); - }); - await mkdir(path.join(fixture, '.modernjs')); - await writeFile( - path.join(fixture, '.modernjs/ultramodern.json'), - await readFile(path.join(workspaceRoot, '.modernjs/ultramodern.json')), - ); - const build = await readJson( - BuildArtifactSchema, - path.join(workspaceRoot, 'verticals/party-registry/shared/ultramodern-build.json'), - ); - const requestedPath = path.join(fixture, 'requested-routes.txt'); - const fetchMockPath = path.join(fixture, 'cloudflare-fetch-mock.mjs'); - await writeFile( - fetchMockPath, - `import { appendFileSync } from 'node:fs'; +it.live( + 'installed Cloudflare CLI preserves API-only routes when synthesizing the real Party contract', + Effect.fn(function* scenario38() { + const fixture = yield* Effect.acquireRelease( + Effect.promise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-api-only-proof-'))), + (dir) => Effect.promise(() => rm(dir, { force: true, recursive: true })), + ); + yield* Effect.promise(() => mkdir(path.join(fixture, '.modernjs'))); + const modernConfig = yield* Effect.promise(() => + readFile(path.join(workspaceRoot, '.modernjs/ultramodern.json')), + ); + yield* Effect.promise(() => + writeFile(path.join(fixture, '.modernjs/ultramodern.json'), modernConfig), + ); + const build = yield* readJson( + BuildArtifactSchema, + path.join(workspaceRoot, 'verticals/party-registry/shared/ultramodern-build.json'), + ); + const requestedPath = path.join(fixture, 'requested-routes.txt'); + const fetchMockPath = path.join(fixture, 'cloudflare-fetch-mock.mjs'); + yield* Effect.promise(() => + writeFile( + fetchMockPath, + `import { appendFileSync } from 'node:fs'; const requestedPath = ${JSON.stringify(requestedPath)}; const publicUrl = ${JSON.stringify(publicUrl)}; const manifestPath = ${JSON.stringify(mfManifestPath)}; @@ -3622,52 +4035,54 @@ const headers = { 'x-content-type-options': 'nosniff', 'x-robots-tag': 'noindex, nofollow', }; -globalThis.fetch = async input => { +globalThis.fetch = input => { const route = new URL(String(input)).pathname; appendFileSync(requestedPath, route + '\\n'); if (route === manifestPath) { - return Response.json({ metaData: { publicPath: publicUrl + '/' } }, { headers }); + return Promise.resolve(Response.json({ metaData: { publicPath: publicUrl + '/' } }, { headers })); } if (route === readinessPath) { - return Response.json({ + return Promise.resolve(Response.json({ checks: { api: 'ready', moduleFederation: 'ready', ssr: 'ready' }, marker: { build: buildMarker }, status: 'ready', - }, { headers }); + }, { headers })); } - return Response.json({ error: 'No owner route or locale exists' }, { headers, status: 404 }); + return Promise.resolve(Response.json({ error: 'No owner route or locale exists' }, { headers, status: 404 })); }; `, - ); - const reportPath = path.join(fixture, 'proof.json'); - runNode( - [ - '--import', - pathToFileURL(fetchMockPath).href, - path.join(generatorRoot, 'templates/workspace-scripts/proof-cloudflare-version.mjs'), - '--app', - partyId, - '--require-public-urls', - '--out', - reportPath, - ], - { - env: { - ULTRAMODERN_PUBLIC_URL_PARTY_REGISTRY: publicUrl, - ULTRAMODERN_WORKSPACE_ROOT: fixture, + ), + ); + const reportPath = path.join(fixture, 'proof.json'); + runNode( + [ + '--import', + pathToFileURL(fetchMockPath).href, + path.join(generatorRoot, 'templates/workspace-scripts/proof-cloudflare-version.mjs'), + '--app', + partyId, + '--require-public-urls', + '--out', + reportPath, + ], + { + env: { + ULTRAMODERN_PUBLIC_URL_PARTY_REGISTRY: publicUrl, + ULTRAMODERN_WORKSPACE_ROOT: fixture, + }, }, - }, - ); - const requestedSource = await readFile(requestedPath, 'utf-8'); - const requested = requestedSource.trimEnd().split('\n'); - assert.deepEqual(requested, [mfManifestPath, readinessPath, readinessPath]); - const report = await readJson(CloudflareReportSchema, reportPath); - assert.equal(report.status, 'pass'); - assert.equal(report.results[0].appId, 'party-registry'); - assert.ok(report.results[0].assertions.every((entry) => entry.status === 'pass')); -}); + ); + const requestedSource = yield* Effect.promise(() => readFile(requestedPath, 'utf-8')); + const requested = requestedSource.trimEnd().split('\n'); + expect(requested).toEqual([mfManifestPath, readinessPath, readinessPath]); + const report = yield* readJson(CloudflareReportSchema, reportPath); + expect(report.status).toBe('pass'); + expect(report.results[0].appId).toBe('party-registry'); + expect(report.results[0].assertions.every((entry) => entry.status === 'pass')).toBe(true); + }), +); -void test('proves generated Layer bindings and API aliases without accepting unused neighbors', () => { +it('proves generated Layer bindings and API aliases without accepting unused neighbors', () => { const source = governedLayerAliasFixture; const resolveImport = (specifier: string) => specifier === generatedSharedApiImport @@ -3677,38 +4092,44 @@ void test('proves generated Layer bindings and API aliases without accepting unu source: governedApiModuleSource, } : unexpectedTopologyImport(specifier); - assert.equal(strictEffectRuntimeTopologyViolation(source, resolveImport), undefined); + expect(strictEffectRuntimeTopologyViolation(source, resolveImport)).toBe(undefined); for (const [before, after] of governedLayerAliasMutations) { - assert.ok(source.includes(before)); - assert.notEqual( + expect(source.includes(before)).toBeTruthy(); + expect( strictEffectRuntimeTopologyViolation(source.replace(before, after), resolveImport), - undefined, - ); + ).not.toBe(undefined); } }); -void test('accepts only the trusted final identity terminator in a governed API slot', async () => { - const identityTerminator = '.pipe(identity)'; - const source = await readFile( - path.join(workspaceRoot, 'verticals/party-registry/shared/api.ts'), - 'utf-8', - ); - assert.ok(source.includes(identityTerminator)); - assert.equal(microVerticalApiBaselineViolation(partyId, source), undefined); - const mutations = [ - source.replace( - "import { Brand, identity } from 'effect';", - "import { Brand } from 'effect';\nimport { identity } from './counterfeit.ts';", - ), - source.replace(identityTerminator, '.pipe(unrelatedIdentity)'), - source.replace(identityTerminator, '.pipe(() => HttpApi.make("DiscardedApi"))'), - source.replace(identityTerminator, '.pipe(identity).addHttpApi(partyRegistryFoundationApi)'), - ]; - for (const mutated of mutations) { - assert.notEqual(mutated, source); - assert.match( - microVerticalApiBaselineViolation(partyId, mutated) ?? '', - /explicitly compose its readiness foundation API/u, +it.live( + 'accepts only the trusted final identity terminator in a governed API slot', + Effect.fn(function* governanceScenario21() { + const identityTerminator = '.pipe(identity)'; + const source = yield* Effect.promise(() => + readFile(path.join(workspaceRoot, 'verticals/party-registry/shared/api.ts'), 'utf-8'), ); - } + expect(source.includes(identityTerminator)).toBeTruthy(); + expect(yield* microVerticalApiBaselineViolation(partyId, source)).toBe(undefined); + const mutations = [ + source.replace( + "import { Brand, identity } from 'effect';", + "import { Brand } from 'effect';\nimport { identity } from './counterfeit.ts';", + ), + source.replace(identityTerminator, '.pipe(unrelatedIdentity)'), + source.replace(identityTerminator, '.pipe(() => HttpApi.make("DiscardedApi"))'), + source.replace(identityTerminator, '.pipe(identity).addHttpApi(partyRegistryFoundationApi)'), + ]; + for (const mutated of mutations) { + expect(mutated).not.toBe(source); + expect(violationText(yield* microVerticalApiBaselineViolation(partyId, mutated))).toMatch( + /explicitly compose its readiness foundation API/u, + ); + } + }), +); + +type GeneratedHttpHandler = ReturnType['createHandler']>; + +afterEach(() => { + rs.restoreAllMocks(); }); diff --git a/app/scripts/tests/audit-database-trust-boundaries.test.mts b/app/scripts/tests/audit-database-trust-boundaries.test.mts index 37fa6e226..5b75d8240 100644 --- a/app/scripts/tests/audit-database-trust-boundaries.test.mts +++ b/app/scripts/tests/audit-database-trust-boundaries.test.mts @@ -1,7 +1,8 @@ -import assert from 'node:assert/strict'; +import { Effect, Cause } from 'effect'; +import { expect, it } from 'effect-rstest'; + import { readFile } from 'node:fs/promises'; -import test from 'node:test'; -import { Cause } from 'effect'; + import { Client } from 'pg'; import { assertDatabaseSessionIdentities, @@ -164,30 +165,31 @@ const reversed = (values: readonly Value[]): Value[] => { return head === undefined ? [] : [...reversed(tail), head]; }; -void test('builds deterministic current-state evidence and identifies the material trust gaps', () => { +it('builds deterministic current-state evidence and identifies the material trust gaps', () => { const report = buildDatabaseTrustBoundaryReport({ ...snapshot, schemas: reversed(snapshot.schemas), tables: reversed(snapshot.tables), }); - assert.deepEqual( - report.schemas.map(({ schema }) => schema), - ['auth', 'contacts', 'core'], - ); - assert.deepEqual( - report.tables.map(({ schema, table }) => `${schema}.${table}`), - ['auth.user', 'contacts.customers', 'core.tenants'], - ); - assert.deepEqual( + expect(report.schemas.map(({ schema }) => schema)).toEqual(['auth', 'contacts', 'core']); + expect(report.tables.map(({ schema, table }) => `${schema}.${table}`)).toEqual([ + 'auth.user', + 'contacts.customers', + 'core.tenants', + ]); + expect( report.defaultPrivileges.map(({ grantee, schema, source }) => `${source}:${grantee}:${schema}`), - ['inherited:analytics_reader:null', 'public:PUBLIC:auth', 'direct:ontos_runtime:contacts'], - ); - assert.deepEqual( - report.findings.map(({ code, severity }) => `${severity}:${code}`), - ['high:runtime_role_can_forge_trusted_context', 'high:runtime_role_has_cross_schema_dml'], - ); - assert.deepEqual(report.summary, { + ).toEqual([ + 'inherited:analytics_reader:null', + 'public:PUBLIC:auth', + 'direct:ontos_runtime:contacts', + ]); + expect(report.findings.map(({ code, severity }) => `${severity}:${code}`)).toEqual([ + 'high:runtime_role_can_forge_trusted_context', + 'high:runtime_role_has_cross_schema_dml', + ]); + expect(report.summary).toEqual({ auditedSchemaCount: 3, defaultPrivilegeCount: 3, dmlSchemaCount: 3, @@ -202,10 +204,10 @@ void test('builds deterministic current-state evidence and identifies the materi tableCount: 3, typeCount: 0, }); - assert.equal(report.schemaVersion, 1); + expect(report.schemaVersion).toBe(1); }); -void test('orders audit evidence by code units rather than locale collation', () => { +it('orders audit evidence by code units rather than locale collation', () => { const report = buildDatabaseTrustBoundaryReport({ ...snapshot, types: [ @@ -214,13 +216,13 @@ void test('orders audit evidence by code units rather than locale collation', () ], }); - assert.deepEqual( - report.types.map(({ schema, type }) => `${schema}.${type}`), - ['zeta.status', 'ärea.status'], - ); + expect(report.types.map(({ schema, type }) => `${schema}.${type}`)).toEqual([ + 'zeta.status', + 'ärea.status', + ]); }); -void test('totally orders default privileges from distinct creator roles', () => { +it('totally orders default privileges from distinct creator roles', () => { const sharedPrivilege = { grantable: false, grantee: 'PUBLIC', @@ -237,34 +239,29 @@ void test('totally orders default privileges from distinct creator roles', () => ], }); - assert.deepEqual( - report.defaultPrivileges.map(({ owner }) => owner), - ['alpha_owner', 'zeta_owner'], - ); + expect(report.defaultPrivileges.map(({ owner }) => owner)).toEqual(['alpha_owner', 'zeta_owner']); }); -void test('extracts typed audit failures from an Effect cause', () => { +it('extracts typed audit failures from an Effect cause', () => { const reason = 'DATABASE_ADMIN_URL and DATABASE_URL must use distinct roles'; - assert.equal( + expect( getDatabaseTrustBoundaryFailureMessage( Cause.fail(new DatabaseTrustBoundaryAuditError({ reason })), ), - reason, - ); - assert.equal( - getDatabaseTrustBoundaryFailureMessage(Cause.die(new Error('driver defect'))), + ).toBe(reason); + expect(getDatabaseTrustBoundaryFailureMessage(Cause.die(new Error('driver defect')))).toBe( 'Database trust-boundary audit failed', ); }); -void test('treats any non-empty post-rollback trusted context as retained', () => { - assert.equal(hasTrustedContextValue(null), false); - assert.equal(hasTrustedContextValue(''), false); - assert.equal(hasTrustedContextValue('pre-existing-tenant-context'), true); +it('treats any non-empty post-rollback trusted context as retained', () => { + expect(hasTrustedContextValue(null)).toBe(false); + expect(hasTrustedContextValue('')).toBe(false); + expect(hasTrustedContextValue('pre-existing-tenant-context')).toBe(true); }); -void test('reports privilege escalation paths without embedding credentials or context values', () => { +it('reports privilege escalation paths without embedding credentials or context values', () => { const report = buildDatabaseTrustBoundaryReport({ ...snapshot, memberships: [ @@ -291,25 +288,25 @@ void test('reports privilege escalation paths without embedding credentials or c ], }); - assert.deepEqual(findingCodes(report), [ + expect(findingCodes(report)).toEqual([ 'runtime_role_is_privileged', 'runtime_role_can_assume_administrative_role', 'runtime_role_has_ddl_authority', 'runtime_role_can_forge_trusted_context', 'runtime_role_has_cross_schema_dml', ]); - assert.doesNotMatch(JSON.stringify(report), /postgresql:|password|secret|tenant-id|entity-id/iu); + expect(JSON.stringify(report)).not.toMatch(/postgresql:|password|secret|tenant-id|entity-id/iu); }); -void test('flags database-level CREATE even when no existing schema is writable', () => { +it('flags database-level CREATE even when no existing schema is writable', () => { const report = buildHardenedReport({ databasePrivileges: { ...snapshot.databasePrivileges, create: true }, }); - assert.deepEqual(findingCodes(report), ['runtime_role_has_ddl_authority']); + expect(findingCodes(report)).toEqual(['runtime_role_has_ddl_authority']); }); -void test('classifies reachable predefined PostgreSQL roles as privileged', () => { +it('classifies reachable predefined PostgreSQL roles as privileged', () => { const report = buildHardenedReport({ memberships: [ { @@ -332,46 +329,46 @@ void test('classifies reachable predefined PostgreSQL roles as privileged', () = ], }); - assert.deepEqual(findingCodes(report), ['runtime_role_can_assume_privileged_role']); + expect(findingCodes(report)).toEqual(['runtime_role_can_assume_privileged_role']); }); -void test('classifies a directly authenticated predefined PostgreSQL role as privileged', () => { +it('classifies a directly authenticated predefined PostgreSQL role as privileged', () => { const report = buildHardenedReport({ role: { ...ordinaryRole, predefinedRole: true }, runtimeRole: 'pg_execute_server_program', }); - assert.deepEqual(findingCodes(report), ['runtime_role_is_privileged']); + expect(findingCodes(report)).toEqual(['runtime_role_is_privileged']); }); -void test('flags effective configuration parameter authority', () => { +it('flags effective configuration parameter authority', () => { const report = buildHardenedReport({ parameterPrivileges: [{ alterSystem: false, parameter: 'session_replication_role', set: true }], }); - assert.deepEqual(findingCodes(report), ['runtime_role_has_parameter_authority']); - assert.equal(report.summary.parameterPrivilegeCount, 1); + expect(findingCodes(report)).toEqual(['runtime_role_has_parameter_authority']); + expect(report.summary.parameterPrivilegeCount).toBe(1); }); -void test('flags grant options on current objects as persistent authority', () => { +it('flags grant options on current objects as persistent authority', () => { const report = buildHardenedReport({ grantOptions: ['relation:contacts.customers:SELECT'], }); - assert.deepEqual(findingCodes(report), ['runtime_role_has_grant_authority']); - assert.equal(report.summary.grantOptionCount, 1); + expect(findingCodes(report)).toEqual(['runtime_role_has_grant_authority']); + expect(report.summary.grantOptionCount).toBe(1); }); -void test('flags creator-default grant options as persistent authority', () => { +it('flags creator-default grant options as persistent authority', () => { const report = buildHardenedReport({ defaultPrivileges: [{ ...snapshot.defaultPrivileges[0], grantable: true }], }); - assert.deepEqual(findingCodes(report), ['runtime_role_has_grant_authority']); - assert.equal(report.summary.grantOptionCount, 1); + expect(findingCodes(report)).toEqual(['runtime_role_has_grant_authority']); + expect(report.summary.grantOptionCount).toBe(1); }); -void test('flags selectable privileged owner-context views but accepts security invokers', () => { +it('flags selectable privileged owner-context views but accepts security invokers', () => { const ownerContextView = { ...snapshot.tables[0], deletable: true, @@ -404,11 +401,10 @@ void test('flags selectable privileged owner-context views but accepts security }; const ownerContextReport = buildDatabaseTrustBoundaryReport(base); - assert.deepEqual( - ownerContextReport.findings.map(({ code }) => code), - ['runtime_role_can_use_privileged_owner_view'], - ); - assert.equal(ownerContextReport.summary.privilegedOwnerViewCount, 1); + expect(ownerContextReport.findings.map(({ code }) => code)).toEqual([ + 'runtime_role_can_use_privileged_owner_view', + ]); + expect(ownerContextReport.summary.privilegedOwnerViewCount).toBe(1); const writableReport = buildDatabaseTrustBoundaryReport({ ...base, @@ -419,7 +415,7 @@ void test('flags selectable privileged owner-context views but accepts security }, ], }); - assert.deepEqual(findingCodes(writableReport), ['runtime_role_can_use_privileged_owner_view']); + expect(findingCodes(writableReport)).toEqual(['runtime_role_can_use_privileged_owner_view']); const readOnlyReport = buildDatabaseTrustBoundaryReport({ ...base, @@ -431,16 +427,16 @@ void test('flags selectable privileged owner-context views but accepts security }, ], }); - assert.deepEqual(readOnlyReport.findings, []); + expect(readOnlyReport.findings).toEqual([]); const invokerReport = buildDatabaseTrustBoundaryReport({ ...base, tables: [{ ...ownerContextView, securityInvoker: true }], }); - assert.deepEqual(invokerReport.findings, []); + expect(invokerReport.findings).toEqual([]); }); -void test('flags owner-context views that bypass RLS through owner-matched dependencies', () => { +it('flags owner-context views that bypass RLS through owner-matched dependencies', () => { const report = buildHardenedReport({ tables: [ { @@ -460,11 +456,11 @@ void test('flags owner-context views that bypass RLS through owner-matched depen }, }); - assert.deepEqual(findingCodes(report), ['runtime_role_can_use_privileged_owner_view']); - assert.equal(report.summary.privilegedOwnerViewCount, 1); + expect(findingCodes(report)).toEqual(['runtime_role_can_use_privileged_owner_view']); + expect(report.summary.privilegedOwnerViewCount).toBe(1); }); -void test('flags privileged owners in nested owner-context views', () => { +it('flags privileged owners in nested owner-context views', () => { const report = buildHardenedReport({ tables: [ { @@ -484,10 +480,10 @@ void test('flags privileged owners in nested owner-context views', () => { }, }); - assert.deepEqual(findingCodes(report), ['runtime_role_can_use_privileged_owner_view']); + expect(findingCodes(report)).toEqual(['runtime_role_can_use_privileged_owner_view']); }); -void test('flags ownership of an audited relation as DDL authority', () => { +it('flags ownership of an audited relation as DDL authority', () => { const report = buildDatabaseTrustBoundaryReport({ ...snapshot, sequences: [], @@ -515,11 +511,11 @@ void test('flags ownership of an audited relation as DDL authority', () => { }, }); - assert.equal(report.tables[0]?.kind, 'materialized-view'); - assert.deepEqual(findingCodes(report), ['runtime_role_has_ddl_authority']); + expect(report.tables[0]?.kind).toBe('materialized-view'); + expect(findingCodes(report)).toEqual(['runtime_role_has_ddl_authority']); }); -void test('flags ownership of an audited routine as DDL authority', () => { +it('flags ownership of an audited routine as DDL authority', () => { const report = buildHardenedReport({ routines: [ { @@ -534,10 +530,10 @@ void test('flags ownership of an audited routine as DDL authority', () => { ], }); - assert.deepEqual(findingCodes(report), ['runtime_role_has_ddl_authority']); + expect(findingCodes(report)).toEqual(['runtime_role_has_ddl_authority']); }); -void test('flags ownership of an audited application type as DDL authority', () => { +it('flags ownership of an audited application type as DDL authority', () => { const report = buildHardenedReport({ types: [ { @@ -549,11 +545,11 @@ void test('flags ownership of an audited application type as DDL authority', () ], }); - assert.deepEqual(findingCodes(report), ['runtime_role_has_ddl_authority']); - assert.equal(report.summary.typeCount, 1); + expect(findingCodes(report)).toEqual(['runtime_role_has_ddl_authority']); + expect(report.summary.typeCount).toBe(1); }); -void test('flags direct relation control and executable security-definer authority', () => { +it('flags direct relation control and executable security-definer authority', () => { const report = buildHardenedReport({ routines: [ { @@ -574,14 +570,14 @@ void test('flags direct relation control and executable security-definer authori ], }); - assert.deepEqual(findingCodes(report), [ + expect(findingCodes(report)).toEqual([ 'runtime_role_has_relation_control_authority', 'runtime_role_can_execute_security_definer', ]); - assert.equal(report.summary.securityDefinerExecutableCount, 1); + expect(report.summary.securityDefinerExecutableCount).toBe(1); }); -void test('flags direct sequence mutation authority', () => { +it('flags direct sequence mutation authority', () => { const report = buildHardenedReport({ sequences: [ { @@ -591,10 +587,10 @@ void test('flags direct sequence mutation authority', () => { ], }); - assert.deepEqual(findingCodes(report), ['runtime_role_has_sequence_mutation_authority']); + expect(findingCodes(report)).toEqual(['runtime_role_has_sequence_mutation_authority']); }); -void test('classifies every assumable role and escalates relation authority', () => { +it('classifies every assumable role and escalates relation authority', () => { const report = buildDatabaseTrustBoundaryReport({ ...snapshot, memberships: [ @@ -631,7 +627,7 @@ void test('classifies every assumable role and escalates relation authority', () ], }); - assert.deepEqual(findingCodes(report), [ + expect(findingCodes(report)).toEqual([ 'runtime_role_can_assume_privileged_role', 'runtime_role_can_assume_other_role', 'runtime_role_can_forge_trusted_context', @@ -639,7 +635,7 @@ void test('classifies every assumable role and escalates relation authority', () ]); }); -void test('treats ADMIN OPTION as an escalation path when SET OPTION is false', () => { +it('treats ADMIN OPTION as an escalation path when SET OPTION is false', () => { const report = buildDatabaseTrustBoundaryReport({ ...snapshot, memberships: [ @@ -661,42 +657,40 @@ void test('treats ADMIN OPTION as an escalation path when SET OPTION is false', ], }); - assert.deepEqual(findingCodes(report), [ + expect(findingCodes(report)).toEqual([ 'runtime_role_can_assume_privileged_role', 'runtime_role_can_forge_trusted_context', 'runtime_role_has_cross_schema_dml', ]); }); -void test('traverses SET OPTION descendants after every ADMIN OPTION role', async () => { - const source = await readFile( - new URL('../database-trust-audit/collect-snapshot.mts', import.meta.url), - 'utf-8', - ); - - assert.equal( - source.match(/where membership\.admin_option or membership\.set_option/gu)?.length, - 3, - ); - assert.match( - source, - /candidate\.oid in \(select role_oid from reachable_roles\) as can_set_role/u, - ); - assert.doesNotMatch( - source, - /or pg_has_role\(\$1, grantee\.oid, 'SET'\)\s+or grantee\.oid in \(select role_oid from administrable_roles\)/u, - ); - assert.match(source, /view_dependencies\(view_oid, referenced_oid, effective_owner_oid\)/u); - assert.match(source, /target_roles\(role_oid, role_name\)/u); - assert.match(source, /format\('role:%I:%s', target\.role_name, authority\.grant_option\)/u); - assert.match(source, /pg_has_role\(effective_owner\.oid, \$3, 'USAGE'\)/u); - assert.match( - source, - /pg_has_role\(\s*dependency\.effective_owner_oid,\s*referenced_relation\.relowner,\s*'USAGE'\s*\)/u, - ); -}); - -void test('treats inherited owner-role authority as effective runtime DDL authority', () => { +it.live( + 'traverses SET OPTION descendants after every ADMIN OPTION role', + Effect.fn(function* scenario1() { + const source = yield* Effect.promise(() => + readFile(new URL('../database-trust-audit/collect-snapshot.mts', import.meta.url), 'utf-8'), + ); + + expect(source.match(/where membership\.admin_option or membership\.set_option/gu)?.length).toBe( + 3, + ); + expect(source).toMatch( + /candidate\.oid in \(select role_oid from reachable_roles\) as can_set_role/u, + ); + expect(source).not.toMatch( + /or pg_has_role\(\$1, grantee\.oid, 'SET'\)\s+or grantee\.oid in \(select role_oid from administrable_roles\)/u, + ); + expect(source).toMatch(/view_dependencies\(view_oid, referenced_oid, effective_owner_oid\)/u); + expect(source).toMatch(/target_roles\(role_oid, role_name\)/u); + expect(source).toMatch(/format\('role:%I:%s', target\.role_name, authority\.grant_option\)/u); + expect(source).toMatch(/pg_has_role\(effective_owner\.oid, \$3, 'USAGE'\)/u); + expect(source).toMatch( + /pg_has_role\(\s*dependency\.effective_owner_oid,\s*referenced_relation\.relowner,\s*'USAGE'\s*\)/u, + ); + }), +); + +it('treats inherited owner-role authority as effective runtime DDL authority', () => { const report = buildHardenedReport({ memberships: [ { @@ -717,13 +711,13 @@ void test('treats inherited owner-role authority as effective runtime DDL author ], }); - assert.deepEqual(findingCodes(report), [ + expect(findingCodes(report)).toEqual([ 'runtime_role_can_assume_privileged_role', 'runtime_role_has_ddl_authority', ]); }); -void test('does not inherit cluster attributes without SET ROLE or ADMIN OPTION', () => { +it('does not inherit cluster attributes without SET ROLE or ADMIN OPTION', () => { const report = buildHardenedReport({ memberships: [ { @@ -744,47 +738,43 @@ void test('does not inherit cluster attributes without SET ROLE or ADMIN OPTION' ], }); - assert.deepEqual(findingCodes(report), ['runtime_role_can_assume_other_role']); + expect(findingCodes(report)).toEqual(['runtime_role_can_assume_other_role']); }); -void test('uses node-postgres effective query-parameter socket endpoints', () => { +it('uses node-postgres effective query-parameter socket endpoints', () => { const client = new Client({ connectionString: 'postgresql://authority_user:password@authority.invalid:5432/ontos?host=%2Fvar%2Frun%2Fruntime-db&port=6432', }); - assert.deepEqual(getEffectiveDatabaseEndpoint(client), { + expect(getEffectiveDatabaseEndpoint(client)).toEqual({ configuredHost: '/var/run/runtime-db', configuredPort: 6432, }); }); -void test('requires direct, distinct live database session identities', () => { - assert.doesNotThrow(() => +it('requires direct, distinct live database session identities', () => { + expect(() => assertDatabaseSessionIdentities( { currentRole: 'ontos_admin', sessionRole: 'ontos_admin' }, { currentRole: 'ontos_runtime', sessionRole: 'ontos_runtime' }, ), - ); - assert.throws( - () => - assertDatabaseSessionIdentities( - { currentRole: 'ontos_admin', sessionRole: 'ontos_admin' }, - { currentRole: 'ontos_admin', sessionRole: 'ontos_admin' }, - ), - /distinct authenticated PostgreSQL roles/u, - ); - assert.throws( - () => - assertDatabaseSessionIdentities( - { currentRole: 'startup_role', sessionRole: 'ontos_runtime' }, - { currentRole: 'ontos_runtime', sessionRole: 'ontos_runtime' }, - ), - /current_user must equal session_user/u, - ); + ).not.toThrow(); + expect(() => + assertDatabaseSessionIdentities( + { currentRole: 'ontos_admin', sessionRole: 'ontos_admin' }, + { currentRole: 'ontos_admin', sessionRole: 'ontos_admin' }, + ), + ).toThrow(/distinct authenticated PostgreSQL roles/u); + expect(() => + assertDatabaseSessionIdentities( + { currentRole: 'startup_role', sessionRole: 'ontos_runtime' }, + { currentRole: 'ontos_runtime', sessionRole: 'ontos_runtime' }, + ), + ).toThrow(/current_user must equal session_user/u); }); -void test('rejects evidence collected from different servers or databases', () => { +it('rejects evidence collected from different servers or databases', () => { const alternateServerAddress = [10, 0, 0, 2].join('.'); const serverAddress = [10, 0, 0, 1].join('.'); const target = { @@ -795,36 +785,32 @@ void test('rejects evidence collected from different servers or databases', () = serverPort: 5432, }; - assert.doesNotThrow(() => assertSameDatabaseTarget(target, { ...target })); - assert.throws( - () => assertSameDatabaseTarget(target, { ...target, database: 'other' }), - /same PostgreSQL server and database/u, - ); - assert.throws( - () => assertSameDatabaseTarget(target, { ...target, serverAddress: alternateServerAddress }), - /same PostgreSQL server and database/u, - ); - assert.throws( - () => - assertSameDatabaseTarget( - { - ...target, - configuredHost: '/var/run/postgresql-a', - serverAddress: null, - serverPort: null, - }, - { - ...target, - configuredHost: '/var/run/postgresql-b', - serverAddress: null, - serverPort: null, - }, - ), + expect(() => assertSameDatabaseTarget(target, { ...target })).not.toThrow(); + expect(() => assertSameDatabaseTarget(target, { ...target, database: 'other' })).toThrow( /same PostgreSQL server and database/u, ); + expect(() => + assertSameDatabaseTarget(target, { ...target, serverAddress: alternateServerAddress }), + ).toThrow(/same PostgreSQL server and database/u); + expect(() => + assertSameDatabaseTarget( + { + ...target, + configuredHost: '/var/run/postgresql-a', + serverAddress: null, + serverPort: null, + }, + { + ...target, + configuredHost: '/var/run/postgresql-b', + serverAddress: null, + serverPort: null, + }, + ), + ).toThrow(/same PostgreSQL server and database/u); }); -void test('treats transaction-local context retention as a critical boundary failure', () => { +it('treats transaction-local context retention as a critical boundary failure', () => { const report = buildDatabaseTrustBoundaryReport({ ...snapshot, tables: snapshot.tables.slice(0, 1), @@ -834,11 +820,8 @@ void test('treats transaction-local context retention as a critical boundary fai }, }); - assert.deepEqual( - report.findings.map(({ code, severity }) => `${severity}:${code}`), - [ - 'high:runtime_role_can_forge_trusted_context', - 'critical:trusted_context_survives_transaction', - ], - ); + expect(report.findings.map(({ code, severity }) => `${severity}:${code}`)).toEqual([ + 'high:runtime_role_can_forge_trusted_context', + 'critical:trusted_context_survives_transaction', + ]); }); diff --git a/app/scripts/tests/authorization-rollout-contract.test.mts b/app/scripts/tests/authorization-rollout-contract.test.mts index 675c8079c..5b38836f8 100644 --- a/app/scripts/tests/authorization-rollout-contract.test.mts +++ b/app/scripts/tests/authorization-rollout-contract.test.mts @@ -1,5 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; + import { validateAuthorizationRolloutContract } from '../authorization/rollout-contract.mts'; const entrypointKey = 'contacts.create-contact'; @@ -22,64 +22,54 @@ const context = { nowEpochMs: Date.parse('2026-09-10T00:00:00.000Z'), }; -await test('rollout contract accepts an active configuration bound to the classified inventory', () => { - assert.deepEqual(validateAuthorizationRolloutContract(contract, context), contract); +it('rollout contract accepts an active configuration bound to the classified inventory', () => { + expect(validateAuthorizationRolloutContract(contract, context)).toEqual(contract); }); -await test('the historical baseline revision does not have to equal the self-referential current commit', () => { - assert.deepEqual( +it('the historical baseline revision does not have to equal the self-referential current commit', () => { + expect( validateAuthorizationRolloutContract( { ...contract, baselineSourceRevision: 'historical-baseline-revision' }, context, ).baselineSourceRevision, - 'historical-baseline-revision', - ); + ).toEqual('historical-baseline-revision'); }); -await test('enforced rollout remains active after the report-only deadline', () => { - assert.equal( +it('enforced rollout remains active after the report-only deadline', () => { + expect( validateAuthorizationRolloutContract( { ...contract, mode: 'enforced' }, { ...context, nowEpochMs: Date.parse('2026-11-01T00:00:00.000Z') }, ).mode, - 'enforced', - ); + ).toBe('enforced'); }); -await test('rollout contract rejects expiry, stale inventory binding, extra fields, and duplicate baseline entries', () => { - assert.throws( - () => - validateAuthorizationRolloutContract(contract, { - ...context, - nowEpochMs: Date.parse(expiry), - }), - /inactive or expired/u, - ); - assert.throws( - () => validateAuthorizationRolloutContract(contract, { ...context, inventoryHash: 'other' }), - /does not match/u, - ); - assert.throws( - () => validateAuthorizationRolloutContract({ ...contract, arbitrary: true }, context), - /malformed/u, - ); - assert.throws( - () => - validateAuthorizationRolloutContract( - { - ...contract, - compatibilityEligibleEntrypoints: [entrypointKey, entrypointKey], - }, - context, - ), - /duplicates/u, - ); - assert.throws( - () => - validateAuthorizationRolloutContract( - { ...contract, compatibilityEligibleEntrypoints: ['contacts.new-action'] }, - context, - ), - /unknown entrypoint/u, - ); +it('rollout contract rejects expiry, stale inventory binding, extra fields, and duplicate baseline entries', () => { + expect(() => + validateAuthorizationRolloutContract(contract, { + ...context, + nowEpochMs: Date.parse(expiry), + }), + ).toThrow(/inactive or expired/u); + expect(() => + validateAuthorizationRolloutContract(contract, { ...context, inventoryHash: 'other' }), + ).toThrow(/does not match/u); + expect(() => + validateAuthorizationRolloutContract({ ...contract, arbitrary: true }, context), + ).toThrow(/malformed/u); + expect(() => + validateAuthorizationRolloutContract( + { + ...contract, + compatibilityEligibleEntrypoints: [entrypointKey, entrypointKey], + }, + context, + ), + ).toThrow(/duplicates/u); + expect(() => + validateAuthorizationRolloutContract( + { ...contract, compatibilityEligibleEntrypoints: ['contacts.new-action'] }, + context, + ), + ).toThrow(/unknown entrypoint/u); }); diff --git a/app/scripts/tests/boundary-source-structure.test.mts b/app/scripts/tests/boundary-source-structure.test.mts index 234552f79..4780b10fd 100644 --- a/app/scripts/tests/boundary-source-structure.test.mts +++ b/app/scripts/tests/boundary-source-structure.test.mts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import { test } from 'node:test'; +import { expect, it } from 'effect-rstest'; import { SyntaxKind } from '@typescript/native/unstable/ast'; import { DelimiterDepth, @@ -20,49 +19,46 @@ const endpointApi = (endpointPath: string, extra = '') => const stockReadPath = '/reads/stock'; -void test('balanced traversal ignores nested separators but preserves source offsets', () => { +it('balanced traversal ignores nested separators but preserves source offsets', () => { const source = "call({ nested: [1, 2], literal: ',);' }, /[,)]/, () => [3, 4]); next();"; const structure = maskNonCode(source); const close = matchingDelimiter(structure, source.indexOf('('), '(', ')'); - assert.equal(close, source.indexOf('; next') - 1); + expect(close).toBe(source.indexOf('; next') - 1); // The semicolon in the string must not terminate the statement. - assert.deepEqual(topLevelSeparators(structure, ';'), [ - source.indexOf('; next'), - source.length - 1, + expect(topLevelSeparators(structure, ';')).toEqual([source.indexOf('; next'), source.length - 1]); + expect(separatedSource(source, topLevelSeparators(structure, ',', 5, close), 5, close)).toEqual([ + "{ nested: [1, 2], literal: ',);' }", + '/[,)]/', + '() => [3, 4]', ]); - assert.deepEqual( - separatedSource(source, topLevelSeparators(structure, ',', 5, close), 5, close), - ["{ nested: [1, 2], literal: ',);' }", '/[,)]/', '() => [3, 4]'], - ); }); -void test('generic parameter commas and arrow returns remain separate lexical concerns', () => { +it('generic parameter commas and arrow returns remain separate lexical concerns', () => { const source = 'value: Map number>, next: number'; - assert.deepEqual(topLevelSeparators(source, ',', 0, source.length, true), [ + expect(topLevelSeparators(source, ',', 0, source.length, true)).toEqual([ source.indexOf(', next'), ]); - assert.deepEqual(topLevelSeparators('value < maximum; next > minimum;', ';'), [15, 31]); + expect(topLevelSeparators('value < maximum; next > minimum;', ';')).toEqual([15, 31]); const depth = new DelimiterDepth(); depth.update(']'); - assert.equal(depth.hasUnmatchedClose(), true); - assert.equal(depth.isTopLevel(), false); - assert.equal(toCamelCase('Stock-list'), 'stockList'); + expect(depth.hasUnmatchedClose()).toBe(true); + expect(depth.isTopLevel()).toBe(false); + expect(toCamelCase('Stock-list')).toBe('stockList'); }); -void test('token rescan retains nested template expressions and excludes regex punctuation', () => { +it('token rescan retains nested template expressions and excludes regex punctuation', () => { const source = `const result = \`outer \${ { nested: \`inner \${value}\` } }\`; const pattern = /[},;]/;`; const kinds = tokenizeGovernedClient(source).map(({ kind }) => kind); - assert.equal(kinds.filter((kind) => kind === SyntaxKind.TemplateTail).length, 2); - assert.equal(kinds.filter((kind) => kind === SyntaxKind.RegularExpressionLiteral).length, 1); - assert.equal(kinds.filter((kind) => kind === SyntaxKind.SemicolonToken).length, 2); + expect(kinds.filter((kind) => kind === SyntaxKind.TemplateTail).length).toBe(2); + expect(kinds.filter((kind) => kind === SyntaxKind.RegularExpressionLiteral).length).toBe(1); + expect(kinds.filter((kind) => kind === SyntaxKind.SemicolonToken).length).toBe(2); }); -void test('endpoint grammar shares only topology, preserving owner path and endpoint identity', () => { - assert.equal( +it('endpoint grammar shares only topology, preserving owner path and endpoint identity', () => { + expect( hasGeneratedModuleApiContract(endpointApi(stockReadPath), 'StockApi', 'stock', 'stock'), - true, - ); - assert.equal( + ).toBe(true); + expect( hasGeneratedProviderApiContract( endpointApi('/inventory.stock/reports/stock'), 'StockApi', @@ -70,9 +66,8 @@ void test('endpoint grammar shares only topology, preserving owner path and endp 'stock', 'report', ), - true, - ); - assert.equal( + ).toBe(true); + expect( hasGeneratedProviderApiContract( endpointApi(stockReadPath), 'StockApi', @@ -80,24 +75,21 @@ void test('endpoint grammar shares only topology, preserving owner path and endp 'stock', 'report', ), - false, - ); - assert.equal( + ).toBe(false); + expect( hasGeneratedModuleApiContract( endpointApi(stockReadPath, 'UnrelatedEndpoint'), 'StockApi', 'stock', 'stock', ), - false, - ); - assert.equal( + ).toBe(false); + expect( hasGeneratedModuleApiContract( endpointApi(stockReadPath).replace("'execute'", "'bypass'"), 'StockApi', 'stock', 'stock', ), - false, - ); + ).toBe(false); }); diff --git a/app/scripts/tests/check-authorization-readiness.test.mts b/app/scripts/tests/check-authorization-readiness.test.mts index 05bb222c8..799575d41 100644 --- a/app/scripts/tests/check-authorization-readiness.test.mts +++ b/app/scripts/tests/check-authorization-readiness.test.mts @@ -1,5 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; + import type { ProtectedEntrypointInventory } from '../authorization/protected-entrypoint-inventory.mts'; import { checkAuthorizationReadiness, @@ -143,104 +143,86 @@ const ready: AuthorizationReadinessInput = { spiceDbSchemaHash: 'e'.repeat(64), }; -void test('readiness emits deterministic evidence bound to the fixed context and exact build', () => { +it('readiness emits deterministic evidence bound to the fixed context and exact build', () => { const evidence = checkAuthorizationReadiness(ready); - assert.equal(evidence.status, 'ready'); - assert.equal(evidence.inventoryHash, inventory.inventoryHash); - assert.equal(evidence.fixedContextHash, ready.contextHash); - assert.equal(evidence.negativeSmokeHash, negativeSmokeHash); + expect(evidence.status).toBe('ready'); + expect(evidence.inventoryHash).toBe(inventory.inventoryHash); + expect(evidence.fixedContextHash).toBe(ready.contextHash); + expect(evidence.negativeSmokeHash).toBe(negativeSmokeHash); }); -void test('readiness rejects unapproved contexts and unresolved or stale impact evidence', () => { - assert.throws( - () => - checkAuthorizationReadiness({ - ...ready, - context: { ...ready.context, approvalStatus: 'pending' }, - }), - /unapproved/u, - ); - assert.throws( - () => - checkAuthorizationReadiness({ - ...ready, - impact: { ...ready.impact, totalWouldDeny: 1 }, - }), - /stale or unresolved/u, - ); - assert.throws( - () => - checkAuthorizationReadiness({ - ...ready, - impact: { ...ready.impact, sourceRevision: 'other' }, - }), - /stale or unresolved/u, - ); +it('readiness rejects unapproved contexts and unresolved or stale impact evidence', () => { + expect(() => + checkAuthorizationReadiness({ + ...ready, + context: { ...ready.context, approvalStatus: 'pending' }, + }), + ).toThrow(/unapproved/u); + expect(() => + checkAuthorizationReadiness({ + ...ready, + impact: { ...ready.impact, totalWouldDeny: 1 }, + }), + ).toThrow(/stale or unresolved/u); + expect(() => + checkAuthorizationReadiness({ + ...ready, + impact: { ...ready.impact, sourceRevision: 'other' }, + }), + ).toThrow(/stale or unresolved/u); }); -void test('readiness rejects missing relationships, module state, worker ownership, and replay migration', () => { +it('readiness rejects missing relationships, module state, worker ownership, and replay migration', () => { for (const key of [ 'verifiedActionEntrypoints', 'verifiedActiveModuleEntrypoints', 'verifiedContextPermissionEntrypoints', 'verifiedWorkerEntrypoints', ] as const) { - assert.throws( - () => - checkAuthorizationReadiness({ - ...ready, - observation: { ...ready.observation, [key]: [] }, - }), - /incomplete/u, - ); - } - assert.throws( - () => + expect(() => checkAuthorizationReadiness({ ...ready, - observation: { ...ready.observation, replayMigrationHash: 'f'.repeat(64) }, + observation: { ...ready.observation, [key]: [] }, }), - /stale/u, - ); + ).toThrow(/incomplete/u); + } + expect(() => + checkAuthorizationReadiness({ + ...ready, + observation: { ...ready.observation, replayMigrationHash: 'f'.repeat(64) }, + }), + ).toThrow(/stale/u); }); -void test('readiness rejects incorrect issuer/audience topology, short observations, and smoke gaps', () => { - assert.throws( - () => - checkAuthorizationReadiness({ - ...ready, - observation: { ...ready.observation, gatewayAudiences: ['other'] }, - }), - /issuer or audience/u, - ); - assert.throws( - () => - checkAuthorizationReadiness({ - ...ready, - observation: { ...ready.observation, gatewayIssuer: 'http://insecure.test' }, - }), - /issuer or audience/u, - ); - assert.throws( - () => - checkAuthorizationReadiness({ - ...ready, - impact: { - ...ready.impact, - observation: { - endedAt: '2026-09-02T00:00:01.000Z', - startedAt: '2026-09-02T00:00:00.000Z', - }, +it('readiness rejects incorrect issuer/audience topology, short observations, and smoke gaps', () => { + expect(() => + checkAuthorizationReadiness({ + ...ready, + observation: { ...ready.observation, gatewayAudiences: ['other'] }, + }), + ).toThrow(/issuer or audience/u); + expect(() => + checkAuthorizationReadiness({ + ...ready, + observation: { ...ready.observation, gatewayIssuer: 'http://insecure.test' }, + }), + ).toThrow(/issuer or audience/u); + expect(() => + checkAuthorizationReadiness({ + ...ready, + impact: { + ...ready.impact, + observation: { + endedAt: '2026-09-02T00:00:01.000Z', + startedAt: '2026-09-02T00:00:00.000Z', }, - }), - /observation/u, - ); - assert.throws( - () => - checkAuthorizationReadiness({ - ...ready, - negativeSmoke: { ...negativeSmoke, scenarios: negativeSmoke.scenarios.slice(1) }, - }), - /smoke evidence is incomplete/u, - ); + }, + }), + ).toThrow(/observation/u); + expect(() => + checkAuthorizationReadiness({ + ...ready, + negativeSmoke: { ...negativeSmoke, scenarios: negativeSmoke.scenarios.slice(1) }, + }), + ).toThrow(/smoke evidence is incomplete/u); }); diff --git a/app/scripts/tests/code-tools-i18n.test.mts b/app/scripts/tests/code-tools-i18n.test.mts index c3c92fd4e..acbef0b95 100644 --- a/app/scripts/tests/code-tools-i18n.test.mts +++ b/app/scripts/tests/code-tools-i18n.test.mts @@ -1,13 +1,9 @@ -/// - -import assert from 'node:assert/strict'; +import { expect, it } from 'effect-rstest'; import { createRequire } from 'node:module'; -import test from 'node:test'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { NodeServices } from '@effect/platform-node'; -import { Effect, FileSystem, Layer, Path, Schema, Stream } from 'effect'; +import { Effect, FileSystem, Path, Schema, Stream } from 'effect'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; -import { makeEffectTestCallback } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); @@ -15,8 +11,10 @@ const codeToolsPackage = '@modern-js/code-tools'; const malformedPluginCase = 'malformed plugin'; const cleanOutput = /^(?:Found 0 warnings and 0 errors\.\r?\nFinished in \d+(?:\.\d+)?(?:ms|s) on [1-9]\d* files? with \d+ rules using [1-9]\d* threads?\.\r?\n)?$/u; -const packageRoot = fileURLToPath(new URL('../..', import.meta.resolve(codeToolsPackage))); -const packageRequire = createRequire(import.meta.resolve(codeToolsPackage)); +const require = createRequire(import.meta.url); +const packageEntry = pathToFileURL(require.resolve(codeToolsPackage)); +const packageRoot = fileURLToPath(new URL('../..', packageEntry)); +const packageRequire = createRequire(packageEntry); const cases = [ { @@ -107,37 +105,32 @@ printOxlintOutput(result); process.exitCode = result.exitCode;`; ); const output = result.stdout + result.stderr; if (fixture.diagnostic === null) { - assert.equal(result.status, 0, output); - assert.match(output, cleanOutput); + expect(result.status, output).toBe(0); + expect(output).toMatch(cleanOutput); } else { - assert.equal(result.status, 1, output); - assert.match(output, fixture.diagnostic); - assert.match(output, /fixture\.tsx/u); + expect(result.status, output).toBe(1); + expect(output).toMatch(fixture.diagnostic); + expect(output).toMatch(/fixture\.tsx/u); if (fixture.name === malformedPluginCase) { - assert.match(output, /Error running JS plugin/u); - assert.doesNotMatch(output, /:0:0: {2}\[Warning\]/u); + expect(output).toMatch(/Error running JS plugin/u); + expect(output).not.toMatch(/:0:0: {2}\[Warning\]/u); } else { - assert.match(output, /fixture\.tsx:1:\d+/u); - assert.doesNotMatch(output, /Error running JS plugin/u); + expect(output).toMatch(/fixture\.tsx:1:\d+/u); + expect(output).not.toMatch(/Error running JS plugin/u); } } }); - void test( - `code-tools ${format}: ${fixture.name}`, - makeEffectTestCallback( - Effect.scoped( - Layer.build(Layer.effectDiscard(testEffect).pipe(Layer.provide(NodeServices.layer))), - ), - ), + it.live(`code-tools ${format}: ${fixture.name}`, () => + testEffect.pipe(Effect.provide(NodeServices.layer)), ); } } -void test('clean i18n output accepts only silence or a zero-diagnostic summary', () => { +it('clean i18n output accepts only silence or a zero-diagnostic summary', () => { const summary = 'Found 0 warnings and 0 errors.\nFinished in 423ms on 2 files with 98 rules using 4 threads.\n'; - assert.match('', cleanOutput); - assert.match(summary, cleanOutput); + expect('').toMatch(cleanOutput); + expect(summary).toMatch(cleanOutput); for (const output of [ summary.replace('0 errors', '1 error'), summary.replace('0 warnings', '1 warning'), @@ -145,6 +138,6 @@ void test('clean i18n output accepts only silence or a zero-diagnostic summary', `${summary}Error running JS plugin\n`, `fixture.tsx:1:1: unexpected diagnostic\n${summary}`, ]) { - assert.doesNotMatch(output, cleanOutput); + expect(output).not.toMatch(cleanOutput); } }); diff --git a/app/scripts/tests/database-access-boundaries.test.mts b/app/scripts/tests/database-access-boundaries.test.mts index 27e770710..a3f9a304e 100644 --- a/app/scripts/tests/database-access-boundaries.test.mts +++ b/app/scripts/tests/database-access-boundaries.test.mts @@ -1,71 +1,79 @@ -import assert from 'node:assert/strict'; +import { Effect } from 'effect'; +import { NodeServices } from '@effect/platform-node'; +import { expect, it } from 'effect-rstest'; import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import test from 'node:test'; import { checkDatabaseAccessBoundaries } from '../check-database-access-boundaries.mts'; -void test('allows owner database factories and rejects Action, read, nested BFF, and hidden Core database bypasses deterministically', async () => { - const root = await mkdtemp(path.join(os.tmpdir(), 'ontos-db-boundary-')); - try { - const files = { - 'apps/shell/api/routes/private.ts': - "const database = import(\n '@app/core-runtime/db/schema'\n);\n", - 'packages/core-runtime/src/testing/actions.ts': - 'export const makeActionTestHarness = () => undefined;\n', - 'verticals/stock/api/index.ts': "import { Pool } from 'pg';\n", - 'verticals/stock/api/routes/export.ts': - "import { coreDatabaseSchema } from '@app/core-runtime';\n", - 'verticals/stock/src/actions/generated-safe.action.ts': - "import { defineAction } from '@app/core-runtime/actions/definition';\n", - 'verticals/stock/src/actions/package-root.action.ts': - "import { InventoryPersistence } from '@app/stock';\nconst reserve = Effect.flatMap(InventoryPersistence, Effect.succeed);\n", - 'verticals/stock/src/actions/reserve.action.ts': - "import { CoreDatabase } from '@app/core-runtime';\nimport { InventoryPersistence } from '../infrastructure/inventory-persistence.ts';\nconst reserve = Effect.gen(function* () { yield* InventoryPersistence; });\n", - 'verticals/stock/src/actions/scoped.action.ts': - "import { makeScopedServices } from '../services/scoped-services.ts';\n", - 'verticals/stock/src/actions/side-effect.action.ts': - "import '../infrastructure/inventory-persistence.ts';\n", - 'verticals/stock/src/db/billing-leak.ts': - "import { invoices } from '../../../billing/src/db/schema.ts';\n", - 'verticals/stock/src/db/cross-owner.ts': - "import { coreDatabaseSchema } from '@app/core-runtime/db/schema';\n", - 'verticals/stock/src/db/dynamic-core.ts': - "const core = import(\n '@app/core-runtime/db/schema'\n);\n", - 'verticals/stock/src/db/service-factory.ts': - "import { drizzle } from 'drizzle-orm/node-postgres';\n", - 'verticals/stock/src/index.ts': - "export { InventoryPersistence } from './infrastructure/inventory-persistence.ts';\n", - 'verticals/stock/src/infrastructure/inventory-persistence.ts': - "import { Pool } from 'pg';\nexport class InventoryPersistence {}\n", - 'verticals/stock/src/reads/list.read.ts': "import { stock } from '../db/schema.ts';\n", - 'verticals/stock/src/reads/side-effect.read.ts': "import 'pg';\n", - 'verticals/stock/src/services/generated-action-service.ts': - "// @generated by OntOS Codesmith Action Service v1\nimport { eq } from 'drizzle-orm';\nimport { stock } from '../db/schema.ts';\nexport const findStock = () => eq(stock.id, 'one');\n", - 'verticals/stock/src/services/scoped-services.ts': - "import { eq } from 'drizzle-orm';\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nexport const makeScopedServices = (_transaction: Pick) => eq;\n", - 'verticals/stock/src/testing-harness-dynamic-leak.ts': - "const harness = import('../../../packages/core-runtime/src/testing/actions.ts');\n", - 'verticals/stock/src/testing-harness-export-leak.ts': - "export { makeActionTestHarness } from '@app/core-runtime/testing/actions';\n", - 'verticals/stock/src/testing-harness-leak.ts': - "import { makeActionTestHarness } from '@app/core-runtime/testing/actions';\n", - 'verticals/stock/tests/generated-source.test.ts': - '// @generated by OntOS Codesmith Governed Contribution\nconst assertion = /CoreDatabase/;\n', - 'verticals/stock/tests/testing-harness.test.ts': - "import { makeActionTestHarness } from '@app/core-runtime/testing/actions';\n", - } as const; - await Promise.all( - Object.entries(files).map(async ([relative, source]) => { - const file = path.join(root, relative); - await mkdir(path.dirname(file), { recursive: true }); - await writeFile(file, source); - }), - ); - const violations = await checkDatabaseAccessBoundaries(root); - assert.deepEqual( - violations.map(({ file, line }) => `${file}:${line}`), - [ +it.live( + 'allows owner database factories and rejects Action, read, nested BFF, and hidden Core database bypasses deterministically', + () => + Effect.gen(function* testEffect1() { + const root = yield* Effect.acquireRelease( + Effect.tryPromise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-db-boundary-'))), + (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), + ); + const files = { + 'apps/shell/api/routes/private.ts': + "const database = import(\n '@app/core-runtime/db/schema'\n);\n", + 'packages/core-runtime/src/testing/actions.ts': + 'export const makeActionTestHarness = () => undefined;\n', + 'verticals/stock/api/index.ts': "import { Pool } from 'pg';\n", + 'verticals/stock/api/routes/export.ts': + "import { coreDatabaseSchema } from '@app/core-runtime';\n", + 'verticals/stock/src/actions/generated-safe.action.ts': + "import { defineAction } from '@app/core-runtime/actions/definition';\n", + 'verticals/stock/src/actions/package-root.action.ts': + "import { InventoryPersistence } from '@app/stock';\nconst reserve = Effect.flatMap(InventoryPersistence, Effect.succeed);\n", + 'verticals/stock/src/actions/reserve.action.ts': + "import { CoreDatabase } from '@app/core-runtime';\nimport { InventoryPersistence } from '../infrastructure/inventory-persistence.ts';\nconst reserve = Effect.gen(function* testEffect2() { yield* InventoryPersistence; });\n", + 'verticals/stock/src/actions/scoped.action.ts': + "import { makeScopedServices } from '../services/scoped-services.ts';\n", + 'verticals/stock/src/actions/side-effect.action.ts': + "import '../infrastructure/inventory-persistence.ts';\n", + 'verticals/stock/src/db/billing-leak.ts': + "import { invoices } from '../../../billing/src/db/schema.ts';\n", + 'verticals/stock/src/db/cross-owner.ts': + "import { coreDatabaseSchema } from '@app/core-runtime/db/schema';\n", + 'verticals/stock/src/db/dynamic-core.ts': + "const core = import(\n '@app/core-runtime/db/schema'\n);\n", + 'verticals/stock/src/db/service-factory.ts': + "import { drizzle } from 'drizzle-orm/node-postgres';\n", + 'verticals/stock/src/index.ts': + "export { InventoryPersistence } from './infrastructure/inventory-persistence.ts';\n", + 'verticals/stock/src/infrastructure/inventory-persistence.ts': + "import { Pool } from 'pg';\nexport class InventoryPersistence {}\n", + 'verticals/stock/src/reads/list.read.ts': "import { stock } from '../db/schema.ts';\n", + 'verticals/stock/src/reads/side-effect.read.ts': "import 'pg';\n", + 'verticals/stock/src/services/generated-action-service.ts': + "// @generated by OntOS Codesmith Action Service v1\nimport { eq } from 'drizzle-orm';\nimport { stock } from '../db/schema.ts';\nexport const findStock = () => eq(stock.id, 'one');\n", + 'verticals/stock/src/services/scoped-services.ts': + "import { eq } from 'drizzle-orm';\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nexport const makeScopedServices = (_transaction: Pick) => eq;\n", + 'verticals/stock/src/testing-harness-dynamic-leak.ts': + "const harness = import('../../../packages/core-runtime/src/testing/actions.ts');\n", + 'verticals/stock/src/testing-harness-export-leak.ts': + "export { makeActionTestHarness } from '@app/core-runtime/testing/actions';\n", + 'verticals/stock/src/testing-harness-leak.ts': + "import { makeActionTestHarness } from '@app/core-runtime/testing/actions';\n", + 'verticals/stock/tests/generated-source.test.ts': + '// @generated by OntOS Codesmith Governed Contribution\nconst assertion = /CoreDatabase/;\n', + 'verticals/stock/tests/testing-harness.test.ts': + "import { makeActionTestHarness } from '@app/core-runtime/testing/actions';\n", + } as const; + yield* Effect.all( + Object.entries(files).map(([relative, source]) => + Effect.gen(function* testEffect3() { + const file = path.join(root, relative); + yield* Effect.tryPromise(() => mkdir(path.dirname(file), { recursive: true })); + yield* Effect.tryPromise(() => writeFile(file, source)); + }), + ), + ); + const violations = yield* checkDatabaseAccessBoundaries(root).pipe( + Effect.provide(NodeServices.layer), + ); + expect(violations.map(({ file, line }) => `${file}:${line}`)).toEqual([ 'apps/shell/api/routes/private.ts:1', 'verticals/stock/api/index.ts:1', 'verticals/stock/api/routes/export.ts:1', @@ -81,9 +89,6 @@ void test('allows owner database factories and rejects Action, read, nested BFF, 'verticals/stock/src/testing-harness-dynamic-leak.ts:1', 'verticals/stock/src/testing-harness-export-leak.ts:1', 'verticals/stock/src/testing-harness-leak.ts:1', - ], - ); - } finally { - await rm(root, { force: true, recursive: true }); - } -}); + ]); + }), +); diff --git a/app/scripts/tests/dependency-declarations.test.mts b/app/scripts/tests/dependency-declarations.test.mts index 1e010b3f6..0d63457d7 100644 --- a/app/scripts/tests/dependency-declarations.test.mts +++ b/app/scripts/tests/dependency-declarations.test.mts @@ -1,11 +1,8 @@ -/// - -import assert from 'node:assert/strict'; +import { expect, it } from 'effect-rstest'; import { spawnSync } from 'node:child_process'; import { appendFileSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import test from 'node:test'; import { fileURLToPath } from 'node:url'; import { Option } from 'effect'; import { Param } from 'effect/unstable/cli'; @@ -42,12 +39,12 @@ assert.equal(pgRole('undefined', { inherit: undefined }).inherit, undefined); `, ); const result = spawnSync(process.execPath, [filename], { encoding: 'utf-8' }); - assert.ifError(result.error); - assert.equal(result.status, 0, result.stdout + result.stderr); + expect(result.error).toBeUndefined(); + expect(result.status, result.stdout + result.stderr).toBe(0); } }; -void test('published dependency declarations retain strict positive and negative contracts', () => { +it('published dependency declarations retain strict positive and negative contracts', () => { const fixture = mkdtempSync(path.join(tmpdir(), 'ontos-declaration-contract-')); try { symlinkSync( @@ -112,11 +109,11 @@ cockroachRole('invalid', { createRole: 1 }); ['-p', path.join(fixture, configFilename), '--pretty', 'false'], { encoding: 'utf-8' }, ); - assert.ifError(result.error); + expect(result.error).toBeUndefined(); const diagnostics = result.stdout + result.stderr; - assert.equal(result.status, errors === 0 ? 0 : 1, diagnostics); - assert.equal(countDiagnostics(diagnostics, /error TS2322:/gu), errors, diagnostics); - assert.equal(countDiagnostics(diagnostics, /error TS\d+:/gu), errors, diagnostics); + expect(result.status, diagnostics).toBe(errors === 0 ? 0 : 1); + expect(countDiagnostics(diagnostics, /error TS2322:/gu), diagnostics).toBe(errors); + expect(countDiagnostics(diagnostics, /error TS\d+:/gu), diagnostics).toBe(errors); } } verifyDrizzleRuntimeFormats(fixture); @@ -149,8 +146,8 @@ const reverse: typeof metadata = expected; ['-p', path.join(fixture, configFilename), '--pretty', 'false'], { encoding: 'utf-8' }, ); - assert.ifError(result.error); - assert.equal(result.status, 0, result.stdout + result.stderr); + expect(result.error).toBeUndefined(); + expect(result.status, result.stdout + result.stderr).toBe(0); appendFileSync( path.join(fixture, metadataFilename), ` @@ -166,21 +163,21 @@ metadata.isOptional = true; ['-p', path.join(fixture, configFilename), '--pretty', 'false'], { encoding: 'utf-8' }, ); - assert.ifError(invalidMetadata.error); + expect(invalidMetadata.error).toBeUndefined(); const diagnostics = invalidMetadata.stdout + invalidMetadata.stderr; - assert.equal(invalidMetadata.status, 1, diagnostics); - assert.equal(countDiagnostics(diagnostics, /error TS2322:/gu), 2, diagnostics); - assert.equal(countDiagnostics(diagnostics, /error TS2375:/gu), 2, diagnostics); - assert.equal(countDiagnostics(diagnostics, /error TS2540:/gu), 1, diagnostics); - assert.equal(countDiagnostics(diagnostics, /error TS\d+:/gu), 5, diagnostics); + expect(invalidMetadata.status, diagnostics).toBe(1); + expect(countDiagnostics(diagnostics, /error TS2322:/gu), diagnostics).toBe(2); + expect(countDiagnostics(diagnostics, /error TS2375:/gu), diagnostics).toBe(2); + expect(countDiagnostics(diagnostics, /error TS2540:/gu), diagnostics).toBe(1); + expect(countDiagnostics(diagnostics, /error TS\d+:/gu), diagnostics).toBe(5); } finally { rmSync(fixture, { force: true, recursive: true }); } }); -void test('published runtime exposes real parameter metadata', () => { +it('published runtime exposes real parameter metadata', () => { const metadata = Param.getParamMetadata(Param.string(Param.flagKind, 'name')); - assert.deepEqual(metadata, { + expect(metadata).toEqual({ isOptional: false, isVariadic: false, variadicMax: Option.none(), diff --git a/app/scripts/tests/effect-rstest-package.test.mts b/app/scripts/tests/effect-rstest-package.test.mts new file mode 100644 index 000000000..74f46cb49 --- /dev/null +++ b/app/scripts/tests/effect-rstest-package.test.mts @@ -0,0 +1,57 @@ +import { Effect, Equal, Hash, Schema } from 'effect'; +import { FastCheck } from 'effect/testing'; +import { addEqualityTesters, expect, it } from 'effect-rstest'; + +class SemanticValue implements Equal.Equal { + readonly #key: string; + readonly representation: string; + constructor(key: string, representation: string) { + this.#key = key; + this.representation = representation; + } + [Equal.symbol](that: Equal.Equal): boolean { + return #key in that && this.#key === that.#key; + } + [Hash.symbol](): number { + return this.#key.length; + } +} + +addEqualityTesters(); + +it('the installed package honors Effect equality without replacing native assertions', () => { + expect(new SemanticValue('same', 'left')).toEqual(new SemanticValue('same', 'right')); + expect(new SemanticValue('left', 'same')).not.toEqual(new SemanticValue('next', 'same')); + expect({ value: 1 }).toEqual({ value: 1 }); + expect({ value: 1 }).toEqual(expect.objectContaining({ value: 1 })); +}); + +it.prop( + 'the installed package generates tuple schemas', + [Schema.Literal('schema'), FastCheck.integer()], + ([label, value]) => { + expect(label).toBe('schema'); + expect(Number.isInteger(value)).toBe(true); + }, +); + +it.prop( + 'the installed package generates record schemas', + { value: Schema.Literal('schema') }, + ({ value }) => { + expect(value).toBe('schema'); + }, +); + +// Promise assimilation would inspect this success value for a `then` property. +const value = new Proxy( + {}, + { + get(): never { + throw new Error('Effect success values must not reach Promise resolution'); + }, + }, +); + +it.effect('discards success values at the runner boundary', () => Effect.succeed(value)); +it.live('discards live success values at the runner boundary', () => Effect.succeed(value)); diff --git a/app/scripts/tests/generated-slot-entries.test.mts b/app/scripts/tests/generated-slot-entries.test.mts index 93fbc1299..b3adc874b 100644 --- a/app/scripts/tests/generated-slot-entries.test.mts +++ b/app/scripts/tests/generated-slot-entries.test.mts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Result } from 'effect'; import { readGeneratedSlotEntries } from '../scaffolding/shared.mts'; @@ -8,38 +7,38 @@ const end = '// slot:end'; const readEntries = (body: string): readonly string[] => readGeneratedSlotEntries(`${start}\n${body}\n${end}`, start, end); -void test('fluent slots split only outer calls, retaining nested multiline fluent chains', () => { +it('fluent slots split only outer calls, retaining nested multiline fluent chains', () => { const nested = `.addHttpApi( FirstApi .add(Group.make('nested')) .pipe(identity), )`; - assert.deepEqual(readEntries(`${nested}\n.addHttpApi(SecondApi)`), [ + expect(readEntries(`${nested}\n.addHttpApi(SecondApi)`)).toEqual([ nested, '.addHttpApi(SecondApi)', ]); }); -void test('slot delimiters inside strings and comments do not terminate entries', () => { +it('slot delimiters inside strings and comments do not terminate entries', () => { const first = "first: { value: 'a,;.[({', /* ; } ] ) */ nested: [1, 2] },"; const second = 'second: call(`comma, semicolon;`, "escaped\\\";"),'; - assert.deepEqual(readEntries(`${first}\n${second}`), [first, second]); + expect(readEntries(`${first}\n${second}`)).toEqual([first, second]); }); -void test('line comments protect fluent-looking text until the newline', () => { +it('line comments protect fluent-looking text until the newline', () => { const first = '.addHttpApi(\n FirstApi // .addHttpApi(FakeApi);\n)'; - assert.deepEqual(readEntries(`${first}\n.addHttpApi(SecondApi)`), [ + expect(readEntries(`${first}\n.addHttpApi(SecondApi)`)).toEqual([ first, '.addHttpApi(SecondApi)', ]); }); -void test('empty generated slots remain empty', () => { - assert.deepEqual(readEntries(' \n'), []); +it('empty generated slots remain empty', () => { + expect(readEntries(' \n')).toEqual([]); }); for (const source of ['.addHttpApi(FirstApi', 'first: "open,', 'first: /* open', 'first: ] ,']) { - void test(`incomplete or unbalanced generated slot fails closed: ${source}`, () => { - assert.ok(Result.isFailure(Result.try(() => readEntries(source)))); + it(`incomplete or unbalanced generated slot fails closed: ${source}`, () => { + expect(Result.isFailure(Result.try(() => readEntries(source)))).toBeTruthy(); }); } diff --git a/app/scripts/tests/initialize-local-development.test.mts b/app/scripts/tests/initialize-local-development.test.mts index c652ec6e7..54382d7d8 100644 --- a/app/scripts/tests/initialize-local-development.test.mts +++ b/app/scripts/tests/initialize-local-development.test.mts @@ -1,12 +1,10 @@ +import { expect, it } from 'effect-rstest'; import { makeModuleContractFixture } from '../../packages/core-runtime/src/testing/module-contract.ts'; -import { runEffectTestPromise } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; -import assert from 'node:assert/strict'; import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import { test } from 'node:test'; -import { NodeFileSystem, NodePath } from '@effect/platform-node'; -import { Effect, Exit, Layer } from 'effect'; +import { NodeServices } from '@effect/platform-node'; +import { Effect, Exit, Schema } from 'effect'; import { ConnectionError, SqlError } from 'effect/unstable/sql/SqlError'; import { makeTestDatabase } from '../../packages/core-runtime/tests/support/database.ts'; import type { deriveOntosModuleDeploymentContract } from '../generate-ontos-module-contract.mts'; @@ -44,194 +42,202 @@ const topology = JSON.stringify({ verticals: [{ id: 'party-registry' }, { id: 'i const moduleContract = ( moduleId: string, -): Awaited> => +): Effect.Success> => makeModuleContractFixture({ appId: 'test-module', buildMarker: 'test-build', moduleId }); -void test('accepts only a development configuration with local service endpoints', async () => { - const configuration = await runEffectTestPromise( - parseLocalDevelopmentConfiguration(localEnvironment), - ); - assert.equal(configuration.email, LOCAL_DEVELOPMENT_CONTEXT.email); - assert.equal(configuration.databaseAdminUrl, localEnvironment.DATABASE_ADMIN_URL); - - await Promise.all( - [ - { ...localEnvironment, ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'stage' }, - { - ...localEnvironment, - DATABASE_ADMIN_URL: 'postgres://ontos_admin:admin@database.example.com:5432/ontos', - }, - { - ...localEnvironment, - SPICEDB_ENDPOINT: 'spicedb.example.com:50051', - SPICEDB_INSECURE: 'false', - }, - ].map( - async (environment) => - await assert.rejects( - runEffectTestPromise(parseLocalDevelopmentConfiguration(environment)), - LocalDevelopmentInitializationError, - ), - ), - ); -}); - -void test('exact reconciliation is idempotent and contradictory records fail in the typed channel', async () => { - const expected = { name: 'OntOS Local Development', status: 'active' } as const; - assert.equal( - await runEffectTestPromise(classifyExactLocalRecord('tenant', undefined, expected)), - 'create', - ); - assert.equal( - await runEffectTestPromise(classifyExactLocalRecord('tenant', expected, expected)), - 'existing', - ); - const conflict = await runEffectTestPromise( - classifyExactLocalRecord('tenant', { ...expected, status: 'suspended' }, expected).pipe( - Effect.flip, - ), - ); - assert.equal(conflict.code, 'local_conflict'); - assert.match(conflict.reason, /status/u); -}); - -void test('module-state reconciliation preserves migrated IDs and rejects identity collisions', async () => { - const expected = { - moduleKey: PARTY_REGISTRY_MODULE_ID, - state: 'active', - tenantId: LOCAL_DEVELOPMENT_CONTEXT.tenantId, - tenantModuleStateId: moduleStateIdFor(PARTY_REGISTRY_MODULE_ID), - } as const; - assert.equal( - await runEffectTestPromise( - classifyLocalModuleState(PARTY_REGISTRY_MODULE_STATE_LABEL, undefined, expected), - ), - 'create', - ); - assert.equal( - await runEffectTestPromise( - classifyLocalModuleState( - PARTY_REGISTRY_MODULE_STATE_LABEL, - { ...expected, tenantModuleStateId: '7f000000-0000-4000-8000-000000000001' }, - expected, - ), - ), - 'existing', - ); - await assert.rejects( - runEffectTestPromise( - classifyLocalModuleState( - PARTY_REGISTRY_MODULE_STATE_LABEL, +it.effect('accepts only a development configuration with local service endpoints', () => + Effect.gen(function* testEffect1() { + const configuration = yield* parseLocalDevelopmentConfiguration(localEnvironment); + expect(configuration.email).toBe(LOCAL_DEVELOPMENT_CONTEXT.email); + expect(configuration.databaseAdminUrl).toBe(localEnvironment.DATABASE_ADMIN_URL); + + yield* Effect.all( + [ + { ...localEnvironment, ULTRAMODERN_DEPLOYMENT_ENVIRONMENT: 'stage' }, { - ...expected, - moduleKey: INVENTORY_MODULE_ID, - tenantModuleStateId: expected.tenantModuleStateId, + ...localEnvironment, + DATABASE_ADMIN_URL: 'postgres://ontos_admin:admin@database.example.com:5432/ontos', }, - expected, - ), - ), - LocalDevelopmentInitializationError, - ); -}); - -void test('derives only configured Party Registry through its generated owner contract', async () => { - const root = await mkdtemp(path.join(os.tmpdir(), LOCAL_MODULES_DIRECTORY_PREFIX)); - await mkdir(path.join(root, TOPOLOGY_DIRECTORY), { recursive: true }); - await writeFile(path.join(root, TOPOLOGY_PATH), topology, 'utf-8'); - const deriveContract = async ({ vertical }: { readonly vertical: string }) => - moduleContract(`${vertical}.core`); - assert.deepEqual( - await runEffectTestPromise( - deriveActivatedModuleIds(root, deriveContract).pipe( - Effect.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)), - ), - ), - ['party-registry.core'], - ); -}); - -void test('rejects duplicate module IDs derived from different verticals', async () => { - const root = await mkdtemp(path.join(os.tmpdir(), LOCAL_MODULES_DIRECTORY_PREFIX)); - await mkdir(path.join(root, TOPOLOGY_DIRECTORY), { recursive: true }); - await writeFile(path.join(root, TOPOLOGY_PATH), topology, 'utf-8'); - const deriveContract = async () => moduleContract('duplicate.core'); - await assert.rejects( - runEffectTestPromise( - deriveActivatedModuleIds(root, deriveContract, ['party-registry', 'inventory']).pipe( - Effect.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)), + { + ...localEnvironment, + SPICEDB_ENDPOINT: 'spicedb.example.com:50051', + SPICEDB_INSECURE: 'false', + }, + ].map((environment) => + Effect.gen(function* testEffect2() { + expect( + Schema.is(LocalDevelopmentInitializationError)( + yield* Effect.flip(parseLocalDevelopmentConfiguration(environment)), + ), + ).toBe(true); + }), ), - ), - LocalDevelopmentInitializationError, - ); -}); - -void test('generates stable module state IDs and complete access relationships', async () => { - assert.equal( - moduleStateIdFor(PARTY_REGISTRY_MODULE_ID), - moduleStateIdFor(PARTY_REGISTRY_MODULE_ID), - ); - assert.notEqual( - moduleStateIdFor(PARTY_REGISTRY_MODULE_ID), - moduleStateIdFor(INVENTORY_MODULE_ID), - ); - const relationships = await runEffectTestPromise( - buildLocalDevelopmentRelationships([PARTY_REGISTRY_MODULE_ID, INVENTORY_MODULE_ID]), - ); - assert.equal(relationships.length, 7); - assert.equal(relationships.filter(({ relation }) => relation === 'accessor').length, 2); - assert.equal(relationships.filter(({ relation }) => relation === 'legal_entity').length, 2); -}); - -void test('a late module conflict rolls back Core bootstrap and retains its typed reason', async () => { - const statements: string[] = []; - const database = makeTestDatabase((sql) => - Effect.sync(() => { - statements.push(sql); - return sql.includes('from "core"."tenant_module_states"') ? [{}, {}] : []; + ); + }), +); + +it.effect( + 'exact reconciliation is idempotent and contradictory records fail in the typed channel', + () => + Effect.gen(function* testEffect3() { + const expected = { name: 'OntOS Local Development', status: 'active' } as const; + expect(yield* classifyExactLocalRecord('tenant', undefined, expected)).toBe('create'); + expect(yield* classifyExactLocalRecord('tenant', expected, expected)).toBe('existing'); + const conflict = yield* classifyExactLocalRecord( + 'tenant', + { ...expected, status: 'suspended' }, + expected, + ).pipe(Effect.flip); + expect(conflict.code).toBe('local_conflict'); + expect(conflict.reason).toMatch(/status/u); }), - ); - - const conflict = await runEffectTestPromise( - reconcileCoreContext(database, LOCAL_AUTH_USER_ID, [PARTY_REGISTRY_MODULE_ID]).pipe( - Effect.flip, - ), - ); - - assert.equal(conflict.code, 'local_conflict'); - assert.match(conflict.reason, /module-state identity conflicts/u); - assert.ok(statements.some((sql) => sql.startsWith('insert into "core"."tenants"'))); - assert.equal(statements.at(-1), 'ROLLBACK'); - assert.ok(!statements.includes('COMMIT')); -}); - -void test('native commit failure becomes a typed bootstrap error', async () => { - const database = makeTestDatabase((sql) => - sql === 'COMMIT' - ? Effect.fail( - new SqlError({ reason: new ConnectionError({ cause: new Error('connection closed') }) }), - ) - : Effect.succeed([]), - ); - - const error = await runEffectTestPromise( - reconcileCoreContext(database, LOCAL_AUTH_USER_ID, []).pipe(Effect.flip), - ); - assert.equal(error.code, 'local_persistence_failed'); -}); - -void test('bootstrap preserves unrelated defects after native rollback', async () => { - const defect = new Error('unexpected query defect'); - const statements: string[] = []; - const database = makeTestDatabase((sql) => - Effect.suspend(() => { - statements.push(sql); - return sql.startsWith('select ') ? Effect.die(defect) : Effect.succeed([]); +); + +it.effect( + 'module-state reconciliation preserves migrated IDs and rejects identity collisions', + () => + Effect.gen(function* testEffect4() { + const expected = { + moduleKey: PARTY_REGISTRY_MODULE_ID, + state: 'active', + tenantId: LOCAL_DEVELOPMENT_CONTEXT.tenantId, + tenantModuleStateId: moduleStateIdFor(PARTY_REGISTRY_MODULE_ID), + } as const; + expect( + yield* classifyLocalModuleState(PARTY_REGISTRY_MODULE_STATE_LABEL, undefined, expected), + ).toBe('create'); + expect( + yield* classifyLocalModuleState( + PARTY_REGISTRY_MODULE_STATE_LABEL, + { ...expected, tenantModuleStateId: '7f000000-0000-4000-8000-000000000001' }, + expected, + ), + ).toBe('existing'); + expect( + Schema.is(LocalDevelopmentInitializationError)( + yield* Effect.flip( + classifyLocalModuleState( + PARTY_REGISTRY_MODULE_STATE_LABEL, + { + ...expected, + moduleKey: INVENTORY_MODULE_ID, + tenantModuleStateId: expected.tenantModuleStateId, + }, + expected, + ), + ), + ), + ).toBe(true); }), - ); - - const exit = await runEffectTestPromise( - reconcileCoreContext(database, LOCAL_AUTH_USER_ID, []).pipe(Effect.exit), - ); - assert.deepEqual(exit, Exit.die(defect)); - assert.equal(statements.at(-1), 'ROLLBACK'); -}); +); + +it.effect('derives only configured Party Registry through its generated owner contract', () => + Effect.gen(function* testEffect5() { + const root = yield* Effect.tryPromise(() => + mkdtemp(path.join(os.tmpdir(), LOCAL_MODULES_DIRECTORY_PREFIX)), + ); + yield* Effect.tryPromise(() => mkdir(path.join(root, TOPOLOGY_DIRECTORY), { recursive: true })); + yield* Effect.tryPromise(() => writeFile(path.join(root, TOPOLOGY_PATH), topology, 'utf-8')); + const deriveContract = ({ vertical }: { readonly vertical: string }) => + Effect.succeed(moduleContract(`${vertical}.core`)); + expect( + yield* deriveActivatedModuleIds(root, deriveContract).pipe( + Effect.provide(NodeServices.layer), + ), + ).toEqual(['party-registry.core']); + }), +); + +it.effect('rejects duplicate module IDs derived from different verticals', () => + Effect.gen(function* testEffect6() { + const root = yield* Effect.tryPromise(() => + mkdtemp(path.join(os.tmpdir(), LOCAL_MODULES_DIRECTORY_PREFIX)), + ); + yield* Effect.tryPromise(() => mkdir(path.join(root, TOPOLOGY_DIRECTORY), { recursive: true })); + yield* Effect.tryPromise(() => writeFile(path.join(root, TOPOLOGY_PATH), topology, 'utf-8')); + const deriveContract = () => Effect.succeed(moduleContract('duplicate.core')); + expect( + Schema.is(LocalDevelopmentInitializationError)( + yield* Effect.flip( + deriveActivatedModuleIds(root, deriveContract, ['party-registry', 'inventory']).pipe( + Effect.provide(NodeServices.layer), + ), + ), + ), + ).toBe(true); + }), +); + +it.effect('generates stable module state IDs and complete access relationships', () => + Effect.gen(function* testEffect7() { + expect(moduleStateIdFor(PARTY_REGISTRY_MODULE_ID)).toBe( + moduleStateIdFor(PARTY_REGISTRY_MODULE_ID), + ); + expect(moduleStateIdFor(PARTY_REGISTRY_MODULE_ID)).not.toBe( + moduleStateIdFor(INVENTORY_MODULE_ID), + ); + const relationships = yield* buildLocalDevelopmentRelationships([ + PARTY_REGISTRY_MODULE_ID, + INVENTORY_MODULE_ID, + ]); + expect(relationships.length).toBe(7); + expect(relationships.filter(({ relation }) => relation === 'accessor').length).toBe(2); + expect(relationships.filter(({ relation }) => relation === 'legal_entity').length).toBe(2); + }), +); + +it.effect('a late module conflict rolls back Core bootstrap and retains its typed reason', () => + Effect.gen(function* testEffect8() { + const statements: string[] = []; + const database = yield* makeTestDatabase((sql) => + Effect.sync(() => { + statements.push(sql); + return sql.includes('from "core"."tenant_module_states"') ? [{}, {}] : []; + }), + ); + + const conflict = yield* reconcileCoreContext(database, LOCAL_AUTH_USER_ID, [ + PARTY_REGISTRY_MODULE_ID, + ]).pipe(Effect.flip); + + expect(conflict.code).toBe('local_conflict'); + expect(conflict.reason).toMatch(/module-state identity conflicts/u); + expect(statements.some((sql) => sql.startsWith('insert into "core"."tenants"'))).toBe(true); + expect(statements.at(-1)).toBe('ROLLBACK'); + expect(!statements.includes('COMMIT')).toBe(true); + }), +); + +it.effect('native commit failure becomes a typed bootstrap error', () => + Effect.gen(function* testEffect9() { + const database = yield* makeTestDatabase((sql) => + sql === 'COMMIT' + ? Effect.fail( + new SqlError({ + reason: new ConnectionError({ cause: new Error('connection closed') }), + }), + ) + : Effect.succeed([]), + ); + + const error = yield* reconcileCoreContext(database, LOCAL_AUTH_USER_ID, []).pipe(Effect.flip); + expect(error.code).toBe('local_persistence_failed'); + }), +); + +it.effect('bootstrap preserves unrelated defects after native rollback', () => + Effect.gen(function* testEffect10() { + const defect = new Error('unexpected query defect'); + const statements: string[] = []; + const database = yield* makeTestDatabase((sql) => + Effect.suspend(() => { + statements.push(sql); + return sql.startsWith('select ') ? Effect.die(defect) : Effect.succeed([]); + }), + ); + + const exit = yield* reconcileCoreContext(database, LOCAL_AUTH_USER_ID, []).pipe(Effect.exit); + expect(exit).toEqual(Exit.die(defect)); + expect(statements.at(-1)).toBe('ROLLBACK'); + }), +); diff --git a/app/scripts/tests/locki-feature.test.mts b/app/scripts/tests/locki-feature.test.mts index 8639e4b41..ed5f9fc35 100644 --- a/app/scripts/tests/locki-feature.test.mts +++ b/app/scripts/tests/locki-feature.test.mts @@ -1,20 +1,24 @@ -import assert from 'node:assert/strict'; +import { Effect } from 'effect'; +import { expect, it } from 'effect-rstest'; import { spawnSync } from 'node:child_process'; import { chmod, cp, mkdir, mkdtemp, readFile, stat, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { execPath } from 'node:process'; -import { test } from 'node:test'; const workspaceRoot = path.resolve(import.meta.dirname, '../..'); const workflowScript = path.join(workspaceRoot, 'scripts/locki-feature.sh'); const featureSlug = 'customer-search'; -void test('pins pnpm to the npm mise backend for cross-platform sandbox installation', async () => { - const miseConfiguration = await readFile(path.join(workspaceRoot, '.mise.toml'), 'utf-8'); - assert.match(miseConfiguration, /\[tool_alias\][\s\S]*pnpm = "npm:pnpm"/u); - assert.match(miseConfiguration, /\[tools\][\s\S]*pnpm = "11\.25\.0"/u); -}); +it.live('pins pnpm to the npm mise backend for cross-platform sandbox installation', () => + Effect.gen(function* testEffect1() { + const miseConfiguration = yield* Effect.tryPromise(() => + readFile(path.join(workspaceRoot, '.mise.toml'), 'utf-8'), + ); + expect(miseConfiguration).toMatch(/\[tool_alias\][\s\S]*pnpm = "npm:pnpm"/u); + expect(miseConfiguration).toMatch(/\[tools\][\s\S]*pnpm = "11\.25\.0"/u); + }), +); interface Fixture { readonly binDirectory: string; @@ -29,26 +33,36 @@ interface WorkflowResult { readonly stdout: string; } -const executable = async (target: string, content: string): Promise => { - await writeFile(target, content, 'utf-8'); - await chmod(target, 0o755); -}; +const executable = (target: string, content: string) => + Effect.gen(function* testEffect2() { + yield* Effect.tryPromise(() => writeFile(target, content, 'utf-8')); + yield* Effect.tryPromise(() => chmod(target, 0o755)); + }); -const makeFixture = async (withEnvironment = true): Promise => { - const root = await mkdtemp(path.join(os.tmpdir(), 'ontos-locki-feature-')); - const sourceRoot = path.join(root, 'source'); - const targetRoot = path.join(root, 'target'); - const binDirectory = path.join(root, 'bin'); - const logPath = path.join(root, 'commands.log'); - await mkdir(path.join(sourceRoot, 'app/scripts'), { recursive: true }); - await mkdir(binDirectory, { recursive: true }); - await cp(workflowScript, path.join(sourceRoot, 'app/scripts/locki-feature.sh')); - if (withEnvironment) { - await writeFile(path.join(sourceRoot, 'app/.env'), Buffer.from('OPAQUE-SECRET\0VALUE\n')); - } - await executable( - path.join(binDirectory, 'git'), - `#!/bin/sh +const makeFixture = (withEnvironment = true) => + Effect.gen(function* testEffect3() { + const root = yield* Effect.tryPromise(() => + mkdtemp(path.join(os.tmpdir(), 'ontos-locki-feature-')), + ); + const sourceRoot = path.join(root, 'source'); + const targetRoot = path.join(root, 'target'); + const binDirectory = path.join(root, 'bin'); + const logPath = path.join(root, 'commands.log'); + yield* Effect.tryPromise(() => + mkdir(path.join(sourceRoot, 'app/scripts'), { recursive: true }), + ); + yield* Effect.tryPromise(() => mkdir(binDirectory, { recursive: true })); + yield* Effect.tryPromise(() => + cp(workflowScript, path.join(sourceRoot, 'app/scripts/locki-feature.sh')), + ); + if (withEnvironment) { + yield* Effect.tryPromise(() => + writeFile(path.join(sourceRoot, 'app/.env'), Buffer.from('OPAQUE-SECRET\0VALUE\n')), + ); + } + yield* executable( + path.join(binDirectory, 'git'), + `#!/bin/sh if [ "$3" = "rev-parse" ]; then printf '%s\\n' "$TEST_SOURCE_ROOT"; exit 0; fi if [ "$3" = "check-ignore" ]; then exit 0; fi if [ "$3" = "cat-file" ]; then @@ -58,25 +72,25 @@ fi if [ "$3" = "diff" ]; then exit 0; fi exit 9 `, - ); - await executable( - path.join(binDirectory, 'mise'), - `#!/bin/sh + ); + yield* executable( + path.join(binDirectory, 'mise'), + `#!/bin/sh printf 'mise %s\\n' "$*" >>"$TEST_LOG" if [ "\${1-}" = "install" ] && [ -n "\${LOCKI_SANDBOX_ID-}" ]; then exit 18; fi if [ "$*" = "exec -- pnpm install --frozen-lockfile" ] && [ "\${ULTRAMODERN_SKIP_CODEX_SKILLS-}" != "1" ]; then exit 19; fi if [ "\${FAIL_PREPARATION-}" = "true" ] && [ "\${1-}" = "install" ]; then exit 17; fi `, - ); - await executable( - path.join(binDirectory, 'docker'), - `#!/bin/sh + ); + yield* executable( + path.join(binDirectory, 'docker'), + `#!/bin/sh printf 'docker %s\\n' "$*" >>"$TEST_LOG" `, - ); - await executable( - path.join(binDirectory, 'locki'), - `#!/bin/sh + ); + yield* executable( + path.join(binDirectory, 'locki'), + `#!/bin/sh command_name=$1 shift printf 'locki %s %s\\n' "$command_name" "$*" >>"$TEST_LOG" @@ -104,9 +118,9 @@ case "$command_name" in *) exit 8 ;; esac `, - ); - return { binDirectory, logPath, sourceRoot, targetRoot }; -}; + ); + return { binDirectory, logPath, sourceRoot, targetRoot }; + }); const runWorkflow = ( fixture: Fixture, @@ -127,103 +141,119 @@ const runWorkflow = ( }, }, ); - assert.ifError(result.error); + expect(result.error).toBeUndefined(); return { code: result.status, stderr: result.stderr, stdout: result.stdout }; }; -void test('creates one sandbox from main, copies .env opaquely, and prepares in order', async () => { - const fixture = await makeFixture(); - const result = runWorkflow(fixture, ['--', featureSlug, '--no-ai']); - assert.equal(result.code, 0, result.stderr); - assert.equal(result.stdout.includes('OPAQUE-SECRET'), false); - assert.deepEqual( - await readFile(path.join(fixture.targetRoot, 'app/.env')), - await readFile(path.join(fixture.sourceRoot, 'app/.env')), - ); - const environmentStat = await stat(path.join(fixture.targetRoot, 'app/.env')); - assert.equal(environmentStat.mode % 0o1000, 0o600); - const log = await readFile(fixture.logPath, 'utf-8'); - assert.match(log, /locki new --from main --branch codex\/customer-search --json/u); - assert.match( - log, - /locki exec --match sandbox-42 -- sh app\/scripts\/locki-feature\.sh --prepare/u, - ); - assert.equal(log.includes('locki ai'), false); - const expectedOrder = [ - 'mise install', - 'mise exec -- pnpm install --frozen-lockfile', - 'mise exec -- pnpm env:local:ensure', - 'docker compose up --detach --wait', - 'mise exec -- pnpm db:migrate', - 'mise exec -- pnpm local:initialize', - 'mise exec -- pnpm db:verify', - ]; - let previous = -1; - for (const command of expectedOrder) { - const index = log.indexOf(command); - assert.ok(index > previous, `${command} must follow the previous preparation step`); - previous = index; - } -}); +it.live('creates one sandbox from main, copies .env opaquely, and prepares in order', () => + Effect.gen(function* testEffect4() { + const fixture = yield* makeFixture(); + const result = runWorkflow(fixture, ['--', featureSlug, '--no-ai']); + expect(result.code, result.stderr).toBe(0); + expect(result.stdout.includes('OPAQUE-SECRET')).toBe(false); + expect( + yield* Effect.tryPromise(() => readFile(path.join(fixture.targetRoot, 'app/.env'))), + ).toEqual(yield* Effect.tryPromise(() => readFile(path.join(fixture.sourceRoot, 'app/.env')))); + const environmentStat = yield* Effect.tryPromise(() => + stat(path.join(fixture.targetRoot, 'app/.env')), + ); + expect(environmentStat.mode % 0o1000).toBe(0o600); + const log = yield* Effect.tryPromise(() => readFile(fixture.logPath, 'utf-8')); + expect(log).toMatch(/locki new --from main --branch codex\/customer-search --json/u); + expect(log).toMatch( + /locki exec --match sandbox-42 -- sh app\/scripts\/locki-feature\.sh --prepare/u, + ); + expect(log.includes('locki ai')).toBe(false); + const expectedOrder = [ + 'mise install', + 'mise exec -- pnpm install --frozen-lockfile', + 'mise exec -- pnpm env:local:ensure', + 'docker compose up --detach --wait', + 'mise exec -- pnpm db:migrate', + 'mise exec -- pnpm local:initialize', + 'mise exec -- pnpm db:verify', + ]; + let previous = -1; + for (const command of expectedOrder) { + const index = log.indexOf(command); + expect(index > previous).toBe(true); + previous = index; + } + }), +); -void test('rejects unsafe slugs and alternate options before creating a sandbox', async () => { - const assertRejected = async (commandArguments: readonly string[]): Promise => { - const fixture = await makeFixture(); - const result = runWorkflow(fixture, commandArguments); - assert.equal(result.code, 2); - await assert.rejects(readFile(fixture.logPath, 'utf-8')); - }; - await assertRejected(['Bad Slug']); - await assertRejected(['feature', '--from', 'main']); -}); +it.live('rejects unsafe slugs and alternate options before creating a sandbox', () => + Effect.gen(function* testEffect5() { + const assertRejected = (commandArguments: readonly string[]) => + Effect.gen(function* testEffect6() { + const fixture = yield* makeFixture(); + const result = runWorkflow(fixture, commandArguments); + expect(result.code).toBe(2); + expect( + yield* Effect.flip(Effect.tryPromise(() => readFile(fixture.logPath, 'utf-8'))), + ).toBeDefined(); + }); + yield* assertRejected(['Bad Slug']); + yield* assertRejected(['feature', '--from', 'main']); + }), +); -void test('fails before Locki when the source environment is missing', async () => { - const fixture = await makeFixture(false); - const result = runWorkflow(fixture, [featureSlug]); - assert.equal(result.code, 1); - assert.match(result.stderr, /Source app\/\.env is required/u); - const log = await readFile(fixture.logPath, 'utf-8'); - assert.equal(log.includes('locki new'), false); -}); +it.live('fails before Locki when the source environment is missing', () => + Effect.gen(function* testEffect7() { + const fixture = yield* makeFixture(false); + const result = runWorkflow(fixture, [featureSlug]); + expect(result.code).toBe(1); + expect(result.stderr).toMatch(/Source app\/\.env is required/u); + const log = yield* Effect.tryPromise(() => readFile(fixture.logPath, 'utf-8')); + expect(log.includes('locki new')).toBe(false); + }), +); -void test('fails before creating a sandbox when the workflow is not committed on main', async () => { - const fixture = await makeFixture(); - const result = runWorkflow(fixture, [featureSlug], { - TEST_WORKFLOW_COMMITTED: 'false', - }); - assert.equal(result.code, 1); - assert.match(result.stderr, /workflow is not yet committed on main/u); - const log = await readFile(fixture.logPath, 'utf-8'); - assert.equal(log.includes('locki new'), false); -}); +it.live('fails before creating a sandbox when the workflow is not committed on main', () => + Effect.gen(function* testEffect8() { + const fixture = yield* makeFixture(); + const result = runWorkflow(fixture, [featureSlug], { + TEST_WORKFLOW_COMMITTED: 'false', + }); + expect(result.code).toBe(1); + expect(result.stderr).toMatch(/workflow is not yet committed on main/u); + const log = yield* Effect.tryPromise(() => readFile(fixture.logPath, 'utf-8')); + expect(log.includes('locki new')).toBe(false); + }), +); -void test('refuses an app path that resolves outside the returned worktree', async () => { - const fixture = await makeFixture(); - const result = runWorkflow(fixture, [featureSlug], { ESCAPE_TARGET: 'true' }); - assert.equal(result.code, 1); - assert.match(result.stderr, /Refusing to copy \.env outside the Locki worktree/u); - assert.deepEqual( - await readFile(path.join(fixture.sourceRoot, 'app/.env')), - Buffer.from('OPAQUE-SECRET\0VALUE\n'), - ); -}); +it.live('refuses an app path that resolves outside the returned worktree', () => + Effect.gen(function* testEffect9() { + const fixture = yield* makeFixture(); + const result = runWorkflow(fixture, [featureSlug], { ESCAPE_TARGET: 'true' }); + expect(result.code).toBe(1); + expect(result.stderr).toMatch(/Refusing to copy \.env outside the Locki worktree/u); + expect( + yield* Effect.tryPromise(() => readFile(path.join(fixture.sourceRoot, 'app/.env'))), + ).toEqual(Buffer.from('OPAQUE-SECRET\0VALUE\n')); + }), +); -void test('preserves a failed sandbox and never launches AI', async () => { - const fixture = await makeFixture(); - const result = runWorkflow(fixture, [featureSlug], { FAIL_PREPARATION: 'true' }); - assert.equal(result.code, 1); - assert.match(result.stdout, /locki exec --match sandbox-42/u); - assert.match(result.stdout, /locki rm --match sandbox-42/u); - const log = await readFile(fixture.logPath, 'utf-8'); - assert.equal(log.includes('locki ai'), false); -}); +it.live('preserves a failed sandbox and never launches AI', () => + Effect.gen(function* testEffect10() { + const fixture = yield* makeFixture(); + const result = runWorkflow(fixture, [featureSlug], { FAIL_PREPARATION: 'true' }); + expect(result.code).toBe(1); + expect(result.stdout).toMatch(/locki exec --match sandbox-42/u); + expect(result.stdout).toMatch(/locki rm --match sandbox-42/u); + const log = yield* Effect.tryPromise(() => readFile(fixture.logPath, 'utf-8')); + expect(log.includes('locki ai')).toBe(false); + }), +); -void test('launches the configured AI only after successful preparation', async () => { - const fixture = await makeFixture(); - const result = runWorkflow(fixture, [featureSlug]); - assert.equal(result.code, 0, result.stderr); - const log = await readFile(fixture.logPath, 'utf-8'); - assert.ok( - log.indexOf('mise exec -- pnpm db:verify') < log.indexOf('locki ai --match sandbox-42'), - ); -}); +it.live('launches the configured AI only after successful preparation', () => + Effect.gen(function* testEffect11() { + const fixture = yield* makeFixture(); + const result = runWorkflow(fixture, [featureSlug]); + expect(result.code, result.stderr).toBe(0); + const log = yield* Effect.tryPromise(() => readFile(fixture.logPath, 'utf-8')); + expect( + log.indexOf('mise exec -- pnpm db:verify') < log.indexOf('locki ai --match sandbox-42'), + ).toBe(true); + }), +); diff --git a/app/scripts/tests/migrate-contacts-authorization.test.mts b/app/scripts/tests/migrate-contacts-authorization.test.mts index 204d881d0..5101447f6 100644 --- a/app/scripts/tests/migrate-contacts-authorization.test.mts +++ b/app/scripts/tests/migrate-contacts-authorization.test.mts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { planContactsAuthorizationContext } from '../migrate-contacts-authorization.mts'; import type { ContactsAuthorizationRelationship } from '../migrate-contacts-authorization.mts'; @@ -12,36 +11,33 @@ const verifyMode = 'verify'; const finalizeMode = 'finalize'; const alreadyPreparedState = 'already_prepared'; -await test('prepare creates Contacts relationships from a legacy-only context', () => { - assert.deepEqual(planContactsAuthorizationContext(prepareMode, legacyRelationships, []), { +it('prepare creates Contacts relationships from a legacy-only context', () => { + expect(planContactsAuthorizationContext(prepareMode, legacyRelationships, [])).toEqual({ deleteLegacy: false, state: 'legacy_only', touchContacts: true, }); }); -await test('prepare and verify accept an exactly prepared context', () => { +it('prepare and verify accept an exactly prepared context', () => { const reordered = [legacyRelationships[1], legacyRelationships[0]] as const; - assert.equal( - planContactsAuthorizationContext(prepareMode, legacyRelationships, reordered).state, + expect(planContactsAuthorizationContext(prepareMode, legacyRelationships, reordered).state).toBe( alreadyPreparedState, ); - assert.equal( - planContactsAuthorizationContext(verifyMode, legacyRelationships, reordered).state, + expect(planContactsAuthorizationContext(verifyMode, legacyRelationships, reordered).state).toBe( alreadyPreparedState, ); }); -await test('finalize removes only an exactly matched legacy context', () => { - assert.deepEqual( +it('finalize removes only an exactly matched legacy context', () => { + expect( planContactsAuthorizationContext(finalizeMode, legacyRelationships, legacyRelationships), - { deleteLegacy: true, state: alreadyPreparedState, touchContacts: false }, - ); + ).toEqual({ deleteLegacy: true, state: alreadyPreparedState, touchContacts: false }); }); -await test('all modes are idempotent after legacy relationships are gone', () => { +it('all modes are idempotent after legacy relationships are gone', () => { for (const mode of [prepareMode, verifyMode, finalizeMode] as const) { - assert.deepEqual(planContactsAuthorizationContext(mode, [], legacyRelationships), { + expect(planContactsAuthorizationContext(mode, [], legacyRelationships)).toEqual({ deleteLegacy: false, state: 'already_finalized', touchContacts: false, @@ -49,20 +45,18 @@ await test('all modes are idempotent after legacy relationships are gone', () => } }); -await test('verify and finalize fail closed when Contacts relationships are missing', () => { +it('verify and finalize fail closed when Contacts relationships are missing', () => { for (const mode of [verifyMode, finalizeMode] as const) { - assert.throws( - () => planContactsAuthorizationContext(mode, legacyRelationships, []), + expect(() => planContactsAuthorizationContext(mode, legacyRelationships, [])).toThrow( /Contacts authorization is missing/u, ); } }); -await test('every mode rejects partial or divergent relationship sets', () => { +it('every mode rejects partial or divergent relationship sets', () => { const partial = legacyRelationships.slice(0, 1); for (const mode of [prepareMode, verifyMode, finalizeMode] as const) { - assert.throws( - () => planContactsAuthorizationContext(mode, legacyRelationships, partial), + expect(() => planContactsAuthorizationContext(mode, legacyRelationships, partial)).toThrow( /relationships differ/u, ); } diff --git a/app/scripts/tests/module-entrypoint-boundaries.test.mts b/app/scripts/tests/module-entrypoint-boundaries.test.mts index 252f10353..98b4b09d4 100644 --- a/app/scripts/tests/module-entrypoint-boundaries.test.mts +++ b/app/scripts/tests/module-entrypoint-boundaries.test.mts @@ -1,12 +1,19 @@ -import { hasGeneratedGovernedServerContract } from '../generated-governed-http-boundary.mts'; -import assert from 'node:assert/strict'; +import { expect, it } from 'effect-rstest'; + +import { existsSync } from 'node:fs'; + import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; + import os from 'node:os'; + import path from 'node:path'; -import nodeTest from 'node:test'; + import { NodeServices } from '@effect/platform-node'; -import { ManagedRuntime, Schema } from 'effect'; + +import { Cause, Effect, Exit, Schema } from 'effect'; + import { checkModuleEntrypointBoundaries as checkModuleEntrypointBoundariesEffect } from '../check-module-entrypoint-boundaries.mts'; + import { assertPublishedCrossMicroVerticalContractUsage, assertPublishedOutboxContractSource, @@ -16,61 +23,97 @@ import { resolvePublishedContractModuleId, } from '../published-outbox-contracts.mts'; -const boundaryCheckRuntime = ManagedRuntime.make(NodeServices.layer); -const checkModuleEntrypointBoundaries = async (root: string): Promise => - await boundaryCheckRuntime.runPromise(checkModuleEntrypointBoundariesEffect(root)); -const test = (name: string, body: () => Promise | void): void => { - void nodeTest(name, body); -}; +import { hasGeneratedGovernedServerContract } from '../generated-governed-http-boundary.mts'; -nodeTest.after(async () => { - await boundaryCheckRuntime.dispose(); -}); +const EXPECTED_EFFECT_FAILURE = 'Expected the Effect to fail'; + +const checkModuleEntrypointBoundaries = (root: string) => + checkModuleEntrypointBoundariesEffect(root).pipe(Effect.provide(NodeServices.layer)); const ARCHIVE_ORGANIZATION_DECLARATION = 'export const archiveOrganizationEngagementAction'; + const PAYLOAD_SCHEMA_PROPERTY = 'payloadSchema:'; + const READ_ACCESS_SOURCE = "access: 'read'"; + const PUBLIC_AUTHORIZATION_SOURCE = "kind: 'public'"; + const AUTHENTICATE_PRINCIPAL_BINDING = 'authenticatePrincipal: authenticateOperationPrincipal'; + const STOCK_LIST_STEM = 'stock-list'; + const GATEWAY_CONTRACT_FILE = 'packages/shared-contracts/src/gateway-context.ts'; + const STOCK_LIST_READ_BINDING = 'registration: stockListRead'; + const STOCK_LIST_READ_FILE = 'verticals/inventory-stock/src/api/stock-list.read.ts'; + const ACTION_HEADER_FOR_TEST = '// @generated by OntOS Codesmith Action v1'; + const GATEWAY_BYPASS_MARKER = 'operationGateway.bypass'; + const GATEWAY_INVOKE_MARKER = 'operationGateway.invoke'; + const GATEWAY_IMPORT = "import { operationGateway } from './action-gateway.ts';"; + const CLIENT_CONFIG_DECLARATION = ' makeGovernedEffectBffClient('; + const AUTHORIZATION_VALUE = ' credential,'; + const AUTHORIZATION_VALUE_TAIL = " credential: Redacted.make('Bearer bypass'),"; + const CORRELATION_VALUE = ' requestCorrelation,'; + const CORRELATION_VALUE_TAIL = " requestCorrelation: requestCorrelation || 'wrong',"; + const DEFAULT_API_PREFIX = "defaultApiPrefix: '/inventory-stock-api'"; + const DEFAULT_API_PREFIX_TAIL = "defaultApiPrefix: '/inventory-stock-api' + '/wrong'"; + const SHARED_GATEWAY_FACTORY = 'makeSharedOperationGateway(ACTION_GATEWAY_AUDIENCE, acquire)'; + const SCHEMA_VERSION_ONE = "schemaVersion: '1'"; + const MODULE_API_KIND = 'module-api'; + const MODULE_API_HEADER = '// @generated by OntOS Codesmith module-api v1\n'; + const MANIFEST_SEARCH_END = ' // '; + const STOCK_LIST_API_BINDING = '.addHttpApi(StockListApi)'; + const ACTION_FILE = 'verticals/inventory-stock/src/actions/reserve.action.ts'; + const WORKER_FILE = 'verticals/inventory-stock/src/workers/project.worker.ts'; + const PARTY_API_CLIENT_EXPORT = './api/client'; + const PARTY_API_CLIENT_SPECIFIER = '@app/party-registry/api/client'; + const PARTY_CLIENT_SOURCE_PATH = './src/api/party-registry-client.ts'; + const PARTY_DEPLOYMENT_ID = 'party-registry'; + const PARTY_MANIFEST_PATH = './vertical.manifest.ts'; + const PARTY_MODULE_ID = 'party.registry'; + const PARTY_OUTBOX_SPECIFIER = '@app/party-registry/outbox/party-created'; + const PARTY_PACKAGE_NAME = '@app/party-registry'; + const INVENTORY_VERTICAL_PATH = 'verticals/inventory-stock'; + const INVENTORY_REGISTRATION_PATH = `${INVENTORY_VERTICAL_PATH}/vertical.registration.ts`; + const INVENTORY_RUNTIME_ID = 'inventory-runtime'; + const validAction = `${ACTION_HEADER_FOR_TEST} // @ontos-action-owner inventory.stock // @ontos-action-slug reserve const entrypoint = defineTenantModuleEntrypoint({ access: 'write', authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, entrypointKey: 'inventory.stock.reserve', moduleKey: 'inventory.stock', role: 'action' }); `; + const validWorker = `// @generated by OntOS Codesmith Outbox Worker v1 // @ontos-outbox-worker-key inventory.stock.project // @ontos-outbox-worker-owner inventory.stock @@ -79,11 +122,12 @@ const validWorker = `// @generated by OntOS Codesmith Outbox Worker v1 const entrypoint = defineTenantModuleEntrypoint({ access: 'background', authorization: { kind: 'owner_local_background' }, entrypointKey: 'inventory.stock.project', moduleKey: 'inventory.stock', role: 'worker' }); `; -const write = async (root: string, file: string, source: string): Promise => { - const target = path.join(root, file); - await mkdir(path.dirname(target), { recursive: true }); - await writeFile(target, source, 'utf-8'); -}; +const write = (root: string, file: string, source: string) => + Effect.gen(function* testEffect1() { + const target = path.join(root, file); + yield* Effect.tryPromise(() => mkdir(path.dirname(target), { recursive: true })); + yield* Effect.tryPromise(() => writeFile(target, source, 'utf-8')); + }); const governedClientFixture = (options: { readonly apiValue: string; @@ -219,7 +263,7 @@ const moduleReadFixture = ( export const ${camel}Entrypoint = defineTenantModuleEntrypoint({ access: 'read', authorization: { kind: 'context_permission', permission: 'module.access' }, entrypointKey: 'inventory.stock.api.${name}', moduleKey: 'inventory.stock', role: 'api' }); export const ${camel}Read = defineRead({ accessKind: 'list', entrypoint: ${camel}Entrypoint, inputSchema: ${type}RequestSchema, resultSchema: ${type}ResponseSchema, legalEntityScope: 'optional', owningModuleKey: 'inventory.stock', permissionTarget: 'tenant', policies: [], readKey: 'inventory.stock.api.${name}', schemaVersion: '1' }, () => Effect.succeed({}), () => Layer.empty, () => Effect.succeed({}));`; -test('governed servers bind the trusted handler, authentication, registration, and problem schemas', () => { +it('governed servers bind the trusted handler, authentication, registration, and problem schemas', () => { const exportedName = 'stockListReadApiLive'; const source = governedServerFixture({ apiValue: 'StockListApi', @@ -238,9 +282,9 @@ test('governed servers bind the trusted handler, authentication, registration, a // .pipe(identity);`, ); - assert.equal(accepts(source), true); - assert.equal(accepts(source.replace(MODULE_API_HEADER, '')), false); - assert.equal(accepts(source.replace(' problems,', ' problems: problems,')), true); + expect(accepts(source)).toBe(true); + expect(accepts(source.replace(MODULE_API_HEADER, ''))).toBe(false); + expect(accepts(source.replace(' problems,', ' problems: problems,'))).toBe(true); for (const [expected, replacement] of [ [AUTHENTICATE_PRINCIPAL_BINDING, 'authenticatePrincipal: forgedPrincipal'], [STOCK_LIST_READ_BINDING, 'registration: otherRead'], @@ -263,38 +307,43 @@ test('governed servers bind the trusted handler, authentication, registration, a ['authentication: StockListAuthenticationProblemSchema', 'authentication: OtherProblemSchema'], [' problems,', ' problems, ...overrides,'], ] as const) { - assert.equal(accepts(source.replace(expected, replacement)), false, expected); + expect(accepts(source.replace(expected, replacement)), expected).toBe(false); } - assert.equal( + expect( accepts( `${source.replace( 'makeGovernedReadHttpHandler({', 'unusedHelper({', )}\nconst unused = makeGovernedReadHttpHandler({});`, ), - false, - ); + ).toBe(false); }); -const assertRejectedSources = async ( +const assertRejectedSources = Effect.fn(function* rejectGovernedSources( root: string, file: string, sources: readonly string[], expected: RegExp, - index = 0, -): Promise => { - const [source, ...remaining] = sources; - if (source === undefined) { - return; +) { + for (const [index, source] of sources.entries()) { + yield* write(root, file, source); + expect( + String(yield* Effect.flip(checkModuleEntrypointBoundaries(root))), + `invalid source ${index}`, + ).toMatch(expected); } - await write(root, file, source); - await assert.rejects(checkModuleEntrypointBoundaries(root), expected, `invalid source ${index}`); - await assertRejectedSources(root, file, remaining, expected, index + 1); -}; +}); -const makeFixture = async (includeParty = false): Promise => { - const root = await mkdtemp(path.join(os.tmpdir(), 'ontos-module-entrypoints-')); - await write( +const makeFixture = Effect.fn(function* mergedScenario1( + includeParty = false, + initialize: (root: string) => Effect.Effect = () => Effect.void, +) { + const root = yield* Effect.acquireRelease( + Effect.promise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-module-entrypoints-'))), + (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), + ); + yield* initialize(root); + yield* write( root, '.modernjs/ultramodern.json', JSON.stringify({ @@ -302,12 +351,14 @@ const makeFixture = async (includeParty = false): Promise => { apps: [ { id: 'shell-super-app', path: 'apps/shell-super-app' }, { id: 'inventory-stock', path: INVENTORY_VERTICAL_PATH }, - ...(includeParty ? [{ id: PARTY_DEPLOYMENT_ID, path: 'verticals/party-registry' }] : []), + ...(includeParty === true + ? [{ id: PARTY_DEPLOYMENT_ID, path: 'verticals/party-registry' }] + : []), ], }, }), ); - await write( + yield* write( root, 'apps/shell-super-app/src/routes/home/route.meta.ts', ` @@ -315,25 +366,25 @@ import { defineSystemModuleEntrypoint } from '@app/core-runtime'; export const routeMeta = { ownerAppId: 'shell-super-app', entrypoint: defineSystemModuleEntrypoint({ access: 'read', authorization: { kind: 'public' }, entrypointKey: 'shell-super-app.page.home', moduleKey: 'shell-super-app', role: 'page' }) }; `, ); - await write( + yield* write( root, 'apps/shell-super-app/src/routes/ultramodern-route-metadata.ts', `export const routes = [{ entrypoint: { entrypointKey: 'shell-super-app.page.home' } }];`, ); - await write( + yield* write( root, 'apps/shell-super-app/shared/api.ts', `import { GatewayContextApiGroup } from '@app/shared-contracts'; export const ShellAuthenticationApi = HttpApi.make('shell').add(GatewayContextApiGroup); export const paths = ['/shell-super-app-api/auth/gateway-context', '/shell-super-app-api/auth/api-key/gateway-context'];`, ); - await write( + yield* write( root, 'apps/shell-super-app/api/index.ts', `export const issueGatewayContext = true; export const issueApiKeyGatewayContext = true;`, ); - await write( + yield* write( root, 'verticals/inventory-stock/src/routes/orders/route.meta.ts', ` @@ -341,14 +392,14 @@ import { defineTenantModuleEntrypoint } from '@app/core-runtime'; export const routeMeta = { moduleId: 'inventory.stock', ownerAppId: 'inventory-stock', entrypoint: defineTenantModuleEntrypoint({ access: 'read', authorization: { kind: 'context_permission', permission: 'module.access' }, entrypointKey: 'inventory.stock.page.orders', moduleKey: 'inventory.stock', role: 'page' }) }; `, ); - await write( + yield* write( root, 'verticals/inventory-stock/src/routes/ultramodern-route-metadata.ts', `export const routes = [{ entrypoint: { entrypointKey: 'inventory.stock.page.orders' } }];`, ); - await write(root, ACTION_FILE, validAction); - await write(root, WORKER_FILE, validWorker); - await write( + yield* write(root, ACTION_FILE, validAction); + yield* write(root, WORKER_FILE, validWorker); + yield* write( root, GATEWAY_CONTRACT_FILE, `export const GatewayContextApiGroup = HttpApiGroup.make('gatewayContext') @@ -356,41 +407,48 @@ export const routeMeta = { moduleId: 'inventory.stock', ownerAppId: 'inventory-s .add(HttpApiEndpoint.post('issueApiKeyGatewayContext', '/auth/api-key/gateway-context', {})); export const shellGatewayContextContract = { issueGatewayContextPath: '/shell-super-app-api/auth/gateway-context', issueApiKeyGatewayContextPath: '/shell-super-app-api/auth/api-key/gateway-context' };`, ); - await write(root, 'packages/core-runtime/src/index.ts', `export const core = true;`); + yield* write(root, 'packages/core-runtime/src/index.ts', `export const core = true;`); return root; -}; - -test('accepts governed generated Actions, pages, Workers, catalogs, and route manifests', async () => { - const root = await makeFixture(); - try { - await checkModuleEntrypointBoundaries(root); - } finally { - await rm(root, { force: true, recursive: true }); - } }); +it.live('accepts governed generated Actions, pages, Workers, catalogs, and route manifests', () => + Effect.gen(function* testEffect3() { + const root = yield* makeFixture(); + yield* checkModuleEntrypointBoundaries(root); + }), +); + const ENGAGEMENT_ACTION_DIRECTORY = 'verticals/party-registry/src/actions'; + const ENGAGEMENT_REGISTRATION_FILE = `${ENGAGEMENT_ACTION_DIRECTORY}/engagement-lifecycle-registration.ts`; + const ENGAGEMENT_ACTIONS = [ 'archive-organization-engagement', 'archive-person-engagement', 'unarchive-organization-engagement', 'unarchive-person-engagement', ] as const; + const ENGAGEMENT_ACTION_FILE = `${ENGAGEMENT_ACTION_DIRECTORY}/${ENGAGEMENT_ACTIONS[0]}.action.ts`; + const ENGAGEMENT_REJECTION = /governed action\/write entrypoint|action_execution authorization/u; -const writeEngagementLifecycleFixture = async (root: string): Promise => { - await Promise.all( +const writeEngagementLifecycleFixture = Effect.fn(function* mergedScenario2(root: string) { + yield* Effect.all( [ ENGAGEMENT_REGISTRATION_FILE, ...ENGAGEMENT_ACTIONS.map((action) => `${ENGAGEMENT_ACTION_DIRECTORY}/${action}.action.ts`), - ].map(async (file) => { - const source = await readFile(path.resolve(import.meta.dirname, '../..', file), 'utf-8'); - await write(root, file, source); - }), + ].map( + Effect.fn(function* mergedScenario1(file) { + const source = yield* Effect.promise(() => + readFile(path.resolve(import.meta.dirname, '../..', file), 'utf-8'), + ); + yield* write(root, file, source); + }), + ), + { concurrency: 'unbounded' }, ); -}; +}); const EngagementInventorySchema = Schema.fromJsonString( Schema.Struct({ @@ -409,19 +467,21 @@ const EngagementInventorySchema = Schema.fromJsonString( }), ); -test('validates all four live shared Action registrations and inventories each concrete identity', async () => { - const root = await makeFixture(true); - try { - await writeEngagementLifecycleFixture(root); - await checkModuleEntrypointBoundaries(root); +it.live( + 'validates all four live shared Action registrations and inventories each concrete identity', + Effect.fn(function* mergedScenario1() { + const root = yield* makeFixture(true); + yield* writeEngagementLifecycleFixture(root); + yield* checkModuleEntrypointBoundaries(root); const inventory = Schema.decodeUnknownSync(EngagementInventorySchema)( - await readFile( - path.join(root, '.codex/reports/authorization/protected-entrypoints.json'), - 'utf-8', + yield* Effect.promise(() => + readFile( + path.join(root, '.codex/reports/authorization/protected-entrypoints.json'), + 'utf-8', + ), ), ); - assert.deepEqual( - inventory.entries.filter((entry) => entry.owner === PARTY_MODULE_ID), + expect(inventory.entries.filter((entry) => entry.owner === PARTY_MODULE_ID)).toEqual( ENGAGEMENT_ACTIONS.map((action) => ({ authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, deployment: PARTY_DEPLOYMENT_ID, @@ -430,16 +490,17 @@ test('validates all four live shared Action registrations and inventories each c surface: 'action', })), ); - } finally { - await rm(root, { force: true, recursive: true }); - } -}); + }), +); -test('rejects shared Action identity, schema, import, shadowing, decoy and override tampering', async () => { - const root = await makeFixture(true); - try { - await writeEngagementLifecycleFixture(root); - const source = await readFile(path.join(root, ENGAGEMENT_ACTION_FILE), 'utf-8'); +it.live( + 'rejects shared Action identity, schema, import, shadowing, decoy and override tampering', + Effect.fn(function* mergedScenario1() { + const root = yield* makeFixture(true); + yield* writeEngagementLifecycleFixture(root); + const source = yield* Effect.promise(() => + readFile(path.join(root, ENGAGEMENT_ACTION_FILE), 'utf-8'), + ); const mutations = [ [ "'./engagement-lifecycle-registration.ts'", @@ -490,7 +551,7 @@ test('rejects shared Action identity, schema, import, shadowing, decoy and overr 'resultSchema: OrganizationEngagementProfileSchema, authorization: { kind: "public" }', ], ] as const; - await assertRejectedSources( + yield* assertRejectedSources( root, ENGAGEMENT_ACTION_FILE, [ @@ -500,16 +561,17 @@ const decoy = defineTenantModuleEntrypoint({ access: 'write', authorization: { k ], ENGAGEMENT_REJECTION, ); - } finally { - await rm(root, { force: true, recursive: true }); - } -}); + }), +); -test('rejects altered shared Action helper governance and constructor provenance', async () => { - const root = await makeFixture(true); - try { - await writeEngagementLifecycleFixture(root); - const source = await readFile(path.join(root, ENGAGEMENT_REGISTRATION_FILE), 'utf-8'); +it.live( + 'rejects altered shared Action helper governance and constructor provenance', + Effect.fn(function* mergedScenario1() { + const root = yield* makeFixture(true); + yield* writeEngagementLifecycleFixture(root); + const source = yield* Effect.promise(() => + readFile(path.join(root, ENGAGEMENT_REGISTRATION_FILE), 'utf-8'), + ); const mutations = [ ["'@app/core-runtime'", "'@app/counterfeit-runtime'"], ['defineTenantModuleEntrypoint', 'defineSystemModuleEntrypoint'], @@ -534,39 +596,53 @@ test('rejects altered shared Action helper governance and constructor provenance 'const defineActionResourcePermission = () => undefined; export const engagementLifecycleRegistration', ], ] as const; - await assertRejectedSources( + yield* assertRejectedSources( root, ENGAGEMENT_REGISTRATION_FILE, mutations.map(([before, after]) => source.replace(before, after)), ENGAGEMENT_REJECTION, ); - await write(root, ENGAGEMENT_REGISTRATION_FILE, ''); - await assert.rejects(checkModuleEntrypointBoundaries(root), ENGAGEMENT_REJECTION); - } finally { - await rm(root, { force: true, recursive: true }); - } -}); + yield* write(root, ENGAGEMENT_REGISTRATION_FILE, ''); + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(ENGAGEMENT_REJECTION), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + }), +); -test('rejects shared Action registrations relocated outside their exact owner and action file', async () => { - const root = await makeFixture(true); - try { - await writeEngagementLifecycleFixture(root); - const source = await readFile(path.join(root, ENGAGEMENT_ACTION_FILE), 'utf-8'); +it.live( + 'rejects shared Action registrations relocated outside their exact owner and action file', + Effect.fn(function* mergedScenario1() { + const root = yield* makeFixture(true); + yield* writeEngagementLifecycleFixture(root); + const source = yield* Effect.promise(() => + readFile(path.join(root, ENGAGEMENT_ACTION_FILE), 'utf-8'), + ); const wrongFile = `${ENGAGEMENT_ACTION_DIRECTORY}/archive-other-engagement.action.ts`; - await write(root, wrongFile, source); - await assert.rejects(checkModuleEntrypointBoundaries(root), ENGAGEMENT_REJECTION); - await rm(path.join(root, wrongFile)); - await write(root, ACTION_FILE, source); - await assert.rejects(checkModuleEntrypointBoundaries(root), ENGAGEMENT_REJECTION); - } finally { - await rm(root, { force: true, recursive: true }); - } -}); + yield* write(root, wrongFile, source); + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(ENGAGEMENT_REJECTION), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* Effect.promise(() => rm(path.join(root, wrongFile))); + yield* write(root, ACTION_FILE, source); + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(ENGAGEMENT_REJECTION), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + }), +); -const writeGovernedModuleApi = async (root: string): Promise => { +const writeGovernedModuleApi = Effect.fn(function* mergedScenario1(root: string) { const vertical = 'verticals/inventory-stock'; const header = MODULE_API_HEADER; - await write( + yield* write( root, `${vertical}/shared/api.ts`, `// @@ -580,17 +656,17 @@ export const api = HttpApi.make('InventoryApi') ; `, ); - await write( + yield* write( root, `${vertical}/shared/apis/stock-list.ts`, moduleContractFixture(STOCK_LIST_STEM, 'StockList', 'stockList'), ); - await write( + yield* write( root, `${vertical}/src/api/stock-list.read.ts`, moduleReadFixture(STOCK_LIST_STEM, 'StockList', 'stockList'), ); - await write( + yield* write( root, `${vertical}/src/api/stock-list-client.ts`, governedClientFixture({ @@ -604,7 +680,7 @@ export const api = HttpApi.make('InventoryApi') publicOperation: 'executeStockList', }), ); - await write( + yield* write( root, `${vertical}/api/stock-list-read-server.ts`, governedServerFixture({ @@ -616,7 +692,7 @@ export const api = HttpApi.make('InventoryApi') readValue: 'stockListRead', }), ); - await write( + yield* write( root, `${vertical}/api/index.ts`, `import { assembleEffectBffRuntime } from '@app/shared-contracts/server/effect-bff-runtime'; @@ -631,7 +707,7 @@ const handlersLive = Layer.mergeAll( ); export default assembleEffectBffRuntime({ api, handlers: handlersLive });`, ); - await write( + yield* write( root, `${vertical}/api/auth/action-principal.ts`, `// @generated by OntOS Codesmith MicroVertical Action Boundary v1 @@ -640,7 +716,7 @@ import { makeMicroverticalHttpPrincipalAuthentication } from '@app/core-runtime/ const verifyOperationPrincipal = true; export const authenticateOperationPrincipal = makeMicroverticalHttpPrincipalAuthentication(verifyOperationPrincipal);`, ); - await write( + yield* write( root, `${vertical}/src/api/action-gateway.ts`, `// @generated by OntOS Codesmith MicroVertical Action Boundary v1 @@ -653,7 +729,7 @@ export const makeOperationGateway = (acquire: OperationGatewayIssuer = issueGate makeSharedOperationGateway(ACTION_GATEWAY_AUDIENCE, acquire); export const operationGateway = makeOperationGateway();`, ); - await write( + yield* write( root, `${vertical}/vertical.manifest.ts`, `// @ontos-module-id inventory.stock @@ -686,7 +762,7 @@ export const manifest = { }, };`, ); - await write( + yield* write( root, INVENTORY_REGISTRATION_PATH, `export const registration = { entrypoints: { @@ -711,13 +787,14 @@ export const manifest = { // // `, ); -}; +}); for (const suffix of ['search', 'report']) { - test(`accepts module API names ending in ${suffix} without reclassifying them`, async () => { - const root = await makeFixture(); - try { - await writeGovernedModuleApi(root); + it.live( + `accepts module API names ending in ${suffix} without reclassifying them`, + Effect.fn(function* governanceScenario24() { + const root = yield* makeFixture(); + yield* writeGovernedModuleApi(root); const files = [ 'shared/api.ts', 'api/index.ts', @@ -728,38 +805,41 @@ for (const suffix of ['search', 'report']) { 'vertical.manifest.ts', 'vertical.registration.ts', ]; - await Promise.all( - files.map(async (file) => { - const original = `${INVENTORY_VERTICAL_PATH}/${file}`; - const source = await readFile(path.join(root, original), 'utf-8'); - const renamed = original.replaceAll(STOCK_LIST_STEM, `stock-${suffix}`); - await write( - root, - renamed, - source - .replaceAll(STOCK_LIST_STEM, `stock-${suffix}`) - .replaceAll('StockList', suffix === 'search' ? 'StockSearch' : 'StockReport') - .replaceAll('stockList', suffix === 'search' ? 'stockSearch' : 'stockReport'), - ); - if (renamed !== original) { - await rm(path.join(root, original)); - } - }), + yield* Effect.all( + files.map( + Effect.fn(function* governanceScenario25(file) { + const original = `${INVENTORY_VERTICAL_PATH}/${file}`; + const source = yield* Effect.promise(() => + readFile(path.join(root, original), 'utf-8'), + ); + const renamed = original.replaceAll(STOCK_LIST_STEM, `stock-${suffix}`); + yield* write( + root, + renamed, + source + .replaceAll(STOCK_LIST_STEM, `stock-${suffix}`) + .replaceAll('StockList', suffix === 'search' ? 'StockSearch' : 'StockReport') + .replaceAll('stockList', suffix === 'search' ? 'stockSearch' : 'stockReport'), + ); + if (renamed !== original) { + yield* Effect.promise(() => rm(path.join(root, original))); + } + }), + ), + { concurrency: 'unbounded' }, ); - await checkModuleEntrypointBoundaries(root); - } finally { - await rm(root, { force: true, recursive: true }); - } - }); + yield* checkModuleEntrypointBoundaries(root); + }), + ); } -test('accepts only a complete generated governed module API seam', async () => { - const root = await makeFixture(); - try { - await writeGovernedModuleApi(root); - await checkModuleEntrypointBoundaries(root); - - await write( +it.live( + 'accepts only a complete generated governed module API seam', + Effect.fn(function* mergedScenario1() { + const root = yield* makeFixture(); + yield* writeGovernedModuleApi(root); + yield* checkModuleEntrypointBoundaries(root); + yield* write( root, STOCK_LIST_READ_FILE, moduleReadFixture(STOCK_LIST_STEM, 'StockList', 'stockList').replace( @@ -767,26 +847,34 @@ test('accepts only a complete generated governed module API seam', async () => { "access: 'historical_read'", ), ); - await checkModuleEntrypointBoundaries(root); - + yield* checkModuleEntrypointBoundaries(root); const sharedApiPath = 'verticals/inventory-stock/shared/api.ts'; const manifestPath = 'verticals/inventory-stock/vertical.manifest.ts'; const registrationPath = INVENTORY_REGISTRATION_PATH; - const sharedApi = await readFile(path.join(root, sharedApiPath), 'utf-8'); - const manifest = await readFile(path.join(root, manifestPath), 'utf-8'); - const registration = await readFile(path.join(root, registrationPath), 'utf-8'); - await write( + const sharedApi = yield* Effect.promise(() => + readFile(path.join(root, sharedApiPath), 'utf-8'), + ); + const manifest = yield* Effect.promise(() => readFile(path.join(root, manifestPath), 'utf-8')); + const registration = yield* Effect.promise(() => + readFile(path.join(root, registrationPath), 'utf-8'), + ); + yield* write( root, sharedApiPath, `${sharedApi.replace(STOCK_LIST_API_BINDING, '')} const decoyApi = HttpApi.make('DecoyApi').addHttpApi(StockListApi);`, ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, - ); - await write(root, sharedApiPath, sharedApi); - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, sharedApiPath, sharedApi); + yield* write( root, sharedApiPath, sharedApi.replace( @@ -794,24 +882,36 @@ const decoyApi = HttpApi.make('DecoyApi').addHttpApi(StockListApi);`, `${STOCK_LIST_API_BINDING}${STOCK_LIST_API_BINDING}`, ), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, - ); - await write(root, sharedApiPath, sharedApi); - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, sharedApiPath, sharedApi); + yield* write( root, sharedApiPath, `${sharedApi}\nexport const decoyApi = HttpApi.make('DecoyApi').addHttpApi(StockListApi);`, ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, - ); - await write(root, sharedApiPath, sharedApi); + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, sharedApiPath, sharedApi); const handlerRootPath = 'verticals/inventory-stock/api/index.ts'; - const handlerRoot = await readFile(path.join(root, handlerRootPath), 'utf-8'); - await write( + const handlerRoot = yield* Effect.promise(() => + readFile(path.join(root, handlerRootPath), 'utf-8'), + ); + yield* write( root, handlerRootPath, handlerRoot @@ -826,7 +926,7 @@ const decoyApi = HttpApi.make('DecoyApi').addHttpApi(StockListApi);`, ); const secondContractPath = 'verticals/inventory-stock/shared/apis/missing-detail.ts'; const moduleApiHeader = MODULE_API_HEADER; - await write( + yield* write( root, sharedApiPath, sharedApi @@ -836,7 +936,7 @@ const decoyApi = HttpApi.make('DecoyApi').addHttpApi(StockListApi);`, ) .replace(STOCK_LIST_API_BINDING, `${STOCK_LIST_API_BINDING}.addHttpApi(MissingDetailApi)`), ); - await write( + yield* write( root, manifestPath, manifest @@ -849,7 +949,7 @@ const decoyApi = HttpApi.make('DecoyApi').addHttpApi(StockListApi);`, " 'missing-detail': MissingDetailApi,\n 'stock-list': StockListApi,", ), ); - await write( + yield* write( root, registrationPath, registration.replace( @@ -857,17 +957,17 @@ const decoyApi = HttpApi.make('DecoyApi').addHttpApi(StockListApi);`, " 'missing-detail': () => import('./src/api/missing-detail-client.ts'),\n 'stock-list': () => import('./src/api/stock-list-client.ts'),", ), ); - await write( + yield* write( root, secondContractPath, moduleContractFixture('missing-detail', 'MissingDetail', 'missingDetail'), ); - await write( + yield* write( root, 'verticals/inventory-stock/src/api/missing-detail.read.ts', moduleReadFixture('missing-detail', 'MissingDetail', 'missingDetail'), ); - await write( + yield* write( root, 'verticals/inventory-stock/src/api/missing-detail-client.ts', governedClientFixture({ @@ -881,7 +981,7 @@ const decoyApi = HttpApi.make('DecoyApi').addHttpApi(StockListApi);`, publicOperation: 'executeMissingDetail', }), ); - await write( + yield* write( root, 'verticals/inventory-stock/api/missing-detail-read-server.ts', governedServerFixture({ @@ -893,33 +993,47 @@ const decoyApi = HttpApi.make('DecoyApi').addHttpApi(StockListApi);`, readValue: 'missingDetailRead', }), ); - await checkModuleEntrypointBoundaries(root); - await rm(path.join(root, secondContractPath)); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, + yield* checkModuleEntrypointBoundaries(root); + yield* Effect.promise(() => rm(path.join(root, secondContractPath))); + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, sharedApiPath, sharedApi); + yield* write(root, manifestPath, manifest); + yield* write(root, registrationPath, registration); + yield* Effect.promise(() => + rm(path.join(root, 'verticals/inventory-stock/src/api/missing-detail.read.ts')), ); - await write(root, sharedApiPath, sharedApi); - await write(root, manifestPath, manifest); - await write(root, registrationPath, registration); - await rm(path.join(root, 'verticals/inventory-stock/src/api/missing-detail.read.ts')); - await rm(path.join(root, 'verticals/inventory-stock/src/api/missing-detail-client.ts')); - await rm(path.join(root, 'verticals/inventory-stock/api/missing-detail-read-server.ts')); - await write(root, handlerRootPath, handlerRoot); - - await write( + yield* Effect.promise(() => + rm(path.join(root, 'verticals/inventory-stock/src/api/missing-detail-client.ts')), + ); + yield* Effect.promise(() => + rm(path.join(root, 'verticals/inventory-stock/api/missing-detail-read-server.ts')), + ); + yield* write(root, handlerRootPath, handlerRoot); + yield* write( root, manifestPath, `${manifest.replace("'stock-list': StockListApi,", "'stock-list': OtherApi,")} const decoy = { 'stock-list': StockListApi };`, ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, - ); - await write(root, manifestPath, manifest); - - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, manifestPath, manifest); + yield* write( root, registrationPath, `${registration.replace( @@ -928,13 +1042,17 @@ const decoy = { 'stock-list': StockListApi };`, )} const decoy = { 'stock-list': () => import('./src/api/stock-list-client.ts') };`, ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, - ); - await write(root, registrationPath, registration); - - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, registrationPath, registration); + yield* write( root, registrationPath, registration.replace( @@ -943,13 +1061,17 @@ const decoy = { 'stock-list': () => import('./src/api/stock-list-client.ts') };` // `, ), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, - ); - await write(root, registrationPath, registration); - - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, registrationPath, registration); + yield* write( root, registrationPath, registration @@ -959,15 +1081,19 @@ const decoy = { 'stock-list': () => import('./src/api/stock-list-client.ts') };` " 'stock-list': () => import('./src/api/stock-list-client.ts'),\n // ", ), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, - ); - await write(root, registrationPath, registration); - + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, registrationPath, registration); const gatewayPath = 'verticals/inventory-stock/src/api/action-gateway.ts'; - const gateway = await readFile(path.join(root, gatewayPath), 'utf-8'); - await assertRejectedSources( + const gateway = yield* Effect.promise(() => readFile(path.join(root, gatewayPath), 'utf-8')); + yield* assertRejectedSources( root, gatewayPath, [ @@ -995,9 +1121,8 @@ Object.assign(operationGateway, { ], /module APIs require an approved Codesmith generator/u, ); - await write(root, gatewayPath, gateway); - - await write( + yield* write(root, gatewayPath, gateway); + yield* write( root, registrationPath, registration.replace( @@ -1006,13 +1131,17 @@ Object.assign(operationGateway, { // `, ), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, - ); - await write(root, registrationPath, registration); - - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, registrationPath, registration); + yield* write( root, INVENTORY_REGISTRATION_PATH, `export const registration = { entrypoints: { api: { @@ -1033,29 +1162,32 @@ Object.assign(operationGateway, { // // `, ); - await checkModuleEntrypointBoundaries(root); - - await write( + yield* checkModuleEntrypointBoundaries(root); + yield* write( root, 'verticals/inventory-stock/api/stock-list-read-server.ts', `${MODULE_API_HEADER}export const server = true;`, ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, - ); - } finally { - await rm(root, { force: true, recursive: true }); - } -}); + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + }), +); -test('rejects generated governed clients that bypass the shared client runtime seam', async () => { - const root = await makeFixture(); - try { - await writeGovernedModuleApi(root); +it.live( + 'rejects generated governed clients that bypass the shared client runtime seam', + Effect.fn(function* mergedScenario4() { + const root = yield* makeFixture(); + yield* writeGovernedModuleApi(root); const readPath = STOCK_LIST_READ_FILE; - const validRead = await readFile(path.join(root, readPath), 'utf-8'); - await assertRejectedSources( + const validRead = yield* Effect.promise(() => readFile(path.join(root, readPath), 'utf-8')); + yield* assertRejectedSources( root, readPath, [ @@ -1072,9 +1204,9 @@ test('rejects generated governed clients that bypass the shared client runtime s ], /module APIs require an approved Codesmith generator/u, ); - await write(root, readPath, validRead); + yield* write(root, readPath, validRead); const clientPath = 'verticals/inventory-stock/src/api/stock-list-client.ts'; - const validClient = await readFile(path.join(root, clientPath), 'utf-8'); + const validClient = yield* Effect.promise(() => readFile(path.join(root, clientPath), 'utf-8')); const deadCanonicalHelperClient = validClient .replace( 'const stockListClient = (', @@ -1087,11 +1219,8 @@ const stockListClient = (`, 'stockListClient(Redacted.make(credential), requestCorrelation, options).pipe(', 'bypassClient(Redacted.make(credential), requestCorrelation, options).pipe(', ); - assert.match(deadCanonicalHelperClient, /bypassClient\(Redacted\.make\(credential\)/u); - assert.doesNotMatch( - deadCanonicalHelperClient, - /stockListClient\(Redacted\.make\(credential\)/u, - ); + expect(deadCanonicalHelperClient).toMatch(/bypassClient\(Redacted\.make\(credential\)/u); + expect(deadCanonicalHelperClient).not.toMatch(/stockListClient\(Redacted\.make\(credential\)/u); const nestedCanonicalHelperClient = validClient .replace( ' options: StockListClientOptions,\n) =>', @@ -1107,7 +1236,7 @@ const stockListClient = (`, }; export const executeStockListWithAuthorization`, ); - assert.match(nestedCanonicalHelperClient, /const deadCanonicalClient = \(\) => \{/u); + expect(nestedCanonicalHelperClient).toMatch(/const deadCanonicalClient = \(\) => \{/u); const decoyOperationClient = `${validClient .replaceAll('executeStockListWithAuthorization', 'decoyWithAuthorization') .replaceAll('executeStockList', 'decoy')} @@ -1215,15 +1344,17 @@ const spoof = 'operationGateway.invoke transportHeaders: authorization: x-correl `${validClient.replace(CORRELATION_VALUE, ' transportMetadata: requestCorrelation,')} // operationGateway.invoke transportHeaders: authorization: 'x-correlation-id':`, ]; - await assertRejectedSources( + yield* assertRejectedSources( root, clientPath, invalidClients, /module APIs require an approved Codesmith generator/u, ); const contractPath = 'verticals/inventory-stock/shared/apis/stock-list.ts'; - const validContract = await readFile(path.join(root, contractPath), 'utf-8'); - await assertRejectedSources( + const validContract = yield* Effect.promise(() => + readFile(path.join(root, contractPath), 'utf-8'), + ); + yield* assertRejectedSources( root, contractPath, [ @@ -1240,9 +1371,8 @@ const spoof = 'operationGateway.invoke transportHeaders: authorization: x-correl ], /module APIs require an approved Codesmith generator/u, ); - await write(root, contractPath, validContract); - - await write( + yield* write(root, contractPath, validContract); + yield* write( root, contractPath, validContract.replace( @@ -1255,31 +1385,41 @@ const spoof = 'operationGateway.invoke transportHeaders: authorization: x-correl .add(HttpApiGroup.make('stockList').add(HttpApiEndpoint.post('execute', '/reads/stock-list', {})))`, ), ); - await write( + yield* write( root, clientPath, validClient.replace('client.stockList.execute', 'client.decoy.execute'), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, - ); - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write( root, contractPath, `${validContract.replace(".add(HttpApiGroup.make('stockList').add(HttpApiEndpoint.post('execute', '/reads/stock-list', {})))", '')} DecoyApi.add(HttpApiGroup.make('decoy'));`, ); - await write( + yield* write( root, clientPath, validClient.replace('client.stockList.execute', 'client.decoy.execute'), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, - ); - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write( root, contractPath, `namespace Decoy { @@ -1287,13 +1427,18 @@ DecoyApi.add(HttpApiGroup.make('decoy'));`, } ${validContract}`, ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, - ); - await write(root, contractPath, validContract); - await write(root, clientPath, validClient); - await assertRejectedSources( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, contractPath, validContract); + yield* write(root, clientPath, validClient); + yield* assertRejectedSources( root, contractPath, [ @@ -1305,24 +1450,34 @@ ${validContract}`, ], /module APIs require an approved Codesmith generator/u, ); - await write( + yield* write( root, contractPath, validContract.replace("'/reads/stock-list'", "'/wrong-endpoint'"), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, - ); - await write(root, contractPath, validContract); - await write(root, clientPath, validClient); - await write(root, contractPath, validContract.replace('})));', '}))).pipe(() => DecoyApi);')); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, - ); - await write(root, contractPath, validContract); - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, contractPath, validContract); + yield* write(root, clientPath, validClient); + yield* write(root, contractPath, validContract.replace('})));', '}))).pipe(() => DecoyApi);')); + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, contractPath, validContract); + yield* write( root, contractPath, validContract.replace( @@ -1330,17 +1485,21 @@ ${validContract}`, "HttpApiGroup.make('stockList') ? DecoyGroup : OwnerGroup", ), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /module APIs require an approved Codesmith generator/u, - ); - await write(root, contractPath, validContract); - - // Provider enforcement has its own descriptor checks; stop exercising the synthetic - // inline module endpoint while independently mutating search and report artifacts. + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /module APIs require an approved Codesmith generator/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, contractPath, validContract); const sharedRootPath = `${INVENTORY_VERTICAL_PATH}/shared/api.ts`; - const sharedRoot = await readFile(path.join(root, sharedRootPath), 'utf-8'); - await write( + const sharedRoot = yield* Effect.promise(() => + readFile(path.join(root, sharedRootPath), 'utf-8'), + ); + yield* write( root, sharedRootPath, sharedRoot.replace( @@ -1364,15 +1523,15 @@ ${validContract}`, invocationKind: 'provider', publicOperation: 'loadInventoryItemsClient', }); - await write(root, providerContractPath, validProviderContract); - await write(root, providerClientPath, providerClient); - await write( + yield* write(root, providerContractPath, validProviderContract); + yield* write(root, providerClientPath, providerClient); + yield* write( root, 'verticals/inventory-stock/src/search/inventory-items.provider.ts', `${providerHeader}export const inventoryItemsEntrypoint = defineTenantModuleEntrypoint({ access: 'read', authorization: { kind: 'context_permission', permission: 'module.access' }, entrypointKey: 'inventory.stock.search.inventory-items', moduleKey: 'inventory.stock', role: 'search' }); export const inventoryItemsRead = defineRead({ accessKind: 'search', entrypoint: inventoryItemsEntrypoint, legalEntityScope: 'required', owningModuleKey: 'inventory.stock', readKey: 'inventory.stock.search.inventory-items', schemaVersion: '1' });`, ); - await write( + yield* write( root, 'verticals/inventory-stock/api/inventory-items-search-server.ts', governedServerFixture({ @@ -1386,9 +1545,13 @@ export const inventoryItemsRead = defineRead({ accessKind: 'search', entrypoint: ); const providerManifestPath = 'verticals/inventory-stock/vertical.manifest.ts'; const providerRegistrationPath = INVENTORY_REGISTRATION_PATH; - const providerManifest = await readFile(path.join(root, providerManifestPath), 'utf-8'); - const providerRegistration = await readFile(path.join(root, providerRegistrationPath), 'utf-8'); - await write( + const providerManifest = yield* Effect.promise(() => + readFile(path.join(root, providerManifestPath), 'utf-8'), + ); + const providerRegistration = yield* Effect.promise(() => + readFile(path.join(root, providerRegistrationPath), 'utf-8'), + ); + yield* write( root, providerManifestPath, providerManifest @@ -1403,7 +1566,7 @@ export const inventoryItemsRead = defineRead({ accessKind: 'search', entrypoint: // `, ), ); - await write( + yield* write( root, providerRegistrationPath, providerRegistration.replace( @@ -1412,8 +1575,8 @@ export const inventoryItemsRead = defineRead({ accessKind: 'search', entrypoint: // `, ), ); - await checkModuleEntrypointBoundaries(root); - await write( + yield* checkModuleEntrypointBoundaries(root); + yield* write( root, providerContractPath, validProviderContract.replace( @@ -1421,23 +1584,35 @@ export const inventoryItemsRead = defineRead({ accessKind: 'search', entrypoint: "HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint.post('execute', '/inventory.stock/search/inventory-items', {})).add(HttpApiEndpoint.post('bypass', '/bypass', {}))", ), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /generated search and report clients require the shared client runtime/u, - ); - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /generated search and report clients require the shared client runtime/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write( root, providerContractPath, `${validProviderContract.replace('/inventory.stock/search/inventory-items', '/wrong-endpoint')} const decoyGroup = HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint.post('execute', '/inventory.stock/search/inventory-items', {}));`, ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /generated search and report clients require the shared client runtime/u, + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /generated search and report clients require the shared client runtime/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, providerContractPath, validProviderContract); + const validProviderManifest = yield* Effect.promise(() => + readFile(path.join(root, providerManifestPath), 'utf-8'), ); - await write(root, providerContractPath, validProviderContract); - const validProviderManifest = await readFile(path.join(root, providerManifestPath), 'utf-8'); - await write( + yield* write( root, providerManifestPath, validProviderManifest @@ -1448,12 +1623,17 @@ const decoyGroup = HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint // `, ), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /generated search and report clients require the shared client runtime/u, - ); - await write(root, providerManifestPath, validProviderManifest); - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /generated search and report clients require the shared client runtime/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, providerManifestPath, validProviderManifest); + yield* write( root, providerManifestPath, validProviderManifest.replace( @@ -1461,12 +1641,17 @@ const decoyGroup = HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint "entrypointKey: 'inventory.stock.search.wrong'", ), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /generated search and report clients require the shared client runtime/u, - ); - await write(root, providerManifestPath, validProviderManifest); - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /generated search and report clients require the shared client runtime/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, providerManifestPath, validProviderManifest); + yield* write( root, providerManifestPath, validProviderManifest.replace( @@ -1475,12 +1660,17 @@ const decoyGroup = HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint // `, ), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /generated search and report clients require the shared client runtime/u, - ); - await write(root, providerManifestPath, validProviderManifest); - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /generated search and report clients require the shared client runtime/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, providerManifestPath, validProviderManifest); + yield* write( root, providerManifestPath, validProviderManifest.replace( @@ -1488,15 +1678,21 @@ const decoyGroup = HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint "{ decoy: { owningModuleId: 'inventory.stock' }, key: 'inventory.stock.inventory-items', owningModuleId: 'wrong.owner' }", ), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /generated search and report clients require the shared client runtime/u, - ); - await write(root, providerManifestPath, validProviderManifest); - + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /generated search and report clients require the shared client runtime/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, providerManifestPath, validProviderManifest); const providerServerPath = 'verticals/inventory-stock/api/inventory-items-search-server.ts'; - const validProviderServer = await readFile(path.join(root, providerServerPath), 'utf-8'); - await write( + const validProviderServer = yield* Effect.promise(() => + readFile(path.join(root, providerServerPath), 'utf-8'), + ); + yield* write( root, providerServerPath, `${validProviderServer.replace( @@ -1504,12 +1700,17 @@ const decoyGroup = HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint 'authenticatePrincipal: unverifiedPrincipal', )}\nconst unverifiedPrincipal = authenticateOperationPrincipal;`, ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /generated search and report clients require the shared client runtime/u, - ); - await write(root, providerServerPath, validProviderServer); - await assertRejectedSources( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /generated search and report clients require the shared client runtime/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, providerServerPath, validProviderServer); + yield* assertRejectedSources( root, providerServerPath, [ @@ -1548,19 +1749,22 @@ const decoyGroup = HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint ], /generated search and report clients require the shared client runtime/u, ); - await write(root, providerServerPath, validProviderServer); - - await rm(path.join(root, providerClientPath)); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /generated search and report clients require the shared client runtime/u, - ); - await write(root, providerClientPath, providerClient); - const validProviderRegistration = await readFile( - path.join(root, providerRegistrationPath), - 'utf-8', + yield* write(root, providerServerPath, validProviderServer); + yield* Effect.promise(() => rm(path.join(root, providerClientPath))); + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /generated search and report clients require the shared client runtime/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, providerClientPath, providerClient); + const validProviderRegistration = yield* Effect.promise(() => + readFile(path.join(root, providerRegistrationPath), 'utf-8'), ); - await write( + yield* write( root, providerRegistrationPath, validProviderRegistration.replace( @@ -1568,12 +1772,17 @@ const decoyGroup = HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint './src/api/other-search-client.ts', ), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /generated search and report clients require the shared client runtime/u, - ); - await write(root, providerRegistrationPath, validProviderRegistration); - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /generated search and report clients require the shared client runtime/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, providerRegistrationPath, validProviderRegistration); + yield* write( root, providerRegistrationPath, validProviderRegistration.replace( @@ -1581,12 +1790,17 @@ const decoyGroup = HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint "'decoy': () => ({ 'inventory-items': () => import('./src/api/inventory-items-search-client.ts') }),", ), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /generated search and report clients require the shared client runtime/u, - ); - await write(root, providerRegistrationPath, validProviderRegistration); - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /generated search and report clients require the shared client runtime/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, providerRegistrationPath, validProviderRegistration); + yield* write( root, providerRegistrationPath, validProviderRegistration @@ -1600,21 +1814,32 @@ const decoyGroup = HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint // `, ), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /generated search and report clients require the shared client runtime/u, - ); - await write(root, providerRegistrationPath, validProviderRegistration); - - await rm(path.join(root, providerContractPath)); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /generated search and report clients require the shared client runtime/u, - ); - await write(root, providerContractPath, validProviderContract); + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /generated search and report clients require the shared client runtime/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, providerRegistrationPath, validProviderRegistration); + yield* Effect.promise(() => rm(path.join(root, providerContractPath))); + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /generated search and report clients require the shared client runtime/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, providerContractPath, validProviderContract); const providerSourcePath = 'verticals/inventory-stock/src/search/inventory-items.provider.ts'; - const validProviderSource = await readFile(path.join(root, providerSourcePath), 'utf-8'); - await write( + const validProviderSource = yield* Effect.promise(() => + readFile(path.join(root, providerSourcePath), 'utf-8'), + ); + yield* write( root, providerSourcePath, validProviderSource.replace( @@ -1622,8 +1847,8 @@ const decoyGroup = HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint 'const inventoryItemsEntrypoint', ), ); - await checkModuleEntrypointBoundaries(root); - await assertRejectedSources( + yield* checkModuleEntrypointBoundaries(root); + yield* assertRejectedSources( root, providerSourcePath, [ @@ -1643,8 +1868,8 @@ const decoyGroup = HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint ], /generated search and report clients require the shared client runtime/u, ); - await write(root, providerSourcePath, validProviderSource); - await write( + yield* write(root, providerSourcePath, validProviderSource); + yield* write( root, providerSourcePath, validProviderSource.replace( @@ -1652,37 +1877,53 @@ const decoyGroup = HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint "schemaVersion: '1', ...{ owningModuleKey: 'attacker.module', readKey: 'attacker.read' }", ), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /generated search and report clients require the shared client runtime/u, - ); - await write(root, providerSourcePath, validProviderSource); - await write( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /generated search and report clients require the shared client runtime/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, providerSourcePath, validProviderSource); + yield* write( root, providerSourcePath, validProviderSource.replace(READ_ACCESS_SOURCE, "access: 'historical_read'"), ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /generated search and report clients require the shared client runtime/u, - ); - await write(root, providerSourcePath, validProviderSource); - - await Promise.all( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /generated search and report clients require the shared client runtime/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, providerSourcePath, validProviderSource); + yield* Effect.all( [providerContractPath, providerClientPath, providerSourcePath, providerServerPath].map( - async (artifact) => await rm(path.join(root, artifact)), + Effect.fn(function* mergedScenario1(artifact) { + return yield* Effect.promise(() => rm(path.join(root, artifact))); + }), ), + { concurrency: 'unbounded' }, ); - await assert.rejects( - checkModuleEntrypointBoundaries(root), - /generated search and report clients require the shared client runtime/u, - ); - await write(root, providerContractPath, validProviderContract); - await write(root, providerClientPath, providerClient); - await write(root, providerSourcePath, validProviderSource); - await write(root, providerServerPath, validProviderServer); - - await assertRejectedSources( + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + /generated search and report clients require the shared client runtime/u, + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, providerContractPath, validProviderContract); + yield* write(root, providerClientPath, providerClient); + yield* write(root, providerSourcePath, validProviderSource); + yield* write(root, providerServerPath, validProviderServer); + yield* assertRejectedSources( root, providerClientPath, [ @@ -1709,8 +1950,7 @@ const decoyGroup = HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint ], /generated search and report clients require the shared client runtime/u, ); - await write(root, providerClientPath, providerClient); - + yield* write(root, providerClientPath, providerClient); const reportHeader = `// @generated by OntOS Codesmith Governed Contribution v1 // @ontos-contribution-kind report `; @@ -1725,19 +1965,19 @@ const decoyGroup = HttpApiGroup.make('inventoryItemsSearch').add(HttpApiEndpoint invocationKind: 'provider', publicOperation: 'loadStockLevelsClient', }); - await write( + yield* write( root, 'verticals/inventory-stock/shared/apis/stock-levels-report.ts', `${reportHeader}export const StockLevelsReportApi = HttpApi.make('StockLevelsReportApi').add(HttpApiGroup.make('stockLevelsReport').add(HttpApiEndpoint.post('execute', '/inventory.stock/reports/stock-levels', {})));`, ); - await write(root, reportClientPath, reportClient); - await write( + yield* write(root, reportClientPath, reportClient); + yield* write( root, 'verticals/inventory-stock/src/reports/stock-levels.provider.ts', `${reportHeader}export const stockLevelsEntrypoint = defineTenantModuleEntrypoint({ access: 'read', authorization: { kind: 'authenticated_principal' }, entrypointKey: 'inventory.stock.report.stock-levels', moduleKey: 'inventory.stock', role: 'report' }); export const stockLevelsRead = defineRead({ accessKind: 'report', entrypoint: stockLevelsEntrypoint, legalEntityScope: 'required', owningModuleKey: 'inventory.stock', readKey: 'inventory.stock.report.stock-levels', schemaVersion: '1' });`, ); - await write( + yield* write( root, 'verticals/inventory-stock/api/stock-levels-report-server.ts', governedServerFixture({ @@ -1749,9 +1989,13 @@ export const stockLevelsRead = defineRead({ accessKind: 'report', entrypoint: st readValue: 'stockLevelsRead', }), ); - const reportManifest = await readFile(path.join(root, providerManifestPath), 'utf-8'); - const reportRegistration = await readFile(path.join(root, providerRegistrationPath), 'utf-8'); - await write( + const reportManifest = yield* Effect.promise(() => + readFile(path.join(root, providerManifestPath), 'utf-8'), + ); + const reportRegistration = yield* Effect.promise(() => + readFile(path.join(root, providerRegistrationPath), 'utf-8'), + ); + yield* write( root, providerManifestPath, reportManifest @@ -1766,7 +2010,7 @@ export const stockLevelsRead = defineRead({ accessKind: 'report', entrypoint: st // `, ), ); - await write( + yield* write( root, providerRegistrationPath, reportRegistration.replace( @@ -1775,8 +2019,8 @@ export const stockLevelsRead = defineRead({ accessKind: 'report', entrypoint: st // `, ), ); - await checkModuleEntrypointBoundaries(root); - await assertRejectedSources( + yield* checkModuleEntrypointBoundaries(root); + yield* assertRejectedSources( root, reportClientPath, [ @@ -1791,12 +2035,12 @@ export const stockLevelsRead = defineRead({ accessKind: 'report', entrypoint: st ], /generated search and report clients require the shared client runtime/u, ); - await write( + yield* write( root, `${INVENTORY_VERTICAL_PATH}/package.json`, JSON.stringify({ modernjs: { appId: INVENTORY_RUNTIME_ID } }), ); - await write( + yield* write( root, '.modernjs/ultramodern.json', JSON.stringify({ @@ -1809,13 +2053,15 @@ export const stockLevelsRead = defineRead({ accessKind: 'report', entrypoint: st }), ); const routeMetadataPath = 'verticals/inventory-stock/src/routes/orders/route.meta.ts'; - const routeMetadata = await readFile(path.join(root, routeMetadataPath), 'utf-8'); - await write( + const routeMetadata = yield* Effect.promise(() => + readFile(path.join(root, routeMetadataPath), 'utf-8'), + ); + yield* write( root, routeMetadataPath, routeMetadata.replace('inventory-stock', INVENTORY_RUNTIME_ID), ); - await Promise.all( + yield* Effect.all( ( [ [clientPath, validClient], @@ -1823,43 +2069,48 @@ export const stockLevelsRead = defineRead({ accessKind: 'report', entrypoint: st [reportClientPath, reportClient], ] as const ).map( - async ([relativePath, client]) => - await write( + Effect.fn(function* mergedScenario2([relativePath, client]) { + return yield* write( root, relativePath, client.replace( "defaultApiPrefix: '/inventory-stock-api'", `defaultApiPrefix: '/${INVENTORY_RUNTIME_ID}-api'`, ), - ), + ); + }), ), + { concurrency: 'unbounded' }, ); - await Promise.all( + yield* Effect.all( [ 'verticals/inventory-stock/api/auth/action-principal.ts', 'verticals/inventory-stock/src/api/action-gateway.ts', - ].map(async (relativePath) => { - const boundary = await readFile(path.join(root, relativePath), 'utf-8'); - await write( - root, - relativePath, - boundary - .replace( - '@ontos-action-boundary-owner inventory-stock', - `@ontos-action-boundary-owner ${INVENTORY_RUNTIME_ID}`, - ) - .replace( - "ACTION_GATEWAY_AUDIENCE = 'inventory-stock'", - `ACTION_GATEWAY_AUDIENCE = '${INVENTORY_RUNTIME_ID}'`, - ), - ); - }), + ].map( + Effect.fn(function* mergedScenario3(relativePath) { + const boundary = yield* Effect.promise(() => + readFile(path.join(root, relativePath), 'utf-8'), + ); + yield* write( + root, + relativePath, + boundary + .replace( + '@ontos-action-boundary-owner inventory-stock', + `@ontos-action-boundary-owner ${INVENTORY_RUNTIME_ID}`, + ) + .replace( + "ACTION_GATEWAY_AUDIENCE = 'inventory-stock'", + `ACTION_GATEWAY_AUDIENCE = '${INVENTORY_RUNTIME_ID}'`, + ), + ); + }), + ), + { concurrency: 'unbounded' }, ); - await checkModuleEntrypointBoundaries(root); - } finally { - await rm(root, { force: true, recursive: true }); - } -}); + yield* checkModuleEntrypointBoundaries(root); + }), +); const violations = [ { @@ -1920,29 +2171,28 @@ const violations = [ ] as const; for (const violation of violations) { - test(`rejects bypass ${violation.file}`, async () => { - const root = await makeFixture(); - try { - await write(root, violation.file, violation.source); - await assert.rejects(checkModuleEntrypointBoundaries(root), (error: Error) => { - assert.match(error.message, violation.expected); - assert.match( - error.message, - new RegExp(violation.file.replaceAll('.', String.raw`\.`), 'u'), - ); - assert.doesNotMatch(error.message, /Widget =|endpoint =|registration =/u); - return true; - }); - } finally { - await rm(root, { force: true, recursive: true }); - } - }); + it.live(`rejects bypass ${violation.file}`, () => + Effect.gen(function* testEffect6() { + const root = yield* makeFixture(); + yield* write(root, violation.file, violation.source); + expect( + ((error: Error) => { + expect(error.message).toMatch(violation.expected); + expect(error.message).toMatch( + new RegExp(violation.file.replaceAll('.', String.raw`\.`), 'u'), + ); + expect(error.message).not.toMatch(/Widget =|endpoint =|registration =/u); + return true; + })(yield* Effect.flip(checkModuleEntrypointBoundaries(root))), + ).toBe(true); + }), + ); } -test('rejects missing headers, spoofed metadata, and stale generated descriptors', async () => { - const root = await makeFixture(); - try { - await write( +it.live('rejects missing headers, spoofed metadata, and stale generated descriptors', () => + Effect.gen(function* testEffect7() { + const root = yield* makeFixture(); + yield* write( root, ACTION_FILE, `${ACTION_HEADER_FOR_TEST} @@ -1951,10 +2201,14 @@ test('rejects missing headers, spoofed metadata, and stale generated descriptors export const stale = true; `, ); - await assert.rejects(checkModuleEntrypointBoundaries(root), /scaffold:action/u); - await write(root, ACTION_FILE, `export const ignored = true;`); - await assert.rejects(checkModuleEntrypointBoundaries(root), /scaffold:action/u); - await write( + expect(String(yield* Effect.flip(checkModuleEntrypointBoundaries(root)))).toMatch( + /scaffold:action/u, + ); + yield* write(root, ACTION_FILE, `export const ignored = true;`); + expect(String(yield* Effect.flip(checkModuleEntrypointBoundaries(root)))).toMatch( + /scaffold:action/u, + ); + yield* write( root, ACTION_FILE, `${ACTION_HEADER_FOR_TEST} @@ -1964,9 +2218,11 @@ export const stale = true; export const spoofed = true; `, ); - await assert.rejects(checkModuleEntrypointBoundaries(root), /scaffold:action/u); - await write(root, ACTION_FILE, validAction); - await write( + expect(String(yield* Effect.flip(checkModuleEntrypointBoundaries(root)))).toMatch( + /scaffold:action/u, + ); + yield* write(root, ACTION_FILE, validAction); + yield* write( root, WORKER_FILE, `// @generated by OntOS Codesmith Outbox Worker v1 @@ -1975,13 +2231,15 @@ export const spoofed = true; export const stale = true; `, ); - await assert.rejects(checkModuleEntrypointBoundaries(root), /scaffold:outbox-worker/u); - await write(root, WORKER_FILE, `export const ignored = true;`); - await assert.rejects(checkModuleEntrypointBoundaries(root), /scaffold:outbox-worker/u); - } finally { - await rm(root, { force: true, recursive: true }); - } -}); + expect(String(yield* Effect.flip(checkModuleEntrypointBoundaries(root)))).toMatch( + /scaffold:outbox-worker/u, + ); + yield* write(root, WORKER_FILE, `export const ignored = true;`); + expect(String(yield* Effect.flip(checkModuleEntrypointBoundaries(root)))).toMatch( + /scaffold:outbox-worker/u, + ); + }), +); const partyResourceSource = `// @generated by OntOS Codesmith Resource v1 // @ontos-resource-owner party.registry @@ -2058,125 +2316,110 @@ const assertPublishedPartyUsage = ( ...overrides, }); -test('accepts an exact generated PartyRef contract import', () => { - assert.doesNotThrow(() => assertPublishedPartyUsage()); - assert.deepEqual(publishedResourceRefContractExports(partyPackage), ['./resources/party']); +it('accepts an exact generated PartyRef contract import', () => { + expect(() => assertPublishedPartyUsage()).not.toThrow(); + expect(publishedResourceRefContractExports(partyPackage)).toEqual(['./resources/party']); }); -test('rejects a ResourceRef contract with a spoofed Codesmith header', () => { - assert.throws( - () => - assertPublishedPartyUsage({ - readExportSource: () => - partyResourceSource.replace('OntOS Codesmith Resource v1', 'handmade'), - }), - /must remain a generated schema-only ResourceRef contract/u, - ); +it('rejects a ResourceRef contract with a spoofed Codesmith header', () => { + expect(() => + assertPublishedPartyUsage({ + readExportSource: () => + partyResourceSource.replace('OntOS Codesmith Resource v1', 'handmade'), + }), + ).toThrow(/must remain a generated schema-only ResourceRef contract/u); }); -test('rejects a ResourceRef export whose target is not its exact generated shared path', () => { - assert.throws( - () => - assertPublishedPartyUsage({ - dependencyPackageJson: { - ...partyPackage, - exports: { './resources/party': './shared/domain/party.ts' }, - }, - }), - /not a published schema-only contract subpath/u, - ); +it('rejects a ResourceRef export whose target is not its exact generated shared path', () => { + expect(() => + assertPublishedPartyUsage({ + dependencyPackageJson: { + ...partyPackage, + exports: { './resources/party': './shared/domain/party.ts' }, + }, + }), + ).toThrow(/not a published schema-only contract subpath/u); }); -test('rejects a ResourceRef import whose public path is not resources/', () => { - assert.throws( - () => - assertPublishedPartyUsage({ - dependencyPackageJson: { - ...partyPackage, - exports: { './resource/party': './shared/resources/party.ts' }, - }, - moduleSpecifiers: ['@app/party-registry/resource/party'], - }), - /not a published schema-only contract subpath/u, - ); +it('rejects a ResourceRef import whose public path is not resources/', () => { + expect(() => + assertPublishedPartyUsage({ + dependencyPackageJson: { + ...partyPackage, + exports: { './resource/party': './shared/resources/party.ts' }, + }, + moduleSpecifiers: ['@app/party-registry/resource/party'], + }), + ).toThrow(/not a published schema-only contract subpath/u); }); -test('rejects owner runtime imports hidden in a generated ResourceRef file', () => { - assert.throws( - () => - assertPublishedPartyUsage({ - readExportSource: () => - `${partyResourceSource}import { repository } from '../../src/db/repository.ts';\nvoid repository;\n`, - }), - /must remain a generated schema-only ResourceRef contract/u, - ); +it('rejects owner runtime imports hidden in a generated ResourceRef file', () => { + expect(() => + assertPublishedPartyUsage({ + readExportSource: () => + `${partyResourceSource}import { repository } from '../../src/db/repository.ts';\nvoid repository;\n`, + }), + ).toThrow(/must remain a generated schema-only ResourceRef contract/u); }); -test('rejects a ResourceRef import without the consuming workspace dependency', () => { - assert.throws( - () => assertPublishedPartyUsage({ dependencyDeclared: false }), +it('rejects a ResourceRef import without the consuming workspace dependency', () => { + expect(() => assertPublishedPartyUsage({ dependencyDeclared: false })).toThrow( /must declare @app\/party-registry as a workspace dependency/u, ); }); -test('rejects a ResourceRef import without the consuming TypeScript project reference', () => { - assert.throws( - () => assertPublishedPartyUsage({ projectReferenceDeclared: false }), +it('rejects a ResourceRef import without the consuming TypeScript project reference', () => { + expect(() => assertPublishedPartyUsage({ projectReferenceDeclared: false })).toThrow( /must project-reference @app\/party-registry/u, ); }); -test('rejects barrels and arbitrary exported owner subpaths', () => { +it('rejects barrels and arbitrary exported owner subpaths', () => { for (const specifier of [ PARTY_PACKAGE_NAME, '@app/party-registry/shared/domain/party', '@app/party-registry/api', '@app/party-registry/actions/create-party', ]) { - assert.throws( - () => assertPublishedPartyUsage({ moduleSpecifiers: [specifier] }), + expect(() => assertPublishedPartyUsage({ moduleSpecifiers: [specifier] })).toThrow( /not a published schema-only contract subpath/u, ); } }); -test('preserves existing exact Outbox contract dependency behavior', () => { - assert.deepEqual(publishedOutboxContractExports(partyPackage), ['./outbox/party-created']); - assert.doesNotThrow(() => +it('preserves existing exact Outbox contract dependency behavior', () => { + expect(publishedOutboxContractExports(partyPackage)).toEqual(['./outbox/party-created']); + expect(() => assertPublishedOutboxDependencyUsage({ dependencyPackageJson: partyPackage, dependencyPackageName: PARTY_PACKAGE_NAME, moduleSpecifiers: [PARTY_OUTBOX_SPECIFIER], }), - ); - assert.throws( - () => - assertPublishedOutboxDependencyUsage({ - dependencyPackageJson: partyPackage, - dependencyPackageName: PARTY_PACKAGE_NAME, - moduleSpecifiers: ['@app/party-registry/resources/party'], - }), - /not a published schema-only Outbox contract subpath/u, - ); - assert.doesNotThrow(() => + ).not.toThrow(); + expect(() => + assertPublishedOutboxDependencyUsage({ + dependencyPackageJson: partyPackage, + dependencyPackageName: PARTY_PACKAGE_NAME, + moduleSpecifiers: ['@app/party-registry/resources/party'], + }), + ).toThrow(/not a published schema-only Outbox contract subpath/u); + expect(() => assertPublishedPartyUsage({ moduleSpecifiers: [PARTY_OUTBOX_SPECIFIER], readExportSource: () => { throw new Error('Outbox source validation remains at its existing boundary'); }, }), - ); - assert.throws( - () => - assertPublishedPartyUsage({ - moduleSpecifiers: ['@app/party-registry/outbox/not-published'], - }), - /not a published schema-only contract subpath/u, - ); + ).not.toThrow(); + expect(() => + assertPublishedPartyUsage({ + moduleSpecifiers: ['@app/party-registry/outbox/not-published'], + }), + ).toThrow(/not a published schema-only contract subpath/u); }); -test('accepts an exact published Party Registry Effect client aggregate', () => { - assert.doesNotThrow(() => +it('accepts an exact published Party Registry Effect client aggregate', () => { + expect(() => assertPublishedPartyUsage({ moduleSpecifiers: [PARTY_API_CLIENT_SPECIFIER], readExportSource: (target) => { @@ -2189,62 +2432,56 @@ test('accepts an exact published Party Registry Effect client aggregate', () => throw new Error(`unexpected provider source ${target}`); }, }), - ); + ).not.toThrow(); }); -test('rejects a published client export that points at backend implementation', () => { - assert.throws( - () => - assertPublishedPartyUsage({ - dependencyPackageJson: { - ...partyPackage, - exports: { ...partyPackage.exports, [PARTY_API_CLIENT_EXPORT]: './api/index.ts' }, - }, - moduleSpecifiers: [PARTY_API_CLIENT_SPECIFIER], - }), - /not a published schema-only contract subpath/u, - ); +it('rejects a published client export that points at backend implementation', () => { + expect(() => + assertPublishedPartyUsage({ + dependencyPackageJson: { + ...partyPackage, + exports: { ...partyPackage.exports, [PARTY_API_CLIENT_EXPORT]: './api/index.ts' }, + }, + moduleSpecifiers: [PARTY_API_CLIENT_SPECIFIER], + }), + ).toThrow(/not a published schema-only contract subpath/u); }); -test('rejects a client aggregate that imports provider backend or private source', () => { +it('rejects a client aggregate that imports provider backend or private source', () => { for (const forbiddenImport of [ '../../api/index.ts', '../db/repository.ts', '../services/party.service.ts', '../actions/create-party.action.ts', ]) { - assert.throws( - () => - assertPublishedPartyUsage({ - moduleSpecifiers: [PARTY_API_CLIENT_SPECIFIER], - readExportSource: (target) => { - if (target === PARTY_CLIENT_SOURCE_PATH) { - return `${partyClientSource}import value from '${forbiddenImport}';\nvoid value;\n`; - } - return generatedPartyDetailClientSource; - }, - }), - /must remain a generated public Effect client aggregate/u, - ); - } -}); - -test('rejects an aggregate whose client leaf has no Codesmith metadata', () => { - assert.throws( - () => + expect(() => assertPublishedPartyUsage({ moduleSpecifiers: [PARTY_API_CLIENT_SPECIFIER], - readExportSource: (target) => - target === PARTY_CLIENT_SOURCE_PATH - ? partyClientSource - : 'export const executePartyDetail = true;\n', + readExportSource: (target) => { + if (target === PARTY_CLIENT_SOURCE_PATH) { + return `${partyClientSource}import value from '${forbiddenImport}';\nvoid value;\n`; + } + return generatedPartyDetailClientSource; + }, }), - /must remain a generated public Effect client aggregate/u, - ); + ).toThrow(/must remain a generated public Effect client aggregate/u); + } +}); + +it('rejects an aggregate whose client leaf has no Codesmith metadata', () => { + expect(() => + assertPublishedPartyUsage({ + moduleSpecifiers: [PARTY_API_CLIENT_SPECIFIER], + readExportSource: (target) => + target === PARTY_CLIENT_SOURCE_PATH + ? partyClientSource + : 'export const executePartyDetail = true;\n', + }), + ).toThrow(/must remain a generated public Effect client aggregate/u); }); -test('accepts an exact generated MicroVertical command client leaf', () => { - assert.doesNotThrow(() => +it('accepts an exact generated MicroVertical command client leaf', () => { + expect(() => assertPublishedPartyUsage({ moduleSpecifiers: [PARTY_API_CLIENT_SPECIFIER], readExportSource: (target) => { @@ -2260,32 +2497,30 @@ test('accepts an exact generated MicroVertical command client leaf', () => { throw new Error(`unexpected provider source ${target}`); }, }), - ); + ).not.toThrow(); }); -test('rejects a command client leaf whose generated owner marker names another deployment', () => { - assert.throws( - () => - assertPublishedPartyUsage({ - moduleSpecifiers: [PARTY_API_CLIENT_SPECIFIER], - readExportSource: (target) => { - if (target === PARTY_CLIENT_SOURCE_PATH) { - return `${partyClientSource}export * from './party-command-client.ts';\n`; - } - if (target === './src/api/party-command-client.ts') { - return generatedPartyCommandClientSource.replace( - '@ontos-command-client-owner party-registry', - '@ontos-command-client-owner contacts', - ); - } - return generatedPartyDetailClientSource; - }, - }), - /must remain a generated public Effect client aggregate/u, - ); +it('rejects a command client leaf whose generated owner marker names another deployment', () => { + expect(() => + assertPublishedPartyUsage({ + moduleSpecifiers: [PARTY_API_CLIENT_SPECIFIER], + readExportSource: (target) => { + if (target === PARTY_CLIENT_SOURCE_PATH) { + return `${partyClientSource}export * from './party-command-client.ts';\n`; + } + if (target === './src/api/party-command-client.ts') { + return generatedPartyCommandClientSource.replace( + '@ontos-command-client-owner party-registry', + '@ontos-command-client-owner contacts', + ); + } + return generatedPartyDetailClientSource; + }, + }), + ).toThrow(/must remain a generated public Effect client aggregate/u); }); -test('rejects API barrels and arbitrary client subpaths', () => { +it('rejects API barrels and arbitrary client subpaths', () => { for (const specifier of [ '@app/party-registry/api', '@app/party-registry/api/client/internal', @@ -2293,8 +2528,7 @@ test('rejects API barrels and arbitrary client subpaths', () => { '@app/party-registry/src/api/party-registry-client', '@app/party-registry/api/server', ]) { - assert.throws( - () => assertPublishedPartyUsage({ moduleSpecifiers: [specifier] }), + expect(() => assertPublishedPartyUsage({ moduleSpecifiers: [specifier] })).toThrow( /not a published schema-only contract subpath/u, ); } @@ -2314,75 +2548,70 @@ export const OutboxPayloadSchema = Schema.Struct({ partyId: Schema.String }); export const outboxProducerModuleKey = '${PARTY_MODULE_ID}' as const; `; -test('validates Outbox producer module identity when deployment appId differs from moduleId', () => { +it('validates Outbox producer module identity when deployment appId differs from moduleId', () => { const moduleId = resolvePublishedContractModuleId({ dependencyPackageJson: partyPackage, dependencyPackageName: PARTY_PACKAGE_NAME, expectedAppId: PARTY_DEPLOYMENT_ID, manifestSource: partyManifestSource, }); - assert.equal(moduleId, PARTY_MODULE_ID); - assert.doesNotThrow(() => + expect(moduleId).toBe(PARTY_MODULE_ID); + expect(() => assertPublishedOutboxContractSource({ moduleId, source: partyOutboxSource, specifier: PARTY_OUTBOX_SPECIFIER, }), - ); + ).not.toThrow(); }); -test('rejects an Outbox producer that substitutes deployment appId for moduleId', () => { - assert.throws( - () => - assertPublishedOutboxContractSource({ - moduleId: PARTY_MODULE_ID, - source: partyOutboxSource.replace( - '@ontos-outbox-producer party.registry', - '@ontos-outbox-producer party-registry', - ), - specifier: PARTY_OUTBOX_SPECIFIER, - }), - /must remain a generated schema-only Outbox contract/u, - ); +it('rejects an Outbox producer that substitutes deployment appId for moduleId', () => { + expect(() => + assertPublishedOutboxContractSource({ + moduleId: PARTY_MODULE_ID, + source: partyOutboxSource.replace( + '@ontos-outbox-producer party.registry', + '@ontos-outbox-producer party-registry', + ), + specifier: PARTY_OUTBOX_SPECIFIER, + }), + ).toThrow(/must remain a generated schema-only Outbox contract/u); }); -test('rejects mismatched package and generated manifest ownership for published contracts', () => { - assert.throws( - () => - resolvePublishedContractModuleId({ - dependencyPackageJson: { ...partyPackage, name: '@app/somewhere-else' }, - dependencyPackageName: PARTY_PACKAGE_NAME, - expectedAppId: PARTY_DEPLOYMENT_ID, - manifestSource: partyManifestSource, - }), - /package and generated manifest ownership disagree/u, - ); +it('rejects mismatched package and generated manifest ownership for published contracts', () => { + expect(() => + resolvePublishedContractModuleId({ + dependencyPackageJson: { ...partyPackage, name: '@app/somewhere-else' }, + dependencyPackageName: PARTY_PACKAGE_NAME, + expectedAppId: PARTY_DEPLOYMENT_ID, + manifestSource: partyManifestSource, + }), + ).toThrow(/package and generated manifest ownership disagree/u); }); -test('keeps executable owner behavior out of published Outbox contracts', () => { - assert.throws( - () => - assertPublishedOutboxContractSource({ - moduleId: PARTY_MODULE_ID, - source: `${partyOutboxSource}import { handler } from '../../src/actions/create-party.action.ts';\n`, - specifier: PARTY_OUTBOX_SPECIFIER, - }), - /must remain a generated schema-only Outbox contract/u, - ); +it('keeps executable owner behavior out of published Outbox contracts', () => { + expect(() => + assertPublishedOutboxContractSource({ + moduleId: PARTY_MODULE_ID, + source: `${partyOutboxSource}import { handler } from '../../src/actions/create-party.action.ts';\n`, + specifier: PARTY_OUTBOX_SPECIFIER, + }), + ).toThrow(/must remain a generated schema-only Outbox contract/u); }); -test('rejects missing, orphaned, and cross-owner route manifest entries', async () => { - const root = await makeFixture(); - try { - await write( + +it.live('rejects missing, orphaned, and cross-owner route manifest entries', () => + Effect.gen(function* testEffect8() { + const root = yield* makeFixture(); + yield* write( root, 'apps/shell-super-app/src/routes/ultramodern-route-metadata.ts', `export const routes = [{ entrypoint: { entrypointKey: 'inventory.stock.page.orders' } }];`, ); - await assert.rejects(checkModuleEntrypointBoundaries(root), /manifest is stale/u); - } finally { - await rm(root, { force: true, recursive: true }); - } -}); + expect(String(yield* Effect.flip(checkModuleEntrypointBoundaries(root)))).toMatch( + /manifest is stale/u, + ); + }), +); for (const [binding, disconnected] of [ [ @@ -2391,18 +2620,24 @@ for (const [binding, disconnected] of [ ], [STOCK_LIST_READ_BINDING, 'registration: unrelatedRead'], ] as const) { - test(`shared governed HTTP handler requires connected ${binding}`, async () => { - const root = await makeFixture(); - const serverFile = 'verticals/inventory-stock/api/stock-list-read-server.ts'; - try { - await writeGovernedModuleApi(root); - const server = await readFile(path.join(root, serverFile), 'utf-8'); - await checkModuleEntrypointBoundaries(root); - await write(root, serverFile, server.replace(binding, disconnected)); - await assert.rejects(checkModuleEntrypointBoundaries(root), /module APIs require/u); - await write(root, serverFile, server); - await checkModuleEntrypointBoundaries(root); - await write( + it.live( + `shared governed HTTP handler requires connected ${binding}`, + Effect.fn(function* mergedScenario1() { + const root = yield* makeFixture(); + const serverFile = 'verticals/inventory-stock/api/stock-list-read-server.ts'; + yield* writeGovernedModuleApi(root); + const server = yield* Effect.promise(() => readFile(path.join(root, serverFile), 'utf-8')); + yield* checkModuleEntrypointBoundaries(root); + yield* write(root, serverFile, server.replace(binding, disconnected)); + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(/module APIs require/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, serverFile, server); + yield* checkModuleEntrypointBoundaries(root); + yield* write( root, serverFile, server.replace( @@ -2410,54 +2645,70 @@ for (const [binding, disconnected] of [ "from './unused-neighbor.ts'", ), ); - await assert.rejects(checkModuleEntrypointBoundaries(root), /module APIs require/u); - } finally { - await rm(root, { force: true, recursive: true }); - } - }); + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(/module APIs require/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + }), + ); } -test('accepts private entrypoint declarations while requiring the registered read reference', async () => { - const root = await makeFixture(); - const readFilePath = STOCK_LIST_READ_FILE; - try { - await writeGovernedModuleApi(root); - const source = await readFile(path.join(root, readFilePath), 'utf-8'); +it.live( + 'accepts private entrypoint declarations while requiring the registered read reference', + Effect.fn(function* mergedScenario1() { + const root = yield* makeFixture(); + const readFilePath = STOCK_LIST_READ_FILE; + yield* writeGovernedModuleApi(root); + const source = yield* Effect.promise(() => readFile(path.join(root, readFilePath), 'utf-8')); const read = source.replace('export const stockListEntrypoint', 'const stockListEntrypoint'); - await write(root, readFilePath, read); - await checkModuleEntrypointBoundaries(root); - await write( + yield* write(root, readFilePath, read); + yield* checkModuleEntrypointBoundaries(root); + yield* write( root, readFilePath, read.replace('entrypoint: stockListEntrypoint', 'entrypoint: unrelatedEntrypoint'), ); - await assert.rejects(checkModuleEntrypointBoundaries(root), /module APIs require/u); - } finally { - await rm(root, { force: true, recursive: true }); - } -}); + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(/module APIs require/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + }), +); -test('shared issuer paths require the mounted gateway group and exact endpoint bindings', async () => { - const root = await makeFixture(); - const contractFile = 'apps/shell-super-app/shared/api.ts'; - const gatewayFile = GATEWAY_CONTRACT_FILE; - const source = await readFile(new URL(`../../${contractFile}`, import.meta.url), 'utf-8'); - const gatewaySource = await readFile(new URL(`../../${gatewayFile}`, import.meta.url), 'utf-8'); - try { - await write(root, contractFile, source); - await write(root, gatewayFile, gatewaySource); - await checkModuleEntrypointBoundaries(root); - await write( +it.live( + 'shared issuer paths require the mounted gateway group and exact endpoint bindings', + Effect.fn(function* mergedScenario1() { + const root = yield* makeFixture(); + const contractFile = 'apps/shell-super-app/shared/api.ts'; + const gatewayFile = GATEWAY_CONTRACT_FILE; + const source = yield* Effect.promise(() => + readFile(new URL(`../../${contractFile}`, import.meta.url), 'utf-8'), + ); + const gatewaySource = yield* Effect.promise(() => + readFile(new URL(`../../${gatewayFile}`, import.meta.url), 'utf-8'), + ); + yield* write(root, contractFile, source); + yield* write(root, gatewayFile, gatewaySource); + yield* checkModuleEntrypointBoundaries(root); + yield* write( root, contractFile, source.replace('.add(GatewayContextApiGroup)', '.add(UnrelatedApiGroup)'), ); - await assert.rejects(checkModuleEntrypointBoundaries(root), /mounted gateway contract/u); - await write(root, contractFile, source); - } finally { - await rm(root, { force: true, recursive: true }); - } -}); + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch(/mounted gateway contract/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* write(root, contractFile, source); + }), +); for (const [before, after] of [ ["post('issueGatewayContext'", "post('unusedNeighbor'"], @@ -2467,16 +2718,66 @@ for (const [before, after] of [ ["make('gatewayContext')", "make('unrelated')"], ['export const GatewayContextApiGroup =', 'export const UnmountedApiGroup ='], ] as const) { - test(`rejects shared issuer binding tampering: ${before}`, async () => { - const root = await makeFixture(); - const gatewayFile = GATEWAY_CONTRACT_FILE; - const gatewaySource = await readFile(new URL(`../../${gatewayFile}`, import.meta.url), 'utf-8'); - try { - assert.ok(gatewaySource.includes(before)); - await write(root, gatewayFile, gatewaySource.replace(before, after)); - await assert.rejects(checkModuleEntrypointBoundaries(root), /mounted gateway contract/u); - } finally { - await rm(root, { force: true, recursive: true }); - } - }); + it.live( + `rejects shared issuer binding tampering: ${before}`, + Effect.fn(function* mergedScenario1() { + const root = yield* makeFixture(); + const gatewayFile = GATEWAY_CONTRACT_FILE; + const gatewaySource = yield* Effect.promise(() => + readFile(new URL(`../../${gatewayFile}`, import.meta.url), 'utf-8'), + ); + expect(gatewaySource.includes(before)).toBeTruthy(); + yield* write(root, gatewayFile, gatewaySource.replace(before, after)); + yield* Effect.matchCause(checkModuleEntrypointBoundaries(root), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch(/mounted gateway contract/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + }), + ); +} + +const EARLY_FAILURE = 'early failure'; + +const PARTIAL_FAILURE = 'partial failure'; + +for (const outcome of ['success', EARLY_FAILURE, PARTIAL_FAILURE, 'interruption'] as const) { + it.live(`releases fixture directories after ${outcome} during initialization`, () => + Effect.gen(function* fixtureCleanupControl() { + let directory = ''; + const failure = new Error('fixture initialization failed'); + const exit = yield* Effect.scoped( + makeFixture(false, (root) => + Effect.gen(function* initializeFixtureControl() { + directory = root; + if (outcome === EARLY_FAILURE) { + yield* Effect.fail(failure); + } + yield* write(root, 'partial.txt', 'partially initialized'); + if (outcome === PARTIAL_FAILURE) { + yield* Effect.fail(failure); + } + if (outcome === 'interruption') { + yield* Effect.interrupt; + } + }), + ), + ).pipe(Effect.exit); + expect(directory).not.toBe(''); + expect(existsSync(directory)).toBe(false); + expect(Exit.isSuccess(exit)).toBe(outcome === 'success'); + expect(Exit.hasInterrupts(exit)).toBe(outcome === 'interruption'); + if (outcome === EARLY_FAILURE || outcome === PARTIAL_FAILURE) { + Exit.match(exit, { + onFailure: (cause) => { + expect(cause.reasons).toHaveLength(1); + expect(Cause.squash(cause)).toBe(failure); + }, + onSuccess: () => expect.unreachable('Expected fixture initialization to fail'), + }); + } + }), + ); } diff --git a/app/scripts/tests/outbox-worker-delivery.test.mts b/app/scripts/tests/outbox-worker-delivery.test.mts index 860b3af2b..737aefac2 100644 --- a/app/scripts/tests/outbox-worker-delivery.test.mts +++ b/app/scripts/tests/outbox-worker-delivery.test.mts @@ -1,11 +1,13 @@ -import assert from 'node:assert/strict'; +import { expect, it } from 'effect-rstest'; import { spawn } from 'node:child_process'; +import type { ChildProcess } from 'node:child_process'; +import { existsSync } from 'node:fs'; import { once } from 'node:events'; +import type { Readable } from 'node:stream'; import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import { test as registerNodeTest } from 'node:test'; -import { Option, Schema } from 'effect'; +import { Cause, Deferred, Effect, Exit, Fiber, Schema } from 'effect'; import { generateOutboxWorkerDeployment } from '../generate-outbox-worker-deployment.mjs'; import { materializeOutboxWorker } from '../materialize-outbox-worker.mjs'; @@ -18,87 +20,131 @@ const WorkerArtifactSchema = Schema.Struct({ sourceInputs: Schema.Array(Schema.String), }); const decodeWorkerArtifact = Schema.decodeUnknownSync(Schema.fromJsonString(WorkerArtifactSchema)); -const decodeExitEvent = Schema.decodeUnknownSync( - Schema.Tuple([Schema.OptionFromNullOr(Schema.Number), Schema.Unknown]), -); const decodeDataEvent = Schema.decodeUnknownSync(Schema.Tuple([Schema.Unknown])); -const test = (name: string, run: () => void | Promise): void => { - void registerNodeTest(name, run); -}; +const waitForExit = (child: ChildProcess) => + Effect.gen(function* awaitChildExit() { + while (child.exitCode === null && child.signalCode === null && child.pid !== undefined) { + yield* Effect.sleep('10 millis'); + } + }); -const makeFixture = async () => { - const root = await mkdtemp(path.join(os.tmpdir(), 'ontos-worker-artifact-')); - await mkdir(path.join(root, LEDGER_PATH, 'src/worker-host'), { recursive: true }); - await mkdir(path.join(root, 'topology'), { recursive: true }); - await writeFile( - path.join(root, LEDGER_PATH, 'package.json'), - JSON.stringify({ - name: LEDGER_PACKAGE, - scripts: { 'worker:start': `node --experimental-strip-types ./${WORKER_HOST_ENTRY}` }, - type: 'module', - }), - ); - await writeFile( - path.join(root, LEDGER_PATH, WORKER_HOST_ENTRY), - '// @generated by scaffold:outbox-worker worker-host\nconsole.log("worker-started"); setInterval(() => {}, 1000);\n', - ); - await writeFile( - path.join(root, 'topology/reference-topology.json'), - JSON.stringify({ - verticals: [ - { - id: 'ledger', - moduleFederation: { manifestUrl: 'http://localhost:4110/mf-manifest.json' }, - package: LEDGER_PACKAGE, - path: LEDGER_PATH, - }, - ], - }), - ); - return root; -}; +const terminateChild = (child: ChildProcess) => + Effect.gen(function* releaseChild() { + if (child.exitCode !== null || child.signalCode !== null || child.pid === undefined) { + return; + } + child.kill('SIGTERM'); + yield* waitForExit(child).pipe( + Effect.timeout('1 second'), + Effect.catchTag('TimeoutError', () => + Effect.gen(function* forceChildExit() { + child.kill('SIGKILL'); + yield* waitForExit(child).pipe(Effect.timeout('1 second')); + }), + ), + Effect.orDie, + ); + }); + +const scopedChild = (start: () => Child) => + Effect.acquireRelease(Effect.sync(start), terminateChild); + +const readWorkerOutput = (child: { readonly stdout: Readable }) => + Effect.gen(function* awaitWorkerOutput() { + const dataEvent: unknown = yield* Effect.tryPromise((signal) => + once(child.stdout, 'data', { signal }), + ).pipe(Effect.timeout('3 seconds')); + const [output] = decodeDataEvent(dataEvent); + return String(output); + }); + +const makeFixture = () => + Effect.gen(function* testEffect1() { + const root = yield* Effect.acquireRelease( + Effect.tryPromise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-worker-artifact-'))), + (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), + ); + yield* Effect.tryPromise(() => + mkdir(path.join(root, LEDGER_PATH, 'src/worker-host'), { recursive: true }), + ); + yield* Effect.tryPromise(() => mkdir(path.join(root, 'topology'), { recursive: true })); + yield* Effect.tryPromise(() => + writeFile( + path.join(root, LEDGER_PATH, 'package.json'), + JSON.stringify({ + name: LEDGER_PACKAGE, + scripts: { 'worker:start': `node --experimental-strip-types ./${WORKER_HOST_ENTRY}` }, + type: 'module', + }), + ), + ); + yield* Effect.tryPromise(() => + writeFile( + path.join(root, LEDGER_PATH, WORKER_HOST_ENTRY), + '// @generated by scaffold:outbox-worker worker-host\nconsole.log("worker-started"); setInterval(() => {}, 1000);\n', + ), + ); + yield* Effect.tryPromise(() => + writeFile( + path.join(root, 'topology/reference-topology.json'), + JSON.stringify({ + verticals: [ + { + id: 'ledger', + moduleFederation: { manifestUrl: 'http://localhost:4110/mf-manifest.json' }, + package: LEDGER_PACKAGE, + path: LEDGER_PATH, + }, + ], + }), + ), + ); + return root; + }); -test('generates a separate supervised worker setup without changing owner configuration', async () => { - const root = await makeFixture(); - try { +it.live('generates a separate supervised worker setup without changing owner configuration', () => + Effect.gen(function* testEffect2() { + const root = yield* makeFixture(); const owner = `zerops:\n - setup: 'ledger'\n build:\n buildCommands:\n - cd app && pnpm --filter '@app/ledger' run build\n - cd app && pnpm run zerops:materialize -- --app 'ledger' --package '@app/ledger' --package-dir 'verticals/ledger'\n - cp 'app/topology/reference-topology.json' 'app/.zerops/runtime/ledger/topology.json'\n deployFiles:\n - 'app/.zerops/runtime/ledger'\n run:\n envVariables:\n PORT: '4110'\n VERTICAL_LEDGER_PORT: '4110'\n ONTOS_KEEP_ME: 'true'\n ULTRAMODERN_ZEROPS_SERVICE: ledger\n healthCheck:\n httpGet:\n path: '/ledger-api/ledger/readiness'\n start: sh -c 'cd app/.zerops/runtime/ledger && exec npm run serve'\n`; - const generated = await generateOutboxWorkerDeployment(root, owner); - assert.match(generated, /setup: 'ledger-worker'/u); - assert.match(generated, /zerops:materialize .* --worker/u); - assert.match(generated, /DATABASE_URL: \$\{ledger_DATABASE_URL\}/u); - assert.match(generated, /OUTBOX_WORKER_HEALTH_PORT: '4110'/u); - assert.doesNotMatch(generated.split("setup: 'ledger-worker'")[1], / run build/u); - assert.doesNotMatch(generated.split("setup: 'ledger-worker'")[1], /(?:^|\s)&(?:\s|$)/u); - assert.equal(generated.match(/ONTOS_KEEP_ME: 'true'/gu)?.length, 2); - assert.equal(await generateOutboxWorkerDeployment(root, generated), generated); - } finally { - await rm(root, { force: true, recursive: true }); - } -}); + const generated = yield* Effect.tryPromise(() => generateOutboxWorkerDeployment(root, owner)); + expect(generated).toMatch(/setup: 'ledger-worker'/u); + expect(generated).toMatch(/zerops:materialize .* --worker/u); + expect(generated).toMatch(/DATABASE_URL: \$\{ledger_DATABASE_URL\}/u); + expect(generated).toMatch(/OUTBOX_WORKER_HEALTH_PORT: '4110'/u); + expect(generated.split("setup: 'ledger-worker'")[1]).not.toMatch(/ run build/u); + expect(generated.split("setup: 'ledger-worker'")[1]).not.toMatch(/(?:^|\s)&(?:\s|$)/u); + expect(generated.match(/ONTOS_KEEP_ME: 'true'/gu)?.length).toBe(2); + expect(yield* Effect.tryPromise(() => generateOutboxWorkerDeployment(root, generated))).toBe( + generated, + ); + }), +); -test('materializes and starts a relocatable production worker artifact', async () => { - const root = await makeFixture(); - try { +it.live('materializes and starts a relocatable production worker artifact', () => + Effect.gen(function* testEffect3() { + const root = yield* makeFixture(); const command = path.resolve('scripts/materialize-zerops-runtime.mjs'); - const child = spawn( - '/usr/bin/env', - [ - `ULTRAMODERN_WORKSPACE_ROOT=${root}`, - process.execPath, - command, - '--app', - 'ledger', - '--package', - LEDGER_PACKAGE, - '--package-dir', - LEDGER_PATH, - '--worker', - ], - { - cwd: root, - stdio: ['ignore', 'pipe', 'pipe'], - }, + const child = yield* scopedChild(() => + spawn( + '/usr/bin/env', + [ + `ULTRAMODERN_WORKSPACE_ROOT=${root}`, + process.execPath, + command, + '--app', + 'ledger', + '--package', + LEDGER_PACKAGE, + '--package-dir', + LEDGER_PATH, + '--worker', + ], + { + cwd: root, + stdio: ['ignore', 'pipe', 'pipe'], + }, + ), ); let diagnostics = ''; child.stdout.on('data', (data) => { @@ -107,67 +153,149 @@ test('materializes and starts a relocatable production worker artifact', async ( child.stderr.on('data', (data) => { diagnostics += String(data); }); - const exitEvent: unknown = await once(child, 'exit'); - const [statusOption] = decodeExitEvent(exitEvent); - const status = Option.getOrNull(statusOption); - assert.equal(status, 0, diagnostics); + yield* Effect.tryPromise((signal) => once(child, 'exit', { signal })); + const status = child.exitCode; + expect(status, diagnostics).toBe(0); const artifact = decodeWorkerArtifact( - await readFile( - path.join(root, '.zerops/runtime/ledger-worker/worker-artifact.json'), - 'utf-8', + yield* Effect.tryPromise(() => + readFile(path.join(root, '.zerops/runtime/ledger-worker/worker-artifact.json'), 'utf-8'), ), ); - assert.equal(artifact.serviceId, 'ledger-worker'); - assert.equal( - artifact.sourceInputs.some((input: string) => input.endsWith(WORKER_HOST_ENTRY)), + expect(artifact.serviceId).toBe('ledger-worker'); + expect(artifact.sourceInputs.some((input: string) => input.endsWith(WORKER_HOST_ENTRY))).toBe( true, ); - const runtime = spawn(process.execPath, ['worker.mjs'], { - cwd: path.join(root, '.zerops/runtime/ledger-worker'), - stdio: ['ignore', 'pipe', 'pipe'], - }); - const dataEvent: unknown = await once(runtime.stdout, 'data', { - signal: AbortSignal.timeout(3000), - }); - const [workerOutput] = decodeDataEvent(dataEvent); - const output = String(workerOutput); - assert.match(output, /worker-started/u); - runtime.kill('SIGTERM'); - } finally { - await rm(root, { force: true, recursive: true }); - } -}); + const runtime = yield* scopedChild(() => + spawn(process.execPath, ['worker.mjs'], { + cwd: path.join(root, '.zerops/runtime/ledger-worker'), + stdio: ['ignore', 'pipe', 'pipe'], + }), + ); + const output = yield* readWorkerOutput(runtime); + expect(output).toMatch(/worker-started/u); + }), +); -test('keeps the live Party Registry worker deployment generated and independently supervised', async () => { - const root = process.cwd(); - const source = await readFile(path.join(root, 'zerops.yaml'), 'utf-8'); - assert.equal(await generateOutboxWorkerDeployment(root, source), source); - const [, worker] = source.split("setup: 'party-registry-worker'"); - assert.match(worker, /DATABASE_URL: \$\{partyregistry_DATABASE_URL\}/u); - assert.match(worker, /OUTBOX_WORKER_HEALTH_PORT: '4102'/u); - assert.match(worker, /cd app\/\.zerops\/runtime\/party-registry-worker/u); - assert.doesNotMatch(worker, /(?:^|\s)&(?:\s|$)/u); -}); +it.live( + 'keeps the live Party Registry worker deployment generated and independently supervised', + () => + Effect.gen(function* testEffect4() { + const root = process.cwd(); + const source = yield* Effect.tryPromise(() => + readFile(path.join(root, 'zerops.yaml'), 'utf-8'), + ); + expect(yield* Effect.tryPromise(() => generateOutboxWorkerDeployment(root, source))).toBe( + source, + ); + const [, worker] = source.split("setup: 'party-registry-worker'"); + expect(worker).toMatch(/DATABASE_URL: \$\{partyregistry_DATABASE_URL\}/u); + expect(worker).toMatch(/OUTBOX_WORKER_HEALTH_PORT: '4102'/u); + expect(worker).toMatch(/cd app\/\.zerops\/runtime\/party-registry-worker/u); + expect(worker).not.toMatch(/(?:^|\s)&(?:\s|$)/u); + }), +); -test('bundles the real Party host including the production Effect HTTP health adapter', async () => { - const runtimeDir = await mkdtemp(path.join(os.tmpdir(), 'ontos-party-worker-bundle-')); - try { - const runtimePackage = await materializeOutboxWorker({ - appId: 'party-registry', - packageDir: 'verticals/party-registry', - packageName: '@app/party-registry', - runtimeDir, - workspaceRoot: process.cwd(), - }); - const bundle = await readFile(path.join(runtimeDir, 'worker.mjs'), 'utf-8'); +it.live('bundles the real Party host including the production Effect HTTP health adapter', () => + Effect.gen(function* testEffect5() { + const runtimeDir = yield* Effect.acquireRelease( + Effect.tryPromise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-party-worker-bundle-'))), + (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), + ); + const runtimePackage = yield* Effect.tryPromise(() => + materializeOutboxWorker({ + appId: 'party-registry', + packageDir: 'verticals/party-registry', + packageName: '@app/party-registry', + runtimeDir, + workspaceRoot: process.cwd(), + }), + ); + const bundle = yield* Effect.tryPromise(() => + readFile(path.join(runtimeDir, 'worker.mjs'), 'utf-8'), + ); const artifact = decodeWorkerArtifact( - await readFile(path.join(runtimeDir, 'worker-artifact.json'), 'utf-8'), - ); - assert.ok(artifact.sourceInputs.includes('packages/core-runtime/src/outbox/health.ts')); - assert.match(bundle, /@effect\/platform-node\/NodeHttpServer/u); - assert.doesNotMatch(bundle, /from ["']@effect\/platform-node["']/u); - assert.equal(runtimePackage.dependencies['@effect/platform-node'], '4.0.0-beta.107'); - } finally { - await rm(runtimeDir, { force: true, recursive: true }); - } -}); + yield* Effect.tryPromise(() => + readFile(path.join(runtimeDir, 'worker-artifact.json'), 'utf-8'), + ), + ); + expect(artifact.sourceInputs.includes('packages/core-runtime/src/outbox/health.ts')).toBe(true); + expect(bundle).toMatch(/@effect\/platform-node\/NodeHttpServer/u); + expect(bundle).not.toMatch(/from ["']@effect\/platform-node["']/u); + expect(runtimePackage.dependencies['@effect/platform-node']).toBe('4.0.0-beta.107'); + }), +); + +const runCleanupControl = ( + interrupt: boolean, + started: Deferred.Deferred<{ child: ChildProcess; root: string }>, + ready: Deferred.Deferred, + cleanupOrder: Deferred.Deferred, +) => + Effect.gen(function* exerciseCleanupControl() { + const root = yield* makeFixture(); + // Registered before the child: LIFO teardown must reap it before removing its cwd. + yield* Effect.addFinalizer(() => + Effect.gen(function* verifyCleanupOrder() { + const { child } = yield* Deferred.await(started); + yield* Deferred.succeed( + cleanupOrder, + (child.exitCode !== null || child.signalCode !== null) && existsSync(root), + ); + }), + ); + const child = yield* scopedChild(() => + spawn( + process.execPath, + [ + '-e', + 'process.on("SIGTERM", () => {}); console.log("unexpected-startup"); setInterval(() => {}, 1000);', + ], + { + cwd: root, + stdio: ['ignore', 'pipe', 'pipe'], + }, + ), + ); + yield* Deferred.succeed(started, { child, root }); + const output = yield* readWorkerOutput(child); + yield* Deferred.succeed(ready, null); + if (interrupt) { + yield* Effect.never; + } else { + expect(output).toMatch(/worker-started/u); + } + }); + +const cleanupControl = (interrupt: boolean) => + Effect.gen(function* verifyChildCleanup() { + const started = yield* Deferred.make<{ child: ChildProcess; root: string }>(); + const ready = yield* Deferred.make(); + const cleanupOrder = yield* Deferred.make(); + const worker = yield* Effect.forkChild( + Effect.scoped(runCleanupControl(interrupt, started, ready, cleanupOrder)), + ); + const { child, root } = yield* Deferred.await(started).pipe(Effect.timeout('5 seconds')); + if (interrupt) { + yield* Deferred.await(ready).pipe(Effect.timeout('5 seconds')); + yield* Fiber.interrupt(worker); + } + const result = yield* Fiber.await(worker); + expect(Exit.isFailure(result)).toBe(true); + if (Exit.isFailure(result)) { + if (interrupt) { + expect(Cause.hasInterrupts(result.cause)).toBe(true); + } else { + expect(Cause.pretty(result.cause)).toContain('worker-started'); + } + } + expect(child.exitCode !== null || child.signalCode !== null).toBe(true); + expect(yield* Deferred.await(cleanupOrder).pipe(Effect.timeout('1 second'))).toBe(true); + expect(child.signalCode).toBe('SIGKILL'); + expect(existsSync(root)).toBe(false); + }); + +it.live('reaps a worker before removing its fixture after failed startup output', () => + cleanupControl(false), +); + +it.live('reaps a worker before removing its fixture on interruption', () => cleanupControl(true)); diff --git a/app/scripts/tests/plan-deployment-impact.test.mts b/app/scripts/tests/plan-deployment-impact.test.mts index 28db02b04..0c0179f5e 100644 --- a/app/scripts/tests/plan-deployment-impact.test.mts +++ b/app/scripts/tests/plan-deployment-impact.test.mts @@ -1,13 +1,10 @@ -/// - -import assert from 'node:assert/strict'; +import { expect, it } from 'effect-rstest'; import { execFileSync } from 'node:child_process'; -import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; +import { access, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import { test as registerNodeTest } from 'node:test'; import { NodeServices } from '@effect/platform-node'; -import { ManagedRuntime } from 'effect'; +import { Cause, Effect, Exit } from 'effect'; import { hashAuthorizationEvidence } from '../check-authorization-readiness.mts'; import { planDeploymentImpact as planDeploymentImpactEffect, @@ -18,6 +15,19 @@ import type { PlanDeploymentImpactOptions, } from '../plan-deployment-impact.mts'; +const planningFailure = (effect: Effect.Effect) => + effect.pipe( + Effect.exit, + Effect.map( + Exit.match({ + onFailure: Cause.pretty, + onSuccess: () => { + throw new Error('Expected planning failure'); + }, + }), + ), + ); + interface FixtureOptions { readonly includeContactOwner?: boolean; readonly includeWorker?: boolean; @@ -77,114 +87,130 @@ const SHELL_OWNER = { const OWNERSHIP_PATH = 'topology/ownership.json'; const DOCUMENTATION_PATH = 'docs/README.md'; -const test = (name: string, run: () => void | Promise): void => { - void registerNodeTest(name, run); -}; - -const deploymentImpactRuntime = ManagedRuntime.make(NodeServices.layer); -const planDeploymentImpact = async (options: PlanDeploymentImpactOptions) => - await deploymentImpactRuntime.runPromise(planDeploymentImpactEffect(options)); - -registerNodeTest.after(async () => { - await deploymentImpactRuntime.dispose(); -}); - -const writeJson = async ( - root: string, - relativePath: string, - value: FixtureDocument, -): Promise => { - const target = path.join(root, relativePath); - await mkdir(path.dirname(target), { recursive: true }); - await writeFile(target, `${JSON.stringify(value, undefined, 2)}\n`, 'utf-8'); -}; +const planDeploymentImpact = (options: PlanDeploymentImpactOptions) => + planDeploymentImpactEffect(options).pipe(Effect.provide(NodeServices.layer)); -const makeFixture = async (options: FixtureOptions = {}): Promise => { - const root = await mkdtemp(path.join(os.tmpdir(), 'ontos-deployment-impact-')); - const verticalId = options.verticalId ?? 'contacts'; - const verticalPackage = `@app/${verticalId}`; - const verticalPath = `verticals/${verticalId}`; - await writeJson(root, 'topology/reference-topology.json', { - schemaVersion: 1, - sharedPackages: [CORE_RUNTIME_OWNER, SHARED_CONTRACTS_OWNER], - shell: { - id: SHELL_ID, - package: SHELL_PACKAGE, - verticalRefs: [verticalId], - }, - verticals: [ - { - id: verticalId, - moduleFederation: { remotes: [], verticalRefs: [] }, - package: verticalPackage, - path: verticalPath, - }, - ], - }); - await writeJson(root, OWNERSHIP_PATH, { - owners: [ - CORE_RUNTIME_OWNER, - SHARED_CONTRACTS_OWNER, - SHELL_OWNER, - ...(options.includeContactOwner === false - ? [] - : [{ id: verticalId, package: verticalPackage, path: verticalPath }]), - ], - schemaVersion: 1, +const writeJson = (root: string, relativePath: string, value: FixtureDocument) => + Effect.gen(function* testEffect1() { + const target = path.join(root, relativePath); + yield* Effect.tryPromise(() => mkdir(path.dirname(target), { recursive: true })); + yield* Effect.tryPromise(() => + writeFile(target, `${JSON.stringify(value, undefined, 2)}\n`, 'utf-8'), + ); }); - if (options.includeWorker === true) { - const workerRoot = path.join(root, verticalPath); - await mkdir(path.join(workerRoot, 'src/worker-host'), { recursive: true }); - await writeFile( - path.join(workerRoot, 'package.json'), - `${JSON.stringify({ name: verticalPackage, scripts: { 'worker:start': 'node --experimental-strip-types ./src/worker-host/main.ts' } })}\n`, + +const makeFixture = (options: FixtureOptions = {}) => + Effect.gen(function* testEffect2() { + const root = yield* Effect.acquireRelease( + Effect.tryPromise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-deployment-impact-'))), + (directory) => + Effect.tryPromise(() => rm(directory, { force: true, recursive: true })).pipe(Effect.orDie), ); - await writeFile( - path.join(workerRoot, 'src/worker-host/main.ts'), - '// @generated by scaffold:outbox-worker worker-host\n', + const verticalId = options.verticalId ?? 'contacts'; + const verticalPackage = `@app/${verticalId}`; + const verticalPath = `verticals/${verticalId}`; + yield* writeJson(root, 'topology/reference-topology.json', { + schemaVersion: 1, + sharedPackages: [CORE_RUNTIME_OWNER, SHARED_CONTRACTS_OWNER], + shell: { + id: SHELL_ID, + package: SHELL_PACKAGE, + verticalRefs: [verticalId], + }, + verticals: [ + { + id: verticalId, + moduleFederation: { remotes: [], verticalRefs: [] }, + package: verticalPackage, + path: verticalPath, + }, + ], + }); + yield* writeJson(root, OWNERSHIP_PATH, { + owners: [ + CORE_RUNTIME_OWNER, + SHARED_CONTRACTS_OWNER, + SHELL_OWNER, + ...(options.includeContactOwner === false + ? [] + : [{ id: verticalId, package: verticalPackage, path: verticalPath }]), + ], + schemaVersion: 1, + }); + if (options.includeWorker === true) { + const workerRoot = path.join(root, verticalPath); + yield* Effect.tryPromise(() => + mkdir(path.join(workerRoot, 'src/worker-host'), { recursive: true }), + ); + yield* Effect.tryPromise(() => + writeFile( + path.join(workerRoot, 'package.json'), + `${JSON.stringify({ name: verticalPackage, scripts: { 'worker:start': 'node --experimental-strip-types ./src/worker-host/main.ts' } })}\n`, + ), + ); + yield* Effect.tryPromise(() => + writeFile( + path.join(workerRoot, 'src/worker-host/main.ts'), + '// @generated by scaffold:outbox-worker worker-host\n', + ), + ); + } + const setups = options.setupIds ?? [ + 'migrator', + 'spicedb', + verticalId, + ...(options.includeWorker === true ? [`${verticalId}-worker`] : []), + 'shellsuperapp', + ]; + const setupLines = setups.map((setup) => ` - setup: '${setup}'`).join('\n'); + yield* Effect.tryPromise(() => + writeFile(path.join(root, 'zerops.yaml'), `zerops:\n${setupLines}\n`, 'utf-8'), ); - } - const setups = options.setupIds ?? [ - 'migrator', - 'spicedb', - verticalId, - ...(options.includeWorker === true ? [`${verticalId}-worker`] : []), - 'shellsuperapp', - ]; - const setupLines = setups.map((setup) => ` - setup: '${setup}'`).join('\n'); - await writeFile(path.join(root, 'zerops.yaml'), `zerops:\n${setupLines}\n`, 'utf-8'); - return root; -}; + return root; + }); -const withFixture = async ( - run: (root: string) => void | Promise, +const withFixture = ( + run: (root: string) => Effect.Effect, options?: FixtureOptions, -): Promise => { - const root = await makeFixture(options); - try { - await run(root); - } finally { - await rm(root, { force: true, recursive: true }); - } -}; - -test('deploys a generated owner worker immediately after its provider', async () => { - await withFixture( - async (root) => { - const plan = await planDeploymentImpact({ - changedPaths: ['verticals/contacts/src/workers/project-contact.worker.ts'], - rootDirectory: root, - }); - assert.deepEqual(plan.units.providers, ['contacts', 'contacts-worker']); - assert.deepEqual( - plan.phases.map((phase) => phase.id), - ['contacts', 'contacts-worker'], - ); - assert.equal(plan.phases[1]?.serviceIdEnv, 'ZEROPS_CONTACTS_WORKER_SERVICE_ID'); - }, - { includeWorker: true }, +) => + Effect.gen(function* testEffect3() { + const root = yield* makeFixture(options); + yield* run(root); + }).pipe(Effect.scoped); + +for (const termination of ['failure', 'interruption'] as const) { + it.live(`removes the fixture after ${termination}`, () => + Effect.gen(function* verifiesFixtureCleanup() { + let fixtureRoot = ''; + const outcome = yield* withFixture((root) => { + fixtureRoot = root; + return termination === 'failure' ? Effect.fail('fixture failure') : Effect.interrupt; + }).pipe(Effect.exit); + expect(Exit.isFailure(outcome)).toBe(true); + expect(fixtureRoot).not.toBe(''); + const remaining = yield* Effect.tryPromise(() => access(fixtureRoot)).pipe(Effect.exit); + expect(Exit.isFailure(remaining)).toBe(true); + }), ); -}); +} + +it.live('deploys a generated owner worker immediately after its provider', () => + Effect.gen(function* testEffect4() { + yield* withFixture( + (root) => + Effect.gen(function* testEffect5() { + const plan = yield* planDeploymentImpact({ + changedPaths: ['verticals/contacts/src/workers/project-contact.worker.ts'], + rootDirectory: root, + }); + expect(plan.units.providers).toEqual(['contacts', 'contacts-worker']); + expect(plan.phases.map((phase) => phase.id)).toEqual(['contacts', 'contacts-worker']); + expect(plan.phases[1]?.serviceIdEnv).toBe('ZEROPS_CONTACTS_WORKER_SERVICE_ID'); + }), + { includeWorker: true }, + ); + }), +); const runGit = (root: string, argumentsList: readonly string[]): string => execFileSync( @@ -196,119 +222,135 @@ const runGit = (root: string, argumentsList: readonly string[]): string => }, ).trim(); -test('plans current Contacts owner-local changes without a hard-coded owner registry', async () => { - await withFixture(async (root) => { - const plan = await planDeploymentImpact({ - changedPaths: ['app/verticals/contacts/src/features/customers/customer-form.tsx'], - rootDirectory: root, - }); - assert.deepEqual(plan.units, { - migrator: false, - providers: ['contacts'], - shell: false, - spicedb: false, - }); - assert.deepEqual( - plan.phases.map((phase) => phase.id), - ['contacts'], +it.live('plans current Contacts owner-local changes without a hard-coded owner registry', () => + Effect.gen(function* testEffect6() { + yield* withFixture((root) => + Effect.gen(function* testEffect7() { + const plan = yield* planDeploymentImpact({ + changedPaths: ['app/verticals/contacts/src/features/customers/customer-form.tsx'], + rootDirectory: root, + }); + expect(plan.units).toEqual({ + migrator: false, + providers: ['contacts'], + shell: false, + spicedb: false, + }); + expect(plan.phases.map((phase) => phase.id)).toEqual(['contacts']); + }), ); - }); -}); - -test('orders authorization schema and replay migration before every affected consumer', async () => { - await withFixture(async (root) => { - const plan = await planDeploymentImpact({ - changedPaths: ['app/scripts/authorization/rollout-contract.mts'], - rootDirectory: root, - }); - assert.deepEqual( - plan.phases.map(({ id }) => id), - ['migrator', 'spicedb', 'contacts', SHELL_ID], + }), +); + +it.live('orders authorization schema and replay migration before every affected consumer', () => + Effect.gen(function* testEffect8() { + yield* withFixture((root) => + Effect.gen(function* testEffect9() { + const plan = yield* planDeploymentImpact({ + changedPaths: ['app/scripts/authorization/rollout-contract.mts'], + rootDirectory: root, + }); + expect(plan.phases.map(({ id }) => id)).toEqual([ + 'migrator', + 'spicedb', + 'contacts', + SHELL_ID, + ]); + }), ); - }); -}); - -test('plans Shell-only changes for the topology-derived Shell owner', async () => { - await withFixture(async (root) => { - const plan = await planDeploymentImpact({ - changedPaths: ['apps/shell-super-app/src/routes/shell-frame.tsx'], - rootDirectory: root, - }); - assert.deepEqual( - plan.phases.map((phase) => phase.id), - [SHELL_ID], + }), +); + +it.live('plans Shell-only changes for the topology-derived Shell owner', () => + Effect.gen(function* testEffect10() { + yield* withFixture((root) => + Effect.gen(function* testEffect11() { + const plan = yield* planDeploymentImpact({ + changedPaths: ['apps/shell-super-app/src/routes/shell-frame.tsx'], + rootDirectory: root, + }); + expect(plan.phases.map((phase) => phase.id)).toEqual([SHELL_ID]); + expect(plan.units.shell).toBe(true); + }), ); - assert.equal(plan.units.shell, true); - }); -}); - -test('adds the migrator before an owner whose schema or migration contract changed', async () => { - await withFixture(async (root) => { - const plan = await planDeploymentImpact({ - changedPaths: ['verticals/contacts/drizzle/0003_add_customer.sql'], - rootDirectory: root, - }); - assert.deepEqual( - plan.phases.map((phase) => phase.id), - ['migrator', 'contacts'], + }), +); + +it.live('adds the migrator before an owner whose schema or migration contract changed', () => + Effect.gen(function* testEffect12() { + yield* withFixture((root) => + Effect.gen(function* testEffect13() { + const plan = yield* planDeploymentImpact({ + changedPaths: ['verticals/contacts/drizzle/0003_add_customer.sql'], + rootDirectory: root, + }); + expect(plan.phases.map((phase) => phase.id)).toEqual(['migrator', 'contacts']); + }), ); - }); -}); + }), +); for (const changedPath of [ 'scripts/run-zerops-migrator.mjs', 'scripts/verify-application-db-schema.mts', 'scripts/postgres/bootstrap-runtime-role.mts', ]) { - test(`includes the migrator for root migration contract ${changedPath}`, async () => { - await withFixture(async (root) => { - const plan = await planDeploymentImpact({ changedPaths: [changedPath], rootDirectory: root }); - assert.deepEqual( - plan.phases.map((phase) => phase.id), - ['migrator'], + it.live(`includes the migrator for root migration contract ${changedPath}`, () => + Effect.gen(function* testEffect14() { + yield* withFixture((root) => + Effect.gen(function* testEffect15() { + const plan = yield* planDeploymentImpact({ + changedPaths: [changedPath], + rootDirectory: root, + }); + expect(plan.phases.map((phase) => phase.id)).toEqual(['migrator']); + }), ); - }); - }); + }), + ); } -test('expands shared-package changes to every consumer in dependency order', async () => { - await withFixture(async (root) => { - const plan = await planDeploymentImpact({ - changedPaths: ['packages/shared-contracts/src/gateway-context.ts'], - rootDirectory: root, - }); - assert.deepEqual( - plan.phases.map((phase) => phase.id), - ['contacts', SHELL_ID], +it.live('expands shared-package changes to every consumer in dependency order', () => + Effect.gen(function* testEffect18() { + yield* withFixture((root) => + Effect.gen(function* testEffect19() { + const plan = yield* planDeploymentImpact({ + changedPaths: ['packages/shared-contracts/src/gateway-context.ts'], + rootDirectory: root, + }); + expect(plan.phases.map((phase) => phase.id)).toEqual(['contacts', SHELL_ID]); + }), ); - }); -}); - -test('expands a provider public-contract change to the dependent Shell', async () => { - await withFixture(async (root) => { - const plan = await planDeploymentImpact({ - changedPaths: ['verticals/contacts/shared/api.ts'], - rootDirectory: root, - }); - assert.deepEqual( - plan.phases.map((phase) => phase.id), - ['contacts', SHELL_ID], + }), +); + +it.live('expands a provider public-contract change to the dependent Shell', () => + Effect.gen(function* testEffect20() { + yield* withFixture((root) => + Effect.gen(function* testEffect21() { + const plan = yield* planDeploymentImpact({ + changedPaths: ['verticals/contacts/shared/api.ts'], + rootDirectory: root, + }); + expect(plan.phases.map((phase) => phase.id)).toEqual(['contacts', SHELL_ID]); + }), ); - }); -}); - -test('orders SpiceDB before all consumers for authorization runtime changes', async () => { - await withFixture(async (root) => { - const plan = await planDeploymentImpact({ - changedPaths: ['packages/core-runtime/spicedb/bootstrap.yaml'], - rootDirectory: root, - }); - assert.deepEqual( - plan.phases.map((phase) => phase.id), - ['spicedb', 'contacts', SHELL_ID], + }), +); + +it.live('orders SpiceDB before all consumers for authorization runtime changes', () => + Effect.gen(function* testEffect22() { + yield* withFixture((root) => + Effect.gen(function* testEffect23() { + const plan = yield* planDeploymentImpact({ + changedPaths: ['packages/core-runtime/spicedb/bootstrap.yaml'], + rootDirectory: root, + }); + expect(plan.phases.map((phase) => phase.id)).toEqual(['spicedb', 'contacts', SHELL_ID]); + }), ); - }); -}); + }), +); for (const changedPath of [ 'scripts/postgres/bootstrap-spicedb-database.mts', @@ -324,171 +366,229 @@ for (const changedPath of [ 'zerops.yaml', 'topology/reference-topology.json', ]) { - test(`conservatively deploys every phase for ${changedPath}`, async () => { - await withFixture(async (root) => { - const plan = await planDeploymentImpact({ changedPaths: [changedPath], rootDirectory: root }); - assert.deepEqual( - plan.phases.map((phase) => phase.id), - ['migrator', 'spicedb', 'contacts', SHELL_ID], + it.live(`conservatively deploys every phase for ${changedPath}`, () => + Effect.gen(function* testEffect24() { + yield* withFixture((root) => + Effect.gen(function* testEffect25() { + const plan = yield* planDeploymentImpact({ + changedPaths: [changedPath], + rootDirectory: root, + }); + expect(plan.phases.map((phase) => phase.id)).toEqual([ + 'migrator', + 'spicedb', + 'contacts', + SHELL_ID, + ]); + }), ); - }); - }); + }), + ); } -test('produces a reviewed no-op for documentation-only changes', async () => { - await withFixture(async (root) => { - const plan = await planDeploymentImpact({ - changedPaths: ['docs/architecture/DEPLOYMENT.md'], - rootDirectory: root, - }); - assert.equal(plan.any, false); - assert.deepEqual(plan.phases, []); - }); -}); - -test('fails closed for the unknown destination of a renamed application directory', async () => { - await withFixture(async (root) => { - await assert.rejects( - planDeploymentImpact({ - changedPaths: ['verticals/contacts/src/index.ts', 'verticals/relationships/src/index.ts'], - rootDirectory: root, +it.live('produces a reviewed no-op for documentation-only changes', () => + Effect.gen(function* testEffect26() { + yield* withFixture((root) => + Effect.gen(function* testEffect27() { + const plan = yield* planDeploymentImpact({ + changedPaths: ['docs/architecture/DEPLOYMENT.md'], + rootDirectory: root, + }); + expect(plan.any).toBe(false); + expect(plan.phases).toEqual([]); }), - /unknown changed path "verticals\/relationships\/src\/index\.ts" in application area "verticals"/u, ); - }); -}); - -test('fails closed when a topology delivery unit has no ownership entry', async () => { - await withFixture( - async (root) => { - await assert.rejects( - planDeploymentImpact({ changedPaths: [DOCUMENTATION_PATH], rootDirectory: root }), - /topology delivery unit "contacts" is missing from topology\/ownership\.json/u, - ); - }, - { includeContactOwner: false }, - ); -}); - -test('fails closed when topology and ownership identities disagree', async () => { - await withFixture(async (root) => { - await writeJson(root, OWNERSHIP_PATH, { - owners: [ - CORE_RUNTIME_OWNER, - SHARED_CONTRACTS_OWNER, - SHELL_OWNER, - { id: 'contacts', package: '@app/contacts-old', path: 'verticals/contacts-old' }, - ], - }); - await assert.rejects( - planDeploymentImpact({ changedPaths: [DOCUMENTATION_PATH], rootDirectory: root }), - /topology and ownership disagree for "contacts"/u, + }), +); + +it.live('fails closed for the unknown destination of a renamed application directory', () => + Effect.gen(function* testEffect28() { + yield* withFixture((root) => + Effect.gen(function* testEffect29() { + expect( + yield* planningFailure( + planDeploymentImpact({ + changedPaths: [ + 'verticals/contacts/src/index.ts', + 'verticals/relationships/src/index.ts', + ], + rootDirectory: root, + }), + ), + ).toMatch( + /unknown changed path "verticals\/relationships\/src\/index\.ts" in application area "verticals"/u, + ); + }), ); - }); -}); - -test('fails closed when shared-package topology and ownership identities disagree', async () => { - await withFixture(async (root) => { - await writeJson(root, OWNERSHIP_PATH, { - owners: [ - { ...CORE_RUNTIME_OWNER, path: 'packages/core-runtime-old' }, - SHARED_CONTRACTS_OWNER, - SHELL_OWNER, - { id: 'contacts', package: '@app/contacts', path: 'verticals/contacts' }, - ], - }); - await assert.rejects( - planDeploymentImpact({ changedPaths: [DOCUMENTATION_PATH], rootDirectory: root }), - /topology and ownership disagree for shared package "core-runtime"/u, + }), +); + +it.live('fails closed when a topology delivery unit has no ownership entry', () => + Effect.gen(function* testEffect30() { + yield* withFixture( + (root) => + Effect.gen(function* testEffect31() { + expect( + yield* planningFailure( + planDeploymentImpact({ changedPaths: [DOCUMENTATION_PATH], rootDirectory: root }), + ), + ).toMatch(/topology delivery unit "contacts" is missing from topology\/ownership\.json/u); + }), + { includeContactOwner: false }, ); - }); -}); - -test('fails closed when a topology unit has no supported stage setup', async () => { - await withFixture( - async (root) => { - await assert.rejects( - planDeploymentImpact({ changedPaths: [DOCUMENTATION_PATH], rootDirectory: root }), - /topology delivery unit "contacts" has unsupported stage setup "contacts"/u, - ); - }, - { setupIds: ['migrator', 'spicedb', 'shellsuperapp'] }, - ); -}); - -test('uses a safe full deployment for an all-zero comparison base', async () => { - await withFixture(async (root) => { - const plan = await planDeploymentImpact({ - baseRevision: '0000000000000000000000000000000000000000', - headRevision: 'HEAD', - rootDirectory: root, - }); - assert.equal(plan.comparison.mode, 'full'); - assert.match(plan.comparison.reason ?? '', /all-zero/u); - assert.deepEqual( - plan.phases.map((phase) => phase.id), - ['migrator', 'spicedb', 'contacts', SHELL_ID], + }), +); + +it.live('fails closed when topology and ownership identities disagree', () => + Effect.gen(function* testEffect32() { + yield* withFixture((root) => + Effect.gen(function* testEffect33() { + yield* writeJson(root, OWNERSHIP_PATH, { + owners: [ + CORE_RUNTIME_OWNER, + SHARED_CONTRACTS_OWNER, + SHELL_OWNER, + { id: 'contacts', package: '@app/contacts-old', path: 'verticals/contacts-old' }, + ], + }); + expect( + yield* planningFailure( + planDeploymentImpact({ changedPaths: [DOCUMENTATION_PATH], rootDirectory: root }), + ), + ).toMatch(/topology and ownership disagree for "contacts"/u); + }), ); - }); -}); - -test('uses a safe full deployment for an unavailable comparison base', async () => { - await withFixture(async (root) => { - const plan = await planDeploymentImpact({ - baseRevision: 'missing-base-revision', - headRevision: 'HEAD', - rootDirectory: root, - }); - assert.equal(plan.comparison.mode, 'full'); - assert.match( - plan.comparison.reason ?? '', - /comparison base "missing-base-revision" is unavailable/u, + }), +); + +it.live('fails closed when shared-package topology and ownership identities disagree', () => + Effect.gen(function* testEffect34() { + yield* withFixture((root) => + Effect.gen(function* testEffect35() { + yield* writeJson(root, OWNERSHIP_PATH, { + owners: [ + { ...CORE_RUNTIME_OWNER, path: 'packages/core-runtime-old' }, + SHARED_CONTRACTS_OWNER, + SHELL_OWNER, + { id: 'contacts', package: '@app/contacts', path: 'verticals/contacts' }, + ], + }); + expect( + yield* planningFailure( + planDeploymentImpact({ changedPaths: [DOCUMENTATION_PATH], rootDirectory: root }), + ), + ).toMatch(/topology and ownership disagree for shared package "core-runtime"/u); + }), ); - }); -}); - -test('uses a safe full deployment when the comparison base is not an ancestor', async () => { - await withFixture(async (root) => { - runGit(root, ['init']); - runGit(root, ['add', '.']); - runGit(root, ['commit', '-m', 'fixture root']); - const rootRevision = runGit(root, ['rev-parse', 'HEAD']); - await writeFile(path.join(root, 'main-marker.txt'), 'main\n', 'utf-8'); - runGit(root, ['add', 'main-marker.txt']); - runGit(root, ['commit', '-m', 'main change']); - const rewrittenBase = runGit(root, ['rev-parse', 'HEAD']); - runGit(root, ['checkout', '-b', 'rewritten', rootRevision]); - await writeFile(path.join(root, 'rewritten-marker.txt'), 'rewritten\n', 'utf-8'); - runGit(root, ['add', 'rewritten-marker.txt']); - runGit(root, ['commit', '-m', 'rewritten change']); - - const plan = await planDeploymentImpact({ - baseRevision: rewrittenBase, - headRevision: 'HEAD', - rootDirectory: root, - }); - assert.equal(plan.comparison.mode, 'full'); - assert.match(plan.comparison.reason ?? '', /is not an ancestor/u); - }); -}); - -test('changing a topology identity changes the plan without editing planner source', async () => { - await withFixture( - async (root) => { - const plan = await planDeploymentImpact({ - changedPaths: ['verticals/relationships/src/index.ts'], - rootDirectory: root, - }); - assert.deepEqual(plan.units.providers, ['relationships']); - assert.deepEqual( - plan.phases.map((phase) => phase.id), - ['relationships'], - ); - assert.equal(plan.phases[0]?.serviceIdEnv, 'ZEROPS_RELATIONSHIPS_SERVICE_ID'); - }, - { verticalId: 'relationships' }, - ); -}); + }), +); + +it.live('fails closed when a topology unit has no supported stage setup', () => + Effect.gen(function* testEffect36() { + yield* withFixture( + (root) => + Effect.gen(function* testEffect37() { + expect( + yield* planningFailure( + planDeploymentImpact({ changedPaths: [DOCUMENTATION_PATH], rootDirectory: root }), + ), + ).toMatch(/topology delivery unit "contacts" has unsupported stage setup "contacts"/u); + }), + { setupIds: ['migrator', 'spicedb', 'shellsuperapp'] }, + ); + }), +); + +it.live('uses a safe full deployment for an all-zero comparison base', () => + Effect.gen(function* testEffect38() { + yield* withFixture((root) => + Effect.gen(function* testEffect39() { + const plan = yield* planDeploymentImpact({ + baseRevision: '0000000000000000000000000000000000000000', + headRevision: 'HEAD', + rootDirectory: root, + }); + expect(plan.comparison.mode).toBe('full'); + expect(plan.comparison.reason ?? '').toMatch(/all-zero/u); + expect(plan.phases.map((phase) => phase.id)).toEqual([ + 'migrator', + 'spicedb', + 'contacts', + SHELL_ID, + ]); + }), + ); + }), +); + +it.live('uses a safe full deployment for an unavailable comparison base', () => + Effect.gen(function* testEffect40() { + yield* withFixture((root) => + Effect.gen(function* testEffect41() { + const plan = yield* planDeploymentImpact({ + baseRevision: 'missing-base-revision', + headRevision: 'HEAD', + rootDirectory: root, + }); + expect(plan.comparison.mode).toBe('full'); + expect(plan.comparison.reason ?? '').toMatch( + /comparison base "missing-base-revision" is unavailable/u, + ); + }), + ); + }), +); + +it.live('uses a safe full deployment when the comparison base is not an ancestor', () => + Effect.gen(function* testEffect42() { + yield* withFixture((root) => + Effect.gen(function* testEffect43() { + runGit(root, ['init']); + runGit(root, ['add', '.']); + runGit(root, ['commit', '-m', 'fixture root']); + const rootRevision = runGit(root, ['rev-parse', 'HEAD']); + yield* Effect.tryPromise(() => + writeFile(path.join(root, 'main-marker.txt'), 'main\n', 'utf-8'), + ); + runGit(root, ['add', 'main-marker.txt']); + runGit(root, ['commit', '-m', 'main change']); + const rewrittenBase = runGit(root, ['rev-parse', 'HEAD']); + runGit(root, ['checkout', '-b', 'rewritten', rootRevision]); + yield* Effect.tryPromise(() => + writeFile(path.join(root, 'rewritten-marker.txt'), 'rewritten\n', 'utf-8'), + ); + runGit(root, ['add', 'rewritten-marker.txt']); + runGit(root, ['commit', '-m', 'rewritten change']); + + const plan = yield* planDeploymentImpact({ + baseRevision: rewrittenBase, + headRevision: 'HEAD', + rootDirectory: root, + }); + expect(plan.comparison.mode).toBe('full'); + expect(plan.comparison.reason ?? '').toMatch(/is not an ancestor/u); + }), + ); + }), +); + +it.live('changing a topology identity changes the plan without editing planner source', () => + Effect.gen(function* testEffect44() { + yield* withFixture( + (root) => + Effect.gen(function* testEffect45() { + const plan = yield* planDeploymentImpact({ + changedPaths: ['verticals/relationships/src/index.ts'], + rootDirectory: root, + }); + expect(plan.units.providers).toEqual(['relationships']); + expect(plan.phases.map((phase) => phase.id)).toEqual(['relationships']); + expect(plan.phases[0]?.serviceIdEnv).toBe('ZEROPS_RELATIONSHIPS_SERVICE_ID'); + }), + { verticalId: 'relationships' }, + ); + }), +); const promotionFixture = (): AuthorizationPromotionGateInput => { const inventory = { @@ -563,7 +663,7 @@ const withoutImpactEvidence = ( input: AuthorizationPromotionGateInput, ): AuthorizationPromotionGateInput => { const { impact, ...remaining } = input; - assert.ok(impact); + expect(impact !== undefined).toBe(true); return remaining; }; @@ -571,7 +671,7 @@ const withoutNegativeSmokeEvidence = ( input: AuthorizationPromotionGateInput, ): AuthorizationPromotionGateInput => { const { negativeSmoke, ...remaining } = input; - assert.ok(negativeSmoke); + expect(negativeSmoke !== undefined).toBe(true); return remaining; }; @@ -579,12 +679,12 @@ const withoutReadinessEvidence = ( input: AuthorizationPromotionGateInput, ): AuthorizationPromotionGateInput => { const { readiness, ...remaining } = input; - assert.ok(readiness); + expect(readiness !== undefined).toBe(true); return remaining; }; -test('requires exact impact, readiness, and negative-smoke evidence for enforced promotion', () => { - assert.deepEqual(validateAuthorizationPromotionGate(promotionFixture()), { +it('requires exact impact, readiness, and negative-smoke evidence for enforced promotion', () => { + expect(validateAuthorizationPromotionGate(promotionFixture())).toEqual({ environment: 'stage', mode: 'enforced', status: 'ready', @@ -594,22 +694,23 @@ test('requires exact impact, readiness, and negative-smoke evidence for enforced withoutNegativeSmokeEvidence(promotionFixture()), withoutReadinessEvidence(promotionFixture()), ]) { - assert.throws(() => validateAuthorizationPromotionGate(changed), /requires impact/u); + expect(() => validateAuthorizationPromotionGate(changed)).toThrow(/requires impact/u); } const stale = promotionFixture(); const staleReadiness = stale.readiness; - assert.ok(staleReadiness); - assert.throws( - () => - validateAuthorizationPromotionGate({ - ...stale, - readiness: { ...staleReadiness, inventoryHash: 'f'.repeat(64) }, - }), - /stale, mismatched/u, - ); + expect(staleReadiness).toBeDefined(); + if (staleReadiness === undefined) { + throw new Error('Expected readiness evidence'); + } + expect(() => + validateAuthorizationPromotionGate({ + ...stale, + readiness: { ...staleReadiness, inventoryHash: 'f'.repeat(64) }, + }), + ).toThrow(/stale, mismatched/u); }); -test('report-only promotion is bounded, explicit-baseline-only, and never allowed in production', () => { +it('report-only promotion is bounded, explicit-baseline-only, and never allowed in production', () => { const enforced = promotionFixture(); const withoutRequiredEvidence = withoutReadinessEvidence( withoutNegativeSmokeEvidence(withoutImpactEvidence(enforced)), @@ -619,28 +720,23 @@ test('report-only promotion is bounded, explicit-baseline-only, and never allowe nowEpochMs: Date.parse('2026-09-10T00:00:00.000Z'), rollout: { ...enforced.rollout, mode: 'report_only' as const }, }; - assert.equal(validateAuthorizationPromotionGate(reportOnly).status, 'observing'); - assert.throws( - () => validateAuthorizationPromotionGate({ ...reportOnly, environment: 'production' }), - /production.*report-only/u, - ); - assert.throws( - () => - validateAuthorizationPromotionGate({ - ...reportOnly, - nowEpochMs: Date.parse(reportOnly.rollout.expiresAt), - }), - /inactive or expired/u, - ); - assert.throws( - () => - validateAuthorizationPromotionGate({ - ...reportOnly, - rollout: { - ...reportOnly.rollout, - compatibilityEligibleEntrypoints: ['contacts.route.new'], - }, - }), - /unknown entrypoint/u, - ); + expect(validateAuthorizationPromotionGate(reportOnly).status).toBe('observing'); + expect(() => + validateAuthorizationPromotionGate({ ...reportOnly, environment: 'production' }), + ).toThrow(/production.*report-only/u); + expect(() => + validateAuthorizationPromotionGate({ + ...reportOnly, + nowEpochMs: Date.parse(reportOnly.rollout.expiresAt), + }), + ).toThrow(/inactive or expired/u); + expect(() => + validateAuthorizationPromotionGate({ + ...reportOnly, + rollout: { + ...reportOnly.rollout, + compatibilityEligibleEntrypoints: ['contacts.route.new'], + }, + }), + ).toThrow(/unknown entrypoint/u); }); diff --git a/app/scripts/tests/protected-entrypoint-inventory.test.mts b/app/scripts/tests/protected-entrypoint-inventory.test.mts index f8d7331d1..9fadec15e 100644 --- a/app/scripts/tests/protected-entrypoint-inventory.test.mts +++ b/app/scripts/tests/protected-entrypoint-inventory.test.mts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { makeProtectedEntrypointInventory, serializeProtectedEntrypointInventory, @@ -25,57 +24,46 @@ const entries = [ }, ]; -void test('inventory normalization, hashing, and serialization are deterministic', () => { +it('inventory normalization, hashing, and serialization are deterministic', () => { const left = makeProtectedEntrypointInventory('revision', entries); const right = makeProtectedEntrypointInventory('revision', [entries[1], entries[0]]); - assert.equal( - serializeProtectedEntrypointInventory(left), + expect(serializeProtectedEntrypointInventory(left)).toBe( serializeProtectedEntrypointInventory(right), ); - assert.match(left.inventoryHash, /^[a-f0-9]{64}$/u); - assert.deepEqual( - left.entries.map((entry) => entry.surface), - ['action', 'route'], - ); + expect(left.inventoryHash).toMatch(/^[a-f0-9]{64}$/u); + expect(left.entries.map((entry) => entry.surface)).toEqual(['action', 'route']); }); -void test('inventory rejects duplicate and unsafe entrypoint identities', () => { - assert.throws( - () => makeProtectedEntrypointInventory('revision', [...entries, entries[0]]), +it('inventory rejects duplicate and unsafe entrypoint identities', () => { + expect(() => makeProtectedEntrypointInventory('revision', [...entries, entries[0]])).toThrow( /duplicate protected entrypoint/u, ); - assert.throws( - () => - makeProtectedEntrypointInventory('revision', [ - { ...entries[0], entrypointKey: 'tenant@example.com' }, - ]), - /stable, non-sensitive identifier/u, - ); + expect(() => + makeProtectedEntrypointInventory('revision', [ + { ...entries[0], entrypointKey: 'tenant@example.com' }, + ]), + ).toThrow(/stable, non-sensitive identifier/u); }); -void test('inventory rejects malformed and excess authorization classification data', () => { +it('inventory rejects malformed and excess authorization classification data', () => { const authorizationWithExcessData = { kind: 'public' as const, permission: 'tenant.access', }; - assert.throws( - () => - makeProtectedEntrypointInventory('revision', [ - { - ...entries[0], - authorization: authorizationWithExcessData, - }, - ]), - /classification is invalid/u, - ); - assert.throws( - () => - makeProtectedEntrypointInventory('revision', [ - { - ...entries[0], - authorization: { kind: 'context_permission', permission: 'tenant@example.com' }, - }, - ]), - /classification is invalid/u, - ); + expect(() => + makeProtectedEntrypointInventory('revision', [ + { + ...entries[0], + authorization: authorizationWithExcessData, + }, + ]), + ).toThrow(/classification is invalid/u); + expect(() => + makeProtectedEntrypointInventory('revision', [ + { + ...entries[0], + authorization: { kind: 'context_permission', permission: 'tenant@example.com' }, + }, + ]), + ).toThrow(/classification is invalid/u); }); diff --git a/app/scripts/tests/provision-current-action-authorization.test.mts b/app/scripts/tests/provision-current-action-authorization.test.mts index 6964e6013..f92f67c27 100644 --- a/app/scripts/tests/provision-current-action-authorization.test.mts +++ b/app/scripts/tests/provision-current-action-authorization.test.mts @@ -1,12 +1,15 @@ -import { runEffectTestPromise } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; -import assert from 'node:assert/strict'; +import type { deriveOntosModuleDeploymentContract as DeriveModuleContract } from '../generate-ontos-module-contract.mts'; + +import { expect, it } from 'effect-rstest'; +import { NodeServices } from '@effect/platform-node'; + import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import { test } from 'node:test'; + import { pathToFileURL } from 'node:url'; import { v1 } from '@authzed/authzed-node'; -import { Effect, Option, Predicate, Schema } from 'effect'; +import { Cause, Effect, Option, Schema } from 'effect'; import { ACTION_AUTHORIZATION_DENIED_PRINCIPAL_ID, ActionAuthorizationProvisioningError, @@ -19,14 +22,14 @@ import type { } from '../../packages/core-runtime/src/install/action-authorization-provisioning.ts'; import { toSpiceDbActionObjectId } from '../../packages/core-runtime/src/permissions/service.ts'; import type { SpiceDbConfigValue } from '../../packages/core-runtime/src/permissions/config.ts'; -import { deriveOntosModuleDeploymentContract } from '../generate-ontos-module-contract.mts'; + import { LOCAL_DEVELOPMENT_CONTEXT } from '../initialize-local-development.mts'; import { - discoverCurrentActionKeys, formatActionAuthorizationProvisioningFailure, runCurrentActionAuthorizationProvisioning, selectActionAuthorizationProvisioningTarget, } from '../provision-current-action-authorization.mts'; +import type { discoverCurrentActionKeys as DiscoverCurrentActionKeys } from '../provision-current-action-authorization.mts'; const attachPersonEngagementAction = 'party.registry.attach-person-engagement'; const restrictedAction = 'core.identity.restricted'; @@ -97,147 +100,159 @@ const stageConfiguration: SpiceDbConfigValue = { const response = (permissionship: v1.CheckPermissionResponse_Permissionship) => v1.CheckPermissionResponse.create({ permissionship }); -const failureOf = async ( - effect: Effect.Effect, -) => await runEffectTestPromise(Effect.flip(effect)); - -const rejectionOf = async (promise: Promise): Promise => { - try { - await promise; - } catch (error) { - if (Predicate.isError(error)) { - return error; - } - return assert.fail('Expected the Promise to reject with an Error'); - } - return assert.fail('Expected the Promise to reject'); -}; +const failureOf = (effect: Effect.Effect) => + Effect.gen(function* testEffect1() { + return yield* Effect.flip(effect); + }); -void test('selects only exact source-controlled development and stage targets', async () => { - const development = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget(developmentConfiguration), +const rejectionOf = ( + effect: Effect.Effect, +) => + effect.pipe( + Effect.matchCause({ + onFailure: Cause.squash, + onSuccess: () => { + throw new Error('Expected the Effect to fail'); + }, + }), ); - assert.equal(development.environment, 'development'); - assert.deepEqual(development.contexts, [ - { - principalId: LOCAL_DEVELOPMENT_CONTEXT.principalId, - tenantId: LOCAL_DEVELOPMENT_CONTEXT.tenantId, - }, - ]); - const stage = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget(stageConfiguration), - ); - assert.equal(stage.environment, 'stage'); - assert.equal(stage.contexts.length, 2); +it.effect( + 'selects only exact source-controlled development and stage targets', + Effect.fn(function* testEffect2() { + const development = + yield* selectActionAuthorizationProvisioningTarget(developmentConfiguration); + expect(development.environment).toBe('development'); + expect(development.contexts).toEqual([ + { + principalId: LOCAL_DEVELOPMENT_CONTEXT.principalId, + tenantId: LOCAL_DEVELOPMENT_CONTEXT.tenantId, + }, + ]); - const { deploymentEnvironment: _environment, ...withoutEnvironment } = developmentConfiguration; - const implicitDevelopment = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget(withoutEnvironment), - ); - assert.deepEqual(implicitDevelopment.contexts, development.contexts); - assert.equal(implicitDevelopment.environment, 'development'); + const stage = yield* selectActionAuthorizationProvisioningTarget(stageConfiguration); + expect(stage.environment).toBe('stage'); + expect(stage.contexts.length).toBe(2); - const ipv6Development = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget({ + const { deploymentEnvironment: _environment, ...withoutEnvironment } = developmentConfiguration; + const implicitDevelopment = + yield* selectActionAuthorizationProvisioningTarget(withoutEnvironment); + expect(implicitDevelopment.contexts).toEqual(development.contexts); + expect(implicitDevelopment.environment).toBe('development'); + + const ipv6Development = yield* selectActionAuthorizationProvisioningTarget({ ...withoutEnvironment, endpoint: '[::1]:50051', - }), - ); - assert.equal(ipv6Development.environment, 'development'); - - await Promise.all( - [ - { ...developmentConfiguration, deploymentEnvironment: 'production' }, - { ...developmentConfiguration, endpoint: 'spicedb.example.com:50051', insecureLocal: false }, - { ...withoutEnvironment, endpoint: 'spicedb.example.com:50051' }, - { ...withoutEnvironment, endpoint: 'spicedb:50051' }, - { ...stageConfiguration, endpoint: 'localhost:50051' }, - { ...stageConfiguration, insecureLocal: false }, - ].map(async (configuration) => { - const error = await failureOf(selectActionAuthorizationProvisioningTarget(configuration)); - assert.equal(error.code, 'action_authorization_configuration_invalid'); - assert.doesNotMatch(error.reason, new RegExp(testPreSharedKey, 'u')); - }), - ); -}); + }); + expect(ipv6Development.environment).toBe('development'); -void test('reports expected provisioning failures and sanitizes unexpected Promise rejections', async () => { - const expected = new ActionAuthorizationProvisioningError({ - code: 'action_authorization_configuration_invalid', - reason: 'The SpiceDB provisioning configuration is invalid', - }); - const expectedRejection = await rejectionOf(runEffectTestPromise(Effect.fail(expected))); - assert.equal( - formatActionAuthorizationProvisioningFailure(expectedRejection), - `${expected.code}: ${expected.reason}`, - ); + yield* Effect.all( + [ + { ...developmentConfiguration, deploymentEnvironment: 'production' }, + { + ...developmentConfiguration, + endpoint: 'spicedb.example.com:50051', + insecureLocal: false, + }, + { ...withoutEnvironment, endpoint: 'spicedb.example.com:50051' }, + { ...withoutEnvironment, endpoint: 'spicedb:50051' }, + { ...stageConfiguration, endpoint: 'localhost:50051' }, + { ...stageConfiguration, insecureLocal: false }, + ].map((configuration) => + Effect.gen(function* testEffect3() { + const error = yield* failureOf( + selectActionAuthorizationProvisioningTarget(configuration), + ); + expect(error.code).toBe('action_authorization_configuration_invalid'); + expect(error.reason).not.toMatch(new RegExp(testPreSharedKey, 'u')); + }), + ), + { concurrency: 'unbounded' }, + ); + }), +); + +it.effect( + 'reports expected provisioning failures and sanitizes unexpected Promise rejections', + Effect.fn(function* testEffect4() { + const expected = new ActionAuthorizationProvisioningError({ + code: 'action_authorization_configuration_invalid', + reason: 'The SpiceDB provisioning configuration is invalid', + }); + const expectedRejection = yield* rejectionOf(Effect.fail(expected)); + expect(formatActionAuthorizationProvisioningFailure(expectedRejection)).toBe( + `${expected.code}: ${expected.reason}`, + ); - const unexpectedMessage = - 'action_authorization_service_unavailable: Unexpected Action authorization provisioning failure'; - for (const error of [undefined, null, testPreSharedKey, new Error(testPreSharedKey), {}]) { - assert.equal(formatActionAuthorizationProvisioningFailure(error), unexpectedMessage); - } - const unexpectedRejection = await rejectionOf( - runEffectTestPromise( + const unexpectedMessage = + 'action_authorization_service_unavailable: Unexpected Action authorization provisioning failure'; + for (const error of [undefined, null, testPreSharedKey, new Error(testPreSharedKey), {}]) { + expect(formatActionAuthorizationProvisioningFailure(error)).toBe(unexpectedMessage); + } + const unexpectedRejection = yield* rejectionOf( Effect.acquireUseRelease( Effect.void, () => Effect.void, () => Effect.die(new Error(`client.close failed with ${testPreSharedKey}`)), ), - ), - ); - assert.equal( - formatActionAuthorizationProvisioningFailure(unexpectedRejection), - unexpectedMessage, - ); -}); - -void test('workspace validation rejects both provisioning spellings in every automatic startup path', async () => { - const source = await readFile( - new URL('../validate-ultramodern-workspace.mts', import.meta.url), - 'utf-8', - ); - // Execute the actual validator block with controlled inputs, without loading the full workspace. - const start = source.indexOf('const actionAuthorizationProvisioningCommand ='); - const end = source.indexOf('if (hasBackendSurfaces)', start); - assert.ok(start !== -1 && end > start); - const block = source.slice(start, end); - const scripts = { - 'authorization:provision-current-actions': - 'node ./scripts/provision-current-action-authorization.mts', - 'local:initialize': 'node ./scripts/initialize-local-development.mts', - }; - const validationRoot = await mkdtemp(path.join(os.tmpdir(), 'ontos-workspace-validation-')); - let validationIndex = 0; - const validate = async ( - sources: Readonly>, - overrides: Readonly> = {}, - ): Promise => { - const modulePath = path.join(validationRoot, `validation-${validationIndex}.mjs`); - validationIndex += 1; - await writeFile( - modulePath, - [ - "import assert from 'node:assert/strict';", - `const sources = ${JSON.stringify(sources)};`, - "const readText = (file) => sources[file] ?? '';", - `const rootPackage = ${JSON.stringify({ scripts: { ...scripts, ...overrides } })};`, - "const SHARED_VALIDATOR_STRING_053 = 'authorization:provision-current-actions';", - "const SHARED_VALIDATOR_STRING_059 = 'cloudflare:build';", - "const SHARED_VALIDATOR_STRING_060 = 'cloudflare:deploy';", - "const SHARED_VALIDATOR_STRING_106 = 'provision-current-action-authorization';", - 'const valueForKey = (entries, key) => entries.find(([candidate]) => candidate === key)?.[1];', - block, - ].join('\n'), - 'utf-8', ); - await import(pathToFileURL(modulePath).href); - }; + expect(formatActionAuthorizationProvisioningFailure(unexpectedRejection)).toBe( + unexpectedMessage, + ); + }), +); + +it.effect( + 'workspace validation rejects both provisioning spellings in every automatic startup path', + Effect.fn(function* testEffect5() { + const source = yield* Effect.tryPromise(() => + readFile(new URL('../validate-ultramodern-workspace.mts', import.meta.url), 'utf-8'), + ); + // Execute the actual validator block with controlled inputs, without loading the full workspace. + const start = source.indexOf('const actionAuthorizationProvisioningCommand ='); + const end = source.indexOf('if (hasBackendSurfaces)', start); + expect(start !== -1 && end > start).toBe(true); + const block = source.slice(start, end); + const scripts = { + 'authorization:provision-current-actions': + 'node ./scripts/provision-current-action-authorization.mts', + 'local:initialize': 'node ./scripts/initialize-local-development.mts', + }; + const validationRoot = yield* Effect.acquireRelease( + Effect.tryPromise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-workspace-validation-'))), + (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), + ); + let validationIndex = 0; + const validate = ( + sources: Readonly>, + overrides: Readonly> = {}, + ) => + Effect.gen(function* testEffect6() { + const modulePath = path.join(validationRoot, `validation-${validationIndex}.mjs`); + validationIndex += 1; + yield* Effect.tryPromise(() => + writeFile( + modulePath, + [ + "import assert from 'node:assert/strict';", + `const sources = ${JSON.stringify(sources)};`, + "const readText = (file) => sources[file] ?? '';", + `const rootPackage = ${JSON.stringify({ scripts: { ...scripts, ...overrides } })};`, + "const SHARED_VALIDATOR_STRING_053 = 'authorization:provision-current-actions';", + "const SHARED_VALIDATOR_STRING_059 = 'cloudflare:build';", + "const SHARED_VALIDATOR_STRING_060 = 'cloudflare:deploy';", + "const SHARED_VALIDATOR_STRING_106 = 'provision-current-action-authorization';", + 'const valueForKey = (entries, key) => entries.find(([candidate]) => candidate === key)?.[1];', + block, + ].join('\n'), + 'utf-8', + ), + ); + yield* Effect.tryPromise(() => import(pathToFileURL(modulePath).href)); + }); - try { - await validate({}); - const validationPromises: Promise[] = []; + yield* validate({}); + const validations: Effect.Effect[] = []; for (const command of [ 'node ./scripts/provision-current-action-authorization.mts', 'pnpm authorization:provision-current-actions', @@ -248,88 +263,113 @@ void test('workspace validation rejects both provisioning spellings in every aut 'docker-compose.yml', 'scripts/run-zerops-spicedb.sh', ]) { - validationPromises.push( - assert.rejects(validate({ [file]: command }), /must not provision Action authorization/u), + validations.push( + validate({ [file]: command }).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error.cause; + }).toThrow(/must not provision Action authorization/u), + ), + ), ); } for (const automaticScript of ['dev', 'build', 'cloudflare:build', 'cloudflare:deploy']) { - validationPromises.push( - assert.rejects( - validate({}, { [automaticScript]: command }), - /must not invoke Action authorization provisioning/u, + validations.push( + validate({}, { [automaticScript]: command }).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error.cause; + }).toThrow(/must not invoke Action authorization provisioning/u), + ), ), ); } - validationPromises.push( - assert.rejects( - validate( - {}, - { - 'local:initialize': `${scripts['local:initialize']} && ${command}`, - }, + validations.push( + validate( + {}, + { + 'local:initialize': `${scripts['local:initialize']} && ${command}`, + }, + ).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error.cause; + }).toThrow(/must not provision Action authorization/u), ), - /must not provision Action authorization/u, ), ); } - await Promise.all(validationPromises); - } finally { - await rm(validationRoot, { recursive: true }); - } -}); - -void test('discovers exactly the current generated Core and Party Registry Action baseline', async () => { - const workspaceRoot = path.resolve(import.meta.dirname, '../..'); - assert.deepEqual(await discoverCurrentActionKeys(workspaceRoot), currentActionKeys); - assert.equal(new Set(currentActionKeys).size, 38); - assert.equal(currentActionKeys.filter((key) => key.startsWith('core.')).length, 8); - assert.equal(currentActionKeys.filter((key) => key.startsWith('party.registry.')).length, 30); -}); - -void test('builds lossless, deterministic Tenant-membership grants for development and stage', async () => { - const development = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget(developmentConfiguration), - ); - const stage = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget(stageConfiguration), - ); - const developmentRelationships = buildActionAuthorizationRelationships( - currentActionKeys, - development.contexts, - ); - const stageRelationships = buildActionAuthorizationRelationships( - currentActionKeys, - stage.contexts, - ); + yield* Effect.all(validations, { concurrency: 'unbounded' }); + }), +); + +it.effect( + 'discovers exactly the current generated Core and Party Registry Action baseline', + Effect.fn(function* testEffect7() { + const workspaceRoot = path.resolve(import.meta.dirname, '../..'); + const { discoverCurrentActionKeys } = yield* Effect.promise( + (): Promise<{ readonly discoverCurrentActionKeys: typeof DiscoverCurrentActionKeys }> => + import( + pathToFileURL( + path.resolve(import.meta.dirname, '../provision-current-action-authorization.mts'), + ).href + ), + ); + expect( + yield* discoverCurrentActionKeys(workspaceRoot).pipe(Effect.provide(NodeServices.layer)), + ).toEqual(currentActionKeys); + expect(new Set(currentActionKeys).size).toBe(38); + expect(currentActionKeys.filter((key) => key.startsWith('core.')).length).toBe(8); + expect(currentActionKeys.filter((key) => key.startsWith('party.registry.')).length).toBe(30); + }), +); + +it.effect( + 'builds lossless, deterministic Tenant-membership grants for development and stage', + Effect.fn(function* testEffect8() { + const development = + yield* selectActionAuthorizationProvisioningTarget(developmentConfiguration); + const stage = yield* selectActionAuthorizationProvisioningTarget(stageConfiguration); + const developmentRelationships = buildActionAuthorizationRelationships( + currentActionKeys, + development.contexts, + ); + const stageRelationships = buildActionAuthorizationRelationships( + currentActionKeys, + stage.contexts, + ); - assert.equal(developmentRelationships.length, 38); - assert.equal(stageRelationships.length, 76); - for (const relationship of [...developmentRelationships, ...stageRelationships]) { - assert.equal(relationship.relation, 'executor'); - assert.equal(relationship.resource?.objectType, 'action'); - assert.equal(relationship.subject?.object?.objectType, 'tenant'); - assert.equal(relationship.subject?.optionalRelation, 'member'); - } - const identifiers = stageRelationships.map( - ({ resource, subject }) => `${resource?.objectId}:${subject?.object?.objectId}`, - ); - assert.ok( - identifiers.every( - (identifier, index) => index === 0 || identifiers[index - 1]?.localeCompare(identifier) <= 0, - ), - ); - assert.equal( - Buffer.from( - toSpiceDbActionObjectId(attachPersonEngagementAction).slice(3), - 'base64url', - ).toString('utf-8'), - attachPersonEngagementAction, - ); - assert.notEqual( - toSpiceDbActionObjectId(attachPersonEngagementAction), - toSpiceDbActionObjectId('contacts-core-attach-person-engagement'), - ); -}); + expect(developmentRelationships.length).toBe(38); + expect(stageRelationships.length).toBe(76); + for (const relationship of [...developmentRelationships, ...stageRelationships]) { + expect(relationship.relation).toBe('executor'); + expect(relationship.resource?.objectType).toBe('action'); + expect(relationship.subject?.object?.objectType).toBe('tenant'); + expect(relationship.subject?.optionalRelation).toBe('member'); + } + const identifiers = stageRelationships.map( + ({ resource, subject }) => `${resource?.objectId}:${subject?.object?.objectId}`, + ); + expect( + identifiers.every( + (identifier, index) => + index === 0 || identifiers[index - 1]?.localeCompare(identifier) <= 0, + ), + ).toBe(true); + expect( + Buffer.from( + toSpiceDbActionObjectId(attachPersonEngagementAction).slice(3), + 'base64url', + ).toString('utf-8'), + ).toBe(attachPersonEngagementAction); + expect(toSpiceDbActionObjectId(attachPersonEngagementAction)).not.toBe( + toSpiceDbActionObjectId('contacts-core-attach-person-engagement'), + ); + }), +); interface ProvisioningClientState { readonly grants: Set; @@ -408,46 +448,46 @@ const makeProvisioningClient = ( }; }; -void test('provisions with TOUCH, verifies both outcomes, and is safe to rerun', async () => { - const target = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget(developmentConfiguration), - ); - const { client, state } = makeProvisioningClient(target.contexts); - const input = { actions: currentActions, contexts: target.contexts }; - - const first = await runEffectTestPromise(provisionActionAuthorization(client, input)); - const second = await runEffectTestPromise(provisionActionAuthorization(client, input)); - - assert.deepEqual(first, { actionCount: 38, grantCount: 38, tenantCount: 1 }); - assert.deepEqual(second, first); - assert.equal(state.schemaWriteCount, 2); - assert.equal(state.relationshipWriteCount, 2); - assert.equal(state.grants.size, 38); - assert.equal(state.updates.length, 76); - assert.ok( - state.updates.every(({ operation }) => operation === v1.RelationshipUpdate_Operation.TOUCH), - ); - assert.ok( - ![...state.grants].some((grant) => grant.includes(ACTION_AUTHORIZATION_DENIED_PRINCIPAL_ID)), - ); -}); - -void test('never grants explicit Actions through Tenant membership and verifies recorded policy outcomes', async () => { - const target = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget(developmentConfiguration), - ); - const [context] = target.contexts; - assert.ok(context !== undefined); - const deniedContext = { - principalId: '00000000-0000-4000-8000-000000000020', - tenantId: '00000000-0000-4000-8000-000000000021', - }; - const contexts = [...target.contexts, deniedContext]; - const { client, state } = makeProvisioningClient(contexts); - state.grants.add(`${toSpiceDbActionObjectId(restrictedAction)}:${context.principalId}`); - - const result = await runEffectTestPromise( - provisionActionAuthorization(client, { +it.effect( + 'provisions with TOUCH, verifies both outcomes, and is safe to rerun', + Effect.fn(function* testEffect9() { + const target = yield* selectActionAuthorizationProvisioningTarget(developmentConfiguration); + const { client, state } = makeProvisioningClient(target.contexts); + const input = { actions: currentActions, contexts: target.contexts }; + + const first = yield* provisionActionAuthorization(client, input); + const second = yield* provisionActionAuthorization(client, input); + + expect(first).toEqual({ actionCount: 38, grantCount: 38, tenantCount: 1 }); + expect(second).toEqual(first); + expect(state.schemaWriteCount).toBe(2); + expect(state.relationshipWriteCount).toBe(2); + expect(state.grants.size).toBe(38); + expect(state.updates.length).toBe(76); + expect( + state.updates.every(({ operation }) => operation === v1.RelationshipUpdate_Operation.TOUCH), + ).toBe(true); + expect( + ![...state.grants].some((grant) => grant.includes(ACTION_AUTHORIZATION_DENIED_PRINCIPAL_ID)), + ).toBe(true); + }), +); + +it.effect( + 'never grants explicit Actions through Tenant membership and verifies recorded policy outcomes', + Effect.fn(function* testEffect10() { + const target = yield* selectActionAuthorizationProvisioningTarget(developmentConfiguration); + const [context] = target.contexts; + expect(context !== undefined).toBe(true); + const deniedContext = { + principalId: '00000000-0000-4000-8000-000000000020', + tenantId: '00000000-0000-4000-8000-000000000021', + }; + const contexts = [...target.contexts, deniedContext]; + const { client, state } = makeProvisioningClient(contexts); + state.grants.add(`${toSpiceDbActionObjectId(restrictedAction)}:${context.principalId}`); + + const result = yield* provisionActionAuthorization(client, { actions: [ { actionKey: attachPersonEngagementAction, @@ -465,233 +505,306 @@ void test('never grants explicit Actions through Tenant membership and verifies ], }, ], - }), - ); - - assert.deepEqual(result, { actionCount: 2, grantCount: 2, tenantCount: 2 }); - assert.equal(state.updates.length, 2); - assert.equal( - state.updates[0]?.relationship?.resource?.objectId, - toSpiceDbActionObjectId(attachPersonEngagementAction), - ); -}); + }); -void test('rejects missing or mismatched explicit Action verification assertions', async () => { - const target = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget(developmentConfiguration), - ); - await Promise.all( - [ - undefined, - [], - [ - { - actionKey: restrictedAction, - assertions: [ - { expected: 'allowed' as const, principalId: target.contexts[0]?.principalId ?? '' }, - ], - }, - ], + expect(result).toEqual({ actionCount: 2, grantCount: 2, tenantCount: 2 }); + expect(state.updates.length).toBe(2); + expect(state.updates[0]?.relationship?.resource?.objectId).toBe( + toSpiceDbActionObjectId(attachPersonEngagementAction), + ); + }), +); + +it.effect( + 'rejects missing or mismatched explicit Action verification assertions', + Effect.fn(function* testEffect11() { + const target = yield* selectActionAuthorizationProvisioningTarget(developmentConfiguration); + yield* Effect.all( [ - { - actionKey: 'core.identity.unknown', - assertions: [ - { expected: 'allowed' as const, principalId: target.contexts[0]?.principalId ?? '' }, - { expected: 'denied' as const, principalId: ACTION_AUTHORIZATION_DENIED_PRINCIPAL_ID }, - ], - }, - ], - ].map(async (explicitActionAssertions) => { - const { client, state } = makeProvisioningClient(target.contexts); - const error = await failureOf( - provisionActionAuthorization(client, { - actions: [{ actionKey: restrictedAction, provisioning: 'explicit' }], - contexts: target.contexts, - explicitActionAssertions, + undefined, + [], + [ + { + actionKey: restrictedAction, + assertions: [ + { expected: 'allowed' as const, principalId: target.contexts[0]?.principalId ?? '' }, + ], + }, + ], + [ + { + actionKey: 'core.identity.unknown', + assertions: [ + { expected: 'allowed' as const, principalId: target.contexts[0]?.principalId ?? '' }, + { + expected: 'denied' as const, + principalId: ACTION_AUTHORIZATION_DENIED_PRINCIPAL_ID, + }, + ], + }, + ], + ].map((explicitActionAssertions) => + Effect.gen(function* testEffect12() { + const { client, state } = makeProvisioningClient(target.contexts); + const error = yield* failureOf( + provisionActionAuthorization(client, { + actions: [{ actionKey: restrictedAction, provisioning: 'explicit' }], + contexts: target.contexts, + explicitActionAssertions, + }), + ); + expect(error.code).toBe('action_authorization_input_invalid'); + expect(state.schemaWriteCount).toBe(0); + expect(state.relationshipWriteCount).toBe(0); }), - ); - assert.equal(error.code, 'action_authorization_input_invalid'); - assert.equal(state.schemaWriteCount, 0); - assert.equal(state.relationshipWriteCount, 0); - }), - ); -}); - -void test('fails promotion when an explicit Action policy contradicts a recorded assertion', async () => { - const target = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget(developmentConfiguration), - ); - const [context] = target.contexts; - assert.ok(context !== undefined); - const deniedPrincipalId = ACTION_AUTHORIZATION_DENIED_PRINCIPAL_ID; - const assertions = [ - { expected: 'allowed' as const, principalId: context.principalId }, - { expected: 'denied' as const, principalId: deniedPrincipalId }, - ]; - - await Promise.all( - [[], [context.principalId, deniedPrincipalId]].map(async (actualAllowedPrincipalIds) => { - const { client, state } = makeProvisioningClient(target.contexts); - for (const principalId of actualAllowedPrincipalIds) { - state.grants.add(`${toSpiceDbActionObjectId(restrictedAction)}:${principalId}`); - } - const error = await failureOf( - provisionActionAuthorization(client, { - actions: [{ actionKey: restrictedAction, provisioning: 'explicit' }], - contexts: target.contexts, - explicitActionAssertions: [{ actionKey: restrictedAction, assertions }], + ), + { concurrency: 'unbounded' }, + ); + }), +); + +it.effect( + 'fails promotion when an explicit Action policy contradicts a recorded assertion', + Effect.fn(function* testEffect13() { + const target = yield* selectActionAuthorizationProvisioningTarget(developmentConfiguration); + const [context] = target.contexts; + expect(context !== undefined).toBe(true); + const deniedPrincipalId = ACTION_AUTHORIZATION_DENIED_PRINCIPAL_ID; + const assertions = [ + { expected: 'allowed' as const, principalId: context.principalId }, + { expected: 'denied' as const, principalId: deniedPrincipalId }, + ]; + + yield* Effect.all( + [[], [context.principalId, deniedPrincipalId]].map((actualAllowedPrincipalIds) => + Effect.gen(function* testEffect14() { + const { client, state } = makeProvisioningClient(target.contexts); + for (const principalId of actualAllowedPrincipalIds) { + state.grants.add(`${toSpiceDbActionObjectId(restrictedAction)}:${principalId}`); + } + const error = yield* failureOf( + provisionActionAuthorization(client, { + actions: [{ actionKey: restrictedAction, provisioning: 'explicit' }], + contexts: target.contexts, + explicitActionAssertions: [{ actionKey: restrictedAction, assertions }], + }), + ); + expect(error.code).toBe('action_authorization_verification_failed'); + expect(state.updates.length).toBe(0); }), - ); - assert.equal(error.code, 'action_authorization_verification_failed'); - assert.equal(state.updates.length, 0); - }), - ); -}); - -void test('rejects invalid input and missing membership before writing grants', async () => { - const target = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget(developmentConfiguration), - ); - const { client, state } = makeProvisioningClient([]); - const missingMembership = await failureOf( - provisionActionAuthorization(client, { - actions: currentActions, - contexts: target.contexts, - }), - ); - assert.equal(missingMembership.code, 'action_authorization_membership_missing'); - assert.equal(state.schemaWriteCount, 1); - assert.equal(state.relationshipWriteCount, 0); - - const duplicate = await failureOf( - provisionActionAuthorization(client, { - actions: [ - { - actionKey: attachPersonEngagementAction, - provisioning: 'tenant_membership_default', - }, - { - actionKey: attachPersonEngagementAction, - provisioning: 'tenant_membership_default', - }, - ], - contexts: target.contexts, - }), - ); - assert.equal(duplicate.code, 'action_authorization_input_invalid'); - assert.equal(state.schemaWriteCount, 1); -}); - -void test('fails closed when authorization returns no permission response', async () => { - const target = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget(developmentConfiguration), - ); - const { client } = makeProvisioningClient(target.contexts); - const noResponseClient: ActionAuthorizationProvisioningClient = { - ...client, - checkPermission: () => Effect.succeed(Option.none()), - }; - const error = await failureOf( - provisionActionAuthorization(noResponseClient, { - actions: currentActions, - contexts: target.contexts, - }), - ); - assert.equal(error.code, 'action_authorization_membership_missing'); -}); - -void test('sanitizes authorization service failures', async () => { - const secret = 'super-secret-credential'; - const upstreamFailure = new Error(secret); - const target = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget(developmentConfiguration), - ); - const unavailable: ActionAuthorizationProvisioningClient = { - checkPermission: () => - Effect.succeed( - Option.some(response(v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION)), ), - writeRelationships: () => Effect.succeed(v1.WriteRelationshipsResponse.create({})), - writeSchema: () => Effect.fail(upstreamFailure), - }; - const error = await failureOf( - provisionActionAuthorization(unavailable, { - actions: currentActions, - contexts: target.contexts, - }), - ); - assert.equal(error.code, 'action_authorization_service_unavailable'); - assert.doesNotMatch(error.reason, new RegExp(secret, 'u')); - assert.equal(decodeProvisioningFailureCause(error).cause, upstreamFailure); -}); + { concurrency: 'unbounded' }, + ); + }), +); + +it.effect( + 'rejects invalid input and missing membership before writing grants', + Effect.fn(function* testEffect15() { + const target = yield* selectActionAuthorizationProvisioningTarget(developmentConfiguration); + const { client, state } = makeProvisioningClient([]); + const missingMembership = yield* failureOf( + provisionActionAuthorization(client, { + actions: currentActions, + contexts: target.contexts, + }), + ); + expect(missingMembership.code).toBe('action_authorization_membership_missing'); + expect(state.schemaWriteCount).toBe(1); + expect(state.relationshipWriteCount).toBe(0); + + const duplicate = yield* failureOf( + provisionActionAuthorization(client, { + actions: [ + { + actionKey: attachPersonEngagementAction, + provisioning: 'tenant_membership_default', + }, + { + actionKey: attachPersonEngagementAction, + provisioning: 'tenant_membership_default', + }, + ], + contexts: target.contexts, + }), + ); + expect(duplicate.code).toBe('action_authorization_input_invalid'); + expect(state.schemaWriteCount).toBe(1); + }), +); + +it.effect( + 'fails closed when authorization returns no permission response', + Effect.fn(function* testEffect16() { + const target = yield* selectActionAuthorizationProvisioningTarget(developmentConfiguration); + const { client } = makeProvisioningClient(target.contexts); + const noResponseClient: ActionAuthorizationProvisioningClient = { + ...client, + checkPermission: () => Effect.succeed(Option.none()), + }; + const error = yield* failureOf( + provisionActionAuthorization(noResponseClient, { + actions: currentActions, + contexts: target.contexts, + }), + ); + expect(error.code).toBe('action_authorization_membership_missing'); + }), +); + +it.effect( + 'sanitizes authorization service failures', + Effect.fn(function* testEffect17() { + const secret = 'super-secret-credential'; + const upstreamFailure = new Error(secret); + const target = yield* selectActionAuthorizationProvisioningTarget(developmentConfiguration); + const unavailable: ActionAuthorizationProvisioningClient = { + checkPermission: () => + Effect.succeed( + Option.some(response(v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION)), + ), + writeRelationships: () => Effect.succeed(v1.WriteRelationshipsResponse.create({})), + writeSchema: () => Effect.fail(upstreamFailure), + }; + const error = yield* failureOf( + provisionActionAuthorization(unavailable, { + actions: currentActions, + contexts: target.contexts, + }), + ); + expect(error.code).toBe('action_authorization_service_unavailable'); + expect(error.reason).not.toMatch(new RegExp(secret, 'u')); + expect(decodeProvisioningFailureCause(error).cause).toBe(upstreamFailure); + }), +); -const writeInventory = async ( +const writeInventory = ( root: string, verticals: readonly { readonly id: string; readonly package: string; readonly path: string }[], -) => { - await mkdir(path.join(root, 'topology'), { recursive: true }); - await Promise.all([ - writeFile(path.join(root, 'topology/reference-topology.json'), JSON.stringify({ verticals })), - writeFile(path.join(root, 'topology/ownership.json'), JSON.stringify({ owners: verticals })), - ]); -}; - -void test('rejects incomplete and duplicate public Action discovery', async () => { - const workspaceRoot = path.resolve(import.meta.dirname, '../..'); - const currentContract = await deriveOntosModuleDeploymentContract({ - vertical: 'party-registry', - workspaceRoot, +) => + Effect.gen(function* testEffect18() { + yield* Effect.tryPromise(() => mkdir(path.join(root, 'topology'), { recursive: true })); + yield* Effect.all( + [ + Effect.promise(() => + writeFile( + path.join(root, 'topology/reference-topology.json'), + JSON.stringify({ verticals }), + ), + ), + Effect.promise(() => + writeFile( + path.join(root, 'topology/ownership.json'), + JSON.stringify({ owners: verticals }), + ), + ), + ], + { concurrency: 'unbounded' }, + ); }); - const [currentPublicAction] = currentContract.manifest.publicSurface.actions; - assert.ok(currentPublicAction !== undefined); - const root = await mkdtemp(path.join(os.tmpdir(), 'ontos-action-discovery-')); - try { + +it.effect( + 'rejects incomplete and duplicate public Action discovery', + Effect.fn(function* testEffect19() { + const workspaceRoot = path.resolve(import.meta.dirname, '../..'); + // Native discovery imports registrations dynamically; keep its private registry in one module instance. + const { discoverCurrentActionKeys } = yield* Effect.promise( + (): Promise<{ readonly discoverCurrentActionKeys: typeof DiscoverCurrentActionKeys }> => + import( + pathToFileURL( + path.resolve(import.meta.dirname, '../provision-current-action-authorization.mts'), + ).href + ), + ); + const { deriveOntosModuleDeploymentContract } = yield* Effect.promise( + (): Promise<{ readonly deriveOntosModuleDeploymentContract: typeof DeriveModuleContract }> => + import( + pathToFileURL(path.resolve(import.meta.dirname, '../generate-ontos-module-contract.mts')) + .href + ), + ); + const { ActionAuthorizationProvisioningError: NativeProvisioningError } = yield* Effect.promise( + (): Promise<{ + readonly ActionAuthorizationProvisioningError: typeof ActionAuthorizationProvisioningError; + }> => + import( + pathToFileURL( + path.resolve( + import.meta.dirname, + '../../packages/core-runtime/src/install/action-authorization-provisioning.ts', + ), + ).href + ), + ); + const currentContract = yield* deriveOntosModuleDeploymentContract({ + vertical: 'party-registry', + workspaceRoot, + }).pipe(Effect.provide(NodeServices.layer)); + const [currentPublicAction] = currentContract.manifest.publicSurface.actions; + expect(currentPublicAction !== undefined).toBe(true); + const root = yield* Effect.acquireRelease( + Effect.tryPromise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-action-discovery-'))), + (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), + ); const vertical = { id: 'example', package: '@app/example', path: 'verticals/example' }; - await writeInventory(root, [vertical]); - const incomplete: typeof deriveOntosModuleDeploymentContract = async () => ({ - ...currentContract, - deployment: { ...currentContract.deployment, appId: 'example' }, - manifest: { - ...currentContract.manifest, - publicSurface: { ...currentContract.manifest.publicSurface, actions: [] }, - }, - }); - const incompleteError = await rejectionOf(discoverCurrentActionKeys(root, incomplete)); - assert.ok(incompleteError instanceof ActionAuthorizationProvisioningError); - assert.equal(incompleteError.code, 'action_authorization_discovery_failed'); - - const duplicate: typeof deriveOntosModuleDeploymentContract = async () => ({ - ...currentContract, - deployment: { ...currentContract.deployment, appId: 'example' }, - manifest: { - ...currentContract.manifest, - publicSurface: { - ...currentContract.manifest.publicSurface, - actions: [{ ...currentPublicAction, actionKey: 'core.identity.bind-managed-api-key' }], + yield* writeInventory(root, [vertical]); + const incomplete: typeof deriveOntosModuleDeploymentContract = () => + Effect.succeed({ + ...currentContract, + deployment: { ...currentContract.deployment, appId: 'example' }, + manifest: { + ...currentContract.manifest, + publicSurface: { ...currentContract.manifest.publicSurface, actions: [] }, }, - }, - }); - const duplicateError = await rejectionOf(discoverCurrentActionKeys(root, duplicate)); - assert.ok(duplicateError instanceof ActionAuthorizationProvisioningError); - assert.equal(duplicateError.code, 'action_authorization_discovery_failed'); - - await writeInventory(root, [vertical, vertical]); - await assert.rejects( - discoverCurrentActionKeys(root, duplicate), - ActionAuthorizationProvisioningError, + }); + const incompleteError = yield* rejectionOf( + discoverCurrentActionKeys(root, incomplete).pipe(Effect.provide(NodeServices.layer)), ); - } finally { - await rm(root, { recursive: true }); - } -}); - -void test('the operator entrypoint rejects every command-line argument before loading configuration', async () => { - const error = await failureOf( - runCurrentActionAuthorizationProvisioning(path.resolve(import.meta.dirname, '../..'), [ - '--tenant', - 'arbitrary', - ]), - ); - assert.equal(error.code, 'action_authorization_configuration_invalid'); - assert.match(error.reason, /no command-line arguments/u); -}); + expect(Schema.is(NativeProvisioningError)(incompleteError)).toBe(true); + expect(Schema.decodeUnknownSync(NativeProvisioningError)(incompleteError).code).toBe( + 'action_authorization_discovery_failed', + ); + + const duplicate: typeof deriveOntosModuleDeploymentContract = () => + Effect.succeed({ + ...currentContract, + deployment: { ...currentContract.deployment, appId: 'example' }, + manifest: { + ...currentContract.manifest, + publicSurface: { + ...currentContract.manifest.publicSurface, + actions: [{ ...currentPublicAction, actionKey: 'core.identity.bind-managed-api-key' }], + }, + }, + }); + const duplicateError = yield* rejectionOf( + discoverCurrentActionKeys(root, duplicate).pipe(Effect.provide(NodeServices.layer)), + ); + expect(Schema.is(NativeProvisioningError)(duplicateError)).toBe(true); + expect(Schema.decodeUnknownSync(NativeProvisioningError)(duplicateError).code).toBe( + 'action_authorization_discovery_failed', + ); + + yield* writeInventory(root, [vertical, vertical]); + const duplicateInventoryError = yield* discoverCurrentActionKeys(root, duplicate).pipe( + Effect.provide(NodeServices.layer), + Effect.flip, + ); + expect(Schema.is(NativeProvisioningError)(duplicateInventoryError)).toBe(true); + expect(duplicateInventoryError.code).toBe('action_authorization_discovery_failed'); + }), +); + +it.effect( + 'the operator entrypoint rejects every command-line argument before loading configuration', + Effect.fn(function* testEffect20() { + const error = yield* failureOf( + runCurrentActionAuthorizationProvisioning(path.resolve(import.meta.dirname, '../..'), [ + '--tenant', + 'arbitrary', + ]), + ); + expect(error.code).toBe('action_authorization_configuration_invalid'); + expect(error.reason).toMatch(/no command-line arguments/u); + }), +); diff --git a/app/scripts/tests/quality-audit-count-domain.test.mts b/app/scripts/tests/quality-audit-count-domain.test.mts index d1bb73dd2..a9bc19df6 100644 --- a/app/scripts/tests/quality-audit-count-domain.test.mts +++ b/app/scripts/tests/quality-audit-count-domain.test.mts @@ -1,12 +1,10 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Effect, Result, Schema } from 'effect'; import { KnipModelEvidenceSchema } from '../../quality-audit/knip-model.mts'; -import { makeEffectTestCallback } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; import { validateReport } from '../quality-audit.mts'; import { validateQualityAuditSummary } from '../quality-audit-gate.mts'; -void test('audit evidence requires finite nonnegative integer source positions', () => { +it('audit evidence requires finite nonnegative integer source positions', () => { const evidence = { kind: 'entry', line: 0, @@ -16,32 +14,29 @@ void test('audit evidence requires finite nonnegative integer source positions', workspace: '.', }; const valid = Schema.is(KnipModelEvidenceSchema); - assert.equal(valid(evidence), true); - assert.equal(valid({ ...evidence, column: 0, line: 1 }), true); + expect(valid(evidence)).toBe(true); + expect(valid({ ...evidence, column: 0, line: 1 })).toBe(true); for (const value of [Number.NaN, Infinity, -Infinity, -1, 0.5]) { - assert.equal(valid({ ...evidence, line: value }), false); - assert.equal(valid({ ...evidence, column: value }), false); + expect(valid({ ...evidence, line: value })).toBe(false); + expect(valid({ ...evidence, column: value })).toBe(false); } }); -void test( - 'audit and gate reject nonfinite, negative and fractional report counts', - makeEffectTestCallback( - Effect.gen(function* invalidCountReports() { - for (const count of ['1e400', '-1e400', '-1', '0.5']) { - const audit = yield* validateReport( - 'jscpd', - `{"duplicates":[],"statistics":{"total":{"clones":0,"sources":${count}}}}`, - ).pipe(Effect.result); - assert.equal(Result.isFailure(audit), true); - const gate = yield* validateQualityAuditSummary( - `{"status":"reported","results":[{"name":"jscpd","status":"reported","diagnostic":"","advisory":false,"files":${count},"findings":0,"coverage":{"tokenEligibleFiles":${count}}}]}`, - ).pipe(Effect.result); - assert.equal(Result.isFailure(gate), true); - if (Result.isFailure(gate)) { - assert.match(gate.failure.message, /Malformed audit summary/u); - } +it.effect('audit and gate reject nonfinite, negative and fractional report counts', () => + Effect.gen(function* invalidCountReports() { + for (const count of ['1e400', '-1e400', '-1', '0.5']) { + const audit = yield* validateReport( + 'jscpd', + `{"duplicates":[],"statistics":{"total":{"clones":0,"sources":${count}}}}`, + ).pipe(Effect.result); + expect(Result.isFailure(audit)).toBe(true); + const gate = yield* validateQualityAuditSummary( + `{"status":"reported","results":[{"name":"jscpd","status":"reported","diagnostic":"","advisory":false,"files":${count},"findings":0,"coverage":{"tokenEligibleFiles":${count}}}]}`, + ).pipe(Effect.result); + expect(Result.isFailure(gate)).toBe(true); + if (Result.isFailure(gate)) { + expect(gate.failure.message).toMatch(/Malformed audit summary/u); } - }), - ), + } + }), ); diff --git a/app/scripts/tests/quality-audit-gate.test.mts b/app/scripts/tests/quality-audit-gate.test.mts index c98edffc3..52d9dc4b7 100644 --- a/app/scripts/tests/quality-audit-gate.test.mts +++ b/app/scripts/tests/quality-audit-gate.test.mts @@ -1,10 +1,11 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Schema } from 'effect'; -import { runEffectTestPromise } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; +import { expect, it } from 'effect-rstest'; +import { Cause, Effect, Schema } from 'effect'; import { validateQualityAuditSummary } from '../quality-audit-gate.mts'; import { validateReport } from '../quality-audit.mts'; +const EXPECTED_PROOF_VALUE = 'Expected a defined proof value'; +const EXPECTED_EFFECT_FAILURE = 'Expected the Effect to fail'; + const FALLOW_FILES = 'fallow-files'; const FALLOW_SIMILARITY = 'fallow-similarity'; const FALLOW_HEALTH = 'fallow-health'; @@ -62,21 +63,30 @@ const clean = () => ({ status: 'reported', }); const encode = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); -const validate = async (summary: ReturnType | Schema.Json) => - await runEffectTestPromise(encode(summary).pipe(Effect.flatMap(validateQualityAuditSummary))); - -await test('complete clean summary succeeds; semantic and UI-only findings remain advisory', async () => { - await validate(clean()); - const summary = clean(); - const semantic = summary.results.find(({ name }) => name === FALLOW_SIMILARITY); - const health = summary.results.find(({ name }) => name === FALLOW_HEALTH); - assert.ok(semantic && health); - semantic.findings = 19; - health.coverage.uiOnlyFindings = 2; - health.coverage.weightedFindings = 2; - await validate(summary); +const validate = Effect.fn(function* mergedScenario1( + summary: ReturnType | Schema.Json, +) { + return yield* encode(summary).pipe(Effect.flatMap(validateQualityAuditSummary)); }); +it.effect( + 'complete clean summary succeeds; semantic and UI-only findings remain advisory', + Effect.fn(function* mergedScenario2() { + yield* validate(clean()); + const summary = clean(); + const semantic = summary.results.find(({ name }) => name === FALLOW_SIMILARITY); + const health = summary.results.find(({ name }) => name === FALLOW_HEALTH); + expect(semantic && health).toBeTruthy(); + if (!(semantic && health)) { + throw new Error(EXPECTED_PROOF_VALUE); + } + semantic.findings = 19; + health.coverage.uiOnlyFindings = 2; + health.coverage.weightedFindings = 2; + yield* validate(summary); + }), +); + const positiveReports = [ [ 'knip', @@ -143,13 +153,17 @@ const positiveReports = [ }), ], ] as const; -await Promise.all( - positiveReports.map(async ([name, source]) => { - await test(`${name} real-positive analyzer report rejects through the normalized gate`, async () => { - const normalized = await runEffectTestPromise(validateReport(name, source)); +for (const [name, source] of positiveReports) { + it.effect( + `${name} real-positive analyzer report rejects through the normalized gate`, + Effect.fn(function* mergedScenario3() { + const normalized = yield* validateReport(name, source); const summary = clean(); const result = summary.results.find((entry) => entry.name === name); - assert.ok(result); + expect(result).toBeTruthy(); + if (!result) { + throw new Error(EXPECTED_PROOF_VALUE); + } Object.assign(result, normalized); if (name === 'knip') { Object.assign(result.coverage, { @@ -157,116 +171,239 @@ await Promise.all( nativeFindingCounts: result.coverage.findingCounts, }); } - await assert.rejects( - validate(summary), - new RegExp(`Quality audit gate failed: ${name}=1`, 'u'), - ); + yield* Effect.matchCause(validate(summary), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch( + new RegExp(`Quality audit gate failed: ${name}=1`, 'u'), + ), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + }), + ); +} + +it.effect( + 'calibrated modeled consumers do not reintroduce native Knip findings', + Effect.fn(function* mergedScenario5() { + const summary = clean(); + const knip = summary.results.find(({ name }) => name === 'knip'); + expect(knip).toBeTruthy(); + if (!knip) { + throw new Error(EXPECTED_PROOF_VALUE); + } + knip.coverage.modeledUsages = 4; + knip.coverage.nativeFindingCounts = { exports: 0, unlisted: 4 }; + yield* validate(summary); + knip.coverage.modeledUsages = 5; + yield* Effect.matchCause(validate(summary), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(/inconsistent/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, }); }), ); -await test('calibrated modeled consumers do not reintroduce native Knip findings', async () => { - const summary = clean(); - const knip = summary.results.find(({ name }) => name === 'knip'); - assert.ok(knip); - knip.coverage.modeledUsages = 4; - knip.coverage.nativeFindingCounts = { exports: 0, unlisted: 4 }; - await validate(summary); - knip.coverage.modeledUsages = 5; - await assert.rejects(validate(summary), /inconsistent/u); -}); - -await test('partial, duplicate, unknown, failed and empty reports fail closed', async () => { - await assert.rejects(validate({}), /Malformed/u); - await assert.rejects(validate({ ...clean(), status: 'error' }), /Malformed/u); - await assert.rejects(validate({ results: [], status: 'reported' }), /six unique/u); - await Promise.all( - names.map(async (name) => { - const summary = clean(); - await assert.rejects( - validate({ ...summary, results: summary.results.filter((entry) => entry.name !== name) }), - /six unique/u, - ); - const result = summary.results.find((entry) => entry.name === name); - assert.ok(result); - await assert.rejects( - validate({ ...summary, results: [...summary.results, result] }), - /six unique/u, - ); - result.status = 'error'; - await assert.rejects(validate(summary), /Malformed/u); - result.status = 'reported'; - result.files = 0; - await assert.rejects(validate(summary), /Malformed/u); - }), - ); - const summary = clean(); - const [first] = summary.results; - first.name = 'unknown'; - await assert.rejects(validate(summary), /Malformed/u); - await Promise.all( - ['', '{broken', 'null', '{"status":"reported","results":{}}'].map(async (source) => { - await assert.rejects(runEffectTestPromise(validateQualityAuditSummary(source)), /Malformed/u); - }), - ); -}); +it.effect( + 'partial, duplicate, unknown, failed and empty reports fail closed', + Effect.fn(function* mergedScenario8() { + yield* Effect.matchCause(validate({}), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(/Malformed/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* Effect.matchCause(validate({ ...clean(), status: 'error' }), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(/Malformed/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* Effect.matchCause(validate({ results: [], status: 'reported' }), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(/six unique/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* Effect.all( + names.map( + Effect.fn(function* mergedScenario6(name) { + const summary = clean(); + yield* Effect.matchCause( + validate({ + ...summary, + results: summary.results.filter((entry) => entry.name !== name), + }), + { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(/six unique/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }, + ); + const result = summary.results.find((entry) => entry.name === name); + expect(result).toBeTruthy(); + if (!result) { + throw new Error(EXPECTED_PROOF_VALUE); + } + yield* Effect.matchCause( + validate({ ...summary, results: [...summary.results, result] }), + { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(/six unique/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }, + ); + result.status = 'error'; + yield* Effect.matchCause(validate(summary), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(/Malformed/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + result.status = 'reported'; + result.files = 0; + yield* Effect.matchCause(validate(summary), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(/Malformed/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + }), + ), + { concurrency: 'unbounded' }, + ); + const summary = clean(); + const [first] = summary.results; + first.name = 'unknown'; + yield* Effect.matchCause(validate(summary), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(/Malformed/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* Effect.all( + ['', '{broken', 'null', '{"status":"reported","results":{}}'].map( + Effect.fn(function* mergedScenario7(source) { + yield* Effect.matchCause(validateQualityAuditSummary(source), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(/Malformed/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + }), + ), + { concurrency: 'unbounded' }, + ); + }), +); -await test('invalid counts, flags, diagnostics and inconsistent coverage cannot imply clean', async () => { - await Promise.all( - names.map(async (name) => { - await Promise.all( - [-1, 0.5, null, '0', undefined, Number.NaN, Number.POSITIVE_INFINITY].map( - async (invalid) => { - const summary = clean(); - const result = summary.results.find((entry) => entry.name === name); - assert.ok(result); - Object.assign(result, { findings: invalid }); - await assert.rejects(validate(summary), /Malformed/u); - }, - ), - ); - }), - ); - await Promise.all( - [ - { advisory: true }, - { diagnostic: 'analysis failed' }, - { coverage: {} }, - { coverage: { tokenEligibleFiles: -1 } }, - { coverage: { tokenEligibleFiles: 1 } }, - ].map(async (patch) => { - const summary = clean(); - const result = summary.results.find(({ name }) => name === 'jscpd'); - assert.ok(result); - Object.assign(result, patch); - await assert.rejects(validate(summary), /Malformed|inconsistent/u); - }), - ); -}); +it.effect( + 'invalid counts, flags, diagnostics and inconsistent coverage cannot imply clean', + Effect.fn(function* mergedScenario12() { + yield* Effect.all( + names.map( + Effect.fn(function* mergedScenario10(name) { + yield* Effect.all( + [-1, 0.5, null, '0', undefined, Number.NaN, Number.POSITIVE_INFINITY].map( + Effect.fn(function* mergedScenario9(invalid) { + const summary = clean(); + const result = summary.results.find((entry) => entry.name === name); + expect(result).toBeTruthy(); + if (!result) { + throw new Error(EXPECTED_PROOF_VALUE); + } + Object.assign(result, { findings: invalid }); + yield* Effect.matchCause(validate(summary), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(/Malformed/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + }), + ), + { concurrency: 'unbounded' }, + ); + }), + ), + { concurrency: 'unbounded' }, + ); + yield* Effect.all( + [ + { advisory: true }, + { diagnostic: 'analysis failed' }, + { coverage: {} }, + { coverage: { tokenEligibleFiles: -1 } }, + { coverage: { tokenEligibleFiles: 1 } }, + ].map( + Effect.fn(function* mergedScenario11(patch) { + const summary = clean(); + const result = summary.results.find(({ name }) => name === 'jscpd'); + expect(result).toBeTruthy(); + if (!result) { + throw new Error(EXPECTED_PROOF_VALUE); + } + Object.assign(result, patch); + yield* Effect.matchCause(validate(summary), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch(/Malformed|inconsistent/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + }), + ), + { concurrency: 'unbounded' }, + ); + }), +); -await test('six-result duplicate and inconsistent normalization fail closed', async () => { - const duplicate = clean(); - const [, repeated] = duplicate.results; - duplicate.results[0] = repeated; - await assert.rejects(validate(duplicate), /six unique/u); - await Promise.all( - [ - ['knip', { findingCounts: {}, nativeFindingCounts: {} }], - ['knip', { processed: 1 }], - ['knip', { total: 3 }], - ['knip', { workspaces: [] }], - ['knip', { findingCounts: { exports: -1, unlisted: 0 } }], - [FALLOW_HEALTH, { controlFlowFindings: 1 }], - [FALLOW_HEALTH, { analyzedFunctions: 0 }], - [FALLOW_HEALTH, { weightedFindings: 1 }], - [FALLOW_HEALTH, { uiOnlyFindings: -1 }], - [FALLOW_FILES, { discoveredFiles: 0 }], - ].map(async ([name, coverage]) => { - const summary = clean(); - const result = summary.results.find((entry) => entry.name === name); - assert.ok(result); - Object.assign(result.coverage, coverage); - await assert.rejects(validate(summary), /Malformed|inconsistent/u); - }), - ); -}); +it.effect( + 'six-result duplicate and inconsistent normalization fail closed', + Effect.fn(function* mergedScenario14() { + const duplicate = clean(); + const [, repeated] = duplicate.results; + duplicate.results[0] = repeated; + yield* Effect.matchCause(validate(duplicate), { + onFailure: (cause) => expect(String(Cause.squash(cause))).toMatch(/six unique/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + yield* Effect.all( + [ + ['knip', { findingCounts: {}, nativeFindingCounts: {} }], + ['knip', { processed: 1 }], + ['knip', { total: 3 }], + ['knip', { workspaces: [] }], + ['knip', { findingCounts: { exports: -1, unlisted: 0 } }], + [FALLOW_HEALTH, { controlFlowFindings: 1 }], + [FALLOW_HEALTH, { analyzedFunctions: 0 }], + [FALLOW_HEALTH, { weightedFindings: 1 }], + [FALLOW_HEALTH, { uiOnlyFindings: -1 }], + [FALLOW_FILES, { discoveredFiles: 0 }], + ].map( + Effect.fn(function* mergedScenario13([name, coverage]) { + const summary = clean(); + const result = summary.results.find((entry) => entry.name === name); + expect(result).toBeTruthy(); + if (!result) { + throw new Error(EXPECTED_PROOF_VALUE); + } + Object.assign(result.coverage, coverage); + yield* Effect.matchCause(validate(summary), { + onFailure: (cause) => + expect(String(Cause.squash(cause))).toMatch(/Malformed|inconsistent/u), + onSuccess: () => { + throw new Error(EXPECTED_EFFECT_FAILURE); + }, + }); + }), + ), + { concurrency: 'unbounded' }, + ); + }), +); diff --git a/app/scripts/tests/quality-audit-model.test.mts b/app/scripts/tests/quality-audit-model.test.mts index 54ca1bab0..4b965d409 100644 --- a/app/scripts/tests/quality-audit-model.test.mts +++ b/app/scripts/tests/quality-audit-model.test.mts @@ -1,5 +1,6 @@ +import { expect, it } from 'effect-rstest'; import { runPinnedKnip } from './quality-audit-test-support.mts'; -import assert from 'node:assert/strict'; + import { mkdirSync, mkdtempSync, @@ -11,11 +12,11 @@ import { } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import test from 'node:test'; + import { NodeServices } from '@effect/platform-node'; import { Effect, Schema } from 'effect'; import { runQualityAudit } from '../quality-audit.mts'; -import { runEffectTestPromise } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; + import { buildKnipModel, KnipConfigSchema } from '../../quality-audit/knip-model.mts'; const rspackPackageName = '@rspack/core'; @@ -28,9 +29,13 @@ const requirePrelude = [ const resolverFile = 'src/resolver.ts'; const directFile = 'src/direct.ts'; const configurationFiles = '*.config.ts'; +const toolsPattern = 'tools/**/*.{ts,mts}'; const sourcePattern = 'src/**/*.{ts,mts}'; const knipManifestFile = 'node_modules/knip/package.json'; const indexFile = 'src/index.ts'; +const rstestConfigFile = 'rstest.config.ts'; +const auditConsumersFile = '.audit/consumers.mts'; +const rstestEnvironmentReason = 'Rstest testEnvironment consumer'; const appRoot = path.resolve(import.meta.dirname, '../..'); const Names = Schema.Array(Schema.Struct({ name: Schema.String })); const ReportSchema = Schema.Struct({ @@ -44,235 +49,250 @@ const ReportSchema = Schema.Struct({ }), ), }); -const stringify = async (value: Schema.Json) => - await runEffectTestPromise(Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(value)); +const stringify = (value: Schema.Json) => + Effect.gen(function* testEffect1() { + return yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(value); + }); const write = (root: string, file: string, source: string) => { mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); writeFileSync(path.join(root, file), source); }; -const fixture = async () => { - const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'ontos-knip-model-'))); - symlinkSync(path.join(appRoot, 'node_modules'), path.join(root, 'node_modules'), 'dir'); - write( - root, - packageFile, - await stringify({ - dependencies: { 'drizzle-orm': '1.0.0-rc.4', effect: '4.0.0-beta.107', jose: '6.2.5' }, - name: 'knip-consumer-controls', - private: true, - type: 'module', - workspaces: ['verticals/*', 'packages/*'], - }), - ); - write( - root, - indexFile, - [ - "import { used } from './helper.ts';", - "console.log(used, new URL('./worker.mts', import.meta.url));", - "void import('declaredRemote/Public');", - "void import('misspelledRemote/Public');", - "void import('shadowedRemote/Public');", - "void import('./resolver.ts'); void import('./direct.ts'); void import('./own-resolver.ts');", - ].join('\n'), - ); - const resolverOwner = '.resolver-fixture/node_modules/owner'; - const resolverTarget = `${resolverOwner}/node_modules/@rspack/core`; - write( - root, - `${resolverOwner}/package.json`, - await stringify({ - dependencies: { [rspackPackageName]: '1.0.0' }, - main: 'index.js', - name: 'fixture-owner', - }), - ); - write(root, `${resolverOwner}/index.js`, fixtureModuleSource); - write( - root, - `${resolverTarget}/package.json`, - await stringify({ main: 'index.js', name: rspackPackageName }), - ); - write(root, `${resolverTarget}/index.js`, fixtureModuleSource); - const resolverAnchor = path.join(root, resolverOwner, 'index.js'); - write( - root, - resolverFile, - [ - ...requirePrelude, - `require.resolve('@rspack/core', { paths: [${JSON.stringify(resolverAnchor)}] });`, - ].join('\n'), - ); - write( - root, - 'src/own-resolver.ts', - [ - ...requirePrelude, - `require.resolve('oxc-parser', { paths: [${JSON.stringify(root)}] });`, - ].join('\n'), - ); - write( - root, - 'verticals/remote/package.json', - await stringify({ - dependencies: { 'drizzle-orm': '1.0.0-rc.4', effect: '4.0.0-beta.107' }, - name: 'remote-controls', - private: true, - type: 'module', - 'zephyr:dependencies': { composed: 'drizzle-orm@workspace:*' }, - }), - ); - write( - root, - 'verticals/remote/src/index.ts', - "void import('childRemote/Public'); void import('misspelledChild/Public'); void import('declaredRemote/Public');", - ); - write( - root, - 'verticals/remote/module-federation.config.ts', - "export default {remotes: {childRemote:'child@https://example.test/remote.js'}};", - ); - write(root, 'src/validated.ts', 'export const unusedValidatedExport = 1;'); - write( - root, - 'scripts/validate-ultramodern-workspace.mts', - [ - "const requiredPaths = ['src/validated.ts']; for (const file of requiredPaths) console.log(file);", - "const workspaceValidationContractDefinition = {topology: {compactConfig: {apps: [{path:'verticals/remote'}]}}};", - 'for (const expectedApp of workspaceValidationContractDefinition.topology.compactConfig.apps) {', - 'const appPath = expectedApp.path;', - `const buildModuleSource = readText(\`\${appPath}/shared/ultramodern-build.ts\`);`, - "console.log(buildModuleSource.includes('export const declaredBuildIdentity')); }", - ].join('\n'), - ); - write( - root, - 'verticals/remote/shared/ultramodern-build.ts', - 'export const declaredBuildIdentity = 1; export const unusedBuildNeighbor = 2;', - ); - write( - root, - 'tools/oxlint/effect-native/report.mts', - "runOxlint(join(pluginDirectory, 'report.config.ts'), []);", - ); - write( - root, - 'tools/oxlint/effect-native/report.config.ts', - 'export default {}; export const unusedConfigNeighbor = 3;', - ); - write(root, directFile, "import '@rspack/core';"); - write(root, 'src/helper.ts', 'export const used = 1; export const unusedNeighbor = 2;'); - write(root, 'src/worker.mts', 'console.log("worker"); export const unusedWorkerExport = 3;'); - write(root, 'src/dead.ts', 'export const genuinelyDead = 1;'); - write(root, 'src/public.ts', 'export const externallyConsumed = 1;'); - write( - root, - 'src/schema.ts', - [ - "import { pgSchema } from 'drizzle-orm/pg-core';", - "export const registeredSchema = pgSchema('registered');", - 'export const unregisteredHelper = () => 7;', - ].join('\n'), - ); - write( - root, - 'module-federation.config.ts', - [ - "throw new Error('configuration must never execute');", - "const remotes = { declaredRemote: 'remote@https://example.test/remote.js' };", - "function shadow() { const remotes = { shadowedRemote: 'wrong' }; return remotes; }", - 'export default {', - 'remotes,', - "shared: { effect: { singleton: true } }, exposes: { './Public': './src/public.ts' } };", - ].join('\n'), - ); - write( - root, - 'drizzle.config.ts', - "throw new Error('configuration must never execute'); export default { schema: './src/schema.ts' };", - ); - return root; -}; - -await test('real pinned Knip models exact consumers and preserves neighboring findings', async () => { - const root = await fixture(); - try { - const base = await runEffectTestPromise( - Schema.decodeUnknownEffect(KnipConfigSchema)({ - workspaces: { - '.': { - drizzle: { config: [] }, - entry: [indexFile, configurationFiles], - lefthook: false, - node: false, - project: [sourcePattern, configurationFiles, 'tools/**/*.{ts,mts}'], - }, - 'verticals/*': { entry: [indexFile, configurationFiles], project: ['**/*.{ts,mts}'] }, - }, +const fixture = () => + Effect.gen(function* testEffect2() { + const root = yield* Effect.acquireRelease( + Effect.sync(() => mkdtempSync(path.join(tmpdir(), 'ontos-knip-model-'))), + (directory) => Effect.sync(() => rmSync(directory, { force: true, recursive: true })), + ).pipe(Effect.map((directory) => realpathSync(directory))); + symlinkSync(path.join(appRoot, 'node_modules'), path.join(root, 'node_modules'), 'dir'); + write( + root, + packageFile, + yield* stringify({ + dependencies: { 'drizzle-orm': '1.0.0-rc.4', effect: '4.0.0-beta.107', jose: '6.2.5' }, + name: 'knip-consumer-controls', + private: true, + type: 'module', + workspaces: ['verticals/*', 'packages/*'], }), ); + write( + root, + indexFile, + [ + "import { used } from './helper.ts';", + "console.log(used, new URL('./worker.mts', import.meta.url));", + "void import('declaredRemote/Public');", + "void import('misspelledRemote/Public');", + "void import('shadowedRemote/Public');", + "void import('./resolver.ts'); void import('./direct.ts'); void import('./own-resolver.ts');", + ].join('\n'), + ); + const resolverOwner = '.resolver-fixture/node_modules/owner'; + const resolverTarget = `${resolverOwner}/node_modules/@rspack/core`; + write( + root, + `${resolverOwner}/package.json`, + yield* stringify({ + dependencies: { [rspackPackageName]: '1.0.0' }, + main: 'index.js', + name: 'fixture-owner', + }), + ); + write(root, `${resolverOwner}/index.js`, fixtureModuleSource); + write( + root, + `${resolverTarget}/package.json`, + yield* stringify({ main: 'index.js', name: rspackPackageName }), + ); + write(root, `${resolverTarget}/index.js`, fixtureModuleSource); + const resolverAnchor = path.join(root, resolverOwner, 'index.js'); + write( + root, + resolverFile, + [ + ...requirePrelude, + `require.resolve('@rspack/core', { paths: [${JSON.stringify(resolverAnchor)}] });`, + ].join('\n'), + ); + write( + root, + 'src/own-resolver.ts', + [ + ...requirePrelude, + `require.resolve('oxc-parser', { paths: [${JSON.stringify(root)}] });`, + ].join('\n'), + ); + write( + root, + 'verticals/remote/package.json', + yield* stringify({ + dependencies: { 'drizzle-orm': '1.0.0-rc.4', effect: '4.0.0-beta.107' }, + name: 'remote-controls', + private: true, + type: 'module', + 'zephyr:dependencies': { composed: 'drizzle-orm@workspace:*' }, + }), + ); + write( + root, + 'verticals/remote/src/index.ts', + "void import('childRemote/Public'); void import('misspelledChild/Public'); void import('declaredRemote/Public');", + ); + write( + root, + 'verticals/remote/module-federation.config.ts', + "export default {remotes: {childRemote:'child@https://example.test/remote.js'}};", + ); + write(root, 'src/validated.ts', 'export const unusedValidatedExport = 1;'); + write( + root, + 'scripts/validate-ultramodern-workspace.mts', + [ + "const requiredPaths = ['src/validated.ts']; for (const file of requiredPaths) console.log(file);", + "const workspaceValidationContractDefinition = {topology: {compactConfig: {apps: [{path:'verticals/remote'}]}}};", + 'for (const expectedApp of workspaceValidationContractDefinition.topology.compactConfig.apps) {', + 'const appPath = expectedApp.path;', + `const buildModuleSource = readText(\`\${appPath}/shared/ultramodern-build.ts\`);`, + "console.log(buildModuleSource.includes('export const declaredBuildIdentity')); }", + ].join('\n'), + ); + write( + root, + 'verticals/remote/shared/ultramodern-build.ts', + 'export const declaredBuildIdentity = 1; export const unusedBuildNeighbor = 2;', + ); + write( + root, + 'tools/oxlint/effect-native/report.mts', + "runOxlint(join(pluginDirectory, 'report.config.ts'), []);", + ); + write( + root, + 'tools/oxlint/effect-native/report.config.ts', + 'export default {}; export const unusedConfigNeighbor = 3;', + ); + write(root, directFile, "import '@rspack/core';"); + write(root, 'src/helper.ts', 'export const used = 1; export const unusedNeighbor = 2;'); + write(root, 'src/worker.mts', 'console.log("worker"); export const unusedWorkerExport = 3;'); + write(root, 'src/dead.ts', 'export const genuinelyDead = 1;'); + write(root, 'src/public.ts', 'export const externallyConsumed = 1;'); + write( + root, + 'src/schema.ts', + [ + "import { pgSchema } from 'drizzle-orm/pg-core';", + "export const registeredSchema = pgSchema('registered');", + 'export const unregisteredHelper = () => 7;', + ].join('\n'), + ); + write( + root, + 'module-federation.config.ts', + [ + "throw new Error('configuration must never execute');", + "const remotes = { declaredRemote: 'remote@https://example.test/remote.js' };", + "function shadow() { const remotes = { shadowedRemote: 'wrong' }; return remotes; }", + 'export default {', + 'remotes,', + "shared: { effect: { singleton: true } }, exposes: { './Public': './src/public.ts' } };", + ].join('\n'), + ); + write( + root, + 'drizzle.config.ts', + "throw new Error('configuration must never execute'); export default { schema: './src/schema.ts' };", + ); + return root; + }); + +it.live( + 'real pinned Knip models exact consumers and preserves neighboring findings', + Effect.fn(function* testEffect3() { + const root = yield* fixture(); + const base = yield* Schema.decodeUnknownEffect(KnipConfigSchema)({ + workspaces: { + '.': { + drizzle: { config: [] }, + entry: [indexFile, configurationFiles], + lefthook: false, + node: false, + project: [sourcePattern, configurationFiles, toolsPattern], + }, + 'verticals/*': { entry: [indexFile, configurationFiles], project: ['**/*.{ts,mts}'] }, + }, + }); const consumerPath = path.join(root, '.codex/knip-model/consumers.mts'); - const model = await runEffectTestPromise( - buildKnipModel(root, base, consumerPath).pipe(Effect.provide(NodeServices.layer)), + const model = yield* buildKnipModel(root, base, consumerPath).pipe( + Effect.provide(NodeServices.layer), + ); + const run = yield* runPinnedKnip(root, consumerPath, model).pipe( + Effect.provide(NodeServices.layer), ); - const run = await runPinnedKnip(root, consumerPath, model); - assert.equal(run.status, 1, `${run.stdout}\n${run.stderr}`); - assert.equal(run.stderr, ''); - const report = await runEffectTestPromise( - Schema.decodeUnknownEffect(Schema.fromJsonString(ReportSchema))(run.stdout), + expect(run.status, `${run.stdout}\n${run.stderr}`).toBe(1); + expect(run.stderr).toBe(''); + const report = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ReportSchema))( + run.stdout, ); const findings = (kind: 'files' | 'exports' | 'dependencies' | 'unlisted') => report.issues.flatMap((issue) => issue[kind].map((finding) => `${issue.file}#${finding.name}`), ); - assert.ok(findings('files').includes('src/dead.ts#src/dead.ts')); - assert.ok(!findings('files').some((finding) => finding.startsWith('src/worker.mts#'))); - assert.ok(!findings('files').some((finding) => finding.startsWith('src/public.ts#'))); - assert.ok(findings('exports').includes('src/helper.ts#unusedNeighbor')); - assert.ok(findings('exports').includes('src/validated.ts#unusedValidatedExport')); - assert.ok( + expect(findings('files').includes('src/dead.ts#src/dead.ts')).toBe(true); + expect(!findings('files').some((finding) => finding.startsWith('src/worker.mts#'))).toBe(true); + expect(!findings('files').some((finding) => finding.startsWith('src/public.ts#'))).toBe(true); + expect(findings('exports').includes('src/helper.ts#unusedNeighbor')).toBe(true); + expect(findings('exports').includes('src/validated.ts#unusedValidatedExport')).toBe(true); + expect( !findings('exports').includes( 'verticals/remote/shared/ultramodern-build.ts#declaredBuildIdentity', ), - ); - assert.ok( + ).toBe(true); + expect( findings('exports').includes( 'verticals/remote/shared/ultramodern-build.ts#unusedBuildNeighbor', ), - ); - assert.ok(!findings('exports').includes('tools/oxlint/effect-native/report.config.ts#default')); - assert.ok( + ).toBe(true); + expect( + !findings('exports').includes('tools/oxlint/effect-native/report.config.ts#default'), + ).toBe(true); + expect( findings('exports').includes( 'tools/oxlint/effect-native/report.config.ts#unusedConfigNeighbor', ), + ).toBe(true); + expect(!findings('files').some((finding) => finding.startsWith('src/validated.ts#'))).toBe( + true, ); - assert.ok(!findings('files').some((finding) => finding.startsWith('src/validated.ts#'))); - assert.ok(findings('exports').includes('src/schema.ts#unregisteredHelper')); - assert.ok(!findings('exports').includes('src/schema.ts#registeredSchema')); - assert.ok(!findings('exports').includes('src/public.ts#externallyConsumed')); - assert.ok(findings('dependencies').includes('package.json#jose')); - assert.ok(!findings('dependencies').includes('package.json#effect')); - assert.ok(findings('unlisted').includes('src/index.ts#misspelledRemote')); - assert.ok(findings('unlisted').includes('src/index.ts#declaredRemote')); - assert.ok(!findings('unlisted').includes('verticals/remote/src/index.ts#childRemote')); - assert.ok(findings('unlisted').includes('verticals/remote/src/index.ts#misspelledChild')); - assert.ok(findings('unlisted').includes('verticals/remote/src/index.ts#declaredRemote')); - assert.ok(findings('dependencies').includes('verticals/remote/package.json#effect')); - assert.ok( - !findings('dependencies').includes('verticals/remote/package.json#drizzle-orm'), - 'a zephyr:dependencies composition reference must count as a dependency consumer', + expect(findings('exports').includes('src/schema.ts#unregisteredHelper')).toBe(true); + expect(!findings('exports').includes('src/schema.ts#registeredSchema')).toBe(true); + expect(!findings('exports').includes('src/public.ts#externallyConsumed')).toBe(true); + expect(findings('dependencies').includes('package.json#jose')).toBe(true); + expect(!findings('dependencies').includes('package.json#effect')).toBe(true); + expect(findings('unlisted').includes('src/index.ts#misspelledRemote')).toBe(true); + expect(findings('unlisted').includes('src/index.ts#declaredRemote')).toBe(true); + expect(!findings('unlisted').includes('verticals/remote/src/index.ts#childRemote')).toBe(true); + expect(findings('unlisted').includes('verticals/remote/src/index.ts#misspelledChild')).toBe( + true, ); - assert.deepEqual(model.config.workspaces['.']?.ignoreDependencies, []); - assert.ok(findings('unlisted').includes('src/index.ts#shadowedRemote')); - assert.ok(findings('unlisted').includes('src/direct.ts#@rspack/core')); - assert.ok(findings('unlisted').includes('src/own-resolver.ts#oxc-parser')); - assert.ok( + expect(findings('unlisted').includes('verticals/remote/src/index.ts#declaredRemote')).toBe( + true, + ); + expect(findings('dependencies').includes('verticals/remote/package.json#effect')).toBe(true); + expect( + findings('dependencies').includes('verticals/remote/package.json#drizzle-orm'), + 'a zephyr:dependencies composition reference must count as a dependency consumer', + ).toBe(false); + expect(model.config.workspaces['.']?.ignoreDependencies).toEqual([]); + expect(findings('unlisted').includes('src/index.ts#shadowedRemote')).toBe(true); + expect(findings('unlisted').includes('src/direct.ts#@rspack/core')).toBe(true); + expect(findings('unlisted').includes('src/own-resolver.ts#oxc-parser')).toBe(true); + expect( !model.evidence.some( (item) => item.kind === 'resolver' && item.source === 'src/own-resolver.ts', ), - ); - assert.ok( + ).toBe(true); + expect( model.evidence.some( (item) => item.kind === 'resolver' && @@ -282,54 +302,60 @@ await test('real pinned Knip models exact consumers and preserves neighboring fi item.column === 17 && item.resolved !== undefined, ), - ); - assert.ok( + ).toBe(true); + expect( !model.evidence.some((item) => item.kind === 'resolver' && item.source === directFile), - ); - assert.ok( + ).toBe(true); + expect( model.evidence.some( (item) => item.target === 'src/schema.ts#registeredSchema' && item.kind === 'export', ), - ); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + ).toBe(true); + }), +); -await test('modeling fails on invalid source instead of silently losing consumer evidence', async () => { - const root = await fixture(); - try { +it.live( + 'modeling fails on invalid source instead of silently losing consumer evidence', + Effect.fn(function* testEffect4() { + const root = yield* fixture(); write(root, 'module-federation.config.ts', 'export default { broken: ;'); - await assert.rejects( - runEffectTestPromise( - buildKnipModel(root, { entry: [indexFile] }).pipe(Effect.provide(NodeServices.layer)), - ), - { reason: /Invalid quality model source/u }, - ); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + yield* buildKnipModel(root, { entry: [indexFile] }) + .pipe(Effect.provide(NodeServices.layer)) + .pipe( + Effect.flip, + Effect.map((error) => + expect( + Schema.decodeUnknownSync(Schema.Struct({ reason: Schema.String }))(error).reason, + ).toMatch(/Invalid quality model source/u), + ), + ); + }), +); -await test('the model regression harness uses the repository-pinned Knip version', async () => { - const manifest = await runEffectTestPromise( - Schema.decodeUnknownEffect( +it.live( + 'the model regression harness uses the repository-pinned Knip version', + Effect.fn(function* testEffect5() { + const manifest = yield* Schema.decodeUnknownEffect( Schema.fromJsonString(Schema.Struct({ version: Schema.Literal('6.34.0') })), - )(readFileSync(path.join(appRoot, knipManifestFile), 'utf-8')), - ); - assert.equal(manifest.version, '6.34.0'); -}); + )(readFileSync(path.join(appRoot, knipManifestFile), 'utf-8')); + expect(manifest.version).toBe('6.34.0'); + }), +); -await test('runner calibrates only the proven resolver record and retains the direct import', async () => { - const root = await fixture(); - const installedManifest = readFileSync(path.join(appRoot, knipManifestFile)); - const installedBinary = readFileSync(path.join(appRoot, 'node_modules/.bin/knip')); - const output = realpathSync(mkdtempSync(path.join(tmpdir(), 'ontos-knip-model-output-'))); - try { +it.live( + 'runner calibrates only the proven resolver record and retains the direct import', + Effect.fn(function* testEffect6() { + const root = yield* fixture(); + const installedManifest = readFileSync(path.join(appRoot, knipManifestFile)); + const installedBinary = readFileSync(path.join(appRoot, 'node_modules/.bin/knip')); + const output = yield* Effect.acquireRelease( + Effect.sync(() => mkdtempSync(path.join(tmpdir(), 'ontos-knip-model-output-'))), + (directory) => Effect.sync(() => rmSync(directory, { force: true, recursive: true })), + ).pipe(Effect.map((directory) => realpathSync(directory))); write( root, 'quality-audit/scope.json', - await stringify({ + yield* stringify({ exclude: [], patterns: [sourcePattern, configurationFiles, 'verticals/**/*.{ts,mts}'], }), @@ -337,14 +363,14 @@ await test('runner calibrates only the proven resolver record and retains the di write( root, 'quality-audit/knip.json', - await stringify({ + yield* stringify({ workspaces: { '.': { drizzle: { config: [] }, entry: [indexFile, configurationFiles], lefthook: false, node: false, - project: [sourcePattern, configurationFiles, 'tools/**/*.{ts,mts}'], + project: [sourcePattern, configurationFiles, toolsPattern], }, 'verticals/*': { entry: [indexFile, configurationFiles], project: ['**/*.{ts,mts}'] }, }, @@ -355,11 +381,9 @@ await test('runner calibrates only the proven resolver record and retains the di 'quality-audit/knip-reporter.mts', readFileSync(path.join(appRoot, 'quality-audit/knip-reporter.mts'), 'utf-8'), ); - await runEffectTestPromise( - runQualityAudit(root, output, 'knip').pipe(Effect.provide(NodeServices.layer)), - ); - assert.deepEqual(readFileSync(path.join(appRoot, knipManifestFile)), installedManifest); - assert.deepEqual(readFileSync(path.join(appRoot, 'node_modules/.bin/knip')), installedBinary); + yield* runQualityAudit(root, output, 'knip').pipe(Effect.provide(NodeServices.layer)); + expect(readFileSync(path.join(appRoot, knipManifestFile))).toEqual(installedManifest); + expect(readFileSync(path.join(appRoot, 'node_modules/.bin/knip'))).toEqual(installedBinary); const summary = Schema.decodeUnknownSync( Schema.fromJsonString( Schema.Struct({ @@ -379,43 +403,46 @@ await test('runner calibrates only the proven resolver record and retains the di ), )(readFileSync(path.join(output, 'summary.json'), 'utf-8')); const result = summary.results.find((entry) => entry.name === 'knip'); - assert.ok(result !== undefined); - assert.equal(result.coverage.modeledUsages, 1); - assert.equal( - result.coverage.nativeFindingCounts.unlisted, + expect(result !== undefined).toBe(true); + if (result === undefined) { + throw new Error('Expected result to be present'); + } + expect(result.coverage.modeledUsages).toBe(1); + expect(result.coverage.nativeFindingCounts.unlisted).toBe( (result.coverage.findingCounts.unlisted ?? 0) + 1, ); const modeled = Schema.decodeUnknownSync( Schema.fromJsonString(Schema.Array(Schema.Struct({ file: Schema.String }))), )(readFileSync(path.join(summary.runDirectory, 'knip/modeled-usages.json'), 'utf-8')); - assert.deepEqual(modeled, [{ file: resolverFile }]); + expect(modeled).toEqual([{ file: resolverFile }]); const raw = Schema.decodeUnknownSync(Schema.fromJsonString(ReportSchema))( readFileSync(path.join(summary.runDirectory, 'knip/report.ndjson'), 'utf-8') .trim() .split('\n')[0], ); - assert.ok( + expect( raw.issues.some( (issue) => issue.file === directFile && issue.unlisted.some((entry) => entry.name === rspackPackageName), ), - ); - } finally { - rmSync(root, { force: true, recursive: true }); - rmSync(output, { force: true, recursive: true }); - } -}); + ).toBe(true); + }), +); -await test('vendor ownership rejects a different installed copy and accepts the same canonical target', async () => { - const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'ontos-knip-copy-controls-'))); - try { - write(root, packageFile, await stringify({ name: 'copy-controls', private: true })); +it.live( + 'vendor ownership rejects a different installed copy and accepts the same canonical target', + Effect.fn(function* testEffect7() { + const root = yield* Effect.acquireRelease( + Effect.sync(() => mkdtempSync(path.join(tmpdir(), 'ontos-knip-copy-controls-'))), + (directory) => Effect.sync(() => rmSync(directory, { force: true, recursive: true })), + ).pipe(Effect.map((directory) => realpathSync(directory))); + write(root, packageFile, yield* stringify({ name: 'copy-controls', private: true })); const owner = 'node_modules/owner'; const producer = `${owner}/node_modules/producer`; const ownerTarget = `${owner}/node_modules/target`; const producerTarget = `${producer}/node_modules/target`; - await Promise.all( + yield* Effect.all( ( [ [owner, 'owner', { producer: '1.0.0' }], @@ -423,14 +450,17 @@ await test('vendor ownership rejects a different installed copy and accepts the [ownerTarget, 'target', {}], [producerTarget, 'target', {}], ] as const - ).map(async ([directory, name, dependencies]) => { - write( - root, - `${directory}/package.json`, - await stringify({ dependencies, main: 'index.js', name }), - ); - write(root, `${directory}/index.js`, fixtureModuleSource); - }), + ).map(([directory, name, dependencies]) => + Effect.gen(function* testEffect8() { + write( + root, + `${directory}/package.json`, + yield* stringify({ dependencies, main: 'index.js', name }), + ); + write(root, `${directory}/index.js`, fixtureModuleSource); + }), + ), + { concurrency: 'unbounded' }, ); write( root, @@ -440,65 +470,299 @@ await test('vendor ownership rejects a different installed copy and accepts the `require.resolve('target', { paths: [${JSON.stringify(path.join(root, owner, 'index.js'))}] });`, ].join('\n'), ); - const consumerPath = path.join(root, '.audit/consumers.mts'); - const build = async () => - await runEffectTestPromise( - buildKnipModel( + const consumerPath = path.join(root, auditConsumersFile); + const build = () => + Effect.gen(function* testEffect9() { + return yield* buildKnipModel( root, { entry: [indexFile], node: false, project: [sourcePattern] }, consumerPath, - ).pipe(Effect.provide(NodeServices.layer)), - ); - const differentCopies = await build(); - assert.ok( + ).pipe(Effect.provide(NodeServices.layer)); + }); + const differentCopies = yield* build(); + expect( !differentCopies.evidence.some( (item) => item.kind === 'resolver' && item.target === 'target', ), - ); + ).toBe(true); const mismatch = differentCopies.evidence.find( (item) => item.kind === 'resolver-unproven' && item.target === 'target', ); - assert.ok(mismatch !== undefined); - assert.equal(mismatch.producerManifest, path.join(root, producer, packageFile)); - assert.equal( - mismatch.producerResolved, + expect(mismatch !== undefined).toBe(true); + if (mismatch === undefined) { + throw new Error('Expected mismatch to be present'); + } + expect(mismatch.producerManifest).toBe(path.join(root, producer, packageFile)); + expect(mismatch.producerResolved).toBe( realpathSync(path.join(root, producerTarget, 'index.js')), ); - assert.equal(mismatch.resolved, realpathSync(path.join(root, ownerTarget, 'index.js'))); - assert.match(mismatch.reason, /different canonical target/u); - const run = await runPinnedKnip(root, consumerPath, differentCopies); - assert.equal(run.status, 1, run.stderr); + expect(mismatch.resolved).toBe(realpathSync(path.join(root, ownerTarget, 'index.js'))); + expect(mismatch.reason).toMatch(/different canonical target/u); + const run = yield* runPinnedKnip(root, consumerPath, differentCopies).pipe( + Effect.provide(NodeServices.layer), + ); + expect(run.status, run.stderr).toBe(1); const report = Schema.decodeUnknownSync(Schema.fromJsonString(ReportSchema))(run.stdout); - assert.ok( + expect( report.issues.some( (issue) => issue.file === indexFile && issue.unlisted.some((item) => item.name === 'target'), ), - ); + ).toBe(true); rmSync(path.join(root, producerTarget), { force: true, recursive: true }); symlinkSync(path.join(root, ownerTarget), path.join(root, producerTarget), 'dir'); - const sameCopy = await build(); - assert.ok( + const sameCopy = yield* build(); + expect( sameCopy.evidence.some( (item) => item.kind === 'resolver' && item.target === 'target' && item.owningManifest === path.join(root, producer, packageFile), ), - ); + ).toBe(true); write( root, indexFile, [ ...requirePrelude, - `require.resolve('target', { paths: [${await stringify(path.join(root, owner, 'missing'))}] });`, + `require.resolve('target', { paths: [${yield* stringify(path.join(root, owner, 'missing'))}] });`, ].join('\n'), ); - const missingAnchor = await build(); - assert.ok( + const missingAnchor = yield* build(); + expect( !missingAnchor.evidence.some((item) => item.kind === 'resolver' && item.target === 'target'), + ).toBe(true); + }), +); + +it.live( + 'Rstest, compiler type tests and Oxlint loaders consume files without hiding unused exports', + Effect.fn(function* testConsumerExports() { + const root = yield* fixture(); + const lintDirectory = 'tools/oxlint/effect-native'; + const policyTest = `${lintDirectory}/tests/repository-policy.test.mts`; + const helperTest = `${lintDirectory}/tests/shared-helpers.test.mts`; + const loadedFiles = [ + rstestConfigFile, + `${lintDirectory}/repository-policy.config.ts`, + `${lintDirectory}/tests/shared-helpers-probe.ts`, + ]; + write( + root, + packageFile, + '{"name":"consumer-controls","type":"module","scripts":{"test":"rstest --project unit"}}', ); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + for (const file of loadedFiles) { + write(root, file, 'export default {}; export const unusedNeighbor = 1;'); + } + write( + root, + policyTest, + "runOxlint(nodePath.join(pluginDirectory, 'repository-policy.config.ts'), []);", + ); + write( + root, + helperTest, + "void { name: 'shared-helpers-probe', specifier: path.join(testsDirectory, 'shared-helpers-probe.ts') };", + ); + write(root, 'tsconfig.json', '{"include":["src"]}'); + const typeTest = 'src/contract.type-test.ts'; + write(root, typeTest, 'export const unusedTypeTestNeighbor = 1;'); + const consumerPath = path.join(root, auditConsumersFile); + const base = { + workspaces: { + '.': { + entry: [policyTest, helperTest], + node: false, + project: [rstestConfigFile, 'src/*.ts', toolsPattern], + rstest: { config: [] }, + }, + }, + }; + const model = yield* buildKnipModel(root, base, consumerPath).pipe( + Effect.provide(NodeServices.layer), + ); + const run = yield* runPinnedKnip(root, consumerPath, model).pipe( + Effect.provide(NodeServices.layer), + ); + expect(run.status, `${run.stdout}\n${run.stderr}`).toBe(1); + const report = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ReportSchema))( + run.stdout, + ); + const files = report.issues.flatMap((issue) => issue.files.map((finding) => finding.name)); + const exports = report.issues.flatMap((issue) => + issue.exports.map((finding) => `${issue.file}#${finding.name}`), + ); + for (const file of loadedFiles) { + expect(files).not.toContain(file); + expect(exports).toContain(`${file}#unusedNeighbor`); + expect(exports).not.toContain(`${file}#default`); + } + expect(files).not.toContain(typeTest); + expect(exports).toContain(`${typeTest}#unusedTypeTestNeighbor`); + expect(files).toContain('src/dead.ts'); + const probeFile = `${lintDirectory}/tests/shared-helpers-probe.ts`; + expect(model.evidence.filter((fact) => fact.source === helperTest)).toEqual([ + expect.objectContaining({ + column: 6, + kind: 'file', + line: 1, + source: helperTest, + target: probeFile, + workspace: '.', + }), + expect.objectContaining({ + column: 6, + kind: 'export', + line: 1, + source: helperTest, + target: `${probeFile}#default`, + workspace: '.', + }), + ]); + for (const specifier of [ + 'undefined', + "'shared-helpers-probe.ts'", + "join(testsDirectory, 'shared-helpers-probe.ts')", + "path.resolve(testsDirectory, 'shared-helpers-probe.ts')", + "path.join('testsDirectory', 'shared-helpers-probe.ts')", + "path.join(otherDirectory, 'shared-helpers-probe.ts')", + "path.join(testsDirectory, 'other-probe.ts')", + ]) { + write(root, helperTest, `void { name: 'shared-helpers-probe', specifier: ${specifier} };`); + const unrecognized = yield* buildKnipModel(root, base, consumerPath).pipe( + Effect.provide(NodeServices.layer), + ); + expect(unrecognized.evidence.some((fact) => fact.source === helperTest)).toBe(false); + } + write( + root, + packageFile, + '{"name":"consumer-controls","type":"module","scripts":{"test":"rstest --config other.ts"}}', + ); + write(root, 'tsconfig.json', '{"include":["src"],"exclude":["src/*.type-test.ts"]}'); + write(root, policyTest, "runOxlint(nodePath.join(pluginDirectory, 'other.config.ts'), []);"); + write( + root, + helperTest, + "void { name: 'other-plugin', specifier: path.join(testsDirectory, 'shared-helpers-probe.ts') };", + ); + const unrelated = yield* buildKnipModel(root, base, consumerPath).pipe( + Effect.provide(NodeServices.layer), + ); + expect( + unrelated.evidence.some( + (fact) => loadedFiles.includes(fact.target) || fact.target === typeTest, + ), + ).toBe(false); + }), +); + +it.live( + 'Rstest environments follow only exported static projects and retain source provenance', + Effect.fn(function* testProjectEnvironments() { + const root = yield* fixture(); + const configFile = rstestConfigFile; + write( + root, + configFile, + [ + "const environment = 'happy-dom' as const;", + 'const browser = { testEnvironment: environment };', + 'const alias = browser;', + 'const cycle = cycle;', + "const unrelated = { testEnvironment: 'unrelated-environment' };", + "function shadow() { const environment = 'shadow-environment'; return environment; }", + 'const projects = [alias, { testEnvironment: `jsdom` }, browser,', + " { testEnvironment: 'node' }, { testEnvironment: choose() }, cycle,", + " { metadata: unrelated }, 'external.config.ts', ...dynamicProjects];", + "export default { testEnvironment: 'node', projects, metadata: unrelated };", + ].join('\n'), + ); + const model = yield* buildKnipModel(root, { entry: [configFile] }).pipe( + Effect.provide(NodeServices.layer), + ); + const environments = model.evidence.filter((fact) => fact.reason === rstestEnvironmentReason); + expect(environments.map((fact) => fact.target)).toEqual(['happy-dom', 'jsdom', 'happy-dom']); + expect(environments[0]).toEqual( + expect.objectContaining({ + kind: 'dependency', + line: 2, + source: configFile, + workspace: '.', + }), + ); + expect(environments[1]?.line).toBe(7); + for (const projects of ['choose()', 'cycle']) { + write( + root, + configFile, + `const cycle = cycle; export default { testEnvironment: 'happy-dom', projects: ${projects} };`, + ); + const dynamic = yield* buildKnipModel(root, { entry: [configFile] }).pipe( + Effect.provide(NodeServices.layer), + ); + expect( + dynamic.evidence + .filter((fact) => fact.reason === rstestEnvironmentReason) + .map((fact) => fact.target), + ).toEqual(['happy-dom']); + } + }), +); + +it.live( + 'pinned Knip consumes project environment evidence without hiding an unused dependency', + Effect.fn(function* testPinnedProjectEnvironment() { + const root = yield* fixture(); + write( + root, + packageFile, + yield* stringify({ + dependencies: { 'happy-dom': '20.8.3', jose: '6.2.5' }, + name: 'project-environment-controls', + private: true, + type: 'module', + }), + ); + write( + root, + rstestConfigFile, + "export default { projects: [{ testEnvironment: 'happy-dom' }] };", + ); + const consumerPath = path.join(root, auditConsumersFile); + const model = yield* buildKnipModel( + root, + { + entry: [rstestConfigFile], + node: false, + project: [rstestConfigFile], + rstest: { config: [] }, + }, + consumerPath, + ).pipe(Effect.provide(NodeServices.layer)); + expect( + model.evidence.some( + (fact) => fact.reason === rstestEnvironmentReason && fact.target === 'happy-dom', + ), + ).toBe(true); + for (const [consumerSource, expectedUnused] of [ + [model.consumerSource, false], + ['', true], + ] as const) { + const run = yield* runPinnedKnip(root, consumerPath, { ...model, consumerSource }).pipe( + Effect.provide(NodeServices.layer), + ); + expect(run.status, `${run.stdout}\n${run.stderr}`).toBe(1); + expect(run.stderr).toBe(''); + const report = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ReportSchema))( + run.stdout, + ); + const dependencies = report.issues.flatMap((issue) => + issue.dependencies.map((item) => item.name), + ); + expect(dependencies.includes('happy-dom')).toBe(expectedUnused); + expect(dependencies).toContain('jose'); + } + }), +); diff --git a/app/scripts/tests/quality-audit-runtime-model.test.mts b/app/scripts/tests/quality-audit-runtime-model.test.mts index e7091e43f..260f8fb43 100644 --- a/app/scripts/tests/quality-audit-runtime-model.test.mts +++ b/app/scripts/tests/quality-audit-runtime-model.test.mts @@ -1,12 +1,13 @@ +import { expect, it } from 'effect-rstest'; import { runPinnedKnip } from './quality-audit-test-support.mts'; -import assert from 'node:assert/strict'; + import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import test from 'node:test'; + import { NodeServices } from '@effect/platform-node'; import { Effect, Schema } from 'effect'; -import { runEffectTestPromise } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; + import { buildKnipModel } from '../../quality-audit/knip-model.mts'; import { buildKnipRuntimeEvidence } from '../../quality-audit/knip-runtime-model.mts'; @@ -30,92 +31,99 @@ const write = (root: string, file: string, source: string) => { mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); writeFileSync(path.join(root, file), source); }; -const stringify = async (value: Schema.Json) => - await runEffectTestPromise(Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(value)); -const facts = async (root: string) => - await runEffectTestPromise( - buildKnipRuntimeEvidence(root).pipe(Effect.provide(NodeServices.layer)), - ); -const fixture = async () => { - const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'ontos-knip-runtime-'))); - write( - root, - 'package.json', - '{"name":"runtime-controls","private":true,"type":"module","devDependencies":{"@effect/tsgo":"0.19.0"}}', - ); - write(root, tsgoPackage, '{"name":"@effect/tsgo","version":"0.19.0"}'); - write( - root, - `${shellRoot}/package.json`, - await stringify({ - dependencies: { - '@fixture/css-comment': '1', - '@fixture/css-dead': '1', - [cssUsed]: '1', - }, - name: '@fixture/shell', - scripts: { bootstrap: 'sh scripts/launch.sh' }, - type: 'module', - }), - ); - write( - root, - layoutFile, - "import './index.css'; export default function Layout() { return null; }", - ); - write( - root, - `${shellRoot}/src/routes/index.css`, - '@import "@fixture/css-used/tokens.css";\n/* @import "@fixture/css-comment"; */', - ); - write(root, `${shellRoot}/src/routes/dead.css`, '@import "@fixture/css-dead";'); - write( - root, - `${shellRoot}/scripts/launch.sh`, - `#!/bin/sh\nscript_directory="$(dirname -- "$0")"\ncd "\${script_directory}/.."\nnode scripts/launched.mts\n# node scripts/dead.mts\n`, - ); - write(root, `${shellRoot}/scripts/launched.mts`, 'export const unusedLauncherExport = 1;'); - write(root, `${shellRoot}/scripts/dead.mts`, 'export const unusedFile = 1;'); - write(root, resetFile, 'export const unusedResetExport = 1;'); - write( - root, - 'zerops.yaml', - 'zerops:\n buildCommands:\n - cd app && PATH="local/bin:$PATH" node scripts/reset.mjs\n', - ); - write( - root, - 'scripts/ultramodern-typecheck.mts', - "const forwardedArgs = []; const args = ['ultramodern', 'typecheck', ...forwardedArgs]; void args;", - ); - write( - root, - `${vendorRoot}/ultramodern-typecheck.mjs`, - "resolveEffectTsgoCompiler({ from: pathToFileURL(join(workspaceRoot, 'package.json')) });", - ); - write(root, tsgoReadme, compilerDocumentation); - write( - root, - compilerConfig, - await stringify({ compilerOptions: { plugins: [{ name: pluginName }] } }), - ); - write( - root, - 'scripts/ultramodern-performance-readiness.mts', - "const forwardedArgs=[]; const args=['ultramodern', 'performance-readiness', ...forwardedArgs]; void args;", - ); - write( - root, - `${vendorRoot}/ultramodern-performance-readiness.mjs`, - `const configPath = '${readinessConfig}'; const moduleUrl = pathToFileURL(path.join(root, configPath)).href; const module = await import(moduleUrl); normalizeConfig(module.default ?? {});`, - ); - write(root, readinessConfig, 'export default {}; export const unusedConfigExport = 1;'); - return root; -}; +const stringify = (value: Schema.Json) => + Effect.gen(function* testEffect1() { + return yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(value); + }); +const facts = (root: string) => + Effect.gen(function* testEffect2() { + return yield* buildKnipRuntimeEvidence(root).pipe(Effect.provide(NodeServices.layer)); + }); +const fixture = () => + Effect.gen(function* testEffect3() { + const root = yield* Effect.acquireRelease( + Effect.sync(() => mkdtempSync(path.join(tmpdir(), 'ontos-knip-runtime-'))), + (directory) => Effect.sync(() => rmSync(directory, { force: true, recursive: true })), + ).pipe(Effect.map((directory) => realpathSync(directory))); + write( + root, + 'package.json', + '{"name":"runtime-controls","private":true,"type":"module","devDependencies":{"@effect/tsgo":"0.19.0"}}', + ); + write(root, tsgoPackage, '{"name":"@effect/tsgo","version":"0.19.0"}'); + write( + root, + `${shellRoot}/package.json`, + yield* stringify({ + dependencies: { + '@fixture/css-comment': '1', + '@fixture/css-dead': '1', + [cssUsed]: '1', + }, + name: '@fixture/shell', + scripts: { bootstrap: 'sh scripts/launch.sh' }, + type: 'module', + }), + ); + write( + root, + layoutFile, + "import './index.css'; export default function Layout() { return null; }", + ); + write( + root, + `${shellRoot}/src/routes/index.css`, + '@import "@fixture/css-used/tokens.css";\n/* @import "@fixture/css-comment"; */', + ); + write(root, `${shellRoot}/src/routes/dead.css`, '@import "@fixture/css-dead";'); + write( + root, + `${shellRoot}/scripts/launch.sh`, + `#!/bin/sh\nscript_directory="$(dirname -- "$0")"\ncd "\${script_directory}/.."\nnode scripts/launched.mts\n# node scripts/dead.mts\n`, + ); + write(root, `${shellRoot}/scripts/launched.mts`, 'export const unusedLauncherExport = 1;'); + write(root, `${shellRoot}/scripts/dead.mts`, 'export const unusedFile = 1;'); + write(root, resetFile, 'export const unusedResetExport = 1;'); + write( + root, + 'zerops.yaml', + 'zerops:\n buildCommands:\n - cd app && PATH="local/bin:$PATH" node scripts/reset.mjs\n', + ); + write( + root, + 'scripts/ultramodern-typecheck.mts', + "const forwardedArgs = []; const args = ['ultramodern', 'typecheck', ...forwardedArgs]; void args;", + ); + write( + root, + `${vendorRoot}/ultramodern-typecheck.mjs`, + "resolveEffectTsgoCompiler({ from: pathToFileURL(join(workspaceRoot, 'package.json')) });", + ); + write(root, tsgoReadme, compilerDocumentation); + write( + root, + compilerConfig, + yield* stringify({ compilerOptions: { plugins: [{ name: pluginName }] } }), + ); + write( + root, + 'scripts/ultramodern-performance-readiness.mts', + "const forwardedArgs=[]; const args=['ultramodern', 'performance-readiness', ...forwardedArgs]; void args;", + ); + write( + root, + `${vendorRoot}/ultramodern-performance-readiness.mjs`, + `const configPath = '${readinessConfig}'; const moduleUrl = pathToFileURL(path.join(root, configPath)).href; const module = await import(moduleUrl); normalizeConfig(module.default ?? {});`, + ); + write(root, readinessConfig, 'export default {}; export const unusedConfigExport = 1;'); + return root; + }); -await test('runtime consumers require the exact CSS, shell, deployment and compiler contracts', async () => { - const root = await fixture(); - try { - const initial = await facts(root); +it.live( + 'runtime consumers require the exact CSS, shell, deployment and compiler contracts', + Effect.fn(function* testEffect4() { + const root = yield* fixture(); + const initial = yield* facts(root); for (const target of [ cssUsed, launchedFile, @@ -124,27 +132,27 @@ await test('runtime consumers require the exact CSS, shell, deployment and compi pluginName, readinessConfig, ]) { - assert.ok( + expect( initial.some((fact) => fact.target === target), target, - ); + ).toBe(true); } for (const target of ['@fixture/css-dead', '@fixture/css-comment', 'scripts/dead.mts']) { - assert.ok(!initial.some((fact) => fact.target === target), target); + expect(!initial.some((fact) => fact.target === target), target).toBe(true); } - assert.equal(initial.find((fact) => fact.target === pluginName)?.kind, compilerOptionKind); - assert.ok( + expect(initial.find((fact) => fact.target === pluginName)?.kind).toBe(compilerOptionKind); + expect( initial.some( (fact) => fact.target === `${readinessConfig}#default` && fact.kind === 'export', ), - ); + ).toBe(true); write(root, layoutFile, emptyLayout); write(root, `${shellRoot}/package.json`, '{"name":"@fixture/shell"}'); write(root, 'zerops.yaml', '# - cd app && node scripts/reset.mjs'); write( root, compilerConfig, - await stringify({ + yield* stringify({ compilerOptions: { plugins: [{ name: pluginName }], types: [pluginName] }, }), ); @@ -153,94 +161,98 @@ await test('runtime consumers require the exact CSS, shell, deployment and compi `${vendorRoot}/ultramodern-performance-readiness.mjs`, `const configPath = '${readinessConfig}'; void configPath;`, ); - const changed = await facts(root); + const changed = yield* facts(root); for (const target of [cssUsed, launchedFile, resetFile, pluginName, readinessConfig]) { - assert.ok(!changed.some((fact) => fact.target === target), target); + expect(!changed.some((fact) => fact.target === target), target).toBe(true); } - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + }), +); -await test('compiler-option proof requires the installed pinned compiler and built-in plugin documentation', async () => { - const root = await fixture(); - try { +it.live( + 'compiler-option proof requires the installed pinned compiler and built-in plugin documentation', + Effect.fn(function* testEffect5() { + const root = yield* fixture(); write(root, tsgoReadme, '"name": "@effect/language-service"'); - const nameOnly = await facts(root); - assert.ok(!nameOnly.some((fact) => fact.kind === compilerOptionKind)); + const nameOnly = yield* facts(root); + expect(!nameOnly.some((fact) => fact.kind === compilerOptionKind)).toBe(true); write(root, tsgoReadme, compilerDocumentation); write(root, tsgoPackage, '{"name":"@effect/tsgo","version":"0.20.0"}'); - const wrongVersion = await facts(root); - assert.ok(!wrongVersion.some((fact) => fact.kind === compilerOptionKind)); + const wrongVersion = yield* facts(root); + expect(!wrongVersion.some((fact) => fact.kind === compilerOptionKind)).toBe(true); write(root, tsgoPackage, '{"name":"@effect/tsgo","version":"0.19.0"}'); write( root, compilerConfig, - await stringify({ + yield* stringify({ compilerOptions: { plugins: [{ name: pluginName }], types: [`${pluginName}/types`], }, }), ); - const typeImport = await facts(root); - assert.ok(!typeImport.some((fact) => fact.kind === compilerOptionKind)); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + const typeImport = yield* facts(root); + expect(!typeImport.some((fact) => fact.kind === compilerOptionKind)).toBe(true); + }), +); -await test('compiler configuration accepts JSONC but rejects malformed and schema-invalid input', async () => { - const root = await fixture(); - try { +it.live( + 'compiler configuration accepts JSONC but rejects malformed and schema-invalid input', + Effect.fn(function* testEffect6() { + const root = yield* fixture(); write( root, compilerConfig, `{ - // TypeScript permits comments and trailing commas. - "compilerOptions": { "plugins": [{ "name": "${pluginName}", }], }, + // TypeScript permits comments and trailing commas. + "compilerOptions": { "plugins": [{ "name": "${pluginName}", }], }, }`, ); - const modeled = await facts(root); - assert.ok(modeled.some((fact) => fact.kind === compilerOptionKind)); + const modeled = yield* facts(root); + expect(modeled.some((fact) => fact.kind === compilerOptionKind)).toBe(true); write(root, compilerConfig, '{ "compilerOptions": {'); - await assert.rejects(facts(root), /InvalidTsconfig/u); + yield* facts(root).pipe( + Effect.flip, + Effect.map((error) => expect(String(error)).toMatch(/InvalidTsconfig/u)), + ); write(root, compilerConfig, '{ "compilerOptions": { "plugins": false } }'); - await assert.rejects(facts(root), /plugins/u); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + yield* facts(root).pipe( + Effect.flip, + Effect.map((error) => expect(String(error)).toMatch(/plugins/u)), + ); + }), +); -await test('DTS compiler resolution belongs to the invoking workspace and excludes commented lookalikes', async () => { - const root = await fixture(); - try { +it.live( + 'DTS compiler resolution belongs to the invoking workspace and excludes commented lookalikes', + Effect.fn(function* testEffect7() { + const root = yield* fixture(); const configFile = `${shellRoot}/module-federation.config.ts`; const source = "import { resolveEffectTsgoCompiler } from '@modern-js/app-tools/config';\nconst compiler = resolveEffectTsgoCompiler({ from: import.meta.url });\nvoid compiler;"; write(root, configFile, source); write(root, `${shellRoot}/${tsgoReadme}`, 'tries `typescript`, then `@typescript/native`'); - const initial = await facts(root); - for (const target of [tsgoName, '@typescript/native']) { - assert.ok(initial.some((fact) => fact.target === target && fact.workspace === shellRoot)); + const initial = yield* facts(root); + for (const target of ['@effect/tsgo', '@typescript/native']) { + expect(initial.some((fact) => fact.target === target && fact.workspace === shellRoot)).toBe( + true, + ); } write(root, configFile, `/* ${source} */\nexport default {};`); - const commented = await facts(root); - assert.ok(!commented.some((fact) => fact.source === configFile)); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + const commented = yield* facts(root); + expect(!commented.some((fact) => fact.source === configFile)).toBe(true); + }), +); -await test('Lefthook configuration proves only intended tool usage and ignores commented hook text', async () => { - const root = await fixture(); - try { +it.live( + 'Lefthook configuration proves only intended tool usage and ignores commented hook text', + Effect.fn(function* testEffect8() { + const root = yield* fixture(); const source = 'pre-commit:\n commands:\n format:\n run: pnpm format\n'; write(root, 'lefthook.yml', source); - const configured = await facts(root); + const configured = yield* facts(root); const tool = configured.find((fact) => fact.target === 'lefthook'); - assert.equal(tool?.kind, 'dependency'); - assert.match(tool?.reason ?? '', /does not establish hook activation/u); + expect(tool?.kind).toBe('dependency'); + expect(tool?.reason ?? '').toMatch(/does not establish hook activation/u); write( root, 'lefthook.yml', @@ -249,132 +261,130 @@ await test('Lefthook configuration proves only intended tool usage and ignores c .map((line) => `# ${line}`) .join('\n'), ); - const commented = await facts(root); - assert.ok(!commented.some((fact) => fact.target === 'lefthook')); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + const commented = yield* facts(root); + expect(!commented.some((fact) => fact.target === 'lefthook')).toBe(true); + }), +); -await test('real Knip keeps unused neighboring files, dependency names and exports after runtime modeling', async () => { - const root = await fixture(); - try { +it.live( + 'real Knip keeps unused neighboring files, dependency names and exports after runtime modeling', + Effect.fn(function* testEffect9() { + const root = yield* fixture(); const consumerPath = path.join(root, '.audit/consumers.mts'); - const model = await runEffectTestPromise( - buildKnipModel( - root, - { - workspaces: { - '.': { entry: [], node: false, project: ['scripts/**/*.mjs'] }, - 'apps/*': { - entry: ['src/routes/layout.tsx'], - node: false, - project: ['scripts/**/*.mts', 'src/**/*.tsx'], - }, + const model = yield* buildKnipModel( + root, + { + workspaces: { + '.': { entry: [], node: false, project: ['scripts/**/*.mjs'] }, + 'apps/*': { + entry: ['src/routes/layout.tsx'], + node: false, + project: ['scripts/**/*.mts', 'src/**/*.tsx'], }, }, - consumerPath, - ).pipe(Effect.provide(NodeServices.layer)), - ); - const run = await runPinnedKnip(root, consumerPath, model); - assert.equal(run.error, undefined); - assert.ok(run.status === 0 || run.status === 1, run.stderr); - const report = await runEffectTestPromise( - Schema.decodeUnknownEffect( - Schema.fromJsonString( - Schema.Struct({ - issues: Schema.Array( - Schema.Struct({ - dependencies: Schema.Array(Schema.Struct({ name: Schema.String })), - exports: Schema.Array(Schema.Struct({ name: Schema.String })), - file: Schema.String, - files: Schema.Array(Schema.Struct({ name: Schema.String })), - }), - ), - }), - ), - )(run.stdout), + }, + consumerPath, + ).pipe(Effect.provide(NodeServices.layer)); + const run = yield* runPinnedKnip(root, consumerPath, model).pipe( + Effect.provide(NodeServices.layer), ); + expect(run.error).toBe(undefined); + expect(run.status === 0 || run.status === 1, run.stderr).toBe(true); + const report = yield* Schema.decodeUnknownEffect( + Schema.fromJsonString( + Schema.Struct({ + issues: Schema.Array( + Schema.Struct({ + dependencies: Schema.Array(Schema.Struct({ name: Schema.String })), + exports: Schema.Array(Schema.Struct({ name: Schema.String })), + file: Schema.String, + files: Schema.Array(Schema.Struct({ name: Schema.String })), + }), + ), + }), + ), + )(run.stdout); const unusedFiles = report.issues.flatMap((issue) => issue.files.map((item) => item.name)); - assert.ok(unusedFiles.some((file) => file.endsWith('/scripts/dead.mts'))); - assert.ok( + expect(unusedFiles.some((file) => file.endsWith('/scripts/dead.mts'))).toBe(true); + expect( !unusedFiles.some( (file) => file.endsWith('/scripts/launched.mts') || file === resetFile || file === readinessConfig, ), - ); + ).toBe(true); const dependencies = new Set( report.issues.flatMap((issue) => issue.dependencies.map((item) => item.name)), ); - assert.ok(dependencies.has('@fixture/css-dead')); - assert.ok(dependencies.has('@fixture/css-comment')); - assert.ok(!dependencies.has(cssUsed)); + expect(dependencies.has('@fixture/css-dead')).toBe(true); + expect(dependencies.has('@fixture/css-comment')).toBe(true); + expect(!dependencies.has(cssUsed)).toBe(true); const exports = new Set( report.issues.flatMap((issue) => issue.exports.map((item) => item.name)), ); - assert.ok( + expect( !report.issues.some( (issue) => issue.file === readinessConfig && issue.exports.some((item) => item.name === 'default'), ), - ); + ).toBe(true); for (const name of ['unusedLauncherExport', 'unusedResetExport', 'unusedConfigExport']) { - assert.ok(exports.has(name), name); + expect(exports.has(name), name).toBe(true); } - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + }), +); -await test('shared framework runner retains compiler/readiness evidence without accepting unused neighbors', async () => { - const root = await fixture(); - const runnerFile = 'scripts/shared/ultramodern-command.mts'; - try { - const runner = readFileSync( - new URL('../shared/ultramodern-command.mts', import.meta.url), - 'utf-8', - ); - write(root, runnerFile, runner); - for (const command of ['typecheck', 'performance-readiness']) { +it.live( + 'shared framework runner retains compiler/readiness evidence without accepting unused neighbors', + Effect.fn(function* mergedScenario1() { + const root = yield* fixture(); + const runnerFile = 'scripts/shared/ultramodern-command.mts'; + try { + const runner = readFileSync( + new URL('../shared/ultramodern-command.mts', import.meta.url), + 'utf-8', + ); + write(root, runnerFile, runner); + for (const command of ['typecheck', 'performance-readiness']) { + write( + root, + `scripts/ultramodern-${command}.mts`, + readFileSync(new URL(`../ultramodern-${command}.mts`, import.meta.url), 'utf-8'), + ); + } + const modeled = yield* facts(root); + for (const target of [tsgoName, pluginName, readinessConfig, `${readinessConfig}#default`]) { + expect( + modeled.some((fact) => fact.target === target), + target, + ).toBeTruthy(); + } write( root, - `scripts/ultramodern-${command}.mts`, - readFileSync(new URL(`../ultramodern-${command}.mts`, import.meta.url), 'utf-8'), + runnerFile, + runner.replace( + 'ChildProcess.make(launch.executable, launch.args,', + 'ChildProcess.make("unrelated", [],', + ), ); - } - const modeled = await facts(root); - for (const target of [tsgoName, pluginName, readinessConfig, `${readinessConfig}#default`]) { - assert.ok( - modeled.some((fact) => fact.target === target), - target, + const disconnected = yield* facts(root); + expect(!disconnected.some((fact) => fact.target === tsgoName)).toBeTruthy(); + expect(!disconnected.some((fact) => fact.target === readinessConfig)).toBeTruthy(); + write(root, runnerFile, runner); + write( + root, + 'scripts/ultramodern-typecheck.mts', + "import { runUltramodernScript } from './shared/unrelated.mts'; runUltramodernScript({ command: 'typecheck' });", ); + write( + root, + 'scripts/ultramodern-performance-readiness.mts', + "import { runUltramodernScript } from './shared/ultramodern-command.mts'; runUltramodernScript({ command: 'unrelated' });", + ); + const neighbors = yield* facts(root); + expect(!neighbors.some((fact) => fact.target === tsgoName)).toBeTruthy(); + expect(!neighbors.some((fact) => fact.target === readinessConfig)).toBeTruthy(); + } finally { + rmSync(root, { force: true, recursive: true }); } - write( - root, - runnerFile, - runner.replace( - 'ChildProcess.make(launch.executable, launch.args,', - 'ChildProcess.make("unrelated", [],', - ), - ); - const disconnected = await facts(root); - assert.ok(!disconnected.some((fact) => fact.target === tsgoName)); - assert.ok(!disconnected.some((fact) => fact.target === readinessConfig)); - write(root, runnerFile, runner); - write( - root, - 'scripts/ultramodern-typecheck.mts', - "import { runUltramodernScript } from './shared/unrelated.mts'; runUltramodernScript({ command: 'typecheck' });", - ); - write( - root, - 'scripts/ultramodern-performance-readiness.mts', - "import { runUltramodernScript } from './shared/ultramodern-command.mts'; runUltramodernScript({ command: 'unrelated' });", - ); - const neighbors = await facts(root); - assert.ok(!neighbors.some((fact) => fact.target === tsgoName)); - assert.ok(!neighbors.some((fact) => fact.target === readinessConfig)); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + }), +); diff --git a/app/scripts/tests/quality-audit-test-support.mts b/app/scripts/tests/quality-audit-test-support.mts index 2ace22af6..cd6d30704 100644 --- a/app/scripts/tests/quality-audit-test-support.mts +++ b/app/scripts/tests/quality-audit-test-support.mts @@ -1,40 +1,41 @@ import { spawnSync } from 'node:child_process'; -import { mkdirSync, writeFileSync } from 'node:fs'; import path from 'node:path'; -import { Schema } from 'effect'; -import { runEffectTestPromise } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; +import { Effect, FileSystem, Schema } from 'effect'; import { KnipConfigSchema } from '../../quality-audit/knip-model.mts'; const appRoot = path.resolve(import.meta.dirname, '../..'); -export const runPinnedKnip = async ( +export const runPinnedKnip = Effect.fn('runPinnedKnip')(function* runPinnedKnipEffect( root: string, consumerPath: string, model: { readonly config: typeof KnipConfigSchema.Type; readonly consumerSource: string; }, -) => { +) { + const fileSystem = yield* FileSystem.FileSystem; const directory = path.dirname(consumerPath); - mkdirSync(directory, { recursive: true }); - writeFileSync(consumerPath, model.consumerSource); + yield* fileSystem.makeDirectory(directory, { recursive: true }); + yield* fileSystem.writeFileString(consumerPath, model.consumerSource); const configPath = path.join(directory, 'knip.json'); - const configuration = await runEffectTestPromise( - Schema.encodeEffect(Schema.fromJsonString(KnipConfigSchema))(model.config), + const configuration = yield* Schema.encodeEffect(Schema.fromJsonString(KnipConfigSchema))( + model.config, ); - writeFileSync(configPath, configuration); - return spawnSync( - process.execPath, - [ - path.join(appRoot, 'node_modules/knip/bin/knip.js'), - '--directory', - root, - '--config', - configPath, - '--reporter', - 'json', - '--no-progress', - ], - { encoding: 'utf-8', timeout: 60_000 }, + yield* fileSystem.writeFileString(configPath, configuration); + return yield* Effect.sync(() => + spawnSync( + process.execPath, + [ + path.join(appRoot, 'node_modules/knip/bin/knip.js'), + '--directory', + root, + '--config', + configPath, + '--reporter', + 'json', + '--no-progress', + ], + { encoding: 'utf-8', timeout: 60_000 }, + ), ); -}; +}); diff --git a/app/scripts/tests/quality-audit.test.mts b/app/scripts/tests/quality-audit.test.mts index c689c7f7b..71885d3f0 100644 --- a/app/scripts/tests/quality-audit.test.mts +++ b/app/scripts/tests/quality-audit.test.mts @@ -1,5 +1,5 @@ -import assert from 'node:assert/strict'; -import { spawnSync } from 'node:child_process'; +import { expect, it } from 'effect-rstest'; + import { copyFileSync, mkdirSync, @@ -12,15 +12,13 @@ import { } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import test from 'node:test'; + import { NodeServices } from '@effect/platform-node'; -import { Effect, Layer, Schema } from 'effect'; +import { Effect, Schema } from 'effect'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; -import { - makeEffectTestCallback, - runEffectTestPromise, -} from '../../packages/core-runtime/src/testing/effect-runtime.ts'; + import { auditSteps, runQualityAudit, validateReport } from '../quality-audit.mts'; +import { collectToolingProcess } from './tooling-process-fixture.mts'; const FALLOW_CLONES = 'fallow-clones'; const FALLOW_SIMILARITY = 'fallow-similarity'; @@ -30,6 +28,7 @@ const REPORT_DIRECTORY = 'reports'; const SUMMARY_FILE = 'summary.json'; const GITIGNORE_FILE = '.gitignore'; const KNIP_CONFIG = 'quality-audit/knip.json'; +const PACKAGE_JSON = 'package.json'; const CALLER_OWNED_FILE = 'caller-owned.txt'; const ProvenanceSchema = Schema.fromJsonString( Schema.Struct({ @@ -38,165 +37,182 @@ const ProvenanceSchema = Schema.fromJsonString( }), ); const appRoot = path.resolve(import.meta.dirname, '../..'); +const includesPolicyFiles = ( + instances: readonly { readonly file: string }[], + policyFiles: readonly string[], +) => { + const names = new Set(instances.map((instance) => path.basename(instance.file))); + return policyFiles.every((file) => names.has(file)); +}; + const reportSchema = Schema.fromJsonString(Schema.Unknown); -const stringify = async (value: Schema.Json) => - await runEffectTestPromise(Schema.encodeEffect(reportSchema)(value)); -const validate = async (name: string, source: string) => - await runEffectTestPromise(validateReport(name, source)); +const encodeReport = Schema.encodeEffect(reportSchema); -await test('report-only analysis accepts findings and rejects empty or malformed reports', async () => { - const report = { - duplicates: [ - { - firstFile: { name: 'a.ts', start: 1 }, - lines: 12, - secondFile: { name: 'b.ts', start: 1 }, - tokens: 110, - }, - ], - statistics: { total: { clones: 1, sources: 2 } }, - }; - assert.deepEqual(await validate('jscpd', await stringify(report)), { - coverage: { tokenEligibleFiles: 2 }, - files: 2, - findings: 1, - }); - await assert.rejects(validate('jscpd', '{}'), /Malformed analyzer report/u); - await assert.rejects(validate('jscpd', '{broken'), /Malformed analyzer report/u); - await assert.rejects( - validate( - 'jscpd', - await stringify({ duplicates: [], statistics: { total: { clones: 0, sources: 0 } } }), - ), - /no files/u, - ); - await assert.rejects( - validate( - 'jscpd', - await stringify({ ...report, statistics: { total: { clones: 0, sources: 2 } } }), - ), - /count disagrees/u, - ); -}); +it.effect( + 'report-only analysis accepts findings and rejects empty or malformed reports', + Effect.fn(function* testEffect3() { + const report = { + duplicates: [ + { + firstFile: { name: 'a.ts', start: 1 }, + lines: 12, + secondFile: { name: 'b.ts', start: 1 }, + tokens: 110, + }, + ], + statistics: { total: { clones: 1, sources: 2 } }, + }; + expect(yield* validateReport('jscpd', yield* encodeReport(report))).toEqual({ + coverage: { tokenEligibleFiles: 2 }, + files: 2, + findings: 1, + }); + const emptyReportError = yield* Effect.flip(validateReport('jscpd', '{}')); + expect(emptyReportError.message).toMatch(/Malformed analyzer report/u); + const malformedReportError = yield* Effect.flip(validateReport('jscpd', '{broken')); + expect(malformedReportError.message).toMatch(/Malformed analyzer report/u); + const emptySourceError = yield* Effect.flip( + validateReport( + 'jscpd', + yield* encodeReport({ duplicates: [], statistics: { total: { clones: 0, sources: 0 } } }), + ), + ); + expect(emptySourceError.message).toMatch(/no files/u); + const cloneCountError = yield* Effect.flip( + validateReport( + 'jscpd', + yield* encodeReport({ ...report, statistics: { total: { clones: 0, sources: 2 } } }), + ), + ); + expect(cloneCountError.message).toMatch(/count disagrees/u); + }), +); -await test('Knip coverage is mandatory and findings count categories rather than files', async () => { - const findings = await stringify({ issues: [] }); - const coverage = await stringify({ - coverage: { processed: 12, total: 12 }, - findingCounts: { exports: 4, files: 2 }, - workspaces: ['.'], - }); - assert.deepEqual(await validate('knip', `${findings}\n${coverage}`), { - coverage: { +it.effect( + 'Knip coverage is mandatory and findings count categories rather than files', + Effect.fn(function* testEffect4() { + const findings = yield* encodeReport({ issues: [] }); + const coverage = yield* encodeReport({ + coverage: { processed: 12, total: 12 }, findingCounts: { exports: 4, files: 2 }, - processed: 12, - total: 12, workspaces: ['.'], - }, - files: 12, - findings: 6, - }); - await assert.rejects(validate('knip', findings), /coverage records/u); - await assert.rejects( - validate( - 'knip', - `${findings}\n${await stringify({ coverage: { processed: 0, total: 0 }, findingCounts: {}, workspaces: ['.'] })}`, - ), - /no files/u, - ); -}); + }); + expect(yield* validateReport('knip', `${findings}\n${coverage}`)).toEqual({ + coverage: { + findingCounts: { exports: 4, files: 2 }, + processed: 12, + total: 12, + workspaces: ['.'], + }, + files: 12, + findings: 6, + }); + const missingCoverageError = yield* Effect.flip(validateReport('knip', findings)); + expect(missingCoverageError.message).toMatch(/coverage records/u); + const emptyCoverageError = yield* Effect.flip( + validateReport( + 'knip', + `${findings}\n${yield* encodeReport({ coverage: { processed: 0, total: 0 }, findingCounts: {}, workspaces: ['.'] })}`, + ), + ); + expect(emptyCoverageError.message).toMatch(/no files/u); + }), +); -await test('Fallow rejects missing discovery, unsupported schema and incomplete workspaces', async () => { - const report = { - clone_groups: [], - kind: 'dupes', - schema_version: 9, - stats: { clone_groups: 0, total_files: 2 }, - version: '3.22.0', - }; - assert.deepEqual(await validate(FALLOW_CLONES, await stringify(report)), { - coverage: { tokenEligibleFiles: 2 }, - files: 2, - findings: 0, - }); - await assert.rejects( - validate(FALLOW_CLONES, await stringify({ ...report, schema_version: 10 })), - /Malformed analyzer report/u, - ); - await assert.rejects( - validate( - FALLOW_CLONES, - await stringify({ - ...report, - workspace_diagnostics: [ - { kind: 'invalid-package-json', message: 'invalid package', path: 'packages/broken' }, - ], - }), - ), - /incomplete workspace/u, - ); - await assert.rejects( - validate('fallow-files', await stringify({ file_count: 2, files: ['a.ts'] })), - /count disagrees/u, - ); -}); +it.effect( + 'Fallow rejects missing discovery, unsupported schema and incomplete workspaces', + Effect.fn(function* testEffect5() { + const report = { + clone_groups: [], + kind: 'dupes', + schema_version: 9, + stats: { clone_groups: 0, total_files: 2 }, + version: '3.22.0', + }; + expect(yield* validateReport(FALLOW_CLONES, yield* encodeReport(report))).toEqual({ + coverage: { tokenEligibleFiles: 2 }, + files: 2, + findings: 0, + }); + const unsupportedSchemaError = yield* Effect.flip( + validateReport(FALLOW_CLONES, yield* encodeReport({ ...report, schema_version: 10 })), + ); + expect(unsupportedSchemaError.message).toMatch(/Malformed analyzer report/u); + const incompleteWorkspaceError = yield* Effect.flip( + validateReport( + FALLOW_CLONES, + yield* encodeReport({ + ...report, + workspace_diagnostics: [ + { kind: 'invalid-package-json', message: 'invalid package', path: 'packages/broken' }, + ], + }), + ), + ); + expect(incompleteWorkspaceError.message).toMatch(/incomplete workspace/u); + const fileCountError = yield* Effect.flip( + validateReport('fallow-files', yield* encodeReport({ file_count: 2, files: ['a.ts'] })), + ); + expect(fileCountError.message).toMatch(/count disagrees/u); + }), +); -await test('tool selection preserves the complete Fallow group', () => { - assert.deepEqual( - auditSteps('/app', '/output', 'fallow').map((step) => step.name), - ['fallow-files', FALLOW_CLONES, FALLOW_SIMILARITY, FALLOW_HEALTH], - ); - assert.deepEqual( - auditSteps('/app', '/output', 'knip').map((step) => step.name), - ['knip'], - ); - assert.equal(auditSteps('/app', '/output', 'all').length, 6); +it('tool selection preserves the complete Fallow group', () => { + expect(auditSteps('/app', '/output', 'fallow').map((step) => step.name)).toEqual([ + 'fallow-files', + FALLOW_CLONES, + FALLOW_SIMILARITY, + FALLOW_HEALTH, + ]); + expect(auditSteps('/app', '/output', 'knip').map((step) => step.name)).toEqual(['knip']); + expect(auditSteps('/app', '/output', 'all').length).toBe(6); }); -const createFixture = async () => { - const root = mkdtempSync(path.join(tmpdir(), 'ontos-quality-test-')); - mkdirSync(path.join(root, CONFIG_DIRECTORY)); - mkdirSync(path.join(root, 'scripts')); - mkdirSync(path.join(root, '.codex')); - writeFileSync(path.join(root, '.codex/caller-owned.txt'), 'keep'); - symlinkSync(path.join(appRoot, 'node_modules'), path.join(root, 'node_modules'), 'dir'); - writeFileSync( - path.join(root, 'package.json'), - await stringify({ name: 'quality-test', private: true, type: 'module' }), - ); - for (const name of ['scope.json', 'fallow.json', 'jscpd.json', 'knip-reporter.mts']) { - copyFileSync( - path.join(appRoot, CONFIG_DIRECTORY, name), - path.join(root, CONFIG_DIRECTORY, name), - ); - } - writeFileSync( - path.join(root, KNIP_CONFIG), - await stringify({ - entry: ['scripts/index.ts'], - lefthook: false, - node: false, - project: ['scripts/**/*.ts'], - }), - ); - const branches = Array.from( - { length: 15 }, - (_, index) => `if (input > ${index}) result += input * ${index};`, - ).join('\n'); - const body = `export function calculate(input: number) {\nlet result = input;\n${branches}\nreturn result;\n}\n`; - writeFileSync(path.join(root, 'scripts/index.ts'), body); - writeFileSync(path.join(root, 'scripts/dead.ts'), body.replace('calculate', 'unusedCalculation')); - return root; -}; +const createFixture = () => + Effect.gen(function* testEffect6() { + const root = yield* Effect.acquireRelease( + Effect.sync(() => mkdtempSync(path.join(tmpdir(), 'ontos-quality-test-'))), + (directory) => Effect.sync(() => rmSync(directory, { force: true, recursive: true })), + ); + mkdirSync(path.join(root, CONFIG_DIRECTORY)); + mkdirSync(path.join(root, 'scripts')); + mkdirSync(path.join(root, '.codex')); + writeFileSync(path.join(root, '.codex/caller-owned.txt'), 'keep'); + symlinkSync(path.join(appRoot, 'node_modules'), path.join(root, 'node_modules'), 'dir'); + writeFileSync( + path.join(root, PACKAGE_JSON), + yield* encodeReport({ name: 'quality-test', private: true, type: 'module' }), + ); + for (const name of ['scope.json', 'fallow.json', 'jscpd.json', 'knip-reporter.mts']) { + copyFileSync( + path.join(appRoot, CONFIG_DIRECTORY, name), + path.join(root, CONFIG_DIRECTORY, name), + ); + } + writeFileSync( + path.join(root, KNIP_CONFIG), + yield* encodeReport({ + entry: ['scripts/index.ts'], + lefthook: false, + node: false, + project: ['scripts/**/*.ts'], + }), + ); + const branches = Array.from( + { length: 15 }, + (_, index) => `if (input > ${index}) result += input * ${index};`, + ).join('\n'); + const body = `export function calculate(input: number) {\nlet result = input;\n${branches}\nreturn result;\n}\n`; + writeFileSync(path.join(root, 'scripts/index.ts'), body); + writeFileSync( + path.join(root, 'scripts/dead.ts'), + body.replace('calculate', 'unusedCalculation'), + ); + return root; + }); -const runFixture = async ( - root: string, - output: string, - tool: 'all' | 'knip' | 'jscpd' | 'fallow', -) => - await runEffectTestPromise( - runQualityAudit(root, output, tool).pipe(Effect.provide(NodeServices.layer)), - ); +const runFixture = (root: string, output: string, tool: 'all' | 'knip' | 'jscpd' | 'fallow') => + runQualityAudit(root, output, tool).pipe(Effect.provide(NodeServices.layer)); const SummarySchema = Schema.Struct({ mode: Schema.Literal('report-only'), @@ -214,22 +230,21 @@ const SummarySchema = Schema.Struct({ runDirectory: Schema.String, status: Schema.String, }); -const summary = async (output: string) => - await runEffectTestPromise( - Schema.decodeUnknownEffect(Schema.fromJsonString(SummarySchema))( - readFileSync(path.join(output, SUMMARY_FILE), 'utf-8'), - ), +const summary = (output: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(SummarySchema))( + readFileSync(path.join(output, SUMMARY_FILE), 'utf-8'), ); -await test('real Fallow separates UI penalties from control-flow complexity without hiding branches', async () => { - const root = await createFixture(); - const output = path.join(root, REPORT_DIRECTORY); - const props = Array.from({ length: 22 }, (_, index) => `p${index + 1}`).join(', '); - const branches = Array.from( - { length: 11 }, - (_, index) => `if (value === ${index + 1}) return ${index + 1};`, - ).join('\n'); - try { +it.live( + 'real Fallow separates UI penalties from control-flow complexity without hiding branches', + Effect.fn(function* testEffect9() { + const root = yield* createFixture(); + const output = path.join(root, REPORT_DIRECTORY); + const props = Array.from({ length: 22 }, (_, index) => `p${index + 1}`).join(', '); + const branches = Array.from( + { length: 11 }, + (_, index) => `if (value === ${index + 1}) return ${index + 1};`, + ).join('\n'); writeFileSync( path.join(root, 'scripts/metric-example.tsx'), `import { useState } from 'react'; @@ -245,52 +260,54 @@ export function branchHeavy(value: number) { } `, ); - await runFixture(root, output, 'fallow'); - const result = await summary(output); + yield* runFixture(root, output, 'fallow'); + const result = yield* summary(output); const healthDirectory = path.join(result.runDirectory, FALLOW_HEALTH); - const rows = await runEffectTestPromise( - Schema.decodeUnknownEffect( - Schema.fromJsonString( - Schema.Array( - Schema.Struct({ - controlFlowCognitive: Schema.Number, - exceedsControlFlowLimits: Schema.Boolean, - name: Schema.String, - weightedCognitive: Schema.Number, - }), - ), + const rows = yield* Schema.decodeUnknownEffect( + Schema.fromJsonString( + Schema.Array( + Schema.Struct({ + controlFlowCognitive: Schema.Number, + exceedsControlFlowLimits: Schema.Boolean, + name: Schema.String, + weightedCognitive: Schema.Number, + }), ), - )(readFileSync(path.join(healthDirectory, 'complexity.json'), 'utf-8')), - ); + ), + )(readFileSync(path.join(healthDirectory, 'complexity.json'), 'utf-8')); const panel = rows.find((row) => row.name === 'Panel'); const branchHeavy = rows.find((row) => row.name === 'branchHeavy'); - assert.ok(panel); - assert.equal(panel.weightedCognitive, 21); - assert.equal(panel.controlFlowCognitive, 0); - assert.equal(panel.exceedsControlFlowLimits, false); - assert.equal(branchHeavy?.exceedsControlFlowLimits, true); + expect(panel).toBeDefined(); + if (panel === undefined) { + throw new Error('Expected panel to be present'); + } + expect(panel.weightedCognitive).toBe(21); + expect(panel.controlFlowCognitive).toBe(0); + expect(panel.exceedsControlFlowLimits).toBe(false); + expect(branchHeavy?.exceedsControlFlowLimits).toBe(true); const raw = readFileSync(path.join(healthDirectory, 'report.json'), 'utf-8'); const corrupted = raw.replace( /(?"cognitive"\s*:\s*)21/u, (_match: string, prefix: string) => `${prefix}22`, ); - assert.notEqual(corrupted, raw); - await assert.rejects(validate(FALLOW_HEALTH, corrupted), /contributions disagree/u); + expect(corrupted).not.toBe(raw); + const contributionsError = yield* Effect.flip(validateReport(FALLOW_HEALTH, corrupted)); + expect(contributionsError.message).toMatch(/contributions disagree/u); const wrongCount = raw.replace( /"functions_above_threshold"\s*:\s*\d+/u, '"functions_above_threshold": 0', ); - await assert.rejects(validate(FALLOW_HEALTH, wrongCount), /count disagrees/u); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + const thresholdCountError = yield* Effect.flip(validateReport(FALLOW_HEALTH, wrongCount)); + expect(thresholdCountError.message).toMatch(/count disagrees/u); + }), +); -await test('primary clone detectors preserve policy literals while semantic similarity stays advisory', async () => { - const root = await createFixture(); - const output = path.join(root, REPORT_DIRECTORY); - const policyFiles = ['policy-read.ts', 'policy-write.ts']; - try { +it.live( + 'primary clone detectors preserve policy literals while semantic similarity stays advisory', + Effect.fn(function* testEffect10() { + const root = yield* createFixture(); + const output = path.join(root, REPORT_DIRECTORY); + const policyFiles = ['policy-read.ts', 'policy-write.ts']; for (const [index, file] of policyFiles.entries()) { const policy = Array.from( { length: 16 }, @@ -302,8 +319,8 @@ await test('primary clone detectors preserve policy literals while semantic simi `export function selectPolicy(subject: string) {\nreturn {\n${policy}\n};\n}\n`, ); } - await runFixture(root, output, 'all'); - const result = await summary(output); + yield* runFixture(root, output, 'all'); + const result = yield* summary(output); const schema = Schema.fromJsonString( Schema.Struct({ clone_groups: Schema.Array( @@ -311,95 +328,90 @@ await test('primary clone detectors preserve policy literals while semantic simi ), }), ); - await Promise.all( - [FALLOW_CLONES, FALLOW_SIMILARITY].map(async (name) => { - const report = await runEffectTestPromise( - Schema.decodeUnknownEffect(schema)( + yield* Effect.all( + [FALLOW_CLONES, FALLOW_SIMILARITY].map((name) => + Effect.gen(function* testEffect11() { + const report = yield* Schema.decodeUnknownEffect(schema)( readFileSync(path.join(result.runDirectory, name, 'report.json'), 'utf-8'), - ), - ); - const matchesDistinctPolicies = report.clone_groups.some((group) => - policyFiles.every((file) => - group.instances.some((instance) => path.basename(instance.file) === file), - ), - ); - assert.equal(matchesDistinctPolicies, name === FALLOW_SIMILARITY, name); - }), - ); - const jscpd = await runEffectTestPromise( - Schema.decodeUnknownEffect( - Schema.fromJsonString( - Schema.Struct({ - duplicates: Schema.Array( - Schema.Struct({ - firstFile: Schema.Struct({ name: Schema.String }), - secondFile: Schema.Struct({ name: Schema.String }), - }), - ), - }), - ), - )(readFileSync(path.join(result.runDirectory, 'jscpd/report.json'), 'utf-8')), + ); + const matchesDistinctPolicies = report.clone_groups.some((group) => + includesPolicyFiles(group.instances, policyFiles), + ); + expect(matchesDistinctPolicies, name).toBe(name === FALLOW_SIMILARITY); + }), + ), + { concurrency: 'unbounded' }, ); - assert.equal( + const jscpd = yield* Schema.decodeUnknownEffect( + Schema.fromJsonString( + Schema.Struct({ + duplicates: Schema.Array( + Schema.Struct({ + firstFile: Schema.Struct({ name: Schema.String }), + secondFile: Schema.Struct({ name: Schema.String }), + }), + ), + }), + ), + )(readFileSync(path.join(result.runDirectory, 'jscpd/report.json'), 'utf-8')); + expect( jscpd.duplicates.some((pair) => policyFiles.every((file) => [pair.firstFile.name, pair.secondFile.name].some((name) => path.basename(name) === file), ), ), - false, - ); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + ).toBe(false); + }), +); -await test('real pinned tools report debt successfully and isolate stale reports after invalid config', async () => { - const root = await createFixture(); - const output = path.join(root, REPORT_DIRECTORY); - try { - await runFixture(root, output, 'all'); - const first = await summary(output); - assert.deepEqual(readdirSync(path.join(root, '.codex')), [CALLER_OWNED_FILE]); - assert.equal(first.status, 'reported'); - assert.equal(first.results.length, 6); +it.live( + 'real pinned tools report debt successfully and isolate stale reports after invalid config', + Effect.fn(function* testEffect12() { + const root = yield* createFixture(); + const output = path.join(root, REPORT_DIRECTORY); + yield* runFixture(root, output, 'all'); + const first = yield* summary(output); + expect(readdirSync(path.join(root, '.codex'))).toEqual([CALLER_OWNED_FILE]); + expect(first.status).toBe('reported'); + expect(first.results.length).toBe(6); for (const name of ['knip', 'jscpd', FALLOW_CLONES, FALLOW_HEALTH]) { - assert.ok( + expect( first.results.some((row) => row.name === name && row.findings > 0), `${name} must report injected debt`, - ); + ).toBe(true); } writeFileSync(path.join(root, 'quality-audit/jscpd.json'), '{invalid unrelated config'); - await runFixture(root, output, 'knip'); + yield* runFixture(root, output, 'knip'); writeFileSync(path.join(root, KNIP_CONFIG), '{invalid'); - await assert.rejects(runFixture(root, output, 'knip'), /analysis failed/u); - const second = await summary(output); - assert.equal(second.status, 'error'); - assert.notEqual(second.runDirectory, first.runDirectory); - assert.equal(second.results[0]?.status, 'error'); - assert.deepEqual(readdirSync(path.join(root, '.codex')), [CALLER_OWNED_FILE]); - assert.equal(readFileSync(path.join(root, '.codex/caller-owned.txt'), 'utf-8'), 'keep'); - assert.match( - readFileSync(path.join(output, 'summary.md'), 'utf-8'), + const invalidConfigError = yield* Effect.flip(runFixture(root, output, 'knip')); + expect(invalidConfigError.message).toMatch(/analysis failed/u); + const second = yield* summary(output); + expect(second.status).toBe('error'); + expect(second.runDirectory).not.toBe(first.runDirectory); + expect(second.results[0]?.status).toBe('error'); + expect(readdirSync(path.join(root, '.codex'))).toEqual([CALLER_OWNED_FILE]); + expect(readFileSync(path.join(root, '.codex/caller-owned.txt'), 'utf-8')).toBe('keep'); + expect(readFileSync(path.join(output, 'summary.md'), 'utf-8')).toMatch( /Malformed .*configs\/knip\.json/u, ); - assert.ok( + expect( readFileSync(path.join(first.runDirectory, 'knip/report.ndjson'), 'utf-8').length > 0, - ); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + ).toBe(true); + }), +); -await test('missing binaries and an empty source scope fail with preserved summaries', async () => { - const root = await createFixture(); - const output = path.join(root, REPORT_DIRECTORY); - try { +it.live( + 'missing binaries and an empty source scope fail with preserved summaries', + Effect.fn(function* testEffect13() { + const root = yield* createFixture(); + const output = path.join(root, REPORT_DIRECTORY); rmSync(path.join(root, 'node_modules')); - await assert.rejects(runFixture(root, output, 'knip'), /analysis failed/u); - const missing = await summary(output); - assert.equal(missing.status, 'error'); + const missingBinaryError = yield* Effect.flip(runFixture(root, output, 'knip')); + expect(missingBinaryError.message).toMatch(/analysis failed/u); + const missing = yield* summary(output); + expect(missing.status).toBe('error'); const failedDirectory = path.join(missing.runDirectory, 'knip'); - assert.deepEqual(missing.results, [ + expect(missing.results).toEqual([ { coverage: {}, diagnostic: readFileSync( @@ -413,17 +425,17 @@ await test('missing binaries and an empty source scope fail with preserved summa status: 'error', }, ]); - assert.match( - readFileSync(path.join(missing.runDirectory, 'knip/metadata.json'), 'utf-8'), + expect(readFileSync(path.join(missing.runDirectory, 'knip/metadata.json'), 'utf-8')).toMatch( /Missing pinned local binary/u, ); writeFileSync( path.join(root, 'quality-audit/scope.json'), - await stringify({ exclude: [], patterns: ['absent/**/*.ts'] }), + yield* encodeReport({ exclude: [], patterns: ['absent/**/*.ts'] }), ); - await assert.rejects(runFixture(root, output, 'jscpd'), /analysis failed/u); - const empty = await summary(output); - assert.deepEqual(empty.results, [ + const emptyScopeError = yield* Effect.flip(runFixture(root, output, 'jscpd')); + expect(emptyScopeError.message).toMatch(/analysis failed/u); + const empty = yield* summary(output); + expect(empty.results).toEqual([ { coverage: {}, diagnostic: 'QualityAuditError: Source inventory: analysis contains no files', @@ -434,25 +446,21 @@ await test('missing binaries and an empty source scope fail with preserved summa status: 'error', }, ]); - assert.match( - readFileSync(path.join(output, SUMMARY_FILE), 'utf-8'), + expect(readFileSync(path.join(output, SUMMARY_FILE), 'utf-8')).toMatch( /Source inventory: analysis contains no files/u, ); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + }), +); -await test('the CLI handles escaped paths, foreign cwd and untracked source provenance', async () => { - const root = await createFixture(); - const output = path.join(root, REPORT_DIRECTORY); - try { - await runEffectTestPromise( - Effect.gen(function* initializeFixtureRepository() { - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - yield* spawner.string(ChildProcess.make('git', ['init', '-q', root])); - }).pipe(Effect.provide(NodeServices.layer)), - ); +it.live( + 'the CLI handles forced CI colors, escaped paths, foreign cwd and untracked source provenance', + Effect.fn(function* testEffect14() { + const root = yield* createFixture(); + const output = path.join(root, REPORT_DIRECTORY); + yield* Effect.gen(function* initializeFixtureRepository() { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + yield* spawner.string(ChildProcess.make('git', ['init', '-q', root])); + }).pipe(Effect.provide(NodeServices.layer)); mkdirSync(path.join(root, 'scripts/shared'), { recursive: true }); copyFileSync( path.join(appRoot, 'scripts/shared/ultramodern-wrapper-source.mts'), @@ -470,239 +478,272 @@ await test('the CLI handles escaped paths, foreign cwd and untracked source prov path.join(root, CONFIG_DIRECTORY, file), ); } - const result = spawnSync(process.execPath, [executable, '--tool', 'knip', '--output', output], { - cwd: tmpdir(), - encoding: 'utf-8', - timeout: 60_000, - }); - assert.equal( - result.error, - undefined, - `CLI spawn failed: ${String(result.error)}\n${result.stdout}\n${result.stderr}`, - ); - assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); - const report = await summary(output); - assert.equal(report.status, 'reported'); - const provenance = await runEffectTestPromise( - Schema.decodeUnknownEffect(ProvenanceSchema)( - readFileSync(path.join(report.runDirectory, 'provenance.json'), 'utf-8'), - ), + const result = yield* collectToolingProcess( + ChildProcess.make(process.execPath, [executable, '--tool', 'knip', '--output', output], { + cwd: tmpdir(), + env: { CI: 'true', FORCE_COLOR: '1', GITHUB_ACTIONS: 'true', NO_COLOR: '1' }, + extendEnv: true, + stderr: 'pipe', + stdin: 'ignore', + stdout: 'pipe', + }), + ).pipe(Effect.scoped, Effect.timeout('60 seconds'), Effect.provide(NodeServices.layer)); + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + const report = yield* summary(output); + expect(report.status).toBe('reported'); + expect(readFileSync(path.join(report.runDirectory, 'knip/stderr.txt'), 'utf-8')).toBe(''); + const provenance = yield* Schema.decodeUnknownEffect(ProvenanceSchema)( + readFileSync(path.join(report.runDirectory, 'provenance.json'), 'utf-8'), ); - assert.equal(provenance.sourceState, 'modified'); - assert.ok(provenance.workingTreeChanges.some((file) => file === '?? scripts/index.ts')); - assert.ok(!provenance.workingTreeChanges.some((file) => file.startsWith('?? reports/'))); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); - -void test( - 'external report directories preserve valid Fallow exclusions and source coverage', - makeEffectTestCallback( - Effect.scoped( - Layer.build( - Layer.effectDiscard( - Effect.gen(function* externalReportDirectory() { - const root = yield* Effect.acquireRelease(Effect.promise(createFixture), (directory) => - Effect.sync(() => rmSync(directory, { force: true, recursive: true })), - ); - const output = yield* Effect.acquireRelease( - Effect.sync(() => mkdtempSync(path.join(tmpdir(), 'ontos-external-report-'))), - (directory) => Effect.sync(() => rmSync(directory, { force: true, recursive: true })), - ); - const generatedTypes = path.join(root, 'apps/shell/@mf-types/remote'); - mkdirSync(generatedTypes, { recursive: true }); - writeFileSync(path.join(root, GITIGNORE_FILE), '**/@mf-types/\n'); - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const initialized = yield* spawner.exitCode( - ChildProcess.make('git', ['init', '-q'], { cwd: root }), - ); - assert.equal(Number(initialized), 0); - writeFileSync( - path.join(generatedTypes, 'index.d.ts'), - 'export declare const remoteComponent: unknown;\n', - ); - yield* runQualityAudit(root, output, 'fallow'); - const result = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(SummarySchema))( - readFileSync(path.join(output, SUMMARY_FILE), 'utf-8'), - ); - assert.equal(result.status, 'reported'); - assert.equal(result.results.length, 4); - assert.ok(result.results.every((row) => row.status === 'reported' && row.files > 0)); - const coverage = yield* Schema.decodeUnknownEffect( - Schema.fromJsonString( - Schema.Struct({ - extra: Schema.Array(Schema.String), - intendedSources: Schema.Number, - missing: Schema.Array(Schema.String), - }), - ), - )(readFileSync(path.join(result.runDirectory, 'coverage.json'), 'utf-8')); - // Two authored fixture sources plus the copied Knip reporter, not remote declarations. - assert.deepEqual(coverage, { extra: [], intendedSources: 3, missing: [] }); - assert.ok(result.results.some((row) => row.name === FALLOW_HEALTH && row.findings > 0)); - assert.equal( - readFileSync(path.join(result.runDirectory, 'configs/fallow.json'), 'utf-8'), - readFileSync(path.join(root, 'quality-audit/fallow.json'), 'utf-8'), - ); - }), - ).pipe(Layer.provide(NodeServices.layer)), - ), - ), - ), + expect(provenance.sourceState).toBe('modified'); + expect(provenance.workingTreeChanges.some((file) => file === '?? scripts/index.ts')).toBe(true); + expect(!provenance.workingTreeChanges.some((file) => file.startsWith('?? reports/'))).toBe( + true, + ); + }), ); -await test('output inside a source root fails before creating analyzer snapshots', async () => { - const root = await createFixture(); - const output = path.join(root, 'scripts/reports'); - try { - await assert.rejects(runFixture(root, output, 'all'), /analysis failed/u); - const report = await summary(output); - assert.equal(report.results[0]?.name, 'setup'); - assert.match( - readFileSync(path.join(output, 'summary.md'), 'utf-8'), +it.live( + 'output inside a source root fails before creating analyzer snapshots', + Effect.fn(function* testEffect15() { + const root = yield* createFixture(); + const output = path.join(root, 'scripts/reports'); + const sourceOutputError = yield* Effect.flip(runFixture(root, output, 'all')); + expect(sourceOutputError.message).toMatch(/analysis failed/u); + const report = yield* summary(output); + expect(report.results[0]?.name).toBe('setup'); + expect(readFileSync(path.join(output, 'summary.md'), 'utf-8')).toMatch( /output directory outside configured source roots/u, ); - assert.deepEqual(readdirSync(report.runDirectory), []); - assert.deepEqual(readdirSync(path.join(root, '.codex')), [CALLER_OWNED_FILE]); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + expect(readdirSync(report.runDirectory)).toEqual([]); + expect(readdirSync(path.join(root, '.codex'))).toEqual([CALLER_OWNED_FILE]); + }), +); -await test('symlink output cannot place snapshots in source roots but permits report targets', async () => { - const root = await createFixture(); - const output = path.join(root, 'reports-link'); - const sourceOutput = path.join(root, 'scripts/reports'); - const safeOutput = path.join(root, REPORT_DIRECTORY); - try { +it.live( + 'symlink output cannot place snapshots in source roots but permits report targets', + Effect.fn(function* testEffect16() { + const root = yield* createFixture(); + const output = path.join(root, 'reports-link'); + const sourceOutput = path.join(root, 'scripts/reports'); + const safeOutput = path.join(root, REPORT_DIRECTORY); mkdirSync(sourceOutput); symlinkSync(sourceOutput, output, 'dir'); - await assert.rejects(runFixture(root, output, 'all'), /analysis failed/u); - const rejected = await summary(output); - assert.equal(rejected.results[0]?.name, 'setup'); - assert.match( - rejected.results[0]?.diagnostic ?? '', + const symlinkOutputError = yield* Effect.flip(runFixture(root, output, 'all')); + expect(symlinkOutputError.message).toMatch(/analysis failed/u); + const rejected = yield* summary(output); + expect(rejected.results[0]?.name).toBe('setup'); + expect(rejected.results[0]?.diagnostic ?? '').toMatch( /output directory outside configured source roots/u, ); - assert.deepEqual(readdirSync(rejected.runDirectory), []); - assert.deepEqual(readdirSync(path.join(root, '.codex')), [CALLER_OWNED_FILE]); + expect(readdirSync(rejected.runDirectory)).toEqual([]); + expect(readdirSync(path.join(root, '.codex'))).toEqual([CALLER_OWNED_FILE]); rmSync(output); mkdirSync(safeOutput); symlinkSync(safeOutput, output, 'dir'); - await runFixture(root, output, 'jscpd'); - const accepted = await summary(output); - assert.equal(accepted.status, 'reported'); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + yield* runFixture(root, output, 'jscpd'); + const accepted = yield* summary(output); + expect(accepted.status).toBe('reported'); + }), +); -await test('custom output does not mark clean source provenance as modified', async () => { - const root = await createFixture(); - const output = path.join(root, REPORT_DIRECTORY); - try { +it.live( + 'custom output does not mark clean source provenance as modified', + Effect.fn(function* testEffect17() { + const root = yield* createFixture(); + const output = path.join(root, REPORT_DIRECTORY); writeFileSync(path.join(root, GITIGNORE_FILE), 'node_modules\n.codex\n'); - await runEffectTestPromise( - Effect.gen(function* commitFixture() { - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const commands = [ - ['init', '-q'], - ['add', GITIGNORE_FILE, 'package.json', CONFIG_DIRECTORY, 'scripts'], - [ - '-c', - `core.hooksPath=${path.join(root, '.git/no-hooks')}`, - '-c', - 'user.name=Audit test', - '-c', - 'user.email=audit@example.invalid', - '-c', - 'commit.gpgSign=false', - 'commit', - '-qm', - 'Fixture source', - ], - ]; - yield* Effect.forEach( - commands, - (args) => - spawner - .exitCode(ChildProcess.make('git', args, { cwd: root })) - .pipe(Effect.tap((code) => Effect.sync(() => assert.equal(Number(code), 0)))), - { concurrency: 1 }, - ); - }).pipe(Effect.provide(NodeServices.layer)), - ); - await runFixture(root, output, 'jscpd'); - const report = await summary(output); - const provenance = await runEffectTestPromise( - Schema.decodeUnknownEffect(ProvenanceSchema)( - readFileSync(path.join(report.runDirectory, 'provenance.json'), 'utf-8'), - ), + yield* Effect.gen(function* commitFixture() { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const commands = [ + ['init', '-q'], + ['add', GITIGNORE_FILE, PACKAGE_JSON, CONFIG_DIRECTORY, 'scripts'], + [ + '-c', + `core.hooksPath=${path.join(root, '.git/no-hooks')}`, + '-c', + 'user.name=Audit test', + '-c', + 'user.email=audit@example.invalid', + '-c', + 'commit.gpgSign=false', + 'commit', + '-qm', + 'Fixture source', + ], + ]; + yield* Effect.forEach( + commands, + (args) => + spawner + .exitCode(ChildProcess.make('git', args, { cwd: root })) + .pipe(Effect.map((code) => expect(Number(code)).toBe(0))), + { concurrency: 1 }, + ); + }).pipe(Effect.provide(NodeServices.layer)); + yield* runFixture(root, output, 'jscpd'); + const report = yield* summary(output); + const provenance = yield* Schema.decodeUnknownEffect(ProvenanceSchema)( + readFileSync(path.join(report.runDirectory, 'provenance.json'), 'utf-8'), ); - assert.equal(provenance.sourceState, 'clean'); - assert.deepEqual(provenance.workingTreeChanges, []); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + expect(provenance.sourceState).toBe('clean'); + expect(provenance.workingTreeChanges).toEqual([]); + }), +); -await test('narrowed workspace and Fallow source discovery produce coverage errors', async () => { - const root = await createFixture(); - const output = path.join(root, REPORT_DIRECTORY); - try { +it.live( + 'narrowed workspace and Fallow source discovery produce coverage errors', + Effect.fn(function* testEffect18() { + const root = yield* createFixture(); + const output = path.join(root, REPORT_DIRECTORY); mkdirSync(path.join(root, 'packages/omitted'), { recursive: true }); writeFileSync( path.join(root, 'packages/omitted/package.json'), - await stringify({ name: 'omitted', private: true }), - ); - await assert.rejects(runFixture(root, output, 'knip'), /analysis failed/u); - const narrowed = await summary(output); - assert.equal(narrowed.results[0]?.status, 'reported'); - assert.equal(narrowed.results.at(-1)?.name, 'coverage'); - assert.match(narrowed.results.at(-1)?.diagnostic ?? '', /Knip workspace coverage mismatch/u); - assert.ok( - narrowed.results.some((result) => result.name === 'coverage' && result.status === 'error'), + yield* encodeReport({ name: 'omitted', private: true }), ); + const narrowedWorkspaceError = yield* Effect.flip(runFixture(root, output, 'knip')); + expect(narrowedWorkspaceError.message).toMatch(/analysis failed/u); + const narrowed = yield* summary(output); + expect(narrowed.results[0]?.status).toBe('reported'); + expect(narrowed.results.at(-1)?.name).toBe('coverage'); + expect(narrowed.results.at(-1)?.diagnostic ?? '').toMatch(/Knip workspace coverage mismatch/u); + expect( + narrowed.results.some((result) => result.name === 'coverage' && result.status === 'error'), + ).toBe(true); writeFileSync( path.join(root, 'quality-audit/fallow.json'), - await stringify({ ignorePatterns: ['scripts/**', 'node_modules/**', 'packages/**'] }), + yield* encodeReport({ ignorePatterns: ['scripts/**', 'node_modules/**', 'packages/**'] }), ); - await assert.rejects(runFixture(root, output, 'fallow'), /analysis failed/u); - const omitted = await summary(output); - assert.ok( + const omittedSourceError = yield* Effect.flip(runFixture(root, output, 'fallow')); + expect(omittedSourceError.message).toMatch(/analysis failed/u); + const omitted = yield* summary(output); + expect( omitted.results.some((result) => result.name === 'coverage' && result.status === 'error'), - ); - assert.match( - readFileSync(path.join(omitted.runDirectory, 'coverage.json'), 'utf-8'), + ).toBe(true); + expect(readFileSync(path.join(omitted.runDirectory, 'coverage.json'), 'utf-8')).toMatch( /scripts\/index.ts/u, ); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + }), +); -await test('a selected tool with the wrong installed version fails before launch', async () => { - const root = await createFixture(); - const output = path.join(root, REPORT_DIRECTORY); - try { +it.live( + 'a selected tool with the wrong installed version fails before launch', + Effect.fn(function* testEffect19() { + const root = yield* createFixture(); + const output = path.join(root, REPORT_DIRECTORY); // Replace only the fixture's symlink; never mutate the shared installed dependencies. rmSync(path.join(root, 'node_modules')); mkdirSync(path.join(root, 'node_modules/knip/bin'), { recursive: true }); writeFileSync(path.join(root, 'node_modules/knip/bin/knip.js'), 'must never execute'); writeFileSync( path.join(root, 'node_modules/knip/package.json'), - await stringify({ version: '0.0.0' }), + yield* encodeReport({ version: '0.0.0' }), ); - await assert.rejects(runFixture(root, output, 'knip'), /analysis failed/u); - const mismatch = await summary(output); - assert.match( - readFileSync(path.join(mismatch.runDirectory, 'knip/metadata.json'), 'utf-8'), + const versionError = yield* Effect.flip(runFixture(root, output, 'knip')); + expect(versionError.message).toMatch(/analysis failed/u); + const mismatch = yield* summary(output); + expect(readFileSync(path.join(mismatch.runDirectory, 'knip/metadata.json'), 'utf-8')).toMatch( /Expected knip 6\.34\.0, found 0\.0\.0/u, ); - assert.equal(readFileSync(path.join(mismatch.runDirectory, 'knip/stdout.txt'), 'utf-8'), ''); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + expect(readFileSync(path.join(mismatch.runDirectory, 'knip/stdout.txt'), 'utf-8')).toBe(''); + }), +); + +it.live( + 'jscpd accepts only its config banner and preserves additional diagnostics on failure', + Effect.fn(function* testAnalyzerDiagnostics() { + const root = yield* createFixture(); + const output = path.join(root, REPORT_DIRECTORY); + // Replace only the fixture symlink, retaining the real installed tools unchanged. + rmSync(path.join(root, 'node_modules')); + const toolDirectory = path.join(root, 'node_modules/jscpd'); + mkdirSync(toolDirectory, { recursive: true }); + writeFileSync( + path.join(toolDirectory, PACKAGE_JSON), + yield* encodeReport({ type: 'module', version: '5.1.2' }), + ); + const report = yield* encodeReport({ + duplicates: [], + statistics: { total: { clones: 0, sources: 2 } }, + }); + const warning = 'Warning: unable to parse scripts/dead.ts'; + for (const diagnostic of ['', `${warning}\n`]) { + writeFileSync( + path.join(toolDirectory, 'run-jscpd.js'), + [ + "import { writeFileSync } from 'node:fs';", + "import path from 'node:path';", + "console.error('Using config from ' + process.argv[3]);", + `process.stderr.write(${JSON.stringify(diagnostic)});`, + `writeFileSync(path.join(process.argv[5], 'jscpd-report.json'), ${JSON.stringify(report)});`, + ].join('\n'), + ); + if (diagnostic) { + const issue = yield* Effect.flip(runFixture(root, output, 'jscpd')); + expect(issue.message).toMatch(/analysis failed/u); + } else { + yield* runFixture(root, output, 'jscpd'); + } + const result = yield* summary(output); + const directory = path.join(result.runDirectory, 'jscpd'); + expect(result.status).toBe(diagnostic ? 'error' : 'reported'); + expect(readFileSync(path.join(directory, 'stderr.txt'), 'utf-8')).toBe( + `Using config from ${result.runDirectory}/jscpd.config.json\n${diagnostic}`, + ); + expect(readFileSync(path.join(directory, 'jscpd-report.json'), 'utf-8')).toBe(report); + if (diagnostic) { + expect(result.results[0]?.diagnostic).toMatch(/Analyzer emitted diagnostics/u); + expect(readFileSync(path.join(directory, 'validation-error.txt'), 'utf-8')).toMatch( + /Analyzer emitted diagnostics/u, + ); + } + } + }), +); + +it.live('external report directories preserve valid Fallow exclusions and source coverage', () => + Effect.gen(function* externalReportDirectory() { + const root = yield* createFixture(); + const output = yield* Effect.acquireRelease( + Effect.sync(() => mkdtempSync(path.join(tmpdir(), 'ontos-external-report-'))), + (directory) => Effect.sync(() => rmSync(directory, { force: true, recursive: true })), + ); + const generatedTypes = path.join(root, 'apps/shell/@mf-types/remote'); + mkdirSync(generatedTypes, { recursive: true }); + writeFileSync(path.join(root, GITIGNORE_FILE), '**/@mf-types/\n'); + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const initialized = yield* spawner.exitCode( + ChildProcess.make('git', ['init', '-q'], { cwd: root }), + ); + expect(Number(initialized)).toBe(0); + writeFileSync( + path.join(generatedTypes, 'index.d.ts'), + 'export declare const remoteComponent: unknown;\n', + ); + yield* runQualityAudit(root, output, 'fallow'); + const result = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(SummarySchema))( + readFileSync(path.join(output, SUMMARY_FILE), 'utf-8'), + ); + expect(result.status).toBe('reported'); + expect(result.results.length).toBe(4); + expect(result.results.every((row) => row.status === 'reported' && row.files > 0)).toBeTruthy(); + const coverage = yield* Schema.decodeUnknownEffect( + Schema.fromJsonString( + Schema.Struct({ + extra: Schema.Array(Schema.String), + intendedSources: Schema.Number, + missing: Schema.Array(Schema.String), + }), + ), + )(readFileSync(path.join(result.runDirectory, 'coverage.json'), 'utf-8')); + // Two authored fixture sources plus the copied Knip reporter, not remote declarations. + expect(coverage).toEqual({ extra: [], intendedSources: 3, missing: [] }); + expect( + result.results.some((row) => row.name === FALLOW_HEALTH && row.findings > 0), + ).toBeTruthy(); + expect(readFileSync(path.join(result.runDirectory, 'configs/fallow.json'), 'utf-8')).toBe( + readFileSync(path.join(root, 'quality-audit/fallow.json'), 'utf-8'), + ); + }).pipe(Effect.provide(NodeServices.layer)), +); diff --git a/app/scripts/tests/quality-cli-lifecycle.test.mts b/app/scripts/tests/quality-cli-lifecycle.test.mts index d4616d912..3a4d79bac 100644 --- a/app/scripts/tests/quality-cli-lifecycle.test.mts +++ b/app/scripts/tests/quality-cli-lifecycle.test.mts @@ -1,9 +1,7 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { NodeServices } from '@effect/platform-node'; -import { Effect, Layer, Schema, Stream } from 'effect'; +import { Effect, Schema, Stream } from 'effect'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; -import { makeEffectTestCallback } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); @@ -31,18 +29,12 @@ const verifyImports = Effect.gen(function* verifyInertImports() { return `import ${encodeJson(url)};`; }) .join('\n'); - assert.deepEqual(yield* runChild(imports), { code: 0, stderr: '', stdout: '' }); + expect(yield* runChild(imports)).toEqual({ code: 0, stderr: '', stdout: '' }); }); -void test( - 'CLI modules are inert when imported', - makeEffectTestCallback( - Effect.scoped( - Layer.build(Layer.effectDiscard(verifyImports).pipe(Layer.provide(NodeServices.layer))), - ), - ), +it.live('CLI modules are inert when imported', () => + verifyImports.pipe(Effect.provide(NodeServices.layer)), ); - const verifyFinalization = Effect.fn('verifyFinalization')(function* verifyCliFinalization( fails: boolean, ) { @@ -55,28 +47,14 @@ const verifyFinalization = Effect.fn('verifyFinalization')(function* verifyCliFi yield* ${fails ? "Effect.fail(new CliFailure({ message: 'expected failure' }))" : 'Effect.void'}; })); `); - assert.equal(result.code, fails ? 1 : 0); - assert.equal(result.stdout, 'acquired\nreleased\n'); - assert.equal(result.stderr, fails ? 'CliFailure: expected failure\n' : ''); + expect(result.code).toBe(fails ? 1 : 0); + expect(result.stdout).toBe('acquired\nreleased\n'); + expect(result.stderr).toBe(fails ? 'CliFailure: expected failure\n' : ''); }); -void test( - 'CLI success finalizes scope and exits zero', - makeEffectTestCallback( - Effect.scoped( - Layer.build( - Layer.effectDiscard(verifyFinalization(false)).pipe(Layer.provide(NodeServices.layer)), - ), - ), - ), +it.live('CLI success finalizes scope and exits zero', () => + verifyFinalization(false).pipe(Effect.provide(NodeServices.layer)), ); -void test( - 'CLI failure finalizes scope, logs once and exits one', - makeEffectTestCallback( - Effect.scoped( - Layer.build( - Layer.effectDiscard(verifyFinalization(true)).pipe(Layer.provide(NodeServices.layer)), - ), - ), - ), +it.live('CLI failure finalizes scope, logs once and exits one', () => + verifyFinalization(true).pipe(Effect.provide(NodeServices.layer)), ); diff --git a/app/scripts/tests/report-fail-closed-authorization-impact.test.mts b/app/scripts/tests/report-fail-closed-authorization-impact.test.mts index 637909ffe..8df733a91 100644 --- a/app/scripts/tests/report-fail-closed-authorization-impact.test.mts +++ b/app/scripts/tests/report-fail-closed-authorization-impact.test.mts @@ -1,5 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; + import { reduceAuthorizationImpact } from '../report-fail-closed-authorization-impact.mts'; const inventoryHash = 'a'.repeat(64); @@ -31,47 +31,45 @@ const event = (changed: EvidenceFixtureOverride = {}) => ({ ...changed, }); -await test('impact reduction is deterministic and aggregates sanitized evidence', () => { +it('impact reduction is deterministic and aggregates sanitized evidence', () => { const report = reduceAuthorizationImpact([event({ timestamp: observationEndedAt }), event()]); - assert.equal(report.totalWouldDeny, 2); - assert.equal(report.aggregates[0]?.count, 2); - assert.deepEqual(report.observation, { + expect(report.totalWouldDeny).toBe(2); + expect(report.aggregates[0]?.count).toBe(2); + expect(report.observation).toEqual({ endedAt: observationEndedAt, startedAt: observationStartedAt, }); }); -await test('impact reduction rejects mixed build evidence and sensitive extra fields', () => { - assert.throws( - () => reduceAuthorizationImpact([event(), event({ sourceRevision: 'other' })]), +it('impact reduction rejects mixed build evidence and sensitive extra fields', () => { + expect(() => reduceAuthorizationImpact([event(), event({ sourceRevision: 'other' })])).toThrow( /mixes/u, ); - assert.throws(() => reduceAuthorizationImpact([event({ principalId: 'secret' })]), /prohibited/u); - assert.throws(() => reduceAuthorizationImpact([event({ tenantId: 'secret' })]), /prohibited/u); + expect(() => reduceAuthorizationImpact([event({ principalId: 'secret' })])).toThrow( + /prohibited/u, + ); + expect(() => reduceAuthorizationImpact([event({ tenantId: 'secret' })])).toThrow(/prohibited/u); }); -await test('a bounded empty observation produces a zero-impact report', () => { +it('a bounded empty observation produces a zero-impact report', () => { const report = reduceAuthorizationImpact([], { endedAt: '2026-09-10T00:00:00.000Z', inventoryHash, sourceRevision, startedAt: observationStartedAt, }); - assert.equal(report.totalWouldDeny, 0); - assert.deepEqual(report.aggregates, []); + expect(report.totalWouldDeny).toBe(0); + expect(report.aggregates).toEqual([]); }); -await test('impact reduction rejects sensitive values smuggled into allowed evidence fields', () => { - assert.throws( - () => reduceAuthorizationImpact([event({ entrypointKey: 'tenant@example.com' })]), +it('impact reduction rejects sensitive values smuggled into allowed evidence fields', () => { + expect(() => reduceAuthorizationImpact([event({ entrypointKey: 'tenant@example.com' })])).toThrow( prohibitedValuePattern, ); - assert.throws( - () => reduceAuthorizationImpact([event({ denialReason: 'principal-a2000000' })]), + expect(() => reduceAuthorizationImpact([event({ denialReason: 'principal-a2000000' })])).toThrow( prohibitedValuePattern, ); - assert.throws( - () => reduceAuthorizationImpact([event({ policyClass: 'raw-relation-tuple' })]), + expect(() => reduceAuthorizationImpact([event({ policyClass: 'raw-relation-tuple' })])).toThrow( prohibitedValuePattern, ); }); diff --git a/app/scripts/tests/root-environment.test.mts b/app/scripts/tests/root-environment.test.mts index 1a2bebeeb..67f9943d1 100644 --- a/app/scripts/tests/root-environment.test.mts +++ b/app/scripts/tests/root-environment.test.mts @@ -1,16 +1,17 @@ -import assert from 'node:assert/strict'; +import { expect, it } from 'effect-rstest'; +import { Effect } from 'effect'; + import { spawnSync } from 'node:child_process'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import test from 'node:test'; import { fileURLToPath } from 'node:url'; const appRoot = path.resolve(import.meta.dirname, '../..'); const repositoryRoot = path.dirname(appRoot); const expectedEnvironmentPath = path.join(appRoot, '.env'); -void test('apps contain no environment files that can override the app-root .env', () => { +it('apps contain no environment files that can override the app-root .env', () => { const result = spawnSync( '/usr/bin/find', [ @@ -26,25 +27,29 @@ void test('apps contain no environment files that can override the app-root .env { encoding: 'utf-8' }, ); - assert.equal(result.status, 0, result.stderr); - assert.equal(result.stdout.trim(), ''); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout.trim()).toBe(''); }); -void test('workspace discovery resolves repository, app, shell, and microvertical directories', async () => { - const { resolveAppWorkspaceRoot } = - await import('../../packages/core-runtime/src/environment/workspace-environment.ts'); +it.live( + 'workspace discovery resolves repository, app, shell, and microvertical directories', + Effect.fn(function* testEffect1() { + const { resolveAppWorkspaceRoot } = yield* Effect.tryPromise( + () => import('../../packages/core-runtime/src/environment/workspace-environment.ts'), + ); - for (const directory of [ - repositoryRoot, - appRoot, - path.join(appRoot, 'apps/shell-super-app'), - path.join(appRoot, 'verticals/party-registry'), - ]) { - assert.equal(resolveAppWorkspaceRoot(directory), appRoot); - } -}); + for (const directory of [ + repositoryRoot, + appRoot, + path.join(appRoot, 'apps/shell-super-app'), + path.join(appRoot, 'verticals/party-registry'), + ]) { + expect(resolveAppWorkspaceRoot(directory)).toBe(appRoot); + } + }), +); -void test('all server configuration resolves the app-root .env from any invocation directory', () => { +it('all server configuration resolves the app-root .env from any invocation directory', () => { const probe = new URL('server-environment-paths.fixture.mts', import.meta.url); const child = spawnSync( '/usr/bin/env', @@ -61,15 +66,15 @@ void test('all server configuration resolves the app-root .env from any invocati }, ); - assert.equal(child.status, 0, child.stderr); - assert.deepEqual(JSON.parse(child.stdout.trim()), [ + expect(child.status, child.stderr).toBe(0); + expect(JSON.parse(child.stdout.trim())).toEqual([ expectedEnvironmentPath, expectedEnvironmentPath, expectedEnvironmentPath, ]); }); -void test('Drizzle configuration remains bundleable as CommonJS', () => { +it('Drizzle configuration remains bundleable as CommonJS', () => { const outputDirectory = mkdtempSync(path.join(tmpdir(), 'ontos-drizzle-cjs-')); try { const result = spawnSync( @@ -84,7 +89,7 @@ void test('Drizzle configuration remains bundleable as CommonJS', () => { ], { encoding: 'utf-8' }, ); - assert.equal(result.status, 0, result.stderr); + expect(result.status, result.stderr).toBe(0); } finally { rmSync(outputDirectory, { force: true, recursive: true }); } diff --git a/app/scripts/tests/tooling-process-fixture.mts b/app/scripts/tests/tooling-process-fixture.mts new file mode 100644 index 000000000..eeff578d8 --- /dev/null +++ b/app/scripts/tests/tooling-process-fixture.mts @@ -0,0 +1,20 @@ +import { Effect, Stream } from 'effect'; +import { ChildProcessSpawner } from 'effect/unstable/process'; + +import type { ChildProcess } from 'effect/unstable/process'; + +export const collectToolingProcess = Effect.fn(function* collectToolingProcess( + command: ChildProcess.Command, +) { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const handle = yield* spawner.spawn(command); + const [status, stdout, stderr] = yield* Effect.all( + [ + handle.exitCode.pipe(Effect.map(Number)), + handle.stdout.pipe(Stream.decodeText(), Stream.mkString), + handle.stderr.pipe(Stream.decodeText(), Stream.mkString), + ], + { concurrency: 'unbounded' }, + ); + return { status, stderr, stdout }; +}); diff --git a/app/scripts/tests/typecheck-project-references.test.mts b/app/scripts/tests/typecheck-project-references.test.mts index a06cccede..a3c666a95 100644 --- a/app/scripts/tests/typecheck-project-references.test.mts +++ b/app/scripts/tests/typecheck-project-references.test.mts @@ -1,18 +1,15 @@ -import assert from 'node:assert/strict'; +import { expect, it } from 'effect-rstest'; + import { mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import test from 'node:test'; + import { fileURLToPath, pathToFileURL } from 'node:url'; import { NodeServices } from '@effect/platform-node'; -import { Config, Effect, ManagedRuntime, Predicate, Schema, Stream } from 'effect'; -import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; +import { Config, Effect, Predicate, Schema } from 'effect'; +import { ChildProcess } from 'effect/unstable/process'; -interface TypecheckResult { - readonly status: number; - readonly stderr: string; - readonly stdout: string; -} +import { collectToolingProcess } from './tooling-process-fixture.mts'; interface WorkspaceScriptPlan { readonly typecheck: string; @@ -39,72 +36,57 @@ const packageJson = Schema.decodeUnknownSync(Schema.fromJsonString(PackageJsonSc ); const typecheckWrapper = path.join(workspaceRoot, 'scripts/ultramodern-typecheck.mts'); const executablePath = path.join(workspaceRoot, 'node_modules/.bin'); -const typecheckRuntime = ManagedRuntime.make(NodeServices.layer); -const runTypecheck = async ( - fixture: string, - commandArguments: readonly string[], -): Promise => - await typecheckRuntime.runPromise( - Effect.gen(function* runTypecheckEffect() { - const inheritedPath = yield* Config.string('PATH').pipe(Config.withDefault('')); - const processSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; - return yield* Effect.scoped( - Effect.gen(function* collectTypecheckResult() { - const handle = yield* processSpawner.spawn( - ChildProcess.make(process.execPath, [typecheckWrapper, ...commandArguments], { - cwd: fixture, - env: { - PATH: `${executablePath}${path.delimiter}${inheritedPath}`, - ULTRAMODERN_WORKSPACE_ROOT: fixture, - }, - extendEnv: true, - stderr: 'pipe', - stdin: 'ignore', - stdout: 'pipe', - }), - ); - const [status, stdout, stderr] = yield* Effect.all( - [ - handle.exitCode.pipe(Effect.map(Number)), - handle.stdout.pipe(Stream.decodeText(), Stream.mkString), - handle.stderr.pipe(Stream.decodeText(), Stream.mkString), - ], - { concurrency: 'unbounded' }, - ); - return { status, stderr, stdout }; - }), - ); - }), - ); - -test.after(async () => { - await typecheckRuntime.dispose(); -}); +const runTypecheck = (fixture: string, commandArguments: readonly string[]) => + Effect.gen(function* runTypecheckEffect() { + const inheritedPath = yield* Config.string('PATH').pipe(Config.withDefault('')); + return yield* collectToolingProcess( + ChildProcess.make(process.execPath, [typecheckWrapper, ...commandArguments], { + cwd: fixture, + env: { + PATH: `${executablePath}${path.delimiter}${inheritedPath}`, + ULTRAMODERN_WORKSPACE_ROOT: fixture, + }, + extendEnv: true, + stderr: 'pipe', + stdin: 'ignore', + stdout: 'pipe', + }), + ); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)); -void test('installed workspace generator keeps build mode as the root typecheck default', async () => { - const generator = Schema.decodeUnknownSync(WorkspaceScriptPlanModuleSchema)( - await import( - pathToFileURL( - path.join( - workspaceRoot, - 'node_modules/@modern-js/create/dist/esm-node/ultramodern-workspace/workspace-script-plan.js', - ), - ).href - ), - ); - const scriptPlan = Schema.decodeUnknownSync(WorkspaceScriptPlanSchema)( - generator.createWorkspaceRootScriptPlan([]), - ); - assert.equal( - scriptPlan.typecheck, - 'node ./scripts/ultramodern-typecheck.mts --build tsconfig.json', - ); -}); +it.live( + 'installed workspace generator keeps build mode as the root typecheck default', + Effect.fn(function* testEffect2() { + const generator = Schema.decodeUnknownSync(WorkspaceScriptPlanModuleSchema)( + yield* Effect.tryPromise( + () => + import( + pathToFileURL( + path.join( + workspaceRoot, + 'node_modules/@modern-js/create/dist/esm-node/ultramodern-workspace/workspace-script-plan.js', + ), + ).href + ), + ), + ); + const scriptPlan = Schema.decodeUnknownSync(WorkspaceScriptPlanSchema)( + generator.createWorkspaceRootScriptPlan([]), + ); + expect(scriptPlan.typecheck).toBe( + 'node ./scripts/ultramodern-typecheck.mts --build tsconfig.json', + ); + }), +); -void test('Drizzle consumer surface compiles in both ESM and CommonJS projects', async () => { - const fixture = mkdtempSync(path.join(os.tmpdir(), 'ontos-drizzle-declarations-')); - try { +it.live( + 'Drizzle consumer surface compiles in both ESM and CommonJS projects', + Effect.fn(function* testEffect3() { + const fixture = yield* Effect.acquireRelease( + Effect.sync(() => mkdtempSync(path.join(os.tmpdir(), 'ontos-drizzle-declarations-'))), + (directory) => Effect.sync(() => rmSync(directory, { force: true, recursive: true })), + ); symlinkSync( path.join(workspaceRoot, 'node_modules'), path.join(fixture, 'node_modules'), @@ -134,16 +116,18 @@ void test('Drizzle consumer surface compiles in both ESM and CommonJS projects', 'export const fixtureTable = pgTable("declaration_fixture", { id: uuid("id") });\n', ); } - const result = await runTypecheck(fixture, ['--project', tsconfigFile]); - assert.equal(result.status, 0, result.stdout + result.stderr); - } finally { - rmSync(fixture, { force: true, recursive: true }); - } -}); + const result = yield* runTypecheck(fixture, ['--project', tsconfigFile]); + expect(result.status, result.stdout + result.stderr).toBe(0); + }), +); -void test('root typecheck checks referenced projects and rejects a newly introduced type error', async () => { - const fixture = mkdtempSync(path.join(os.tmpdir(), 'ontos-typecheck-references-')); - try { +it.live( + 'root typecheck checks referenced projects and rejects a newly introduced type error', + Effect.fn(function* testEffect4() { + const fixture = yield* Effect.acquireRelease( + Effect.sync(() => mkdtempSync(path.join(os.tmpdir(), 'ontos-typecheck-references-'))), + (directory) => Effect.sync(() => rmSync(directory, { force: true, recursive: true })), + ); mkdirSync(path.join(fixture, 'referenced')); symlinkSync( path.join(workspaceRoot, 'node_modules'), @@ -172,22 +156,20 @@ void test('root typecheck checks referenced projects and rejects a newly introdu const sourceFile = path.join(fixture, 'referenced/index.ts'); writeFileSync(sourceFile, 'export const referenceGateFixture: number = 1;\n'); const [runtime, wrapper, ...args] = packageJson.scripts.typecheck.split(' '); - assert.equal(runtime, 'node'); - assert.equal(wrapper, './scripts/ultramodern-typecheck.mts'); - assert.equal(path.join(workspaceRoot, wrapper), typecheckWrapper); - const initial = await runTypecheck(fixture, args); - assert.equal(initial.status, 0, initial.stdout + initial.stderr); - assert.ok( + expect(runtime).toBe('node'); + expect(wrapper).toBe('./scripts/ultramodern-typecheck.mts'); + expect(path.join(workspaceRoot, wrapper)).toBe(typecheckWrapper); + const initial = yield* runTypecheck(fixture, args); + expect(initial.status, initial.stdout + initial.stderr).toBe(0); + expect( readFileSync(path.join(fixture, 'referenced/output/index.d.ts'), 'utf-8').includes( 'referenceGateFixture', ), 'the referenced project must actually be built; a root files:[] project check is a no-op', - ); + ).toBe(true); writeFileSync(sourceFile, 'export const referenceGateFixture: number = "invalid";\n'); - const invalid = await runTypecheck(fixture, args); - assert.notEqual(invalid.status, 0, 'a referenced source type error must fail the root gate'); - assert.match(invalid.stdout + invalid.stderr, /referenced[/\\]index\.ts.*TS2322/u); - } finally { - rmSync(fixture, { force: true, recursive: true }); - } -}); + const invalid = yield* runTypecheck(fixture, args); + expect(invalid.status, 'a referenced source type error must fail the root gate').not.toBe(0); + expect(invalid.stdout + invalid.stderr).toMatch(/referenced[/\\]index\.ts.*TS2322/u); + }), +); diff --git a/app/scripts/tests/ultramodern-command.test.mts b/app/scripts/tests/ultramodern-command.test.mts index 2669ab9e5..5e041157f 100644 --- a/app/scripts/tests/ultramodern-command.test.mts +++ b/app/scripts/tests/ultramodern-command.test.mts @@ -1,14 +1,10 @@ -/// - -import assert from 'node:assert/strict'; +import { expect, it } from 'effect-rstest'; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import test from 'node:test'; -import type { TestContext } from 'node:test'; import { fileURLToPath } from 'node:url'; import { NodeServices } from '@effect/platform-node'; -import { Effect, ManagedRuntime, Stream } from 'effect'; +import { Effect, Stream } from 'effect'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; const workspaceRoot = fileURLToPath(new URL('../..', import.meta.url)); @@ -23,93 +19,94 @@ const wrappers = [ ['verify-cloudflare-output', 'cloudflare-output-verify'], ] as const; -const fixtureDirectory = (context: TestContext) => { - const directory = mkdtempSync(path.join(os.tmpdir(), 'ontos-command-')); - context.after(() => rmSync(directory, { force: true, recursive: true })); - return directory; -}; - -const wrapperRuntime = ManagedRuntime.make(NodeServices.layer); -test.after(async () => { - await wrapperRuntime.dispose(); -}); +const fixtureDirectory = () => + Effect.acquireRelease( + Effect.sync(() => mkdtempSync(path.join(os.tmpdir(), 'ontos-command-'))), + (directory) => Effect.sync(() => rmSync(directory, { force: true, recursive: true })), + ); -const invokeWrapper = async ( +const invokeWrapper = ( script: string, environment: Readonly>, args: readonly string[] = [], ) => - await wrapperRuntime.runPromise( - Effect.gen(function* invokeWrapperEffect() { - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const child = yield* spawner.spawn( - ChildProcess.make( - process.execPath, - [path.join(workspaceRoot, 'scripts', `${script}.mts`), ...args], - { - cwd: workspaceRoot, - env: environment, - extendEnv: true, - stderr: 'pipe', - stdin: 'ignore', - stdout: 'pipe', - }, - ), - ); - return yield* Effect.all( + Effect.gen(function* invokeWrapperEffect() { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const child = yield* spawner.spawn( + ChildProcess.make( + process.execPath, + [path.join(workspaceRoot, 'scripts', `${script}.mts`), ...args], { - status: child.exitCode.pipe(Effect.map(Number)), - stderr: child.stderr.pipe(Stream.decodeText(), Stream.mkString), - stdout: child.stdout.pipe(Stream.decodeText(), Stream.mkString), + cwd: workspaceRoot, + env: environment, + extendEnv: true, + stderr: 'pipe', + stdin: 'ignore', + stdout: 'pipe', }, - { concurrency: 'unbounded' }, - ); - }).pipe(Effect.scoped), - ); - -for (const [script, command] of wrappers) { - void test(`${script} forwards arguments, workspace and child exit status`, async (context) => { - const fixture = fixtureDirectory(context); - const createBin = path.join(fixture, 'create.mjs'); - writeFileSync( - createBin, - 'console.log(process.argv.slice(2).join("|")); console.log(process.env.ULTRAMODERN_WORKSPACE_ROOT); process.exitCode = 7;', + ), ); - const result = await invokeWrapper( - script, - { ULTRAMODERN_CREATE_BIN: createBin, ULTRAMODERN_WORKSPACE_ROOT: fixture }, - ['--probe', 'argument with spaces'], + return yield* Effect.all( + { + status: child.exitCode.pipe(Effect.map(Number)), + stderr: child.stderr.pipe(Stream.decodeText(), Stream.mkString), + stdout: child.stdout.pipe(Stream.decodeText(), Stream.mkString), + }, + { concurrency: 'unbounded' }, ); - assert.equal(result.status, 7, result.stderr); - assert.equal( - result.stdout, - `ultramodern|${command}|--probe|argument with spaces\n${fixture}\n`, - ); - }); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)); + +for (const [script, command] of wrappers) { + it.live( + `${script} forwards arguments, workspace and child exit status`, + Effect.fn(function* mergedScenario1() { + const fixture = yield* fixtureDirectory(); + const createBin = path.join(fixture, 'create.mjs'); + writeFileSync( + createBin, + 'console.log(process.argv.slice(2).join("|")); console.log(process.env.ULTRAMODERN_WORKSPACE_ROOT); process.exitCode = 7;', + ); + const result = yield* invokeWrapper( + script, + { ULTRAMODERN_CREATE_BIN: createBin, ULTRAMODERN_WORKSPACE_ROOT: fixture }, + ['--probe', 'argument with spaces'], + ); + expect(result.status, result.stderr).toBe(7); + expect(result.stdout).toBe( + `ultramodern|${command}|--probe|argument with spaces\n${fixture}\n`, + ); + }), + ); } -void test('route generation continues compatibility generation after a nonzero framework exit', async (context) => { - const fixture = fixtureDirectory(context); - const createBin = path.join(fixture, 'create.mjs'); - writeFileSync(createBin, 'process.exitCode = 7;'); - mkdirSync(path.join(fixture, '.modernjs')); - writeFileSync(path.join(fixture, '.modernjs/ultramodern.json'), '{"topology":{"apps":[]}}'); - const result = await invokeWrapper('generate-tanstack-routes', { - ULTRAMODERN_CREATE_BIN: createBin, - ULTRAMODERN_WORKSPACE_ROOT: fixture, - }); - assert.equal(result.status, 0, result.stderr); - assert.match(result.stderr, /continuing with the repository compatibility manifest/u); -}); +it.live( + 'route generation continues compatibility generation after a nonzero framework exit', + Effect.fn(function* mergedScenario2() { + const fixture = yield* fixtureDirectory(); + const createBin = path.join(fixture, 'create.mjs'); + writeFileSync(createBin, 'process.exitCode = 7;'); + mkdirSync(path.join(fixture, '.modernjs')); + writeFileSync(path.join(fixture, '.modernjs/ultramodern.json'), '{"topology":{"apps":[]}}'); + const result = yield* invokeWrapper('generate-tanstack-routes', { + ULTRAMODERN_CREATE_BIN: createBin, + ULTRAMODERN_WORKSPACE_ROOT: fixture, + }); + expect(result.status, result.stderr).toBe(0); + expect(result.stderr).toMatch(/continuing with the repository compatibility manifest/u); + }), +); -void test('missing PATH launcher reports a typed launch failure and exits one', async (context) => { - const fixture = fixtureDirectory(context); - const result = await invokeWrapper('assert-mf-types', { - PATH: fixture, - ULTRAMODERN_CREATE_BIN: '', - ULTRAMODERN_WORKSPACE_ROOT: fixture, - }); - assert.equal(result.status, 1); - assert.match(result.stderr, /Failed to launch modern-js-create from PATH/u); - assert.match(result.stderr, /UltraModern command "mf-types"/u); -}); +it.live( + 'missing PATH launcher reports a typed launch failure and exits one', + Effect.fn(function* mergedScenario3() { + const fixture = yield* fixtureDirectory(); + const result = yield* invokeWrapper('assert-mf-types', { + PATH: fixture, + ULTRAMODERN_CREATE_BIN: '', + ULTRAMODERN_WORKSPACE_ROOT: fixture, + }); + expect(result.status).toBe(1); + expect(result.stderr).toMatch(/Failed to launch modern-js-create from PATH/u); + expect(result.stderr).toMatch(/UltraModern command "mf-types"/u); + }), +); diff --git a/app/scripts/validate-ultramodern-workspace.mts b/app/scripts/validate-ultramodern-workspace.mts index a31424b9d..3eacb6d14 100644 --- a/app/scripts/validate-ultramodern-workspace.mts +++ b/app/scripts/validate-ultramodern-workspace.mts @@ -1,3 +1,4 @@ +import type { nodeFileTrace as traceNodeFiles } from '@vercel/nft'; import { hasUltramodernDispatch, hasUltramodernSkillsDispatch, @@ -528,12 +529,10 @@ const workspaceValidationContractDefinition = { 'quality:audit:gate': 'node ./scripts/quality-audit-gate.mts', 'quality:check': 'pnpm quality:audit && pnpm quality:audit:gate', 'test:deployment-impact': - 'node scripts/generate-outbox-worker-deployment.mjs && node --test scripts/tests/plan-deployment-impact.test.mts scripts/tests/outbox-worker-delivery.test.mts', - 'test:generation': - 'node --test scripts/scaffolding/tests/module-contract-generator.test.mts scripts/scaffolding/tests/resource-generator.test.mts scripts/scaffolding/tests/retire-contribution.test.mts scripts/scaffolding/tests/scaffold-generators.test.mts', + 'node scripts/generate-outbox-worker-deployment.mjs && rstest --project scripts scripts/tests/plan-deployment-impact scripts/tests/outbox-worker-delivery', + 'test:generation': 'rstest --project generation', 'test:integration': 'pnpm -r --if-present run test:integration', - 'test:scripts': - 'node --test scripts/tests/boundary-source-structure.test.mts scripts/local-environment-values.test.mts scripts/tests/audit-database-trust-boundaries.test.mts scripts/tests/authorization-rollout-contract.test.mts scripts/tests/check-authorization-readiness.test.mts scripts/tests/database-access-boundaries.test.mts scripts/tests/initialize-local-development.test.mts scripts/tests/locki-feature.test.mts scripts/tests/migrate-contacts-authorization.test.mts scripts/tests/module-entrypoint-boundaries.test.mts scripts/tests/plan-deployment-impact.test.mts scripts/tests/protected-entrypoint-inventory.test.mts scripts/tests/provision-current-action-authorization.test.mts scripts/tests/report-fail-closed-authorization-impact.test.mts scripts/tests/api-only-tooling.test.mts scripts/tests/generated-slot-entries.test.mts scripts/tests/root-environment.test.mts scripts/tests/typecheck-project-references.test.mts scripts/tests/ultramodern-command.test.mts scripts/tests/code-tools-i18n.test.mts scripts/tests/dependency-declarations.test.mts', + 'test:scripts': 'rstest --project scripts', 'test:unit': 'pnpm -r --if-present run test:unit && pnpm -r --if-present run test:component', }, cloudflareSecurity: createCloudflareSecurityContract(), @@ -2138,10 +2137,7 @@ interface TsConfig { const BuildArtifactSchema = Schema.Struct({ deliveryUnit: Schema.optionalKey(DeliveryUnitSchema), }); -type NodeFileTrace = ( - files: readonly string[], - options: { readonly base: string; readonly log: boolean; readonly processCwd: string }, -) => Promise; +type NodeFileTrace = typeof traceNodeFiles; const NodeFileTraceSchema = Schema.declare((input): input is NodeFileTrace => Predicate.isFunction(input), ); @@ -6122,11 +6118,11 @@ for (const [scriptName, expectedCommand] of Object.entries( } const coreRuntimePackage = readJson(PackageJsonSchema, SHARED_VALIDATOR_STRING_093); assert( - coreRuntimePackage.scripts?.['test:unit'] === 'node --test tests/unit/*.test.ts', + coreRuntimePackage.scripts?.['test:unit'] === 'rstest --project unit', 'Core runtime must expose its complete unit test surface', ); assert( - coreRuntimePackage.scripts?.['test:integration'] === 'node --test tests/integration/*.test.ts', + coreRuntimePackage.scripts?.['test:integration'] === 'rstest --project integration', 'Core runtime must expose its complete service-backed integration test surface', ); assert( diff --git a/app/tools/oxlint/effect-native/README.md b/app/tools/oxlint/effect-native/README.md index 6b3e09236..712334632 100644 --- a/app/tools/oxlint/effect-native/README.md +++ b/app/tools/oxlint/effect-native/README.md @@ -1,12 +1,12 @@ # Effect-native Oxlint rules -71 custom diagnostic rules derived from +Custom diagnostic rules derived from [`EFFECT_V4_ANTIPATTERN_AUDIT.md`](../../../docs/architecture/EFFECT_V4_ANTIPATTERN_AUDIT.md). All are explicitly registered and configured at **error** severity. There are **no autofixers or suggestions**. This change introduces enforcement, not an application migration. See the [audit-to-rule catalog and diagnostic snapshot](../../../docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md) -for all 71 rule counts, primary audit mappings, and intentionally non-static guarantees. +for the original rule counts, primary audit mappings, and intentionally non-static guarantees. ## Run from the app workspace @@ -23,7 +23,7 @@ pnpm lint:effect pnpm lint:effect --json # One rule's fixtures while developing it. -RULE=no-nested-effect-run node --test tools/oxlint/effect-native/tests/fixtures.test.mts +RULE=no-nested-effect-run pnpm exec rstest --project lint-rules tools/oxlint/effect-native/tests/fixtures.test.mts # Development probe: uses FIXTURE options, which may differ from production. node tools/oxlint/effect-native/tests/run-on-repo.mts no-nested-effect-run @@ -49,7 +49,8 @@ for its other lint rules. Each rule's source documents its audit mapping, defaul exceptions, and limitations. Configured re-export barrels are explicit trust assumptions, not cross-file resolution. -The audit remains authoritative. Preserve forced outer process/framework Promise adapters, correct +The audit records the original findings; focused architecture documents own current behavior. +Preserve forced outer process/framework Promise adapters, correct Drizzle JSONB and HttpApi encoding, external test-body JSON serialization, deliberate malformed rejection fixtures, legitimate `as const`/`satisfies`, line-preserving `.env` editing, native collection operations, and correctly scoped fibers. Startup `Layer.orDie` requires the deliberate outer seam @@ -124,3 +125,37 @@ Fixture config template: Place examples under `invalid/` and `valid/`; `// expect-count: N` at the start of a positive fixture pins its positive diagnostic count. False-positive repairs need negative regressions. Preserve existing test evidence unless its expectation conflicts with the audit, and explain such corrections. + +## Native interface and operator policy + +`no-promise-shaped-port` covers application packages, scripts, tests, and TSX. Owned interfaces +return Effect, including generic aliases, overloads, and callback parameters. Real SDK and test +runner callbacks keep their required Promise boundary. Private helpers qualify only when lexical +references establish that boundary; an exported Promise helper remains an owned interface. + +`repository-policy.config.ts` checks all repository source for `instanceof` and manual `_tag` +comparisons, switches, and assertions. Negative lint fixtures are excluded because they deliberately +contain forbidden syntax. Use Schema, native predicates, and Effect failure combinators to inspect +values; full serialized-object assertions and diagnostic tag output remain valid. + +## Test runtime policy + +`no-effect-run-in-tests` rejects references, calls, imports, re-exports, and dynamic imports of +`Effect.run*` inside tests, including test support and harness directories. Use `it.effect`, +`it.live`, and `it.layer` from `effect-rstest` so the runner owns services, scopes, +test time, and configuration. The external package owns the runner boundary; there is no +repository-owned implementation or harness-path allowlist. The immutable upstream canary currently +uses a temporary pnpm patch for [effect-rstest PR #4](https://github.com/ScriptedAlchemy/effect-rstest/pull/4). +[OntOS #507](https://github.com/TechsioCZ/ontos/issues/507) tracks replacing it with a published +upstream package and deleting the patch; do not add a local runner alias or wrapper. + +Playwright/e2e adapters remain exempt through `ignorePaths`. Type-only imports, non-Effect +bindings, and ManagedRuntime instance methods are not Effect root-function violations. Nested +Effect re-entry is diagnosed by `no-nested-effect-run`. Additional rule options are `testPaths`, +`effectModules`, and `effectModuleSources`; there is no fixer or suggestion. + +The workspace import policy rejects `node:test`, `node:assert`, `node:assert/strict`, +and `@rstest/core` in application, +package, vertical, script, and tooling tests, with `tests/e2e/**` exempt for Playwright. +Test files disable Sonar's hard-coded runner detector and the async-Promise-function rule because +Effect-native test APIs and `Effect.promise`/`Effect.tryPromise` thunks are intentional. diff --git a/app/tools/oxlint/effect-native/index.ts b/app/tools/oxlint/effect-native/index.ts index 3f9307ff4..24d78d518 100644 --- a/app/tools/oxlint/effect-native/index.ts +++ b/app/tools/oxlint/effect-native/index.ts @@ -1,3 +1,4 @@ +import { rule as noInstanceof } from './rules/no-instanceof.ts'; import { eslintCompatPlugin } from '@oxlint/plugins'; import { rule as noAdHocArgvInScripts } from './rules/no-ad-hoc-argv-in-scripts.ts'; @@ -76,6 +77,7 @@ import { rule as requireTimeoutOnExternalEffect } from './rules/require-timeout- const effectNativePlugin = eslintCompatPlugin({ meta: { name: 'effect-native' }, rules: { + 'no-instanceof': noInstanceof, 'no-ad-hoc-argv-in-scripts': noAdHocArgvInScripts, 'no-ambient-date': noAmbientDate, 'no-ambient-process-env': noAmbientProcessEnv, diff --git a/app/tools/oxlint/effect-native/repository-policy.config.ts b/app/tools/oxlint/effect-native/repository-policy.config.ts new file mode 100644 index 000000000..7809ede16 --- /dev/null +++ b/app/tools/oxlint/effect-native/repository-policy.config.ts @@ -0,0 +1,37 @@ +import { testRestrictedImports } from './shared/test-restricted-imports.ts'; +import { defineConfig } from 'oxlint'; + +/** Repository policies also cover tooling tests and root configuration files. */ +export default defineConfig({ + categories: { correctness: 'off' }, + ignorePatterns: [ + '**/node_modules/**', + '**/dist/**', + '**/build/**', + '**/.output/**', + '**/dist-cloudflare/**', + '**/.modern-js/**', + '**/@mf-types/**', + '**/repos/**', + '**/tools/oxlint/**/tests/fixtures/**', + ], + jsPlugins: [{ name: 'effect-native', specifier: './index.ts' }], + overrides: [ + { + files: ['tools/**/tests/**'], + rules: { + 'effect-native/no-effect-run-in-tests': 'error', + 'eslint/no-restricted-imports': [ + 'error', + { + paths: testRestrictedImports, + }, + ], + }, + }, + ], + rules: { + 'effect-native/no-instanceof': 'error', + 'effect-native/no-manual-tag-comparison': ['error', { adtTags: [], include: ['**'] }], + }, +}); diff --git a/app/tools/oxlint/effect-native/rules/no-effect-run-in-tests.ts b/app/tools/oxlint/effect-native/rules/no-effect-run-in-tests.ts index 426024f7a..49865f2d5 100644 --- a/app/tools/oxlint/effect-native/rules/no-effect-run-in-tests.ts +++ b/app/tools/oxlint/effect-native/rules/no-effect-run-in-tests.ts @@ -38,8 +38,8 @@ import { collectNamedImports } from '../shared/imports.ts'; * * ## What is deliberately allowed * - * - The repository-owned harness itself (`harnessPaths`): `itEffect`/`itLayer` must be free to call - * `Effect.runPromise` exactly once, in one place, on an effect that already has its test Layer. + * - The external `effect-rstest` runner owns the Effect.run* boundary. + * Test support and harness directories have no exemption. * - D-tier Promise adapters forced by the framework: Playwright / e2e specs (`ignorePaths`). * - Type-only imports and type-only specifiers (`import type { runPromise } from "effect/Effect"`, * `import { type runSync } …`): erased before runtime, so they cannot open a fiber. @@ -77,12 +77,6 @@ const ERASED_WRAPPERS = new Set([ /** Guard against pathological alias chains / cycles when resolving `const E = Effect`. */ const MAX_ALIAS_HOPS = 8; -const DEFAULT_HARNESS_PATHS: readonly string[] = [ - '**/tests/support/effect-harness.{ts,mts}', - '**/tests/support/it-effect.{ts,mts}', - '**/tests/harness/**', -]; - /** D-tier: Promise adapters forced by Playwright and other browser drivers. */ const DEFAULT_IGNORE_PATHS: readonly string[] = [ '**/tests/e2e/**', @@ -100,10 +94,10 @@ const DEFAULT_EFFECT_MODULE_SOURCES: readonly string[] = [ 'effect', 'effect/**', '@modern-js/plugin-bff/effect-edge', + '@modern-js/plugin-bff/effect-client', ]; interface RuleOptions { - readonly harnessPaths?: readonly string[]; readonly ignorePaths?: readonly string[]; readonly testPaths?: readonly string[]; readonly effectModules?: readonly string[]; @@ -123,7 +117,6 @@ interface Range { function readOptions(context: Context): Required { const raw = (context.options[0] ?? {}) as RuleOptions; return { - harnessPaths: raw.harnessPaths ?? DEFAULT_HARNESS_PATHS, ignorePaths: raw.ignorePaths ?? DEFAULT_IGNORE_PATHS, testPaths: raw.testPaths ?? [], effectModules: raw.effectModules ?? DEFAULT_EFFECT_MODULES, @@ -208,34 +201,33 @@ export const rule = defineRule({ docs: { description: 'Audit B2 + A1: tests must not call Effect.run* directly. Route every test program through the ' + - 'repository-owned itEffect/itLayer harness (effect/testing, TestClock, scoped Layer, ' + + 'upstream effect-rstest it.effect/it.layer harness (effect/testing, TestClock, scoped Layer, ' + 'ConfigProvider.fromMap) instead of building an ad hoc runtime per assertion.', }, messages: { effectRunInTest: - 'Do not call Effect.{{member}} in a test. Run through the shared itEffect/itLayer harness ' + + 'Do not call Effect.{{member}} in a test. Run through the shared effect-rstest it.effect/it.layer harness ' + '(effect/testing, TestClock, scoped Layer, ConfigProvider.fromMap) so services, time and ' + 'configuration are substitutable.', effectRunReferenceInTest: 'Do not hand Effect.{{member}} around in a test (point-free, mock factory or destructured ' + - 'reference). Expose the effect and let the shared itEffect/itLayer harness run it with ' + + 'reference). Expose the effect and let the shared effect-rstest it.effect/it.layer harness run it with ' + 'effect/testing, TestClock, a scoped Layer and ConfigProvider.fromMap.', effectRunImportInTest: - 'Do not import "{{member}}" from effect/Effect into a test. Import the shared itEffect/itLayer ' + + 'Do not import "{{member}}" from effect/Effect into a test. Import the shared effect-rstest it.effect/it.layer ' + 'harness instead, so services, time and configuration stay substitutable.', effectRunReexportInTest: 'Do not re-export "{{member}}" from effect/Effect out of a test module. A re-export hands every ' + - 'importing test an ad hoc root fiber; export the shared itEffect/itLayer harness ' + + 'importing test an ad hoc root fiber; export the shared effect-rstest it.effect/it.layer harness ' + '(effect/testing, TestClock, scoped Layer, ConfigProvider.fromMap) instead.', effectRunDynamicImportInTest: 'Do not reach Effect.{{member}} through `await import("effect/Effect")` in a test. Import the ' + - 'shared itEffect/itLayer harness so services, time and configuration stay substitutable.', + 'shared effect-rstest it.effect/it.layer harness so services, time and configuration stay substitutable.', }, schema: [ { type: 'object', properties: { - harnessPaths: { type: 'array', items: { type: 'string' } }, ignorePaths: { type: 'array', items: { type: 'string' } }, testPaths: { type: 'array', items: { type: 'string' } }, effectModules: { type: 'array', items: { type: 'string' } }, @@ -246,7 +238,6 @@ export const rule = defineRule({ ], defaultOptions: [ { - harnessPaths: [...DEFAULT_HARNESS_PATHS], ignorePaths: [...DEFAULT_IGNORE_PATHS], testPaths: [], effectModules: [...DEFAULT_EFFECT_MODULES], @@ -257,8 +248,7 @@ export const rule = defineRule({ create(context) { const options = readOptions(context); const filename = context.filename; - if (matchesAny(filename, options.harnessPaths) || matchesAny(filename, options.ignorePaths)) - return {}; + if (matchesAny(filename, options.ignorePaths)) return {}; if (!isTestFile(filename) && !matchesAny(filename, options.testPaths)) return {}; let bindings: EffectBindings = { namespaces: new Map(), importsEffect: false }; diff --git a/app/tools/oxlint/effect-native/rules/no-instanceof.ts b/app/tools/oxlint/effect-native/rules/no-instanceof.ts new file mode 100644 index 000000000..ac1aa84fe --- /dev/null +++ b/app/tools/oxlint/effect-native/rules/no-instanceof.ts @@ -0,0 +1,23 @@ +import { defineRule } from '@oxlint/plugins'; + +/** Constructor identity is never an application failure or value-discrimination contract. */ +export const rule = defineRule({ + meta: { + type: 'problem', + docs: { + description: 'Forbid every instanceof operator, including Error and Exception classes.', + }, + schema: [], + messages: { + forbidden: + 'The instanceof operator is forbidden. Use Effect.catchTag, Match, Schema, or a native guard at a foreign boundary.', + }, + }, + create(context) { + return { + BinaryExpression(node) { + if (node.operator === 'instanceof') context.report({ node, messageId: 'forbidden' }); + }, + }; + }, +}); diff --git a/app/tools/oxlint/effect-native/rules/no-manual-tag-comparison.ts b/app/tools/oxlint/effect-native/rules/no-manual-tag-comparison.ts index 6d801125e..1a6f8294e 100644 --- a/app/tools/oxlint/effect-native/rules/no-manual-tag-comparison.ts +++ b/app/tools/oxlint/effect-native/rules/no-manual-tag-comparison.ts @@ -62,13 +62,9 @@ * - **Effect's built-in ADT tags** (`Some`, `None`, `Success`, `Failure`, `Left`, `Right` — * `adtTags`). `exit._tag === 'Failure'` is the sibling rule `no-raw-effect-adt-tag-check`'s * concern; reporting it here too would double-report the same span. - * - **`switch (error._tag)`** — switch exhaustiveness belongs to `prefer-match-over-tag-switch`; - * this rule never looks at a `SwitchStatement` discriminant or its `case` tests. * - **Type-level `_tag`** — `Extract`, `P['_tag']`, * `Failure extends { readonly _tag: infer Tag }`. Type positions contain no `BinaryExpression` or * `CallExpression`, so they are structurally unreachable from these visitors. - * - **Tag-to-tag equality** (`a._tag === b._tag`, and the same through aliases) — comparing two - * discriminants is an identity test, not a hand-written case analysis over a closed vocabulary. * - **Reading, building and annotating a tag** — `{ _tag: tag }`, `{ failureTag: error._tag }`, * `const { _tag } = error` on its own. Only narrowing reports. * - **The Effect-native forms themselves**: `Effect.catchTag(s)`, `Match.tag`/`Match.tags`/ @@ -90,14 +86,13 @@ * suggestions. Reassigned aliases are not traced; no control-flow value inference is attempted. */ import { defineRule } from '@oxlint/plugins'; - import type { Context, ESTree, Scope, Variable } from '@oxlint/plugins'; +import { unwrapNode, templateText, asNamedMember } from '../shared/ast.ts'; import { collectEffectBindings } from '../shared/effect-imports.ts'; import type { EffectBindings } from '../shared/effect-imports.ts'; -import { isTestFile, scopePath, matchesGlobs } from '../shared/paths.ts'; import { optionRecord, stringArray } from '../shared/options.ts'; -import { unwrapNode, templateText, asNamedMember } from '../shared/ast.ts'; +import { isTestFile, scopePath, matchesGlobs } from '../shared/paths.ts'; const DEFAULT_INCLUDE = ['apps/**', 'verticals/**', 'packages/**', 'scripts/**']; @@ -235,21 +230,279 @@ function isNamedIdentifier(node: ESTree.Node, name: string): boolean { return node.type === 'Identifier' && node.name === name; } -/** Single-assignment `const NAME = ` initialiser for an identifier reference, or null. */ -function constInitialiser(context: Context, node: ESTree.Node): ESTree.Node | null { - const expression = unwrap(node); - if (expression.type !== 'Identifier') return null; - const variable = resolveVariable(context, expression.name, expression); +/** A single-assignment variable declarator, resolved at the identifier's lexical scope. */ +function immutableDeclarator( + context: Context, + node: Extract, +): ESTree.VariableDeclarator | null { + const variable = resolveVariable(context, node.name, node); if (variable === null || variable.defs.length !== 1) return null; if (variable.references.some((reference) => reference.isWrite() && !reference.init)) return null; const def = variable.defs[0]; if (def === undefined || def.type !== 'Variable') return null; const declarator = def.node as ESTree.Node; - if (declarator.type !== 'VariableDeclarator') return null; - if (!isNamedIdentifier(declarator.id, expression.name)) return null; + return declarator.type === 'VariableDeclarator' ? declarator : null; +} + +/** Single-assignment `const NAME = ` initialiser for an identifier reference, or null. */ +function constInitialiser(context: Context, node: ESTree.Node): ESTree.Node | null { + const expression = unwrap(node); + if (expression.type !== 'Identifier') return null; + const declarator = immutableDeclarator(context, expression); + if (declarator === null || !isNamedIdentifier(declarator.id, expression.name)) return null; return declarator.init ?? null; } +function assertionPropertyName( + context: Context, + property: { readonly key: ESTree.Node; readonly computed: boolean }, +): string | null { + return !property.computed && property.key.type === 'Identifier' + ? property.key.name + : staticString(context, property.key); +} + +/** Trace a simple immutable destructured method without losing the source binding's scope. */ +function destructuredMethod( + context: Context, + node: ESTree.Node, +): { source: ESTree.Node; method: string } | null { + if (node.type !== 'Identifier') return null; + const declarator = immutableDeclarator(context, node); + if ( + declarator === null || + declarator.id.type !== 'ObjectPattern' || + declarator.init === null || + declarator.parent.type !== 'VariableDeclaration' || + declarator.parent.kind !== 'const' + ) + return null; + const property = declarator.id.properties.find( + (entry) => entry.type === 'Property' && isNamedIdentifier(entry.value, node.name), + ); + if (property?.type !== 'Property') return null; + const method = assertionPropertyName(context, property); + return method === null ? null : { source: declarator.init, method }; +} + +interface AssertionCall { + readonly method: string; + readonly subject: ESTree.CallExpression | null; + readonly expectedWrapper?: boolean; +} + +/** Node assertions allow strict/default namespace prefixes but never an expect subject. */ +function nodeAssertion( + members: string[], + subject: ESTree.CallExpression | null, +): AssertionCall | null { + if (subject !== null) return null; + while (members[0] === 'strict' || members[0] === 'default') members.shift(); + const method = members[0]; + return members.length === 1 && method !== undefined ? { method, subject: null } : null; +} + +/** Static expect helpers are expected values, never subject-bearing assertions. */ +function staticAssertion(members: string[]): AssertionCall | null { + const index = members[0] === 'expect' && members[1] === 'not' ? 2 : 1; + const method = members[index]; + if (members.length !== index + 1 || method === undefined) return null; + if (members[0] === 'assert') return { method, subject: null }; + return members[0] === 'expect' && EXPECTED_WRAPPERS.has(method) + ? { method, subject: null, expectedWrapper: true } + : null; +} + +/** Match the supported assertion APIs only after proving the import's lexical identity. */ +function importedAssertion( + source: string, + members: string[], + subject: ESTree.CallExpression | null, +): AssertionCall | null { + if (/^(?:node:)?assert(?:\/strict)?$/u.test(source)) return nodeAssertion(members, subject); + if (!['@rstest/core', 'effect-rstest', 'vitest', '@jest/globals', 'expect'].includes(source)) + return null; + if (subject === null) return staticAssertion(members); + if (members.shift() !== 'expect') return null; + const method = members.pop(); + if ( + method === undefined || + !members.every((member) => ['not', 'resolves', 'rejects'].includes(member)) + ) + return null; + return { method, subject }; +} + +function assertionImport( + context: Context, + expression: ESTree.Node, + members: string[], + subject: ESTree.CallExpression | null, +): AssertionCall | null { + if (expression.type !== 'Identifier') return null; + const variable = resolveVariable(context, expression.name, expression); + const definition = variable?.defs.find((entry) => entry.type === 'ImportBinding'); + const specifier = definition?.node as ESTree.Node | undefined; + if (specifier === undefined) return null; + const declaration = context.sourceCode.ast.body.find( + (statement) => + statement.type === 'ImportDeclaration' && + statement.specifiers.some((entry) => entry === specifier), + ); + if (declaration?.type !== 'ImportDeclaration') return null; + const path = [...members]; + if (specifier.type === 'ImportSpecifier') + members.unshift( + specifier.imported.type === 'Identifier' ? specifier.imported.name : specifier.imported.value, + ); + const source = declaration.source.value; + if (specifier.type === 'ImportDefaultSpecifier' && source === 'expect') members.unshift('expect'); + return stableAssertion(context, expression, path, importedAssertion(source, members, subject)); +} + +function assertionAlias( + context: Context, + expression: ESTree.Node, + members: string[], +): ESTree.Node | null { + const destructured = destructuredMethod(context, expression); + if (destructured === null) return constInitialiser(context, expression); + members.unshift(destructured.method); + return destructured.source; +} + +/** Writes through a receiver alias invalidate only the corresponding helper path. */ +function assertionPathWrite(node: ESTree.Node): boolean { + const parent = node.parent; + switch (parent?.type) { + case 'AssignmentExpression': + return parent.left === node; + case 'UpdateExpression': + return parent.argument === node; + case 'UnaryExpression': + return parent.operator === 'delete' && parent.argument === node; + default: + return false; + } +} + +function assertionAliasTarget( + context: Context, + pattern: ESTree.Node, + members: readonly string[], +): { node: ESTree.Node; members: readonly string[] } | null { + if (pattern.type === 'Identifier') + return immutableDeclarator(context, pattern) === null ? null : { node: pattern, members }; + if (pattern.type !== 'ObjectPattern') return null; + const property = pattern.properties.find( + (entry) => entry.type === 'Property' && assertionPropertyName(context, entry) === members[0], + ); + return property?.type === 'Property' + ? assertionAliasTarget(context, property.value, members.slice(1)) + : null; +} + +function assertionMemberTail( + node: ESTree.Node, + members: readonly string[], +): readonly string[] | null { + const parent = node.parent; + if (members.length === 0 || parent?.type !== 'MemberExpression' || parent.object !== node) + return null; + const member = memberPropertyName(parent); + return member === null || member === members[0] ? members.slice(1) : null; +} + +function assertionReferenceWrite( + context: Context, + node: ESTree.Node, + members: readonly string[], + seen: Map>, +): boolean { + let current = node; + for (let depth = 0; depth < MAX_DEPTH; depth += 1) { + if (assertionPathWrite(current)) return true; + const parent = current.parent; + if (parent === null) return false; + if (unwrap(parent) === current) { + current = parent; + continue; + } + if (parent.type === 'VariableDeclarator' && parent.init === current) { + const alias = assertionAliasTarget(context, parent.id, members); + return alias !== null && assertionBindingWrite(context, alias.node, alias.members, seen); + } + const tail = assertionMemberTail(current, members); + if (tail === null) return false; + members = tail; + current = parent; + } + return true; +} + +/** Follow local receiver aliases too, so `alias.objectContaining = fake` is not trusted. */ +function assertionBindingWrite( + context: Context, + node: ESTree.Node, + members: readonly string[], + seen = new Map>(), +): boolean { + if (node.type !== 'Identifier') return false; + const variable = resolveVariable(context, node.name, node); + if (variable === null) return false; + const path = members.join('.'); + const paths = seen.get(variable) ?? new Set(); + if (paths.has(path)) return false; + if (seen.size >= MAX_DEPTH) return true; + seen.set(variable, paths.add(path)); + return variable.references.some( + (reference) => + (reference.isWrite() && !reference.init) || + assertionReferenceWrite(context, reference.identifier, members, seen), + ); +} + +/** Apply mutation checks only to the newly recognized expected-value helpers. */ +function stableAssertion( + context: Context, + expression: ESTree.Node, + members: readonly string[], + assertion: AssertionCall | null, +): AssertionCall | null { + return assertion?.expectedWrapper === true && assertionBindingWrite(context, expression, members) + ? null + : assertion; +} + +/** Resolve assertion imports through lexical bindings, aliases, and matcher modifiers. */ +function assertionCall(context: Context, call: ESTree.CallExpression): AssertionCall | null { + let expression = unwrap(call.callee); + const members: string[] = []; + const seen = new Set(); + let subject: ESTree.CallExpression | null = null; + for (let depth = 0; depth < MAX_DEPTH && !seen.has(expression); depth += 1) { + seen.add(expression); + const alias = assertionAlias(context, expression, members); + if (alias !== null) { + expression = unwrap(alias); + continue; + } + if (expression.type === 'MemberExpression') { + const member = memberPropertyName(expression); + if (member === null) return null; + members.unshift(member); + expression = unwrap(expression.object as ESTree.Node); + continue; + } + if (expression.type !== 'CallExpression') + return assertionImport(context, expression, members, subject); + if (subject !== null) return null; + subject = expression; + expression = unwrap(expression.callee); + } + return null; +} + /** * A statically known string, following one level of `const KEY = '_tag'` indirection and folding * literal `'_' + 'tag'` concatenation — the two spellings that hide a computed `_tag` key. @@ -267,6 +520,28 @@ function staticString(context: Context, node: ESTree.Node): string | null { return initialiser === null ? null : asStringLiteral(initialiser); } +/** Array paths permit static strings and numeric indexes, but never holes or spreads. */ +function validTagPathSegment(context: Context, element: ESTree.Node | null): boolean { + if (element === null || element.type === 'SpreadElement') return false; + if (staticString(context, element) !== null) return true; + const value = unwrap(element); + return value.type === 'Literal' && typeof value.value === 'number'; +} + +/** Resolve only the asserted property: dotted strings and literal array-path segments. */ +function tagPropertyPath(context: Context, node: ESTree.Node): boolean { + const path = staticString(context, node); + if (path !== null) { + const segments = path.split('.'); + return segments.length <= MAX_DEPTH && segments.at(-1) === TAG_PROPERTY; + } + const expression = unwrap(node); + if (expression.type !== 'ArrayExpression' || expression.elements.length > MAX_DEPTH) return false; + if (!expression.elements.every((element) => validTagPathSegment(context, element))) return false; + const last = expression.elements.at(-1); + return last !== undefined && last !== null && staticString(context, last) === TAG_PROPERTY; +} + /** The `_tag` member access itself (`x._tag`, `x?._tag`, `x!._tag`, `x["_tag"]`, `x[KEY]`), or null. */ function asTagMember(context: Context, node: ESTree.Node): ESTree.MemberExpression | null { return asNamedMember(node, TAG_PROPERTY, (key) => staticString(context, key), { @@ -491,7 +766,10 @@ function tagReference( const member = asTagMember(context, expression); if (member !== null) - return { origin: member.object as ESTree.Node, fallback: describe(context, expression) }; + return { + origin: member.object as ESTree.Node, + fallback: describe(context, expression), + }; if (expression.type === 'Identifier') { if (!options.includeIndirectTags) return null; @@ -574,6 +852,152 @@ function containerElements(expression: ESTree.Node): ESTree.ArrayExpression['ele return first.type === 'ArrayExpression' ? first.elements : null; } +/** Only map callback results propagate tag values; arbitrary callback reads do not. */ +function mappedTagValues( + context: Context, + expression: ESTree.CallExpression, +): readonly ESTree.Node[] { + const callee = unwrap(expression.callee); + if (callee.type !== 'MemberExpression' || memberPropertyName(callee) !== 'map') return []; + const first = firstArgument(expression); + if (first === null) return []; + const callback = unwrap(constInitialiser(context, first) ?? first); + if (callback.type !== 'ArrowFunctionExpression' && callback.type !== 'FunctionExpression') + return []; + return callback.body === null ? [] : [callback.body]; +} + +/** Follow values reaching a comparison, without descending into unrelated predicates or fields. */ +function comparedValues(context: Context, expression: ESTree.Node): readonly ESTree.Node[] { + switch (expression.type) { + case 'ArrayExpression': + return expression.elements.filter((element) => element !== null); + case 'SpreadElement': + return [expression.argument]; + case 'LogicalExpression': + return [expression.left, expression.right]; + case 'ConditionalExpression': + return [expression.consequent, expression.alternate]; + case 'SequenceExpression': + return expression.expressions.slice(-1); + case 'CallExpression': + return mappedTagValues(context, expression); + default: + return returnedTagValues(expression); + } +} + +/** Statement bodies expose return values, not conditions or arbitrary expression statements. */ +function returnedTagValues(expression: ESTree.Node): readonly ESTree.Node[] { + switch (expression.type) { + case 'BlockStatement': + return expression.body; + case 'ReturnStatement': + return expression.argument === null ? [] : [expression.argument]; + case 'IfStatement': + return expression.alternate === null + ? [expression.consequent] + : [expression.consequent, expression.alternate]; + default: + return []; + } +} + +const ASSERTION_METHODS = new Set([ + 'equal', + 'strictEqual', + 'notEqual', + 'notStrictEqual', + 'deepEqual', + 'deepStrictEqual', + 'notDeepEqual', + 'notDeepStrictEqual', + 'toBe', + 'toEqual', + 'toStrictEqual', + 'toContain', + 'toContainEqual', + 'toMatch', + 'toMatchObject', + 'match', + 'doesNotMatch', +]); + +const OBJECT_ASSERTION_METHODS = new Set([ + 'toMatchObject', + 'toEqual', + 'toStrictEqual', + 'toContainEqual', + 'deepEqual', + 'deepStrictEqual', + 'notDeepEqual', + 'notDeepStrictEqual', +]); + +const EXPECTED_WRAPPERS = new Set(['objectContaining', 'arrayContaining']); + +type ExpectedShapeVisits = readonly [Set, Set]; + +/** An object's payload is not another discriminant: inspect only its own `_tag`. */ +function expectedObjectTags(context: Context, shape: ESTree.Node): readonly ESTree.Node[] { + if (shape.type !== 'ObjectExpression') return []; + const tag = shape.properties.find( + (property) => + property.type === 'Property' && assertionPropertyName(context, property) === TAG_PROPERTY, + ); + return tag?.type === 'Property' ? [tag.value] : []; +} + +/** Only a proven arrayContaining opens an array's contained expected shapes. */ +function expectedContainedTags( + context: Context, + shape: ESTree.Node, + depth: number, + seen: ExpectedShapeVisits, +): readonly ESTree.Node[] { + if (shape.type !== 'ArrayExpression') return []; + return shape.elements.flatMap((element) => + element === null ? [] : expectedShapeTags(context, element, false, depth + 1, seen), + ); +} + +/** Bounded immutable aliases and framework wrappers; never walk arbitrary payload properties. */ +function expectedShapeTags( + context: Context, + expected: ESTree.Node, + contained = false, + depth = 0, + seen: ExpectedShapeVisits = [new Set(), new Set()], +): readonly ESTree.Node[] { + const shape = unwrap(expected); + const visited = seen[contained ? 1 : 0]; + if (depth > MAX_DEPTH || visited.has(shape)) return []; + visited.add(shape); + const initialiser = constInitialiser(context, shape); + if (initialiser !== null) + return expectedShapeTags(context, initialiser, contained, depth + 1, seen); + if (contained) return expectedContainedTags(context, shape, depth, seen); + if (shape.type !== 'CallExpression') return expectedObjectTags(context, shape); + const wrapper = assertionCall(context, shape); + const argument = firstArgument(shape); + return wrapper?.expectedWrapper === true && argument !== null + ? expectedShapeTags(context, argument, wrapper.method === 'arrayContaining', depth + 1, seen) + : []; +} + +/** Resolve callable aliases without applying object-shape depth limits to the original walk. */ +function assertionCallee(context: Context, node: ESTree.Node): ESTree.Node { + let callee = unwrap(node); + const seen = new Set(); + while (callee.type === 'Identifier' && !seen.has(callee)) { + seen.add(callee); + const initialiser = constInitialiser(context, callee); + if (initialiser === null) break; + callee = unwrap(initialiser); + } + return callee; +} + const REPLACEMENTS = "`Match.value(x).pipe(Match.tag('Tag', onTag), Match.exhaustive)`, `Schema.is(TaggedError)(x)`, or " + '`Effect.catchTag`/`Effect.catchTags` on the error channel'; @@ -593,6 +1017,8 @@ export const rule = defineRule({ '`Schema.is(TaggedError)`, or `Effect.catchTag(s)`.', }, messages: { + tagSwitch: + 'Manual `_tag` switching must use Effect Match.tag/Match.tags or typed error handlers.', tagEquality: "Manual `_tag` comparison on `{{text}}` (`{{operator}} '{{tag}}'`) re-implements pattern matching by " + 'hand and silently stops matching when the tag vocabulary moves (audit A4 / C2). Use ' + @@ -669,6 +1095,22 @@ export const rule = defineRule({ !options.includeErrorCombinators && insideErrorCombinator(context, node, bindings); const tagOf = (node: ESTree.Node): TagReference | null => tagReference(context, node, options); + const comparedTag = (node: ESTree.Node, seen = new Set()): TagReference | null => { + if (seen.has(node)) return null; + seen.add(node); + const direct = tagOf(node); + if (direct !== null) return direct; + const expression = unwrap(node); + const initialiser = constInitialiser(context, expression); + if (initialiser !== null) return comparedTag(initialiser, seen); + const values = comparedValues(context, expression); + for (const value of values) { + const found = comparedTag(value, seen); + if (found !== null) return found; + } + return null; + }; + /** `[…]`/`new Set([…])` of nothing but Effect's own ADT tags — the sibling rule's territory. */ const containerIsAdtOnly = (node: ESTree.Node): boolean => { const expression = unwrap(node); @@ -714,7 +1156,7 @@ export const rule = defineRule({ const left = tagOf(first); const reference = left ?? tagOf(second); const other = left === null ? first : second; - if (reference === null || tagOf(other) !== null) return null; + if (reference === null) return null; const tag = asStringLiteral(other); if (tag !== null && exempt.has(tag)) return null; return { reference, other, tag }; @@ -736,7 +1178,11 @@ export const rule = defineRule({ context.report({ node, messageId: 'tagEqualityDynamic', - data: { text, operator: node.operator, other: describe(context, other) }, + data: { + text, + operator: node.operator, + other: describe(context, other), + }, }); return; } @@ -803,7 +1249,10 @@ export const rule = defineRule({ context.report({ node, messageId: 'tagStringProbe', - data: { text: referenceText(context, reference), method: `.${method}(…)` }, + data: { + text: referenceText(context, reference), + method: `.${method}(…)`, + }, }); return true; } @@ -861,18 +1310,125 @@ export const rule = defineRule({ }); } } - function checkCall(node: ESTree.CallExpression) { - if (checkShape(node) || checkEqualityCall(node)) return; - const callee = unwrap(node.callee); - if (callee.type !== 'MemberExpression') return; - const method = memberPropertyName(callee); - if (method === null) return; - const receiver = callee.object as ESTree.Node; + function reportAssertion(node: ESTree.CallExpression, text: string) { + context.report({ + node, + messageId: 'tagEqualityCall', + data: { callee: describe(context, node.callee), text }, + }); + } + function exemptStaticTag(node: ESTree.Node | undefined): boolean { + if (node === undefined) return false; + const literal = staticString(context, node); + return literal !== null && exempt.has(literal); + } + + function checkPropertyAssertion(node: ESTree.CallExpression, subject: ESTree.CallExpression) { + const assertedPath = firstArgument(node); + const target = firstArgument(subject); + if ( + assertedPath === null || + target === null || + !tagPropertyPath(context, assertedPath) || + suppressed(node) + ) + return; + const expected = node.arguments[1]; + if (expected?.type === 'SpreadElement') return; + if (exemptStaticTag(expected)) return; + context.report({ + node, + messageId: expected === undefined ? 'tagPresenceCheck' : 'tagEqualityCall', + data: { callee: describe(context, node.callee), text: describe(context, target) }, + }); + } + + /** Object matchers inspect only the expected top-level discriminant, never payload fields. */ + function checkObjectAssertion(node: ESTree.CallExpression, assertion: AssertionCall): boolean { + if (!OBJECT_ASSERTION_METHODS.has(assertion.method)) return false; + const expected = node.arguments[assertion.subject === null ? 1 : 0]; + if (expected === undefined || expected.type === 'SpreadElement') return false; + const tags = expectedShapeTags(context, expected); + if (!tags.some((tag) => !exemptStaticTag(tag))) return false; + if (suppressed(node)) return false; + reportAssertion( + node, + describe(context, assertion.subject?.arguments[0] ?? node.arguments[0] ?? node), + ); + return true; + } + + function exemptAssertionValue(other: ESTree.Node | undefined): boolean { + if (other === undefined || other.type === 'SpreadElement') return false; + const literal = asStringLiteral(other); + return (literal !== null && exempt.has(literal)) || containerIsAdtOnly(other); + } + + function checkAssertionValues(node: ESTree.CallExpression, assertion: AssertionCall): boolean { + const compared = + assertion.subject === null + ? node.arguments.slice(0, 2) + : [...assertion.subject.arguments.slice(0, 1), ...node.arguments.slice(0, 1)]; + for (const [index, argument] of compared.entries()) { + if (argument.type === 'SpreadElement') continue; + const reference = comparedTag(argument); + if (reference === null || exemptAssertionValue(compared[index === 0 ? 1 : 0])) continue; + if (suppressed(node)) return true; + reportAssertion(node, referenceText(context, reference)); + return true; + } + return false; + } + + function checkAssertion(node: ESTree.CallExpression, assertion: AssertionCall | null): boolean { + if (assertion === null) return false; + // Property assertions are shape/equality probes only on a proven `expect(subject)` chain. + if (assertion.method === 'toHaveProperty' && assertion.subject !== null) { + checkPropertyAssertion(node, assertion.subject); + return true; + } + if (!ASSERTION_METHODS.has(assertion.method)) return false; + return checkObjectAssertion(node, assertion) || checkAssertionValues(node, assertion); + } + function checkReceiverProbes( + node: ESTree.CallExpression, + receiver: ESTree.Node, + method: string, + ) { if (checkStringProbe(node, receiver, method)) return; if (checkRegexProbe(node, receiver, method)) return; checkMembership(node, receiver, method); } - return { BinaryExpression: checkBinary, CallExpression: checkCall }; + + function checkCall(node: ESTree.CallExpression) { + if (checkShape(node) || checkEqualityCall(node)) return; + const callee = assertionCallee(context, node.callee); + const assertion = assertionCall(context, node); + const method = + callee.type === 'MemberExpression' ? memberPropertyName(callee) : assertion?.method; + const receiver = callee.type === 'MemberExpression' ? callee.object : null; + if (method === null || method === undefined) return; + if (checkAssertion(node, assertion) || receiver === null) return; + + checkReceiverProbes(node, receiver, method); + } + return { + SwitchStatement(node) { + if (tagOf(node.discriminant) === null || suppressed(node)) return; + const labels = node.cases.flatMap((branch) => (branch.test === null ? [] : [branch.test])); + if ( + labels.length > 0 && + labels.every((label) => { + const literal = asStringLiteral(label); + return literal !== null && exempt.has(literal); + }) + ) + return; + context.report({ node, messageId: 'tagSwitch' }); + }, + BinaryExpression: checkBinary, + CallExpression: checkCall, + }; }, }); diff --git a/app/tools/oxlint/effect-native/rules/no-promise-shaped-port.ts b/app/tools/oxlint/effect-native/rules/no-promise-shaped-port.ts index 91b403b31..46f41f403 100644 --- a/app/tools/oxlint/effect-native/rules/no-promise-shaped-port.ts +++ b/app/tools/oxlint/effect-native/rules/no-promise-shaped-port.ts @@ -1,4 +1,3 @@ -import { optionRecord } from '../shared/options.ts'; /** * effect-native/no-promise-shaped-port * @@ -9,29 +8,36 @@ import { optionRecord } from '../shared/options.ts'; * route entrypoints, the Modern.js adapter, and private helpers used only by such boundaries. * Known SDK/fetch/import provenance can identify a local structural mirror. Nested function- * returned records (e.g. Drizzle fluent continuations) are not classified as first-party ports. - * Limitations: no cross-file SDK/type inference; inferred non-async Promise returns, dynamic - * member names and arbitrary higher-order value flow are not resolved. Name/path options are + * Owned test callbacks also recognize known Promise constructors, factories, and local returns. + * Limitations: no cross-file SDK/type inference; other inferred returns, dynamic member names + * and arbitrary higher-order value flow are not resolved. Name/path options are * explicit policy controls, not proof of ownership. No fixer or suggestions. */ import { defineRule } from '@oxlint/plugins'; - import type { Context, ESTree } from '@oxlint/plugins'; -import { globToRegExp, isTestFile, scopePath, matchesGlobs } from '../shared/paths.ts'; +import { parentOf } from '../shared/ast.ts'; +import { optionRecord } from '../shared/options.ts'; import { stringArray, booleanOption as boolean } from '../shared/options.ts'; -import { parentOf, typeNameSegments } from '../shared/ast.ts'; +import { isTestFile, scopePath, matchesGlobs } from '../shared/paths.ts'; -const DEFAULT_INCLUDE = ['apps/**', 'verticals/**', 'packages/**']; +import { createPromisePortTypeResolver } from '../shared/no-promise-port-types.ts'; + +const DEFAULT_INCLUDE = [ + 'apps/**', + 'verticals/**', + 'packages/**', + 'scripts/**', + 'tools/**/tests/**', +]; const DEFAULT_IGNORE = [ '**/dist/**', '**/build/**', '**/node_modules/**', - 'tools/**', '**/*.d.ts', '**/*.config.ts', '**/*.config.mts', '**/module-federation.config.ts', - '**/scripts/**', ]; /** pg `Pool` driver edge: the single place a `Promise` contract is the real contract. */ const DEFAULT_ALLOW_PATHS = ['**/db/client.ts', '**/auth/db/client.ts']; @@ -60,8 +66,8 @@ function readOptions(context: Context) { return { include: stringArray(record.include, DEFAULT_INCLUDE), ignore: stringArray(record.ignore, DEFAULT_IGNORE), - includeTests: boolean(record.includeTests, false), - includeTsx: boolean(record.includeTsx, false), + includeTests: boolean(record.includeTests, true), + includeTsx: boolean(record.includeTsx, true), allowPaths: stringArray(record.allowPaths, DEFAULT_ALLOW_PATHS), driverCallbacks: stringArray(record.driverCallbacks, DEFAULT_DRIVER_CALLBACKS), allowNames: stringArray(record.allowNames, DEFAULT_ALLOW_NAMES), @@ -115,6 +121,8 @@ export const rule = defineRule({ "Audit A5: ownership-shaped implementation '{{member}}' is async outside a recognized adapter. First-party services should return Effect.Effect, with Promise conversion at the driver/framework edge.", promiseReturningImplementation: "Audit A5: implementation '{{member}}' explicitly returns '{{wrapper}}' outside a recognized adapter. Expose Effect.Effect from first-party services.", + testPromiseRoundTrip: + 'Audit A1: owned tests must not wrap Effect Promise runners or Rstest resolves/rejects assertions in an Effect Promise adapter. Compose the Effect directly and assert its value or Exit; reserve Promise adapters for foreign APIs.', }, schema: [ { @@ -138,8 +146,8 @@ export const rule = defineRule({ { include: DEFAULT_INCLUDE, ignore: [...DEFAULT_IGNORE], - includeTests: false, - includeTsx: false, + includeTests: true, + includeTsx: true, allowPaths: DEFAULT_ALLOW_PATHS, driverCallbacks: DEFAULT_DRIVER_CALLBACKS, allowNames: DEFAULT_ALLOW_NAMES, @@ -205,11 +213,37 @@ export const rule = defineRule({ def.type === 'Variable' && def.parent?.kind === 'const' && !variable.references.some((r: any) => r.isWrite() && !r.init); + const staticMemberKey = (node: any): string | null => { + const key = node.computed ? computedKey(node.property) : node.property.name; + return typeof key === 'string' ? key : null; + }; + /** Find a registration argument through transparent TypeScript expression wrappers. */ + const callbackRegistration = (node: any): any => { + let callback = node; + while (callback.parent && wrappers.has(callback.parent.type)) callback = callback.parent; + const call = callback.parent; + return call?.type === 'CallExpression' && call.arguments.includes(callback) ? call : null; + }; + /** Wrapped suite/layer callbacks receive the runner as their first parameter. */ + const importedRunnerParameter = (def: any, seen: Set): string | null => { + if (def.type !== 'Parameter' || def.node.params?.[0] !== def.name) return null; + const registration = callbackRegistration(def.node); + if (!registration) return null; + let factory = unwrap(registration.callee); + while (factory?.type === 'CallExpression') factory = unwrap(factory.callee); + return /^effect-rstest:(?:\*\.)?(?:describeWrapped|(?:(?:it|test)\.)?layer)$/u.test( + imported(factory, seen) ?? '', + ) + ? 'effect-rstest:it' + : null; + }; const importedIdentifier = (node: any, seen: Set): string | null => { if (node.type !== 'Identifier') return null; const variable = variableFor(node, node.name); for (const def of variable?.defs ?? []) { if (def.type === 'ImportBinding') return importBindingPath(def); + const runner = importedRunnerParameter(def, seen); + if (runner) return runner; if (immutableDefinition(def, variable)) return imported(def.node.init, seen); } return !variable?.defs.length && node.name === 'globalThis' ? 'globalThis' : null; @@ -220,9 +254,14 @@ export const rule = defineRule({ seen.add(node); if (node.type === 'MemberExpression') { const left = imported(node.object, seen); - const key = node.computed ? computedKey(node.property) : node.property.name; - return left && typeof key === 'string' ? `${left}.${key}` : null; + const key = staticMemberKey(node); + return left && key !== null ? `${left}.${key}` : null; } + // Playwright's extend factory preserves test identity, unlike arbitrary factories. + if (node.type === 'CallExpression') + return imported(node.callee, seen) === '@playwright/test:test.extend' + ? '@playwright/test:test' + : null; return importedIdentifier(node, seen); }; const walk = (node: any, visit: (node: any) => void): void => { @@ -311,6 +350,29 @@ export const rule = defineRule({ ); }; + const isEffectPromiseRunner = (node: any): boolean => + /^effect:(?:root\.)?Effect\.runPromise(?:Exit)?$/u.test(imported(node) ?? ''); + const isPromiseAssertion = (node: any): boolean => { + if (node.type !== 'MemberExpression') return false; + if (!['resolves', 'rejects'].includes(staticMemberKey(node) ?? '')) return false; + const assertion = unwrap(node.object); + return ( + assertion?.type === 'CallExpression' && + /^(?:effect-rstest|@rstest\/core):(?:\*\.)?expect$/u.test(imported(assertion.callee) ?? '') + ); + }; + + /** Syntax-only test round trips: real imports, not arbitrary methods or cross-file runners. */ + const checkTestPromiseAdapter = (call: ESTree.CallExpression): void => { + if (!isTestFile(path) || !isPromiseBoundaryCall(call)) return; + let roundTrip = false; + walk(call.arguments, (node) => { + if (node.type === 'CallExpression' && isEffectPromiseRunner(node.callee)) roundTrip = true; + if (isPromiseAssertion(node)) roundTrip = true; + }); + if (roundTrip) context.report({ node: call, messageId: 'testPromiseRoundTrip' }); + }; + /** A `.transaction(...)` / `.then(...)` style driver callback the Promise protocol forces. */ const isDriverCallbackCall = (call: ESTree.CallExpression): boolean => { const segments = memberSegments(call.callee as ESTree.Node); @@ -342,6 +404,77 @@ export const rule = defineRule({ return false; }; + /** Unit-test callbacks are owned programs, unlike SDK mocks and Playwright adapters. */ + const atOwnedTestCallback = (node: any): boolean => { + if (!isTestFile(path)) return false; + const call = callbackRegistration(node); + if (!call) return false; + let callee = call.callee; + // Parameterized registrations, e.g. test.each(rows)(name, callback). + while (callee.type === 'CallExpression') callee = callee.callee; + return /^(?:effect-rstest|@rstest\/core|vitest|node:test):(?:\*\.)?(?:test|it)(?:\.|$)/u.test( + imported(callee) ?? '', + ); + }; + + /** A callback supplied directly to the imported test runner is a framework entrypoint. */ + const atTestBoundary = (node: any): boolean => { + if (!isTestFile(path)) return false; + for (let current = node; current?.parent; current = current.parent) { + if (current !== node && FUNCTION_TYPES.has(current.type)) return false; + const call = current.parent; + if ( + call.type === 'CallExpression' && + call.arguments.includes(current) && + /^(?:node:test:(?:test|it|before|after|beforeEach|afterEach|\*)(?:\.|$)|(?:@playwright\/test|@rstest\/core|vitest):(?:test|it|beforeAll|afterAll|beforeEach|afterEach|rstest\.mock)(?:\.|$)|effect-rstest:(?:\*\.)?(?:beforeAll|afterAll|beforeEach|afterEach)$)/u.test( + imported(call.callee) ?? '', + ) + ) + return true; + } + return false; + }; + + const invokedAtDriverEdge = (value: any): boolean => + value.parent?.type === 'CallExpression' && + value.parent.callee === value && + atDriverEdge(value.parent); + const foreignThunkValue = (value: any): any => { + if ( + value.parent?.type === 'Property' && + value.parent.value === value && + value.parent.key.name === 'try' + ) + return value.parent.parent; + return value; + }; + const isForeignThunkReference = (ref: any): boolean => { + if (invokedAtDriverEdge(ref.identifier)) return true; + const value = foreignThunkValue(ref.identifier); + const call = value.parent; + return ( + call?.type === 'CallExpression' && + call.arguments.includes(value) && + isPromiseBoundaryCall(call) + ); + }; + const annotatedFunctionParameter = (node: any): any => { + let current = node; + while (current?.parent && current.parent.type !== 'Identifier') current = current.parent; + const parameter = current?.parent; + return parameter && FUNCTION_TYPES.has(parameter.parent?.type) ? parameter : null; + }; + + /** Only the actual thunk forwarded to Effect's foreign Promise conversion has that contract. */ + const isForeignThunkParameter = (node: any): boolean => { + const parameter = annotatedFunctionParameter(node); + if (!parameter) return false; + if (isTestFile(path) && exemptHelper(parameter.parent)) return true; + const variable = variableFor(parameter, parameter.name); + const refs = variable?.references.filter((ref: any) => ref.isRead()) ?? []; + return refs.length > 0 && refs.every(isForeignThunkReference); + }; + /** Better Auth owns this exact hook signature, not arbitrary services nested in its options. */ const isAuthHookPath = (keys: readonly string[]): boolean => keys.length === 4 && @@ -392,49 +525,78 @@ export const rule = defineRule({ }; /** The `Promise` / `PromiseLike` reference of a return/value annotation, if any. */ - const promiseReference = ( - annotation: ESTree.TSTypeAnnotation | null | undefined, - ): string | null => { - if (annotation === null || annotation === undefined) return null; - const isGlobalPromiseReference = ( - raw: any, - names: readonly string[], - name: string, - ): boolean => - names.length === 2 && - names[0] === 'globalThis' && - !variableFor(raw, 'globalThis')?.defs.length && - options.promiseTypes.includes(name); - const resolveReference = (raw: any, seen: Set): string | null => { - const names = typeNameSegments(raw.typeName); - if (!names) return null; - const name = names.at(-1)!; - if (isGlobalPromiseReference(raw, names, name)) return `${name}<…>`; - if (names.length !== 1) return null; - const variable = variableFor(raw.typeName, name); - const alias = variable?.defs.find( - (d: any) => d.node.type === 'TSTypeAliasDeclaration', - )?.node; - if (alias) return resolve(alias.typeAnnotation, seen); - if (variable?.defs.length || !options.promiseTypes.includes(name)) return null; - return `${name}<…>`; - }; - const resolve = (raw: any, seen = new Set()): string | null => { - if (!raw || seen.has(raw)) return null; - seen.add(raw); - if (raw.type === 'TSTypeAnnotation' || raw.type === 'TSParenthesizedType') - return resolve(raw.typeAnnotation, seen); - if (raw.type === 'TSUnionType' || raw.type === 'TSIntersectionType') { - for (const item of raw.types) { - const result = resolve(item, new Set(seen)); - if (result) return result; - } - return null; - } - if (raw.type !== 'TSTypeReference') return null; - return resolveReference(raw, seen); + const promiseReference = createPromisePortTypeResolver(variableFor, options.promiseTypes); + + const knownPromiseBinding = (node: any, seen: Set): boolean => { + const variable = variableFor(node, node.name); + return (variable?.defs ?? []).some( + (def: any) => + immutableDefinition(def, variable) && + !FUNCTION_TYPES.has(unwrap(def.node.init)?.type) && + returnsKnownPromise(def.node.init, seen), + ); + }; + /** Walk returns in this body, never returns belonging to a nested function. */ + const bodyReturnsPromise = (node: any, seen: Set): boolean => { + const visit = (statement: any): boolean => { + if (!statement || typeof statement !== 'object') return false; + if (Array.isArray(statement)) return statement.some(visit); + if (FUNCTION_TYPES.has(statement.type)) return false; + if (statement.type === 'ReturnStatement') + return returnsKnownPromise(statement.argument, new Set(seen)); + return Object.entries(statement).some(([key, value]) => key !== 'parent' && visit(value)); }; - return resolve(annotation); + return visit(node); + }; + const isGlobalPromiseConstruction = (node: any): boolean => { + const segments = memberSegments(unwrap(node.callee)); + if (!segments || variableFor(node.callee, segments[0]!)?.defs.length) return false; + const globalSegments = segments[0] === 'globalThis' ? segments.slice(1) : segments; + if (globalSegments[0] !== 'Promise') return false; + if (node.type === 'NewExpression') return globalSegments.length === 1; + return ( + globalSegments.length === 2 && + ['resolve', 'reject', 'all', 'allSettled', 'any', 'race'].includes(globalSegments[1]!) + ); + }; + const localCallReturnsPromise = (callee: any, seen: Set): boolean => { + if (callee.type !== 'Identifier') return false; + const variable = variableFor(callee, callee.name); + return (variable?.defs ?? []).some((def: any) => { + const fn = unwrap(def.type === 'FunctionName' ? def.node : def.node.init); + return ( + FUNCTION_TYPES.has(fn?.type) && + !variable.references.some((ref: any) => ref.isWrite() && !ref.init) && + returnsKnownPromise(fn, seen) + ); + }); + }; + const callReturnsPromise = (node: any, seen: Set): boolean => { + if (isGlobalPromiseConstruction(node)) return true; + if (node.type !== 'CallExpression') return false; + return isEffectPromiseRunner(node.callee) || localCallReturnsPromise(node.callee, seen); + }; + const functionReturnsPromise = (node: any, seen: Set): boolean => + node.async === true || + promiseReference(node.returnType) !== null || + returnsKnownPromise(node.body, seen); + + /** Bounded same-file Promise provenance for owned test registrations, not general type inference. */ + const returnsKnownPromise = (raw: any, seen = new Set()): boolean => { + const node = unwrap(raw); + if (!node || seen.has(node)) return false; + seen.add(node); + if (FUNCTION_TYPES.has(node.type)) return functionReturnsPromise(node, seen); + if (node.type === 'Identifier') return knownPromiseBinding(node, seen); + if (node.type === 'BlockStatement') return bodyReturnsPromise(node, seen); + if (node.type === 'ConditionalExpression') + return ( + returnsKnownPromise(node.consequent, new Set(seen)) || + returnsKnownPromise(node.alternate, new Set(seen)) + ); + if (node.type === 'CallExpression' || node.type === 'NewExpression') + return callReturnsPromise(node, seen); + return false; }; /** Human-readable name for the reported member, used in the message. */ @@ -492,40 +654,71 @@ export const rule = defineRule({ if (withinMirror(node)) return; // A5 owns the service surface, not signatures of fluent driver continuations. // Nested function-returned records are intentionally outside this AST-only port model. + let child: any = node; for (let ancestor = (node as any).parent; ancestor; ancestor = ancestor.parent) { if (['TSTypeAliasDeclaration', 'TSInterfaceDeclaration'].includes(ancestor.type)) break; - if (ancestor.type === 'TSFunctionType') return; + if (ancestor.type === 'TSFunctionType' && ancestor.returnType === child) return; + child = ancestor; } context.report({ node, messageId, data } as never); }; // ---------------------------------------------------------------- (A) declared ports + const isPortBindingOwner = (owner: AnyNode): boolean => { + let declaration = parentOf(owner); + if ( + declaration && + ['AssignmentPattern', 'TSParameterProperty', 'RestElement'].includes(declaration.type) + ) + declaration = parentOf(declaration); + if (!declaration) return false; + return ( + declaration.type === 'VariableDeclarator' || + FUNCTION_TYPES.has(declaration.type) || + [ + 'TSDeclareFunction', + 'TSEmptyBodyFunctionExpression', + 'TSFunctionType', + 'TSMethodSignature', + 'TSCallSignatureDeclaration', + ].includes(declaration.type) + ); + }; + const isPortAnnotationOwner = (owner: AnyNode | null): boolean => { + if (!owner) return false; + if (owner.type === 'Identifier' || owner.type === 'RestElement') + return isPortBindingOwner(owner); + return [ + 'TSPropertySignature', + 'TSIndexSignature', + 'PropertyDefinition', + 'TSAbstractPropertyDefinition', + ].includes(owner.type); + }; + /** `TSFunctionType` positions that declare a port member rather than a callback parameter. */ const isPortFunctionTypePosition = (node: AnyNode): boolean => { - let current = node; - let parent = parentOf(current); + let parent = parentOf(node); while ( parent && - ['TSUnionType', 'TSIntersectionType', 'TSParenthesizedType'].includes(parent.type) - ) { - current = parent; - parent = parentOf(current); - } + [ + 'TSUnionType', + 'TSIntersectionType', + 'TSParenthesizedType', + 'TSTypeParameterInstantiation', + 'TSTypeReference', + 'TSArrayType', + 'TSOptionalType', + 'TSRestType', + 'TSTypeOperator', + ].includes(parent.type) + ) + parent = parentOf(parent); if (parent === null) return false; if (parent.type === 'TSTypeAliasDeclaration') return true; if (parent.type !== 'TSTypeAnnotation') return false; - const owner = parentOf(parent); - if (owner === null) return false; - // `const deleteRecovery: (id: string) => Promise = ...` — a declared binding, not a - // callback parameter (whose annotated `Identifier` has a function as its parent). - if (owner.type === 'Identifier') return parentOf(owner)?.type === 'VariableDeclarator'; - return [ - 'TSPropertySignature', - 'TSIndexSignature', - 'PropertyDefinition', - 'TSAbstractPropertyDefinition', - ].includes(owner.type); + return isPortAnnotationOwner(parentOf(parent)); }; // ---------------------------------------------------------------- (B) implementations @@ -593,27 +786,45 @@ export const rule = defineRule({ const exportedOwner = (owner: any): boolean => owner.parent?.type === 'ExportNamedDeclaration' || owner.parent?.parent?.type === 'ExportNamedDeclaration'; - const exemptReference = (ref: any, seen: Set): boolean => { - const call = ref.identifier.parent; - if (call?.type !== 'CallExpression' || call.callee !== ref.identifier) return false; - if (atDriverEdge(call)) return true; - for (let current = call.parent; current; current = current.parent) + const exemptReference = (ref: any, fn: any, seen: Set): boolean => { + if (atDriverEdge(ref.identifier) || atTestBoundary(ref.identifier)) return true; + for (let current = ref.identifier.parent; current; current = current.parent) { + if (current === fn) return true; + if (FUNCTION_TYPES.has(current.type)) return exemptHelper(current, new Set(seen)); + } + return false; + }; + const exemptAnonymousCallback = (fn: any, seen: Set): boolean => { + const call = fn.parent; + if (call?.type !== 'CallExpression' || !call.arguments.includes(fn)) return false; + for (let current = call.parent; current; current = current.parent) { if (FUNCTION_TYPES.has(current.type)) return exemptHelper(current, new Set(seen)); + } return false; }; + const outsideFunction = (ref: any, fn: any): boolean => { + for (let current = ref.identifier.parent; current; current = current.parent) { + if (current === fn) return false; + } + return true; + }; + const exemptNamedHelper = (owner: any, fn: any, seen: Set): boolean => { + if (exportedOwner(owner)) return false; + const variable = variableFor(owner.id, owner.id.name); + const refs = (variable?.references.filter((ref: any) => ref.isRead()) ?? []).filter( + (ref: any) => outsideFunction(ref, fn), + ); + return refs.length > 0 && refs.every((ref: any) => exemptReference(ref, fn, seen)); + }; const exemptHelper = (fn: any, seen = new Set()): boolean => { if (seen.has(fn)) return false; seen.add(fn); - const body = functionBody(fn); - if (isDirectAdapter(body)) return true; + if (atTestBoundary(fn)) return true; + if (isDirectAdapter(functionBody(fn))) return true; const owner = namedOwner(fn); const id = (owner as any).id; - if (!id || id.type !== 'Identifier') return false; - if (exportedOwner(owner)) return false; - const variable = variableFor(id, id.name); - const refs = variable?.references.filter((r: any) => r.isRead()) ?? []; - if (!refs.length) return false; - return refs.every((ref: any) => exemptReference(ref, seen)); + if (!id || id.type !== 'Identifier') return exemptAnonymousCallback(fn, seen); + return exemptNamedHelper(owner, fn, seen); }; const ownsImplementation = (node: AnyNode): boolean => @@ -625,7 +836,32 @@ export const rule = defineRule({ /(?:^|\/)src\/routes\//u.test(path) && ['VariableDeclarator', 'FunctionDeclaration'].includes(namedOwner(node).type); const atImplementationBoundary = (node: AnyNode): boolean => - atDriverEdge(node) || atAuthHook(node) || exemptHelper(node); + atDriverEdge(node) || atAuthHook(node) || atTestBoundary(node) || exemptHelper(node); + const reportImplementation = ( + node: AnyNode, + member: string, + isAsync: boolean, + wrapper: string | null, + ): void => { + reportedFunctions.add(node.start); + report(node as ESTree.Node, isAsync ? 'asyncPort' : 'promiseReturningImplementation', { + member, + wrapper: wrapper ?? 'Promise', + }); + }; + const reportOwnedImplementation = ( + node: AnyNode, + isAsync: boolean, + wrapper: string | null, + ): void => { + if (!ownsImplementation(node)) return; + if (atImplementationBoundary(node)) return; + if (insideReportedFunction(node)) return; + const member = nameOf(node); + // Framework router entrypoints (`loader`, `action`, ...) are forced to return a Promise. + if (allowedRoute(node, member)) return; + reportImplementation(node, member, isAsync, wrapper); + }; const checkImplementation = (node: AnyNode): void => { const fn = node as unknown as { readonly async?: boolean; @@ -633,25 +869,51 @@ export const rule = defineRule({ }; const wrapper = promiseReference(fn.returnType); const isAsync = fn.async === true; - if (!isAsync && wrapper === null) return; - if (!ownsImplementation(node)) return; - if (atImplementationBoundary(node)) return; - if (insideReportedFunction(node)) return; - const member = nameOf(node); - // Framework router entrypoints (`loader`, `action`, ...) are forced to return a Promise. - if (allowedRoute(node, member)) return; - reportedFunctions.add(node.start); - report(node as ESTree.Node, isAsync ? 'asyncPort' : 'promiseReturningImplementation', { - member, - wrapper: wrapper ?? 'Promise', - }); + const ownedTest = atOwnedTestCallback(node); + if (!isAsync && wrapper === null && !(ownedTest && returnsKnownPromise(node))) return; + if (ownedTest) { + reportImplementation(node, 'test callback', isAsync, wrapper); + return; + } + reportOwnedImplementation(node, isAsync, wrapper); + }; + const isAliasPortReference = (node: AnyNode): boolean => { + const annotation = parentOf(node); + if (annotation?.type === 'TSTypeAliasDeclaration') return true; + if (annotation?.type !== 'TSTypeAnnotation') return false; + const owner = parentOf(annotation); + return owner?.type === 'Identifier' || owner?.type === 'RestElement'; }; return { + CallExpression: checkTestPromiseAdapter, + TSInterfaceHeritage: (node: ESTree.TSInterfaceHeritage) => { + const wrapper = promiseReference(node); + if (wrapper === null) return; + report(node, 'promisePort', { + member: + node.parent.type === 'TSInterfaceDeclaration' ? node.parent.id.name : 'this interface', + wrapper, + }); + }, + TSTypeReference: (node: ESTree.TSTypeReference) => { + if (!isAliasPortReference(node as unknown as AnyNode)) return; + if (!isPortFunctionTypePosition(node as unknown as AnyNode)) return; + const wrapper = promiseReference(node, true); + if (wrapper === null) return; + if (isForeignThunkParameter(node) || atTestBoundary(node) || atDriverEdge(node)) return; + report(node, 'promisePort', { + member: nameOf(node as unknown as AnyNode), + wrapper, + }); + }, TSMethodSignature: (node: ESTree.TSMethodSignature) => { const wrapper = promiseReference(node.returnType); if (wrapper === null) return; - report(node, 'promisePort', { member: memberName(node as unknown as AnyNode), wrapper }); + report(node, 'promisePort', { + member: memberName(node as unknown as AnyNode), + wrapper, + }); }, TSCallSignatureDeclaration: (node: ESTree.TSCallSignatureDeclaration) => { const wrapper = promiseReference(node.returnType); @@ -661,13 +923,20 @@ export const rule = defineRule({ TSConstructSignatureDeclaration: (node: ESTree.TSConstructSignatureDeclaration) => { const wrapper = promiseReference(node.returnType); if (wrapper === null) return; - report(node, 'promisePort', { member: 'the construct signature', wrapper }); + report(node, 'promisePort', { + member: 'the construct signature', + wrapper, + }); }, TSFunctionType: (node: ESTree.TSFunctionType) => { const wrapper = promiseReference(node.returnType); if (wrapper === null) return; if (!isPortFunctionTypePosition(node as unknown as AnyNode)) return; - report(node, 'promisePort', { member: nameOf(node as unknown as AnyNode), wrapper }); + if (isForeignThunkParameter(node) || atTestBoundary(node)) return; + report(node, 'promisePort', { + member: nameOf(node as unknown as AnyNode), + wrapper, + }); }, TSPropertySignature: (node: ESTree.TSPropertySignature) => { const wrapper = promiseReference(node.typeAnnotation); diff --git a/app/tools/oxlint/effect-native/shared/no-promise-port-types.ts b/app/tools/oxlint/effect-native/shared/no-promise-port-types.ts new file mode 100644 index 000000000..58f331e7c --- /dev/null +++ b/app/tools/oxlint/effect-native/shared/no-promise-port-types.ts @@ -0,0 +1,151 @@ +import type { ESTree } from '@oxlint/plugins'; + +import { typeNameSegments } from './ast.ts'; + +interface TypeBinding { + readonly node: any; + readonly substitutions: ReadonlyMap; +} + +interface Resolution { + readonly seen: Set; + readonly substitutions: ReadonlyMap; + readonly insideFunction: boolean; + readonly functionAliasOnly: boolean; +} + +type VariableLookup = (node: any, name: string) => any; + +/** Same-file type resolution: applied arguments belong to the caller's environment, + * while defaults and constraints belong to the progressively bound declaration. */ +export function createPromisePortTypeResolver( + variableFor: VariableLookup, + promiseTypes: readonly string[], +) { + const branch = (state: Resolution): Resolution => ({ ...state, seen: new Set(state.seen) }); + const promiseName = (name: string, state: Resolution): string | null => + state.functionAliasOnly && !state.insideFunction ? null : `${name}<…>`; + + const resolveFirst = (nodes: readonly any[], state: Resolution): string | null => { + for (const node of nodes) { + const result = resolve(node, branch(state)); + if (result) return result; + } + return null; + }; + + const resolveMembers = (raw: any, state: Resolution): string | null => { + // Direct members have their own visitors. Function-returned records are + // fluent continuations, not service ports, even with applied arguments. + if (state.substitutions.size === 0 || state.insideFunction) return null; + for (const member of raw.members ?? raw.body) { + const isValue = member.type === 'TSPropertySignature' || member.type === 'TSIndexSignature'; + const result = resolve(isValue ? member.typeAnnotation : member.returnType, { + ...branch(state), + insideFunction: state.insideFunction || !isValue, + }); + if (result) return result; + } + return null; + }; + + const bindArguments = (raw: any, alias: any, state: Resolution) => { + const applied = new Map(state.substitutions); + for (const [index, parameter] of (alias.typeParameters?.params ?? []).entries()) { + const argument = raw.typeArguments?.params[index]; + const value = argument ?? parameter.default ?? parameter.constraint; + if (!value) continue; + const binding = variableFor(parameter.name, parameter.name.name); + if (binding) + applied.set(binding, { + node: value, + substitutions: argument ? state.substitutions : applied, + }); + } + return applied; + }; + + const resolveAlias = (raw: any, alias: any, state: Resolution): string | null => { + const applied = bindArguments(raw, alias, state); + if (state.seen.has(alias)) return null; + state.seen.add(alias); + const inheritedState = { ...state, substitutions: applied }; + const own = resolve(alias.typeAnnotation ?? alias.body, branch(inheritedState)); + return own ?? resolveFirst(alias.extends ?? [], inheritedState); + }; + + const resolveLocalReference = ( + raw: any, + typeName: any, + name: string, + state: Resolution, + ): string | null => { + const variable = variableFor(typeName, name); + const bound = state.substitutions.get(variable); + if (bound) return resolve(bound.node, { ...state, substitutions: bound.substitutions }); + const alias = variable?.defs.find((def: any) => + ['TSTypeAliasDeclaration', 'TSInterfaceDeclaration'].includes(def.node.type), + )?.node; + if (alias) return resolveAlias(raw, alias, state); + const parameter = variable?.defs.find((def: any) => def.node.type === 'TSTypeParameter')?.node; + if (parameter) return resolve(parameter, state); + if (variable?.defs.length || !promiseTypes.includes(name)) return null; + return promiseName(name, state); + }; + + const resolveReference = (raw: any, state: Resolution): string | null => { + const typeName = raw.typeName ?? raw.expression; + const names = typeNameSegments(typeName); + if (!names) return null; + const name = names.at(-1)!; + if (names.length === 1) return resolveLocalReference(raw, typeName, name, state); + if ( + names.length === 2 && + names[0] === 'globalThis' && + !variableFor(raw, 'globalThis')?.defs.length && + promiseTypes.includes(name) + ) + return promiseName(name, state); + return null; + }; + + const resolveShape = (raw: any, state: Resolution): string | null => { + if (raw.type === 'TSFunctionType') + return state.substitutions.size === 0 + ? null + : resolve(raw.returnType, { ...state, insideFunction: true }); + if (raw.type === 'TSTypeLiteral' || raw.type === 'TSInterfaceBody') + return resolveMembers(raw, state); + if (raw.type === 'TSTypeReference' || raw.type === 'TSInterfaceHeritage') + return resolveReference(raw, state); + return null; + }; + + const resolve = (raw: any, state: Resolution): string | null => { + if (!raw || state.seen.has(raw)) return null; + state.seen.add(raw); + if (raw.type === 'TSTypeAnnotation' || raw.type === 'TSParenthesizedType') + return resolve(raw.typeAnnotation, state); + if (raw.type === 'TSTypeParameter') return resolveFirst([raw.constraint, raw.default], state); + if (raw.type === 'TSUnionType' || raw.type === 'TSIntersectionType') + return resolveFirst(raw.types, state); + return resolveShape(raw, state); + }; + + return function promiseReference( + annotation: + | ESTree.TSTypeAnnotation + | ESTree.TSTypeReference + | ESTree.TSInterfaceHeritage + | null + | undefined, + functionAliasOnly = false, + ): string | null { + return resolve(annotation, { + seen: new Set(), + substitutions: new Map(), + insideFunction: false, + functionAliasOnly, + }); + }; +} diff --git a/app/tools/oxlint/effect-native/shared/test-restricted-imports.ts b/app/tools/oxlint/effect-native/shared/test-restricted-imports.ts new file mode 100644 index 000000000..710bf9771 --- /dev/null +++ b/app/tools/oxlint/effect-native/shared/test-restricted-imports.ts @@ -0,0 +1,16 @@ +/** Test APIs must use the Effect-native runner in both lint entrypoints. */ +export const testRestrictedImports = [ + { message: 'Import test APIs from effect-rstest instead.', name: 'node:test' }, + { + message: 'Import assertions from effect-rstest instead.', + name: 'node:assert', + }, + { + message: 'Import assertions from effect-rstest instead.', + name: 'node:assert/strict', + }, + { + message: 'Import test APIs from effect-rstest instead.', + name: '@rstest/core', + }, +]; diff --git a/app/tools/oxlint/effect-native/tests/discover-rules.test.mts b/app/tools/oxlint/effect-native/tests/discover-rules.test.mts index a1bf6dde7..275136757 100644 --- a/app/tools/oxlint/effect-native/tests/discover-rules.test.mts +++ b/app/tools/oxlint/effect-native/tests/discover-rules.test.mts @@ -1,41 +1,48 @@ -import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { copyFileSync, mkdirSync, realpathSync, symlinkSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { test } from 'node:test'; +import path from 'node:path'; import { pathToFileURL } from 'node:url'; +import { expect, it } from 'effect-rstest'; +import { Effect, Predicate } from 'effect'; + import { discoverRules } from '../shared/discover-rules.ts'; import { pluginDirectory } from './oxlint.mts'; import { withTemporaryWorkspace } from './temporary-workspace.mts'; -test('rule discovery loads the selected production rule and rejects unknown names', async () => { - const rules = await discoverRules(['no-native-timers']); - assert.deepEqual(Object.keys(rules), ['no-native-timers']); - assert.equal(typeof rules['no-native-timers']?.create, 'function'); - await assert.rejects(discoverRules(['not-a-rule']), /Unknown fixture rule: not-a-rule/u); -}); +it.effect('rule discovery loads the selected production rule and rejects unknown names', () => + Effect.gen(function* ruleDiscoveryEffect() { + const rules = yield* Effect.tryPromise(() => discoverRules(['no-native-timers'])); + expect(Object.keys(rules)).toEqual(['no-native-timers']); + expect(Predicate.isFunction(rules['no-native-timers']?.create)).toBe(true); + const error = yield* Effect.flip(Effect.tryPromise(() => discoverRules(['not-a-rule']))); + const message: unknown = expect.stringMatching(/Unknown fixture rule: not-a-rule/u); + expect(error.cause).toMatchObject({ message }); + }), +); -test('rule discovery uses file URLs in workspaces containing spaces, URL delimiters, and Unicode', () => { +it('rule discovery uses file URLs in workspaces containing spaces, URL delimiters, and Unicode', () => { withTemporaryWorkspace((directory) => { - const workspace = join(directory, 'workspace #rules % café'); - const shared = join(workspace, 'shared'); - const rules = join(workspace, 'rules'); + const workspace = path.join(directory, 'workspace #rules % café'); + const shared = path.join(workspace, 'shared'); + const rules = path.join(workspace, 'rules'); + const selectedFile = 'selected.ts'; + const discoveryFile = 'discover-rules.ts'; mkdirSync(shared, { recursive: true }); mkdirSync(rules, { recursive: true }); - writeFileSync(join(workspace, 'package.json'), JSON.stringify({ type: 'module' })); + writeFileSync(path.join(workspace, 'package.json'), JSON.stringify({ type: 'module' })); copyFileSync( - join(pluginDirectory, 'shared', 'discover-rules.ts'), - join(shared, 'discover-rules.ts'), + path.join(pluginDirectory, 'shared', discoveryFile), + path.join(shared, discoveryFile), ); - writeFileSync(join(rules, 'selected.ts'), 'export const rule = { marker: "selected" };'); + writeFileSync(path.join(rules, selectedFile), 'export const rule = { marker: "selected" };'); writeFileSync( - join(rules, 'unselected.ts'), + path.join(rules, 'unselected.ts'), 'throw new Error("unselected rule must not load"); export const rule = {};', ); - const alias = join(directory, 'workspace-link'); + const alias = path.join(directory, 'workspace-link'); symlinkSync(workspace, alias, 'dir'); - const modulePath = join(alias, 'shared', 'discover-rules.ts'); + const modulePath = path.join(alias, 'shared', 'discover-rules.ts'); const moduleUrl = pathToFileURL(modulePath).href; // Node resolves symlinked workspace roots before exposing the import's parent URL. const resolvedModuleUrl = pathToFileURL(realpathSync(modulePath)).href; @@ -68,10 +75,10 @@ test('rule discovery uses file URLs in workspaces containing spaces, URL delimit } `, ], - { cwd: workspace, encoding: 'utf8', timeout: 30_000 }, + { cwd: workspace, encoding: 'utf-8', timeout: 30_000 }, ); - assert.equal(result.error, undefined); - assert.equal(result.status, 0, result.stderr || result.stdout); - assert.equal(result.stderr, ''); + expect(result.error).toBe(undefined); + expect(result.status, result.stderr || result.stdout).toBe(0); + expect(result.stderr).toBe(''); }); }); diff --git a/app/tools/oxlint/effect-native/tests/fixtures.test.mts b/app/tools/oxlint/effect-native/tests/fixtures.test.mts index 57671b675..a8e88e2fd 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures.test.mts +++ b/app/tools/oxlint/effect-native/tests/fixtures.test.mts @@ -1,7 +1,7 @@ -import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; -import { join, relative } from 'node:path'; -import { test } from 'node:test'; +import path from 'node:path'; + +import { expect, it } from 'effect-rstest'; import { fixtureConfigPath, @@ -15,34 +15,39 @@ const onlyRule = process.env.RULE; const rules = listFixtureRules().filter((rule) => onlyRule === undefined || rule === onlyRule); if (rules.length === 0) { - test('fixtures exist', () => - assert.fail(`No fixture directories found${onlyRule ? ` for ${onlyRule}` : ''}.`)); + it('fixtures exist', () => { + const suffix = onlyRule === undefined || onlyRule === '' ? '' : ` for ${onlyRule}`; + expect(rules, `No fixture directories found${suffix}.`).not.toHaveLength(0); + }); } -function appendValidFailures( +const appendValidFailures = ( fixtureDirectory: string, valid: readonly string[], byFile: ReadonlyMap, failures: string[], -): void { +): void => { for (const file of valid) { - const key = relative(fixtureDirectory, file).replaceAll('\\', '/'); + const key = path.relative(fixtureDirectory, file).replaceAll('\\', '/'); const count = byFile.get(key) ?? 0; - if (count !== 0) failures.push(`${key} must not report (false positive: ${count})`); + if (count !== 0) { + failures.push(`${key} must not report (false positive: ${count})`); + } } -} +}; -function fixtureFailures( +const fixtureFailures = ( fixtureDirectory: string, invalid: readonly string[], valid: readonly string[], byFile: ReadonlyMap, -): string[] { +): string[] => { const failures: string[] = []; for (const file of invalid) { - const key = relative(fixtureDirectory, file).replaceAll('\\', '/'); + const key = path.relative(fixtureDirectory, file).replaceAll('\\', '/'); const count = byFile.get(key) ?? 0; - const expected = /^\/\/\s*expect-count:\s*(\d+)/u.exec(readFileSync(file, 'utf8'))?.[1]; + const expected = /^\/\/\s*expect-count:\s*(?\d+)/u.exec(readFileSync(file, 'utf-8')) + ?.groups?.count; if (expected !== undefined) { if (Number(expected) <= 0 || count !== Number(expected)) { failures.push(`${key} expected ${expected} positive diagnostics, got ${count}`); @@ -53,39 +58,36 @@ function fixtureFailures( } appendValidFailures(fixtureDirectory, valid, byFile, failures); return failures; -} +}; for (const rule of rules) { - test(`effect-native/${rule} fixtures`, () => { - const fixtureDirectory = join(fixturesDirectory, rule); - const invalid = listFilesRecursively(join(fixtureDirectory, 'invalid')); - const valid = listFilesRecursively(join(fixtureDirectory, 'valid')); - const paths = [...invalid, ...valid].map((file) => relative(fixtureDirectory, file)); + it(`effect-native/${rule} fixtures`, () => { + const fixtureDirectory = path.join(fixturesDirectory, rule); + const invalid = listFilesRecursively(path.join(fixtureDirectory, 'invalid')); + const valid = listFilesRecursively(path.join(fixtureDirectory, 'valid')); + const paths = [...invalid, ...valid].map((file) => path.relative(fixtureDirectory, file)); const run = runOxlint(fixtureConfigPath(rule), paths, fixtureDirectory); - assert.ok( + expect( !run.stderr.includes('Failed to') && !run.stderr.includes('Error'), `oxlint failed for ${rule}:\n${run.stderr}`, - ); + ).toBe(true); const code = `effect-native(${rule})`; const byFile = new Map(); for (const diagnostic of run.diagnostics) { - assert.equal( + expect( diagnostic.code, - code, `unexpected diagnostic ${diagnostic.code} in ${diagnostic.filename}`, - ); + ).toBe(code); const key = diagnostic.filename.replaceAll('\\', '/'); byFile.set(key, (byFile.get(key) ?? 0) + 1); } - assert.ok(invalid.length > 0, `${rule}: add at least one file under invalid/`); - assert.ok(valid.length > 0, `${rule}: add at least one file under valid/`); - assert.equal(run.exitCode, 1, `${rule}: invalid fixtures must make Oxlint fail`); - assert.equal( - run.numberOfFiles, + expect(invalid.length, `${rule}: add at least one file under invalid/`).toBeGreaterThan(0); + expect(valid.length, `${rule}: add at least one file under valid/`).toBeGreaterThan(0); + expect(run.exitCode, `${rule}: invalid fixtures must make Oxlint fail`).toBe(1); + expect(run.numberOfFiles, `${rule}: not every fixture was linted`).toBe( invalid.length + valid.length, - `${rule}: not every fixture was linted`, ); const failures = fixtureFailures(fixtureDirectory, invalid, valid, byFile); - assert.deepEqual(failures, [], `${rule}:\n${failures.join('\n')}`); + expect(failures, `${rule}:\n${failures.join('\n')}`).toStrictEqual([]); }); } diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/effect-client.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/effect-client.test.ts new file mode 100644 index 000000000..b2e93c525 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/effect-client.test.ts @@ -0,0 +1,8 @@ +// expect-count: 3 +import { Effect } from "@modern-js/plugin-bff/effect-client"; +import { Effect as E } from "@modern-js/plugin-bff/effect-client"; +import * as Client from "@modern-js/plugin-bff/effect-client"; + +export const direct = Effect.runSync(Effect.succeed(1)); +export const aliased = E.runSync(E.succeed(1)); +export const namespace = Client.Effect.runSync(Client.Effect.succeed(1)); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/tests/harness/it-layer.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/tests/harness/it-layer.ts new file mode 100644 index 000000000..9f3bc09e4 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/tests/harness/it-layer.ts @@ -0,0 +1,5 @@ +// expect-count: 1 +// Former harness paths must not bypass the shared runner. +import { Effect } from "effect"; + +export const result = Effect.runSync(Effect.succeed(1)); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/tests/support/effect-harness.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/tests/support/effect-harness.ts new file mode 100644 index 000000000..9f3bc09e4 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/tests/support/effect-harness.ts @@ -0,0 +1,5 @@ +// expect-count: 1 +// Former harness paths must not bypass the shared runner. +import { Effect } from "effect"; + +export const result = Effect.runSync(Effect.succeed(1)); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/tests/support/it-effect.mts b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/tests/support/it-effect.mts new file mode 100644 index 000000000..9f3bc09e4 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/tests/support/it-effect.mts @@ -0,0 +1,5 @@ +// expect-count: 1 +// Former harness paths must not bypass the shared runner. +import { Effect } from "effect"; + +export const result = Effect.runSync(Effect.succeed(1)); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/effect-client.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/effect-client.test.ts new file mode 100644 index 000000000..5ed766212 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/effect-client.test.ts @@ -0,0 +1,7 @@ +import { Effect } from "@modern-js/plugin-bff/effect-client"; +import { Effect as E } from "@modern-js/plugin-bff/effect-client"; +import * as Client from "@modern-js/plugin-bff/effect-client"; + +export const direct = () => Effect.succeed(1); +export const aliased = () => E.succeed(1); +export const namespace = () => Client.Effect.succeed(1); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/harness-usage.test.tsx b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/harness-usage.test.tsx index 36ce8b362..6c0948537 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/harness-usage.test.tsx +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/harness-usage.test.tsx @@ -1,21 +1,23 @@ import { Effect, Layer, ManagedRuntime, Schema } from "effect"; import { TestClock } from "effect/testing"; -import { itEffect, itLayer } from "./tests/support/effect-harness.ts"; +import { it } from "effect-rstest"; declare const ContactsLayer: Layer.Layer; declare const resolve: (id: string) => Effect.Effect; -itEffect( +it.effect( "resolves through the harness", - Effect.gen(function* () { + () => Effect.gen(function* () { yield* TestClock.adjust("1 second"); const value = yield* resolve("x"); return Schema.decodeUnknownSync(Schema.String)(value); }), ); -itLayer("resolves with an explicit layer", ContactsLayer, resolve("y")); +it.layer(ContactsLayer)("contacts", (it) => { + it.effect("resolves with an explicit layer", () => resolve("y")); +}); // A long-lived ManagedRuntime instance is the A1 target, not an ad hoc Effect.run* entry point. const runtime = ManagedRuntime.make(ContactsLayer); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/tests/harness/it-layer.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/tests/harness/it-layer.ts index f9678d727..0319c7d1d 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/tests/harness/it-layer.ts +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/tests/harness/it-layer.ts @@ -1,8 +1,7 @@ -// `**/tests/harness/**` is harness territory: the single owned Effect → Promise seam lives here. +// A harness directory is ordinary test code, not a runtime exemption. +import { it } from "effect-rstest"; import { Effect, Layer } from "effect"; -declare const test: (name: string, body: () => Promise) => void; - -export const runInLayer = (name: string, layer: Layer.Layer, effect: Effect.Effect): void => { - test(name, () => Effect.runPromise(Effect.provide(effect, layer) as Effect.Effect)); -}; +it.layer(Layer.empty)("shared layer", (it) => { + it.effect("uses the shared layer runner", () => Effect.void); +}); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/tests/support/effect-harness.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/tests/support/effect-harness.ts index 98151d864..fcfdc987c 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/tests/support/effect-harness.ts +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/tests/support/effect-harness.ts @@ -1,18 +1,5 @@ -// The repository-owned harness (B2 target) is the one place allowed to leave Effect. -import { ConfigProvider, Effect, Layer } from "effect"; +// Test support delegates runtime ownership to the shared runner. +import { it } from "effect-rstest"; +import { Effect } from "effect"; -declare const test: (name: string, body: () => Promise) => void; - -const TestConfig = Layer.setConfigProvider(ConfigProvider.fromMap(new Map([["APP_ENV", "test"]]))); - -export const itEffect = (name: string, effect: Effect.Effect, layer = TestConfig): void => { - test(name, () => Effect.runPromise(Effect.scoped(Effect.provide(effect, layer)) as Effect.Effect)); -}; - -export const itLayer = ( - name: string, - layer: Layer.Layer, - effect: Effect.Effect, -): void => { - test(name, () => Effect.runPromise(Effect.scoped(Effect.provide(effect, layer)) as Effect.Effect)); -}; +it.effect("uses the shared runner from test support", () => Effect.void); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-instanceof/.oxlintrc.json b/app/tools/oxlint/effect-native/tests/fixtures/no-instanceof/.oxlintrc.json new file mode 100644 index 000000000..2dfb0c9f1 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-instanceof/.oxlintrc.json @@ -0,0 +1,5 @@ +{ + "jsPlugins": [{ "name": "effect-native", "specifier": "../../fixture-plugin.ts" }], + "categories": { "correctness": "off" }, + "rules": { "effect-native/no-instanceof": "error" } +} diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-instanceof/invalid/scripts/every-constructor.mts b/app/tools/oxlint/effect-native/tests/fixtures/no-instanceof/invalid/scripts/every-constructor.mts new file mode 100644 index 000000000..dca885e93 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-instanceof/invalid/scripts/every-constructor.mts @@ -0,0 +1,9 @@ +// expect-count: 6 +const check = (value: unknown) => [ + value instanceof Error, + value instanceof LocalDevelopmentInitializationError, + value instanceof ProviderException, + value instanceof globalThis.Date, + value instanceof SDK.APIError, + value instanceof Alias, +]; diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-instanceof/valid/scripts/native-guards.mts b/app/tools/oxlint/effect-native/tests/fixtures/no-instanceof/valid/scripts/native-guards.mts new file mode 100644 index 000000000..0e99c38a5 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-instanceof/valid/scripts/native-guards.mts @@ -0,0 +1,8 @@ +import { Effect, Match, Predicate, Schema } from 'effect'; +const error = Predicate.isError(value); +const tagged = Predicate.isTagged(value, 'Missing'); +const decoded = Schema.is(Missing)(value); +const recovered = operation.pipe(Effect.catchTag('Missing', () => Effect.void)); +const matched = Match.value(value).pipe(Match.tag('Missing', () => true), Match.orElse(() => false)); +const lexical = 'instanceof'; +const grammar = /\binstanceof\b/u; diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/.oxlintrc.json b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/.oxlintrc.json index 1bd6a32de..0449d79f9 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/.oxlintrc.json +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/.oxlintrc.json @@ -1,9 +1,44 @@ { - "jsPlugins": [{ "name": "effect-native", "specifier": "../../fixture-plugin.ts" }], - "categories": { "correctness": "off" }, - "rules": { "effect-native/no-manual-tag-comparison": "error" }, - "overrides": [{ - "files": ["**/error-combinators-disabled/**"], - "rules": { "effect-native/no-manual-tag-comparison": ["error", { "includeErrorCombinators": false }] } - }] + "jsPlugins": [ + { + "name": "effect-native", + "specifier": "../../fixture-plugin.ts" + } + ], + "categories": { + "correctness": "off" + }, + "rules": { + "effect-native/no-manual-tag-comparison": "error" + }, + "overrides": [ + { + "files": [ + "**/error-combinators-disabled/**" + ], + "rules": { + "effect-native/no-manual-tag-comparison": [ + "error", + { + "includeErrorCombinators": false + } + ] + } + }, + { + "files": [ + "**/configured-assertions.ts" + ], + "rules": { + "effect-native/no-manual-tag-comparison": [ + "error", + { + "allowTags": [ + "Legacy" + ] + } + ] + } + } + ] } diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/apps/property-assertions.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/apps/property-assertions.ts new file mode 100644 index 000000000..91c68bff9 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/apps/property-assertions.ts @@ -0,0 +1,28 @@ +// expect-count: 16 +import { expect } from 'effect-rstest'; +import * as rstest from '@rstest/core'; +import { expect as check } from 'vitest'; +import jestExpect from 'expect'; +declare const error: unknown; +declare const dynamicTag: string; +const key = '_tag'; +const nestedKey = 'cause._tag'; +expect(error).toHaveProperty('_tag', 'Missing'); +expect(error).toHaveProperty('_tag'); +expect(error).not.toHaveProperty('_tag', 'Missing'); +expect(error).rejects.toHaveProperty('_tag'); +expect(error).toHaveProperty(`_tag`, 'Missing'); +expect(error).toHaveProperty('_' + 'tag', 'Missing'); +expect(error).toHaveProperty(key, 'Missing'); +expect(error).toHaveProperty(['_tag'], 'Missing'); +expect(error).toHaveProperty(['cause', '_tag']); +expect(error).toHaveProperty('cause._tag', 'Missing'); +expect(error).toHaveProperty(nestedKey, 'Missing'); +expect(error)['toHaveProperty']('_tag', dynamicTag); +rstest.expect(error).toHaveProperty(['causes', 0, key], 'Missing'); +check(error).toHaveProperty('_tag', 'Missing'); +jestExpect(error).toHaveProperty('_tag', 'Missing'); +const alias = expect; +function nested(expect: unknown) { + alias(error).toHaveProperty('_tag', 'Missing'); +} diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/apps/property-options/configured-assertions.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/apps/property-options/configured-assertions.ts new file mode 100644 index 000000000..00c74ea41 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/apps/property-options/configured-assertions.ts @@ -0,0 +1,5 @@ +// expect-count: 2 +import { expect } from 'effect-rstest'; +declare const error: unknown; +expect(error).toHaveProperty('_tag'); +expect(error).toHaveProperty('_tag', 'Missing'); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/configured-assertions.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/configured-assertions.ts new file mode 100644 index 000000000..7b835bd87 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/configured-assertions.ts @@ -0,0 +1,13 @@ +// expect-count: 5 +import assert from 'node:assert/strict'; +import { expect } from '@rstest/core'; +assert.equal(value._tag, 'Missing', 'Legacy'); +expect(value._tag).toBe('Missing', 'Legacy'); +assert.deepEqual(values.map(value => value._tag), ['Legacy', 'Missing']); + +switch (value._tag) { + case 'Legacy': break; + case 'Missing': break; +} + +expect(value).toEqual(expect.arrayContaining([{ _tag: 'Legacy' }, { _tag: 'Missing' }])); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/destructured-assertions.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/destructured-assertions.ts new file mode 100644 index 000000000..142c55912 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/destructured-assertions.ts @@ -0,0 +1,17 @@ +// expect-count: 5 +import assert from 'node:assert/strict'; +import * as assertions from 'node:assert'; +import { expect } from 'effect-rstest'; +declare const error: { _tag: string }; +const { strictEqual } = assert; +strictEqual(error._tag, 'Missing'); +const { strictEqual: equal } = assert; +equal(error._tag, 'Missing'); +const { ['deepStrictEqual']: deepEqual } = assertions; +deepEqual([error._tag], ['Missing']); +const source = assertions.strict; +const { equal: check } = source; +const alias = check; +alias(error._tag, 'Missing'); +const { toBe: matches } = expect(error._tag); +matches('Missing'); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/expected-wrappers.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/expected-wrappers.ts new file mode 100644 index 000000000..30f21ead1 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/expected-wrappers.ts @@ -0,0 +1,37 @@ +// expect-count: 19 +import { expect } from 'effect-rstest'; +import { expect as check } from '@rstest/core'; +import * as testing from 'vitest'; +import jestExpect from 'expect'; +import { expect as jestCheck } from '@jest/globals'; +declare const error: unknown; +declare const tag: string; +expect(error).toEqual(expect.objectContaining({ _tag: 'Missing' })); +check(error).not.toMatchObject(check.objectContaining({ ['_tag']: 'Missing' })); +testing.expect(error).toStrictEqual(testing.expect.objectContaining({ _tag: tag })); +jestExpect(error).toEqual(jestExpect.objectContaining({ _tag: 'Missing' })); +jestCheck(error).toEqual(jestCheck.objectContaining({ _tag: 'Missing' })); +const alias = expect; +alias(error).toEqual(alias.objectContaining({ _tag: 'Missing' })); +const contains = expect.objectContaining; +expect(error).toEqual(contains({ _tag: 'Missing' })); +const { objectContaining: shape } = expect; +expect(error).toEqual(shape({ _tag: 'Missing' })); +const { expect: namespaceExpect } = testing; +expect(error).toEqual(namespaceExpect.objectContaining({ _tag: 'Missing' })); +const { ['arrayContaining']: items } = alias; +expect(error).toEqual(items([{ _tag: 'Missing' }])); +expect(error).toEqual(expect.arrayContaining([expect.objectContaining({ _tag: 'Missing' })])); +expect(error).toEqual(expect.arrayContaining([expect.arrayContaining([{ _tag: 'Missing' }])])); +const expected = { _tag: 'Missing' }; +const wrapped = shape(expected); +expect(error).toEqual(wrapped); +const shapes = [{ _tag: 'Failure' }, { _tag: 'Missing' }]; +expect(error).toEqual(items(shapes)); +expect(error).toEqual(items([{ _tag: 'Missing' }, { _tag: 'Other' }])); +const outer = expect.objectContaining(expect.objectContaining(expected)); +expect(error).toEqual(outer); +expect(error).rejects.toEqual(expect.objectContaining(expected)); +expect([error]).toContainEqual(expect.objectContaining(expected)); + +expect(error).toEqual(items([items(expected), shape(expected)])); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/negated-expected-wrappers.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/negated-expected-wrappers.ts new file mode 100644 index 000000000..29b21ab38 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/negated-expected-wrappers.ts @@ -0,0 +1,21 @@ +// expect-count: 10 +import { expect } from 'effect-rstest'; +import { expect as check } from '@rstest/core'; +import * as testing from 'vitest'; +import jestExpect from 'expect'; +import { expect as jestCheck } from '@jest/globals'; +declare const error: unknown; +expect(error).toEqual(expect.not.objectContaining({ _tag: 'Missing' })); +check(error).toEqual(check.not.arrayContaining([{ _tag: 'Missing' }])); +testing.expect(error).toStrictEqual(testing.expect.not.objectContaining({ _tag: 'Missing' })); +jestExpect(error).toEqual(jestExpect.not.objectContaining({ _tag: 'Missing' })); +jestCheck(error).toEqual(jestCheck.not.arrayContaining([{ _tag: 'Missing' }])); +const negative = expect.not; +expect(error).toEqual(negative.objectContaining({ _tag: 'Missing' })); +const { not: namespaceNegative } = testing.expect; +expect(error).toEqual(namespaceNegative.arrayContaining([{ _tag: 'Missing' }])); +const { objectContaining: absent } = expect.not; +expect(error).toEqual(absent({ _tag: 'Missing' })); +const missing = expect['not']['arrayContaining']([expect.not.objectContaining({ _tag: 'Missing' })]); +expect(error).toEqual(missing); +expect(error).not.toEqual(expect.not.objectContaining({ _tag: 'Missing' })); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/object-equality-assertions.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/object-equality-assertions.ts new file mode 100644 index 000000000..74d80c810 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/object-equality-assertions.ts @@ -0,0 +1,17 @@ +// expect-count: 10 +import assert from 'node:assert/strict'; +import { expect } from 'effect-rstest'; +declare const error: unknown; +declare const tag: string; +expect(error).toEqual({ _tag: 'Missing', reason: 'denied' }); +expect(error).not.toStrictEqual({ ['_tag']: 'Missing' }); +expect([error]).toContainEqual({ _tag: 'Missing' }); +const expected = { _tag: tag }; +expect(error).toEqual(expected); +assert.deepEqual(error, { _tag: 'Missing' }); +assert.deepStrictEqual(error, { _tag: 'Missing' }); +assert.notDeepEqual(error, { _tag: 'Missing' }); +assert.notDeepStrictEqual(error, { _tag: 'Missing' }); +const { deepStrictEqual: equalShape } = assert; +equalShape(error, expected); +expect(error).rejects.toStrictEqual({ _tag: 'Missing' }); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/partial-object-assertions.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/partial-object-assertions.ts new file mode 100644 index 000000000..678c5e61a --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/partial-object-assertions.ts @@ -0,0 +1,10 @@ +// expect-count: 4 +import { expect } from 'effect-rstest'; +declare const error: unknown; +declare const tag: string; +expect(error).toMatchObject({ _tag: 'ModuleStateDeniedError' }); +expect(error).not.toMatchObject({ ['_tag']: 'Missing' }); +const expected = { _tag: tag }; +expect(error).toMatchObject(expected); +const assertion = expect; +assertion(error).rejects.toMatchObject({ _tag: 'Missing', reason: 'denied' }); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/runtime.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/runtime.ts index 9d89af078..4f60b4fe9 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/runtime.ts +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/runtime.ts @@ -1,4 +1,4 @@ -// expect-count: 4 +// expect-count: 5 import type { Exit } from "effect"; interface DomainError { diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/stable-expected-wrappers.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/stable-expected-wrappers.ts new file mode 100644 index 000000000..4f52ad196 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/stable-expected-wrappers.ts @@ -0,0 +1,14 @@ +// expect-count: 3 +import { expect } from 'effect-rstest'; +declare const error: unknown; +const fake = (value: unknown) => value; +let reassigned = expect; +reassigned = fake; +let { arrayContaining: detached } = expect; +detached = fake; +expect.objectContaining = fake; +expect(error).toEqual(expect.arrayContaining([{ _tag: 'Missing' }])); +const alias = expect; +expect(error).toEqual(alias.arrayContaining([{ _tag: 'Missing' }])); +const { arrayContaining: items } = expect; +expect(error).toEqual(items([{ _tag: 'Missing' }])); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/tag-identity.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/tag-identity.ts new file mode 100644 index 000000000..8dadc7e15 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/tag-identity.ts @@ -0,0 +1,3 @@ +// expect-count: 2 +export const sameTag = (a: { readonly _tag: string }, b: { readonly _tag: string }): boolean => a._tag === b._tag; +export const same = (a: { readonly _tag: string }, b: { readonly _tag: string }): boolean => Object.is(a._tag, b._tag); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/tests/unit/assertions.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/tests/unit/assertions.test.ts new file mode 100644 index 000000000..ea3c1a5b7 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/tests/unit/assertions.test.ts @@ -0,0 +1,37 @@ +// expect-count: 22 +import assert, { strictEqual as equal } from 'node:assert/strict'; +import { assert as rstestAssert, expect, expect as check } from '@rstest/core'; +import { assert as effectAssert } from 'effect-rstest'; +import * as testing from '@rstest/core'; +import * as assertions from 'node:assert/strict'; +import { strict as strictAssert } from 'node:assert'; +assert.equal(error._tag, 'Missing'); +assert.strictEqual(error['_tag'], expected); +assert.deepEqual(errors.map(error => error._tag), ['Missing']); +const tags = errors.map(error => error._tag); +assert.deepEqual(tags, ['Missing']); +expect(error._tag).toBe('Missing'); +expect(error._tag).not.toEqual('Missing'); +expect(error._tag).resolves.toStrictEqual('Missing'); +assert.equal(error._tag, 'Missing', 'Some'); +expect(error._tag).toBe('Missing', 'Some'); +assert.equal(guard && failure._tag, 'Missing'); + +equal(error._tag, 'Missing'); +const same = assert.strictEqual; +same(error._tag, 'Missing'); +const equalAgain = equal; +equalAgain(error._tag, 'Missing'); + +assertions.deepStrictEqual(error._tag, 'Missing'); +strictAssert.equal(error._tag, 'Missing'); +check(error._tag).not.toBe('Missing'); +testing.expect(error._tag).toEqual('Missing'); + +rstestAssert.strictEqual(error._tag, 'Missing'); +effectAssert.deepEqual(error._tag, 'Missing'); + +assert.equal(flag ? error._tag : other._tag, 'Missing'); +assert.deepEqual(errors.map(error => { return error._tag; }), ['Missing']); +const projectTag = error => { if (flag) return error._tag; return other._tag; }; +assert.deepEqual(errors.map(projectTag), ['Missing']); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/shared-contracts/src/switch.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/shared-contracts/src/switch.ts similarity index 89% rename from app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/shared-contracts/src/switch.ts rename to app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/shared-contracts/src/switch.ts index bfd70472b..a0206a1e5 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/shared-contracts/src/switch.ts +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/shared-contracts/src/switch.ts @@ -11,7 +11,6 @@ export const label = (error: { readonly _tag: string }): string => { }; /** Comparing two discriminants is an identity test, not a case analysis. */ -export const sameTag = (a: { readonly _tag: string }, b: { readonly _tag: string }): boolean => a._tag === b._tag; /** String methods on something that is not a `_tag` access. */ export const known = ["ShellTargetNotFoundProblem"]; diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/apps/error-combinators-disabled/property-assertions.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/apps/error-combinators-disabled/property-assertions.ts new file mode 100644 index 000000000..9b83f4e9a --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/apps/error-combinators-disabled/property-assertions.ts @@ -0,0 +1,8 @@ +import { expect } from 'effect-rstest'; +import { Effect } from 'effect'; +declare const program: Effect.Effect; +program.pipe(Effect.catch((error) => { + expect(error).toHaveProperty('_tag'); + expect(error).toHaveProperty('cause._tag', 'Missing'); + return Effect.void; +})); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/apps/property-assertions.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/apps/property-assertions.ts new file mode 100644 index 000000000..ac936df0c --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/apps/property-assertions.ts @@ -0,0 +1,38 @@ +import { expect } from 'effect-rstest'; +import { expect as foreignExpect } from 'foreign-assertions'; +import * as assert from 'node:assert'; +declare const error: unknown; +declare const dynamicPath: string; +declare const foreign: typeof expect; +const failure = 'Failure'; +expect(error).toHaveProperty('_tag', 'Failure'); +expect(error).toHaveProperty('cause._tag', failure); +expect(error).toHaveProperty(['cause', '_tag'], 'Success'); +expect(error).toHaveProperty('tag', 'Missing'); +expect(error).toHaveProperty('_tags', 'Missing'); +expect(error).toHaveProperty('cause.not_tag', 'Missing'); +expect(error).toHaveProperty('cause._tagSuffix'); +expect(error).toHaveProperty(['cause._tag'], 'Missing'); +expect(error).toHaveProperty(dynamicPath, 'Missing'); +expect(error).toHaveProperty([], 'Missing'); +expect(error).toHaveProperty(['cause', dynamicPath], 'Missing'); +expect(error).toHaveProperty('message', { _tag: 'Missing' }); +foreignExpect(error).toHaveProperty('_tag', 'Missing'); +function shadowed(expect: typeof foreign) { + expect(error).toHaveProperty('_tag', 'Missing'); +} +let mutable = expect; +mutable = foreign; +mutable(error).toHaveProperty('_tag', 'Missing'); +let key = '_tag'; +key = 'message'; +expect(error).toHaveProperty(key, 'Missing'); +assert.toHaveProperty(error, '_tag', 'Missing'); +const ordinary = { toHaveProperty: (...args: unknown[]) => args }; +ordinary.toHaveProperty('_tag', 'Missing'); +function shadowedKey() { + const key = 'message'; + expect(error).toHaveProperty(key, 'Missing'); +} +expect(error).toHaveProperty('_tag.length', 7); +expect(error).toHaveProperty(['_tag', 'length'], 7); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/apps/property-options/configured-assertions.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/apps/property-options/configured-assertions.ts new file mode 100644 index 000000000..93d7c1bd9 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/apps/property-options/configured-assertions.ts @@ -0,0 +1,5 @@ +import { expect } from 'effect-rstest'; +declare const error: unknown; +expect(error).toHaveProperty('_tag', 'Legacy'); +expect(error).toHaveProperty('cause._tag', 'Legacy'); +expect(error).toHaveProperty(['_tag'], 'Legacy'); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/adt.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/adt.ts index 2217b6d96..ac69b1f12 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/adt.ts +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/adt.ts @@ -1,3 +1,5 @@ +import assert, { strictEqual as equal } from 'node:assert/strict'; +import { expect } from '@rstest/core'; import { Exit, Option, Result } from "effect"; /** Effect's own ADT tags belong to `no-raw-effect-adt-tag-check`; this rule must stay silent. */ @@ -13,3 +15,16 @@ export const isLeftOrRight = (value: { readonly _tag: string }): boolean => /** Combinator-based inspection is the target state. */ export const viaCombinators = (option: Option.Option, result: Result.Result): boolean => Option.isSome(option) && Result.isSuccess(result); + +assert.equal(option._tag, 'Some'); +equal('None', option._tag); +expect(option._tag).not.toEqual('None'); +assert.deepEqual(values.map(value => value._tag), ['Some', 'None']); + +switch (option._tag) { + case 'Some': break; + case 'None': break; + default: break; +} + +assert.equal(option._tag === 'Some', true); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/configured-assertions.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/configured-assertions.ts new file mode 100644 index 000000000..3a80d6389 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/configured-assertions.ts @@ -0,0 +1,14 @@ +import assert, { strictEqual as equal } from 'node:assert/strict'; +import { expect } from '@rstest/core'; +assert.equal(value._tag, 'Legacy'); +equal('Legacy', value._tag); +expect(value._tag).not.toEqual('Legacy'); +assert.deepEqual(values.map(value => value._tag), ['Legacy']); + +switch (value._tag) { + case 'Legacy': break; + default: break; +} + +expect(value).toEqual(expect.objectContaining({ _tag: 'Legacy' })); +expect(value).toEqual(expect.arrayContaining([{ _tag: 'Legacy' }, { _tag: 'Failure' }])); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/destructured-assertions.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/destructured-assertions.ts new file mode 100644 index 000000000..79b2328c9 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/destructured-assertions.ts @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict'; +import { expect } from 'effect-rstest'; +import foreign from 'foreign-assertions'; +declare const error: { _tag: string }; +const { strictEqual: foreignEqual } = foreign; +foreignEqual(error._tag, 'Missing'); +function shadow(assert: typeof import('node:assert/strict')) { + const { strictEqual } = assert; + strictEqual(error._tag, 'Missing'); +} +let { strictEqual: mutable } = assert; +mutable = foreignEqual; +mutable(error._tag, 'Missing'); +const { strictEqual: adt } = assert; +adt(error._tag, 'Failure'); +const { strictEqual: ordinary } = assert; +ordinary(error, error); +const { strictEqual: defaulted = foreignEqual } = assert; +defaulted(error._tag, 'Missing'); +const fixture = { _tag: 'Missing' }; +expect(error).toMatchObject({ message: 'Missing' }); +expect(error).toMatchObject({ _tag: 'Failure' }); +foreign(error).toMatchObject(fixture); +function shadowExpect(expect: typeof import('effect-rstest').expect) { + expect(error).toMatchObject({ _tag: 'Missing' }); +} +export { shadow, shadowExpect }; + +const field = '_tag'; +expect(error).toMatchObject({ field: 'Missing' }); + +expect(error).toEqual({ message: 'Missing' }); +expect(error).toStrictEqual({ _tag: 'Failure' }); +expect(error).toEqual({ field: 'Missing' }); +expect([error]).toContainEqual({ _tag: 'Some' }); +assert.deepEqual(error, { message: 'Missing' }); +assert.deepStrictEqual(error, { _tag: 'Failure' }); +assert.notDeepEqual(error, { field: 'Missing' }); +assert.notDeepStrictEqual(error, { _tag: 'None' }); +foreign(error).toEqual(fixture); +function shadowObjectExpect(expect: typeof import('effect-rstest').expect) { + expect(error).toStrictEqual({ _tag: 'Missing' }); +} +export { shadowObjectExpect }; diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/expected-wrappers.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/expected-wrappers.ts new file mode 100644 index 000000000..371086407 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/expected-wrappers.ts @@ -0,0 +1,41 @@ +import { expect } from 'effect-rstest'; +import { expect as check } from '@rstest/core'; +import { expect as foreign } from 'foreign-assertions'; +import assert from 'node:assert'; +declare const error: unknown; +expect(error).toEqual(expect.objectContaining({ _tag: 'Failure' })); +expect(error).toEqual(expect.arrayContaining([{ _tag: 'Some' }, { _tag: 'None' }])); +expect(error).toEqual(expect.arrayContaining([expect.objectContaining({ _tag: 'Left' })])); +expect(error).toEqual(expect.objectContaining({ payload: { _tag: 'Missing' } })); +expect(error).toEqual(expect.objectContaining({ payload: expect.objectContaining({ _tag: 'Missing' }) })); +expect(error).toEqual(expect.arrayContaining([{ payload: { _tag: 'Missing' } }])); +expect(error).toEqual([{ _tag: 'Missing' }]); +expect(error).toEqual(expect.objectContaining([{ _tag: 'Missing' }])); +expect(error).toEqual(expect.arrayContaining({ _tag: 'Missing' })); +expect(error).toEqual(foreign.objectContaining({ _tag: 'Missing' })); +expect(error).toEqual(assert.objectContaining({ _tag: 'Missing' })); +const fake = { objectContaining: (value: unknown) => value }; +expect(error).toEqual(fake.objectContaining({ _tag: 'Missing' })); +function shadow(expect: typeof import('effect-rstest').expect) { + check(error).toEqual(expect.objectContaining({ _tag: 'Missing' })); +} +function shadowHelper(objectContaining: (value: unknown) => unknown) { + expect(error).toEqual(objectContaining({ _tag: 'Missing' })); +} +let helper = expect.objectContaining; +helper = fake.objectContaining; +expect(error).toEqual(helper({ _tag: 'Missing' })); +let mutableExpect = expect; +mutableExpect = foreign; +expect(error).toEqual(mutableExpect.objectContaining({ _tag: 'Missing' })); +const cycle = other; +const other = cycle; +expect(error).toEqual(expect.arrayContaining(cycle)); +const recursive = [recursive]; +expect(error).toEqual(expect.arrayContaining(recursive)); +expect(error).toEqual(expect.objectContaining()); +expect(error).toEqual(expect.arrayContaining([])); +export { shadow, shadowHelper }; + +expect(error).toEqual(expect.customContaining({ _tag: 'Missing' })); +expect(error).toEqual(expect(error).objectContaining({ _tag: 'Missing' })); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/mutated-expected-wrappers.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/mutated-expected-wrappers.ts new file mode 100644 index 000000000..6523a6c5f --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/mutated-expected-wrappers.ts @@ -0,0 +1,36 @@ +import { expect } from 'effect-rstest'; +import { expect as changed } from '@rstest/core'; +import * as testing from 'vitest'; +import { expect as source } from '@jest/globals'; +import imported from 'expect'; +import { expect as typed } from 'vitest'; +import * as destructuredNamespace from '@rstest/core'; +import { expect as deleted } from '@jest/globals'; +declare const error: unknown; +const fake = (value: unknown) => value; +expect.objectContaining = fake; +expect(error).toEqual(expect.objectContaining({ _tag: 'Missing' })); +const { objectContaining } = expect; +expect(error).toEqual(objectContaining({ _tag: 'Missing' })); +const alias = changed; +alias.arrayContaining = fake; +expect(error).toEqual(changed.arrayContaining([{ _tag: 'Missing' }])); +testing.expect.objectContaining = fake; +expect(error).toEqual(testing.expect.objectContaining({ _tag: 'Missing' })); +const { expect: destructured } = testing; +expect(error).toEqual(destructured.objectContaining({ _tag: 'Missing' })); +const { objectContaining: detached } = source; +source.objectContaining = fake; +expect(error).toEqual(detached({ _tag: 'Missing' })); +imported = expect; +expect(error).toEqual(imported.objectContaining({ _tag: 'Missing' })); + +const typedAlias = typed as typeof typed; +typedAlias.arrayContaining = fake; +expect(error).toEqual(typed.arrayContaining([{ _tag: 'Missing' }])); + +const { expect: destructuredAlias } = destructuredNamespace; +destructuredAlias.arrayContaining = fake; +expect(error).toEqual(destructuredNamespace.expect.arrayContaining([{ _tag: 'Missing' }])); +delete deleted.objectContaining; +expect(error).toEqual(deleted.objectContaining({ _tag: 'Missing' })); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/mutated-negated-wrappers.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/mutated-negated-wrappers.ts new file mode 100644 index 000000000..90d037072 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/mutated-negated-wrappers.ts @@ -0,0 +1,16 @@ +import { expect } from 'effect-rstest'; +import { expect as changed } from '@rstest/core'; +import * as testing from 'vitest'; +import { expect as deleted } from '@jest/globals'; +declare const error: unknown; +const fake = (value: unknown) => value; +expect.not.objectContaining = fake; +expect(error).toEqual(expect.not.objectContaining({ _tag: 'Missing' })); +const negative = changed.not; +negative.arrayContaining = fake; +expect(error).toEqual(changed.not.arrayContaining([{ _tag: 'Missing' }])); +const { not: alias } = testing.expect; +alias.objectContaining = fake; +expect(error).toEqual(testing.expect.not.objectContaining({ _tag: 'Missing' })); +delete deleted.not; +expect(error).toEqual(deleted.not.objectContaining({ _tag: 'Missing' })); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/native-assertions.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/native-assertions.ts new file mode 100644 index 000000000..4c0739b09 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/native-assertions.ts @@ -0,0 +1,25 @@ +import { Predicate, Schema, Exit, Match, Struct } from 'effect'; +import assert from 'node:assert/strict'; +assert.ok(Predicate.isTagged(error, 'Missing')); +assert.equal(Exit.isFailure(exit), true); +assert.ok(Schema.is(Missing)(error)); +assert.deepEqual(Struct.omit(error, ['_tag']), { message: 'gone' }); +assert.fail(`Unexpected failure ${error._tag}`); +expect(Predicate.isTagged(error, 'Missing')).toBe(true); + +function unrelatedCallback(equal: (actual: unknown, expected: unknown) => void) { + equal(error._tag, 'Missing'); +} + +router.match('/failure', () => log(error._tag)); +comparison.equal(error._tag, 'Missing'); +function shadowedAssertion(assert: typeof import('node:assert/strict')) { + assert.equal(error._tag, 'Missing'); +} +function shadowedExpectation(expect: (value: unknown) => { toBe: (expected: unknown) => void }) { + expect(error._tag).toBe('Missing'); +} + +const readTag = () => error._tag; +assert.strictEqual(readTag, readTag); +assert.equal(log(error._tag), undefined); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/negated-expected-wrappers.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/negated-expected-wrappers.ts new file mode 100644 index 000000000..2441e88e5 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/negated-expected-wrappers.ts @@ -0,0 +1,17 @@ +import { expect } from 'effect-rstest'; +import { expect as check } from '@rstest/core'; +import { expect as foreign } from 'foreign-assertions'; +import assert from 'node:assert'; +declare const error: unknown; +expect(error).toEqual(expect.not.objectContaining({ _tag: 'Failure' })); +expect(error).toEqual(expect.not.arrayContaining([{ _tag: 'Some' }, { _tag: 'None' }])); +expect(error).toEqual(expect.not.objectContaining({ payload: { _tag: 'Missing' } })); +expect(error).toEqual(foreign.not.objectContaining({ _tag: 'Missing' })); +expect(error).toEqual(assert.not.objectContaining({ _tag: 'Missing' })); +expect(error).toEqual(expect.not.not.objectContaining({ _tag: 'Missing' })); +expect(error).toEqual(expect.resolves.objectContaining({ _tag: 'Missing' })); +expect(error).toEqual(expect.not.customContaining({ _tag: 'Missing' })); +function shadow(expect: typeof import('effect-rstest').expect) { + check(error).toEqual(expect.not.objectContaining({ _tag: 'Missing' })); +} +export { shadow }; diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/shared-contracts/src/edge-lookalikes.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/shared-contracts/src/edge-lookalikes.ts index 400b411b7..1075c16c0 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/shared-contracts/src/edge-lookalikes.ts +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/shared-contracts/src/edge-lookalikes.ts @@ -25,7 +25,6 @@ export class Holder { } /** Comparing two discriminants is identity, not case analysis — including through computed access. */ -export const same = (a: Failure, b: Failure): boolean => a._tag === b?.["_tag"]; /** Effect's own ADT tags belong to `no-raw-effect-adt-tag-check`. */ export const adt = (value: Failure): boolean => value._tag === "Some" || value._tag !== "Failure"; diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/shared-contracts/src/indirect-lookalikes.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/shared-contracts/src/indirect-lookalikes.ts index 28083cb00..96c71a54e 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/shared-contracts/src/indirect-lookalikes.ts +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/shared-contracts/src/indirect-lookalikes.ts @@ -54,7 +54,6 @@ export const isAdt = (value: { readonly _tag: string }): boolean => ADT_TAGS.includes(value._tag) || ["Success", "Failure"].includes(value._tag); /** `Object.is` between two discriminants is identity, not case analysis. */ -export const sameTag = (a: Failure, b: Failure): boolean => Object.is(a._tag, b._tag); /** A shadowed `Object` global is not the shape probe. */ export const shadowedGlobal = (value: { readonly _tag: string }): boolean => { diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-dynamic-extend.spec.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-dynamic-extend.spec.ts new file mode 100644 index 000000000..79afca92e --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-dynamic-extend.spec.ts @@ -0,0 +1,11 @@ +// expect-count: 1 +import { test as base } from '@playwright/test'; + +declare const method: string; +const test = base[method]({}); +async function ownedService() { + return 'owned'; +} +test('unknown factory', async () => { + await ownedService(); +}); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-extend-owned-service.spec.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-extend-owned-service.spec.ts new file mode 100644 index 000000000..d05e3f6cb --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-extend-owned-service.spec.ts @@ -0,0 +1,14 @@ +// expect-count: 2 +import { test as base } from '@playwright/test'; +import { test as unitTest } from 'effect-rstest'; + +const test = base.extend({}); +async function ownedService() { + return 'owned'; +} +test('browser caller', async () => { + await ownedService(); +}); +unitTest('owned async program', async () => { + await ownedService(); +}); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-fake-factory.spec.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-fake-factory.spec.ts new file mode 100644 index 000000000..0cb753042 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-fake-factory.spec.ts @@ -0,0 +1,10 @@ +// expect-count: 1 +import { test as base } from './fake-playwright'; + +const test = base.extend({}).extend({}); +async function ownedService() { + return 'owned'; +} +test('fake factory', async () => { + await ownedService(); +}); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-mutable-extend.spec.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-mutable-extend.spec.ts new file mode 100644 index 000000000..5603f3884 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-mutable-extend.spec.ts @@ -0,0 +1,11 @@ +// expect-count: 1 +import { test as base } from '@playwright/test'; + +let test = base.extend({}); +test = anotherTest; +async function ownedService() { + return 'owned'; +} +test('mutable factory', async () => { + await ownedService(); +}); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-shadowed.spec.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-shadowed.spec.ts new file mode 100644 index 000000000..92f071c1f --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-shadowed.spec.ts @@ -0,0 +1,14 @@ +// expect-count: 1 +import { test as base } from '@playwright/test'; + +async function ownedService() { + return 'owned'; +} + +function register(base: any) { + const test = base.extend({}); + test('shadowed factory', async () => { + await ownedService(); + }); +} +register({ extend: () => () => {} }); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/rstest-extend.spec.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/rstest-extend.spec.ts new file mode 100644 index 000000000..48b651ad4 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/rstest-extend.spec.ts @@ -0,0 +1,10 @@ +// expect-count: 1 +import { test as base } from '@rstest/core'; + +const test = base.extend({}); +async function ownedService() { + return 'owned'; +} +test('not Playwright', async () => { + await ownedService(); +}); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/async-test-callback.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/async-test-callback.test.ts new file mode 100644 index 000000000..9e6549448 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/async-test-callback.test.ts @@ -0,0 +1,13 @@ +// expect-count: 6 +import { it as check, test } from 'effect-rstest'; +import * as suite from 'effect-rstest'; +import { test as rawTest } from '@rstest/core'; +import { it as vitestIt } from 'vitest'; +import { test as nodeTest } from 'node:test'; + +test('plain callback', async () => {}); +check.live('live callback', async () => {}); +suite.test.each([1])('parameterized callback', async () => {}); +rawTest('raw callback', async () => {}); +vitestIt('vitest callback', async () => {}); +nodeTest('node callback', async () => {}); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/describe-wrapped-test-callback.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/describe-wrapped-test-callback.test.ts new file mode 100644 index 000000000..ca3cc6077 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/describe-wrapped-test-callback.test.ts @@ -0,0 +1,26 @@ +// expect-count: 7 +import { describeWrapped, describeWrapped as describeSuite } from 'effect-rstest'; +import * as suite from 'effect-rstest'; +import { Layer } from 'effect'; + +describeWrapped('suite', (it) => { + it('async callback', async () => {}); + it('inferred Promise callback', () => Promise.resolve()); + describeWrapped('nested wrapper', (nestedIt) => { + nestedIt('nested wrapper async callback', async () => {}); + }); + it.layer(Layer.empty)('nested layer', (nestedIt) => { + nestedIt('nested async callback', async () => {}); + }); +}); +describeSuite('named alias', (it) => { + const check = it; + check('local test alias', async () => {}); +}); +suite.describeWrapped('namespace', function (it) { + it('function expression suite', async () => {}); +}); +const localDescribe = suite.describeWrapped; +localDescribe('local suite alias', ((it) => { + it('wrapped callback', (async () => {}) satisfies () => unknown); +}) satisfies Parameters[1]); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/effect-rstest-hook-provenance.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/effect-rstest-hook-provenance.test.ts new file mode 100644 index 000000000..12432291d --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/effect-rstest-hook-provenance.test.ts @@ -0,0 +1,33 @@ +// expect-count: 10 +import { beforeAll, it, test } from 'effect-rstest'; +import * as runner from 'effect-rstest'; +import { beforeAll as fakeHook } from './owned-runner'; + +const fakeSetup = async () => { await initialize(); }; +fakeHook(fakeSetup); + +const shadowSetup = async () => { await initialize(); }; +function shadowed(beforeAll: (setup: unknown) => void) { + beforeAll(shadowSetup); +} + +let mutableHook = beforeAll; +mutableHook = fakeHook; +const mutableSetup = async () => { await initialize(); }; +mutableHook(mutableSetup); + +const mixedSetup = async () => { await initialize(); }; +beforeAll(mixedSetup); +ownedService(mixedSetup); + +it.effect('owned program', async () => { await initialize(); }); +test.effect('owned test program', async () => { await initialize(); }); +runner.it.effect('namespace program', async () => { await initialize(); }); +const namedProgram = async () => { await initialize(); }; +it.effect('named owned program', namedProgram); + +export const ownedTestService = async () => { await initialize(); }; +beforeAll(ownedTestService); + +const fakeMemberSetup = async () => { await initialize(); }; +runner.beforeAll.owned(fakeMemberSetup); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/inferred-promise-callback.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/inferred-promise-callback.test.ts new file mode 100644 index 000000000..fe69f6b91 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/inferred-promise-callback.test.ts @@ -0,0 +1,18 @@ +// expect-count: 7 +import { it } from 'effect-rstest'; +import { Effect } from 'effect'; + +const complete = () => Promise.resolve(); +it('local Promise factory', () => complete()); +it('local Promise value', () => { + const result = Promise.resolve(); + return result; +}); +it('resolved Promise', () => Promise.resolve()); +it('block return', () => { + const value = 1; + return Promise.resolve(value); +}); +it('global Promise', () => globalThis.Promise.all([])); +it('Effect runner', () => Effect.runPromise(Effect.void)); +it('Promise constructor', () => new Promise((resolve) => resolve(undefined))); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/layer-test-callback.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/layer-test-callback.test.ts new file mode 100644 index 000000000..529f2d7f6 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/layer-test-callback.test.ts @@ -0,0 +1,13 @@ +// expect-count: 3 +import { it, layer } from 'effect-rstest'; +import { Layer } from 'effect'; + +it.layer(Layer.empty)('suite', (suiteIt) => { + suiteIt('owned callback', async () => {}); + suiteIt.layer(Layer.empty)('nested', (nestedIt) => { + nestedIt('nested callback', async () => {}); + }); +}); +layer(Layer.empty)('standalone', (suiteIt) => { + suiteIt('owned callback', async () => {}); +}); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/db/evasion-then-built-store.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/db/evasion-then-built-store.ts index 91ee94b3e..83db282e9 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/db/evasion-then-built-store.ts +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/db/evasion-then-built-store.ts @@ -1,4 +1,4 @@ -// expect-count: 4 +// expect-count: 5 /** Evasion: the Promise-shaped service record is built anywhere inside a driver-callback subtree, * which `atDriverEdge` blesses to an unbounded depth. Only the callback itself is forced. */ const ready = Promise.resolve({ delete: (_id: string) => Promise.resolve(), load: (_id: string) => Promise.resolve("x") }); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/src/edge-callback-parameters.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/edge-callback-parameters.ts similarity index 83% rename from app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/src/edge-callback-parameters.ts rename to app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/edge-callback-parameters.ts index 80a6163e4..30c9d3732 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/src/edge-callback-parameters.ts +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/edge-callback-parameters.ts @@ -1,4 +1,5 @@ -/** Promise-shaped *callback parameters* are third-party continuations, not first-party ports. */ +// expect-count: 6 +/** First-party callback contracts must accept Effect operations. */ export function withRetry(run: (attempt: number) => Promise): void { void run; } diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/generic-interface-heritage.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/generic-interface-heritage.ts new file mode 100644 index 000000000..ed7e3fd5e --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/generic-interface-heritage.ts @@ -0,0 +1,32 @@ +// expect-count: 14 +type Service = { run: () => T }; +interface Port extends Service> {} +interface Base { run(): T; } +interface Middle extends Base {} +interface Deep extends Middle {} +declare const service: Deep>; +type Applied = Deep>; +export type ExportedPort = Service>; +interface AppliedInterface extends Deep> {} +interface Defaults extends Base {} +declare const defaults: Defaults>; +interface ConcreteDefault> extends Base {} +declare const defaulted: ConcreteDefault; +interface Partial extends Base { sync(): void; } +interface Container { port: Partial>; } +type Alias = Deep; +declare const aliased: Alias>; +interface Multiple extends Empty, Deep {} +interface Empty {} +declare const multiple: Multiple>; + +// A cyclic branch must not hide a separate Promise-returning base. +interface Cyclic extends Cyclic, Base {} +declare const cyclicPort: Cyclic>; +namespace Lexical { + type Service = { sync: T }; + interface Local extends Service {} +} +interface Outside extends Service> {} +interface ShadowedParameter extends Base {} +declare const shadowedParameter: ShadowedParameter>; diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/generic-operation-ports.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/generic-operation-ports.ts new file mode 100644 index 000000000..2859026d7 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/generic-operation-ports.ts @@ -0,0 +1,34 @@ +// expect-count: 21 +import type { Effect } from 'effect'; +type Operation = PromiseLike | Effect.Effect; +interface Constrained = Operation> { run(): R; } +interface Direct> { run(): R; } +type Identity = T; +interface Substituted { run(): Identity>; } +type Default> = T; +interface Defaulted { run(): Default; } +type Alias = Identity; +interface Nested { run(): Alias>; } +export declare function execute>(operation: () => R): R; + +type Callback = () => T; +interface AppliedCallback { run: Callback>; } +interface AppliedThenableCallback { run: Callback>; } +class AppliedField { declare run: Callback>; } +declare const appliedBinding: Callback>; +declare function register(operation: Callback>): void; +type NestedCallback = Callback; +interface NestedCallbackPort { run: NestedCallback>; } +interface CallbackFactory { make(): Callback>; } +type DefaultCallback> = () => T; +interface DefaultCallbackPort { run: DefaultCallback; } + +type Service = { run: () => T }; +declare const service: Service>; +interface ServicePort { service: Service>; } +type MethodService = { run(): T }; +declare const methodService: MethodService>; +interface GenericService { run: () => T; } +declare const genericService: GenericService>; +type NestedService = { inner: { run: () => T } }; +declare const nestedService: NestedService>; diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/unused-recursive-operation.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/unused-recursive-operation.ts new file mode 100644 index 000000000..fc8ea2aec --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/unused-recursive-operation.ts @@ -0,0 +1,3 @@ +// expect-count: 1 +const loop = async (): Promise => { await loop(); }; +void loop; diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/tests/unit/store.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/tests/unit/store.test.ts similarity index 70% rename from app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/tests/unit/store.test.ts rename to app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/tests/unit/store.test.ts index dde9eb6c0..0fef7e09d 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/tests/unit/store.test.ts +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/tests/unit/store.test.ts @@ -1,4 +1,4 @@ -/** Tests are excluded by default (includeTests: false). */ +// expect-count: 2 export interface FakeStore { readonly load: () => Promise; } diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/tests/unit/test-local-ports.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/tests/unit/test-local-ports.test.ts new file mode 100644 index 000000000..747e3a349 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/tests/unit/test-local-ports.test.ts @@ -0,0 +1,11 @@ +// expect-count: 2 +import test from 'node:test'; + +test('a runner callback does not turn its local service into a framework boundary', () => { + const load: () => Promise = () => Promise.resolve(); + const service = { + save: async () => {}, + }; + void load; + void service; +}); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/example/tests/promise-round-trip.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/example/tests/promise-round-trip.test.ts new file mode 100644 index 000000000..1afcb1fc0 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/example/tests/promise-round-trip.test.ts @@ -0,0 +1,32 @@ +// expect-count: 6 +import { expect, it } from 'effect-rstest'; +import * as rstest from '@rstest/core'; +import { Cause, Effect } from 'effect'; +import * as E from 'effect'; +import { promise as adapt, runPromise as run } from 'effect/Effect'; +import { runBrowserEffect } from '../browser-runtime'; + +const check = expect; +const execute = run; +const promise = Effect.promise; + +it.effect('browser runtime assertion', () => + Effect.promise(() => expect(runBrowserEffect(Effect.succeed('ready'))).resolves.toBe('ready')), +); +it.effect('direct runner', () => Effect.promise(() => Effect.runPromise(Effect.succeed('ready')))); +it.effect('runner aliases', () => adapt(() => execute(Effect.succeed('ready')))); +it.effect('object tryPromise and namespace', () => + E.Effect.tryPromise({ + try: () => E.Effect['runPromiseExit'](Effect.succeed('ready')), + catch: (cause) => new Cause.UnknownError(cause), + }), +); +it.effect('assertion alias and transparent wrappers', () => + (promise!)(() => (check!(runBrowserEffect(Effect.succeed('ready'))) as any)['resolves'].toBe('ready')), +); +it.effect('Rstest namespace rejects', () => + Effect.tryPromise({ + try: () => rstest.expect(runBrowserEffect(Effect.fail('failed')))[`rejects`].toBe('failed'), + catch: (cause) => new Cause.UnknownError(cause), + }), +); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/scripts/overloaded-operation.mts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/scripts/overloaded-operation.mts new file mode 100644 index 000000000..52bc565f7 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/scripts/overloaded-operation.mts @@ -0,0 +1,5 @@ +// expect-count: 2 +import { Effect } from 'effect'; +export function execute(): Promise; +export function execute(): Effect.Effect; +export function execute(): Promise | Effect.Effect { return Effect.succeed(1); } diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/tools/example/tests/async-tooling.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/tools/example/tests/async-tooling.test.ts new file mode 100644 index 000000000..e76f4170e --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/tools/example/tests/async-tooling.test.ts @@ -0,0 +1,4 @@ +// expect-count: 1 +import { it } from 'effect-rstest'; + +it('owned tooling callback', async () => {}); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/verticals/contacts/src/routes/edge-generic-jsx.tsx b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/verticals/contacts/src/routes/edge-generic-jsx.tsx similarity index 76% rename from app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/verticals/contacts/src/routes/edge-generic-jsx.tsx rename to app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/verticals/contacts/src/routes/edge-generic-jsx.tsx index b06fa084a..eb2ca1c9a 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/verticals/contacts/src/routes/edge-generic-jsx.tsx +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/verticals/contacts/src/routes/edge-generic-jsx.tsx @@ -1,4 +1,4 @@ -/** .tsx: JSX, generic arrows and Promise props are the framework adapter surface (includeTsx: false). */ +// expect-count: 2 const identity = (value: T): T => value; export interface PageProps { diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/verticals/contacts/src/routes/page.tsx b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/verticals/contacts/src/routes/page.tsx similarity index 68% rename from app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/verticals/contacts/src/routes/page.tsx rename to app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/verticals/contacts/src/routes/page.tsx index aa4167183..c025999c6 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/verticals/contacts/src/routes/page.tsx +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/verticals/contacts/src/routes/page.tsx @@ -1,4 +1,4 @@ -/** .tsx is the framework adapter surface (includeTsx: false). */ +// expect-count: 1 export const loader = async () => await Promise.resolve({ ok: true }); export interface PageProps { diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/api/modules/fp-third-party-sdk-mirror.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/api/modules/fp-third-party-sdk-mirror.ts index 845e0e171..8302026e0 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/api/modules/fp-third-party-sdk-mirror.ts +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/api/modules/fp-third-party-sdk-mirror.ts @@ -47,7 +47,7 @@ export const verticalClients: readonly ApprovedVerticalPageClient[] = [ { componentKey: "contacts.core.page-contacts", load: async () => await import("node:os") }, ]; -/** A structural narrowing of the Drizzle query builder, converted once by `attempt`. */ +/** A structural narrowing of the Drizzle query builder, converted at the SDK call. */ interface CustomerInsertTransaction { readonly insert: (table: string) => { readonly values: (values: { readonly name: string }) => { @@ -56,8 +56,8 @@ interface CustomerInsertTransaction { }; } -const attempt = (operation: () => PromiseLike) => - Effect.tryPromise({ catch: () => "contacts_persistence_unavailable" as const, try: operation }); - export const createCustomer = (transaction: CustomerInsertTransaction, name: string) => - attempt(() => transaction.insert("customers").values({ name }).returning()); + Effect.tryPromise({ + catch: () => "contacts_persistence_unavailable" as const, + try: () => transaction.insert("customers").values({ name }).returning(), + }); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/async-test-boundaries.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/async-test-boundaries.test.ts new file mode 100644 index 000000000..280003734 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/async-test-boundaries.test.ts @@ -0,0 +1,17 @@ +import { it, test, rstest } from 'effect-rstest'; +import { test as browserTest } from '@playwright/test'; +import { Effect } from 'effect'; + +it.effect('Effect callback', () => Effect.succeed('ready')); +it.live('Promise boundary', () => Effect.promise(async () => 'ready')); +test('SDK mock', () => { + const sdk = rstest.fn(async () => 'ready'); + sdk(); +}); +browserTest('browser contract', async ({ page }) => { + await page.goto('/'); +}); +// Spelling alone is not evidence of an imported test registration. +function unrelated(test: (name: string, callback: () => void) => void) { + test('foreign callback', async () => {}); +} diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/describe-wrapped-test-callback.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/describe-wrapped-test-callback.test.ts new file mode 100644 index 000000000..13ed82e35 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/describe-wrapped-test-callback.test.ts @@ -0,0 +1,30 @@ +import { describeWrapped, describeWrapped as describeSuite } from 'effect-rstest'; +import * as suite from 'effect-rstest'; +import { Effect, Layer } from 'effect'; + +describeWrapped('suite', (it) => { + it('synchronous callback', () => {}); + it.effect('Effect callback', () => Effect.void); + it.layer(Layer.empty)('nested layer', (nestedIt) => { + nestedIt.effect('nested Effect callback', () => Effect.void); + }); + describeSuite('nested wrapper', (nestedIt) => { + nestedIt.effect('nested wrapper Effect callback', () => Effect.void); + }); +}); +const localDescribe = suite.describeWrapped; +localDescribe('local alias', ((it) => { + const check = it; + check.effect('aliased Effect callback', () => Effect.void); +}) satisfies Parameters[1]); +function unrelated(callbacks: { + describeWrapped: ( + name: string, + callback: (it: (name: string, callback: () => unknown) => void) => void, + ) => void; +}) { + callbacks.describeWrapped('ordinary callback object', (it) => { + it('unrelated async callback', async () => {}); + }); +} +void unrelated; diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/effect-rstest-hooks.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/effect-rstest-hooks.test.ts new file mode 100644 index 000000000..64869aaea --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/effect-rstest-hooks.test.ts @@ -0,0 +1,24 @@ +import { beforeAll, afterAll as teardown, beforeEach, afterEach } from 'effect-rstest'; +import * as runner from 'effect-rstest'; + +const setup = async () => { await initialize(); }; +beforeAll(setup); +const cleanup = async () => { await release(); }; +teardown(cleanup); +const prepare = async () => { await initialize(); }; +beforeEach(prepare); +const reset = async () => { await release(); }; +afterEach(reset); + +const register = runner.beforeAll; +const aliasedSetup = async () => { await initialize(); }; +register(aliasedSetup); +const namespaceCleanup = async () => { await release(); }; +runner.afterAll(namespaceCleanup); +const namespacePrepare = async () => { await initialize(); }; +runner['beforeEach'](namespacePrepare); +const namespaceReset = async () => { await release(); }; +runner.afterEach(namespaceReset); + +beforeAll(async () => { await initialize(); }); +runner.afterAll(async () => { await release(); }); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/playwright-extended.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/playwright-extended.test.ts new file mode 100644 index 000000000..272466c7c --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/playwright-extended.test.ts @@ -0,0 +1,25 @@ +import { test as base } from '@playwright/test'; + +const alias = base; +const test = alias.extend<{ ready: boolean }>({ ready: true }); +const extended = test['extend']({}).extend({}); +const chained = base.extend({})[`extend`]({}); +const finalTest = chained; + +async function gotoHydratedLogin(page: { goto: (url: string) => Promise }) { + await page.goto('/login'); +} + +async function visit(page: { goto: (url: string) => Promise }) { + await gotoHydratedLogin(page); +} + +test('extended browser test', async ({ page }) => { + await visit(page); +}); +extended.beforeEach(async ({ page }) => { + await visit(page); +}); +finalTest('chained browser test', async ({ page }) => { + await Promise.all([page].map(async (browserPage) => gotoHydratedLogin(browserPage))); +}); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/src/db/driver-edge.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/src/db/driver-edge.ts index 0c13d21ed..d1d00e9a1 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/src/db/driver-edge.ts +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/src/db/driver-edge.ts @@ -1,12 +1,13 @@ /** The blessed driver edge: every Promise lives inside Effect.tryPromise / Effect.promise. */ import { Effect } from "effect"; +import { drizzle } from "drizzle-orm/node-postgres"; import * as Eff from "effect/Effect"; import * as E from "effect"; import { tryPromise } from "effect/Effect"; const executor = { insert: (rows: readonly string[]) => Promise.resolve(rows) }; const pool = { end: () => Promise.resolve() }; -const db = { transaction: (run: (tx: unknown) => Promise) => run({}) }; +const db = drizzle(); const decodeFailure = (cause: unknown) => cause; export const insertAll = (rows: readonly string[]) => diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/src/generic-effect-ports.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/src/generic-effect-ports.ts new file mode 100644 index 000000000..b7bc3e81e --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/src/generic-effect-ports.ts @@ -0,0 +1,29 @@ +import type { Effect } from 'effect'; +type Identity = T; +type Default> = T; +interface Service> { run(): R; } +interface Plain { run(): Identity; } +interface Native { run(): Identity>; } +interface NativeDefault { run(): Default; } +type Shadow = T; +interface Safe { run(): Shadow; } + +type Callback = () => T; +interface NativeCallback { run: Callback>; } +interface SynchronousCallback { run: Callback; } +declare const nativeCallback: Callback>; +declare function register(operation: Callback>): void; +type DefaultCallback> = () => T; +interface NativeDefaultCallback { run: DefaultCallback; } + +type Port = { run: () => T }; +declare const nativePort: Port>; +interface PortHolder { port: Port>; } +type MethodPort = { run(): T }; +declare const nativeMethodPort: MethodPort>; +interface GenericPort { run: () => T; } +declare const nativeGenericPort: GenericPort>; +type NestedPort = { inner: { run: () => T } }; +declare const nativeNestedPort: NestedPort>; +type PlainPort = { run: () => Effect.Effect }; +declare const plainPort: PlainPort; diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/src/generic-interface-heritage.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/src/generic-interface-heritage.ts new file mode 100644 index 000000000..4b704e68d --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/src/generic-interface-heritage.ts @@ -0,0 +1,43 @@ +import type { Effect } from 'effect'; +import type { Service as ForeignService } from 'foreign-sdk'; +type Service = { run: () => T }; +interface Base { run(): T; } +interface Middle extends Base {} +interface Synchronous extends Service {} +interface EffectPort extends Middle> {} +declare const effectPort: Middle>; +interface Foreign extends ForeignService> {} +namespace Shadowed { + interface Promise { value: T; } + interface Local extends Service> {} + declare const local: Middle>; +} +interface GenericPromise extends Service {} +interface Outer extends Service<{ run: () => T }> {} +declare const shadowedGeneric: Outer>; +interface Nested extends Service<{ value: T }> {} +declare const nestedNonPort: Nested>; +interface Recursive extends Recursive {} +declare const cycle: Recursive>; +interface Left extends Right {} +interface Right extends Left {} +declare const mutualCycle: Left>; +interface Defaults extends Base {} +declare const override: Defaults, void>; + +namespace Lexical { + type Service = { sync(): void }; + interface Local extends Service> {} + declare const local: Local; +} +interface InnerDefault { run(): R; } +interface Rebound extends InnerDefault {} +declare const rebound: Rebound>; +// The Promise is data nested below a function return, not a first-party operation. +interface Fluent extends Service<{ run(): T }> {} +declare const fluent: Fluent>; +interface GenericMethod extends Service<() => T> {} +declare const genericMethod: GenericMethod>; + +export type SynchronousAlias = Service; +export type EffectAlias = Service>; diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/example/tests/promise-round-trip.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/example/tests/promise-round-trip.test.ts new file mode 100644 index 000000000..5132ac79a --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/example/tests/promise-round-trip.test.ts @@ -0,0 +1,42 @@ +import { expect, it, rstest } from 'effect-rstest'; +import { Effect, Fiber } from 'effect'; +import { client } from 'external-sdk'; +import { expect as foreignExpect } from 'external-assertions'; +import { runBrowserEffect } from '../browser-runtime'; + +it.effect('SDK Promise adaptation', () => Effect.promise(() => client.request())); +it.effect('SDK mocks', () => { + const request = rstest.fn().mockResolvedValue('ready'); + return Effect.promise(() => request()); +}); +it.effect('foreign names', () => { + const foreign = { runPromise: () => client.request() }; + return Effect.promise(() => foreign.runPromise()); +}); +it.effect('foreign assertion', () => + Effect.promise(() => foreignExpect(client.request()).resolves.toBe('ready')), +); +it.effect('shadowed expect', () => { + const expect = foreignExpect; + return Effect.promise(() => expect(client.request()).rejects.toBe('failed')); +}); +it.effect('shadowed Effect runner', () => + Effect.promise(() => { + const Effect = client; + return Effect.runPromise(); + }), +); +it.effect('shadowed adapter', () => { + const Effect = { promise: (value: unknown) => value }; + return Effect.promise(() => expect(client.request()).resolves.toBe('ready')); +}); +it.effect('native assertions and Fiber', () => + Effect.gen(function* () { + const fiber = yield* Effect.forkChild(Effect.succeed('ready')); + expect(yield* Fiber.join(fiber)).toBe('ready'); + }), +); +// Cross-file runner provenance is intentionally not inferred from its name. +it.effect('opaque imported browser runtime', () => + Effect.promise(() => runBrowserEffect(Effect.succeed('ready'))), +); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/tools/example/tests/synchronous-tooling.test.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/tools/example/tests/synchronous-tooling.test.ts new file mode 100644 index 000000000..3bca399f4 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/tools/example/tests/synchronous-tooling.test.ts @@ -0,0 +1,23 @@ +import { it, layer } from 'effect-rstest'; +import { Effect, Layer } from 'effect'; +import { test } from '@playwright/test'; + +it('synchronous callback', () => {}); +it.effect('native program', () => Effect.void); +layer(Layer.empty)('suite', (suiteIt) => { + suiteIt('synchronous layer callback', () => {}); + suiteIt.effect('native layer program', () => Effect.void); +}); +it('shadowed Promise', () => { + const Promise = { resolve: () => 1 }; + return Promise.resolve(); +}); +it('unused nested Promise thunk', () => { + const thunk = () => Promise.resolve(); + void thunk; +}); +function registerLocally(suiteIt: (name: string, callback: () => void) => void) { + suiteIt('unrelated parameter', async () => {}); +} +void registerLocally; +test('SDK adapter', async () => {}); diff --git a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/verticals/contacts/src/effect-native-service.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/verticals/contacts/src/effect-native-service.ts index c8c88c8b5..58e117ef4 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/verticals/contacts/src/effect-native-service.ts +++ b/app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/verticals/contacts/src/effect-native-service.ts @@ -15,8 +15,8 @@ export const persistence: CustomerContactPersistence = { loadAll: () => Effect.succeed([]), }; -/** Callback parameters that accept a Promise-shaped third-party continuation are not ports. */ -export function withRetry(run: (attempt: number) => Promise): void { +/** First-party callbacks accept native Effect operations. */ +export function withRetry(run: (attempt: number) => Effect.Effect): void { void run; } diff --git a/app/tools/oxlint/effect-native/tests/json-rule-scope.test.mts b/app/tools/oxlint/effect-native/tests/json-rule-scope.test.mts index 716c54af7..d22b6c5b8 100644 --- a/app/tools/oxlint/effect-native/tests/json-rule-scope.test.mts +++ b/app/tools/oxlint/effect-native/tests/json-rule-scope.test.mts @@ -1,37 +1,29 @@ -import assert from 'node:assert/strict'; -import { test } from 'node:test'; +import { expect, it } from 'effect-rstest'; import { jsonExpressionSnippet } from '../shared/json-globals.ts'; import { inJsonRuleScope } from '../shared/json-rule-scope.ts'; +const sourceFile = 'apps/example/main.ts'; +const testFile = 'apps/example/main.test.ts'; + const includePaths = ['apps/**', 'scripts/**']; -test('JSON rule scope preserves defaults, overrides, and exclusions', () => { - assert.equal(inJsonRuleScope('/workspace/apps/example/main.ts', undefined, includePaths), true); - assert.equal(inJsonRuleScope('packages/example/main.ts', undefined, includePaths), false); - assert.equal(inJsonRuleScope('apps/example/main.test.ts', undefined, includePaths), false); - assert.equal( - inJsonRuleScope('apps/example/main.test.ts', { ignoreTestFiles: false }, includePaths), - true, - ); - assert.equal( - inJsonRuleScope('apps/example/main.ts', { allowPaths: ['apps/**'] }, includePaths), - false, - ); - assert.equal(inJsonRuleScope('apps/example/main.ts', { includePaths: [] }, includePaths), true); - assert.equal(inJsonRuleScope('apps/example/main.ts', { includePaths: [1] }, includePaths), true); - assert.equal( +it('JSON rule scope preserves defaults, overrides, and exclusions', () => { + expect(inJsonRuleScope('/workspace/apps/example/main.ts', undefined, includePaths)).toBe(true); + expect(inJsonRuleScope('packages/example/main.ts', undefined, includePaths)).toBe(false); + expect(inJsonRuleScope(testFile, undefined, includePaths)).toBe(false); + expect(inJsonRuleScope(testFile, { ignoreTestFiles: false }, includePaths)).toBe(true); + expect(inJsonRuleScope(sourceFile, { allowPaths: ['apps/**'] }, includePaths)).toBe(false); + expect(inJsonRuleScope(sourceFile, { includePaths: [] }, includePaths)).toBe(true); + expect(inJsonRuleScope(sourceFile, { includePaths: [1] }, includePaths)).toBe(true); + expect( inJsonRuleScope('packages/example/main.ts', { includePaths: ['packages/**'] }, includePaths), - true, - ); - assert.equal( - inJsonRuleScope('apps/example/main.test.ts', { ignoreTestFiles: 'false' }, includePaths), - false, - ); + ).toBe(true); + expect(inJsonRuleScope(testFile, { ignoreTestFiles: 'false' }, includePaths)).toBe(false); }); -test('JSON snippets retain whitespace normalization, boundary, and ASCII truncation', () => { - assert.equal(jsonExpressionSnippet(' JSON.stringify(\n value ) '), 'JSON.stringify( value )'); - assert.equal(jsonExpressionSnippet('x'.repeat(72)), 'x'.repeat(72)); - assert.equal(jsonExpressionSnippet('x'.repeat(73)), `${'x'.repeat(69)}...`); +it('JSON snippets retain whitespace normalization, boundary, and ASCII truncation', () => { + expect(jsonExpressionSnippet(' JSON.stringify(\n value ) ')).toBe('JSON.stringify( value )'); + expect(jsonExpressionSnippet('x'.repeat(72))).toBe('x'.repeat(72)); + expect(jsonExpressionSnippet('x'.repeat(73))).toBe(`${'x'.repeat(69)}...`); }); diff --git a/app/tools/oxlint/effect-native/tests/launcher.test.mts b/app/tools/oxlint/effect-native/tests/launcher.test.mts index 6effbcc92..db1da5ef5 100644 --- a/app/tools/oxlint/effect-native/tests/launcher.test.mts +++ b/app/tools/oxlint/effect-native/tests/launcher.test.mts @@ -1,57 +1,70 @@ -import assert from 'node:assert/strict'; -import childProcess from 'node:child_process'; +import { expect, it, rstest } from 'effect-rstest'; +import { Schema } from 'effect'; +import { spawnSync } from 'node:child_process'; import { readFileSync, writeFileSync } from 'node:fs'; -import { syncBuiltinESMExports } from 'node:module'; -import { join } from 'node:path'; -import { mock, test } from 'node:test'; -import { fileURLToPath } from 'node:url'; +import nodePath from 'node:path'; +import { createRequire } from 'node:module'; import { appRoot, runOxlint } from './oxlint.mts'; import { withTemporaryWorkspace } from './temporary-workspace.mts'; -test('Oxlint launches its JavaScript entry point through Node without a platform shim', () => { +const decodePackageScripts = Schema.decodeUnknownSync( + Schema.fromJsonString(Schema.Struct({ scripts: Schema.Record(Schema.String, Schema.String) })), +); + +rstest.mock('node:child_process', () => { + const original = process.getBuiltinModule('node:child_process'); + return { ...original, spawnSync: rstest.fn(original.spawnSync) }; +}); + +it('Oxlint launches its JavaScript entry point through Node without a platform shim', () => { withTemporaryWorkspace((directory) => { - const config = join(directory, 'lint config.json'); + const config = nodePath.join(directory, 'lint config.json'); const input = 'source with spaces.ts'; writeFileSync(config, JSON.stringify({ categories: { correctness: 'off' } })); - writeFileSync(join(directory, input), 'export const value = 1;'); - const spawn = mock.method(childProcess, 'spawnSync'); - syncBuiltinESMExports(); + writeFileSync(nodePath.join(directory, input), 'export const value = 1;'); + const spawn = rstest.mocked(spawnSync); + spawn.mockClear(); try { const run = runOxlint(config, [input], directory); - assert.equal(run.exitCode, 0); - assert.equal(run.numberOfFiles, 1); - assert.deepEqual(run.diagnostics, []); - assert.equal(spawn.mock.callCount(), 1); - const args: readonly unknown[] = spawn.mock.calls[0]!.arguments; - assert.equal(args[0], process.execPath); - assert.ok(Array.isArray(args[1])); - assert.equal( - args[1][0], - fileURLToPath(new URL('bin/oxlint', import.meta.resolve('oxlint/package.json'))), + expect(run.exitCode).toBe(0); + expect(run.numberOfFiles).toBe(1); + expect(run.diagnostics).toEqual([]); + expect(spawn.mock.calls.length).toBe(1); + const args: readonly unknown[] = spawn.mock.calls[0]; + expect(args[0]).toBe(process.execPath); + expect(Array.isArray(args[1])).toBe(true); + if (!Array.isArray(args[1])) { + throw new TypeError('Expected spawn arguments array'); + } + expect(args[1][0]).toBe( + nodePath.join( + nodePath.dirname(createRequire(import.meta.url).resolve('oxlint/package.json')), + 'bin/oxlint', + ), ); - assert.ok(args[1].includes(input)); - assert.ok(args[1].includes(config)); + expect(args[1].includes(input)).toBe(true); + expect(args[1].includes(config)).toBe(true); } finally { - spawn.mock.restore(); - syncBuiltinESMExports(); + spawn.mockClear(); } }); }); -test('lint and lint:fix cover the same directories without changing reporting-only commands', () => { - const { scripts } = JSON.parse(readFileSync(join(appRoot, 'package.json'), 'utf8')) as { - scripts: Record; - }; - const lint = scripts['lint']!.split(/\s+/u); - const fix = scripts['lint:fix']!.split(/\s+/u); - assert.deepEqual( - fix.filter((argument) => argument !== '--fix'), - lint, +it('lint and lint:fix cover the same directories without changing reporting-only commands', () => { + const { scripts } = decodePackageScripts( + readFileSync(nodePath.join(appRoot, 'package.json'), 'utf-8'), ); - assert.equal(fix.filter((argument) => argument === '--fix').length, 1); - assert.ok(lint.includes('scripts')); + expect(scripts.lint).toBeDefined(); + expect(scripts['lint:fix']).toBeDefined(); + const lint = (scripts.lint ?? '').split(/\s+/u); + const fix = (scripts['lint:fix'] ?? '').split(/\s+/u); + expect(fix.filter((argument) => argument !== '--fix')).toEqual(lint); + expect(fix.filter((argument) => argument === '--fix').length).toBe(1); + expect(lint.includes('scripts')).toBe(true); for (const name of ['lint', 'lint:effect', 'test:lint-rules', 'check']) { - assert.ok(!scripts[name]!.includes('--fix'), `${name} must remain reporting-only`); + expect(!(scripts[name] ?? '').includes('--fix'), `${name} must remain reporting-only`).toBe( + true, + ); } }); diff --git a/app/tools/oxlint/effect-native/tests/oxlint.mts b/app/tools/oxlint/effect-native/tests/oxlint.mts index 0666b7927..fe4fbd872 100644 --- a/app/tools/oxlint/effect-native/tests/oxlint.mts +++ b/app/tools/oxlint/effect-native/tests/oxlint.mts @@ -1,5 +1,6 @@ import { spawnSync } from 'node:child_process'; import { existsSync, readdirSync, statSync } from 'node:fs'; +import { createRequire } from 'node:module'; import { basename, dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -7,8 +8,10 @@ export const testsDirectory = dirname(fileURLToPath(import.meta.url)); export const pluginDirectory = resolve(testsDirectory, '..'); export const appRoot = resolve(pluginDirectory, '..', '..', '..'); export const fixturesDirectory = join(testsDirectory, 'fixtures'); -const oxlintEntryPoint = fileURLToPath( - new URL('bin/oxlint', import.meta.resolve('oxlint/package.json')), +// Rstest bundles this harness through Rspack, which has no `import.meta.resolve`. +const oxlintEntryPoint = join( + dirname(createRequire(import.meta.url).resolve('oxlint/package.json')), + 'bin/oxlint', ); interface Diagnostic { @@ -65,7 +68,9 @@ export function parseOxlintOutput(stdout: string, stderr: string, status: number try { parsed = JSON.parse(stdout); } catch (cause) { - throw new Error(`Oxlint did not return a JSON report:\n${stdout}`, { cause }); + throw new Error(`Oxlint did not return a JSON report:\n${stdout}`, { + cause, + }); } validateReport(parsed, stdout); for (const diagnostic of parsed.diagnostics) validateDiagnostic(diagnostic); @@ -105,7 +110,9 @@ export function runOxlint( }, ); if (result.error) - throw new Error(`Could not execute Oxlint: ${result.error.message}`, { cause: result.error }); + throw new Error(`Could not execute Oxlint: ${result.error.message}`, { + cause: result.error, + }); return parseOxlintOutput(result.stdout ?? '', result.stderr ?? '', result.status); } diff --git a/app/tools/oxlint/effect-native/tests/oxlint.test.mts b/app/tools/oxlint/effect-native/tests/oxlint.test.mts index 07e9a6ab3..b8aa8acbf 100644 --- a/app/tools/oxlint/effect-native/tests/oxlint.test.mts +++ b/app/tools/oxlint/effect-native/tests/oxlint.test.mts @@ -1,32 +1,31 @@ -import assert from 'node:assert/strict'; -import { test } from 'node:test'; +import { expect, it } from 'effect-rstest'; import { parseOxlintOutput } from './oxlint.mts'; const diagnostic = { code: 'effect-native(example)', filename: 'invalid/example.ts', + labels: [], message: 'Example violation', severity: 'error', - labels: [], }; const report = (diagnostics: unknown[] = [], files = 1) => JSON.stringify({ diagnostics, number_of_files: files }); -test('accepts a successful clean lint run', () => { +it('accepts a successful clean lint run', () => { const run = parseOxlintOutput(report(), '', 0); - assert.equal(run.numberOfFiles, 1); - assert.deepEqual(run.diagnostics, []); - assert.equal(run.exitCode, 0); + expect(run.numberOfFiles).toBe(1); + expect(run.diagnostics).toEqual([]); + expect(run.exitCode).toBe(0); }); -test('accepts actual lint failures as diagnostics, not a loader crash', () => { +it('accepts actual lint failures as diagnostics, not a loader crash', () => { const run = parseOxlintOutput(report([diagnostic]), '', 1); - assert.deepEqual(run.diagnostics, [diagnostic]); - assert.equal(run.exitCode, 1); + expect(run.diagnostics).toEqual([diagnostic]); + expect(run.exitCode).toBe(1); }); -test('rejects loader failures on stdout, including a JSON-looking suffix', () => { +it('rejects loader failures on stdout, including a JSON-looking suffix', () => { for (const stdout of [ 'Failed to load plugin', `Failed to load plugin\n${report()}`, @@ -34,11 +33,11 @@ test('rejects loader failures on stdout, including a JSON-looking suffix', () => '{bad', 'null', ]) { - assert.throws(() => parseOxlintOutput(stdout, '', 1)); + expect(() => parseOxlintOutput(stdout, '', 1)).toThrow(); } }); -test('rejects empty-file runs and missing report fields', () => { +it('rejects empty-file runs and missing report fields', () => { for (const stdout of [ report([], 0), report([], -1), @@ -46,25 +45,25 @@ test('rejects empty-file runs and missing report fields', () => { '{}', '{"diagnostics":[]}', ]) { - assert.throws(() => parseOxlintOutput(stdout, '', 0), /incomplete or empty-file/); + expect(() => parseOxlintOutput(stdout, '', 0)).toThrow(/incomplete or empty-file/u); } }); -test('rejects crashes, stderr failures, and inconsistent exit statuses', () => { - assert.throws(() => parseOxlintOutput(report(), '', null), /did not complete/); - assert.throws(() => parseOxlintOutput(report(), '', 2), /did not complete/); - assert.throws(() => parseOxlintOutput(report(), 'plugin crashed', 0), /stderr/); - assert.throws(() => parseOxlintOutput(report(), '', 1), /contradicts/); - assert.throws(() => parseOxlintOutput(report([diagnostic]), '', 0), /contradicts/); +it('rejects crashes, stderr failures, and inconsistent exit statuses', () => { + expect(() => parseOxlintOutput(report(), '', null)).toThrow(/did not complete/u); + expect(() => parseOxlintOutput(report(), '', 2)).toThrow(/did not complete/u); + expect(() => parseOxlintOutput(report(), 'plugin crashed', 0)).toThrow(/stderr/u); + expect(() => parseOxlintOutput(report(), '', 1)).toThrow(/contradicts/u); + expect(() => parseOxlintOutput(report([diagnostic]), '', 0)).toThrow(/contradicts/u); }); -test('rejects malformed diagnostics rather than hiding them', () => { +it('rejects malformed diagnostics rather than hiding them', () => { for (const entry of [ null, {}, { ...diagnostic, severity: 'unknown' }, { ...diagnostic, labels: null }, ]) { - assert.throws(() => parseOxlintOutput(report([entry]), '', 1), /malformed diagnostic/); + expect(() => parseOxlintOutput(report([entry]), '', 1)).toThrow(/malformed diagnostic/u); } }); diff --git a/app/tools/oxlint/effect-native/tests/paths.test.mts b/app/tools/oxlint/effect-native/tests/paths.test.mts index cd44c94b9..1d0c128ce 100644 --- a/app/tools/oxlint/effect-native/tests/paths.test.mts +++ b/app/tools/oxlint/effect-native/tests/paths.test.mts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import { test } from 'node:test'; +import { expect, it } from 'effect-rstest'; import { isScriptFile, normalisePath } from '../shared/paths.ts'; @@ -11,7 +10,7 @@ const scriptPaths = [ 'packages/core-runtime/scripts/postgres/verify.mts', ]; -test('script classification agrees for relative, absolute, and normalized workspace paths', () => { +it('script classification agrees for relative, absolute, and normalized workspace paths', () => { for (const path of scriptPaths) { const variants = [ path, @@ -21,13 +20,13 @@ test('script classification agrees for relative, absolute, and normalized worksp `\\\\server\\share\\app\\${path.replaceAll('/', '\\')}`, ]; for (const filename of variants) { - assert.equal(isScriptFile(filename), true, filename); - assert.equal(isScriptFile(normalisePath(filename)), true, filename); + expect(isScriptFile(filename), filename).toBe(true); + expect(isScriptFile(normalisePath(filename)), filename).toBe(true); } } }); -test('script classification requires a complete scripts directory segment', () => { +it('script classification requires a complete scripts directory segment', () => { for (const path of [ 'packages/core-runtime/src/verify.ts', 'apps/shell-super-app/src/scripts.ts', @@ -42,8 +41,8 @@ test('script classification requires a complete scripts directory segment', () = `/workspace/app/${path}`, `C:\\workspace\\app\\${path.replaceAll('/', '\\')}`, ]) { - assert.equal(isScriptFile(filename), false, filename); - assert.equal(isScriptFile(normalisePath(filename)), false, filename); + expect(isScriptFile(filename), filename).toBe(false); + expect(isScriptFile(normalisePath(filename)), filename).toBe(false); } } }); diff --git a/app/tools/oxlint/effect-native/tests/production-options.test.mts b/app/tools/oxlint/effect-native/tests/production-options.test.mts index deb310155..625d97c72 100644 --- a/app/tools/oxlint/effect-native/tests/production-options.test.mts +++ b/app/tools/oxlint/effect-native/tests/production-options.test.mts @@ -1,7 +1,7 @@ -import assert from 'node:assert/strict'; +import { expect, it } from 'effect-rstest'; +import { Schema } from 'effect'; import { cpSync, readFileSync } from 'node:fs'; -import { join, relative } from 'node:path'; -import { test } from 'node:test'; +import nodePath from 'node:path'; import { listRuleNames } from '../shared/discover-rules.ts'; import { globToRegExp } from '../shared/paths.ts'; @@ -14,44 +14,54 @@ import { } from './oxlint.mts'; import { withTemporaryWorkspace } from './temporary-workspace.mts'; +const RuleSetting = Schema.Union([Schema.String, Schema.Array(Schema.Unknown)]); +const RuleMap = Schema.Record(Schema.String, RuleSetting); +const FixtureConfig = Schema.fromJsonString( + Schema.Struct({ + overrides: Schema.optional( + Schema.Array(Schema.Struct({ files: Schema.Array(Schema.String), rules: RuleMap })), + ), + rules: RuleMap, + }), +); +const decodeFixtureConfig = Schema.decodeUnknownSync(FixtureConfig); + // Stage outside tools/**/tests: absolute fixture ancestors must not alter production scope. for (const rule of listRuleNames()) { - test(`effect-native/${rule} production settings report a positive fixture`, () => { + it(`effect-native/${rule} production settings report a positive fixture`, () => { withTemporaryWorkspace((directory) => { - for (const kind of ['invalid', 'valid']) - cpSync(join(fixturesDirectory, rule, kind), join(directory, kind), { recursive: true }); - const paths = listFilesRecursively(directory).map((file) => relative(directory, file)); + for (const kind of ['invalid', 'valid']) { + cpSync(nodePath.join(fixturesDirectory, rule, kind), nodePath.join(directory, kind), { + recursive: true, + }); + } + const paths = listFilesRecursively(directory).map((file) => + nodePath.relative(directory, file), + ); const run = runOxlint( - join(testsDirectory, 'production-fixture.config.ts'), + nodePath.join(testsDirectory, 'production-fixture.config.ts'), paths, directory, rule, ); - assert.equal( - run.numberOfFiles, - paths.length, - `${rule}: production run skipped fixture files`, - ); - assert.equal(run.exitCode, 1, `${rule}: production defaults must have a positive fixture`); - assert.ok( + expect(run.numberOfFiles, `${rule}: production run skipped fixture files`).toBe(paths.length); + expect(run.exitCode, `${rule}: production defaults must have a positive fixture`).toBe(1); + expect( run.diagnostics.some((diagnostic) => diagnostic.filename.startsWith('invalid/')), `${rule}: no positive production fixture`, - ); - for (const diagnostic of run.diagnostics) - assert.equal(diagnostic.code, `effect-native(${rule})`); - assert.deepEqual( + ).toBe(true); + for (const diagnostic of run.diagnostics) { + expect(diagnostic.code).toBe(`effect-native(${rule})`); + } + expect( run.diagnostics.filter( (diagnostic) => diagnostic.filename.startsWith('valid/') && diagnostic.filename.endsWith('/production-default.ts'), ), - [], `${rule}: explicit default negative reported`, - ); - const fixture: { - rules: Record; - overrides?: { files: string[]; rules: Record }[]; - } = JSON.parse(readFileSync(fixtureConfigPath(rule), 'utf8')); + ).toEqual([]); + const fixture = decodeFixtureConfig(readFileSync(fixtureConfigPath(rule), 'utf-8')); const key = `effect-native/${rule}`; if (fixture.rules[key] === 'error') { // Non-default option fixtures remain owned by the ordinary fixture suite. @@ -60,14 +70,13 @@ for (const rule of listRuleNames()) { (override) => key in override.rules && override.files.some((glob) => globToRegExp(glob).test(file)), ) ?? false; - assert.deepEqual( + expect( run.diagnostics.filter( (diagnostic) => diagnostic.filename.startsWith('valid/') && !usesOverride(diagnostic.filename), ), - [], `${rule}: production false positive`, - ); + ).toEqual([]); } }); }); diff --git a/app/tools/oxlint/effect-native/tests/registration.test.mts b/app/tools/oxlint/effect-native/tests/registration.test.mts index 720196cf2..fda91e4f0 100644 --- a/app/tools/oxlint/effect-native/tests/registration.test.mts +++ b/app/tools/oxlint/effect-native/tests/registration.test.mts @@ -1,77 +1,131 @@ -import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { test } from 'node:test'; +import { expect, it } from 'effect-rstest'; +import { Schema } from 'effect'; +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import nodePath from 'node:path'; import { pathToFileURL } from 'node:url'; -import type { Rule } from '@oxlint/plugins'; - +import plugin from '../index.ts'; import { listRuleNames } from '../shared/discover-rules.ts'; -import { appRoot, listFixtureRules, pluginDirectory } from './oxlint.mts'; +import { appRoot, listFixtureRules, pluginDirectory, runOxlint } from './oxlint.mts'; +import { withTemporaryWorkspace } from './temporary-workspace.mts'; -const pluginModule = await import(pathToFileURL(join(pluginDirectory, 'index.ts')).href); -const configModule = await import(pathToFileURL(join(appRoot, 'oxlint.config.ts')).href); -const plugin: { rules: Record } = pluginModule.default; -const config: { - rules: Record; - options: { typeAware?: boolean; typeCheck?: boolean; denyWarnings?: boolean }; - jsPlugins: unknown[]; -} = configModule.default; +const RuleSetting = Schema.Union([Schema.String, Schema.Array(Schema.Unknown)]); +// The production config is typed against oxlint's own definitions; only the fields this suite +// asserts on are decoded here, at the module boundary. +const ProductionConfigModule = Schema.Struct({ + default: Schema.Struct({ + jsPlugins: Schema.optional(Schema.Array(Schema.Unknown)), + options: Schema.optional( + Schema.Struct({ + denyWarnings: Schema.optional(Schema.Boolean), + typeAware: Schema.optional(Schema.Boolean), + typeCheck: Schema.optional(Schema.Boolean), + }), + ), + overrides: Schema.Array( + Schema.Struct({ + excludeFiles: Schema.optional(Schema.Array(Schema.String)), + files: Schema.Array(Schema.String), + rules: Schema.Record(Schema.String, RuleSetting), + }), + ), + rules: Schema.optional(Schema.Record(Schema.String, RuleSetting)), + }), +}); +const FixtureConfig = Schema.fromJsonString( + Schema.Struct({ + ignorePatterns: Schema.optional(Schema.Array(Schema.String)), + rules: Schema.Record(Schema.String, RuleSetting), + }), +); +const NamedPluginEntry = Schema.Struct({ name: Schema.String, specifier: Schema.String }); +const isNamedPluginEntry = Schema.is(NamedPluginEntry); +const decodeFixtureConfig = Schema.decodeUnknownSync(FixtureConfig); +const { default: config } = Schema.decodeUnknownSync(ProductionConfigModule)( + await import(pathToFileURL(nodePath.join(appRoot, 'oxlint.config.ts')).href), +); +const configuredRules = config.rules ?? {}; const rules = listRuleNames(); -test('every rule is actually exported, enabled at error severity, and covered by fixtures', () => { - assert.ok(rules.length > 0, 'the plugin cannot be empty'); - assert.deepEqual(Object.keys(plugin.rules).sort(), rules); - assert.deepEqual([...listFixtureRules()].sort(), rules); - const configured = Object.keys(config.rules).filter((name) => name.startsWith('effect-native/')); - assert.deepEqual( - configured.sort(), - rules.map((name) => `effect-native/${name}`), +it('every rule is actually exported, enabled at error severity, and covered by fixtures', () => { + expect(rules.length > 0, 'the plugin cannot be empty').toBe(true); + expect(Object.keys(plugin.rules).toSorted()).toEqual(rules); + expect([...listFixtureRules()].toSorted()).toEqual(rules); + const configured = Object.keys(configuredRules).filter((name) => + name.startsWith('effect-native/'), ); + expect(configured.toSorted()).toEqual(rules.map((name) => `effect-native/${name}`)); for (const rule of rules) { - const setting = config.rules[`effect-native/${rule}`]; - assert.equal( - Array.isArray(setting) ? setting[0] : setting, - 'error', - `${rule} must be an error`, - ); + const setting = configuredRules[`effect-native/${rule}`]; + expect(Array.isArray(setting) ? setting[0] : setting, `${rule} must be an error`).toBe('error'); } }); -test('production configuration loads the plugin and preserves strict typed linting', () => { - assert.ok( - config.jsPlugins.some( - (plugin) => - typeof plugin === 'object' && - plugin !== null && - 'name' in plugin && - plugin.name === 'effect-native' && - 'specifier' in plugin && - plugin.specifier === './tools/oxlint/effect-native/index.ts', +it('production configuration loads the plugin and preserves strict typed linting', () => { + expect( + (config.jsPlugins ?? []).some( + (entry) => + isNamedPluginEntry(entry) && + entry.name === 'effect-native' && + entry.specifier === './tools/oxlint/effect-native/index.ts', ), - ); - assert.equal(config.options.typeAware, true); - assert.equal(config.options.typeCheck, true); - assert.equal(config.options.denyWarnings, true); + ).toBe(true); + expect(config.options?.typeAware).toBe(true); + expect(config.options?.typeCheck).toBe(true); + expect(config.options?.denyWarnings).toBe(true); }); -test('every rule is reporting-only and declares diagnostic metadata', () => { +it('every rule is reporting-only and declares diagnostic metadata', () => { for (const [name, rule] of Object.entries(plugin.rules)) { - assert.ok(rule.meta, `${name} needs metadata`); - assert.ok(Object.keys(rule.meta.messages ?? {}).length > 0, `${name} needs messages`); - assert.equal(rule.meta.fixable, undefined, `${name} must not advertise fixes`); - assert.ok(!rule.meta.hasSuggestions, `${name} must not advertise suggestions`); + expect(rule.meta, `${name} needs metadata`).toBeDefined(); + if (rule.meta === undefined) { + throw new Error(`${name} needs metadata`); + } + expect(Object.keys(rule.meta.messages ?? {}).length > 0, `${name} needs messages`).toBe(true); + expect(rule.meta.fixable, `${name} must not advertise fixes`).toBe(undefined); + expect(!(rule.meta.hasSuggestions ?? false), `${name} must not advertise suggestions`).toBe( + true, + ); } }); -test('fixture configs enable only their owned rule without file-ignore shortcuts', () => { +it('fixture configs enable only their owned rule without file-ignore shortcuts', () => { for (const rule of rules) { - const fixture = JSON.parse( - readFileSync(join(pluginDirectory, 'tests', 'fixtures', rule, '.oxlintrc.json'), 'utf8'), + const fixture = decodeFixtureConfig( + readFileSync( + nodePath.join(pluginDirectory, 'tests', 'fixtures', rule, '.oxlintrc.json'), + 'utf-8', + ), ); - assert.deepEqual(Object.keys(fixture.rules), [`effect-native/${rule}`]); + expect(Object.keys(fixture.rules)).toEqual([`effect-native/${rule}`]); const setting = fixture.rules[`effect-native/${rule}`]; - assert.equal(Array.isArray(setting) ? setting[0] : setting, 'error'); - assert.ok(!fixture.ignorePatterns?.length, `${rule} must exercise fixtures, not ignore them`); + expect(Array.isArray(setting) ? setting[0] : setting).toBe('error'); + expect( + (fixture.ignorePatterns ?? []).length === 0, + `${rule} must exercise fixtures, not ignore them`, + ).toBe(true); } }); + +it('production import policy rejects node:test in application tests but not e2e adapters', () => { + withTemporaryWorkspace((directory) => { + const overrides = config.overrides.filter( + (override) => 'eslint/no-restricted-imports' in override.rules, + ); + expect(overrides.length).toBe(1); + const configPath = nodePath.join(directory, '.oxlintrc.json'); + writeFileSync(configPath, JSON.stringify({ categories: { correctness: 'off' }, overrides })); + const paths = ['apps/x/tests/y.test.ts', 'apps/x/tests/e2e/y.test.ts']; + for (const file of paths) { + const fullPath = nodePath.join(directory, file); + mkdirSync(nodePath.dirname(fullPath), { recursive: true }); + writeFileSync(fullPath, "import { test } from 'node:test';\ntest('example', () => {});\n"); + } + const run = runOxlint(configPath, paths, directory); + expect(run.numberOfFiles).toBe(2); + expect(run.exitCode).toBe(1); + expect(run.diagnostics.map(({ code, filename }) => ({ code, filename }))).toEqual([ + { code: 'eslint(no-restricted-imports)', filename: paths[0] }, + ]); + }); +}); diff --git a/app/tools/oxlint/effect-native/tests/repository-policy.test.mts b/app/tools/oxlint/effect-native/tests/repository-policy.test.mts new file mode 100644 index 000000000..8f3023fc9 --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/repository-policy.test.mts @@ -0,0 +1,17 @@ +import { expect, it } from 'effect-rstest'; +import nodePath from 'node:path'; +import { appRoot, pluginDirectory, runOxlint } from './oxlint.mts'; + +it('all repository source, including tools and root configuration, follows the Effect discrimination policy', () => { + const run = runOxlint( + nodePath.join(pluginDirectory, 'repository-policy.config.ts'), + ['.', '--ignore-pattern', 'tools/oxlint/**/tests/fixtures/**'], + appRoot, + ); + expect( + run.diagnostics.map( + ({ code, filename, labels }) => `${filename}:${labels[0]?.span.line} ${code}`, + ), + ).toEqual([]); + expect(run.exitCode).toBe(0); +}); diff --git a/app/tools/oxlint/effect-native/tests/rule-file-policy.test.mts b/app/tools/oxlint/effect-native/tests/rule-file-policy.test.mts index eacd93c28..c48ded90d 100644 --- a/app/tools/oxlint/effect-native/tests/rule-file-policy.test.mts +++ b/app/tools/oxlint/effect-native/tests/rule-file-policy.test.mts @@ -1,44 +1,51 @@ -import assert from 'node:assert/strict'; -import { test } from 'node:test'; +import { expect, it } from 'effect-rstest'; import { acceptsRuleFile, ruleFilePolicyProperties } from '../shared/rule-file-policy.ts'; -const policy = { include: ['packages/**'], ignore: [], ignoreTests: false }; +const sourceFile = 'packages/core/src/schema.ts'; -test('rule file policy keeps source and test files in scope by default', () => { - assert.equal(acceptsRuleFile('packages/core/src/schema.ts', policy), true); - assert.equal(acceptsRuleFile('packages/core/tests/schema.test.ts', policy), true); - assert.equal(acceptsRuleFile('apps/shell/src/schema.ts', policy), false); - assert.equal(acceptsRuleFile('packages/core/src/schema.ts', { ...policy, include: [] }), false); +const policy = { ignore: [], ignoreTests: false, include: ['packages/**'] }; + +it('rule file policy keeps source and test files in scope by default', () => { + expect(acceptsRuleFile(sourceFile, policy)).toBe(true); + expect(acceptsRuleFile('packages/core/tests/schema.test.ts', policy)).toBe(true); + expect(acceptsRuleFile('apps/shell/src/schema.ts', policy)).toBe(false); + expect(acceptsRuleFile(sourceFile, { ...policy, include: [] })).toBe(false); }); -test('rule file policy applies ignore and optional test exclusion', () => { - assert.equal( - acceptsRuleFile('packages/core/src/schema.ts', { ...policy, ignore: ['packages/core/**'] }), - false, - ); - assert.equal( - acceptsRuleFile('packages/core/tests/schema.test.ts', { ...policy, ignoreTests: true }), - false, - ); - assert.equal( - acceptsRuleFile('packages/core/src/schema.ts', { ...policy, ignoreTests: true }), - true, - ); +it('rule file policy applies ignore and optional test exclusion', () => { + expect( + acceptsRuleFile(sourceFile, { + ...policy, + ignore: ['packages/core/**'], + }), + ).toBe(false); + expect( + acceptsRuleFile('packages/core/tests/schema.test.ts', { + ...policy, + ignoreTests: true, + }), + ).toBe(false); + expect( + acceptsRuleFile(sourceFile, { + ...policy, + ignoreTests: true, + }), + ).toBe(true); }); -test('rule file policy normalizes absolute and fixture paths before filtering', () => { +it('rule file policy normalizes absolute and fixture paths before filtering', () => { for (const filename of [ '/workspace/app/packages/core/src/schema.ts', - 'C:\\workspace\\app\\packages\\core\\src\\schema.ts', + String.raw`C:\workspace\app\packages\core\src\schema.ts`, 'tools/oxlint/effect-native/tests/fixtures/no-nullable-schema-field/invalid/packages/core/src/schema.ts', ]) { - assert.equal(acceptsRuleFile(filename, policy), true, filename); + expect(acceptsRuleFile(filename, policy), filename).toBe(true); } }); -test('rule file policy exposes the existing JSON option schemas', () => { - assert.deepEqual(ruleFilePolicyProperties, { +it('rule file policy exposes the existing JSON option schemas', () => { + expect(ruleFilePolicyProperties).toStrictEqual({ ignore: { items: { type: 'string' }, type: 'array' }, ignoreTests: { type: 'boolean' }, include: { items: { type: 'string' }, type: 'array' }, diff --git a/app/tools/oxlint/effect-native/tests/scaffold-text-unicode.test.mts b/app/tools/oxlint/effect-native/tests/scaffold-text-unicode.test.mts index 59185c44a..9b8fc0a51 100644 --- a/app/tools/oxlint/effect-native/tests/scaffold-text-unicode.test.mts +++ b/app/tools/oxlint/effect-native/tests/scaffold-text-unicode.test.mts @@ -1,23 +1,21 @@ -import assert from 'node:assert/strict'; -import { test } from 'node:test'; +import { expect, it } from 'effect-rstest'; import { maskText } from '../shared/scaffold-text.ts'; -test('scaffold masking preserves UTF-16 offsets after astral string data', () => { +it('scaffold masking preserves UTF-16 offsets after astral string data', () => { const text = 'const icon = "😀😀😀😀😀"; throw new Error("bad");'; const masked = maskText(text, true); - assert.equal(masked.length, text.length); - assert.equal(masked.indexOf('throw'), text.indexOf('throw')); + expect(masked.length).toBe(text.length); + expect(masked.indexOf('throw')).toBe(text.indexOf('throw')); }); -test('scaffold masking preserves astral comment offsets and CRLF', () => { +it('scaffold masking preserves astral comment offsets and CRLF', () => { const text = '// 😀😀\r\n/* 😀\r\n😀 */ throw new Error("bad");'; const masked = maskText(text); - assert.equal(masked.length, text.length); - assert.equal(masked.indexOf('throw'), text.indexOf('throw')); - assert.deepEqual( - [...masked.matchAll(/\r\n/gu)].map((match) => match.index), + expect(masked.length).toBe(text.length); + expect(masked.indexOf('throw')).toBe(text.indexOf('throw')); + expect([...masked.matchAll(/\r\n/gu)].map((match) => match.index)).toStrictEqual( [...text.matchAll(/\r\n/gu)].map((match) => match.index), ); - assert.ok(masked.endsWith('throw new Error("bad");')); + expect(masked.endsWith('throw new Error("bad");')).toBeTruthy(); }); diff --git a/app/tools/oxlint/effect-native/tests/scaffold-unicode.test.mts b/app/tools/oxlint/effect-native/tests/scaffold-unicode.test.mts index 67e184a61..2a766ff3d 100644 --- a/app/tools/oxlint/effect-native/tests/scaffold-unicode.test.mts +++ b/app/tools/oxlint/effect-native/tests/scaffold-unicode.test.mts @@ -1,7 +1,7 @@ -import assert from 'node:assert/strict'; import { mkdirSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { test } from 'node:test'; +import path from 'node:path'; + +import { expect, it } from 'effect-rstest'; import { maskText } from '../shared/scaffold-text.ts'; import { runOxlint, testsDirectory } from './oxlint.mts'; @@ -9,41 +9,46 @@ import { withTemporaryWorkspace } from './temporary-workspace.mts'; const supplementaryCharacter = '\u{1F600}'; -test('scaffold masking preserves UTF-16 offsets and line endings', () => { +it('scaffold masking preserves UTF-16 offsets and line endings', () => { const source = `/* ${supplementaryCharacter}\r\n */ const label="${supplementaryCharacter.repeat(20)}"; process.env.X`; for (const strings of [false, true]) { const masked = maskText(source, strings); - assert.equal(masked.length, source.length); - assert.equal(masked.indexOf('\r\n'), source.indexOf('\r\n')); - assert.equal(masked.indexOf('process.env.X'), source.indexOf('process.env.X')); - assert.equal(masked.includes(supplementaryCharacter), !strings); + expect(masked.length).toBe(source.length); + expect(masked.indexOf('\r\n')).toBe(source.indexOf('\r\n')); + expect(masked.indexOf('process.env.X')).toBe(source.indexOf('process.env.X')); + expect(masked.includes(supplementaryCharacter)).toBe(!strings); } }); -test('manual configuration rule detects access after supplementary Unicode but ignores quoted data', () => { +it('manual configuration rule detects access after supplementary Unicode but ignores quoted data', () => { withTemporaryWorkspace((directory) => { - const scaffoldDirectory = join(directory, 'scripts/scaffolding'); + const scaffoldDirectory = path.join(directory, 'scripts/scaffolding'); mkdirSync(scaffoldDirectory, { recursive: true }); const positive = 'scripts/scaffolding/unicode-positive.mts'; const negative = 'scripts/scaffolding/unicode-negative.mts'; const prefix = `const label="${supplementaryCharacter.repeat(20)}"; `; writeFileSync( - join(directory, positive), + path.join(directory, positive), `export const source = \`${prefix}process.env.X${' '.repeat(30)}\`;`, ); writeFileSync( - join(directory, negative), + path.join(directory, negative), `export const source = \`${prefix}const example = "process.env.X";\`;`, ); - const config = join(directory, '.oxlintrc.json'); + const config = path.join(directory, '.oxlintrc.json'); writeFileSync( config, JSON.stringify({ + categories: { correctness: 'off' }, jsPlugins: [ - { name: 'effect-native', specifier: join(testsDirectory, 'fixture-plugin.ts') }, + { + name: 'effect-native', + specifier: path.join(testsDirectory, 'fixture-plugin.ts'), + }, ], - categories: { correctness: 'off' }, - rules: { 'effect-native/no-manual-config-in-scaffold-templates': 'error' }, + rules: { + 'effect-native/no-manual-config-in-scaffold-templates': 'error', + }, }), ); const result = runOxlint( @@ -52,13 +57,12 @@ test('manual configuration rule detects access after supplementary Unicode but i directory, 'no-manual-config-in-scaffold-templates', ); - assert.equal(result.numberOfFiles, 2); - assert.equal(result.exitCode, 1); - assert.equal(result.diagnostics.length, 1); - assert.equal( - result.diagnostics[0]?.code, + expect(result.numberOfFiles).toBe(2); + expect(result.exitCode).toBe(1); + expect(result.diagnostics.length).toBe(1); + expect(result.diagnostics[0]?.code).toBe( 'effect-native(no-manual-config-in-scaffold-templates)', ); - assert.equal(result.diagnostics[0]?.filename.replaceAll('\\', '/'), positive); + expect(result.diagnostics[0]?.filename.replaceAll('\\', '/')).toBe(positive); }); }); diff --git a/app/tools/oxlint/effect-native/tests/script-scope.test.mts b/app/tools/oxlint/effect-native/tests/script-scope.test.mts index 41f5f75fb..5e4b892ae 100644 --- a/app/tools/oxlint/effect-native/tests/script-scope.test.mts +++ b/app/tools/oxlint/effect-native/tests/script-scope.test.mts @@ -1,7 +1,7 @@ -import assert from 'node:assert/strict'; import { mkdirSync, realpathSync, symlinkSync, writeFileSync } from 'node:fs'; -import { dirname, join, resolve } from 'node:path'; -import { test } from 'node:test'; +import nodePath from 'node:path'; + +import { expect, it } from 'effect-rstest'; import { runOxlint, testsDirectory } from './oxlint.mts'; import { withTemporaryWorkspace } from './temporary-workspace.mts'; @@ -26,10 +26,10 @@ export const makeOutboxProcessor = (dependencies: OutboxProcessorDependencies) = ]; for (const { rule, source } of cases) { - test(`${rule} excludes nested scripts by default and honors includeScripts`, () => { + it(`${rule} excludes nested scripts by default and honors includeScripts`, () => { withTemporaryWorkspace((root) => { - const directory = join(root, 'workspace'); - const alias = join(root, 'workspace-link'); + const directory = nodePath.join(root, 'workspace'); + const alias = nodePath.join(root, 'workspace-link'); mkdirSync(directory); symlinkSync(directory, alias, 'dir'); const workspaces = ['apps/shell-super-app', 'verticals/contacts', 'packages/core-runtime']; @@ -37,19 +37,22 @@ for (const { rule, source } of cases) { const scripts = workspaces.map((workspace) => `${workspace}/scripts/operation.mts`); const paths = [...sources, ...scripts]; for (const path of paths) { - const file = join(directory, path); - mkdirSync(dirname(file), { recursive: true }); + const file = nodePath.join(directory, path); + mkdirSync(nodePath.dirname(file), { recursive: true }); writeFileSync(file, source); } - const config = join(directory, '.oxlintrc.json'); + const config = nodePath.join(directory, '.oxlintrc.json'); for (const includeScripts of [false, true]) { writeFileSync( config, JSON.stringify({ + categories: { correctness: 'off' }, jsPlugins: [ - { name: 'effect-native', specifier: join(testsDirectory, 'fixture-plugin.ts') }, + { + name: 'effect-native', + specifier: nodePath.join(testsDirectory, 'fixture-plugin.ts'), + }, ], - categories: { correctness: 'off' }, rules: { [`effect-native/${rule}`]: includeScripts ? ['error', { includeScripts: true }] @@ -62,32 +65,32 @@ for (const { rule, source } of cases) { config, pathMode === 'relative' ? paths - : paths.map((path) => join(pathMode === 'symlink' ? alias : directory, path)), + : paths.map((path) => + nodePath.join(pathMode === 'symlink' ? alias : directory, path), + ), directory, rule, ); - assert.equal( - run.numberOfFiles, - paths.length, - `${rule}: every staged file must be linted`, - ); - assert.equal(run.exitCode, 1, `${rule}: ordinary source must still report`); - for (const diagnostic of run.diagnostics) - assert.equal(diagnostic.code, `effect-native(${rule})`); + expect(run.numberOfFiles, `${rule}: every staged file must be linted`).toBe(paths.length); + expect(run.exitCode, `${rule}: ordinary source must still report`).toBe(1); + for (const diagnostic of run.diagnostics) { + expect(diagnostic.code).toBe(`effect-native(${rule})`); + } const reported = [ ...new Set( // Oxlint may retain absolute spellings when input paths cross a symlink. run.diagnostics.map((diagnostic) => - realpathSync(resolve(directory, diagnostic.filename)), + realpathSync(nodePath.resolve(directory, diagnostic.filename)), ), ), ]; - assert.deepEqual( - reported.sort(), + expect( + reported.toSorted(), + `${rule}: includeScripts=${includeScripts}, pathMode=${pathMode}`, + ).toEqual( (includeScripts ? paths : sources) - .map((path) => realpathSync(join(directory, path))) + .map((path) => realpathSync(nodePath.join(directory, path))) .toSorted(), - `${rule}: includeScripts=${includeScripts}, pathMode=${pathMode}`, ); } } diff --git a/app/tools/oxlint/effect-native/tests/shared-helpers-probe.ts b/app/tools/oxlint/effect-native/tests/shared-helpers-probe.ts new file mode 100644 index 000000000..358a7a30e --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/shared-helpers-probe.ts @@ -0,0 +1,230 @@ +import { defineRule, eslintCompatPlugin } from '@oxlint/plugins'; +import type { Context, ESTree } from '@oxlint/plugins'; +import { Predicate } from 'effect'; + +import { asNode, identityUnwrap, memberName, syntax, unwrapNode, walk } from '../shared/ast.ts'; +import type { Syntax } from '../shared/ast.ts'; +import { isUnshadowedGlobal, resolvesToImport } from '../shared/bindings.ts'; +import { bindingPath, effectOrigin, isGenCallee } from '../shared/effect-identity.ts'; +import { collectEffectBindings } from '../shared/effect-imports.ts'; +import { + collectDirectMemberImports, + collectRootNamespaces, + collectSchemaLocals, +} from '../shared/imports.ts'; +import { provenance } from '../shared/provenance.ts'; +import { + isInErasedTypePosition, + isInTypePosition, + isNonReferencePosition, +} from '../shared/reference-positions.ts'; +import { snippet } from '../shared/reporting.ts'; +import { emittedText, maskText, reportNode } from '../shared/scaffold-text.ts'; +import { schemaIdentity } from '../shared/schema-identity.ts'; +import { isEntryPosition } from '../shared/script-entry.ts'; + +type ProbeValue = string | number | boolean | null | readonly ProbeValue[]; +type Probe = (context: Context, program: ESTree.Program) => readonly ProbeValue[]; + +const expression = (program: ESTree.Program): ESTree.Expression => { + const last = program.body.at(-1); + if (last?.type !== 'ExpressionStatement') { + throw new Error('Probe requires a final expression'); + } + return last.expression; +}; + +/** These probes run inside Oxlint: AST parents, scope bindings, and references are real. */ +const probes = new Map([ + [ + 'sequence', + (_context, program) => { + const node = expression(program); + return [unwrapNode(node).type, identityUnwrap(node).type]; + }, + ], + [ + 'await', + (_context, program) => { + const node = expression(program); + return [unwrapNode(node).type, syntax(node)?.type ?? null]; + }, + ], + [ + 'wrappers', + (_context, program) => { + const node = expression(program); + return [ + unwrapNode(node, { wrappers: new Set() }) === node, + asNode({ type: 'Identifier' }, true) === null, + ]; + }, + ], + [ + 'members', + (_context, program) => { + const node = expression(program); + return [ + memberName(node), + memberName(node, { templates: true }), + memberName(node, { unwrap: {} }), + ]; + }, + ], + [ + 'imports', + (_context, program) => { + const imported = collectDirectMemberImports(program).get('tgen'); + return [ + [...collectRootNamespaces(program)], + collectRootNamespaces(program, (source) => source === 'effect', { + valueOnly: true, + }).size, + imported?.namespace ?? null, + imported?.member ?? null, + collectDirectMemberImports(program, undefined, { valueOnly: true }).has('tgen'), + collectSchemaLocals(program, collectEffectBindings(program)).schema.has('S'), + ]; + }, + ], + [ + 'binding', + (context, program) => [ + bindingPath(context, expression(program)), + isGenCallee(context, expression(program), ['gen']), + ], + ], + [ + 'origin', + (context, program) => [ + bindingPath(context, expression(program)), + effectOrigin(context, expression(program), []), + ], + ], + [ + 'barrel', + (context, program) => [ + bindingPath(context, expression(program), ['@app/*']), + effectOrigin(context, expression(program), ['@app/*']), + ], + ], + [ + 'provenance', + (context, program) => [ + provenance(context, expression(program)), + bindingPath(context, expression(program)), + ], + ], + [ + 'globals', + (context, program) => { + const node = expression(program); + return [ + isUnshadowedGlobal(context, node, 'JSON'), + isUnshadowedGlobal(context, node, 'JSON', true), + resolvesToImport(context, node), + resolvesToImport(context, node, true), + ]; + }, + ], + ['schema', (context, program) => [schemaIdentity(context, expression(program))]], + [ + 'schema-syntax', + (context, program) => [ + schemaIdentity(context, expression(program)), + schemaIdentity(context, expression(program), [], 0, { + templates: true, + unwrap: {}, + }), + ], + ], + [ + 'schema-wrappers', + (context, program) => { + const node = expression(program); + return [ + schemaIdentity(context, node), + schemaIdentity(context, node, [], 0, { + unwrap: { wrappers: new Set() }, + }), + schemaIdentity(context, node, [], 0, { unwrap: { maxDepth: 0 } }), + ]; + }, + ], + [ + 'template', + (_context, program) => { + const node = expression(program); + if (node.type !== 'TemplateLiteral') { + throw new Error('Probe requires a template expression'); + } + const text = '// hi\nconst x = "a";'; + return [ + emittedText(node), + reportNode(node, 0, 4).type, + reportNode(node, 0, 12) === node, + maskText(text).length === text.length, + maskText('"a"', true), + snippet('abcdef', 4, 2), + ]; + }, + ], + [ + 'entry', + (context, program) => { + const calls: Syntax[] = []; + walk(program, context.sourceCode.visitorKeys, (node) => { + if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression') { + calls.push(node); + } + }); + return calls.map((node) => isEntryPosition(context, node)); + }, + ], + [ + 'references', + (context, program) => { + const identifiers: Syntax[] = []; + walk(program, context.sourceCode.visitorKeys, (node) => { + if (node.type === 'Identifier') { + identifiers.push(node); + } + }); + const binding = identifiers.find((node) => node.name === 'result'); + if (binding === undefined) { + throw new Error('Probe requires a result binding'); + } + const schema = identifiers.filter((node) => node.name === 'Schema'); + return [ + isNonReferencePosition(binding), + isNonReferencePosition(binding, { variableBindings: true }), + schema.map((node) => isInErasedTypePosition(node)), + schema.map((node) => isInTypePosition(node, new Set(['TSAsExpression']))), + ]; + }, + ], +]); + +export default eslintCompatPlugin({ + meta: { name: 'shared-helpers-probe' }, + rules: { + inspect: defineRule({ + create(context) { + const [name] = context.options; + const probe = Predicate.isString(name) ? probes.get(name) : undefined; + if (probe === undefined) { + throw new Error('Unknown shared helper probe'); + } + return { + 'Program:exit'(program) { + context.report({ + message: JSON.stringify(probe(context, program)), + node: program, + }); + }, + }; + }, + meta: { schema: [{ type: 'string' }], type: 'problem' }, + }), + }, +}); diff --git a/app/tools/oxlint/effect-native/tests/shared-helpers.test.mts b/app/tools/oxlint/effect-native/tests/shared-helpers.test.mts index 3b7e9e73e..458d2b953 100644 --- a/app/tools/oxlint/effect-native/tests/shared-helpers.test.mts +++ b/app/tools/oxlint/effect-native/tests/shared-helpers.test.mts @@ -1,25 +1,8 @@ -import assert from 'node:assert/strict'; -import { test } from 'node:test'; -import { parseSync, visitorKeys } from 'oxc-parser'; -import type { Context, ESTree, Variable } from '@oxlint/plugins'; -import { - asNode, - childrenOf, - identityUnwrap, - memberName, - syntax, - unwrapNode, - walk, - type Syntax, -} from '../shared/ast.ts'; -import { isUnshadowedGlobal, resolvesToImport } from '../shared/bindings.ts'; -import { bindingPath, effectOrigin, isGenCallee } from '../shared/effect-identity.ts'; -import { collectEffectBindings } from '../shared/effect-imports.ts'; -import { - collectDirectMemberImports, - collectRootNamespaces, - collectSchemaLocals, -} from '../shared/imports.ts'; +import { writeFileSync } from 'node:fs'; +import path from 'node:path'; + +import { expect, it } from 'effect-rstest'; + import { booleanOption, compile, @@ -36,277 +19,218 @@ import { scriptScope, workspacePath, } from '../shared/paths.ts'; -import { provenance } from '../shared/provenance.ts'; -import { snippet } from '../shared/reporting.ts'; -import { - isInErasedTypePosition, - isInTypePosition, - isNonReferencePosition, -} from '../shared/reference-positions.ts'; -import { emittedText, maskText, reportNode, type StringNode } from '../shared/scaffold-text.ts'; -import { schemaIdentity } from '../shared/schema-identity.ts'; -import { isEntryPosition } from '../shared/script-entry.ts'; - -function parse(source: string): ESTree.Program { - const parsed = parseSync('helpers.ts', source); - assert.deepEqual(parsed.errors, []); - const program = parsed.program as unknown as ESTree.Program; - walk(program, visitorKeys, (node) => { - for (const child of childrenOf(node, visitorKeys)) child.parent = node; - }); - return program; -} -function expression(source: string): ESTree.Node { - const statement = parse(source).body[0]; - assert.equal(statement?.type, 'ExpressionStatement'); - return (statement as ESTree.ExpressionStatement).expression; -} - -/** Explicit single-scope test double, not an attempt to reconstruct the lint engine's scopes. */ -function contextFor(program: ESTree.Program): { - context: Context; - variables: Map; -} { - const variables = new Map(); - const define = (name: string, definition: unknown) => - variables.set(name, { - defs: [definition], - references: [], - identifiers: [], - } as unknown as Variable); - for (const declaration of program.body) { - if (declaration.type === 'ImportDeclaration') { - for (const specifier of declaration.specifiers) - define(specifier.local.name, { - type: 'ImportBinding', - node: specifier, - parent: declaration, - }); - } - if (declaration.type === 'VariableDeclaration') { - for (const declarator of declaration.declarations) { - walk(declarator.id, visitorKeys, (node) => { - if (node.type === 'Identifier') - define(node.name, { type: 'Variable', node: declarator, parent: declaration }); - }); - } - } - } - const context = { - sourceCode: { - ast: program, - getScope: () => ({ set: variables, upper: null }), - } as unknown as Context['sourceCode'], - } as Context; - return { context, variables }; -} -function lastExpression(program: ESTree.Program): ESTree.Node { - const last = program.body.at(-1); - assert.equal(last?.type, 'ExpressionStatement'); - return (last as ESTree.ExpressionStatement).expression; -} +import { runOxlint, testsDirectory } from './oxlint.mts'; +import { withTemporaryWorkspace } from './temporary-workspace.mts'; -test('shared option parsers preserve rejection, sparse arrays and regex flags', () => { +it('shared option parsers preserve rejection, sparse arrays and regex flags', () => { const fallback = ['default']; - assert.equal(stringArray(['yes', 1], fallback), fallback); - assert.equal(stringArray(new Array(2), fallback), fallback); - assert.equal(stringList(new Array(2), fallback).length, 2); - assert.equal(booleanOption('false', true), true); - assert.equal(positiveInteger(0, 3), 3); - assert.equal(positiveInteger(0, 3, 0), 0); - assert.equal(compile('[', 'fallback', 'iu').source, 'fallback'); - assert.equal(compile('[', 'fallback', 'iu').flags, 'iu'); - assert.equal(compilePatterns(['[', 'ok']).length, 1); - assert.equal(safeRegExp('', 'fallback').source, '(?:)'); - assert.equal(compile('', 'fallback').source, 'fallback'); -}); - -test('shared path policies distinguish earliest and latest markers and script scope', () => { + const sparse: string[] = []; + sparse.length = 2; + expect(stringArray(['yes', 1], fallback)).toBe(fallback); + expect(stringArray(sparse, fallback)).toBe(fallback); + expect(stringList(sparse, fallback).length).toBe(2); + expect(booleanOption('false', true)).toBe(true); + expect(positiveInteger(0, 3)).toBe(3); + expect(positiveInteger(0, 3, 0)).toBe(0); + expect(compile('[', 'fallback', 'iu').source).toBe('fallback'); + expect(compile('[', 'fallback', 'iu').flags).toBe('iu'); + expect(compilePatterns(['[', 'ok']).length).toBe(1); + expect(safeRegExp('', 'fallback').source).toBe('(?:)'); + expect(compile('', 'fallback').source).toBe('fallback'); +}); + +it('shared path policies distinguish earliest and latest markers and script scope', () => { + const nestedScript = 'packages/p/scripts/apps/demo.ts'; const fixture = '/repo/tools/oxlint/effect-native/tests/fixtures/x/invalid/packages/p/scripts/apps/demo.ts'; - assert.equal(scopePath(fixture), 'packages/p/scripts/apps/demo.ts'); - assert.equal(scriptScope(fixture), 'packages/p/scripts/apps/demo.ts'); - assert.equal(workspacePath(fixture), 'apps/demo.ts'); - assert.equal(inScriptScope(scriptScope(fixture)), true); - assert.equal(inScriptScope('scripts/a.test.ts'), false); - assert.equal(scriptScope('packages/p/scripts/apps/demo.ts'), 'packages/p/scripts/apps/demo.ts'); - assert.equal(globToRegExp('**/*.{ts,mts}').test('a.ts'), true); - assert.equal(globToRegExp('a{').test('a{'), true); - assert.equal(globToRegExp('x/?*.ts').test('x/a.ts'), true); -}); - -test('shared unwrapping preserves sequence and await opt-ins and narrow wrapper policy', () => { - const sequence = expression('(ignored, Effect.gen)'); - assert.equal(unwrapNode(sequence).type, 'SequenceExpression'); - assert.equal(identityUnwrap(sequence).type, 'MemberExpression'); - const awaited = expression('await Effect.gen'); - assert.equal(unwrapNode(awaited).type, 'AwaitExpression'); - assert.equal(syntax(awaited)?.type, 'MemberExpression'); - const wrapped = expression('Effect as unknown'); - assert.equal(unwrapNode(wrapped, { wrappers: new Set() }), wrapped); - assert.equal(asNode({ type: 'Identifier' }, true), null); -}); - -test('shared static members distinguish templates and wrapped keys', () => { - const computed = expression('Effect[`gen`]'); - assert.equal(memberName(computed), null); - assert.equal(memberName(computed, { templates: true }), 'gen'); - const wrapped = expression('Effect[("gen" as const)]'); - assert.equal(memberName(wrapped), null); - assert.equal(memberName(wrapped, { unwrap: {} }), 'gen'); - assert.equal(memberName(expression('Effect[method]')), null); -}); - -test('shared imports preserve aliases and optional type-only filtering', () => { - const program = parse( - 'import type * as Types from "effect"; import { type gen as tgen, gen as g } from "effect/Effect"; import * as S from "effect/Schema";', - ); - assert.deepEqual([...collectRootNamespaces(program)], ['Types']); - assert.equal( - collectRootNamespaces(program, (source) => source === 'effect', { valueOnly: true }).size, - 0, - ); - assert.deepEqual(collectDirectMemberImports(program).get('tgen'), { - namespace: 'Effect', - member: 'gen', - }); - assert.equal( - collectDirectMemberImports(program, undefined, { valueOnly: true }).has('tgen'), - false, - ); - const schema = collectSchemaLocals(program, collectEffectBindings(program)); - assert.equal(schema.schema.has('S'), true); -}); - -test('shared Effect identities preserve const aliases and direct submodule imports', () => { - const program = parse('import { Effect as E } from "effect"; const { gen: g } = E; g;'); - const { context } = contextFor(program); - assert.deepEqual(bindingPath(context, lastExpression(program)), ['Effect', 'gen']); - assert.equal(isGenCallee(context, lastExpression(program), ['gen']), true); - const direct = parse('import { gen as g } from "effect/Effect"; g;'); - assert.deepEqual(bindingPath(contextFor(direct).context, lastExpression(direct)), [ - 'Effect', - 'gen', - ]); -}); - -test('shared Effect origin policies preserve glob barrels and default-import differences', () => { - const program = parse('import E from "effect/Effect"; E.gen;'); - const { context } = contextFor(program); - assert.equal(bindingPath(context, lastExpression(program)), null); - assert.deepEqual(effectOrigin(context, lastExpression(program), []), ['Effect', 'gen']); - const barrel = parse('import { Effect as E } from "@app/barrel"; E.gen;'); - const b = contextFor(barrel).context; - assert.equal(bindingPath(b, lastExpression(barrel), ['@app/*']), null); - assert.deepEqual(effectOrigin(b, lastExpression(barrel), ['@app/*']), ['Effect', 'gen']); -}); - -test('shared script provenance accepts unwritten let aliases but rejects later writes', () => { - const program = parse('import * as p from "node:process"; let { stderr: sink } = p; sink.write;'); - const { context, variables } = contextFor(program); - assert.equal(provenance(context, lastExpression(program)), 'process.stderr.write'); - assert.equal(bindingPath(context, lastExpression(program)), null); - const variable = variables.get('sink')!; - variables.set('sink', { - ...variable, - references: [{ init: false, isWrite: () => true }], - } as unknown as Variable); - assert.equal(provenance(context, lastExpression(program)), null); -}); - -test('shared script provenance follows dynamic import, require and ambient containers', () => { - const program = parse('await import("node:process");'); - assert.equal(provenance(contextFor(program).context, lastExpression(program)), 'process'); - const required = parse('require("node:console").warn;'); - assert.equal(provenance(contextFor(required).context, lastExpression(required)), 'console.warn'); - const global = parse('globalThis.process.stderr.write;'); - assert.equal( - provenance(contextFor(global).context, lastExpression(global)), - 'process.stderr.write', - ); -}); - -test('shared global/import resolution retains opt-in type-only policy', () => { - const program = parse('import type { JSON } from "types"; JSON;'); - const { context } = contextFor(program); - const node = lastExpression(program); - assert.equal(isUnshadowedGlobal(context, node, 'JSON'), false); - assert.equal(isUnshadowedGlobal(context, node, 'JSON', true), true); - assert.equal(resolvesToImport(context, node), true); - assert.equal(resolvesToImport(context, node, true), false); -}); - -test('shared Schema identity follows aliases while rejecting mutable declarations', () => { - const program = parse( - 'import * as E from "effect"; const S = E.Schema; const { decodeUnknownSync: decode } = S; decode;', - ); - assert.equal( - schemaIdentity(contextFor(program).context, lastExpression(program)), - 'decodeUnknownSync', - ); - const mutable = parse('import { Schema } from "effect"; let S = Schema; S.Json;'); - assert.equal(schemaIdentity(contextFor(mutable).context, lastExpression(mutable)), null); -}); - -test('shared template text preserves cooked offsets and diagnostic budgets', () => { - const node = expression('`first ${name} last`') as StringNode; - assert.equal(emittedText(node), 'first _ last'); - assert.equal(reportNode(node, 0, 4).type, 'TemplateElement'); - assert.equal(reportNode(node, 0, 12), node); - assert.equal(maskText('// hi\nconst x = "a";').length, '// hi\nconst x = "a";'.length); - assert.equal(maskText('"a"', true), ' '); - assert.equal(snippet('abcdef', 4, 2), 'ab…'); -}); - -test('shared entry detection includes module evaluation and top-level IIFEs only', () => { - const program = parse( - '(() => { console.warn("x"); })(); function nested() { console.warn("y"); }', - ); - const context = contextFor(program).context; - const calls: Syntax[] = []; - walk(program, visitorKeys, (node) => { - if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression') calls.push(node); - }); - assert.equal(isEntryPosition(context, calls[0]!), true); - assert.equal(isEntryPosition(context, calls[1]!), false); -}); - -test('shared reference policies preserve declaration keys and TS expression edges', () => { - const program = parse('const result = Schema as unknown; type Result = typeof Schema;'); - const identifiers: Syntax[] = []; - walk(program, visitorKeys, (node) => { - if (node.type === 'Identifier') identifiers.push(node); + expect(scopePath(fixture)).toBe(nestedScript); + expect(scriptScope(fixture)).toBe(nestedScript); + expect(workspacePath(fixture)).toBe('apps/demo.ts'); + expect(inScriptScope(scriptScope(fixture))).toBe(true); + expect(inScriptScope('scripts/a.test.ts')).toBe(false); + expect(scriptScope(nestedScript)).toBe(nestedScript); + expect(globToRegExp('**/*.{ts,mts}').test('a.ts')).toBe(true); + expect(globToRegExp('a{').test('a{')).toBe(true); + expect(globToRegExp('x/?*.ts').test('x/a.ts')).toBe(true); +}); + +const cases = [ + { + expected: ['SequenceExpression', 'MemberExpression'], + name: 'sequence wrappers are opt-in', + probe: 'sequence', + source: '(ignored, Effect.gen);', + }, + { + expected: ['AwaitExpression', 'MemberExpression'], + name: 'await wrappers are opt-in', + probe: 'await', + source: 'await Effect.gen;', + }, + { + expected: [true, true], + name: 'narrow wrappers and required spans stay strict', + probe: 'wrappers', + source: 'Effect as unknown;', + }, + { + expected: [null, 'gen', 'gen'], + name: 'template member keys are opt-in', + probe: 'members', + source: 'Effect[`gen`];', + }, + { + expected: [null, null, 'gen'], + name: 'wrapped member keys are opt-in', + probe: 'members', + source: 'Effect[("gen" as const)];', + }, + { + expected: [null, null, null], + name: 'dynamic member keys remain unknown', + probe: 'members', + source: 'Effect[method];', + }, + { + expected: [['Types'], 0, 'Effect', 'gen', false, true], + name: 'imports preserve aliases and type-only filtering', + probe: 'imports', + source: + 'import type * as Types from "effect"; import { type gen as tgen, gen as g } from "effect/Effect"; import * as S from "effect/Schema";', + }, + { + expected: [['Effect', 'gen'], true], + name: 'Effect identities follow const destructuring', + probe: 'binding', + source: 'import { Effect as E } from "effect"; const { gen: g } = E; g;', + }, + { + expected: [['Effect', 'gen'], true], + name: 'Effect identities follow direct submodule imports', + probe: 'binding', + source: 'import { gen as g } from "effect/Effect"; g;', + }, + { + expected: [null, ['Effect', 'gen']], + name: 'Effect origin preserves default-import differences', + probe: 'origin', + source: 'import E from "effect/Effect"; E.gen;', + }, + { + expected: [null, ['Effect', 'gen']], + name: 'Effect origin preserves glob barrel differences', + probe: 'barrel', + source: 'import { Effect as E } from "@app/barrel"; E.gen;', + }, + { + expected: ['process.stderr.write', null], + name: 'script provenance accepts unwritten let aliases', + probe: 'provenance', + source: 'import * as p from "node:process"; let { stderr: sink } = p; sink.write;', + }, + { + expected: [null, null], + name: 'script provenance rejects later writes', + probe: 'provenance', + source: + 'import * as p from "node:process"; let { stderr: sink } = p; sink = p.stdout; sink.write;', + }, + { + expected: ['process', null], + name: 'script provenance follows dynamic imports', + probe: 'provenance', + source: 'await import("node:process");', + }, + { + expected: ['console.warn', null], + name: 'script provenance follows require', + probe: 'provenance', + source: 'require("node:console").warn;', + }, + { + expected: ['process.stderr.write', null], + name: 'script provenance follows ambient containers', + probe: 'provenance', + source: 'globalThis.process.stderr.write;', + }, + { + expected: [false, true, true, false], + name: 'global/import resolution retains type-only policy', + probe: 'globals', + source: 'import type { JSON } from "types"; JSON;', + }, + { + expected: ['decodeUnknownSync'], + name: 'Schema identity follows aliases', + probe: 'schema', + source: + 'import * as E from "effect"; const S = E.Schema; const { decodeUnknownSync: decode } = S; decode;', + }, + { + expected: [null], + name: 'Schema identity rejects mutable declarations', + probe: 'schema', + source: 'import { Schema } from "effect"; let S = Schema; S.Json;', + }, + { + expected: ['first _ last', 'TemplateElement', true, true, ' ', 'ab…'], + name: 'template text preserves cooked offsets and budgets', + probe: 'template', + source: `\`first \${name} last\`;`, + }, + { + expected: [true, false], + name: 'entry detection includes top-level IIFEs only', + probe: 'entry', + source: '(() => { console.warn("x"); })(); function nested() { console.warn("y"); }', + }, + { + expected: [false, true, [false, true], [false, true]], + name: 'reference policies preserve declaration and TS edges', + probe: 'references', + source: 'const result = Schema as unknown; type Result = typeof Schema;', + }, + { + expected: ['Json', null, null], + name: 'Schema identity preserves expression wrapper limits', + probe: 'schema-wrappers', + source: 'import { Schema } from "effect"; const S = Schema as unknown; S.Json;', + }, + ...['`decodeUnknownSync`', '("decodeUnknownSync" as const)'].map((key) => ({ + expected: [null, 'decodeUnknownSync'], + name: `Schema syntax policy survives aliased key ${key}`, + probe: 'schema-syntax', + source: `import { Schema } from "effect"; const S = Schema; const codec = S[${key}]; codec;`, + })), +]; + +for (const { expected, name, probe, source } of cases) { + it(`shared helpers: ${name}`, () => { + withTemporaryWorkspace((directory) => { + writeFileSync(path.join(directory, 'probe.ts'), source); + const config = path.join(directory, '.oxlintrc.json'); + writeFileSync( + config, + JSON.stringify({ + categories: { correctness: 'off' }, + jsPlugins: [ + { + name: 'shared-helpers-probe', + specifier: path.join(testsDirectory, 'shared-helpers-probe.ts'), + }, + ], + rules: { 'shared-helpers-probe/inspect': ['error', probe] }, + }), + ); + const run = runOxlint(config, ['probe.ts'], directory); + expect(run.exitCode).toBe(1); + expect(run.numberOfFiles).toBe(1); + expect(run.diagnostics.map(({ code, message }) => ({ code, message }))).toStrictEqual([ + { + code: 'shared-helpers-probe(inspect)', + message: JSON.stringify(expected), + }, + ]); + }); }); - const binding = identifiers.find((node) => node.name === 'result')!; - assert.equal(isNonReferencePosition(binding), false); - assert.equal(isNonReferencePosition(binding, { variableBindings: true }), true); - const schema = identifiers.filter((node) => node.name === 'Schema'); - assert.equal(isInErasedTypePosition(schema[0]!), false); - assert.equal(isInErasedTypePosition(schema[1]!), true); - assert.equal(isInTypePosition(schema[0]!, new Set(['TSAsExpression'])), false); - assert.equal(isInTypePosition(schema[1]!, new Set(['TSAsExpression'])), true); -}); - -test('Schema identity syntax policy survives aliases without widening default members', () => { - for (const key of ['`decodeUnknownSync`', '("decodeUnknownSync" as const)']) { - const program = parse( - `import { Schema } from "effect"; const S = Schema; const codec = S[${key}]; codec;`, - ); - const { context } = contextFor(program); - const node = lastExpression(program); - assert.equal(schemaIdentity(context, node), null); - assert.equal( - schemaIdentity(context, node, [], 0, { templates: true, unwrap: {} }), - 'decodeUnknownSync', - ); - } -}); - -test('Schema identity preserves rule-specific expression wrapper limits', () => { - const program = parse('import { Schema } from "effect"; const S = Schema as unknown; S.Json;'); - const { context } = contextFor(program); - const node = lastExpression(program); - assert.equal(schemaIdentity(context, node), 'Json'); - assert.equal(schemaIdentity(context, node, [], 0, { unwrap: { wrappers: new Set() } }), null); - assert.equal(schemaIdentity(context, node, [], 0, { unwrap: { maxDepth: 0 } }), null); -}); +} diff --git a/app/tools/oxlint/effect-native/tests/source-rule-scope.test.mts b/app/tools/oxlint/effect-native/tests/source-rule-scope.test.mts index bf0c332d3..643a706d0 100644 --- a/app/tools/oxlint/effect-native/tests/source-rule-scope.test.mts +++ b/app/tools/oxlint/effect-native/tests/source-rule-scope.test.mts @@ -1,52 +1,58 @@ -import assert from 'node:assert/strict'; -import { test } from 'node:test'; +import { expect, it } from 'effect-rstest'; import { isSourceRuleInScope } from '../shared/source-rule-scope.ts'; const defaults = { - includePaths: ['apps/**', 'packages/**', 'verticals/**'], ignore: [], + includePaths: ['apps/**', 'packages/**', 'verticals/**'], includeScripts: false, includeTests: false, }; -test('source rule scope applies include and ignore gates before opt-ins', () => { - assert.equal(isSourceRuleInScope('apps/example/src/main.ts', defaults), true); - assert.equal(isSourceRuleInScope('tools/example.ts', defaults), false); - assert.equal( - isSourceRuleInScope('apps/example/src/main.ts', { ...defaults, ignore: ['apps/**'] }), - false, - ); - assert.equal( - isSourceRuleInScope('scripts/main.ts', { ...defaults, includeScripts: true }), - false, - ); +it('source rule scope applies include and ignore gates before opt-ins', () => { + expect(isSourceRuleInScope('apps/example/src/main.ts', defaults)).toBe(true); + expect(isSourceRuleInScope('tools/example.ts', defaults)).toBe(false); + expect( + isSourceRuleInScope('apps/example/src/main.ts', { + ...defaults, + ignore: ['apps/**'], + }), + ).toBe(false); + expect( + isSourceRuleInScope('scripts/main.ts', { + ...defaults, + includeScripts: true, + }), + ).toBe(false); }); -test('source rule scope independently gates scripts and tests', () => { - const defaults = { - includePaths: ['apps/**', 'scripts/**'], +it('source rule scope independently gates scripts and tests', () => { + const scriptDefaults = { ignore: [], + includePaths: ['apps/**', 'scripts/**'], includeScripts: false, includeTests: false, }; const script = 'scripts/main.ts'; const testFile = 'apps/example/src/main.test.ts'; const scriptTest = 'scripts/main.test.ts'; - assert.equal(isSourceRuleInScope(script, defaults), false); - assert.equal(isSourceRuleInScope(script, { ...defaults, includeScripts: true }), true); - assert.equal(isSourceRuleInScope(testFile, defaults), false); - assert.equal(isSourceRuleInScope(testFile, { ...defaults, includeTests: true }), true); - assert.equal(isSourceRuleInScope(scriptTest, { ...defaults, includeScripts: true }), false); - assert.equal(isSourceRuleInScope(scriptTest, { ...defaults, includeTests: true }), false); - assert.equal( - isSourceRuleInScope(scriptTest, { ...defaults, includeScripts: true, includeTests: true }), - true, - ); + expect(isSourceRuleInScope(script, scriptDefaults)).toBe(false); + expect(isSourceRuleInScope(script, { ...scriptDefaults, includeScripts: true })).toBe(true); + expect(isSourceRuleInScope(testFile, scriptDefaults)).toBe(false); + expect(isSourceRuleInScope(testFile, { ...scriptDefaults, includeTests: true })).toBe(true); + expect(isSourceRuleInScope(scriptTest, { ...scriptDefaults, includeScripts: true })).toBe(false); + expect(isSourceRuleInScope(scriptTest, { ...scriptDefaults, includeTests: true })).toBe(false); + expect( + isSourceRuleInScope(scriptTest, { + ...scriptDefaults, + includeScripts: true, + includeTests: true, + }), + ).toBe(true); }); -test('source rule scope preserves fixture path normalization', () => { +it('source rule scope preserves fixture path normalization', () => { const prefix = 'tools/oxlint/effect-native/tests/fixtures/no-dependency-parameters/invalid/'; - assert.equal(isSourceRuleInScope(`${prefix}apps/example/src/main.ts`, defaults), true); - assert.equal(isSourceRuleInScope(`${prefix}apps/example/src/main.test.ts`, defaults), false); + expect(isSourceRuleInScope(`${prefix}apps/example/src/main.ts`, defaults)).toBe(true); + expect(isSourceRuleInScope(`${prefix}apps/example/src/main.test.ts`, defaults)).toBe(false); }); diff --git a/app/tools/oxlint/effect-native/tests/temporary-workspace.mts b/app/tools/oxlint/effect-native/tests/temporary-workspace.mts index 91599e897..7f1829d96 100644 --- a/app/tools/oxlint/effect-native/tests/temporary-workspace.mts +++ b/app/tools/oxlint/effect-native/tests/temporary-workspace.mts @@ -1,11 +1,11 @@ -import { mkdtempSync, rmSync } from 'node:fs'; +import { mkdtempSync, realpathSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const owned = new Set(); const release = (directory: string): void => { if (!owned.has(directory)) return; - rmSync(directory, { recursive: true, force: true }); + rmSync(directory, { force: true, recursive: true }); owned.delete(directory); }; @@ -26,7 +26,8 @@ export function withTemporaryWorkspace( run: (directory: string) => T, root = process.env.EFFECT_NATIVE_TEST_TMPDIR ?? tmpdir(), ): T { - const directory = mkdtempSync(join(root, 'effect-policy-')); + // macOS tmpdir() is a symlink; Oxlint canonicalizes file paths before matching override globs. + const directory = mkdtempSync(join(realpathSync(root), 'effect-policy-')); owned.add(directory); let result: T; try { diff --git a/app/tools/oxlint/effect-native/tests/temporary-workspace.test.mts b/app/tools/oxlint/effect-native/tests/temporary-workspace.test.mts index 7b4bb1757..041ab2bf7 100644 --- a/app/tools/oxlint/effect-native/tests/temporary-workspace.test.mts +++ b/app/tools/oxlint/effect-native/tests/temporary-workspace.test.mts @@ -1,14 +1,15 @@ -import assert from 'node:assert/strict'; +import { expect, it } from 'effect-rstest'; import { spawnSync } from 'node:child_process'; import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { test } from 'node:test'; +import nodePath from 'node:path'; import { withTemporaryWorkspace } from './temporary-workspace.mts'; -test('process termination cleans workspaces and preserves caller-owned roots', () => { +const callerOwned = 'caller-owned'; + +it('process termination cleans workspaces and preserves caller-owned roots', () => { withTemporaryWorkspace((root) => { - writeFileSync(join(root, 'caller-owned'), 'preserve'); - const helper = new URL('./temporary-workspace.mts', import.meta.url).href; + writeFileSync(nodePath.join(root, callerOwned), 'preserve'); + const helper = new URL('temporary-workspace.mts', import.meta.url).href; for (const [termination, status] of [ ['process.exit(23)', 23], ["process.emit('SIGINT')", 130], @@ -16,47 +17,44 @@ test('process termination cleans workspaces and preserves caller-owned roots', ( ] as const) { const script = `import { withTemporaryWorkspace } from ${JSON.stringify(helper)}; withTemporaryWorkspace(() => { ${termination}; });`; const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], { - encoding: 'utf8', + encoding: 'utf-8', env: { ...process.env, EFFECT_NATIVE_TEST_TMPDIR: root }, timeout: 5000, }); - assert.equal(result.error, undefined); - assert.equal(result.status, status, result.stderr); - assert.equal(result.stderr, ''); - assert.deepEqual(readdirSync(root), ['caller-owned']); + expect(result.error).toBe(undefined); + expect(result.status, result.stderr).toBe(status); + expect(result.stderr).toBe(''); + expect(readdirSync(root)).toEqual([callerOwned]); } }); }); -test('temporary workspace is removed after success', () => { +it('temporary workspace is removed after success', () => { let created = ''; - assert.equal( + expect( withTemporaryWorkspace((directory) => { created = directory; return 42; }), - 42, - ); - assert.equal(existsSync(created), false); + ).toBe(42); + expect(existsSync(created)).toBe(false); }); -test('early and partially initialized failures retain their cause and clean owned children', () => { +it('early and partially initialized failures retain their cause and clean owned children', () => { withTemporaryWorkspace((root) => { - writeFileSync(join(root, 'caller-owned'), 'preserve'); + writeFileSync(nodePath.join(root, callerOwned), 'preserve'); const failure = new Error('injected fixture initialization failure'); for (const partial of [false, true]) { - assert.throws( - () => - withTemporaryWorkspace((directory) => { - if (partial) { - mkdirSync(join(directory, 'partial')); - writeFileSync(join(directory, 'partial', 'file'), 'data'); - } - throw failure; - }, root), - (error) => error === failure, - ); - assert.deepEqual(readdirSync(root), ['caller-owned']); + expect(() => + withTemporaryWorkspace((directory) => { + if (partial) { + mkdirSync(nodePath.join(directory, 'partial')); + writeFileSync(nodePath.join(directory, 'partial', 'file'), 'data'); + } + throw failure; + }, root), + ).toThrow({ asymmetricMatch: (error: Error) => error === failure }); + expect(readdirSync(root)).toEqual([callerOwned]); } }); }); diff --git a/app/verticals/party-registry/package.json b/app/verticals/party-registry/package.json index a8bdf55b3..7c56648e4 100644 --- a/app/verticals/party-registry/package.json +++ b/app/verticals/party-registry/package.json @@ -54,9 +54,9 @@ "dev": "node ../../scripts/prepare-dev-module-contract.mts party-registry && modern dev", "dev:worker": "node --experimental-strip-types ./src/worker-host/main.ts", "serve": "modern serve", - "test:component": "rstest --config rstest.config.ts", - "test:integration": "node --test tests/integration/*.test.ts", - "test:unit": "node --test tests/unit/*.test.ts", + "test:component": "rstest --project component", + "test:integration": "rstest --project integration", + "test:unit": "rstest --project unit", "typecheck": "node ../../scripts/ultramodern-typecheck.mts --project tsconfig.json", "worker:start": "node --experimental-strip-types ./src/worker-host/main.ts" }, @@ -87,7 +87,7 @@ "@modern-js/adapter-rstest": "npm:@bleedingdev/modern-js-adapter-rstest@3.8.2-ultramodern.12", "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12", "@rsbuild/plugin-tailwindcss": "^2.0.3", - "@rstest/core": "0.11.10", + "@rstest/core": "0.11.11", "@testing-library/dom": "10.4.1", "@testing-library/react": "16.3.2", "@types/node": "^20", @@ -102,7 +102,8 @@ "tailwindcss": "^4.3.2", "typescript": "7.0.2", "wrangler": "4.110.0", - "zephyr-rspack-plugin": "1.2.4" + "zephyr-rspack-plugin": "1.2.4", + "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc" }, "modernjs": { "preset": "presetUltramodern", diff --git a/app/verticals/party-registry/rstest.config.ts b/app/verticals/party-registry/rstest.config.ts index 5159e7131..2a0274fbc 100644 --- a/app/verticals/party-registry/rstest.config.ts +++ b/app/verticals/party-registry/rstest.config.ts @@ -4,15 +4,35 @@ import { defineConfig } from '@rstest/core'; Object.assign(globalThis, { require: createRequire(import.meta.url) }); +// Migration scripts contain generic arrows in .mts files, supported by TypeScript. +const swc = { jsc: { parser: { disallowAmbiguousJsxLike: false, syntax: 'typescript' } } } as const; + export default defineConfig({ - clearMocks: true, - extends: withModernConfig({ - configPath: './modern.rstest.config.ts', - }), - include: ['tests/components/**/*.{test,spec}.?(c|m)[jt]s?(x)'], - output: { - module: true, - }, - restoreMocks: true, - testEnvironment: 'happy-dom', + projects: [ + { + name: 'component', + clearMocks: true, + extends: withModernConfig({ + configPath: './modern.rstest.config.ts', + }), + include: ['tests/components/**/*.{test,spec}.?(c|m)[jt]s?(x)'], + output: { + module: true, + }, + restoreMocks: true, + testEnvironment: 'happy-dom', + }, + { + name: 'integration', + testEnvironment: 'node', + include: ['tests/integration/**/*.test.ts'], + testTimeout: 30_000, + }, + { + name: 'unit', + testEnvironment: 'node', + include: ['tests/unit/**/*.test.ts'], + tools: { swc }, + }, + ], }); diff --git a/app/verticals/party-registry/scripts/prepare-contacts-migration.mts b/app/verticals/party-registry/scripts/prepare-contacts-migration.mts index 9787e5980..a5c35d355 100644 --- a/app/verticals/party-registry/scripts/prepare-contacts-migration.mts +++ b/app/verticals/party-registry/scripts/prepare-contacts-migration.mts @@ -44,26 +44,20 @@ const RootConfigProvider = ConfigProvider.layer( const databaseFailure = (message: string, cause: unknown): ContactsMigrationError => new ContactsMigrationError({ cause, message }); -const invokePromiseWithoutSignal = - (operation: () => PromiseLike) => - (_signal: AbortSignal): PromiseLike => - operation(); - const query = ( client: Client, text: string, -): Effect.Effect, ContactsMigrationError> => { - const executeQuery: (queryText: string) => Promise> = client.query.bind(client); - return Effect.tryPromise({ +): Effect.Effect, ContactsMigrationError> => + Effect.tryPromise({ catch: (cause) => databaseFailure(`PostgreSQL query failed: ${text}`, cause), - try: invokePromiseWithoutSignal(executeQuery.bind(undefined, text)), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => client.query(text), }).pipe( Effect.timeoutOrElse({ duration: POSTGRES_OPERATION_TIMEOUT, orElse: () => Effect.fail(databaseFailure(`PostgreSQL query timed out: ${text}`, 'timeout')), }), ); -}; const connect = Effect.fn('ContactsMigration.connect')(function* connectEffect( connectionString: Redacted.Redacted, @@ -74,7 +68,8 @@ const connect = Effect.fn('ContactsMigration.connect')(function* connectEffect( }); yield* Effect.tryPromise({ catch: (cause) => databaseFailure('Unable to connect to PostgreSQL', cause), - try: invokePromiseWithoutSignal(client.connect.bind(client)), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => client.connect(), }).pipe( Effect.timeoutOrElse({ duration: POSTGRES_OPERATION_TIMEOUT, @@ -88,7 +83,8 @@ const connect = Effect.fn('ContactsMigration.connect')(function* connectEffect( const close = (client: Client) => Effect.tryPromise({ catch: (cause) => databaseFailure('Unable to close the PostgreSQL connection', cause), - try: invokePromiseWithoutSignal(client.end.bind(client)), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => client.end(), }).pipe( Effect.timeoutOrElse({ duration: POSTGRES_OPERATION_TIMEOUT, diff --git a/app/verticals/party-registry/tests/components/contacts-page.test.tsx b/app/verticals/party-registry/tests/components/contacts-page.test.tsx index 89aca7620..8e28f6b4a 100644 --- a/app/verticals/party-registry/tests/components/contacts-page.test.tsx +++ b/app/verticals/party-registry/tests/components/contacts-page.test.tsx @@ -1,4 +1,4 @@ -import { afterEach, expect, rstest, test } from '@rstest/core'; +import { afterEach, expect, rstest, test } from 'effect-rstest'; import { cleanup, render, screen } from '@testing-library/react'; import csCatalog from '../../locales/cs/party-registry.json'; import enCatalog from '../../locales/en/party-registry.json'; diff --git a/app/verticals/party-registry/tests/integration/ares-governed.test.ts b/app/verticals/party-registry/tests/integration/ares-governed.test.ts index 82e4f37da..feb03e408 100644 --- a/app/verticals/party-registry/tests/integration/ares-governed.test.ts +++ b/app/verticals/party-registry/tests/integration/ares-governed.test.ts @@ -1,8 +1,5 @@ -import { - makeEffectTestCallback as nativeTestCallback, - runEffectTestPromise, - runEffectTestSync as runNativeSync, -} from '@app/core-runtime/testing/effect-runtime'; +import { assert, expect, it } from 'effect-rstest'; + import { DatabaseConfig, loadDatabaseConnectionPair } from '@app/core-runtime'; import { makeLiveOperationFixture } from '@app/core-runtime/testing/actions'; @@ -15,18 +12,14 @@ import { Effect, Layer, Match, - Exit as NativeExit, - Scope as NativeScope, Option, Redacted, - Result, Schema, + Predicate, } from 'effect'; import { FetchHttpClient, HttpClient, HttpClientResponse } from 'effect/unstable/http'; import { SignJWT, exportJWK, generateKeyPair } from 'jose'; -import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; -import test, { after as afterNativeDatabase } from 'node:test'; import { Pool } from 'pg'; import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.ts'; import { aresLookupReadApiLive } from '../../api/ares-lookup-read-server.ts'; @@ -77,11 +70,6 @@ import { } from '../../src/db/schema.ts'; import { AresSubjectServiceLive } from '../../src/integrations/ares/ares-subject.service.ts'; -const nativeDatabaseScope = runNativeSync(NativeScope.make()); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), -); - const subjectEvidence = [ { kind: 'ACTOR_ATTESTATION' as const, @@ -110,46 +98,42 @@ const rawSubject = { }; const emptyRequestContext = Context.makeUnsafe(new Map()); const lookupIco = Schema.decodeUnknownSync(AresSubjectLookupIcoSchema)('27074358'); -const endPool = (pool: Pool) => pool.end(); -const promiseEffect = (operation: () => PromiseLike) => Effect.promise(operation); +const endPool = (pool: Pool) => Effect.promise(() => pool.end()); -test('exported ARES coordinator uses real authorized HTTP commands, canonical persistence and reviewed correction', () => - runEffectTestPromise( - Effect.scoped( - Effect.gen(function* aresGovernedTestEffect() { - const connections = yield* loadDatabaseConnectionPair(); - const fixture = yield* Effect.acquireRelease( - makeLiveOperationFixture({ - actionKeys: [ - addContactPointAction, - addPartyOfficialIdentifierAction, - correctPartyFactAction, - createPartyAction, - resolveDuplicateCandidateCreateAction, - updatePartyAction, - ].map(({ descriptor }) => descriptor.actionKey), - runtimeConnectionString: Redacted.make(connections.runtime.connectionString), - }).pipe(Effect.orDie), - (resource) => resource.close().pipe(Effect.orDie), - ); - const pool = yield* Effect.acquireRelease( - Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), - (resource) => promiseEffect(endPool.bind(undefined, resource)).pipe(Effect.orDie), - ); - const admin = yield* makeTestDatabaseFromPool(pool, partyRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ); - const { privateKey, publicKey } = yield* promiseEffect( - generateKeyPair.bind(undefined, 'Ed25519'), - ); - const kid = `ares-live-${randomUUID()}`; - const issuer = 'https://disposable-shell.ontos.test'; - const publicJwk = yield* promiseEffect(exportJWK.bind(undefined, publicKey)); - const jwk = { ...publicJwk, alg: 'EdDSA', kid, use: 'sig' }; - const encodedPublicJwks = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))( - { keys: [jwk] }, - ); - const sign = (principal: typeof fixture.manager) => +it.live( + 'exported ARES coordinator uses real authorized HTTP commands, canonical persistence and reviewed correction', + () => + Effect.gen(function* aresGovernedTestEffect() { + const connections = yield* loadDatabaseConnectionPair(); + const fixture = yield* Effect.acquireRelease( + makeLiveOperationFixture({ + actionKeys: [ + addContactPointAction, + addPartyOfficialIdentifierAction, + correctPartyFactAction, + createPartyAction, + resolveDuplicateCandidateCreateAction, + updatePartyAction, + ].map(({ descriptor }) => descriptor.actionKey), + runtimeConnectionString: Redacted.make(connections.runtime.connectionString), + }).pipe(Effect.orDie), + (resource) => resource.close().pipe(Effect.orDie), + ); + const pool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), + endPool, + ); + const admin = yield* makeTestDatabaseFromPool(pool, partyRelations); + const { privateKey, publicKey } = yield* Effect.promise(() => generateKeyPair('Ed25519')); + const kid = `ares-live-${randomUUID()}`; + const issuer = 'https://disposable-shell.ontos.test'; + const publicJwk = yield* Effect.promise(exportJWK.bind(undefined, publicKey)); + const jwk = { ...publicJwk, alg: 'EdDSA', kid, use: 'sig' }; + const encodedPublicJwks = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ + keys: [jwk], + }); + const sign = (principal: typeof fixture.manager) => + Effect.promise(() => new SignJWT({ principal, ver: 1 }) .setProtectedHeader({ alg: 'EdDSA', kid, typ: 'JWT' }) .setIssuer(issuer) @@ -158,434 +142,428 @@ test('exported ARES coordinator uses real authorized HTTP commands, canonical pe .setIssuedAt() .setExpirationTime('5m') .setJti(randomUUID()) - .sign(privateKey); - const authorization = () => - promiseEffect(sign.bind(undefined, fixture.manager)).pipe( - Effect.map((signedToken) => `Bearer ${signedToken}`), - ); - const gateway = makeOperationGateway(() => - promiseEffect(sign.bind(undefined, fixture.manager)).pipe( - Effect.map((signedToken) => ({ expiresAt: 0, token: signedToken })), - ), + .sign(privateKey), ); - let providerRequests = 0; - const provider = HttpClient.make((request, url) => { - assert.equal( - url.href, - 'https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/ekonomicke-subjekty/27074358', - ); - providerRequests += 1; - return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(rawSubject))); - }); - const upstream = AresSubjectServiceLive.pipe( - Layer.provide(Layer.succeed(HttpClient.HttpClient, provider)), + const authorization = () => + sign(fixture.manager).pipe(Effect.map((token) => `Bearer ${token}`)); + const gateway = makeOperationGateway(() => + sign(fixture.manager).pipe( + Effect.map((signedToken) => ({ expiresAt: 0, token: signedToken })), + ), + ); + let providerRequests = 0; + const provider = HttpClient.make((request, url) => { + expect(url.href).toBe( + 'https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/ekonomicke-subjekty/27074358', ); - const redemption = GatewayAssertionRedemptionLive.pipe( - Layer.provide(GatewayAssertionRedemptionDatabaseLive), - Layer.provide(Layer.succeed(DatabaseConfig, connections.runtime)), - ); - const api = HttpApi.make('PartyRegistryApi') - .add(partyRegistryApi.groups.partyCommands) - .add(partyRegistryApi.groups.aresLookup) - .add(partyRegistryApi.groups.partyDetail) - .add(partyRegistryApi.groups.partyOfficialIdentifierHistory) - .add(partyRegistryApi.groups.partyContactPoints); - const handlers = Layer.mergeAll( - partyRegistryCommandsLive, - partyDetailReadApiLive, - partyOfficialIdentifierHistoryReadApiLive, - partyContactPointsReadApiLive, - aresLookupReadApiLive.pipe(Layer.provide(upstream)), - ).pipe( - Layer.provide(ActionPrincipalVerifierLive), - Layer.provide(redemption), - Layer.provide(fixture.layer), - Layer.provide( - ConfigProvider.layer( - ConfigProvider.fromUnknown({ - ONTOS_GATEWAY_ISSUER: issuer, - ONTOS_GATEWAY_PUBLIC_JWKS: encodedPublicJwks, - }), - ), + providerRequests += 1; + return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(rawSubject))); + }); + const upstream = AresSubjectServiceLive.pipe( + Layer.provide(Layer.succeed(HttpClient.HttpClient, provider)), + ); + const redemption = GatewayAssertionRedemptionLive.pipe( + Layer.provide(GatewayAssertionRedemptionDatabaseLive), + Layer.provide(Layer.succeed(DatabaseConfig, connections.runtime)), + ); + const api = HttpApi.make('PartyRegistryApi') + .add(partyRegistryApi.groups.partyCommands) + .add(partyRegistryApi.groups.aresLookup) + .add(partyRegistryApi.groups.partyDetail) + .add(partyRegistryApi.groups.partyOfficialIdentifierHistory) + .add(partyRegistryApi.groups.partyContactPoints); + const handlers = Layer.mergeAll( + partyRegistryCommandsLive, + partyDetailReadApiLive, + partyOfficialIdentifierHistoryReadApiLive, + partyContactPointsReadApiLive, + aresLookupReadApiLive.pipe(Layer.provide(upstream)), + ).pipe( + Layer.provide(ActionPrincipalVerifierLive), + Layer.provide(redemption), + Layer.provide(fixture.layer), + Layer.provide( + ConfigProvider.layer( + ConfigProvider.fromUnknown({ + ONTOS_GATEWAY_ISSUER: issuer, + ONTOS_GATEWAY_PUBLIC_JWKS: encodedPublicJwks, + }), ), - ); - const app = yield* Effect.acquireRelease( - Effect.sync(() => - HttpRouter.toWebHandler( - HttpApiBuilder.layer(api).pipe( - Layer.provide(handlers), - Layer.provideMerge(fixture.layer), - Layer.provideMerge(upstream), - Layer.provide(HttpServer.layerServices), - ), - { disableLogger: true }, + ), + ); + const app = yield* Effect.acquireRelease( + Effect.sync(() => + HttpRouter.toWebHandler( + HttpApiBuilder.layer(api).pipe( + Layer.provide(handlers), + Layer.provideMerge(fixture.layer), + Layer.provideMerge(upstream), + Layer.provide(HttpServer.layerServices), ), + { disableLogger: true }, ), - (resource) => promiseEffect(resource.dispose.bind(resource)).pipe(Effect.orDie), - ); - const inMemoryFetch: typeof fetch = (input, init) => - app.handler(new Request(input, init), emptyRequestContext); - const runHttpEffect = (effect: Effect.Effect) => - effect.pipe(Effect.provideService(FetchHttpClient.Fetch, inMemoryFetch)); - const baseUrl = 'https://party.ontos.test'; - const options = () => ({ - baseUrl, - correlationId: randomUUID(), - idempotencyKey: randomUUID(), - }); - const create = Effect.fn('AresGovernedTest.create')(function* createEffect() { - const created = yield* runHttpEffect( - createPartyWithAuthorization( - { - candidate: { - partyType: 'ORGANIZATION', - officialIdentifiers: [], - evidenceRefs: ['live-review/ares'], - subjectEvidence: subjectEvidence.map((item) => ({ - ...item, - subjectKey: partySubjectKeyFromString(randomUUID()), - })), - provenance: { method: 'DOCUMENT', source: 'live-acceptance' }, - validFrom: DateTime.makeUnsafe('2020-01-01T00:00:00.000Z'), - }, - }, - yield* authorization(), - options(), - ), - ); - assert.equal(created.outcome, 'AMBIGUOUS'); - assert.ok(created.outcome === 'AMBIGUOUS'); - const reviewed = yield* runHttpEffect( - resolveDuplicateCandidateCreateWithAuthorization( - { - caseRef: created.caseRef, - expectedRevision: 1, - reason: 'Reviewed concrete organization without a strong identifier', - }, - yield* authorization(), - options(), - ), - ); - assert.ok(reviewed.partyRef); - return reviewed.partyRef; - }); - const lookup = Effect.fn('AresGovernedTest.lookup')(function* lookupEffect() { - return yield* runHttpEffect( - executeAresLookupWithAuthorization( - { ico: lookupIco }, - yield* authorization(), - randomUUID(), - { baseUrl }, - ), - ); - }); - const detail = Effect.fn('AresGovernedTest.detail')(function* detailEffect( - partyRef: PartyRef, - ) { - return yield* runHttpEffect( - executePartyDetailWithAuthorization( - { partyRef, includeFactHistory: true }, - yield* authorization(), - randomUUID(), - { baseUrl }, - ), - ); - }); - const state = Effect.fn('AresGovernedTest.state')(() => - Effect.all( + ), + (resource) => Effect.promise(resource.dispose.bind(resource)).pipe(Effect.orDie), + ); + const inMemoryFetch: typeof fetch = (input, init) => + app.handler(new Request(input, init), emptyRequestContext); + const runHttpEffect = (effect: Effect.Effect) => + effect.pipe(Effect.provideService(FetchHttpClient.Fetch, inMemoryFetch)); + const baseUrl = 'https://party.ontos.test'; + const options = () => ({ + baseUrl, + correlationId: randomUUID(), + idempotencyKey: randomUUID(), + }); + const create = Effect.fn('AresGovernedTest.create')(function* createEffect() { + const created = yield* runHttpEffect( + createPartyWithAuthorization( { - assertions: admin - .select() - .from(partyFactAssertions) - .where(eq(partyFactAssertions.tenantId, fixture.tenantId)), - claims: admin - .select() - .from(partyIdentifierClaims) - .where(eq(partyIdentifierClaims.tenantId, fixture.tenantId)), - contacts: admin - .select() - .from(partyContactPoints) - .where(eq(partyContactPoints.tenantId, fixture.tenantId)), - core: fixture.evidence(), - identifiers: admin - .select() - .from(partyOfficialIdentifiers) - .where(eq(partyOfficialIdentifiers.tenantId, fixture.tenantId)), + candidate: { + partyType: 'ORGANIZATION', + officialIdentifiers: [], + evidenceRefs: ['live-review/ares'], + subjectEvidence: subjectEvidence.map((item) => ({ + ...item, + subjectKey: partySubjectKeyFromString(randomUUID()), + })), + provenance: { method: 'DOCUMENT', source: 'live-acceptance' }, + validFrom: DateTime.makeUnsafe('2020-01-01T00:00:00.000Z'), + }, }, - { concurrency: 5 }, + yield* authorization(), + options(), ), ); - const partyRef = yield* create(); - const replayAuthorization = yield* authorization(); - const replayLookup = () => - runHttpEffect( - executeAresLookupWithAuthorization( - { ico: lookupIco }, - replayAuthorization, - randomUUID(), - { baseUrl }, - ), - ); - yield* replayLookup(); - const providerRequestsBeforeReplay = providerRequests; - const replayRejected = yield* replayLookup().pipe( - Effect.as(false), - Effect.catchTag('AresLookupAuthenticationProblem', () => Effect.succeed(true)), - ); - assert.equal(replayRejected, true); - assert.equal(providerRequests, providerRequestsBeforeReplay); - const observation = yield* lookup(); - const encodedObservation = - yield* Schema.encodeEffect(AresSubjectEvidenceSchema)(observation); - const requestFor = (target: PartyRef): AresApplyRequest => ({ - correlationId: randomUUID(), - observation: encodedObservation, - partyRef: target, - userConfirmed: true, - selections: [ - { - fact: 'BUSINESS_NAME', - route: 'PARTY_UPDATE', - idempotencyKey: randomUUID(), - payload: { - partyRef: target, - displayName: rawSubject.obchodniJmeno, - expectedRevision: 1, - validFrom: observation.observedAt, - provenanceMethod: 'ARES_USER_CONFIRMED', - provenanceSource: 'ARES', - }, - }, - { - fact: 'ICO', - route: 'IDENTIFIER_ADD', - idempotencyKey: randomUUID(), - payload: { - partyRef: target, - identifier: { identifierType: 'ICO', value: '27074358', verification: 'VERIFIED' }, - validFrom: observation.observedAt, - provenanceMethod: 'ARES_USER_CONFIRMED', - provenanceSource: 'ARES', - }, - }, + expect(created.outcome).toBe('AMBIGUOUS'); + assert.isOk(created.outcome === 'AMBIGUOUS'); + + const reviewed = yield* runHttpEffect( + resolveDuplicateCandidateCreateWithAuthorization( { - fact: 'REGISTERED_ADDRESS', - route: 'CONTACT_POINT_ADD', - idempotencyKey: randomUUID(), - payload: { - partyRef: target, - privacyClassification: 'PUBLIC', - validFrom: observation.observedAt, - contactPoint: { - type: 'ADDRESS', - address: { - addressLine1: 'Main 10', - city: 'Praha', - countryCode: 'CZ', - postalCode: '11000', - }, - purposes: [ - { - preferred: false, - purpose: 'REGISTERED', - registryContext: { jurisdiction: 'CZ', registryKey: 'ARES' }, - }, - ], - }, - provenance: { - authoritative: true, - evidenceReference: 'live-review/ares', - method: 'PROVIDER_OBSERVATION', - source: 'EXTERNAL_EVIDENCE', - }, - verification: { state: 'UNVERIFIED' }, - }, + caseRef: created.caseRef, + expectedRevision: 1, + reason: 'Reviewed concrete organization without a strong identifier', }, - ], - }); - const request = requestFor(partyRef); - const beforeUnconfirmed = yield* state(); - const unconfirmed = yield* runHttpEffect( - applyAresObservation({ ...request, userConfirmed: false }, { gateway, baseUrl }).pipe( - Effect.result, - ), - ); - assert.ok(Result.isFailure(unconfirmed)); - assert.ok( - Schema.is(Schema.TaggedStruct('AresApplySelectionInvalid', {}))(unconfirmed.failure), - ); - const afterUnconfirmed = yield* state(); - assert.equal( - afterUnconfirmed.core.invocations.length, - beforeUnconfirmed.core.invocations.length, - ); - const applied = yield* runHttpEffect(applyAresObservation(request, { gateway, baseUrl })); - const appliedMessage = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))( - applied, - ); - assert.ok( - Schema.is(Schema.TaggedStruct('AresApplyCompleted', {}))(applied), - appliedMessage, - ); - assert.equal(applied.completed.length, 3); - const persisted = yield* state(); - assert.equal(persisted.claims.length, 1); - assert.equal(persisted.contacts.length, 1); - const identifierEvidence = persisted.identifiers[0]?.externalEvidence; - const contactEvidence = persisted.contacts[0]?.externalEvidence; - assert.ok(identifierEvidence); - assert.ok(contactEvidence); - assert.equal(identifierEvidence.queryIco, '27074358'); - assert.equal(identifierEvidence.observedAt, encodedObservation.observedAt); - assert.equal(identifierEvidence.servedAt, encodedObservation.servedAt); - assert.equal(identifierEvidence.providerChangedOn, encodedObservation.providerChangedOn); - assert.equal(identifierEvidence.providerRecordRef, encodedObservation.providerRecordRef); - assert.equal(contactEvidence.observedAt, encodedObservation.observedAt); - assert.equal(contactEvidence.providerChangedOn, encodedObservation.providerChangedOn); - assert.equal(contactEvidence.providerRecordRef, encodedObservation.providerRecordRef); - assert.equal( - persisted.assertions.find((item) => item.factKind === 'DISPLAY_NAME')?.externalEvidence - ?.decidedAt, - encodedObservation.servedAt, - ); - assert.equal(persisted.core.events.length, 4); - assert.equal(persisted.core.outbox.length, 4); - assert.ok(persisted.core.invocations.every((item) => item.status === 'succeeded')); - const replay = yield* runHttpEffect(applyAresObservation(request, { gateway, baseUrl })); - assert.ok(Schema.is(Schema.TaggedStruct('AresApplyCompleted', {}))(replay)); - assert.equal(replay.completed.length, 0); - assert.equal(replay.skipped.length, 3); - const afterReplay = yield* state(); - assert.equal(afterReplay.core.events.length, persisted.core.events.length); - const deniedGateway = makeOperationGateway(() => - promiseEffect(sign.bind(undefined, fixture.denied)).pipe( - Effect.map((signedToken) => ({ expiresAt: 0, token: signedToken })), + yield* authorization(), + options(), ), ); - const denied = yield* runHttpEffect( - applyAresObservation(request, { gateway: deniedGateway, baseUrl }).pipe(Effect.result), - ); - assert.ok(Result.isFailure(denied)); - assert.ok(Schema.is(Schema.TaggedStruct('AresLookupForbiddenProblem', {}))(denied.failure)); - const afterDenied = yield* state(); - assert.equal(afterDenied.core.invocations.length, persisted.core.invocations.length); + assert.isOk(reviewed.partyRef); - const collisionParty = yield* create(); - const collision = yield* runHttpEffect( - applyAresObservation(requestFor(collisionParty), { gateway, baseUrl }), - ); - const collisionOutcome = Match.value(collision).pipe( - Match.tag('AresApplyPartiallyCompleted', (outcome) => outcome), - Match.orElse(() => - assert.fail(`Expected partial completion, received ${collision._tag}`), + return reviewed.partyRef; + }); + const lookup = Effect.fn('AresGovernedTest.lookup')(function* lookupEffect() { + return yield* runHttpEffect( + executeAresLookupWithAuthorization( + { ico: lookupIco }, + yield* authorization(), + randomUUID(), + { baseUrl }, ), ); - assert.equal(collisionOutcome.completed.length, 1); - assert.equal(collisionOutcome.failed.fact, 'ICO'); - assert.equal(collisionOutcome.failed.recovery, 'RESOLVE_STANDARD_ACTION_BEFORE_RETRY'); - const afterCollision = yield* state(); - assert.equal(afterCollision.claims.length, 1); - assert.equal(afterCollision.contacts.length, 1); - const collisionDetail = yield* detail(collisionParty); - assert.equal( - Option.getOrUndefined(collisionDetail.party.displayName), - rawSubject.obchodniJmeno, + }); + const detail = Effect.fn('AresGovernedTest.detail')(function* detailEffect( + partyRef: PartyRef, + ) { + return yield* runHttpEffect( + executePartyDetailWithAuthorization( + { partyRef, includeFactHistory: true }, + yield* authorization(), + randomUUID(), + { baseUrl }, + ), ); - - const erroneousParty = yield* create(); - const logical = deriveAresEvidenceApplication({ - canonical: { - archived: false, - displayName: null, - icoValues: [], - identityAmbiguous: false, - partyType: 'ORGANIZATION', - registeredAddresses: [], + }); + const state = Effect.fn('AresGovernedTest.state')(() => + Effect.all( + { + assertions: admin + .select() + .from(partyFactAssertions) + .where(eq(partyFactAssertions.tenantId, fixture.tenantId)), + claims: admin + .select() + .from(partyIdentifierClaims) + .where(eq(partyIdentifierClaims.tenantId, fixture.tenantId)), + contacts: admin + .select() + .from(partyContactPoints) + .where(eq(partyContactPoints.tenantId, fixture.tenantId)), + core: fixture.evidence(), + identifiers: admin + .select() + .from(partyOfficialIdentifiers) + .where(eq(partyOfficialIdentifiers.tenantId, fixture.tenantId)), }, - decidedAt: encodedObservation.servedAt, - evidence: encodedObservation, - selectedFacts: ['BUSINESS_NAME'], - userConfirmed: true, - }); - const [decision] = logical.factDecisions; - assert.ok(decision); - yield* runHttpEffect( - updatePartyWithAuthorization( - { - partyRef: erroneousParty, - displayName: 'Clerical wrong name', + { concurrency: 5 }, + ), + ); + const partyRef = yield* create(); + const replayAuthorization = yield* authorization(); + const replayLookup = () => + runHttpEffect( + executeAresLookupWithAuthorization( + { ico: lookupIco }, + replayAuthorization, + randomUUID(), + { baseUrl }, + ), + ); + yield* replayLookup(); + const providerRequestsBeforeReplay = providerRequests; + const replayRejected = yield* replayLookup().pipe( + Effect.as(false), + Effect.catchTag('AresLookupAuthenticationProblem', () => Effect.succeed(true)), + ); + expect(replayRejected).toBe(true); + expect(providerRequests).toBe(providerRequestsBeforeReplay); + const observation = yield* lookup(); + const encodedObservation = yield* Schema.encodeEffect(AresSubjectEvidenceSchema)(observation); + const requestFor = (target: PartyRef): AresApplyRequest => ({ + correlationId: randomUUID(), + observation: encodedObservation, + partyRef: target, + userConfirmed: true, + selections: [ + { + fact: 'BUSINESS_NAME', + route: 'PARTY_UPDATE', + idempotencyKey: randomUUID(), + payload: { + partyRef: target, + displayName: rawSubject.obchodniJmeno, expectedRevision: 1, validFrom: observation.observedAt, provenanceMethod: 'ARES_USER_CONFIRMED', provenanceSource: 'ARES', - externalEvidence: makeAresAppliedEvidence(logical, decision), }, - yield* authorization(), - options(), - ), - ); - const erroneous = yield* detail(erroneousParty); - const assertion = erroneous.currentFactAssertions.find( - (item) => item.factKind === 'DISPLAY_NAME', - ); - assert.ok(assertion); - const correctionPayload = yield* Schema.decodeUnknownEffect( - IdentityCorrectionCommandSchema, - )({ - partyId: erroneousParty.resourceId, - factKind: 'DISPLAY_NAME' as const, - targetAssertionId: assertion.assertionId, - replacementValue: rawSubject.obchodniJmeno, - evidenceRefs: ['live-review/ares'], - evidenceSource: 'MANUAL_REVIEW' as const, - policyVersion: 'party-correction.v1' as const, - reasonCode: 'WRONG_IDENTITY_VALUE' as const, - provenance: { method: 'DOCUMENT_REVIEW', source: 'live-acceptance' }, - subjectEvidence, - }); - const beforeReview = yield* state(); - const review = yield* runHttpEffect( - applyAresObservation( - { - correlationId: randomUUID(), - observation: encodedObservation, - partyRef: erroneousParty, - userConfirmed: true, - selections: [ - { - fact: 'BUSINESS_NAME', - route: 'PARTY_CORRECTION', - idempotencyKey: randomUUID(), - payload: correctionPayload, + }, + { + fact: 'ICO', + route: 'IDENTIFIER_ADD', + idempotencyKey: randomUUID(), + payload: { + partyRef: target, + identifier: { identifierType: 'ICO', value: '27074358', verification: 'VERIFIED' }, + validFrom: observation.observedAt, + provenanceMethod: 'ARES_USER_CONFIRMED', + provenanceSource: 'ARES', + }, + }, + { + fact: 'REGISTERED_ADDRESS', + route: 'CONTACT_POINT_ADD', + idempotencyKey: randomUUID(), + payload: { + partyRef: target, + privacyClassification: 'PUBLIC', + validFrom: observation.observedAt, + contactPoint: { + type: 'ADDRESS', + address: { + addressLine1: 'Main 10', + city: 'Praha', + countryCode: 'CZ', + postalCode: '11000', }, - ], + purposes: [ + { + preferred: false, + purpose: 'REGISTERED', + registryContext: { jurisdiction: 'CZ', registryKey: 'ARES' }, + }, + ], + }, + provenance: { + authoritative: true, + evidenceReference: 'live-review/ares', + method: 'PROVIDER_OBSERVATION', + source: 'EXTERNAL_EVIDENCE', + }, + verification: { state: 'UNVERIFIED' }, }, - { gateway, baseUrl }, - ), - ); - const reviewOutcome = Match.value(review).pipe( - Match.tag('AresApplyDeferred', (outcome) => outcome), - Match.orElse(() => assert.fail(`Expected deferred review, received ${review._tag}`)), - ); - assert.equal(reviewOutcome.application.outcome, 'CORRECTION_CANDIDATE'); - assert.equal( - reviewOutcome.correctionCandidates[0]?.targetAssertionId, - assertion.assertionId, - ); - const afterReview = yield* state(); - assert.equal(afterReview.core.invocations.length, beforeReview.core.invocations.length); - yield* runHttpEffect( - correctPartyFactWithAuthorization(correctionPayload, yield* authorization(), options()), - ); - const corrected = yield* detail(erroneousParty); - assert.equal(Option.getOrUndefined(corrected.party.displayName), rawSubject.obchodniJmeno); - const correctedState = yield* state(); - assert.ok( - correctedState.assertions.some( - (item) => item.assertionId === assertion.assertionId && item.state !== 'ACTIVE', - ), - ); - assert.ok(providerRequests >= 1); - }), - ), - )); + }, + ], + }); + const request = requestFor(partyRef); + const beforeUnconfirmed = yield* state(); + const unconfirmed = yield* runHttpEffect( + applyAresObservation({ ...request, userConfirmed: false }, { gateway, baseUrl }).pipe( + Effect.result, + ), + ); + assert.isOk( + 'failure' in unconfirmed && + Predicate.isTagged(unconfirmed.failure, 'AresApplySelectionInvalid'), + ); + + const afterUnconfirmed = yield* state(); + expect(afterUnconfirmed.core.invocations.length).toBe( + beforeUnconfirmed.core.invocations.length, + ); + const applied = yield* runHttpEffect(applyAresObservation(request, { gateway, baseUrl })); + const appliedMessage = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))( + applied, + ); + assert.isOk(Predicate.isTagged(applied, 'AresApplyCompleted'), appliedMessage); + + expect(applied.completed.length).toBe(3); + const persisted = yield* state(); + expect(persisted.claims.length).toBe(1); + expect(persisted.contacts.length).toBe(1); + const identifierEvidence = persisted.identifiers[0]?.externalEvidence; + const contactEvidence = persisted.contacts[0]?.externalEvidence; + assert.ok(identifierEvidence); + assert.ok(contactEvidence); + expect(identifierEvidence.queryIco).toBe('27074358'); + expect(identifierEvidence.observedAt).toBe(encodedObservation.observedAt); + expect(identifierEvidence.servedAt).toBe(encodedObservation.servedAt); + expect(identifierEvidence.providerChangedOn).toBe(encodedObservation.providerChangedOn); + expect(identifierEvidence.providerRecordRef).toBe(encodedObservation.providerRecordRef); + expect(contactEvidence.observedAt).toBe(encodedObservation.observedAt); + expect(contactEvidence.providerChangedOn).toBe(encodedObservation.providerChangedOn); + expect(contactEvidence.providerRecordRef).toBe(encodedObservation.providerRecordRef); + expect( + persisted.assertions.find((item) => item.factKind === 'DISPLAY_NAME')?.externalEvidence + ?.decidedAt, + ).toBe(encodedObservation.servedAt); + expect(persisted.core.events.length).toBe(4); + expect(persisted.core.outbox.length).toBe(4); + assert.isOk(persisted.core.invocations.every((item) => item.status === 'succeeded')); + + const replay = yield* runHttpEffect(applyAresObservation(request, { gateway, baseUrl })); + assert.isOk(Predicate.isTagged(replay, 'AresApplyCompleted')); + + expect(replay.completed.length).toBe(0); + expect(replay.skipped.length).toBe(3); + const afterReplay = yield* state(); + expect(afterReplay.core.events.length).toBe(persisted.core.events.length); + const deniedGateway = makeOperationGateway(() => + sign(fixture.denied).pipe( + Effect.map((signedToken) => ({ expiresAt: 0, token: signedToken })), + ), + ); + const denied = yield* runHttpEffect( + applyAresObservation(request, { gateway: deniedGateway, baseUrl }).pipe(Effect.result), + ); + assert.isOk( + 'failure' in denied && Predicate.isTagged(denied.failure, 'AresLookupForbiddenProblem'), + ); + + const afterDenied = yield* state(); + expect(afterDenied.core.invocations.length).toBe(persisted.core.invocations.length); + + const collisionParty = yield* create(); + const collision = yield* runHttpEffect( + applyAresObservation(requestFor(collisionParty), { gateway, baseUrl }), + ); + const collisionOutcome = Match.value(collision).pipe( + Match.tag('AresApplyPartiallyCompleted', (outcome) => outcome), + Match.orElse(() => assert.fail(`Expected partial completion, received ${collision._tag}`)), + ); + expect(collisionOutcome.completed.length).toBe(1); + expect(collisionOutcome.failed.fact).toBe('ICO'); + expect(collisionOutcome.failed.recovery).toBe('RESOLVE_STANDARD_ACTION_BEFORE_RETRY'); + const afterCollision = yield* state(); + expect(afterCollision.claims.length).toBe(1); + expect(afterCollision.contacts.length).toBe(1); + const collisionDetail = yield* detail(collisionParty); + expect(Option.getOrUndefined(collisionDetail.party.displayName)).toBe( + rawSubject.obchodniJmeno, + ); + + const erroneousParty = yield* create(); + const logical = deriveAresEvidenceApplication({ + canonical: { + archived: false, + displayName: null, + icoValues: [], + identityAmbiguous: false, + partyType: 'ORGANIZATION', + registeredAddresses: [], + }, + decidedAt: encodedObservation.servedAt, + evidence: encodedObservation, + selectedFacts: ['BUSINESS_NAME'], + userConfirmed: true, + }); + const [decision] = logical.factDecisions; + assert.isOk(decision); + + yield* runHttpEffect( + updatePartyWithAuthorization( + { + partyRef: erroneousParty, + displayName: 'Clerical wrong name', + expectedRevision: 1, + validFrom: observation.observedAt, + provenanceMethod: 'ARES_USER_CONFIRMED', + provenanceSource: 'ARES', + externalEvidence: makeAresAppliedEvidence(logical, decision), + }, + yield* authorization(), + options(), + ), + ); + const erroneous = yield* detail(erroneousParty); + const assertion = erroneous.currentFactAssertions.find( + (item) => item.factKind === 'DISPLAY_NAME', + ); + assert.isOk(assertion); + + const correctionPayload = yield* Schema.decodeUnknownEffect(IdentityCorrectionCommandSchema)({ + partyId: erroneousParty.resourceId, + factKind: 'DISPLAY_NAME' as const, + targetAssertionId: assertion.assertionId, + replacementValue: rawSubject.obchodniJmeno, + evidenceRefs: ['live-review/ares'], + evidenceSource: 'MANUAL_REVIEW' as const, + policyVersion: 'party-correction.v1' as const, + reasonCode: 'WRONG_IDENTITY_VALUE' as const, + provenance: { method: 'DOCUMENT_REVIEW', source: 'live-acceptance' }, + subjectEvidence, + }); + const beforeReview = yield* state(); + const review = yield* runHttpEffect( + applyAresObservation( + { + correlationId: randomUUID(), + observation: encodedObservation, + partyRef: erroneousParty, + userConfirmed: true, + selections: [ + { + fact: 'BUSINESS_NAME', + route: 'PARTY_CORRECTION', + idempotencyKey: randomUUID(), + payload: correctionPayload, + }, + ], + }, + { gateway, baseUrl }, + ), + ); + const reviewOutcome = Match.value(review).pipe( + Match.tag('AresApplyDeferred', (outcome) => outcome), + Match.orElse(() => assert.fail(`Expected deferred review, received ${review._tag}`)), + ); + expect(reviewOutcome.application.outcome).toBe('CORRECTION_CANDIDATE'); + expect(reviewOutcome.correctionCandidates[0]?.targetAssertionId).toBe(assertion.assertionId); + const afterReview = yield* state(); + expect(afterReview.core.invocations.length).toBe(beforeReview.core.invocations.length); + yield* runHttpEffect( + correctPartyFactWithAuthorization(correctionPayload, yield* authorization(), options()), + ); + const corrected = yield* detail(erroneousParty); + expect(Option.getOrUndefined(corrected.party.displayName)).toBe(rawSubject.obchodniJmeno); + const correctedState = yield* state(); + assert.isOk( + correctedState.assertions.some( + (item) => item.assertionId === assertion.assertionId && item.state !== 'ACTIVE', + ), + ); + + assert.isOk(providerRequests >= 1); + }), +); diff --git a/app/verticals/party-registry/tests/integration/database-boundary.test.ts b/app/verticals/party-registry/tests/integration/database-boundary.test.ts index 625c4912a..97364dc05 100644 --- a/app/verticals/party-registry/tests/integration/database-boundary.test.ts +++ b/app/verticals/party-registry/tests/integration/database-boundary.test.ts @@ -1,19 +1,11 @@ -import { - makeEffectTestCallback as nativeTestCallback, - runEffectTestPromise, - runEffectTestSync as runNativeSync, -} from '@app/core-runtime/testing/effect-runtime'; +import { hasPostgreSqlCode, openBoundaryDatabases } from '../support/database-boundary.ts'; +import { purgeFixtureRows } from '../../../../packages/core-runtime/tests/support/fixture-cleanup.ts'; +import { assert, expect, it } from 'effect-rstest'; -import { DateTime, Effect, Exit as NativeExit, Scope as NativeScope } from 'effect'; -// @effect-diagnostics asyncFunction:off globalDate:off -- Existing compatibility boundary; expires: 2026-12-31. +import { DateTime, Effect, Schema } from 'effect'; import { and, eq, gt, inArray, isNull, lte, or, sql } from 'drizzle-orm'; -import assert from 'node:assert/strict'; -import test, { after as afterNativeDatabase } from 'node:test'; -import type { Pool } from 'pg'; import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.ts'; -import { hasPostgreSqlCode, openBoundaryDatabases } from '../support/database-boundary.ts'; -import { purgeFixtureRows } from '../../../../packages/core-runtime/tests/support/fixture-cleanup.ts'; import { RuleKeySchema } from '../../shared/domain/matching-contracts.ts'; import { counterparties, @@ -37,20 +29,10 @@ import { } from '../../src/db/schema.ts'; import type { PartyTransaction } from '../../src/db/types.ts'; -const nativeDatabaseScope = runNativeSync(NativeScope.make()); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), -); - -/** Both boundary roles read the same owned schema through the scope closed after these tests. */ -const openPartyDatabase = async (pool: Pool) => - await runEffectTestPromise( - makeTestDatabaseFromPool(pool, partyRelations).pipe(NativeScope.provide(nativeDatabaseScope)), - ); - const tenantA = 'a1000000-0000-4000-8000-000000000001'; const tenantB = 'a1000000-0000-4000-8000-000000000002'; const legalEntityA = 'a2000000-0000-4000-8000-000000000001'; +const legalEntityB = 'a2000000-0000-4000-8000-000000000002'; const partyOrganizationA = 'a3000000-0000-4000-8000-000000000001'; const partyOrganizationA2 = 'a3000000-0000-4000-8000-000000000002'; const partyPersonA = 'a3000000-0000-4000-8000-000000000003'; @@ -64,19 +46,23 @@ const emailA = 'a6000000-0000-4000-8000-000000000001'; const emailA2 = 'a6000000-0000-4000-8000-000000000002'; const addressA = 'a6000000-0000-4000-8000-000000000003'; const addressA2 = 'a6000000-0000-4000-8000-000000000004'; +const addressB = 'a6000000-0000-4000-8000-000000000005'; const counterpartyA = 'a7000000-0000-4000-8000-000000000001'; +const counterpartyB = 'a7000000-0000-4000-8000-000000000002'; const relationshipA = 'a8000000-0000-4000-8000-000000000001'; const caseA = 'a9000000-0000-4000-8000-000000000001'; const actionA = 'aa000000-0000-4000-8000-000000000001'; const principalA = 'ab000000-0000-4000-8000-000000000001'; const fixtureTenants = [tenantA, tenantB] as const; -test('enforces Party owner invariants, tenant isolation, and independent fact lifecycles', async () => { - const { admin, adminPool, runtime, runtimePool } = await openBoundaryDatabases(openPartyDatabase); +it.live('enforces Party owner invariants, tenant isolation, and independent fact lifecycles', () => + Effect.gen(function* testEffect1() { + const { admin, runtime } = yield* openBoundaryDatabases( + (pool) => makeTestDatabaseFromPool(pool, partyRelations), + 1, + ); - // Ordered child-before-parent so every delete respects the owned foreign keys. - const cleanup = async (): Promise => { - await runEffectTestPromise( + const cleanup = () => purgeFixtureRows( [ partyCorrections, @@ -97,15 +83,12 @@ test('enforces Party owner invariants, tenant isolation, and independent fact li partyFactAssertions, parties, ].map((table) => admin.delete(table).where(inArray(table.tenantId, fixtureTenants))), - ), - ); - }; + ); - const withTenant = ( - tenantId: string, - operation: (transaction: PartyTransaction) => Effect.Effect, - ): Promise => - runEffectTestPromise( + const withTenant = ( + tenantId: string, + operation: (transaction: PartyTransaction) => Effect.Effect, + ) => runtime.transaction((transaction) => Effect.gen(function* transactionTestBody() { yield* transaction.execute( @@ -114,66 +97,55 @@ test('enforces Party owner invariants, tenant isolation, and independent fact li ); return yield* operation(transaction); }), - ), - ); - - try { - const runtimeRole = await runEffectTestPromise( - runtime.execute<{ rolbypassrls: boolean; rolsuper: boolean }>( - sql`select rolbypassrls, rolsuper from pg_roles where rolname = current_user`, - 'objects', - ), - ); - assert.deepEqual(runtimeRole, [{ rolbypassrls: false, rolsuper: false }]); - await cleanup(); - await runEffectTestPromise( - admin.insert(parties).values([ - { - currentDisplayName: 'Organization A', - currentType: 'ORGANIZATION', - partyId: partyOrganizationA, - tenantId: tenantA, - }, - { - currentDisplayName: 'Organization A2', - currentType: 'ORGANIZATION', - partyId: partyOrganizationA2, - tenantId: tenantA, - }, - { - currentDisplayName: 'Person A', - currentType: 'PERSON', - partyId: partyPersonA, - tenantId: tenantA, - }, - { - currentType: 'ORGANIZATION', - partyId: partyOrganizationB, - tenantId: tenantB, - }, - ]), - ); - - const [unnamedParty] = await runEffectTestPromise( - admin - .select({ displayName: parties.currentDisplayName }) - .from(parties) - .where(eq(parties.partyId, partyOrganizationB)), - ); - assert.equal(unnamedParty?.displayName, null); + ); - assert.deepEqual(await runEffectTestPromise(runtime.select().from(parties)), []); - assert.deepEqual( - await withTenant(tenantA, (transaction) => + yield* Effect.addFinalizer(() => cleanup().pipe(Effect.orDie)); + const runtimeRole = yield* runtime.execute<{ rolbypassrls: boolean; rolsuper: boolean }>( + sql`select rolbypassrls, rolsuper from pg_roles where rolname = current_user`, + 'objects', + ); + expect(runtimeRole).toEqual([{ rolbypassrls: false, rolsuper: false }]); + yield* cleanup(); + yield* admin.insert(parties).values([ + { + currentDisplayName: 'Organization A', + currentType: 'ORGANIZATION', + partyId: partyOrganizationA, + tenantId: tenantA, + }, + { + currentDisplayName: 'Organization A2', + currentType: 'ORGANIZATION', + partyId: partyOrganizationA2, + tenantId: tenantA, + }, + { + currentDisplayName: 'Person A', + currentType: 'PERSON', + partyId: partyPersonA, + tenantId: tenantA, + }, + { + currentType: 'ORGANIZATION', + partyId: partyOrganizationB, + tenantId: tenantB, + }, + ]); + const [unnamedParty] = yield* admin + .select({ displayName: parties.currentDisplayName }) + .from(parties) + .where(eq(parties.partyId, partyOrganizationB)); + expect(unnamedParty?.displayName).toBe(null); + expect(yield* runtime.select().from(parties)).toEqual([]); + expect( + yield* withTenant(tenantA, (transaction) => transaction.select({ partyId: parties.partyId }).from(parties).orderBy(parties.partyId), ), - [ - { partyId: partyOrganizationA }, - { partyId: partyOrganizationA2 }, - { partyId: partyPersonA }, - ], - ); - + ).toEqual([ + { partyId: partyOrganizationA }, + { partyId: partyOrganizationA2 }, + { partyId: partyPersonA }, + ]); const identifierValues = (tenantId: string, partyId: string, identifierId: string) => ({ acceptedByActionInvocationId: actionA, acceptedByPrincipalId: principalA, @@ -206,88 +178,83 @@ test('enforces Party owner invariants, tenant isolation, and independent fact li reasonCode: 'selected_missing_fact_confirmed', servedAt: '2026-01-01T12:00:00.000Z', }; - await runEffectTestPromise( - admin.insert(partyOfficialIdentifiers).values([ - { - ...identifierValues(tenantA, partyOrganizationA, identifierA), - externalEvidence: sql`${JSON.stringify(externalEvidence)}::jsonb`, - }, - identifierValues(tenantA, partyOrganizationA2, identifierA2), - identifierValues(tenantB, partyOrganizationB, identifierB), - ]), - ); - const [persistedExternalEvidence] = await runEffectTestPromise( - admin - .select({ - externalEvidence: partyOfficialIdentifiers.externalEvidence, - validFrom: partyOfficialIdentifiers.validFrom, - }) - .from(partyOfficialIdentifiers) - .where(eq(partyOfficialIdentifiers.officialIdentifierId, identifierA)), - ); - assert.deepEqual(persistedExternalEvidence?.externalEvidence, externalEvidence); - assert.notEqual( - persistedExternalEvidence?.validFrom.toISOString(), + yield* admin.insert(partyOfficialIdentifiers).values([ + { + ...identifierValues(tenantA, partyOrganizationA, identifierA), + externalEvidence: sql`${yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(externalEvidence)}::jsonb`, + }, + identifierValues(tenantA, partyOrganizationA2, identifierA2), + identifierValues(tenantB, partyOrganizationB, identifierB), + ]); + const [persistedExternalEvidence] = yield* admin + .select({ + externalEvidence: partyOfficialIdentifiers.externalEvidence, + validFrom: partyOfficialIdentifiers.validFrom, + }) + .from(partyOfficialIdentifiers) + .where(eq(partyOfficialIdentifiers.officialIdentifierId, identifierA)); + expect(persistedExternalEvidence?.externalEvidence).toEqual(externalEvidence); + expect(persistedExternalEvidence?.validFrom.toISOString()).not.toBe( externalEvidence.observedAt, ); - await assert.rejects( - runEffectTestPromise( - admin - .update(partyOfficialIdentifiers) - .set({ - externalEvidence: sql`${JSON.stringify({ ...externalEvidence, rawPayload: { forbidden: true } })}::jsonb`, - }) - .where(eq(partyOfficialIdentifiers.officialIdentifierId, identifierA)), + expect( + hasPostgreSqlCode('23514')( + yield* Effect.flip( + admin + .update(partyOfficialIdentifiers) + .set({ + externalEvidence: sql`${yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ ...externalEvidence, rawPayload: { forbidden: true } })}::jsonb`, + }) + .where(eq(partyOfficialIdentifiers.officialIdentifierId, identifierA)), + ), ), - hasPostgreSqlCode('23514'), - ); - await assert.rejects( - runEffectTestPromise( - admin - .update(partyOfficialIdentifiers) - .set({ - externalEvidence: sql`${JSON.stringify({ ...externalEvidence, provider: null })}::jsonb`, - }) - .where(eq(partyOfficialIdentifiers.officialIdentifierId, identifierA)), + ).toBe(true); + expect( + hasPostgreSqlCode('23514')( + yield* Effect.flip( + admin + .update(partyOfficialIdentifiers) + .set({ + externalEvidence: sql`${yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ ...externalEvidence, provider: null })}::jsonb`, + }) + .where(eq(partyOfficialIdentifiers.officialIdentifierId, identifierA)), + ), ), - hasPostgreSqlCode('23514'), - ); - await runEffectTestPromise( - admin.insert(partyIdentifierClaims).values([ - { - identifierClaimId: claimA, - identifierTypeKey: 'ICO', - namespace: 'CZ:ICO', - normalizedValue: '00123456', - officialIdentifierId: identifierA, - partyId: partyOrganizationA, - tenantId: tenantA, - }, - { - identifierClaimId: claimB, - identifierTypeKey: 'ICO', - namespace: 'CZ:ICO', - normalizedValue: '00123456', - officialIdentifierId: identifierB, - partyId: partyOrganizationB, - tenantId: tenantB, - }, - ]), - ); - await assert.rejects( - runEffectTestPromise( - admin.insert(partyIdentifierClaims).values({ - identifierTypeKey: 'ICO', - namespace: 'CZ:ICO', - normalizedValue: '00123456', - officialIdentifierId: identifierA2, - partyId: partyOrganizationA2, - tenantId: tenantA, - }), + ).toBe(true); + yield* admin.insert(partyIdentifierClaims).values([ + { + identifierClaimId: claimA, + identifierTypeKey: 'ICO', + namespace: 'CZ:ICO', + normalizedValue: '00123456', + officialIdentifierId: identifierA, + partyId: partyOrganizationA, + tenantId: tenantA, + }, + { + identifierClaimId: claimB, + identifierTypeKey: 'ICO', + namespace: 'CZ:ICO', + normalizedValue: '00123456', + officialIdentifierId: identifierB, + partyId: partyOrganizationB, + tenantId: tenantB, + }, + ]); + expect( + hasPostgreSqlCode('23505')( + yield* Effect.flip( + admin.insert(partyIdentifierClaims).values({ + identifierTypeKey: 'ICO', + namespace: 'CZ:ICO', + normalizedValue: '00123456', + officialIdentifierId: identifierA2, + partyId: partyOrganizationA2, + tenantId: tenantA, + }), + ), ), - hasPostgreSqlCode('23505'), - ); - + ).toBe(true); const contactEvidence = { acceptedByActionInvocationId: actionA, acceptedByPrincipalId: principalA, @@ -298,52 +265,61 @@ test('enforces Party owner invariants, tenant isolation, and independent fact li provenanceSource: 'USER', validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), } as const; - await runEffectTestPromise( - admin.insert(partyContactPoints).values([ - { - ...contactEvidence, - contactPointId: emailA, - contactPointType: 'EMAIL', - displayValue: 'Shared@Example.test', - normalizationVersion: 'email.v1', - normalizedValue: 'shared@example.test', - partyId: partyOrganizationA, - tenantId: tenantA, - }, - { - ...contactEvidence, - contactPointId: emailA2, - contactPointType: 'EMAIL', - displayValue: 'shared@example.test', - normalizationVersion: 'email.v1', - normalizedValue: 'shared@example.test', - partyId: partyOrganizationA2, - tenantId: tenantA, - }, - { - ...contactEvidence, - addressLine1: 'Main 1', - city: 'Prague', - contactPointId: addressA, - contactPointType: 'ADDRESS', - countryCode: 'CZ', - partyId: partyOrganizationA, - postalCode: '11000', - tenantId: tenantA, - }, - { - ...contactEvidence, - addressLine1: 'Other 2', - city: 'Prague', - contactPointId: addressA2, - contactPointType: 'ADDRESS', - countryCode: 'CZ', - partyId: partyOrganizationA, - postalCode: '12000', - tenantId: tenantA, - }, - ]), - ); + yield* admin.insert(partyContactPoints).values([ + { + ...contactEvidence, + contactPointId: emailA, + contactPointType: 'EMAIL', + displayValue: 'Shared@Example.test', + normalizationVersion: 'email.v1', + normalizedValue: 'shared@example.test', + partyId: partyOrganizationA, + tenantId: tenantA, + }, + { + ...contactEvidence, + contactPointId: emailA2, + contactPointType: 'EMAIL', + displayValue: 'shared@example.test', + normalizationVersion: 'email.v1', + normalizedValue: 'shared@example.test', + partyId: partyOrganizationA2, + tenantId: tenantA, + }, + { + ...contactEvidence, + addressLine1: 'Main 1', + city: 'Prague', + contactPointId: addressA, + contactPointType: 'ADDRESS', + countryCode: 'CZ', + partyId: partyOrganizationA, + postalCode: '11000', + tenantId: tenantA, + }, + { + ...contactEvidence, + addressLine1: 'Other 2', + city: 'Prague', + contactPointId: addressA2, + contactPointType: 'ADDRESS', + countryCode: 'CZ', + partyId: partyOrganizationA, + postalCode: '12000', + tenantId: tenantA, + }, + { + ...contactEvidence, + addressLine1: 'Independent 3', + city: 'Prague', + contactPointId: addressB, + contactPointType: 'ADDRESS', + countryCode: 'CZ', + partyId: partyOrganizationB, + postalCode: '13000', + tenantId: tenantB, + }, + ]); const purposeEvidence = { acceptedByActionInvocationId: actionA, acceptedByPrincipalId: principalA, @@ -355,133 +331,158 @@ test('enforces Party owner invariants, tenant isolation, and independent fact li tenantId: tenantA, validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), } as const; - await runEffectTestPromise( - admin.insert(partyContactPointPurposes).values([ - { ...purposeEvidence, contactPointId: addressA, purposeKey: 'BILLING' }, - { ...purposeEvidence, contactPointId: addressA, purposeKey: 'DELIVERY' }, - ]), - ); - await assert.rejects( - runEffectTestPromise( - admin.insert(partyContactPointPurposes).values({ - ...purposeEvidence, - contactPointId: addressA2, - purposeKey: 'BILLING', - }), + yield* admin.insert(partyContactPointPurposes).values([ + { ...purposeEvidence, contactPointId: addressA, purposeKey: 'BILLING' }, + { ...purposeEvidence, contactPointId: addressA, purposeKey: 'DELIVERY' }, + ]); + expect( + hasPostgreSqlCode('23505')( + yield* Effect.flip( + admin.insert(partyContactPointPurposes).values({ + ...purposeEvidence, + contactPointId: addressA2, + purposeKey: 'BILLING', + }), + ), ), - hasPostgreSqlCode('23505'), - ); - const contactEndRecordedAt = await runEffectTestPromise(DateTime.nowAsDate); - const futureContactEnd = new Date('2099-01-01T00:00:00.000Z'); - await runEffectTestPromise( - admin - .update(partyContactPoints) - .set({ - additionalEvidenceRefs: ['evidence:additional-contact:1'], - endEvidenceRefs: [], - endProvenanceMethod: 'MANUAL_CONFIRMATION', - endProvenanceSource: 'USER_ASSERTION', - endReason: 'Future email retirement scheduled', - endedByActionInvocationId: 'aa000000-0000-4000-8000-000000000005', - endedByPrincipalId: principalA, - endedRecordedAt: contactEndRecordedAt, - validTo: futureContactEnd, - }) - .where(eq(partyContactPoints.contactPointId, emailA2)), - ); - await runEffectTestPromise( - admin - .update(partyContactPointPurposes) - .set({ - endEvidenceRefs: ['evidence:delivery-purpose-end:1'], - endProvenanceMethod: 'DOCUMENT_REVIEW', - endProvenanceSource: 'EXTERNAL_EVIDENCE', - endReason: 'Future delivery purpose retirement scheduled', - endedByActionInvocationId: 'aa000000-0000-4000-8000-000000000006', - endedByPrincipalId: principalA, - endedRecordedAt: contactEndRecordedAt, - validTo: futureContactEnd, - }) - .where(eq(partyContactPointPurposes.purposeKey, 'DELIVERY')), - ); - const [scheduledContactEnd] = await runEffectTestPromise( - admin.select().from(partyContactPoints).where(eq(partyContactPoints.contactPointId, emailA2)), - ); - assert.ok(scheduledContactEnd); - assert.equal(scheduledContactEnd.isCurrent, true); - assert.equal(scheduledContactEnd.endReason, 'Future email retirement scheduled'); - assert.equal(scheduledContactEnd.evidenceReference, 'evidence:original-contact:1'); - assert.deepEqual(scheduledContactEnd.additionalEvidenceRefs, ['evidence:additional-contact:1']); - assert.deepEqual(scheduledContactEnd.endEvidenceRefs, []); - const [scheduledPurposeEnd] = await runEffectTestPromise( - admin + ).toBe(true); + const otherTenantPurposes = yield* admin + .insert(partyContactPointPurposes) + .values({ + ...purposeEvidence, + contactPointId: addressB, + partyId: partyOrganizationB, + purposeKey: 'DELIVERY', + tenantId: tenantB, + }) + .returning(); + const contactEndRecordedAt = yield* DateTime.nowAsDate; + const futureContactEnd = DateTime.toDateUtc(DateTime.makeUnsafe('2099-01-01T00:00:00.000Z')); + yield* admin + .update(partyContactPoints) + .set({ + additionalEvidenceRefs: ['evidence:additional-contact:1'], + endEvidenceRefs: [], + endProvenanceMethod: 'MANUAL_CONFIRMATION', + endProvenanceSource: 'USER_ASSERTION', + endReason: 'Future email retirement scheduled', + endedByActionInvocationId: 'aa000000-0000-4000-8000-000000000005', + endedByPrincipalId: principalA, + endedRecordedAt: contactEndRecordedAt, + validTo: futureContactEnd, + }) + .where(eq(partyContactPoints.contactPointId, emailA2)); + yield* admin + .update(partyContactPointPurposes) + .set({ + endEvidenceRefs: ['evidence:delivery-purpose-end:1'], + endProvenanceMethod: 'DOCUMENT_REVIEW', + endProvenanceSource: 'EXTERNAL_EVIDENCE', + endReason: 'Future delivery purpose retirement scheduled', + endedByActionInvocationId: 'aa000000-0000-4000-8000-000000000006', + endedByPrincipalId: principalA, + endedRecordedAt: contactEndRecordedAt, + validTo: futureContactEnd, + }) + .where( + and( + eq(partyContactPointPurposes.tenantId, tenantA), + eq(partyContactPointPurposes.contactPointId, addressA), + eq(partyContactPointPurposes.purposeKey, 'DELIVERY'), + ), + ); + expect( + yield* admin .select() .from(partyContactPointPurposes) - .where(eq(partyContactPointPurposes.purposeKey, 'DELIVERY')), - ); - assert.equal(scheduledPurposeEnd?.isCurrent, true); - assert.equal(scheduledPurposeEnd?.endProvenanceSource, 'EXTERNAL_EVIDENCE'); - assert.deepEqual(scheduledPurposeEnd?.endEvidenceRefs, ['evidence:delivery-purpose-end:1']); - await assert.rejects( - runEffectTestPromise( - admin - .update(partyContactPointPurposes) - .set({ validTo: futureContactEnd }) - .where(eq(partyContactPointPurposes.purposeKey, 'BILLING')), + .where(eq(partyContactPointPurposes.contactPointId, addressB)), + ).toEqual(otherTenantPurposes); + const [scheduledContactEnd] = yield* admin + .select() + .from(partyContactPoints) + .where(eq(partyContactPoints.contactPointId, emailA2)); + assert.ok(scheduledContactEnd); + expect(scheduledContactEnd.isCurrent).toBe(true); + expect(scheduledContactEnd.endReason).toBe('Future email retirement scheduled'); + expect(scheduledContactEnd.evidenceReference).toBe('evidence:original-contact:1'); + expect(scheduledContactEnd.additionalEvidenceRefs).toEqual(['evidence:additional-contact:1']); + expect(scheduledContactEnd.endEvidenceRefs).toEqual([]); + const [scheduledPurposeEnd] = yield* admin + .select() + .from(partyContactPointPurposes) + .where( + and( + eq(partyContactPointPurposes.tenantId, tenantA), + eq(partyContactPointPurposes.contactPointId, addressA), + eq(partyContactPointPurposes.purposeKey, 'DELIVERY'), + ), + ); + expect(scheduledPurposeEnd?.isCurrent).toBe(true); + expect(scheduledPurposeEnd?.endProvenanceSource).toBe('EXTERNAL_EVIDENCE'); + expect(scheduledPurposeEnd?.endEvidenceRefs).toEqual(['evidence:delivery-purpose-end:1']); + expect( + hasPostgreSqlCode('23514')( + yield* Effect.flip( + admin + .update(partyContactPointPurposes) + .set({ validTo: futureContactEnd }) + .where( + and( + eq(partyContactPointPurposes.tenantId, tenantA), + eq(partyContactPointPurposes.contactPointId, addressA), + eq(partyContactPointPurposes.purposeKey, 'BILLING'), + ), + ), + ), ), - hasPostgreSqlCode('23514'), - ); - - await runEffectTestPromise( - admin.insert(partyRelationships).values({ - acceptedByActionInvocationId: actionA, - acceptedByPrincipalId: principalA, - fromPartyId: partyPersonA, - policyVersion: 'party.relationship.v1', - provenanceMethod: 'DECLARED', - provenanceSource: 'USER', - relationshipId: relationshipA, - relationshipType: 'CONTACT_PERSON_OF', - tenantId: tenantA, - toPartyId: partyOrganizationA, - validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), - validTo: new Date('2026-12-31T00:00:00.000Z'), - }), - ); - await assert.rejects( - runEffectTestPromise( - admin.insert(partyRelationships).values({ - acceptedByActionInvocationId: actionA, - acceptedByPrincipalId: principalA, - fromPartyId: partyPersonA, - policyVersion: 'party.relationship.v1', - provenanceMethod: 'DECLARED', - provenanceSource: 'USER', - relationshipType: 'CONTACT_PERSON_OF', - tenantId: tenantA, - toPartyId: partyOrganizationA, - validFrom: new Date('2026-06-01T00:00:00.000Z'), - }), + ).toBe(true); + yield* admin.insert(partyRelationships).values({ + acceptedByActionInvocationId: actionA, + acceptedByPrincipalId: principalA, + fromPartyId: partyPersonA, + policyVersion: 'party.relationship.v1', + provenanceMethod: 'DECLARED', + provenanceSource: 'USER', + relationshipId: relationshipA, + relationshipType: 'CONTACT_PERSON_OF', + tenantId: tenantA, + toPartyId: partyOrganizationA, + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), + validTo: DateTime.toDateUtc(DateTime.makeUnsafe('2026-12-31T00:00:00.000Z')), + }); + expect( + hasPostgreSqlCode('23P01')( + yield* Effect.flip( + admin.insert(partyRelationships).values({ + acceptedByActionInvocationId: actionA, + acceptedByPrincipalId: principalA, + fromPartyId: partyPersonA, + policyVersion: 'party.relationship.v1', + provenanceMethod: 'DECLARED', + provenanceSource: 'USER', + relationshipType: 'CONTACT_PERSON_OF', + tenantId: tenantA, + toPartyId: partyOrganizationA, + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2026-06-01T00:00:00.000Z')), + }), + ), ), - hasPostgreSqlCode('23P01'), - ); - await runEffectTestPromise( - admin.insert(partyRelationships).values({ - acceptedByActionInvocationId: actionA, - acceptedByPrincipalId: principalA, - fromPartyId: partyPersonA, - policyVersion: 'party.relationship.v1', - provenanceMethod: 'DECLARED', - provenanceSource: 'USER', - relationshipType: 'CONTACT_PERSON_OF', - tenantId: tenantA, - toPartyId: partyOrganizationA, - validFrom: new Date('2026-12-31T00:00:00.000Z'), - }), - ); - const effectiveRelationshipCount = async (effectiveAt: Date) => { - const relationships = await runEffectTestPromise( - admin + ).toBe(true); + yield* admin.insert(partyRelationships).values({ + acceptedByActionInvocationId: actionA, + acceptedByPrincipalId: principalA, + fromPartyId: partyPersonA, + policyVersion: 'party.relationship.v1', + provenanceMethod: 'DECLARED', + provenanceSource: 'USER', + relationshipType: 'CONTACT_PERSON_OF', + tenantId: tenantA, + toPartyId: partyOrganizationA, + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2026-12-31T00:00:00.000Z')), + }); + const effectiveRelationshipCount = (effectiveAt: Date) => + Effect.gen(function* testEffect3() { + const relationships = yield* admin .select() .from(partyRelationships) .where( @@ -492,138 +493,180 @@ test('enforces Party owner invariants, tenant isolation, and independent fact li lte(partyRelationships.validFrom, effectiveAt), or(isNull(partyRelationships.validTo), gt(partyRelationships.validTo, effectiveAt)), ), - ), - ); - return relationships.length; - }; - assert.equal(await effectiveRelationshipCount(new Date('2026-06-01T00:00:00.000Z')), 1); - assert.equal(await effectiveRelationshipCount(new Date('2027-01-01T00:00:00.000Z')), 1); - const [unknownStart] = await runEffectTestPromise( - admin - .insert(partyRelationships) - .values({ - acceptedByActionInvocationId: actionA, - acceptedByPrincipalId: principalA, - fromPartyId: partyPersonA, - policyVersion: 'party.relationship.v1', - provenanceMethod: 'DOCUMENT_REVIEW', - provenanceSource: 'USER', - relationshipType: 'CONTACT_PERSON_OF', - tenantId: tenantA, - toPartyId: partyOrganizationA2, - validTo: new Date('2030-01-01T00:00:00.000Z'), - }) - .returning(), - ); - assert.ok(unknownStart); - await runEffectTestPromise( - admin - .update(partyRelationships) - .set({ validFrom: new Date('2028-01-01T00:00:00.000Z') }) - .where(eq(partyRelationships.relationshipId, unknownStart.relationshipId)), - ); - await assert.rejects( - runEffectTestPromise( - admin.insert(partyRelationships).values({ - acceptedByActionInvocationId: actionA, - acceptedByPrincipalId: principalA, - fromPartyId: partyOrganizationA2, - policyVersion: 'party.relationship.v1', - provenanceMethod: 'DECLARED', - provenanceSource: 'USER', - relationshipType: 'CONTACT_PERSON_OF', - tenantId: tenantA, - toPartyId: partyOrganizationA, - validFrom: new Date('2030-01-01T00:00:00.000Z'), - validTo: new Date('2030-01-01T00:00:00.000Z'), - }), + ); + return relationships.length; + }); + expect( + yield* effectiveRelationshipCount( + DateTime.toDateUtc(DateTime.makeUnsafe('2026-06-01T00:00:00.000Z')), ), - hasPostgreSqlCode('23514'), - ); + ).toBe(1); + expect( + yield* effectiveRelationshipCount( + DateTime.toDateUtc(DateTime.makeUnsafe('2027-01-01T00:00:00.000Z')), + ), + ).toBe(1); + const [unknownStart] = yield* admin + .insert(partyRelationships) + .values({ + acceptedByActionInvocationId: actionA, + acceptedByPrincipalId: principalA, + fromPartyId: partyPersonA, + policyVersion: 'party.relationship.v1', + provenanceMethod: 'DOCUMENT_REVIEW', + provenanceSource: 'USER', + relationshipType: 'CONTACT_PERSON_OF', + tenantId: tenantA, + toPartyId: partyOrganizationA2, + validTo: DateTime.toDateUtc(DateTime.makeUnsafe('2030-01-01T00:00:00.000Z')), + }) + .returning(); + assert.isOk(unknownStart); - await runEffectTestPromise( - admin.insert(counterparties).values({ + yield* admin + .update(partyRelationships) + .set({ validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2028-01-01T00:00:00.000Z')) }) + .where(eq(partyRelationships.relationshipId, unknownStart.relationshipId)); + expect( + hasPostgreSqlCode('23514')( + yield* Effect.flip( + admin.insert(partyRelationships).values({ + acceptedByActionInvocationId: actionA, + acceptedByPrincipalId: principalA, + fromPartyId: partyOrganizationA2, + policyVersion: 'party.relationship.v1', + provenanceMethod: 'DECLARED', + provenanceSource: 'USER', + relationshipType: 'CONTACT_PERSON_OF', + tenantId: tenantA, + toPartyId: partyOrganizationA, + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2030-01-01T00:00:00.000Z')), + validTo: DateTime.toDateUtc(DateTime.makeUnsafe('2030-01-01T00:00:00.000Z')), + }), + ), + ), + ).toBe(true); + yield* admin.insert(counterparties).values({ + acceptedByActionInvocationId: actionA, + acceptedByPrincipalId: principalA, + counterpartyId: counterpartyA, + creationReason: 'Signed commercial agreement', + evidenceRefs: ['evidence:agreement:1'], + legalEntityId: legalEntityA, + partyId: partyOrganizationA, + policyVersion: 'party.counterparty.v1', + provenanceMethod: 'CONTRACT', + provenanceSource: 'COMMERCE', + sourceRecordRefs: ['commerce:agreement:1'], + tenantId: tenantA, + }); + expect( + hasPostgreSqlCode('23505')( + yield* Effect.flip( + admin.insert(counterparties).values({ + acceptedByActionInvocationId: actionA, + acceptedByPrincipalId: principalA, + creationReason: 'Signed commercial agreement', + evidenceRefs: ['evidence:agreement:1'], + legalEntityId: legalEntityA, + partyId: partyOrganizationA, + policyVersion: 'party.counterparty.v1', + provenanceMethod: 'CONTRACT', + provenanceSource: 'COMMERCE', + sourceRecordRefs: ['commerce:agreement:1'], + tenantId: tenantA, + }), + ), + ), + ).toBe(true); + yield* admin.insert(counterparties).values({ + acceptedByActionInvocationId: actionA, + acceptedByPrincipalId: principalA, + counterpartyId: counterpartyB, + creationReason: 'Independent tenant agreement', + evidenceRefs: ['evidence:other-tenant:1'], + legalEntityId: legalEntityB, + partyId: partyOrganizationB, + policyVersion: 'party.counterparty.v1', + provenanceMethod: 'CONTRACT', + provenanceSource: 'COMMERCE', + sourceRecordRefs: ['commerce:other-tenant:1'], + tenantId: tenantB, + }); + const otherTenantRoles = yield* admin + .insert(counterpartyRolePeriods) + .values({ + acceptedByActionInvocationId: actionA, + acceptedByPrincipalId: principalA, + addEvidenceRefs: ['evidence:other-tenant-customer:1'], + addReason: 'Independent customer agreement began', + counterpartyId: counterpartyB, + legalEntityId: legalEntityB, + policyVersion: 'party.counterparty-role.v1', + provenanceMethod: 'CONTRACT', + provenanceSource: 'COMMERCE', + roleType: 'CUSTOMER', + tenantId: tenantB, + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), + }) + .returning(); + yield* admin.insert(counterpartyRolePeriods).values([ + { acceptedByActionInvocationId: actionA, acceptedByPrincipalId: principalA, + addEvidenceRefs: ['evidence:customer-role:1'], + addReason: 'Customer agreement began', counterpartyId: counterpartyA, - creationReason: 'Signed commercial agreement', - evidenceRefs: ['evidence:agreement:1'], legalEntityId: legalEntityA, - partyId: partyOrganizationA, - policyVersion: 'party.counterparty.v1', + policyVersion: 'party.counterparty-role.v1', provenanceMethod: 'CONTRACT', provenanceSource: 'COMMERCE', - sourceRecordRefs: ['commerce:agreement:1'], + roleType: 'CUSTOMER', tenantId: tenantA, - }), - ); - await assert.rejects( - runEffectTestPromise( - admin.insert(counterparties).values({ - acceptedByActionInvocationId: actionA, - acceptedByPrincipalId: principalA, - creationReason: 'Signed commercial agreement', - evidenceRefs: ['evidence:agreement:1'], - legalEntityId: legalEntityA, - partyId: partyOrganizationA, - policyVersion: 'party.counterparty.v1', - provenanceMethod: 'CONTRACT', - provenanceSource: 'COMMERCE', - sourceRecordRefs: ['commerce:agreement:1'], - tenantId: tenantA, - }), - ), - hasPostgreSqlCode('23505'), - ); - await runEffectTestPromise( - admin.insert(counterpartyRolePeriods).values([ - { - acceptedByActionInvocationId: actionA, - acceptedByPrincipalId: principalA, - addEvidenceRefs: ['evidence:customer-role:1'], - addReason: 'Customer agreement began', - counterpartyId: counterpartyA, - legalEntityId: legalEntityA, - policyVersion: 'party.counterparty-role.v1', - provenanceMethod: 'CONTRACT', - provenanceSource: 'COMMERCE', - roleType: 'CUSTOMER', - tenantId: tenantA, - validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), - }, - { - acceptedByActionInvocationId: actionA, - acceptedByPrincipalId: principalA, - addEvidenceRefs: ['evidence:supplier-role:1'], - addReason: 'Supplier agreement began', - counterpartyId: counterpartyA, - legalEntityId: legalEntityA, - policyVersion: 'party.counterparty-role.v1', - provenanceMethod: 'CONTRACT', - provenanceSource: 'COMMERCE', - roleType: 'SUPPLIER', - tenantId: tenantA, - validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), - }, - ]), - ); - await runEffectTestPromise( - admin - .update(counterpartyRolePeriods) - .set({ - endEvidenceRefs: ['evidence:customer-role-end:1'], - endProvenanceMethod: 'CONTRACT_TERMINATION', - endProvenanceSource: 'COMMERCE', - endReason: 'Customer agreement ended', - endedByActionInvocationId: 'aa000000-0000-4000-8000-000000000003', - endedByPrincipalId: principalA, - endedRecordedAt: new Date('2026-06-30T00:00:00.000Z'), - isCurrent: false, - state: 'ENDED', - validTo: new Date('2026-06-30T00:00:00.000Z'), - }) - .where(eq(counterpartyRolePeriods.roleType, 'CUSTOMER')), - ); + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), + }, + { + acceptedByActionInvocationId: actionA, + acceptedByPrincipalId: principalA, + addEvidenceRefs: ['evidence:supplier-role:1'], + addReason: 'Supplier agreement began', + counterpartyId: counterpartyA, + legalEntityId: legalEntityA, + policyVersion: 'party.counterparty-role.v1', + provenanceMethod: 'CONTRACT', + provenanceSource: 'COMMERCE', + roleType: 'SUPPLIER', + tenantId: tenantA, + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), + }, + ]); + yield* admin + .update(counterpartyRolePeriods) + .set({ + endEvidenceRefs: ['evidence:customer-role-end:1'], + endProvenanceMethod: 'CONTRACT_TERMINATION', + endProvenanceSource: 'COMMERCE', + endReason: 'Customer agreement ended', + endedByActionInvocationId: 'aa000000-0000-4000-8000-000000000003', + endedByPrincipalId: principalA, + endedRecordedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-06-30T00:00:00.000Z')), + isCurrent: false, + state: 'ENDED', + validTo: DateTime.toDateUtc(DateTime.makeUnsafe('2026-06-30T00:00:00.000Z')), + }) + .where( + and( + eq(counterpartyRolePeriods.tenantId, tenantA), + eq(counterpartyRolePeriods.counterpartyId, counterpartyA), + eq(counterpartyRolePeriods.roleType, 'CUSTOMER'), + ), + ); + expect( + yield* admin + .select() + .from(counterpartyRolePeriods) + .where(eq(counterpartyRolePeriods.counterpartyId, counterpartyB)), + ).toEqual(otherTenantRoles); const futureRoleEvidence = { acceptedByActionInvocationId: actionA, acceptedByPrincipalId: principalA, @@ -639,252 +682,233 @@ test('enforces Party owner invariants, tenant isolation, and independent fact li state: 'ACTIVE', tenantId: tenantA, } as const; - await runEffectTestPromise( - admin.insert(counterpartyRolePeriods).values([ - { - ...futureRoleEvidence, - endEvidenceRefs: ['evidence:future-customer-role-end:1'], - endProvenanceMethod: 'CONTRACT_SCHEDULE', - endProvenanceSource: 'COMMERCE', - endReason: 'First future agreement is time-bounded', - endedByActionInvocationId: 'aa000000-0000-4000-8000-000000000004', - endedByPrincipalId: principalA, - endedRecordedAt: new Date('2026-07-01T00:00:00.000Z'), - validFrom: new Date('2099-01-01T00:00:00.000Z'), - validTo: new Date('2099-02-01T00:00:00.000Z'), - }, - { - ...futureRoleEvidence, - addEvidenceRefs: ['evidence:future-customer-role:2'], - validFrom: new Date('2099-02-01T00:00:00.000Z'), - }, - ]), - ); - await assert.rejects( - runEffectTestPromise( - admin.insert(counterpartyRolePeriods).values({ - ...futureRoleEvidence, - addEvidenceRefs: ['evidence:overlapping-future-customer-role:1'], - validFrom: new Date('2099-01-15T00:00:00.000Z'), - }), + yield* admin.insert(counterpartyRolePeriods).values([ + { + ...futureRoleEvidence, + endEvidenceRefs: ['evidence:future-customer-role-end:1'], + endProvenanceMethod: 'CONTRACT_SCHEDULE', + endProvenanceSource: 'COMMERCE', + endReason: 'First future agreement is time-bounded', + endedByActionInvocationId: 'aa000000-0000-4000-8000-000000000004', + endedByPrincipalId: principalA, + endedRecordedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-07-01T00:00:00.000Z')), + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2099-01-01T00:00:00.000Z')), + validTo: DateTime.toDateUtc(DateTime.makeUnsafe('2099-02-01T00:00:00.000Z')), + }, + { + ...futureRoleEvidence, + addEvidenceRefs: ['evidence:future-customer-role:2'], + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2099-02-01T00:00:00.000Z')), + }, + ]); + expect( + hasPostgreSqlCode('23P01')( + yield* Effect.flip( + admin.insert(counterpartyRolePeriods).values({ + ...futureRoleEvidence, + addEvidenceRefs: ['evidence:overlapping-future-customer-role:1'], + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2099-01-15T00:00:00.000Z')), + }), + ), ), - hasPostgreSqlCode('23P01'), - ); + ).toBe(true); const effectiveAt = DateTime.toDateUtc(DateTime.makeUnsafe('2026-09-01T00:00:00.000Z')); - const effectiveRoles = await runEffectTestPromise( - admin - .select() - .from(counterpartyRolePeriods) - .where( - and( - eq(counterpartyRolePeriods.counterpartyId, counterpartyA), - eq(counterpartyRolePeriods.state, 'ACTIVE'), - lte(counterpartyRolePeriods.validFrom, effectiveAt), - or( - isNull(counterpartyRolePeriods.validTo), - gt(counterpartyRolePeriods.validTo, effectiveAt), - ), + const effectiveRoles = yield* admin + .select() + .from(counterpartyRolePeriods) + .where( + and( + eq(counterpartyRolePeriods.counterpartyId, counterpartyA), + eq(counterpartyRolePeriods.state, 'ACTIVE'), + lte(counterpartyRolePeriods.validFrom, effectiveAt), + or( + isNull(counterpartyRolePeriods.validTo), + gt(counterpartyRolePeriods.validTo, effectiveAt), ), ), - ); - assert.equal(effectiveRoles.length, 1); - const persistedCounterparties = await runEffectTestPromise( - admin.select().from(counterparties).where(eq(counterparties.counterpartyId, counterpartyA)), - ); - assert.equal(persistedCounterparties.length, 1); - - const [counterpartySource] = await runEffectTestPromise( - admin.select().from(counterparties).where(eq(counterparties.counterpartyId, counterpartyA)), - ); - assert.ok(counterpartySource); - await runEffectTestPromise( - admin.insert(counterpartyAdminReadModels).values({ - archivedAt: counterpartySource.archivedAt, - counterpartyId: counterpartySource.counterpartyId, - createdAt: counterpartySource.createdAt, - legalEntityId: counterpartySource.legalEntityId, - storedPartyId: counterpartySource.partyId, - tenantId: counterpartySource.tenantId, - }), - ); - const roleSources = await runEffectTestPromise( - admin - .select() - .from(counterpartyRolePeriods) - .where(eq(counterpartyRolePeriods.counterpartyId, counterpartyA)), - ); - await runEffectTestPromise( - admin.insert(counterpartyRoleAdminReadModels).values( - roleSources.map((role) => ({ - addEvidenceRefs: role.addEvidenceRefs, - addReason: role.addReason, - counterpartyId: role.counterpartyId, - endEvidenceRefs: role.endEvidenceRefs, - endProvenanceMethod: role.endProvenanceMethod, - endProvenanceSource: role.endProvenanceSource, - endReason: role.endReason, - provenanceMethod: role.provenanceMethod, - provenanceSource: role.provenanceSource, - recordedAt: role.recordedAt, - rolePeriodId: role.rolePeriodId, - roleType: role.roleType, - state: role.state, - tenantId: role.tenantId, - validFrom: role.validFrom, - validTo: role.validTo, - })), - ), - ); + ); + expect(effectiveRoles.length).toBe(1); + const persistedCounterparties = yield* admin + .select() + .from(counterparties) + .where(eq(counterparties.counterpartyId, counterpartyA)); + expect(persistedCounterparties.length).toBe(1); + const [counterpartySource] = yield* admin + .select() + .from(counterparties) + .where(eq(counterparties.counterpartyId, counterpartyA)); + assert.isOk(counterpartySource); - assert.deepEqual( - await withTenant(tenantA, (transaction) => transaction.select().from(counterparties)), - [], - ); - assert.deepEqual( - await runEffectTestPromise(runtime.select().from(counterpartyAdminReadModels)), - [], - ); - const tenantCounterpartyModels = await withTenant(tenantA, (transaction) => + yield* admin.insert(counterpartyAdminReadModels).values({ + archivedAt: counterpartySource.archivedAt, + counterpartyId: counterpartySource.counterpartyId, + createdAt: counterpartySource.createdAt, + legalEntityId: counterpartySource.legalEntityId, + storedPartyId: counterpartySource.partyId, + tenantId: counterpartySource.tenantId, + }); + const roleSources = yield* admin + .select() + .from(counterpartyRolePeriods) + .where(eq(counterpartyRolePeriods.counterpartyId, counterpartyA)); + yield* admin.insert(counterpartyRoleAdminReadModels).values( + roleSources.map((role) => ({ + addEvidenceRefs: role.addEvidenceRefs, + addReason: role.addReason, + counterpartyId: role.counterpartyId, + endEvidenceRefs: role.endEvidenceRefs, + endProvenanceMethod: role.endProvenanceMethod, + endProvenanceSource: role.endProvenanceSource, + endReason: role.endReason, + provenanceMethod: role.provenanceMethod, + provenanceSource: role.provenanceSource, + recordedAt: role.recordedAt, + rolePeriodId: role.rolePeriodId, + roleType: role.roleType, + state: role.state, + tenantId: role.tenantId, + validFrom: role.validFrom, + validTo: role.validTo, + })), + ); + expect( + yield* withTenant(tenantA, (transaction) => transaction.select().from(counterparties)), + ).toEqual([]); + expect(yield* runtime.select().from(counterpartyAdminReadModels)).toEqual([]); + const tenantCounterpartyModels = yield* withTenant(tenantA, (transaction) => transaction.select().from(counterpartyAdminReadModels), ); - assert.equal(tenantCounterpartyModels.length, 1); - const tenantRoleModels = await withTenant(tenantA, (transaction) => + expect(tenantCounterpartyModels.length).toBe(1); + const tenantRoleModels = yield* withTenant(tenantA, (transaction) => transaction.select().from(counterpartyRoleAdminReadModels), ); - assert.equal(tenantRoleModels.length, roleSources.length); - assert.deepEqual( - await withTenant(tenantB, (transaction) => + expect(tenantRoleModels.length).toBe(roleSources.length); + expect( + yield* withTenant(tenantB, (transaction) => transaction.select().from(counterpartyAdminReadModels), ), - [], - ); - assert.deepEqual( - await withTenant(tenantA, (transaction) => + ).toEqual([]); + expect( + yield* withTenant(tenantA, (transaction) => transaction.select().from(counterpartyRolePeriods), ), - [], - ); - const scopedCounterparties = await runEffectTestPromise( - runtime.transaction((transaction) => - Effect.gen(function* transactionTestBody() { - yield* transaction.execute( - sql`select set_config('ontos.tenant_id', ${tenantA}, true)`, - 'objects', - ); - yield* transaction.execute( - sql`select set_config('ontos.legal_entity_id', ${legalEntityA}, true)`, - 'objects', - ); - return yield* transaction.select().from(counterparties); - }), - ), - ); - assert.equal(scopedCounterparties.length, 1); - - await runEffectTestPromise( - admin.insert(duplicateCandidateCases).values({ - candidateCaseId: caseA, - candidateFingerprint: 'a'.repeat(64), - candidateSnapshot: { - names: ['Ambiguous'], - provenance: { method: 'DOCUMENT_REVIEW', source: 'USER_ASSERTION' }, - validFrom: '2026-01-01T00:00:00.000Z', - }, - evaluatedEvidence: [ - { - reason: 'One identifier points to conflicting candidates', - ruleKey: RuleKeySchema.make('ico.v1'), - }, - ], - evaluationFingerprint: 'c'.repeat(64), - matchRuleVersion: 'party-match.v1', - tenantId: tenantA, + ).toEqual([]); + const scopedCounterparties = yield* runtime.transaction((transaction) => + Effect.gen(function* transactionTestBody() { + yield* transaction.execute( + sql`select set_config('ontos.tenant_id', ${tenantA}, true)`, + 'objects', + ); + yield* transaction.execute( + sql`select set_config('ontos.legal_entity_id', ${legalEntityA}, true)`, + 'objects', + ); + return yield* transaction.select().from(counterparties); }), ); - await runEffectTestPromise( - admin.insert(duplicateCandidateCaseParties).values({ - candidateCaseId: caseA, - evidenceExplanation: { - reason: 'Authoritative conflict', + expect(scopedCounterparties.length).toBe(1); + yield* admin.insert(duplicateCandidateCases).values({ + candidateCaseId: caseA, + candidateFingerprint: 'a'.repeat(64), + candidateSnapshot: { + names: ['Ambiguous'], + provenance: { method: 'DOCUMENT_REVIEW', source: 'USER_ASSERTION' }, + validFrom: '2026-01-01T00:00:00.000Z', + }, + evaluatedEvidence: [ + { + reason: 'One identifier points to conflicting candidates', ruleKey: RuleKeySchema.make('ico.v1'), }, - partyId: partyOrganizationA, - rank: 1, - tenantId: tenantA, - }), - ); - await runEffectTestPromise( - admin.insert(partyMatchDecisions).values({ - actionInvocationId: actionA, - candidateCaseId: caseA, - candidateFingerprint: 'a'.repeat(64), - evidenceExplanation: [ - { - reason: 'One identifier points to conflicting candidates', - ruleKey: RuleKeySchema.make('ico.v1'), - }, - ], - matchRuleVersion: 'party-match.v1', - outcome: 'AMBIGUOUS', - tenantId: tenantA, - }), - ); - await assert.rejects( - runEffectTestPromise( - admin.insert(partyMatchDecisions).values({ - actionInvocationId: actionA, - candidateFingerprint: 'b'.repeat(64), - evidenceExplanation: [], - matchRuleVersion: 'party-match.v1', - outcome: 'NO_MATCH', - tenantId: tenantA, - }), + ], + evaluationFingerprint: 'c'.repeat(64), + matchRuleVersion: 'party-match.v1', + tenantId: tenantA, + }); + yield* admin.insert(duplicateCandidateCaseParties).values({ + candidateCaseId: caseA, + evidenceExplanation: { + reason: 'Authoritative conflict', + ruleKey: RuleKeySchema.make('ico.v1'), + }, + partyId: partyOrganizationA, + rank: 1, + tenantId: tenantA, + }); + yield* admin.insert(partyMatchDecisions).values({ + actionInvocationId: actionA, + candidateCaseId: caseA, + candidateFingerprint: 'a'.repeat(64), + evidenceExplanation: [ + { + reason: 'One identifier points to conflicting candidates', + ruleKey: RuleKeySchema.make('ico.v1'), + }, + ], + matchRuleVersion: 'party-match.v1', + outcome: 'AMBIGUOUS', + tenantId: tenantA, + }); + expect( + hasPostgreSqlCode('23505')( + yield* Effect.flip( + admin.insert(partyMatchDecisions).values({ + actionInvocationId: actionA, + candidateFingerprint: 'b'.repeat(64), + evidenceExplanation: [], + matchRuleVersion: 'party-match.v1', + outcome: 'NO_MATCH', + tenantId: tenantA, + }), + ), ), - hasPostgreSqlCode('23505'), - ); + ).toBe(true); + expect( + hasPostgreSqlCode('55000')( + yield* Effect.flip( + withTenant(tenantA, (transaction) => + Effect.gen(function* transactionTestBody() { + const [fact] = yield* transaction + .insert(partyFactAssertions) + .values({ + acceptedByActionInvocationId: actionA, + acceptedByPrincipalId: principalA, + factKind: 'DISPLAY_NAME', + normalizedValue: 'Wrong name', + partyId: partyOrganizationA, + policyVersion: 'party.fact.v1', + provenanceMethod: 'DECLARED', + provenanceSource: 'USER', + tenantId: tenantA, + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), + }) + .returning({ assertionId: partyFactAssertions.assertionId }); + assert.isOk(fact); - await assert.rejects( - withTenant(tenantA, (transaction) => - Effect.gen(function* transactionTestBody() { - const [fact] = yield* transaction - .insert(partyFactAssertions) - .values({ - acceptedByActionInvocationId: actionA, - acceptedByPrincipalId: principalA, - factKind: 'DISPLAY_NAME', - normalizedValue: 'Wrong name', - partyId: partyOrganizationA, - policyVersion: 'party.fact.v1', - provenanceMethod: 'DECLARED', - provenanceSource: 'USER', - tenantId: tenantA, - validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), - }) - .returning({ assertionId: partyFactAssertions.assertionId }); - assert.ok(fact); - const [correction] = yield* transaction - .insert(partyCorrections) - .values({ - actingPrincipalId: principalA, - actionInvocationId: 'aa000000-0000-4000-8000-000000000002', - evidenceRefs: ['evidence:1'], - partyFactAssertionId: fact.assertionId, - partyId: partyOrganizationA, - policyVersion: 'party.correction.v1', - reason: 'Original assertion was wrong', - tenantId: tenantA, - }) - .returning({ correctionId: partyCorrections.correctionId }); - assert.ok(correction); - yield* transaction - .update(partyCorrections) - .set({ reason: 'Mutation must fail' }) - .where(eq(partyCorrections.correctionId, correction.correctionId)); - }), - ), - hasPostgreSqlCode('55000'), - ); + const [correction] = yield* transaction + .insert(partyCorrections) + .values({ + actingPrincipalId: principalA, + actionInvocationId: 'aa000000-0000-4000-8000-000000000002', + evidenceRefs: ['evidence:1'], + partyFactAssertionId: fact.assertionId, + partyId: partyOrganizationA, + policyVersion: 'party.correction.v1', + reason: 'Original assertion was wrong', + tenantId: tenantA, + }) + .returning({ correctionId: partyCorrections.correctionId }); + assert.isOk(correction); + yield* transaction + .update(partyCorrections) + .set({ reason: 'Mutation must fail' }) + .where(eq(partyCorrections.correctionId, correction.correctionId)); + }), + ), + ), + ), + ).toBe(true); const mergePartyRefs = [partyOrganizationA, partyOrganizationA2].map((resourceId) => ({ moduleId: 'party.registry' as const, resourceId, @@ -892,8 +916,10 @@ test('enforces Party owner invariants, tenant isolation, and independent fact li tenantId: tenantA, })); const [survivorPartyRef, absorbedPartyRef] = mergePartyRefs; - assert.ok(survivorPartyRef); - assert.ok(absorbedPartyRef); + assert.isOk(survivorPartyRef); + + assert.isOk(absorbedPartyRef); + const candidateSnapshots = mergePartyRefs.map((partyRef) => ({ candidate: { authoritativeEvidenceRank: 1, @@ -908,52 +934,49 @@ test('enforces Party owner invariants, tenant isolation, and independent fact li eligibleBefore: true, retainedAfter: true, })); - await runEffectTestPromise( - admin.insert(partyMerges).values({ - policyVersion: 'party.merge-readiness.v1', - readinessEvidence: { - absorbedPartyRefs: [absorbedPartyRef], - blockingReasons: ['Consumer dry-run is required'], - confirmedDuplicateDecisionId: 'confirmed-duplicate:fixture', - consumerStatuses: [{ consumerKey: 'contacts', status: 'BLOCKED' }], - decisionActorPrincipalId: principalA, - selectionEvidenceChain: ( - ['CONFIRMED_DUPLICATE_SET', 'IDENTITY_SAFETY', 'STABLE_RESOURCE_IDENTITY'] as const - ).map((criterion) => ({ - candidatePartyRefs: mergePartyRefs, - candidateSnapshots, - criterion, - evidenceRefs: ['evidence:fixture'], - explanation: 'Prepared-only fixture for database alias constraints', - winnerPartyRef: criterion === 'STABLE_RESOURCE_IDENTITY' ? survivorPartyRef : null, - })), - selectionPolicyVersion: 'party-merge-survivor-selection.v1', - selectionReason: 'STABLE_RESOURCE_IDENTITY', - version: 1, - }, - status: 'BLOCKED', - survivorPartyId: partyOrganizationA, - tenantId: tenantA, - }), - ); - const [merge] = await runEffectTestPromise( - admin.select({ mergeId: partyMerges.mergeId }).from(partyMerges).limit(1), - ); - assert.ok(merge); - await assert.rejects( - runEffectTestPromise( - admin.insert(partyAliases).values({ - aliasPartyId: partyOrganizationA, - canonicalPartyId: partyOrganizationA, - mergeId: merge.mergeId, - tenantId: tenantA, - }), + yield* admin.insert(partyMerges).values({ + policyVersion: 'party.merge-readiness.v1', + readinessEvidence: { + absorbedPartyRefs: [absorbedPartyRef], + blockingReasons: ['Consumer dry-run is required'], + confirmedDuplicateDecisionId: 'confirmed-duplicate:fixture', + consumerStatuses: [{ consumerKey: 'contacts', status: 'BLOCKED' }], + decisionActorPrincipalId: principalA, + selectionEvidenceChain: ( + ['CONFIRMED_DUPLICATE_SET', 'IDENTITY_SAFETY', 'STABLE_RESOURCE_IDENTITY'] as const + ).map((criterion) => ({ + candidatePartyRefs: mergePartyRefs, + candidateSnapshots, + criterion, + evidenceRefs: ['evidence:fixture'], + explanation: 'Prepared-only fixture for database alias constraints', + winnerPartyRef: criterion === 'STABLE_RESOURCE_IDENTITY' ? survivorPartyRef : null, + })), + selectionPolicyVersion: 'party-merge-survivor-selection.v1', + selectionReason: 'STABLE_RESOURCE_IDENTITY', + version: 1, + }, + status: 'BLOCKED', + survivorPartyId: partyOrganizationA, + tenantId: tenantA, + }); + const [merge] = yield* admin + .select({ mergeId: partyMerges.mergeId }) + .from(partyMerges) + .limit(1); + assert.isOk(merge); + + expect( + hasPostgreSqlCode('23514')( + yield* Effect.flip( + admin.insert(partyAliases).values({ + aliasPartyId: partyOrganizationA, + canonicalPartyId: partyOrganizationA, + mergeId: merge.mergeId, + tenantId: tenantA, + }), + ), ), - hasPostgreSqlCode('23514'), - ); - } finally { - await cleanup(); - await runtimePool.end(); - await adminPool.end(); - } -}); + ).toBe(true); + }), +); diff --git a/app/verticals/party-registry/tests/integration/engagement-database-boundary.test.ts b/app/verticals/party-registry/tests/integration/engagement-database-boundary.test.ts index eb0acac56..ddae71d88 100644 --- a/app/verticals/party-registry/tests/integration/engagement-database-boundary.test.ts +++ b/app/verticals/party-registry/tests/integration/engagement-database-boundary.test.ts @@ -1,75 +1,50 @@ -import { - makeEffectTestCallback as nativeTestCallback, - runEffectTestPromise, - runEffectTestSync as runNativeSync, -} from '@app/core-runtime/testing/effect-runtime'; - -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. +import { hasPostgreSqlCode, openBoundaryDatabases } from '../support/database-boundary.ts'; +import { purgeFixtureRows } from '../../../../packages/core-runtime/tests/support/fixture-cleanup.ts'; +import { expect, it } from 'effect-rstest'; import { eq, inArray, sql } from 'drizzle-orm'; -import { Effect, Exit as NativeExit, Scope as NativeScope } from 'effect'; -import assert from 'node:assert/strict'; -import test, { after as afterNativeDatabase } from 'node:test'; -import type { Pool } from 'pg'; +import { Effect } from 'effect'; import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.ts'; -import { hasPostgreSqlCode, openBoundaryDatabases } from '../support/database-boundary.ts'; -import { purgeFixtureRows } from '../../../../packages/core-runtime/tests/support/fixture-cleanup.ts'; import { contactsRelations, organizationEngagementProfiles, personEngagementProfiles, } from '../../src/db/engagement-schema.ts'; -const nativeDatabaseScope = runNativeSync(NativeScope.make()); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), -); - -/** Both boundary roles read the same owned schema through the scope closed after these tests. */ -const openContactsDatabase = async (pool: Pool) => - await runEffectTestPromise( - makeTestDatabaseFromPool(pool, contactsRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ), - ); - const tenantA = 'c1000000-0000-4000-8000-000000000001'; const tenantB = 'c1000000-0000-4000-8000-000000000002'; const fixtureTenants = [tenantA, tenantB] as const; -test('enforces tenant isolation and canonical-reference uniqueness without cross-vertical FKs', async () => { - const { admin, adminPool, runtime, runtimePool } = - await openBoundaryDatabases(openContactsDatabase); - // Ordered child-before-parent so every delete respects the owned foreign keys. - const cleanup = async (): Promise => { - await runEffectTestPromise( - purgeFixtureRows( - [personEngagementProfiles, organizationEngagementProfiles].map((table) => - admin.delete(table).where(inArray(table.tenantId, fixtureTenants)), - ), - ), - ); - }; - - try { - await cleanup(); - assert.deepEqual( - await runEffectTestPromise(runtime.select().from(organizationEngagementProfiles)), - [], - ); - await assert.rejects( - runEffectTestPromise( - runtime.insert(organizationEngagementProfiles).values({ - counterpartyResourceId: 'counterparty-a', - partyResourceId: 'party-a', - tenantId: tenantA, - }), - ), - hasPostgreSqlCode('42501'), - ); +it.live( + 'enforces tenant isolation and canonical-reference uniqueness without cross-vertical FKs', + () => + Effect.gen(function* testEffect1() { + const { admin, runtime } = yield* openBoundaryDatabases( + (pool) => makeTestDatabaseFromPool(pool, contactsRelations), + 1, + ); + const cleanup = () => + purgeFixtureRows( + [personEngagementProfiles, organizationEngagementProfiles].map((table) => + admin.delete(table).where(inArray(table.tenantId, fixtureTenants)), + ), + ); - await runEffectTestPromise( - runtime.transaction((transaction) => + yield* Effect.addFinalizer(() => cleanup().pipe(Effect.orDie)); + yield* cleanup(); + expect(yield* runtime.select().from(organizationEngagementProfiles)).toEqual([]); + expect( + hasPostgreSqlCode('42501')( + yield* Effect.flip( + runtime.insert(organizationEngagementProfiles).values({ + counterpartyResourceId: 'counterparty-a', + partyResourceId: 'party-a', + tenantId: tenantA, + }), + ), + ), + ).toBe(true); + yield* runtime.transaction((transaction) => Effect.gen(function* transactionTestBody() { yield* transaction.execute( sql`select set_config('ontos.tenant_id', ${tenantA}, true)`, @@ -92,7 +67,7 @@ test('enforces tenant isolation and canonical-reference uniqueness without cross tenantId: tenantA, }) .returning(); - assert.equal(prospect?.counterpartyResourceId, null); + expect(prospect?.counterpartyResourceId).toBe(null); const [unresolvedPerson] = yield* transaction .insert(personEngagementProfiles) .values({ @@ -100,50 +75,47 @@ test('enforces tenant isolation and canonical-reference uniqueness without cross tenantId: tenantA, }) .returning(); - assert.equal(unresolvedPerson?.counterpartyResourceId, null); + expect(unresolvedPerson?.counterpartyResourceId).toBe(null); }), - ), - ); - - await assert.rejects( - runEffectTestPromise( - runtime.transaction((transaction) => - Effect.gen(function* transactionTestBody() { - yield* transaction.execute( - sql`select set_config('ontos.tenant_id', ${tenantA}, true)`, - 'objects', - ); - yield* transaction.insert(personEngagementProfiles).values({ - partyResourceId: 'unresolved-person-a', - tenantId: tenantA, - }); - }), + ); + expect( + hasPostgreSqlCode('23505')( + yield* Effect.flip( + runtime.transaction((transaction) => + Effect.gen(function* transactionTestBody() { + yield* transaction.execute( + sql`select set_config('ontos.tenant_id', ${tenantA}, true)`, + 'objects', + ); + yield* transaction.insert(personEngagementProfiles).values({ + partyResourceId: 'unresolved-person-a', + tenantId: tenantA, + }); + }), + ), + ), ), - ), - hasPostgreSqlCode('23505'), - ); - - await assert.rejects( - runEffectTestPromise( - runtime.transaction((transaction) => - Effect.gen(function* transactionTestBody() { - yield* transaction.execute( - sql`select set_config('ontos.tenant_id', ${tenantA}, true)`, - 'objects', - ); - yield* transaction.insert(organizationEngagementProfiles).values({ - counterpartyResourceId: 'counterparty-a', - partyResourceId: 'party-b', - tenantId: tenantA, - }); - }), + ).toBe(true); + expect( + hasPostgreSqlCode('23505')( + yield* Effect.flip( + runtime.transaction((transaction) => + Effect.gen(function* transactionTestBody() { + yield* transaction.execute( + sql`select set_config('ontos.tenant_id', ${tenantA}, true)`, + 'objects', + ); + yield* transaction.insert(organizationEngagementProfiles).values({ + counterpartyResourceId: 'counterparty-a', + partyResourceId: 'party-b', + tenantId: tenantA, + }); + }), + ), + ), ), - ), - hasPostgreSqlCode('23505'), - ); - - await runEffectTestPromise( - runtime.transaction((transaction) => + ).toBe(true); + yield* runtime.transaction((transaction) => Effect.gen(function* transactionTestBody() { yield* transaction.execute( sql`select set_config('ontos.tenant_id', ${tenantB}, true)`, @@ -154,19 +126,13 @@ test('enforces tenant isolation and canonical-reference uniqueness without cross partyResourceId: 'party-a', tenantId: tenantB, }); - assert.deepEqual( + expect( yield* transaction .select() .from(organizationEngagementProfiles) .where(eq(organizationEngagementProfiles.tenantId, tenantA)), - [], - ); + ).toEqual([]); }), - ), - ); - } finally { - await cleanup(); - await runtimePool.end(); - await adminPool.end(); - } -}); + ); + }), +); diff --git a/app/verticals/party-registry/tests/integration/governed-identity.test.ts b/app/verticals/party-registry/tests/integration/governed-identity.test.ts index fc6d3e71d..f449b4795 100644 --- a/app/verticals/party-registry/tests/integration/governed-identity.test.ts +++ b/app/verticals/party-registry/tests/integration/governed-identity.test.ts @@ -1,8 +1,5 @@ -import { - makeEffectTestCallback as nativeTestCallback, - runEffectTestPromise, - runEffectTestSync as runNativeSync, -} from '@app/core-runtime/testing/effect-runtime'; +import { assert, expect, it } from 'effect-rstest'; + import type { TrustedPrincipalContext } from '@app/core-runtime'; import { CoreSearchQueryRuntimeLive, @@ -14,10 +11,8 @@ import { import { makeLiveOperationFixture } from '@app/core-runtime/testing/actions'; import { and, eq } from 'drizzle-orm'; -import { Effect, Exit, Layer, Exit as NativeExit, Scope as NativeScope, Redacted } from 'effect'; -import assert from 'node:assert/strict'; +import { Effect, Exit, Layer, Redacted, Predicate } from 'effect'; import { randomUUID } from 'node:crypto'; -import test, { after as afterNativeDatabase } from 'node:test'; import { Pool } from 'pg'; import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.ts'; import type { PartyCandidateSchema } from '../../shared/domain/identity-contracts.ts'; @@ -50,11 +45,6 @@ import { PartySearchProjectionGatewayLive, } from '../../src/search/parties.provider.ts'; -const nativeDatabaseScope = runNativeSync(NativeScope.make()); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), -); - type EncodedPartyCandidate = typeof PartyCandidateSchema.Encoded; const candidate = ( ico: string, @@ -82,13 +72,6 @@ const transport = (idempotencyKey = randomUUID()) => ({ idempotencyKey, targetModuleKey: 'party.registry', }); -const tag = (effect: Effect.Effect) => - effect.pipe( - Effect.match({ - onSuccess: () => 'SUCCESS', - onFailure: (error) => error._tag, - }), - ); const readPartyDetail = (partyRef: PartyRef, principal: TrustedPrincipalContext) => ReadRuntime.pipe( Effect.flatMap((runtime) => @@ -100,11 +83,11 @@ const readPartyDetail = (partyRef: PartyRef, principal: TrustedPrincipalContext) }), ), ); -const endPool = (pool: Pool) => pool.end(); -const promiseEffect = (operation: () => PromiseLike) => Effect.promise(operation); +const endPool = (pool: Pool) => Effect.promise(() => pool.end()); -test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims, recovery and temporal authorization', () => - runEffectTestPromise( +it.live( + 'governed Party identity uses real PostgreSQL and SpiceDB for atomic claims, recovery and temporal authorization', + () => Effect.scoped( Effect.gen(function* governedIdentityTestEffect() { const connections = yield* loadDatabaseConnectionPair(); @@ -136,11 +119,9 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims ); const adminPool = yield* Effect.acquireRelease( Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), - (pool) => promiseEffect(endPool.bind(undefined, pool)).pipe(Effect.orDie), - ); - const admin = yield* makeTestDatabaseFromPool(adminPool, partyRelations).pipe( - NativeScope.provide(nativeDatabaseScope), + endPool, ); + const admin = yield* makeTestDatabaseFromPool(adminPool, partyRelations); const fixtureContext = yield* Layer.build(fixture.layer); const otherContext = yield* Layer.build(other.layer); const run = (effect: Effect.Effect>) => @@ -187,23 +168,28 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims const concurrent = yield* Effect.all([run(create(exact)), run(create(exact))], { concurrency: 2, }); - assert.deepEqual(concurrent.map((result) => result.outcome).toSorted(), [ + expect(concurrent.map((result) => result.outcome).toSorted()).toEqual([ 'CREATED', 'MATCHED_EXISTING', ]); const created = concurrent.find((result) => result.outcome === 'CREATED'); - assert.ok(created && created.outcome === 'CREATED'); + assert.isOk(created && created.outcome === 'CREATED'); + const { partyRef } = created; let state = yield* snapshot(); - assert.equal(state.partyRows.length, 1); - assert.equal(state.claims.length, 1); - assert.equal(state.decisions.length, 2); - assert.equal(state.assertions.length, 1); - assert.equal(state.core.events.length, 1); - assert.equal(state.core.outbox.length, 1); - assert.equal(state.core.audits.length, 2); - assert.ok(state.core.invocations.every((invocation) => invocation.status === 'succeeded')); - assert.ok(state.assertions[0]?.evidenceEvaluation?.subjectEligible); + expect(state.partyRows.length).toBe(1); + expect(state.claims.length).toBe(1); + expect(state.decisions.length).toBe(2); + expect(state.assertions.length).toBe(1); + expect(state.core.events.length).toBe(1); + expect(state.core.outbox.length).toBe(1); + expect(state.core.audits.length).toBe(2); + assert.isOk( + state.core.invocations.every((invocation) => invocation.status === 'succeeded'), + ); + + assert.isOk(state.assertions[0]?.evidenceEvaluation?.subjectEligible); + const attachment = yield* run( create( candidate('27074358', { @@ -214,15 +200,15 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims }), ), ); - assert.equal(attachment.outcome, 'MATCHED_EXISTING'); + expect(attachment.outcome).toBe('MATCHED_EXISTING'); state = yield* snapshot(); - assert.equal(state.partyRows.length, 1); - assert.equal(state.claims.length, 2); - assert.equal(state.core.events.length, 2); - assert.equal(state.core.outbox.length, 2); + expect(state.partyRows.length).toBe(1); + expect(state.claims.length).toBe(2); + expect(state.core.events.length).toBe(2); + expect(state.core.outbox.length).toBe(2); const second = yield* run(create(candidate('26168685'))); - assert.equal(second.outcome, 'CREATED'); + expect(second.outcome).toBe('CREATED'); const split = candidate('26168685', { officialIdentifiers: [ { identifierType: 'ICO', value: '26168685', verification: 'VERIFIED' }, @@ -231,16 +217,16 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims }); const ambiguity = yield* run(create(split)); const repeated = yield* run(create(split)); - assert.ok(ambiguity.outcome === 'AMBIGUOUS' && repeated.outcome === 'AMBIGUOUS'); - assert.deepEqual(repeated.caseRef, ambiguity.caseRef); + assert.isOk(ambiguity.outcome === 'AMBIGUOUS' && repeated.outcome === 'AMBIGUOUS'); + + expect(repeated.caseRef).toEqual(ambiguity.caseRef); state = yield* snapshot(); - assert.equal(state.partyRows.length, 2); - assert.equal(state.cases.length, 1); - assert.equal( + expect(state.partyRows.length).toBe(2); + expect(state.cases.length).toBe(1); + expect( state.decisions.filter((decision) => decision.committedCreateOutcome === 'AMBIGUOUS') .length, - 2, - ); + ).toBe(2); // Every Create outcome survives actual lost commit acknowledgement, followed by a new governed Read. const recoveryCandidates = [candidate('45274649'), exact, split]; @@ -250,19 +236,29 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims function* verifyCommitRecoveryEffect(value) { const key = randomUUID(); fixture.faultNextTransaction('lost-ack'); - assert.equal(yield* run(create(value, key).pipe(tag)), 'ActionCommitIndeterminate'); + assert.isOk( + Predicate.isTagged( + yield* run(create(value, key).pipe(Effect.flip)), + 'ActionCommitIndeterminate', + ), + ); + const before = yield* snapshot(); const invocation = before.core.invocations.find((row) => row.idempotencyKey === key); - assert.ok(invocation); - assert.equal( - yield* run( - resolveActionCommit({ - invocationId: invocation.actionInvocationId, - principal: fixture.manager, - }).pipe(tag), + assert.isOk(invocation); + + assert.isOk( + Predicate.isTagged( + yield* run( + resolveActionCommit({ + invocationId: invocation.actionInvocationId, + principal: fixture.manager, + }).pipe(Effect.flip), + ), + 'ActionAlreadyCommitted', ), - 'ActionAlreadyCommitted', ); + const recovered = yield* run( ReadRuntime.pipe( Effect.flatMap((runtime) => @@ -278,67 +274,92 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims const original = before.decisions.find( (row) => row.actionInvocationId === invocation.actionInvocationId, ); - assert.ok(original); + assert.isOk(original); + const recoveredResult = committedCreateResult(recovered); - assert.ok(recoveredResult); - assert.equal(recoveredResult.outcome, original.committedCreateOutcome); - assert.equal(recoveredResult.decisionRef.resourceId, original.matchDecisionId); - assert.equal(recovered.partyRef?.resourceId ?? null, original.partyId); - assert.equal(recovered.caseRef?.resourceId ?? null, original.candidateCaseId); - assert.equal(yield* run(create(value, key).pipe(tag)), 'ActionAlreadyCommitted'); + assert.isOk(recoveredResult); + + expect(recoveredResult.outcome).toBe(original.committedCreateOutcome); + expect(recoveredResult.decisionRef.resourceId).toBe(original.matchDecisionId); + expect(recovered.partyRef?.resourceId ?? null).toBe(original.partyId); + expect(recovered.caseRef?.resourceId ?? null).toBe(original.candidateCaseId); + assert.isOk( + Predicate.isTagged( + yield* run(create(value, key).pipe(Effect.flip)), + 'ActionAlreadyCommitted', + ), + ); + const after = yield* snapshot(); - assert.deepEqual(after.partyRows, before.partyRows); - assert.deepEqual(after.decisions, before.decisions); - assert.deepEqual(after.core.events, before.core.events); - assert.deepEqual(after.core.outbox, before.core.outbox); - assert.equal( - yield* run(readPartyDetail(partyRef, fixture.denied).pipe(tag)), - 'ReadPermissionDenied', + expect(after.partyRows).toEqual(before.partyRows); + expect(after.decisions).toEqual(before.decisions); + expect(after.core.events).toEqual(before.core.events); + expect(after.core.outbox).toEqual(before.core.outbox); + assert.isOk( + Predicate.isTagged( + yield* run(readPartyDetail(partyRef, fixture.denied).pipe(Effect.flip)), + 'ReadPermissionDenied', + ), ); }, ), { concurrency: 1, discard: true }, ); const beforeDenied = yield* snapshot(); - assert.equal( - yield* run(create(candidate('00006947', { subjectEvidence: [] })).pipe(tag)), - 'PartyEvidenceInsufficient', + assert.isOk( + Predicate.isTagged( + yield* run(create(candidate('00006947', { subjectEvidence: [] })).pipe(Effect.flip)), + 'PartyEvidenceInsufficient', + ), ); + const afterDenied = yield* snapshot(); - assert.deepEqual(afterDenied.partyRows, beforeDenied.partyRows); - assert.deepEqual(afterDenied.decisions, beforeDenied.decisions); - assert.deepEqual(afterDenied.cases, beforeDenied.cases); + expect(afterDenied.partyRows).toEqual(beforeDenied.partyRows); + expect(afterDenied.decisions).toEqual(beforeDenied.decisions); + expect(afterDenied.cases).toEqual(beforeDenied.cases); fixture.faultNextTransaction('rollback'); - assert.notEqual(yield* run(create(candidate('00006947')).pipe(tag)), 'SUCCESS'); + yield* run(create(candidate('00006947')).pipe(Effect.flip)); const rolledBack = yield* snapshot(); - assert.deepEqual(rolledBack.partyRows, beforeDenied.partyRows); - assert.deepEqual(rolledBack.assertions, beforeDenied.assertions); - assert.deepEqual(rolledBack.claims, beforeDenied.claims); - assert.deepEqual(rolledBack.cases, beforeDenied.cases); - assert.deepEqual(rolledBack.core.audits, beforeDenied.core.audits); - assert.deepEqual(rolledBack.decisions, beforeDenied.decisions); - assert.deepEqual(rolledBack.core.events, beforeDenied.core.events); - assert.deepEqual(rolledBack.core.outbox, beforeDenied.core.outbox); + expect(rolledBack.partyRows).toEqual(beforeDenied.partyRows); + expect(rolledBack.assertions).toEqual(beforeDenied.assertions); + expect(rolledBack.claims).toEqual(beforeDenied.claims); + expect(rolledBack.cases).toEqual(beforeDenied.cases); + expect(rolledBack.core.audits).toEqual(beforeDenied.core.audits); + expect(rolledBack.decisions).toEqual(beforeDenied.decisions); + expect(rolledBack.core.events).toEqual(beforeDenied.core.events); + expect(rolledBack.core.outbox).toEqual(beforeDenied.core.outbox); const independent = yield* create(exact, randomUUID(), other.manager).pipe( Effect.provideContext(otherContext), ); - assert.ok(independent.outcome === 'CREATED'); - assert.notEqual(independent.partyRef.resourceId, partyRef.resourceId); - assert.equal( - yield* run(readPartyDetail(independent.partyRef, fixture.manager).pipe(tag)), - 'ReadHandlerNotFound', + assert.isOk(independent.outcome === 'CREATED'); + + expect(independent.partyRef.resourceId).not.toBe(partyRef.resourceId); + assert.isOk( + Predicate.isTagged( + yield* run(readPartyDetail(independent.partyRef, fixture.manager).pipe(Effect.flip)), + 'ReadHandlerNotFound', + ), ); - assert.equal( - yield* run( - create(candidate('00006947'), randomUUID(), fixture.legalEntityOnly).pipe(tag), + + assert.isOk( + Predicate.isTagged( + yield* run( + create(candidate('00006947'), randomUUID(), fixture.legalEntityOnly).pipe( + Effect.flip, + ), + ), + 'ActionPermissionDenied', ), - 'ActionPermissionDenied', ); - assert.equal( - yield* run(readPartyDetail(partyRef, fixture.legalEntityOnly).pipe(tag)), - 'ReadPermissionDenied', + + assert.isOk( + Predicate.isTagged( + yield* run(readPartyDetail(partyRef, fixture.legalEntityOnly).pipe(Effect.flip)), + 'ReadPermissionDenied', + ), ); + const searchLayer = PartySearchProjectionGatewayLive.pipe( Layer.provide(CoreSearchQueryRuntimeLive), ); @@ -356,22 +377,27 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims ), Effect.provideContext(searchContext), ); - assert.equal(yield* run(deniedSearch.pipe(tag)), 'ReadPermissionDenied'); - assert.equal( - yield* run( - ReadRuntime.pipe( - Effect.flatMap((runtime) => - runtime.runRead({ - registration: partyMatchDecisionRead, - input: { decisionRef: independent.decisionRef }, - principal: fixture.manager, - transport: { correlationId: randomUUID() }, - }), + assert.isOk( + Predicate.isTagged(yield* run(deniedSearch.pipe(Effect.flip)), 'ReadPermissionDenied'), + ); + + assert.isOk( + Predicate.isTagged( + yield* run( + ReadRuntime.pipe( + Effect.flatMap((runtime) => + runtime.runRead({ + registration: partyMatchDecisionRead, + input: { decisionRef: independent.decisionRef }, + principal: fixture.manager, + transport: { correlationId: randomUUID() }, + }), + ), + Effect.flip, ), - tag, ), + 'ReadHandlerNotFound', ), - 'ReadHandlerNotFound', ); const provenance = { @@ -390,10 +416,11 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims const counterparties = yield* Effect.all([run(counterparty()), run(counterparty())], { concurrency: 2, }); - assert.deepEqual(counterparties.map((item) => item.created).toSorted(), [false, true]); - assert.deepEqual(counterparties[0]?.counterpartyRef, counterparties[1]?.counterpartyRef); + expect(counterparties.map((item) => item.created).toSorted()).toEqual([false, true]); + expect(counterparties[0]?.counterpartyRef).toEqual(counterparties[1]?.counterpartyRef); const counterpartyRef = counterparties[0]?.counterpartyRef; - assert.ok(counterpartyRef); + assert.isOk(counterpartyRef); + const readCounterparty = () => run( ReadRuntime.pipe( @@ -409,26 +436,24 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims ); // Owning a business Counterparty does not itself grant resource permission. const forbiddenCounterpartyRead = yield* Effect.exit(readCounterparty()); - assert.ok(Exit.isFailure(forbiddenCounterpartyRead)); + assert.isOk(Exit.isFailure(forbiddenCounterpartyRead)); + yield* fixture.grantResourceAccess(counterpartyRef, fixture.legalEntityOnly.principalId); const projection = yield* readCounterparty(); - assert.deepEqual(Object.keys(projection.party).toSorted(), [ + expect(Object.keys(projection.party).toSorted()).toEqual([ 'archived', 'canonicalPartyRef', 'displayName', 'partyType', 'storedPartyRef', ]); - assert.notEqual( - yield* run( - runAction({ - registration: counterpartyCreateAction, - payload: { partyRef, provenance }, - principal: fixture.manager, - transport: transport(), - }).pipe(tag), - ), - 'SUCCESS', + yield* run( + runAction({ + registration: counterpartyCreateAction, + payload: { partyRef, provenance }, + principal: fixture.manager, + transport: transport(), + }).pipe(Effect.flip), ); yield* fixture.grantResourceAccess( counterpartyRef, @@ -465,10 +490,9 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims }), ); const counterpartyAfterRoleEnd = yield* readCounterparty(); - assert.deepEqual( - counterpartyAfterRoleEnd.currentRoles.map((item) => item.roleType), - ['SUPPLIER'], - ); + expect(counterpartyAfterRoleEnd.currentRoles.map((item) => item.roleType)).toEqual([ + 'SUPPLIER', + ]); const person = yield* run( create( @@ -489,7 +513,8 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims }), ), ); - assert.ok(person.outcome === 'AMBIGUOUS'); + assert.isOk(person.outcome === 'AMBIGUOUS'); + const reviewedPerson = yield* run( runAction({ registration: resolveDuplicateCandidateCreateAction, @@ -502,7 +527,8 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims transport: transport(), }), ); - assert.ok(reviewedPerson.partyRef); + assert.isOk(reviewedPerson.partyRef); + const relationship = yield* run( runAction({ registration: createPartyRelationshipAction, @@ -519,14 +545,22 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims }), ); // Domain relationships never provision access to Party records. - assert.equal( - yield* run(readPartyDetail(reviewedPerson.partyRef, fixture.legalEntityOnly).pipe(tag)), - 'ReadPermissionDenied', + assert.isOk( + Predicate.isTagged( + yield* run( + readPartyDetail(reviewedPerson.partyRef, fixture.legalEntityOnly).pipe(Effect.flip), + ), + 'ReadPermissionDenied', + ), ); - assert.equal( - yield* run(readPartyDetail(partyRef, fixture.legalEntityOnly).pipe(tag)), - 'ReadPermissionDenied', + + assert.isOk( + Predicate.isTagged( + yield* run(readPartyDetail(partyRef, fixture.legalEntityOnly).pipe(Effect.flip)), + 'ReadPermissionDenied', + ), ); + const updatedRelationship = yield* run( runAction({ registration: updatePartyRelationshipAction, @@ -541,7 +575,7 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims transport: transport(), }), ); - assert.equal(updatedRelationship.outcome, 'CHANGED'); + expect(updatedRelationship.outcome).toBe('CHANGED'); const endedRelationship = yield* run( runAction({ registration: endPartyRelationshipAction, @@ -556,12 +590,13 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims transport: transport(), }), ); - assert.equal(endedRelationship.outcome, 'CHANGED'); + expect(endedRelationship.outcome).toBe('CHANGED'); // Seed a legacy unclaimed identifier assertion only in owner storage, then exercise // the public unarchive Action against another Party's real current exact claim. const legacy = yield* run(create(candidate('00006947'))); - assert.ok(legacy.outcome === 'CREATED'); + assert.isOk(legacy.outcome === 'CREATED'); + const legacyArchived = yield* run( runAction({ registration: archivePartyAction, @@ -584,7 +619,8 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims ), ) .limit(1); - assert.ok(identifierTemplate); + assert.isOk(identifierTemplate); + yield* admin.insert(partyOfficialIdentifiers).values({ ...identifierTemplate, officialIdentifierId: randomUUID(), @@ -602,7 +638,7 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims transport: transport(), }), ); - assert.ok( + assert.isOk( collision.outcome === 'BLOCKED' && collision.reasonCode === 'EXACT_CLAIM_CONFLICT', ); @@ -620,10 +656,11 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims }), ); const archivedParty = yield* run(readPartyDetail(partyRef, fixture.manager)); - assert.ok(archivedParty.party.archivedAt); + assert.isOk(archivedParty.party.archivedAt); + const archivedCounterparty = yield* readCounterparty(); - assert.equal(archivedCounterparty.party.archived, true); - assert.notEqual(yield* run(counterparty().pipe(tag)), 'SUCCESS'); + expect(archivedCounterparty.party.archived).toBe(true); + yield* run(counterparty().pipe(Effect.flip)); const unarchive = yield* run( runAction({ registration: unarchivePartyAction, @@ -636,10 +673,10 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims transport: transport(), }), ); - assert.equal(unarchive.outcome, 'BLOCKED'); - assert.ok( + expect(unarchive.outcome).toBe('BLOCKED'); + assert.isOk( unarchive.outcome === 'BLOCKED' && unarchive.reasonCode === 'OPEN_DUPLICATE_CASE', ); }), ), - )); +); diff --git a/app/verticals/party-registry/tests/integration/identity-concurrency.test.ts b/app/verticals/party-registry/tests/integration/identity-concurrency.test.ts index c94a162b8..8f517f511 100644 --- a/app/verticals/party-registry/tests/integration/identity-concurrency.test.ts +++ b/app/verticals/party-registry/tests/integration/identity-concurrency.test.ts @@ -1,18 +1,9 @@ -import { - makeEffectTestCallback as nativeTestCallback, - runEffectTestPromise, - runEffectTestSync as runNativeSync, -} from '@app/core-runtime/testing/effect-runtime'; - -// @effect-diagnostics asyncFunction:off globalDate:off -- Existing compatibility boundary; expires: 2026-12-31. -import { eq, sql } from 'drizzle-orm'; -import { DateTime, Effect, Exit as NativeExit, Scope as NativeScope } from 'effect'; -import assert from 'node:assert/strict'; -import test, { after as afterNativeDatabase } from 'node:test'; -import type { Pool } from 'pg'; -import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.ts'; import { openBoundaryDatabases } from '../support/database-boundary.ts'; import { purgeFixtureRows } from '../../../../packages/core-runtime/tests/support/fixture-cleanup.ts'; +import { expect, it } from 'effect-rstest'; +import { eq, sql } from 'drizzle-orm'; +import { DateTime, Effect, Option } from 'effect'; +import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.ts'; import { normalizeOfficialIdentifier } from '../../shared/domain/identifier-contracts.ts'; import { partySubjectKeyFromString } from '../../shared/domain/identity-contracts.ts'; import { @@ -33,29 +24,18 @@ import { import { createOrMatchParty } from '../../src/services/party-matching-persistence.service.ts'; import { addOfficialIdentifierRecord } from '../../src/services/party-official-identifier-persistence.service.ts'; -const nativeDatabaseScope = runNativeSync(NativeScope.make()); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), -); - -/** Both boundary roles read the same owned schema through the scope closed after these tests. */ -const openPartyDatabase = async (pool: Pool) => - await runEffectTestPromise( - makeTestDatabaseFromPool(pool, partyRelations).pipe(NativeScope.provide(nativeDatabaseScope)), - ); - const tenantId = 'bc100000-0000-4000-8000-000000000001'; const principalId = 'bc200000-0000-4000-8000-000000000001'; -test('real PostgreSQL identity locks serialize concurrent exact creates and repeated identifier acceptance', async () => { - const { admin, adminPool, runtime, runtimePool } = await openBoundaryDatabases( - openPartyDatabase, - 2, - ); - // Ordered child-before-parent so every delete respects the owned foreign keys. - const cleanup = async (): Promise => { - await runEffectTestPromise( - purgeFixtureRows( +it.live( + 'real PostgreSQL identity locks serialize concurrent exact creates and repeated identifier acceptance', + () => + Effect.gen(function* identityConcurrencyTest() { + const { admin, runtime } = yield* openBoundaryDatabases( + (pool) => makeTestDatabaseFromPool(pool, partyRelations), + 2, + ); + const cleanup = purgeFixtureRows( [ partyMatchDecisions, duplicateCandidateCaseParties, @@ -65,113 +45,98 @@ test('real PostgreSQL identity locks serialize concurrent exact creates and repe partyFactAssertions, parties, ].map((table) => admin.delete(table).where(eq(table.tenantId, tenantId))), - ), - ); - }; - const scoped = ( - operation: (transaction: PartyTransaction) => Effect.Effect, - ) => - runEffectTestPromise( - runtime.transaction((transaction) => - Effect.gen(function* transactionTestBody() { - yield* transaction.execute( - sql`select set_config('ontos.tenant_id', ${tenantId}, true)`, - 'objects', - ); - return yield* operation(transaction); - }), - ), - ); - try { - await cleanup(); - const candidate = { - evidenceRefs: ['evidence-artifact:identity-concurrency:registry'], - officialIdentifiers: [ - { identifierType: 'ICO' as const, value: '27074358', verification: 'VERIFIED' as const }, - ], - partyType: 'ORGANIZATION' as const, - provenance: { method: 'REGISTRY', source: 'identity-concurrency-test' }, - subjectEvidence: [ - { - basis: 'REVIEWED_DOCUMENT' as const, - evidenceRef: 'record/42', - kind: 'ACTOR_ATTESTATION' as const, - observedSubject: 'ORGANIZATION' as const, - statement: 'Reviewed this external organization', - subjectKey: partySubjectKeyFromString('one-subject'), - }, - ], - validFrom: DateTime.makeUnsafe('2020-01-01T00:00:00.000Z'), - }; - const results = await Promise.all( - ['bc300000-0000-4000-8000-000000000001', 'bc300000-0000-4000-8000-000000000002'].map( - (actionInvocationId) => - scoped((transaction) => - createOrMatchParty(transaction, { + ); + const scoped = ( + operation: (transaction: PartyTransaction) => Effect.Effect, + ) => + runtime.transaction((transaction) => + Effect.gen(function* transactionTestBody() { + yield* transaction.execute( + sql`select set_config('ontos.tenant_id', ${tenantId}, true)`, + 'objects', + ); + return yield* operation(transaction); + }), + ); + yield* Effect.acquireRelease(cleanup, () => cleanup.pipe(Effect.orDie)); + const candidate = { + evidenceRefs: ['evidence-artifact:identity-concurrency:registry'], + officialIdentifiers: [ + { identifierType: 'ICO' as const, value: '27074358', verification: 'VERIFIED' as const }, + ], + partyType: 'ORGANIZATION' as const, + provenance: { method: 'REGISTRY', source: 'identity-concurrency-test' }, + subjectEvidence: [ + { + basis: 'REVIEWED_DOCUMENT' as const, + evidenceRef: 'record/42', + kind: 'ACTOR_ATTESTATION' as const, + observedSubject: 'ORGANIZATION' as const, + statement: 'Reviewed this external organization', + subjectKey: partySubjectKeyFromString('one-subject'), + }, + ], + validFrom: DateTime.makeUnsafe('2020-01-01T00:00:00.000Z'), + }; + const results = yield* Effect.all( + ['bc300000-0000-4000-8000-000000000001', 'bc300000-0000-4000-8000-000000000002'].map( + (actionInvocationId) => + scoped((transaction) => + createOrMatchParty(transaction, { + actionInvocationId, + candidate, + principalId, + tenantId, + }), + ), + ), + { concurrency: 'unbounded' }, + ); + expect(results.map((result) => result.outcome).toSorted()).toEqual([ + 'CREATED', + 'MATCHED_EXISTING', + ]); + const created = Option.getOrThrow( + Option.fromNullishOr(results.find((result) => result.outcome === 'CREATED')), + ); + expect(created.outcome).toBe('CREATED'); + const partyId = created.partyRef.resourceId; + const canonical = yield* admin.select().from(parties).where(eq(parties.tenantId, tenantId)); + expect(canonical).toHaveLength(1); + expect(canonical[0]?.currentDisplayName).toBe(null); + const nameAssertions = yield* admin + .select() + .from(partyFactAssertions) + .where(eq(partyFactAssertions.tenantId, tenantId)); + expect(nameAssertions.some((row) => row.factKind === 'DISPLAY_NAME')).toBe(false); + const identifier = normalizeOfficialIdentifier({ + identifierType: 'CZ_DIC', + value: 'CZ27074358', + verification: 'VERIFIED', + }); + const add = (actionInvocationId: string) => + scoped((transaction) => + Effect.gen(function* acceptIdentifier() { + yield* lockTenantIdentityWrites(transaction, tenantId); + yield* lockAndResolveClaims(transaction, tenantId, [identifier]); + return yield* addOfficialIdentifierRecord(transaction, tenantId, partyId, identifier, { actionInvocationId, - candidate, + matchRuleVersion: 'party-exact-claims.v1', + partyType: 'ORGANIZATION', principalId, - tenantId, - }), - ), - ), - ); - assert.deepEqual(results.map((result) => result.outcome).toSorted(), [ - 'CREATED', - 'MATCHED_EXISTING', - ]); - const created = results.find((result) => result.outcome === 'CREATED'); - assert.ok(created && created.outcome === 'CREATED'); - const partyId = created.partyRef.resourceId; - const canonical = await runEffectTestPromise( - admin.select().from(parties).where(eq(parties.tenantId, tenantId)), - ); - assert.equal(canonical.length, 1); - assert.equal(canonical[0]?.currentDisplayName, null); - const nameAssertions = await runEffectTestPromise( - admin.select().from(partyFactAssertions).where(eq(partyFactAssertions.tenantId, tenantId)), - ); - assert.equal( - nameAssertions.some((row) => row.factKind === 'DISPLAY_NAME'), - false, - ); - const identifier = normalizeOfficialIdentifier({ - identifierType: 'CZ_DIC', - value: 'CZ27074358', - verification: 'VERIFIED', - }); - const add = (actionInvocationId: string) => - scoped((transaction) => - Effect.gen(function* acceptIdentifier() { - yield* lockTenantIdentityWrites(transaction, tenantId); - yield* lockAndResolveClaims(transaction, tenantId, [identifier]); - return yield* addOfficialIdentifierRecord(transaction, tenantId, partyId, identifier, { - actionInvocationId, - matchRuleVersion: 'party-exact-claims.v1', - partyType: 'ORGANIZATION', - principalId, - provenanceMethod: 'REGISTRY', - provenanceSource: 'identity-concurrency-test', - validFrom: candidate.validFrom, - }); - }), - ); - const first = await add('bc300000-0000-4000-8000-000000000003'); - const repeated = await add('bc300000-0000-4000-8000-000000000004'); - assert.equal(repeated.officialIdentifierId, first.officialIdentifierId); - const claims = await runEffectTestPromise( - admin + provenanceMethod: 'REGISTRY', + provenanceSource: 'identity-concurrency-test', + validFrom: candidate.validFrom, + }); + }), + ); + const first = yield* add('bc300000-0000-4000-8000-000000000003'); + const repeated = yield* add('bc300000-0000-4000-8000-000000000004'); + expect(repeated.officialIdentifierId).toBe(first.officialIdentifierId); + const claims = yield* admin .select() .from(partyIdentifierClaims) - .where(eq(partyIdentifierClaims.tenantId, tenantId)), - ); - assert.equal(claims.length, 2); - } finally { - try { - await cleanup(); - } finally { - await runtimePool.end(); - await adminPool.end(); - } - } -}); + .where(eq(partyIdentifierClaims.tenantId, tenantId)); + expect(claims).toHaveLength(2); + }), +); diff --git a/app/verticals/party-registry/tests/support/database-boundary.ts b/app/verticals/party-registry/tests/support/database-boundary.ts index 80feba755..07b701a3c 100644 --- a/app/verticals/party-registry/tests/support/database-boundary.ts +++ b/app/verticals/party-registry/tests/support/database-boundary.ts @@ -1,4 +1,3 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import { findPostgresFailure, loadDatabaseConnectionPair } from '@app/core-runtime'; import { Effect, Option } from 'effect'; import { Pool } from 'pg'; @@ -9,33 +8,28 @@ export const hasPostgreSqlCode = (error: Parameters[0]): boolean => Option.exists(findPostgresFailure(error), ({ code }) => code === expected); -/** - * A database boundary proves two roles against one schema: an admin connection that seeds and - * purges fixtures, and a deliberately narrow runtime connection that shows what the runtime role - * may actually do. The caller owns the schema binding and the scope that closes both pools. - */ -export const openBoundaryDatabases = ( - openDatabase: (pool: Pool) => Promise, +/** Binds both roles to one schema; the test scope closes databases before their owned pools. */ +export const openBoundaryDatabases = ( + openDatabase: (pool: Pool) => Effect.Effect, runtimeConnections = 1, -): Promise<{ - readonly admin: Database; - readonly adminPool: Pool; - readonly runtime: Database; - readonly runtimePool: Pool; -}> => - runEffectTestPromise( - Effect.gen(function* openDatabases() { - const connections = yield* loadDatabaseConnectionPair(); - const adminPool = new Pool({ connectionString: connections.admin.connectionString }); - const runtimePool = new Pool({ - connectionString: connections.runtime.connectionString, - max: runtimeConnections, - }); - return { - admin: yield* Effect.promise(() => openDatabase(adminPool)), - adminPool, - runtime: yield* Effect.promise(() => openDatabase(runtimePool)), - runtimePool, - }; - }), - ); +) => + Effect.gen(function* openDatabases() { + const connections = yield* loadDatabaseConnectionPair(); + const adminPool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), + (pool) => Effect.promise(() => pool.end()).pipe(Effect.orDie), + ); + const runtimePool = yield* Effect.acquireRelease( + Effect.sync( + () => + new Pool({ + connectionString: connections.runtime.connectionString, + max: runtimeConnections, + }), + ), + (pool) => Effect.promise(() => pool.end()).pipe(Effect.orDie), + ); + const admin = yield* openDatabase(adminPool); + const runtime = yield* openDatabase(runtimePool); + return { admin, runtime }; + }); diff --git a/app/verticals/party-registry/tests/unit/api-integration-ares-application.test.ts b/app/verticals/party-registry/tests/unit/api-integration-ares-application.test.ts index efca71c12..d4f158f54 100644 --- a/app/verticals/party-registry/tests/unit/api-integration-ares-application.test.ts +++ b/app/verticals/party-registry/tests/unit/api-integration-ares-application.test.ts @@ -1,8 +1,5 @@ -// @effect-diagnostics asyncFunction:off instanceOfSchema:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; - -import { DateTime, Effect, ManagedRuntime, Match, Option, Result, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { DateTime, Effect, Layer, Match, Option, Result, Schema } from 'effect'; import { TestClock } from 'effect/testing'; import { AresAppliedEvidenceSchema, @@ -16,7 +13,6 @@ import { TargetAssertionIdSchema, } from '../../shared/domain/correction-contracts.ts'; import { PartyIdSchema } from '../../shared/domain/identity-contracts.ts'; - import { AresApplySelectionInvalid, applyAresObservationWithActions as applyAresObservation, @@ -34,7 +30,6 @@ const partyRef = { resourceType: 'party.registry.party' as const, tenantId: '20000000-0000-4000-8000-000000000001', }; - const confirmedAt = '2026-09-03T10:10:00.000Z'; const confirmedInstant = DateTime.makeUnsafe(confirmedAt); const confirmedAtEpoch = DateTime.toEpochMillis(confirmedInstant); @@ -44,12 +39,6 @@ const partyUpdatedAt = DateTime.makeUnsafe('2026-09-03T10:00:00.000Z'); const currentAssertionId = Result.getOrThrow( Schema.decodeUnknownResult(AssertionIdSchema)('30000000-0000-4000-8000-000000000001'), ); -const aresTestRuntime = ManagedRuntime.make(TestClock.layer()); -test.after(() => aresTestRuntime.dispose()); -const runAresEffectTestPromise = ( - effect: Effect.Effect, -): Promise => - aresTestRuntime.runPromise(TestClock.setTime(confirmedAtEpoch).pipe(Effect.andThen(effect))); const application = { decidedAt: confirmedAt, evidence: { @@ -104,7 +93,6 @@ const application = { const decodedObservation = Result.getOrThrow( Schema.decodeUnknownResult(AresSubjectEvidenceSchema)(application.evidence), ); - const request: AresApplyRequest = { correlationId: 'ares-test-correlation', observation: application.evidence, @@ -142,11 +130,9 @@ const request: AresApplyRequest = { ], userConfirmed: true, }; - class TestFailure extends Schema.TaggedError()('TestFailure', { action: Schema.String, }) {} - const makeInvoker = ( calls: string[], failAction?: string, @@ -179,11 +165,9 @@ const makeInvoker = ( }), }; }; - const gateway = makeOperationGateway(() => Effect.succeed({ expiresAt: 1_788_430_000, token: 'signed-gateway-token' }), ); - const makeReads = (displayName: string | null = null): AresApplyReads => ({ contactPoints: () => Effect.succeed({ items: [] }), identifiers: () => Effect.succeed({ items: [] }), @@ -209,665 +193,728 @@ const makeReads = (displayName: string | null = null): AresApplyReads => ({ }, }), }); - -test('runs only explicitly selected standard Actions and preserves every result', async () => { - const calls: string[] = []; - const outcome = await runAresEffectTestPromise( - applyAresObservation(request, makeInvoker(calls), { gateway, reads: makeReads() }), - ); - - assert.deepEqual(calls, [ - 'update-party|Bearer signed-gateway-token', - 'add-party-official-identifier|Bearer signed-gateway-token', - ]); - assert.equal(outcome._tag, 'AresApplyCompleted'); - assert.deepEqual( - outcome.completed.map(({ route }) => route), - ['PARTY_UPDATE', 'IDENTIFIER_ADD'], - ); -}); - -test('propagates bounded evidence and independent command delivery keys', async () => { - const calls: string[] = []; - const recorded: { - readonly evidenceRef: string | undefined; - readonly idempotencyKey: string; - }[] = []; - const delegate = makeInvoker(calls); - const invoker: PartyRegistryStandardActionInvoker = { - ...delegate, - addPartyOfficialIdentifier: (payload, authorization, options) => { - recorded.push({ - evidenceRef: payload.externalEvidence?.evidenceRef, - idempotencyKey: options.idempotencyKey, - }); - return delegate.addPartyOfficialIdentifier(payload, authorization, options); - }, - updateParty: (payload, authorization, options) => { - recorded.push({ - evidenceRef: payload.externalEvidence?.evidenceRef, - idempotencyKey: options.idempotencyKey, +it.layer(Layer.effectDiscard(TestClock.setTime(confirmedAtEpoch)))('ARES application', (aresIt) => { + aresIt.effect('runs only explicitly selected standard Actions and preserves every result', () => + Effect.gen(function* runsOnlyExplicitlySelectedStandard() { + const calls: string[] = []; + const outcome = yield* applyAresObservation(request, makeInvoker(calls), { + gateway, + reads: makeReads(), }); - return delegate.updateParty(payload, authorization, options); - }, - }; - - await runAresEffectTestPromise( - applyAresObservation(request, invoker, { - baseUrl: 'https://party.example/party-registry-api', - gateway, - reads: makeReads(), + expect(calls).toEqual([ + 'update-party|Bearer signed-gateway-token', + 'add-party-official-identifier|Bearer signed-gateway-token', + ]); + expect( + Match.value(outcome).pipe( + Match.tag('AresApplyCompleted', () => true), + Match.orElse(() => false), + ), + ).toBe(true); + expect(outcome.completed.map(({ route }) => route)).toEqual([ + 'PARTY_UPDATE', + 'IDENTIFIER_ADD', + ]); }), ); - - assert.deepEqual( - recorded.map(({ idempotencyKey }) => idempotencyKey), - ['ares-name-1', 'ares-ico-1'], - ); - assert.equal( - recorded.every(({ evidenceRef }) => - evidenceRef === undefined ? false : evidenceRef.includes('ares:12345678'), - ), - true, - ); -}); - -test('stops after the first failed Action and returns a typed partial outcome', async () => { - const calls: string[] = []; - const outcome = await runAresEffectTestPromise( - applyAresObservation( - request, - makeInvoker(calls, 'add-party-official-identifier|Bearer signed-gateway-token'), - { gateway, reads: makeReads() }, - ), - ); - - assert.deepEqual(calls, [ - 'update-party|Bearer signed-gateway-token', - 'add-party-official-identifier|Bearer signed-gateway-token', - ]); - const partial = Match.value(outcome).pipe( - Match.tag('AresApplyPartiallyCompleted', (value) => value), - Match.orElse(() => assert.fail('Expected a partially completed ARES application')), + aresIt.effect('propagates bounded evidence and independent command delivery keys', () => + Effect.gen(function* propagatesBoundedEvidenceAndIndependent() { + const calls: string[] = []; + const recorded: { + readonly evidenceRef: string | undefined; + readonly idempotencyKey: string; + }[] = []; + const delegate = makeInvoker(calls); + const invoker: PartyRegistryStandardActionInvoker = { + ...delegate, + addPartyOfficialIdentifier: (payload, authorization, options) => { + recorded.push({ + evidenceRef: payload.externalEvidence?.evidenceRef, + idempotencyKey: options.idempotencyKey, + }); + return delegate.addPartyOfficialIdentifier(payload, authorization, options); + }, + updateParty: (payload, authorization, options) => { + recorded.push({ + evidenceRef: payload.externalEvidence?.evidenceRef, + idempotencyKey: options.idempotencyKey, + }); + return delegate.updateParty(payload, authorization, options); + }, + }; + yield* applyAresObservation(request, invoker, { + baseUrl: 'https://party.example/party-registry-api', + gateway, + reads: makeReads(), + }); + expect(recorded.map(({ idempotencyKey }) => idempotencyKey)).toEqual([ + 'ares-name-1', + 'ares-ico-1', + ]); + expect( + recorded.every(({ evidenceRef }) => + evidenceRef === undefined ? false : evidenceRef.includes('ares:12345678'), + ), + ).toBe(true); + }), ); - assert.deepEqual( - partial.completed.map(({ route }) => route), - ['PARTY_UPDATE'], + aresIt.effect('stops after the first failed Action and returns a typed partial outcome', () => + Effect.gen(function* stopsAfterTheFirstFailed() { + const calls: string[] = []; + const outcome = yield* applyAresObservation( + request, + makeInvoker(calls, 'add-party-official-identifier|Bearer signed-gateway-token'), + { gateway, reads: makeReads() }, + ); + expect(calls).toEqual([ + 'update-party|Bearer signed-gateway-token', + 'add-party-official-identifier|Bearer signed-gateway-token', + ]); + const partial = Match.value(outcome).pipe( + Match.tag('AresApplyPartiallyCompleted', (value) => value), + Match.orElse(() => expect.unreachable('Expected a partially completed ARES application')), + ); + expect(partial.completed.map(({ route }) => route)).toEqual(['PARTY_UPDATE']); + expect(partial.failed.route).toBe('IDENTIFIER_ADD'); + expect( + Match.value(partial.failed.error).pipe( + Match.tag('TestFailure', () => true), + Match.orElse(() => false), + ), + ).toBe(true); + }), ); - assert.equal(partial.failed.route, 'IDENTIFIER_ADD'); - assert.equal(partial.failed.error._tag, 'TestFailure'); -}); - -test('resumes a replay after a prior selected fact is already satisfied', async () => { - const calls: string[] = []; - const outcome = await runAresEffectTestPromise( - applyAresObservation(request, makeInvoker(calls), { - gateway, - reads: makeReads('Example s.r.o.'), + aresIt.effect('resumes a replay after a prior selected fact is already satisfied', () => + Effect.gen(function* resumesAReplayAfterA() { + const calls: string[] = []; + const outcome = yield* applyAresObservation(request, makeInvoker(calls), { + gateway, + reads: makeReads('Example s.r.o.'), + }); + expect(calls).toEqual(['add-party-official-identifier|Bearer signed-gateway-token']); + expect( + Match.value(outcome).pipe( + Match.tag('AresApplyCompleted', () => true), + Match.orElse(() => false), + ), + ).toBe(true); + expect(outcome.skipped).toEqual([ + { fact: 'BUSINESS_NAME', reason: 'ALREADY_SATISFIED', route: 'PARTY_UPDATE' }, + ]); + expect(outcome.completed.map(({ route }) => route)).toEqual(['IDENTIFIER_ADD']); }), ); - - assert.deepEqual(calls, ['add-party-official-identifier|Bearer signed-gateway-token']); - assert.equal(outcome._tag, 'AresApplyCompleted'); - assert.deepEqual(outcome.skipped, [ - { fact: 'BUSINESS_NAME', reason: 'ALREADY_SATISFIED', route: 'PARTY_UPDATE' }, - ]); - assert.deepEqual( - outcome.completed.map(({ route }) => route), - ['IDENTIFIER_ADD'], + aresIt.effect('defers when canonical revision or refreshed evidence changed', () => + Effect.gen(function* defersWhenCanonicalRevisionOr() { + const calls: string[] = []; + const revisionRequest: AresApplyRequest = { + ...request, + selections: request.selections.map((selection) => + selection.route === 'PARTY_UPDATE' + ? { ...selection, payload: { ...selection.payload, expectedRevision: 2 } } + : selection, + ), + }; + const changedReads: AresApplyReads = { + ...makeReads(), + observation: () => + Effect.succeed({ + ...decodedObservation, + subject: { + ...decodedObservation.subject, + businessName: Option.some('Changed at provider'), + }, + }), + }; + const [revisionOutcome, changedOutcome] = yield* Effect.all( + [ + applyAresObservation(revisionRequest, makeInvoker(calls), { + gateway, + reads: makeReads(), + }), + applyAresObservation(request, makeInvoker(calls), { gateway, reads: changedReads }), + ], + { concurrency: 'unbounded' }, + ); + expect( + Match.value(revisionOutcome).pipe( + Match.tag('AresApplyDeferred', () => true), + Match.orElse(() => false), + ), + ).toBe(true); + expect( + Match.value(changedOutcome).pipe( + Match.tag('AresApplyDeferred', () => true), + Match.orElse(() => false), + ), + ).toBe(true); + expect(calls).toEqual([]); + }), ); -}); - -test('defers when canonical revision or refreshed evidence changed', async () => { - const calls: string[] = []; - const revisionRequest: AresApplyRequest = { - ...request, - selections: request.selections.map((selection) => - selection.route === 'PARTY_UPDATE' - ? { ...selection, payload: { ...selection.payload, expectedRevision: 2 } } - : selection, - ), - }; - const changedReads: AresApplyReads = { - ...makeReads(), - observation: () => - Effect.succeed({ - ...decodedObservation, - subject: { - ...decodedObservation.subject, - businessName: Option.some('Changed at provider'), - }, - }), - }; - const [revisionOutcome, changedOutcome] = await Promise.all([ - runAresEffectTestPromise( - applyAresObservation(revisionRequest, makeInvoker(calls), { gateway, reads: makeReads() }), - ), - runAresEffectTestPromise( - applyAresObservation(request, makeInvoker(calls), { gateway, reads: changedReads }), - ), - ]); - - assert.equal(revisionOutcome._tag, 'AresApplyDeferred'); - assert.equal(changedOutcome._tag, 'AresApplyDeferred'); - assert.deepEqual(calls, []); -}); - -test('rejects unconfirmed or observation-mismatched selections before invoking an Action', async () => { - const calls: string[] = []; - const invalidRequests: readonly AresApplyRequest[] = [ - { - ...request, - userConfirmed: false, - }, - { - correlationId: request.correlationId, - observation: request.observation, - partyRef, - selections: [ - { - fact: 'BUSINESS_NAME', - idempotencyKey: 'ares-invalid-name-1', - payload: { - displayName: 'Injected name', - expectedRevision: 1, - partyRef, - provenanceMethod: 'ARES_USER_CONFIRMED', - provenanceSource: 'ares:12345678', - validFrom: actionValidFrom, + aresIt.effect( + 'rejects unconfirmed or observation-mismatched selections before invoking an Action', + () => + Effect.gen(function* rejectsUnconfirmedOrObservationmismatchedSelections() { + const calls: string[] = []; + const invalidRequests: readonly AresApplyRequest[] = [ + { + ...request, + userConfirmed: false, }, - route: 'PARTY_UPDATE', - }, - ], - userConfirmed: true, - }, - ]; - - const results = await Promise.all( - invalidRequests.map((invalidRequest) => - runAresEffectTestPromise( - applyAresObservation(invalidRequest, makeInvoker(calls), { - gateway, - reads: makeReads(), - }).pipe(Effect.result), - ), - ), - ); - for (const result of results) { - assert.equal('failure' in result, true); - if ('failure' in result) { - assert.equal(result.failure instanceof AresApplySelectionInvalid, true); - } - } - assert.deepEqual(calls, []); -}); - -test('does not accept a different street number as the observed registered address', async () => { - const calls: string[] = []; - const invalidRequest: AresApplyRequest = { - correlationId: request.correlationId, - observation: request.observation, - partyRef, - selections: [ - { - fact: 'REGISTERED_ADDRESS', - idempotencyKey: 'ares-address-1', - payload: { - contactPoint: { - address: { - addressLine1: 'Main 100', - city: 'Prague', - countryCode: 'CZ', - postalCode: '11000', - }, - purposes: [ + { + correlationId: request.correlationId, + observation: request.observation, + partyRef, + selections: [ { - preferred: false, - purpose: 'REGISTERED', - registryContext: { jurisdiction: 'CZ', registryKey: 'ARES' }, + fact: 'BUSINESS_NAME', + idempotencyKey: 'ares-invalid-name-1', + payload: { + displayName: 'Injected name', + expectedRevision: 1, + partyRef, + provenanceMethod: 'ARES_USER_CONFIRMED', + provenanceSource: 'ares:12345678', + validFrom: actionValidFrom, + }, + route: 'PARTY_UPDATE', }, ], - type: 'ADDRESS', + userConfirmed: true, }, + ]; + const results = yield* Effect.all( + invalidRequests.map((invalidRequest) => + applyAresObservation(invalidRequest, makeInvoker(calls), { + gateway, + reads: makeReads(), + }).pipe(Effect.result), + ), + { concurrency: 'unbounded' }, + ); + for (const result of results) { + expect('failure' in result).toBe(true); + if ('failure' in result) { + expect(Schema.is(AresApplySelectionInvalid)(result.failure)).toBe(true); + } + } + expect(calls).toEqual([]); + }), + ); + aresIt.effect( + 'does not accept a different street number as the observed registered address', + () => + Effect.gen(function* doesNotAcceptADifferent() { + const calls: string[] = []; + const invalidRequest: AresApplyRequest = { + correlationId: request.correlationId, + observation: request.observation, partyRef, - privacyClassification: 'PUBLIC', - provenance: { - authoritative: true, - evidenceReference: 'ares:12345678', - method: 'PROVIDER_OBSERVATION', - source: 'EXTERNAL_EVIDENCE', - }, - validFrom: decodedObservation.observedAt, - verification: { state: 'UNVERIFIED' }, - }, - route: 'CONTACT_POINT_ADD', + selections: [ + { + fact: 'REGISTERED_ADDRESS', + idempotencyKey: 'ares-address-1', + payload: { + contactPoint: { + address: { + addressLine1: 'Main 100', + city: 'Prague', + countryCode: 'CZ', + postalCode: '11000', + }, + purposes: [ + { + preferred: false, + purpose: 'REGISTERED', + registryContext: { jurisdiction: 'CZ', registryKey: 'ARES' }, + }, + ], + type: 'ADDRESS', + }, + partyRef, + privacyClassification: 'PUBLIC', + provenance: { + authoritative: true, + evidenceReference: 'ares:12345678', + method: 'PROVIDER_OBSERVATION', + source: 'EXTERNAL_EVIDENCE', + }, + validFrom: decodedObservation.observedAt, + verification: { state: 'UNVERIFIED' }, + }, + route: 'CONTACT_POINT_ADD', + }, + ], + userConfirmed: true, + }; + const result = yield* applyAresObservation(invalidRequest, makeInvoker(calls), { + gateway, + reads: makeReads(), + }).pipe(Effect.result); + expect('failure' in result).toBe(true); + expect(calls).toEqual([]); + }), + ); + const historicalEvidence = (fact: 'BUSINESS_NAME' | 'ICO'): AresAppliedEvidence => { + const result = deriveAresEvidenceApplication({ + canonical: { + archived: false, + displayName: null, + icoValues: [], + identityAmbiguous: false, + partyType: 'ORGANIZATION', + registeredAddresses: [], }, - ], - userConfirmed: true, + decidedAt: application.decidedAt, + evidence: application.evidence, + selectedFacts: [fact], + userConfirmed: true, + }); + const decision = Option.getOrThrow(Option.fromNullishOr(result.factDecisions[0])); + expect(decision).toBeDefined(); + return makeAresAppliedEvidence(result, decision); }; - const result = await runAresEffectTestPromise( - applyAresObservation(invalidRequest, makeInvoker(calls), { gateway, reads: makeReads() }).pipe( - Effect.result, - ), - ); - assert.equal('failure' in result, true); - assert.deepEqual(calls, []); -}); - -const historicalEvidence = (fact: 'BUSINESS_NAME' | 'ICO'): AresAppliedEvidence => { - const result = deriveAresEvidenceApplication({ - canonical: { - archived: false, - displayName: null, - icoValues: [], - identityAmbiguous: false, - partyType: 'ORGANIZATION', - registeredAddresses: [], + const correctionSelection: AresApplyRequest['selections'][number] = { + fact: 'BUSINESS_NAME', + idempotencyKey: 'review-only', + payload: { + evidenceRefs: ['review:ares'], + evidenceSource: 'MANUAL_REVIEW', + factKind: 'DISPLAY_NAME', + partyId: Result.getOrThrow(Schema.decodeUnknownResult(PartyIdSchema)(partyRef.resourceId)), + policyVersion: 'party-correction.v1', + provenance: { method: 'REVIEW', source: 'ARES' }, + reasonCode: 'WRONG_IDENTITY_VALUE', + replacementValue: 'Example s.r.o.', + targetAssertionId: Result.getOrThrow( + Schema.decodeUnknownResult(TargetAssertionIdSchema)('30000000-0000-4000-8000-000000000001'), + ), }, - decidedAt: application.decidedAt, - evidence: application.evidence, - selectedFacts: [fact], - userConfirmed: true, - }); - const [decision] = result.factDecisions; - assert.ok(decision); - return makeAresAppliedEvidence(result, decision); -}; -const correctionSelection: AresApplyRequest['selections'][number] = { - fact: 'BUSINESS_NAME', - idempotencyKey: 'review-only', - payload: { - evidenceRefs: ['review:ares'], - evidenceSource: 'MANUAL_REVIEW', - factKind: 'DISPLAY_NAME', - partyId: Result.getOrThrow(Schema.decodeUnknownResult(PartyIdSchema)(partyRef.resourceId)), - policyVersion: 'party-correction.v1', - provenance: { method: 'REVIEW', source: 'ARES' }, - reasonCode: 'WRONG_IDENTITY_VALUE', - replacementValue: 'Example s.r.o.', - targetAssertionId: Result.getOrThrow( - Schema.decodeUnknownResult(TargetAssertionIdSchema)('30000000-0000-4000-8000-000000000001'), - ), - }, - route: 'PARTY_CORRECTION', -}; - -test('review-authorized assertion context returns explicit Correction handoff without a write', async () => { - const calls: string[] = []; - const reads = makeReads('Wrong name'); - let reviewed = false; - const result = await runAresEffectTestPromise( - applyAresObservation({ ...request, selections: [correctionSelection] }, makeInvoker(calls), { - gateway, - reads: { - ...reads, - party: (payload, ...args) => { - reviewed = payload.includeFactHistory === true; - return reads.party(payload, ...args).pipe( - Effect.map((detail) => ({ - ...detail, - currentFactAssertions: [ - { - assertionId: currentAssertionId, - externalEvidence: Option.some(historicalEvidence('BUSINESS_NAME')), - factKind: 'DISPLAY_NAME' as const, - isCurrent: true, - partyRef, - recordedAt: confirmedInstant, - retractsAssertionId: Option.none(), - state: 'ACTIVE' as const, - supersedesAssertionId: Option.none(), - validFrom: confirmedInstant, - validTo: Option.none(), - value: 'Wrong name', - }, - ], - })), - ); - }, - }, - }), + route: 'PARTY_CORRECTION', + }; + aresIt.effect( + 'review-authorized assertion context returns explicit Correction handoff without a write', + () => + Effect.gen(function* reviewauthorizedAssertionContextReturnsExplicit() { + const calls: string[] = []; + const reads = makeReads('Wrong name'); + let reviewed = false; + const result = yield* applyAresObservation( + { ...request, selections: [correctionSelection] }, + makeInvoker(calls), + { + gateway, + reads: { + ...reads, + party: (payload, ...args) => { + reviewed = payload.includeFactHistory === true; + return reads.party(payload, ...args).pipe( + Effect.map((detail) => ({ + ...detail, + currentFactAssertions: [ + { + assertionId: currentAssertionId, + externalEvidence: Option.some(historicalEvidence('BUSINESS_NAME')), + factKind: 'DISPLAY_NAME' as const, + isCurrent: true, + partyRef, + recordedAt: confirmedInstant, + retractsAssertionId: Option.none(), + state: 'ACTIVE' as const, + supersedesAssertionId: Option.none(), + validFrom: confirmedInstant, + validTo: Option.none(), + value: 'Wrong name', + }, + ], + })), + ); + }, + }, + }, + ); + expect(reviewed).toBe(true); + const deferred = Match.value(result).pipe( + Match.tag('AresApplyDeferred', (value) => value), + Match.orElse(() => expect.unreachable('Expected a deferred ARES application')), + ); + expect(deferred.application.outcome).toBe('CORRECTION_CANDIDATE'); + expect(deferred.correctionCandidates[0]?.targetAssertionId).toBe( + '30000000-0000-4000-8000-000000000001', + ); + expect(deferred.correctionCandidates[0]?.observedValue).toBe('Example s.r.o.'); + expect(calls).toEqual([]); + }), ); - assert.equal(reviewed, true); - const deferred = Match.value(result).pipe( - Match.tag('AresApplyDeferred', (value) => value), - Match.orElse(() => assert.fail('Expected a deferred ARES application')), + aresIt.effect( + 'governed identifier history supports ICO correction suspicion without claiming the identifier', + () => + Effect.gen(function* governedIdentifierHistorySupportsIco() { + const calls: string[] = []; + const selection = Option.getOrThrow(Option.fromNullishOr(request.selections[1])); + expect(selection).toBeDefined(); + const encodedEvidence = yield* Schema.encodeEffect(AresAppliedEvidenceSchema)( + historicalEvidence('ICO'), + ); + const outcome = yield* applyAresObservation( + { ...request, selections: [selection] }, + makeInvoker(calls), + { + gateway, + reads: { + ...makeReads(), + identifiers: () => + Effect.succeed({ + items: [ + { + externalEvidence: encodedEvidence, + identifierType: 'ICO' as const, + namespace: 'CZ:ICO', + normalizedValue: '87654321', + officialIdentifierRef: { + moduleId: 'party.registry' as const, + resourceId: '40000000-0000-4000-8000-000000000001', + resourceType: 'party.registry.party-official-identifier' as const, + tenantId: partyRef.tenantId, + }, + partyRef, + recordedAt: application.decidedAt, + state: 'ACTIVE' as const, + validFrom: application.decidedAt, + validTo: null, + verification: 'VERIFIED' as const, + }, + ], + }), + }, + }, + ); + const deferred = Match.value(outcome).pipe( + Match.tag('AresApplyDeferred', (value) => value), + Match.orElse(() => expect.unreachable('Expected a deferred ARES application')), + ); + expect(deferred.application.outcome).toBe('CORRECTION_CANDIDATE'); + expect(deferred.correctionCandidates[0]?.fact).toBe('ICO'); + expect(calls).toEqual([]); + }), ); - assert.equal(deferred.application.outcome, 'CORRECTION_CANDIDATE'); - assert.equal( - deferred.correctionCandidates[0]?.targetAssertionId, - '30000000-0000-4000-8000-000000000001', + aresIt.effect( + 'every governed read and selected Action receives fresh audience-scoped authorization', + () => + Effect.gen(function* everyGovernedReadAndSelected() { + const tokens: string[] = []; + const calls: string[] = []; + const delegate = makeReads(); + const issued = makeOperationGateway(() => { + const token = `token-${tokens.length + 1}`; + tokens.push(token); + return Effect.succeed({ expiresAt: 1_788_430_000, token }); + }); + const authorized: string[] = []; + const reads: AresApplyReads = { + contactPoints: (payload, authorization, ...rest) => { + authorized.push(authorization); + return delegate.contactPoints(payload, authorization, ...rest); + }, + identifiers: (payload, authorization, ...rest) => { + authorized.push(authorization); + return delegate.identifiers(payload, authorization, ...rest); + }, + observation: (payload, authorization, ...rest) => { + authorized.push(authorization); + return delegate.observation(payload, authorization, ...rest); + }, + party: (payload, authorization, ...rest) => { + authorized.push(authorization); + expect(payload.includeFactHistory).toBe(undefined); + return delegate.party(payload, authorization, ...rest); + }, + }; + yield* applyAresObservation(request, makeInvoker(calls), { gateway: issued, reads }); + expect(authorized).toEqual([ + 'Bearer token-1', + 'Bearer token-2', + 'Bearer token-3', + 'Bearer token-4', + ]); + expect(calls).toEqual([ + 'update-party|Bearer token-5', + 'add-party-official-identifier|Bearer token-6', + ]); + }), ); - assert.equal(deferred.correctionCandidates[0]?.observedValue, 'Example s.r.o.'); - assert.deepEqual(calls, []); -}); - -test('governed identifier history supports ICO correction suspicion without claiming the identifier', async () => { - const calls: string[] = []; - const [, selection] = request.selections; - assert.ok(selection); - const outcome = await runAresEffectTestPromise( - applyAresObservation({ ...request, selections: [selection] }, makeInvoker(calls), { - gateway, - reads: { - ...makeReads(), - identifiers: () => - Effect.succeed({ - items: [ - { - externalEvidence: Schema.encodeSync(AresAppliedEvidenceSchema)( - historicalEvidence('ICO'), - ), - identifierType: 'ICO' as const, - namespace: 'CZ:ICO', - normalizedValue: '87654321', - officialIdentifierRef: { - moduleId: 'party.registry' as const, - resourceId: '40000000-0000-4000-8000-000000000001', - resourceType: 'party.registry.party-official-identifier' as const, - tenantId: partyRef.tenantId, - }, - partyRef, - recordedAt: application.decidedAt, - state: 'ACTIVE' as const, - validFrom: application.decidedAt, - validTo: null, - verification: 'VERIFIED' as const, - }, - ], - }), - }, + aresIt.effect('read denial fails before writes and preserves its declared error', () => + Effect.gen(function* readDenialFailsBeforeWrites() { + const calls: string[] = []; + const denied = { + _tag: 'PartyDetailForbiddenProblem' as const, + detail: 'denied', + status: 403 as const, + title: 'Forbidden', + type: 'urn:test:forbidden', + }; + const result = yield* applyAresObservation(request, makeInvoker(calls), { + gateway, + reads: { ...makeReads(), party: () => Effect.fail(denied) }, + }).pipe(Effect.result); + expect('failure' in result && result.failure === denied).toBe(true); + expect(calls).toEqual([]); }), ); - const deferred = Match.value(outcome).pipe( - Match.tag('AresApplyDeferred', (value) => value), - Match.orElse(() => assert.fail('Expected a deferred ARES application')), - ); - assert.equal(deferred.application.outcome, 'CORRECTION_CANDIDATE'); - assert.equal(deferred.correctionCandidates[0]?.fact, 'ICO'); - assert.deepEqual(calls, []); -}); - -test('every governed read and selected Action receives fresh audience-scoped authorization', async () => { - const tokens: string[] = []; - const calls: string[] = []; - const delegate = makeReads(); - const issued = makeOperationGateway(() => { - const token = `token-${tokens.length + 1}`; - tokens.push(token); - return Effect.succeed({ expiresAt: 1_788_430_000, token }); - }); - const authorized: string[] = []; - const reads: AresApplyReads = { - contactPoints: (payload, authorization, ...rest) => { - authorized.push(authorization); - return delegate.contactPoints(payload, authorization, ...rest); - }, - identifiers: (payload, authorization, ...rest) => { - authorized.push(authorization); - return delegate.identifiers(payload, authorization, ...rest); - }, - observation: (payload, authorization, ...rest) => { - authorized.push(authorization); - return delegate.observation(payload, authorization, ...rest); - }, - party: (payload, authorization, ...rest) => { - authorized.push(authorization); - assert.equal(payload.includeFactHistory, undefined); - return delegate.party(payload, authorization, ...rest); - }, - }; - await runAresEffectTestPromise( - applyAresObservation(request, makeInvoker(calls), { gateway: issued, reads }), - ); - assert.deepEqual(authorized, [ - 'Bearer token-1', - 'Bearer token-2', - 'Bearer token-3', - 'Bearer token-4', - ]); - assert.deepEqual(calls, [ - 'update-party|Bearer token-5', - 'add-party-official-identifier|Bearer token-6', - ]); -}); - -test('read denial fails before writes and preserves its declared error', async () => { - const calls: string[] = []; - const denied = { - _tag: 'PartyDetailForbiddenProblem' as const, - detail: 'denied', - status: 403 as const, - title: 'Forbidden', - type: 'urn:test:forbidden', - }; - const result = await runAresEffectTestPromise( - applyAresObservation(request, makeInvoker(calls), { - gateway, - reads: { ...makeReads(), party: () => Effect.fail(denied) }, - }).pipe(Effect.result), + aresIt.effect('alias and archived targets never dispatch selected writes', () => + Effect.all( + (['ALIAS', 'ARCHIVED'] as const).map((kind) => + Effect.gen(function* aliasAndArchivedTargetsNever() { + const calls: string[] = []; + const reads = makeReads(); + const result = yield* applyAresObservation(request, makeInvoker(calls), { + gateway, + reads: { + ...reads, + party: (...args) => + reads.party(...args).pipe( + Effect.map((detail) => ({ + ...detail, + party: { + ...detail.party, + archivedAt: + kind === 'ARCHIVED' ? Option.some(confirmedInstant) : Option.none(), + }, + resolution: { + ...detail.resolution, + kind: kind === 'ALIAS' ? ('ALIAS' as const) : ('DIRECT' as const), + }, + })), + ), + }, + }).pipe(Effect.result); + if (kind === 'ALIAS') { + expect('failure' in result).toBe(true); + } else { + expect('success' in result).toBe(true); + if ('success' in result) { + Match.value(result.success).pipe( + Match.tag('AresApplyDeferred', () => null), + Match.orElse(() => + expect.unreachable('Expected an archived Party to defer ARES application'), + ), + ); + } + } + expect(calls).toEqual([]); + }), + ), + { concurrency: 'unbounded' }, + ), ); - assert.equal('failure' in result && result.failure === denied, true); - assert.deepEqual(calls, []); -}); - -test('alias and archived targets never dispatch selected writes', async () => { - await Promise.all( - (['ALIAS', 'ARCHIVED'] as const).map(async (kind) => { + aresIt.effect('provider revision change alone invalidates the earlier confirmation', () => + Effect.gen(function* providerRevisionChangeAloneInvalidates() { const calls: string[] = []; const reads = makeReads(); - const result = await runAresEffectTestPromise( - applyAresObservation(request, makeInvoker(calls), { - gateway, - reads: { - ...reads, - party: (...args) => - reads.party(...args).pipe( - Effect.map((detail) => ({ - ...detail, - party: { - ...detail.party, - archivedAt: kind === 'ARCHIVED' ? Option.some(confirmedInstant) : Option.none(), - }, - resolution: { - ...detail.resolution, - kind: kind === 'ALIAS' ? ('ALIAS' as const) : ('DIRECT' as const), - }, - })), - ), + const outcome = yield* applyAresObservation(request, makeInvoker(calls), { + gateway, + reads: { + ...reads, + observation: (...args) => + reads.observation(...args).pipe( + Effect.map((observed) => ({ + ...observed, + providerChangedOn: Option.some(DateTime.makeUnsafe('2026-09-03')), + })), + ), + }, + }); + expect( + Match.value(outcome).pipe( + Match.tag('AresApplyDeferred', () => true), + Match.orElse(() => false), + ), + ).toBe(true); + expect(calls).toEqual([]); + }), + ); + aresIt.effect( + 'retry preserves exact command payload and reports required standard recovery', + () => + Effect.gen(function* retryPreservesExactCommandPayload() { + const payloads: unknown[] = []; + const calls: string[] = []; + const delegate = makeInvoker(calls, 'update-party|Bearer signed-gateway-token'); + const invoker: PartyRegistryStandardActionInvoker = { + ...delegate, + updateParty: (payload, auth, options) => { + payloads.push({ options, payload }); + return delegate.updateParty(payload, auth, options); }, - }).pipe(Effect.result), - ); - if (kind === 'ALIAS') { - assert.equal('failure' in result, true); - } else { - assert.equal('success' in result, true); - if ('success' in result) { - Match.value(result.success).pipe( - Match.tag('AresApplyDeferred', () => null), - Match.orElse(() => assert.fail('Expected an archived Party to defer ARES application')), + }; + for (let retry = 0; retry < 2; retry += 1) { + const result = yield* applyAresObservation(request, invoker, { + gateway, + reads: makeReads(), + }); + const partial = Match.value(result).pipe( + Match.tag('AresApplyPartiallyCompleted', (value) => value), + Match.orElse(() => + expect.unreachable('Expected a partially completed ARES application'), + ), ); + expect(partial.failed.idempotencyKey).toBe('ares-name-1'); + expect(partial.failed.recovery).toBe('RESOLVE_STANDARD_ACTION_BEFORE_RETRY'); } - } - assert.deepEqual(calls, []); - }), + expect(payloads[0]).toEqual(payloads[1]); + expect(calls).toEqual([ + 'update-party|Bearer signed-gateway-token', + 'update-party|Bearer signed-gateway-token', + ]); + }), ); -}); - -test('provider revision change alone invalidates the earlier confirmation', async () => { - const calls: string[] = []; - const reads = makeReads(); - const outcome = await runAresEffectTestPromise( - applyAresObservation(request, makeInvoker(calls), { - gateway, - reads: { - ...reads, - observation: (...args) => - reads.observation(...args).pipe( - Effect.map((observed) => ({ - ...observed, - providerChangedOn: Option.some(DateTime.makeUnsafe('2026-09-03')), - })), + aresIt.effect( + 'failed second Action stops the following supported address and retains prior commit receipt', + () => + Effect.gen(function* failedSecondActionStopsThe() { + const calls: string[] = []; + const address: AresApplyRequest['selections'][number] = { + fact: 'REGISTERED_ADDRESS', + idempotencyKey: 'ares-address-1', + payload: { + contactPoint: { + address: { + addressLine1: 'Main 10', + city: 'Prague', + countryCode: 'CZ', + postalCode: '11000', + }, + purposes: [ + { + preferred: false, + purpose: 'REGISTERED', + registryContext: { jurisdiction: 'CZ', registryKey: 'ARES' }, + }, + ], + type: 'ADDRESS', + }, + partyRef, + privacyClassification: 'PUBLIC', + provenance: { + authoritative: true, + evidenceReference: 'ares:12345678', + method: 'PROVIDER_OBSERVATION', + source: 'EXTERNAL_EVIDENCE', + }, + validFrom: decodedObservation.observedAt, + verification: { state: 'UNVERIFIED' }, + }, + route: 'CONTACT_POINT_ADD', + }; + const delegate = makeInvoker( + calls, + 'add-party-official-identifier|Bearer signed-gateway-token', + ); + const outcome = yield* applyAresObservation( + { ...request, selections: [...request.selections, address] }, + { + ...delegate, + addContactPoint: () => { + calls.push('unexpected-address'); + return Effect.fail(new TestFailure({ action: 'address' })); + }, + }, + { gateway, reads: makeReads() }, + ); + expect( + Match.value(outcome).pipe( + Match.tag('AresApplyPartiallyCompleted', () => true), + Match.orElse(() => false), ), - }, - }), + ).toBe(true); + expect(outcome.completed.length).toBe(1); + expect(calls).toEqual([ + 'update-party|Bearer signed-gateway-token', + 'add-party-official-identifier|Bearer signed-gateway-token', + ]); + }), ); - assert.equal(outcome._tag, 'AresApplyDeferred'); - assert.deepEqual(calls, []); -}); - -test('retry preserves exact command payload and reports required standard recovery', async () => { - const payloads: unknown[] = []; - const calls: string[] = []; - const delegate = makeInvoker(calls, 'update-party|Bearer signed-gateway-token'); - const invoker: PartyRegistryStandardActionInvoker = { - ...delegate, - updateParty: (payload, auth, options) => { - payloads.push({ options, payload }); - return delegate.updateParty(payload, auth, options); - }, - }; - for (let retry = 0; retry < 2; retry += 1) { - // eslint-disable-next-line no-await-in-loop -- Retry must follow the completed first attempt. - const result = await runAresEffectTestPromise( - applyAresObservation(request, invoker, { gateway, reads: makeReads() }), - ); - const partial = Match.value(result).pipe( - Match.tag('AresApplyPartiallyCompleted', (value) => value), - Match.orElse(() => assert.fail('Expected a partially completed ARES application')), - ); - assert.equal(partial.failed.idempotencyKey, 'ares-name-1'); - assert.equal(partial.failed.recovery, 'RESOLVE_STANDARD_ACTION_BEFORE_RETRY'); - } - assert.deepEqual(payloads[0], payloads[1]); - assert.deepEqual(calls, [ - 'update-party|Bearer signed-gateway-token', - 'update-party|Bearer signed-gateway-token', - ]); -}); - -test('failed second Action stops the following supported address and retains prior commit receipt', async () => { - const calls: string[] = []; - const address: AresApplyRequest['selections'][number] = { - fact: 'REGISTERED_ADDRESS', - idempotencyKey: 'ares-address-1', - payload: { - contactPoint: { - address: { - addressLine1: 'Main 10', - city: 'Prague', - countryCode: 'CZ', - postalCode: '11000', - }, - purposes: [ + aresIt.effect( + 'stale refreshed evidence and missing canonical target cannot execute enrichment', + () => + Effect.gen(function* staleRefreshedEvidenceAndMissing() { + const calls: string[] = []; + const stale = yield* applyAresObservation(request, makeInvoker(calls), { + gateway, + reads: { + ...makeReads(), + observation: () => + Effect.succeed({ + ...decodedObservation, + observedAt: DateTime.makeUnsafe('2026-09-03T09:59:00.000Z'), + servedAt: DateTime.makeUnsafe('2026-09-03T10:00:00.000Z'), + }), + }, + }); + expect( + Match.value(stale).pipe( + Match.tag('AresApplyDeferred', () => true), + Match.orElse(() => false), + ), + ).toBe(true); + const absent = yield* applyAresObservation( + { ...request, partyRef: null }, + makeInvoker(calls), { - preferred: false, - purpose: 'REGISTERED', - registryContext: { jurisdiction: 'CZ', registryKey: 'ARES' }, + gateway, + reads: makeReads(), }, - ], - type: 'ADDRESS', - }, - partyRef, - privacyClassification: 'PUBLIC', - provenance: { - authoritative: true, - evidenceReference: 'ares:12345678', - method: 'PROVIDER_OBSERVATION', - source: 'EXTERNAL_EVIDENCE', - }, - validFrom: decodedObservation.observedAt, - verification: { state: 'UNVERIFIED' }, - }, - route: 'CONTACT_POINT_ADD', - }; - const delegate = makeInvoker(calls, 'add-party-official-identifier|Bearer signed-gateway-token'); - const outcome = await runAresEffectTestPromise( - applyAresObservation( - { ...request, selections: [...request.selections, address] }, - { - ...delegate, - addContactPoint: () => { - calls.push('unexpected-address'); - return Effect.fail(new TestFailure({ action: 'address' })); - }, - }, - { gateway, reads: makeReads() }, - ), + ).pipe(Effect.result); + expect('failure' in absent).toBe(true); + expect(calls).toEqual([]); + }), ); - assert.equal(outcome._tag, 'AresApplyPartiallyCompleted'); - assert.equal(outcome.completed.length, 1); - assert.deepEqual(calls, [ - 'update-party|Bearer signed-gateway-token', - 'add-party-official-identifier|Bearer signed-gateway-token', - ]); -}); - -test('stale refreshed evidence and missing canonical target cannot execute enrichment', async () => { - const calls: string[] = []; - const stale = await runAresEffectTestPromise( - applyAresObservation(request, makeInvoker(calls), { - gateway, - reads: { - ...makeReads(), - observation: () => - Effect.succeed({ - ...decodedObservation, - observedAt: DateTime.makeUnsafe('2026-09-03T09:59:00.000Z'), - servedAt: DateTime.makeUnsafe('2026-09-03T10:00:00.000Z'), - }), - }, + aresIt.effect('fresh identical refresh cannot revive an expired original confirmation', () => + Effect.gen(function* freshIdenticalRefreshCannotRevive() { + const calls: string[] = []; + const outcome = yield* applyAresObservation( + { + ...request, + observation: { + ...request.observation, + observedAt: '2026-09-03T09:59:00.000Z', + servedAt: '2026-09-03T10:00:00.000Z', + }, + }, + makeInvoker(calls), + { gateway, reads: makeReads() }, + ); + const deferred = Match.value(outcome).pipe( + Match.tag('AresApplyDeferred', (value) => value), + Match.orElse(() => + expect.unreachable('Expected an expired confirmation to defer ARES application'), + ), + ); + expect(deferred.application.factDecisions[0]?.reasonCode).toBe('observation_not_fresh'); + expect(deferred.correctionCandidates).toEqual([]); + expect(calls).toEqual([]); }), ); - assert.equal(stale._tag, 'AresApplyDeferred'); - const absent = await runAresEffectTestPromise( - applyAresObservation({ ...request, partyRef: null }, makeInvoker(calls), { - gateway, - reads: makeReads(), - }).pipe(Effect.result), - ); - assert.equal('failure' in absent, true); - assert.deepEqual(calls, []); -}); - -test('fresh identical refresh cannot revive an expired original confirmation', async () => { - const calls: string[] = []; - const outcome = await runAresEffectTestPromise( - applyAresObservation( - { - ...request, - observation: { - ...request.observation, - observedAt: '2026-09-03T09:59:00.000Z', - servedAt: '2026-09-03T10:00:00.000Z', + aresIt.effect('a correction route is never historical-error evidence by itself', () => + Effect.gen(function* aCorrectionRouteIsNever() { + const calls: string[] = []; + const outcome = yield* applyAresObservation( + { ...request, selections: [correctionSelection] }, + makeInvoker(calls), + { + gateway, + reads: makeReads('Wrong name'), }, - }, - makeInvoker(calls), - { gateway, reads: makeReads() }, - ), - ); - const deferred = Match.value(outcome).pipe( - Match.tag('AresApplyDeferred', (value) => value), - Match.orElse(() => assert.fail('Expected an expired confirmation to defer ARES application')), - ); - assert.equal(deferred.application.factDecisions[0]?.reasonCode, 'observation_not_fresh'); - assert.deepEqual(deferred.correctionCandidates, []); - assert.deepEqual(calls, []); -}); - -test('a correction route is never historical-error evidence by itself', async () => { - const calls: string[] = []; - const outcome = await runAresEffectTestPromise( - applyAresObservation({ ...request, selections: [correctionSelection] }, makeInvoker(calls), { - gateway, - reads: makeReads('Wrong name'), + ); + const deferred = Match.value(outcome).pipe( + Match.tag('AresApplyDeferred', (value) => value), + Match.orElse(() => + expect.unreachable('Expected the correction selection to defer ARES application'), + ), + ); + expect(deferred.application.outcome).toBe('NEEDS_CONFIRMATION'); + expect(deferred.correctionCandidates).toEqual([]); + expect(calls).toEqual([]); }), ); - const deferred = Match.value(outcome).pipe( - Match.tag('AresApplyDeferred', (value) => value), - Match.orElse(() => assert.fail('Expected the correction selection to defer ARES application')), - ); - assert.equal(deferred.application.outcome, 'NEEDS_CONFIRMATION'); - assert.deepEqual(deferred.correctionCandidates, []); - assert.deepEqual(calls, []); }); diff --git a/app/verticals/party-registry/tests/unit/api-integration-client-url.test.ts b/app/verticals/party-registry/tests/unit/api-integration-client-url.test.ts index 72cfecfa1..598340839 100644 --- a/app/verticals/party-registry/tests/unit/api-integration-client-url.test.ts +++ b/app/verticals/party-registry/tests/unit/api-integration-client-url.test.ts @@ -1,7 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema } from 'effect'; import { FetchHttpClient } from 'effect/unstable/http'; @@ -13,24 +10,31 @@ import { AresSubjectLookupIcoSchema } from '../../shared/domain/ares-evidence.ts const ico = Schema.decodeUnknownSync(AresSubjectLookupIcoSchema)('12345678'); -test('targets the mounted owner BFF prefix and supports a separate owner deployment', async () => { - const requests: string[] = []; - const fakeFetch: typeof globalThis.fetch = (input) => { - requests.push(String(input)); - return Promise.resolve(new Response(null, { status: 503 })); - }; - const location = Object.getOwnPropertyDescriptor(globalThis, 'location'); - Object.defineProperty(globalThis, 'location', { - configurable: true, - value: { origin: 'https://shell.example', pathname: '/en/contacts' }, - }); - const capture = (request: Effect.Effect) => - runEffectTestPromise( - request.pipe(Effect.result, Effect.provideService(FetchHttpClient.Fetch, fakeFetch)), +it.effect('targets the mounted owner BFF prefix and supports a separate owner deployment', () => + Effect.gen(function* testProgram1() { + const requests: string[] = []; + const fakeFetch: typeof globalThis.fetch = (input) => { + requests.push(String(input)); + return Promise.resolve(new Response(null, { status: 503 })); + }; + const location = Object.getOwnPropertyDescriptor(globalThis, 'location'); + yield* Effect.addFinalizer(() => + Effect.sync(() => { + if (location === undefined) { + Reflect.deleteProperty(globalThis, 'location'); + } else { + Object.defineProperty(globalThis, 'location', location); + } + }), ); - try { - await capture(executeAresLookupWithAuthorization({ ico }, 'Bearer test', 'test')); - await capture( + Object.defineProperty(globalThis, 'location', { + configurable: true, + value: { origin: 'https://shell.example', pathname: '/en/contacts' }, + }); + const capture = (request: Effect.Effect) => + request.pipe(Effect.result, Effect.provideService(FetchHttpClient.Fetch, fakeFetch)); + yield* capture(executeAresLookupWithAuthorization({ ico }, 'Bearer test', 'test')); + yield* capture( executePartyDetailWithAuthorization( { partyRef: { @@ -44,23 +48,17 @@ test('targets the mounted owner BFF prefix and supports a separate owner deploym 'test', ), ); - await capture(loadPartiesClientWithAuthorization({ query: 'Example' }, 'Bearer test', 'test')); - await capture( + yield* capture(loadPartiesClientWithAuthorization({ query: 'Example' }, 'Bearer test', 'test')); + yield* capture( executeAresLookupWithAuthorization({ ico }, 'Bearer test', 'test', { baseUrl: 'https://party.example/party-registry-api', }), ); - assert.deepEqual(requests, [ + expect(requests).toEqual([ 'https://shell.example/party-registry-api/reads/ares-lookup', 'https://shell.example/party-registry-api/reads/party-detail', 'https://shell.example/party-registry-api/party.registry/search/parties', 'https://party.example/party-registry-api/reads/ares-lookup', ]); - } finally { - if (location === undefined) { - Reflect.deleteProperty(globalThis, 'location'); - } else { - Object.defineProperty(globalThis, 'location', location); - } - } -}); + }), +); diff --git a/app/verticals/party-registry/tests/unit/api-integration-command-client.test.ts b/app/verticals/party-registry/tests/unit/api-integration-command-client.test.ts index 430d54d01..361d0d936 100644 --- a/app/verticals/party-registry/tests/unit/api-integration-command-client.test.ts +++ b/app/verticals/party-registry/tests/unit/api-integration-command-client.test.ts @@ -1,8 +1,5 @@ import { makeCommandAssertionFetch } from '../support/command-assertion-fetch.ts'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Effect } from 'effect'; import { FetchHttpClient } from 'effect/unstable/http'; import { @@ -10,77 +7,79 @@ import { requestSearchRebuildWithAuthorization, } from '../../src/api/party-command-client.ts'; -test('fresh assertions and command metadata reach the independent owner deployment', async () => { - const { requests, assertions, fakeFetch } = makeCommandAssertionFetch( - () => Response.json({ requestId: '10000000-0000-4000-8000-000000000001', status: 'QUEUED' }), - 'token', - ); - const options = { - baseUrl: 'https://party.example/party-registry-api', - correlationId: 'command-correlation', - gateway: { baseUrl: 'https://shell.example/shell-super-app-api' }, - idempotencyKey: 'rebuild-1', - traceId: 'command-trace', - }; - const invoke = () => - runEffectTestPromise( +it.effect('fresh assertions and command metadata reach the independent owner deployment', () => + Effect.gen(function* testProgram1() { + const { requests, assertions, fakeFetch } = makeCommandAssertionFetch( + () => Response.json({ requestId: '10000000-0000-4000-8000-000000000001', status: 'QUEUED' }), + 'token', + ); + const options = { + baseUrl: 'https://party.example/party-registry-api', + correlationId: 'command-correlation', + gateway: { baseUrl: 'https://shell.example/shell-super-app-api' }, + idempotencyKey: 'rebuild-1', + traceId: 'command-trace', + }; + const invoke = () => requestSearchRebuild({}, options).pipe( Effect.provideService(FetchHttpClient.Fetch, fakeFetch), + ); + const first = yield* invoke(); + const second = yield* invoke(); + expect(first.status).toBe('QUEUED'); + expect(second.status).toBe('QUEUED'); + expect(assertions()).toBe(2); + const commands = requests.filter( + (request) => new URL(request.url).hostname === 'party.example', + ); + expect(commands.map((request) => request.url)).toEqual( + Array.from( + { length: 2 }, + () => + 'https://party.example/party-registry-api/party-registry/actions/request-search-rebuild', ), ); - const first = await invoke(); - const second = await invoke(); - assert.equal(first.status, 'QUEUED'); - assert.equal(second.status, 'QUEUED'); - assert.equal(assertions(), 2); - const commands = requests.filter((request) => new URL(request.url).hostname === 'party.example'); - assert.deepEqual( - commands.map((request) => request.url), - Array.from( - { length: 2 }, - () => - 'https://party.example/party-registry-api/party-registry/actions/request-search-rebuild', - ), - ); - assert.deepEqual( - commands.map((request) => request.headers.get('authorization')), - ['Bearer token-1', 'Bearer token-2'], - ); - for (const request of commands) { - assert.equal(request.headers.get('x-correlation-id'), 'command-correlation'); - assert.equal(request.headers.get('x-trace-id'), 'command-trace'); - assert.equal(request.headers.get('idempotency-key'), 'rebuild-1'); - } -}); + expect(commands.map((request) => request.headers.get('authorization'))).toEqual([ + 'Bearer token-1', + 'Bearer token-2', + ]); + for (const request of commands) { + expect(request.headers.get('x-correlation-id')).toBe('command-correlation'); + expect(request.headers.get('x-trace-id')).toBe('command-trace'); + expect(request.headers.get('idempotency-key')).toBe('rebuild-1'); + } + }), +); -test('the browser default uses the relative mounted BFF prefix', async () => { - const urls: string[] = []; - const fakeFetch: typeof fetch = (input) => { - urls.push(String(input)); - return Promise.resolve( - Response.json({ requestId: '10000000-0000-4000-8000-000000000001', status: 'QUEUED' }), - ); - }; - const location = Object.getOwnPropertyDescriptor(globalThis, 'location'); - Object.defineProperty(globalThis, 'location', { - configurable: true, - value: { origin: 'https://shell.example', pathname: '/en' }, - }); - try { - await runEffectTestPromise( - requestSearchRebuildWithAuthorization({}, 'Bearer test', { - correlationId: 'relative', - idempotencyKey: 'rebuild-1', - }).pipe(Effect.provideService(FetchHttpClient.Fetch, fakeFetch)), +it.effect('the browser default uses the relative mounted BFF prefix', () => + Effect.gen(function* testProgram3() { + const urls: string[] = []; + const fakeFetch: typeof fetch = (input) => { + urls.push(String(input)); + return Promise.resolve( + Response.json({ requestId: '10000000-0000-4000-8000-000000000001', status: 'QUEUED' }), + ); + }; + const location = Object.getOwnPropertyDescriptor(globalThis, 'location'); + yield* Effect.addFinalizer(() => + Effect.sync(() => { + if (location === undefined) { + Reflect.deleteProperty(globalThis, 'location'); + } else { + Object.defineProperty(globalThis, 'location', location); + } + }), ); - assert.deepEqual(urls, [ + Object.defineProperty(globalThis, 'location', { + configurable: true, + value: { origin: 'https://shell.example', pathname: '/en' }, + }); + yield* requestSearchRebuildWithAuthorization({}, 'Bearer test', { + correlationId: 'relative', + idempotencyKey: 'rebuild-1', + }).pipe(Effect.provideService(FetchHttpClient.Fetch, fakeFetch)); + expect(urls).toEqual([ 'https://shell.example/party-registry-api/party-registry/actions/request-search-rebuild', ]); - } finally { - if (location === undefined) { - Reflect.deleteProperty(globalThis, 'location'); - } else { - Object.defineProperty(globalThis, 'location', location); - } - } -}); + }), +); diff --git a/app/verticals/party-registry/tests/unit/api-integration-command-contract.test.ts b/app/verticals/party-registry/tests/unit/api-integration-command-contract.test.ts index f35be715e..9cec10ed3 100644 --- a/app/verticals/party-registry/tests/unit/api-integration-command-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/api-integration-command-contract.test.ts @@ -1,8 +1,7 @@ -// @effect-diagnostics asyncFunction:off nodeBuiltinImport:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; +// @effect-diagnostics nodeBuiltinImport:off -- Inspect source files through the Node filesystem boundary; expires: 2026-12-31. +import { expect, it } from 'effect-rstest'; import { readFile, readdir } from 'node:fs/promises'; -import test from 'node:test'; -import { Schema } from 'effect'; +import { Effect, Schema, Struct } from 'effect'; import { partyRegistryCommandsApi, PartyCommandAliasWriteRejectedProblemSchema, @@ -16,61 +15,76 @@ const ref = (id: string) => ({ tenantId: '10000000-0000-4000-8000-000000000001', }); -test('every generated Action has its own statically named command endpoint', async () => { - const files = await readdir(new URL('../../src/actions/', import.meta.url)); - const actions = files - .filter((file) => file.endsWith('.action.ts')) - .map((file) => file.replace('.action.ts', '')) - .filter((slug) => !slug.includes('engagement')); - const endpoints = Object.values(partyRegistryCommandsApi.groups.partyCommands.endpoints); - assert.equal(endpoints.length, actions.length); - assert.deepEqual( - endpoints.map((endpoint) => endpoint.path).toSorted(), - actions.map((slug) => `/party-registry/actions/${slug}`).toSorted(), - ); - for (const slug of actions) { - const name = slug - .split('-') - .map((part, index) => (index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1))) - .join(''); - assert.ok(Object.hasOwn(partyRegistryCommandsApi.groups.partyCommands.endpoints, name)); - } -}); +it.effect('every generated Action has its own statically named command endpoint', () => + Effect.gen(function* testProgram1() { + const files = yield* Effect.promise(() => + readdir(new URL('../../src/actions/', import.meta.url)), + ); + const actions = files + .filter((file) => file.endsWith('.action.ts')) + .map((file) => file.replace('.action.ts', '')) + .filter((slug) => !slug.includes('engagement')); + const endpoints = Object.values(partyRegistryCommandsApi.groups.partyCommands.endpoints); + expect(endpoints.length).toBe(actions.length); + expect(endpoints.map((endpoint) => endpoint.path).toSorted()).toEqual( + actions.map((slug) => `/party-registry/actions/${slug}`).toSorted(), + ); + for (const slug of actions) { + const name = slug + .split('-') + .map((part, index) => (index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1))) + .join(''); + expect(Object.hasOwn(partyRegistryCommandsApi.groups.partyCommands.endpoints, name)).toBe( + true, + ); + } + }), +); -test('missing idempotency reaches the declared 428 while malformed supplied values fail decoding', () => { - assert.deepEqual(Schema.decodeUnknownSync(PartyCommandHeadersSchema)({}), {}); - assert.throws(() => - Schema.decodeUnknownSync(PartyCommandHeadersSchema)({ 'idempotency-key': '' }), - ); -}); +it.effect( + 'missing idempotency reaches the declared 428 while malformed supplied values fail decoding', + () => + Effect.gen(function* decodeContract1() { + expect(yield* Schema.decodeUnknownEffect(PartyCommandHeadersSchema)({})).toEqual({}); + expect(() => + Schema.decodeUnknownSync(PartyCommandHeadersSchema)({ 'idempotency-key': '' }), + ).toThrow(); + }), +); -test('alias conflict preserves both canonical and submitted references', () => { - const input = { - _tag: 'PartyCommandAliasWriteRejectedProblem', - aliasPartyRef: ref('10000000-0000-4000-8000-000000000002'), - canonicalPartyRef: ref('10000000-0000-4000-8000-000000000003'), - code: 'party_alias_write_rejected', - detail: 'Retry with the canonical Party.', - status: 409, - title: 'Canonical Party required', - type: 'urn:ontos:party:alias-write-rejected', - }; - assert.deepEqual( - Schema.decodeUnknownSync(PartyCommandAliasWriteRejectedProblemSchema)(input), - input, - ); -}); +it.effect('alias conflict preserves both canonical and submitted references', () => + Effect.gen(function* decodeContract2() { + const input = { + _tag: 'PartyCommandAliasWriteRejectedProblem', + aliasPartyRef: ref('10000000-0000-4000-8000-000000000002'), + canonicalPartyRef: ref('10000000-0000-4000-8000-000000000003'), + code: 'party_alias_write_rejected', + detail: 'Retry with the canonical Party.', + status: 409, + title: 'Canonical Party required', + type: 'urn:ontos:party:alias-write-rejected', + }; + const decodedProblem = yield* Schema.decodeUnknownEffect( + PartyCommandAliasWriteRejectedProblemSchema, + )(input); + expect(Schema.is(PartyCommandAliasWriteRejectedProblemSchema)(decodedProblem)).toBe(true); + expect(Struct.omit(decodedProblem, ['_tag'])).toEqual(Struct.omit(input, ['_tag'])); + }), +); -test('public commands and clients never import Action runtime implementations', async () => { - const sources = await Promise.all( - ['../../shared/command-api.ts', '../../src/api/party-command-client.ts'].map((path) => - readFile(new URL(path, import.meta.url), 'utf-8'), - ), - ); - for (const source of sources) { - assert.doesNotMatch( - source, - /from\s+['"][^'"]*src\/actions|from\s+['"]\.\.\/actions|\.action\.ts|Schema\.(?:Unknown|Any)\b/u, +it.effect('public commands and clients never import Action runtime implementations', () => + Effect.gen(function* testProgram2() { + const sources = yield* Effect.promise(() => + Promise.all( + ['../../shared/command-api.ts', '../../src/api/party-command-client.ts'].map((path) => + readFile(new URL(path, import.meta.url), 'utf-8'), + ), + ), ); - } -}); + for (const source of sources) { + expect(source).not.toMatch( + /from\s+['"][^'"]*src\/actions|from\s+['"]\.\.\/actions|\.action\.ts|Schema\.(?:Unknown|Any)\b/u, + ); + } + }), +); diff --git a/app/verticals/party-registry/tests/unit/api-integration-command-recovery.test.ts b/app/verticals/party-registry/tests/unit/api-integration-command-recovery.test.ts index 1e7c67f65..71983770a 100644 --- a/app/verticals/party-registry/tests/unit/api-integration-command-recovery.test.ts +++ b/app/verticals/party-registry/tests/unit/api-integration-command-recovery.test.ts @@ -1,12 +1,10 @@ import { makeCommandAssertionFetch } from '../support/command-assertion-fetch.ts'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Match, Result, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Match, Result, Schema, Struct } from 'effect'; import { FetchHttpClient } from 'effect/unstable/http'; import { PartyCommandCommitIndeterminateProblemSchema, + PartyCommandConflictProblemSchema, PartyCommandAlreadyCommittedProblemSchema, ResolvePartyCommandCommitPayloadSchema, ResolvePartyCommandCommitResultSchema, @@ -27,7 +25,8 @@ const invocationId = Schema.decodeUnknownSync(ActionInvocationIdSchema)( const recoveryProblems = [ { name: 'already committed is terminal and carries the invocation for governed refresh', - decode: Schema.decodeUnknownSync(PartyCommandAlreadyCommittedProblemSchema), + decode: Schema.decodeUnknownEffect(PartyCommandAlreadyCommittedProblemSchema), + is: Schema.is(PartyCommandAlreadyCommittedProblemSchema), problem: { _tag: 'PartyCommandAlreadyCommittedProblem', code: 'action_already_committed', @@ -42,7 +41,8 @@ const recoveryProblems = [ }, { name: 'commit uncertainty retains a resolution handle and never instructs blind command retry', - decode: Schema.decodeUnknownSync(PartyCommandCommitIndeterminateProblemSchema), + decode: Schema.decodeUnknownEffect(PartyCommandCommitIndeterminateProblemSchema), + is: Schema.is(PartyCommandCommitIndeterminateProblemSchema), problem: { _tag: 'PartyCommandCommitIndeterminateProblem', detail: 'Resolve the invocation before deciding the next step.', @@ -56,43 +56,59 @@ const recoveryProblems = [ }, ]; -for (const { name, decode, problem } of recoveryProblems) { - test(name, () => { - assert.deepEqual(decode(problem), problem); - for (const endpoint of Object.values(partyRegistryCommandsApi.groups.partyCommands.endpoints)) { - assert.ok([...endpoint.error].some((schema) => Schema.is(schema)(problem))); - } - assert.throws(() => decode({ ...problem, retryCommand: true })); - }); +for (const { name, decode, is, problem } of recoveryProblems) { + it.effect(name, () => + Effect.gen(function* decodeRecoveryContract() { + const decoded = yield* decode(problem); + expect(is(decoded)).toBe(true); + expect(Struct.omit(decoded, ['_tag'])).toEqual(Struct.omit(problem, ['_tag'])); + for (const endpoint of Object.values( + partyRegistryCommandsApi.groups.partyCommands.endpoints, + )) { + expect([...endpoint.error].some((schema) => Schema.is(schema)(problem))).toBe(true); + } + const invalid: Effect.Effect = decode({ + ...problem, + retryCommand: true, + }); + expect(Result.isFailure(yield* Effect.result(invalid))).toBe(true); + }), + ); } -test('recovery rejects an invalid invocation handle', () => { - assert.throws(() => +it('recovery rejects an invalid invocation handle', () => { + expect(() => Schema.decodeUnknownSync(ResolvePartyCommandCommitPayloadSchema)({ invocationId: 'invalid' }), - ); + ).toThrow(); }); -test('recovery is separate from the unchanged set of explicit mutation endpoints', () => { - assert.equal(Object.keys(partyRegistryCommandsApi.groups.partyCommands.endpoints).length, 24); - const endpoint = partyRegistryCommandRecoveryApi.groups.partyCommandRecovery.endpoints.resolve; - assert.equal(endpoint.path, '/party-registry/action-commits/resolve'); - for (const state of ['OPEN', 'COMMITTED']) { - assert.deepEqual( - Schema.decodeUnknownSync(ResolvePartyCommandCommitResultSchema)({ +it.effect('recovery is separate from the unchanged set of explicit mutation endpoints', () => + Effect.gen(function* decodeContract3() { + expect(Object.keys(partyRegistryCommandsApi.groups.partyCommands.endpoints).length).toBe(24); + const endpoint = partyRegistryCommandRecoveryApi.groups.partyCommandRecovery.endpoints.resolve; + expect(endpoint.path).toBe('/party-registry/action-commits/resolve'); + for (const state of ['OPEN', 'COMMITTED']) { + const resolution = yield* Schema.decodeUnknownEffect(ResolvePartyCommandCommitResultSchema)({ _tag: 'PartyCommandCommitResolution', invocationId, retryCommand: false, state, - }), - { _tag: 'PartyCommandCommitResolution', invocationId, retryCommand: false, state }, - ); - } -}); + }); + expect(Schema.is(ResolvePartyCommandCommitResultSchema)(resolution)).toBe(true); + expect(Struct.omit(resolution, ['_tag'])).toEqual({ + invocationId, + retryCommand: false, + state, + }); + } + }), +); -for (const { name, problem } of [ +for (const { name, is, problem } of [ ...recoveryProblems, { name: 'declared conflict retains its tag and stable conflict code', + is: Schema.is(PartyCommandConflictProblemSchema), problem: { _tag: 'PartyCommandConflictProblem', code: 'action_request_hash_conflict', @@ -103,122 +119,141 @@ for (const { name, problem } of [ }, }, ]) { - test(`command client HTTP decoding: ${name}`, async () => { - const fakeFetch: typeof fetch = () => - Promise.resolve( - Response.json(problem, { - headers: { 'content-type': 'application/problem+json' }, - status: problem.status, - }), - ); - const result = await runEffectTestPromise( - requestSearchRebuildWithAuthorization({}, 'Bearer test', { + it.effect(`command client HTTP decoding: ${name}`, () => + Effect.gen(function* decodeHttpProblem() { + const fakeFetch: typeof fetch = () => + Promise.resolve( + Response.json(problem, { + headers: { 'content-type': 'application/problem+json' }, + status: problem.status, + }), + ); + const result = yield* requestSearchRebuildWithAuthorization({}, 'Bearer test', { baseUrl: 'https://party.example/party-registry-api', correlationId: 'problem-decoding', idempotencyKey: 'same-key', - }).pipe(Effect.result, Effect.provideService(FetchHttpClient.Fetch, fakeFetch)), - ); - assert.ok(Result.isFailure(result)); - assert.deepEqual(result.failure, problem); - }); -} - -test('recovery acquires a fresh assertion without submitting an idempotency key or re-running a command', async () => { - const { requests, assertions, fakeFetch } = makeCommandAssertionFetch( - () => - Response.json({ - _tag: 'PartyCommandCommitResolution', - invocationId, - retryCommand: false, - state: 'COMMITTED', - }), - 'fresh', - ); - const result = await runEffectTestPromise( - resolvePartyCommandCommit( - { invocationId }, - { - baseUrl: 'https://party.example/party-registry-api', - correlationId: 'recovery', - gateway: { baseUrl: 'https://shell.example/shell-super-app-api' }, - traceId: 'trace', - }, - ).pipe(Effect.provideService(FetchHttpClient.Fetch, fakeFetch)), - ); - assert.equal(result.state, 'COMMITTED'); - assert.equal(assertions(), 1); - assert.equal(requests.length, 2); - const [, request] = requests; - assert.ok(request); - assert.equal( - request.url, - 'https://party.example/party-registry-api/party-registry/action-commits/resolve', + }).pipe(Effect.result, Effect.provideService(FetchHttpClient.Fetch, fakeFetch)); + expect(Result.isFailure(result)).toBe(true); + if (!Result.isFailure(result)) { + return expect.unreachable('Expected HTTP problem decoding to fail'); + } + expect(is(result.failure)).toBe(true); + expect(Struct.omit(result.failure, ['_tag'])).toEqual(Struct.omit(problem, ['_tag'])); + }), ); - assert.equal(request.headers.get('authorization'), 'Bearer fresh-1'); - assert.equal(request.headers.get('idempotency-key'), null); - assert.equal(request.headers.get('x-correlation-id'), 'recovery'); - assert.equal(request.headers.get('x-trace-id'), 'trace'); - assert.deepEqual(await request.json(), { invocationId }); -}); +} -test('Create recovery resolves commit and returns exact original operation result with fresh read authority', async () => { - await Promise.all( - (['CREATED', 'MATCHED_EXISTING', 'AMBIGUOUS'] as const).map(async (outcome) => { - const partyRef = { - moduleId: 'party.registry', - resourceId: invocationId, - resourceType: 'party.registry.party', - tenantId: invocationId, - }; - const decisionRef = { ...partyRef, resourceType: 'party.registry.party-match-decision' }; - const caseRef = { ...partyRef, resourceType: 'party.registry.duplicate-candidate-case' }; - const { requests, assertions, fakeFetch } = makeCommandAssertionFetch((request) => { - if (request.url.endsWith('/resolve')) { - return Response.json({ +it.effect( + 'recovery acquires a fresh assertion without submitting an idempotency key or re-running a command', + () => + Effect.gen(function* testProgram3() { + const { requests, assertions, fakeFetch } = makeCommandAssertionFetch( + () => + Response.json({ _tag: 'PartyCommandCommitResolution', invocationId, retryCommand: false, state: 'COMMITTED', - }); - } - return Response.json({ - caseRef: outcome === 'AMBIGUOUS' ? caseRef : null, - committedCreateOutcome: outcome, - decidedAt: '2026-09-04T00:00:00Z', - decisionRef, - evidenceExplanation: [], - matchRuleVersion: 'party-exact-claims.v1', - operation: 'CREATE', - outcome: outcome === 'MATCHED_EXISTING' ? 'MATCHED' : outcome, - partyRef: outcome === 'AMBIGUOUS' ? null : partyRef, - }); - }, 'fresh'); - const recovered = await runEffectTestPromise( - recoverPartyCreate( - { invocationId }, - { - baseUrl: 'https://party.example/party-registry-api', - correlationId: 'recover', - gateway: { baseUrl: 'https://shell.example/shell-super-app-api' }, - }, - ).pipe(Effect.provideService(FetchHttpClient.Fetch, fakeFetch)), + }), + 'fresh', ); - const recoveredResult = Match.value(recovered).pipe( - Match.tag('PartyCreateRecovered', ({ result }) => result), - Match.tag('PartyCreateRecoveryPending', ({ resolution }) => - assert.fail(`Expected committed recovery, received ${resolution.state}`), - ), - Match.exhaustive, - ); - assert.equal(recoveredResult.outcome, outcome); - assert.equal(assertions(), 2); - assert.equal(requests.length, 4); - assert.ok( - requests.every( - (request) => - !request.url.includes('/commands/') && request.headers.get('idempotency-key') === null, - ), + const result = yield* resolvePartyCommandCommit( + { invocationId }, + { + baseUrl: 'https://party.example/party-registry-api', + correlationId: 'recovery', + gateway: { baseUrl: 'https://shell.example/shell-super-app-api' }, + traceId: 'trace', + }, + ).pipe(Effect.provideService(FetchHttpClient.Fetch, fakeFetch)); + expect(result.state).toBe('COMMITTED'); + expect(assertions()).toBe(1); + expect(requests.length).toBe(2); + const [, request] = requests; + expect(Boolean(request)).toBe(true); + if (request === undefined) { + throw new Error('Expected truthy value'); + } + expect(request.url).toBe( + 'https://party.example/party-registry-api/party-registry/action-commits/resolve', ); + expect(request.headers.get('authorization')).toBe('Bearer fresh-1'); + expect(request.headers.get('idempotency-key')).toBe(null); + expect(request.headers.get('x-correlation-id')).toBe('recovery'); + expect(request.headers.get('x-trace-id')).toBe('trace'); + expect(yield* Effect.promise(() => request.json())).toEqual({ invocationId }); }), - ); -}); +); + +it.effect( + 'Create recovery resolves commit and returns exact original operation result with fresh read authority', + () => + Effect.all( + (['CREATED', 'MATCHED_EXISTING', 'AMBIGUOUS'] as const).map((outcome) => + Effect.gen(function* testProgram5() { + const partyRef = { + moduleId: 'party.registry', + resourceId: invocationId, + resourceType: 'party.registry.party', + tenantId: invocationId, + }; + const decisionRef = { + ...partyRef, + resourceType: 'party.registry.party-match-decision', + }; + const caseRef = { + ...partyRef, + resourceType: 'party.registry.duplicate-candidate-case', + }; + const { requests, assertions, fakeFetch } = makeCommandAssertionFetch((request) => { + if (request.url.endsWith('/resolve')) { + return Response.json({ + _tag: 'PartyCommandCommitResolution', + invocationId, + retryCommand: false, + state: 'COMMITTED', + }); + } + return Response.json({ + caseRef: outcome === 'AMBIGUOUS' ? caseRef : null, + committedCreateOutcome: outcome, + decidedAt: '2026-09-04T00:00:00Z', + decisionRef, + evidenceExplanation: [], + matchRuleVersion: 'party-exact-claims.v1', + operation: 'CREATE', + outcome: outcome === 'MATCHED_EXISTING' ? 'MATCHED' : outcome, + partyRef: outcome === 'AMBIGUOUS' ? null : partyRef, + }); + }, 'fresh'); + const recovered = yield* recoverPartyCreate( + { invocationId }, + { + baseUrl: 'https://party.example/party-registry-api', + correlationId: 'recover', + gateway: { baseUrl: 'https://shell.example/shell-super-app-api' }, + }, + ).pipe(Effect.provideService(FetchHttpClient.Fetch, fakeFetch)); + const recoveredResult = Match.value(recovered).pipe( + Match.tag('PartyCreateRecovered', ({ result }) => result), + Match.tag('PartyCreateRecoveryPending', ({ resolution }) => + (() => { + throw new Error(`Expected committed recovery, received ${resolution.state}`); + })(), + ), + Match.exhaustive, + ); + expect(recoveredResult.outcome).toBe(outcome); + expect(assertions()).toBe(2); + expect(requests.length).toBe(4); + expect( + requests.every( + (request) => + !request.url.includes('/commands/') && + request.headers.get('idempotency-key') === null, + ), + ).toBe(true); + }), + ), + ), +); diff --git a/app/verticals/party-registry/tests/unit/api-integration-command-runtime.test.ts b/app/verticals/party-registry/tests/unit/api-integration-command-runtime.test.ts index f43f91074..3972c4347 100644 --- a/app/verticals/party-registry/tests/unit/api-integration-command-runtime.test.ts +++ b/app/verticals/party-registry/tests/unit/api-integration-command-runtime.test.ts @@ -1,9 +1,8 @@ -// @effect-diagnostics asyncFunction:off nodeBuiltinImport:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; +import { assert, expect, it } from 'effect-rstest'; import { randomUUID } from 'node:crypto'; -import test from 'node:test'; -import { ConfigProvider, Context, Effect, Layer, Logger, Schema } from 'effect'; + +import { ConfigProvider, Context, Effect, Layer, Logger, Schema, Predicate, Struct } from 'effect'; + import { ActionHandlerExecutionError, ActionIdempotencyKeyRequired, @@ -39,6 +38,7 @@ import { ReadRuntime, TrustedPrincipalContextSchema, } from '@app/core-runtime'; + import type { ActionCoreError, ActionRuntimeService, @@ -46,28 +46,47 @@ import type { ReadCoreError, ReadRuntimeService, } from '@app/core-runtime'; + import { HttpApi, HttpApiBuilder, HttpRouter, HttpServer } from '@modern-js/plugin-bff/effect-edge'; + import { bindActionTestServices, makeActionTestHarness } from '@app/core-runtime/testing/actions'; + import { SignJWT, exportJWK, generateKeyPair } from 'jose'; + import { partyRegistryApi } from '../../shared/api.ts'; -import { PartyCommandInvalidRequestProblemSchema } from '../../shared/command-api.ts'; + +import { + PartyCommandInvalidRequestProblemSchema, + ResolvePartyCommandCommitResultSchema, +} from '../../shared/command-api.ts'; + import { partyRegistryCommandRecoveryLive, partyRegistryCommandsLive, } from '../../api/party-command-server.ts'; + import { organizationEngagementMutationsLive } from '../../api/engagement-profile-server.ts'; + import { ActionPrincipalVerifierLive } from '../../api/auth/action-principal.ts'; + import { archivePartyAction } from '../../src/actions/archive-party.action.ts'; + import { createPartyAction } from '../../src/actions/create-party.action.ts'; + import { PartyEvidenceInsufficient, PartyPersistenceUnavailable, PartySchema, } from '../../shared/domain/identity-contracts.ts'; + import { PartyAliasWriteRejected } from '../../shared/domain/merge-alias-resolution.ts'; + import { partyMatchDecisionReadApiLive } from '../../api/party-match-decision-read-server.ts'; + import { PartyMatchDecisionRequestSchema } from '../../shared/apis/party-match-decision.ts'; + import { RuleKeySchema } from '../../shared/domain/matching-contracts.ts'; + import type { PartyMatchDecisionRecordSchema } from '../../shared/domain/matching-contracts.ts'; const principal = { @@ -77,17 +96,21 @@ const principal = { principalId: 'a2000000-0000-4000-8000-000000000001', tenantId: 'a3000000-0000-4000-8000-000000000001', } as const; + const partyRef = { moduleId: 'party.registry', resourceId: 'a4000000-0000-4000-8000-000000000001', resourceType: 'party.registry.party', tenantId: principal.tenantId, } as const; + const otherPartyRef = { ...partyRef, resourceId: 'a4000000-0000-4000-8000-000000000002', } as const; + const archivePayload = { expectedRevision: 1, partyRef, reason: 'No longer active' }; + const archivedParty = Schema.decodeUnknownSync(PartySchema)({ archivedAt: '2026-09-01T00:00:00.000Z', createdAt: '2026-09-01T00:00:00.000Z', @@ -97,6 +120,7 @@ const archivedParty = Schema.decodeUnknownSync(PartySchema)({ revision: 1, updatedAt: '2026-09-01T00:00:00.000Z', }); + const createPayload = { candidate: { displayName: 'Example organization', @@ -107,6 +131,7 @@ const createPayload = { validFrom: '2026-09-01T00:00:00.000Z', }, } as const; + const relationshipPayload = { fromPartyRef: partyRef, provenance: { method: 'DOCUMENT', source: 'operator' }, @@ -115,6 +140,7 @@ const relationshipPayload = { validFrom: '2026-09-01T00:00:00.000Z', validTo: null, } as const; + type CommandTestPayload = | Readonly> | typeof archivePayload @@ -125,6 +151,7 @@ type CommandTestPayload = readonly displayName: string; }; }>; + type EngagementTestPayload = | Readonly<{ partyRef: typeof partyRef }> | Readonly<{ @@ -135,7 +162,26 @@ type EngagementTestPayload = readonly tenantId: string; }; }>; + +const makeMissingDecisionReadRuntime = () => { + let reads = 0; + const readRuntime: ReadRuntimeService = { + runRead: () => + Effect.suspend(() => { + reads += 1; + return Effect.fail( + new ReadHandlerNotFound({ + code: 'read_handler_not_found', + reason: 'No fixture decision', + }), + ); + }), + }; + return { readCount: () => reads, readRuntime }; +}; + const issuer = 'https://shell.ontos.test'; + const actionSlugs = [ 'add-contact-point', 'add-party-official-identifier', @@ -162,6 +208,7 @@ const actionSlugs = [ 'update-party-official-identifier', 'update-party-relationship', ] as const; + const endpointNames = [ 'addContactPoint', 'addPartyOfficialIdentifier', @@ -189,51 +236,61 @@ const endpointNames = [ 'updatePartyRelationship', ] as const; -const makeAssertion = async ( +const makeAssertion = ( audience = 'party-registry', options: { readonly expiresAt?: number; readonly tokenIssuer?: string } = {}, -) => { - const { privateKey, publicKey } = await generateKeyPair('Ed25519'); - const publicJwk = { - ...(await exportJWK(publicKey)), - alg: 'EdDSA', - kid: 'party-command-test', - use: 'sig', - }; - const token = await new SignJWT({ principal, ver: 1 }) - .setProtectedHeader({ alg: 'EdDSA', kid: 'party-command-test', typ: 'JWT' }) - .setIssuer(options.tokenIssuer ?? issuer) - .setAudience(audience) - .setSubject(principal.principalId) - .setIssuedAt() - .setExpirationTime(options.expiresAt ?? '5m') - .setJti(randomUUID()) - .sign(privateKey); - const otherPrincipal = { ...principal, principalId: randomUUID() }; - const otherToken = await new SignJWT({ principal: otherPrincipal, ver: 1 }) - .setProtectedHeader({ alg: 'EdDSA', kid: 'party-command-test', typ: 'JWT' }) - .setIssuer(issuer) - .setAudience(audience) - .setSubject(otherPrincipal.principalId) - .setIssuedAt() - .setExpirationTime('5m') - .setJti(randomUUID()) - .sign(privateKey); - return { - environment: { - ONTOS_GATEWAY_ISSUER: issuer, - ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify({ keys: [publicJwk] }), - }, - token, - otherToken, - }; -}; +) => + Effect.gen(function* signPrincipalAssertions() { + const { privateKey, publicKey } = yield* Effect.promise(() => generateKeyPair('Ed25519')); + const publicJwk = { + ...(yield* Effect.promise(() => exportJWK(publicKey))), + alg: 'EdDSA', + kid: 'party-command-test', + use: 'sig', + }; + const token = yield* Effect.promise(() => + new SignJWT({ principal, ver: 1 }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'party-command-test', typ: 'JWT' }) + .setIssuer(options.tokenIssuer ?? issuer) + .setAudience(audience) + .setSubject(principal.principalId) + .setIssuedAt() + .setExpirationTime(options.expiresAt ?? '5m') + .setJti(randomUUID()) + .sign(privateKey), + ); + const otherPrincipal = { ...principal, principalId: randomUUID() }; + const otherToken = yield* Effect.promise(() => + new SignJWT({ principal: otherPrincipal, ver: 1 }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'party-command-test', typ: 'JWT' }) + .setIssuer(issuer) + .setAudience(audience) + .setSubject(otherPrincipal.principalId) + .setIssuedAt() + .setExpirationTime('5m') + .setJti(randomUUID()) + .sign(privateKey), + ); + return { + environment: { + ONTOS_GATEWAY_ISSUER: issuer, + ONTOS_GATEWAY_PUBLIC_JWKS: yield* Schema.encodeEffect( + Schema.fromJsonString(Schema.Unknown), + )({ + keys: [publicJwk], + }), + }, + token, + otherToken, + }; + }); const nonPersistingRedemption: GatewayAssertionRedemption = { consume: () => Effect.void }; + const ProblemTagSchema = Schema.Struct({ _tag: Schema.String }); const mounted = ( - harness: ReturnType, + harness: Effect.Success>, environment: Readonly>, readRuntime?: ReadRuntimeService, redemption: GatewayAssertionRedemption = nonPersistingRedemption, @@ -312,6 +369,21 @@ const mountedOrganizationEngagement = ( ); }; +const mountApp = ( + harness: Effect.Success>, + environment: Readonly>, + readRuntime?: ReadRuntimeService, + redemption: GatewayAssertionRedemption = nonPersistingRedemption, + actionRuntime: ActionRuntimeService = harness.runtime, + observedLogs?: string[], +) => + Effect.acquireRelease( + Effect.sync(() => + mounted(harness, environment, readRuntime, redemption, actionRuntime, observedLogs), + ), + (app) => Effect.promise(() => app.dispose()).pipe(Effect.orDie), + ); + const makeSingleUseRedemption = (): GatewayAssertionRedemption => { const redeemed = new Set(); return { @@ -331,35 +403,15 @@ const makeSingleUseRedemption = (): GatewayAssertionRedemption => { }; }; -// The mounted layers provide every runtime service; the handler's conservative unknown requirement -// still requires an explicitly empty per-request context. const emptyRequestContext = Context.makeUnsafe(new Map()); + const handle = (app: ReturnType, request: Request) => - app.handler(request, emptyRequestContext); - -const withMountedApp = ( - app: ReturnType, - run: (app: ReturnType) => Promise, -): Promise => - runEffectTestPromise( - Effect.acquireUseRelease( - Effect.succeed(app), - (resource) => Effect.promise(() => run(resource)), - (resource) => Effect.promise(() => resource.dispose()), - ), - ); + Effect.promise(() => app.handler(request, emptyRequestContext)); -const forEachSequential = ( +const forEachSequential = ( items: Iterable, - run: (item: Item) => Promise, -): Promise => { - const iterator = items[Symbol.iterator](); - const advance = (): Promise => { - const item = iterator.next(); - return item.done === true ? Promise.resolve() : run(item.value).then(advance); - }; - return advance(); -}; + run: (item: Item) => Effect.Effect, +) => Effect.forEach(items, run, { discard: true }); const recoveryRequest = (invocationId: string, token?: string) => { const headers = new Headers({ @@ -429,806 +481,840 @@ const engagementRequest = (path: string, payload: EngagementTestPayload, token: method: 'POST', }); -test('every registered command is mounted and rejects missing structural input or authentication before the lifecycle', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness(); - await withMountedApp(mounted(harness, assertion.environment), async (app) => { - assert.equal(Object.keys(partyRegistryApi.groups.partyCommands.endpoints).length, 24); - assert.deepEqual( - Object.keys(partyRegistryApi.groups.partyCommands.endpoints).toSorted(), - [...endpointNames].toSorted(), - ); - assert.deepEqual( - Object.values(partyRegistryApi.groups.partyCommands.endpoints) - .map((endpoint) => endpoint.path) - .toSorted(), - actionSlugs.map((slug) => `/party-registry/actions/${slug}`).toSorted(), - ); - await forEachSequential( - Object.values(partyRegistryApi.groups.partyCommands.endpoints), - async (endpoint) => { - const response = await handle( - app, - new Request(`https://party.ontos.test${endpoint.path}`, { - method: 'POST', - body: '{}', - headers: { - 'content-type': 'application/json', - 'x-correlation-id': 'mounted-command-test', - }, - }), - ); - assert.ok( - response.status === 400 || response.status === 401, - `${endpoint.path}: ${response.status}`, - ); - assert.match(response.headers.get('content-type') ?? '', /application\/problem\+json/u); - const body = await response.json(); - assert.ok( - Schema.is( - Schema.TaggedStruct( - response.status === 400 - ? 'PartyCommandInvalidRequestProblem' - : 'PartyCommandAuthenticationProblem', - {}, - ), - )(body), - ); - assert.equal(body.status, response.status); - }, - ); - const malformed = await handle( - app, - new Request('https://party.ontos.test/party-registry/actions/archive-party', { - body: '{not-json', - headers: { - 'content-type': 'application/json', - 'x-correlation-id': 'malformed-test', - }, - method: 'POST', - }), - ); - assert.equal(malformed.status, 400); - assert.match(malformed.headers.get('content-type') ?? '', /application\/problem\+json/u); - const malformedBody = await malformed.json(); - assert.ok( - Schema.is(Schema.TaggedStruct('PartyCommandInvalidRequestProblem', {}))(malformedBody), - ); - assert.equal(harness.snapshot().invocations.length, 0); - }); -}); +it.live( + 'every registered command is mounted and rejects missing structural input or authentication before the lifecycle', + () => + Effect.gen(function* rejectInvalidMountedCommands() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness(); + const app = yield* mountApp(harness, assertion.environment); -test('missing, malformed, expired, tampered, wrong-audience, and wrong-issuer assertions are challenged without creating invocations', async () => { - const expired = await makeAssertion('party-registry', { - expiresAt: 1, - }); - const wrongAudience = await makeAssertion('contacts'); - const wrongIssuer = await makeAssertion('party-registry', { - tokenIssuer: 'https://untrusted-shell.ontos.test', - }); - const valid = await makeAssertion(); - const signatureStart = valid.token.lastIndexOf('.') + 1; - const signatureFirstCharacter = valid.token.at(signatureStart); - assert.notEqual(signatureFirstCharacter, undefined); - const tampered = `${valid.token.slice(0, signatureStart)}${signatureFirstCharacter === 'A' ? 'B' : 'A'}${valid.token.slice(signatureStart + 1)}`; - const cases = [ - { assertion: valid, token: undefined }, - { assertion: valid, token: 'not-a-jwt' }, - { assertion: expired, token: expired.token }, - { assertion: valid, token: tampered }, - { assertion: wrongAudience, token: wrongAudience.token }, - { assertion: wrongIssuer, token: wrongIssuer.token }, - ]; - await forEachSequential(cases, async ({ assertion, token }) => { - const harness = makeActionTestHarness(); - await withMountedApp(mounted(harness, assertion.environment), async (app) => { - const response = await handle( + expect(Object.keys(partyRegistryApi.groups.partyCommands.endpoints).length).toBe(24); + expect(Object.keys(partyRegistryApi.groups.partyCommands.endpoints).toSorted()).toEqual( + [...endpointNames].toSorted(), + ); + expect( + Object.values(partyRegistryApi.groups.partyCommands.endpoints) + .map((endpoint) => endpoint.path) + .toSorted(), + ).toEqual(actionSlugs.map((slug) => `/party-registry/actions/${slug}`).toSorted()); + yield* forEachSequential( + Object.values(partyRegistryApi.groups.partyCommands.endpoints), + (endpoint) => + Effect.gen(function* rejectInvalidEndpointRequest() { + const response = yield* handle( + app, + new Request(`https://party.ontos.test${endpoint.path}`, { + method: 'POST', + body: '{}', + headers: { + 'content-type': 'application/json', + 'x-correlation-id': 'mounted-command-test', + }, + }), + ); + expect(response.status === 400 || response.status === 401).toBe(true); + if (!(response.status === 400 || response.status === 401)) { + throw new Error(`${endpoint.path}: ${response.status}`); + } + expect(response.headers.get('content-type') ?? '').toMatch( + /application\/problem\+json/u, + ); + const body = yield* Effect.promise(() => response.json()); + expect( + Predicate.isTagged( + body, + response.status === 400 + ? 'PartyCommandInvalidRequestProblem' + : 'PartyCommandAuthenticationProblem', + ), + ).toBe(true); + expect(body.status).toBe(response.status); + }), + ); + const malformed = yield* handle( app, - commandRequest('request-search-rebuild', {}, token, { - 'idempotency-key': 'authentication-test', + new Request('https://party.ontos.test/party-registry/actions/archive-party', { + body: '{not-json', + headers: { + 'content-type': 'application/json', + 'x-correlation-id': 'malformed-test', + }, + method: 'POST', }), ); - assert.equal(response.status, 401); - assert.equal(response.headers.get('www-authenticate'), 'Bearer'); - assert.match(response.headers.get('content-type') ?? '', /application\/problem\+json/u); - const body = await response.json(); - assert.ok(Schema.is(Schema.TaggedStruct('PartyCommandAuthenticationProblem', {}))(body)); - assert.equal(body.status, 401); - assert.equal(JSON.stringify(body).includes(assertion.token), false); - assert.equal(harness.snapshot().invocations.length, 0); - }); - }); -}); + expect(malformed.status).toBe(400); + expect(malformed.headers.get('content-type') ?? '').toMatch(/application\/problem\+json/u); + const malformedBody = yield* Effect.promise(() => malformed.json()); + expect(Predicate.isTagged(malformedBody, 'PartyCommandInvalidRequestProblem')).toBe(true); + expect(harness.snapshot().invocations.length).toBe(0); + }), +); -test('missing and malformed verification configuration are retryable and never reach the lifecycle', async () => { - const assertion = await makeAssertion(); - await forEachSequential( - [ - {}, - { ...assertion.environment, ONTOS_GATEWAY_ISSUER: 'not-an-absolute-http-url' }, - { ...assertion.environment, ONTOS_GATEWAY_PUBLIC_JWKS: '{malformed' }, - ], - async (environment) => { - const harness = makeActionTestHarness(); - await withMountedApp(mounted(harness, environment), async (app) => { - const response = await handle( - app, - commandRequest('request-search-rebuild', {}, assertion.token, { - 'idempotency-key': 'configuration-test', - }), - ); - assert.equal(response.status, 503); - assert.equal(response.headers.get('www-authenticate'), null); - assert.match(response.headers.get('content-type') ?? '', /application\/problem\+json/u); - const body = await response.json(); - assert.ok(Schema.is(Schema.TaggedStruct('PartyCommandUnavailableProblem', {}))(body)); - assert.equal(body.retryable, true); - assert.equal(harness.snapshot().invocations.length, 0); +it.live( + 'missing, malformed, expired, tampered, wrong-audience, and wrong-issuer assertions are challenged without creating invocations', + () => + Effect.gen(function* rejectInvalidAssertions() { + const expired = yield* makeAssertion('party-registry', { + expiresAt: 1, }); - }, - ); -}); - -test('redemption storage outages return safe retryable problems before Action and Read lifecycles', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness(); - let reads = 0; - const readRuntime: ReadRuntimeService = { - runRead: () => - Effect.sync(() => { - reads += 1; - }).pipe( - Effect.andThen( - Effect.fail( - new ReadHandlerNotFound({ - code: 'read_handler_not_found', - reason: 'No fixture decision', + const wrongAudience = yield* makeAssertion('contacts'); + const wrongIssuer = yield* makeAssertion('party-registry', { + tokenIssuer: 'https://untrusted-shell.ontos.test', + }); + const valid = yield* makeAssertion(); + const signatureStart = valid.token.lastIndexOf('.') + 1; + const signatureFirstCharacter = valid.token.at(signatureStart); + expect(signatureFirstCharacter).not.toBe(undefined); + const tampered = `${valid.token.slice(0, signatureStart)}${signatureFirstCharacter === 'A' ? 'B' : 'A'}${valid.token.slice(signatureStart + 1)}`; + const cases = [ + { assertion: valid, token: undefined }, + { assertion: valid, token: 'not-a-jwt' }, + { assertion: expired, token: expired.token }, + { assertion: valid, token: tampered }, + { assertion: wrongAudience, token: wrongAudience.token }, + { assertion: wrongIssuer, token: wrongIssuer.token }, + ]; + yield* forEachSequential(cases, ({ assertion, token }) => + Effect.gen(function* rejectInvalidAssertionCase() { + const harness = yield* makeActionTestHarness(); + const app = yield* mountApp(harness, assertion.environment); + const response = yield* handle( + app, + commandRequest('request-search-rebuild', {}, token, { + 'idempotency-key': 'authentication-test', }), - ), - ), - ), - }; - const app = mounted(harness, assertion.environment, readRuntime, { - consume: () => - Effect.fail( - new GatewayAssertionRedemptionUnavailableError({ - reason: 'private redemption storage diagnostic', + ); + expect(response.status).toBe(401); + expect(response.headers.get('www-authenticate')).toBe('Bearer'); + expect(response.headers.get('content-type') ?? '').toMatch(/application\/problem\+json/u); + const body = yield* Effect.promise(() => response.json()); + expect(Predicate.isTagged(body, 'PartyCommandAuthenticationProblem')).toBe(true); + expect(body.status).toBe(401); + expect( + (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(body)).includes( + assertion.token, + ), + ).toBe(false); + expect(harness.snapshot().invocations.length).toBe(0); }), - ), - }); - const before = harness.snapshot(); - try { - await forEachSequential( - [ - { - request: commandRequest('request-search-rebuild', {}, assertion.token, { - 'idempotency-key': 'redemption-unavailable', + ); + }), +); + +it.live( + 'missing and malformed verification configuration are retryable and never reach the lifecycle', + () => + Effect.gen(function* rejectInvalidVerificationConfiguration() { + const assertion = yield* makeAssertion(); + yield* forEachSequential( + [ + {}, + { ...assertion.environment, ONTOS_GATEWAY_ISSUER: 'not-an-absolute-http-url' }, + { ...assertion.environment, ONTOS_GATEWAY_PUBLIC_JWKS: '{malformed' }, + ], + (environment) => + Effect.gen(function* rejectInvalidConfigurationCase() { + const harness = yield* makeActionTestHarness(); + const app = yield* mountApp(harness, environment); + const response = yield* handle( + app, + commandRequest('request-search-rebuild', {}, assertion.token, { + 'idempotency-key': 'configuration-test', + }), + ); + expect(response.status).toBe(503); + expect(response.headers.get('www-authenticate')).toBe(null); + expect(response.headers.get('content-type') ?? '').toMatch( + /application\/problem\+json/u, + ); + const body = yield* Effect.promise(() => response.json()); + expect(Predicate.isTagged(body, 'PartyCommandUnavailableProblem')).toBe(true); + expect(body.retryable).toBe(true); + expect(harness.snapshot().invocations.length).toBe(0); }), - tag: 'PartyCommandUnavailableProblem', - }, - { - request: decisionRequest(randomUUID(), assertion.token), - tag: 'PartyMatchDecisionUnavailableProblem', - }, - ], - async ({ request, tag }) => { - const response = await handle(app, request); - assert.equal(response.status, 503); - assert.equal(response.headers.get('www-authenticate'), null); - assert.match(response.headers.get('content-type') ?? '', /application\/problem\+json/u); - const body = await response.json(); - assert.equal(body._tag, tag); - assert.equal(body.status, 503); - assert.equal(body.retryable, true); - const encoded = JSON.stringify(body); - assert.equal(encoded.includes('private redemption'), false); - assert.equal(encoded.includes(assertion.token), false); - assert.equal(encoded.includes(principal.principalId), false); - assert.equal(encoded.includes(principal.tenantId), false); - assert.equal(reads, 0); - assert.deepEqual(harness.snapshot(), before); - }, - ); - } finally { - await app.dispose(); - } -}); + ); + }), +); -test('generated governed reads authenticate through the shared adapter before starting ReadRuntime', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness(); - let reads = 0; - const receivedPrincipals: unknown[] = []; - const readRuntime: ReadRuntimeService = { - runRead: (input) => - Effect.sync(() => { - reads += 1; - receivedPrincipals.push(input.principal); - }).pipe( - Effect.andThen( +it.live( + 'redemption storage outages return safe retryable problems before Action and Read lifecycles', + () => + Effect.gen(function* reportRedemptionOutages() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness(); + const { readRuntime, readCount } = makeMissingDecisionReadRuntime(); + const app = yield* mountApp(harness, assertion.environment, readRuntime, { + consume: () => Effect.fail( - new ReadHandlerNotFound({ - code: 'read_handler_not_found', - reason: 'No fixture decision', + new GatewayAssertionRedemptionUnavailableError({ + reason: 'private redemption storage diagnostic', }), ), - ), - ), - }; - await withMountedApp(mounted(harness, assertion.environment, readRuntime), async (app) => { - await forEachSequential([undefined, 'not-a-jwt'], async (token) => { - const response = await handle(app, decisionRequest(randomUUID(), token)); - assert.equal(response.status, 401); - assert.equal(response.headers.get('www-authenticate'), 'Bearer'); - assert.match(response.headers.get('content-type') ?? '', /application\/problem\+json/u); - const body = await response.json(); - assert.ok( - Schema.is(Schema.TaggedStruct('PartyMatchDecisionAuthenticationProblem', {}))(body), - ); - assert.equal(reads, 0); - }); - const missingCorrelation = await handle( - app, - decisionRequest(randomUUID(), assertion.token, { 'x-correlation-id': '' }), - ); - assert.equal(missingCorrelation.status, 400); - assert.equal(reads, 0); - const valid = await handle(app, decisionRequest(randomUUID(), assertion.token)); - assert.equal(valid.status, 404); - assert.equal(reads, 1); - assert.deepEqual(receivedPrincipals, [principal]); - }); - - await withMountedApp(mounted(harness, {}, readRuntime), async (unavailableApp) => { - const unavailable = await handle( - unavailableApp, - decisionRequest(randomUUID(), assertion.otherToken), - ); - assert.equal(unavailable.status, 503); - assert.equal(unavailable.headers.get('www-authenticate'), null); - const body = await unavailable.json(); - assert.ok(Schema.is(Schema.TaggedStruct('PartyMatchDecisionUnavailableProblem', {}))(body)); - assert.equal(body.retryable, true); - assert.equal(reads, 1); - }); -}); - -test('the complete generated governed Read seam maps every Core failure to its declared HTTP problem', async () => { - const assertion = await makeAssertion(); - const reason = 'private governed Read diagnostic'; - const initialFailure = new ModuleStateCheckUnavailableError({ - code: 'module_state_check_unavailable', - reason, - }); - const cases: readonly [ReadCoreError, number, string][] = [ - [initialFailure, 503, 'PartyMatchDecisionUnavailableProblem'], - [ - new ModuleStateDeniedError({ code: 'module_state_denied', reason }), - 403, - 'PartyMatchDecisionForbiddenProblem', - ], - [ - new OperationAuthenticationRequired({ code: 'operation_authentication_required', reason }), - 401, - 'PartyMatchDecisionAuthenticationProblem', - ], - [ - new OperationContextDenied({ code: 'operation_context_denied', reason }), - 403, - 'PartyMatchDecisionForbiddenProblem', - ], - [ - new OperationContextInvalid({ code: 'operation_context_invalid', reason }), - 403, - 'PartyMatchDecisionForbiddenProblem', - ], - [ - new OperationContextUnavailable({ code: 'operation_context_unavailable', reason }), - 503, - 'PartyMatchDecisionUnavailableProblem', - ], - [ - new ReadEvidencePersistenceError({ code: 'read_evidence_persistence_failed', reason }), - 503, - 'PartyMatchDecisionUnavailableProblem', - ], - [ - new ReadEvidenceValidationError({ code: 'read_evidence_invalid', reason }), - 500, - 'PartyMatchDecisionInternalProblem', - ], - [ - new ReadHandlerExecutionError({ code: 'read_handler_execution_failed', reason }), - 500, - 'PartyMatchDecisionInternalProblem', - ], - [ - new ReadHandlerNotFound({ code: 'read_handler_not_found', reason }), - 404, - 'PartyMatchDecisionNotFoundProblem', - ], - [ - new ReadHandlerUnavailable({ code: 'read_handler_unavailable', reason }), - 503, - 'PartyMatchDecisionUnavailableProblem', - ], - [ - new ReadInputValidationError({ code: 'read_input_invalid', reason }), - 400, - 'PartyMatchDecisionInvalidProblem', - ], - [ - new ReadPermissionDenied({ code: 'read_permission_denied', reason }), - 403, - 'PartyMatchDecisionForbiddenProblem', - ], - [ - new ReadPermissionUnavailable({ code: 'read_permission_unavailable', reason }), - 503, - 'PartyMatchDecisionUnavailableProblem', - ], - [ - new ReadPolicyDenied({ - code: 'read_policy_denied', - httpStatus: 409, - policyReasonCode: 'policy_conflict', - reason, - }), - 409, - 'PartyMatchDecisionPolicyConflictProblem', - ], - [ - new ReadPolicyDenied({ - code: 'read_policy_denied', - httpStatus: 422, - policyReasonCode: 'policy_ineligible', - reason, - }), - 422, - 'PartyMatchDecisionPolicyProblem', - ], - [ - new ReadPolicyEvaluationError({ code: 'read_policy_evaluation_failed', reason }), - 503, - 'PartyMatchDecisionUnavailableProblem', - ], - [ - new ReadResultValidationError({ code: 'read_result_invalid', reason }), - 500, - 'PartyMatchDecisionInternalProblem', - ], - ]; - let failure: ReadCoreError = initialFailure; - let reads = 0; - const readRuntime: ReadRuntimeService = { - runRead: () => { - reads += 1; - return Effect.fail(failure); - }, - }; - await withMountedApp( - mounted(makeActionTestHarness(), assertion.environment, readRuntime), - async (app) => { - await forEachSequential(cases, async ([nextFailure, expectedStatus, expectedTag]) => { - failure = nextFailure; - const response = await handle(app, decisionRequest(randomUUID(), assertion.token)); - assert.equal(response.status, expectedStatus, nextFailure._tag); - assert.match(response.headers.get('content-type') ?? '', /application\/problem\+json/u); - assert.equal( - response.headers.get('www-authenticate'), - expectedStatus === 401 ? 'Bearer' : null, - ); - const body = await response.json(); - assert.ok(Schema.is(Schema.TaggedStruct(expectedTag, {}))(body), nextFailure._tag); - assert.equal(body.status, expectedStatus, nextFailure._tag); - assert.equal(JSON.stringify(body).includes(reason), false, nextFailure._tag); - if (expectedStatus === 503) { - assert.equal(body.retryable, true, nextFailure._tag); - } }); - assert.equal(reads, cases.length); - }, - ); -}); - -test('the generated governed Read seam sanitizes unexpected runtime defects', async () => { - const assertion = await makeAssertion(); - const readRuntime: ReadRuntimeService = { - runRead: () => Effect.die('private governed Read defect'), - }; - await withMountedApp( - mounted(makeActionTestHarness(), assertion.environment, readRuntime), - async (app) => { - const response = await handle(app, decisionRequest(randomUUID(), assertion.token)); - assert.equal(response.status, 500); - assert.match(response.headers.get('content-type') ?? '', /application\/problem\+json/u); - const body = await response.json(); - assert.ok(Schema.is(Schema.TaggedStruct('PartyMatchDecisionInternalProblem', {}))(body)); - assert.equal(body.status, 500); - assert.equal(JSON.stringify(body).includes('private'), false); - }, - ); -}); - -test('replayed assertions are challenged before a second Action or generated Read lifecycle', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness(); - let reads = 0; - const readRuntime: ReadRuntimeService = { - runRead: () => - Effect.sync(() => { - reads += 1; - }).pipe( - Effect.andThen( - Effect.fail( - new ReadHandlerNotFound({ - code: 'read_handler_not_found', - reason: 'No fixture decision', + const before = harness.snapshot(); + yield* forEachSequential( + [ + { + request: commandRequest('request-search-rebuild', {}, assertion.token, { + 'idempotency-key': 'redemption-unavailable', }), + tag: 'PartyCommandUnavailableProblem', + }, + { + request: decisionRequest(randomUUID(), assertion.token), + tag: 'PartyMatchDecisionUnavailableProblem', + }, + ], + ({ request, tag }) => + Effect.gen(function* reportRedemptionOutageCase() { + const response = yield* handle(app, request); + expect(response.status).toBe(503); + expect(response.headers.get('www-authenticate')).toBe(null); + expect(response.headers.get('content-type') ?? '').toMatch( + /application\/problem\+json/u, + ); + const body = yield* Effect.promise(() => response.json()); + expect(Predicate.isTagged(body, tag)).toBe(true); + expect(body.status).toBe(503); + expect(body.retryable).toBe(true); + const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(body); + expect(encoded.includes('private redemption')).toBe(false); + expect(encoded.includes(assertion.token)).toBe(false); + expect(encoded.includes(principal.principalId)).toBe(false); + expect(encoded.includes(principal.tenantId)).toBe(false); + expect(readCount()).toBe(0); + expect(harness.snapshot()).toEqual(before); + }), + ); + }), +); + +it.live( + 'generated governed reads authenticate through the shared adapter before starting ReadRuntime', + () => + Effect.gen(function* authenticateGovernedReads() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness(); + let reads = 0; + const receivedPrincipals: unknown[] = []; + const readRuntime: ReadRuntimeService = { + runRead: (input) => + Effect.sync(() => { + reads += 1; + receivedPrincipals.push(input.principal); + }).pipe( + Effect.andThen( + Effect.fail( + new ReadHandlerNotFound({ + code: 'read_handler_not_found', + reason: 'No fixture decision', + }), + ), + ), ), - ), - ), - }; - await withMountedApp( - mounted(harness, assertion.environment, readRuntime, makeSingleUseRedemption()), - async (app) => { - const firstAction = await handle( - app, - commandRequest('request-search-rebuild', {}, assertion.token, { - 'idempotency-key': 'first-redemption', + }; + const app = yield* mountApp(harness, assertion.environment, readRuntime); + yield* forEachSequential([undefined, 'not-a-jwt'], (token) => + Effect.gen(function* rejectMissingGovernedCredentials() { + const response = yield* handle(app, decisionRequest(randomUUID(), token)); + expect(response.status).toBe(401); + expect(response.headers.get('www-authenticate')).toBe('Bearer'); + expect(response.headers.get('content-type') ?? '').toMatch(/application\/problem\+json/u); + const body = yield* Effect.promise(() => response.json()); + expect( + Schema.is(Schema.TaggedStruct('PartyMatchDecisionAuthenticationProblem', {}))(body), + ).toBe(true); + expect(reads).toBe(0); }), ); - assert.notEqual(firstAction.status, 401); - assert.equal(harness.snapshot().invocations.length, 1); - - const replayedAction = await handle( + const missingCorrelation = yield* handle( app, - commandRequest('request-search-rebuild', {}, assertion.token, { - 'idempotency-key': 'second-redemption', - }), + decisionRequest(randomUUID(), assertion.token, { 'x-correlation-id': '' }), ); - assert.equal(replayedAction.status, 401); - assert.equal(replayedAction.headers.get('www-authenticate'), 'Bearer'); - assert.match(replayedAction.headers.get('content-type') ?? '', /application\/problem\+json/u); - assert.equal(harness.snapshot().invocations.length, 1); + expect(missingCorrelation.status).toBe(400); + expect(reads).toBe(0); + const valid = yield* handle(app, decisionRequest(randomUUID(), assertion.token)); + expect(valid.status).toBe(404); + expect(reads).toBe(1); + expect(receivedPrincipals).toEqual([principal]); - const actionAssertionReadReplay = await handle( - app, - decisionRequest(randomUUID(), assertion.token), + const unavailableApp = yield* mountApp(harness, {}, readRuntime); + const unavailable = yield* handle( + unavailableApp, + decisionRequest(randomUUID(), assertion.otherToken), ); - assert.equal(actionAssertionReadReplay.status, 401); - assert.equal(actionAssertionReadReplay.headers.get('www-authenticate'), 'Bearer'); - assert.equal(reads, 0); - - const firstRead = await handle(app, decisionRequest(randomUUID(), assertion.otherToken)); - assert.equal(firstRead.status, 404); - assert.equal(reads, 1); - - const replayedRead = await handle(app, decisionRequest(randomUUID(), assertion.otherToken)); - assert.equal(replayedRead.status, 401); - assert.equal(replayedRead.headers.get('www-authenticate'), 'Bearer'); - assert.match(replayedRead.headers.get('content-type') ?? '', /application\/problem\+json/u); - assert.equal(reads, 1); - }, - ); -}); + expect(unavailable.status).toBe(503); + expect(unavailable.headers.get('www-authenticate')).toBe(null); + const body = yield* Effect.promise(() => unavailable.json()); + expect(Schema.is(Schema.TaggedStruct('PartyMatchDecisionUnavailableProblem', {}))(body)).toBe( + true, + ); + expect(body.retryable).toBe(true); + expect(reads).toBe(1); + }), +); -test('correlation and idempotency are mandatory before the Core Action lifecycle', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness(); - let runtimeCalls = 0; - const observingRuntime: ActionRuntimeService = { - resolveActionCommit: harness.runtime.resolveActionCommit, - runAction: (input) => { - runtimeCalls += 1; - return harness.runtime.runAction(input); - }, - }; - await withMountedApp( - mounted(harness, assertion.environment, undefined, nonPersistingRedemption, observingRuntime), - async (app) => { - const missingKey = await handle( - app, - commandRequest('request-search-rebuild', {}, assertion.token), +it.live( + 'the complete generated governed Read seam maps every Core failure to its declared HTTP problem', + () => + Effect.gen(function* mapEveryGovernedReadFailure() { + const assertion = yield* makeAssertion(); + const reason = 'private governed Read diagnostic'; + const initialFailure = new ModuleStateCheckUnavailableError({ + code: 'module_state_check_unavailable', + reason, + }); + const cases: readonly [ReadCoreError, number, string][] = [ + [initialFailure, 503, 'PartyMatchDecisionUnavailableProblem'], + [ + new ModuleStateDeniedError({ code: 'module_state_denied', reason }), + 403, + 'PartyMatchDecisionForbiddenProblem', + ], + [ + new OperationAuthenticationRequired({ + code: 'operation_authentication_required', + reason, + }), + 401, + 'PartyMatchDecisionAuthenticationProblem', + ], + [ + new OperationContextDenied({ code: 'operation_context_denied', reason }), + 403, + 'PartyMatchDecisionForbiddenProblem', + ], + [ + new OperationContextInvalid({ code: 'operation_context_invalid', reason }), + 403, + 'PartyMatchDecisionForbiddenProblem', + ], + [ + new OperationContextUnavailable({ code: 'operation_context_unavailable', reason }), + 503, + 'PartyMatchDecisionUnavailableProblem', + ], + [ + new ReadEvidencePersistenceError({ code: 'read_evidence_persistence_failed', reason }), + 503, + 'PartyMatchDecisionUnavailableProblem', + ], + [ + new ReadEvidenceValidationError({ code: 'read_evidence_invalid', reason }), + 500, + 'PartyMatchDecisionInternalProblem', + ], + [ + new ReadHandlerExecutionError({ code: 'read_handler_execution_failed', reason }), + 500, + 'PartyMatchDecisionInternalProblem', + ], + [ + new ReadHandlerNotFound({ code: 'read_handler_not_found', reason }), + 404, + 'PartyMatchDecisionNotFoundProblem', + ], + [ + new ReadHandlerUnavailable({ code: 'read_handler_unavailable', reason }), + 503, + 'PartyMatchDecisionUnavailableProblem', + ], + [ + new ReadInputValidationError({ code: 'read_input_invalid', reason }), + 400, + 'PartyMatchDecisionInvalidProblem', + ], + [ + new ReadPermissionDenied({ code: 'read_permission_denied', reason }), + 403, + 'PartyMatchDecisionForbiddenProblem', + ], + [ + new ReadPermissionUnavailable({ code: 'read_permission_unavailable', reason }), + 503, + 'PartyMatchDecisionUnavailableProblem', + ], + [ + new ReadPolicyDenied({ + code: 'read_policy_denied', + httpStatus: 409, + policyReasonCode: 'policy_conflict', + reason, + }), + 409, + 'PartyMatchDecisionPolicyConflictProblem', + ], + [ + new ReadPolicyDenied({ + code: 'read_policy_denied', + httpStatus: 422, + policyReasonCode: 'policy_ineligible', + reason, + }), + 422, + 'PartyMatchDecisionPolicyProblem', + ], + [ + new ReadPolicyEvaluationError({ code: 'read_policy_evaluation_failed', reason }), + 503, + 'PartyMatchDecisionUnavailableProblem', + ], + [ + new ReadResultValidationError({ code: 'read_result_invalid', reason }), + 500, + 'PartyMatchDecisionInternalProblem', + ], + ]; + let failure: ReadCoreError = initialFailure; + let reads = 0; + const readRuntime: ReadRuntimeService = { + runRead: () => { + reads += 1; + return Effect.fail(failure); + }, + }; + const harness = yield* makeActionTestHarness(); + const app = yield* mountApp(harness, assertion.environment, readRuntime); + yield* forEachSequential(cases, ([nextFailure, expectedStatus, expectedTag]) => + Effect.gen(function* verifyGovernedReadFailure() { + failure = nextFailure; + const response = yield* handle(app, decisionRequest(randomUUID(), assertion.token)); + expect(response.status, nextFailure.code).toBe(expectedStatus); + expect(response.headers.get('content-type') ?? '').toMatch(/application\/problem\+json/u); + expect(response.headers.get('www-authenticate')).toBe( + expectedStatus === 401 ? 'Bearer' : null, + ); + const body = yield* Effect.promise(() => response.json()); + expect(Schema.is(Schema.TaggedStruct(expectedTag, {}))(body), nextFailure.code).toBe( + true, + ); + expect(body.status, nextFailure.code).toBe(expectedStatus); + expect( + (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(body)).includes( + reason, + ), + nextFailure.code, + ).toBe(false); + if (expectedStatus === 503) { + expect(body.retryable, nextFailure.code).toBe(true); + } + }), ); - assert.equal(missingKey.status, 428); - const missingKeyBody = await missingKey.json(); - assert.ok( - Schema.is(Schema.TaggedStruct('PartyCommandPreconditionRequiredProblem', {}))( - missingKeyBody, - ), + expect(reads).toBe(cases.length); + }), +); + +it.live('the generated governed Read seam sanitizes unexpected runtime defects', () => + Effect.gen(function* sanitizeGovernedReadDefect() { + const assertion = yield* makeAssertion(); + const readRuntime: ReadRuntimeService = { + runRead: () => Effect.die('private governed Read defect'), + }; + const harness = yield* makeActionTestHarness(); + const app = yield* mountApp(harness, assertion.environment, readRuntime); + const response = yield* handle(app, decisionRequest(randomUUID(), assertion.token)); + expect(response.status).toBe(500); + expect(response.headers.get('content-type') ?? '').toMatch(/application\/problem\+json/u); + const body = yield* Effect.promise(() => response.json()); + expect(Schema.is(Schema.TaggedStruct('PartyMatchDecisionInternalProblem', {}))(body)).toBe( + true, + ); + expect(body.status).toBe(500); + expect( + (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(body)).includes('private'), + ).toBe(false); + }), +); + +it.live( + 'replayed assertions are challenged before a second Action or generated Read lifecycle', + () => + Effect.gen(function* rejectAssertionReplays() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness(); + const { readRuntime, readCount } = makeMissingDecisionReadRuntime(); + const app = yield* mountApp( + harness, + assertion.environment, + readRuntime, + makeSingleUseRedemption(), ); - assert.equal(runtimeCalls, 1); - const missingCorrelation = await handle( + const firstAction = yield* handle( app, commandRequest('request-search-rebuild', {}, assertion.token, { - 'idempotency-key': 'correlation-test', - 'x-correlation-id': '', + 'idempotency-key': 'first-redemption', }), ); - assert.equal(missingCorrelation.status, 400); - const missingCorrelationBody = await missingCorrelation.json(); - assert.ok( - Schema.is(Schema.TaggedStruct('PartyCommandInvalidRequestProblem', {}))( - missingCorrelationBody, - ), - ); - assert.equal(runtimeCalls, 1); - const oversizedCorrelation = await handle( + expect(firstAction.status).not.toBe(401); + expect(harness.snapshot().invocations.length).toBe(1); + const replayedAction = yield* handle( app, commandRequest('request-search-rebuild', {}, assertion.token, { - 'idempotency-key': 'oversized-correlation-test', - 'x-correlation-id': 'x'.repeat(201), + 'idempotency-key': 'second-redemption', }), ); - assert.equal(oversizedCorrelation.status, 400); - Schema.decodeUnknownSync(PartyCommandInvalidRequestProblemSchema)( - await oversizedCorrelation.json(), + expect(replayedAction.status).toBe(401); + expect(replayedAction.headers.get('www-authenticate')).toBe('Bearer'); + expect(replayedAction.headers.get('content-type') ?? '').toMatch( + /application\/problem\+json/u, ); - assert.equal(runtimeCalls, 1); - assert.equal(harness.snapshot().invocations.length, 0); - }, - ); -}); + expect(harness.snapshot().invocations.length).toBe(1); + const actionAssertionReadReplay = yield* handle( + app, + decisionRequest(randomUUID(), assertion.token), + ); + expect(actionAssertionReadReplay.status).toBe(401); + expect(actionAssertionReadReplay.headers.get('www-authenticate')).toBe('Bearer'); + expect(readCount()).toBe(0); + const firstRead = yield* handle(app, decisionRequest(randomUUID(), assertion.otherToken)); + expect(firstRead.status).toBe(404); + expect(readCount()).toBe(1); + const replayedRead = yield* handle(app, decisionRequest(randomUUID(), assertion.otherToken)); + expect(replayedRead.status).toBe(401); + expect(replayedRead.headers.get('www-authenticate')).toBe('Bearer'); + expect(replayedRead.headers.get('content-type') ?? '').toMatch(/application\/problem\+json/u); + expect(readCount()).toBe(1); + }), +); -test('the governed runner passes safe transport metadata through one complete Action execution', async () => { - const assertion = await makeAssertion(); - const correlationId = 'x'.repeat(200); - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - tenantPermission: 'allowed', - }); - await withMountedApp(mounted(harness, assertion.environment), async (app) => { - const response = await handle( +it.live('correlation and idempotency are mandatory before the Core Action lifecycle', () => + Effect.gen(function* validateActionTransport() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness(); + let runtimeCalls = 0; + const observingRuntime: ActionRuntimeService = { + resolveActionCommit: harness.runtime.resolveActionCommit, + runAction: (input) => { + runtimeCalls += 1; + return harness.runtime.runAction(input); + }, + }; + const app = yield* mountApp( + harness, + assertion.environment, + undefined, + nonPersistingRedemption, + observingRuntime, + ); + const missingKey = yield* handle( + app, + commandRequest('request-search-rebuild', {}, assertion.token), + ); + expect(missingKey.status).toBe(428); + const missingKeyBody = yield* Effect.promise(() => missingKey.json()); + expect( + Schema.is(Schema.TaggedStruct('PartyCommandPreconditionRequiredProblem', {}))(missingKeyBody), + ).toBe(true); + expect(runtimeCalls).toBe(1); + const missingCorrelation = yield* handle( app, commandRequest('request-search-rebuild', {}, assertion.token, { - 'idempotency-key': 'transport-test', - 'x-correlation-id': correlationId, - 'x-trace-id': 'trace-transport-test', + 'idempotency-key': 'correlation-test', + 'x-correlation-id': '', }), ); - assert.equal(response.status, 200); - const snapshot = harness.snapshot(); - assert.equal(snapshot.invocations.length, 1); - assert.equal(snapshot.committed.length, 1); - assert.equal(snapshot.transactionCount, 1); - assert.deepEqual(snapshot.committed[0]?.transport, { - correlationId, - idempotencyKey: 'transport-test', - traceId: 'trace-transport-test', - }); - assert.deepEqual(snapshot.committed[0]?.principal, principal); - assert.equal(snapshot.committed[0]?.actionKey, 'party.registry.request-search-rebuild'); - }); -}); + expect(missingCorrelation.status).toBe(400); + const missingCorrelationBody = yield* Effect.promise(() => missingCorrelation.json()); + expect( + Schema.is(Schema.TaggedStruct('PartyCommandInvalidRequestProblem', {}))( + missingCorrelationBody, + ), + ).toBe(true); + expect(runtimeCalls).toBe(1); + const oversizedCorrelation = yield* handle( + app, + commandRequest('request-search-rebuild', {}, assertion.token, { + 'idempotency-key': 'oversized-correlation-test', + 'x-correlation-id': 'x'.repeat(201), + }), + ); + expect(oversizedCorrelation.status).toBe(400); + yield* Schema.decodeUnknownEffect(PartyCommandInvalidRequestProblemSchema)( + yield* Effect.promise(() => oversizedCorrelation.json()), + ); + expect(runtimeCalls).toBe(1); + expect(harness.snapshot().invocations.length).toBe(0); + }), +); -test('a decoded relationship timestamp reaches the Action runtime exactly once', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness(); - const failure = new ActionPayloadValidationError({ - code: 'action_payload_invalid', - reason: 'runtime observation fixture', - }); - let runtimeCalls = 0; - const actionRuntime: ActionRuntimeService = { - resolveActionCommit: harness.runtime.resolveActionCommit, - runAction: () => { - runtimeCalls += 1; - return Effect.fail(failure); - }, - }; - await withMountedApp( - mounted(harness, assertion.environment, undefined, nonPersistingRedemption, actionRuntime), - async (app) => { - const response = await handle( +it.live( + 'the governed runner passes safe transport metadata through one complete Action execution', + () => + Effect.gen(function* preserveSafeActionTransport() { + const assertion = yield* makeAssertion(); + const correlationId = 'x'.repeat(200); + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'allowed', + }); + const app = yield* mountApp(harness, assertion.environment); + const response = yield* handle( app, - commandRequest('create-party-relationship', relationshipPayload, assertion.token, { - 'idempotency-key': 'relationship-timestamp-test', + commandRequest('request-search-rebuild', {}, assertion.token, { + 'idempotency-key': 'transport-test', + 'x-correlation-id': correlationId, + 'x-trace-id': 'trace-transport-test', }), ); - assert.equal(response.status, 400); - const body = await response.json(); - assert.equal(body._tag, 'PartyCommandInvalidRequestProblem'); - assert.equal(runtimeCalls, 1); - }, - ); -}); + expect(response.status).toBe(200); + const snapshot = harness.snapshot(); + expect(snapshot.invocations.length).toBe(1); + expect(snapshot.committed.length).toBe(1); + expect(snapshot.transactionCount).toBe(1); + expect(snapshot.committed[0]?.transport).toEqual({ + correlationId, + idempotencyKey: 'transport-test', + traceId: 'trace-transport-test', + }); + expect(snapshot.committed[0]?.principal).toEqual(principal); + expect(snapshot.committed[0]?.actionKey).toBe('party.registry.request-search-rebuild'); + }), +); -test('an unexpected runtime defect is sanitized by the governed outer HTTP seam', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness(); - const defectiveRuntime: ActionRuntimeService = { - resolveActionCommit: () => Effect.die('private resolution defect'), - runAction: () => Effect.die('private governed runner defect'), - }; - const observedLogs: string[] = []; - await withMountedApp( - mounted( +it.live('a decoded relationship timestamp reaches the Action runtime exactly once', () => + Effect.gen(function* decodeRelationshipTimestampOnce() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness(); + const failure = new ActionPayloadValidationError({ + code: 'action_payload_invalid', + reason: 'runtime observation fixture', + }); + let runtimeCalls = 0; + const actionRuntime: ActionRuntimeService = { + resolveActionCommit: harness.runtime.resolveActionCommit, + runAction: () => { + runtimeCalls += 1; + return Effect.fail(failure); + }, + }; + const app = yield* mountApp( + harness, + assertion.environment, + undefined, + nonPersistingRedemption, + actionRuntime, + ); + const response = yield* handle( + app, + commandRequest('create-party-relationship', relationshipPayload, assertion.token, { + 'idempotency-key': 'relationship-timestamp-test', + }), + ); + expect(response.status).toBe(400); + const body = yield* Effect.promise(() => response.json()); + expect(Predicate.isTagged(body, 'PartyCommandInvalidRequestProblem')).toBe(true); + expect(runtimeCalls).toBe(1); + }), +); + +it.live('an unexpected runtime defect is sanitized by the governed outer HTTP seam', () => + Effect.gen(function* sanitizeActionRuntimeDefect() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness(); + const defectiveRuntime: ActionRuntimeService = { + resolveActionCommit: () => Effect.die('private resolution defect'), + runAction: () => Effect.die('private governed runner defect'), + }; + const observedLogs: string[] = []; + const app = yield* mountApp( harness, assertion.environment, undefined, nonPersistingRedemption, defectiveRuntime, observedLogs, - ), - async (app) => { - const response = await handle( - app, - commandRequest('request-search-rebuild', {}, assertion.token, { - 'idempotency-key': 'runner-defect-test', + ); + const response = yield* handle( + app, + commandRequest('request-search-rebuild', {}, assertion.token, { + 'idempotency-key': 'runner-defect-test', + }), + ); + expect(response.status).toBe(500); + const body = yield* Effect.promise(() => response.json()); + expect(Predicate.isTagged(body, 'PartyCommandInternalProblem')).toBe(true); + expect(body.status).toBe(500); + expect( + (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(body)).includes( + 'private governed runner defect', + ), + ).toBe(false); + expect(harness.snapshot().invocations.length).toBe(0); + expect(observedLogs.length).toBe(1); + const [entry] = observedLogs; + assert.isOk(entry); + expect(entry).toMatch(/Unexpected governed Action HTTP defect/u); + expect(entry).toMatch(/private governed runner defect/u); + expect(entry).toMatch(/party\.registry\.request-search-rebuild/u); + expect(entry).toMatch(/party-command-test/u); + expect(entry).not.toMatch(new RegExp(assertion.token, 'u')); + expect(entry).not.toMatch(/runner-defect-test/u); + }), +); + +it.live('the endpoint-owned mapper preserves representative Core failure semantics', () => + Effect.gen(function* mapRepresentativeActionFailures() { + const assertion = yield* makeAssertion(); + const cases: readonly [failure: ActionCoreError, status: number, tag: string][] = [ + [ + new ActionPayloadValidationError({ + code: 'action_payload_invalid', + reason: 'invalid payload fixture', }), - ); - assert.equal(response.status, 500); - const body = await response.json(); - assert.equal(body._tag, 'PartyCommandInternalProblem'); - assert.equal(body.status, 500); - assert.equal(JSON.stringify(body).includes('private governed runner defect'), false); - assert.equal(harness.snapshot().invocations.length, 0); - assert.equal(observedLogs.length, 1); - const [entry] = observedLogs; - assert.ok(entry); - assert.match(entry, /Unexpected governed Action HTTP defect/u); - assert.match(entry, /private governed runner defect/u); - assert.match(entry, /party\.registry\.request-search-rebuild/u); - assert.match(entry, /party-command-test/u); - assert.doesNotMatch(entry, new RegExp(assertion.token, 'u')); - assert.doesNotMatch(entry, /runner-defect-test/u); - }, - ); -}); + 400, + 'PartyCommandInvalidRequestProblem', + ], + [ + new ActionPermissionDenied({ + code: 'action_permission_denied', + reason: 'permission denied fixture', + }), + 403, + 'PartyCommandForbiddenProblem', + ], + [ + new ActionPermissionCheckError({ + code: 'action_permission_check_failed', + reason: 'permission unavailable fixture', + }), + 503, + 'PartyCommandUnavailableProblem', + ], + [ + new ModuleStateDeniedError({ + code: 'module_state_denied', + reason: 'module denied fixture', + }), + 403, + 'PartyCommandForbiddenProblem', + ], + [ + new ModuleStateCheckUnavailableError({ + code: 'module_state_check_unavailable', + reason: 'module unavailable fixture', + }), + 503, + 'PartyCommandUnavailableProblem', + ], + [ + new ActionIdempotencyKeyRequired({ + code: 'action_idempotency_key_required', + reason: 'idempotency fixture', + }), + 428, + 'PartyCommandPreconditionRequiredProblem', + ], + [ + new ActionRequestHashConflict({ + code: 'action_request_hash_conflict', + reason: 'conflict fixture', + }), + 409, + 'PartyCommandConflictProblem', + ], + [ + new ActionInvocationNotFound({ + code: 'action_invocation_not_found', + reason: 'not-found fixture', + }), + 404, + 'PartyCommandNotFoundProblem', + ], + [ + new ActionInvocationPersistenceError({ + code: 'action_invocation_persistence_failed', + reason: 'persistence fixture', + }), + 503, + 'PartyCommandUnavailableProblem', + ], + [ + new ActionPolicyDenied({ + code: 'action_policy_denied', + policyReasonCode: 'fixture_ineligible', + reason: 'policy denial fixture', + }), + 422, + 'PartyCommandUnprocessableProblem', + ], + [ + new ActionPolicyEvaluationError({ + code: 'action_policy_evaluation_failed', + reason: 'policy evaluation fixture', + }), + 503, + 'PartyCommandUnavailableProblem', + ], + [ + new ActionHandlerExecutionError({ + code: 'action_handler_execution_failed', + reason: 'handler failure fixture', + }), + 500, + 'PartyCommandInternalProblem', + ], + ]; -test('the endpoint-owned mapper preserves representative Core failure semantics', async () => { - const assertion = await makeAssertion(); - const cases: readonly [failure: ActionCoreError, status: number, tag: string][] = [ - [ - new ActionPayloadValidationError({ - code: 'action_payload_invalid', - reason: 'invalid payload fixture', - }), - 400, - 'PartyCommandInvalidRequestProblem', - ], - [ - new ActionPermissionDenied({ - code: 'action_permission_denied', - reason: 'permission denied fixture', - }), - 403, - 'PartyCommandForbiddenProblem', - ], - [ - new ActionPermissionCheckError({ - code: 'action_permission_check_failed', - reason: 'permission unavailable fixture', - }), - 503, - 'PartyCommandUnavailableProblem', - ], - [ - new ModuleStateDeniedError({ - code: 'module_state_denied', - reason: 'module denied fixture', - }), - 403, - 'PartyCommandForbiddenProblem', - ], - [ - new ModuleStateCheckUnavailableError({ - code: 'module_state_check_unavailable', - reason: 'module unavailable fixture', - }), - 503, - 'PartyCommandUnavailableProblem', - ], - [ - new ActionIdempotencyKeyRequired({ - code: 'action_idempotency_key_required', - reason: 'idempotency fixture', - }), - 428, - 'PartyCommandPreconditionRequiredProblem', - ], - [ - new ActionRequestHashConflict({ - code: 'action_request_hash_conflict', - reason: 'conflict fixture', - }), - 409, - 'PartyCommandConflictProblem', - ], - [ - new ActionInvocationNotFound({ - code: 'action_invocation_not_found', - reason: 'not-found fixture', - }), - 404, - 'PartyCommandNotFoundProblem', - ], - [ - new ActionInvocationPersistenceError({ - code: 'action_invocation_persistence_failed', - reason: 'persistence fixture', - }), - 503, - 'PartyCommandUnavailableProblem', - ], - [ - new ActionPolicyDenied({ - code: 'action_policy_denied', - policyReasonCode: 'fixture_ineligible', - reason: 'policy denial fixture', - }), - 422, - 'PartyCommandUnprocessableProblem', - ], - [ - new ActionPolicyEvaluationError({ - code: 'action_policy_evaluation_failed', - reason: 'policy evaluation fixture', - }), - 503, - 'PartyCommandUnavailableProblem', - ], - [ - new ActionHandlerExecutionError({ - code: 'action_handler_execution_failed', - reason: 'handler failure fixture', - }), - 500, - 'PartyCommandInternalProblem', - ], - ]; - - await forEachSequential(cases, async ([failure, expectedStatus, expectedTag]) => { - const harness = makeActionTestHarness(); - const failingRuntime: ActionRuntimeService = { - resolveActionCommit: harness.runtime.resolveActionCommit, - runAction: () => Effect.fail(failure), - }; - await withMountedApp( - mounted(harness, assertion.environment, undefined, nonPersistingRedemption, failingRuntime), - async (app) => { - const response = await handle( + yield* forEachSequential(cases, ([failure, expectedStatus, expectedTag]) => + Effect.gen(function* verifyActionFailureMapping() { + const harness = yield* makeActionTestHarness(); + const failingRuntime: ActionRuntimeService = { + resolveActionCommit: harness.runtime.resolveActionCommit, + runAction: () => Effect.fail(failure), + }; + const app = yield* mountApp( + harness, + assertion.environment, + undefined, + nonPersistingRedemption, + failingRuntime, + ); + const response = yield* handle( app, commandRequest('request-search-rebuild', {}, assertion.token, { - 'idempotency-key': `mapping-${failure._tag}`, + 'idempotency-key': `mapping-${failure.code}`, }), ); - assert.equal(response.status, expectedStatus, failure._tag); - const body = await response.json(); - assert.equal(body._tag, expectedTag, failure._tag); - }, + expect(response.status, failure.code).toBe(expectedStatus); + const body = yield* Effect.promise(() => response.json()); + expect(Predicate.isTagged(body, expectedTag), failure.code).toBe(true); + }), ); - }); -}); + }), +); -test('endpoint-local mappings keep declared not-found capability distinct over HTTP', async () => { - const assertion = await makeAssertion(); - const failure = new ActionInvocationNotFound({ - code: 'action_invocation_not_found', - reason: 'endpoint capability fixture', - }); - const actionRuntime: ActionRuntimeService = { - resolveActionCommit: () => Effect.die('commit recovery is outside the fixture'), - runAction: () => Effect.fail(failure), - }; - const app = mountedOrganizationEngagement(assertion.environment, actionRuntime); - const profileRef = { - moduleId: 'party.registry', - resourceId: randomUUID(), - resourceType: 'party.registry.organization-engagement-profile', - tenantId: principal.tenantId, - } as const; - try { - const attachResponse = await handle( +it.live('endpoint-local mappings keep declared not-found capability distinct over HTTP', () => + Effect.gen(function* preserveEndpointNotFoundCapabilities() { + const assertion = yield* makeAssertion(); + const failure = new ActionInvocationNotFound({ + code: 'action_invocation_not_found', + reason: 'endpoint capability fixture', + }); + const actionRuntime: ActionRuntimeService = { + resolveActionCommit: () => Effect.die('commit recovery is outside the fixture'), + runAction: () => Effect.fail(failure), + }; + const app = yield* Effect.acquireRelease( + Effect.sync(() => mountedOrganizationEngagement(assertion.environment, actionRuntime)), + (resource) => Effect.promise(() => resource.dispose()).pipe(Effect.orDie), + ); + const profileRef = { + moduleId: 'party.registry', + resourceId: randomUUID(), + resourceType: 'party.registry.organization-engagement-profile', + tenantId: principal.tenantId, + } as const; + const attachResponse = yield* handle( app, engagementRequest('/contacts/engagement/organizations/attach', { partyRef }, assertion.token), ); - assert.equal(attachResponse.status, 500); - const attachBody = Schema.decodeUnknownSync(ProblemTagSchema)(await attachResponse.json()); - assert.equal(attachBody._tag, 'ContactsInternalProblem'); - - const archiveResponse = await handle( + expect(attachResponse.status).toBe(500); + const attachBody = yield* Schema.decodeUnknownEffect(ProblemTagSchema)( + yield* Effect.promise(() => attachResponse.json()), + ); + expect(Predicate.isTagged(attachBody, 'ContactsInternalProblem')).toBe(true); + const archiveResponse = yield* handle( app, engagementRequest( '/contacts/engagement/organizations/archive', @@ -1236,469 +1322,526 @@ test('endpoint-local mappings keep declared not-found capability distinct over H assertion.otherToken, ), ); - assert.equal(archiveResponse.status, 404); - const archiveBody = Schema.decodeUnknownSync(ProblemTagSchema)(await archiveResponse.json()); - assert.equal(archiveBody._tag, 'ContactsNotFoundProblem'); - } finally { - await app.dispose(); - } -}); + expect(archiveResponse.status).toBe(404); + const archiveBody = yield* Schema.decodeUnknownEffect(ProblemTagSchema)( + yield* Effect.promise(() => archiveResponse.json()), + ); + expect(Predicate.isTagged(archiveBody, 'ContactsNotFoundProblem')).toBe(true); + }), +); -test('real Core permission denial is a durable 403 and does not execute the command', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness({ - actionPermission: 'denied', - tenantPermission: 'allowed', - }); - await withMountedApp(mounted(harness, assertion.environment), async (app) => { - const response = await handle( +it.live('real Core permission denial is a durable 403 and does not execute the command', () => + Effect.gen(function* persistPermissionDenial() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness({ + actionPermission: 'denied', + tenantPermission: 'allowed', + }); + const app = yield* mountApp(harness, assertion.environment); + + const response = yield* handle( app, commandRequest('request-search-rebuild', {}, assertion.token, { 'idempotency-key': 'permission-test', }), ); - assert.equal(response.status, 403); - const body = await response.json(); - assert.ok(Schema.is(Schema.TaggedStruct('PartyCommandForbiddenProblem', {}))(body)); - assert.equal(harness.snapshot().invocations.length, 1); - assert.equal(harness.snapshot().permissionDenials.length, 1); - }); -}); + expect(response.status).toBe(403); + const body = yield* Effect.promise(() => response.json()); + expect(Predicate.isTagged(body, 'PartyCommandForbiddenProblem')).toBe(true); + expect(harness.snapshot().invocations.length).toBe(1); + expect(harness.snapshot().permissionDenials.length).toBe(1); + }), +); -test('the real handler translates domain conflicts and rolls back without successful evidence', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - tenantPermission: 'allowed', - services: [ - bindActionTestServices(archivePartyAction, { - transition: () => Effect.succeed({ _tag: 'conflict' as const, value: archivedParty }), - }), - ], - }); - await withMountedApp(mounted(harness, assertion.environment), async (app) => { - const response = await handle( - app, - commandRequest('archive-party', archivePayload, assertion.token, { - 'idempotency-key': 'conflict-test', - }), - ); - assert.equal(response.status, 409); - const body = await response.json(); - assert.ok(Schema.is(Schema.TaggedStruct('PartyCommandConflictProblem', {}))(body)); - assert.equal(body.code, 'party_lifecycle_conflict'); - assert.equal(harness.snapshot().invocations.length, 1); - assert.equal(harness.snapshot().committed.length, 0); - }); -}); +it.live( + 'the real handler translates domain conflicts and rolls back without successful evidence', + () => + Effect.gen(function* rollBackDomainConflict() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'allowed', + services: [ + bindActionTestServices(archivePartyAction, { + transition: () => Effect.succeed({ _tag: 'conflict' as const, value: archivedParty }), + }), + ], + }); + const app = yield* mountApp(harness, assertion.environment); -test('alias conflicts preserve only safe canonical recovery metadata', async () => { - const assertion = await makeAssertion(); - const canonicalPartyRef = { ...partyRef, resourceId: 'a4000000-0000-4000-8000-000000000002' }; - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - tenantPermission: 'allowed', - services: [ - bindActionTestServices(archivePartyAction, { - transition: () => - Effect.fail( - new PartyAliasWriteRejected({ - aliasPartyRef: partyRef, - canonicalPartyRef, - code: 'party_alias_write_rejected', - reason: 'Private diagnostic must never leave the owner boundary', - }), - ), - }), - ], - }); - await withMountedApp(mounted(harness, assertion.environment), async (app) => { - const response = await handle( + const response = yield* handle( + app, + commandRequest('archive-party', archivePayload, assertion.token, { + 'idempotency-key': 'conflict-test', + }), + ); + expect(response.status).toBe(409); + const body = yield* Effect.promise(() => response.json()); + expect(Predicate.isTagged(body, 'PartyCommandConflictProblem')).toBe(true); + expect(body.code).toBe('party_lifecycle_conflict'); + expect(harness.snapshot().invocations.length).toBe(1); + expect(harness.snapshot().committed.length).toBe(0); + }), +); + +it.live('alias conflicts preserve only safe canonical recovery metadata', () => + Effect.gen(function* preserveSafeAliasRecoveryMetadata() { + const assertion = yield* makeAssertion(); + const canonicalPartyRef = { ...partyRef, resourceId: 'a4000000-0000-4000-8000-000000000002' }; + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'allowed', + services: [ + bindActionTestServices(archivePartyAction, { + transition: () => + Effect.fail( + new PartyAliasWriteRejected({ + aliasPartyRef: partyRef, + canonicalPartyRef, + code: 'party_alias_write_rejected', + reason: 'Private diagnostic must never leave the owner boundary', + }), + ), + }), + ], + }); + const app = yield* mountApp(harness, assertion.environment); + + const response = yield* handle( app, commandRequest('archive-party', archivePayload, assertion.token, { 'idempotency-key': 'alias-test', }), ); - assert.equal(response.status, 409); - const body = await response.json(); - assert.ok(Schema.is(Schema.TaggedStruct('PartyCommandAliasWriteRejectedProblem', {}))(body)); - assert.deepEqual(body.aliasPartyRef, partyRef); - assert.deepEqual(body.canonicalPartyRef, canonicalPartyRef); - assert.equal(JSON.stringify(body).includes('Private diagnostic'), false); - assert.equal(harness.snapshot().committed.length, 0); - }); -}); + expect(response.status).toBe(409); + const body = yield* Effect.promise(() => response.json()); + expect(Predicate.isTagged(body, 'PartyCommandAliasWriteRejectedProblem')).toBe(true); + expect(body.aliasPartyRef).toEqual(partyRef); + expect(body.canonicalPartyRef).toEqual(canonicalPartyRef); + expect( + (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(body)).includes( + 'Private diagnostic', + ), + ).toBe(false); + expect(harness.snapshot().committed.length).toBe(0); + }), +); -test('committed request replay stays a terminal 409 and does not execute or emit twice', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - tenantPermission: 'allowed', - }); - await withMountedApp(mounted(harness, assertion.environment), async (app) => { - const first = await handle( +it.live('committed request replay stays a terminal 409 and does not execute or emit twice', () => + Effect.gen(function* rejectCommittedCommandReplay() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'allowed', + }); + const app = yield* mountApp(harness, assertion.environment); + + const first = yield* handle( app, commandRequest('request-search-rebuild', {}, assertion.token, { 'idempotency-key': 'replay-test', }), ); - assert.equal(first.status, 200); - const result = await first.json(); - assert.equal(result.status, 'QUEUED'); + expect(first.status).toBe(200); + const result = yield* Effect.promise(() => first.json()); + expect(result.status).toBe('QUEUED'); const { committed } = harness.snapshot(); - assert.equal(committed.length, 1); - const replay = await handle( + expect(committed.length).toBe(1); + const replay = yield* handle( app, commandRequest('request-search-rebuild', {}, assertion.token, { 'idempotency-key': 'replay-test', }), ); - assert.equal(replay.status, 409); - const body = await replay.json(); - assert.ok(Schema.is(Schema.TaggedStruct('PartyCommandAlreadyCommittedProblem', {}))(body)); - assert.equal(body.code, 'action_already_committed'); - assert.equal(body.invocationId, harness.snapshot().invocations[0]?.actionInvocationId); - assert.equal(body.retryCommand, false); - assert.equal(body.resolution, 'REFRESH_GOVERNED_READS'); - assert.equal(harness.snapshot().invocations.length, 1); - assert.deepEqual(harness.snapshot().committed, committed); - }); -}); + expect(replay.status).toBe(409); + const body = yield* Effect.promise(() => replay.json()); + expect(Predicate.isTagged(body, 'PartyCommandAlreadyCommittedProblem')).toBe(true); + expect(body.code).toBe('action_already_committed'); + expect(body.invocationId).toBe(harness.snapshot().invocations[0]?.actionInvocationId); + expect(body.retryCommand).toBe(false); + expect(body.resolution).toBe('REFRESH_GOVERNED_READS'); + expect(harness.snapshot().invocations.length).toBe(1); + expect(harness.snapshot().committed).toEqual(committed); + }), +); -test('declared not-found, capability-unavailable and unexpected defects retain safe distinct HTTP statuses', async () => { - const assertion = await makeAssertion(); - const cases = [ - { - status: 404, - tag: 'PartyCommandNotFoundProblem', - service: bindActionTestServices(archivePartyAction, { - transition: () => Effect.succeed({ _tag: 'not_found' as const }), - }), - }, - { - status: 503, - tag: 'PartyCommandUnavailableProblem', - service: bindActionTestServices(archivePartyAction, { - transition: () => - Effect.fail( - new PartyPersistenceUnavailable({ - code: 'party_persistence_unavailable', - reason: 'private database diagnostic', +it.live( + 'declared not-found, capability-unavailable and unexpected defects retain safe distinct HTTP statuses', + () => + Effect.gen(function* preserveDistinctFailureStatuses() { + const assertion = yield* makeAssertion(); + const cases = [ + { + status: 404, + tag: 'PartyCommandNotFoundProblem', + service: bindActionTestServices(archivePartyAction, { + transition: () => Effect.succeed({ _tag: 'not_found' as const }), + }), + }, + { + status: 503, + tag: 'PartyCommandUnavailableProblem', + service: bindActionTestServices(archivePartyAction, { + transition: () => + Effect.fail( + new PartyPersistenceUnavailable({ + code: 'party_persistence_unavailable', + reason: 'private database diagnostic', + }), + ), + }), + }, + { + status: 500, + tag: 'PartyCommandInternalProblem', + service: bindActionTestServices(archivePartyAction, { + transition: () => Effect.die('private unexpected diagnostic'), + }), + }, + ]; + yield* forEachSequential(cases, (item) => + Effect.gen(function* verifySafeFailureResponse() { + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'allowed', + services: [item.service], + }); + const app = yield* mountApp(harness, assertion.environment); + + const response = yield* handle( + app, + commandRequest('archive-party', archivePayload, assertion.token, { + 'idempotency-key': `failure-${item.status}`, }), - ), - }), - }, - { - status: 500, - tag: 'PartyCommandInternalProblem', - service: bindActionTestServices(archivePartyAction, { - transition: () => Effect.die('private unexpected diagnostic'), - }), - }, - ]; - await forEachSequential(cases, async (item) => { - const harness = makeActionTestHarness({ + ); + expect(response.status).toBe(item.status); + expect(response.headers.get('content-type') ?? '').toMatch(/application\/problem\+json/u); + const body = yield* Effect.promise(() => response.json()); + expect(Predicate.isTagged(body, item.tag)).toBe(true); + expect(body.status).toBe(item.status); + expect( + (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(body)).includes( + 'private', + ), + ).toBe(false); + if (item.status === 503) { + expect(body.retryable).toBe(true); + } + expect(harness.snapshot().committed.length).toBe(0); + }), + ); + }), +); + +it.live('semantically insufficient Party evidence is a declared 422, not a server defect', () => + Effect.gen(function* rejectInsufficientPartyEvidence() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness({ actionPermission: 'allowed', tenantPermission: 'allowed', - services: [item.service], - }); - await withMountedApp(mounted(harness, assertion.environment), async (app) => { - const response = await handle( - app, - commandRequest('archive-party', archivePayload, assertion.token, { - 'idempotency-key': `failure-${item.status}`, + services: [ + bindActionTestServices(createPartyAction, { + createOrMatch: () => + Effect.fail( + new PartyEvidenceInsufficient({ + code: 'party_evidence_insufficient', + reason: 'Private evidence diagnostics', + }), + ), }), - ); - assert.equal(response.status, item.status); - assert.match(response.headers.get('content-type') ?? '', /application\/problem\+json/u); - const body = await response.json(); - assert.ok(Schema.is(Schema.TaggedStruct(item.tag, {}))(body)); - assert.equal(body.status, item.status); - assert.equal(JSON.stringify(body).includes('private'), false); - if (item.status === 503) { - assert.equal(body.retryable, true); - } - assert.equal(harness.snapshot().committed.length, 0); + ], }); - }); -}); + const app = yield* mountApp(harness, assertion.environment); -test('semantically insufficient Party evidence is a declared 422, not a server defect', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - tenantPermission: 'allowed', - services: [ - bindActionTestServices(createPartyAction, { - createOrMatch: () => - Effect.fail( - new PartyEvidenceInsufficient({ - code: 'party_evidence_insufficient', - reason: 'Private evidence diagnostics', - }), - ), - }), - ], - }); - await withMountedApp(mounted(harness, assertion.environment), async (app) => { - const response = await handle( + const response = yield* handle( app, commandRequest('create-party', createPayload, assertion.token, { 'idempotency-key': 'evidence-test', }), ); - assert.equal(response.status, 422); - const body = await response.json(); - assert.equal(body.code, 'party_evidence_insufficient'); - assert.equal(body.status, 422); - assert.equal(JSON.stringify(body).includes('Private evidence'), false); - assert.equal(harness.snapshot().committed.length, 0); - }); -}); + expect(response.status).toBe(422); + const body = yield* Effect.promise(() => response.json()); + expect(body.code).toBe('party_evidence_insufficient'); + expect(body.status).toBe(422); + expect( + (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(body)).includes( + 'Private evidence', + ), + ).toBe(false); + expect(harness.snapshot().committed.length).toBe(0); + }), +); -test('the Core request hash rejects reuse of an idempotency key for a different command payload', async () => { - const assertion = await makeAssertion(); - let executions = 0; - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - tenantPermission: 'allowed', - services: [ - bindActionTestServices(createPartyAction, { - createOrMatch: () => - Effect.sync(() => { - executions += 1; - return { - outcome: 'CREATED' as const, - partyRef, - decisionRef: { - ...partyRef, - resourceType: 'party.registry.party-match-decision' as const, - }, - }; +it.live( + 'the Core request hash rejects reuse of an idempotency key for a different command payload', + () => + Effect.gen(function* rejectIdempotencyPayloadMismatch() { + const assertion = yield* makeAssertion(); + let executions = 0; + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'allowed', + services: [ + bindActionTestServices(createPartyAction, { + createOrMatch: () => + Effect.sync(() => { + executions += 1; + return { + outcome: 'CREATED' as const, + partyRef, + decisionRef: { + ...partyRef, + resourceType: 'party.registry.party-match-decision' as const, + }, + }; + }), }), - }), - ], - }); - await withMountedApp(mounted(harness, assertion.environment), async (app) => { - const first = await handle( - app, - commandRequest('create-party', createPayload, assertion.token, { - 'idempotency-key': 'hash-test', - }), - ); - assert.equal(first.status, 200); - const changed = await handle( - app, - commandRequest( - 'create-party', - { candidate: { ...createPayload.candidate, displayName: 'Different organization' } }, - assertion.token, - { 'idempotency-key': 'hash-test' }, - ), - ); - assert.equal(changed.status, 409); - const changedBody = await changed.json(); - assert.equal(changedBody.code, 'action_request_hash_conflict'); - assert.equal(executions, 1); - assert.equal(harness.snapshot().committed.length, 1); - }); -}); + ], + }); + const app = yield* mountApp(harness, assertion.environment); -test('commit resolution requires authentication and a valid invocation without creating an Action', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness(); - await withMountedApp(mounted(harness, assertion.environment), async (app) => { - const missingAuth = await handle(app, recoveryRequest(randomUUID())); - assert.equal(missingAuth.status, 401); - assert.equal(missingAuth.headers.get('www-authenticate'), 'Bearer'); - const malformed = await handle(app, recoveryRequest('not-an-id', assertion.token)); - assert.equal(malformed.status, 400); - const malformedBody = await malformed.json(); - assert.ok( - Schema.is(Schema.TaggedStruct('PartyCommandInvalidRequestProblem', {}))(malformedBody), - ); - const absent = await handle(app, recoveryRequest(randomUUID(), assertion.token)); - assert.equal(absent.status, 404); - assert.equal(harness.snapshot().invocations.length, 0); - }); -}); + const first = yield* handle( + app, + commandRequest('create-party', createPayload, assertion.token, { + 'idempotency-key': 'hash-test', + }), + ); + expect(first.status).toBe(200); + const changed = yield* handle( + app, + commandRequest( + 'create-party', + { candidate: { ...createPayload.candidate, displayName: 'Different organization' } }, + assertion.token, + { 'idempotency-key': 'hash-test' }, + ), + ); + expect(changed.status).toBe(409); + const changedBody = yield* Effect.promise(() => changed.json()); + expect(changedBody.code).toBe('action_request_hash_conflict'); + expect(executions).toBe(1); + expect(harness.snapshot().committed.length).toBe(1); + }), +); -test('an open invocation resolves explicitly without authorizing automatic command retry', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - services: [ - bindActionTestServices(archivePartyAction, { - transition: () => Effect.succeed({ _tag: 'conflict' as const, value: archivedParty }), - }), - ], - tenantPermission: 'allowed', - }); - await withMountedApp(mounted(harness, assertion.environment), async (app) => { - const failed = await handle( +it.live( + 'commit resolution requires authentication and a valid invocation without creating an Action', + () => + Effect.gen(function* validateCommitResolutionRequest() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness(); + const app = yield* mountApp(harness, assertion.environment); + + const missingAuth = yield* handle(app, recoveryRequest(randomUUID())); + expect(missingAuth.status).toBe(401); + expect(missingAuth.headers.get('www-authenticate')).toBe('Bearer'); + const malformed = yield* handle(app, recoveryRequest('not-an-id', assertion.token)); + expect(malformed.status).toBe(400); + const malformedBody = yield* Effect.promise(() => malformed.json()); + expect(Predicate.isTagged(malformedBody, 'PartyCommandInvalidRequestProblem')).toBe(true); + const absent = yield* handle(app, recoveryRequest(randomUUID(), assertion.token)); + expect(absent.status).toBe(404); + expect(harness.snapshot().invocations.length).toBe(0); + }), +); + +it.live('an open invocation resolves explicitly without authorizing automatic command retry', () => + Effect.gen(function* resolveOpenInvocationWithoutRetry() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + services: [ + bindActionTestServices(archivePartyAction, { + transition: () => Effect.succeed({ _tag: 'conflict' as const, value: archivedParty }), + }), + ], + tenantPermission: 'allowed', + }); + const app = yield* mountApp(harness, assertion.environment); + + const failed = yield* handle( app, commandRequest('archive-party', archivePayload, assertion.token, { 'idempotency-key': 'pending-resolution', }), ); - assert.equal(failed.status, 409); + expect(failed.status).toBe(409); const invocationId = harness.snapshot().invocations[0]?.actionInvocationId; - assert.ok(invocationId); - const resolution = await handle(app, recoveryRequest(invocationId, assertion.token)); - assert.equal(resolution.status, 200); - assert.deepEqual(await resolution.json(), { - _tag: 'PartyCommandCommitResolution', + expect(Boolean(invocationId)).toBe(true); + if (invocationId === undefined || invocationId.length === 0) { + throw new Error('Expected truthy value'); + } + const resolution = yield* handle(app, recoveryRequest(invocationId, assertion.token)); + expect(resolution.status).toBe(200); + const resolutionBody = yield* Effect.promise(() => resolution.json()); + expect(Schema.is(ResolvePartyCommandCommitResultSchema)(resolutionBody)).toBe(true); + expect(Struct.omit(resolutionBody, ['_tag'])).toEqual({ invocationId, retryCommand: false, state: 'OPEN', }); - assert.equal(harness.snapshot().invocations.length, 1); - assert.equal(harness.snapshot().committed.length, 0); - }); -}); + expect(harness.snapshot().invocations.length).toBe(1); + expect(harness.snapshot().committed.length).toBe(0); + }), +); -test('actual Core commit acknowledgement loss resolves and the mounted governed Read returns the original decision without rerunning the Action', async () => { - const assertion = await makeAssertion(); - const decisions = new Map(); - let executions = 0; - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - tenantPermission: 'allowed', - commitAcknowledgement: 'indeterminate-once', - services: [ - bindActionTestServices(createPartyAction, { - createOrMatch: (_candidate, actionInvocationId) => - Effect.sync(() => { - executions += 1; - const decisionRef = { - ...partyRef, - resourceId: randomUUID(), - resourceType: 'party.registry.party-match-decision' as const, - }; - decisions.set(actionInvocationId, { - caseRef: null, - committedCreateOutcome: 'CREATED', - decidedAt: '2026-09-01T00:00:00.000Z', - decisionRef, - evidenceEvaluation: null, - evidenceExplanation: [ - { - reason: 'Verified creation evidence', - ruleKey: RuleKeySchema.make('creation-evidence'), - }, - ], - matchRuleVersion: 'test-original-rule-v1', - operation: 'CREATE', - outcome: 'CREATED', - partyRef, - }); - return { outcome: 'CREATED' as const, partyRef, decisionRef }; +it.live( + 'actual Core commit acknowledgement loss resolves and the mounted governed Read returns the original decision without rerunning the Action', + () => + Effect.gen(function* recoverOriginalDecisionAfterLostCommitAcknowledgement() { + const assertion = yield* makeAssertion(); + const decisions = new Map(); + let executions = 0; + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'allowed', + commitAcknowledgement: 'indeterminate-once', + services: [ + bindActionTestServices(createPartyAction, { + createOrMatch: (_candidate, actionInvocationId) => + Effect.sync(() => { + executions += 1; + const decisionRef = { + ...partyRef, + resourceId: randomUUID(), + resourceType: 'party.registry.party-match-decision' as const, + }; + decisions.set(actionInvocationId, { + caseRef: null, + committedCreateOutcome: 'CREATED', + decidedAt: '2026-09-01T00:00:00.000Z', + decisionRef, + evidenceEvaluation: null, + evidenceExplanation: [ + { + reason: 'Verified creation evidence', + ruleKey: RuleKeySchema.make('creation-evidence'), + }, + ], + matchRuleVersion: 'test-original-rule-v1', + operation: 'CREATE', + outcome: 'CREATED', + partyRef, + }); + return { outcome: 'CREATED' as const, partyRef, decisionRef }; + }), }), - }), - ], - }); - // Only the ReadRuntime is a typed double: the Action runtime and mounted BFFs are real. - // Its projection is the exact decision written by the original Action service above. - const reads: ReadRuntimeService = { - runRead: (input) => - Effect.gen(function* readOriginalDecision() { - const actor = yield* Schema.decodeUnknownEffect(TrustedPrincipalContextSchema)( - input.principal, - ).pipe( - Effect.mapError( - () => - new ReadPermissionDenied({ code: 'read_permission_denied', reason: 'Invalid actor' }), - ), - ); - if (actor.principalId !== principal.principalId || actor.tenantId !== principal.tenantId) { - return yield* new ReadPermissionDenied({ - code: 'read_permission_denied', - reason: 'Decision belongs to another principal', - }); - } - const query = yield* Schema.decodeUnknownEffect(PartyMatchDecisionRequestSchema)( - input.input, - ).pipe( - Effect.mapError( - () => - new ReadHandlerNotFound({ + ], + }); + // Only the ReadRuntime is a typed double: the Action runtime and mounted BFFs are real. + // Its projection is the exact decision written by the original Action service above. + const reads: ReadRuntimeService = { + runRead: (input) => + Effect.gen(function* readOriginalDecision() { + const actor = yield* Schema.decodeUnknownEffect(TrustedPrincipalContextSchema)( + input.principal, + ).pipe( + Effect.mapError( + () => + new ReadPermissionDenied({ + code: 'read_permission_denied', + reason: 'Invalid actor', + }), + ), + ); + if ( + actor.principalId !== principal.principalId || + actor.tenantId !== principal.tenantId + ) { + return yield* new ReadPermissionDenied({ + code: 'read_permission_denied', + reason: 'Decision belongs to another principal', + }); + } + const query = yield* Schema.decodeUnknownEffect(PartyMatchDecisionRequestSchema)( + input.input, + ).pipe( + Effect.mapError( + () => + new ReadHandlerNotFound({ + code: 'read_handler_not_found', + reason: 'No decision identity', + }), + ), + ); + const decision = + query.actionInvocationId === undefined + ? undefined + : decisions.get(query.actionInvocationId); + if (decision === undefined) { + return yield* new ReadHandlerNotFound({ code: 'read_handler_not_found', - reason: 'No decision identity', - }), - ), - ); - const decision = - query.actionInvocationId === undefined - ? undefined - : decisions.get(query.actionInvocationId); - if (decision === undefined) { - return yield* new ReadHandlerNotFound({ - code: 'read_handler_not_found', - reason: 'No persisted decision', - }); - } - return yield* Schema.decodeUnknownEffect(input.registration.descriptor.resultSchema)( - decision, - ).pipe( - Effect.mapError( - () => - new ReadResultValidationError({ - code: 'read_result_invalid', - reason: 'Invalid decision fixture', - }), - ), - ); - }), - }; - await withMountedApp(mounted(harness, assertion.environment, reads), async (app) => { - const uncertain = await handle( - app, - commandRequest('create-party', createPayload, assertion.token, { - 'idempotency-key': 'uncertain-create', - }), - ); - assert.equal(uncertain.status, 503); - const body = await uncertain.json(); - assert.ok(Schema.is(Schema.TaggedStruct('PartyCommandCommitIndeterminateProblem', {}))(body)); - assert.equal(body.resolution, 'RESOLVE_COMMIT'); - assert.equal(body.retryCommand, false); - const invocationId = harness.snapshot().invocations[0]?.actionInvocationId; - assert.ok(invocationId); - assert.equal(body.invocationId, invocationId); - assert.equal(harness.snapshot().committed.length, 1); - const committedSnapshot = harness.snapshot(); - const deniedRecovery = await handle(app, recoveryRequest(invocationId, assertion.otherToken)); - assert.equal(deniedRecovery.status, 404); - const resolution = await handle(app, recoveryRequest(invocationId, assertion.token)); - assert.equal(resolution.status, 200); - assert.deepEqual(await resolution.json(), { - _tag: 'PartyCommandCommitResolution', - invocationId, - retryCommand: false, - state: 'COMMITTED', - }); - const missingReadAuth = await handle(app, decisionRequest(invocationId)); - assert.equal(missingReadAuth.status, 401); - const deniedRead = await handle(app, decisionRequest(invocationId, assertion.otherToken)); - assert.equal(deniedRead.status, 403); - const recovered = await handle(app, decisionRequest(invocationId, assertion.token)); - assert.equal(recovered.status, 200); - assert.deepEqual(await recovered.json(), decisions.get(invocationId)); - const replay = await handle( - app, - commandRequest('create-party', createPayload, assertion.token, { - 'idempotency-key': 'uncertain-create', - }), - ); - assert.equal(replay.status, 409); - const replayBody = await replay.json(); - assert.ok( - Schema.is(Schema.TaggedStruct('PartyCommandAlreadyCommittedProblem', {}))(replayBody), - ); - assert.equal(replayBody.invocationId, invocationId); - assert.equal(replayBody.retryCommand, false); - assert.equal(executions, 1); - assert.deepEqual(harness.snapshot().committed, committedSnapshot.committed); - assert.equal(harness.snapshot().invocations.length, 1); - }); -}); + reason: 'No persisted decision', + }); + } + return yield* Schema.decodeUnknownEffect(input.registration.descriptor.resultSchema)( + decision, + ).pipe( + Effect.mapError( + () => + new ReadResultValidationError({ + code: 'read_result_invalid', + reason: 'Invalid decision fixture', + }), + ), + ); + }), + }; + const app = yield* mountApp(harness, assertion.environment, reads); + + const uncertain = yield* handle( + app, + commandRequest('create-party', createPayload, assertion.token, { + 'idempotency-key': 'uncertain-create', + }), + ); + expect(uncertain.status).toBe(503); + const body = yield* Effect.promise(() => uncertain.json()); + expect(Predicate.isTagged(body, 'PartyCommandCommitIndeterminateProblem')).toBe(true); + expect(body.resolution).toBe('RESOLVE_COMMIT'); + expect(body.retryCommand).toBe(false); + const invocationId = harness.snapshot().invocations[0]?.actionInvocationId; + expect(Boolean(invocationId)).toBe(true); + if (invocationId === undefined || invocationId.length === 0) { + throw new Error('Expected truthy value'); + } + expect(body.invocationId).toBe(invocationId); + expect(harness.snapshot().committed.length).toBe(1); + const committedSnapshot = harness.snapshot(); + const deniedRecovery = yield* handle( + app, + recoveryRequest(invocationId, assertion.otherToken), + ); + expect(deniedRecovery.status).toBe(404); + const resolution = yield* handle(app, recoveryRequest(invocationId, assertion.token)); + expect(resolution.status).toBe(200); + const resolutionBody = yield* Effect.promise(() => resolution.json()); + expect(Schema.is(ResolvePartyCommandCommitResultSchema)(resolutionBody)).toBe(true); + expect(Struct.omit(resolutionBody, ['_tag'])).toEqual({ + invocationId, + retryCommand: false, + state: 'COMMITTED', + }); + const missingReadAuth = yield* handle(app, decisionRequest(invocationId)); + expect(missingReadAuth.status).toBe(401); + const deniedRead = yield* handle(app, decisionRequest(invocationId, assertion.otherToken)); + expect(deniedRead.status).toBe(403); + const recovered = yield* handle(app, decisionRequest(invocationId, assertion.token)); + expect(recovered.status).toBe(200); + expect(yield* Effect.promise(() => recovered.json())).toEqual(decisions.get(invocationId)); + const replay = yield* handle( + app, + commandRequest('create-party', createPayload, assertion.token, { + 'idempotency-key': 'uncertain-create', + }), + ); + expect(replay.status).toBe(409); + const replayBody = yield* Effect.promise(() => replay.json()); + expect(Predicate.isTagged(replayBody, 'PartyCommandAlreadyCommittedProblem')).toBe(true); + expect(replayBody.invocationId).toBe(invocationId); + expect(replayBody.retryCommand).toBe(false); + expect(executions).toBe(1); + expect(harness.snapshot().committed).toEqual(committedSnapshot.committed); + expect(harness.snapshot().invocations.length).toBe(1); + }), +); diff --git a/app/verticals/party-registry/tests/unit/api-integration-contract.test.ts b/app/verticals/party-registry/tests/unit/api-integration-contract.test.ts index 195b7bafd..84b3c9986 100644 --- a/app/verticals/party-registry/tests/unit/api-integration-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/api-integration-contract.test.ts @@ -1,9 +1,8 @@ -// @effect-diagnostics asyncFunction:off nodeBuiltinImport:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; +// @effect-diagnostics nodeBuiltinImport:off -- Inspect source files through the Node filesystem boundary; expires: 2026-12-31. +import { expect, it } from 'effect-rstest'; import { readFile } from 'node:fs/promises'; -import test from 'node:test'; -import { Schema } from 'effect'; +import { Effect, Schema } from 'effect'; import { partyRegistryApi, @@ -69,9 +68,9 @@ void operationContextRejectsIdentityMetadata; const literalTypeProof: ReadinessPathRemainsLiteral & ReadinessOperationRouteRemainsLiteral = true; void literalTypeProof; -test('aggregates every governed read and search API beside readiness', () => { - assert.deepEqual(Object.keys(partyRegistryApi.groups).toSorted(), apiNames); - assert.deepEqual(partyRegistryApiContract, { +it('aggregates every governed read and search API beside readiness', () => { + expect(Object.keys(partyRegistryApi.groups).toSorted()).toEqual(apiNames); + expect(partyRegistryApiContract).toEqual({ apiPrefix: '/party-registry-api', basePath: '/party-registry-api/party-registry', ownerId: 'party-registry', @@ -81,21 +80,17 @@ test('aggregates every governed read and search API beside readiness', () => { const endpointPaths = Object.values(partyRegistryApi.groups).flatMap((group) => Object.values(group.endpoints).map(({ path }) => path), ); - assert.equal(new Set(Object.keys(partyRegistryApi.groups)).size, apiNames.length); - assert.equal(new Set(endpointPaths).size, endpointPaths.length); - assert.equal(endpointPaths.includes('/party-registry/readiness'), true); - assert.equal( - endpointPaths.some((path) => path === '/party-registry'), - false, - ); - assert.equal( + expect(new Set(Object.keys(partyRegistryApi.groups)).size).toBe(apiNames.length); + expect(new Set(endpointPaths).size).toBe(endpointPaths.length); + expect(endpointPaths.includes('/party-registry/readiness')).toBe(true); + expect(endpointPaths.some((path) => path === '/party-registry')).toBe(false); + expect( endpointPaths.some((path) => path === '/actions' || path === '/party-registry/actions'), - false, - ); + ).toBe(false); }); -test('keeps readiness tied to the immutable build marker', () => { - assert.equal( +it('keeps readiness tied to the immutable build marker', () => { + expect( Schema.is(partyRegistryReadinessSchema)({ checks: { api: 'ready', @@ -107,53 +102,61 @@ test('keeps readiness tied to the immutable build marker', () => { status: 'ready', versionSkew: 'none', }), - true, - ); + ).toBe(true); }); -test('re-exports every governed generated client without exposing private executors', async () => { - const source = await readFile( - new URL('../../src/api/party-registry-client.ts', import.meta.url), - 'utf-8', - ); +it.effect('re-exports every governed generated client without exposing private executors', () => + Effect.gen(function* testProgram2() { + const source = yield* Effect.promise(() => + readFile(new URL('../../src/api/party-registry-client.ts', import.meta.url), 'utf-8'), + ); - for (const client of apiNames.filter( - (name) => - name !== 'foundation' && - name !== 'organizationEngagementMutations' && - name !== 'partyCommands' && - name !== 'partyCommandRecovery' && - name !== 'personEngagementMutations', - )) { - const file = client.replaceAll(/[A-Z]/gu, (value) => `-${value.toLowerCase()}`); - assert.match(source, new RegExp(`\\./${file}-client\\.ts`, 'u')); - } - assert.match(source, /\.\/engagement-profile-client\.ts/u); - assert.match(source, /getPartyRegistryReadiness/u); - assert.match(source, /party-command-client/u); - assert.match(source, /export const partyRegistryClient =/u); - assert.match(source, /createPartyRegistryHttpClient/u); - assert.doesNotMatch(source, /createPartyRegistryClient/u); - assert.doesNotMatch(source, /makeEffectHttpApiClient\(partyRegistryApi/u); - assert.doesNotMatch( - source, - /export const (?:createPartyRegistry|listPartyRegistry|getPartyRegistry)\s*=/u, - ); - assert.doesNotMatch(source, /action\.ts|runAction|ActionRuntime/u); -}); + for (const client of apiNames.filter( + (name) => + name !== 'foundation' && + name !== 'organizationEngagementMutations' && + name !== 'partyCommands' && + name !== 'partyCommandRecovery' && + name !== 'personEngagementMutations', + )) { + const file = client.replaceAll(/[A-Z]/gu, (value) => `-${value.toLowerCase()}`); + expect(source).toMatch(new RegExp(`\\./${file}-client\\.ts`, 'u')); + } + expect(source).toMatch(/\.\/engagement-profile-client\.ts/u); + expect(source).toMatch(/getPartyRegistryReadiness/u); + expect(source).toMatch(/party-command-client/u); + expect(source).toMatch(/export const partyRegistryClient =/u); + expect(source).toMatch(/createPartyRegistryHttpClient/u); + expect(source).not.toMatch(/createPartyRegistryClient/u); + expect(source).not.toMatch(/makeEffectHttpApiClient\(partyRegistryApi/u); + expect(source).not.toMatch( + /export const (?:createPartyRegistry|listPartyRegistry|getPartyRegistry)\s*=/u, + ); + expect(source).not.toMatch(/action\.ts|runAction|ActionRuntime/u); + }), +); -test('exposes only the backend Effect API and no placeholder UI module', async () => { - const [frontendFederation, backendFederation, packageSource] = await Promise.all([ - readFile(new URL('../../module-federation.config.ts', import.meta.url), 'utf-8'), - readFile(new URL('../../backend-federation.config.ts', import.meta.url), 'utf-8'), - readFile(new URL('../../package.json', import.meta.url), 'utf-8'), - ]); - const packageJson: { readonly exports: Record } = JSON.parse(packageSource); +it.effect('exposes only the backend Effect API and no placeholder UI module', () => + Effect.gen(function* testProgram3() { + const [frontendFederation, backendFederation, packageSource] = yield* Effect.promise(() => + Promise.all([ + readFile(new URL('../../module-federation.config.ts', import.meta.url), 'utf-8'), + readFile(new URL('../../backend-federation.config.ts', import.meta.url), 'utf-8'), + readFile(new URL('../../package.json', import.meta.url), 'utf-8'), + ]), + ); + const packageJson: { readonly exports: Record } = + yield* Schema.decodeUnknownEffect( + Schema.fromJsonString( + Schema.Struct({ exports: Schema.Record(Schema.String, Schema.String) }), + ), + )(packageSource); - assert.doesNotMatch(frontendFederation, /['"]\.\/Route['"]|['"]\.\/Widget['"]/u); - assert.match(backendFederation, /['"]\.\/effect-api['"]/u); - assert.equal(packageJson.exports['./Route'], undefined); - assert.equal(packageJson.exports['./Widget'], undefined); - assert.equal(packageJson.exports['./api'], './shared/api.ts'); - assert.equal(packageJson.exports['./api/client'], './src/api/party-registry-client.ts'); -}); + expect(frontendFederation).not.toMatch(/['"]\.\/Route['"]|['"]\.\/Widget['"]/u); + expect(backendFederation).toMatch(/['"]\.\/effect-api['"]/u); + expect(packageJson.exports['./Route']).toBe(undefined); + expect(packageJson.exports['./Widget']).toBe(undefined); + expect(packageJson.exports['./api']).toBe('./shared/api.ts'); + expect(packageJson.exports['./api/client']).toBe('./src/api/party-registry-client.ts'); + }), +); diff --git a/app/verticals/party-registry/tests/unit/api-integration-correction-client.test.ts b/app/verticals/party-registry/tests/unit/api-integration-correction-client.test.ts index e61e10296..9fcb8090f 100644 --- a/app/verticals/party-registry/tests/unit/api-integration-correction-client.test.ts +++ b/app/verticals/party-registry/tests/unit/api-integration-correction-client.test.ts @@ -1,9 +1,12 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; + import { DateTime, Effect, Option, Schema } from 'effect'; -import { FetchHttpClient } from 'effect/unstable/http'; +import { + FetchHttpClient, + HttpRouter, + HttpServerRequest, + HttpServerResponse, +} from 'effect/unstable/http'; import { CorrectPartyFactPayloadSchema } from '../../shared/command-api.ts'; import { correctPartyFactWithAuthorization, @@ -34,147 +37,174 @@ const originalAssertion = { value: 'Incorrect recorded name', }; -test('public clients discover the first assertion and submit a governed correction with its ID', async () => { - let corrected = false; - const requests: Request[] = []; - const fakeFetch: typeof fetch = async (input, init) => { - const request = new Request(input, init); - requests.push(request); - const { pathname } = new URL(request.url); - if (pathname.endsWith('/actions/create-party')) { - return Response.json({ - decisionRef: { ...partyRef, resourceType: 'party.registry.party-match-decision' }, - outcome: 'CREATED', - partyRef, - }); - } - if (pathname.endsWith('/actions/correct-party-fact')) { - const payload = Schema.decodeUnknownSync(CorrectPartyFactPayloadSchema)(await request.json()); - if (payload.factKind === 'RELATIONSHIP') { - throw new Error('Expected identity correction'); - } - assert.equal(payload.factKind, 'DISPLAY_NAME'); - assert.equal(payload.targetAssertionId, originalAssertionId); - assert.equal(payload.partyId, partyRef.resourceId); - corrected = true; - return Response.json({ - correctionRef: { ...partyRef, resourceType: 'party.registry.party-correction' }, - factKind: 'DISPLAY_NAME', - followUp: 'ENRICHMENT_REVIEW', - partyRef, - relationshipRef: null, - replacementAssertionId, - replacementRelationshipRef: null, - retractedAssertionId: originalAssertionId, +it.effect( + 'public clients discover the first assertion and submit a governed correction with its ID', + () => + Effect.gen(function* apiIntegrationCorrectionClientCase1() { + let corrected = false; + const requests: Request[] = []; + const handleRequest = Effect.gen(function* handleCorrectionRequest() { + const request = yield* HttpServerRequest.HttpServerRequest; + const pathname = request.url; + if (pathname.endsWith('/actions/create-party')) { + return HttpServerResponse.jsonUnsafe({ + decisionRef: { ...partyRef, resourceType: 'party.registry.party-match-decision' }, + outcome: 'CREATED', + partyRef, + }); + } + if (pathname.endsWith('/actions/correct-party-fact')) { + const payload = yield* Schema.decodeUnknownEffect(CorrectPartyFactPayloadSchema)( + yield* request.json, + ); + if (payload.factKind === 'RELATIONSHIP') { + throw new Error('Expected identity correction'); + } + expect(payload.factKind).toBe('DISPLAY_NAME'); + expect(payload.targetAssertionId).toBe(originalAssertionId); + expect(payload.partyId).toBe(partyRef.resourceId); + corrected = true; + return HttpServerResponse.jsonUnsafe({ + correctionRef: { ...partyRef, resourceType: 'party.registry.party-correction' }, + factKind: 'DISPLAY_NAME', + followUp: 'ENRICHMENT_REVIEW', + partyRef, + relationshipRef: null, + replacementAssertionId, + replacementRelationshipRef: null, + retractedAssertionId: originalAssertionId, + }); + } + expect(pathname.endsWith('/reads/party-detail')).toBe(true); + const currentAssertion = corrected + ? { + ...originalAssertion, + assertionId: replacementAssertionId, + supersedesAssertionId: originalAssertionId, + value: 'Corrected name', + } + : originalAssertion; + return HttpServerResponse.jsonUnsafe({ + currentFactAssertions: [currentAssertion], + factHistory: corrected + ? [{ ...originalAssertion, isCurrent: false, state: 'SUPERSEDED' }, currentAssertion] + : [originalAssertion], + party: { + archivedAt: null, + createdAt: timestamp, + displayName: currentAssertion.value, + partyRef, + partyType: 'ORGANIZATION', + revision: corrected ? 2 : 1, + updatedAt: timestamp, + }, + resolution: { + aliasChain: [], + canonicalPartyRef: partyRef, + kind: 'DIRECT', + requestedPartyRef: partyRef, + }, + }); }); - } - assert.equal(pathname.endsWith('/reads/party-detail'), true); - const currentAssertion = corrected - ? { - ...originalAssertion, - assertionId: replacementAssertionId, - supersedesAssertionId: originalAssertionId, - value: 'Corrected name', + const server = yield* Effect.acquireRelease( + Effect.sync(() => + HttpRouter.toWebHandler(HttpRouter.add('*', '/*', handleRequest), { + disableLogger: true, + }), + ), + (resource) => Effect.promise(() => resource.dispose()), + ); + const fakeFetch: typeof fetch = (input, init) => { + const request = new Request(input, init); + requests.push(request); + return server.handler(request); + }; + const options = { + baseUrl: 'https://party.example/party-registry-api', + correlationId: 'correction-discovery', + idempotencyKey: 'create-for-correction', + }; + const program = Effect.gen(function* verifyPublicCorrectionWorkflow() { + const created = yield* createPartyWithAuthorization( + { + candidate: { + displayName: originalAssertion.value, + evidenceRefs: ['document:original'], + officialIdentifiers: [], + partyType: 'ORGANIZATION', + provenance: { method: 'MANUAL_REVIEW', source: 'document:original' }, + validFrom: DateTime.makeUnsafe(timestamp), + }, + }, + 'Bearer test-assertion', + options, + ); + expect(created.outcome).toBe('CREATED'); + const before = yield* executePartyDetailWithAuthorization( + { includeFactHistory: true, partyRef }, + 'Bearer test-assertion', + options.correlationId, + options, + ); + const target = before.currentFactAssertions.find( + ({ factKind }) => factKind === 'DISPLAY_NAME', + ); + expect(target).toBeDefined(); + if (target === undefined) { + throw new Error('Expected target to be defined'); } - : originalAssertion; - return Response.json({ - currentFactAssertions: [currentAssertion], - factHistory: corrected - ? [{ ...originalAssertion, isCurrent: false, state: 'SUPERSEDED' }, currentAssertion] - : [originalAssertion], - party: { - archivedAt: null, - createdAt: timestamp, - displayName: currentAssertion.value, - partyRef, - partyType: 'ORGANIZATION', - revision: corrected ? 2 : 1, - updatedAt: timestamp, - }, - resolution: { - aliasChain: [], - canonicalPartyRef: partyRef, - kind: 'DIRECT', - requestedPartyRef: partyRef, - }, - }); - }; - const options = { - baseUrl: 'https://party.example/party-registry-api', - correlationId: 'correction-discovery', - idempotencyKey: 'create-for-correction', - }; - const program = Effect.gen(function* verifyPublicCorrectionWorkflow() { - const created = yield* createPartyWithAuthorization( - { + const correctionPayload = yield* Schema.decodeUnknownEffect(CorrectPartyFactPayloadSchema)({ + evidenceRefs: ['document:reviewed-error'], + evidenceSource: 'DOCUMENT', + factKind: 'DISPLAY_NAME', + partyId: partyRef.resourceId, + policyVersion: 'party-correction.v1', + provenance: { method: 'MANUAL_REVIEW', source: 'document:reviewed-error' }, + reasonCode: 'WRONG_IDENTITY_VALUE', + replacementValue: 'Corrected name', + targetAssertionId: target.assertionId, + }); + const correction = yield* correctPartyFactWithAuthorization( + correctionPayload, + 'Bearer test-assertion', + { ...options, idempotencyKey: 'correct-first-assertion' }, + ); + expect(correction.retractedAssertionId).toBe(target.assertionId); + const after = yield* executePartyDetailWithAuthorization( + { includeFactHistory: true, partyRef }, + 'Bearer test-assertion', + options.correlationId, + options, + ); + expect(after.currentFactAssertions[0]?.assertionId).toBe(replacementAssertionId); + expect( + Option.getOrElse(after.factHistory, () => []).some( + ({ assertionId, state }) => + assertionId === originalAssertionId && state === 'SUPERSEDED', + ), + ).toBe(true); + }); + yield* program.pipe(Effect.provideService(FetchHttpClient.Fetch, fakeFetch)); + expect(requests.length).toBe(4); + const createRequest = requests.find(({ url }) => url.endsWith('/actions/create-party')); + expect(createRequest).toBeDefined(); + if (createRequest === undefined) { + throw new Error('Expected createRequest to be defined'); + } + expect(yield* Effect.promise(() => createRequest.json())).toEqual({ candidate: { displayName: originalAssertion.value, evidenceRefs: ['document:original'], officialIdentifiers: [], partyType: 'ORGANIZATION', provenance: { method: 'MANUAL_REVIEW', source: 'document:original' }, - validFrom: DateTime.makeUnsafe(timestamp), + validFrom: timestamp, }, - }, - 'Bearer test-assertion', - options, - ); - assert.equal(created.outcome, 'CREATED'); - const before = yield* executePartyDetailWithAuthorization( - { includeFactHistory: true, partyRef }, - 'Bearer test-assertion', - options.correlationId, - options, - ); - const target = before.currentFactAssertions.find(({ factKind }) => factKind === 'DISPLAY_NAME'); - assert.ok(target); - const correctionPayload = yield* Schema.decodeUnknownEffect(CorrectPartyFactPayloadSchema)({ - evidenceRefs: ['document:reviewed-error'], - evidenceSource: 'DOCUMENT', - factKind: 'DISPLAY_NAME', - partyId: partyRef.resourceId, - policyVersion: 'party-correction.v1', - provenance: { method: 'MANUAL_REVIEW', source: 'document:reviewed-error' }, - reasonCode: 'WRONG_IDENTITY_VALUE', - replacementValue: 'Corrected name', - targetAssertionId: target.assertionId, - }); - const correction = yield* correctPartyFactWithAuthorization( - correctionPayload, - 'Bearer test-assertion', - { ...options, idempotencyKey: 'correct-first-assertion' }, - ); - assert.equal(correction.retractedAssertionId, target.assertionId); - const after = yield* executePartyDetailWithAuthorization( - { includeFactHistory: true, partyRef }, - 'Bearer test-assertion', - options.correlationId, - options, - ); - assert.equal(after.currentFactAssertions[0]?.assertionId, replacementAssertionId); - assert.equal( - Option.getOrElse(after.factHistory, () => []).some( - ({ assertionId, state }) => assertionId === originalAssertionId && state === 'SUPERSEDED', - ), - true, - ); - }); - await runEffectTestPromise(program.pipe(Effect.provideService(FetchHttpClient.Fetch, fakeFetch))); - assert.equal(requests.length, 4); - const createRequest = requests.find(({ url }) => url.endsWith('/actions/create-party')); - assert.ok(createRequest); - assert.deepEqual(await createRequest.json(), { - candidate: { - displayName: originalAssertion.value, - evidenceRefs: ['document:original'], - officialIdentifiers: [], - partyType: 'ORGANIZATION', - provenance: { method: 'MANUAL_REVIEW', source: 'document:original' }, - validFrom: timestamp, - }, - }); - assert.equal( - requests.every((request) => request.headers.get('authorization') === 'Bearer test-assertion'), - true, - ); -}); + }); + expect( + requests.every( + (request) => request.headers.get('authorization') === 'Bearer test-assertion', + ), + ).toBe(true); + }), +); diff --git a/app/verticals/party-registry/tests/unit/api-integration-runtime.test.ts b/app/verticals/party-registry/tests/unit/api-integration-runtime.test.ts index 1853c1e47..9fcbdd545 100644 --- a/app/verticals/party-registry/tests/unit/api-integration-runtime.test.ts +++ b/app/verticals/party-registry/tests/unit/api-integration-runtime.test.ts @@ -1,7 +1,5 @@ -// @effect-diagnostics asyncFunction:off cryptoRandomUUID:off nodeBuiltinImport:off -- Test-only HTTP/JWT fixture; expires: 2027-09-07. -import assert from 'node:assert/strict'; +import { assert, expect, it } from 'effect-rstest'; import { randomUUID } from 'node:crypto'; -import test from 'node:test'; import { ActionRuntime, GatewayAssertionRedemptionService, ReadRuntime } from '@app/core-runtime'; import type { ActionRuntimeService, ReadRuntimeService } from '@app/core-runtime'; @@ -43,195 +41,220 @@ const commaSeparatedHeader = (value: string | null): readonly string[] => .map((item) => item.trim()) .filter((item) => item.length > 0) .toSorted(); -const makeAssertion = async () => { - const issuer = 'https://shell.runtime-assembly.test'; - const { privateKey, publicKey } = await generateKeyPair('Ed25519'); - const publicJwk = { - ...(await exportJWK(publicKey)), - alg: 'EdDSA', - kid: 'party-runtime-assembly-test', - use: 'sig', - }; - const token = await new SignJWT({ principal, ver: 1 }) - .setProtectedHeader({ alg: 'EdDSA', kid: 'party-runtime-assembly-test', typ: 'JWT' }) - .setIssuer(issuer) - .setAudience('party-registry') - .setSubject(principal.principalId) - .setIssuedAt() - .setExpirationTime('5m') - .setJti(randomUUID()) - .sign(privateKey); - return { issuer, publicJwks: JSON.stringify({ keys: [publicJwk] }), token }; -}; - -test('serves readiness and rejects the removed placeholder write without business dependencies', async () => { - const readinessApi = HttpApi.make('PartyRegistryApi').add(partyRegistryApi.groups.foundation); - const server = HttpRouter.toWebHandler( - HttpApiBuilder.layer(readinessApi).pipe( - Layer.provide(partyRegistryFoundationLive), - Layer.provide(HttpServer.layerServices), - ), - { disableLogger: true }, - ); - try { - const response = await server.handler( - new Request('http://localhost/party-registry/readiness'), - Context.empty(), +const makeAssertion = () => + Effect.gen(function* signRuntimeAssemblyAssertion() { + const issuer = 'https://shell.runtime-assembly.test'; + const { privateKey, publicKey } = yield* Effect.promise(() => generateKeyPair('Ed25519')); + const publicJwk = { + ...(yield* Effect.promise(() => exportJWK(publicKey))), + alg: 'EdDSA', + kid: 'party-runtime-assembly-test', + use: 'sig', + }; + const token = yield* Effect.promise(() => + new SignJWT({ principal, ver: 1 }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'party-runtime-assembly-test', typ: 'JWT' }) + .setIssuer(issuer) + .setAudience('party-registry') + .setSubject(principal.principalId) + .setIssuedAt() + .setExpirationTime('5m') + .setJti(randomUUID()) + .sign(privateKey), ); - assert.equal(response.status, 200); - const readiness = Schema.decodeUnknownSync(partyRegistryReadinessSchema)(await response.json()); - assert.deepEqual(readiness.marker, ultramodernApiMarker); - assert.equal(readiness.status, 'ready'); - - const removedWrite = await server.handler( - new Request('http://localhost/party-registry', { - body: JSON.stringify({ name: 'Must not create an item' }), - headers: { 'content-type': 'application/json' }, - method: 'POST', + return { + issuer, + publicJwks: yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ + keys: [publicJwk], }), - Context.empty(), - ); - assert.equal(removedWrite.status, 404); - } finally { - await server.dispose(); - } -}); + token, + }; + }); -test('builds every declared handler and preserves owner-local CORS through the injectable runtime', async () => { - const assertion = await makeAssertion(); - let redemptionCalls = 0; - let actionCalls = 0; - let actionCommitCalls = 0; - let aresCalls = 0; - let aresLayerLoads = 0; - let counterpartySearchCalls = 0; - let partySearchCalls = 0; - let readCalls = 0; - let searchLayerLoads = 0; - const actionRuntime: ActionRuntimeService = { - resolveActionCommit: () => - Effect.sync(() => { - actionCommitCalls += 1; - throw new Error('Action commit substitute reached through the assembled runtime'); - }), - runAction: () => - Effect.sync(() => { - actionCalls += 1; - throw new Error('Action substitute reached through the assembled runtime'); - }), - }; - const aresSubjectService = { - subject: (_input) => - Effect.sync(() => { - aresCalls += 1; - throw new Error('ARES substitute reached through the assembled runtime'); - }), - } satisfies AresSubjectServiceContract; - const searchProjectionGateway = { - searchCounterparties: (_input) => - Effect.sync(() => { - counterpartySearchCalls += 1; - throw new Error('Counterparty search substitute reached through the assembled runtime'); - }), - searchParties: (_input) => - Effect.sync(() => { - partySearchCalls += 1; - throw new Error('Party search substitute reached through the assembled runtime'); - }), - } satisfies PartySearchProjectionGatewayService; - const readRuntime: ReadRuntimeService = { - runRead: ({ registration }) => - Effect.context().pipe( - Effect.flatMap((context) => { - readCalls += 1; - if (registration.descriptor.readKey === 'party.registry.api.ares-lookup') { - const service = Context.getOrUndefined(context, AresSubjectService); - assert.ok(service !== undefined); - return service - .subject({ - correlationId: 'runtime-assembly-proof', - ico: '27074358', - }) - .pipe( - Effect.orDie, - Effect.andThen(Effect.die('ARES substitute completed unexpectedly')), - ); - } - if (registration.descriptor.readKey === 'party.registry.search.parties') { - const service = Context.getOrUndefined(context, PartySearchProjectionGateway); - assert.ok(service !== undefined); - return service - .searchParties({ - includeArchived: false, - query: 'runtime assembly proof', - tenantId: principal.tenantId, - }) - .pipe( - Effect.orDie, - Effect.andThen(Effect.die('Party search substitute completed unexpectedly')), - ); - } - if (registration.descriptor.readKey === 'party.registry.search.counterparties') { - const service = Context.getOrUndefined(context, PartySearchProjectionGateway); - assert.ok(service !== undefined); - return service - .searchCounterparties({ - effectiveAt: '2026-09-07T00:00:00.000Z', - includeArchived: false, - legalEntityId: 'a5000000-0000-4000-8000-000000000001', - query: 'runtime assembly proof', - tenantId: principal.tenantId, - }) - .pipe( - Effect.orDie, - Effect.andThen(Effect.die('Counterparty search substitute completed unexpectedly')), - ); - } - return Effect.die('Read substitute reached through the assembled runtime'); - }), - ), - }; - const actionRuntimeLayer = Layer.mergeAll( - Layer.succeed(ActionRuntime, actionRuntime), - ConfigProvider.layer( - ConfigProvider.fromUnknown({ - ONTOS_GATEWAY_ISSUER: assertion.issuer, - ONTOS_GATEWAY_PUBLIC_JWKS: assertion.publicJwks, - }), - ), - ); - const aresSubjectLayer = Layer.effect( - AresSubjectService, - Effect.sync(() => { - aresLayerLoads += 1; - return aresSubjectService; - }), - ); - const searchProjectionLayer = Layer.effect( - PartySearchProjectionGateway, - Effect.sync(() => { - searchLayerLoads += 1; - return searchProjectionGateway; +it.effect( + 'serves readiness and rejects the removed placeholder write without business dependencies', + () => + Effect.gen(function* apiIntegrationRuntimeCase1() { + const readinessApi = HttpApi.make('PartyRegistryApi').add(partyRegistryApi.groups.foundation); + const server = yield* Effect.acquireRelease( + Effect.sync(() => + HttpRouter.toWebHandler( + HttpApiBuilder.layer(readinessApi).pipe( + Layer.provide(partyRegistryFoundationLive), + Layer.provide(HttpServer.layerServices), + ), + { disableLogger: true }, + ), + ), + (resource) => Effect.promise(() => resource.dispose()), + ); + const response = yield* Effect.promise(() => + server.handler(new Request('http://localhost/party-registry/readiness'), Context.empty()), + ); + expect(response.status).toBe(200); + const readiness = yield* Schema.decodeUnknownEffect(partyRegistryReadinessSchema)( + yield* Effect.promise(() => response.json()), + ); + expect(readiness.marker).toEqual(ultramodernApiMarker); + expect(readiness.status).toBe('ready'); + + const removedWrite = yield* Effect.promise(() => + server.handler( + new Request('http://localhost/party-registry', { + body: '{"name":"Must not create an item"}', + headers: { 'content-type': 'application/json' }, + method: 'POST', + }), + Context.empty(), + ), + ); + expect(removedWrite.status).toBe(404); }), - ); - const assembledRuntime = makePartyRegistryApiRuntime( - Layer.succeed(ReadRuntime, readRuntime), - aresSubjectLayer, - searchProjectionLayer, - actionRuntimeLayer, - Layer.succeed(GatewayAssertionRedemptionService, { - consume: () => +); + +it.live( + 'builds every declared handler and preserves owner-local CORS through the injectable runtime', + () => + Effect.gen(function* verifyRuntimeAssemblyAndCors() { + const assertion = yield* makeAssertion(); + let redemptionCalls = 0; + let actionCalls = 0; + let actionCommitCalls = 0; + let aresCalls = 0; + let aresLayerLoads = 0; + let counterpartySearchCalls = 0; + let partySearchCalls = 0; + let readCalls = 0; + let searchLayerLoads = 0; + const actionRuntime: ActionRuntimeService = { + resolveActionCommit: () => + Effect.suspend(() => { + actionCommitCalls += 1; + return Effect.die('Action commit substitute reached through the assembled runtime'); + }), + runAction: () => + Effect.suspend(() => { + actionCalls += 1; + return Effect.die('Action substitute reached through the assembled runtime'); + }), + }; + const aresSubjectService = { + subject: (_input) => + Effect.suspend(() => { + aresCalls += 1; + return Effect.die('ARES substitute reached through the assembled runtime'); + }), + } satisfies AresSubjectServiceContract; + const searchProjectionGateway = { + searchCounterparties: (_input) => + Effect.suspend(() => { + counterpartySearchCalls += 1; + return Effect.die( + 'Counterparty search substitute reached through the assembled runtime', + ); + }), + searchParties: (_input) => + Effect.suspend(() => { + partySearchCalls += 1; + return Effect.die('Party search substitute reached through the assembled runtime'); + }), + } satisfies PartySearchProjectionGatewayService; + const readRuntime: ReadRuntimeService = { + runRead: ({ registration }) => + Effect.context().pipe( + Effect.flatMap((context) => { + readCalls += 1; + if (registration.descriptor.readKey === 'party.registry.api.ares-lookup') { + const service = Context.getOrUndefined(context, AresSubjectService); + assert.isOk(service !== undefined); + return service + .subject({ + correlationId: 'runtime-assembly-proof', + ico: '27074358', + }) + .pipe( + Effect.orDie, + Effect.andThen(Effect.die('ARES substitute completed unexpectedly')), + ); + } + if (registration.descriptor.readKey === 'party.registry.search.parties') { + const service = Context.getOrUndefined(context, PartySearchProjectionGateway); + assert.isOk(service !== undefined); + return service + .searchParties({ + includeArchived: false, + query: 'runtime assembly proof', + tenantId: principal.tenantId, + }) + .pipe( + Effect.orDie, + Effect.andThen(Effect.die('Party search substitute completed unexpectedly')), + ); + } + if (registration.descriptor.readKey === 'party.registry.search.counterparties') { + const service = Context.getOrUndefined(context, PartySearchProjectionGateway); + assert.isOk(service !== undefined); + return service + .searchCounterparties({ + effectiveAt: '2026-09-07T00:00:00.000Z', + includeArchived: false, + legalEntityId: 'a5000000-0000-4000-8000-000000000001', + query: 'runtime assembly proof', + tenantId: principal.tenantId, + }) + .pipe( + Effect.orDie, + Effect.andThen( + Effect.die('Counterparty search substitute completed unexpectedly'), + ), + ); + } + return Effect.die('Read substitute reached through the assembled runtime'); + }), + ), + }; + const actionRuntimeLayer = Layer.mergeAll( + Layer.succeed(ActionRuntime, actionRuntime), + ConfigProvider.layer( + ConfigProvider.fromUnknown({ + ONTOS_GATEWAY_ISSUER: assertion.issuer, + ONTOS_GATEWAY_PUBLIC_JWKS: assertion.publicJwks, + }), + ), + ); + const aresSubjectLayer = Layer.effect( + AresSubjectService, Effect.sync(() => { - redemptionCalls += 1; + aresLayerLoads += 1; + return aresSubjectService; }), - }), - ); - const runtime = assembledRuntime.createHandler(); + ); + const searchProjectionLayer = Layer.effect( + PartySearchProjectionGateway, + Effect.sync(() => { + searchLayerLoads += 1; + return searchProjectionGateway; + }), + ); + const assembledRuntime = makePartyRegistryApiRuntime( + Layer.succeed(ReadRuntime, readRuntime), + aresSubjectLayer, + searchProjectionLayer, + actionRuntimeLayer, + Layer.succeed(GatewayAssertionRedemptionService, { + consume: () => + Effect.sync(() => { + redemptionCalls += 1; + }), + }), + ); + const runtime = yield* Effect.acquireRelease( + Effect.sync(() => assembledRuntime.createHandler()), + (resource) => Effect.promise(() => resource.dispose()).pipe(Effect.orDie), + ); - try { - const unauthenticatedRead = await runtime.handler( - new Request('http://localhost/reads/party-detail', { - body: JSON.stringify({ + const unauthenticatedReadRequest = new Request('http://localhost/reads/party-detail', { + body: yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ partyRef: { moduleId: 'party.registry', resourceId: 'a4000000-0000-4000-8000-000000000001', @@ -244,263 +267,268 @@ test('builds every declared handler and preserves owner-local CORS through the i 'x-correlation-id': 'runtime-missing-credentials', }, method: 'POST', - }), - ); - assert.equal(unauthenticatedRead.status, 401); - assert.equal(redemptionCalls, 0); - assert.equal(unauthenticatedRead.headers.get('www-authenticate'), 'Bearer'); - Schema.decodeUnknownSync(PartyDetailAuthenticationProblemSchema)( - await unauthenticatedRead.json(), - ); - - const endpoints = Object.values(partyRegistryApi.groups).flatMap((group) => - Object.values(group.endpoints), - ); - const headers = { - authorization: `Bearer ${assertion.token}`, - 'content-type': 'application/json', - 'x-correlation-id': 'runtime-assembly-proof', - }; - const counterpartyRef = { - moduleId: 'party.registry', - resourceId: 'a6000000-0000-4000-8000-000000000001', - resourceType: 'party.registry.counterparty', - tenantId: principal.tenantId, - }; - const provenance = { - evidenceReference: 'runtime-assembly-proof', - method: 'TEST', - source: 'runtime-assembly-proof', - }; - const candidate = { - displayName: 'Runtime assembly proof', - evidenceRefs: ['document:verified'], - officialIdentifiers: [], - partyType: 'ORGANIZATION', - provenance: { method: 'DOCUMENT', source: 'operator' }, - validFrom: '2026-09-07T00:00:00.000Z', - }; - const contactPointRef = { - moduleId: 'party.registry', - resourceId: 'a9000000-0000-4000-8000-000000000001', - resourceType: 'party.registry.party-contact-point', - tenantId: principal.tenantId, - }; - const contactPointProvenance = { - authoritative: true, - method: 'DOCUMENT_REVIEW', - source: 'USER_ASSERTION', - }; - const addContactPointPayload = { - contactPoint: { preferred: false, type: 'EMAIL', value: 'contact@example.test' }, - partyRef, - privacyClassification: 'PUBLIC', - provenance: contactPointProvenance, - validFrom: '2026-09-07T00:00:00.000Z', - verification: { state: 'UNVERIFIED' }, - } as const; - const invalidContactResponse = await runtime.handler( - new Request('http://localhost/party-registry/actions/add-contact-point', { - body: JSON.stringify({ - ...addContactPointPayload, - contactPoint: { ...addContactPointPayload.contactPoint, value: 'not-an-email' }, - }), - headers: { ...headers, 'idempotency-key': 'invalid-contact-point' }, - method: 'POST', - }), - ); - assert.equal(invalidContactResponse.status, 400); - assert.match( - invalidContactResponse.headers.get('content-type') ?? '', - /application\/problem\+json/u, - ); - Schema.decodeUnknownSync(PartyCommandInvalidRequestProblemSchema)( - await invalidContactResponse.json(), - ); - assert.equal(actionCalls, 0); - assert.equal(redemptionCalls, 0); - const manualPayloads = { - '/party-registry/actions/add-contact-point': addContactPointPayload, - '/party-registry/actions/add-party-official-identifier': { - identifier: { - identifierType: 'ICO', - value: '27074358', - verification: 'VERIFIED', - }, - partyRef, - provenanceMethod: 'DOCUMENT', - provenanceSource: 'runtime-assembly-proof', - validFrom: '2026-09-07T00:00:00.000Z', - }, - '/party-registry/actions/counterparty-role-add': { - counterpartyRef, - provenance, - roleType: 'CUSTOMER', + }); + const unauthenticatedRead = yield* Effect.promise(() => + runtime.handler(unauthenticatedReadRequest), + ); + expect(unauthenticatedRead.status).toBe(401); + expect(redemptionCalls).toBe(0); + expect(unauthenticatedRead.headers.get('www-authenticate')).toBe('Bearer'); + yield* Schema.decodeUnknownEffect(PartyDetailAuthenticationProblemSchema)( + yield* Effect.promise(() => unauthenticatedRead.json()), + ); + const endpoints = Object.values(partyRegistryApi.groups).flatMap((group) => + Object.values(group.endpoints), + ); + const headers = { + authorization: `Bearer ${assertion.token}`, + 'content-type': 'application/json', + 'x-correlation-id': 'runtime-assembly-proof', + }; + const counterpartyRef = { + moduleId: 'party.registry', + resourceId: 'a6000000-0000-4000-8000-000000000001', + resourceType: 'party.registry.counterparty', + tenantId: principal.tenantId, + }; + const provenance = { + evidenceReference: 'runtime-assembly-proof', + method: 'TEST', + source: 'runtime-assembly-proof', + }; + const candidate = { + displayName: 'Runtime assembly proof', + evidenceRefs: ['document:verified'], + officialIdentifiers: [], + partyType: 'ORGANIZATION', + provenance: { method: 'DOCUMENT', source: 'operator' }, validFrom: '2026-09-07T00:00:00.000Z', - }, - '/party-registry/actions/counterparty-role-end': { - counterpartyRef, - provenance, - rolePeriodRef: { - moduleId: 'party.registry', - resourceId: 'a7000000-0000-4000-8000-000000000001', - resourceType: 'party.registry.counterparty-role-period', - tenantId: principal.tenantId, - }, - validTo: '2026-09-07T00:00:00.000Z', - }, - '/party-registry/actions/create-party': { candidate }, - '/party-registry/actions/end-contact-point': { - contactPointRef, - effectiveEnd: '2026-09-07T00:00:00.000Z', - provenance: contactPointProvenance, - reason: 'Runtime assembly proof', - target: { type: 'WHOLE_CONTACT_POINT' }, - }, - '/party-registry/actions/match-party': { candidate }, - '/party-registry/actions/update-party': { - displayName: 'Updated runtime assembly proof', - expectedRevision: 1, + }; + const contactPointRef = { + moduleId: 'party.registry', + resourceId: 'a9000000-0000-4000-8000-000000000001', + resourceType: 'party.registry.party-contact-point', + tenantId: principal.tenantId, + }; + const contactPointProvenance = { + authoritative: true, + method: 'DOCUMENT_REVIEW', + source: 'USER_ASSERTION', + }; + const addContactPointPayload = { + contactPoint: { preferred: false, type: 'EMAIL', value: 'contact@example.test' }, partyRef, - provenanceMethod: 'DOCUMENT', - provenanceSource: 'runtime-assembly-proof', - validFrom: '2026-09-07T00:00:00.000Z', - }, - '/party-registry/actions/update-contact-point': { - change: { preferred: true, type: 'SET_CHANNEL_PREFERRED' }, - contactPointRef, - expectedRevision: 1, + privacyClassification: 'PUBLIC', provenance: contactPointProvenance, - }, - '/party-registry/actions/update-party-official-identifier': { - change: { - expectedVerification: 'UNVERIFIED', - type: 'SET_VERIFICATION', - verification: 'VERIFIED', + validFrom: '2026-09-07T00:00:00.000Z', + verification: { state: 'UNVERIFIED' }, + } as const; + const invalidContactResponseRequest = new Request( + 'http://localhost/party-registry/actions/add-contact-point', + { + body: yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ + ...addContactPointPayload, + contactPoint: { ...addContactPointPayload.contactPoint, value: 'not-an-email' }, + }), + headers: { ...headers, 'idempotency-key': 'invalid-contact-point' }, + method: 'POST', }, - evidenceRefs: ['document:verified'], - officialIdentifierRef: { - moduleId: 'party.registry', - resourceId: 'a8000000-0000-4000-8000-000000000001', - resourceType: 'party.registry.party-official-identifier', - tenantId: principal.tenantId, + ); + const invalidContactResponse = yield* Effect.promise(() => + runtime.handler(invalidContactResponseRequest), + ); + expect(invalidContactResponse.status).toBe(400); + expect(invalidContactResponse.headers.get('content-type') ?? '').toMatch( + /application\/problem\+json/u, + ); + yield* Schema.decodeUnknownEffect(PartyCommandInvalidRequestProblemSchema)( + yield* Effect.promise(() => invalidContactResponse.json()), + ); + expect(actionCalls).toBe(0); + expect(redemptionCalls).toBe(0); + const manualPayloads = { + '/party-registry/actions/add-contact-point': addContactPointPayload, + '/party-registry/actions/add-party-official-identifier': { + identifier: { + identifierType: 'ICO', + value: '27074358', + verification: 'VERIFIED', + }, + partyRef, + provenanceMethod: 'DOCUMENT', + provenanceSource: 'runtime-assembly-proof', + validFrom: '2026-09-07T00:00:00.000Z', }, - reason: 'Runtime assembly proof', - }, - '/party-registry/actions/update-party-relationship': { - changeReason: 'Runtime assembly proof', - expectedRevision: 1, - provenance: { method: 'TEST', source: 'runtime-assembly-proof' }, - relationshipRef: { - moduleId: 'party.registry', - resourceId: 'aa000000-0000-4000-8000-000000000001', - resourceType: 'party.registry.party-relationship', - tenantId: principal.tenantId, + '/party-registry/actions/counterparty-role-add': { + counterpartyRef, + provenance, + roleType: 'CUSTOMER', + validFrom: '2026-09-07T00:00:00.000Z', }, - }, - '/reads/party-match': { candidate }, - } as const; - for (const [index, endpoint] of endpoints.entries()) { - const callsBefore: number = actionCalls + actionCommitCalls + readCalls; - const payloadSchema = endpoint.payload.get('application/json')?.schemas[0]; - const manualPayload = Object.entries(manualPayloads).find( - ([path]) => path === endpoint.path, - )?.[1]; - const payload = - payloadSchema === undefined - ? undefined - : (manualPayload ?? - Schema.encodeSync(payloadSchema)( - FastCheck.sample(Schema.toArbitrary(payloadSchema)(FastCheck), { - numRuns: 1, - seed: index + 1, - })[0], - )); - const request = - payload === undefined - ? new Request(`http://localhost${endpoint.path}`, { - headers: { ...headers, 'idempotency-key': `runtime-assembly-${index + 1}` }, - method: endpoint.method, - }) - : new Request(`http://localhost${endpoint.path}`, { - body: JSON.stringify(payload), - headers: { ...headers, 'idempotency-key': `runtime-assembly-${index + 1}` }, - method: endpoint.method, - }); - // oxlint-disable-next-line no-await-in-loop -- Counter deltas prove each endpoint reaches exactly one substitute before the next request. - const response = await runtime.handler(request); - if (endpoint.path === '/party-registry/readiness') { - assert.equal(response.status, 200); - assert.equal(actionCalls + actionCommitCalls + readCalls, callsBefore); - } else { - assert.equal( - response.status, - 500, - `${endpoint.method} ${endpoint.path} must execute a supplied runtime substitute`, - ); - assert.equal( - actionCalls + actionCommitCalls + readCalls, - callsBefore + 1, - `${endpoint.method} ${endpoint.path} must reach exactly one supplied core runtime`, - ); - } - } - assert.ok(redemptionCalls > 0); - assert.ok(actionCalls > 0); - assert.equal(actionCommitCalls, 1); - assert.ok(readCalls > 0); - assert.equal(aresCalls, 1); - assert.equal(aresLayerLoads, 1); - assert.equal(partySearchCalls, 1); - assert.equal(counterpartySearchCalls, 1); - assert.equal(searchLayerLoads, 1); - - const preflight = await runtime.handler( - new Request('http://localhost/party-registry/readiness', { - headers: { - 'access-control-request-headers': 'Authorization, X-Correlation-Id', - 'access-control-request-method': 'GET', - origin: 'http://localhost:3020', + '/party-registry/actions/counterparty-role-end': { + counterpartyRef, + provenance, + rolePeriodRef: { + moduleId: 'party.registry', + resourceId: 'a7000000-0000-4000-8000-000000000001', + resourceType: 'party.registry.counterparty-role-period', + tenantId: principal.tenantId, + }, + validTo: '2026-09-07T00:00:00.000Z', }, - method: 'OPTIONS', - }), - ); - assert.equal(preflight.status, 204); - assert.equal(preflight.headers.get('access-control-allow-origin'), 'http://localhost:3020'); - assert.deepEqual( - commaSeparatedHeader(preflight.headers.get('access-control-allow-methods')), - [...partyRegistryCorsAllowedMethods].toSorted(), - ); - assert.deepEqual( - commaSeparatedHeader(preflight.headers.get('access-control-allow-headers')), - [...partyRegistryCorsAllowedHeaders].toSorted(), - ); - assert.equal(preflight.headers.get('access-control-max-age'), '600'); - - const loopbackPreflight = await runtime.handler( - new Request('http://localhost/party-registry/readiness', { - headers: { - 'access-control-request-method': 'GET', - origin: 'http://127.0.0.1:3020', + '/party-registry/actions/create-party': { candidate }, + '/party-registry/actions/end-contact-point': { + contactPointRef, + effectiveEnd: '2026-09-07T00:00:00.000Z', + provenance: contactPointProvenance, + reason: 'Runtime assembly proof', + target: { type: 'WHOLE_CONTACT_POINT' }, }, - method: 'OPTIONS', - }), - ); - assert.equal( - loopbackPreflight.headers.get('access-control-allow-origin'), - 'http://127.0.0.1:3020', - ); - const foreignPreflight = await runtime.handler( - new Request('http://localhost/party-registry/readiness', { - headers: { - 'access-control-request-method': 'GET', - origin: 'https://foreign.example.test', + '/party-registry/actions/match-party': { candidate }, + '/party-registry/actions/update-party': { + displayName: 'Updated runtime assembly proof', + expectedRevision: 1, + partyRef, + provenanceMethod: 'DOCUMENT', + provenanceSource: 'runtime-assembly-proof', + validFrom: '2026-09-07T00:00:00.000Z', }, - method: 'OPTIONS', - }), - ); - assert.equal(foreignPreflight.headers.get('access-control-allow-origin'), null); - } finally { - await runtime.dispose(); - } -}); + '/party-registry/actions/update-contact-point': { + change: { preferred: true, type: 'SET_CHANNEL_PREFERRED' }, + contactPointRef, + expectedRevision: 1, + provenance: contactPointProvenance, + }, + '/party-registry/actions/update-party-official-identifier': { + change: { + expectedVerification: 'UNVERIFIED', + type: 'SET_VERIFICATION', + verification: 'VERIFIED', + }, + evidenceRefs: ['document:verified'], + officialIdentifierRef: { + moduleId: 'party.registry', + resourceId: 'a8000000-0000-4000-8000-000000000001', + resourceType: 'party.registry.party-official-identifier', + tenantId: principal.tenantId, + }, + reason: 'Runtime assembly proof', + }, + '/party-registry/actions/update-party-relationship': { + changeReason: 'Runtime assembly proof', + expectedRevision: 1, + provenance: { method: 'TEST', source: 'runtime-assembly-proof' }, + relationshipRef: { + moduleId: 'party.registry', + resourceId: 'aa000000-0000-4000-8000-000000000001', + resourceType: 'party.registry.party-relationship', + tenantId: principal.tenantId, + }, + }, + '/reads/party-match': { candidate }, + } as const; + for (const [index, endpoint] of endpoints.entries()) { + const callsBefore: number = actionCalls + actionCommitCalls + readCalls; + const rawPayloadSchema = endpoint.payload.get('application/json')?.schemas[0]; + // Runtime HTTP descriptors erase codec types. These wire codecs require no services. + const payloadSchema = + rawPayloadSchema === undefined + ? undefined + : Schema.make>(rawPayloadSchema.ast); + const manualPayload = Object.entries(manualPayloads).find( + ([path]) => path === endpoint.path, + )?.[1]; + const payload = + payloadSchema === undefined + ? undefined + : (manualPayload ?? + (yield* Schema.encodeEffect(payloadSchema)( + FastCheck.sample(Schema.toArbitrary(payloadSchema)(FastCheck), { + numRuns: 1, + seed: index + 1, + })[0], + ))); + const request = + payload === undefined + ? new Request(`http://localhost${endpoint.path}`, { + headers: { ...headers, 'idempotency-key': `runtime-assembly-${index + 1}` }, + method: endpoint.method, + }) + : new Request(`http://localhost${endpoint.path}`, { + body: yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(payload), + headers: { ...headers, 'idempotency-key': `runtime-assembly-${index + 1}` }, + method: endpoint.method, + }); + const response = yield* Effect.promise(() => runtime.handler(request)); + if (endpoint.path === '/party-registry/readiness') { + expect(response.status).toBe(200); + expect(actionCalls + actionCommitCalls + readCalls).toBe(callsBefore); + } else { + expect( + response.status, + `${endpoint.method} ${endpoint.path} must execute a supplied runtime substitute`, + ).toBe(500); + expect( + actionCalls + actionCommitCalls + readCalls, + `${endpoint.method} ${endpoint.path} must reach exactly one supplied core runtime`, + ).toBe(callsBefore + 1); + } + } + assert.isOk(redemptionCalls > 0); + assert.isOk(actionCalls > 0); + expect(actionCommitCalls).toBe(1); + assert.isOk(readCalls > 0); + expect(aresCalls).toBe(1); + expect(aresLayerLoads).toBe(1); + expect(partySearchCalls).toBe(1); + expect(counterpartySearchCalls).toBe(1); + expect(searchLayerLoads).toBe(1); + const preflight = yield* Effect.promise(() => + runtime.handler( + new Request('http://localhost/party-registry/readiness', { + headers: { + 'access-control-request-headers': 'Authorization, X-Correlation-Id', + 'access-control-request-method': 'GET', + origin: 'http://localhost:3020', + }, + method: 'OPTIONS', + }), + ), + ); + expect(preflight.status).toBe(204); + expect(preflight.headers.get('access-control-allow-origin')).toBe('http://localhost:3020'); + expect(commaSeparatedHeader(preflight.headers.get('access-control-allow-methods'))).toEqual( + [...partyRegistryCorsAllowedMethods].toSorted(), + ); + expect(commaSeparatedHeader(preflight.headers.get('access-control-allow-headers'))).toEqual( + [...partyRegistryCorsAllowedHeaders].toSorted(), + ); + expect(preflight.headers.get('access-control-max-age')).toBe('600'); + const loopbackPreflight = yield* Effect.promise(() => + runtime.handler( + new Request('http://localhost/party-registry/readiness', { + headers: { + 'access-control-request-method': 'GET', + origin: 'http://127.0.0.1:3020', + }, + method: 'OPTIONS', + }), + ), + ); + expect(loopbackPreflight.headers.get('access-control-allow-origin')).toBe( + 'http://127.0.0.1:3020', + ); + const foreignPreflight = yield* Effect.promise(() => + runtime.handler( + new Request('http://localhost/party-registry/readiness', { + headers: { + 'access-control-request-method': 'GET', + origin: 'https://foreign.example.test', + }, + method: 'OPTIONS', + }), + ), + ); + expect(foreignPreflight.headers.get('access-control-allow-origin')).toBe(null); + }), +); diff --git a/app/verticals/party-registry/tests/unit/ares-application-policy.test.ts b/app/verticals/party-registry/tests/unit/ares-application-policy.test.ts index c3a7b6a15..169f9c526 100644 --- a/app/verticals/party-registry/tests/unit/ares-application-policy.test.ts +++ b/app/verticals/party-registry/tests/unit/ares-application-policy.test.ts @@ -1,5 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; + import { DateTime, Option, Result, Schema } from 'effect'; import { AresAppliedEvidenceSchema, @@ -57,12 +57,13 @@ const derive = (snapshot: AresCanonicalSnapshot | null = canonical, confirmed = userConfirmed: confirmed, }); -test('#246 fixed owner policy enriches only selected missing facts after explicit confirmation', () => { - assert.deepEqual( - derive().factDecisions.map((decision) => decision.route), - ['PARTY_UPDATE', 'IDENTIFIER_ADD', 'CONTACT_POINT_ADD'], - ); - assert.equal(derive(canonical, false).outcome, 'NEEDS_CONFIRMATION'); +it('#246 fixed owner policy enriches only selected missing facts after explicit confirmation', () => { + expect(derive().factDecisions.map((decision) => decision.route)).toEqual([ + 'PARTY_UPDATE', + 'IDENTIFIER_ADD', + 'CONTACT_POINT_ADD', + ]); + expect(derive(canonical, false).outcome).toBe('NEEDS_CONFIRMATION'); const selected = deriveAresEvidenceApplication({ canonical, decidedAt: '2026-09-03T08:01:00.000Z', @@ -70,14 +71,11 @@ test('#246 fixed owner policy enriches only selected missing facts after explici selectedFacts: ['ICO'], userConfirmed: true, }); - assert.deepEqual( - selected.factDecisions.map((decision) => decision.fact), - ['ICO'], - ); - assert.equal(selected.factDecisions[0]?.authorityPolicyKey, 'party_registry.ares_enrichment'); + expect(selected.factDecisions.map((decision) => decision.fact)).toEqual(['ICO']); + expect(selected.factDecisions[0]?.authorityPolicyKey).toBe('party_registry.ares_enrichment'); }); -test('#246 canonical equality is no-change and conflicting facts never authorize overwrite', () => { +it('#246 canonical equality is no-change and conflicting facts never authorize overwrite', () => { const result = derive({ ...canonical, displayName: 'Example', @@ -86,8 +84,8 @@ test('#246 canonical equality is no-change and conflicting facts never authorize { addressLine1: 'Main 10', city: 'Praha', countryCode: 'CZ', postalCode: '12000' }, ], }); - assert.equal(result.outcome, 'NO_CHANGE'); - assert.ok(result.factDecisions.every((decision) => decision.route === null)); + expect(result.outcome).toBe('NO_CHANGE'); + expect(result.factDecisions.every((decision) => decision.route === null)).toBe(true); const conflict = derive({ ...canonical, displayName: 'Different', @@ -95,25 +93,25 @@ test('#246 canonical equality is no-change and conflicting facts never authorize { addressLine1: 'Main 100', city: 'Praha', countryCode: 'CZ', postalCode: '12000' }, ], }); - assert.equal(conflict.factDecisions[0]?.outcome, 'NEEDS_CONFIRMATION'); - assert.equal(conflict.factDecisions[2]?.outcome, 'NEEDS_CONFIRMATION'); + expect(conflict.factDecisions[0]?.outcome).toBe('NEEDS_CONFIRMATION'); + expect(conflict.factDecisions[2]?.outcome).toBe('NEEDS_CONFIRMATION'); }); -test('#246 conflicting official identity blocks all enrichment and no Party remains prefill-only', () => { +it('#246 conflicting official identity blocks all enrichment and no Party remains prefill-only', () => { for (const snapshot of [ { ...canonical, icoValues: ['87654321'] }, { ...canonical, identityAmbiguous: true }, ]) { - assert.ok( + expect( derive(snapshot).factDecisions.every( (decision) => decision.outcome === 'IDENTITY_AMBIGUITY' && decision.route === null, ), - ); + ).toBe(true); } - assert.equal(derive(null).outcome, 'PREFILL_ONLY'); + expect(derive(null).outcome).toBe('PREFILL_ONLY'); }); -test('#246 candidate prefill cannot authorize create against an existing Party', () => { +it('#246 candidate prefill cannot authorize create against an existing Party', () => { const result = deriveAresEvidenceApplication({ canonical: { ...canonical, archived: true, identityAmbiguous: true }, decidedAt: '2026-09-03T08:01:00.000Z', @@ -121,34 +119,36 @@ test('#246 candidate prefill cannot authorize create against an existing Party', selectedFacts: ['PARTY_CANDIDATE'], userConfirmed: true, }); - assert.equal(result.outcome, 'NEEDS_CONFIRMATION'); - assert.equal(result.factDecisions[0]?.route, null); + expect(result.outcome).toBe('NEEDS_CONFIRMATION'); + expect(result.factDecisions[0]?.route).toBe(null); }); -test('#246 structural registered address comparison cannot confuse substrings or unknown fields', () => { +it('#246 structural registered address comparison cannot confuse substrings or unknown fields', () => { const observed = evidence.subject.registeredAddress; - assert.ok(observed); + expect(observed).toBeDefined(); + if (observed === null) { + throw new Error('Expected observed to be defined'); + } const current = { addressLine1: 'Main 10', city: 'Praha', countryCode: 'CZ', postalCode: '12000', }; - assert.equal(aresRegisteredAddressMatches(observed, current), true); - assert.equal( - aresRegisteredAddressMatches(observed, { ...current, addressLine1: 'Main 100' }), + expect(aresRegisteredAddressMatches(observed, current)).toBe(true); + expect(aresRegisteredAddressMatches(observed, { ...current, addressLine1: 'Main 100' })).toBe( false, ); - assert.equal(aresRegisteredAddressMatches(observed, { ...current, region: 'Extra' }), false); - assert.equal(aresRegisteredAddressMatches(observed, { ...current, postalCode: '13000' }), false); + expect(aresRegisteredAddressMatches(observed, { ...current, region: 'Extra' })).toBe(false); + expect(aresRegisteredAddressMatches(observed, { ...current, postalCode: '13000' })).toBe(false); }); -test('#246 stale observations and archived Parties cannot be enriched', () => { - assert.ok( +it('#246 stale observations and archived Parties cannot be enriched', () => { + expect( derive({ ...canonical, archived: true }).factDecisions.every( (decision) => decision.route === null, ), - ); + ).toBe(true); const stale = deriveAresEvidenceApplication({ canonical, decidedAt: '2026-09-03T09:01:00.000Z', @@ -156,31 +156,32 @@ test('#246 stale observations and archived Parties cannot be enriched', () => { selectedFacts: ['ICO'], userConfirmed: true, }); - assert.equal(stale.outcome, 'NEEDS_CONFIRMATION'); + expect(stale.outcome).toBe('NEEDS_CONFIRMATION'); }); -test('#246 durable evidence retains observation and authority metadata without raw provider body', () => { +it('#246 durable evidence retains observation and authority metadata without raw provider body', () => { const application = derive(); const [decision] = application.factDecisions; - assert.ok(decision); + expect(decision).toBeDefined(); + if (decision === undefined) { + throw new Error('Expected decision to be defined'); + } const durable = makeAresAppliedEvidence(application, decision); - assert.equal(DateTime.formatIso(durable.observedAt), evidence.observedAt); - assert.equal(DateTime.formatIso(durable.decidedAt), DateTime.formatIso(application.decidedAt)); - assert.equal( + expect(DateTime.formatIso(durable.observedAt)).toBe(evidence.observedAt); + expect(DateTime.formatIso(durable.decidedAt)).toBe(DateTime.formatIso(application.decidedAt)); + expect( Option.match(durable.providerChangedOn, { onNone: () => null, onSome: DateTime.formatIsoDateUtc, }), - evidence.providerChangedOn, - ); - assert.equal(durable.fact, 'BUSINESS_NAME'); - assert.ok(durable.evidenceRef.startsWith('ares:01234567:')); - assert.equal(Object.hasOwn(durable, 'subject'), false); + ).toBe(evidence.providerChangedOn); + expect(durable.fact).toBe('BUSINESS_NAME'); + expect(durable.evidenceRef.startsWith('ares:01234567:')).toBe(true); + expect(Object.hasOwn(durable, 'subject')).toBe(false); const encoded = Result.getOrThrow(Schema.encodeUnknownResult(AresAppliedEvidenceSchema)(durable)); - assert.equal(encoded.observedAt, evidence.observedAt); - assert.equal(encoded.providerChangedOn, evidence.providerChangedOn); - assert.deepEqual( - Result.getOrThrow(Schema.decodeUnknownResult(AresAppliedEvidenceSchema)(encoded)), + expect(encoded.observedAt).toBe(evidence.observedAt); + expect(encoded.providerChangedOn).toBe(evidence.providerChangedOn); + expect(Result.getOrThrow(Schema.decodeUnknownResult(AresAppliedEvidenceSchema)(encoded))).toEqual( durable, ); }); @@ -188,7 +189,10 @@ test('#246 durable evidence retains observation and authority metadata without r const acceptedEvidence = (fact: 'BUSINESS_NAME' | 'ICO') => { const result = derive(); const decision = result.factDecisions.find((item) => item.fact === fact); - assert.ok(decision); + expect(decision).toBeDefined(); + if (decision === undefined) { + throw new Error('Expected decision to be defined'); + } return Result.getOrThrow( Schema.encodeUnknownResult(AresAppliedEvidenceSchema)( makeAresAppliedEvidence(result, decision), @@ -217,19 +221,22 @@ const decideName = (snapshot = conflictingName, observed = evidence) => userConfirmed: true, }); -test('unchanged provider revision nominates an exact conflicting accepted assertion for review', () => { +it('unchanged provider revision nominates an exact conflicting accepted assertion for review', () => { const result = decideName(); - assert.equal(result.outcome, 'CORRECTION_CANDIDATE'); - assert.equal(result.factDecisions[0]?.route, null); + expect(result.outcome).toBe('CORRECTION_CANDIDATE'); + expect(result.factDecisions[0]?.route).toBe(null); const [review] = deriveAresCorrectionReviewHandoffs(result, conflictingName); - assert.equal(review?.targetAssertionId, conflictingName.factEvidence?.[0]?.assertionId); - assert.equal(review?.observedValue, 'Example'); - assert.equal(review?.evidence.outcome, 'CORRECTION_CANDIDATE'); + expect(review?.targetAssertionId).toBe(conflictingName.factEvidence?.[0]?.assertionId); + expect(review?.observedValue).toBe('Example'); + expect(review?.evidence.outcome).toBe('CORRECTION_CANDIDATE'); }); -test('ordinary change, missing provenance, ambiguous assertions and temporal mismatch are not historical proof', () => { +it('ordinary change, missing provenance, ambiguous assertions and temporal mismatch are not historical proof', () => { const assertion = conflictingName.factEvidence?.[0]; - assert.ok(assertion); + expect(assertion).toBeDefined(); + if (assertion === undefined) { + throw new Error('Expected assertion to be defined'); + } const prior = acceptedEvidence('BUSINESS_NAME'); for (const snapshot of [ { ...conflictingName, factEvidence: [] }, @@ -249,23 +256,24 @@ test('ordinary change, missing provenance, ambiguous assertions and temporal mis ...conflictingName, factEvidence: [{ ...assertion, externalEvidence: { ...prior, providerChangedOn: null } }], }, - { ...conflictingName, factEvidence: [{ ...assertion, validFrom: '2026-09-03T07:59:00.000Z' }] }, + { + ...conflictingName, + factEvidence: [{ ...assertion, validFrom: '2026-09-03T07:59:00.000Z' }], + }, ]) { - assert.equal(decideName(snapshot).outcome, 'NEEDS_CONFIRMATION'); - assert.deepEqual(deriveAresCorrectionReviewHandoffs(decideName(snapshot), snapshot), []); + expect(decideName(snapshot).outcome).toBe('NEEDS_CONFIRMATION'); + expect(deriveAresCorrectionReviewHandoffs(decideName(snapshot), snapshot)).toEqual([]); } - assert.equal( + expect( decideName(conflictingName, { ...evidence, providerChangedOn: '2026-09-02' }).outcome, - 'NEEDS_CONFIRMATION', - ); - assert.equal( + ).toBe('NEEDS_CONFIRMATION'); + expect( decideName(conflictingName, { ...evidence, observedAt: '2026-09-03T07:00:00.000Z' }).outcome, - 'NEEDS_CONFIRMATION', - ); - assert.equal(decideName({ ...conflictingName, archived: true }).outcome, 'NEEDS_CONFIRMATION'); + ).toBe('NEEDS_CONFIRMATION'); + expect(decideName({ ...conflictingName, archived: true }).outcome).toBe('NEEDS_CONFIRMATION'); }); -test('historical ICO suspicion nominates only its assertion and never permits other enrichment', () => { +it('historical ICO suspicion nominates only its assertion and never permits other enrichment', () => { const snapshot: AresCanonicalSnapshot = { ...canonical, factEvidence: [ @@ -280,32 +288,30 @@ test('historical ICO suspicion nominates only its assertion and never permits ot icoValues: ['87654321'], }; const result = derive(snapshot); - assert.equal( - result.factDecisions.find((item) => item.fact === 'ICO')?.outcome, + expect(result.factDecisions.find((item) => item.fact === 'ICO')?.outcome).toBe( 'CORRECTION_CANDIDATE', ); - assert.equal( - result.factDecisions.find((item) => item.fact === 'BUSINESS_NAME')?.outcome, + expect(result.factDecisions.find((item) => item.fact === 'BUSINESS_NAME')?.outcome).toBe( 'IDENTITY_AMBIGUITY', ); - assert.ok(result.factDecisions.every((item) => item.route === null)); - assert.equal(deriveAresCorrectionReviewHandoffs(result, snapshot)[0]?.fact, 'ICO'); - assert.equal(derive({ ...snapshot, identityAmbiguous: true }).outcome, 'IDENTITY_AMBIGUITY'); + expect(result.factDecisions.every((item) => item.route === null)).toBe(true); + expect(deriveAresCorrectionReviewHandoffs(result, snapshot)[0]?.fact).toBe('ICO'); + expect(derive({ ...snapshot, identityAmbiguous: true }).outcome).toBe('IDENTITY_AMBIGUITY'); }); -test('candidate prefill supplies a proposal without declaring subject type or actor evidence', () => { +it('candidate prefill supplies a proposal without declaring subject type or actor evidence', () => { const candidate = prefillPartyCandidateFromAres(evidence); - assert.equal(candidate.partyType, 'UNRESOLVED'); - assert.deepEqual(candidate.subjectEvidence, []); - assert.deepEqual(candidate.officialIdentifiers, [ + expect(candidate.partyType).toBe('UNRESOLVED'); + expect(candidate.subjectEvidence).toEqual([]); + expect(candidate.officialIdentifiers).toEqual([ { identifierType: 'ICO', value: evidence.subject.ico, verification: 'UNVERIFIED' }, ]); - assert.equal(candidate.provenance.externalEvidence, undefined); - assert.equal(candidate.displayName, evidence.subject.businessName); + expect(candidate.provenance.externalEvidence).toBe(undefined); + expect(candidate.displayName).toBe(evidence.subject.businessName); }); -test('six amended outcomes remain reachable and unsupported name or address never applies', () => { - assert.deepEqual( +it('six amended outcomes remain reachable and unsupported name or address never applies', () => { + expect( new Set([ derive(null).outcome, derive().outcome, @@ -314,6 +320,7 @@ test('six amended outcomes remain reachable and unsupported name or address neve decideName().outcome, derive({ ...canonical, identityAmbiguous: true }).outcome, ]), + ).toEqual( new Set([ 'PREFILL_ONLY', 'APPLY_ENRICHMENT', @@ -333,10 +340,10 @@ test('six amended outcomes remain reachable and unsupported name or address neve selectedFacts: ['BUSINESS_NAME', 'REGISTERED_ADDRESS'], userConfirmed: true, }); - assert.equal(result.outcome, 'NO_CHANGE'); + expect(result.outcome).toBe('NO_CHANGE'); }); -test('authoritative ICO enrichment requires an ORGANIZATION and address enrichment requires supported Czech structure', () => { +it('authoritative ICO enrichment requires an ORGANIZATION and address enrichment requires supported Czech structure', () => { for (const partyType of ['PERSON', 'UNRESOLVED'] as const) { const result = deriveAresEvidenceApplication({ canonical: { ...canonical, partyType }, @@ -345,14 +352,16 @@ test('authoritative ICO enrichment requires an ORGANIZATION and address enrichme selectedFacts: ['ICO'], userConfirmed: true, }); - assert.equal(result.outcome, 'NEEDS_CONFIRMATION'); - assert.equal( - result.factDecisions[0]?.reasonCode, + expect(result.outcome).toBe('NEEDS_CONFIRMATION'); + expect(result.factDecisions[0]?.reasonCode).toBe( 'party_type_not_supported_for_authoritative_ico', ); } const address = evidence.subject.registeredAddress; - assert.ok(address); + expect(address).toBeDefined(); + if (address === null) { + throw new Error('Expected address to be defined'); + } for (const registeredAddress of [ { ...address, countryCode: 'DE' }, { ...address, buildingNumber: null, street: null }, @@ -364,6 +373,6 @@ test('authoritative ICO enrichment requires an ORGANIZATION and address enrichme selectedFacts: ['REGISTERED_ADDRESS'], userConfirmed: true, }); - assert.equal(result.outcome, 'NO_CHANGE'); + expect(result.outcome).toBe('NO_CHANGE'); } }); diff --git a/app/verticals/party-registry/tests/unit/ares-evidence-contract.test.ts b/app/verticals/party-registry/tests/unit/ares-evidence-contract.test.ts index 377a151a7..96bdb1d38 100644 --- a/app/verticals/party-registry/tests/unit/ares-evidence-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/ares-evidence-contract.test.ts @@ -1,6 +1,6 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; + +import { Effect, Schema } from 'effect'; import { AresCanonicalRouteSchema, AresEvidenceApplicationSchema, @@ -43,113 +43,88 @@ const evidence = { }, } as const; -test('normalizes only surrounding whitespace and preserves leading zeroes in an exact IČO', () => { - assert.equal(Schema.decodeUnknownSync(AresSubjectLookupIcoSchema)(' 01234567 '), '01234567'); - assert.deepEqual(Schema.decodeUnknownSync(AresLookupRequestSchema)({ ico: ' 01234567 ' }), { - ico: '01234567', - }); +it.effect( + 'normalizes only surrounding whitespace and preserves leading zeroes in an exact IČO', + () => + Effect.gen(function* validateContract1() { + expect(yield* Schema.decodeUnknownEffect(AresSubjectLookupIcoSchema)(' 01234567 ')).toBe( + '01234567', + ); + expect( + yield* Schema.decodeUnknownEffect(AresLookupRequestSchema)({ ico: ' 01234567 ' }), + ).toEqual({ + ico: '01234567', + }); - for (const ico of ['1234567', '123456789', '1234 5678', 'abcdefgh', '']) { - assert.throws(() => Schema.decodeUnknownSync(AresSubjectLookupIcoSchema)(ico)); - } -}); + for (const ico of ['1234567', '123456789', '1234 5678', 'abcdefgh', '']) { + expect(() => Schema.decodeUnknownSync(AresSubjectLookupIcoSchema)(ico)).toThrow(); + } + }), +); -test('returns one bounded evidence envelope and strips unowned provider payload fields', () => { - const decoded = Schema.decodeUnknownSync(AresLookupResponseSchema)({ - ...evidence, - rawResponse: { privateProviderBody: true }, - subject: { - ...evidence.subject, - czNace: ['62010'], - seznamRegistraci: { unsafe: 'unbounded' }, - }, - }); - const encoded = Schema.encodeSync(AresLookupResponseSchema)(decoded); +it.effect('returns one bounded evidence envelope and strips unowned provider payload fields', () => + Effect.gen(function* validateContract2() { + const decoded = yield* Schema.decodeUnknownEffect(AresLookupResponseSchema)({ + ...evidence, + rawResponse: { privateProviderBody: true }, + subject: { + ...evidence.subject, + czNace: ['62010'], + seznamRegistraci: { unsafe: 'unbounded' }, + }, + }); + const encoded = yield* Schema.encodeEffect(AresLookupResponseSchema)(decoded); - assert.deepEqual(encoded, evidence); - assert.equal(Object.hasOwn(decoded, 'rawResponse'), false); - assert.equal(Object.hasOwn(decoded.subject, 'czNace'), false); - assert.deepEqual(Schema.decodeUnknownSync(AresSubjectEvidenceSchema)(encoded), decoded); -}); + expect(encoded).toEqual(evidence); + expect(Object.hasOwn(decoded, 'rawResponse')).toBe(false); + expect(Object.hasOwn(decoded.subject, 'czNace')).toBe(false); + expect(yield* Schema.decodeUnknownEffect(AresSubjectEvidenceSchema)(encoded)).toEqual(decoded); + }), +); -test('keeps observed time separate from provider change time and cache-serving time', () => { - assert.throws(() => - Schema.decodeUnknownSync(AresSubjectEvidenceSchema)({ +it.effect('keeps observed time separate from provider change time and cache-serving time', () => + Effect.gen(function* validateContract3() { + expect(() => + Schema.decodeUnknownSync(AresSubjectEvidenceSchema)({ + ...evidence, + observedAt: '2026-02-30T08:00:00.000Z', + }), + ).toThrow(); + const cached = yield* Schema.decodeUnknownEffect(AresSubjectEvidenceSchema)({ ...evidence, - observedAt: '2026-02-30T08:00:00.000Z', - }), - ); - const cached = Schema.decodeUnknownSync(AresSubjectEvidenceSchema)({ - ...evidence, - cacheAgeSeconds: 120, - servedAt: '2026-09-03T08:02:00.000Z', - }); - const encoded = Schema.encodeSync(AresSubjectEvidenceSchema)(cached); + cacheAgeSeconds: 120, + servedAt: '2026-09-03T08:02:00.000Z', + }); + const encoded = yield* Schema.encodeEffect(AresSubjectEvidenceSchema)(cached); - assert.equal(encoded.observedAt, '2026-09-03T08:00:00.000Z'); - assert.equal(encoded.servedAt, '2026-09-03T08:02:00.000Z'); - assert.equal(encoded.providerChangedOn, '2026-09-01'); - assert.equal(encoded.cacheAgeSeconds, 120); -}); + expect(encoded.observedAt).toBe('2026-09-03T08:00:00.000Z'); + expect(encoded.servedAt).toBe('2026-09-03T08:02:00.000Z'); + expect(encoded.providerChangedOn).toBe('2026-09-01'); + expect(encoded.cacheAgeSeconds).toBe(120); + }), +); -test('allows ARES evidence to route only through standard Party-owned lifecycle Actions', () => { - const routes = [ - 'PARTY_UPDATE', - 'IDENTIFIER_ADD', - 'CONTACT_POINT_ADD', - 'PARTY_CORRECTION', - ] as const; - for (const route of routes) { - assert.equal(Schema.decodeUnknownSync(AresCanonicalRouteSchema)(route), route); - } - for (const forbiddenRoute of [ - 'PARTY_CREATE', - 'ARES_APPLY', - 'PARTY_MERGE', - 'RAW_PROVIDER_OVERWRITE', - ]) { - assert.throws(() => Schema.decodeUnknownSync(AresCanonicalRouteSchema)(forbiddenRoute)); - } +it.effect('allows ARES evidence to route only through standard Party-owned lifecycle Actions', () => + Effect.gen(function* validateContract4() { + const routes = [ + 'PARTY_UPDATE', + 'IDENTIFIER_ADD', + 'CONTACT_POINT_ADD', + 'PARTY_CORRECTION', + ] as const; + for (const route of routes) { + expect(yield* Schema.decodeUnknownEffect(AresCanonicalRouteSchema)(route)).toBe(route); + } + for (const forbiddenRoute of [ + 'PARTY_CREATE', + 'ARES_APPLY', + 'PARTY_MERGE', + 'RAW_PROVIDER_OVERWRITE', + ]) { + expect(() => Schema.decodeUnknownSync(AresCanonicalRouteSchema)(forbiddenRoute)).toThrow(); + } - const application = Schema.decodeUnknownSync(AresEvidenceApplicationSchema)({ - decidedAt: '2026-09-03T08:01:00.000Z', - evidence, - factDecisions: [ - { - authorityPolicyKey: 'party.registry.ares.ico', - authorityPolicyVersion: '1', - fact: 'ICO', - outcome: 'APPLY_ENRICHMENT', - reasonCode: 'missing_supported_ico', - route: 'IDENTIFIER_ADD', - }, - { - authorityPolicyKey: 'party.registry.ares.business-name', - authorityPolicyVersion: '1', - fact: 'BUSINESS_NAME', - outcome: 'APPLY_ENRICHMENT', - reasonCode: 'missing_supported_name', - route: 'PARTY_UPDATE', - }, - { - authorityPolicyKey: 'party.registry.ares.registered-address', - authorityPolicyVersion: '1', - fact: 'REGISTERED_ADDRESS', - outcome: 'APPLY_ENRICHMENT', - reasonCode: 'missing_supported_address', - route: 'CONTACT_POINT_ADD', - }, - ], - outcome: 'APPLY_ENRICHMENT', - userConfirmed: true, - }); - assert.equal(application.factDecisions.length, 3); - assert.equal(application.userConfirmed, true); -}); - -test('rejects unattended enrichment and mutation routes on non-applying outcomes', () => { - assert.throws(() => - Schema.decodeUnknownSync(AresEvidenceApplicationSchema)({ + const application = yield* Schema.decodeUnknownEffect(AresEvidenceApplicationSchema)({ decidedAt: '2026-09-03T08:01:00.000Z', evidence, factDecisions: [ @@ -161,13 +136,71 @@ test('rejects unattended enrichment and mutation routes on non-applying outcomes reasonCode: 'missing_supported_ico', route: 'IDENTIFIER_ADD', }, + { + authorityPolicyKey: 'party.registry.ares.business-name', + authorityPolicyVersion: '1', + fact: 'BUSINESS_NAME', + outcome: 'APPLY_ENRICHMENT', + reasonCode: 'missing_supported_name', + route: 'PARTY_UPDATE', + }, + { + authorityPolicyKey: 'party.registry.ares.registered-address', + authorityPolicyVersion: '1', + fact: 'REGISTERED_ADDRESS', + outcome: 'APPLY_ENRICHMENT', + reasonCode: 'missing_supported_address', + route: 'CONTACT_POINT_ADD', + }, ], outcome: 'APPLY_ENRICHMENT', - userConfirmed: false, - }), - ); - assert.throws(() => - Schema.decodeUnknownSync(AresEvidenceApplicationSchema)({ + userConfirmed: true, + }); + expect(application.factDecisions.length).toBe(3); + expect(application.userConfirmed).toBe(true); + }), +); + +it.effect('rejects unattended enrichment and mutation routes on non-applying outcomes', () => + Effect.gen(function* validateContract5() { + expect(() => + Schema.decodeUnknownSync(AresEvidenceApplicationSchema)({ + decidedAt: '2026-09-03T08:01:00.000Z', + evidence, + factDecisions: [ + { + authorityPolicyKey: 'party.registry.ares.ico', + authorityPolicyVersion: '1', + fact: 'ICO', + outcome: 'APPLY_ENRICHMENT', + reasonCode: 'missing_supported_ico', + route: 'IDENTIFIER_ADD', + }, + ], + outcome: 'APPLY_ENRICHMENT', + userConfirmed: false, + }), + ).toThrow(); + expect(() => + Schema.decodeUnknownSync(AresEvidenceApplicationSchema)({ + decidedAt: '2026-09-03T08:01:00.000Z', + evidence, + factDecisions: [ + { + authorityPolicyKey: 'party.registry.ares.ico', + authorityPolicyVersion: '1', + fact: 'ICO', + outcome: 'APPLY_ENRICHMENT', + reasonCode: 'missing_supported_ico', + route: 'IDENTIFIER_ADD', + }, + ], + outcome: 'NEEDS_CONFIRMATION', + userConfirmed: true, + }), + ).toThrow(); + + const conflict = yield* Schema.decodeUnknownEffect(AresEvidenceApplicationSchema)({ decidedAt: '2026-09-03T08:01:00.000Z', evidence, factDecisions: [ @@ -175,31 +208,14 @@ test('rejects unattended enrichment and mutation routes on non-applying outcomes authorityPolicyKey: 'party.registry.ares.ico', authorityPolicyVersion: '1', fact: 'ICO', - outcome: 'APPLY_ENRICHMENT', - reasonCode: 'missing_supported_ico', - route: 'IDENTIFIER_ADD', + outcome: 'IDENTITY_AMBIGUITY', + reasonCode: 'conflicting_authoritative_ico', + route: null, }, ], - outcome: 'NEEDS_CONFIRMATION', + outcome: 'IDENTITY_AMBIGUITY', userConfirmed: true, - }), - ); - - const conflict = Schema.decodeUnknownSync(AresEvidenceApplicationSchema)({ - decidedAt: '2026-09-03T08:01:00.000Z', - evidence, - factDecisions: [ - { - authorityPolicyKey: 'party.registry.ares.ico', - authorityPolicyVersion: '1', - fact: 'ICO', - outcome: 'IDENTITY_AMBIGUITY', - reasonCode: 'conflicting_authoritative_ico', - route: null, - }, - ], - outcome: 'IDENTITY_AMBIGUITY', - userConfirmed: true, - }); - assert.equal(conflict.factDecisions[0]?.route, null); -}); + }); + expect(conflict.factDecisions[0]?.route).toBe(null); + }), +); diff --git a/app/verticals/party-registry/tests/unit/ares-lookup-read.test.ts b/app/verticals/party-registry/tests/unit/ares-lookup-read.test.ts index eca1edfd1..3672b44f3 100644 --- a/app/verticals/party-registry/tests/unit/ares-lookup-read.test.ts +++ b/app/verticals/party-registry/tests/unit/ares-lookup-read.test.ts @@ -1,9 +1,8 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off nodeBuiltinImport:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; +// @effect-diagnostics nodeBuiltinImport:off -- Read-only architecture assertions use native filesystem promises. expires: 2026-12-31. +import { assert, expect, it } from 'effect-rstest'; import { readFile, readdir } from 'node:fs/promises'; -import test from 'node:test'; -import { Effect, Schema, SchemaAST } from 'effect'; + +import { Effect, Schema, SchemaAST, Predicate, Struct } from 'effect'; import { getReadHandler } from '../../../../packages/core-runtime/src/reads/definition.ts'; import { AresLookupApi, @@ -48,13 +47,13 @@ const evidenceWire = { registeredAddress: null, }, } as const; - const problemTag = (schema: Schema.Top): SchemaAST.LiteralValue => { - assert.ok(SchemaAST.isObjects(schema.ast)); + assert.isOk(SchemaAST.isObjects(schema.ast)); const tag = schema.ast.propertySignatures.find(({ name }) => name === '_tag')?.type; - assert.ok(tag !== undefined && SchemaAST.isLiteral(tag)); + assert.isOk(tag !== undefined && SchemaAST.isLiteral(tag)); return tag.literal; }; + const evidence = Schema.decodeUnknownSync(AresLookupResponseSchema)(evidenceWire); const scope = Object.freeze({ @@ -68,165 +67,190 @@ const scope = Object.freeze({ }); const request = Schema.decodeUnknownSync(AresLookupRequestSchema)({ ico: '48039101' }); -test('declares a tenant-authorized Party evidence Read with optional Legal Entity context', () => { - assert.equal(aresLookupRead.descriptor.accessKind, 'detail'); - assert.equal(aresLookupRead.descriptor.legalEntityScope, 'optional'); - assert.equal(aresLookupRead.descriptor.permissionTarget, 'tenant'); - assert.equal(aresLookupRead.descriptor.owningModuleKey, 'party.registry'); - assert.equal(aresLookupRead.descriptor.readKey, 'party.registry.api.ares-lookup'); - assert.equal(aresLookupRead.descriptor.evidencePolicy.captureMode, 'metadata_only'); +it('declares a tenant-authorized Party evidence Read with optional Legal Entity context', () => { + expect(aresLookupRead.descriptor.accessKind).toBe('detail'); + expect(aresLookupRead.descriptor.legalEntityScope).toBe('optional'); + expect(aresLookupRead.descriptor.permissionTarget).toBe('tenant'); + expect(aresLookupRead.descriptor.owningModuleKey).toBe('party.registry'); + expect(aresLookupRead.descriptor.readKey).toBe('party.registry.api.ares-lookup'); + expect(aresLookupRead.descriptor.evidencePolicy.captureMode).toBe('metadata_only'); }); -test('passes trusted correlation to the private adapter and returns exactly one evidence result', async () => { - const calls: unknown[] = []; - const result = await runEffectTestPromise( - getReadHandler(aresLookupRead)(request, { - readKey: aresLookupRead.descriptor.readKey, - scope, - services: { - lookup: (input) => { - calls.push(input); - return Effect.succeed(evidence); +it.effect( + 'passes trusted correlation to the private adapter and returns exactly one evidence result', + () => + Effect.gen(function* aresLookupReadCase1() { + const calls: unknown[] = []; + const result = yield* getReadHandler(aresLookupRead)(request, { + readKey: aresLookupRead.descriptor.readKey, + scope, + services: { + lookup: (input) => { + calls.push(input); + return Effect.succeed(evidence); + }, }, - }, - }).pipe( - Effect.provideService(AresSubjectService, { - subject: () => Effect.die('The handler test supplies services directly'), - }), - ), - ); - - assert.deepEqual(calls, [{ correlationId: scope.correlationId, ico: '48039101' }]); - assert.deepEqual(result, { evidence: { resultCount: 1 }, result: evidence }); -}); + }).pipe( + Effect.provideService(AresSubjectService, { + subject: () => Effect.die('The handler test supplies services directly'), + }), + ); -test('maps provider failures to the closed governed Read error vocabulary without leaking details', async () => { - const failures = [ - [ - new AresSubjectNotFound({ code: 'ares_subject_not_found', reason: 'private 404 body' }), - 'ReadHandlerNotFound', - ], - [ - new AresSubjectDenied({ code: 'ares_subject_denied', reason: 'private denial' }), - 'ReadHandlerUnavailable', - ], - [ - new AresSubjectThrottled({ code: 'ares_subject_throttled', reason: 'private throttle' }), - 'ReadHandlerUnavailable', - ], - [ - new AresSubjectTimeout({ code: 'ares_subject_timeout', reason: 'private timeout' }), - 'ReadHandlerUnavailable', - ], - [ - new AresSubjectUnavailable({ code: 'ares_subject_unavailable', reason: 'private transport' }), - 'ReadHandlerUnavailable', - ], - [ - new AresSubjectResponseInvalid({ - code: 'ares_subject_response_invalid', - reason: 'private payload', - }), - 'ReadHandlerExecutionError', - ], - ] as const; + expect(calls).toEqual([{ correlationId: scope.correlationId, ico: '48039101' }]); + expect(result).toEqual({ evidence: { resultCount: 1 }, result: evidence }); + }), +); - const errors = await runEffectTestPromise( - Effect.all( - failures.map(([failure, expectedTag]) => - Effect.flip( - getReadHandler(aresLookupRead)(request, { - readKey: aresLookupRead.descriptor.readKey, - scope, - services: { lookup: () => Effect.fail(failure) }, +it.effect( + 'maps provider failures to the closed governed Read error vocabulary without leaking details', + () => + Effect.gen(function* aresLookupReadCase2() { + const failures = [ + [ + new AresSubjectNotFound({ code: 'ares_subject_not_found', reason: 'private 404 body' }), + 'ReadHandlerNotFound', + ], + [ + new AresSubjectDenied({ code: 'ares_subject_denied', reason: 'private denial' }), + 'ReadHandlerUnavailable', + ], + [ + new AresSubjectThrottled({ code: 'ares_subject_throttled', reason: 'private throttle' }), + 'ReadHandlerUnavailable', + ], + [ + new AresSubjectTimeout({ code: 'ares_subject_timeout', reason: 'private timeout' }), + 'ReadHandlerUnavailable', + ], + [ + new AresSubjectUnavailable({ + code: 'ares_subject_unavailable', + reason: 'private transport', }), - ).pipe( - Effect.provideService(AresSubjectService, { - subject: () => Effect.die('The handler test supplies services directly'), + 'ReadHandlerUnavailable', + ], + [ + new AresSubjectResponseInvalid({ + code: 'ares_subject_response_invalid', + reason: 'private payload', }), - Effect.map((error) => ({ error, expectedTag })), + 'ReadHandlerExecutionError', + ], + ] as const; + + const errors = yield* Effect.all( + failures.map(([failure, expectedTag]) => + Effect.flip( + getReadHandler(aresLookupRead)(request, { + readKey: aresLookupRead.descriptor.readKey, + scope, + services: { lookup: () => Effect.fail(failure) }, + }), + ).pipe( + Effect.provideService(AresSubjectService, { + subject: () => Effect.die('The handler test supplies services directly'), + }), + Effect.map((error) => ({ error, expectedTag })), + ), ), - ), - ), - ); - for (const { error, expectedTag } of errors) { - assert.equal(error._tag, expectedTag); - assert.equal(JSON.stringify(error).includes('private'), false); - } -}); + ); + for (const { error, expectedTag } of errors) { + expect(Predicate.isTagged(error, expectedTag)).toBe(true); + expect( + (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(error)).includes( + 'private', + ), + ).toBe(false); + } + }), +); -test('publishes safe status-matched Problem Details and no provider payload schema', () => { - interface ProblemFixture { - readonly _tag: string; - readonly detail: string; - readonly retryable?: true; - readonly status: number; - readonly title: string; - readonly type: string; - } - const schemas = [ - [AresLookupInvalidProblemSchema, 'AresLookupInvalidProblem', 400], - [AresLookupAuthenticationProblemSchema, 'AresLookupAuthenticationProblem', 401], - [AresLookupForbiddenProblemSchema, 'AresLookupForbiddenProblem', 403], - [AresLookupNotFoundProblemSchema, 'AresLookupNotFoundProblem', 404], - [AresLookupPolicyConflictProblemSchema, 'AresLookupPolicyConflictProblem', 409], - [AresLookupPolicyProblemSchema, 'AresLookupPolicyProblem', 422], - [AresLookupUnavailableProblemSchema, 'AresLookupUnavailableProblem', 503], - [AresLookupInternalProblemSchema, 'AresLookupInternalProblem', 500], - ] as const; - for (const [schema, tag, status] of schemas) { - const fixture: ProblemFixture = - status === 503 - ? { - _tag: tag, - detail: 'safe detail', - retryable: true, - status, - title: 'safe title', - type: 'https://ontos.dev/problems/test', - } - : { - _tag: tag, - detail: 'safe detail', - status, - title: 'safe title', - type: 'https://ontos.dev/problems/test', - }; - assert.equal(Schema.decodeUnknownSync(schema)(fixture).status, status); - assert.deepEqual(schema.ast.annotations?.['~httpApiEncoding'], { - _tag: 'Json', - contentType: 'application/problem+json', - }); - } - assert.deepEqual([...AresLookupApi.groups.aresLookup.endpoints.execute.error].map(problemTag), [ - 'AresLookupInvalidProblem', - 'AresLookupAuthenticationProblem', - 'AresLookupForbiddenProblem', - 'AresLookupNotFoundProblem', - 'AresLookupPolicyConflictProblem', - 'AresLookupPolicyProblem', - 'AresLookupUnavailableProblem', - 'AresLookupInternalProblem', - ]); - assert.deepEqual(Schema.decodeUnknownSync(AresLookupRequestSchema)({ ico: '48039101' }), { - ico: '48039101', - }); - assert.deepEqual(Schema.decodeUnknownSync(AresLookupResponseSchema)(evidenceWire), evidence); - assert.equal(AresLookupApi.identifier, 'AresLookupApi'); -}); +it.effect('publishes safe status-matched Problem Details and no provider payload schema', () => + Effect.gen(function* validateContract2() { + interface ProblemFixture { + readonly _tag: string; + readonly detail: string; + readonly retryable?: true; + readonly status: number; + readonly title: string; + readonly type: string; + } + const schemas = [ + [AresLookupInvalidProblemSchema, 'AresLookupInvalidProblem', 400], + [AresLookupAuthenticationProblemSchema, 'AresLookupAuthenticationProblem', 401], + [AresLookupForbiddenProblemSchema, 'AresLookupForbiddenProblem', 403], + [AresLookupNotFoundProblemSchema, 'AresLookupNotFoundProblem', 404], + [AresLookupPolicyConflictProblemSchema, 'AresLookupPolicyConflictProblem', 409], + [AresLookupPolicyProblemSchema, 'AresLookupPolicyProblem', 422], + [AresLookupUnavailableProblemSchema, 'AresLookupUnavailableProblem', 503], + [AresLookupInternalProblemSchema, 'AresLookupInternalProblem', 500], + ] as const; + for (const [schema, tag, status] of schemas) { + const fixture: ProblemFixture = + status === 503 + ? { + _tag: tag, + detail: 'safe detail', + retryable: true, + status, + title: 'safe title', + type: 'https://ontos.dev/problems/test', + } + : { + _tag: tag, + detail: 'safe detail', + status, + title: 'safe title', + type: 'https://ontos.dev/problems/test', + }; + expect((yield* Schema.decodeUnknownEffect(schema)(fixture)).status).toBe(status); + const encoding = schema.ast.annotations?.['~httpApiEncoding']; + expect(Predicate.isTagged(encoding, 'Json')).toBe(true); + if (!Predicate.isTagged(encoding, 'Json')) { + throw new Error('Expected JSON HTTP API encoding'); + } + expect(Struct.omit(encoding, ['_tag'])).toEqual({ + contentType: 'application/problem+json', + }); + } + expect([...AresLookupApi.groups.aresLookup.endpoints.execute.error].map(problemTag)).toEqual([ + 'AresLookupInvalidProblem', + 'AresLookupAuthenticationProblem', + 'AresLookupForbiddenProblem', + 'AresLookupNotFoundProblem', + 'AresLookupPolicyConflictProblem', + 'AresLookupPolicyProblem', + 'AresLookupUnavailableProblem', + 'AresLookupInternalProblem', + ]); + expect(yield* Schema.decodeUnknownEffect(AresLookupRequestSchema)({ ico: '48039101' })).toEqual( + { + ico: '48039101', + }, + ); + expect(yield* Schema.decodeUnknownEffect(AresLookupResponseSchema)(evidenceWire)).toEqual( + evidence, + ); + expect(AresLookupApi.identifier).toBe('AresLookupApi'); + }), +); -test('keeps the ARES integration read-only and exposes no ARES Action', async () => { - const sourceFiles = [ - new URL('../../src/integrations/ares/ares-subject.service.ts', import.meta.url), - new URL('../../src/api/ares-lookup.read.ts', import.meta.url), - ]; - const sources = await Promise.all(sourceFiles.map((sourceFile) => readFile(sourceFile, 'utf-8'))); - for (const source of sources) { - assert.doesNotMatch(source, /from ['"].*(?:\/db\/|\/actions\/|\/services\/party-)/u); - } - const actionFiles = await readdir(new URL('../../src/actions/', import.meta.url)); - assert.equal( - actionFiles.some((name) => name.includes('ares')), - false, - ); -}); +it.effect('keeps the ARES integration read-only and exposes no ARES Action', () => + Effect.gen(function* aresLookupReadCase3() { + const sourceFiles = [ + new URL('../../src/integrations/ares/ares-subject.service.ts', import.meta.url), + new URL('../../src/api/ares-lookup.read.ts', import.meta.url), + ]; + const sources = yield* Effect.all( + sourceFiles.map((sourceFile) => Effect.promise(() => readFile(sourceFile, 'utf-8'))), + { concurrency: 'unbounded' }, + ); + for (const source of sources) { + expect(source).not.toMatch(/from ['"].*(?:\/db\/|\/actions\/|\/services\/party-)/u); + } + const actionFiles = yield* Effect.promise(() => + readdir(new URL('../../src/actions/', import.meta.url)), + ); + expect(actionFiles.some((name) => name.includes('ares'))).toBe(false); + }), +); diff --git a/app/verticals/party-registry/tests/unit/ares-subject.service.test.ts b/app/verticals/party-registry/tests/unit/ares-subject.service.test.ts index fefd41845..4347650ee 100644 --- a/app/verticals/party-registry/tests/unit/ares-subject.service.test.ts +++ b/app/verticals/party-registry/tests/unit/ares-subject.service.test.ts @@ -1,8 +1,7 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off strictEffectProvide:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { DateTime, Effect, Fiber, Layer, Logger, Option } from 'effect'; +// @effect-diagnostics strictEffectProvide:off -- Tests intentionally provide isolated adapter and logger layers. expires: 2026-12-31. +import { expect, it } from 'effect-rstest'; + +import { DateTime, Effect, Fiber, Logger, Option, Predicate, Schema } from 'effect'; import { TestClock } from 'effect/testing'; import { HttpClient, HttpClientError, HttpClientResponse } from 'effect/unstable/http'; import type { HttpClientRequest } from 'effect/unstable/http'; @@ -70,314 +69,358 @@ const capturedLoggerLayer = (entries: string[]) => }), ]); -test('maps a bounded ARES observation and sends an exact credential-free JSON request', async () => { - const requests: { readonly request: HttpClientRequest.HttpClientRequest; readonly url: URL }[] = - []; - const client = clientFrom((request, url) => { - requests.push({ request, url }); - return Effect.succeed( - jsonResponse(request, 200, { - ...rawSubject('01234567'), - czNace: ['must not escape'], - seznamRegistraci: { mustNotEscape: true }, - }), +it.effect('maps a bounded ARES observation and sends an exact credential-free JSON request', () => + Effect.gen(function* aresSubjectServiceCase1() { + const requests: { readonly request: HttpClientRequest.HttpClientRequest; readonly url: URL }[] = + []; + const client = clientFrom((request, url) => { + requests.push({ request, url }); + return Effect.succeed( + jsonResponse(request, 200, { + ...rawSubject('01234567'), + czNace: ['must not escape'], + seznamRegistraci: { mustNotEscape: true }, + }), + ); + }); + const result = yield* lookup(client, ' 01234567 '); + + expect(result.status).toBe('FOUND'); + expect(result.provider).toBe('ares'); + expect(result.queryIco).toBe('01234567'); + expect(result.subject.ico).toBe('01234567'); + expect(Option.getOrUndefined(result.subject.businessName)).toBe('J.E.S., spol. s r.o.'); + const registeredAddress = Option.getOrUndefined(result.subject.registeredAddress); + expect(registeredAddress).toBeDefined(); + if (registeredAddress === undefined) { + throw new Error('Expected registeredAddress to be defined'); + } + expect(Option.getOrUndefined(registeredAddress.municipality)).toBe('Praha'); + expect( + result.providerChangedOn.pipe(Option.map(DateTime.formatIsoDateUtc), Option.getOrUndefined), + ).toBe('2026-09-01'); + expect(Option.getOrUndefined(result.providerRecordRef)).toBe('provider-record-123'); + expect(Object.hasOwn(result, 'czNace')).toBe(false); + expect(Object.hasOwn(result, 'seznamRegistraci')).toBe(false); + expect(requests.length).toBe(1); + expect(requests[0]?.url.href).toBe( + 'https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/ekonomicke-subjekty/01234567', ); - }); - const result = await runEffectTestPromise(lookup(client, ' 01234567 ')); + expect(requests[0]?.request.method).toBe('GET'); + expect(requests[0]?.request.headers['accept']).toBe('application/json'); + expect(requests[0]?.request.headers['authorization']).toBe(undefined); + expect(requests[0]?.request.headers['cookie']).toBe(undefined); + }), +); - assert.equal(result.status, 'FOUND'); - assert.equal(result.provider, 'ares'); - assert.equal(result.queryIco, '01234567'); - assert.equal(result.subject.ico, '01234567'); - assert.equal(Option.getOrUndefined(result.subject.businessName), 'J.E.S., spol. s r.o.'); - const registeredAddress = Option.getOrUndefined(result.subject.registeredAddress); - assert.ok(registeredAddress); - assert.equal(Option.getOrUndefined(registeredAddress.municipality), 'Praha'); - assert.equal( - result.providerChangedOn.pipe(Option.map(DateTime.formatIsoDateUtc), Option.getOrUndefined), - '2026-09-01', - ); - assert.equal(Option.getOrUndefined(result.providerRecordRef), 'provider-record-123'); - assert.equal(Object.hasOwn(result, 'czNace'), false); - assert.equal(Object.hasOwn(result, 'seznamRegistraci'), false); - assert.equal(requests.length, 1); - assert.equal( - requests[0]?.url.href, - 'https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/ekonomicke-subjekty/01234567', - ); - assert.equal(requests[0]?.request.method, 'GET'); - assert.equal(requests[0]?.request.headers['accept'], 'application/json'); - assert.equal(requests[0]?.request.headers['authorization'], undefined); - assert.equal(requests[0]?.request.headers['cookie'], undefined); -}); +it.effect('rejects malformed IČOs before provider I/O', () => + Effect.gen(function* aresSubjectServiceCase2() { + let requests = 0; + const client = clientFrom((request) => { + requests += 1; + return Effect.succeed(jsonResponse(request, 200, rawSubject())); + }); -test('rejects malformed IČOs before provider I/O', async () => { - let requests = 0; - const client = clientFrom((request) => { - requests += 1; - return Effect.succeed(jsonResponse(request, 200, rawSubject())); - }); + yield* Effect.all( + ['1234567', '123456789', '1234 5678', 'abcdefgh', '../48039101'].map((ico) => + Effect.gen(function* aresSubjectServiceCase3() { + const error = yield* Effect.flip(lookup(client, ico)); + expect(Predicate.isTagged(error, 'AresSubjectInvalidIco')).toBe(true); + }), + ), + { concurrency: 'unbounded' }, + ); + expect(requests).toBe(0); + }), +); - await Promise.all( - ['1234567', '123456789', '1234 5678', 'abcdefgh', '../48039101'].map(async (ico) => { - const error = await runEffectTestPromise(Effect.flip(lookup(client, ico))); - assert.equal(error._tag, 'AresSubjectInvalidIco'); - }), - ); - assert.equal(requests, 0); -}); +it.effect( + 'represents absent optional provider facts explicitly without inventing Party facts', + () => + Effect.gen(function* aresSubjectServiceCase4() { + const client = clientFrom((request) => + Effect.succeed( + jsonResponse(request, 200, { + ico: '48039101', + obchodniJmeno: null, + sidlo: {}, + }), + ), + ); + const result = yield* client.pipe(lookup); -test('represents absent optional provider facts explicitly without inventing Party facts', async () => { - const client = clientFrom((request) => - Effect.succeed( - jsonResponse(request, 200, { + expect(result.subject).toEqual({ + businessName: Option.none(), + dic: Option.none(), + dissolvedOn: Option.none(), + establishedOn: Option.none(), ico: '48039101', - obchodniJmeno: null, - sidlo: {}, - }), - ), - ); - const result = await runEffectTestPromise(client.pipe(lookup)); - - assert.deepEqual(result.subject, { - businessName: Option.none(), - dic: Option.none(), - dissolvedOn: Option.none(), - establishedOn: Option.none(), - ico: '48039101', - legalFormCode: Option.none(), - registeredAddress: Option.none(), - }); - assert.ok(Option.isNone(result.providerChangedOn)); - assert.ok(Option.isNone(result.providerRecordRef)); -}); + legalFormCode: Option.none(), + registeredAddress: Option.none(), + }); + expect(Option.isNone(result.providerChangedOn)).toBe(true); + expect(Option.isNone(result.providerRecordRef)).toBe(true); + }), +); -test('keeps not-found, denial, throttling, timeout, and unavailable failures distinct and safe', async () => { - const statusCases = [ - [400, 'AresSubjectResponseInvalid', 1], - [401, 'AresSubjectDenied', 1], - [403, 'AresSubjectDenied', 1], - [404, 'AresSubjectNotFound', 1], - [418, 'AresSubjectResponseInvalid', 1], - [429, 'AresSubjectThrottled', 3], - [500, 'AresSubjectUnavailable', 3], - [502, 'AresSubjectUnavailable', 3], - ] as const; - await Promise.all( - statusCases.map(async ([status, tag, expectedAttempts]) => { - let attempts = 0; - const client = clientFrom((request) => { - attempts += 1; - return Effect.succeed( - jsonResponse(request, status, { - kod: 'PRIVATE_PROVIDER_CODE', - popis: 'private provider detail', +it.effect( + 'keeps not-found, denial, throttling, timeout, and unavailable failures distinct and safe', + () => + Effect.gen(function* aresSubjectServiceCase5() { + const statusCases = [ + [400, 'AresSubjectResponseInvalid', 1], + [401, 'AresSubjectDenied', 1], + [403, 'AresSubjectDenied', 1], + [404, 'AresSubjectNotFound', 1], + [418, 'AresSubjectResponseInvalid', 1], + [429, 'AresSubjectThrottled', 3], + [500, 'AresSubjectUnavailable', 3], + [502, 'AresSubjectUnavailable', 3], + ] as const; + yield* Effect.all( + statusCases.map(([status, tag, expectedAttempts]) => + Effect.gen(function* aresSubjectServiceCase6() { + let attempts = 0; + const client = clientFrom((request) => { + attempts += 1; + return Effect.succeed( + jsonResponse(request, status, { + kod: 'PRIVATE_PROVIDER_CODE', + popis: 'private provider detail', + }), + ); + }); + const program = Effect.flip(lookup(client)); + const fiberProgram = Effect.gen(function* finishRetries() { + const fiber = yield* program.pipe(Effect.forkChild); + yield* Effect.yieldNow; + yield* TestClock.adjust('10 seconds'); + return yield* Fiber.join(fiber); + }); + const error = yield* expectedAttempts === 3 ? fiberProgram : program; + expect(Predicate.isTagged(error, tag)).toBe(true); + expect(attempts).toBe(expectedAttempts); + expect( + (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(error)).includes( + 'PRIVATE_PROVIDER_CODE', + ), + ).toBe(false); + expect( + (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(error)).includes( + 'private provider detail', + ), + ).toBe(false); }), - ); - }); - const program = Effect.flip(lookup(client)); - const fiberProgram = Effect.gen(function* finishRetries() { - const fiber = yield* program.pipe(Effect.forkChild); - yield* Effect.yieldNow; - yield* TestClock.adjust('10 seconds'); - return yield* Fiber.join(fiber); - }).pipe(Effect.provide(TestClock.layer())); - const error = await runEffectTestPromise(expectedAttempts === 3 ? fiberProgram : program); - assert.equal(error._tag, tag); - assert.equal(attempts, expectedAttempts); - assert.equal(JSON.stringify(error).includes('PRIVATE_PROVIDER_CODE'), false); - assert.equal(JSON.stringify(error).includes('private provider detail'), false); + ), + { concurrency: 'unbounded' }, + ); }), - ); -}); +); -test('retries transport faults with bounded backoff without exposing diagnostics', async () => { - const logs: string[] = []; - let attempts = 0; - const client = clientFrom((request) => { - attempts += 1; - return Effect.fail( - new HttpClientError.HttpClientError({ - reason: new HttpClientError.TransportError({ - cause: new Error('private socket diagnostic'), - description: 'transport unavailable', - request, +it.effect('retries transport faults with bounded backoff without exposing diagnostics', () => + Effect.gen(function* aresSubjectServiceCase7() { + const logs: string[] = []; + let attempts = 0; + const client = clientFrom((request) => { + attempts += 1; + return Effect.fail( + new HttpClientError.HttpClientError({ + reason: new HttpClientError.TransportError({ + cause: new Error('private socket diagnostic'), + description: 'transport unavailable', + request, + }), }), - }), - ); - }); - const program = Effect.gen(function* runTransportRetries() { - const fiber = yield* Effect.flip(lookup(client, '48039101', 'corr\nprivate')).pipe( - Effect.forkChild, - ); - yield* Effect.yieldNow; - yield* TestClock.adjust('10 seconds'); - return yield* Fiber.join(fiber); - }).pipe(Effect.provide(Layer.mergeAll(TestClock.layer(), capturedLoggerLayer(logs)))); - const error = await runEffectTestPromise(program); + ); + }); + const program = Effect.gen(function* runTransportRetries() { + const fiber = yield* Effect.flip(lookup(client, '48039101', 'corr\nprivate')).pipe( + Effect.forkChild, + ); + yield* Effect.yieldNow; + yield* TestClock.adjust('10 seconds'); + return yield* Fiber.join(fiber); + }).pipe(Effect.provide(capturedLoggerLayer(logs))); + const error = yield* program; - assert.equal(error._tag, 'AresSubjectUnavailable'); - assert.equal(attempts, 3); - assert.equal(JSON.stringify(error).includes('private socket diagnostic'), false); - assert.match(logs.join('\n'), /private socket diagnostic/u); - assert.match(logs.join('\n'), /corr private/u); -}); + expect(Predicate.isTagged(error, 'AresSubjectUnavailable')).toBe(true); + expect(attempts).toBe(3); + expect( + (yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(error)).includes( + 'private socket diagnostic', + ), + ).toBe(false); + expect(logs.join('\n')).toMatch(/private socket diagnostic/u); + expect(logs.join('\n')).toMatch(/corr private/u); + }), +); -test('times out and aborts each of the three bounded attempts', async () => { - const signals: AbortSignal[] = []; - const client = clientFrom((_request, _url, signal) => { - signals.push(signal); - return Effect.never; - }); - const program = Effect.gen(function* runTimeouts() { - const fiber = yield* Effect.flip(lookup(client)).pipe(Effect.forkChild); - yield* Effect.yieldNow; - yield* TestClock.adjust('30 seconds'); - return yield* Fiber.join(fiber); - }).pipe(Effect.provide(TestClock.layer())); - const error = await runEffectTestPromise(program); +it.effect('times out and aborts each of the three bounded attempts', () => + Effect.gen(function* aresSubjectServiceCase8() { + const signals: AbortSignal[] = []; + const client = clientFrom((_request, _url, signal) => { + signals.push(signal); + return Effect.never; + }); + const program = Effect.gen(function* runTimeouts() { + const fiber = yield* Effect.flip(lookup(client)).pipe(Effect.forkChild); + yield* Effect.yieldNow; + yield* TestClock.adjust('30 seconds'); + return yield* Fiber.join(fiber); + }); + const error = yield* program; - assert.equal(error._tag, 'AresSubjectTimeout'); - assert.equal(signals.length, 3); - assert.equal( - signals.every((signal) => signal.aborted), - true, - ); -}); + expect(Predicate.isTagged(error, 'AresSubjectTimeout')).toBe(true); + expect(signals.length).toBe(3); + expect(signals.every((signal) => signal.aborted)).toBe(true); + }), +); -test('bounds stalled response bodies with the same three-attempt timeout policy', async () => { - let attempts = 0; - const client = clientFrom((request) => { - attempts += 1; - return Effect.succeed( - HttpClientResponse.fromWeb( - request, - new Response(new ReadableStream(), { - headers: { 'content-type': 'application/json' }, - status: 200, - }), - ), - ); - }); - const program = Effect.gen(function* runBodyTimeouts() { - const fiber = yield* Effect.flip(lookup(client).pipe(Effect.timeout('20 seconds'))).pipe( - Effect.forkChild, - ); - yield* Effect.yieldNow; - yield* TestClock.adjust('30 seconds'); - return yield* Fiber.join(fiber); - }).pipe(Effect.provide(TestClock.layer())); - const error = await runEffectTestPromise(program); - assert.equal(error._tag, 'AresSubjectTimeout'); - assert.equal(attempts, 3); -}); +it.effect('bounds stalled response bodies with the same three-attempt timeout policy', () => + Effect.gen(function* aresSubjectServiceCase9() { + let attempts = 0; + const client = clientFrom((request) => { + attempts += 1; + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(new ReadableStream(), { + headers: { 'content-type': 'application/json' }, + status: 200, + }), + ), + ); + }); + const program = Effect.gen(function* runBodyTimeouts() { + const fiber = yield* Effect.flip(lookup(client).pipe(Effect.timeout('20 seconds'))).pipe( + Effect.forkChild, + ); + yield* Effect.yieldNow; + yield* TestClock.adjust('30 seconds'); + return yield* Fiber.join(fiber); + }); + const error = yield* program; + expect(Predicate.isTagged(error, 'AresSubjectTimeout')).toBe(true); + expect(attempts).toBe(3); + }), +); -test('rejects malformed JSON, schema drift, mismatched IČO, and oversized text without partial evidence', async () => { - const responses: readonly (( - request: HttpClientRequest.HttpClientRequest, - ) => HttpClientResponse.HttpClientResponse)[] = [ - (request) => rawResponse(request, 200, '{'), - (request) => jsonResponse(request, 200, { obchodniJmeno: 'missing IČO' }), - (request) => jsonResponse(request, 200, rawSubject('12345678')), - (request) => jsonResponse(request, 200, { ...rawSubject(), obchodniJmeno: 'x'.repeat(501) }), - ]; +it.effect( + 'rejects malformed JSON, schema drift, mismatched IČO, and oversized text without partial evidence', + () => + Effect.gen(function* aresSubjectServiceCase10() { + const responses: readonly (( + request: HttpClientRequest.HttpClientRequest, + ) => HttpClientResponse.HttpClientResponse)[] = [ + (request) => rawResponse(request, 200, '{'), + (request) => jsonResponse(request, 200, { obchodniJmeno: 'missing IČO' }), + (request) => jsonResponse(request, 200, rawSubject('12345678')), + (request) => + jsonResponse(request, 200, { ...rawSubject(), obchodniJmeno: 'x'.repeat(501) }), + ]; - await Promise.all( - responses.map(async (response) => { - let requests = 0; - const client = clientFrom((request) => { - requests += 1; - return Effect.succeed(response(request)); - }); - const error = await runEffectTestPromise(Effect.flip(lookup(client))); - assert.equal(error._tag, 'AresSubjectResponseInvalid'); - assert.equal(requests, 1); + yield* Effect.all( + responses.map((response) => + Effect.gen(function* aresSubjectServiceCase11() { + let requests = 0; + const client = clientFrom((request) => { + requests += 1; + return Effect.succeed(response(request)); + }); + const error = yield* Effect.flip(lookup(client)); + expect(Predicate.isTagged(error, 'AresSubjectResponseInvalid')).toBe(true); + expect(requests).toBe(1); + }), + ), + { concurrency: 'unbounded' }, + ); }), - ); -}); +); -test('coalesces identical requests and exposes cache age without changing observedAt', async () => { - let requests = 0; - const client = clientFrom((request) => { - requests += 1; - return Effect.sleep('1 second').pipe( - Effect.andThen(Effect.succeed(jsonResponse(request, 200, rawSubject()))), +it.effect('coalesces identical requests and exposes cache age without changing observedAt', () => + Effect.gen(function* aresSubjectServiceCase12() { + let requests = 0; + const client = clientFrom((request) => { + requests += 1; + return Effect.sleep('1 second').pipe( + Effect.andThen(Effect.succeed(jsonResponse(request, 200, rawSubject()))), + ); + }); + const program = Effect.gen(function* exerciseCache() { + const service = yield* AresSubjectService; + const concurrent = yield* Effect.all( + [ + service.subject({ correlationId: 'first', ico: '48039101' }), + service.subject({ correlationId: 'second', ico: '48039101' }), + ], + { concurrency: 'unbounded' }, + ).pipe(Effect.forkChild); + yield* Effect.yieldNow; + expect(requests).toBe(1); + yield* TestClock.adjust('1 second'); + const initial = yield* Fiber.join(concurrent); + yield* TestClock.adjust('2 minutes'); + const cached = yield* service.subject({ correlationId: 'cached', ico: '48039101' }); + return { cached, initial }; + }).pipe( + Effect.provide(AresSubjectServiceLive), + Effect.provideService(HttpClient.HttpClient, client), ); - }); - const program = Effect.gen(function* exerciseCache() { - const service = yield* AresSubjectService; - const concurrent = yield* Effect.all( - [ - service.subject({ correlationId: 'first', ico: '48039101' }), - service.subject({ correlationId: 'second', ico: '48039101' }), - ], - { concurrency: 'unbounded' }, - ).pipe(Effect.forkChild); - yield* Effect.yieldNow; - assert.equal(requests, 1); - yield* TestClock.adjust('1 second'); - const initial = yield* Fiber.join(concurrent); - yield* TestClock.adjust('2 minutes'); - const cached = yield* service.subject({ correlationId: 'cached', ico: '48039101' }); - return { cached, initial }; - }).pipe( - Effect.provide(AresSubjectServiceLive), - Effect.provideService(HttpClient.HttpClient, client), - Effect.provide(TestClock.layer()), - ); - const result = await runEffectTestPromise(program); + const result = yield* program; - assert.equal(requests, 1); - assert.equal(result.initial[0]?.observedAt, result.initial[1]?.observedAt); - assert.equal(result.cached.observedAt, result.initial[0]?.observedAt); - assert.equal(result.cached.cacheAgeSeconds, 120); - assert.notEqual(result.cached.servedAt, result.cached.observedAt); -}); + expect(requests).toBe(1); + expect(result.initial[0]?.observedAt).toBe(result.initial[1]?.observedAt); + expect(result.cached.observedAt).toBe(result.initial[0]?.observedAt); + expect(result.cached.cacheAgeSeconds).toBe(120); + expect(result.cached.servedAt).not.toBe(result.cached.observedAt); + }), +); -test('bounds distinct upstream lookups to four concurrent requests', async () => { - let active = 0; - let maximumActive = 0; - let requests = 0; - const client = clientFrom((request, url) => - Effect.sync(() => { - active += 1; - requests += 1; - maximumActive = Math.max(maximumActive, active); - return url.pathname.slice(-8); - }).pipe( - Effect.flatMap((ico) => Effect.sleep('1 second').pipe(Effect.as(ico))), - Effect.map((ico) => jsonResponse(request, 200, rawSubject(ico))), - Effect.ensuring( - Effect.sync(() => { - active -= 1; - }), - ), - ), - ); - const program = Effect.gen(function* exerciseConcurrencyLimit() { - const service = yield* AresSubjectService; - const fiber = yield* Effect.all( - Array.from({ length: 8 }, (_, index) => - service.subject({ - correlationId: `concurrency-${index}`, - ico: String(index + 1).padStart(8, '0'), - }), +it.effect('bounds distinct upstream lookups to four concurrent requests', () => + Effect.gen(function* aresSubjectServiceCase13() { + let active = 0; + let maximumActive = 0; + let requests = 0; + const client = clientFrom((request, url) => + Effect.sync(() => { + active += 1; + requests += 1; + maximumActive = Math.max(maximumActive, active); + return url.pathname.slice(-8); + }).pipe( + Effect.flatMap((ico) => Effect.sleep('1 second').pipe(Effect.as(ico))), + Effect.map((ico) => jsonResponse(request, 200, rawSubject(ico))), + Effect.ensuring( + Effect.sync(() => { + active -= 1; + }), + ), ), - { concurrency: 'unbounded' }, - ).pipe(Effect.forkChild); - yield* Effect.yieldNow; - assert.equal(active, 4); - yield* TestClock.adjust('2 seconds'); - return yield* Fiber.join(fiber); - }).pipe( - Effect.provide(AresSubjectServiceLive), - Effect.provideService(HttpClient.HttpClient, client), - Effect.provide(TestClock.layer()), - ); - const results = await runEffectTestPromise(program); + ); + const program = Effect.gen(function* exerciseConcurrencyLimit() { + const service = yield* AresSubjectService; + const fiber = yield* Effect.all( + Array.from({ length: 8 }, (_, index) => + service.subject({ + correlationId: `concurrency-${index}`, + ico: String(index + 1).padStart(8, '0'), + }), + ), + { concurrency: 'unbounded' }, + ).pipe(Effect.forkChild); + yield* Effect.yieldNow; + expect(active).toBe(4); + yield* TestClock.adjust('2 seconds'); + return yield* Fiber.join(fiber); + }).pipe( + Effect.provide(AresSubjectServiceLive), + Effect.provideService(HttpClient.HttpClient, client), + ); + const results = yield* program; - assert.equal(results.length, 8); - assert.equal(requests, 8); - assert.equal(maximumActive, 4); - assert.equal(active, 0); -}); + expect(results.length).toBe(8); + expect(requests).toBe(8); + expect(maximumActive).toBe(4); + expect(active).toBe(0); + }), +); diff --git a/app/verticals/party-registry/tests/unit/attach-engagement-handler.test.ts b/app/verticals/party-registry/tests/unit/attach-engagement-handler.test.ts index d741ef458..7c7b5b255 100644 --- a/app/verticals/party-registry/tests/unit/attach-engagement-handler.test.ts +++ b/app/verticals/party-registry/tests/unit/attach-engagement-handler.test.ts @@ -1,12 +1,11 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; +import { assert, it } from 'effect-rstest'; import { Effect, Result } from 'effect'; import { EngagementProfileConflict } from '../../shared/domain/engagement-profile.ts'; import { handleAttachEngagement } from '../../src/actions/attach-engagement-handler.ts'; -test('engagement creation runs only after successful validation and preserves the result', () => - runEffectTestPromise( +it.effect( + 'engagement creation runs only after successful validation and preserves the result', + () => Effect.gen(function* createsAfterValidation() { const calls: string[] = []; const payload = { partyId: 'party' }; @@ -29,30 +28,29 @@ test('engagement creation runs only after successful validation and preserves th assert.equal(result, profile); assert.deepEqual(calls, ['validate', 'create']); }), - )); +); -test('engagement validation failure retains its typed error and prevents persistence', () => - runEffectTestPromise( - Effect.gen(function* rejectsBeforeCreation() { - const conflict = new EngagementProfileConflict({ - code: 'contacts_party_type_mismatch', - reason: 'The Party has the wrong engagement type', - }); - let created = false; - const result = yield* handleAttachEngagement( - {}, - { - services: { - validate: () => Effect.fail(conflict), - create: () => - Effect.sync(() => { - created = true; - }), - }, +it.effect('engagement validation failure retains its typed error and prevents persistence', () => + Effect.gen(function* rejectsBeforeCreation() { + const conflict = new EngagementProfileConflict({ + code: 'contacts_party_type_mismatch', + reason: 'The Party has the wrong engagement type', + }); + let created = false; + const result = yield* handleAttachEngagement( + {}, + { + services: { + validate: () => Effect.fail(conflict), + create: () => + Effect.sync(() => { + created = true; + }), }, - ).pipe(Effect.result); - assert.equal(created, false); - assert.ok(Result.isFailure(result)); - assert.equal(result.failure, conflict); - }), - )); + }, + ).pipe(Effect.result); + assert.equal(created, false); + assert.ok(Result.isFailure(result)); + assert.equal(result.failure, conflict); + }), +); diff --git a/app/verticals/party-registry/tests/unit/audit-evidence-contract.test.ts b/app/verticals/party-registry/tests/unit/audit-evidence-contract.test.ts index 81261e8bf..355f2ab9e 100644 --- a/app/verticals/party-registry/tests/unit/audit-evidence-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/audit-evidence-contract.test.ts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Schema } from 'effect'; import { PartySubjectEvidenceSchema, @@ -11,7 +10,7 @@ import { makeDuplicateCandidateCaseRef } from '../../shared/resources/duplicate- const tenant = '11111111-1111-4111-8111-111111111111'; const id = '22222222-2222-4222-8222-222222222222'; -test('typed subject evidence accepts arbitrary reference spelling, rejects unsupported authority', () => { +it('typed subject evidence accepts arbitrary reference spelling, rejects unsupported authority', () => { const evidence = { basis: 'DIRECT_INTERACTION', evidenceRef: 'meeting/42', @@ -20,18 +19,18 @@ test('typed subject evidence accepts arbitrary reference spelling, rejects unsup statement: 'Met the human who submitted this request', subjectKey: 'request-subject', }; - assert.deepEqual(Schema.decodeUnknownSync(PartySubjectEvidenceSchema)(evidence), evidence); - assert.throws(() => + expect(Schema.decodeUnknownSync(PartySubjectEvidenceSchema)(evidence)).toEqual(evidence); + expect(() => Schema.decodeUnknownSync(PartySubjectEvidenceSchema)({ ...evidence, kind: 'AUTHORITATIVE_REGISTRY', }), - ); - assert.throws(() => + ).toThrow(); + expect(() => Schema.decodeUnknownSync(PartySubjectEvidenceSchema)({ ...evidence, statement: '' }), - ); + ).toThrow(); }); -test('Create recovery distinguishes matching outcome and enforces reference invariants', () => { +it('Create recovery distinguishes matching outcome and enforces reference invariants', () => { const record = { caseRef: null, committedCreateOutcome: 'MATCHED_EXISTING', @@ -44,16 +43,16 @@ test('Create recovery distinguishes matching outcome and enforces reference inva partyRef: makePartyRef(tenant, id), }; const decode = Schema.decodeUnknownSync(PartyMatchDecisionRecordSchema); - assert.equal(decode(record).committedCreateOutcome, 'MATCHED_EXISTING'); - assert.equal(decode(record).decidedAt, '2026-09-04T00:00:00.000Z'); - assert.throws(() => decode({ ...record, decidedAt: 'September 4, 2026' })); - assert.throws(() => decode({ ...record, committedCreateOutcome: 'MATCHED' })); - assert.throws(() => decode({ ...record, operation: 'MATCH' })); - assert.throws(() => decode({ ...record, caseRef: makeDuplicateCandidateCaseRef(tenant, id) })); - assert.throws(() => + expect(decode(record).committedCreateOutcome).toBe('MATCHED_EXISTING'); + expect(decode(record).decidedAt).toBe('2026-09-04T00:00:00.000Z'); + expect(() => decode({ ...record, decidedAt: 'September 4, 2026' })).toThrow(); + expect(() => decode({ ...record, committedCreateOutcome: 'MATCHED' })).toThrow(); + expect(() => decode({ ...record, operation: 'MATCH' })).toThrow(); + expect(() => decode({ ...record, caseRef: makeDuplicateCandidateCaseRef(tenant, id) })).toThrow(); + expect(() => decode({ ...record, committedCreateOutcome: null, outcome: 'NO_MATCH', partyRef: null }), - ); - assert.equal( + ).toThrow(); + expect( decode({ ...record, committedCreateOutcome: null, @@ -61,11 +60,9 @@ test('Create recovery distinguishes matching outcome and enforces reference inva outcome: 'NO_MATCH', partyRef: null, }).outcome, - 'NO_MATCH', - ); + ).toBe('NO_MATCH'); }); - -test('matching decision JSON keeps nullable and optional wire fields compatible', () => { +it('matching decision JSON keeps nullable and optional wire fields compatible', () => { const record = { caseRef: null, committedCreateOutcome: null, @@ -82,8 +79,7 @@ test('matching decision JSON keeps nullable and optional wire fields compatible' const encoded = Schema.encodeUnknownSync(Schema.toCodecJson(PartyMatchDecisionRecordSchema))( decoded, ); - assert.deepEqual(encoded, record); - + expect(encoded).toEqual(record); const omitted = { caseRef: record.caseRef, decidedAt: record.decidedAt, @@ -100,6 +96,6 @@ test('matching decision JSON keeps nullable and optional wire fields compatible' const omittedEncodedObject = Schema.decodeUnknownSync(Schema.Record(Schema.String, Schema.Json))( omittedEncoded, ); - assert.equal('committedCreateOutcome' in omittedEncodedObject, false); - assert.equal('evidenceEvaluation' in omittedEncodedObject, false); + expect('committedCreateOutcome' in omittedEncodedObject).toBe(false); + expect('evidenceEvaluation' in omittedEncodedObject).toBe(false); }); diff --git a/app/verticals/party-registry/tests/unit/catalog-contract.test.ts b/app/verticals/party-registry/tests/unit/catalog-contract.test.ts index 7a9d7d1ad..cc1245195 100644 --- a/app/verticals/party-registry/tests/unit/catalog-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/catalog-contract.test.ts @@ -1,22 +1,21 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { assert, expect, it } from 'effect-rstest'; import { comparePartyCatalog, expectedPartyTableCatalog } from '../../src/db/catalog.ts'; -test('reports exact Party Registry catalog differences', () => { - assert.equal(expectedPartyTableCatalog.length, 17); - assert.equal(expectedPartyTableCatalog[0], 'party.counterparties'); - assert.equal(expectedPartyTableCatalog.at(-1), 'party.party_relationships'); - assert.deepEqual(comparePartyCatalog(expectedPartyTableCatalog.slice(1)), { +it('reports exact Party Registry catalog differences', () => { + expect(expectedPartyTableCatalog.length).toBe(17); + expect(expectedPartyTableCatalog[0]).toBe('party.counterparties'); + expect(expectedPartyTableCatalog.at(-1)).toBe('party.party_relationships'); + expect(comparePartyCatalog(expectedPartyTableCatalog.slice(1))).toEqual({ missing: ['party.counterparties'], unexpected: [], }); - assert.deepEqual(comparePartyCatalog([...expectedPartyTableCatalog, 'party.unexpected']), { + expect(comparePartyCatalog([...expectedPartyTableCatalog, 'party.unexpected'])).toEqual({ missing: [], unexpected: ['party.unexpected'], }); }); -test('compares catalog sets without input order, duplicates, or previous results affecting differences', () => { +it('compares catalog sets without input order, duplicates, or previous results affecting differences', () => { const actual = ['party.z_extra', 'party.a_extra', 'party.z_extra']; const difference = comparePartyCatalog(actual); assert.deepEqual(difference, { diff --git a/app/verticals/party-registry/tests/unit/contact-point-contract.test.ts b/app/verticals/party-registry/tests/unit/contact-point-contract.test.ts index d626b143f..33231800c 100644 --- a/app/verticals/party-registry/tests/unit/contact-point-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/contact-point-contract.test.ts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Schema } from 'effect'; import { AddressContactPointValueSchema, @@ -35,72 +34,67 @@ const partyRef = { resourceType: 'party.registry.party', tenantId: '20000000-0000-4000-8000-000000000001', } as const; - const provenance = { authoritative: false, method: 'MANUAL_CONFIRMATION', source: 'USER_ASSERTION', } as const; - -test('normalizes EMAIL without provider-specific identity heuristics', () => { - assert.deepEqual(normalizeEmail(' Qa.Test+case@EXAMPLE.COM '), { +it('normalizes EMAIL without provider-specific identity heuristics', () => { + expect(normalizeEmail(' Qa.Test+case@EXAMPLE.COM ')).toEqual({ displayValue: 'Qa.Test+case@EXAMPLE.COM', lookupValue: 'Qa.Test+case@example.com', }); - assert.notEqual( - normalizeEmail('qa.test+one@example.com').lookupValue, + expect(normalizeEmail('qa.test+one@example.com').lookupValue).not.toBe( normalizeEmail('qatest+two@example.com').lookupValue, ); - assert.throws(() => + expect(() => Schema.decodeUnknownSync(EmailContactPointInputSchema)({ preferred: false, type: 'EMAIL', value: 'not-an-email', }), - ); + ).toThrow(); }); - -test('normalizes PHONE only with explicit international or country context and preserves extension', () => { - assert.deepEqual(normalizePhone('+420 (777) 123-456', undefined, '42'), { +it('normalizes PHONE only with explicit international or country context and preserves extension', () => { + expect(normalizePhone('+420 (777) 123-456', undefined, '42')).toEqual({ countryCode: 'CZ', displayValue: '+420 (777) 123-456', extension: '42', lookupValue: '+420777123456', }); - assert.deepEqual(normalizePhone('777 123 456', 'CZ'), { + expect(normalizePhone('777 123 456', 'CZ')).toEqual({ countryCode: 'CZ', displayValue: '777 123 456', extension: null, lookupValue: '+420777123456', }); - assert.throws(() => normalizePhone('777 123 456')); - assert.throws(() => + expect(() => normalizePhone('777 123 456')).toThrow(); + expect(() => Schema.decodeUnknownSync(PhoneContactPointInputSchema)({ countryCode: 'CZ', preferred: false, type: 'PHONE', value: '+0123456789', }), - ); - assert.throws(() => + ).toThrow(); + expect(() => Schema.decodeUnknownSync(PhoneContactPointInputSchema)({ extension: '1234567890123', preferred: false, type: 'PHONE', value: '+420777123456', }), - ); - assert.doesNotThrow(() => normalizePhone('+420777123456', 'CZ', '123456789012')); - assert.throws(() => + ).toThrow(); + expect(() => normalizePhone('+420777123456', 'CZ', '123456789012')).not.toThrow(); + expect(() => Schema.decodeUnknownSync(PhoneContactPointInputSchema)({ preferred: false, type: 'PHONE', value: '777 123 456', }), - ); + ).toThrow(); }); - -test('keeps ADDRESS structured, multi-purpose, and preferred independently per purpose', () => { +it('keeps ADDRESS structured, multi-purpose, and preferred independently per purpose', () => { const decoded = Schema.decodeUnknownSync(AddressContactPointInputSchema)({ address: { addressLine1: ' Na Prikope 1 ', @@ -118,7 +112,7 @@ test('keeps ADDRESS structured, multi-purpose, and preferred independently per p ], type: 'ADDRESS', }); - assert.deepEqual(normalizeAddress(decoded.address), { + expect(normalizeAddress(decoded.address)).toEqual({ addressLine1: 'Na Prikope 1', addressLine2: null, city: 'Praha', @@ -126,24 +120,20 @@ test('keeps ADDRESS structured, multi-purpose, and preferred independently per p postalCode: '110 00', region: null, }); - assert.deepEqual( - decoded.purposes.map(({ preferred, purpose }) => ({ preferred, purpose })), - [ - { preferred: true, purpose: 'REGISTERED' }, - { preferred: false, purpose: 'CORRESPONDENCE' }, - ], - ); - assert.throws(() => + expect(decoded.purposes.map(({ preferred, purpose }) => ({ preferred, purpose }))).toEqual([ + { preferred: true, purpose: 'REGISTERED' }, + { preferred: false, purpose: 'CORRESPONDENCE' }, + ]); + expect(() => Schema.decodeUnknownSync(AddressContactPointInputSchema)({ address: { addressLine1: 'One', city: 'Prague', countryCode: 'CZ' }, purposes: [{ preferred: false, purpose: 'OTHER' }], type: 'ADDRESS', }), - ); + ).toThrow(); }); - -test('requires authoritative, registry-scoped evidence only for REGISTERED', () => { - assert.throws(() => +it('requires authoritative, registry-scoped evidence only for REGISTERED', () => { + expect(() => assertAddressPurposeRules( [ { @@ -154,8 +144,8 @@ test('requires authoritative, registry-scoped evidence only for REGISTERED', () ], provenance, ), - ); - assert.doesNotThrow(() => + ).toThrow(); + expect(() => assertAddressPurposeRules( [ { @@ -167,10 +157,9 @@ test('requires authoritative, registry-scoped evidence only for REGISTERED', () ], { ...provenance, authoritative: true, evidenceReference: 'evidence:ares:subject:1' }, ), - ); + ).not.toThrow(); }); - -test('keeps the contact-point catalog closed to EMAIL, PHONE, and ADDRESS', () => { +it('keeps the contact-point catalog closed to EMAIL, PHONE, and ADDRESS', () => { for (const input of [ { preferred: false, type: 'EMAIL', value: 'a@example.test' }, { countryCode: 'CZ', preferred: false, type: 'PHONE', value: '777123456' }, @@ -180,18 +169,17 @@ test('keeps the contact-point catalog closed to EMAIL, PHONE, and ADDRESS', () = type: 'ADDRESS', }, ]) { - assert.doesNotThrow(() => Schema.decodeUnknownSync(ContactPointInputSchema)(input)); + expect(() => Schema.decodeUnknownSync(ContactPointInputSchema)(input)).not.toThrow(); } - assert.throws(() => Schema.decodeUnknownSync(ContactPointInputSchema)({ type: 'OTHER' })); + expect(() => Schema.decodeUnknownSync(ContactPointInputSchema)({ type: 'OTHER' })).toThrow(); }); - -test('declares tenant-authorized idempotent Actions and prevents value overwrite through UPDATE', () => { +it('declares tenant-authorized idempotent Actions and prevents value overwrite through UPDATE', () => { for (const action of [addContactPointAction, updateContactPointAction, endContactPointAction]) { - assert.equal(action.descriptor.legalEntityScope, 'optional'); - assert.equal(action.descriptor.idempotency, 'required'); - assert.notEqual(action.descriptor.tenantPermission, undefined); + expect(action.descriptor.legalEntityScope).toBe('optional'); + expect(action.descriptor.idempotency).toBe('required'); + expect(action.descriptor.tenantPermission).not.toBe(undefined); } - assert.doesNotThrow(() => + expect(() => Schema.decodeUnknownSync(AddContactPointPayloadSchema)({ contactPoint: { preferred: true, type: 'EMAIL', value: 'user@example.test' }, partyRef, @@ -200,8 +188,8 @@ test('declares tenant-authorized idempotent Actions and prevents value overwrite validFrom: '2026-09-01T00:00:00.000Z', verification: { state: 'UNVERIFIED' }, }), - ); - assert.throws(() => + ).not.toThrow(); + expect(() => Schema.decodeUnknownSync(UpdateContactPointPayloadSchema, { onExcessProperty: 'error' })({ change: { preferred: true, type: 'SET_CHANNEL_PREFERRED' }, contactPointRef: { @@ -212,38 +200,34 @@ test('declares tenant-authorized idempotent Actions and prevents value overwrite provenance, value: 'replacement@example.test', }), - ); + ).toThrow(); }); - -test('governs contact reads with tenant Party authority even when Legal Entity context is optional', () => { +it('governs contact reads with tenant Party authority even when Legal Entity context is optional', () => { for (const read of [partyContactPointsRead, partyContactPointDetailRead]) { - assert.equal(read.descriptor.legalEntityScope, 'optional'); - assert.equal(read.descriptor.permissionTarget, 'tenant'); + expect(read.descriptor.legalEntityScope).toBe('optional'); + expect(read.descriptor.permissionTarget).toBe('tenant'); } }); - -test('publishes stable references instead of mutable contact data', () => { +it('publishes stable references instead of mutable contact data', () => { const contactPointRef = { ...partyRef, resourceType: 'party.registry.party-contact-point' }; - assert.deepEqual( + expect( Schema.decodeUnknownSync(ContactPointAddedOutboxPayloadSchema)({ contactPointRef, partyRef }), - { contactPointRef, partyRef }, - ); - assert.throws(() => + ).toEqual({ contactPointRef, partyRef }); + expect(() => Schema.decodeUnknownSync(ContactPointAddedOutboxPayloadSchema, { onExcessProperty: 'error' })({ contactPointRef, displayValue: 'private@example.test', partyRef, }), - ); + ).toThrow(); }); - -test('models removal as a reasoned temporal end of a whole contact or one ADDRESS purpose', () => { +it('models removal as a reasoned temporal end of a whole contact or one ADDRESS purpose', () => { const contactPointRef = { ...partyRef, resourceType: 'party.registry.party-contact-point' }; for (const target of [ { type: 'WHOLE_CONTACT_POINT' }, { target: { purpose: 'DELIVERY' }, type: 'ADDRESS_PURPOSE' }, ] as const) { - assert.doesNotThrow(() => + expect(() => Schema.decodeUnknownSync(EndContactPointPayloadSchema)({ contactPointRef, effectiveEnd: '2026-09-03T10:00:00.000Z', @@ -251,9 +235,9 @@ test('models removal as a reasoned temporal end of a whole contact or one ADDRES reason: 'Party confirmed that this contact is no longer used', target, }), - ); + ).not.toThrow(); } - assert.throws(() => + expect(() => Schema.decodeUnknownSync(EndContactPointPayloadSchema)({ contactPointRef, effectiveEnd: '2026-09-03T10:00:00.000Z', @@ -261,9 +245,8 @@ test('models removal as a reasoned temporal end of a whole contact or one ADDRES reason: '', target: { type: 'WHOLE_CONTACT_POINT' }, }), - ); - - assert.doesNotThrow(() => + ).toThrow(); + expect(() => Schema.decodeUnknownSync(UpdateContactPointPayloadSchema)({ change: { effectiveEnd: '2026-09-03T10:00:00.000Z', @@ -275,20 +258,19 @@ test('models removal as a reasoned temporal end of a whole contact or one ADDRES expectedRevision: 1, provenance, }), - ); - assert.throws(() => + ).not.toThrow(); + expect(() => Schema.decodeUnknownSync(UpdateContactPointPayloadSchema)({ change: { target: { purpose: 'DELIVERY' }, type: 'END_ADDRESS_PURPOSE' }, contactPointRef, expectedRevision: 1, provenance, }), - ); + ).toThrow(); }); - -test('models an originally wrong Contact Point as an explicit correction with optional validated replacement', () => { +it('models an originally wrong Contact Point as an explicit correction with optional validated replacement', () => { const contactPointRef = { ...partyRef, resourceType: 'party.registry.party-contact-point' }; - assert.doesNotThrow(() => + expect(() => Schema.decodeUnknownSync(UpdateContactPointPayloadSchema)({ change: { evidenceReferences: ['evidence:customer-confirmation:42'], @@ -310,8 +292,8 @@ test('models an originally wrong Contact Point as an explicit correction with op expectedRevision: 3, provenance: { ...provenance, evidenceReference: 'evidence:customer-confirmation:42' }, }), - ); - assert.throws(() => + ).not.toThrow(); + expect(() => Schema.decodeUnknownSync(UpdateContactPointPayloadSchema)({ change: { evidenceReferences: [], @@ -322,10 +304,9 @@ test('models an originally wrong Contact Point as an explicit correction with op expectedRevision: 3, provenance, }), - ); + ).toThrow(); }); - -test('projects independently auditable whole-contact and ADDRESS-purpose ends', () => { +it('projects independently auditable whole-contact and ADDRESS-purpose ends', () => { const encodedEnd = { effectiveEnd: '2026-10-01T00:00:00.000Z', endedByActionInvocationId: '30000000-0000-4000-8000-000000000001', @@ -365,6 +346,6 @@ test('projects independently auditable whole-contact and ADDRESS-purpose ends', ], type: 'ADDRESS', }); - assert.deepEqual(address.purposes[0]?.end, end); - assert.equal(address.purposes[0]?.current, true, 'a future end remains current before boundary'); + expect(address.purposes[0]?.end).toEqual(end); + expect(address.purposes[0]?.current, 'a future end remains current before boundary').toBe(true); }); diff --git a/app/verticals/party-registry/tests/unit/contact-point-correction-action.test.ts b/app/verticals/party-registry/tests/unit/contact-point-correction-action.test.ts index fa44552bb..fbf5a3100 100644 --- a/app/verticals/party-registry/tests/unit/contact-point-correction-action.test.ts +++ b/app/verticals/party-registry/tests/unit/contact-point-correction-action.test.ts @@ -1,6 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { DateTime, Effect } from 'effect'; import { createActionCollector } from '../../../../packages/core-runtime/src/actions/collector.ts'; import { getActionHandler } from '../../../../packages/core-runtime/src/actions/definition.ts'; @@ -49,9 +47,9 @@ const replacement: PartyContactPoint = { }, verification: { state: 'UNVERIFIED' }, }; - -test('correction publishes the corrected stable ref while returning the validated replacement', () => - runEffectTestPromise( +it.effect( + 'correction publishes the corrected stable ref while returning the validated replacement', + () => Effect.gen(function* correctionScenario() { const collector = createActionCollector( updateContactPointAction.descriptor.domainEvents, @@ -96,18 +94,18 @@ test('correction publishes the corrected stable ref while returning the validate services: { update: () => Effect.succeed(replacement) }, }, ); - assert.deepEqual(result.contactPointRef, replacement.contactPointRef); + expect(result.contactPointRef).toEqual(replacement.contactPointRef); const snapshot = collector.snapshot(); - assert.equal(snapshot.domainEvents.length, 1); - assert.equal(snapshot.domainEvents[0]?.subjectResourceId, originalContactPointRef.resourceId); - assert.deepEqual(snapshot.domainEvents[0]?.payloadJson, { + expect(snapshot.domainEvents.length).toBe(1); + expect(snapshot.domainEvents[0]?.subjectResourceId).toBe(originalContactPointRef.resourceId); + expect(snapshot.domainEvents[0]?.payloadJson).toEqual({ contactPointRef: originalContactPointRef, partyRef, revision: 3, }); - assert.deepEqual(snapshot.outboxMessages[0]?.message.payloadJson, { + expect(snapshot.outboxMessages[0]?.message.payloadJson).toEqual({ contactPointRef: originalContactPointRef, partyRef, }); }), - )); +); diff --git a/app/verticals/party-registry/tests/unit/contact-point-persistence.service.test.ts b/app/verticals/party-registry/tests/unit/contact-point-persistence.service.test.ts index ca9ff690e..e254e163c 100644 --- a/app/verticals/party-registry/tests/unit/contact-point-persistence.service.test.ts +++ b/app/verticals/party-registry/tests/unit/contact-point-persistence.service.test.ts @@ -1,10 +1,9 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; +import { TestClock } from 'effect/testing'; +import { expect, it } from 'effect-rstest'; /* eslint-disable anti-slop/no-chained-type-assertions, anti-slop/no-unsafe-dictionary-type -- This focused harness models only the Drizzle native Effect query surface exercised by Contact Point ending. expires: 2026-12-31. */ -import { SQL } from 'drizzle-orm'; +import { is, SQL } from 'drizzle-orm'; import { PgDialect } from 'drizzle-orm/pg-core'; -import { DateTime, Effect, Option, Schema } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { DateTime, Effect, Option, Schema, Predicate } from 'effect'; import { AresAppliedEvidenceSchema } from '../../shared/domain/ares-application.ts'; import { PartyAliasWriteRejected } from '../../shared/domain/merge-alias-resolution.ts'; import { makePartyAliasResolutionService } from '../../src/merge/party-alias-resolution.service.ts'; @@ -43,7 +42,6 @@ const updateContactPointRecord = ( command: Parameters[2], aliases = directAliases, ) => updateRecord(transaction, scope, command, aliases); - const contactRow = (overrides: Readonly> = {}) => ({ acceptedByActionInvocationId: actionInvocationId, acceptedByPrincipalId: principalId, @@ -93,7 +91,6 @@ const contactRow = (overrides: Readonly> = {}) => ({ verifierReference: null, ...overrides, }); - const purposeRow = (overrides: Readonly> = {}) => ({ acceptedByActionInvocationId: actionInvocationId, acceptedByPrincipalId: principalId, @@ -131,14 +128,12 @@ const purposeRow = (overrides: Readonly> = {}) => ({ verifierReference: null, ...overrides, }); - interface Harness { readonly insertValues: readonly Readonly>[]; readonly transaction: Parameters[0]; readonly updateSets: readonly Readonly>[]; readonly selectWheres: readonly SQL[]; } - const transactionHarness = ( selects: readonly (readonly Readonly>[])[], returningRows: readonly (readonly Readonly>[])[] = [], @@ -197,14 +192,12 @@ const transactionHarness = ( >[0]; return { insertValues, selectWheres, transaction, updateSets }; }; - const scope = { authMethod: 'system' as const, correlationId: 'contact-end-test', principalId, tenantId, }; - const wholeEndCommand = (effectiveEnd: string, reason = 'Party retired this mailbox') => ({ acceptedByActionInvocationId: actionInvocationId, acceptedByPrincipalId: principalId, @@ -224,117 +217,117 @@ const wholeEndCommand = (effectiveEnd: string, reason = 'Party retired this mail reason, target: { type: 'WHOLE_CONTACT_POINT' as const }, }); - -test('stores future end provenance while keeping the contact current until the boundary', () => - runEffectTestPromise( - Effect.gen(function* contactPointScenario() { - const effectiveEnd = '2099-01-01T00:00:00.000Z'; - const updated = contactRow({ - endEvidenceRefs: ['evidence:contact-end:1'], - endProvenanceMethod: 'MANUAL_CONFIRMATION', - endProvenanceSource: 'USER_ASSERTION', - endReason: 'Party retired this mailbox', - endedByActionInvocationId: actionInvocationId, - endedByPrincipalId: principalId, - endedRecordedAt: instantAsDate('2026-09-03T00:00:00.000Z'), - revision: 2, - validTo: instantAsDate(effectiveEnd), - }); - const harness = transactionHarness([ - [contactRow()], - [{ partyId }], - [contactRow()], - [updated], - [], - ]); - - const result = yield* endContactPointRecord( - harness.transaction, - scope, - wholeEndCommand(effectiveEnd), - ); - - assert.equal(harness.updateSets[0]?.['state'], 'ACTIVE'); - assert.equal(harness.updateSets[0]?.['isCurrent'], true); - assert.deepEqual(harness.updateSets[0]?.['endEvidenceRefs'], ['evidence:contact-end:1']); - assert.equal(harness.updateSets[0]?.['endReason'], 'Party retired this mailbox'); - assert.deepEqual(result.contactPoint.end?.provenance.evidenceReferences, [ - 'evidence:contact-end:1', - ]); - }), - )); - -test('stores end provenance on both a last ADDRESS purpose and its owning address', () => - runEffectTestPromise( +it.effect('stores future end provenance while keeping the contact current until the boundary', () => + Effect.gen(function* contactPointScenario() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T12:00:00.000Z')), + ); + const effectiveEnd = '2099-01-01T00:00:00.000Z'; + const updated = contactRow({ + endEvidenceRefs: ['evidence:contact-end:1'], + endProvenanceMethod: 'MANUAL_CONFIRMATION', + endProvenanceSource: 'USER_ASSERTION', + endReason: 'Party retired this mailbox', + endedByActionInvocationId: actionInvocationId, + endedByPrincipalId: principalId, + endedRecordedAt: instantAsDate('2026-09-03T00:00:00.000Z'), + revision: 2, + validTo: instantAsDate(effectiveEnd), + }); + const harness = transactionHarness([ + [contactRow()], + [{ partyId }], + [contactRow()], + [updated], + [], + ]); + const result = yield* endContactPointRecord( + harness.transaction, + scope, + wholeEndCommand(effectiveEnd), + ); + expect(harness.updateSets[0]?.['state']).toBe('ACTIVE'); + expect(harness.updateSets[0]?.['isCurrent']).toBe(true); + expect(harness.updateSets[0]?.['endEvidenceRefs']).toEqual(['evidence:contact-end:1']); + expect(harness.updateSets[0]?.['endReason']).toBe('Party retired this mailbox'); + expect(result.contactPoint.end?.provenance.evidenceReferences).toEqual([ + 'evidence:contact-end:1', + ]); + }), +); +it.effect('stores end provenance on both a last ADDRESS purpose and its owning address', () => + Effect.gen(function* contactPointScenario() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T12:00:00.000Z')), + ); + const effectiveEnd = '2026-02-01T00:00:00.000Z'; + const address = contactRow({ + addressLine1: 'Na Prikope 1', + city: 'Praha', + contactPointType: 'ADDRESS', + countryCode: 'CZ', + displayValue: null, + normalizationVersion: null, + normalizedValue: null, + preferred: false, + }); + const endedAddress = contactRow({ + ...address, + endEvidenceRefs: ['evidence:contact-end:1'], + endProvenanceMethod: 'MANUAL_CONFIRMATION', + endProvenanceSource: 'USER_ASSERTION', + endReason: 'Party retired this mailbox', + endedByActionInvocationId: actionInvocationId, + endedByPrincipalId: principalId, + endedRecordedAt: instantAsDate('2026-09-03T12:00:00.000Z'), + isCurrent: false, + revision: 2, + state: 'ENDED', + validTo: instantAsDate(effectiveEnd), + }); + const endedPurpose = purposeRow({ + endEvidenceRefs: ['evidence:contact-end:1'], + endProvenanceMethod: 'MANUAL_CONFIRMATION', + endProvenanceSource: 'USER_ASSERTION', + endReason: 'Party retired this mailbox', + endedByActionInvocationId: actionInvocationId, + endedByPrincipalId: principalId, + endedRecordedAt: instantAsDate('2026-09-03T12:00:00.000Z'), + isCurrent: false, + revision: 2, + state: 'ENDED', + validTo: instantAsDate(effectiveEnd), + }); + const harness = transactionHarness([ + [address], + [{ partyId }], + [address], + [purposeRow()], + [endedAddress], + [endedPurpose], + ]); + const command = { + ...wholeEndCommand(effectiveEnd), + target: { target: { purpose: 'DELIVERY' as const }, type: 'ADDRESS_PURPOSE' as const }, + }; + const result = yield* endContactPointRecord(harness.transaction, scope, command); + expect(harness.updateSets.length).toBe(2); + expect(harness.updateSets[0]?.['endProvenanceSource']).toBe('USER_ASSERTION'); + expect(harness.updateSets[1]?.['endProvenanceMethod']).toBe('MANUAL_CONFIRMATION'); + expect(result.contactPoint.value.type).toBe('ADDRESS'); + expect( + result.contactPoint.value.type === 'ADDRESS' && + result.contactPoint.value.purposes[0]?.end?.reason, + ).toBe('Party retired this mailbox'); + }), +); +it.effect( + 'reuses only an exact end request and rejects changed evidence at the same boundary', + () => Effect.gen(function* contactPointScenario() { - const effectiveEnd = '2026-02-01T00:00:00.000Z'; - const address = contactRow({ - addressLine1: 'Na Prikope 1', - city: 'Praha', - contactPointType: 'ADDRESS', - countryCode: 'CZ', - displayValue: null, - normalizationVersion: null, - normalizedValue: null, - preferred: false, - }); - const endedAddress = contactRow({ - ...address, - endEvidenceRefs: ['evidence:contact-end:1'], - endProvenanceMethod: 'MANUAL_CONFIRMATION', - endProvenanceSource: 'USER_ASSERTION', - endReason: 'Party retired this mailbox', - endedByActionInvocationId: actionInvocationId, - endedByPrincipalId: principalId, - endedRecordedAt: instantAsDate('2026-09-03T12:00:00.000Z'), - isCurrent: false, - revision: 2, - state: 'ENDED', - validTo: instantAsDate(effectiveEnd), - }); - const endedPurpose = purposeRow({ - endEvidenceRefs: ['evidence:contact-end:1'], - endProvenanceMethod: 'MANUAL_CONFIRMATION', - endProvenanceSource: 'USER_ASSERTION', - endReason: 'Party retired this mailbox', - endedByActionInvocationId: actionInvocationId, - endedByPrincipalId: principalId, - endedRecordedAt: instantAsDate('2026-09-03T12:00:00.000Z'), - isCurrent: false, - revision: 2, - state: 'ENDED', - validTo: instantAsDate(effectiveEnd), - }); - const harness = transactionHarness([ - [address], - [{ partyId }], - [address], - [purposeRow()], - [endedAddress], - [endedPurpose], - ]); - const command = { - ...wholeEndCommand(effectiveEnd), - target: { target: { purpose: 'DELIVERY' as const }, type: 'ADDRESS_PURPOSE' as const }, - }; - - const result = yield* endContactPointRecord(harness.transaction, scope, command); - - assert.equal(harness.updateSets.length, 2); - assert.equal(harness.updateSets[0]?.['endProvenanceSource'], 'USER_ASSERTION'); - assert.equal(harness.updateSets[1]?.['endProvenanceMethod'], 'MANUAL_CONFIRMATION'); - assert.equal(result.contactPoint.value.type, 'ADDRESS'); - assert.equal( - result.contactPoint.value.type === 'ADDRESS' && - result.contactPoint.value.purposes[0]?.end?.reason, - 'Party retired this mailbox', + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T12:00:00.000Z')), ); - }), - )); - -test('reuses only an exact end request and rejects changed evidence at the same boundary', () => - runEffectTestPromise( - Effect.gen(function* contactPointScenario() { const effectiveEnd = '2026-02-01T00:00:00.000Z'; const ended = contactRow({ endEvidenceRefs: ['evidence:contact-end:1'], @@ -355,9 +348,8 @@ test('reuses only an exact end request and rejects changed evidence at the same scope, wholeEndCommand(effectiveEnd), ); - assert.equal(exact.changed, false); - assert.equal(exactHarness.updateSets.length, 0); - + expect(exact.changed).toBe(false); + expect(exactHarness.updateSets.length).toBe(0); const changedHarness = transactionHarness([[ended], [{ partyId }], [ended]]); const changed = yield* Effect.exit( endContactPointRecord( @@ -366,56 +358,54 @@ test('reuses only an exact end request and rejects changed evidence at the same wholeEndCommand(effectiveEnd, 'A different reason'), ), ); - assert.equal(changed._tag, 'Failure'); - assert.equal(changedHarness.updateSets.length, 0); + expect(Predicate.isTagged(changed, 'Failure')).toBe(true); + expect(changedHarness.updateSets.length).toBe(0); }), - )); - -test('stores correction end provenance on the preserved original Contact Point', () => - runEffectTestPromise( - Effect.gen(function* contactPointScenario() { - const original = contactRow(); - const corrected = contactRow({ - endEvidenceRefs: ['evidence:wrong-mailbox:1'], - endProvenanceMethod: 'MANUAL_CONFIRMATION', - endProvenanceSource: 'USER_ASSERTION', - endReason: 'Mailbox was attached to the wrong Party', - endedByActionInvocationId: actionInvocationId, - endedByPrincipalId: principalId, - endedRecordedAt: instantAsDate('2026-09-03T12:00:00.000Z'), - isCurrent: false, - preferred: false, - revision: 2, - state: 'RETRACTED', - validTo: instantAsDate('2026-09-03T12:00:00.000Z'), - }); - const harness = transactionHarness([[original], [{ partyId }], [original], [corrected], []]); - - const result = yield* updateContactPointRecord(harness.transaction, scope, { - acceptedByActionInvocationId: actionInvocationId, - acceptedByPrincipalId: principalId, - change: { - evidenceReferences: ['evidence:wrong-mailbox:1'], - reason: 'Mailbox was attached to the wrong Party', - type: 'CORRECT_CONTACT_POINT', - }, - contactPointRef: wholeEndCommand('2099-01-01T00:00:00.000Z').contactPointRef, - expectedRevision: 1, - provenance: { - authoritative: false, - method: 'MANUAL_CONFIRMATION', - source: 'USER_ASSERTION', - }, - }); - - assert.equal(harness.updateSets[0]?.['state'], 'RETRACTED'); - assert.equal(harness.updateSets[0]?.['endReason'], 'Mailbox was attached to the wrong Party'); - assert.deepEqual(harness.updateSets[0]?.['endEvidenceRefs'], ['evidence:wrong-mailbox:1']); - assert.equal(harness.insertValues[0]?.['contactPointId'], contactPointId); - assert.equal(result.end?.reason, 'Mailbox was attached to the wrong Party'); - }), - )); - +); +it.effect('stores correction end provenance on the preserved original Contact Point', () => + Effect.gen(function* contactPointScenario() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T12:00:00.000Z')), + ); + const original = contactRow(); + const corrected = contactRow({ + endEvidenceRefs: ['evidence:wrong-mailbox:1'], + endProvenanceMethod: 'MANUAL_CONFIRMATION', + endProvenanceSource: 'USER_ASSERTION', + endReason: 'Mailbox was attached to the wrong Party', + endedByActionInvocationId: actionInvocationId, + endedByPrincipalId: principalId, + endedRecordedAt: instantAsDate('2026-09-03T12:00:00.000Z'), + isCurrent: false, + preferred: false, + revision: 2, + state: 'RETRACTED', + validTo: instantAsDate('2026-09-03T12:00:00.000Z'), + }); + const harness = transactionHarness([[original], [{ partyId }], [original], [corrected], []]); + const result = yield* updateContactPointRecord(harness.transaction, scope, { + acceptedByActionInvocationId: actionInvocationId, + acceptedByPrincipalId: principalId, + change: { + evidenceReferences: ['evidence:wrong-mailbox:1'], + reason: 'Mailbox was attached to the wrong Party', + type: 'CORRECT_CONTACT_POINT', + }, + contactPointRef: wholeEndCommand('2099-01-01T00:00:00.000Z').contactPointRef, + expectedRevision: 1, + provenance: { + authoritative: false, + method: 'MANUAL_CONFIRMATION', + source: 'USER_ASSERTION', + }, + }); + expect(harness.updateSets[0]?.['state']).toBe('RETRACTED'); + expect(harness.updateSets[0]?.['endReason']).toBe('Mailbox was attached to the wrong Party'); + expect(harness.updateSets[0]?.['endEvidenceRefs']).toEqual(['evidence:wrong-mailbox:1']); + expect(harness.insertValues[0]?.['contactPointId']).toBe(contactPointId); + expect(result.end?.reason).toBe('Mailbox was attached to the wrong Party'); + }), +); const addressRow = (overrides: Readonly> = {}) => contactRow({ addressLine1: 'Na Prikope 1', @@ -428,7 +418,6 @@ const addressRow = (overrides: Readonly> = {}) => preferred: false, ...overrides, }); - const updateCommand = (change: Parameters[2]['change']) => ({ acceptedByActionInvocationId: actionInvocationId, acceptedByPrincipalId: principalId, @@ -442,207 +431,216 @@ const updateCommand = (change: Parameters[2]['c source: 'EXTERNAL_EVIDENCE' as const, }, }); - -test('re-adds a scheduled-ended purpose as a new period without reopening its history', () => - runEffectTestPromise( - Effect.gen(function* contactPointScenario() { - const stalePurpose = purposeRow({ - endEvidenceRefs: ['evidence:contact-end:1'], - endProvenanceMethod: 'MANUAL_CONFIRMATION', - endProvenanceSource: 'USER_ASSERTION', - endReason: 'Previous delivery period ended', - endedByActionInvocationId: actionInvocationId, - endedByPrincipalId: principalId, - endedRecordedAt: instantAsDate('2026-01-02T00:00:00.000Z'), - validTo: instantAsDate('2026-02-01T00:00:00.000Z'), - }); - const renewedPurpose = purposeRow({ - contactPointPurposeId: '60000000-0000-4000-8000-000000000002', - validFrom: instantAsDate('2026-09-03T12:00:00.000Z'), - }); - const address = addressRow(); - const harness = transactionHarness([ - [address], - [{ partyId }], - [address], - [stalePurpose], - [addressRow({ revision: 2 })], - [{ ...stalePurpose, isCurrent: false, state: 'ENDED' }, renewedPurpose], - ]); - const result = yield* updateContactPointRecord( +it.effect('re-adds a scheduled-ended purpose as a new period without reopening its history', () => + Effect.gen(function* contactPointScenario() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T12:00:00.000Z')), + ); + const stalePurpose = purposeRow({ + endEvidenceRefs: ['evidence:contact-end:1'], + endProvenanceMethod: 'MANUAL_CONFIRMATION', + endProvenanceSource: 'USER_ASSERTION', + endReason: 'Previous delivery period ended', + endedByActionInvocationId: actionInvocationId, + endedByPrincipalId: principalId, + endedRecordedAt: instantAsDate('2026-01-02T00:00:00.000Z'), + validTo: instantAsDate('2026-02-01T00:00:00.000Z'), + }); + const renewedPurpose = purposeRow({ + contactPointPurposeId: '60000000-0000-4000-8000-000000000002', + validFrom: instantAsDate('2026-09-03T12:00:00.000Z'), + }); + const address = addressRow(); + const harness = transactionHarness([ + [address], + [{ partyId }], + [address], + [stalePurpose], + [addressRow({ revision: 2 })], + [{ ...stalePurpose, isCurrent: false, state: 'ENDED' }, renewedPurpose], + ]); + const result = yield* updateContactPointRecord( + harness.transaction, + scope, + updateCommand({ + assignment: { preferred: true, purpose: 'DELIVERY' }, + type: 'SET_ADDRESS_PURPOSE', + }), + ); + expect(harness.updateSets[0]?.['state']).toBe('ENDED'); + expect(harness.updateSets[0]?.['revision']).toBe(2); + expect(harness.insertValues.length).toBe(1); + expect(harness.insertValues[0]?.['validTo']).toBe(undefined); + expect(harness.insertValues[0]?.['endReason']).toBe(undefined); + expect(result.value.type).toBe('ADDRESS'); + if (result.value.type === 'ADDRESS') { + expect(result.value.purposes.length).toBe(2); + expect(result.value.purposes[0]?.current).toBe(false); + expect(result.value.purposes[1]?.validTo).toBe(null); + } + }), +); +it.effect('rejects a REGISTERED context collision as a typed domain conflict before mutation', () => + Effect.gen(function* contactPointScenario() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T12:00:00.000Z')), + ); + const address = addressRow(); + const conflicting = purposeRow({ + contactPointId: '30000000-0000-4000-8000-000000000002', + jurisdiction: 'CZ', + purposeKey: 'REGISTERED', + registryContext: 'ARES', + }); + const harness = transactionHarness([[address], [{ partyId }], [address], [conflicting]]); + const error = yield* Effect.flip( + updateContactPointRecord( harness.transaction, scope, updateCommand({ - assignment: { preferred: true, purpose: 'DELIVERY' }, + assignment: { + preferred: true, + purpose: 'REGISTERED', + registryContext: { jurisdiction: 'CZ', registryKey: 'ARES' }, + }, type: 'SET_ADDRESS_PURPOSE', }), - ); - assert.equal(harness.updateSets[0]?.['state'], 'ENDED'); - assert.equal(harness.updateSets[0]?.['revision'], 2); - assert.equal(harness.insertValues.length, 1); - assert.equal(harness.insertValues[0]?.['validTo'], undefined); - assert.equal(harness.insertValues[0]?.['endReason'], undefined); - assert.equal(result.value.type, 'ADDRESS'); - if (result.value.type === 'ADDRESS') { - assert.equal(result.value.purposes.length, 2); - assert.equal(result.value.purposes[0]?.current, false); - assert.equal(result.value.purposes[1]?.validTo, null); - } - }), - )); - -test('rejects a REGISTERED context collision as a typed domain conflict before mutation', () => - runEffectTestPromise( - Effect.gen(function* contactPointScenario() { - const address = addressRow(); - const conflicting = purposeRow({ - contactPointId: '30000000-0000-4000-8000-000000000002', - jurisdiction: 'CZ', - purposeKey: 'REGISTERED', - registryContext: 'ARES', - }); - const harness = transactionHarness([[address], [{ partyId }], [address], [conflicting]]); - const error = yield* Effect.flip( - updateContactPointRecord( - harness.transaction, - scope, - updateCommand({ - assignment: { + ), + ); + expect(Predicate.isTagged(error, 'PartyContactPointAlreadyExists')).toBe(true); + expect(harness.updateSets.length).toBe(0); + expect(harness.insertValues.length).toBe(0); + const condition = harness.selectWheres.at(3); + expect(condition).toBeDefined(); + if (condition === undefined) { + return expect.unreachable('Expected SQL condition'); + } + const query = new PgDialect().sqlToQuery(condition); + expect(query.sql).toMatch(/registry_context/u); + expect(query.sql).toMatch(/jurisdiction/u); + expect(query.params.includes('ARES')).toBe(true); + expect(query.params.includes('CZ')).toBe(true); + const addHarness = transactionHarness([[{ partyId }], [], [conflicting]]); + const addError = yield* Effect.flip( + addContactPointRecord(addHarness.transaction, scope, { + acceptedByActionInvocationId: actionInvocationId, + acceptedByPrincipalId: principalId, + contactPoint: { + address: { addressLine1: 'Another street 2', city: 'Praha', countryCode: 'CZ' }, + purposes: [ + { preferred: true, purpose: 'REGISTERED', registryContext: { jurisdiction: 'CZ', registryKey: 'ARES' }, }, - type: 'SET_ADDRESS_PURPOSE', - }), - ), - ); - assert.equal(error._tag, 'PartyContactPointAlreadyExists'); - assert.equal(harness.updateSets.length, 0); - assert.equal(harness.insertValues.length, 0); - const condition = harness.selectWheres.at(3); - assert.ok(condition); - const query = new PgDialect().sqlToQuery(condition); - assert.match(query.sql, /registry_context/u); - assert.match(query.sql, /jurisdiction/u); - assert.ok(query.params.includes('ARES')); - assert.ok(query.params.includes('CZ')); - - const addHarness = transactionHarness([[{ partyId }], [], [conflicting]]); - const addError = yield* Effect.flip( - addContactPointRecord(addHarness.transaction, scope, { - acceptedByActionInvocationId: actionInvocationId, - acceptedByPrincipalId: principalId, - contactPoint: { - address: { addressLine1: 'Another street 2', city: 'Praha', countryCode: 'CZ' }, - purposes: [ - { - preferred: true, - purpose: 'REGISTERED', - registryContext: { jurisdiction: 'CZ', registryKey: 'ARES' }, - }, - ], - type: 'ADDRESS', - }, - partyRef: { - moduleId: 'party.registry', - resourceId: partyId, - resourceType: 'party.registry.party', - tenantId, - }, - privacyClassification: 'PUBLIC', - provenance: { - authoritative: true, - evidenceReference: 'evidence:registry:2', - method: 'DOCUMENT_REVIEW', - source: 'EXTERNAL_EVIDENCE', - }, - validFrom: '2026-01-01T00:00:00.000Z', - verification: { state: 'UNVERIFIED' }, - }), - ); - assert.equal(addError._tag, 'PartyContactPointAlreadyExists'); - assert.equal(addHarness.insertValues.length, 0); - assert.equal(addHarness.updateSets.length, 0); - }), - )); - -test('advances revisions on both the transferred purpose and its owning address', () => - runEffectTestPromise( - Effect.gen(function* contactPointScenario() { - const address = addressRow(); - const current = purposeRow({ preferred: false }); - const previousPreferred = purposeRow({ - contactPointId: '30000000-0000-4000-8000-000000000002', - contactPointPurposeId: '60000000-0000-4000-8000-000000000002', - revision: 7, - }); - const harness = transactionHarness([ - [address], - [{ partyId }], - [address], - [current, previousPreferred], - [addressRow({ revision: 2 })], - [{ ...current, preferred: true, revision: 2 }], - ]); - yield* updateContactPointRecord( - harness.transaction, - scope, - updateCommand({ - assignment: { preferred: true, purpose: 'DELIVERY' }, - type: 'SET_ADDRESS_PURPOSE', - }), - ); - assert.equal(harness.updateSets[0]?.['revision'], 8); - assert.equal(harness.updateSets[0]?.['preferred'], false); - const revision = harness.updateSets[1]?.['revision']; - assert.ok(revision instanceof SQL); - assert.match(new PgDialect().sqlToQuery(revision).sql, /revision.*\+ 1/u); - assert.equal(harness.updateSets[2]?.['revision'], 2); - assert.equal(harness.updateSets[3]?.['revision'], 2); - }), - )); - -test('preserves original provenance evidence and appends deduplicated enrichment', () => - runEffectTestPromise( - Effect.gen(function* contactPointScenario() { - const row = contactRow({ - additionalEvidenceRefs: ['evidence:second'], - evidenceReference: 'evidence:first', - }); - const updated = contactRow({ - additionalEvidenceRefs: ['evidence:second', 'evidence:third'], - evidenceReference: 'evidence:first', - revision: 2, - }); - const harness = transactionHarness([[row], [{ partyId }], [row], [updated], []]); - const result = yield* updateContactPointRecord( - harness.transaction, - scope, - updateCommand({ - provenance: { - authoritative: false, - evidenceReference: 'evidence:third', - method: 'MANUAL_CONFIRMATION', - source: 'USER_ASSERTION', - }, - type: 'ADD_PROVENANCE', - }), - ); - assert.equal(harness.updateSets[0]?.['evidenceReference'], undefined); - assert.deepEqual(harness.updateSets[0]?.['additionalEvidenceRefs'], [ - 'evidence:second', - 'evidence:third', - ]); - assert.deepEqual(result.provenance.evidenceReferences, [ - 'evidence:first', - 'evidence:second', - 'evidence:third', - ]); - }), - )); - -test('rejects invalid E.164 and oversized extensions through the service typed-error path', () => - runEffectTestPromise( + ], + type: 'ADDRESS', + }, + partyRef: { + moduleId: 'party.registry', + resourceId: partyId, + resourceType: 'party.registry.party', + tenantId, + }, + privacyClassification: 'PUBLIC', + provenance: { + authoritative: true, + evidenceReference: 'evidence:registry:2', + method: 'DOCUMENT_REVIEW', + source: 'EXTERNAL_EVIDENCE', + }, + validFrom: '2026-01-01T00:00:00.000Z', + verification: { state: 'UNVERIFIED' }, + }), + ); + expect(Predicate.isTagged(addError, 'PartyContactPointAlreadyExists')).toBe(true); + expect(addHarness.insertValues.length).toBe(0); + expect(addHarness.updateSets.length).toBe(0); + }), +); +it.effect('advances revisions on both the transferred purpose and its owning address', () => + Effect.gen(function* contactPointScenario() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T12:00:00.000Z')), + ); + const address = addressRow(); + const current = purposeRow({ preferred: false }); + const previousPreferred = purposeRow({ + contactPointId: '30000000-0000-4000-8000-000000000002', + contactPointPurposeId: '60000000-0000-4000-8000-000000000002', + revision: 7, + }); + const harness = transactionHarness([ + [address], + [{ partyId }], + [address], + [current, previousPreferred], + [addressRow({ revision: 2 })], + [{ ...current, preferred: true, revision: 2 }], + ]); + yield* updateContactPointRecord( + harness.transaction, + scope, + updateCommand({ + assignment: { preferred: true, purpose: 'DELIVERY' }, + type: 'SET_ADDRESS_PURPOSE', + }), + ); + expect(harness.updateSets[0]?.['revision']).toBe(8); + expect(harness.updateSets[0]?.['preferred']).toBe(false); + const revision = harness.updateSets[1]?.['revision']; + expect(is(revision, SQL)).toBe(true); + if (!is(revision, SQL)) { + return expect.unreachable('Expected SQL revision'); + } + expect(new PgDialect().sqlToQuery(revision).sql).toMatch(/revision.*\+ 1/u); + expect(harness.updateSets[2]?.['revision']).toBe(2); + expect(harness.updateSets[3]?.['revision']).toBe(2); + }), +); +it.effect('preserves original provenance evidence and appends deduplicated enrichment', () => + Effect.gen(function* contactPointScenario() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T12:00:00.000Z')), + ); + const row = contactRow({ + additionalEvidenceRefs: ['evidence:second'], + evidenceReference: 'evidence:first', + }); + const updated = contactRow({ + additionalEvidenceRefs: ['evidence:second', 'evidence:third'], + evidenceReference: 'evidence:first', + revision: 2, + }); + const harness = transactionHarness([[row], [{ partyId }], [row], [updated], []]); + const result = yield* updateContactPointRecord( + harness.transaction, + scope, + updateCommand({ + provenance: { + authoritative: false, + evidenceReference: 'evidence:third', + method: 'MANUAL_CONFIRMATION', + source: 'USER_ASSERTION', + }, + type: 'ADD_PROVENANCE', + }), + ); + expect(harness.updateSets[0]?.['evidenceReference']).toBe(undefined); + expect(harness.updateSets[0]?.['additionalEvidenceRefs']).toEqual([ + 'evidence:second', + 'evidence:third', + ]); + expect(result.provenance.evidenceReferences).toEqual([ + 'evidence:first', + 'evidence:second', + 'evidence:third', + ]); + }), +); +it.effect( + 'rejects invalid E.164 and oversized extensions through the service typed-error path', + () => Effect.all( [ { preferred: false, type: 'PHONE' as const, value: '+0123456789' }, @@ -676,17 +674,20 @@ test('rejects invalid E.164 and oversized extensions through the service typed-e verification: { state: 'UNVERIFIED' }, }), ); - assert.equal(error._tag, 'PartyContactPointInvalid'); - assert.equal(harness.selectWheres.length, 0); - assert.equal(harness.insertValues.length, 0); + expect(Predicate.isTagged(error, 'PartyContactPointInvalid')).toBe(true); + expect(harness.selectWheres.length).toBe(0); + expect(harness.insertValues.length).toBe(0); }), ), ).pipe(Effect.asVoid), - )); - -test('rejects an explicit alias Party add but keeps durable ContactPoint updates readable through the full chain', () => - runEffectTestPromise( +); +it.effect( + 'rejects an explicit alias Party add but keeps durable ContactPoint updates readable through the full chain', + () => Effect.gen(function* contactPointScenario() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T12:00:00.000Z')), + ); const intermediatePartyId = '20000000-0000-4000-8000-000000000002'; const canonicalPartyId = '20000000-0000-4000-8000-000000000003'; const aliases = makePartyAliasResolutionService({ @@ -740,10 +741,12 @@ test('rejects an explicit alias Party add but keeps durable ContactPoint updates aliases, ), ); - assert.ok(Schema.is(PartyAliasWriteRejected)(rejected)); - assert.equal(rejected.canonicalPartyRef.resourceId, canonicalPartyId); - assert.equal(addHarness.insertValues.length, 0); - + expect(Schema.is(PartyAliasWriteRejected)(rejected)).toBe(true); + expect( + (yield* Schema.decodeUnknownEffect(PartyAliasWriteRejected)(rejected)).canonicalPartyRef + .resourceId, + ).toBe(canonicalPartyId); + expect(addHarness.insertValues.length).toBe(0); const row = contactRow(); const updateHarness = transactionHarness([ [row], @@ -761,10 +764,9 @@ test('rejects an explicit alias Party add but keeps durable ContactPoint updates }), aliases, ); - assert.equal(updated.partyRef.resourceId, canonicalPartyId); - assert.equal(updated.storedPartyRef?.resourceId, partyId); - assert.equal(updateHarness.updateSets.length, 1); - + expect(updated.partyRef.resourceId).toBe(canonicalPartyId); + expect(updated.storedPartyRef?.resourceId).toBe(partyId); + expect(updateHarness.updateSets.length).toBe(1); const readHarness = transactionHarness([[row], []]); const detail = yield* findPartyContactPointRecord( readHarness.transaction, @@ -772,9 +774,8 @@ test('rejects an explicit alias Party add but keeps durable ContactPoint updates contactPointId, aliases, ); - assert.equal(Option.getOrThrow(detail).partyRef.resourceId, canonicalPartyId); - assert.equal(Option.getOrThrow(detail).storedPartyRef.resourceId, partyId); - + expect(Option.getOrThrow(detail).partyRef.resourceId).toBe(canonicalPartyId); + expect(Option.getOrThrow(detail).storedPartyRef.resourceId).toBe(partyId); const ended = contactRow({ endEvidenceRefs: ['evidence:contact-end:1'], endProvenanceMethod: 'MANUAL_CONFIRMATION', @@ -799,37 +800,44 @@ test('rejects an explicit alias Party add but keeps durable ContactPoint updates wholeEndCommand('2099-01-01T00:00:00.000Z'), aliases, ); - assert.equal(endResult.contactPoint.partyRef.resourceId, canonicalPartyId); - assert.equal(endHarness.updateSets.length, 1); + expect(endResult.contactPoint.partyRef.resourceId).toBe(canonicalPartyId); + expect(endHarness.updateSets.length).toBe(1); }), - )); - -test('advances the replaced channel preference revision as well as the selected contact', () => - runEffectTestPromise( +); +it.effect('advances the replaced channel preference revision as well as the selected contact', () => + Effect.gen(function* contactPointScenario() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T12:00:00.000Z')), + ); + const row = contactRow({ preferred: false }); + const harness = transactionHarness([ + [row], + [{ partyId }], + [row], + [contactRow({ revision: 2 })], + [], + ]); + yield* updateContactPointRecord( + harness.transaction, + scope, + updateCommand({ preferred: true, type: 'SET_CHANNEL_PREFERRED' }), + ); + const revision = harness.updateSets[0]?.['revision']; + expect(is(revision, SQL)).toBe(true); + if (!is(revision, SQL)) { + return expect.unreachable('Expected SQL revision'); + } + expect(new PgDialect().sqlToQuery(revision).sql).toMatch(/revision.*\+ 1/u); + expect(harness.updateSets[1]?.['revision']).toBe(2); + }), +); +it.effect( + 'persists bounded ARES provenance on the address and purpose without using observation time as effective time', + () => Effect.gen(function* contactPointScenario() { - const row = contactRow({ preferred: false }); - const harness = transactionHarness([ - [row], - [{ partyId }], - [row], - [contactRow({ revision: 2 })], - [], - ]); - yield* updateContactPointRecord( - harness.transaction, - scope, - updateCommand({ preferred: true, type: 'SET_CHANNEL_PREFERRED' }), + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T12:00:00.000Z')), ); - const revision = harness.updateSets[0]?.['revision']; - assert.ok(revision instanceof SQL); - assert.match(new PgDialect().sqlToQuery(revision).sql, /revision.*\+ 1/u); - assert.equal(harness.updateSets[1]?.['revision'], 2); - }), - )); - -test('persists bounded ARES provenance on the address and purpose without using observation time as effective time', () => - runEffectTestPromise( - Effect.gen(function* contactPointScenario() { const externalEvidence = yield* Schema.decodeUnknownEffect(AresAppliedEvidenceSchema)({ authorityPolicyKey: 'party_registry.ares_enrichment', authorityPolicyVersion: '1', @@ -887,28 +895,29 @@ test('persists bounded ARES provenance on the address and purpose without using validFrom: '2026-08-01T00:00:00.000Z', verification: { state: 'UNVERIFIED' }, }); - assert.deepEqual(harness.insertValues[0]?.['externalEvidence'], encodedExternalEvidence); - assert.deepEqual(harness.insertValues[1]?.['externalEvidence'], encodedExternalEvidence); - assert.deepEqual( - harness.insertValues[0]?.['validFrom'], + expect(harness.insertValues[0]?.['externalEvidence']).toEqual(encodedExternalEvidence); + expect(harness.insertValues[1]?.['externalEvidence']).toEqual(encodedExternalEvidence); + expect(harness.insertValues[0]?.['validFrom']).toEqual( instantAsDate('2026-08-01T00:00:00.000Z'), ); - assert.equal( + expect( result.provenance.externalEvidence === undefined ? undefined : DateTime.formatIso(result.provenance.externalEvidence.observedAt), - '2026-09-03T10:00:00.000Z', - ); - assert.equal(result.value.type, 'ADDRESS'); + ).toBe('2026-09-03T10:00:00.000Z'); + expect(result.value.type).toBe('ADDRESS'); if (result.value.type === 'ADDRESS') { - assert.deepEqual(result.value.purposes[0]?.provenance.externalEvidence, externalEvidence); + expect(result.value.purposes[0]?.provenance.externalEvidence).toEqual(externalEvidence); } }), - )); - -test('treats PHONE extensions as distinct endpoints while rejecting an exact duplicate extension', () => - runEffectTestPromise( +); +it.effect( + 'treats PHONE extensions as distinct endpoints while rejecting an exact duplicate extension', + () => Effect.gen(function* contactPointScenario() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T12:00:00.000Z')), + ); const existing = contactRow({ contactPointType: 'PHONE', displayValue: '+420777123456', @@ -943,21 +952,23 @@ test('treats PHONE extensions as distinct endpoints while rejecting an exact dup }); const newHarness = transactionHarness([[{ partyId }], [existing], []], [[created]]); const result = yield* addContactPointRecord(newHarness.transaction, scope, command('102')); - assert.equal(newHarness.insertValues.length, 1); - assert.equal(result.value.type === 'PHONE' && result.value.extension, '102'); - + expect(newHarness.insertValues.length).toBe(1); + expect(result.value.type === 'PHONE' && result.value.extension).toBe('102'); const duplicateHarness = transactionHarness([[{ partyId }], [existing]]); const duplicate = yield* Effect.flip( addContactPointRecord(duplicateHarness.transaction, scope, command('101')), ); - assert.equal(duplicate._tag, 'PartyContactPointAlreadyExists'); - assert.equal(duplicateHarness.insertValues.length, 0); + expect(Predicate.isTagged(duplicate, 'PartyContactPointAlreadyExists')).toBe(true); + expect(duplicateHarness.insertValues.length).toBe(0); }), - )); - -test('whole ADDRESS end preserves an earlier purpose end and its independent accepted evidence', () => - runEffectTestPromise( +); +it.effect( + 'whole ADDRESS end preserves an earlier purpose end and its independent accepted evidence', + () => Effect.gen(function* contactPointScenario() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T12:00:00.000Z')), + ); const address = addressRow(); const earlierEndAudit = { endEvidenceRefs: ['evidence:delivery-contract-ended'], @@ -1000,26 +1011,23 @@ test('whole ADDRESS end preserves an earlier purpose end and its independent acc scope, wholeEndCommand('2099-01-01T00:00:00.000Z'), ); - assert.equal( - harness.updateSets.length, + expect(harness.updateSets.length, 'only the address and still-open purpose are changed').toBe( 2, - 'only the address and still-open purpose are changed', ); - assert.equal(harness.updateSets[1]?.['revision'], 2); + expect(harness.updateSets[1]?.['revision']).toBe(2); if (result.contactPoint.value.type === 'ADDRESS') { const [preserved] = result.contactPoint.value.purposes; - assert.equal( + expect( preserved?.validTo === null || preserved?.validTo === undefined ? preserved?.validTo : DateTime.formatIso(preserved.validTo), - '2090-01-01T00:00:00.000Z', - ); - assert.equal(preserved?.end?.reason, 'Independent delivery contract end'); - assert.deepEqual(preserved?.end?.provenance.evidenceReferences, [ + ).toBe('2090-01-01T00:00:00.000Z'); + expect(preserved?.end?.reason).toBe('Independent delivery contract end'); + expect(preserved?.end?.provenance.evidenceReferences).toEqual([ 'evidence:delivery-contract-ended', ]); } else { - assert.fail('Expected ADDRESS result'); + expect.unreachable('Expected ADDRESS result'); } }), - )); +); diff --git a/app/verticals/party-registry/tests/unit/correction-contract.test.ts b/app/verticals/party-registry/tests/unit/correction-contract.test.ts index 51b2d7829..96e848150 100644 --- a/app/verticals/party-registry/tests/unit/correction-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/correction-contract.test.ts @@ -1,9 +1,6 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off globalDate:off -- Existing compatibility boundary; expires: 2026-12-31. +import { expect, it } from 'effect-rstest'; /* eslint-disable anti-slop/no-chained-type-assertions, anti-slop/no-unsafe-dictionary-type -- This harness implements the correction service's Drizzle boundary. expires: 2026-12-31. */ -import { Effect, Match, Option, Schema } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { DateTime, Effect, Match, Option, Schema, Predicate } from 'effect'; import { PartyCorrectionCommandSchema, PartyCorrectionDetailSchema, @@ -54,10 +51,9 @@ const relationshipCommandEncoded = { const relationshipCommand = decode(SupersedeRelationshipCorrectionCommandSchema)( relationshipCommandEncoded, ); - -test('correction is closed to Party type, display name, and official identifier assertions', () => { +it('correction is closed to Party type, display name, and official identifier assertions', () => { for (const factKind of ['PARTY_TYPE', 'DISPLAY_NAME', 'OFFICIAL_IDENTIFIER']) { - assert.doesNotThrow(() => + expect(() => decode(PartyCorrectionCommandSchema)({ ...evidence, factKind, @@ -66,9 +62,9 @@ test('correction is closed to Party type, display name, and official identifier replacementValue: factKind === 'PARTY_TYPE' ? 'PERSON' : 'replacement', targetAssertionId: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb', }), - ); + ).not.toThrow(); } - assert.throws(() => + expect(() => decode(PartyCorrectionCommandSchema)({ ...evidence, factKind: 'CONTACT_POINT', @@ -77,25 +73,22 @@ test('correction is closed to Party type, display name, and official identifier replacementValue: 'x', targetAssertionId: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb', }), - ); + ).toThrow(); }); - -test('correction follow-up is typed and duplicate confirmation remains readiness-only', () => { - assert.equal(classifyCorrectionRoute('PARTY_TYPE'), 'LIFECYCLE_REVIEW'); - assert.equal(classifyCorrectionRoute('DISPLAY_NAME'), 'ENRICHMENT_REVIEW'); - assert.equal(classifyCorrectionRoute('OFFICIAL_IDENTIFIER'), 'CLAIM_REASSIGNMENT_REVIEW'); - assert.equal(classifyCorrectionRoute('RELATIONSHIP'), 'RELATIONSHIP_REVIEW'); - assert.equal( - confirmDuplicatePartiesAction.descriptor.actionKey, +it('correction follow-up is typed and duplicate confirmation remains readiness-only', () => { + expect(classifyCorrectionRoute('PARTY_TYPE')).toBe('LIFECYCLE_REVIEW'); + expect(classifyCorrectionRoute('DISPLAY_NAME')).toBe('ENRICHMENT_REVIEW'); + expect(classifyCorrectionRoute('OFFICIAL_IDENTIFIER')).toBe('CLAIM_REASSIGNMENT_REVIEW'); + expect(classifyCorrectionRoute('RELATIONSHIP')).toBe('RELATIONSHIP_REVIEW'); + expect(confirmDuplicatePartiesAction.descriptor.actionKey).toBe( 'party.registry.confirm-duplicate-parties', ); - assert.equal( + expect( Object.hasOwn( confirmDuplicatePartiesAction.descriptor.domainEvents, 'party.registry.party-merged.v1', ), - false, - ); + ).toBe(false); const partyTypeCommand = decode(PartyCorrectionCommandSchema)({ ...evidence, factKind: 'PARTY_TYPE', @@ -113,34 +106,30 @@ test('correction follow-up is typed and duplicate confirmation remains readiness ], targetAssertionId: assertionId, }); - assert.equal( - correctPartyFactAction.descriptor.tenantPermission?.(partyTypeCommand), + expect(correctPartyFactAction.descriptor.tenantPermission?.(partyTypeCommand)).toBe( 'manage_party_identity', ); - assert.equal(correctPartyFactAction.descriptor.auditProfile, 'sensitive'); - assert.equal( - correctPartyFactAction.descriptor.tenantPermission?.(relationshipCommand), + expect(correctPartyFactAction.descriptor.auditProfile).toBe('sensitive'); + expect(correctPartyFactAction.descriptor.tenantPermission?.(relationshipCommand)).toBe( 'manage_party_relationships', ); }); - -test('relationship correction is closed, revisioned, interval checked, and has no caller authority hints', () => { +it('relationship correction is closed, revisioned, interval checked, and has no caller authority hints', () => { const strictDecode = Schema.decodeUnknownSync(PartyCorrectionCommandSchema, { onExcessProperty: 'error', }); const decoded = strictDecode(relationshipCommandEncoded); - assert.deepEqual( - Schema.encodeSync(PartyCorrectionCommandSchema)(decoded), + expect(Schema.encodeSync(PartyCorrectionCommandSchema)(decoded)).toEqual( relationshipCommandEncoded, ); - assert.throws(() => strictDecode({ ...relationshipCommandEncoded, reasonCode: 'OTHER' })); - assert.throws(() => strictDecode({ ...relationshipCommandEncoded, expectedRevision: 0 })); - assert.throws(() => + expect(() => strictDecode({ ...relationshipCommandEncoded, reasonCode: 'OTHER' })).toThrow(); + expect(() => strictDecode({ ...relationshipCommandEncoded, expectedRevision: 0 })).toThrow(); + expect(() => strictDecode({ ...relationshipCommandEncoded, replacementValidFrom: '2026-03-01T00:00:00.000Z', }), - ); + ).toThrow(); for (const field of [ 'fromPartyRef', 'toPartyRef', @@ -149,12 +138,11 @@ test('relationship correction is closed, revisioned, interval checked, and has n 'actingPrincipalId', 'approvingPrincipalId', ]) { - assert.throws(() => + expect(() => strictDecode({ ...relationshipCommandEncoded, [field]: 'caller-controlled' }), - ); + ).toThrow(); } }); - const relationshipRow = (overrides: Readonly> = {}) => ({ assertionState: 'ACTIVE', endProvenanceMethod: null, @@ -164,7 +152,7 @@ const relationshipRow = (overrides: Readonly> = {}) => ( fromPartyId: partyId, provenanceMethod: 'DECLARED', provenanceSource: 'USER', - recordedAt: new Date('2026-01-01T00:00:00.000Z'), + recordedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), relationshipId: assertionId, relationshipType: 'CONTACT_PERSON_OF', revision: 1, @@ -174,12 +162,16 @@ const relationshipRow = (overrides: Readonly> = {}) => ( validTo: null, ...overrides, }); - const transactionHarness = ( selects: readonly (readonly Readonly>[])[], inserts: readonly (readonly Readonly>[])[] = [], updates: readonly (readonly Readonly>[])[] = [], - insertFailure?: { readonly cause: { readonly code: string; readonly constraint: string } }, + insertFailure?: { + readonly cause: { + readonly code: string; + readonly constraint: string; + }; + }, ) => { const selectQueue = [...selects]; const insertQueue = [...inserts]; @@ -226,208 +218,223 @@ const transactionHarness = ( } as unknown as Parameters[0]; return { insertValues, transaction, updateSets }; }; - -test('relationship supersession preserves endpoint/type identity and stores trusted actor plus old/new links', async () => { - const original = relationshipRow(); - const replacement = relationshipRow({ relationshipId: replacementId }); - const h = transactionHarness( - [[], [original], [], [{ partyId }], [], [{ partyId: organizationId }]], - [[replacement], [{ correctionId }]], - [[original]], - ); - const result = await runEffectTestPromise( - correctPartyFactRecord(h.transaction, tenantId, relationshipCommand, { - actionInvocationId, - principalId, +it.effect( + 'relationship supersession preserves endpoint/type identity and stores trusted actor plus old/new links', + () => + Effect.gen(function* correctionScenario1() { + const original = relationshipRow(); + const replacement = relationshipRow({ relationshipId: replacementId }); + const h = transactionHarness( + [[], [original], [], [{ partyId }], [], [{ partyId: organizationId }]], + [[replacement], [{ correctionId }]], + [[original]], + ); + const result = yield* correctPartyFactRecord(h.transaction, tenantId, relationshipCommand, { + actionInvocationId, + principalId, + }); + expect(h.updateSets[0]).toEqual({ assertionState: 'SUPERSEDED', revision: 2 }); + expect(h.insertValues[0]?.['fromPartyId']).toBe(partyId); + expect(h.insertValues[0]?.['toPartyId']).toBe(organizationId); + expect(h.insertValues[0]?.['relationshipType']).toBe('CONTACT_PERSON_OF'); + expect(h.insertValues[0]?.['supersedesRelationshipId']).toBe(assertionId); + expect(h.insertValues[0]?.['validFrom']).toBe(null); + expect(h.insertValues[1]?.['actingPrincipalId']).toBe(principalId); + expect(h.insertValues[1]?.['relationshipId']).toBe(assertionId); + expect(h.insertValues[1]?.['replacementRelationshipId']).toBe(replacementId); + expect(Object.hasOwn(h.insertValues[1] ?? {}, 'approvingPrincipalId')).toBe(false); + expect(Option.getOrThrow(result.relationshipRef).resourceId).toBe(assertionId); + expect(Option.getOrThrow(result.replacementRelationshipRef).resourceId).toBe(replacementId); }), - ); - assert.deepEqual(h.updateSets[0], { assertionState: 'SUPERSEDED', revision: 2 }); - assert.equal(h.insertValues[0]?.['fromPartyId'], partyId); - assert.equal(h.insertValues[0]?.['toPartyId'], organizationId); - assert.equal(h.insertValues[0]?.['relationshipType'], 'CONTACT_PERSON_OF'); - assert.equal(h.insertValues[0]?.['supersedesRelationshipId'], assertionId); - assert.equal(h.insertValues[0]?.['validFrom'], null); - assert.equal(h.insertValues[1]?.['actingPrincipalId'], principalId); - assert.equal(h.insertValues[1]?.['relationshipId'], assertionId); - assert.equal(h.insertValues[1]?.['replacementRelationshipId'], replacementId); - assert.equal(Object.hasOwn(h.insertValues[1] ?? {}, 'approvingPrincipalId'), false); - assert.equal(Option.getOrThrow(result.relationshipRef).resourceId, assertionId); - assert.equal(Option.getOrThrow(result.replacementRelationshipRef).resourceId, replacementId); -}); - -test('relationship retraction retains original effective validity and creates no replacement', async () => { - const command = decode(PartyCorrectionCommandSchema)({ - ...evidence, - correctionMode: 'RETRACT', - expectedRevision: 1, - factKind: 'RELATIONSHIP', - relationshipRef, - }); - const original = relationshipRow({ validFrom: new Date('2025-01-01T00:00:00.000Z') }); - const h = transactionHarness( - [[], [original], [], [{ partyId }], [], [{ partyId: organizationId }]], - [[{ correctionId }]], - [[original]], - ); - const result = await runEffectTestPromise( - correctPartyFactRecord(h.transaction, tenantId, command, { actionInvocationId, principalId }), - ); - assert.deepEqual(h.updateSets[0], { assertionState: 'RETRACTED', revision: 2 }); - assert.equal(h.insertValues.length, 1); - assert.ok(Option.isNone(result.replacementAssertionId)); -}); - -test('stale revision and foreign-tenant relationship correction fail before business writes', async () => { - await Promise.all( - [ - decode(SupersedeRelationshipCorrectionCommandSchema)({ - ...relationshipCommandEncoded, - expectedRevision: 2, - }), - decode(SupersedeRelationshipCorrectionCommandSchema)({ - ...relationshipCommandEncoded, - relationshipRef: { ...relationshipRef, tenantId: organizationId }, - }), - ].map(async (command) => { - const h = transactionHarness([[], [relationshipRow()]]); - const error = await runEffectTestPromise( - Effect.flip( - correctPartyFactRecord(h.transaction, tenantId, command, { +); +it.effect( + 'relationship retraction retains original effective validity and creates no replacement', + () => + Effect.gen(function* correctionScenario2() { + const command = decode(PartyCorrectionCommandSchema)({ + ...evidence, + correctionMode: 'RETRACT', + expectedRevision: 1, + factKind: 'RELATIONSHIP', + relationshipRef, + }); + const original = relationshipRow({ + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2025-01-01T00:00:00.000Z')), + }); + const h = transactionHarness( + [[], [original], [], [{ partyId }], [], [{ partyId: organizationId }]], + [[{ correctionId }]], + [[original]], + ); + const result = yield* correctPartyFactRecord(h.transaction, tenantId, command, { + actionInvocationId, + principalId, + }); + expect(h.updateSets[0]).toEqual({ assertionState: 'RETRACTED', revision: 2 }); + expect(h.insertValues.length).toBe(1); + expect(Option.isNone(result.replacementAssertionId)).toBe(true); + }), +); +it.effect( + 'stale revision and foreign-tenant relationship correction fail before business writes', + () => + Effect.all( + [ + decode(SupersedeRelationshipCorrectionCommandSchema)({ + ...relationshipCommandEncoded, + expectedRevision: 2, + }), + decode(SupersedeRelationshipCorrectionCommandSchema)({ + ...relationshipCommandEncoded, + relationshipRef: { ...relationshipRef, tenantId: organizationId }, + }), + ].map((command) => + Effect.gen(function* correctionScenario4() { + const h = transactionHarness([[], [relationshipRow()]]); + const error = yield* Effect.flip( + correctPartyFactRecord(h.transaction, tenantId, command, { + actionInvocationId, + principalId, + }), + ); + expect(Predicate.isTagged(error, 'PartyCorrectionConflict')).toBe(true); + expect(h.updateSets.length).toBe(0); + expect(h.insertValues.length).toBe(0); + }), + ), + { concurrency: 1 }, + ), +); +it.effect( + 'detail exposes immutable original/result semantics, governance, and source distinct from actor', + () => + Effect.gen(function* correctionScenario6() { + const h = transactionHarness([ + [ + { + actingPrincipalId: principalId, actionInvocationId, - principalId, + approvingPrincipalId: null, + correctionId, + evidenceRefs: evidence.evidenceRefs, + officialIdentifierId: null, + partyFactAssertionId: null, + partyId, + policyVersion: evidence.policyVersion, + reason: encodeStoredCorrectionReason(relationshipCommand), + recordedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-09-03T00:00:00.000Z')), + relationshipId: assertionId, + replacementOfficialIdentifierId: null, + replacementPartyFactAssertionId: null, + replacementRelationshipId: replacementId, + }, + ], + [ + relationshipRow({ + assertionState: 'SUPERSEDED', + endProvenanceMethod: 'DOCUMENT_REVIEW', + endProvenanceSource: 'ORIGINAL_END_RECORD', + endReason: null, + endedRecordedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-16T00:00:00.000Z')), + validTo: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-15T00:00:00.000Z')), }), + ], + [ + relationshipRow({ + relationshipId: replacementId, + validTo: DateTime.toDateUtc( + DateTime.makeUnsafe(relationshipCommandEncoded.replacementValidTo), + ), + }), + ], + ]); + const found = yield* findPartyCorrection(h.transaction, tenantId, correctionId); + const detail = Match.value(found).pipe( + Match.tag('found', ({ value }) => Schema.encodeSync(PartyCorrectionDetailSchema)(value)), + Match.tag('not_found', () => + expect.unreachable('Expected the correction detail to be found'), + ), + Match.exhaustive, + ); + expect( + yield* Schema.encodeEffect(PartyCorrectionDetailSchema)( + yield* Schema.decodeUnknownEffect(PartyCorrectionDetailSchema)(detail), ), + ).toEqual(detail); + expect(detail.actingPrincipalId).toBe(principalId); + expect(detail.approvingPrincipalId).toBe(null); + expect(detail.evidenceSource).toBe('DOCUMENT'); + expect(detail.actionInvocationId).toBe(actionInvocationId); + expect(detail.originalAssertion.assertionId).toBe(assertionId); + expect(detail.originalAssertion.validTo).toBe('2026-01-15T00:00:00.000Z'); + expect(detail.originalAssertion.factKind).toBe('RELATIONSHIP'); + if (detail.originalAssertion.factKind === 'RELATIONSHIP') { + expect(detail.originalAssertion.endEvidence?.reason).toBe(null); + expect(detail.originalAssertion.endEvidence?.provenance.source).toBe('ORIGINAL_END_RECORD'); + expect(detail.originalAssertion.endEvidence?.recordedAt).toBe('2026-01-16T00:00:00.000Z'); + } + expect(detail.resultingAssertion?.assertionId).toBe(replacementId); + expect(detail.resultingAssertion?.validTo).toBe( + relationshipCommandEncoded.replacementValidTo, ); - assert.equal(error._tag, 'PartyCorrectionConflict'); - assert.equal(h.updateSets.length, 0); - assert.equal(h.insertValues.length, 0); + expect(detail.governance.legalHolds).toBe('HONOR_GOVERNED_LEGAL_HOLDS'); + expect(detail.governance.policyVersion).toBe(detail.policyVersion); }), - ); -}); - -test('detail exposes immutable original/result semantics, governance, and source distinct from actor', async () => { - const h = transactionHarness([ - [ - { - actingPrincipalId: principalId, - actionInvocationId, - approvingPrincipalId: null, - correctionId, - evidenceRefs: evidence.evidenceRefs, - officialIdentifierId: null, - partyFactAssertionId: null, - partyId, - policyVersion: evidence.policyVersion, - reason: encodeStoredCorrectionReason(relationshipCommand), - recordedAt: new Date('2026-09-03T00:00:00.000Z'), - relationshipId: assertionId, - replacementOfficialIdentifierId: null, - replacementPartyFactAssertionId: null, - replacementRelationshipId: replacementId, - }, - ], - [ - relationshipRow({ - assertionState: 'SUPERSEDED', - endProvenanceMethod: 'DOCUMENT_REVIEW', - endProvenanceSource: 'ORIGINAL_END_RECORD', - endReason: null, - endedRecordedAt: new Date('2026-01-16T00:00:00.000Z'), - validTo: new Date('2026-01-15T00:00:00.000Z'), - }), - ], - [ - relationshipRow({ - relationshipId: replacementId, - validTo: new Date(relationshipCommandEncoded.replacementValidTo), - }), - ], - ]); - const found = await runEffectTestPromise( - findPartyCorrection(h.transaction, tenantId, correctionId), - ); - const detail = Match.value(found).pipe( - Match.tag('found', ({ value }) => Schema.encodeSync(PartyCorrectionDetailSchema)(value)), - Match.tag('not_found', () => assert.fail('Expected the correction detail to be found')), - Match.exhaustive, - ); - assert.deepEqual( - Schema.encodeSync(PartyCorrectionDetailSchema)(decode(PartyCorrectionDetailSchema)(detail)), - detail, - ); - assert.equal(detail.actingPrincipalId, principalId); - assert.equal(detail.approvingPrincipalId, null); - assert.equal(detail.evidenceSource, 'DOCUMENT'); - assert.equal(detail.actionInvocationId, actionInvocationId); - assert.equal(detail.originalAssertion.assertionId, assertionId); - assert.equal(detail.originalAssertion.validTo, '2026-01-15T00:00:00.000Z'); - assert.equal(detail.originalAssertion.factKind, 'RELATIONSHIP'); - if (detail.originalAssertion.factKind === 'RELATIONSHIP') { - assert.equal(detail.originalAssertion.endEvidence?.reason, null); - assert.equal(detail.originalAssertion.endEvidence?.provenance.source, 'ORIGINAL_END_RECORD'); - assert.equal(detail.originalAssertion.endEvidence?.recordedAt, '2026-01-16T00:00:00.000Z'); - } - assert.equal(detail.resultingAssertion?.assertionId, replacementId); - assert.equal(detail.resultingAssertion?.validTo, relationshipCommandEncoded.replacementValidTo); - assert.equal(detail.governance.legalHolds, 'HONOR_GOVERNED_LEGAL_HOLDS'); - assert.equal(detail.governance.policyVersion, detail.policyVersion); -}); - -test('relationship overlap is a typed conflict and no correction journal is written after failed replacement', async () => { - const original = relationshipRow(); - const h = transactionHarness( - [[], [original], [], [{ partyId }], [], [{ partyId: organizationId }]], - [], - [[original]], - { cause: { code: '23P01', constraint: 'party_relationships_no_overlap_excl' } }, - ); - const error = await runEffectTestPromise( - Effect.flip( - correctPartyFactRecord(h.transaction, tenantId, relationshipCommand, { - actionInvocationId, - principalId, - }), - ), - ); - assert.equal(error._tag, 'PartyCorrectionConflict'); - assert.match(error.reason, /overlaps/u); - assert.equal(h.insertValues.length, 1); - // The failed Effect leaves the enclosing Core transaction to roll back the original transition. - assert.equal(h.insertValues[0]?.['supersedesRelationshipId'], assertionId); -}); - -test('correction of a durable relationship preserves stored alias endpoints', async () => { - const canonicalId = '90000000-0000-4000-8000-000000000001'; - const original = relationshipRow(); - const h = transactionHarness( - [ - [], - [original], - [{ aliasPartyId: partyId, canonicalPartyId: canonicalId, tenantId }], - [], - [{ partyId: canonicalId }], - [], - [{ partyId: organizationId }], - ], - [[relationshipRow({ relationshipId: replacementId })], [{ correctionId }]], - [[original]], - ); - await runEffectTestPromise( - correctPartyFactRecord(h.transaction, tenantId, relationshipCommand, { +); +it.effect( + 'relationship overlap is a typed conflict and no correction journal is written after failed replacement', + () => + Effect.gen(function* correctionScenario7() { + const original = relationshipRow(); + const h = transactionHarness( + [[], [original], [], [{ partyId }], [], [{ partyId: organizationId }]], + [], + [[original]], + { cause: { code: '23P01', constraint: 'party_relationships_no_overlap_excl' } }, + ); + const error = yield* Effect.flip( + correctPartyFactRecord(h.transaction, tenantId, relationshipCommand, { + actionInvocationId, + principalId, + }), + ); + expect(Predicate.isTagged(error, 'PartyCorrectionConflict')).toBe(true); + expect(error.reason).toMatch(/overlaps/u); + expect(h.insertValues.length).toBe(1); + // The failed Effect leaves the enclosing Core transaction to roll back the original transition. + expect(h.insertValues[0]?.['supersedesRelationshipId']).toBe(assertionId); + }), +); +it.effect('correction of a durable relationship preserves stored alias endpoints', () => + Effect.gen(function* correctionScenario8() { + const canonicalId = '90000000-0000-4000-8000-000000000001'; + const original = relationshipRow(); + const h = transactionHarness( + [ + [], + [original], + [{ aliasPartyId: partyId, canonicalPartyId: canonicalId, tenantId }], + [], + [{ partyId: canonicalId }], + [], + [{ partyId: organizationId }], + ], + [[relationshipRow({ relationshipId: replacementId })], [{ correctionId }]], + [[original]], + ); + yield* correctPartyFactRecord(h.transaction, tenantId, relationshipCommand, { actionInvocationId, principalId, - }), - ); - assert.equal(h.insertValues[0]?.['fromPartyId'], partyId); - assert.notEqual(h.insertValues[0]?.['fromPartyId'], canonicalId); -}); - -test('correction history requires reviewer authority; ordinary identity read permission is insufficient', () => { + }); + expect(h.insertValues[0]?.['fromPartyId']).toBe(partyId); + expect(h.insertValues[0]?.['fromPartyId']).not.toBe(canonicalId); + }), +); +it('correction history requires reviewer authority; ordinary identity read permission is insufficient', () => { const target = partyCorrectionPermissionTarget(); - assert.deepEqual(target, { kind: 'tenant', permission: 'review_party_identity' }); - assert.notDeepEqual(target, { kind: 'tenant', permission: 'read_party_identity' }); + expect(target).toEqual({ kind: 'tenant', permission: 'review_party_identity' }); + expect(target).not.toEqual({ kind: 'tenant', permission: 'read_party_identity' }); }); - for (const scenario of [ { name: 'UNRESOLVED Party Type enrichment is rejected before mutation by correction', @@ -457,87 +464,92 @@ for (const scenario of [ reason: /exclusive identifier claims/u, }, ]) { - test(scenario.name, async () => { - const h = transactionHarness([ - [], - [{ partyId }], - [], - [{ partyId }], - [ - { - assertionId, - factKind: 'PARTY_TYPE', - isCurrent: true, - normalizedValue: scenario.original, - partyId, - state: 'ACTIVE', - }, - ], - ...scenario.claimReads, - ]); - const command = decode(PartyCorrectionCommandSchema)({ - ...evidence, - factKind: 'PARTY_TYPE', - partyId, - replacementValue: scenario.replacement, - subjectEvidence: [ - { - basis: 'REVIEWED_DOCUMENT', - evidenceRef: 'record/42', - kind: 'ACTOR_ATTESTATION', - observedSubject: scenario.replacement, - statement: 'Reviewed this external organization', - subjectKey: 'one-subject', - }, - ], - targetAssertionId: assertionId, - }); - const error = await runEffectTestPromise( - Effect.flip( + it.effect(scenario.name, () => + Effect.gen(function* rejectCorrectionScenario() { + const h = transactionHarness([ + [], + [{ partyId }], + [], + [{ partyId }], + [ + { + assertionId, + factKind: 'PARTY_TYPE', + isCurrent: true, + normalizedValue: scenario.original, + partyId, + state: 'ACTIVE', + }, + ], + ...scenario.claimReads, + ]); + const command = decode(PartyCorrectionCommandSchema)({ + ...evidence, + factKind: 'PARTY_TYPE', + partyId, + replacementValue: scenario.replacement, + subjectEvidence: [ + { + basis: 'REVIEWED_DOCUMENT', + evidenceRef: 'record/42', + kind: 'ACTOR_ATTESTATION', + observedSubject: scenario.replacement, + statement: 'Reviewed this external organization', + subjectKey: 'one-subject', + }, + ], + targetAssertionId: assertionId, + }); + const error = yield* Effect.flip( correctPartyFactRecord(h.transaction, tenantId, command, { actionInvocationId, principalId, }), - ), - ); - assert.equal(error._tag, 'PartyCorrectionConflict'); - assert.match(error.reason, scenario.reason); - assert.equal(h.updateSets.length, 0); - assert.equal(h.insertValues.length, 0); - }); + ); + expect(Predicate.isTagged(error, 'PartyCorrectionConflict')).toBe(true); + expect(error.reason).toMatch(scenario.reason); + expect(h.updateSets.length).toBe(0); + expect(h.insertValues.length).toBe(0); + }), + ); } -test('type Correction cannot treat a reviewer decision or source label as subject evidence', async () => { - const h = transactionHarness([ - [], - [{ partyId }], - [], - [{ partyId }], - [ - { - assertionId, +it.effect( + 'type Correction cannot treat a reviewer decision or source label as subject evidence', + () => + Effect.gen(function* correctionScenario10() { + const h = transactionHarness([ + [], + [{ partyId }], + [], + [{ partyId }], + [ + { + assertionId, + factKind: 'PARTY_TYPE', + isCurrent: true, + normalizedValue: 'PERSON', + partyId, + state: 'ACTIVE', + }, + ], + ]); + const command = decode(PartyCorrectionCommandSchema)({ + ...evidence, factKind: 'PARTY_TYPE', - isCurrent: true, - normalizedValue: 'PERSON', partyId, - state: 'ACTIVE', - }, - ], - ]); - const command = decode(PartyCorrectionCommandSchema)({ - ...evidence, - factKind: 'PARTY_TYPE', - partyId, - replacementValue: 'ORGANIZATION', - targetAssertionId: assertionId, - }); - const error = await runEffectTestPromise( - Effect.flip( - correctPartyFactRecord(h.transaction, tenantId, command, { actionInvocationId, principalId }), - ), - ); - assert.equal(error._tag, 'PartyCorrectionConflict'); - assert.equal(error.reason, 'subject_evidence_required'); - assert.equal(h.insertValues.length, 0); - assert.equal(h.updateSets.length, 0); -}); + replacementValue: 'ORGANIZATION', + targetAssertionId: assertionId, + }); + const error = yield* Effect.flip( + correctPartyFactRecord(h.transaction, tenantId, command, { + actionInvocationId, + principalId, + }), + ); + expect(Predicate.isTagged(error, 'PartyCorrectionConflict')).toBe(true); + expect(error.reason).toBe('subject_evidence_required'); + expect(h.insertValues.length).toBe(0); + expect(h.updateSets.length).toBe(0); + }), +); diff --git a/app/verticals/party-registry/tests/unit/cors-origin.test.ts b/app/verticals/party-registry/tests/unit/cors-origin.test.ts index 6754cfd98..9acc22cb1 100644 --- a/app/verticals/party-registry/tests/unit/cors-origin.test.ts +++ b/app/verticals/party-registry/tests/unit/cors-origin.test.ts @@ -1,13 +1,12 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { partyRegistryCorsAllowedOrigins, resolvePartyRegistryShellOrigin, } from '../../api/read-server-support.ts'; -test('Party CORS accepts only the configured nonlocal Shell origin without a localhost fallback', () => { +it('Party CORS accepts only the configured nonlocal Shell origin without a localhost fallback', () => { const shellOrigin = 'https://operations.example.test'; - assert.deepEqual(partyRegistryCorsAllowedOrigins(resolvePartyRegistryShellOrigin(shellOrigin)), [ + expect(partyRegistryCorsAllowedOrigins(resolvePartyRegistryShellOrigin(shellOrigin))).toEqual([ shellOrigin, ]); }); diff --git a/app/verticals/party-registry/tests/unit/counterparty-contract.test.ts b/app/verticals/party-registry/tests/unit/counterparty-contract.test.ts index dba6f3d0d..1f0253137 100644 --- a/app/verticals/party-registry/tests/unit/counterparty-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/counterparty-contract.test.ts @@ -1,6 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Schema } from 'effect'; import { CounterpartyCreatePayloadSchema, CounterpartyCreateResultSchema, @@ -80,8 +79,8 @@ const provenance = { source: 'contracts.core', } as const; -test('declares required Legal Entity scope and Counterparty resource authorization', () => { - assert.deepEqual( +it('declares required Legal Entity scope and Counterparty resource authorization', () => { + expect( [counterpartyCreateAction, counterpartyRoleAddAction, counterpartyRoleEndAction].map( ({ descriptor }) => ({ actionKey: descriptor.actionKey, @@ -91,353 +90,359 @@ test('declares required Legal Entity scope and Counterparty resource authorizati resourcePermission: descriptor.resourcePermission?.kind, }), ), - [ - { - actionKey: 'party.registry.counterparty-create', - idempotency: 'required', - legalEntityPermission: 'manage_counterparty', - legalEntityScope: 'required', - resourcePermission: undefined, - }, - { - actionKey: 'party.registry.counterparty-role-add', - idempotency: 'required', - legalEntityPermission: undefined, - legalEntityScope: 'required', - resourcePermission: 'resource', - }, - { - actionKey: 'party.registry.counterparty-role-end', - idempotency: 'required', - legalEntityPermission: undefined, - legalEntityScope: 'required', - resourcePermission: 'resource', - }, - ], - ); -}); - -test('creates a durable Counterparty without inventing an implicit role', () => { - const payload = Schema.decodeUnknownSync(CounterpartyCreatePayloadSchema, { - onExcessProperty: 'error', - })({ partyRef, provenance }); - assert.deepEqual(payload, { partyRef, provenance }); - assert.throws(() => - Schema.decodeUnknownSync(CounterpartyCreatePayloadSchema, { onExcessProperty: 'error' })({ - partyRef, - provenance, - roleType: 'CUSTOMER', - }), - ); - assert.throws(() => - Schema.decodeUnknownSync(CounterpartyCreatePayloadSchema)({ - partyRef, - provenance: { method: 'SIGNED_CONTRACT', reason: 'Evidence is mandatory.', source: 'test' }, - }), - ); - assert.deepEqual( - Schema.decodeUnknownSync(CounterpartyCreateResultSchema)({ - counterpartyRef, - created: true, - legalEntityRef, - partyRef, - }), - { counterpartyRef, created: true, legalEntityRef, partyRef }, - ); + ).toEqual([ + { + actionKey: 'party.registry.counterparty-create', + idempotency: 'required', + legalEntityPermission: 'manage_counterparty', + legalEntityScope: 'required', + resourcePermission: undefined, + }, + { + actionKey: 'party.registry.counterparty-role-add', + idempotency: 'required', + legalEntityPermission: undefined, + legalEntityScope: 'required', + resourcePermission: 'resource', + }, + { + actionKey: 'party.registry.counterparty-role-end', + idempotency: 'required', + legalEntityPermission: undefined, + legalEntityScope: 'required', + resourcePermission: 'resource', + }, + ]); }); -test('publishes only stable references and bounded lifecycle facts', () => { - assert.deepEqual( - Schema.decodeUnknownSync(CounterpartyCreatedOutboxPayloadSchema, { - onExcessProperty: 'error', - })({ counterpartyRef, legalEntityRef, partyRef }), - { counterpartyRef, legalEntityRef, partyRef }, - ); - const added = { - counterpartyRef, - rolePeriodRef, - roleType: 'SUPPLIER' as const, - validFrom: '2026-09-03T10:00:00.000Z', - validTo: null, - }; - assert.deepEqual( - Schema.decodeUnknownSync(CounterpartyRoleAddedOutboxPayloadSchema)(added), - added, - ); - assert.equal( - Schema.decodeUnknownSync(CounterpartyRoleEndedOutboxPayloadSchema)({ - ...added, - validTo: '2027-01-31T23:59:59.000Z', - }).validTo, - '2027-01-31T23:59:59.000Z', - ); - assert.throws(() => - Schema.decodeUnknownSync(CounterpartyCreatedOutboxPayloadSchema, { +it.effect('creates a durable Counterparty without inventing an implicit role', () => + Effect.gen(function* contractScenario2() { + const payload = yield* Schema.decodeUnknownEffect(CounterpartyCreatePayloadSchema, { onExcessProperty: 'error', - })({ counterpartyRef, displayName: 'ACME', legalEntityRef, partyRef }), - ); -}); + })({ partyRef, provenance }); + expect(payload).toEqual({ partyRef, provenance }); + expect( + yield* Effect.flip( + Schema.decodeUnknownEffect(CounterpartyCreatePayloadSchema, { onExcessProperty: 'error' })({ + partyRef, + provenance, + roleType: 'CUSTOMER', + }), + ), + ).toBeDefined(); + expect( + yield* Effect.flip( + Schema.decodeUnknownEffect(CounterpartyCreatePayloadSchema)({ + partyRef, + provenance: { + method: 'SIGNED_CONTRACT', + reason: 'Evidence is mandatory.', + source: 'test', + }, + }), + ), + ).toBeDefined(); + expect( + yield* Schema.decodeUnknownEffect(CounterpartyCreateResultSchema)({ + counterpartyRef, + created: true, + legalEntityRef, + partyRef, + }), + ).toEqual({ counterpartyRef, created: true, legalEntityRef, partyRef }); + }), +); -test('preserves Counterparty JSON round trips for timestamps, references, and absence', () => { - const timestamp = '2026-09-03T10:00:00.000Z'; - assert.equal( - Schema.encodeSync(CounterpartyIsoTimestampSchema)( - Schema.decodeUnknownSync(CounterpartyIsoTimestampSchema)(timestamp), - ), - timestamp, - ); - assert.deepEqual( - Schema.encodeSync(LegalEntityRefSchema)( - Schema.decodeUnknownSync(LegalEntityRefSchema)(legalEntityRef), - ), - legalEntityRef, - ); +it.effect('publishes only stable references and bounded lifecycle facts', () => + Effect.gen(function* contractScenario3() { + expect( + yield* Schema.decodeUnknownEffect(CounterpartyCreatedOutboxPayloadSchema, { + onExcessProperty: 'error', + })({ counterpartyRef, legalEntityRef, partyRef }), + ).toEqual({ counterpartyRef, legalEntityRef, partyRef }); + const added = { + counterpartyRef, + rolePeriodRef, + roleType: 'SUPPLIER' as const, + validFrom: '2026-09-03T10:00:00.000Z', + validTo: null, + }; + expect( + yield* Schema.decodeUnknownEffect(CounterpartyRoleAddedOutboxPayloadSchema)(added), + ).toEqual(added); + expect( + (yield* Schema.decodeUnknownEffect(CounterpartyRoleEndedOutboxPayloadSchema)({ + ...added, + validTo: '2027-01-31T23:59:59.000Z', + })).validTo, + ).toBe('2027-01-31T23:59:59.000Z'); + expect( + yield* Effect.flip( + Schema.decodeUnknownEffect(CounterpartyCreatedOutboxPayloadSchema, { + onExcessProperty: 'error', + })({ counterpartyRef, displayName: 'ACME', legalEntityRef, partyRef }), + ), + ).toBeDefined(); + }), +); - const roleWithoutEndProvenance = { - provenance, - recordedAt: timestamp, - rolePeriodRef, - roleType: 'CUSTOMER' as const, - state: 'ACTIVE' as const, - validFrom: timestamp, - validTo: null, - }; - assert.deepEqual( - Schema.encodeSync(CounterpartyRolePeriodSchema)( - Schema.decodeUnknownSync(CounterpartyRolePeriodSchema)(roleWithoutEndProvenance), - ), - roleWithoutEndProvenance, - ); - assert.deepEqual( - Schema.encodeSync(CounterpartyRolePeriodSchema)( - Schema.decodeUnknownSync(CounterpartyRolePeriodSchema)({ - ...roleWithoutEndProvenance, - endProvenance: null, - }), - ), - { ...roleWithoutEndProvenance, endProvenance: null }, - ); - assert.deepEqual( - Schema.encodeSync(CounterpartyAuditEvidenceSchema)( - Schema.decodeUnknownSync(CounterpartyAuditEvidenceSchema)({ - evidenceReference: null, - provenanceMethod: provenance.method, - provenanceReason: provenance.reason, - provenanceSource: provenance.source, - }), - ), - { +it.effect('preserves Counterparty JSON round trips for timestamps, references, and absence', () => + Effect.gen(function* contractScenario4() { + const timestamp = '2026-09-03T10:00:00.000Z'; + expect( + yield* Schema.encodeEffect(CounterpartyIsoTimestampSchema)( + yield* Schema.decodeUnknownEffect(CounterpartyIsoTimestampSchema)(timestamp), + ), + ).toBe(timestamp); + expect( + yield* Schema.encodeEffect(LegalEntityRefSchema)( + yield* Schema.decodeUnknownEffect(LegalEntityRefSchema)(legalEntityRef), + ), + ).toEqual(legalEntityRef); + + const roleWithoutEndProvenance = { + provenance, + recordedAt: timestamp, + rolePeriodRef, + roleType: 'CUSTOMER' as const, + state: 'ACTIVE' as const, + validFrom: timestamp, + validTo: null, + }; + expect( + yield* Schema.encodeEffect(CounterpartyRolePeriodSchema)( + yield* Schema.decodeUnknownEffect(CounterpartyRolePeriodSchema)(roleWithoutEndProvenance), + ), + ).toEqual(roleWithoutEndProvenance); + expect( + yield* Schema.encodeEffect(CounterpartyRolePeriodSchema)( + yield* Schema.decodeUnknownEffect(CounterpartyRolePeriodSchema)({ + ...roleWithoutEndProvenance, + endProvenance: null, + }), + ), + ).toEqual({ ...roleWithoutEndProvenance, endProvenance: null }); + expect( + yield* Schema.encodeEffect(CounterpartyAuditEvidenceSchema)( + yield* Schema.decodeUnknownEffect(CounterpartyAuditEvidenceSchema)({ + evidenceReference: null, + provenanceMethod: provenance.method, + provenanceReason: provenance.reason, + provenanceSource: provenance.source, + }), + ), + ).toEqual({ evidenceReference: null, provenanceMethod: provenance.method, provenanceReason: provenance.reason, provenanceSource: provenance.source, - }, - ); - assert.deepEqual( - Schema.encodeSync(CounterpartyPartyProjectionSchema)( - Schema.decodeUnknownSync(CounterpartyPartyProjectionSchema)({ - archived: false, - canonicalPartyRef: partyRef, - displayName: null, - partyType: 'ORGANIZATION', - storedPartyRef: partyRef, - }), - ), - { + }); + expect( + yield* Schema.encodeEffect(CounterpartyPartyProjectionSchema)( + yield* Schema.decodeUnknownEffect(CounterpartyPartyProjectionSchema)({ + archived: false, + canonicalPartyRef: partyRef, + displayName: null, + partyType: 'ORGANIZATION', + storedPartyRef: partyRef, + }), + ), + ).toEqual({ archived: false, canonicalPartyRef: partyRef, displayName: null, partyType: 'ORGANIZATION', storedPartyRef: partyRef, - }, - ); -}); + }); + }), +); -test('accepts only CUSTOMER and SUPPLIER role periods with explicit evidence', () => { - for (const roleType of ['CUSTOMER', 'SUPPLIER'] as const) { - assert.equal( - Schema.decodeUnknownSync(CounterpartyRoleAddPayloadSchema)({ - counterpartyRef, - provenance, - roleType, - validFrom: '2026-09-03T10:00:00.000Z', - }).roleType, - roleType, - ); - } - for (const roleType of ['BUSINESS_PARTNER', 'OTHER']) { - assert.throws(() => - Schema.decodeUnknownSync(CounterpartyRoleAddPayloadSchema)({ - counterpartyRef, - provenance, - roleType, - validFrom: '2026-09-03T10:00:00.000Z', - }), - ); - } - for (const validFrom of ['2026-02-30T00:00:00.000Z', '2026-01-01T00:00:00Z']) { - assert.throws(() => - Schema.decodeUnknownSync(CounterpartyRoleAddPayloadSchema)({ +it.effect('accepts only CUSTOMER and SUPPLIER role periods with explicit evidence', () => + Effect.gen(function* contractScenario5() { + for (const roleType of ['CUSTOMER', 'SUPPLIER'] as const) { + expect( + (yield* Schema.decodeUnknownEffect(CounterpartyRoleAddPayloadSchema)({ + counterpartyRef, + provenance, + roleType, + validFrom: '2026-09-03T10:00:00.000Z', + })).roleType, + ).toBe(roleType); + } + for (const roleType of ['BUSINESS_PARTNER', 'OTHER']) { + expect( + yield* Effect.flip( + Schema.decodeUnknownEffect(CounterpartyRoleAddPayloadSchema)({ + counterpartyRef, + provenance, + roleType, + validFrom: '2026-09-03T10:00:00.000Z', + }), + ), + ).toBeDefined(); + } + for (const validFrom of ['2026-02-30T00:00:00.000Z', '2026-01-01T00:00:00Z']) { + expect( + yield* Effect.flip( + Schema.decodeUnknownEffect(CounterpartyRoleAddPayloadSchema)({ + counterpartyRef, + provenance, + roleType: 'CUSTOMER', + validFrom, + }), + ), + ).toBeDefined(); + } + expect( + yield* Schema.decodeUnknownEffect(CounterpartyRoleAddResultSchema)({ counterpartyRef, - provenance, + rolePeriodRef, roleType: 'CUSTOMER', - validFrom, + validFrom: '2026-09-03T10:00:00.000Z', + validTo: null, }), - ); - } - assert.deepEqual( - Schema.decodeUnknownSync(CounterpartyRoleAddResultSchema)({ + ).toEqual({ counterpartyRef, rolePeriodRef, roleType: 'CUSTOMER', validFrom: '2026-09-03T10:00:00.000Z', validTo: null, - }), - { + }); + expect( + (yield* Schema.decodeUnknownEffect(CounterpartyRoleAddPayloadSchema)({ + counterpartyRef, + provenance: { + evidenceReference: provenance.evidenceReference, + method: provenance.method, + source: provenance.source, + }, + roleType: 'CUSTOMER', + validFrom: '2026-09-03T10:00:00.000Z', + })).provenance.reason, + ).toBe(undefined); + }), +); + +it.effect('ends one named role period without deleting Counterparty history', () => + Effect.gen(function* contractScenario6() { + const payload = yield* Schema.decodeUnknownEffect(CounterpartyRoleEndPayloadSchema)({ counterpartyRef, + provenance, rolePeriodRef, - roleType: 'CUSTOMER', - validFrom: '2026-09-03T10:00:00.000Z', - validTo: null, - }, - ); - assert.equal( - Schema.decodeUnknownSync(CounterpartyRoleAddPayloadSchema)({ + validTo: '2027-01-31T23:59:59.000Z', + }); + expect(payload).toEqual({ counterpartyRef, - provenance: { - evidenceReference: provenance.evidenceReference, - method: provenance.method, - source: provenance.source, - }, - roleType: 'CUSTOMER', - validFrom: '2026-09-03T10:00:00.000Z', - }).provenance.reason, - undefined, - ); -}); + provenance, + rolePeriodRef, + validTo: '2027-01-31T23:59:59.000Z', + }); + expect( + (yield* Schema.decodeUnknownEffect(CounterpartyRoleEndResultSchema)({ + counterpartyRef, + rolePeriodRef, + roleType: 'SUPPLIER', + validFrom: '2026-09-03T10:00:00.000Z', + validTo: '2027-01-31T23:59:59.000Z', + })).validTo, + ).toBe('2027-01-31T23:59:59.000Z'); + expect( + (yield* Schema.decodeUnknownEffect(CounterpartyRoleEndPayloadSchema)({ + counterpartyRef, + provenance: { + evidenceReference: provenance.evidenceReference, + method: 'CONFIRMED_SUPPLIER_RELATIONSHIP_END', + source: provenance.source, + }, + rolePeriodRef, + validTo: '2027-01-31T23:59:59.000Z', + })).provenance.reason, + ).toBe(undefined); + }), +); -test('ends one named role period without deleting Counterparty history', () => { - const payload = Schema.decodeUnknownSync(CounterpartyRoleEndPayloadSchema)({ - counterpartyRef, - provenance, - rolePeriodRef, - validTo: '2027-01-31T23:59:59.000Z', - }); - assert.deepEqual(payload, { - counterpartyRef, - provenance, - rolePeriodRef, - validTo: '2027-01-31T23:59:59.000Z', - }); - assert.equal( - Schema.decodeUnknownSync(CounterpartyRoleEndResultSchema)({ +it.effect('publishes a minimum Party projection and keeps full role history separate', () => + Effect.gen(function* contractScenario7() { + const request = yield* Schema.decodeUnknownEffect(CounterpartyReadRequestSchema)({ counterpartyRef, + }); + expect(request).toEqual({ counterpartyRef }); + const currentRole = { + provenance, + recordedAt: '2026-09-03T10:01:00.000Z', rolePeriodRef, - roleType: 'SUPPLIER', + roleType: 'CUSTOMER', + state: 'ACTIVE', validFrom: '2026-09-03T10:00:00.000Z', - validTo: '2027-01-31T23:59:59.000Z', - }).validTo, - '2027-01-31T23:59:59.000Z', - ); - assert.equal( - Schema.decodeUnknownSync(CounterpartyRoleEndPayloadSchema)({ + validTo: null, + } as const; + const result = yield* Schema.decodeUnknownEffect(CounterpartyReadResponseSchema, { + onExcessProperty: 'error', + })({ counterpartyRef, - provenance: { - evidenceReference: provenance.evidenceReference, - method: 'CONFIRMED_SUPPLIER_RELATIONSHIP_END', - source: provenance.source, + createdAt: '2026-09-03T10:00:00.000Z', + currentRoles: [currentRole], + legalEntityRef, + party: { + archived: false, + canonicalPartyRef: partyRef, + displayName: 'ACME s.r.o.', + partyType: 'ORGANIZATION', + storedPartyRef: partyRef, }, - rolePeriodRef, - validTo: '2027-01-31T23:59:59.000Z', - }).provenance.reason, - undefined, - ); -}); - -test('publishes a minimum Party projection and keeps full role history separate', () => { - const request = Schema.decodeUnknownSync(CounterpartyReadRequestSchema)({ counterpartyRef }); - assert.deepEqual(request, { counterpartyRef }); - const currentRole = { - provenance, - recordedAt: '2026-09-03T10:01:00.000Z', - rolePeriodRef, - roleType: 'CUSTOMER', - state: 'ACTIVE', - validFrom: '2026-09-03T10:00:00.000Z', - validTo: null, - } as const; - const result = Schema.decodeUnknownSync(CounterpartyReadResponseSchema, { - onExcessProperty: 'error', - })({ - counterpartyRef, - createdAt: '2026-09-03T10:00:00.000Z', - currentRoles: [currentRole], - legalEntityRef, - party: { - archived: false, - canonicalPartyRef: partyRef, - displayName: 'ACME s.r.o.', - partyType: 'ORGANIZATION', - storedPartyRef: partyRef, - }, - }); - assert.equal(result.party.displayName, 'ACME s.r.o.'); - assert.equal( - Schema.decodeUnknownSync(CounterpartyReadResponseSchema)({ - ...result, - party: { ...result.party, displayName: null }, - }).party.displayName, - null, - ); - assert.deepEqual( - result.currentRoles.map(({ roleType }) => roleType), - ['CUSTOMER'], - ); - assert.deepEqual( - Schema.decodeUnknownSync(CounterpartyReadResponseSchema)({ - ...result, - currentRoles: [], - }).currentRoles, - [], - ); - assert.throws(() => - Schema.decodeUnknownSync(CounterpartyReadResponseSchema, { onExcessProperty: 'error' })({ - ...result, - party: { ...result.party, contactPoints: [] }, - }), - ); + }); + expect(result.party.displayName).toBe('ACME s.r.o.'); + expect( + (yield* Schema.decodeUnknownEffect(CounterpartyReadResponseSchema)({ + ...result, + party: { ...result.party, displayName: null }, + })).party.displayName, + ).toBe(null); + expect(result.currentRoles.map(({ roleType }) => roleType)).toEqual(['CUSTOMER']); + expect( + (yield* Schema.decodeUnknownEffect(CounterpartyReadResponseSchema)({ + ...result, + currentRoles: [], + })).currentRoles, + ).toEqual([]); + expect( + yield* Effect.flip( + Schema.decodeUnknownEffect(CounterpartyReadResponseSchema, { onExcessProperty: 'error' })({ + ...result, + party: { ...result.party, contactPoints: [] }, + }), + ), + ).toBeDefined(); - assert.deepEqual( - Schema.decodeUnknownSync(CounterpartyRoleHistoryRequestSchema)({ counterpartyRef }), - { counterpartyRef }, - ); - assert.equal( - Schema.decodeUnknownSync(CounterpartyRoleHistoryResponseSchema)({ - counterpartyRef, - roles: [{ ...currentRole, state: 'ENDED', validTo: '2027-01-31T23:59:59.000Z' }], - }).roles[0]?.state, - 'ENDED', - ); - assert.equal(counterpartyReadRead.descriptor.permissionTarget, 'resource'); - assert.equal(counterpartyRoleHistoryRead.descriptor.permissionTarget, 'resource'); - assert.equal(counterpartyReadRead.descriptor.legalEntityScope, 'optional'); - assert.equal(counterpartyRoleHistoryRead.descriptor.legalEntityScope, 'optional'); - assert.deepEqual(counterpartyReadPermissionTarget({ counterpartyRef }), { - kind: 'any_of', - targets: [ - { - kind: 'resource', - resource: { - moduleId: 'party.registry', - resourceId: counterpartyId, - resourceType: 'party.registry.counterparty', + expect( + yield* Schema.decodeUnknownEffect(CounterpartyRoleHistoryRequestSchema)({ counterpartyRef }), + ).toEqual({ counterpartyRef }); + expect( + (yield* Schema.decodeUnknownEffect(CounterpartyRoleHistoryResponseSchema)({ + counterpartyRef, + roles: [{ ...currentRole, state: 'ENDED', validTo: '2027-01-31T23:59:59.000Z' }], + })).roles[0]?.state, + ).toBe('ENDED'); + expect(counterpartyReadRead.descriptor.permissionTarget).toBe('resource'); + expect(counterpartyRoleHistoryRead.descriptor.permissionTarget).toBe('resource'); + expect(counterpartyReadRead.descriptor.legalEntityScope).toBe('optional'); + expect(counterpartyRoleHistoryRead.descriptor.legalEntityScope).toBe('optional'); + expect(counterpartyReadPermissionTarget({ counterpartyRef })).toEqual({ + kind: 'any_of', + targets: [ + { + kind: 'resource', + resource: { + moduleId: 'party.registry', + resourceId: counterpartyId, + resourceType: 'party.registry.counterparty', + }, }, - }, - { kind: 'tenant', permission: 'manage_party_identity' }, - ], - }); - assert.deepEqual( - counterpartyRoleHistoryPermissionTarget({ counterpartyRef }), - counterpartyReadPermissionTarget({ counterpartyRef }), - ); -}); + { kind: 'tenant', permission: 'manage_party_identity' }, + ], + }); + expect(counterpartyRoleHistoryPermissionTarget({ counterpartyRef })).toEqual( + counterpartyReadPermissionTarget({ counterpartyRef }), + ); + }), +); diff --git a/app/verticals/party-registry/tests/unit/counterparty-persistence.service.test.ts b/app/verticals/party-registry/tests/unit/counterparty-persistence.service.test.ts index f81677f28..13268de0f 100644 --- a/app/verticals/party-registry/tests/unit/counterparty-persistence.service.test.ts +++ b/app/verticals/party-registry/tests/unit/counterparty-persistence.service.test.ts @@ -1,10 +1,9 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { DateTime, Effect } from 'effect'; +import { TestClock } from 'effect/testing'; +import { expect, it } from 'effect-rstest'; +import { DateTime, Effect, Predicate, Struct } from 'effect'; /* eslint-disable anti-slop/no-chained-type-assertions, anti-slop/no-unsafe-dictionary-type -- This focused test harness models the narrow Drizzle native Effect query surface used by the owner-local service. expires: 2026-12-31. */ import type { Table } from 'drizzle-orm'; import { getTableName } from 'drizzle-orm'; -import assert from 'node:assert/strict'; -import test from 'node:test'; import { addCounterpartyRoleRecord, createCounterpartyRecord, @@ -146,159 +145,202 @@ const endInput = (validTo: string, method: string) => ({ validTo, }); -test('keeps a future-ended role active until its exclusive effective end', () => { - const futureEnd = '2099-01-01T00:00:00.000Z'; - const updated = roleRow({ - endEvidenceRefs: ['contract:end'], - endProvenanceMethod: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', - endProvenanceSource: 'contracts.core', - endReason: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', - endedByActionInvocationId: actionInvocationId, - endedByPrincipalId: principalId, - endedRecordedAt: date('2026-09-03T00:00:00.000Z'), - validTo: date(futureEnd), - }); - const harness = transactionHarness([[counterpartyRow], [roleRow()]], [[updated]]); - - return runEffectTestPromise( - endCounterpartyRoleRecord( +it.effect('keeps a future-ended role active until its exclusive effective end', () => + Effect.gen(function* testScenario1() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T00:00:00.000Z')), + ); + const futureEnd = '2099-01-01T00:00:00.000Z'; + const updated = roleRow({ + endEvidenceRefs: ['contract:end'], + endProvenanceMethod: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', + endProvenanceSource: 'contracts.core', + endReason: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', + endedByActionInvocationId: actionInvocationId, + endedByPrincipalId: principalId, + endedRecordedAt: date('2026-09-03T00:00:00.000Z'), + validTo: date(futureEnd), + }); + const harness = transactionHarness([[counterpartyRow], [roleRow()]], [[updated]]); + + const result = yield* endCounterpartyRoleRecord( harness.transaction, endInput(futureEnd, 'CONFIRMED_CUSTOMER_RELATIONSHIP_END'), - ), - ).then((result) => { - assert.equal(result._tag, 'found'); - assert.equal(harness.updateSets[0]?.['state'], 'ACTIVE'); - assert.equal(harness.updateSets[0]?.['isCurrent'], true); - assert.equal(harness.updateSets[0]?.['endProvenanceSource'], 'contracts.core'); - assert.equal( - harness.updateSets[0]?.['endProvenanceMethod'], + ); + + expect(Predicate.isTagged(result, 'found')).toBe(true); + if (!Predicate.isTagged(result, 'found')) { + return yield* Effect.die(new Error('Unexpected result variant')); + } + expect(harness.updateSets[0]?.['state']).toBe('ACTIVE'); + expect(harness.updateSets[0]?.['isCurrent']).toBe(true); + expect(harness.updateSets[0]?.['endProvenanceSource']).toBe('contracts.core'); + expect(harness.updateSets[0]?.['endProvenanceMethod']).toBe( 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', ); - assert.equal(harness.insertValues.length, 2); - assert.equal( - harness.insertValues[1]?.['endProvenanceMethod'], + expect(harness.insertValues.length).toBe(2); + expect(harness.insertValues[1]?.['endProvenanceMethod']).toBe( 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', ); - }); -}); + }), +); + +it.effect('records a retrospective end as historical without deleting the role period', () => + Effect.gen(function* testScenario2() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T00:00:00.000Z')), + ); + const pastEnd = '2021-01-01T00:00:00.000Z'; + const updated = roleRow({ + endEvidenceRefs: ['contract:end'], + endProvenanceMethod: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', + endProvenanceSource: 'contracts.core', + endReason: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', + state: 'ENDED', + validTo: date(pastEnd), + }); + const harness = transactionHarness([[counterpartyRow], [roleRow()]], [[updated]]); -test('records a retrospective end as historical without deleting the role period', () => { - const pastEnd = '2021-01-01T00:00:00.000Z'; - const updated = roleRow({ - endEvidenceRefs: ['contract:end'], - endProvenanceMethod: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', - endProvenanceSource: 'contracts.core', - endReason: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', - state: 'ENDED', - validTo: date(pastEnd), - }); - const harness = transactionHarness([[counterpartyRow], [roleRow()]], [[updated]]); - - return runEffectTestPromise( - endCounterpartyRoleRecord( + yield* endCounterpartyRoleRecord( harness.transaction, endInput(pastEnd, 'CONFIRMED_CUSTOMER_RELATIONSHIP_END'), - ), - ).then(() => { - assert.equal(harness.updateSets[0]?.['state'], 'ENDED'); - assert.equal(harness.updateSets[0]?.['isCurrent'], false); - }); -}); + ); -test('rejects inactivity evidence before persisting a CUSTOMER end', () => { - const harness = transactionHarness([[counterpartyRow], [roleRow()]]); + expect(harness.updateSets[0]?.['state']).toBe('ENDED'); + expect(harness.updateSets[0]?.['isCurrent']).toBe(false); + }), +); + +it.effect('rejects inactivity evidence before persisting a CUSTOMER end', () => + Effect.gen(function* testScenario3() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T00:00:00.000Z')), + ); + const harness = transactionHarness([[counterpartyRow], [roleRow()]]); - return runEffectTestPromise( - endCounterpartyRoleRecord( + const result = yield* endCounterpartyRoleRecord( harness.transaction, endInput('2027-01-01T00:00:00.000Z', 'ENGAGEMENT_INACTIVITY'), - ), - ).then((result) => { - assert.deepEqual(result, { - _tag: 'evidence_insufficient', + ); + + expect(Predicate.isTagged(result, 'evidence_insufficient')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ method: 'ENGAGEMENT_INACTIVITY', roleType: 'CUSTOMER', }); - assert.equal(harness.updateSets.length, 0); - }); -}); + expect(harness.updateSets.length).toBe(0); + }), +); -test('reuses an exactly repeated end without another write', () => { - const validTo = '2025-01-01T00:00:00.000Z'; - const ended = roleRow({ - endEvidenceRefs: ['contract:end'], - endProvenanceMethod: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', - endProvenanceSource: 'contracts.core', - endReason: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', - state: 'ENDED', - validTo: date(validTo), - }); - const harness = transactionHarness([[counterpartyRow], [ended]]); - - return runEffectTestPromise( - endCounterpartyRoleRecord( +it.effect('reuses an exactly repeated end without another write', () => + Effect.gen(function* testScenario4() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T00:00:00.000Z')), + ); + const validTo = '2025-01-01T00:00:00.000Z'; + const ended = roleRow({ + endEvidenceRefs: ['contract:end'], + endProvenanceMethod: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', + endProvenanceSource: 'contracts.core', + endReason: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', + state: 'ENDED', + validTo: date(validTo), + }); + const harness = transactionHarness([[counterpartyRow], [ended]]); + + const result = yield* endCounterpartyRoleRecord( harness.transaction, endInput(validTo, 'CONFIRMED_CUSTOMER_RELATIONSHIP_END'), - ), - ).then((result) => { - assert.equal(result._tag, 'found'); - assert.equal(result.changed, false); - assert.equal(harness.updateSets.length, 0); - }); -}); + ); + + expect(Predicate.isTagged(result, 'found')).toBe(true); + if (!Predicate.isTagged(result, 'found')) { + return yield* Effect.die(new Error('Unexpected result variant')); + } + expect(result.changed).toBe(false); + expect(harness.updateSets.length).toBe(0); + }), +); + +it.effect('reads end provenance independently from the role-add provenance', () => + Effect.gen(function* testScenario5() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T00:00:00.000Z')), + ); + const ended = roleRow({ + endEvidenceRefs: ['contract:end'], + endProvenanceMethod: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', + endProvenanceSource: 'customer-offboarding.core', + endReason: 'Customer agreement terminated', + state: 'ENDED', + validTo: date('2025-01-01T00:00:00.000Z'), + }); + const harness = transactionHarness([[counterpartyRow], [ended]]); -test('reads end provenance independently from the role-add provenance', () => { - const ended = roleRow({ - endEvidenceRefs: ['contract:end'], - endProvenanceMethod: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', - endProvenanceSource: 'customer-offboarding.core', - endReason: 'Customer agreement terminated', - state: 'ENDED', - validTo: date('2025-01-01T00:00:00.000Z'), - }); - const harness = transactionHarness([[counterpartyRow], [ended]]); - - return runEffectTestPromise( - listCounterpartyRoleHistory(harness.transaction, tenantId, legalEntityId, counterpartyId), - ).then((result) => { - assert.equal(result._tag, 'found'); - assert.deepEqual(result.value[0]?.endProvenance, { + const result = yield* listCounterpartyRoleHistory( + harness.transaction, + tenantId, + legalEntityId, + counterpartyId, + ); + + expect(Predicate.isTagged(result, 'found')).toBe(true); + if (!Predicate.isTagged(result, 'found')) { + return yield* Effect.die(new Error('Unexpected result variant')); + } + expect(result.value[0]?.endProvenance).toEqual({ evidenceReference: 'contract:end', method: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', reason: 'Customer agreement terminated', source: 'customer-offboarding.core', }); - }); -}); + }), +); -test('allows the authorized tenant-admin path to read history without payload Legal Entity data', () => { - const harness = transactionHarness([ - [{ ...counterpartyRow, storedPartyId: partyId }], - [roleRow()], - ]); - - return runEffectTestPromise( - listCounterpartyRoleHistory(harness.transaction, tenantId, undefined, counterpartyId), - ).then((result) => { - assert.equal(result._tag, 'found'); - assert.equal(result.value[0]?.roleType, 'CUSTOMER'); - assert.deepEqual(harness.selectedTables, [ - 'counterparty_admin_read_models', - 'counterparty_role_admin_read_models', - ]); - }); -}); +it.effect( + 'allows the authorized tenant-admin path to read history without payload Legal Entity data', + () => + Effect.gen(function* testScenario6() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T00:00:00.000Z')), + ); + const harness = transactionHarness([ + [{ ...counterpartyRow, storedPartyId: partyId }], + [roleRow()], + ]); + + const result = yield* listCounterpartyRoleHistory( + harness.transaction, + tenantId, + undefined, + counterpartyId, + ); + + expect(Predicate.isTagged(result, 'found')).toBe(true); + if (!Predicate.isTagged(result, 'found')) { + return yield* Effect.die(new Error('Unexpected result variant')); + } + expect(result.value[0]?.roleType).toBe('CUSTOMER'); + expect(harness.selectedTables).toEqual([ + 'counterparty_admin_read_models', + 'counterparty_role_admin_read_models', + ]); + }), +); -test('rejects an alias Party create target with canonical survivor guidance', () => { - const survivorId = '40000000-0000-4000-8000-000000000002'; - const harness = transactionHarness([ - [{ aliasPartyId: partyId, canonicalPartyId: survivorId, tenantId }], - [], - [{ partyId: survivorId }], - ]); +it.effect('rejects an alias Party create target with canonical survivor guidance', () => + Effect.gen(function* testScenario7() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T00:00:00.000Z')), + ); + const survivorId = '40000000-0000-4000-8000-000000000002'; + const harness = transactionHarness([ + [{ aliasPartyId: partyId, canonicalPartyId: survivorId, tenantId }], + [], + [{ partyId: survivorId }], + ]); - return runEffectTestPromise( - createCounterpartyRecord(harness.transaction, { + const result = yield* createCounterpartyRecord(harness.transaction, { actionInvocationId, legalEntityId, partyId, @@ -311,57 +353,76 @@ test('rejects an alias Party create target with canonical survivor guidance', () source: 'contracts.core', }, tenantId, - }), - ).then((result) => { - assert.equal(result._tag, 'party_alias'); - assert.equal(result.canonicalPartyRef.resourceId, survivorId); - assert.equal(harness.insertValues.length, 0); - }); -}); + }); -test('admin detail follows a complete Party alias chain while retaining the stored reference', () => { - const middleId = '40000000-0000-4000-8000-000000000002'; - const survivorId = '40000000-0000-4000-8000-000000000003'; - const harness = transactionHarness([ - [{ ...counterpartyRow, storedPartyId: partyId }], - [{ aliasPartyId: partyId, canonicalPartyId: middleId, tenantId }], - [{ aliasPartyId: middleId, canonicalPartyId: survivorId, tenantId }], - [], - [{ partyId: survivorId }], - [ - { - archivedAt: null, - currentDisplayName: 'Survivor', - currentType: 'ORGANIZATION', - partyId: survivorId, + expect(Predicate.isTagged(result, 'party_alias')).toBe(true); + if (!Predicate.isTagged(result, 'party_alias')) { + return yield* Effect.die(new Error('Unexpected result variant')); + } + expect(result.canonicalPartyRef.resourceId).toBe(survivorId); + expect(harness.insertValues.length).toBe(0); + }), +); + +it.effect( + 'admin detail follows a complete Party alias chain while retaining the stored reference', + () => + Effect.gen(function* testScenario8() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T00:00:00.000Z')), + ); + const middleId = '40000000-0000-4000-8000-000000000002'; + const survivorId = '40000000-0000-4000-8000-000000000003'; + const harness = transactionHarness([ + [{ ...counterpartyRow, storedPartyId: partyId }], + [{ aliasPartyId: partyId, canonicalPartyId: middleId, tenantId }], + [{ aliasPartyId: middleId, canonicalPartyId: survivorId, tenantId }], + [], + [{ partyId: survivorId }], + [ + { + archivedAt: null, + currentDisplayName: 'Survivor', + currentType: 'ORGANIZATION', + partyId: survivorId, + tenantId, + }, + ], + [], + ]); + + const result = yield* findCounterpartyRecord( + harness.transaction, tenantId, - }, - ], - [], - ]); - - return runEffectTestPromise( - findCounterpartyRecord(harness.transaction, tenantId, undefined, counterpartyId), - ).then((result) => { - assert.equal(result._tag, 'found'); - assert.equal(result.value.party.storedPartyRef.resourceId, partyId); - assert.equal(result.value.party.canonicalPartyRef.resourceId, survivorId); - assert.equal(result.value.legalEntityRef.resourceId, legalEntityId); - assert.equal(harness.selectedTables.includes('counterparties'), false); - assert.equal(harness.selectedTables.includes('counterparty_role_periods'), false); - }); -}); + undefined, + counterpartyId, + ); + + expect(Predicate.isTagged(result, 'found')).toBe(true); + if (!Predicate.isTagged(result, 'found')) { + return yield* Effect.die(new Error('Unexpected result variant')); + } + expect(result.value.party.storedPartyRef.resourceId).toBe(partyId); + expect(result.value.party.canonicalPartyRef.resourceId).toBe(survivorId); + expect(result.value.legalEntityRef.resourceId).toBe(legalEntityId); + expect(harness.selectedTables.includes('counterparties')).toBe(false); + expect(harness.selectedTables.includes('counterparty_role_periods')).toBe(false); + }), +); -test('creates the tenant-admin snapshot atomically without creating an implicit role', () => { - const party = { archivedAt: null, partyId, tenantId }; - const harness = transactionHarness( - [[], [{ partyId }], [], [{ partyId }], [party]], - [], - [[counterpartyRow]], - ); +it.effect('creates the tenant-admin snapshot atomically without creating an implicit role', () => + Effect.gen(function* testScenario9() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T00:00:00.000Z')), + ); + const party = { archivedAt: null, partyId, tenantId }; + const harness = transactionHarness( + [[], [{ partyId }], [], [{ partyId }], [party]], + [], + [[counterpartyRow]], + ); - return runEffectTestPromise( - createCounterpartyRecord(harness.transaction, { + const result = yield* createCounterpartyRecord(harness.transaction, { actionInvocationId, legalEntityId, partyId, @@ -374,25 +435,31 @@ test('creates the tenant-admin snapshot atomically without creating an implicit source: 'contracts.core', }, tenantId, - }), - ).then((result) => { - assert.equal(result._tag, 'found'); - assert.deepEqual(harness.insertedTables, ['counterparties', 'counterparty_admin_read_models']); - assert.equal(harness.insertValues[1]?.['storedPartyId'], partyId); - }); -}); + }); -test('adds a future role and its admin history projection in the same transaction seam', () => { - const futureStart = '2099-01-01T00:00:00.000Z'; - const futureRole = roleRow({ isCurrent: false, validFrom: date(futureStart) }); - const harness = transactionHarness( - [[counterpartyRow], [], [{ partyId }], [{ archivedAt: null, partyId, tenantId }], []], - [], - [[futureRole]], - ); - - return runEffectTestPromise( - addCounterpartyRoleRecord(harness.transaction, { + expect(Predicate.isTagged(result, 'found')).toBe(true); + if (!Predicate.isTagged(result, 'found')) { + return yield* Effect.die(new Error('Unexpected result variant')); + } + expect(harness.insertedTables).toEqual(['counterparties', 'counterparty_admin_read_models']); + expect(harness.insertValues[1]?.['storedPartyId']).toBe(partyId); + }), +); + +it.effect('adds a future role and its admin history projection in the same transaction seam', () => + Effect.gen(function* testScenario10() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T00:00:00.000Z')), + ); + const futureStart = '2099-01-01T00:00:00.000Z'; + const futureRole = roleRow({ isCurrent: false, validFrom: date(futureStart) }); + const harness = transactionHarness( + [[counterpartyRow], [], [{ partyId }], [{ archivedAt: null, partyId, tenantId }], []], + [], + [[futureRole]], + ); + + const result = yield* addCounterpartyRoleRecord(harness.transaction, { actionInvocationId, counterpartyId, legalEntityId, @@ -407,14 +474,17 @@ test('adds a future role and its admin history projection in the same transactio tenantId, validFrom: futureStart, validTo: null, - }), - ).then((result) => { - assert.equal(result._tag, 'found'); - assert.equal(harness.insertValues[0]?.['isCurrent'], false); - assert.deepEqual(harness.insertedTables, [ + }); + + expect(Predicate.isTagged(result, 'found')).toBe(true); + if (!Predicate.isTagged(result, 'found')) { + return yield* Effect.die(new Error('Unexpected result variant')); + } + expect(harness.insertValues[0]?.['isCurrent']).toBe(false); + expect(harness.insertedTables).toEqual([ 'counterparty_role_periods', 'counterparty_admin_read_models', 'counterparty_role_admin_read_models', ]); - }); -}); + }), +); diff --git a/app/verticals/party-registry/tests/unit/counterparty-read-support.test.ts b/app/verticals/party-registry/tests/unit/counterparty-read-support.test.ts index 3dc660bad..c1fac147e 100644 --- a/app/verticals/party-registry/tests/unit/counterparty-read-support.test.ts +++ b/app/verticals/party-registry/tests/unit/counterparty-read-support.test.ts @@ -1,8 +1,6 @@ import { ReadHandlerNotFound, ReadHandlerUnavailable } from '@app/core-runtime'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import { Effect, Schema } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { assert, it } from 'effect-rstest'; import { CounterpartyPersistenceUnavailable } from '../../shared/domain/counterparty-errors.ts'; import { resolveCounterpartyRead } from '../../src/api/counterparty-read-support.ts'; @@ -14,67 +12,64 @@ const ref = { } as const; const reason = 'Counterparty persistence is temporarily unavailable'; -test('cross-tenant counterparty reads never resolve the persistence service', () => - runEffectTestPromise( - Effect.gen(function* rejectCrossTenant() { - let calls = 0; - const failure = yield* resolveCounterpartyRead( - ref, - 'another-tenant', - () => { - calls += 1; - return Effect.succeed({ _tag: 'not_found' } as const); - }, - reason, - ).pipe(Effect.flip); - assert.equal(calls, 0); - assert.ok(Schema.is(ReadHandlerNotFound)(failure)); - assert.equal(failure.reason, 'The Counterparty does not exist in the trusted Tenant'); - }), - )); +it.effect('cross-tenant counterparty reads never resolve the persistence service', () => + Effect.gen(function* rejectCrossTenant() { + let calls = 0; + const failure = yield* resolveCounterpartyRead( + ref, + 'another-tenant', + () => { + calls += 1; + return Effect.succeed({ _tag: 'not_found' } as const); + }, + reason, + ).pipe(Effect.flip); + assert.equal(calls, 0); + assert.ok(Schema.is(ReadHandlerNotFound)(failure)); + assert.equal(failure.reason, 'The Counterparty does not exist in the trusted Tenant'); + }), +); -test('counterparty lookup preserves found values and authorized-context absence', () => - runEffectTestPromise( - Effect.gen(function* preserveLookupResult() { - const value = [{ role: 'CUSTOMER' }]; - const found = yield* resolveCounterpartyRead( - ref, - ref.tenantId, - (id) => { - assert.equal(id, ref.resourceId); - return Effect.succeed({ _tag: 'found', value } as const); - }, - reason, - ); - assert.equal(found, value); - const missing = yield* resolveCounterpartyRead( - ref, - ref.tenantId, - () => Effect.succeed({ _tag: 'not_found' } as const), - reason, - ).pipe(Effect.flip); - assert.ok(Schema.is(ReadHandlerNotFound)(missing)); - assert.equal(missing.reason, 'The Counterparty does not exist in the authorized context'); - }), - )); +it.effect('counterparty lookup preserves found values and authorized-context absence', () => + Effect.gen(function* preserveLookupResult() { + const value = [{ role: 'CUSTOMER' }]; + const found = yield* resolveCounterpartyRead( + ref, + ref.tenantId, + (id) => { + assert.equal(id, ref.resourceId); + return Effect.succeed({ _tag: 'found', value } as const); + }, + reason, + ); + assert.equal(found, value); + const missing = yield* resolveCounterpartyRead( + ref, + ref.tenantId, + () => Effect.succeed({ _tag: 'not_found' } as const), + reason, + ).pipe(Effect.flip); + assert.ok(Schema.is(ReadHandlerNotFound)(missing)); + assert.equal(missing.reason, 'The Counterparty does not exist in the authorized context'); + }), +); -test('counterparty failures preserve the per-read reason and nonenumerable cause', () => - runEffectTestPromise( - Effect.gen(function* preserveFailureCause() { - const cause = new CounterpartyPersistenceUnavailable({ - code: 'counterparty_persistence_unavailable', - reason: 'database unavailable', - }); - const historyReason = 'Counterparty Role history is temporarily unavailable'; - const failure = yield* resolveCounterpartyRead( - ref, - ref.tenantId, - () => Effect.fail(cause), - historyReason, - ).pipe(Effect.flip); - assert.ok(Schema.is(ReadHandlerUnavailable)(failure)); - assert.equal(failure.reason, historyReason); - assert.equal(failure.cause, cause); - assert.equal(Object.getOwnPropertyDescriptor(failure, 'cause')?.enumerable, false); - }), - )); +it.effect('counterparty failures preserve the per-read reason and nonenumerable cause', () => + Effect.gen(function* preserveFailureCause() { + const cause = new CounterpartyPersistenceUnavailable({ + code: 'counterparty_persistence_unavailable', + reason: 'database unavailable', + }); + const historyReason = 'Counterparty Role history is temporarily unavailable'; + const failure = yield* resolveCounterpartyRead( + ref, + ref.tenantId, + () => Effect.fail(cause), + historyReason, + ).pipe(Effect.flip); + assert.ok(Schema.is(ReadHandlerUnavailable)(failure)); + assert.equal(failure.reason, historyReason); + assert.equal(failure.cause, cause); + assert.equal(Object.getOwnPropertyDescriptor(failure, 'cause')?.enumerable, false); + }), +); diff --git a/app/verticals/party-registry/tests/unit/counterparty-role-lifecycle.test.ts b/app/verticals/party-registry/tests/unit/counterparty-role-lifecycle.test.ts index 10ca16527..cfc552631 100644 --- a/app/verticals/party-registry/tests/unit/counterparty-role-lifecycle.test.ts +++ b/app/verticals/party-registry/tests/unit/counterparty-role-lifecycle.test.ts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { counterpartyContextEvidenceIsSufficient, roleEvidenceIsSufficient, @@ -9,49 +8,43 @@ import { rolePeriodsOverlap, } from '../../shared/domain/counterparty-role-period.ts'; -test('requires evidence that establishes a commercial context rather than mere discovery', () => { +it('requires evidence that establishes a commercial context rather than mere discovery', () => { for (const method of ['ENGAGEMENT_LEAD', 'SEARCH_RESULT', 'TECHNICAL_REFERENCE']) { - assert.equal(counterpartyContextEvidenceIsSufficient(method), false); + expect(counterpartyContextEvidenceIsSufficient(method)).toBe(false); } for (const method of ['SIGNED_CONTRACT', 'APPROVED_COMMERCIAL_RELATIONSHIP', 'BINDING_ORDER']) { - assert.equal(counterpartyContextEvidenceIsSufficient(method), true); + expect(counterpartyContextEvidenceIsSufficient(method)).toBe(true); } }); -test('applies CUSTOMER and SUPPLIER evidence thresholds without waiting for first completion', () => { - assert.equal(roleEvidenceIsSufficient('CUSTOMER', 'ENGAGEMENT_PROSPECT'), false); - assert.equal(roleEvidenceIsSufficient('CUSTOMER', 'BINDING_ORDER'), true); - assert.equal(roleEvidenceIsSufficient('CUSTOMER', 'APPROVED_PURCHASING_RELATIONSHIP'), true); - assert.equal(roleEvidenceIsSufficient('SUPPLIER', 'VENDOR_CANDIDATE'), false); - assert.equal(roleEvidenceIsSufficient('SUPPLIER', 'COMPLETED_VENDOR_ONBOARDING'), true); - assert.equal(roleEvidenceIsSufficient('SUPPLIER', 'BINDING_PURCHASE_ORDER'), true); +it('applies CUSTOMER and SUPPLIER evidence thresholds without waiting for first completion', () => { + expect(roleEvidenceIsSufficient('CUSTOMER', 'ENGAGEMENT_PROSPECT')).toBe(false); + expect(roleEvidenceIsSufficient('CUSTOMER', 'BINDING_ORDER')).toBe(true); + expect(roleEvidenceIsSufficient('CUSTOMER', 'APPROVED_PURCHASING_RELATIONSHIP')).toBe(true); + expect(roleEvidenceIsSufficient('SUPPLIER', 'VENDOR_CANDIDATE')).toBe(false); + expect(roleEvidenceIsSufficient('SUPPLIER', 'COMPLETED_VENDOR_ONBOARDING')).toBe(true); + expect(roleEvidenceIsSufficient('SUPPLIER', 'BINDING_PURCHASE_ORDER')).toBe(true); }); -test('requires explicit relationship-end evidence and rejects operational inactivity', () => { - assert.equal(roleEndEvidenceIsSufficient('CUSTOMER', 'ENGAGEMENT_INACTIVITY'), false); - assert.equal(roleEndEvidenceIsSufficient('CUSTOMER', 'TRANSACTION_INACTIVITY'), false); - assert.equal( - roleEndEvidenceIsSufficient('CUSTOMER', 'CONFIRMED_CUSTOMER_RELATIONSHIP_END'), - true, - ); - assert.equal(roleEndEvidenceIsSufficient('SUPPLIER', 'TEMPORARY_PROCUREMENT_BLOCK'), false); - assert.equal(roleEndEvidenceIsSufficient('SUPPLIER', 'PURCHASE_SUSPENSION'), false); - assert.equal( - roleEndEvidenceIsSufficient('SUPPLIER', 'CONFIRMED_SUPPLIER_RELATIONSHIP_END'), - true, - ); +it('requires explicit relationship-end evidence and rejects operational inactivity', () => { + expect(roleEndEvidenceIsSufficient('CUSTOMER', 'ENGAGEMENT_INACTIVITY')).toBe(false); + expect(roleEndEvidenceIsSufficient('CUSTOMER', 'TRANSACTION_INACTIVITY')).toBe(false); + expect(roleEndEvidenceIsSufficient('CUSTOMER', 'CONFIRMED_CUSTOMER_RELATIONSHIP_END')).toBe(true); + expect(roleEndEvidenceIsSufficient('SUPPLIER', 'TEMPORARY_PROCUREMENT_BLOCK')).toBe(false); + expect(roleEndEvidenceIsSufficient('SUPPLIER', 'PURCHASE_SUSPENSION')).toBe(false); + expect(roleEndEvidenceIsSufficient('SUPPLIER', 'CONFIRMED_SUPPLIER_RELATIONSHIP_END')).toBe(true); }); -test('derives current role state from lifecycle and effective time', () => { +it('derives current role state from lifecycle and effective time', () => { const active = { state: 'ACTIVE' as const, validFrom: '2026-01-01T00:00:00.000Z', validTo: '2027-01-01T00:00:00.000Z', }; - assert.equal(rolePeriodIsCurrentAt(active, '2025-12-31T23:59:59.000Z'), false); - assert.equal(rolePeriodIsCurrentAt(active, '2026-06-01T00:00:00.000Z'), true); - assert.equal(rolePeriodIsCurrentAt(active, '2027-01-01T00:00:00.000Z'), false); - assert.equal( + expect(rolePeriodIsCurrentAt(active, '2025-12-31T23:59:59.000Z')).toBe(false); + expect(rolePeriodIsCurrentAt(active, '2026-06-01T00:00:00.000Z')).toBe(true); + expect(rolePeriodIsCurrentAt(active, '2027-01-01T00:00:00.000Z')).toBe(false); + expect( rolePeriodIsCurrentAt( { state: 'ACTIVE', @@ -60,23 +53,20 @@ test('derives current role state from lifecycle and effective time', () => { }, '2026-06-01T00:00:00.000Z', ), - false, - ); - assert.equal( - rolePeriodIsCurrentAt({ ...active, state: 'ENDED' }, '2026-06-01T00:00:00.000Z'), + ).toBe(false); + expect(rolePeriodIsCurrentAt({ ...active, state: 'ENDED' }, '2026-06-01T00:00:00.000Z')).toBe( false, ); }); -test('stores future, current, future-ended, and historical periods by their interval', () => { - assert.deepEqual( +it('stores future, current, future-ended, and historical periods by their interval', () => { + expect( rolePeriodStorageStateAt( { validFrom: '2027-01-01T00:00:00.000Z', validTo: null }, '2026-06-01T00:00:00.000Z', ), - { isCurrent: false, state: 'ACTIVE' }, - ); - assert.deepEqual( + ).toEqual({ isCurrent: false, state: 'ACTIVE' }); + expect( rolePeriodStorageStateAt( { validFrom: '2026-01-01T00:00:00.000Z', @@ -84,9 +74,8 @@ test('stores future, current, future-ended, and historical periods by their inte }, '2026-06-01T00:00:00.000Z', ), - { isCurrent: true, state: 'ACTIVE' }, - ); - assert.deepEqual( + ).toEqual({ isCurrent: true, state: 'ACTIVE' }); + expect( rolePeriodStorageStateAt( { validFrom: '2026-01-01T00:00:00.000Z', @@ -94,27 +83,24 @@ test('stores future, current, future-ended, and historical periods by their inte }, '2027-01-01T00:00:00.000Z', ), - { isCurrent: false, state: 'ENDED' }, - ); + ).toEqual({ isCurrent: false, state: 'ENDED' }); }); -test('rejects overlapping periods of the same role while allowing adjacent reactivation', () => { +it('rejects overlapping periods of the same role while allowing adjacent reactivation', () => { const historical = { validFrom: '2025-01-01T00:00:00.000Z', validTo: '2026-01-01T00:00:00.000Z', }; - assert.equal( + expect( rolePeriodsOverlap(historical, { validFrom: '2025-12-01T00:00:00.000Z', validTo: null, }), - true, - ); - assert.equal( + ).toBe(true); + expect( rolePeriodsOverlap(historical, { validFrom: '2026-01-01T00:00:00.000Z', validTo: null, }), - false, - ); + ).toBe(false); }); diff --git a/app/verticals/party-registry/tests/unit/database-client.test.ts b/app/verticals/party-registry/tests/unit/database-client.test.ts index 017ab6189..cf35cf86b 100644 --- a/app/verticals/party-registry/tests/unit/database-client.test.ts +++ b/app/verticals/party-registry/tests/unit/database-client.test.ts @@ -1,28 +1,25 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Predicate } from 'effect'; import { acquirePoolResource, makePartyDatabase } from '../../src/db/client.ts'; -test('finalizes the Party Registry pool when its Effect scope closes', async () => { - let finalized = false; - await runEffectTestPromise( - Effect.scoped( +it.effect('finalizes the Party Registry pool when its Effect scope closes', () => + Effect.gen(function* testScenario1() { + let finalized = false; + yield* Effect.scoped( acquirePoolResource(() => ({ end: () => { finalized = true; return Promise.resolve(); }, })), - ), - ); - assert.equal(finalized, true); -}); + ); + expect(finalized).toBe(true); + }), +); -test('keeps Party Registry pool acquisition failure in the typed error channel', async () => { - const error = await runEffectTestPromise( - Effect.flip( +it.effect('keeps Party Registry pool acquisition failure in the typed error channel', () => + Effect.gen(function* testScenario2() { + const error = yield* Effect.flip( Effect.scoped( makePartyDatabase( { @@ -37,8 +34,8 @@ test('keeps Party Registry pool acquisition failure in the typed error channel', }, ), ), - ), - ); - assert.equal(error._tag, 'PartyDatabaseConnectionError'); - assert.equal(error.reason, 'Unable to initialize the Party Registry PostgreSQL connection pool'); -}); + ); + expect(Predicate.isTagged(error, 'PartyDatabaseConnectionError')).toBe(true); + expect(error.reason).toBe('Unable to initialize the Party Registry PostgreSQL connection pool'); + }), +); diff --git a/app/verticals/party-registry/tests/unit/engagement-catalog-contract.test.ts b/app/verticals/party-registry/tests/unit/engagement-catalog-contract.test.ts index dc5ffcd98..1edc58fce 100644 --- a/app/verticals/party-registry/tests/unit/engagement-catalog-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/engagement-catalog-contract.test.ts @@ -1,23 +1,22 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { assert, expect, it } from 'effect-rstest'; import { compareContactsCatalog, expectedContactsTableCatalog, } from '../../src/db/engagement-catalog.ts'; -test('reports exact Contacts table catalog differences', () => { - assert.deepEqual(expectedContactsTableCatalog, [ +it('reports exact Contacts table catalog differences', () => { + expect(expectedContactsTableCatalog).toEqual([ 'contacts.gateway_assertion_redemptions', 'contacts.organization_engagement_profiles', 'contacts.person_engagement_profiles', ]); - assert.deepEqual(compareContactsCatalog(['contacts.organization_engagement_profiles']), { + expect(compareContactsCatalog(['contacts.organization_engagement_profiles'])).toEqual({ missing: ['contacts.gateway_assertion_redemptions', 'contacts.person_engagement_profiles'], unexpected: [], }); }); -test('keeps Contacts inventory separate while rejecting duplicate unknown tables once', () => { +it('keeps Contacts inventory separate while rejecting duplicate unknown tables once', () => { assert.deepEqual( compareContactsCatalog([ ...expectedContactsTableCatalog, diff --git a/app/verticals/party-registry/tests/unit/engagement-lifecycle-handler.test.ts b/app/verticals/party-registry/tests/unit/engagement-lifecycle-handler.test.ts index 7264d95ea..f75baad79 100644 --- a/app/verticals/party-registry/tests/unit/engagement-lifecycle-handler.test.ts +++ b/app/verticals/party-registry/tests/unit/engagement-lifecycle-handler.test.ts @@ -1,7 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { assert, it } from 'effect-rstest'; import { Effect } from 'effect'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import { EngagementProfilePersistenceUnavailable } from '../../shared/domain/engagement-profile.ts'; import { handleEngagementLifecycle } from '../../src/actions/engagement-lifecycle-handler.ts'; @@ -10,8 +8,9 @@ const payload = { profileRef: { resourceId: '10000000-0000-4000-8000-00000000000 for (const state of ['active', 'archived'] as const) { const handle = handleEngagementLifecycle(state); - test(`engagement transition to ${state} forwards the reference and returns the persisted value`, () => - runEffectTestPromise( + it.effect( + `engagement transition to ${state} forwards the reference and returns the persisted value`, + () => Effect.gen(function* verifyTransition() { const value = yield* handle(payload, { services: { @@ -23,49 +22,46 @@ for (const state of ['active', 'archived'] as const) { }); assert.equal(value, 'persisted-profile'); }), - )); + ); - test(`engagement conflict reports the requested ${state} state`, () => - runEffectTestPromise( - Effect.gen(function* verifyConflict() { - const reason = yield* handle(payload, { - services: { - transition: () => Effect.succeed({ _tag: 'conflict', value: 'existing-profile' }), - }, - }).pipe( - Effect.catchTag('EngagementProfileConflict', (error) => Effect.succeed(error.reason)), - ); - assert.equal(reason, `The engagement profile is already ${state}`); - }), - )); + it.effect(`engagement conflict reports the requested ${state} state`, () => + Effect.gen(function* verifyConflict() { + const reason = yield* handle(payload, { + services: { + transition: () => Effect.succeed({ _tag: 'conflict', value: 'existing-profile' }), + }, + }).pipe( + Effect.catchTag('EngagementProfileConflict', (error) => Effect.succeed(error.reason)), + ); + assert.equal(reason, `The engagement profile is already ${state}`); + }), + ); - test(`engagement transition to ${state} retains the missing profile identity`, () => - runEffectTestPromise( - Effect.gen(function* verifyMissing() { - const profileId = yield* handle(payload, { - services: { transition: () => Effect.succeed({ _tag: 'not_found' }) }, - }).pipe( - Effect.catchTag('EngagementProfileNotFound', (error) => Effect.succeed(error.profileId)), - ); - assert.equal(profileId, payload.profileRef.resourceId); - }), - )); + it.effect(`engagement transition to ${state} retains the missing profile identity`, () => + Effect.gen(function* verifyMissing() { + const profileId = yield* handle(payload, { + services: { transition: () => Effect.succeed({ _tag: 'not_found' }) }, + }).pipe( + Effect.catchTag('EngagementProfileNotFound', (error) => Effect.succeed(error.profileId)), + ); + assert.equal(profileId, payload.profileRef.resourceId); + }), + ); - test(`engagement transition to ${state} preserves the typed persistence failure`, () => - runEffectTestPromise( - Effect.gen(function* verifyPersistenceFailure() { - const failure = new EngagementProfilePersistenceUnavailable({ - code: 'contacts_engagement_profile_persistence_unavailable', - reason: 'Storage unavailable', - }); - const result = yield* handle(payload, { - services: { transition: () => Effect.fail(failure) }, - }).pipe( - Effect.catchTag('EngagementProfilePersistenceUnavailable', (error) => - Effect.succeed(error), - ), - ); - assert.equal(result, failure); - }), - )); + it.effect(`engagement transition to ${state} preserves the typed persistence failure`, () => + Effect.gen(function* verifyPersistenceFailure() { + const failure = new EngagementProfilePersistenceUnavailable({ + code: 'contacts_engagement_profile_persistence_unavailable', + reason: 'Storage unavailable', + }); + const result = yield* handle(payload, { + services: { transition: () => Effect.fail(failure) }, + }).pipe( + Effect.catchTag('EngagementProfilePersistenceUnavailable', (error) => + Effect.succeed(error), + ), + ); + assert.equal(result, failure); + }), + ); } diff --git a/app/verticals/party-registry/tests/unit/engagement-lifecycle-registration.test.ts b/app/verticals/party-registry/tests/unit/engagement-lifecycle-registration.test.ts index 4ba57b422..7bd73d239 100644 --- a/app/verticals/party-registry/tests/unit/engagement-lifecycle-registration.test.ts +++ b/app/verticals/party-registry/tests/unit/engagement-lifecycle-registration.test.ts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { assert, it } from 'effect-rstest'; import { OrganizationEngagementLifecyclePayloadSchema, OrganizationEngagementProfileSchema, @@ -39,7 +38,7 @@ const cases = [ ] as const; for (const [slug, action, payloadSchema, resultSchema] of cases) { - test(`${slug} engagement retains its governed registration and exact schemas`, () => { + it(`${slug} engagement retains its governed registration and exact schemas`, () => { const { descriptor } = action; const key = `party.registry.${slug}-engagement`; assert.equal(descriptor.actionKey, key); diff --git a/app/verticals/party-registry/tests/unit/engagement-profile-api-contract.test.ts b/app/verticals/party-registry/tests/unit/engagement-profile-api-contract.test.ts index 863477edd..dcf8c2f5a 100644 --- a/app/verticals/party-registry/tests/unit/engagement-profile-api-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/engagement-profile-api-contract.test.ts @@ -1,9 +1,7 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics nodeBuiltinImport:off asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; +import { expect, it } from 'effect-rstest'; +// @effect-diagnostics nodeBuiltinImport:off -- Source-contract test reads actual module files; expires: 2026-12-31. import { readFile } from 'node:fs/promises'; -import test from 'node:test'; -import { Effect, Schema } from 'effect'; +import { Effect, Option, Schema } from 'effect'; import { FetchHttpClient } from 'effect/unstable/http'; import { AttachOrganizationEngagementPayloadSchema, @@ -27,82 +25,84 @@ const counterpartyRef = { tenantId, } as const; -test('publishes engagement operations from the Party Registry API boundary', () => { - assert.equal( - partyRegistryApiContract.readinessPath, +it('publishes engagement operations from the Party Registry API boundary', () => { + expect(partyRegistryApiContract.readinessPath).toBe( '/party-registry-api/party-registry/readiness', ); - assert.deepEqual( + expect( Object.values(engagementProfileOperationContexts) .map(({ routePath }) => routePath) .toSorted(), - [ - '/contacts/engagement/organizations/archive', - '/contacts/engagement/organizations/attach', - '/contacts/engagement/organizations/unarchive', - '/contacts/engagement/people/archive', - '/contacts/engagement/people/attach', - '/contacts/engagement/people/unarchive', - '/reads/organization-engagement-profile', - '/reads/person-engagement-profile', - ], - ); + ).toEqual([ + '/contacts/engagement/organizations/archive', + '/contacts/engagement/organizations/attach', + '/contacts/engagement/organizations/unarchive', + '/contacts/engagement/people/archive', + '/contacts/engagement/people/attach', + '/contacts/engagement/people/unarchive', + '/reads/organization-engagement-profile', + '/reads/person-engagement-profile', + ]); }); -test('attach contracts accept only public Party Registry refs', () => { - const payload = { counterpartyRef, partyRef }; - assert.deepEqual( - Schema.decodeUnknownSync(AttachOrganizationEngagementPayloadSchema)(payload), - payload, - ); - assert.deepEqual(Schema.decodeUnknownSync(AttachPersonEngagementPayloadSchema)(payload), payload); - - for (const schema of [ - AttachOrganizationEngagementPayloadSchema, - AttachPersonEngagementPayloadSchema, - ] as const) { - assert.deepEqual( - Schema.decodeUnknownSync(schema, { onExcessProperty: 'error' })({ partyRef }), - { partyRef }, - ); - assert.throws(() => - Schema.decodeUnknownSync(schema, { onExcessProperty: 'error' })({ - ...payload, - customerId: 'd4000000-0000-4000-8000-000000000001', - }), +it.effect('attach contracts accept only public Party Registry refs', () => + Effect.gen(function* decodeContracts() { + const payload = { counterpartyRef, partyRef }; + expect( + yield* Schema.decodeUnknownEffect(AttachOrganizationEngagementPayloadSchema)(payload), + ).toEqual(payload); + expect(yield* Schema.decodeUnknownEffect(AttachPersonEngagementPayloadSchema)(payload)).toEqual( + payload, ); - } -}); -test('public engagement mutations use the owner audience and preserve HTTP context', async () => { - const requests: Request[] = []; - const timestamp = '2026-09-07T00:00:00.000Z'; - const traceparent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'; - const fakeFetch: typeof fetch = async (input, init) => { - const request = new Request(input, init); - requests.push(request); - const { pathname } = new URL(request.url); - if (pathname === '/auth/gateway-context') { - return Response.json({ expiresAt: 1, token: 'test-gateway-token' }); + for (const schema of [ + AttachOrganizationEngagementPayloadSchema, + AttachPersonEngagementPayloadSchema, + ] as const) { + expect( + yield* Schema.decodeUnknownEffect(schema, { onExcessProperty: 'error' })({ partyRef }), + ).toEqual({ partyRef }); + expect( + yield* Schema.decodeUnknownEffect(schema, { onExcessProperty: 'error' })({ + ...payload, + customerId: 'd4000000-0000-4000-8000-000000000001', + }).pipe(Effect.isFailure), + ).toBe(true); } - assert.equal(pathname, '/party-registry-api/contacts/engagement/organizations/attach'); - return Response.json({ - archivedAt: null, - counterpartyRef, - createdAt: timestamp, - partyRef, - profileRef: { - moduleId: 'party.registry', - resourceId: 'd5000000-0000-4000-8000-000000000001', - resourceType: 'party.registry.organization-engagement-profile', - tenantId, - }, - updatedAt: timestamp, - }); - }; + }), +); + +it.effect('public engagement mutations preserve owner request context at the HTTP boundary', () => + Effect.gen(function* verifyCase3() { + const requests: Request[] = []; + const timestamp = '2026-09-07T00:00:00.000Z'; + const traceparent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'; + const fakeFetch: typeof fetch = (input, init) => { + const request = new Request(input, init); + requests.push(request); + const { pathname } = new URL(request.url); + if (pathname === '/auth/gateway-context') { + return Promise.resolve(Response.json({ expiresAt: 1, token: 'test-gateway-token' })); + } + expect(pathname).toBe('/party-registry-api/contacts/engagement/organizations/attach'); + return Promise.resolve( + Response.json({ + archivedAt: null, + counterpartyRef, + createdAt: timestamp, + partyRef, + profileRef: { + moduleId: 'party.registry', + resourceId: 'd5000000-0000-4000-8000-000000000001', + resourceType: 'party.registry.organization-engagement-profile', + tenantId, + }, + updatedAt: timestamp, + }), + ); + }; - await runEffectTestPromise( - attachOrganizationEngagement( + yield* attachOrganizationEngagement( { counterpartyRef, partyRef }, { baseUrl: 'https://party.example/party-registry-api', @@ -113,44 +113,51 @@ test('public engagement mutations use the owner audience and preserve HTTP conte traceId: 'engagement-trace', traceparent, }, - ).pipe(Effect.provideService(FetchHttpClient.Fetch, fakeFetch)), - ); + ).pipe(Effect.provideService(FetchHttpClient.Fetch, fakeFetch)); - const mutationRequest = requests.find(({ url }) => url.includes('/contacts/engagement/')); - const gatewayRequest = requests.find(({ url }) => url.endsWith('/auth/gateway-context')); - assert.ok(gatewayRequest); - assert.ok(mutationRequest); - assert.deepEqual(await gatewayRequest.json(), { audience: 'party-registry' }); - assert.equal(mutationRequest.headers.get('authorization'), 'Bearer test-gateway-token'); - assert.equal(mutationRequest.headers.get('accept-language'), 'cs'); - assert.equal(mutationRequest.headers.get('x-trace-id'), 'engagement-trace'); - assert.equal(mutationRequest.headers.get('traceparent'), traceparent); - assert.equal(mutationRequest.headers.get('x-correlation-id'), 'engagement-correlation'); - assert.equal( - mutationRequest.headers.get('x-operation-id'), - engagementProfileOperationContexts.attachOrganizationEngagement.operationId, - ); - assert.deepEqual( - JSON.parse(mutationRequest.headers.get('x-modernjs-bff-operation-context') ?? ''), - engagementProfileOperationContexts.attachOrganizationEngagement, - ); -}); - -test('public Party Registry engagement API does not expose legacy identity operations', async () => { - const [apiSource, clientSource] = await Promise.all([ - readFile(new URL('../../shared/engagement-profile-api.ts', import.meta.url), 'utf-8'), - readFile(new URL('../../src/api/engagement-profile-client.ts', import.meta.url), 'utf-8'), - ]); - - for (const source of [apiSource, clientSource]) { - assert.doesNotMatch( - source, - /\b(?:createCustomer|editCustomer|archiveCustomer|unarchiveCustomer)\b/u, + const mutationRequest = requests.find(({ url }) => url.includes('/contacts/engagement/')); + const gatewayRequest = requests.find(({ url }) => url.endsWith('/auth/gateway-context')); + expect(gatewayRequest).toBeDefined(); + expect(mutationRequest).toBeDefined(); + const gatewayPayload = yield* Effect.promise(() => + Option.getOrThrow(Option.fromNullishOr(gatewayRequest)).json(), ); - assert.doesNotMatch( - source, - /\b(?:createContact|editContact|archiveContact|unarchiveContact)\b/u, + expect(gatewayPayload).toEqual({ audience: 'party-registry' }); + expect(mutationRequest?.headers.get('authorization')).toBe('Bearer test-gateway-token'); + expect(mutationRequest?.headers.get('accept-language')).toBe('cs'); + expect(mutationRequest?.headers.get('x-trace-id')).toBe('engagement-trace'); + expect(mutationRequest?.headers.get('traceparent')).toBe(traceparent); + expect(mutationRequest?.headers.get('x-correlation-id')).toBe('engagement-correlation'); + expect(mutationRequest?.headers.get('x-operation-id')).toBe( + engagementProfileOperationContexts.attachOrganizationEngagement.operationId, ); - assert.doesNotMatch(source, /CustomerAresLookup|customerId|contactId/u); - } -}); + expect( + yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))( + mutationRequest?.headers.get('x-modernjs-bff-operation-context') ?? '', + ), + ).toEqual(engagementProfileOperationContexts.attachOrganizationEngagement); + }), +); + +it.effect('public Party Registry engagement API does not expose legacy identity operations', () => + Effect.gen(function* verifyCase4() { + const [apiSource, clientSource] = yield* Effect.all([ + Effect.promise(() => + readFile(new URL('../../shared/engagement-profile-api.ts', import.meta.url), 'utf-8'), + ), + Effect.promise(() => + readFile(new URL('../../src/api/engagement-profile-client.ts', import.meta.url), 'utf-8'), + ), + ]); + + for (const source of [apiSource, clientSource]) { + expect(source).not.toMatch( + /\b(?:createCustomer|editCustomer|archiveCustomer|unarchiveCustomer)\b/u, + ); + expect(source).not.toMatch( + /\b(?:createContact|editContact|archiveContact|unarchiveContact)\b/u, + ); + expect(source).not.toMatch(/CustomerAresLookup|customerId|contactId/u); + } + }), +); diff --git a/app/verticals/party-registry/tests/unit/engagement-profile-persistence-service.test.ts b/app/verticals/party-registry/tests/unit/engagement-profile-persistence-service.test.ts index c1bcfed6f..5eed5a832 100644 --- a/app/verticals/party-registry/tests/unit/engagement-profile-persistence-service.test.ts +++ b/app/verticals/party-registry/tests/unit/engagement-profile-persistence-service.test.ts @@ -1,15 +1,12 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. -/* eslint-disable anti-slop/no-chained-type-assertions -- Focused harness implements only the mutation insert's Drizzle seam. expires: 2026-12-31. */ -import { DateTime, Effect } from 'effect'; import type { SQL } from 'drizzle-orm'; import { PgDialect } from 'drizzle-orm/pg-core'; import { organizationEngagementProfiles, personEngagementProfiles, } from '../../src/db/engagement-schema.ts'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { assert, expect, it } from 'effect-rstest'; +/* eslint-disable anti-slop/no-chained-type-assertions -- Focused harness implements only the mutation insert's Drizzle seam. expires: 2026-12-31. */ +import { DateTime, Effect, Match, Predicate } from 'effect'; import type { OrganizationEngagementProfileRecord } from '../../src/db/engagement-schema.ts'; import { createOrganizationEngagementProfile, @@ -56,35 +53,34 @@ const rejectingMutationTransaction = (failure: Failure) => }), }) as unknown as Parameters[0]; -test('reconstructs typed references from the owner-local persistence record', () => { +it('reconstructs typed references from the owner-local persistence record', () => { const result = organizationEngagementProfileFromRecord(row); - assert.deepEqual(result.partyRef, refs.partyRef); - assert.deepEqual(result.counterpartyRef, refs.counterpartyRef); - assert.equal('name' in result, false); - assert.equal('ico' in result, false); - assert.equal( + expect(result.partyRef).toEqual(refs.partyRef); + expect(result.counterpartyRef).toEqual(refs.counterpartyRef); + expect('name' in result).toBe(false); + expect('ico' in result).toBe(false); + expect( organizationEngagementProfileFromRecord({ ...row, counterpartyResourceId: null }) .counterpartyRef, - null, - ); + ).toBe(null); }); -test('fails closed when a caller-supplied ref crosses the trusted tenant', async () => { - const failure = await runEffectTestPromise( - Effect.flip( +it.effect('fails closed when a caller-supplied ref crosses the trusted tenant', () => + Effect.gen(function* verifyCase2() { + const failure = yield* Effect.flip( ensureReferencesBelongToTenant(tenantId, { ...refs, partyRef: { ...refs.partyRef, tenantId: 'c9000000-0000-4000-8000-000000000001' }, }), - ), - ); - assert.equal(failure._tag, 'EngagementProfileConflict'); - assert.equal(failure.code, 'contacts_party_counterparty_mismatch'); -}); + ); + expect(Predicate.isTagged(failure, 'EngagementProfileConflict')).toBe(true); + expect(failure.code).toBe('contacts_party_counterparty_mismatch'); + }), +); -test('maps a wrapped owner uniqueness constraint to the declared engagement conflict', async () => { - const failure = await runEffectTestPromise( - Effect.flip( +it.effect('maps a wrapped owner uniqueness constraint to the declared engagement conflict', () => + Effect.gen(function* verifyCase3() { + const failure = yield* Effect.flip( createOrganizationEngagementProfile( rejectingMutationTransaction({ cause: { @@ -96,20 +92,19 @@ test('maps a wrapped owner uniqueness constraint to the declared engagement conf }), { ...refs, tenantId }, ), - ), - ); + ); - assert.equal(failure._tag, 'EngagementProfileConflict'); - assert.equal(failure.code, 'contacts_engagement_profile_already_exists'); - assert.equal( - failure.reason, - 'An engagement profile already exists for these canonical references', - ); -}); + expect(Predicate.isTagged(failure, 'EngagementProfileConflict')).toBe(true); + expect(failure.code).toBe('contacts_engagement_profile_already_exists'); + expect(failure.reason).toBe( + 'An engagement profile already exists for these canonical references', + ); + }), +); -test('continues past an unrelated wrapper code to the owner uniqueness constraint', async () => { - const failure = await runEffectTestPromise( - Effect.flip( +it.effect('continues past an unrelated wrapper code to the owner uniqueness constraint', () => + Effect.gen(function* verifyCase4() { + const failure = yield* Effect.flip( createOrganizationEngagementProfile( rejectingMutationTransaction({ code: 'ERR_QUERY_FAILED', @@ -120,16 +115,16 @@ test('continues past an unrelated wrapper code to the owner uniqueness constrain }), { ...refs, tenantId }, ), - ), - ); + ); - assert.equal(failure._tag, 'EngagementProfileConflict'); - assert.equal(failure.code, 'contacts_engagement_profile_already_exists'); -}); + expect(Predicate.isTagged(failure, 'EngagementProfileConflict')).toBe(true); + expect(failure.code).toBe('contacts_engagement_profile_already_exists'); + }), +); -test('maps an unrelated uniqueness constraint to the existing persistence fallback', async () => { - const failure = await runEffectTestPromise( - Effect.flip( +it.effect('maps an unrelated uniqueness constraint to the existing persistence fallback', () => + Effect.gen(function* verifyCase5() { + const failure = yield* Effect.flip( createOrganizationEngagementProfile( rejectingMutationTransaction({ code: '23505', @@ -137,16 +132,15 @@ test('maps an unrelated uniqueness constraint to the existing persistence fallba }), { ...refs, tenantId }, ), - ), - ); + ); - assert.equal(failure._tag, 'EngagementProfilePersistenceUnavailable'); - assert.equal(failure.code, 'contacts_engagement_profile_persistence_unavailable'); - assert.equal( - failure.reason, - 'Contacts engagement profile persistence is temporarily unavailable', - ); -}); + expect(Predicate.isTagged(failure, 'EngagementProfilePersistenceUnavailable')).toBe(true); + expect(failure.code).toBe('contacts_engagement_profile_persistence_unavailable'); + expect(failure.reason).toBe( + 'Contacts engagement profile persistence is temporarily unavailable', + ); + }), +); const profileKinds = [ { @@ -166,8 +160,9 @@ const profileKinds = [ ] as const; for (const kind of profileKinds) { - test(`${kind.resourceType} binds creation, lookup and lifecycle to its own tenant-qualified table`, () => - runEffectTestPromise( + it.effect( + `${kind.resourceType} binds creation, lookup and lifecycle to its own tenant-qualified table`, + () => Effect.gen(function* verifyProfilePersistence() { let current: OrganizationEngagementProfileRecord | undefined = row; let writes = 0; @@ -234,13 +229,26 @@ for (const kind of profileKinds) { } as unknown as Parameters[0]; const created = yield* kind.create(transaction, { ...refs, tenantId }); assert.equal(created.profileRef.resourceType, kind.resourceType); - assert.deepEqual(yield* kind.find(transaction, tenantId, row.engagementProfileId), { - _tag: 'found', - value: created, - }); + const found = yield* kind.find(transaction, tenantId, row.engagementProfileId); assert.deepEqual( - yield* kind.transition(transaction, tenantId, row.engagementProfileId, 'active'), - { _tag: 'conflict', value: created }, + Match.value(found).pipe( + Match.tag('found', ({ value }) => value), + Match.orElse(() => expect.unreachable('Expected found profile')), + ), + created, + ); + const conflict = yield* kind.transition( + transaction, + tenantId, + row.engagementProfileId, + 'active', + ); + assert.deepEqual( + Match.value(conflict).pipe( + Match.tag('conflict', ({ value }) => value), + Match.orElse(() => expect.unreachable('Expected conflicting profile')), + ), + created, ); assert.equal(writes, 0); const archived = yield* kind.transition( @@ -258,15 +266,20 @@ for (const kind of profileKinds) { assert.equal(current?.archivedAt, null); assert.equal(writes, 2); current = undefined; - assert.deepEqual(yield* kind.find(transaction, tenantId, row.engagementProfileId), { - _tag: 'not_found', - }); - assert.deepEqual( - yield* kind.transition(transaction, tenantId, row.engagementProfileId, 'archived'), - { _tag: 'not_found' }, - ); + expect( + Predicate.isTagged( + yield* kind.find(transaction, tenantId, row.engagementProfileId), + 'not_found', + ), + ).toBe(true); + expect( + Predicate.isTagged( + yield* kind.transition(transaction, tenantId, row.engagementProfileId, 'archived'), + 'not_found', + ), + ).toBe(true); assert.equal(writes, 2); assert.equal(locks, 4); }), - )); + ); } diff --git a/app/verticals/party-registry/tests/unit/engagement-reference-validation.test.ts b/app/verticals/party-registry/tests/unit/engagement-reference-validation.test.ts index c4088656c..a35807d88 100644 --- a/app/verticals/party-registry/tests/unit/engagement-reference-validation.test.ts +++ b/app/verticals/party-registry/tests/unit/engagement-reference-validation.test.ts @@ -1,6 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Effect } from 'effect'; import type { PartyRef } from '../../shared/party-registry-references.ts'; import { validatePartyRegistryReferences } from '../../src/services/engagement-reference-validation.service.ts'; @@ -25,21 +23,15 @@ const operations: PartyRegistryReferenceOperations = { }), }; -test('validates engagement references through owner-local Party Registry operations', () => - runEffectTestPromise( - validatePartyRegistryReferences( - operations, - { partyRef }, - { expectedPartyType: 'ORGANIZATION' }, - ), - )); +it.effect('validates engagement references through owner-local Party Registry operations', () => + validatePartyRegistryReferences(operations, { partyRef }, { expectedPartyType: 'ORGANIZATION' }), +); -test('rejects a profile whose Party type belongs to a different engagement kind', () => - runEffectTestPromise( - Effect.gen(function* verifyPartyTypeMismatch() { - const error = yield* Effect.flip( - validatePartyRegistryReferences(operations, { partyRef }, { expectedPartyType: 'PERSON' }), - ); - assert.equal(error.code, 'contacts_party_type_mismatch'); - }), - )); +it.effect('rejects a profile whose Party type belongs to a different engagement kind', () => + Effect.gen(function* verifyPartyTypeMismatch() { + const error = yield* Effect.flip( + validatePartyRegistryReferences(operations, { partyRef }, { expectedPartyType: 'PERSON' }), + ); + expect(error.code).toBe('contacts_party_type_mismatch'); + }), +); diff --git a/app/verticals/party-registry/tests/unit/engagement-schema-contract.test.ts b/app/verticals/party-registry/tests/unit/engagement-schema-contract.test.ts index 025a07b0e..0f33a27cc 100644 --- a/app/verticals/party-registry/tests/unit/engagement-schema-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/engagement-schema-contract.test.ts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { getTableConfig } from 'drizzle-orm/pg-core'; import { CONTACTS_SCHEMA_NAME, @@ -11,42 +10,36 @@ import { const organizationConfig = getTableConfig(organizationEngagementProfiles); const personConfig = getTableConfig(personEngagementProfiles); -test('owns two engagement profile tables plus gateway replay protection', () => { +it('owns two engagement profile tables plus gateway replay protection', () => { const qualifiedNames = [organizationConfig, personConfig] .map((config) => `${config.schema}.${config.name}`) .toSorted(); - assert.equal(CONTACTS_SCHEMA_NAME, 'contacts'); - assert.deepEqual(CONTACTS_TABLE_INVENTORY, [ + expect(CONTACTS_SCHEMA_NAME).toBe('contacts'); + expect(CONTACTS_TABLE_INVENTORY).toEqual([ 'gateway_assertion_redemptions', 'organization_engagement_profiles', 'person_engagement_profiles', ]); - assert.deepEqual(qualifiedNames, [ + expect(qualifiedNames).toEqual([ 'contacts.organization_engagement_profiles', 'contacts.person_engagement_profiles', ]); }); -test('stores references and profile lifecycle, never Party identity facts', () => { +it('stores references and profile lifecycle, never Party identity facts', () => { for (const config of [organizationConfig, personConfig]) { - assert.deepEqual( - config.columns.map((column) => column.name), - [ - 'engagement_profile_id', - 'tenant_id', - 'party_resource_id', - 'counterparty_resource_id', - 'created_at', - 'updated_at', - 'archived_at', - ], - ); - assert.equal(config.foreignKeys.length, 0); + expect(config.columns.map((column) => column.name)).toEqual([ + 'engagement_profile_id', + 'tenant_id', + 'party_resource_id', + 'counterparty_resource_id', + 'created_at', + 'updated_at', + 'archived_at', + ]); + expect(config.foreignKeys.length).toBe(0); for (const forbidden of ['customer_id', 'contact_id', 'name', 'ico', 'dic', 'email', 'phone']) { - assert.equal( - config.columns.some((column) => column.name === forbidden), - false, - ); + expect(config.columns.some((column) => column.name === forbidden)).toBe(false); } for (const required of [ 'engagement_profile_id', @@ -55,31 +48,34 @@ test('stores references and profile lifecycle, never Party identity facts', () = 'created_at', 'updated_at', ]) { - assert.equal(config.columns.find((column) => column.name === required)?.notNull, true); + expect(config.columns.find((column) => column.name === required)?.notNull).toBe(true); } - assert.equal( + expect( config.columns.find((column) => column.name === 'counterparty_resource_id')?.notNull, - false, - ); + ).toBe(false); } }); -test('forces tenant RLS with complete CRUD policies on both profile tables', () => { +it('forces tenant RLS with complete CRUD policies on both profile tables', () => { for (const [config, prefix] of [ [organizationConfig, 'contacts_organization_engagement_profiles_tenant'], [personConfig, 'contacts_person_engagement_profiles_tenant'], ] as const) { - assert.equal(config.enableRLS, true); - assert.deepEqual( - config.policies.map((policy) => policy.name), - [`${prefix}_select`, `${prefix}_insert`, `${prefix}_update`, `${prefix}_delete`], - ); - assert.deepEqual( - config.policies.map((policy) => policy.for), - ['select', 'insert', 'update', 'delete'], - ); + expect(config.enableRLS).toBe(true); + expect(config.policies.map((policy) => policy.name)).toEqual([ + `${prefix}_select`, + `${prefix}_insert`, + `${prefix}_update`, + `${prefix}_delete`, + ]); + expect(config.policies.map((policy) => policy.for)).toEqual([ + 'select', + 'insert', + 'update', + 'delete', + ]); for (const policy of config.policies) { - assert.equal(policy.to, 'ontos_runtime'); + expect(policy.to).toBe('ontos_runtime'); } } }); diff --git a/app/verticals/party-registry/tests/unit/gateway-assertion-redemption-runtime.test.ts b/app/verticals/party-registry/tests/unit/gateway-assertion-redemption-runtime.test.ts index 2cf1cb1de..59ab59855 100644 --- a/app/verticals/party-registry/tests/unit/gateway-assertion-redemption-runtime.test.ts +++ b/app/verticals/party-registry/tests/unit/gateway-assertion-redemption-runtime.test.ts @@ -1,4 +1,8 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; +import { assert, expect, it } from 'effect-rstest'; +import { + GatewayAssertionRedemptionUnavailableError, + GatewayAssertionReplayError, +} from '@app/core-runtime'; import { GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS } from '@app/shared-contracts'; import { PgClient } from '@effect/sql-pg'; import { makeWithDefaults } from 'drizzle-orm/effect-postgres'; @@ -7,8 +11,6 @@ import { TestClock } from 'effect/testing'; import { Reactivity } from 'effect/unstable/reactivity'; import { testSqlConnection } from '../../../../packages/core-runtime/tests/support/sql-connection.ts'; import { ConnectionError, SqlError } from 'effect/unstable/sql/SqlError'; -import assert from 'node:assert/strict'; -import test from 'node:test'; import { makeGatewayAssertionRedemption } from '../../src/auth/gateway-assertion-redemption-runtime.ts'; import { partyRelations } from '../../src/db/schema.ts'; @@ -43,58 +45,57 @@ const makeRedemptionFixture = ( }); for (const settlement of ['COMMIT', 'ROLLBACK']) { - test(`maps native ${settlement} SQL defects to redemption unavailability`, () => - runEffectTestPromise( - Effect.gen(function* rejectSqlSettlementFailure() { - const sqlFailure = new SqlError({ - reason: new ConnectionError({ - cause: new Error('private fixture database details'), - message: 'private fixture connection details', - operation: settlement, - }), - }); - const statements: string[] = []; - const fixture = yield* makeRedemptionFixture((sql) => { - statements.push(sql); - if (sql === settlement || (settlement === 'ROLLBACK' && sql.startsWith('insert'))) { - return Effect.fail(sqlFailure); - } - return Effect.succeed(sql.startsWith('insert') ? [{ jti: assertion.jti }] : []); - }); - const failure = yield* fixture.redemption - .consume(assertion) - .pipe(Effect.provideService(Clock.Clock, fixture.clock), Effect.flip); - assert.equal(failure._tag, 'GatewayAssertionRedemptionUnavailableError'); - const serializedFailure = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))( - failure, - ); - assert.doesNotMatch(serializedFailure, /private fixture/u); - assert.ok(statements.includes(settlement)); - }).pipe(Effect.scoped), - )); -} - -test('preserves unrelated transaction defects', () => - runEffectTestPromise( - Effect.gen(function* preserveProgrammingDefect() { - const defect = new Error('fixture programming defect'); - const fixture = yield* makeRedemptionFixture((sql) => - sql === 'COMMIT' - ? Effect.die(defect) - : Effect.succeed(sql.startsWith('insert') ? [{ jti: assertion.jti }] : []), - ); - const exit = yield* fixture.redemption + it.effect(`maps native ${settlement} SQL defects to redemption unavailability`, () => + Effect.gen(function* rejectSqlSettlementFailure() { + const sqlFailure = new SqlError({ + reason: new ConnectionError({ + cause: new Error('private fixture database details'), + message: 'private fixture connection details', + operation: settlement, + }), + }); + const statements: string[] = []; + const fixture = yield* makeRedemptionFixture((sql) => { + statements.push(sql); + if (sql === settlement || (settlement === 'ROLLBACK' && sql.startsWith('insert'))) { + return Effect.fail(sqlFailure); + } + return Effect.succeed(sql.startsWith('insert') ? [{ jti: assertion.jti }] : []); + }); + const failure = yield* fixture.redemption .consume(assertion) - .pipe(Effect.provideService(Clock.Clock, fixture.clock), Effect.exit); - assert.ok(Exit.isFailure(exit)); - assert.ok( - exit.cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect === defect), + .pipe(Effect.provideService(Clock.Clock, fixture.clock), Effect.flip); + expect(Schema.is(GatewayAssertionRedemptionUnavailableError)(failure)).toBe(true); + const serializedFailure = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))( + failure, ); - }).pipe(Effect.scoped), - )); + expect(serializedFailure).not.toMatch(/private fixture/u); + expect(statements.includes(settlement)).toBe(true); + }), + ); +} + +it.effect('preserves unrelated transaction defects', () => + Effect.gen(function* preserveProgrammingDefect() { + const defect = new Error('fixture programming defect'); + const fixture = yield* makeRedemptionFixture((sql) => + sql === 'COMMIT' + ? Effect.die(defect) + : Effect.succeed(sql.startsWith('insert') ? [{ jti: assertion.jti }] : []), + ); + const exit = yield* fixture.redemption + .consume(assertion) + .pipe(Effect.provideService(Clock.Clock, fixture.clock), Effect.exit); + assert.isOk(Exit.isFailure(exit)); + expect( + exit.cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect === defect), + ).toBe(true); + }), +); -test('rejects assertions crossing expiry before redemption without deleting replay evidence', () => - runEffectTestPromise( +it.effect( + 'rejects assertions crossing expiry before redemption without deleting replay evidence', + () => Effect.gen(function* preserveExpiredReplayEvidence() { const statements: string[] = []; const fixture = yield* makeRedemptionFixture((sql) => { @@ -104,15 +105,17 @@ test('rejects assertions crossing expiry before redemption without deleting repl yield* fixture.redemption .consume(assertion) .pipe(Effect.provideService(Clock.Clock, fixture.clock)); - assert.ok(statements.includes('COMMIT'), 'the last millisecond of skew remains usable'); + expect(statements.includes('COMMIT'), 'the last millisecond of skew remains usable').toBe( + true, + ); statements.length = 0; for (const now of [expiryWithSkewMs, expiryWithSkewMs + 1]) { yield* fixture.clock.setTime(now); const failure = yield* fixture.redemption .consume(assertion) .pipe(Effect.provideService(Clock.Clock, fixture.clock), Effect.flip); - assert.equal(failure._tag, 'GatewayAssertionReplayError'); + expect(Schema.is(GatewayAssertionReplayError)(failure)).toBe(true); } - assert.deepEqual(statements, [], 'expired assertions cannot run replay-evidence cleanup'); - }).pipe(Effect.scoped), - )); + expect(statements, 'expired assertions cannot run replay-evidence cleanup').toEqual([]); + }), +); diff --git a/app/verticals/party-registry/tests/unit/identifier-contract.test.ts b/app/verticals/party-registry/tests/unit/identifier-contract.test.ts index 88ef9e0b7..f5008dc59 100644 --- a/app/verticals/party-registry/tests/unit/identifier-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/identifier-contract.test.ts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Schema } from 'effect'; import { OfficialIdentifierInputSchema, @@ -16,78 +15,78 @@ import { const decode = Schema.decodeUnknownSync; -test('Official Identifier V1 accepts only IČO and Czech DIČ', () => { +it('Official Identifier V1 accepts only IČO and Czech DIČ', () => { const ico = decode(OfficialIdentifierInputSchema)({ identifierType: 'ICO', value: '27074358', verification: 'VERIFIED', }); - assert.equal(normalizeOfficialIdentifier(ico).normalizedValue, '27074358'); - assert.equal(normalizeOfficialIdentifier(ico).namespace, 'CZ:ICO'); + expect(normalizeOfficialIdentifier(ico).normalizedValue).toBe('27074358'); + expect(normalizeOfficialIdentifier(ico).namespace).toBe('CZ:ICO'); const legacyShortIco = decode(OfficialIdentifierInputSchema)({ identifierType: 'ICO', value: '1000004', verification: 'VERIFIED', }); - assert.equal(normalizeOfficialIdentifier(legacyShortIco).normalizedValue, '01000004'); + expect(normalizeOfficialIdentifier(legacyShortIco).normalizedValue).toBe('01000004'); const dic = decode(OfficialIdentifierInputSchema)({ identifierType: 'CZ_DIC', value: 'cz27074358', verification: 'VERIFIED', }); - assert.equal(normalizeOfficialIdentifier(dic).normalizedValue, 'CZ27074358'); - assert.equal(normalizeOfficialIdentifier(dic).namespace, 'CZ:DIC'); + expect(normalizeOfficialIdentifier(dic).normalizedValue).toBe('CZ27074358'); + expect(normalizeOfficialIdentifier(dic).namespace).toBe('CZ:DIC'); - assert.throws(() => + expect(() => decode(OfficialIdentifierInputSchema)({ identifierType: 'ICO', namespace: 'caller-controlled', value: '27074358', verification: 'VERIFIED', }), - ); + ).toThrow(); - assert.throws(() => + expect(() => decode(OfficialIdentifierInputSchema)({ identifierType: 'ICO', value: '270 74 358', verification: 'VERIFIED', }), - ); + ).toThrow(); - assert.throws(() => + expect(() => decode(OfficialIdentifierInputSchema)({ identifierType: 'VAT_ID', value: 'CZ27074358', verification: 'VERIFIED', }), - ); - assert.throws(() => + ).toThrow(); + expect(() => decode(OfficialIdentifierInputSchema)({ identifierType: 'OTHER', value: '1', verification: 'VERIFIED', }), - ); + ).toThrow(); }); -test('Official Identifier writes require tenant Party identity authority and idempotency', () => { +it('Official Identifier writes require tenant Party identity authority and idempotency', () => { for (const action of [ addPartyOfficialIdentifierAction, endPartyOfficialIdentifierAction, updatePartyOfficialIdentifierAction, ]) { - assert.equal(action.descriptor.legalEntityScope, 'optional'); - assert.equal(action.descriptor.idempotency, 'required'); + expect(action.descriptor.legalEntityScope).toBe('optional'); + expect(action.descriptor.idempotency).toBe('required'); // SAFETY: these permission selectors are payload-independent; no handler receives this sentinel. - assert.equal(action.descriptor.tenantPermission?.({} as never), 'manage_party_identity'); + expect(action.descriptor.tenantPermission?.({} as never)).toBe('manage_party_identity'); } }); -test('only verified, formally valid identifiers create deterministic exclusive claim keys', () => { - assert.equal( +it('only verified, formally valid identifiers create deterministic exclusive claim keys', () => { + expect( qualifyingClaimKey( { identifierType: 'ICO', @@ -97,9 +96,8 @@ test('only verified, formally valid identifiers create deterministic exclusive c 'ORGANIZATION', 'party-exact-claims.v1', ), - 'ICO\u0000CZ:ICO\u000027074358', - ); - assert.equal( + ).toBe('ICO\u0000CZ:ICO\u000027074358'); + expect( qualifyingClaimKey( { identifierType: 'ICO', @@ -109,9 +107,8 @@ test('only verified, formally valid identifiers create deterministic exclusive c 'ORGANIZATION', 'party-exact-claims.v1', ), - undefined, - ); - assert.equal( + ).toBe(undefined); + expect( qualifyingClaimKey( { identifierType: 'ICO', @@ -121,9 +118,8 @@ test('only verified, formally valid identifiers create deterministic exclusive c 'PERSON', 'party-exact-claims.v1', ), - undefined, - ); - assert.equal( + ).toBe(undefined); + expect( qualifyingClaimKey( { identifierType: 'CZ_DIC', @@ -133,8 +129,7 @@ test('only verified, formally valid identifiers create deterministic exclusive c 'PERSON', 'party-exact-claims.v1', ), - undefined, - ); + ).toBe(undefined); }); const identifierRef = { @@ -144,7 +139,7 @@ const identifierRef = { tenantId: '10000000-0000-4000-8000-000000000001', } as const; -test('Identifier Update is a closed evidence-backed metadata or validity command, never an identity patch', () => { +it('Identifier Update is a closed evidence-backed metadata or validity command, never an identity patch', () => { const command = { change: { expectedVerification: 'UNVERIFIED', @@ -155,37 +150,35 @@ test('Identifier Update is a closed evidence-backed metadata or validity command officialIdentifierRef: identifierRef, reason: 'Registry confirmed the existing identifier', }; - assert.equal( - decode(UpdatePartyOfficialIdentifierPayloadSchema)(command).change.type, + expect(decode(UpdatePartyOfficialIdentifierPayloadSchema)(command).change.type).toBe( 'SET_VERIFICATION', ); - assert.equal( + expect( decode(UpdatePartyOfficialIdentifierPayloadSchema)({ ...command, change: { type: 'END_VALIDITY', validTo: '2026-01-01T00:00:00.000Z' }, }).change.type, - 'END_VALIDITY', - ); + ).toBe('END_VALIDITY'); for (const forbidden of ['value', 'normalizedValue', 'identifierType', 'namespace', 'partyRef']) { - assert.throws(() => + expect(() => decode(UpdatePartyOfficialIdentifierPayloadSchema)({ ...command, [forbidden]: 'changed-identity', }), - ); + ).toThrow(); } - assert.throws(() => + expect(() => decode(UpdatePartyOfficialIdentifierPayloadSchema)({ ...command, evidenceRefs: [] }), - ); - assert.throws(() => + ).toThrow(); + expect(() => decode(UpdatePartyOfficialIdentifierPayloadSchema)({ ...command, change: { type: 'REPLACE_VALUE', value: '12345678' }, }), - ); + ).toThrow(); }); -test('Identifier Update event retains before and after verification evidence', () => { +it('Identifier Update event retains before and after verification evidence', () => { const before = { state: 'ACTIVE', validTo: null, @@ -209,6 +202,6 @@ test('Identifier Update event retains before and after verification evidence', ( partyRef: { ...identifierRef, resourceType: 'party.registry.party' }, reason: 'New evidence superseded the previous verification', }); - assert.deepEqual(event.before, before); - assert.deepEqual(event.after, after); + expect(event.before).toEqual(before); + expect(event.after).toEqual(after); }); diff --git a/app/verticals/party-registry/tests/unit/identifier-persistence.service.test.ts b/app/verticals/party-registry/tests/unit/identifier-persistence.service.test.ts index deadd944c..7fa3a1bc6 100644 --- a/app/verticals/party-registry/tests/unit/identifier-persistence.service.test.ts +++ b/app/verticals/party-registry/tests/unit/identifier-persistence.service.test.ts @@ -1,10 +1,8 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { DateTime, Effect, Match, Schema } from 'effect'; -// @effect-diagnostics asyncFunction:off globalDate:off -- Existing compatibility boundary; expires: 2026-12-31. +import { TestClock } from 'effect/testing'; +import { expect, it } from 'effect-rstest'; +import { DateTime, Effect, Match, Schema, Predicate } from 'effect'; /* eslint-disable anti-slop/no-chained-type-assertions, anti-slop/no-unsafe-dictionary-type -- Focused harness implements only the owner service's Drizzle seam. expires: 2026-12-31. */ import type { SQL } from 'drizzle-orm'; -import assert from 'node:assert/strict'; -import test from 'node:test'; import { AresAppliedEvidenceSchema } from '../../shared/domain/ares-application.ts'; import type { partyAliases } from '../../src/db/schema.ts'; import { parties, partyIdentifierClaims, partyOfficialIdentifiers } from '../../src/db/schema.ts'; @@ -148,106 +146,120 @@ const harness = ( return { deleted: () => deletes, inserts, lockedTables, transaction, updates }; }; -test('Add reuses a current same-Party identifier instead of duplicating an assertion', async () => { - const db = harness(); - const result = await runEffectTestPromise( - addOfficialIdentifierRecord(db.transaction, tenantId, partyId, identifier, { - actionInvocationId: 'invocation', - matchRuleVersion: 'party-exact-claims.v1', - partyType: 'ORGANIZATION', - principalId, - provenanceMethod: 'MANUAL', - provenanceSource: 'USER', - validFrom: '2026-01-01T00:00:00.000Z', - }), - ); - assert.equal(result.officialIdentifierId, officialIdentifierId); - assert.equal(db.inserts.length, 0); -}); +it.effect('Add reuses a current same-Party identifier instead of duplicating an assertion', () => + Effect.gen(function* verifyCase1() { + const db = harness(); + const result = yield* addOfficialIdentifierRecord( + db.transaction, + tenantId, + partyId, + identifier, + { + actionInvocationId: 'invocation', + matchRuleVersion: 'party-exact-claims.v1', + partyType: 'ORGANIZATION', + principalId, + provenanceMethod: 'MANUAL', + provenanceSource: 'USER', + validFrom: '2026-01-01T00:00:00.000Z', + }, + ); + expect(result.officialIdentifierId).toBe(officialIdentifierId); + expect(db.inserts.length).toBe(0); + }), +); -test('Add retains ARES evidence separately from the accepting actor and only claims eligible Party types', async () => { - const externalEvidenceWire = { - authorityPolicyKey: 'party_registry.ares_enrichment', - authorityPolicyVersion: '1', - cacheAgeSeconds: 0, - decidedAt: '2026-01-01T00:00:00.000Z', - evidenceRef: 'ares:27074358:confirmation', - fact: 'ICO', - observedAt: '2026-01-01T00:00:00.000Z', - outcome: 'APPLY_ENRICHMENT', - provider: 'ares', - providerChangedOn: null, - providerRecordRef: null, - queryIco: '27074358', - reasonCode: 'authoritative_ico', - servedAt: '2026-01-01T00:00:00.000Z', - } as const; - const externalEvidence = - Schema.decodeUnknownSync(AresAppliedEvidenceSchema)(externalEvidenceWire); - await Promise.all( - (['ORGANIZATION', 'PERSON'] as const).map(async (partyType) => { - const db = harness({ absent: true }); - await runEffectTestPromise( - addOfficialIdentifierRecord(db.transaction, tenantId, partyId, identifier, { - actionInvocationId: 'invocation', - externalEvidence, - matchRuleVersion: 'party-exact-claims.v1', - partyType, - principalId, - provenanceMethod: 'REGISTRY_CONFIRMATION', - provenanceSource: 'ARES', - validFrom: '2026-01-01T00:00:00.000Z', - }), - ); - const storedEvidence = db.inserts[0]?.values['externalEvidence']; - assert.deepEqual(storedEvidence, externalEvidenceWire); - assert.deepEqual( - Schema.decodeUnknownSync(AresAppliedEvidenceSchema)(storedEvidence), - externalEvidence, - ); - assert.equal(db.inserts[0]?.values['acceptedByPrincipalId'], principalId); - assert.equal( - db.inserts.filter((entry) => entry.table === partyIdentifierClaims).length, - partyType === 'ORGANIZATION' ? 1 : 0, +it.effect( + 'Add retains ARES evidence separately from the accepting actor and only claims eligible Party types', + () => + Effect.gen(function* verifyCase2() { + const externalEvidenceWire = { + authorityPolicyKey: 'party_registry.ares_enrichment', + authorityPolicyVersion: '1', + cacheAgeSeconds: 0, + decidedAt: '2026-01-01T00:00:00.000Z', + evidenceRef: 'ares:27074358:confirmation', + fact: 'ICO', + observedAt: '2026-01-01T00:00:00.000Z', + outcome: 'APPLY_ENRICHMENT', + provider: 'ares', + providerChangedOn: null, + providerRecordRef: null, + queryIco: '27074358', + reasonCode: 'authoritative_ico', + servedAt: '2026-01-01T00:00:00.000Z', + } as const; + const externalEvidence = + yield* Schema.decodeUnknownEffect(AresAppliedEvidenceSchema)(externalEvidenceWire); + yield* Effect.all( + (['ORGANIZATION', 'PERSON'] as const).map((partyType) => + Effect.gen(function* verifyCase3() { + const db = harness({ absent: true }); + yield* addOfficialIdentifierRecord(db.transaction, tenantId, partyId, identifier, { + actionInvocationId: 'invocation', + externalEvidence, + matchRuleVersion: 'party-exact-claims.v1', + partyType, + principalId, + provenanceMethod: 'REGISTRY_CONFIRMATION', + provenanceSource: 'ARES', + validFrom: '2026-01-01T00:00:00.000Z', + }); + const storedEvidence = db.inserts[0]?.values['externalEvidence']; + expect(storedEvidence).toEqual(externalEvidenceWire); + expect( + yield* Schema.decodeUnknownEffect(AresAppliedEvidenceSchema)(storedEvidence), + ).toEqual(externalEvidence); + expect(db.inserts[0]?.values['acceptedByPrincipalId']).toBe(principalId); + expect(db.inserts.filter((entry) => entry.table === partyIdentifierClaims).length).toBe( + partyType === 'ORGANIZATION' ? 1 : 0, + ); + }), + ), ); }), - ); -}); +); -test('ending an identifier preserves its fact and releases its current claim', async () => { - const db = harness({ - claimOwner: partyId, - current: row({ verificationState: 'VERIFIED', verifiedAt: date('2026-01-01T00:00:00.000Z') }), - }); - const result = await runEffectTestPromise( - endOfficialIdentifierRecord( +it.effect('ending an identifier preserves its fact and releases its current claim', () => + Effect.gen(function* verifyCase4() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-07T00:00:00.000Z')), + ); + const db = harness({ + claimOwner: partyId, + current: row({ verificationState: 'VERIFIED', verifiedAt: date('2026-01-01T00:00:00.000Z') }), + }); + const result = yield* endOfficialIdentifierRecord( db.transaction, tenantId, officialIdentifierId, '2026-02-01T00:00:00.000Z', - ), - ); - assert.equal(result._tag, 'found'); - assert.equal(db.updates[0]?.['state'], 'ENDED'); - assert.equal(db.updates[0]?.['isCurrent'], false); - assert.equal(db.deleted(), 1); - assert.deepEqual(db.lockedTables, [parties, partyOfficialIdentifiers]); -}); + ); + expect(Predicate.isTagged(result, 'found')).toBe(true); + expect(db.updates[0]?.['state']).toBe('ENDED'); + expect(db.updates[0]?.['isCurrent']).toBe(false); + expect(db.deleted()).toBe(1); + expect(db.lockedTables).toEqual([parties, partyOfficialIdentifiers]); + }), +); -test('a future end does not release a presently valid claim', async () => { - const db = harness(); - const result = await runEffectTestPromise( - endOfficialIdentifierRecord( +it.effect('a future end does not release a presently valid claim', () => + Effect.gen(function* verifyCase5() { + yield* TestClock.setTime( + DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-07T00:00:00.000Z')), + ); + const db = harness(); + const result = yield* endOfficialIdentifierRecord( db.transaction, tenantId, officialIdentifierId, '2099-01-01T00:00:00.000Z', - ), - ); - assert.equal(result._tag, 'conflict'); - assert.equal(db.updates.length, 0); - assert.equal(db.deleted(), 0); -}); + ); + expect(Predicate.isTagged(result, 'conflict')).toBe(true); + expect(db.updates.length).toBe(0); + expect(db.deleted()).toBe(0); + }), +); const verificationCommand = { expectedVerification: 'UNVERIFIED', @@ -256,100 +268,120 @@ const verificationCommand = { verification: 'VERIFIED', } as const; -test('verification collision changes neither metadata nor claim ownership', async () => { - const db = harness({ claimOwner: 'another-party' }); - const result = await runEffectTestPromise( - updateOfficialIdentifierVerificationRecord( +it.effect('verification collision changes neither metadata nor claim ownership', () => + Effect.gen(function* verifyCase6() { + const db = harness({ claimOwner: 'another-party' }); + const result = yield* updateOfficialIdentifierVerificationRecord( db.transaction, tenantId, officialIdentifierId, verificationCommand, - ), - ); - assert.equal(result._tag, 'claim_conflict'); - assert.equal(db.updates.length, 0); - assert.equal(db.inserts.length, 0); -}); + ); + expect(Predicate.isTagged(result, 'claim_conflict')).toBe(true); + expect(db.updates.length).toBe(0); + expect(db.inserts.length).toBe(0); + }), +); -test('verification preserves before-state and immutable identity/provenance while acquiring an eligible claim', async () => { - const db = harness(); - const result = await runEffectTestPromise( - updateOfficialIdentifierVerificationRecord( - db.transaction, - tenantId, - officialIdentifierId, - verificationCommand, - ), - ); - assert.equal(result._tag, 'found'); - const found = Match.value(result).pipe( - Match.tag('found', (value) => value), - Match.orElse(() => assert.fail('Expected the identifier verification update to succeed')), - ); - assert.equal(found.previous.verificationState, 'UNVERIFIED'); - assert.equal(found.value.verificationState, 'VERIFIED'); - assert.equal(found.value.provenanceSource, 'USER_ASSERTION'); - assert.deepEqual(Object.keys(db.updates[0] ?? {}).toSorted(), [ - 'verificationState', - 'verifiedAt', - 'verifiedByPrincipalId', - ]); - assert.equal(db.inserts[0]?.table, partyIdentifierClaims); -}); +it.effect( + 'verification preserves before-state and immutable identity/provenance while acquiring an eligible claim', + () => + Effect.gen(function* verifyCase7() { + const db = harness(); + const result = yield* updateOfficialIdentifierVerificationRecord( + db.transaction, + tenantId, + officialIdentifierId, + verificationCommand, + ); + expect(Predicate.isTagged(result, 'found')).toBe(true); + const found = Match.value(result).pipe( + Match.tag('found', (value) => value), + Match.orElse(() => + (() => { + throw new Error('Expected the identifier verification update to succeed'); + })(), + ), + ); + expect(found.previous.verificationState).toBe('UNVERIFIED'); + expect(found.value.verificationState).toBe('VERIFIED'); + expect(found.value.provenanceSource).toBe('USER_ASSERTION'); + expect(Object.keys(db.updates[0] ?? {}).toSorted()).toEqual([ + 'verificationState', + 'verifiedAt', + 'verifiedByPrincipalId', + ]); + expect(db.inserts[0]?.table).toBe(partyIdentifierClaims); + }), +); -test('PERSON verification cannot acquire an implicit strong identifier claim', async () => { - const db = harness({ partyType: 'PERSON' }); - const result = await runEffectTestPromise( - updateOfficialIdentifierVerificationRecord( +it.effect('PERSON verification cannot acquire an implicit strong identifier claim', () => + Effect.gen(function* verifyCase8() { + const db = harness({ partyType: 'PERSON' }); + const result = yield* updateOfficialIdentifierVerificationRecord( db.transaction, tenantId, officialIdentifierId, verificationCommand, - ), - ); - assert.equal(result._tag, 'found'); - assert.equal(db.inserts.length, 0); -}); + ); + expect(Predicate.isTagged(result, 'found')).toBe(true); + expect(db.inserts.length).toBe(0); + }), +); -test('verification downgrade releases its claim without erasing the previous verification evidence', async () => { - const verifiedAt = date('2026-01-01T00:00:00.000Z'); - const db = harness({ - claimOwner: partyId, - current: row({ verificationState: 'VERIFIED', verifiedAt, verifiedByPrincipalId: principalId }), - }); - const result = await runEffectTestPromise( - updateOfficialIdentifierVerificationRecord(db.transaction, tenantId, officialIdentifierId, { - ...verificationCommand, - expectedVerification: 'VERIFIED', - verification: 'REJECTED', +it.effect( + 'verification downgrade releases its claim without erasing the previous verification evidence', + () => + Effect.gen(function* verifyCase9() { + const verifiedAt = date('2026-01-01T00:00:00.000Z'); + const db = harness({ + claimOwner: partyId, + current: row({ + verificationState: 'VERIFIED', + verifiedAt, + verifiedByPrincipalId: principalId, + }), + }); + const result = yield* updateOfficialIdentifierVerificationRecord( + db.transaction, + tenantId, + officialIdentifierId, + { + ...verificationCommand, + expectedVerification: 'VERIFIED', + verification: 'REJECTED', + }, + ); + expect(Predicate.isTagged(result, 'found')).toBe(true); + const found = Match.value(result).pipe( + Match.tag('found', (value) => value), + Match.orElse(() => + (() => { + throw new Error('Expected the identifier verification downgrade to succeed'); + })(), + ), + ); + expect(found.previous.verifiedAt).toBe(verifiedAt); + expect(found.previous.verifiedByPrincipalId).toBe(principalId); + expect(found.value.verifiedAt).toBe(null); + expect(db.deleted()).toBe(1); }), - ); - assert.equal(result._tag, 'found'); - const found = Match.value(result).pipe( - Match.tag('found', (value) => value), - Match.orElse(() => assert.fail('Expected the identifier verification downgrade to succeed')), - ); - assert.equal(found.previous.verifiedAt, verifiedAt); - assert.equal(found.previous.verifiedByPrincipalId, principalId); - assert.equal(found.value.verifiedAt, null); - assert.equal(db.deleted(), 1); -}); +); -test('archived Party and stale verification updates are rejected before mutation', async () => { - await Promise.all( +it.effect('archived Party and stale verification updates are rejected before mutation', () => + Effect.all( [harness({ archived: true }), harness({ current: row({ verificationState: 'REJECTED' }) })].map( - async (db) => { - const result = await runEffectTestPromise( - updateOfficialIdentifierVerificationRecord( + (db) => + Effect.gen(function* verifyCase11() { + const result = yield* updateOfficialIdentifierVerificationRecord( db.transaction, tenantId, officialIdentifierId, verificationCommand, - ), - ); - assert.equal(result._tag, 'conflict'); - assert.equal(db.updates.length, 0); - }, + ); + expect(Predicate.isTagged(result, 'conflict')).toBe(true); + expect(db.updates.length).toBe(0); + }), ), - ); -}); + ), +); diff --git a/app/verticals/party-registry/tests/unit/identifier-update-outbox.test.ts b/app/verticals/party-registry/tests/unit/identifier-update-outbox.test.ts index e179c83ed..eb33a04f7 100644 --- a/app/verticals/party-registry/tests/unit/identifier-update-outbox.test.ts +++ b/app/verticals/party-registry/tests/unit/identifier-update-outbox.test.ts @@ -1,6 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { DateTime, Effect, Option, Schema } from 'effect'; import { createActionCollector } from '../../../../packages/core-runtime/src/actions/collector.ts'; import { getActionHandler } from '../../../../packages/core-runtime/src/actions/definition.ts'; @@ -36,8 +34,9 @@ const changes: readonly UpdatePartyOfficialIdentifierPayload['change'][] = [ ]; for (const change of changes) { - test(`${change.type} links one stable-reference outbox message to its committed Domain Event`, () => - runEffectTestPromise( + it.effect( + `${change.type} links one stable-reference outbox message to its committed Domain Event`, + () => Effect.gen(function* successfulUpdate() { const collector = createActionCollector( updatePartyOfficialIdentifierAction.descriptor.domainEvents, @@ -92,21 +91,20 @@ for (const change of changes) { }, ); const snapshot = collector.snapshot(); - assert.equal(snapshot.domainEvents.length, 1); - assert.equal(snapshot.outboxMessages.length, 1); - assert.equal(snapshot.outboxMessages[0]?.domainEventIndex, 0); - assert.equal( - snapshot.outboxMessages[0]?.message.topic, + expect(snapshot.domainEvents.length).toBe(1); + expect(snapshot.outboxMessages.length).toBe(1); + expect(snapshot.outboxMessages[0]?.domainEventIndex).toBe(0); + expect(snapshot.outboxMessages[0]?.message.topic).toBe( 'party.registry.official-identifier-updated.v1', ); - assert.deepEqual(snapshot.outboxMessages[0]?.message.payloadJson, { + expect(snapshot.outboxMessages[0]?.message.payloadJson).toEqual({ officialIdentifierRef, partyRef, }); - assert.equal(snapshot.domainEvents[0]?.subjectResourceId, officialIdentifierRef.resourceId); + expect(snapshot.domainEvents[0]?.subjectResourceId).toBe(officialIdentifierRef.resourceId); const event = snapshot.domainEvents[0]?.payloadJson; - assert.ok(event !== undefined); - assert.deepEqual(event, { + expect(event !== undefined).toBe(true); + expect(event).toEqual({ after, before, changeType: change.type, @@ -116,86 +114,87 @@ for (const change of changes) { reason: 'Accepted registry evidence', }); }), - )); + ); } -test('rejected identifier updates publish neither Domain Event nor outbox message', () => - runEffectTestPromise( - Effect.gen(function* rejectedUpdate() { - const collector = createActionCollector( - updatePartyOfficialIdentifierAction.descriptor.domainEvents, - 'party.registry', - updatePartyOfficialIdentifierAction.descriptor.accessEvidencePolicy, - ); - const failure = new OfficialIdentifierClaimConflict({ - code: 'party_identifier_claim_conflict', - reason: 'Already claimed by another Party', - }); - const error = yield* getActionHandler(updatePartyOfficialIdentifierAction)( - { - change: { - expectedVerification: 'UNVERIFIED', - type: 'SET_VERIFICATION', - verification: 'VERIFIED', - }, - evidenceRefs: ['evidence:identifier-update'], - officialIdentifierRef, - reason: 'Accepted registry evidence', +it.effect('rejected identifier updates publish neither Domain Event nor outbox message', () => + Effect.gen(function* rejectedUpdate() { + const collector = createActionCollector( + updatePartyOfficialIdentifierAction.descriptor.domainEvents, + 'party.registry', + updatePartyOfficialIdentifierAction.descriptor.accessEvidencePolicy, + ); + const failure = new OfficialIdentifierClaimConflict({ + code: 'party_identifier_claim_conflict', + reason: 'Already claimed by another Party', + }); + const error = yield* getActionHandler(updatePartyOfficialIdentifierAction)( + { + change: { + expectedVerification: 'UNVERIFIED', + type: 'SET_VERIFICATION', + verification: 'VERIFIED', }, - { - actionInvocationId: '50000000-0000-4000-8000-000000000001', - addDomainEvent: collector.addDomainEvent, - addOutboxMessage: collector.addOutboxMessage, - recordAuditEvidence: collector.recordAuditEvidence, - recordDataAccess: collector.recordDataAccess, - scope: { - authMethod: 'system', - correlationId: 'identifier-outbox-test', - principalId: '40000000-0000-4000-8000-000000000001', - tenantId, - }, - services: { update: () => Effect.fail(failure) }, + evidenceRefs: ['evidence:identifier-update'], + officialIdentifierRef, + reason: 'Accepted registry evidence', + }, + { + actionInvocationId: '50000000-0000-4000-8000-000000000001', + addDomainEvent: collector.addDomainEvent, + addOutboxMessage: collector.addOutboxMessage, + recordAuditEvidence: collector.recordAuditEvidence, + recordDataAccess: collector.recordDataAccess, + scope: { + authMethod: 'system', + correlationId: 'identifier-outbox-test', + principalId: '40000000-0000-4000-8000-000000000001', + tenantId, }, - ).pipe(Effect.flip); - assert.equal(error, failure); - assert.equal(collector.snapshot().domainEvents.length, 0); - assert.equal(collector.snapshot().outboxMessages.length, 0); - }), - )); + services: { update: () => Effect.fail(failure) }, + }, + ).pipe(Effect.flip); + expect(error).toBe(failure); + expect(collector.snapshot().domainEvents.length).toBe(0); + expect(collector.snapshot().outboxMessages.length).toBe(0); + }), +); -test('published identifier update payload contains references only', () => { - const decode = Schema.decodeUnknownSync(OutboxPayloadSchema, { onExcessProperty: 'error' }); - assert.deepEqual(decode({ officialIdentifierRef, partyRef }), { - officialIdentifierRef, - partyRef, - }); - assert.throws(() => decode({ officialIdentifierRef, partyRef, verification: 'VERIFIED' })); -}); +it.effect('published identifier update payload contains references only', () => + Effect.gen(function* verifyOutboxPayload() { + const decode = Schema.decodeUnknownEffect(OutboxPayloadSchema, { onExcessProperty: 'error' }); + expect(yield* decode({ officialIdentifierRef, partyRef })).toEqual({ + officialIdentifierRef, + partyRef, + }); + const error = yield* Effect.flip( + decode({ officialIdentifierRef, partyRef, verification: 'VERIFIED' }), + ); + expect(error).toBeDefined(); + }), +); -test('identifier update results keep DateTime and Option internally with nullable JSON', () => - runEffectTestPromise( - Effect.gen(function* identifierUpdateResultWireRoundTrip() { - const wire = { - officialIdentifierRef, - partyRef, - state: 'ENDED', - validTo: '2026-01-02T00:00:00.000Z', - verification: 'VERIFIED', - } as const; - const decoded = yield* Schema.decodeUnknownEffect(UpdatePartyOfficialIdentifierResultSchema)( - wire, - ); - assert.equal(Option.isSome(decoded.validTo), true); - assert.equal( - Option.match(decoded.validTo, { - onNone: () => null, - onSome: DateTime.formatIso, - }), - wire.validTo, - ); - assert.deepEqual( - yield* Schema.encodeEffect(UpdatePartyOfficialIdentifierResultSchema)(decoded), - wire, - ); - }), - )); +it.effect('identifier update results keep DateTime and Option internally with nullable JSON', () => + Effect.gen(function* identifierUpdateResultWireRoundTrip() { + const wire = { + officialIdentifierRef, + partyRef, + state: 'ENDED', + validTo: '2026-01-02T00:00:00.000Z', + verification: 'VERIFIED', + } as const; + const decoded = yield* Schema.decodeUnknownEffect(UpdatePartyOfficialIdentifierResultSchema)( + wire, + ); + expect(Option.isSome(decoded.validTo)).toBe(true); + expect( + Option.match(decoded.validTo, { + onNone: () => null, + onSome: DateTime.formatIso, + }), + ).toBe(wire.validTo); + expect(yield* Schema.encodeEffect(UpdatePartyOfficialIdentifierResultSchema)(decoded)).toEqual( + wire, + ); + }), +); diff --git a/app/verticals/party-registry/tests/unit/identity-action-evidence.test.ts b/app/verticals/party-registry/tests/unit/identity-action-evidence.test.ts index bc87df5b4..5bfb9cd90 100644 --- a/app/verticals/party-registry/tests/unit/identity-action-evidence.test.ts +++ b/app/verticals/party-registry/tests/unit/identity-action-evidence.test.ts @@ -1,7 +1,5 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { DateTime, Effect, Option, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { DateTime, Effect, Option, Schema, Predicate } from 'effect'; import { bindActionTestServices, makeActionTestHarness } from '@app/core-runtime/testing/actions'; import { createActionCollector } from '../../../../packages/core-runtime/src/actions/collector.ts'; import { getActionHandler } from '../../../../packages/core-runtime/src/actions/definition.ts'; @@ -33,68 +31,67 @@ const party = Schema.decodeUnknownSync(PartySchema)({ }); const assertInvariantEvidence = (snapshot: ActionEvidenceSnapshot) => { - assert.equal(snapshot.dataAccessEvents.length, 1); + expect(snapshot.dataAccessEvents.length).toBe(1); const [access] = snapshot.dataAccessEvents; - assert.equal(access?.evidenceCaptureMode, 'metadata_only'); - assert.equal(access?.targetResourceId, partyId); - assert.equal(access?.targetResourceType, 'party.registry.party'); - assert.equal(access?.resultCount, 1); - assert.equal(access?.evidencePayloadJson, undefined); - assert.equal(snapshot.domainEvents.length, 1); - assert.equal(snapshot.outboxMessages.length, 1); + expect(access?.evidenceCaptureMode).toBe('metadata_only'); + expect(access?.targetResourceId).toBe(partyId); + expect(access?.targetResourceType).toBe('party.registry.party'); + expect(access?.resultCount).toBe(1); + expect(access?.evidencePayloadJson).toBe(undefined); + expect(snapshot.domainEvents.length).toBe(1); + expect(snapshot.outboxMessages.length).toBe(1); }; -test('Update Party records metadata-only invariant evidence with its event and outbox', () => - runEffectTestPromise( - Effect.gen(function* verifyUpdateEvidence() { - const collector = createActionCollector( - updatePartyAction.descriptor.domainEvents, - 'party.registry', - updatePartyAction.descriptor.accessEvidencePolicy, - ); - yield* getActionHandler(updatePartyAction)( - { - displayName: 'Example organization', - expectedRevision: 1, - partyRef: party.partyRef, - provenanceMethod: 'MANUAL', - provenanceSource: 'test', - validFrom: DateTime.makeUnsafe('2026-01-01T00:00:00.000Z'), - }, - { - ...collector, - actionInvocationId, - scope, - services: { update: () => Effect.succeed({ _tag: 'found', value: party }) }, - }, - ); - assertInvariantEvidence(collector.snapshot()); - }), - )); +it.effect('Update Party records metadata-only invariant evidence with its event and outbox', () => + Effect.gen(function* verifyUpdateEvidence() { + const collector = createActionCollector( + updatePartyAction.descriptor.domainEvents, + 'party.registry', + updatePartyAction.descriptor.accessEvidencePolicy, + ); + yield* getActionHandler(updatePartyAction)( + { + displayName: 'Example organization', + expectedRevision: 1, + partyRef: party.partyRef, + provenanceMethod: 'MANUAL', + provenanceSource: 'test', + validFrom: DateTime.makeUnsafe('2026-01-01T00:00:00.000Z'), + }, + { + ...collector, + actionInvocationId, + scope, + services: { update: () => Effect.succeed({ _tag: 'found', value: party }) }, + }, + ); + assertInvariantEvidence(collector.snapshot()); + }), +); -test('Archive Party records metadata-only invariant evidence with its event and outbox', () => - runEffectTestPromise( - Effect.gen(function* verifyArchiveEvidence() { - const collector = createActionCollector( - archivePartyAction.descriptor.domainEvents, - 'party.registry', - archivePartyAction.descriptor.accessEvidencePolicy, - ); - yield* getActionHandler(archivePartyAction)( - { expectedRevision: 1, partyRef: party.partyRef, reason: 'No longer active' }, - { - ...collector, - actionInvocationId, - scope, - services: { transition: () => Effect.succeed({ _tag: 'found', value: party }) }, - }, - ); - assertInvariantEvidence(collector.snapshot()); - }), - )); +it.effect('Archive Party records metadata-only invariant evidence with its event and outbox', () => + Effect.gen(function* verifyArchiveEvidence() { + const collector = createActionCollector( + archivePartyAction.descriptor.domainEvents, + 'party.registry', + archivePartyAction.descriptor.accessEvidencePolicy, + ); + yield* getActionHandler(archivePartyAction)( + { expectedRevision: 1, partyRef: party.partyRef, reason: 'No longer active' }, + { + ...collector, + actionInvocationId, + scope, + services: { transition: () => Effect.succeed({ _tag: 'found', value: party }) }, + }, + ); + assertInvariantEvidence(collector.snapshot()); + }), +); -test('Unarchive Party records metadata-only invariant evidence with its event and outbox', () => - runEffectTestPromise( +it.effect( + 'Unarchive Party records metadata-only invariant evidence with its event and outbox', + () => Effect.gen(function* verifyUnarchiveEvidence() { const collector = createActionCollector( unarchivePartyAction.descriptor.domainEvents, @@ -112,10 +109,11 @@ test('Unarchive Party records metadata-only invariant evidence with its event an ); assertInvariantEvidence(collector.snapshot()); }), - )); +); -test('Unarchive review outcome commits once and replays without an unarchive event or outbox', () => - runEffectTestPromise( +it.effect( + 'Unarchive review outcome commits once and replays without an unarchive event or outbox', + () => Effect.gen(function* verifyUnarchiveConflictEvidence() { let calls = 0; const blocked = { @@ -128,7 +126,7 @@ test('Unarchive review outcome commits once and replays without an unarchive eve }, reasonCode: 'EXACT_CLAIM_CONFLICT' as const, }; - const harness = makeActionTestHarness({ + const harness = yield* makeActionTestHarness({ actionPermission: 'allowed', services: [ bindActionTestServices(unarchivePartyAction, { @@ -153,15 +151,15 @@ test('Unarchive review outcome commits once and replays without an unarchive eve registration: unarchivePartyAction, transport: { correlationId: 'unarchive-review', idempotencyKey: 'unarchive-once' }, }; - assert.deepEqual(yield* harness.runtime.runAction(request), blocked); + expect(yield* harness.runtime.runAction(request)).toEqual(blocked); const replay = yield* harness.runtime.runAction(request).pipe(Effect.flip); - assert.equal(replay._tag, 'ActionAlreadyCommitted'); - assert.equal(calls, 1); + expect(Predicate.isTagged(replay, 'ActionAlreadyCommitted')).toBe(true); + expect(calls).toBe(1); const snapshot = harness.snapshot(); - assert.equal(snapshot.committed.length, 1); - assert.equal(snapshot.invocations[0]?.status, 'succeeded'); - assert.equal(snapshot.committed[0]?.evidence.dataAccessEvents.length, 1); - assert.deepEqual(snapshot.committed[0]?.evidence.domainEvents, []); - assert.deepEqual(snapshot.committed[0]?.evidence.outboxMessages, []); + expect(snapshot.committed.length).toBe(1); + expect(snapshot.invocations[0]?.status).toBe('succeeded'); + expect(snapshot.committed[0]?.evidence.dataAccessEvents.length).toBe(1); + expect(snapshot.committed[0]?.evidence.domainEvents).toEqual([]); + expect(snapshot.committed[0]?.evidence.outboxMessages).toEqual([]); }), - )); +); diff --git a/app/verticals/party-registry/tests/unit/identity-contract.test.ts b/app/verticals/party-registry/tests/unit/identity-contract.test.ts index 42becbb23..55893aa80 100644 --- a/app/verticals/party-registry/tests/unit/identity-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/identity-contract.test.ts @@ -1,6 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { DateTime, Option, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, DateTime, Option, Schema, Struct } from 'effect'; import { PartyCandidateSchema, IsoTimestampSchema, @@ -19,12 +18,12 @@ import { makePartyMatchDecisionRef } from '../../shared/resources/party-match-de const decode = Schema.decodeUnknownSync; -test('Party V1 admits only PERSON, ORGANIZATION, and evidenced UNRESOLVED identity', () => { +it('Party V1 admits only PERSON, ORGANIZATION, and evidenced UNRESOLVED identity', () => { for (const partyType of ['PERSON', 'ORGANIZATION', 'UNRESOLVED']) { - assert.equal(decode(PartyTypeSchema)(partyType), partyType); + expect(decode(PartyTypeSchema)(partyType)).toBe(partyType); } - assert.throws(() => decode(PartyTypeSchema)('OTHER')); - assert.throws(() => + expect(() => decode(PartyTypeSchema)('OTHER')).toThrow(); + expect(() => decode(PartyCandidateSchema)({ displayName: ' ', evidenceRefs: [], @@ -33,107 +32,112 @@ test('Party V1 admits only PERSON, ORGANIZATION, and evidenced UNRESOLVED identi provenance: { method: 'MANUAL', source: 'test' }, validFrom: '2026-01-01T00:00:00.000Z', }), - ); + ).toThrow(); }); -test('Party Type update is enrichment-only; cross-kind changes require Correction', () => { - assert.equal(isPartyTypeEnrichment('UNRESOLVED', 'PERSON'), true); - assert.equal(isPartyTypeEnrichment('UNRESOLVED', 'ORGANIZATION'), true); - assert.equal(isPartyTypeEnrichment('PERSON', 'ORGANIZATION'), false); - assert.equal(isPartyTypeEnrichment('ORGANIZATION', 'PERSON'), false); +it('Party Type update is enrichment-only; cross-kind changes require Correction', () => { + expect(isPartyTypeEnrichment('UNRESOLVED', 'PERSON')).toBe(true); + expect(isPartyTypeEnrichment('UNRESOLVED', 'ORGANIZATION')).toBe(true); + expect(isPartyTypeEnrichment('PERSON', 'ORGANIZATION')).toBe(false); + expect(isPartyTypeEnrichment('ORGANIZATION', 'PERSON')).toBe(false); }); -test('identity timestamps decode to canonical UTC values', () => { - assert.throws(() => decode(IsoTimestampSchema)('not-a-timestamp')); +it('identity timestamps decode to canonical UTC values', () => { + expect(() => decode(IsoTimestampSchema)('not-a-timestamp')).toThrow(); const leapDay = decode(IsoTimestampSchema)('2024-02-29T00:00:00Z'); - assert.equal(DateTime.formatIso(leapDay), '2024-02-29T00:00:00.000Z'); + expect(DateTime.formatIso(leapDay)).toBe('2024-02-29T00:00:00.000Z'); }); -test('Party JSON round-trips timestamps as strings and absent values as null', () => { - const encoded = { - archivedAt: null, - createdAt: '2025-01-01T00:00:00.000Z', - displayName: null, - partyRef: makePartyRef( - '11111111-1111-4111-8111-111111111111', - '22222222-2222-4222-8222-222222222222', - ), - partyType: 'UNRESOLVED' as const, - revision: 1, - updatedAt: '2026-01-01T00:00:00.000Z', - }; - const decoded = decode(PartySchema)(encoded); +it.effect('Party JSON round-trips timestamps as strings and absent values as null', () => + Effect.gen(function* verifySchema1() { + const encoded = { + archivedAt: null, + createdAt: '2025-01-01T00:00:00.000Z', + displayName: null, + partyRef: makePartyRef( + '11111111-1111-4111-8111-111111111111', + '22222222-2222-4222-8222-222222222222', + ), + partyType: 'UNRESOLVED' as const, + revision: 1, + updatedAt: '2026-01-01T00:00:00.000Z', + }; + const decoded = decode(PartySchema)(encoded); - assert.equal(Option.isNone(decoded.archivedAt), true); - assert.equal(Option.isNone(decoded.displayName), true); - assert.deepEqual(Schema.encodeSync(PartySchema)(decoded), encoded); - assert.throws(() => decode(PartySchema)({ ...encoded, archivedAt: undefined })); - const { displayName: _displayName, ...missingDisplayName } = encoded; - assert.throws(() => decode(PartySchema)(missingDisplayName)); + expect(Option.isNone(decoded.archivedAt)).toBe(true); + expect(Option.isNone(decoded.displayName)).toBe(true); + expect(yield* Schema.encodeEffect(PartySchema)(decoded)).toEqual(encoded); + expect(() => decode(PartySchema)({ ...encoded, archivedAt: undefined })).toThrow(); + const { displayName: _displayName, ...missingDisplayName } = encoded; + expect(() => decode(PartySchema)(missingDisplayName)).toThrow(); - const presentEncoded = { - ...encoded, - archivedAt: '2026-02-01T00:00:00.000Z', - displayName: 'Example organization', - }; - assert.deepEqual( - Schema.encodeSync(PartySchema)(decode(PartySchema)(presentEncoded)), - presentEncoded, - ); -}); + const presentEncoded = { + ...encoded, + archivedAt: '2026-02-01T00:00:00.000Z', + displayName: 'Example organization', + }; + expect(yield* Schema.encodeEffect(PartySchema)(decode(PartySchema)(presentEncoded))).toEqual( + presentEncoded, + ); + }), +); -test('Party Candidate accepts an evidenced identifier without inventing a display name', () => { - const encoded = { - evidenceRefs: ['source:official-record'], - officialIdentifiers: [{ identifierType: 'ICO', value: '27074358', verification: 'VERIFIED' }], - partyType: 'ORGANIZATION' as const, - provenance: { method: 'IMPORT', source: 'official-register' }, - validFrom: '2026-01-01T00:00:00.000Z', - }; - const candidate = decode(PartyCandidateSchema)(encoded); - assert.equal(candidate.displayName, undefined); - assert.equal(candidate.officialIdentifiers.length, 1); - assert.deepEqual(Schema.encodeSync(PartyCandidateSchema)(candidate), encoded); -}); +it.effect('Party Candidate accepts an evidenced identifier without inventing a display name', () => + Effect.gen(function* verifySchema2() { + const encoded = { + evidenceRefs: ['source:official-record'], + officialIdentifiers: [{ identifierType: 'ICO', value: '27074358', verification: 'VERIFIED' }], + partyType: 'ORGANIZATION' as const, + provenance: { method: 'IMPORT', source: 'official-register' }, + validFrom: '2026-01-01T00:00:00.000Z', + }; + const candidate = decode(PartyCandidateSchema)(encoded); + expect(candidate.displayName).toBe(undefined); + expect(candidate.officialIdentifiers.length).toBe(1); + expect(yield* Schema.encodeEffect(PartyCandidateSchema)(candidate)).toEqual(encoded); + }), +); -test('Party references retain tenant, module, resource type, and resource identity', () => { - assert.deepEqual( +it('Party references retain tenant, module, resource type, and resource identity', () => { + expect( makePartyRef('11111111-1111-4111-8111-111111111111', '22222222-2222-4222-8222-222222222222'), - { - moduleId: 'party.registry', - resourceId: '22222222-2222-4222-8222-222222222222', - resourceType: 'party.registry.party', - tenantId: '11111111-1111-4111-8111-111111111111', - }, - ); + ).toEqual({ + moduleId: 'party.registry', + resourceId: '22222222-2222-4222-8222-222222222222', + resourceType: 'party.registry.party', + tenantId: '11111111-1111-4111-8111-111111111111', + }); }); -test('Party identity failures retain branded identifiers in encoded JSON', () => { - const partyId = '22222222-2222-4222-8222-222222222222'; - const failure = new PartyNotFound({ - code: 'party_not_found', - partyId: partyIdFromString(partyId), - reason: 'The Party does not exist', - }); +it.effect('Party identity failures retain branded identifiers in encoded JSON', () => + Effect.gen(function* verifySchema3() { + const partyId = '22222222-2222-4222-8222-222222222222'; + const failure = new PartyNotFound({ + code: 'party_not_found', + partyId: partyIdFromString(partyId), + reason: 'The Party does not exist', + }); - assert.deepEqual(Schema.encodeSync(PartyNotFound)(failure), { - _tag: 'PartyNotFound', - code: 'party_not_found', - partyId, - reason: 'The Party does not exist', - }); -}); + const encodedFailure = yield* Schema.encodeEffect(PartyNotFound)(failure); + expect(Schema.is(Schema.toEncoded(PartyNotFound))(encodedFailure)).toBe(true); + expect(Struct.omit(encodedFailure, ['_tag'])).toEqual({ + code: 'party_not_found', + partyId, + reason: 'The Party does not exist', + }); + }), +); -test('Party identity Actions are tenant-authorized, optionally scoped, and idempotent', () => { +it('Party identity Actions are tenant-authorized, optionally scoped, and idempotent', () => { for (const action of [createPartyAction, updatePartyAction, unarchivePartyAction]) { - assert.equal(action.descriptor.legalEntityScope, 'optional'); - assert.equal(action.descriptor.idempotency, 'required'); + expect(action.descriptor.legalEntityScope).toBe('optional'); + expect(action.descriptor.idempotency).toBe('required'); // SAFETY: These identity permission callbacks are constant and do not inspect payload fields. - assert.equal(action.descriptor.tenantPermission?.({} as never), 'manage_party_identity'); + expect(action.descriptor.tenantPermission?.({} as never)).toBe('manage_party_identity'); } }); -test('Party unarchive declares durable blocked outcomes with case and decision references', () => { +it('Party unarchive declares durable blocked outcomes with case and decision references', () => { const tenantId = '11111111-1111-4111-8111-111111111111'; const firstPartyId = '22222222-2222-4222-8222-222222222222'; const secondPartyId = '33333333-3333-4333-8333-333333333333'; @@ -158,9 +162,9 @@ test('Party unarchive declares durable blocked outcomes with case and decision r }, reasonCode, }); - assert.equal(result.outcome, 'BLOCKED'); + expect(result.outcome).toBe('BLOCKED'); if (result.outcome === 'BLOCKED') { - assert.equal(result.reasonCode, reasonCode); + expect(result.reasonCode).toBe(reasonCode); } } }); diff --git a/app/verticals/party-registry/tests/unit/identity-create-without-strong-identifier.test.ts b/app/verticals/party-registry/tests/unit/identity-create-without-strong-identifier.test.ts index 20e529caa..33d4bf15f 100644 --- a/app/verticals/party-registry/tests/unit/identity-create-without-strong-identifier.test.ts +++ b/app/verticals/party-registry/tests/unit/identity-create-without-strong-identifier.test.ts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { DateTime } from 'effect'; import { partySubjectKeyFromString } from '../../shared/domain/identity-contracts.ts'; import type { @@ -37,12 +36,13 @@ const candidate = (overrides: Partial = {}): PartyCandidate => ( }); const decide = (value: PartyCandidate) => decideCreateWithoutStrongIdentifier(value, { requireIdentityReview: false }); -test('concrete subject evidence needs neither a name nor an official ID', () => { - assert.equal(decide(candidate()).decision, 'ALLOW'); - assert.equal(decide(candidate({ displayName: 'A' })).decision, 'ALLOW'); - assert.equal(decide(candidate({ displayName: 'Unknown' })).decision, 'ALLOW'); +it('concrete subject evidence needs neither a name nor an official ID', () => { + expect(decide(candidate()).decision).toBe('ALLOW'); + expect(decide(candidate({ displayName: 'A' })).decision).toBe('ALLOW'); + expect(decide(candidate({ displayName: 'Unknown' })).decision).toBe('ALLOW'); }); -test('names, reference prefixes and identifiers never substitute for subject evidence', () => { + +it('names, reference prefixes and identifiers never substitute for subject evidence', () => { for (const input of [ candidate({ displayName: 'Jane Smith', subjectEvidence: [] }), candidate({ @@ -54,28 +54,28 @@ test('names, reference prefixes and identifiers never substitute for subject evi subjectEvidence: [], }), ]) { - assert.equal(decide(input).decision, 'DENY'); + expect(decide(input).decision).toBe('DENY'); } }); -test('type support is separate from evidence of a concrete subject', () => { + +it('type support is separate from evidence of a concrete subject', () => { for (const partyType of ['PERSON', 'ORGANIZATION'] as const) { - assert.equal(decide(candidate({ partyType })).reasonCode, 'party_type_evidence_required'); - assert.equal( - decide(candidate({ partyType, subjectEvidence: [evidence(partyType)] })).decision, + expect(decide(candidate({ partyType })).reasonCode).toBe('party_type_evidence_required'); + expect(decide(candidate({ partyType, subjectEvidence: [evidence(partyType)] })).decision).toBe( 'ALLOW', ); } - assert.equal( + expect( decide(candidate({ subjectEvidence: [evidence('PERSON'), evidence('ORGANIZATION')] })) .reasonCode, - 'conflicting_type_evidence', - ); + ).toBe('conflicting_type_evidence'); }); -test('technical records, managed Legal Entities and multiple subjects fail closed', () => { + +it('technical records, managed Legal Entities and multiple subjects fail closed', () => { for (const kind of ['TECHNICAL_RECORD', 'MANAGED_LEGAL_ENTITY'] as const) { - assert.equal(decide(candidate({ subjectEvidence: [evidence(kind)] })).decision, 'DENY'); + expect(decide(candidate({ subjectEvidence: [evidence(kind)] })).decision).toBe('DENY'); } - assert.equal( + expect( decide( candidate({ subjectEvidence: [ @@ -84,30 +84,29 @@ test('technical records, managed Legal Entities and multiple subjects fail close ], }), ).reasonCode, - 'one_concrete_subject_required', - ); + ).toBe('one_concrete_subject_required'); }); -test('review configuration cannot waive evidence and eligible review remains atomic', () => { - assert.deepEqual(createPartyAction.descriptor.policies, []); - assert.equal( - decideAtomicCreateWithoutStrongIdentifier(candidate(), false).decision, + +it('review configuration cannot waive evidence and eligible review remains atomic', () => { + expect(createPartyAction.descriptor.policies).toEqual([]); + expect(decideAtomicCreateWithoutStrongIdentifier(candidate(), false).decision).toBe( 'REVIEW_REQUIRED', ); - assert.equal(decideAtomicCreateWithoutStrongIdentifier(candidate(), true).decision, 'ALLOW'); - assert.equal( + expect(decideAtomicCreateWithoutStrongIdentifier(candidate(), true).decision).toBe('ALLOW'); + expect( decideAtomicCreateWithoutStrongIdentifier(candidate({ subjectEvidence: [] }), true).decision, - 'DENY', - ); + ).toBe('DENY'); }); -test('reference spelling is neutral; meaningful evidence and independent versions are retained', () => { + +it('reference spelling is neutral; meaningful evidence and independent versions are retained', () => { const original = candidate(); const arbitrary = candidate({ subjectEvidence: [{ ...evidence('CONCRETE_SUBJECT'), evidenceRef: 'anything' }], }); - assert.equal(decide(arbitrary).decision, 'ALLOW'); - assert.notEqual(candidateFingerprint(original), candidateFingerprint(arbitrary)); + expect(decide(arbitrary).decision).toBe('ALLOW'); + expect(candidateFingerprint(original)).not.toBe(candidateFingerprint(arbitrary)); const result = evaluatePartySubjectEvidence(original); - assert.equal(result.subjectEligibilityVersion, 'party-concrete-subject.v1'); - assert.equal(result.typeRuleVersion, 'party-subject-type.v1'); - assert.deepEqual(result.evidence, original.subjectEvidence); + expect(result.subjectEligibilityVersion).toBe('party-concrete-subject.v1'); + expect(result.typeRuleVersion).toBe('party-subject-type.v1'); + expect(result.evidence).toEqual(original.subjectEvidence); }); diff --git a/app/verticals/party-registry/tests/unit/identity-party-detail-alias.test.ts b/app/verticals/party-registry/tests/unit/identity-party-detail-alias.test.ts index 9d17133cc..d2f23cc99 100644 --- a/app/verticals/party-registry/tests/unit/identity-party-detail-alias.test.ts +++ b/app/verticals/party-registry/tests/unit/identity-party-detail-alias.test.ts @@ -1,7 +1,5 @@ -import { runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { DateTime, Effect, Option, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { DateTime, Effect, Option, Schema, Predicate } from 'effect'; import { PartyDetailResponseSchema } from '../../shared/apis/party-detail.ts'; import { PartySchema } from '../../shared/domain/identity-contracts.ts'; import type { Party } from '../../shared/domain/identity-contracts.ts'; @@ -63,77 +61,85 @@ const makeServices = ( }; }; -test('Party Detail reads the final canonical Party after the complete historical alias chain', () => { - const { lookups, services } = makeServices([ - alias('party-b', 'party-a'), - alias('party-a', 'party-c'), - ]); - const result = runEffectTestSync( - readPartyDetailFromServices(partyRef('party-b'), tenantId, services), - ); +it.effect( + 'Party Detail reads the final canonical Party after the complete historical alias chain', + () => + Effect.gen(function* verifyPartyDetail1() { + const { lookups, services } = makeServices([ + alias('party-b', 'party-a'), + alias('party-a', 'party-c'), + ]); + const result = yield* readPartyDetailFromServices(partyRef('party-b'), tenantId, services); - assert.deepEqual(result, { - currentFactAssertions: [], - factHistory: Option.none(), - party: canonicalParty, - resolution: { - aliasChain: [partyRef('party-b'), partyRef('party-a')], - canonicalPartyRef: partyRef('party-c'), - kind: 'ALIAS', - requestedPartyRef: partyRef('party-b'), - }, - }); - assert.deepEqual(lookups, ['party-c']); - assert.equal(Schema.is(PartyDetailResponseSchema)(result), true); - const encoded = Schema.encodeSync(PartyDetailResponseSchema)(result); - assert.equal(encoded.factHistory, null); - assert.deepEqual(encoded.party, canonicalPartyWire); -}); + expect(result).toEqual({ + currentFactAssertions: [], + factHistory: Option.none(), + party: canonicalParty, + resolution: { + aliasChain: [partyRef('party-b'), partyRef('party-a')], + canonicalPartyRef: partyRef('party-c'), + kind: 'ALIAS', + requestedPartyRef: partyRef('party-b'), + }, + }); + expect(lookups).toEqual(['party-c']); + expect(Schema.is(PartyDetailResponseSchema)(result)).toBe(true); + const encoded = yield* Schema.encodeEffect(PartyDetailResponseSchema)(result); + expect(encoded.factHistory).toBe(null); + expect(encoded.party).toEqual(canonicalPartyWire); + }), +); -test('Party Detail preserves archived lifecycle independently of direct resolution metadata', () => { - const archivedAt = '2026-09-02T10:00:00.000Z'; - const archivedParty = Schema.decodeUnknownSync(PartySchema)({ - ...canonicalPartyWire, - archivedAt, - }); - const { services } = makeServices([], archivedParty); - const result = runEffectTestSync( - readPartyDetailFromServices(partyRef('party-c'), tenantId, services), - ); +it.effect( + 'Party Detail preserves archived lifecycle independently of direct resolution metadata', + () => + Effect.gen(function* verifyPartyDetail2() { + const archivedAt = '2026-09-02T10:00:00.000Z'; + const archivedParty = yield* Schema.decodeUnknownEffect(PartySchema)({ + ...canonicalPartyWire, + archivedAt, + }); + const { services } = makeServices([], archivedParty); + const result = yield* readPartyDetailFromServices(partyRef('party-c'), tenantId, services); - assert.deepEqual(result.party.archivedAt, Option.some(DateTime.makeUnsafe(archivedAt))); - assert.deepEqual(result.resolution, { - aliasChain: [], - canonicalPartyRef: partyRef('party-c'), - kind: 'DIRECT', - requestedPartyRef: partyRef('party-c'), - }); -}); + expect(result.party.archivedAt).toEqual(Option.some(DateTime.makeUnsafe(archivedAt))); + expect(result.resolution).toEqual({ + aliasChain: [], + canonicalPartyRef: partyRef('party-c'), + kind: 'DIRECT', + requestedPartyRef: partyRef('party-c'), + }); + }), +); -test('Party Detail fails closed for cycles and broken historical chains without reading an alias Party', () => { - for (const aliases of [ - [alias('party-a', 'party-b'), alias('party-b', 'party-a')], - [alias('party-a', 'missing')], - ]) { - const { lookups, services } = makeServices(aliases); - const error = runEffectTestSync( - Effect.flip(readPartyDetailFromServices(partyRef('party-a'), tenantId, services)), - ); - assert.equal(error._tag, 'ReadHandlerUnavailable'); - assert.deepEqual(lookups, []); - } -}); +it.effect( + 'Party Detail fails closed for cycles and broken historical chains without reading an alias Party', + () => + Effect.gen(function* verifyPartyDetail3() { + for (const aliases of [ + [alias('party-a', 'party-b'), alias('party-b', 'party-a')], + [alias('party-a', 'missing')], + ]) { + const { lookups, services } = makeServices(aliases); + const error = yield* Effect.flip( + readPartyDetailFromServices(partyRef('party-a'), tenantId, services), + ); + expect(Predicate.isTagged(error, 'ReadHandlerUnavailable')).toBe(true); + expect(lookups).toEqual([]); + } + }), +); -test('Party Detail hides a missing direct Party and a cross-tenant requested reference', () => { - const { lookups, services } = makeServices([]); - for (const requested of [ - partyRef('missing'), - { ...partyRef('party-c'), tenantId: otherTenantId }, - ]) { - const error = runEffectTestSync( - Effect.flip(readPartyDetailFromServices(requested, tenantId, services)), - ); - assert.equal(error._tag, 'ReadHandlerNotFound'); - } - assert.deepEqual(lookups, []); -}); +it.effect('Party Detail hides a missing direct Party and a cross-tenant requested reference', () => + Effect.gen(function* verifyPartyDetail4() { + const { lookups, services } = makeServices([]); + for (const requested of [ + partyRef('missing'), + { ...partyRef('party-c'), tenantId: otherTenantId }, + ]) { + const error = yield* Effect.flip(readPartyDetailFromServices(requested, tenantId, services)); + expect(Predicate.isTagged(error, 'ReadHandlerNotFound')).toBe(true); + } + expect(lookups).toEqual([]); + }), +); diff --git a/app/verticals/party-registry/tests/unit/identity-party-detail-history.test.ts b/app/verticals/party-registry/tests/unit/identity-party-detail-history.test.ts index a9a68ddac..93865eaef 100644 --- a/app/verticals/party-registry/tests/unit/identity-party-detail-history.test.ts +++ b/app/verticals/party-registry/tests/unit/identity-party-detail-history.test.ts @@ -1,7 +1,5 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from 'effect-rstest'; import { Effect, Option, Schema } from 'effect'; -import assert from 'node:assert/strict'; -import { test } from 'node:test'; import { makeTestDatabase } from '../../../../packages/core-runtime/tests/support/database.ts'; import { PartyFactAssertionSchema } from '../../shared/apis/party-detail.ts'; import { PartySchema } from '../../shared/domain/identity-contracts.ts'; @@ -36,95 +34,103 @@ const wireFact = { } as const; const fact = Schema.decodeUnknownSync(PartyFactAssertionSchema)(wireFact); -test('Party fact assertion contract exposes usable correction identities without sensitive evidence', () => { - assert.deepEqual(Schema.encodeSync(PartyFactAssertionSchema)(fact), wireFact); - assert.throws(() => - Schema.decodeUnknownSync(PartyFactAssertionSchema)({ ...wireFact, assertionId: 'not-a-uuid' }), - ); - const decodedWithSensitiveFields = Schema.decodeUnknownSync(PartyFactAssertionSchema)({ - ...wireFact, - evidenceRefs: ['secret'], - provenance: { source: 'secret' }, - }); - assert.deepEqual( - Schema.encodeSync(PartyFactAssertionSchema)(decodedWithSensitiveFields), - wireFact, - ); -}); +it.effect( + 'Party fact assertion contract exposes usable correction identities without sensitive evidence', + () => + Effect.gen(function* verifySchema1() { + expect(yield* Schema.encodeEffect(PartyFactAssertionSchema)(fact)).toEqual(wireFact); + expect(() => + Schema.decodeUnknownSync(PartyFactAssertionSchema)({ + ...wireFact, + assertionId: 'not-a-uuid', + }), + ).toThrow(); + const decodedWithSensitiveFields = yield* Schema.decodeUnknownEffect( + PartyFactAssertionSchema, + )({ + ...wireFact, + evidenceRefs: ['secret'], + provenance: { source: 'secret' }, + }); + expect( + yield* Schema.encodeEffect(PartyFactAssertionSchema)(decodedWithSensitiveFields), + ).toEqual(wireFact); + }), +); -test('Party Detail history derives reviewer authority while current fact targets retain normal read authority', () => { - assert.equal(partyDetailPermissionTarget({ partyRef }).permission, 'read_party_identity'); - assert.equal( - partyDetailPermissionTarget({ includeFactHistory: false, partyRef }).permission, +it('Party Detail history derives reviewer authority while current fact targets retain normal read authority', () => { + expect(partyDetailPermissionTarget({ partyRef }).permission).toBe('read_party_identity'); + expect(partyDetailPermissionTarget({ includeFactHistory: false, partyRef }).permission).toBe( 'read_party_identity', ); - assert.equal( - partyDetailPermissionTarget({ includeFactHistory: true, partyRef }).permission, + expect(partyDetailPermissionTarget({ includeFactHistory: true, partyRef }).permission).toBe( 'review_party_identity', ); }); -test('Party Detail persistence reads safe current and immutable historical assertions through a tenant-scoped query', () => { - const queries: string[] = []; - const values: (readonly unknown[])[] = []; - const rows = [ - [ - previousId, - null, - 'DISPLAY_NAME', - false, - '2026-09-01T10:00:00.000Z', - null, - 'SUPERSEDED', - null, - '2026-09-01T10:00:00.000Z', - '2026-09-03T10:00:00.000Z', - 'Original name', - ], - [ - assertionId, - null, - 'DISPLAY_NAME', - true, - '2026-09-03T10:00:00.000Z', - null, - 'ACTIVE', - previousId, - '2026-09-03T10:00:00.000Z', - null, - 'Corrected name', - ], - ]; - const database = makeTestDatabase((text, parameters) => - Effect.sync(() => { - queries.push(text); - values.push(parameters); - return rows.map((row) => - Object.fromEntries(row.map((value, index) => [String(index), value])), +it.effect( + 'Party Detail persistence reads safe current and immutable historical assertions through a tenant-scoped query', + () => + Effect.gen(function* verifyPartyDetail1() { + const queries: string[] = []; + const values: (readonly unknown[])[] = []; + const rows = [ + [ + previousId, + null, + 'DISPLAY_NAME', + false, + '2026-09-01T10:00:00.000Z', + null, + 'SUPERSEDED', + null, + '2026-09-01T10:00:00.000Z', + '2026-09-03T10:00:00.000Z', + 'Original name', + ], + [ + assertionId, + null, + 'DISPLAY_NAME', + true, + '2026-09-03T10:00:00.000Z', + null, + 'ACTIVE', + previousId, + '2026-09-03T10:00:00.000Z', + null, + 'Corrected name', + ], + ]; + const database = yield* makeTestDatabase((text: string, parameters: readonly unknown[]) => + Effect.sync(() => { + queries.push(text); + values.push(parameters); + return rows.map((row) => + Object.fromEntries(row.map((value, index) => [String(index), value])), + ); + }), ); - }), - ); - return runEffectTestPromise( - Effect.gen(function* checkSafeHistory() { + const result = yield* findPartyDetailAssertions(database, tenantId, partyId, true); - assert.deepEqual(result.currentFactAssertions, [fact]); + expect(result.currentFactAssertions).toEqual([fact]); const history = Option.getOrThrow(result.factHistory); - assert.equal(history.length, 2); - assert.equal(history[0]?.value, 'Original name'); - assert.equal(history[0]?.state, 'SUPERSEDED'); + expect(history.length).toBe(2); + expect(history[0]?.value).toBe('Original name'); + expect(history[0]?.state).toBe('SUPERSEDED'); const current = yield* findPartyDetailAssertions(database, tenantId, partyId, false); - assert.deepEqual(current, { currentFactAssertions: [fact], factHistory: Option.none() }); - assert.deepEqual(values, [ + expect(current).toEqual({ currentFactAssertions: [fact], factHistory: Option.none() }); + expect(values).toEqual([ [tenantId, partyId], [tenantId, partyId, 'ACTIVE', true], ]); const [historyQuery = '', currentQuery = ''] = queries; - assert.match(historyQuery, /"tenant_id" = \$1/u); - assert.match(historyQuery, /"party_id" = \$2/u); - assert.doesNotMatch(historyQuery, /provenance|principal|invocation|verification/u); - assert.doesNotMatch(currentQuery, /external_evidence/u); - assert.match(currentQuery, /"state" = \$3/u); - assert.match(currentQuery, /"is_current" = \$4/u); + expect(historyQuery).toMatch(/"tenant_id" = \$1/u); + expect(historyQuery).toMatch(/"party_id" = \$2/u); + expect(historyQuery).not.toMatch(/provenance|principal|invocation|verification/u); + expect(currentQuery).not.toMatch(/external_evidence/u); + expect(currentQuery).toMatch(/"state" = \$3/u); + expect(currentQuery).toMatch(/"is_current" = \$4/u); const detail = yield* readPartyDetailFromServices( partyRef, tenantId, @@ -154,10 +160,9 @@ test('Party Detail persistence reads safe current and immutable historical asser }, true, ); - assert.equal(detail.currentFactAssertions[0]?.assertionId, assertionId); + expect(detail.currentFactAssertions[0]?.assertionId).toBe(assertionId); const detailHistory = Option.getOrThrow(detail.factHistory); - assert.equal(detailHistory[0]?.assertionId, previousId); - assert.equal(detailHistory[0]?.value, 'Original name'); + expect(detailHistory[0]?.assertionId).toBe(previousId); + expect(detailHistory[0]?.value).toBe('Original name'); }), - ); -}); +); diff --git a/app/verticals/party-registry/tests/unit/identity-persistence.service.test.ts b/app/verticals/party-registry/tests/unit/identity-persistence.service.test.ts index 4a50594b4..69760a87e 100644 --- a/app/verticals/party-registry/tests/unit/identity-persistence.service.test.ts +++ b/app/verticals/party-registry/tests/unit/identity-persistence.service.test.ts @@ -1,9 +1,10 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; +import { TestClock } from 'effect/testing'; +import { expect, it } from 'effect-rstest'; + import type { SQL } from 'drizzle-orm'; import { PgDialect } from 'drizzle-orm/pg-core'; -import { DateTime, Effect, Match, Option, Result, Schema } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { DateTime, Effect, Layer, Match, Option, Result, Schema, Predicate, Struct } from 'effect'; + import type { AresAppliedEvidence } from '../../shared/domain/ares-application.ts'; import { AresAppliedEvidenceSchema } from '../../shared/domain/ares-application.ts'; import { partySubjectKeyFromString } from '../../shared/domain/identity-contracts.ts'; @@ -143,25 +144,29 @@ const transactionHarness = ( /* eslint-enable anti-slop/no-unknown-parameters, anti-slop/no-unknown-returns, anti-slop/no-chained-type-assertions */ const assertNoIdentityWrites = (harness: ReturnType) => { - assert.deepEqual(harness.insertedValues, []); - assert.deepEqual(harness.updateSets, []); - assert.deepEqual(harness.deletedTargets, []); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets).toEqual([]); + expect(harness.deletedTargets).toEqual([]); }; const assertTenantLockIsFirst = (harness: ReturnType) => { // SAFETY: Every service under test first calls the tenant lock with one Drizzle SQL lock selection. const selection = harness.selectSelections[0] as { readonly lock: SQL }; const query = new PgDialect().sqlToQuery(selection.lock); - assert.match(query.sql, /pg_advisory_xact_lock/u); - assert.deepEqual(query.params, [tenantIdentityWriteLockKey(tenantId)]); + expect(query.sql).toMatch(/pg_advisory_xact_lock/u); + expect(query.params).toEqual([tenantIdentityWriteLockKey(tenantId)]); }; -test('ended Party facts are made non-current as part of the same transition', () => { - assert.deepEqual(endedPartyFactTransition, { isCurrent: false, state: 'ENDED' }); -}); +it.layer( + Layer.effectDiscard( + TestClock.setTime(DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-01T00:00:00.000Z'))), + ), +)('identity-persistence.service', (testIt) => { + it('ended Party facts are made non-current as part of the same transition', () => { + expect(endedPartyFactTransition).toEqual({ isCurrent: false, state: 'ENDED' }); + }); -test('unnamed Party insertion persists no fabricated display-name assertion', () => - runEffectTestPromise( + testIt.effect('unnamed Party insertion persists no fabricated display-name assertion', () => Effect.gen(function* verifyIdentityPersistence() { const candidateEvidence: AresAppliedEvidence = { ...appliedEvidence, @@ -206,84 +211,79 @@ test('unnamed Party insertion persists no fabricated display-name assertion', () principalId: '77777777-7777-4777-8777-777777777777', }, ); - assert.ok(Option.isNone(result.displayName)); + expect(Option.isNone(result.displayName)).toBe(true); assertTenantLockIsFirst(harness); // SAFETY: The first insert captured by insertPartyRecord targets the parties table. - assert.equal( - (harness.insertedValues[0] as typeof parties.$inferInsert).currentDisplayName, + expect((harness.insertedValues[0] as typeof parties.$inferInsert).currentDisplayName).toBe( null, ); // SAFETY: The second insert captured by insertPartyRecord targets the typed fact-assertion table. const assertions = harness .insertedValues[1] as readonly (typeof partyFactAssertions.$inferInsert)[]; - assert.deepEqual( - assertions.map((assertion) => assertion.factKind), - ['PARTY_TYPE'], - ); - assert.deepEqual(assertions[0]?.externalEvidence, encodedCandidateEvidence); + expect(assertions.map((assertion) => assertion.factKind)).toEqual(['PARTY_TYPE']); + expect(assertions[0]?.externalEvidence).toEqual(encodedCandidateEvidence); }), - )); - -test('identity updates close the preceding assertion before accepting its replacement', () => - runEffectTestPromise( - Effect.gen(function* verifyIdentityPersistence() { - const current = partyRow({ archivedAt: null }); - const harness = transactionHarness( - [[current], [], [{ partyId }]], - [[{ ...current, currentDisplayName: 'New name', revision: 5 }], []], - ); - const encodedAppliedEvidence = - yield* Schema.encodeEffect(AresAppliedEvidenceSchema)(appliedEvidence); + ); - const result = yield* updatePartyIdentityRecord(harness.transaction, tenantId, { - actionInvocationId: '66666666-6666-4666-8666-666666666666', - displayName: 'New name', - expectedRevision: 4, - externalEvidence: appliedEvidence, - partyId, - principalId: '77777777-7777-4777-8777-777777777777', - provenanceMethod: 'MANUAL', - provenanceSource: 'test', - validFrom: '2026-01-01T00:00:00.000Z', - }); - assert.equal(result._tag, 'found'); - assertTenantLockIsFirst(harness); - assert.deepEqual(harness.updateSets[1], { - isCurrent: false, - state: 'ENDED', - validTo: instantAsDate('2026-01-01T00:00:00.000Z'), - }); - assert.equal(harness.insertedValues.length, 1); - // SAFETY: The update service inserts only the replacement fact assertions captured here. - const assertions = harness - .insertedValues[0] as readonly (typeof partyFactAssertions.$inferInsert)[]; - assert.deepEqual(assertions[0]?.externalEvidence, encodedAppliedEvidence); - }), - )); + testIt.effect( + 'identity updates close the preceding assertion before accepting its replacement', + () => + Effect.gen(function* verifyIdentityPersistence() { + const current = partyRow({ archivedAt: null }); + const harness = transactionHarness( + [[current], [], [{ partyId }]], + [[{ ...current, currentDisplayName: 'New name', revision: 5 }], []], + ); + const encodedAppliedEvidence = + yield* Schema.encodeEffect(AresAppliedEvidenceSchema)(appliedEvidence); + + const result = yield* updatePartyIdentityRecord(harness.transaction, tenantId, { + actionInvocationId: '66666666-6666-4666-8666-666666666666', + displayName: 'New name', + expectedRevision: 4, + externalEvidence: appliedEvidence, + partyId, + principalId: '77777777-7777-4777-8777-777777777777', + provenanceMethod: 'MANUAL', + provenanceSource: 'test', + validFrom: '2026-01-01T00:00:00.000Z', + }); + expect(Predicate.isTagged(result, 'found')).toBe(true); + assertTenantLockIsFirst(harness); + expect(harness.updateSets[1]).toEqual({ + isCurrent: false, + state: 'ENDED', + validTo: instantAsDate('2026-01-01T00:00:00.000Z'), + }); + expect(harness.insertedValues.length).toBe(1); + // SAFETY: The update service inserts only the replacement fact assertions captured here. + const assertions = harness + .insertedValues[0] as readonly (typeof partyFactAssertions.$inferInsert)[]; + expect(assertions[0]?.externalEvidence).toEqual(encodedAppliedEvidence); + }), + ); -test('unarchive owner classification distinguishes conflict from ambiguity deterministically', () => { - assert.deepEqual(classifyUnarchiveClaimOwners(partyId, [{}, { partyId }]), { - _tag: 'available', - }); - assert.deepEqual(classifyUnarchiveClaimOwners(partyId, [{ partyId: firstOwnerId }]), { - _tag: 'identity_conflict', - conflictingPartyId: firstOwnerId, - }); - assert.deepEqual( - classifyUnarchiveClaimOwners(partyId, [ + it('unarchive owner classification distinguishes conflict from ambiguity deterministically', () => { + const availableOwners = classifyUnarchiveClaimOwners(partyId, [{}, { partyId }]); + expect(Predicate.isTagged(availableOwners, 'available')).toBe(true); + expect(Struct.omit(availableOwners, ['_tag'])).toEqual({}); + const conflictingOwner = classifyUnarchiveClaimOwners(partyId, [{ partyId: firstOwnerId }]); + expect(Predicate.isTagged(conflictingOwner, 'identity_conflict')).toBe(true); + expect(Struct.omit(conflictingOwner, ['_tag'])).toEqual({ + conflictingPartyId: firstOwnerId, + }); + const ambiguousOwners = classifyUnarchiveClaimOwners(partyId, [ { partyId: secondOwnerId }, { partyId: firstOwnerId }, { partyId: secondOwnerId }, - ]), - { - _tag: 'identity_ambiguous', + ]); + expect(Predicate.isTagged(ambiguousOwners, 'identity_ambiguous')).toBe(true); + expect(Struct.omit(ambiguousOwners, ['_tag'])).toEqual({ candidatePartyIds: [firstOwnerId, secondOwnerId], - }, - ); -}); + }); + }); -test('future-effective identity updates do not replace current facts early', () => - runEffectTestPromise( + testIt.effect('future-effective identity updates do not replace current facts early', () => Effect.gen(function* verifyIdentityPersistence() { const harness = transactionHarness([[partyRow({ archivedAt: null })], [], [{ partyId }]]); const result = yield* updatePartyIdentityRecord(harness.transaction, tenantId, { @@ -296,56 +296,17 @@ test('future-effective identity updates do not replace current facts early', () provenanceSource: 'test', validFrom: '2999-01-01T00:00:00.000Z', }); - assert.equal(result._tag, 'conflict'); - assert.deepEqual(harness.insertedValues, []); - assert.deepEqual(harness.updateSets, []); + expect(Predicate.isTagged(result, 'conflict')).toBe(true); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets).toEqual([]); }), - )); - -test('unarchive keeps the Party archived when an exact claim belongs to another Party', () => - runEffectTestPromise( - Effect.gen(function* verifyIdentityPersistence() { - const harness = transactionHarness([ - [partyRow()], - [], - [{ partyId }], - [], - [identifierRow()], - [{}], - [{ partyId: firstOwnerId }], - ]); - - const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); + ); - assert.deepEqual(result, { - _tag: 'identity_conflict', - conflictingPartyId: firstOwnerId, - }); - assertTenantLockIsFirst(harness); - assert.deepEqual(harness.insertedValues, []); - assert.deepEqual(harness.updateSets, []); - }), - )); - -test('blocked unarchive persists a case and decision without mutating Party, then reuses the case on a fresh attempt', () => - runEffectTestPromise( - Effect.gen(function* verifyDurableUnarchiveReview() { - const candidateCaseId = '66666666-6666-4666-8666-666666666666'; - const decisionId = '77777777-7777-4777-8777-777777777777'; - const caseRow = { - candidateCaseId, - candidateFingerprint: 'a'.repeat(64), - evaluatedEvidence: [ - { - outcome: 'AMBIGUOUS', - reason: 'Exact claim conflict', - ruleKey: 'party-unarchive-review.v1:EXACT_CLAIM_CONFLICT', - }, - ], - matchRuleVersion: 'party-exact-claims.v1', - }; - const harness = transactionHarness( - [ + testIt.effect( + 'unarchive keeps the Party archived when an exact claim belongs to another Party', + () => + Effect.gen(function* verifyIdentityPersistence() { + const harness = transactionHarness([ [partyRow()], [], [{ partyId }], @@ -353,125 +314,173 @@ test('blocked unarchive persists a case and decision without mutating Party, the [identifierRow()], [{}], [{ partyId: firstOwnerId }], - [partyRow()], - [identifierRow()], + ]); + + const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); + + expect(Predicate.isTagged(result, 'identity_conflict')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ + conflictingPartyId: firstOwnerId, + }); + assertTenantLockIsFirst(harness); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets).toEqual([]); + }), + ); + + testIt.effect( + 'blocked unarchive persists a case and decision without mutating Party, then reuses the case on a fresh attempt', + () => + Effect.gen(function* verifyDurableUnarchiveReview() { + const candidateCaseId = '66666666-6666-4666-8666-666666666666'; + const decisionId = '77777777-7777-4777-8777-777777777777'; + const caseRow = { + candidateCaseId, + candidateFingerprint: 'a'.repeat(64), + evaluatedEvidence: [ + { + outcome: 'AMBIGUOUS', + reason: 'Exact claim conflict', + ruleKey: 'party-unarchive-review.v1:EXACT_CLAIM_CONFLICT', + }, + ], + matchRuleVersion: 'party-exact-claims.v1', + }; + const harness = transactionHarness( + [ + [partyRow()], + [], + [{ partyId }], + [], + [identifierRow()], + [{}], + [{ partyId: firstOwnerId }], + [partyRow()], + [identifierRow()], + [], + [], + ], [], + [[caseRow], [], [{ matchDecisionId: decisionId }]], + ); + const result = yield* unarchivePartyWithReview( + harness.transaction, + tenantId, + partyId, + 4, + decisionId, + ); + expect(Predicate.isTagged(result, 'blocked')).toBe(true); + const blocked = Match.value(result).pipe( + Match.tag('blocked', ({ value }) => value), + Match.orElse(() => + (() => { + throw new Error('Expected unarchive to be blocked'); + })(), + ), + ); + assertTenantLockIsFirst(harness); + expect(harness.updateSets).toEqual([]); + expect(harness.deletedTargets).toEqual([]); + expect(harness.insertedValues.length).toBe(3); + // SAFETY: These are precisely the case, membership and decision inserts captured from the owner service. + const persistedCase = harness + .insertedValues[0] as typeof duplicateCandidateCases.$inferInsert; + // SAFETY: The second insert is the case's deterministic membership set. + const members = harness + .insertedValues[1] as readonly (typeof duplicateCandidateCaseParties.$inferInsert)[]; + // SAFETY: The third insert is the durable Action-linked match decision. + const decision = harness.insertedValues[2] as typeof partyMatchDecisions.$inferInsert; + expect(persistedCase.candidateSnapshot.intent).toBe('UNARCHIVE'); + expect(persistedCase.candidateSnapshot.names).toEqual(['Archived organization']); + expect(persistedCase.candidateSnapshot.officialIdentifiers?.[0]?.normalizedValue).toBe( + '27074358', + ); + expect(persistedCase.evaluationFingerprint).toMatch(/^[0-9a-f]{64}$/u); + expect(members.map((member) => member.partyId)).toEqual([partyId, firstOwnerId]); + expect(decision.actionInvocationId).toBe(decisionId); + expect(decision.candidateCaseId).toBe(candidateCaseId); + expect(decision.outcome).toBe('AMBIGUOUS'); + expect(blocked.reasonCode).toBe('EXACT_CLAIM_CONFLICT'); + expect(Option.isSome(blocked.party.archivedAt)).toBe(true); + expect(DateTime.formatIso(Option.getOrThrow(blocked.party.archivedAt))).toBe( + '2026-01-01T00:00:00.000Z', + ); + expect(blocked.party.revision).toBe(4); + + const secondDecisionId = '88888888-8888-4888-8888-888888888888'; + const retryHarness = transactionHarness( + [[partyRow()], [], [{ partyId }], [{ candidateCaseId }], [partyRow()], [caseRow]], [], - ], - [], - [[caseRow], [], [{ matchDecisionId: decisionId }]], - ); - const result = yield* unarchivePartyWithReview( - harness.transaction, - tenantId, - partyId, - 4, - decisionId, - ); - assert.equal(result._tag, 'blocked'); - const blocked = Match.value(result).pipe( - Match.tag('blocked', ({ value }) => value), - Match.orElse(() => assert.fail('Expected unarchive to be blocked')), - ); - assertTenantLockIsFirst(harness); - assert.deepEqual(harness.updateSets, []); - assert.deepEqual(harness.deletedTargets, []); - assert.equal(harness.insertedValues.length, 3); - // SAFETY: These are precisely the case, membership and decision inserts captured from the owner service. - const persistedCase = harness - .insertedValues[0] as typeof duplicateCandidateCases.$inferInsert; - // SAFETY: The second insert is the case's deterministic membership set. - const members = harness - .insertedValues[1] as readonly (typeof duplicateCandidateCaseParties.$inferInsert)[]; - // SAFETY: The third insert is the durable Action-linked match decision. - const decision = harness.insertedValues[2] as typeof partyMatchDecisions.$inferInsert; - assert.equal(persistedCase.candidateSnapshot.intent, 'UNARCHIVE'); - assert.deepEqual(persistedCase.candidateSnapshot.names, ['Archived organization']); - assert.equal( - persistedCase.candidateSnapshot.officialIdentifiers?.[0]?.normalizedValue, - '27074358', - ); - assert.match(persistedCase.evaluationFingerprint, /^[0-9a-f]{64}$/u); - assert.deepEqual( - members.map((member) => member.partyId), - [partyId, firstOwnerId], - ); - assert.equal(decision.actionInvocationId, decisionId); - assert.equal(decision.candidateCaseId, candidateCaseId); - assert.equal(decision.outcome, 'AMBIGUOUS'); - assert.equal(blocked.reasonCode, 'EXACT_CLAIM_CONFLICT'); - assert.ok(Option.isSome(blocked.party.archivedAt)); - assert.equal( - DateTime.formatIso(Option.getOrThrow(blocked.party.archivedAt)), - '2026-01-01T00:00:00.000Z', - ); - assert.equal(blocked.party.revision, 4); + [[{ matchDecisionId: secondDecisionId }]], + ); + const retry = yield* unarchivePartyWithReview( + retryHarness.transaction, + tenantId, + partyId, + 4, + secondDecisionId, + ); + expect(Predicate.isTagged(retry, 'blocked')).toBe(true); + const retryBlocked = Match.value(retry).pipe( + Match.tag('blocked', ({ value }) => value), + Match.orElse(() => + (() => { + throw new Error('Expected retry to be blocked'); + })(), + ), + ); + expect(retryHarness.insertedValues.length).toBe(1); + expect(retryHarness.updateSets).toEqual([]); + expect(retryBlocked.caseRef).toEqual(blocked.caseRef); + expect(retryBlocked.decisionRef).not.toEqual(blocked.decisionRef); + expect(retryBlocked.party).toEqual(blocked.party); + }), + ); - const secondDecisionId = '88888888-8888-4888-8888-888888888888'; - const retryHarness = transactionHarness( - [[partyRow()], [], [{ partyId }], [{ candidateCaseId }], [partyRow()], [caseRow]], - [], - [[{ matchDecisionId: secondDecisionId }]], - ); - const retry = yield* unarchivePartyWithReview( - retryHarness.transaction, - tenantId, - partyId, - 4, - secondDecisionId, - ); - assert.equal(retry._tag, 'blocked'); - const retryBlocked = Match.value(retry).pipe( - Match.tag('blocked', ({ value }) => value), - Match.orElse(() => assert.fail('Expected retry to be blocked')), - ); - assert.equal(retryHarness.insertedValues.length, 1); - assert.deepEqual(retryHarness.updateSets, []); - assert.deepEqual(retryBlocked.caseRef, blocked.caseRef); - assert.notDeepEqual(retryBlocked.decisionRef, blocked.decisionRef); - assert.deepEqual(retryBlocked.party, blocked.party); - }), - )); - -test('unresolved unnamed unarchive review persists no invented display-name evidence', () => - runEffectTestPromise( - Effect.gen(function* verifyUnresolvedUnarchiveReview() { - const current = partyRow({ currentDisplayName: null, currentType: 'UNRESOLVED' }); - const caseRow = { - candidateCaseId: firstOwnerId, - candidateFingerprint: 'b'.repeat(64), - evaluatedEvidence: [], - matchRuleVersion: 'party-exact-claims.v1', - }; - const harness = transactionHarness( - [[current], [], [{ partyId }], [], [], [current], [], [], []], - [], - [[caseRow], [], [{ matchDecisionId: secondOwnerId }]], - ); - const result = yield* unarchivePartyWithReview( - harness.transaction, - tenantId, - partyId, - 4, - secondOwnerId, - ); - assert.equal(result._tag, 'blocked'); - const blocked = Match.value(result).pipe( - Match.tag('blocked', ({ value }) => value), - Match.orElse(() => assert.fail('Expected unarchive to be blocked')), - ); - // SAFETY: The first captured insert is the immutable candidate case. - const persistedCase = harness - .insertedValues[0] as typeof duplicateCandidateCases.$inferInsert; - assert.deepEqual(persistedCase.candidateSnapshot.names, []); - assert.deepEqual(persistedCase.candidateSnapshot.officialIdentifiers, []); - assert.deepEqual(harness.updateSets, []); - assert.equal(blocked.reasonCode, 'UNRESOLVED_IDENTITY'); - }), - )); + testIt.effect( + 'unresolved unnamed unarchive review persists no invented display-name evidence', + () => + Effect.gen(function* verifyUnresolvedUnarchiveReview() { + const current = partyRow({ currentDisplayName: null, currentType: 'UNRESOLVED' }); + const caseRow = { + candidateCaseId: firstOwnerId, + candidateFingerprint: 'b'.repeat(64), + evaluatedEvidence: [], + matchRuleVersion: 'party-exact-claims.v1', + }; + const harness = transactionHarness( + [[current], [], [{ partyId }], [], [], [current], [], [], []], + [], + [[caseRow], [], [{ matchDecisionId: secondOwnerId }]], + ); + const result = yield* unarchivePartyWithReview( + harness.transaction, + tenantId, + partyId, + 4, + secondOwnerId, + ); + expect(Predicate.isTagged(result, 'blocked')).toBe(true); + const blocked = Match.value(result).pipe( + Match.tag('blocked', ({ value }) => value), + Match.orElse(() => + (() => { + throw new Error('Expected unarchive to be blocked'); + })(), + ), + ); + // SAFETY: The first captured insert is the immutable candidate case. + const persistedCase = harness + .insertedValues[0] as typeof duplicateCandidateCases.$inferInsert; + expect(persistedCase.candidateSnapshot.names).toEqual([]); + expect(persistedCase.candidateSnapshot.officialIdentifiers).toEqual([]); + expect(harness.updateSets).toEqual([]); + expect(blocked.reasonCode).toBe('UNRESOLVED_IDENTITY'); + }), + ); -test('archive acquires the tenant identity lock before any Party row lock', () => - runEffectTestPromise( + testIt.effect('archive acquires the tenant identity lock before any Party row lock', () => Effect.gen(function* verifyIdentityPersistence() { const harness = transactionHarness([[]]); const result = yield* transitionPartyRecord( @@ -481,50 +490,49 @@ test('archive acquires the tenant identity lock before any Party row lock', () = 4, 'ARCHIVED', ); - assert.equal(result._tag, 'not_found'); + expect(Predicate.isTagged(result, 'not_found')).toBe(true); assertTenantLockIsFirst(harness); }), - )); - -test('unarchive restores an unclaimed eligible identifier before activating the Party', () => - runEffectTestPromise( - Effect.gen(function* verifyIdentityPersistence() { - const activeParty = partyRow({ archivedAt: null, revision: 5 }); - const harness = transactionHarness( - [[partyRow()], [], [{ partyId }], [], [identifierRow()], [{}], []], - [[activeParty]], - ); - - const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); + ); - assert.equal(result._tag, 'found'); - assert.deepEqual(harness.insertedValues, [ - [ - { - identifierTypeKey: 'ICO', - namespace: 'CZ:ICO', - normalizedValue: '27074358', - officialIdentifierId, - partyId, - tenantId, - }, - ], - ]); - assert.equal(harness.updateSets.length, 1); - // SAFETY: Unarchive's only update targets the parties table; the harness captures its exact set value. - assert.deepEqual( - Object.fromEntries( - Object.entries(harness.updateSets[0] as Partial).filter( - ([key]) => key !== 'updatedAt', + testIt.effect( + 'unarchive restores an unclaimed eligible identifier before activating the Party', + () => + Effect.gen(function* verifyIdentityPersistence() { + const activeParty = partyRow({ archivedAt: null, revision: 5 }); + const harness = transactionHarness( + [[partyRow()], [], [{ partyId }], [], [identifierRow()], [{}], []], + [[activeParty]], + ); + + const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); + + expect(Predicate.isTagged(result, 'found')).toBe(true); + expect(harness.insertedValues).toEqual([ + [ + { + identifierTypeKey: 'ICO', + namespace: 'CZ:ICO', + normalizedValue: '27074358', + officialIdentifierId, + partyId, + tenantId, + }, + ], + ]); + expect(harness.updateSets.length).toBe(1); + // SAFETY: Unarchive's only update targets the parties table; the harness captures its exact set value. + expect( + Object.fromEntries( + Object.entries(harness.updateSets[0] as Partial).filter( + ([key]) => key !== 'updatedAt', + ), ), - ), - { archivedAt: null, revision: 5 }, - ); - }), - )); + ).toEqual({ archivedAt: null, revision: 5 }); + }), + ); -test('unarchive rejects an alias rather than forwarding the write to its survivor', () => - runEffectTestPromise( + testIt.effect('unarchive rejects an alias rather than forwarding the write to its survivor', () => Effect.gen(function* verifyIdentityPersistence() { const harness = transactionHarness([ [partyRow()], @@ -536,14 +544,13 @@ test('unarchive rejects an alias rather than forwarding the write to its survivo const error = yield* Effect.flip( unarchivePartyRecord(harness.transaction, tenantId, partyId, 4), ); - assert.equal(error._tag, 'PartyAliasWriteRejected'); - assert.deepEqual(harness.insertedValues, []); - assert.deepEqual(harness.updateSets, []); + expect(Predicate.isTagged(error, 'PartyAliasWriteRejected')).toBe(true); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets).toEqual([]); }), - )); + ); -test('unarchive reports ambiguous exact claims without changing archived state', () => - runEffectTestPromise( + testIt.effect('unarchive reports ambiguous exact claims without changing archived state', () => Effect.gen(function* verifyIdentityPersistence() { const harness = transactionHarness([ [partyRow()], @@ -565,17 +572,16 @@ test('unarchive reports ambiguous exact claims without changing archived state', ]); const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); - assert.deepEqual(result, { - _tag: 'identity_ambiguous', + expect(Predicate.isTagged(result, 'identity_ambiguous')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ candidatePartyIds: [firstOwnerId, secondOwnerId], }); - assert.deepEqual(harness.insertedValues, []); - assert.deepEqual(harness.updateSets, []); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets).toEqual([]); }), - )); + ); -test('unarchive does not promote a PERSON ICO into an exclusive strong claim', () => - runEffectTestPromise( + testIt.effect('unarchive does not promote a PERSON ICO into an exclusive strong claim', () => Effect.gen(function* verifyIdentityPersistence() { const currentParty = partyRow({ currentType: 'PERSON' }); const harness = transactionHarness( @@ -584,79 +590,81 @@ test('unarchive does not promote a PERSON ICO into an exclusive strong claim', ( ); const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); - assert.equal(result._tag, 'found'); - assert.deepEqual(harness.insertedValues, []); - assert.equal(harness.updateSets.length, 1); - }), - )); - -test('unarchive requires review while a duplicate case involving the Party remains open', () => - runEffectTestPromise( - Effect.gen(function* verifyIdentityPersistence() { - const caseId = '88888888-8888-4888-8888-888888888888'; - const harness = transactionHarness([ - [partyRow()], - [], - [{ partyId }], - [{ candidateCaseId: caseId }], - ]); - const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); - assert.deepEqual(result, { - _tag: 'review_required', - caseIds: [caseId], - reasonCode: 'OPEN_DUPLICATE_CASE', - }); - assert.deepEqual(harness.insertedValues, []); - assert.deepEqual(harness.updateSets, []); - }), - )); - -test('unarchive requires review for unresolved identity without any eligible strong claim', () => - runEffectTestPromise( - Effect.gen(function* verifyIdentityPersistence() { - const harness = transactionHarness([ - [partyRow({ currentType: 'UNRESOLVED' })], - [], - [{ partyId }], - [], - [], - ]); - const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); - assert.deepEqual(result, { - _tag: 'review_required', - caseIds: [], - reasonCode: 'UNRESOLVED_IDENTITY', - }); - assert.deepEqual(harness.insertedValues, []); - assert.deepEqual(harness.updateSets, []); + expect(Predicate.isTagged(result, 'found')).toBe(true); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets.length).toBe(1); }), - )); + ); -test('reviewed UNRESOLVED Party can unarchive using retained accepted creation evidence', () => - runEffectTestPromise( - Effect.gen(function* restoreReviewedUnresolved() { - const current = partyRow({ currentType: 'UNRESOLVED' }); - const harness = transactionHarness( - [ - [current], + testIt.effect( + 'unarchive requires review while a duplicate case involving the Party remains open', + () => + Effect.gen(function* verifyIdentityPersistence() { + const caseId = '88888888-8888-4888-8888-888888888888'; + const harness = transactionHarness([ + [partyRow()], [], [{ partyId }], + [{ candidateCaseId: caseId }], + ]); + const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); + expect(Predicate.isTagged(result, 'review_required')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ + caseIds: [caseId], + reasonCode: 'OPEN_DUPLICATE_CASE', + }); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets).toEqual([]); + }), + ); + + testIt.effect( + 'unarchive requires review for unresolved identity without any eligible strong claim', + () => + Effect.gen(function* verifyIdentityPersistence() { + const harness = transactionHarness([ + [partyRow({ currentType: 'UNRESOLVED' })], [], - [{ candidateCaseId: '88888888-8888-4888-8888-888888888888' }], + [{ partyId }], [], [], - ], - [[{ ...current, archivedAt: null, revision: 5 }]], - ); - const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); - assert.equal(result._tag, 'found'); - assert.equal(harness.updateSets.length, 1); - assert.deepEqual(harness.insertedValues, []); - }), - )); + ]); + const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); + expect(Predicate.isTagged(result, 'review_required')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ + caseIds: [], + reasonCode: 'UNRESOLVED_IDENTITY', + }); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets).toEqual([]); + }), + ); -test('Party type enrichment refuses another owner of a newly eligible identifier', () => - runEffectTestPromise( + testIt.effect( + 'reviewed UNRESOLVED Party can unarchive using retained accepted creation evidence', + () => + Effect.gen(function* restoreReviewedUnresolved() { + const current = partyRow({ currentType: 'UNRESOLVED' }); + const harness = transactionHarness( + [ + [current], + [], + [{ partyId }], + [], + [{ candidateCaseId: '88888888-8888-4888-8888-888888888888' }], + [], + [], + ], + [[{ ...current, archivedAt: null, revision: 5 }]], + ); + const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); + expect(Predicate.isTagged(result, 'found')).toBe(true); + expect(harness.updateSets.length).toBe(1); + expect(harness.insertedValues).toEqual([]); + }), + ); + + testIt.effect('Party type enrichment refuses another owner of a newly eligible identifier', () => Effect.gen(function* preventEnrichmentCollision() { const current = partyRow({ archivedAt: null, currentType: 'UNRESOLVED' }); const harness = transactionHarness([ @@ -688,13 +696,12 @@ test('Party type enrichment refuses another owner of a newly eligible identifier ], validFrom: '2026-01-01T00:00:00.000Z', }); - assert.equal(result._tag, 'conflict'); + expect(Predicate.isTagged(result, 'conflict')).toBe(true); assertNoIdentityWrites(harness); }), - )); + ); -test('Party type enrichment atomically claims identifiers that newly qualify', () => - runEffectTestPromise( + testIt.effect('Party type enrichment atomically claims identifiers that newly qualify', () => Effect.gen(function* claimEnrichedIdentifier() { const current = partyRow({ archivedAt: null, currentType: 'UNRESOLVED' }); const harness = transactionHarness( @@ -721,8 +728,8 @@ test('Party type enrichment atomically claims identifiers that newly qualify', ( ], validFrom: '2026-01-01T00:00:00.000Z', }); - assert.equal(result._tag, 'found'); - assert.deepEqual(harness.insertedValues[0], [ + expect(Predicate.isTagged(result, 'found')).toBe(true); + expect(harness.insertedValues[0]).toEqual([ { identifierTypeKey: 'ICO', namespace: 'CZ:ICO', @@ -732,66 +739,68 @@ test('Party type enrichment atomically claims identifiers that newly qualify', ( tenantId, }, ]); - assert.equal(harness.updateSets.length, 2); + expect(harness.updateSets.length).toBe(2); }), - )); + ); -test('type correction reconciliation releases an ICO claim no longer eligible for a PERSON', () => - runEffectTestPromise( - Effect.gen(function* releaseIneligibleClaim() { - const harness = transactionHarness([ - [identifierRow()], - [ - { - identifierClaimId: '99999999-9999-4999-8999-999999999999', - identifierTypeKey: 'ICO', - namespace: 'CZ:ICO', - normalizedValue: '27074358', - officialIdentifierId, - partyId, - tenantId, - }, - ], - ]); - const result = yield* reconcilePartyIdentifierClaims( - harness.transaction, - tenantId, - partyId, - 'PERSON', - ); - assert.deepEqual(result, { _tag: 'available', eligibleClaimCount: 0 }); - assert.equal(harness.deletedTargets.length, 1); - assert.deepEqual(harness.insertedValues, []); - assertTenantLockIsFirst(harness); - }), - )); + testIt.effect( + 'type correction reconciliation releases an ICO claim no longer eligible for a PERSON', + () => + Effect.gen(function* releaseIneligibleClaim() { + const harness = transactionHarness([ + [identifierRow()], + [ + { + identifierClaimId: '99999999-9999-4999-8999-999999999999', + identifierTypeKey: 'ICO', + namespace: 'CZ:ICO', + normalizedValue: '27074358', + officialIdentifierId, + partyId, + tenantId, + }, + ], + ]); + const result = yield* reconcilePartyIdentifierClaims( + harness.transaction, + tenantId, + partyId, + 'PERSON', + ); + expect(Predicate.isTagged(result, 'available')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ eligibleClaimCount: 0 }); + expect(harness.deletedTargets.length).toBe(1); + expect(harness.insertedValues).toEqual([]); + assertTenantLockIsFirst(harness); + }), + ); -test('identity updates reject a historical end earlier than the assertion being replaced', () => - runEffectTestPromise( - Effect.gen(function* rejectInvalidHistoricalInterval() { - const harness = transactionHarness([ - [partyRow({ archivedAt: null })], - [], - [{ partyId }], - [{ validFrom: instantAsDate('2026-05-01T00:00:00.000Z') }], - ]); - const result = yield* updatePartyIdentityRecord(harness.transaction, tenantId, { - actionInvocationId: '66666666-6666-4666-8666-666666666666', - displayName: 'Historical name', - expectedRevision: 4, - partyId, - principalId: '77777777-7777-4777-8777-777777777777', - provenanceMethod: 'MANUAL', - provenanceSource: 'test', - validFrom: '2026-01-01T00:00:00.000Z', - }); - assert.equal(result._tag, 'conflict'); - assertNoIdentityWrites(harness); - }), - )); + testIt.effect( + 'identity updates reject a historical end earlier than the assertion being replaced', + () => + Effect.gen(function* rejectInvalidHistoricalInterval() { + const harness = transactionHarness([ + [partyRow({ archivedAt: null })], + [], + [{ partyId }], + [{ validFrom: instantAsDate('2026-05-01T00:00:00.000Z') }], + ]); + const result = yield* updatePartyIdentityRecord(harness.transaction, tenantId, { + actionInvocationId: '66666666-6666-4666-8666-666666666666', + displayName: 'Historical name', + expectedRevision: 4, + partyId, + principalId: '77777777-7777-4777-8777-777777777777', + provenanceMethod: 'MANUAL', + provenanceSource: 'test', + validFrom: '2026-01-01T00:00:00.000Z', + }); + expect(Predicate.isTagged(result, 'conflict')).toBe(true); + assertNoIdentityWrites(harness); + }), + ); -test('type enrichment rejects unevidenced type before accepting facts or claims', () => - runEffectTestPromise( + testIt.effect('type enrichment rejects unevidenced type before accepting facts or claims', () => Effect.gen(function* rejectUnsupportedType() { const current = partyRow({ archivedAt: null, currentType: 'UNRESOLVED' }); const harness = transactionHarness([[current], [], [{ partyId }]]); @@ -807,8 +816,9 @@ test('type enrichment rejects unevidenced type before accepting facts or claims' validFrom: '2026-01-01T00:00:00.000Z', }), ); - assert.equal(error._tag, 'PartyEvidenceInsufficient'); - assert.deepEqual(harness.insertedValues, []); - assert.deepEqual(harness.updateSets, []); + expect(Predicate.isTagged(error, 'PartyEvidenceInsufficient')).toBe(true); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets).toEqual([]); }), - )); + ); +}); diff --git a/app/verticals/party-registry/tests/unit/matching-contract.test.ts b/app/verticals/party-registry/tests/unit/matching-contract.test.ts index 170f89331..9493e2ac6 100644 --- a/app/verticals/party-registry/tests/unit/matching-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/matching-contract.test.ts @@ -1,6 +1,5 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; + import { Effect } from 'effect'; import { createActionCollector } from '../../../../packages/core-runtime/src/actions/collector.ts'; import { getActionHandler } from '../../../../packages/core-runtime/src/actions/definition.ts'; @@ -27,8 +26,9 @@ const evidenceScope = { tenantId: evidenceTenantId, }; -test('reviewed Create records metadata-only invariant evidence and commits its created event', () => - runEffectTestPromise( +it.effect( + 'reviewed Create records metadata-only invariant evidence and commits its created event', + () => Effect.gen(function* reviewedCreateEvidence() { const collector = createActionCollector( resolveDuplicateCandidateCreateAction.descriptor.domainEvents, @@ -50,14 +50,15 @@ test('reviewed Create records metadata-only invariant evidence and commits its c }), }, }); - assert.equal(collector.snapshot().dataAccessEvents[0]?.evidenceCaptureMode, 'metadata_only'); - assert.equal(collector.snapshot().domainEvents.length, 1); - assert.equal(collector.snapshot().outboxMessages.length, 1); + expect(collector.snapshot().dataAccessEvents[0]?.evidenceCaptureMode).toBe('metadata_only'); + expect(collector.snapshot().domainEvents.length).toBe(1); + expect(collector.snapshot().outboxMessages.length).toBe(1); }), - )); +); -test('reviewed duplicate confirmation records safe invariant evidence without executing merge', () => - runEffectTestPromise( +it.effect( + 'reviewed duplicate confirmation records safe invariant evidence without executing merge', + () => Effect.gen(function* confirmationEvidence() { const collector = createActionCollector( confirmDuplicatePartiesAction.descriptor.domainEvents, @@ -79,20 +80,19 @@ test('reviewed duplicate confirmation records safe invariant evidence without ex }), }, }); - assert.equal(collector.snapshot().dataAccessEvents[0]?.evidenceCaptureMode, 'metadata_only'); - assert.deepEqual(collector.snapshot().domainEvents, []); - assert.deepEqual(collector.snapshot().outboxMessages, []); + expect(collector.snapshot().dataAccessEvents[0]?.evidenceCaptureMode).toBe('metadata_only'); + expect(collector.snapshot().domainEvents).toEqual([]); + expect(collector.snapshot().outboxMessages).toEqual([]); }), - )); +); -test('claim locks are acquired in one deterministic order', () => { - assert.deepEqual( +it('claim locks are acquired in one deterministic order', () => { + expect( sortClaimKeys(['CZ_DIC\u0000cz:dic\u0000CZ27074358', 'ICO\u0000cz:ico\u000027074358']), - ['CZ_DIC\u0000cz:dic\u0000CZ27074358', 'ICO\u0000cz:ico\u000027074358'], - ); + ).toEqual(['CZ_DIC\u0000cz:dic\u0000CZ27074358', 'ICO\u0000cz:ico\u000027074358']); }); -test('all Duplicate Candidate resolutions are reviewed, idempotent tenant Actions', () => { +it('all Duplicate Candidate resolutions are reviewed, idempotent tenant Actions', () => { for (const action of [ resolveDuplicateCandidateMatchAction, resolveDuplicateCandidateCreateAction, @@ -100,15 +100,15 @@ test('all Duplicate Candidate resolutions are reviewed, idempotent tenant Action dismissDuplicateCandidateAction, confirmDuplicatePartiesAction, ]) { - assert.equal(action.descriptor.legalEntityScope, 'optional'); - assert.equal(action.descriptor.idempotency, 'required'); + expect(action.descriptor.legalEntityScope).toBe('optional'); + expect(action.descriptor.idempotency).toBe('required'); // SAFETY: these descriptors use a constant permission resolver and never inspect the payload. - assert.equal(action.descriptor.tenantPermission?.({} as never), 'review_party_identity'); + expect(action.descriptor.tenantPermission?.({} as never)).toBe('review_party_identity'); } }); -test('claim lock identity is tenant-qualified, normalized, sorted, and deduplicated', () => { - assert.deepEqual( +it('claim lock identity is tenant-qualified, normalized, sorted, and deduplicated', () => { + expect( tenantClaimLockKeys('tenant-b', [ { identifierType: 'ICO', @@ -129,9 +129,11 @@ test('claim lock identity is tenant-qualified, normalized, sorted, and deduplica verification: 'VERIFIED', }, ]), - ['["tenant-b","CZ_DIC","CZ:DIC","CZ27074358"]', '["tenant-b","ICO","CZ:ICO","27074358"]'], - ); - assert.notDeepEqual( + ).toEqual([ + '["tenant-b","CZ_DIC","CZ:DIC","CZ27074358"]', + '["tenant-b","ICO","CZ:ICO","27074358"]', + ]); + expect( tenantClaimLockKeys('tenant-a', [ { identifierType: 'ICO', @@ -140,6 +142,7 @@ test('claim lock identity is tenant-qualified, normalized, sorted, and deduplica verification: 'VERIFIED', }, ]), + ).not.toEqual( tenantClaimLockKeys('tenant-b', [ { identifierType: 'ICO', @@ -149,7 +152,7 @@ test('claim lock identity is tenant-qualified, normalized, sorted, and deduplica }, ]), ); - assert.equal( + expect( tenantClaimLockKeys('tenant-b', [ { identifierType: 'ICO', @@ -158,24 +161,22 @@ test('claim lock identity is tenant-qualified, normalized, sorted, and deduplica verification: 'VERIFIED', }, ]).some((key) => key.includes('\u0000')), - false, - ); + ).toBe(false); }); -test('authoritative exact claims cannot be outvoted by weak evidence', () => { - assert.deepEqual(evaluateExactClaims([]), { outcome: 'NO_MATCH', partyIds: [] }); - assert.deepEqual(evaluateExactClaims(['aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa']), { +it('authoritative exact claims cannot be outvoted by weak evidence', () => { + expect(evaluateExactClaims([])).toEqual({ outcome: 'NO_MATCH', partyIds: [] }); + expect(evaluateExactClaims(['aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'])).toEqual({ outcome: 'MATCHED', partyIds: ['aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'], }); - assert.deepEqual( + expect( evaluateExactClaims([ 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb', 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', ]), - { - outcome: 'AMBIGUOUS', - partyIds: ['aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb'], - }, - ); + ).toEqual({ + outcome: 'AMBIGUOUS', + partyIds: ['aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb'], + }); }); diff --git a/app/verticals/party-registry/tests/unit/matching-persistence.test.ts b/app/verticals/party-registry/tests/unit/matching-persistence.test.ts index 47047bc80..edbdffead 100644 --- a/app/verticals/party-registry/tests/unit/matching-persistence.test.ts +++ b/app/verticals/party-registry/tests/unit/matching-persistence.test.ts @@ -1,9 +1,10 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; +import { TestClock } from 'effect/testing'; +import { expect, it } from 'effect-rstest'; + /* eslint-disable anti-slop/no-chained-type-assertions, anti-slop/no-unsafe-dictionary-type -- This harness implements the narrow Drizzle Effect boundary exercised by the owner-local matching service. expires: 2026-12-31. */ import type { SQL } from 'drizzle-orm'; -import { DateTime, Effect, Schema } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { DateTime, Effect, Layer, Schema, Predicate } from 'effect'; + import { createActionCollector } from '../../../../packages/core-runtime/src/actions/collector.ts'; import { getActionHandler } from '../../../../packages/core-runtime/src/actions/definition.ts'; import type { PartyCandidate } from '../../shared/domain/identity-contracts.ts'; @@ -99,12 +100,12 @@ type HarnessTable = | typeof partyOfficialIdentifiers; type RecordedValues = Row | readonly Row[] | undefined; const recordedRow = (values: RecordedValues): Row => { - assert.ok(values !== undefined && !Array.isArray(values)); + expect(values !== undefined && !Array.isArray(values)).toBe(true); // SAFETY: the recorded insert was checked to be a present single row rather than a batch. return values as Row; }; const recordedRows = (values: RecordedValues): readonly Row[] => { - assert.ok(Array.isArray(values)); + expect(Array.isArray(values)).toBe(true); // SAFETY: the recorded insert was checked to be the batch of rows used by this harness. return values as readonly Row[]; }; @@ -196,753 +197,739 @@ const ambiguousHarness = (existingCase: boolean) => ]), ); -test('durable Party Match commits an ambiguity decision, complete case references, and original evidence without mutating a Party', () => - runEffectTestPromise( - Effect.gen(function* durablePartyMatchCommitsAnAmbiguityDecision() { - const subject = ambiguousHarness(false); - const result = yield* matchParty(subject.transaction, { - actionInvocationId, - candidate: candidate(), - tenantId, - }); - assert.equal(result.outcome, 'AMBIGUOUS'); - assert.deepEqual( - result.candidateParties.map((ref) => ref.resourceId), - [partyA, partyB], - ); - assert.equal(result.decisionRef.resourceId, decisionId); - assert.deepEqual( - subject.inserts.map(({ table }) => table), - [duplicateCandidateCases, duplicateCandidateCaseParties, partyMatchDecisions], - ); - const caseValues = recordedRow(subject.inserts[0]?.values); - // SAFETY: this fixture records the concrete candidateSnapshot inserted by the matching service. - const snapshot = caseValues['candidateSnapshot'] as Row; - assert.deepEqual(snapshot['names'], []); - assert.deepEqual(snapshot['provenance'], candidate().provenance); - assert.equal(snapshot['validFrom'], instant); - const linked = recordedRows(subject.inserts[1]?.values); - assert.deepEqual( - linked.map((row) => [row['partyId'], row['rank']]), - [ +it.layer( + Layer.effectDiscard( + TestClock.setTime(DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-01T00:00:00.000Z'))), + ), +)('matching-persistence', (testIt) => { + testIt.effect( + 'durable Party Match commits an ambiguity decision, complete case references, and original evidence without mutating a Party', + () => + Effect.gen(function* durablePartyMatchCommitsAnAmbiguityDecision() { + const subject = ambiguousHarness(false); + const result = yield* matchParty(subject.transaction, { + actionInvocationId, + candidate: candidate(), + tenantId, + }); + expect(result.outcome).toBe('AMBIGUOUS'); + expect(result.candidateParties.map((ref) => ref.resourceId)).toEqual([partyA, partyB]); + expect(result.decisionRef.resourceId).toBe(decisionId); + expect(subject.inserts.map(({ table }) => table)).toEqual([ + duplicateCandidateCases, + duplicateCandidateCaseParties, + partyMatchDecisions, + ]); + const caseValues = recordedRow(subject.inserts[0]?.values); + // SAFETY: this fixture records the concrete candidateSnapshot inserted by the matching service. + const snapshot = caseValues['candidateSnapshot'] as Row; + expect(snapshot['names']).toEqual([]); + expect(snapshot['provenance']).toEqual(candidate().provenance); + expect(snapshot['validFrom']).toBe(instant); + const linked = recordedRows(subject.inserts[1]?.values); + expect(linked.map((row) => [row['partyId'], row['rank']])).toEqual([ [partyA, 1], [partyB, 2], - ], - ); - assert.equal(recordedRow(subject.inserts[2]?.values)['candidateCaseId'], candidateCaseId); - assert.equal(subject.updates.length, 0); - assert.notEqual( - subject.reads[0]?.table, - partyIdentifierClaims, - 'tenant serialization lock precedes row/claim reads', - ); - }), - )); + ]); + expect(recordedRow(subject.inserts[2]?.values)['candidateCaseId']).toBe(candidateCaseId); + expect(subject.updates.length).toBe(0); + expect( + subject.reads[0]?.table, + 'tenant serialization lock precedes row/claim reads', + ).not.toBe(partyIdentifierClaims); + }), + ); -test('an unchanged open ambiguity reuses its immutable evaluated case without rewriting candidate links', () => - runEffectTestPromise( - Effect.gen(function* anUnchangedOpenAmbiguityReusesItsImmutable() { - const subject = ambiguousHarness(true); - yield* matchParty(subject.transaction, { - actionInvocationId, - candidate: candidate(), - tenantId, - }); - assert.deepEqual( - subject.inserts.map(({ table }) => table), - [partyMatchDecisions], - ); - assert.equal(subject.updates.length, 0, 'the original case snapshot is never overwritten'); - }), - )); + testIt.effect( + 'an unchanged open ambiguity reuses its immutable evaluated case without rewriting candidate links', + () => + Effect.gen(function* anUnchangedOpenAmbiguityReusesItsImmutable() { + const subject = ambiguousHarness(true); + yield* matchParty(subject.transaction, { + actionInvocationId, + candidate: candidate(), + tenantId, + }); + expect(subject.inserts.map(({ table }) => table)).toEqual([partyMatchDecisions]); + expect(subject.updates.length, 'the original case snapshot is never overwritten').toBe(0); + }), + ); -test('PERSON IČO cannot acquire organization auto-match authority and NO_MATCH still records a decision', () => - runEffectTestPromise( - Effect.gen(function* personIOCannotAcquireOrganizationAuto() { - const subject = harness(); - const result = yield* matchParty(subject.transaction, { - actionInvocationId, - candidate: candidate({ - officialIdentifiers: [ - { identifierType: 'ICO', value: '27074358', verification: 'VERIFIED' }, - ], - partyType: 'PERSON', - subjectEvidence: [ - { - basis: 'DIRECT_INTERACTION', - evidenceRef: 'meeting/42', - kind: 'ACTOR_ATTESTATION', - observedSubject: 'PERSON', - statement: 'Met this human', - subjectKey: partySubjectKeyFromString('one-subject'), - }, - ], - }), - tenantId, - }); - assert.equal(result.outcome, 'NO_MATCH'); - const durable = recordedRow( - subject.inserts.find(({ table }) => table === partyMatchDecisions)?.values, - ); - assert.equal(durable['operation'], 'MATCH'); - assert.equal(durable['committedCreateOutcome'], null); - assert.deepEqual(result.candidateParties, []); - assert.deepEqual( - subject.inserts.map(({ table }) => table), - [partyMatchDecisions], - ); - assert.equal( - subject.reads.some(({ table }) => table === partyIdentifierClaims), - false, - ); - }), - )); + testIt.effect( + 'PERSON IČO cannot acquire organization auto-match authority and NO_MATCH still records a decision', + () => + Effect.gen(function* personIOCannotAcquireOrganizationAuto() { + const subject = harness(); + const result = yield* matchParty(subject.transaction, { + actionInvocationId, + candidate: candidate({ + officialIdentifiers: [ + { identifierType: 'ICO', value: '27074358', verification: 'VERIFIED' }, + ], + partyType: 'PERSON', + subjectEvidence: [ + { + basis: 'DIRECT_INTERACTION', + evidenceRef: 'meeting/42', + kind: 'ACTOR_ATTESTATION', + observedSubject: 'PERSON', + statement: 'Met this human', + subjectKey: partySubjectKeyFromString('one-subject'), + }, + ], + }), + tenantId, + }); + expect(result.outcome).toBe('NO_MATCH'); + const durable = recordedRow( + subject.inserts.find(({ table }) => table === partyMatchDecisions)?.values, + ); + expect(durable['operation']).toBe('MATCH'); + expect(durable['committedCreateOutcome']).toBe(null); + expect(result.candidateParties).toEqual([]); + expect(subject.inserts.map(({ table }) => table)).toEqual([partyMatchDecisions]); + expect(subject.reads.some(({ table }) => table === partyIdentifierClaims)).toBe(false); + }), + ); -const resolutionInput = { - actionInvocationId, - candidateCaseId, - expectedRevision: 1, - principalId, - reason: 'Reviewed authoritative evidence', - selectedPartyId: partyC, - selectedPartyTenantId: tenantId, - tenantId, -}; + const resolutionInput = { + actionInvocationId, + candidateCaseId, + expectedRevision: 1, + principalId, + reason: 'Reviewed authoritative evidence', + selectedPartyId: partyC, + selectedPartyTenantId: tenantId, + tenantId, + }; -test('an unarchive review cannot create a replacement Party or attach its facts through Candidate matching', () => - runEffectTestPromise( - Effect.gen(function* unarchiveIntentBoundary() { - for (const resolution of ['CREATE', 'MATCH']) { - const original = caseRow(); - const subject = harness( - new Map([ - [ - duplicateCandidateCases, + testIt.effect( + 'an unarchive review cannot create a replacement Party or attach its facts through Candidate matching', + () => + Effect.gen(function* unarchiveIntentBoundary() { + for (const resolution of ['CREATE', 'MATCH']) { + const original = caseRow(); + const subject = harness( + new Map([ [ + duplicateCandidateCases, [ - { - ...original, - candidateSnapshot: { ...original.candidateSnapshot, intent: 'UNARCHIVE' }, - }, + [ + { + ...original, + candidateSnapshot: { ...original.candidateSnapshot, intent: 'UNARCHIVE' }, + }, + ], ], ], - ], + ]), + ); + const error = + resolution === 'CREATE' + ? yield* Effect.flip( + resolveDuplicateCandidateCreate(subject.transaction, resolutionInput), + ) + : yield* Effect.flip( + resolveDuplicateCandidateMatch(subject.transaction, resolutionInput), + ); + expect(Predicate.isTagged(error, 'DuplicateCandidateConflict')).toBe(true); + expect(error.reason).toMatch(/unarchive/iu); + expect(subject.inserts).toEqual([]); + expect(subject.updates).toEqual([]); + } + }), + ); + + const activePartyRow = (partyId: string) => ({ + archivedAt: null, + createdAt: DateTime.toDateUtc(DateTime.makeUnsafe(instant)), + currentDisplayName: null, + currentType: 'ORGANIZATION', + partyId, + revision: 1, + tenantId, + updatedAt: DateTime.toDateUtc(DateTime.makeUnsafe(instant)), + }); + const actionScope = { + authMethod: 'system' as const, + correlationId: 'matching-events', + principalId, + tenantId, + }; + + testIt.effect( + 'Create Party matching an existing subject publishes each newly accepted identifier without fabricating Party Created', + () => + Effect.gen(function* createPartyMatchingAnExistingSubjectPublishes() { + const subject = harness( + new Map([ + [partyIdentifierClaims, [[{ partyId: partyA }], []]], + [parties, [[activePartyRow(partyA)]]], + [partyOfficialIdentifiers, [[]]], ]), ); - const error = - resolution === 'CREATE' - ? yield* Effect.flip( - resolveDuplicateCandidateCreate(subject.transaction, resolutionInput), - ) - : yield* Effect.flip( - resolveDuplicateCandidateMatch(subject.transaction, resolutionInput), - ); - assert.equal(error._tag, 'DuplicateCandidateConflict'); - assert.match(error.reason, /unarchive/iu); - assert.deepEqual(subject.inserts, []); - assert.deepEqual(subject.updates, []); - } - }), - )); - -const activePartyRow = (partyId: string) => ({ - archivedAt: null, - createdAt: DateTime.toDateUtc(DateTime.makeUnsafe(instant)), - currentDisplayName: null, - currentType: 'ORGANIZATION', - partyId, - revision: 1, - tenantId, - updatedAt: DateTime.toDateUtc(DateTime.makeUnsafe(instant)), -}); -const actionScope = { - authMethod: 'system' as const, - correlationId: 'matching-events', - principalId, - tenantId, -}; + const collector = createActionCollector( + createPartyAction.descriptor.domainEvents, + 'party.registry', + createPartyAction.descriptor.accessEvidencePolicy, + ); + const result = yield* getActionHandler(createPartyAction)( + { candidate: candidate() }, + { + ...collector, + actionInvocationId, + scope: actionScope, + services: { + createOrMatch: (value, invocationId) => + createOrMatchParty(subject.transaction, { + actionInvocationId: invocationId, + candidate: value, + principalId, + tenantId, + }), + }, + }, + ); + expect(result.outcome).toBe('MATCHED_EXISTING'); + const durable = recordedRow( + subject.inserts.find(({ table }) => table === partyMatchDecisions)?.values, + ); + expect(durable['operation']).toBe('CREATE'); + expect(durable['committedCreateOutcome']).toBe('MATCHED_EXISTING'); + expect( + 'addedOfficialIdentifierRefs' in result, + 'mutation metadata stays private to the Action', + ).toBe(false); + const evidence = collector.snapshot(); + expect(evidence.domainEvents.map((event) => event.eventType)).toEqual([ + 'party.registry.official-identifier-added.v1', + ]); + expect(evidence.outboxMessages.length).toBe(1); + expect(evidence.outboxMessages[0]?.domainEventIndex).toBe(0); + expect(evidence.outboxMessages[0]?.message.payloadJson).toEqual({ + officialIdentifierRef: { + moduleId: 'party.registry', + resourceId: officialIdentifierId, + resourceType: 'party.registry.party-official-identifier', + tenantId, + }, + partyRef: makePartyRef(tenantId, partyA), + }); + }), + ); -test('Create Party matching an existing subject publishes each newly accepted identifier without fabricating Party Created', () => - runEffectTestPromise( - Effect.gen(function* createPartyMatchingAnExistingSubjectPublishes() { - const subject = harness( - new Map([ - [partyIdentifierClaims, [[{ partyId: partyA }], []]], - [parties, [[activePartyRow(partyA)]]], - [partyOfficialIdentifiers, [[]]], - ]), - ); + const invokeReviewedMatch = (subject: ReturnType) => + Effect.gen(function* invokeReviewedMatchEffect() { const collector = createActionCollector( - createPartyAction.descriptor.domainEvents, + resolveDuplicateCandidateMatchAction.descriptor.domainEvents, 'party.registry', - createPartyAction.descriptor.accessEvidencePolicy, + resolveDuplicateCandidateMatchAction.descriptor.accessEvidencePolicy, ); - const result = yield* getActionHandler(createPartyAction)( - { candidate: candidate() }, + const result = yield* getActionHandler(resolveDuplicateCandidateMatchAction)( + { + caseRef: makeDuplicateCandidateCaseRef(tenantId, candidateCaseId), + expectedRevision: 1, + reason: resolutionInput.reason, + selectedPartyRef: makePartyRef(tenantId, partyC), + }, { ...collector, actionInvocationId, scope: actionScope, services: { - createOrMatch: (value, invocationId) => - createOrMatchParty(subject.transaction, { - actionInvocationId: invocationId, - candidate: value, - principalId, - tenantId, - }), + resolve: () => resolveDuplicateCandidateMatch(subject.transaction, resolutionInput), }, }, ); - assert.equal(result.outcome, 'MATCHED_EXISTING'); - const durable = recordedRow( - subject.inserts.find(({ table }) => table === partyMatchDecisions)?.values, - ); - assert.equal(durable['operation'], 'CREATE'); - assert.equal(durable['committedCreateOutcome'], 'MATCHED_EXISTING'); - assert.equal( - 'addedOfficialIdentifierRefs' in result, - false, - 'mutation metadata stays private to the Action', - ); - const evidence = collector.snapshot(); - assert.deepEqual( - evidence.domainEvents.map((event) => event.eventType), - ['party.registry.official-identifier-added.v1'], - ); - assert.equal(evidence.outboxMessages.length, 1); - assert.equal(evidence.outboxMessages[0]?.domainEventIndex, 0); - assert.deepEqual(evidence.outboxMessages[0]?.message.payloadJson, { - officialIdentifierRef: { - moduleId: 'party.registry', - resourceId: officialIdentifierId, - resourceType: 'party.registry.party-official-identifier', - tenantId, - }, - partyRef: makePartyRef(tenantId, partyA), - }); - }), - )); - -const invokeReviewedMatch = (subject: ReturnType) => - Effect.gen(function* invokeReviewedMatchEffect() { - const collector = createActionCollector( - resolveDuplicateCandidateMatchAction.descriptor.domainEvents, - 'party.registry', - resolveDuplicateCandidateMatchAction.descriptor.accessEvidencePolicy, - ); - const result = yield* getActionHandler(resolveDuplicateCandidateMatchAction)( - { - caseRef: makeDuplicateCandidateCaseRef(tenantId, candidateCaseId), - expectedRevision: 1, - reason: resolutionInput.reason, - selectedPartyRef: makePartyRef(tenantId, partyC), - }, - { - ...collector, - actionInvocationId, - scope: actionScope, - services: { - resolve: () => resolveDuplicateCandidateMatch(subject.transaction, resolutionInput), - }, - }, - ); - return { collector, result }; - }); + return { collector, result }; + }); -test('reviewed matching publishes the accepted identifier through its declared Action event and linked outbox', () => - runEffectTestPromise( - Effect.gen(function* reviewedMatchingPublishesTheAcceptedIdentifierThrough() { - const subject = harness( - new Map([ - [duplicateCandidateCases, [[caseRow()]]], - [partyAliases, [[]]], - [parties, [[activePartyRow(partyC)], [activePartyRow(partyC)]]], - [partyIdentifierClaims, [[]]], - [partyOfficialIdentifiers, [[]]], - ]), - ); - const { collector, result } = yield* invokeReviewedMatch(subject); - assert.equal(result.outcome, 'MATCH_EXISTING'); - assert.equal('addedOfficialIdentifierRefs' in result, false); - const evidence = collector.snapshot(); - assert.deepEqual( - evidence.domainEvents.map((event) => event.eventType), - ['party.registry.official-identifier-added.v1'], - ); - assert.equal(evidence.outboxMessages.length, 1); - assert.equal(evidence.outboxMessages[0]?.domainEventIndex, 0); - assert.equal( - evidence.outboxMessages[0]?.message.topic, - 'party.registry.official-identifier-added.v1', - ); - assert.deepEqual( - evidence.outboxMessages[0]?.message.payloadJson, - evidence.domainEvents[0]?.payloadJson, - ); - }), - )); + testIt.effect( + 'reviewed matching publishes the accepted identifier through its declared Action event and linked outbox', + () => + Effect.gen(function* reviewedMatchingPublishesTheAcceptedIdentifierThrough() { + const subject = harness( + new Map([ + [duplicateCandidateCases, [[caseRow()]]], + [partyAliases, [[]]], + [parties, [[activePartyRow(partyC)], [activePartyRow(partyC)]]], + [partyIdentifierClaims, [[]]], + [partyOfficialIdentifiers, [[]]], + ]), + ); + const { collector, result } = yield* invokeReviewedMatch(subject); + expect(result.outcome).toBe('MATCH_EXISTING'); + expect('addedOfficialIdentifierRefs' in result).toBe(false); + const evidence = collector.snapshot(); + expect(evidence.domainEvents.map((event) => event.eventType)).toEqual([ + 'party.registry.official-identifier-added.v1', + ]); + expect(evidence.outboxMessages.length).toBe(1); + expect(evidence.outboxMessages[0]?.domainEventIndex).toBe(0); + expect(evidence.outboxMessages[0]?.message.topic).toBe( + 'party.registry.official-identifier-added.v1', + ); + expect(evidence.outboxMessages[0]?.message.payloadJson).toEqual( + evidence.domainEvents[0]?.payloadJson, + ); + }), + ); -test('matched Create reusing an existing identifier does not republish an acceptance event', () => - runEffectTestPromise( - Effect.gen(function* matchedCreateReusingAnExistingIdentifierDoes() { - const subject = harness( - new Map([ - [partyIdentifierClaims, [[{ partyId: partyA }]]], - [parties, [[activePartyRow(partyA)]]], - [ - partyOfficialIdentifiers, + testIt.effect( + 'matched Create reusing an existing identifier does not republish an acceptance event', + () => + Effect.gen(function* matchedCreateReusingAnExistingIdentifierDoes() { + const subject = harness( + new Map([ + [partyIdentifierClaims, [[{ partyId: partyA }]]], + [parties, [[activePartyRow(partyA)]]], [ + partyOfficialIdentifiers, [ - { - acceptedByActionInvocationId: 'prior-acceptance', - officialIdentifierId, - partyId: partyA, - }, + [ + { + acceptedByActionInvocationId: 'prior-acceptance', + officialIdentifierId, + partyId: partyA, + }, + ], ], ], - ], - ]), - ); - const collector = createActionCollector( - createPartyAction.descriptor.domainEvents, - 'party.registry', - createPartyAction.descriptor.accessEvidencePolicy, - ); - const result = yield* getActionHandler(createPartyAction)( - { - candidate: candidate({ - officialIdentifiers: [ - { identifierType: 'ICO', value: '27074358', verification: 'VERIFIED' }, - { identifierType: 'CZ_DIC', value: 'CZ27074358', verification: 'UNVERIFIED' }, - ], - }), - }, - { - ...collector, - actionInvocationId, - scope: actionScope, - services: { - createOrMatch: (value, invocationId) => - createOrMatchParty(subject.transaction, { - actionInvocationId: invocationId, - candidate: value, - principalId, - tenantId, - }), + ]), + ); + const collector = createActionCollector( + createPartyAction.descriptor.domainEvents, + 'party.registry', + createPartyAction.descriptor.accessEvidencePolicy, + ); + const result = yield* getActionHandler(createPartyAction)( + { + candidate: candidate({ + officialIdentifiers: [ + { identifierType: 'ICO', value: '27074358', verification: 'VERIFIED' }, + { identifierType: 'CZ_DIC', value: 'CZ27074358', verification: 'UNVERIFIED' }, + ], + }), }, - }, - ); - assert.equal(result.outcome, 'MATCHED_EXISTING'); - assert.deepEqual(collector.snapshot().domainEvents, []); - assert.deepEqual(collector.snapshot().outboxMessages, []); - assert.equal( - subject.inserts.some(({ table }) => table === partyOfficialIdentifiers), - false, - ); - }), - )); + { + ...collector, + actionInvocationId, + scope: actionScope, + services: { + createOrMatch: (value, invocationId) => + createOrMatchParty(subject.transaction, { + actionInvocationId: invocationId, + candidate: value, + principalId, + tenantId, + }), + }, + }, + ); + expect(result.outcome).toBe('MATCHED_EXISTING'); + expect(collector.snapshot().domainEvents).toEqual([]); + expect(collector.snapshot().outboxMessages).toEqual([]); + expect(subject.inserts.some(({ table }) => table === partyOfficialIdentifiers)).toBe(false); + }), + ); -test('repeated Candidate facts accepted in one matching transaction publish one identifier event', () => - runEffectTestPromise( - Effect.gen(function* repeatedCandidateFactsAcceptedInOneMatching() { - const subject = harness( - new Map([ - [partyIdentifierClaims, [[{ partyId: partyA }]]], - [parties, [[activePartyRow(partyA)]]], - [ - partyOfficialIdentifiers, + testIt.effect( + 'repeated Candidate facts accepted in one matching transaction publish one identifier event', + () => + Effect.gen(function* repeatedCandidateFactsAcceptedInOneMatching() { + const subject = harness( + new Map([ + [partyIdentifierClaims, [[{ partyId: partyA }]]], + [parties, [[activePartyRow(partyA)]]], [ - [], + partyOfficialIdentifiers, [ - { - acceptedByActionInvocationId: actionInvocationId, - officialIdentifierId, - partyId: partyA, - }, + [], + [ + { + acceptedByActionInvocationId: actionInvocationId, + officialIdentifierId, + partyId: partyA, + }, + ], ], ], - ], - ]), - ); - const identifier = { - identifierType: 'CZ_DIC' as const, - value: 'CZ27074358', - verification: 'UNVERIFIED' as const, - }; - const collector = createActionCollector( - createPartyAction.descriptor.domainEvents, - 'party.registry', - createPartyAction.descriptor.accessEvidencePolicy, - ); - yield* getActionHandler(createPartyAction)( - { - candidate: candidate({ - officialIdentifiers: [ - { identifierType: 'ICO', value: '27074358', verification: 'VERIFIED' }, - identifier, - identifier, - ], - }), - }, - { - ...collector, - actionInvocationId, - scope: actionScope, - services: { - createOrMatch: (value, invocationId) => - createOrMatchParty(subject.transaction, { - actionInvocationId: invocationId, - candidate: value, - principalId, - tenantId, - }), + ]), + ); + const identifier = { + identifierType: 'CZ_DIC' as const, + value: 'CZ27074358', + verification: 'UNVERIFIED' as const, + }; + const collector = createActionCollector( + createPartyAction.descriptor.domainEvents, + 'party.registry', + createPartyAction.descriptor.accessEvidencePolicy, + ); + yield* getActionHandler(createPartyAction)( + { + candidate: candidate({ + officialIdentifiers: [ + { identifierType: 'ICO', value: '27074358', verification: 'VERIFIED' }, + identifier, + identifier, + ], + }), }, - }, - ); - assert.equal( - subject.inserts.filter(({ table }) => table === partyOfficialIdentifiers).length, - 1, - ); - assert.equal(collector.snapshot().domainEvents.length, 1); - assert.equal(collector.snapshot().outboxMessages.length, 1); - }), - )); + { + ...collector, + actionInvocationId, + scope: actionScope, + services: { + createOrMatch: (value, invocationId) => + createOrMatchParty(subject.transaction, { + actionInvocationId: invocationId, + candidate: value, + principalId, + tenantId, + }), + }, + }, + ); + expect( + subject.inserts.filter(({ table }) => table === partyOfficialIdentifiers).length, + ).toBe(1); + expect(collector.snapshot().domainEvents.length).toBe(1); + expect(collector.snapshot().outboxMessages.length).toBe(1); + }), + ); -test('reviewed matching with already-owned claims creates no duplicate identifier notifications', () => - runEffectTestPromise( - Effect.gen(function* reviewedMatchingWithAlreadyOwnedClaimsCreates() { - const subject = harness( - new Map([ - [duplicateCandidateCases, [[caseRow()]]], - [partyAliases, [[], []]], - [parties, [[activePartyRow(partyC)], [activePartyRow(partyC)], [activePartyRow(partyC)]]], - [partyIdentifierClaims, [[{ officialIdentifierId, partyId: partyC }]]], - ]), - ); - const { collector, result } = yield* invokeReviewedMatch(subject); - assert.equal(result.outcome, 'MATCH_EXISTING'); - assert.deepEqual(collector.snapshot().domainEvents, []); - assert.deepEqual(collector.snapshot().outboxMessages, []); - }), - )); + testIt.effect( + 'reviewed matching with already-owned claims creates no duplicate identifier notifications', + () => + Effect.gen(function* reviewedMatchingWithAlreadyOwnedClaimsCreates() { + const subject = harness( + new Map([ + [duplicateCandidateCases, [[caseRow()]]], + [partyAliases, [[], []]], + [ + parties, + [[activePartyRow(partyC)], [activePartyRow(partyC)], [activePartyRow(partyC)]], + ], + [partyIdentifierClaims, [[{ officialIdentifierId, partyId: partyC }]]], + ]), + ); + const { collector, result } = yield* invokeReviewedMatch(subject); + expect(result.outcome).toBe('MATCH_EXISTING'); + expect(collector.snapshot().domainEvents).toEqual([]); + expect(collector.snapshot().outboxMessages).toEqual([]); + }), + ); -test('reviewed matching locks and rejects an archived canonical target before any attachment or resolution', () => - runEffectTestPromise( - Effect.gen(function* reviewedMatchingLocksAndRejectsAnArchived() { - const subject = harness( - new Map([ - [duplicateCandidateCases, [[caseRow()]]], - [partyAliases, [[]]], - [ - parties, + testIt.effect( + 'reviewed matching locks and rejects an archived canonical target before any attachment or resolution', + () => + Effect.gen(function* reviewedMatchingLocksAndRejectsAnArchived() { + const subject = harness( + new Map([ + [duplicateCandidateCases, [[caseRow()]]], + [partyAliases, [[]]], [ - [{ partyId: partyC }], + parties, [ - { - archivedAt: DateTime.toDateUtc(DateTime.makeUnsafe(instant)), - currentType: 'ORGANIZATION', - partyId: partyC, - }, + [{ partyId: partyC }], + [ + { + archivedAt: DateTime.toDateUtc(DateTime.makeUnsafe(instant)), + currentType: 'ORGANIZATION', + partyId: partyC, + }, + ], ], ], - ], - ]), - ); - const failure = yield* Effect.flip( - resolveDuplicateCandidateMatch(subject.transaction, resolutionInput), - ); - assert.equal(failure._tag, 'DuplicateCandidateConflict'); - assert.equal( - subject.reads.some(({ table, locked }) => table === parties && locked), - true, - ); - assert.deepEqual(subject.inserts, []); - assert.deepEqual(subject.updates, []); - }), - )); + ]), + ); + const failure = yield* Effect.flip( + resolveDuplicateCandidateMatch(subject.transaction, resolutionInput), + ); + expect(Predicate.isTagged(failure, 'DuplicateCandidateConflict')).toBe(true); + expect(subject.reads.some(({ table, locked }) => table === parties && locked)).toBe(true); + expect(subject.inserts).toEqual([]); + expect(subject.updates).toEqual([]); + }), + ); -test('reviewed matching rejects a cross-tenant selected reference without resolving its identity', () => - runEffectTestPromise( - Effect.gen(function* reviewedMatchingRejectsACrossTenantSelected() { - const subject = harness(); - const failure = yield* Effect.flip( - resolveDuplicateCandidateMatch(subject.transaction, { - ...resolutionInput, - selectedPartyTenantId: '90000000-0000-4000-8000-000000000001', - }), - ); - assert.equal(failure._tag, 'DuplicateCandidateConflict'); - assert.equal( - subject.reads.length, - 1, - 'only the trusted tenant serialization lock is acquired', - ); - assert.deepEqual(subject.inserts, []); - }), - )); + testIt.effect( + 'reviewed matching rejects a cross-tenant selected reference without resolving its identity', + () => + Effect.gen(function* reviewedMatchingRejectsACrossTenantSelected() { + const subject = harness(); + const failure = yield* Effect.flip( + resolveDuplicateCandidateMatch(subject.transaction, { + ...resolutionInput, + selectedPartyTenantId: '90000000-0000-4000-8000-000000000001', + }), + ); + expect(Predicate.isTagged(failure, 'DuplicateCandidateConflict')).toBe(true); + expect(subject.reads.length, 'only the trusted tenant serialization lock is acquired').toBe( + 1, + ); + expect(subject.inserts).toEqual([]); + }), + ); -test('reviewed matching rejects an absorbed target with the full-chain canonical survivor reference', () => - runEffectTestPromise( - Effect.gen(function* reviewedMatchingRejectsAnAbsorbedTargetWith() { - const subject = harness( - new Map([ - [duplicateCandidateCases, [[caseRow()]]], - [ - partyAliases, + testIt.effect( + 'reviewed matching rejects an absorbed target with the full-chain canonical survivor reference', + () => + Effect.gen(function* reviewedMatchingRejectsAnAbsorbedTargetWith() { + const subject = harness( + new Map([ + [duplicateCandidateCases, [[caseRow()]]], [ - [{ aliasPartyId: partyB, canonicalPartyId: partyA, tenantId }], - [{ aliasPartyId: partyA, canonicalPartyId: partyC, tenantId }], - [], + partyAliases, + [ + [{ aliasPartyId: partyB, canonicalPartyId: partyA, tenantId }], + [{ aliasPartyId: partyA, canonicalPartyId: partyC, tenantId }], + [], + ], ], - ], - [parties, [[{ partyId: partyC }]]], - ]), - ); - const failure = yield* Effect.flip( - resolveDuplicateCandidateMatch(subject.transaction, { - ...resolutionInput, - selectedPartyId: partyB, - }), - ); - assert.ok(Schema.is(PartyAliasWriteRejected)(failure)); - assert.equal(failure.canonicalPartyRef.resourceId, partyC); - assert.deepEqual(subject.inserts, []); - assert.deepEqual(subject.updates, []); - }), - )); + [parties, [[{ partyId: partyC }]]], + ]), + ); + const failure = yield* Effect.flip( + resolveDuplicateCandidateMatch(subject.transaction, { + ...resolutionInput, + selectedPartyId: partyB, + }), + ); + expect(Schema.is(PartyAliasWriteRejected)(failure)).toBe(true); + const rejection = yield* Schema.decodeUnknownEffect(PartyAliasWriteRejected)(failure); + expect(rejection.canonicalPartyRef.resourceId).toBe(partyC); + expect(subject.inserts).toEqual([]); + expect(subject.updates).toEqual([]); + }), + ); -test('future-effective evidence is rejected before a current decision or Party can be persisted', () => - runEffectTestPromise( - Effect.gen(function* futureEffectiveEvidenceIsRejectedBeforeA() { - const subject = harness(); - const failure = yield* Effect.flip( - matchParty(subject.transaction, { + testIt.effect( + 'future-effective evidence is rejected before a current decision or Party can be persisted', + () => + Effect.gen(function* futureEffectiveEvidenceIsRejectedBeforeA() { + const subject = harness(); + const failure = yield* Effect.flip( + matchParty(subject.transaction, { + actionInvocationId, + candidate: candidate({ validFrom: DateTime.makeUnsafe('2099-01-01T00:00:00.000Z') }), + tenantId, + }), + ); + expect(Predicate.isTagged(failure, 'PartyEvidenceInsufficient')).toBe(true); + expect(subject.inserts).toEqual([]); + }), + ); + + it('durable matching is an idempotent identity Action and the separate UX preview remains a governed read', () => { + expect(matchPartyAction.descriptor.idempotency).toBe('required'); + expect(matchPartyAction.descriptor.tenantPermission?.({ candidate: candidate() })).toBe( + 'manage_party_identity', + ); + expect(matchPartyAction.descriptor.legalEntityScope).toBe('optional'); + expect(partyMatchRead.descriptor.accessKind).toBe('detail'); + }); + + testIt.effect( + 'weak exact canonical evidence produces review rather than automatic identity or NO_MATCH', + () => + Effect.gen(function* weakExactCanonicalEvidenceProducesReviewRather() { + const subject = harness( + new Map([ + [partyOfficialIdentifiers, [[{ partyId: partyA }]]], + [partyAliases, [[]]], + [parties, [[{ partyId: partyA }]]], + ]), + ); + const result = yield* matchParty(subject.transaction, { actionInvocationId, - candidate: candidate({ validFrom: DateTime.makeUnsafe('2099-01-01T00:00:00.000Z') }), + candidate: candidate({ + officialIdentifiers: [ + { identifierType: 'ICO', value: '27074358', verification: 'UNVERIFIED' }, + ], + partyType: 'PERSON', + subjectEvidence: [ + { + basis: 'DIRECT_INTERACTION', + evidenceRef: 'meeting/42', + kind: 'ACTOR_ATTESTATION', + observedSubject: 'PERSON', + statement: 'Met this human', + subjectKey: partySubjectKeyFromString('one-subject'), + }, + ], + }), tenantId, - }), - ); - assert.equal(failure._tag, 'PartyEvidenceInsufficient'); - assert.deepEqual(subject.inserts, []); - }), - )); - -test('durable matching is an idempotent identity Action and the separate UX preview remains a governed read', () => { - assert.equal(matchPartyAction.descriptor.idempotency, 'required'); - assert.equal( - matchPartyAction.descriptor.tenantPermission?.({ candidate: candidate() }), - 'manage_party_identity', + }); + expect(result.outcome).toBe('AMBIGUOUS'); + expect(result.caseRef?.resourceId).toBe(candidateCaseId); + expect(result.candidateParties.map((ref) => ref.resourceId)).toEqual([partyA]); + expect(subject.inserts.some(({ table }) => table === parties)).toBe(false); + }), ); - assert.equal(matchPartyAction.descriptor.legalEntityScope, 'optional'); - assert.equal(partyMatchRead.descriptor.accessKind, 'detail'); -}); - -test('weak exact canonical evidence produces review rather than automatic identity or NO_MATCH', () => - runEffectTestPromise( - Effect.gen(function* weakExactCanonicalEvidenceProducesReviewRather() { - const subject = harness( - new Map([ - [partyOfficialIdentifiers, [[{ partyId: partyA }]]], - [partyAliases, [[]]], - [parties, [[{ partyId: partyA }]]], - ]), - ); - const result = yield* matchParty(subject.transaction, { - actionInvocationId, - candidate: candidate({ - officialIdentifiers: [ - { identifierType: 'ICO', value: '27074358', verification: 'UNVERIFIED' }, - ], - partyType: 'PERSON', - subjectEvidence: [ - { - basis: 'DIRECT_INTERACTION', - evidenceRef: 'meeting/42', - kind: 'ACTOR_ATTESTATION', - observedSubject: 'PERSON', - statement: 'Met this human', - subjectKey: partySubjectKeyFromString('one-subject'), - }, - ], - }), - tenantId, - }); - assert.equal(result.outcome, 'AMBIGUOUS'); - assert.equal(result.caseRef?.resourceId, candidateCaseId); - assert.deepEqual( - result.candidateParties.map((ref) => ref.resourceId), - [partyA], - ); - assert.equal( - subject.inserts.some(({ table }) => table === parties), - false, - ); - }), - )); -test('initial no-strong Create review captures relevant same-name canonical Parties in its immutable snapshot', () => - runEffectTestPromise( - Effect.gen(function* initialNoStrongCreateReviewCapturesRelevant() { - const subject = harness( - new Map([ - [parties, [[{ partyId: partyA }], [{ partyId: partyA }]]], - [partyAliases, [[]]], - ]), - ); - const result = yield* createOrMatchParty(subject.transaction, { - actionInvocationId, - candidate: candidate({ - displayName: 'Northwind Workshop', - evidenceRefs: ['business-record:contract:42'], - officialIdentifiers: [], - partyType: 'UNRESOLVED', - }), - principalId, - tenantId, - }); - assert.equal(result.outcome, 'AMBIGUOUS'); - const links = recordedRows( - subject.inserts.find(({ table }) => table === duplicateCandidateCaseParties)?.values, - ); - assert.deepEqual( - links.map((row) => row['partyId']), - [partyA], - ); - assert.equal( - subject.inserts.some(({ table }) => table === parties), - false, - ); - }), - )); + testIt.effect( + 'initial no-strong Create review captures relevant same-name canonical Parties in its immutable snapshot', + () => + Effect.gen(function* initialNoStrongCreateReviewCapturesRelevant() { + const subject = harness( + new Map([ + [parties, [[{ partyId: partyA }], [{ partyId: partyA }]]], + [partyAliases, [[]]], + ]), + ); + const result = yield* createOrMatchParty(subject.transaction, { + actionInvocationId, + candidate: candidate({ + displayName: 'Northwind Workshop', + evidenceRefs: ['business-record:contract:42'], + officialIdentifiers: [], + partyType: 'UNRESOLVED', + }), + principalId, + tenantId, + }); + expect(result.outcome).toBe('AMBIGUOUS'); + const links = recordedRows( + subject.inserts.find(({ table }) => table === duplicateCandidateCaseParties)?.values, + ); + expect(links.map((row) => row['partyId'])).toEqual([partyA]); + expect(subject.inserts.some(({ table }) => table === parties)).toBe(false); + }), + ); -test('a new material evaluation creates a linked successor without rewriting the prior case', () => - runEffectTestPromise( - Effect.gen(function* aNewMaterialEvaluationCreatesALinked() { - const priorId = '30000000-0000-4000-8000-000000000099'; - const subject = harness( - new Map([ - [ - partyIdentifierClaims, + testIt.effect( + 'a new material evaluation creates a linked successor without rewriting the prior case', + () => + Effect.gen(function* aNewMaterialEvaluationCreatesALinked() { + const priorId = '30000000-0000-4000-8000-000000000099'; + const subject = harness( + new Map([ [ - [{ officialIdentifierId: '70000000-0000-4000-8000-000000000001', partyId: partyA }], - [{ partyId: partyB }], + partyIdentifierClaims, + [ + [{ officialIdentifierId: '70000000-0000-4000-8000-000000000001', partyId: partyA }], + [{ partyId: partyB }], + ], ], - ], - [partyAliases, [[], []]], - [parties, [[{ partyId: partyA }], [{ partyId: partyB }]]], - [ - duplicateCandidateCases, - [[], [{ ...caseRow(), candidateCaseId: priorId, lifecycleState: 'RESOLVED' }]], - ], - ]), - ); - const result = yield* matchParty(subject.transaction, { - actionInvocationId, - candidate: candidate(), - tenantId, - }); - const insertedCase = recordedRow( - subject.inserts.find(({ table }) => table === duplicateCandidateCases)?.values, - ); - assert.equal(insertedCase['priorCandidateCaseId'], priorId); - assert.match(String(insertedCase['evaluationFingerprint']), /^[0-9a-f]{64}$/u); - assert.equal( - result.evidenceExplanation[0]?.officialIdentifierRef?.resourceId, - '70000000-0000-4000-8000-000000000001', - ); - assert.equal(result.evidenceExplanation[0]?.identifierType, 'ICO'); - assert.equal(result.evidenceExplanation[0]?.normalizedValue, '27074358'); - assert.deepEqual(subject.updates, []); - }), - )); + [partyAliases, [[], []]], + [parties, [[{ partyId: partyA }], [{ partyId: partyB }]]], + [ + duplicateCandidateCases, + [[], [{ ...caseRow(), candidateCaseId: priorId, lifecycleState: 'RESOLVED' }]], + ], + ]), + ); + const result = yield* matchParty(subject.transaction, { + actionInvocationId, + candidate: candidate(), + tenantId, + }); + const insertedCase = recordedRow( + subject.inserts.find(({ table }) => table === duplicateCandidateCases)?.values, + ); + expect(insertedCase['priorCandidateCaseId']).toBe(priorId); + expect(String(insertedCase['evaluationFingerprint'])).toMatch(/^[0-9a-f]{64}$/u); + expect(result.evidenceExplanation[0]?.officialIdentifierRef?.resourceId).toBe( + '70000000-0000-4000-8000-000000000001', + ); + expect(result.evidenceExplanation[0]?.identifierType).toBe('ICO'); + expect(result.evidenceExplanation[0]?.normalizedValue).toBe('27074358'); + expect(subject.updates).toEqual([]); + }), + ); -test('explicit prior-case continuation rejects foreign or missing review references', () => - runEffectTestPromise( - Effect.forEach( - [tenantId, '90000000-0000-4000-8000-000000000001'], - (priorCaseTenantId) => - Effect.gen(function* rejectInvalidPriorCaseReference() { - const subject = harness(); - const failure = yield* Effect.flip( - matchParty(subject.transaction, { - actionInvocationId, - candidate: candidate(), - priorCandidateCaseId: candidateCaseId, - priorCaseTenantId, - tenantId, - }), - ); - assert.equal(failure._tag, 'PartyEvidenceInsufficient'); - assert.deepEqual(subject.inserts, []); - }), - { concurrency: 'unbounded', discard: true }, - ), - )); + testIt.effect( + 'explicit prior-case continuation rejects foreign or missing review references', + () => + Effect.forEach( + [tenantId, '90000000-0000-4000-8000-000000000001'], + (priorCaseTenantId) => + Effect.gen(function* rejectInvalidPriorCaseReference() { + const subject = harness(); + const failure = yield* Effect.flip( + matchParty(subject.transaction, { + actionInvocationId, + candidate: candidate(), + priorCandidateCaseId: candidateCaseId, + priorCaseTenantId, + tenantId, + }), + ); + expect(Predicate.isTagged(failure, 'PartyEvidenceInsufficient')).toBe(true); + expect(subject.inserts).toEqual([]); + }), + { concurrency: 'unbounded', discard: true }, + ), + ); -test('equivalent Candidate property and evidence ordering has one deterministic fingerprint', () => { - const original = candidate({ - displayName: 'Northwind', - evidenceRefs: ['evidence:b', 'evidence:a'], + it('equivalent Candidate property and evidence ordering has one deterministic fingerprint', () => { + const original = candidate({ + displayName: 'Northwind', + evidenceRefs: ['evidence:b', 'evidence:a'], + }); + const reordered: PartyCandidate = { + displayName: 'Northwind', + evidenceRefs: original.evidenceRefs.toReversed(), + officialIdentifiers: original.officialIdentifiers.toReversed(), + partyType: original.partyType, + provenance: { method: original.provenance.method, source: original.provenance.source }, + subjectEvidence: original.subjectEvidence ?? [], + validFrom: original.validFrom, + }; + expect(candidateFingerprint(original)).toBe(candidateFingerprint(reordered)); }); - const reordered: PartyCandidate = { - displayName: 'Northwind', - evidenceRefs: original.evidenceRefs.toReversed(), - officialIdentifiers: original.officialIdentifiers.toReversed(), - partyType: original.partyType, - provenance: { method: original.provenance.method, source: original.provenance.source }, - subjectEvidence: original.subjectEvidence ?? [], - validFrom: original.validFrom, - }; - assert.equal(candidateFingerprint(original), candidateFingerprint(reordered)); -}); -test('insufficient typed evidence cannot persist a case or decision even with a verified identifier', () => - runEffectTestPromise( - Effect.gen(function* denyUnevidencedSubject() { - for (const operation of ['CREATE', 'MATCH'] as const) { - const subject = harness(); - const input = { - actionInvocationId, - candidate: candidate({ subjectEvidence: [] }), - principalId, - tenantId, - }; - const failure = yield* operation === 'CREATE' - ? Effect.flip(createOrMatchParty(subject.transaction, input)) - : Effect.flip(matchParty(subject.transaction, input)); - assert.equal(failure._tag, 'PartyEvidenceInsufficient'); - assert.equal(subject.inserts.length, 0); - } - }), - )); + testIt.effect( + 'insufficient typed evidence cannot persist a case or decision even with a verified identifier', + () => + Effect.gen(function* denyUnevidencedSubject() { + for (const operation of ['CREATE', 'MATCH'] as const) { + const subject = harness(); + const input = { + actionInvocationId, + candidate: candidate({ subjectEvidence: [] }), + principalId, + tenantId, + }; + const failure = yield* operation === 'CREATE' + ? Effect.flip(createOrMatchParty(subject.transaction, input)) + : Effect.flip(matchParty(subject.transaction, input)); + expect(Predicate.isTagged(failure, 'PartyEvidenceInsufficient')).toBe(true); + expect(subject.inserts.length).toBe(0); + } + }), + ); -test('reviewer selection cannot waive missing subject/type evidence from a retained case', () => - runEffectTestPromise( - Effect.gen(function* denyUnevidencedReview() { - const row = caseRow(); - const subject = harness( - new Map([ - [ - duplicateCandidateCases, - [[{ ...row, candidateSnapshot: { ...row.candidateSnapshot, subjectEvidence: [] } }]], - ], - ]), - ); - const failure = yield* Effect.flip( - resolveDuplicateCandidateMatch(subject.transaction, { - actionInvocationId, - candidateCaseId, - expectedRevision: 1, - principalId, - reason: 'reviewed', - selectedPartyId: partyA, - selectedPartyTenantId: tenantId, - tenantId, - }), - ); - assert.equal(failure._tag, 'DuplicateCandidateConflict'); - assert.equal(subject.inserts.length, 0); - assert.equal(subject.updates.length, 0); - }), - )); + testIt.effect( + 'reviewer selection cannot waive missing subject/type evidence from a retained case', + () => + Effect.gen(function* denyUnevidencedReview() { + const row = caseRow(); + const subject = harness( + new Map([ + [ + duplicateCandidateCases, + [[{ ...row, candidateSnapshot: { ...row.candidateSnapshot, subjectEvidence: [] } }]], + ], + ]), + ); + const failure = yield* Effect.flip( + resolveDuplicateCandidateMatch(subject.transaction, { + actionInvocationId, + candidateCaseId, + expectedRevision: 1, + principalId, + reason: 'reviewed', + selectedPartyId: partyA, + selectedPartyTenantId: tenantId, + tenantId, + }), + ); + expect(Predicate.isTagged(failure, 'DuplicateCandidateConflict')).toBe(true); + expect(subject.inserts.length).toBe(0); + expect(subject.updates.length).toBe(0); + }), + ); +}); diff --git a/app/verticals/party-registry/tests/unit/merge-alias-resolution-service.test.ts b/app/verticals/party-registry/tests/unit/merge-alias-resolution-service.test.ts index dc7b95be5..b356f1577 100644 --- a/app/verticals/party-registry/tests/unit/merge-alias-resolution-service.test.ts +++ b/app/verticals/party-registry/tests/unit/merge-alias-resolution-service.test.ts @@ -1,7 +1,6 @@ -import { runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Option } from 'effect'; +import { expect, it } from 'effect-rstest'; + +import { Effect, Match, Option, Predicate } from 'effect'; import { makePartyAliasResolutionService } from '../../src/merge/party-alias-resolution.service.ts'; import type { PartyAliasLookup } from '../../src/merge/party-alias-resolution.service.ts'; @@ -20,79 +19,93 @@ const lookup = (overrides: Partial = {}): PartyAliasLookup => ...overrides, }); -test('central resolution service walks the complete canonical alias chain in one scoped transaction seam', () => { - const service = makePartyAliasResolutionService(lookup()); - const result = runEffectTestSync(service.resolvePartyAlias(tenantId, 'party-b')); - - assert.deepEqual(result, { - canonicalPartyId: 'party-c', - requestedPartyId: 'party-b', - traversedAliasIds: ['party-b', 'party-a'], - wasAlias: true, - }); -}); +it.effect( + 'central resolution service walks the complete canonical alias chain in one scoped transaction seam', + () => + Effect.gen(function* aliasResolution1() { + const service = makePartyAliasResolutionService(lookup()); + const result = yield* service.resolvePartyAlias(tenantId, 'party-b'); -test('central resolution fails closed for cycles, cross-tenant targets, and broken chains', () => { - const cycle = makePartyAliasResolutionService( - lookup({ - findAlias: (_requestedTenantId, aliasPartyId) => - Effect.succeed( - Option.some( - aliasPartyId === 'party-a' - ? { aliasPartyId: 'party-a', canonicalPartyId: 'party-b', tenantId } - : { aliasPartyId: 'party-b', canonicalPartyId: 'party-a', tenantId }, - ), - ), + expect(result).toEqual({ + canonicalPartyId: 'party-c', + requestedPartyId: 'party-b', + traversedAliasIds: ['party-b', 'party-a'], + wasAlias: true, + }); }), - ); - const cycleError = runEffectTestSync(Effect.flip(cycle.resolvePartyAlias(tenantId, 'party-a'))); - assert.equal(cycleError._tag, 'PartyAliasResolutionCycle'); +); - const crossTenant = makePartyAliasResolutionService( - lookup({ - findAlias: () => - Effect.succeed( - Option.some({ - aliasPartyId: 'party-b', - canonicalPartyId: 'party-a', - tenantId: '22222222-2222-4222-8222-222222222222', - }), - ), - }), - ); - const crossTenantError = runEffectTestSync( - Effect.flip(crossTenant.resolvePartyAlias(tenantId, 'party-b')), - ); - assert.equal(crossTenantError._tag, 'PartyAliasResolutionCrossTenant'); +it.effect( + 'central resolution fails closed for cycles, cross-tenant targets, and broken chains', + () => + Effect.gen(function* aliasResolution2() { + const cycle = makePartyAliasResolutionService( + lookup({ + findAlias: (_requestedTenantId, aliasPartyId) => + Effect.succeed( + Option.some( + aliasPartyId === 'party-a' + ? { aliasPartyId: 'party-a', canonicalPartyId: 'party-b', tenantId } + : { aliasPartyId: 'party-b', canonicalPartyId: 'party-a', tenantId }, + ), + ), + }), + ); + const cycleError = yield* Effect.flip(cycle.resolvePartyAlias(tenantId, 'party-a')); + expect(Predicate.isTagged(cycleError, 'PartyAliasResolutionCycle')).toBe(true); - const broken = makePartyAliasResolutionService( - lookup({ - findAlias: () => Effect.succeed(Option.none()), - partyExists: () => Effect.succeed(false), + const crossTenant = makePartyAliasResolutionService( + lookup({ + findAlias: () => + Effect.succeed( + Option.some({ + aliasPartyId: 'party-b', + canonicalPartyId: 'party-a', + tenantId: '22222222-2222-4222-8222-222222222222', + }), + ), + }), + ); + const crossTenantError = yield* Effect.flip( + crossTenant.resolvePartyAlias(tenantId, 'party-b'), + ); + expect(Predicate.isTagged(crossTenantError, 'PartyAliasResolutionCrossTenant')).toBe(true); + + const broken = makePartyAliasResolutionService( + lookup({ + findAlias: () => Effect.succeed(Option.none()), + partyExists: () => Effect.succeed(false), + }), + ); + const brokenError = yield* Effect.flip(broken.resolvePartyAlias(tenantId, 'missing')); + expect(Predicate.isTagged(brokenError, 'PartyAliasResolutionBrokenChain')).toBe(true); }), - ); - const brokenError = runEffectTestSync(Effect.flip(broken.resolvePartyAlias(tenantId, 'missing'))); - assert.equal(brokenError._tag, 'PartyAliasResolutionBrokenChain'); -}); +); -test('central write guard returns typed canonical-survivor guidance and never forwards', () => { - const service = makePartyAliasResolutionService(lookup()); - const rejection = runEffectTestSync( - Effect.flip(service.requireCanonicalWriteTarget(tenantId, 'party-b')), - ); +it.effect('central write guard returns typed canonical-survivor guidance and never forwards', () => + Effect.gen(function* aliasResolution3() { + const service = makePartyAliasResolutionService(lookup()); + const rejection = yield* Effect.flip(service.requireCanonicalWriteTarget(tenantId, 'party-b')); - assert.equal(rejection._tag, 'PartyAliasWriteRejected'); - assert.deepEqual(rejection.aliasPartyRef, { - moduleId: 'party.registry', - resourceId: 'party-b', - resourceType: 'party.registry.party', - tenantId, - }); - assert.deepEqual(rejection.canonicalPartyRef, { - moduleId: 'party.registry', - resourceId: 'party-c', - resourceType: 'party.registry.party', - tenantId, - }); - assert.equal(rejection.code, 'party_alias_write_rejected'); -}); + expect(Predicate.isTagged(rejection, 'PartyAliasWriteRejected')).toBe(true); + const aliasRejection = Match.value(rejection).pipe( + Match.tag('PartyAliasWriteRejected', (failure) => failure), + Match.orElse(() => { + throw new Error('Expected alias write rejection'); + }), + ); + expect(aliasRejection.aliasPartyRef).toEqual({ + moduleId: 'party.registry', + resourceId: 'party-b', + resourceType: 'party.registry.party', + tenantId, + }); + expect(aliasRejection.canonicalPartyRef).toEqual({ + moduleId: 'party.registry', + resourceId: 'party-c', + resourceType: 'party.registry.party', + tenantId, + }); + expect(rejection.code).toBe('party_alias_write_rejected'); + }), +); diff --git a/app/verticals/party-registry/tests/unit/merge-alias-resolution.test.ts b/app/verticals/party-registry/tests/unit/merge-alias-resolution.test.ts index a9e7f342a..79be05b0f 100644 --- a/app/verticals/party-registry/tests/unit/merge-alias-resolution.test.ts +++ b/app/verticals/party-registry/tests/unit/merge-alias-resolution.test.ts @@ -1,6 +1,6 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { DateTime } from 'effect'; +import { expect, it } from 'effect-rstest'; + +import { DateTime, Predicate, Struct } from 'effect'; import type { PartyRef } from '../../shared/resources/party.ts'; import { assertCanonicalWriteTarget, @@ -26,52 +26,64 @@ const alias = (aliasPartyId: string, survivorPartyId: string, tenant = tenantId) survivorPartyRef: party(survivorPartyId, tenant), }); -test('resolves an historical alias chain to one final canonical Party', () => { +it('resolves an historical alias chain to one final canonical Party', () => { const result = resolveCanonicalPartyRef(party('party-b'), [ alias('party-b', 'party-a'), alias('party-a', 'party-c'), ]); - assert.deepEqual(result, { - _tag: 'CanonicalPartyResolved', + expect(Predicate.isTagged(result, 'CanonicalPartyResolved')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ canonicalPartyRef: party('party-c'), requestedAlias: party('party-b'), traversedAliasPartyRefs: [party('party-b'), party('party-a')], }); }); -test('rejects alias cycles, self aliases, and cross-tenant targets', () => { - assert.equal( - resolveCanonicalPartyRef(party('party-a'), [ - alias('party-a', 'party-b'), - alias('party-b', 'party-a'), - ])._tag, - 'PartyAliasCycleRejected', - ); - assert.equal( - resolveCanonicalPartyRef(party('party-a'), [alias('party-a', 'party-a')])._tag, - 'PartyAliasSelfReferenceRejected', - ); - assert.equal( - resolveCanonicalPartyRef(party('party-a'), [ - { - ...alias('party-a', 'party-b'), - survivorPartyRef: party('party-b', '22222222-2222-4222-8222-222222222222'), - }, - ])._tag, - 'PartyAliasCrossTenantRejected', - ); +it('rejects alias cycles, self aliases, and cross-tenant targets', () => { + expect( + Predicate.isTagged( + resolveCanonicalPartyRef(party('party-a'), [ + alias('party-a', 'party-b'), + alias('party-b', 'party-a'), + ]), + 'PartyAliasCycleRejected', + ), + ).toBe(true); + expect( + Predicate.isTagged( + resolveCanonicalPartyRef(party('party-a'), [alias('party-a', 'party-a')]), + 'PartyAliasSelfReferenceRejected', + ), + ).toBe(true); + expect( + Predicate.isTagged( + resolveCanonicalPartyRef(party('party-a'), [ + { + ...alias('party-a', 'party-b'), + survivorPartyRef: party('party-b', '22222222-2222-4222-8222-222222222222'), + }, + ]), + 'PartyAliasCrossTenantRejected', + ), + ).toBe(true); }); -test('rejects new writes addressed to an absorbed alias instead of forwarding them', () => { - assert.deepEqual(assertCanonicalWriteTarget(party('party-b'), [alias('party-b', 'party-a')]), { - _tag: 'AliasWriteRejected', +it('rejects new writes addressed to an absorbed alias instead of forwarding them', () => { + const aliasWriteRejection = assertCanonicalWriteTarget(party('party-b'), [ + alias('party-b', 'party-a'), + ]); + expect(Predicate.isTagged(aliasWriteRejection, 'AliasWriteRejected')).toBe(true); + expect(Struct.omit(aliasWriteRejection, ['_tag'])).toEqual({ aliasPartyRef: party('party-b'), canonicalPartyRef: party('party-a'), code: 'ALIAS_WRITE_FORBIDDEN', }); - assert.deepEqual(assertCanonicalWriteTarget(party('party-a'), [alias('party-b', 'party-a')]), { - _tag: 'CanonicalWriteTargetAccepted', + const canonicalWriteAcceptance = assertCanonicalWriteTarget(party('party-a'), [ + alias('party-b', 'party-a'), + ]); + expect(Predicate.isTagged(canonicalWriteAcceptance, 'CanonicalWriteTargetAccepted')).toBe(true); + expect(Struct.omit(canonicalWriteAcceptance, ['_tag'])).toEqual({ partyRef: party('party-a'), }); }); diff --git a/app/verticals/party-registry/tests/unit/merge-collision-reference.test.ts b/app/verticals/party-registry/tests/unit/merge-collision-reference.test.ts index 3b8a7b69f..191755fb6 100644 --- a/app/verticals/party-registry/tests/unit/merge-collision-reference.test.ts +++ b/app/verticals/party-registry/tests/unit/merge-collision-reference.test.ts @@ -1,6 +1,6 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { DateTime, Match } from 'effect'; +import { expect, it } from 'effect-rstest'; + +import { DateTime, Match, Struct, Predicate } from 'effect'; import type { PartyRef } from '../../shared/resources/party.ts'; import { analyzeMergeCollisions } from '../../src/merge/merge-collision-analysis.ts'; import { planReferencePreservation } from '../../src/merge/reference-preservation-plan.ts'; @@ -13,7 +13,7 @@ const party = (resourceId: string): PartyRef => ({ tenantId, }); -test('blocks authoritative non-strong identifier conflicts without unrelated relationship blockers', () => { +it('blocks authoritative non-strong identifier conflicts without unrelated relationship blockers', () => { const collisions = analyzeMergeCollisions({ absorbedPartyRefs: [party('party-b')], connectorCorrelations: [], @@ -41,13 +41,10 @@ test('blocks authoritative non-strong identifier conflicts without unrelated rel })), survivorPartyRef: party('party-a'), }); - assert.deepEqual( - collisions.map(({ code }) => code), - ['STRONG_IDENTIFIER_CONFLICT'], - ); + expect(collisions.map(({ code }) => code)).toEqual(['STRONG_IDENTIFIER_CONFLICT']); }); -test('requires reconciliation for Counterparty and consumer uniqueness collisions', () => { +it('requires reconciliation for Counterparty and consumer uniqueness collisions', () => { const collisions = analyzeMergeCollisions({ absorbedPartyRefs: [party('party-b')], connectorCorrelations: [ @@ -78,18 +75,15 @@ test('requires reconciliation for Counterparty and consumer uniqueness collision survivorPartyRef: party('party-a'), }); - assert.deepEqual( - collisions.map(({ code, ownerKey }) => ({ code, ownerKey })), - [ - { code: 'COUNTERPARTY_COLLISION', ownerKey: 'party.registry' }, - { code: 'CONSUMER_PROFILE_COLLISION', ownerKey: 'engagement' }, - { code: 'CONNECTOR_CORRELATION_COLLISION', ownerKey: 'erp' }, - ], - ); - assert.ok(collisions.every(({ resolution }) => resolution === 'RECONCILIATION_REQUIRED')); + expect(collisions.map(({ code, ownerKey }) => ({ code, ownerKey }))).toEqual([ + { code: 'COUNTERPARTY_COLLISION', ownerKey: 'party.registry' }, + { code: 'CONSUMER_PROFILE_COLLISION', ownerKey: 'engagement' }, + { code: 'CONNECTOR_CORRELATION_COLLISION', ownerKey: 'erp' }, + ]); + expect(collisions.every(({ resolution }) => resolution === 'RECONCILIATION_REQUIRED')).toBe(true); }); -test('blocks strong identifier conflicts and flags forbidden relationship and role overlaps', () => { +it('blocks strong identifier conflicts and flags forbidden relationship and role overlaps', () => { const collisions = analyzeMergeCollisions({ absorbedPartyRefs: [party('party-b')], connectorCorrelations: [], @@ -149,18 +143,18 @@ test('blocks strong identifier conflicts and flags forbidden relationship and ro survivorPartyRef: party('party-a'), }); - assert.deepEqual( - collisions.map(({ code, resolution }) => ({ code, resolution })), - [ - { code: 'STRONG_IDENTIFIER_CONFLICT', resolution: 'CORRECTION_REQUIRED' }, - { code: 'RELATIONSHIP_SELF_REFERENCE', resolution: 'RECONCILIATION_REQUIRED' }, - { code: 'COUNTERPARTY_ROLE_PERIOD_COLLISION', resolution: 'RECONCILIATION_REQUIRED' }, - ], - ); + expect(collisions.map(({ code, resolution }) => ({ code, resolution }))).toEqual([ + { code: 'STRONG_IDENTIFIER_CONFLICT', resolution: 'CORRECTION_REQUIRED' }, + { code: 'RELATIONSHIP_SELF_REFERENCE', resolution: 'RECONCILIATION_REQUIRED' }, + { code: 'COUNTERPARTY_ROLE_PERIOD_COLLISION', resolution: 'RECONCILIATION_REQUIRED' }, + ]); }); -test('plans canonical resolution for supported refs without rewriting historical snapshots', () => { - const snapshot = Object.freeze({ address: 'Historical street 1', name: 'Historical Party B' }); +it('plans canonical resolution for supported refs without rewriting historical snapshots', () => { + const snapshot = Object.freeze({ + address: 'Historical street 1', + name: 'Historical Party B', + }); const result = planReferencePreservation({ aliases: [ { @@ -212,18 +206,20 @@ test('plans canonical resolution for supported refs without rewriting historical const planned = Match.value(result).pipe( Match.tag('ReferencePreservationPlanned', (value) => value), Match.tag('ReferencePreservationBlocked', ({ blockers }) => - assert.fail(`Expected a reference plan, but planning was blocked: ${String(blockers)}`), + (() => { + throw new Error(`Expected a reference plan, but planning was blocked: ${String(blockers)}`); + })(), ), Match.exhaustive, ); - assert.ok( + expect( planned.references.every(({ canonicalPartyRef }) => canonicalPartyRef.resourceId === 'party-a'), - ); - assert.deepEqual(planned.references.at(-1)?.historicalSnapshot, snapshot); - assert.equal(planned.requiresPhysicalRewrite, false); + ).toBe(true); + expect(planned.references.at(-1)?.historicalSnapshot).toEqual(snapshot); + expect(planned.requiresPhysicalRewrite).toBe(false); }); -test('detects overlapping resolved relationship periods but permits adjacent role periods', () => { +it('detects overlapping resolved relationship periods but permits adjacent role periods', () => { const collisions = analyzeMergeCollisions({ absorbedPartyRefs: [party('party-b')], connectorCorrelations: [], @@ -271,7 +267,7 @@ test('detects overlapping resolved relationship periods but permits adjacent rol survivorPartyRef: party('party-a'), }); - assert.deepEqual(collisions, [ + expect(collisions).toEqual([ { code: 'RELATIONSHIP_PERIOD_COLLISION', ownerKey: 'party.registry', @@ -281,7 +277,7 @@ test('detects overlapping resolved relationship periods but permits adjacent rol ]); }); -test('blocks readiness for unsupported references and incomplete retry contracts', () => { +it('blocks readiness for unsupported references and incomplete retry contracts', () => { const result = planReferencePreservation({ aliases: [], consumerReconciliation: [ @@ -299,8 +295,8 @@ test('blocks readiness for unsupported references and incomplete retry contracts ], }); - assert.deepEqual(result, { - _tag: 'ReferencePreservationBlocked', + expect(Predicate.isTagged(result, 'ReferencePreservationBlocked')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ blockers: [ { code: 'UNSUPPORTED_REFERENCE_CLASS', ownerKey: 'custom-module' }, { code: 'CONSUMER_PARTIAL_RETRY_UNPROVEN', ownerKey: 'engagement' }, @@ -308,14 +304,14 @@ test('blocks readiness for unsupported references and incomplete retry contracts }); }); -test('blocks every external reference owner without reconciliation evidence', () => { +it('blocks every external reference owner without reconciliation evidence', () => { const result = planReferencePreservation({ aliases: [], references: [{ class: 'COMMERCE_PROFILE', ownerKey: 'commerce', partyRef: party('party-b') }], }); - assert.deepEqual(result, { - _tag: 'ReferencePreservationBlocked', + expect(Predicate.isTagged(result, 'ReferencePreservationBlocked')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ blockers: [{ code: 'CONSUMER_RECONCILIATION_UNPROVEN', ownerKey: 'commerce' }], }); }); diff --git a/app/verticals/party-registry/tests/unit/merge-readiness-contract.test.ts b/app/verticals/party-registry/tests/unit/merge-readiness-contract.test.ts index cffca29bb..90652f36f 100644 --- a/app/verticals/party-registry/tests/unit/merge-readiness-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/merge-readiness-contract.test.ts @@ -1,8 +1,7 @@ -// @effect-diagnostics nodeBuiltinImport:off -- Node's test runner reads source-only contracts; remove-when: manifests are importable without TSX loaders. -import assert from 'node:assert/strict'; +// @effect-diagnostics nodeBuiltinImport:off -- Source-only contract checks require reading TypeScript files; remove-when: manifests are importable without TSX loaders. +import { expect, it } from 'effect-rstest'; import { readFile } from 'node:fs/promises'; -import test from 'node:test'; -import { Match, Schema } from 'effect'; +import { Effect, Match, Schema, Struct, Predicate } from 'effect'; import { PartyMergeReadinessRequestSchema, PartyMergeReadinessResponseSchema, @@ -32,224 +31,237 @@ const party = (resourceId: string) => ({ tenantId, }); -test('publishes a tenant-governed read-only readiness contract that always reports execution disabled', () => { - const request = Schema.decodeUnknownSync(PartyMergeReadinessRequestSchema, { - onExcessProperty: 'error', - })({ - partyRefs: [party('party-a'), party('party-b')], - policyVersion: 'party-merge-readiness.v1', - }); - assert.deepEqual(request.partyRefs, [party('party-a'), party('party-b')]); - assert.throws(() => - Schema.decodeUnknownSync(PartyMergeReadinessRequestSchema)({ - partyRefs: [party('party-a'), party('party-a')], - policyVersion: 'party-merge-readiness.v1', - }), - ); - assert.throws(() => - Schema.decodeUnknownSync(PartyMergeReadinessRequestSchema)({ - partyRefs: [ - party('party-a'), - { ...party('party-b'), tenantId: '22222222-2222-4222-8222-222222222222' }, - ], - policyVersion: 'party-merge-readiness.v1', - }), - ); - assert.deepEqual(partyMergeReadinessRead.descriptor, { - accessKind: 'detail', - entrypoint: partyMergeReadinessRead.descriptor.entrypoint, - evidencePolicy: { - captureMode: 'metadata_only', - policyKey: 'party.registry.api.party-merge-readiness.evidence.v1', - }, - inputSchema: PartyMergeReadinessRequestSchema, - legalEntityScope: 'optional', - owningModuleKey: 'party.registry', - permissionTarget: 'tenant', - policies: [], - readKey: 'party.registry.api.party-merge-readiness', - resultSchema: PartyMergeReadinessResponseSchema, - schemaVersion: '1', - }); +it.effect( + 'publishes a tenant-governed read-only readiness contract that always reports execution disabled', + () => + Effect.gen(function* schemaContract1() { + const request = yield* Schema.decodeUnknownEffect(PartyMergeReadinessRequestSchema, { + onExcessProperty: 'error', + })({ + partyRefs: [party('party-a'), party('party-b')], + policyVersion: 'party-merge-readiness.v1', + }); + expect(request.partyRefs).toEqual([party('party-a'), party('party-b')]); + expect(() => + Schema.decodeUnknownSync(PartyMergeReadinessRequestSchema)({ + partyRefs: [party('party-a'), party('party-a')], + policyVersion: 'party-merge-readiness.v1', + }), + ).toThrow(); + expect(() => + Schema.decodeUnknownSync(PartyMergeReadinessRequestSchema)({ + partyRefs: [ + party('party-a'), + { ...party('party-b'), tenantId: '22222222-2222-4222-8222-222222222222' }, + ], + policyVersion: 'party-merge-readiness.v1', + }), + ).toThrow(); + expect(partyMergeReadinessRead.descriptor).toEqual({ + accessKind: 'detail', + entrypoint: partyMergeReadinessRead.descriptor.entrypoint, + evidencePolicy: { + captureMode: 'metadata_only', + policyKey: 'party.registry.api.party-merge-readiness.evidence.v1', + }, + inputSchema: PartyMergeReadinessRequestSchema, + legalEntityScope: 'optional', + owningModuleKey: 'party.registry', + permissionTarget: 'tenant', + policies: [], + readKey: 'party.registry.api.party-merge-readiness', + resultSchema: PartyMergeReadinessResponseSchema, + schemaVersion: '1', + }); - const rejection = rejectProductionMergeExecution(); - assert.deepEqual(rejection, { - _tag: 'ProductionMergeExecutionRejected', - code: 'PRODUCTION_MERGE_DISABLED', - detail: - 'Party Merge execution is disabled until consumer reconciliation and wrong-merge recovery are behaviorally proven.', - }); - const unavailable = evaluateDisabledMergeReadiness(request.partyRefs); - assert.equal(unavailable.mergeExecutionEnabled, false); - assert.deepEqual(unavailable.analysis, { - collisionCodes: [], - referencePlanStatus: 'PLANNED', - selectedSurvivorPartyRef: null, - selectionStatus: 'BLOCKED', - }); - assert.deepEqual( - unavailable.blockers.map(({ code }) => code), - [ - 'PRODUCTION_MERGE_DISABLED', - 'CONSUMER_RECONCILIATION_UNPROVEN', - 'WRONG_MERGE_RECOVERY_UNPROVEN', - 'DUPLICATE_SET_NOT_CONFIRMED', - 'PREPARED_STATE_UNAVAILABLE', - ], - ); -}); + const rejection = rejectProductionMergeExecution(); + expect(Predicate.isTagged(rejection, 'ProductionMergeExecutionRejected')).toBe(true); + expect(Struct.omit(rejection, ['_tag'])).toEqual({ + code: 'PRODUCTION_MERGE_DISABLED', + detail: + 'Party Merge execution is disabled until consumer reconciliation and wrong-merge recovery are behaviorally proven.', + }); + const unavailable = evaluateDisabledMergeReadiness(request.partyRefs); + expect(unavailable.mergeExecutionEnabled).toBe(false); + expect(unavailable.analysis).toEqual({ + collisionCodes: [], + referencePlanStatus: 'PLANNED', + selectedSurvivorPartyRef: null, + selectionStatus: 'BLOCKED', + }); + expect(unavailable.blockers.map(({ code }) => code)).toEqual([ + 'PRODUCTION_MERGE_DISABLED', + 'CONSUMER_RECONCILIATION_UNPROVEN', + 'WRONG_MERGE_RECOVERY_UNPROVEN', + 'DUPLICATE_SET_NOT_CONFIRMED', + 'PREPARED_STATE_UNAVAILABLE', + ]); + }), +); -test('keeps prepared merge and permanent alias schemas explainable without enabling execution', () => { - const selection = selectCanonicalSurvivor({ - candidates: ['party-a', 'party-b'].map((id, index) => ({ - authoritativeEvidenceRank: 2 - index, - blockingAuthoritativeConflict: false, - completenessRank: 1, - createdAt: '2024-01-01T00:00:00.000Z', - lifecycle: 'ACTIVE', - partyRef: party(id), - referenceStabilityRank: 1, - })), - confirmation: { - confirmedDuplicateDecisionId: 'decision-1', - confirmedPartyRefs: [party('party-a'), party('party-b')], - decisionActorPrincipalId: 'principal-1', - evidenceRefs: ['evidence-1'], - }, - }); - const selected = Match.value(selection).pipe( - Match.tag('CanonicalSurvivorSelected', (value) => value), - Match.tag('SurvivorSelectionBlocked', ({ blocker }) => - assert.fail(`Expected canonical survivor selection, but it was blocked: ${blocker}`), - ), - Match.exhaustive, - ); - const merge = Schema.decodeUnknownSync(PartyMergeSchema)({ - absorbedPartyRefs: [party('party-b')], - confirmedDuplicateDecisionId: 'decision-1', - createdAt: '2026-09-03T10:00:00.000Z', - decisionActorPrincipalId: 'principal-1', - mergeRef: { - moduleId: 'party.registry', - resourceId: 'merge-1', - resourceType: 'party.registry.party-merge', - tenantId, - }, - policyVersion: 'party-merge-readiness.v1', - selectionEvidenceChain: selected.evidenceChain, - selectionReason: 'AUTHORITATIVE_EVIDENCE', - state: 'PREPARED', - survivorPartyRef: party('party-a'), - }); - const alias = Schema.decodeUnknownSync(PartyAliasSchema)({ - aliasPartyRef: party('party-b'), - createdAt: '2026-09-03T10:00:00.000Z', - mergeRef: merge.mergeRef, - survivorPartyRef: party('party-a'), - }); +it.effect( + 'keeps prepared merge and permanent alias schemas explainable without enabling execution', + () => + Effect.gen(function* schemaContract2() { + const selection = selectCanonicalSurvivor({ + candidates: ['party-a', 'party-b'].map((id, index) => ({ + authoritativeEvidenceRank: 2 - index, + blockingAuthoritativeConflict: false, + completenessRank: 1, + createdAt: '2024-01-01T00:00:00.000Z', + lifecycle: 'ACTIVE', + partyRef: party(id), + referenceStabilityRank: 1, + })), + confirmation: { + confirmedDuplicateDecisionId: 'decision-1', + confirmedPartyRefs: [party('party-a'), party('party-b')], + decisionActorPrincipalId: 'principal-1', + evidenceRefs: ['evidence-1'], + }, + }); + const selected = Match.value(selection).pipe( + Match.tag('CanonicalSurvivorSelected', (value) => value), + Match.tag('SurvivorSelectionBlocked', ({ blocker }) => + ((message: string): never => { + throw new Error(message); + })(`Expected canonical survivor selection, but it was blocked: ${blocker}`), + ), + Match.exhaustive, + ); + const merge = yield* Schema.decodeUnknownEffect(PartyMergeSchema)({ + absorbedPartyRefs: [party('party-b')], + confirmedDuplicateDecisionId: 'decision-1', + createdAt: '2026-09-03T10:00:00.000Z', + decisionActorPrincipalId: 'principal-1', + mergeRef: { + moduleId: 'party.registry', + resourceId: 'merge-1', + resourceType: 'party.registry.party-merge', + tenantId, + }, + policyVersion: 'party-merge-readiness.v1', + selectionEvidenceChain: selected.evidenceChain, + selectionReason: 'AUTHORITATIVE_EVIDENCE', + state: 'PREPARED', + survivorPartyRef: party('party-a'), + }); + const alias = yield* Schema.decodeUnknownEffect(PartyAliasSchema)({ + aliasPartyRef: party('party-b'), + createdAt: '2026-09-03T10:00:00.000Z', + mergeRef: merge.mergeRef, + survivorPartyRef: party('party-a'), + }); - assert.equal(merge.state, 'PREPARED'); - assert.equal(merge.selectionReason, 'AUTHORITATIVE_EVIDENCE'); - assert.equal(merge.selectionEvidenceChain.length, 3); - assert.equal(merge.selectionEvidenceChain[2]?.candidateSnapshots[0]?.criterionValue, 2); - assert.throws(() => - Schema.decodeUnknownSync(PartyMergeSchema)({ - ...merge, - selectionEvidenceChain: merge.selectionEvidenceChain.map((step) => ({ - ...step, - candidateSnapshots: undefined, - })), - }), - ); - assert.deepEqual(alias.aliasPartyRef, party('party-b')); - assert.throws(() => - Schema.decodeUnknownSync(PartyMergeSchema)({ - ...merge, - absorbedPartyRefs: [party('party-a')], + expect(merge.state).toBe('PREPARED'); + expect(merge.selectionReason).toBe('AUTHORITATIVE_EVIDENCE'); + expect(merge.selectionEvidenceChain.length).toBe(3); + expect(merge.selectionEvidenceChain[2]?.candidateSnapshots[0]?.criterionValue).toBe(2); + expect(() => + Schema.decodeUnknownSync(PartyMergeSchema)({ + ...merge, + selectionEvidenceChain: merge.selectionEvidenceChain.map((step) => ({ + ...step, + candidateSnapshots: undefined, + })), + }), + ).toThrow(); + expect(alias.aliasPartyRef).toEqual(party('party-b')); + expect(() => + Schema.decodeUnknownSync(PartyMergeSchema)({ + ...merge, + absorbedPartyRefs: [party('party-a')], + }), + ).toThrow(); + expect(() => + Schema.decodeUnknownSync(PartyMergeSchema)({ + ...merge, + selectionReason: 'STABLE_RESOURCE_IDENTITY', + }), + ).toThrow(); + expect(() => + Schema.decodeUnknownSync(PartyAliasSchema)({ + ...alias, + survivorPartyRef: { + ...party('party-a'), + tenantId: '22222222-2222-4222-8222-222222222222', + }, + }), + ).toThrow(); + expect(partyMergeResourceDescriptor.capabilities.searchable).toBe(false); + expect(partyAliasResourceDescriptor.capabilities.searchable).toBe(false); }), - ); - assert.throws(() => - Schema.decodeUnknownSync(PartyMergeSchema)({ - ...merge, - selectionReason: 'STABLE_RESOURCE_IDENTITY', - }), - ); - assert.throws(() => - Schema.decodeUnknownSync(PartyAliasSchema)({ - ...alias, - survivorPartyRef: { - ...party('party-a'), - tenantId: '22222222-2222-4222-8222-222222222222', - }, - }), - ); - assert.equal(partyMergeResourceDescriptor.capabilities.searchable, false); - assert.equal(partyAliasResourceDescriptor.capabilities.searchable, false); -}); +); -test('has no registered Party Merge Action, event, outbox consumer, or write endpoint', () => - Promise.all([ - readFile(new URL('../../vertical.manifest.ts', import.meta.url), 'utf-8'), - readFile(new URL('../../vertical.registration.ts', import.meta.url), 'utf-8'), - ]).then(([manifestSource, registrationSource]) => { - assert.doesNotMatch(manifestSource, /merge[^\n]*Action|Action[^\n]*merge/iu); - assert.doesNotMatch(registrationSource, /merge[^\n]*Action|Action[^\n]*merge/iu); - assert.match(registrationSource, /'party-merge-readiness'/u); - const endpoints = Object.values(partyRegistryApi.groups).flatMap((group) => - Object.values(group.endpoints), - ); - assert.ok(Object.keys(partyRegistryApi.groups.partyCommands.endpoints).length > 0); - assert.deepEqual( - endpoints.filter(({ path }) => /merge/iu.test(path)).map(({ path }) => path), - ['/reads/party-merge-readiness'], - ); - })); +it.effect('has no registered Party Merge Action, event, outbox consumer, or write endpoint', () => + Effect.map( + Effect.all([ + Effect.promise(() => + readFile(new URL('../../vertical.manifest.ts', import.meta.url), 'utf-8'), + ), + Effect.promise(() => + readFile(new URL('../../vertical.registration.ts', import.meta.url), 'utf-8'), + ), + ]), + ([manifestSource, registrationSource]) => { + expect(manifestSource).not.toMatch(/merge[^\n]*Action|Action[^\n]*merge/iu); + expect(registrationSource).not.toMatch(/merge[^\n]*Action|Action[^\n]*merge/iu); + expect(registrationSource).toMatch(/'party-merge-readiness'/u); + const endpoints = Object.values(partyRegistryApi.groups).flatMap((group) => + Object.values(group.endpoints), + ); + expect(Object.keys(partyRegistryApi.groups.partyCommands.endpoints).length > 0).toBe(true); + expect(endpoints.filter(({ path }) => /merge/iu.test(path)).map(({ path }) => path)).toEqual([ + '/reads/party-merge-readiness', + ]); + }, + ), +); -test('serves the generated OntOS module contract before i18n redirects in development', () => - readFile(new URL('../../modern.config.ts', import.meta.url), 'utf-8').then( +it.effect('serves the generated OntOS module contract before i18n redirects in development', () => + Effect.map( + Effect.promise(() => readFile(new URL('../../modern.config.ts', import.meta.url), 'utf-8')), (modernConfigSource) => { - assert.match( - modernConfigSource, + expect(modernConfigSource).toMatch( /new URL\('\.dev-public\/\.well-known\/ontos-module-manifest\.json', import\.meta\.url\)/u, ); - assert.match(modernConfigSource, /setupMiddlewares:/u); - assert.match( - modernConfigSource, + expect(modernConfigSource).toMatch(/setupMiddlewares:/u); + expect(modernConfigSource).toMatch( /request\.url\?\.split\('\?', 1\)\[0\] !== '\/\.well-known\/ontos-module-manifest\.json'/u, ); - assert.match( - modernConfigSource, + expect(modernConfigSource).toMatch( /response\.setHeader\('Content-Type', 'application\/json'\)/u, ); - assert.match(modernConfigSource, /ignoreRedirectRoutes: \[\s*'\/\.well-known'/u); - assert.match( - modernConfigSource, + expect(modernConfigSource).toMatch(/ignoreRedirectRoutes: \[\s*'\/\.well-known'/u); + expect(modernConfigSource).toMatch( /publicDir: \['\.\/locales', '\.\/assets', '\.\/\.dev-public'\]/u, ); }, - )); + ), +); -test('readiness response schema cannot claim production merge is enabled', () => { - assert.deepEqual( - Schema.decodeUnknownSync(PartyMergeReadinessResponseSchema)({ - analysis: { - collisionCodes: [], - referencePlanStatus: 'BLOCKED', - selectedSurvivorPartyRef: null, - selectionStatus: 'BLOCKED', - }, - blockers: [ - { - code: 'PRODUCTION_MERGE_DISABLED', - detail: 'Production merge is disabled.', - ownerKey: 'party.registry', +it.effect('readiness response schema cannot claim production merge is enabled', () => + Effect.gen(function* schemaContract3() { + expect( + yield* Schema.decodeUnknownEffect(PartyMergeReadinessResponseSchema)({ + analysis: { + collisionCodes: [], + referencePlanStatus: 'BLOCKED', + selectedSurvivorPartyRef: null, + selectionStatus: 'BLOCKED', }, - ], - mergeExecutionEnabled: false, - partyRefs: [party('party-a'), party('party-b')], - status: 'DISABLED', - }), - { + blockers: [ + { + code: 'PRODUCTION_MERGE_DISABLED', + detail: 'Production merge is disabled.', + ownerKey: 'party.registry', + }, + ], + mergeExecutionEnabled: false, + partyRefs: [party('party-a'), party('party-b')], + status: 'DISABLED', + }), + ).toEqual({ analysis: { collisionCodes: [], referencePlanStatus: 'BLOCKED', @@ -266,25 +278,25 @@ test('readiness response schema cannot claim production merge is enabled', () => mergeExecutionEnabled: false, partyRefs: [party('party-a'), party('party-b')], status: 'DISABLED', - }, - ); - assert.throws(() => - Schema.decodeUnknownSync(PartyMergeReadinessResponseSchema)({ - analysis: { - collisionCodes: [], - referencePlanStatus: 'PLANNED', - selectedSurvivorPartyRef: party('party-a'), - selectionStatus: 'SELECTED', - }, - blockers: [], - mergeExecutionEnabled: true, - partyRefs: [party('party-a'), party('party-b')], - status: 'READY', - }), - ); -}); + }); + expect(() => + Schema.decodeUnknownSync(PartyMergeReadinessResponseSchema)({ + analysis: { + collisionCodes: [], + referencePlanStatus: 'PLANNED', + selectedSurvivorPartyRef: party('party-a'), + selectionStatus: 'SELECTED', + }, + blockers: [], + mergeExecutionEnabled: true, + partyRefs: [party('party-a'), party('party-b')], + status: 'READY', + }), + ).toThrow(); + }), +); -test('readiness invokes survivor, collision, and reference analyzers while remaining disabled', () => { +it('readiness invokes survivor, collision, and reference analyzers while remaining disabled', () => { const result = analyzePreparedMergeReadiness({ aliases: [], collisionInput: { @@ -332,14 +344,16 @@ test('readiness invokes survivor, collision, and reference analyzers while remai }, }); - assert.equal(result.status, 'DISABLED'); - assert.equal(result.mergeExecutionEnabled, false); - assert.deepEqual(result.analysis, { + expect(result.status).toBe('DISABLED'); + expect(result.mergeExecutionEnabled).toBe(false); + expect(result.analysis).toEqual({ collisionCodes: ['COUNTERPARTY_COLLISION'], referencePlanStatus: 'BLOCKED', selectedSurvivorPartyRef: party('party-a'), selectionStatus: 'SELECTED', }); - assert.ok(result.blockers.some(({ code }) => code === 'COUNTERPARTY_COLLISION')); - assert.ok(result.blockers.some(({ code }) => code === 'CONSUMER_RECONCILIATION_UNPROVEN')); + expect(result.blockers.some(({ code }) => code === 'COUNTERPARTY_COLLISION')).toBe(true); + expect(result.blockers.some(({ code }) => code === 'CONSUMER_RECONCILIATION_UNPROVEN')).toBe( + true, + ); }); diff --git a/app/verticals/party-registry/tests/unit/merge-survivor-selection.test.ts b/app/verticals/party-registry/tests/unit/merge-survivor-selection.test.ts index 0f497502d..2d7003968 100644 --- a/app/verticals/party-registry/tests/unit/merge-survivor-selection.test.ts +++ b/app/verticals/party-registry/tests/unit/merge-survivor-selection.test.ts @@ -1,6 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Match } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Match, Predicate, Struct } from 'effect'; import type { PartyRef } from '../../shared/resources/party.ts'; import type { MergeSurvivorCandidate, @@ -46,12 +45,14 @@ const expectSelected = (result: CanonicalSurvivorSelection) => Match.value(result).pipe( Match.tag('CanonicalSurvivorSelected', (selected) => selected), Match.tag('SurvivorSelectionBlocked', ({ blocker }) => - assert.fail(`Expected a canonical survivor, but selection was blocked by ${blocker}`), + ((message: string): never => { + throw new Error(message); + })(`Expected a canonical survivor, but selection was blocked by ${blocker}`), ), Match.exhaustive, ); -test('blocks survivor selection when authoritative identity truth is unresolved', () => { +it('blocks survivor selection when authoritative identity truth is unresolved', () => { const result = selectCanonicalSurvivor( confirmedSelection([ candidate('party-a'), @@ -59,14 +60,14 @@ test('blocks survivor selection when authoritative identity truth is unresolved' ]), ); - assert.deepEqual(result, { - _tag: 'SurvivorSelectionBlocked', + expect(Predicate.isTagged(result, 'SurvivorSelectionBlocked')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ blocker: 'AUTHORITATIVE_IDENTITY_CONFLICT', conflictingPartyRefs: [party('party-b')], }); }); -test('uses the governed hierarchy before reference count, lifecycle, completeness, or age', () => { +it('uses the governed hierarchy before reference count, lifecycle, completeness, or age', () => { const result = selectCanonicalSurvivor( confirmedSelection([ candidate('well-established', { @@ -84,36 +85,37 @@ test('uses the governed hierarchy before reference count, lifecycle, completenes ]), ); - assert.equal(result._tag, 'CanonicalSurvivorSelected'); + expect(Predicate.isTagged(result, 'CanonicalSurvivorSelected')).toBe(true); const selected = expectSelected(result); - assert.deepEqual(selected.survivorPartyRef, party('authoritative')); - assert.equal(selected.decidingCriterion, 'AUTHORITATIVE_EVIDENCE'); - assert.equal(selected.policyVersion, 'party-merge-survivor-selection.v1'); - assert.equal(selected.confirmedDuplicateDecisionId, 'decision-1'); - assert.deepEqual( - selected.evidenceChain.map(({ criterion }) => criterion), - ['CONFIRMED_DUPLICATE_SET', 'IDENTITY_SAFETY', 'AUTHORITATIVE_EVIDENCE'], - ); + expect(selected.survivorPartyRef).toEqual(party('authoritative')); + expect(selected.decidingCriterion).toBe('AUTHORITATIVE_EVIDENCE'); + expect(selected.policyVersion).toBe('party-merge-survivor-selection.v1'); + expect(selected.confirmedDuplicateDecisionId).toBe('decision-1'); + expect(selected.evidenceChain.map(({ criterion }) => criterion)).toEqual([ + 'CONFIRMED_DUPLICATE_SET', + 'IDENTITY_SAFETY', + 'AUTHORITATIVE_EVIDENCE', + ]); }); -test('uses reference stability, lifecycle, completeness, age, then resource identity deterministically', () => { +it('uses reference stability, lifecycle, completeness, age, then resource identity deterministically', () => { const referenceWinner = selectCanonicalSurvivor( confirmedSelection([ candidate('a', { referenceStabilityRank: 1 }), candidate('b', { referenceStabilityRank: 2 }), ]), ); - assert.equal(expectSelected(referenceWinner).decidingCriterion, 'REFERENCE_STABILITY'); + expect(expectSelected(referenceWinner).decidingCriterion).toBe('REFERENCE_STABILITY'); const deterministic = selectCanonicalSurvivor( confirmedSelection([candidate('party-b'), candidate('party-a')]), ); const selected = expectSelected(deterministic); - assert.deepEqual(selected.survivorPartyRef, party('party-a')); - assert.equal(selected.decidingCriterion, 'STABLE_RESOURCE_IDENTITY'); + expect(selected.survivorPartyRef).toEqual(party('party-a')); + expect(selected.decidingCriterion).toBe('STABLE_RESOURCE_IDENTITY'); }); -test('rejects a cross-tenant merge set before selection', () => { +it('rejects a cross-tenant merge set before selection', () => { const result = selectCanonicalSurvivor( confirmedSelection([ candidate('party-a'), @@ -123,8 +125,8 @@ test('rejects a cross-tenant merge set before selection', () => { ]), ); - assert.deepEqual(result, { - _tag: 'SurvivorSelectionBlocked', + expect(Predicate.isTagged(result, 'SurvivorSelectionBlocked')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ blocker: 'CROSS_TENANT_MERGE_SET', conflictingPartyRefs: [ party('party-a'), @@ -133,35 +135,38 @@ test('rejects a cross-tenant merge set before selection', () => { }); }); -test('rejects selection without an explicit confirmed duplicate decision and matching evidence set', () => { +it('rejects selection without an explicit confirmed duplicate decision and matching evidence set', () => { const candidates = [candidate('party-a'), candidate('party-b')]; - assert.deepEqual(selectCanonicalSurvivor({ candidates, confirmation: null }), { - _tag: 'SurvivorSelectionBlocked', + const unconfirmedSelection = selectCanonicalSurvivor({ candidates, confirmation: null }); + expect(Predicate.isTagged(unconfirmedSelection, 'SurvivorSelectionBlocked')).toBe(true); + expect(Struct.omit(unconfirmedSelection, ['_tag'])).toEqual({ blocker: 'DUPLICATE_SET_NOT_CONFIRMED', conflictingPartyRefs: [party('party-a'), party('party-b')], }); - assert.equal( - selectCanonicalSurvivor({ - candidates, - confirmation: { - confirmedDuplicateDecisionId: ConfirmedDuplicateDecisionIdSchema.make('decision-1'), - confirmedPartyRefs: [party('party-a')], - decisionActorPrincipalId: DecisionActorPrincipalIdSchema.make('principal-1'), - evidenceRefs: ['evidence-1'], - }, - })._tag, - 'SurvivorSelectionBlocked', - ); + expect( + Predicate.isTagged( + selectCanonicalSurvivor({ + candidates, + confirmation: { + confirmedDuplicateDecisionId: ConfirmedDuplicateDecisionIdSchema.make('decision-1'), + confirmedPartyRefs: [party('party-a')], + decisionActorPrincipalId: DecisionActorPrincipalIdSchema.make('principal-1'), + evidenceRefs: ['evidence-1'], + }, + }), + 'SurvivorSelectionBlocked', + ), + ).toBe(true); }); -test('retains immutable evaluated values and explains progressive elimination for three candidates', () => { +it('retains immutable evaluated values and explains progressive elimination for three candidates', () => { const candidates = [ candidate('party-a', { authoritativeEvidenceRank: 3, referenceStabilityRank: 2 }), candidate('party-b', { authoritativeEvidenceRank: 3, referenceStabilityRank: 1 }), candidate('party-c', { authoritativeEvidenceRank: 1, referenceStabilityRank: 100 }), ]; const result = selectCanonicalSurvivor(confirmedSelection(candidates)); - assert.equal(result._tag, 'CanonicalSurvivorSelected'); + expect(Predicate.isTagged(result, 'CanonicalSurvivorSelected')).toBe(true); const selected = expectSelected(result); const authority = selected.evidenceChain.find( ({ criterion }) => criterion === 'AUTHORITATIVE_EVIDENCE', @@ -169,45 +174,55 @@ test('retains immutable evaluated values and explains progressive elimination fo const stability = selected.evidenceChain.find( ({ criterion }) => criterion === 'REFERENCE_STABILITY', ); - assert.ok(authority); - assert.ok(stability); - assert.match(authority.explanation, /2 of 3 eligible candidates remain/u); - assert.equal(selected.decidingCriterion, 'REFERENCE_STABILITY'); - assert.deepEqual( + expect(authority).toBeDefined(); + if (authority === undefined) { + throw new Error('Expected authority'); + } + expect(stability).toBeDefined(); + if (stability === undefined) { + throw new Error('Expected stability'); + } + expect(authority.explanation).toMatch(/2 of 3 eligible candidates remain/u); + expect(selected.decidingCriterion).toBe('REFERENCE_STABILITY'); + expect( authority.candidateSnapshots.map(({ candidate: snapshot, criterionValue, retainedAfter }) => ({ criterionValue, id: snapshot.partyRef.resourceId, retainedAfter, })), - [ - { criterionValue: 3, id: 'party-a', retainedAfter: true }, - { criterionValue: 3, id: 'party-b', retainedAfter: true }, - { criterionValue: 1, id: 'party-c', retainedAfter: false }, - ], - ); - assert.deepEqual( + ).toEqual([ + { criterionValue: 3, id: 'party-a', retainedAfter: true }, + { criterionValue: 3, id: 'party-b', retainedAfter: true }, + { criterionValue: 1, id: 'party-c', retainedAfter: false }, + ]); + expect( stability.candidateSnapshots.map(({ eligibleBefore, retainedAfter }) => ({ eligibleBefore, retainedAfter, })), - [ - { eligibleBefore: true, retainedAfter: true }, - { eligibleBefore: true, retainedAfter: false }, - { eligibleBefore: false, retainedAfter: false }, - ], - ); + ).toEqual([ + { eligibleBefore: true, retainedAfter: true }, + { eligibleBefore: true, retainedAfter: false }, + { eligibleBefore: false, retainedAfter: false }, + ]); const [saved] = authority.candidateSnapshots; const [original] = candidates; - assert.ok(saved); - assert.ok(original); - assert.deepEqual(saved.candidate, original); + expect(saved).toBeDefined(); + if (saved === undefined) { + throw new Error('Expected saved'); + } + expect(original).toBeDefined(); + if (original === undefined) { + throw new Error('Expected original'); + } + expect(saved.candidate).toEqual(original); Object.assign(original, { authoritativeEvidenceRank: 999, createdAt: '2030-01-01T00:00:00.000Z', }); - assert.equal(saved.candidate.authoritativeEvidenceRank, 3); - assert.equal(saved.candidate.createdAt, '2024-01-01T00:00:00.000Z'); - assert.ok(Object.isFrozen(saved.candidate)); - assert.ok(Object.isFrozen(saved.candidate.partyRef)); - assert.ok(Object.isFrozen(authority.candidateSnapshots)); + expect(saved.candidate.authoritativeEvidenceRank).toBe(3); + expect(saved.candidate.createdAt).toBe('2024-01-01T00:00:00.000Z'); + expect(Object.isFrozen(saved.candidate)).toBe(true); + expect(Object.isFrozen(saved.candidate.partyRef)).toBe(true); + expect(Object.isFrozen(authority.candidateSnapshots)).toBe(true); }); diff --git a/app/verticals/party-registry/tests/unit/party-search-worker.test.ts b/app/verticals/party-registry/tests/unit/party-search-worker.test.ts index 103ba823f..7570a4cc9 100644 --- a/app/verticals/party-registry/tests/unit/party-search-worker.test.ts +++ b/app/verticals/party-registry/tests/unit/party-search-worker.test.ts @@ -1,8 +1,6 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { assert, it } from 'effect-rstest'; import { defineTenantModuleEntrypoint } from '@app/core-runtime'; import type { OutboxWorkerHandlerContext } from '@app/core-runtime'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import { Effect, Schema } from 'effect'; import { PartySearchProjectionUnavailable } from '../../shared/domain/search-projection-error.ts'; import { PartySearchProjector } from '../../src/services/party-search-projection.service.ts'; @@ -31,8 +29,9 @@ const entrypoint = defineTenantModuleEntrypoint({ }); for (const targetField of ['partyId', 'counterpartyId'] as const) { - test(`search worker forwards ${targetField}, trusted context, and typed retryable failure`, () => - runEffectTestPromise( + it.effect( + `search worker forwards ${targetField}, trusted context, and typed retryable failure`, + () => Effect.gen(function* forwardsSearchProjection() { const { handle, worker } = definePartySearchWorker( { @@ -77,7 +76,10 @@ for (const targetField of ['partyId', 'counterpartyId'] as const) { project: (receivedContext, target) => { calls += 1; assert.equal(receivedContext, context); - assert.deepEqual(target, { [targetField]: 'target' }); + assert.deepEqual( + target, + targetField === 'partyId' ? { partyId: 'target' } : { counterpartyId: 'target' }, + ); return Effect.fail(failure); }, }), @@ -86,5 +88,5 @@ for (const targetField of ['partyId', 'counterpartyId'] as const) { assert.equal(result, failure); assert.equal(calls, 1); }), - )); + ); } diff --git a/app/verticals/party-registry/tests/unit/prepare-contacts-migration.test.ts b/app/verticals/party-registry/tests/unit/prepare-contacts-migration.test.ts index 9accf91a2..eb062b151 100644 --- a/app/verticals/party-registry/tests/unit/prepare-contacts-migration.test.ts +++ b/app/verticals/party-registry/tests/unit/prepare-contacts-migration.test.ts @@ -1,6 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { Effect } from 'effect'; import { Client } from 'pg'; import { @@ -20,6 +18,8 @@ const journalClient = ( ): JournalClientFixture => { const queries: string[] = []; const client = new Client(); + // Accepted foreign API fixture: pg Client.query returns Promises, consumed by the + // production Effect.tryPromise boundary; this is not an Effect test helper. Object.defineProperty(client, 'query', { value: (query: string) => { queries.push(query); @@ -35,71 +35,62 @@ const journalClient = ( return { client, queries }; }; -test('classifies fresh, legacy, migrated, and ambiguous Contacts journal states', () => { - assert.equal(classifyContactsJournalState(false, false), 'fresh'); - assert.equal(classifyContactsJournalState(true, false), 'legacy'); - assert.equal(classifyContactsJournalState(false, true), 'contacts'); - assert.equal(classifyContactsJournalState(true, true), 'ambiguous'); +it('classifies fresh, legacy, migrated, and ambiguous Contacts journal states', () => { + expect(classifyContactsJournalState(false, false)).toBe('fresh'); + expect(classifyContactsJournalState(true, false)).toBe('legacy'); + expect(classifyContactsJournalState(false, true)).toBe('contacts'); + expect(classifyContactsJournalState(true, true)).toBe('ambiguous'); }); -test('atomically renames the legacy journal before the Contacts migration chain', () => - runEffectTestPromise( - Effect.gen(function* atomicallyRenamesLegacyJournal() { - const fixture = journalClient(true, false); +it.effect('atomically renames the legacy journal before the Contacts migration chain', () => + Effect.gen(function* atomicallyRenamesLegacyJournal() { + const fixture = journalClient(true, false); - assert.equal(yield* prepareContactsMigration(fixture.client), 'legacy'); - assert.deepEqual(fixture.queries, [ - 'begin', - `select + expect(yield* prepareContactsMigration(fixture.client)).toBe('legacy'); + expect(fixture.queries).toEqual([ + 'begin', + `select to_regclass('drizzle.__drizzle_migrations_crm') is not null as legacy, to_regclass('drizzle.__drizzle_migrations_contacts') is not null as contacts`, - 'alter table drizzle.__drizzle_migrations_crm rename to __drizzle_migrations_contacts', - 'commit', - ]); - }), - )); + 'alter table drizzle.__drizzle_migrations_crm rename to __drizzle_migrations_contacts', + 'commit', + ]); + }), +); -test('fresh and already-migrated journal states are committed no-ops', () => - runEffectTestPromise( - Effect.forEach( - [ - [false, false, 'fresh'], - [false, true, 'contacts'], - ] as const, - ([legacy, contacts, expected]) => - Effect.gen(function* commitsJournalStateNoOp() { - const fixture = journalClient(legacy, contacts); - assert.equal(yield* prepareContactsMigration(fixture.client), expected); - assert.equal(fixture.queries[0], 'begin'); - assert.equal(fixture.queries.at(-1), 'commit'); - assert.equal( - fixture.queries.some((query) => query.startsWith('alter table')), - false, - ); - }), - { concurrency: 'unbounded', discard: true }, - ), - )); +it.effect('fresh and already-migrated journal states are committed no-ops', () => + Effect.forEach( + [ + [false, false, 'fresh'], + [false, true, 'contacts'], + ] as const, + ([legacy, contacts, expected]) => + Effect.gen(function* commitsJournalStateNoOp() { + const fixture = journalClient(legacy, contacts); + expect(yield* prepareContactsMigration(fixture.client)).toBe(expected); + expect(fixture.queries[0]).toBe('begin'); + expect(fixture.queries.at(-1)).toBe('commit'); + expect(fixture.queries.some((query) => query.startsWith('alter table'))).toBe(false); + }), + { concurrency: 'unbounded', discard: true }, + ), +); -test('ambiguous or failed journal handoff rolls back without claiming success', () => - runEffectTestPromise( - Effect.gen(function* rollsBackFailedJournalHandoff() { - const ambiguous = journalClient(true, true); - const ambiguousFailure = yield* Effect.flip(prepareContactsMigration(ambiguous.client)); - assert.match(ambiguousFailure.message, /both CRM and Contacts journals exist/u); - assert.equal(ambiguousFailure.cause, 'ambiguous'); - assert.equal(ambiguous.queries.at(-1), 'rollback'); - assert.equal( - ambiguous.queries.some((query) => query.startsWith('alter table')), - false, - ); +it.effect('ambiguous or failed journal handoff rolls back without claiming success', () => + Effect.gen(function* rollsBackFailedJournalHandoff() { + const ambiguous = journalClient(true, true); + const ambiguousFailure = yield* Effect.flip(prepareContactsMigration(ambiguous.client)); + expect(ambiguousFailure.message).toMatch(/both CRM and Contacts journals exist/u); + expect(ambiguousFailure.cause).toBe('ambiguous'); + expect(ambiguous.queries.at(-1)).toBe('rollback'); + expect(ambiguous.queries.some((query) => query.startsWith('alter table'))).toBe(false); - const renameError = new Error('rename failed'); - const renameFailure = journalClient(true, false, renameError); - const failure = yield* Effect.flip(prepareContactsMigration(renameFailure.client)); - assert.match(failure.message, /PostgreSQL query failed/u); - assert.equal(failure.cause, renameError); - assert.equal(renameFailure.queries.at(-1), 'rollback'); - assert.equal(renameFailure.queries.includes('commit'), false); - }), - )); + const renameError = new Error('rename failed'); + const renameFailure = journalClient(true, false, renameError); + const failure = yield* Effect.flip(prepareContactsMigration(renameFailure.client)); + expect(failure.message).toMatch(/PostgreSQL query failed/u); + expect(failure.cause).toBe(renameError); + expect(renameFailure.queries.at(-1)).toBe('rollback'); + expect(renameFailure.queries.includes('commit')).toBe(false); + }), +); diff --git a/app/verticals/party-registry/tests/unit/read-outcome.test.ts b/app/verticals/party-registry/tests/unit/read-outcome.test.ts index 3a7b0d584..ed50daf38 100644 --- a/app/verticals/party-registry/tests/unit/read-outcome.test.ts +++ b/app/verticals/party-registry/tests/unit/read-outcome.test.ts @@ -1,29 +1,26 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import { Effect } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { assert, it } from 'effect-rstest'; import { readDetailResult, readUnavailable, requireReadValue } from '../../src/api/read-outcome.ts'; -test('detail lookup preserves the value and one-result evidence', () => { +it.effect('detail lookup preserves the value and one-result evidence', () => { const value = { revision: 7 }; - return runEffectTestPromise( - requireReadValue('Missing record')({ _tag: 'found', value }).pipe( - Effect.map(readDetailResult), - Effect.tap((output) => - Effect.sync(() => { - assert.equal(output.result, value); - assert.deepEqual(output.evidence, { resultCount: 1 }); - }), - ), - Effect.asVoid, + return requireReadValue('Missing record')({ _tag: 'found', value }).pipe( + Effect.map(readDetailResult), + Effect.tap((output) => + Effect.sync(() => { + assert.equal(output.result, value); + assert.deepEqual(output.evidence, { resultCount: 1 }); + }), ), + Effect.asVoid, ); }); -test('missing detail produces the caller-specific typed failure without result evidence', () => { - const reason = 'The Official Identifier does not exist'; - return runEffectTestPromise( - requireReadValue(reason)({ _tag: 'not_found' }).pipe( +it.effect( + 'missing detail produces the caller-specific typed failure without result evidence', + () => { + const reason = 'The Official Identifier does not exist'; + return requireReadValue(reason)({ _tag: 'not_found' }).pipe( Effect.map(() => assert.fail('Missing lookup must not succeed')), Effect.catchTag('ReadHandlerNotFound', (failure) => Effect.sync(() => { @@ -31,11 +28,11 @@ test('missing detail produces the caller-specific typed failure without result e assert.equal(failure.reason, reason); }), ), - ), - ); -}); + ); + }, +); -test('unavailable mapping retains hidden diagnostic cause and descriptor policy', () => { +it('unavailable mapping retains hidden diagnostic cause and descriptor policy', () => { const cause = { diagnostic: 'private' }; for (const configurable of [false, true]) { const failure = readUnavailable('Read storage unavailable', configurable)(cause); diff --git a/app/verticals/party-registry/tests/unit/relationship-domain-contract.test.ts b/app/verticals/party-registry/tests/unit/relationship-domain-contract.test.ts index 598c6369e..547bef2cb 100644 --- a/app/verticals/party-registry/tests/unit/relationship-domain-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/relationship-domain-contract.test.ts @@ -1,6 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { DateTime, Option, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, DateTime, Option, Schema, Predicate, Struct } from 'effect'; import { ContactPersonOfRelationshipType, CreatePartyRelationshipPayloadSchema, @@ -40,311 +39,313 @@ const instant = DateTime.makeUnsafe; const absentInstant = Option.none(); const presentInstant = (value: string) => Option.some(instant(value)); -test('the production catalog contains only CONTACT_PERSON_OF', () => { - assert.equal( - Schema.decodeUnknownSync(PartyRelationshipTypeSchema)('CONTACT_PERSON_OF'), - ContactPersonOfRelationshipType, - ); - for (const deferred of ['EMPLOYEE_OF', 'BRANCH_OF', 'OTHER']) { - assert.throws(() => Schema.decodeUnknownSync(PartyRelationshipTypeSchema)(deferred)); - } -}); +it.effect('the production catalog contains only CONTACT_PERSON_OF', () => + Effect.gen(function* schemaContract1() { + expect( + yield* Schema.decodeUnknownEffect(PartyRelationshipTypeSchema)('CONTACT_PERSON_OF'), + ).toBe(ContactPersonOfRelationshipType); + for (const deferred of ['EMPLOYEE_OF', 'BRANCH_OF', 'OTHER']) { + expect(() => Schema.decodeUnknownSync(PartyRelationshipTypeSchema)(deferred)).toThrow(); + } + }), +); -test('create accepts one provenance-backed PERSON to ORGANIZATION period shape', () => { - const payload = { - fromPartyRef, - provenance, - relationshipType: 'CONTACT_PERSON_OF', - toPartyRef, - validFrom: '2026-09-01T10:00:00.000Z', - validTo: null, - } as const; - const decoded = Schema.decodeUnknownSync(CreatePartyRelationshipPayloadSchema)(payload); - assert.equal(decoded.relationshipType, 'CONTACT_PERSON_OF'); - assert.throws(() => - Schema.decodeUnknownSync(CreatePartyRelationshipPayloadSchema)({ - ...payload, - toPartyRef: fromPartyRef, - }), - ); - assert.ok( - Option.isNone( +it.effect('create accepts one provenance-backed PERSON to ORGANIZATION period shape', () => + Effect.gen(function* schemaContract2() { + const payload = { + fromPartyRef, + provenance, + relationshipType: 'CONTACT_PERSON_OF', + toPartyRef, + validFrom: '2026-09-01T10:00:00.000Z', + validTo: null, + } as const; + const decoded = yield* Schema.decodeUnknownEffect(CreatePartyRelationshipPayloadSchema)( + payload, + ); + expect(decoded.relationshipType).toBe('CONTACT_PERSON_OF'); + expect(() => Schema.decodeUnknownSync(CreatePartyRelationshipPayloadSchema)({ ...payload, - validFrom: null, - }).validFrom, - ), - ); - assert.equal(DateTime.formatIso(Option.getOrThrow(decoded.validFrom)), payload.validFrom); - assert.throws(() => - Schema.decodeUnknownSync(CreatePartyRelationshipPayloadSchema)({ - ...payload, + toPartyRef: fromPartyRef, + }), + ).toThrow(); + expect( + Option.isNone( + (yield* Schema.decodeUnknownEffect(CreatePartyRelationshipPayloadSchema)({ + ...payload, + validFrom: null, + })).validFrom, + ), + ).toBe(true); + expect(DateTime.formatIso(Option.getOrThrow(decoded.validFrom))).toBe(payload.validFrom); + expect(() => + Schema.decodeUnknownSync(CreatePartyRelationshipPayloadSchema)({ + ...payload, + validTo: '2026-09-01T10:00:00.000Z', + }), + ).toThrow(); + expect(() => + Schema.decodeUnknownSync(CreatePartyRelationshipPayloadSchema)({ + ...payload, + validFrom: '2026-02-30T10:00:00.000Z', + }), + ).toThrow(); + expect(() => + Schema.decodeUnknownSync(CreatePartyRelationshipPayloadSchema)({ + ...payload, + validFrom: '2026-09-01T10:00:00Z', + }), + ).toThrow(); + }), +); + +it.effect('relationship timestamps and nullable periods preserve their JSON encoding', () => + Effect.gen(function* schemaContract3() { + const wire = { + fromPartyRef, + provenance, + relationshipType: 'CONTACT_PERSON_OF' as const, + toPartyRef, + validFrom: null, validTo: '2026-09-01T10:00:00.000Z', - }), - ); - assert.throws(() => - Schema.decodeUnknownSync(CreatePartyRelationshipPayloadSchema)({ - ...payload, - validFrom: '2026-02-30T10:00:00.000Z', - }), - ); - assert.throws(() => - Schema.decodeUnknownSync(CreatePartyRelationshipPayloadSchema)({ - ...payload, - validFrom: '2026-09-01T10:00:00Z', - }), - ); -}); + }; + const decoded = yield* Schema.decodeUnknownEffect(CreatePartyRelationshipPayloadSchema)({ + ...wire, + validTo: null, + }); + expect(yield* Schema.encodeEffect(CreatePartyRelationshipPayloadSchema)(decoded)).toEqual({ + ...wire, + validTo: null, + }); + const updated = yield* Schema.decodeUnknownEffect(UpdatePartyRelationshipPayloadSchema)({ + changeReason: 'Clarified end', + expectedRevision: 1, + provenance, + relationshipRef, + validTo: null, + }); + expect(updated.validTo !== undefined && Option.isNone(updated.validTo)).toBe(true); + expect(yield* Schema.encodeEffect(UpdatePartyRelationshipPayloadSchema)(updated)).toEqual({ + changeReason: 'Clarified end', + expectedRevision: 1, + provenance, + relationshipRef, + validTo: null, + }); + }), +); -test('relationship timestamps and nullable periods preserve their JSON encoding', () => { - const wire = { - fromPartyRef, - provenance, - relationshipType: 'CONTACT_PERSON_OF' as const, - toPartyRef, - validFrom: null, - validTo: '2026-09-01T10:00:00.000Z', - }; - const decoded = Schema.decodeUnknownSync(CreatePartyRelationshipPayloadSchema)({ - ...wire, - validTo: null, - }); - assert.deepEqual(Schema.encodeSync(CreatePartyRelationshipPayloadSchema)(decoded), { - ...wire, - validTo: null, - }); - const updated = Schema.decodeUnknownSync(UpdatePartyRelationshipPayloadSchema)({ - changeReason: 'Clarified end', - expectedRevision: 1, - provenance, - relationshipRef, - validTo: null, - }); - assert.ok(updated.validTo !== undefined && Option.isNone(updated.validTo)); - assert.deepEqual(Schema.encodeSync(UpdatePartyRelationshipPayloadSchema)(updated), { - changeReason: 'Clarified end', - expectedRevision: 1, - provenance, - relationshipRef, - validTo: null, - }); -}); +it.effect('update cannot accept endpoint or relationship type mutation fields', () => + Effect.gen(function* schemaContract4() { + const decoded = yield* Schema.decodeUnknownEffect(UpdatePartyRelationshipPayloadSchema, { + onExcessProperty: 'error', + })({ + changeReason: 'The planned assignment was extended', + expectedRevision: 2, + provenance, + relationshipRef, + validFrom: '2026-10-01T10:00:00.000Z', + validTo: '2026-12-01T10:00:00.000Z', + }); + expect(decoded.expectedRevision).toBe(2); + for (const forbiddenField of ['fromPartyRef', 'toPartyRef', 'relationshipType']) { + expect(() => + Schema.decodeUnknownSync(UpdatePartyRelationshipPayloadSchema, { + onExcessProperty: 'error', + })({ + changeReason: 'The planned assignment was extended', + expectedRevision: 2, + provenance, + relationshipRef, + validFrom: '2026-10-01T10:00:00.000Z', + validTo: '2026-12-01T10:00:00.000Z', + [forbiddenField]: fromPartyRef, + }), + ).toThrow(); + } + }), +); -test('update cannot accept endpoint or relationship type mutation fields', () => { - const decoded = Schema.decodeUnknownSync(UpdatePartyRelationshipPayloadSchema, { - onExcessProperty: 'error', - })({ - changeReason: 'The planned assignment was extended', - expectedRevision: 2, - provenance, - relationshipRef, - validFrom: '2026-10-01T10:00:00.000Z', - validTo: '2026-12-01T10:00:00.000Z', - }); - assert.equal(decoded.expectedRevision, 2); - for (const forbiddenField of ['fromPartyRef', 'toPartyRef', 'relationshipType']) { - assert.throws(() => - Schema.decodeUnknownSync(UpdatePartyRelationshipPayloadSchema, { - onExcessProperty: 'error', - })({ - changeReason: 'The planned assignment was extended', - expectedRevision: 2, +it.effect( + 'end requires effective time, provenance, and revision without inventing a generic reason', + () => + Effect.gen(function* schemaContract5() { + const decoded = yield* Schema.decodeUnknownEffect(EndPartyRelationshipPayloadSchema)({ + effectiveAt: '2026-09-02T10:00:00.000Z', + expectedRevision: 3, provenance, + reason: 'The person is no longer a contact', relationshipRef, - validFrom: '2026-10-01T10:00:00.000Z', - validTo: '2026-12-01T10:00:00.000Z', - [forbiddenField]: fromPartyRef, - }), - ); - } -}); - -test('end requires effective time, provenance, and revision without inventing a generic reason', () => { - const decoded = Schema.decodeUnknownSync(EndPartyRelationshipPayloadSchema)({ - effectiveAt: '2026-09-02T10:00:00.000Z', - expectedRevision: 3, - provenance, - reason: 'The person is no longer a contact', - relationshipRef, - }); - assert.equal(decoded.expectedRevision, 3); - assert.equal( - Schema.decodeUnknownSync(EndPartyRelationshipPayloadSchema)({ - effectiveAt: '2026-09-02T10:00:00.000Z', - expectedRevision: 3, - provenance, - relationshipRef, - }).reason, - undefined, - ); -}); + }); + expect(decoded.expectedRevision).toBe(3); + expect( + (yield* Schema.decodeUnknownEffect(EndPartyRelationshipPayloadSchema)({ + effectiveAt: '2026-09-02T10:00:00.000Z', + expectedRevision: 3, + provenance, + relationshipRef, + })).reason, + ).toBe(undefined); + }), +); -test('validity uses an exclusive end boundary', () => { - assert.equal( +it('validity uses an exclusive end boundary', () => { + expect( classifyRelationshipValidity(absentInstant, absentInstant, instant('2026-09-01T09:59:59.999Z')), - 'CURRENT', - ); - assert.equal( + ).toBe('CURRENT'); + expect( classifyRelationshipValidity( presentInstant('2026-09-01T10:00:00.000Z'), absentInstant, instant('2026-09-01T09:59:59.999Z'), ), - 'SCHEDULED', - ); - assert.equal( + ).toBe('SCHEDULED'); + expect( classifyRelationshipValidity( presentInstant('2026-09-01T10:00:00.000Z'), presentInstant('2026-09-02T10:00:00.000Z'), instant('2026-09-02T09:59:59.999Z'), ), - 'CURRENT', - ); - assert.equal( + ).toBe('CURRENT'); + expect( classifyRelationshipValidity( presentInstant('2026-09-01T10:00:00.000Z'), presentInstant('2026-09-02T10:00:00.000Z'), instant('2026-09-02T10:00:00.000Z'), ), - 'HISTORICAL', - ); + ).toBe('HISTORICAL'); }); -test('create reuses an exact period and conflicts on a distinct overlap', () => { +it('create reuses an exact period and conflicts on a distinct overlap', () => { const existing = { relationshipId: relationshipRef.resourceId, validFrom: presentInstant('2026-09-01T10:00:00.000Z'), validTo: presentInstant('2026-10-01T10:00:00.000Z'), } as const; - assert.deepEqual(decideRelationshipCreate([existing], { ...existing }), { - _tag: 'reuse', + const exactPeriodDecision = decideRelationshipCreate([existing], { ...existing }); + expect(Predicate.isTagged(exactPeriodDecision, 'reuse')).toBe(true); + expect(Struct.omit(exactPeriodDecision, ['_tag'])).toEqual({ relationshipId: relationshipRef.resourceId, }); - assert.deepEqual( - decideRelationshipCreate([existing], { - relationshipId: 'ignored', - validFrom: presentInstant('2026-09-15T10:00:00.000Z'), - validTo: absentInstant, - }), - { _tag: 'overlap', relationshipId: relationshipRef.resourceId }, - ); - assert.deepEqual( - decideRelationshipCreate([existing], { - relationshipId: 'ignored', - validFrom: presentInstant('2026-10-01T10:00:00.000Z'), - validTo: absentInstant, - }), - { _tag: 'create' }, - ); + const overlappingPeriodDecision = decideRelationshipCreate([existing], { + relationshipId: 'ignored', + validFrom: presentInstant('2026-09-15T10:00:00.000Z'), + validTo: absentInstant, + }); + expect(Predicate.isTagged(overlappingPeriodDecision, 'overlap')).toBe(true); + expect(Struct.omit(overlappingPeriodDecision, ['_tag'])).toEqual({ + relationshipId: relationshipRef.resourceId, + }); + const adjacentPeriodDecision = decideRelationshipCreate([existing], { + relationshipId: 'ignored', + validFrom: presentInstant('2026-10-01T10:00:00.000Z'), + validTo: absentInstant, + }); + expect(Predicate.isTagged(adjacentPeriodDecision, 'create')).toBe(true); + expect(Struct.omit(adjacentPeriodDecision, ['_tag'])).toEqual({}); }); -test('only a still-future validity plan is ordinarily updateable', () => { - assert.deepEqual( - decideRelationshipUpdate( - { - revision: 2, - validFrom: presentInstant('2026-01-01T00:00:00.000Z'), - validTo: presentInstant('2026-12-01T00:00:00.000Z'), - }, - { - expectedRevision: 2, - validFrom: undefined, - validTo: presentInstant('2027-01-01T00:00:00.000Z'), - }, - instant('2026-09-03T00:00:00.000Z'), - ), - { _tag: 'update' }, +it('only a still-future validity plan is ordinarily updateable', () => { + const futureEndUpdate = decideRelationshipUpdate( + { + revision: 2, + validFrom: presentInstant('2026-01-01T00:00:00.000Z'), + validTo: presentInstant('2026-12-01T00:00:00.000Z'), + }, + { + expectedRevision: 2, + validFrom: undefined, + validTo: presentInstant('2027-01-01T00:00:00.000Z'), + }, + instant('2026-09-03T00:00:00.000Z'), ); - assert.deepEqual( - decideRelationshipUpdate( - { - revision: 2, - validFrom: presentInstant('2026-01-01T00:00:00.000Z'), - validTo: presentInstant('2026-08-01T00:00:00.000Z'), - }, - { - expectedRevision: 2, - validFrom: undefined, - validTo: presentInstant('2027-01-01T00:00:00.000Z'), - }, - instant('2026-09-03T00:00:00.000Z'), - ), - { _tag: 'correction_required', fact: 'validTo' }, + expect(Predicate.isTagged(futureEndUpdate, 'update')).toBe(true); + expect(Struct.omit(futureEndUpdate, ['_tag'])).toEqual({}); + const historicalEndUpdate = decideRelationshipUpdate( + { + revision: 2, + validFrom: presentInstant('2026-01-01T00:00:00.000Z'), + validTo: presentInstant('2026-08-01T00:00:00.000Z'), + }, + { + expectedRevision: 2, + validFrom: undefined, + validTo: presentInstant('2027-01-01T00:00:00.000Z'), + }, + instant('2026-09-03T00:00:00.000Z'), ); - assert.deepEqual( - decideRelationshipUpdate( - { - revision: 2, - validFrom: presentInstant('2026-01-01T00:00:00.000Z'), - validTo: absentInstant, - }, - { - expectedRevision: 2, - validTo: presentInstant('2026-08-01T00:00:00.000Z'), - }, - instant('2026-09-03T00:00:00.000Z'), - ), - { _tag: 'end_required' }, + expect(Predicate.isTagged(historicalEndUpdate, 'correction_required')).toBe(true); + expect(Struct.omit(historicalEndUpdate, ['_tag'])).toEqual({ fact: 'validTo' }); + const pastEndUpdate = decideRelationshipUpdate( + { + revision: 2, + validFrom: presentInstant('2026-01-01T00:00:00.000Z'), + validTo: absentInstant, + }, + { + expectedRevision: 2, + validTo: presentInstant('2026-08-01T00:00:00.000Z'), + }, + instant('2026-09-03T00:00:00.000Z'), ); - assert.deepEqual( - decideRelationshipUpdate( - { - revision: 2, - validFrom: presentInstant('2026-01-01T00:00:00.000Z'), - validTo: absentInstant, - }, - { expectedRevision: 1, validFrom: undefined, validTo: absentInstant }, - instant('2026-09-03T00:00:00.000Z'), - ), - { _tag: 'revision_conflict', actualRevision: 2 }, + expect(Predicate.isTagged(pastEndUpdate, 'end_required')).toBe(true); + expect(Struct.omit(pastEndUpdate, ['_tag'])).toEqual({}); + const staleRevisionUpdate = decideRelationshipUpdate( + { + revision: 2, + validFrom: presentInstant('2026-01-01T00:00:00.000Z'), + validTo: absentInstant, + }, + { expectedRevision: 1, validFrom: undefined, validTo: absentInstant }, + instant('2026-09-03T00:00:00.000Z'), ); - assert.deepEqual( - decideRelationshipUpdate( - { revision: 2, validFrom: absentInstant, validTo: absentInstant }, - { - expectedRevision: 2, - validFrom: instant('2025-01-01T00:00:00.000Z'), - validTo: undefined, - }, - instant('2026-09-03T00:00:00.000Z'), - ), - { _tag: 'update' }, + expect(Predicate.isTagged(staleRevisionUpdate, 'revision_conflict')).toBe(true); + expect(Struct.omit(staleRevisionUpdate, ['_tag'])).toEqual({ actualRevision: 2 }); + const unknownStartUpdate = decideRelationshipUpdate( + { revision: 2, validFrom: absentInstant, validTo: absentInstant }, + { + expectedRevision: 2, + validFrom: instant('2025-01-01T00:00:00.000Z'), + validTo: undefined, + }, + instant('2026-09-03T00:00:00.000Z'), ); - assert.deepEqual( - decideRelationshipUpdate( - { - revision: 2, - validFrom: presentInstant('2027-01-01T00:00:00.000Z'), - validTo: absentInstant, - }, - { - expectedRevision: 2, - validFrom: instant('2027-02-01T00:00:00.000Z'), - validTo: undefined, - }, - instant('2026-09-03T00:00:00.000Z'), - ), - { _tag: 'update' }, + expect(Predicate.isTagged(unknownStartUpdate, 'update')).toBe(true); + expect(Struct.omit(unknownStartUpdate, ['_tag'])).toEqual({}); + const futureStartUpdate = decideRelationshipUpdate( + { + revision: 2, + validFrom: presentInstant('2027-01-01T00:00:00.000Z'), + validTo: absentInstant, + }, + { + expectedRevision: 2, + validFrom: instant('2027-02-01T00:00:00.000Z'), + validTo: undefined, + }, + instant('2026-09-03T00:00:00.000Z'), ); - assert.deepEqual( - decideRelationshipUpdate( - { - revision: 2, - validFrom: presentInstant('2026-01-01T00:00:00.000Z'), - validTo: absentInstant, - }, - { - expectedRevision: 2, - validFrom: instant('2026-02-01T00:00:00.000Z'), - validTo: undefined, - }, - instant('2026-09-03T00:00:00.000Z'), - ), - { _tag: 'correction_required', fact: 'validFrom' }, + expect(Predicate.isTagged(futureStartUpdate, 'update')).toBe(true); + expect(Struct.omit(futureStartUpdate, ['_tag'])).toEqual({}); + const historicalStartUpdate = decideRelationshipUpdate( + { + revision: 2, + validFrom: presentInstant('2026-01-01T00:00:00.000Z'), + validTo: absentInstant, + }, + { + expectedRevision: 2, + validFrom: instant('2026-02-01T00:00:00.000Z'), + validTo: undefined, + }, + instant('2026-09-03T00:00:00.000Z'), ); + expect(Predicate.isTagged(historicalStartUpdate, 'correction_required')).toBe(true); + expect(Struct.omit(historicalStartUpdate, ['_tag'])).toEqual({ fact: 'validFrom' }); }); -test('end retry is exact and changed historical evidence requires correction', () => { +it('end retry is exact and changed historical evidence requires correction', () => { const current = { endProvenanceMethod: 'MANUAL_CONFIRMATION', endProvenanceSource: 'ENGAGEMENT_REVIEW', @@ -359,23 +360,21 @@ test('end retry is exact and changed historical evidence requires correction', ( provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, reason: 'No longer a contact', } as const; - assert.deepEqual(decideRelationshipEnd(current, exact, instant('2026-09-03T00:00:00.000Z')), { - _tag: 'unchanged', - }); - assert.deepEqual( - decideRelationshipEnd( - current, - { ...exact, reason: 'A different historical explanation' }, - instant('2026-09-03T00:00:00.000Z'), - ), - { _tag: 'correction_required', fact: 'validTo' }, + const exactEndRetry = decideRelationshipEnd(current, exact, instant('2026-09-03T00:00:00.000Z')); + expect(Predicate.isTagged(exactEndRetry, 'unchanged')).toBe(true); + expect(Struct.omit(exactEndRetry, ['_tag'])).toEqual({}); + const changedEndRetry = decideRelationshipEnd( + current, + { ...exact, reason: 'A different historical explanation' }, + instant('2026-09-03T00:00:00.000Z'), ); - assert.deepEqual( - decideRelationshipEnd( - { ...current, endProvenanceMethod: null, endProvenanceSource: null, endReason: null }, - exact, - instant('2026-09-03T00:00:00.000Z'), - ), - { _tag: 'attach_end_evidence' }, + expect(Predicate.isTagged(changedEndRetry, 'correction_required')).toBe(true); + expect(Struct.omit(changedEndRetry, ['_tag'])).toEqual({ fact: 'validTo' }); + const missingEndEvidence = decideRelationshipEnd( + { ...current, endProvenanceMethod: null, endProvenanceSource: null, endReason: null }, + exact, + instant('2026-09-03T00:00:00.000Z'), ); + expect(Predicate.isTagged(missingEndEvidence, 'attach_end_evidence')).toBe(true); + expect(Struct.omit(missingEndEvidence, ['_tag'])).toEqual({}); }); diff --git a/app/verticals/party-registry/tests/unit/relationship-operation-contract.test.ts b/app/verticals/party-registry/tests/unit/relationship-operation-contract.test.ts index c6b0543e3..b3b407c16 100644 --- a/app/verticals/party-registry/tests/unit/relationship-operation-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/relationship-operation-contract.test.ts @@ -1,9 +1,7 @@ -import { runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { DateTime, Option, Schema } from 'effect'; -import { createPartyRelationshipAction } from '../../src/actions/create-party-relationship.action.ts'; import { encodeRelationshipEventPayload } from '../../src/actions/relationship-event-payload.ts'; +import { expect, it } from 'effect-rstest'; +import { Effect, DateTime, Option, Schema } from 'effect'; +import { createPartyRelationshipAction } from '../../src/actions/create-party-relationship.action.ts'; import { endPartyRelationshipAction } from '../../src/actions/end-party-relationship.action.ts'; import { updatePartyRelationshipAction } from '../../src/actions/update-party-relationship.action.ts'; import { partyRelationshipDetailRead } from '../../src/api/party-relationship-detail.read.ts'; @@ -27,117 +25,122 @@ const relationshipRef = { tenantId, } as const; -test('relationship writes are idempotent tenant Actions with dedicated authority', () => { +it('relationship writes are idempotent tenant Actions with dedicated authority', () => { const actions = [ createPartyRelationshipAction, updatePartyRelationshipAction, endPartyRelationshipAction, ] as const; - assert.deepEqual( - actions.map(({ descriptor }) => descriptor.actionKey), - [ - 'party.registry.create-party-relationship', - 'party.registry.update-party-relationship', - 'party.registry.end-party-relationship', - ], - ); + expect(actions.map(({ descriptor }) => descriptor.actionKey)).toEqual([ + 'party.registry.create-party-relationship', + 'party.registry.update-party-relationship', + 'party.registry.end-party-relationship', + ]); for (const { descriptor } of actions) { - assert.equal(descriptor.idempotency, 'required'); - assert.equal(descriptor.legalEntityScope, 'optional'); - assert.equal(descriptor.owningModuleKey, 'party.registry'); + expect(descriptor.idempotency).toBe('required'); + expect(descriptor.legalEntityScope).toBe('optional'); + expect(descriptor.owningModuleKey).toBe('party.registry'); // SAFETY: These resolvers are intentionally payload-independent tenant authority declarations. - assert.equal(descriptor.tenantPermission?.({} as never), 'manage_party_relationships'); + expect(descriptor.tenantPermission?.({} as never)).toBe('manage_party_relationships'); } - assert.deepEqual(Object.keys(createPartyRelationshipAction.descriptor.domainEvents), [ + expect(Object.keys(createPartyRelationshipAction.descriptor.domainEvents)).toEqual([ 'party.registry.relationship-created.v1', ]); - assert.deepEqual(Object.keys(updatePartyRelationshipAction.descriptor.domainEvents), [ + expect(Object.keys(updatePartyRelationshipAction.descriptor.domainEvents)).toEqual([ 'party.registry.relationship-updated.v1', ]); - assert.deepEqual(Object.keys(endPartyRelationshipAction.descriptor.domainEvents), [ + expect(Object.keys(endPartyRelationshipAction.descriptor.domainEvents)).toEqual([ 'party.registry.relationship-ended.v1', ]); }); -test('relationship detail is a tenant-authorized governed read of one ResourceRef', () => { - assert.equal(partyRelationshipDetailRead.descriptor.legalEntityScope, 'optional'); - assert.equal(partyRelationshipDetailRead.descriptor.permissionTarget, 'tenant'); - assert.equal(partyRelationshipDetailRead.descriptor.accessKind, 'detail'); - assert.deepEqual( - Schema.decodeUnknownSync(PartyRelationshipDetailRequestSchema)({ relationshipRef }), - { relationshipRef }, - ); -}); +it.effect('relationship detail is a tenant-authorized governed read of one ResourceRef', () => + Effect.gen(function* schemaContract1() { + expect(partyRelationshipDetailRead.descriptor.legalEntityScope).toBe('optional'); + expect(partyRelationshipDetailRead.descriptor.permissionTarget).toBe('tenant'); + expect(partyRelationshipDetailRead.descriptor.accessKind).toBe('detail'); + expect( + yield* Schema.decodeUnknownEffect(PartyRelationshipDetailRequestSchema)({ relationshipRef }), + ).toEqual({ relationshipRef }); + }), +); -test('relationship detail preserves canonical and stored alias endpoint context', () => { - const storedFrom = partyRef('22222222-2222-4222-8222-222222222222'); - const canonicalFrom = partyRef('55555555-5555-4555-8555-555555555555'); - const to = partyRef('33333333-3333-4333-8333-333333333333'); - const detail = Schema.decodeUnknownSync(PartyRelationshipDetailResponseSchema)({ - assertionState: 'ACTIVE', - endHistory: [ - { - effectiveAt: '2026-09-01T00:00:00.000Z', - provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, - reason: 'No longer the contact', - recordedAt: '2026-08-20T10:00:00.000Z', +it.effect('relationship detail preserves canonical and stored alias endpoint context', () => + Effect.gen(function* schemaContract2() { + const storedFrom = partyRef('22222222-2222-4222-8222-222222222222'); + const canonicalFrom = partyRef('55555555-5555-4555-8555-555555555555'); + const to = partyRef('33333333-3333-4333-8333-333333333333'); + const detail = yield* Schema.decodeUnknownEffect(PartyRelationshipDetailResponseSchema)({ + assertionState: 'ACTIVE', + endHistory: [ + { + effectiveAt: '2026-09-01T00:00:00.000Z', + provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, + reason: 'No longer the contact', + recordedAt: '2026-08-20T10:00:00.000Z', + }, + ], + from: { + canonicalPartyRef: canonicalFrom, + requestedAlias: storedFrom, + storedPartyRef: storedFrom, }, - ], - from: { - canonicalPartyRef: canonicalFrom, - requestedAlias: storedFrom, - storedPartyRef: storedFrom, - }, - provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, - recordedAt: '2026-09-01T10:00:00.000Z', - relationshipRef, - relationshipType: 'CONTACT_PERSON_OF', - revision: 4, - state: 'HISTORICAL', - to: { canonicalPartyRef: to, requestedAlias: null, storedPartyRef: to }, - validFrom: '2026-01-01T00:00:00.000Z', - validTo: '2026-09-01T00:00:00.000Z', - }); - assert.deepEqual(runEffectTestSync(encodeRelationshipEventPayload(detail)), { - fromPartyRef: canonicalFrom, - relationshipRef, - relationshipType: 'CONTACT_PERSON_OF', - revision: 4, - toPartyRef: to, - validFrom: '2026-01-01T00:00:00.000Z', - validTo: '2026-09-01T00:00:00.000Z', - }); - assert.equal(detail.from.canonicalPartyRef.resourceId, canonicalFrom.resourceId); - assert.equal(Option.getOrThrow(detail.from.requestedAlias).resourceId, storedFrom.resourceId); - assert.equal(detail.state, 'HISTORICAL'); - const [endEvidence] = detail.endHistory; - assert.ok(endEvidence); - assert.equal(Option.getOrThrow(endEvidence.reason), 'No longer the contact'); - assert.equal(DateTime.formatIso(Option.getOrThrow(detail.validTo)), '2026-09-01T00:00:00.000Z'); -}); + provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, + recordedAt: '2026-09-01T10:00:00.000Z', + relationshipRef, + relationshipType: 'CONTACT_PERSON_OF', + revision: 4, + state: 'HISTORICAL', + to: { canonicalPartyRef: to, requestedAlias: null, storedPartyRef: to }, + validFrom: '2026-01-01T00:00:00.000Z', + validTo: '2026-09-01T00:00:00.000Z', + }); + expect(yield* encodeRelationshipEventPayload(detail)).toEqual({ + fromPartyRef: canonicalFrom, + relationshipRef, + relationshipType: 'CONTACT_PERSON_OF', + revision: 4, + toPartyRef: to, + validFrom: '2026-01-01T00:00:00.000Z', + validTo: '2026-09-01T00:00:00.000Z', + }); + expect(detail.from.canonicalPartyRef.resourceId).toBe(canonicalFrom.resourceId); + expect(Option.getOrThrow(detail.from.requestedAlias).resourceId).toBe(storedFrom.resourceId); + expect(detail.state).toBe('HISTORICAL'); + const [endEvidence] = detail.endHistory; + expect(endEvidence).toBeDefined(); + if (endEvidence === undefined) { + throw new Error('Expected endEvidence'); + } + expect(Option.getOrThrow(endEvidence.reason)).toBe('No longer the contact'); + expect(DateTime.formatIso(Option.getOrThrow(detail.validTo))).toBe('2026-09-01T00:00:00.000Z'); + }), +); -test('outbox payloads carry stable refs and no mutable Party or authorization copy', () => { - const payload = { - fromPartyRef: partyRef('22222222-2222-4222-8222-222222222222'), - relationshipRef, - relationshipType: 'CONTACT_PERSON_OF', - revision: 1, - toPartyRef: partyRef('33333333-3333-4333-8333-333333333333'), - validFrom: '2026-09-01T10:00:00.000Z', - validTo: null, - } as const; - const decoded = Schema.decodeUnknownSync(RelationshipCreatedOutboxSchema)(payload); - assert.deepEqual(Schema.encodeSync(RelationshipCreatedOutboxSchema)(decoded), payload); - assert.throws(() => - Schema.decodeUnknownSync(RelationshipCreatedOutboxSchema, { onExcessProperty: 'error' })({ - ...payload, - authorizationGranted: true, - }), - ); - assert.throws(() => - Schema.decodeUnknownSync(RelationshipCreatedOutboxSchema, { onExcessProperty: 'error' })({ - ...payload, - party: { displayName: 'mutable copy' }, - }), - ); -}); +it.effect('outbox payloads carry stable refs and no mutable Party or authorization copy', () => + Effect.gen(function* schemaContract3() { + const payload = { + fromPartyRef: partyRef('22222222-2222-4222-8222-222222222222'), + relationshipRef, + relationshipType: 'CONTACT_PERSON_OF', + revision: 1, + toPartyRef: partyRef('33333333-3333-4333-8333-333333333333'), + validFrom: '2026-09-01T10:00:00.000Z', + validTo: null, + } as const; + const decoded = yield* Schema.decodeUnknownEffect(RelationshipCreatedOutboxSchema)(payload); + expect(yield* Schema.encodeEffect(RelationshipCreatedOutboxSchema)(decoded)).toEqual(payload); + expect(() => + Schema.decodeUnknownSync(RelationshipCreatedOutboxSchema, { onExcessProperty: 'error' })({ + ...payload, + authorizationGranted: true, + }), + ).toThrow(); + expect(() => + Schema.decodeUnknownSync(RelationshipCreatedOutboxSchema, { onExcessProperty: 'error' })({ + ...payload, + party: { displayName: 'mutable copy' }, + }), + ).toThrow(); + }), +); diff --git a/app/verticals/party-registry/tests/unit/relationship-persistence.service.test.ts b/app/verticals/party-registry/tests/unit/relationship-persistence.service.test.ts index dd31b1f04..86e86c875 100644 --- a/app/verticals/party-registry/tests/unit/relationship-persistence.service.test.ts +++ b/app/verticals/party-registry/tests/unit/relationship-persistence.service.test.ts @@ -1,9 +1,8 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off globalDate:off -- Existing compatibility boundary; expires: 2026-12-31. +import { TestClock } from 'effect/testing'; +import { expect, it } from 'effect-rstest'; +// @effect-diagnostics globalDate:off -- Existing compatibility boundary; expires: 2026-12-31. /* eslint-disable anti-slop/no-chained-type-assertions, anti-slop/no-unsafe-dictionary-type -- This focused harness models only the Drizzle system boundary used by the Relationship service. expires: 2026-12-31. */ -import { DateTime, Effect, Option, Schema } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { DateTime, Effect, Layer, Option, Schema, Predicate } from 'effect'; import { PartyAliasWriteRejected } from '../../shared/domain/merge-alias-resolution.ts'; import { CreatePartyRelationshipPayloadSchema, @@ -59,7 +58,7 @@ const relationshipRow = (overrides: Readonly> = {}) => ( policyVersion: 'party.relationship.contact-person-of.v1', provenanceMethod: 'MANUAL_CONFIRMATION', provenanceSource: 'ENGAGEMENT_REVIEW', - recordedAt: new Date('2026-01-01T00:00:00.000Z'), + recordedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), relationshipId, relationshipType: 'CONTACT_PERSON_OF', retractsRelationshipId: null, @@ -132,413 +131,493 @@ const transactionHarness = ( return { insertValues, transaction, updateSets }; }; -test('create persists an active assertion with unknown start and derives current state', async () => { - const created = relationshipRow(); - const harness = transactionHarness( - [ - [ - { archivedAt: null, currentType: 'PERSON', partyId: fromPartyId }, - { archivedAt: null, currentType: 'ORGANIZATION', partyId: toPartyId }, - ], - ...canonicalEndpointReads, - [], - ], - [[created]], +it.layer( + Layer.effectDiscard( + TestClock.setTime(DateTime.toEpochMillis(DateTime.makeUnsafe('2026-09-03T10:00:00.000Z'))), + ), +)('relationship persistence', (relationshipIt) => { + relationshipIt.effect( + 'create persists an active assertion with unknown start and derives current state', + () => + Effect.gen(function* testProgram1() { + const created = relationshipRow(); + const harness = transactionHarness( + [ + [ + { archivedAt: null, currentType: 'PERSON', partyId: fromPartyId }, + { archivedAt: null, currentType: 'ORGANIZATION', partyId: toPartyId }, + ], + ...canonicalEndpointReads, + [], + ], + [[created]], + ); + + const result = yield* createPartyRelationshipRecord( + harness.transaction, + tenantId, + principalId, + actionInvocationId, + decodeCreatePayload({ + fromPartyRef: ref(fromPartyId), + provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, + relationshipType: 'CONTACT_PERSON_OF', + toPartyRef: ref(toPartyId), + validFrom: null, + validTo: null, + }), + ); + + expect(result.outcome).toBe('CREATED'); + expect(result.relationship.state).toBe('CURRENT'); + expect(harness.insertValues[0]?.['assertionState']).toBe('ACTIVE'); + expect(harness.insertValues[0]?.['validFrom']).toBe(null); + expect('state' in (harness.insertValues[0] ?? {})).toBe(false); + expect('isCurrent' in (harness.insertValues[0] ?? {})).toBe(false); + }), ); - const result = await runEffectTestPromise( - createPartyRelationshipRecord( - harness.transaction, - tenantId, - principalId, - actionInvocationId, - decodeCreatePayload({ - fromPartyRef: ref(fromPartyId), - provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, - relationshipType: 'CONTACT_PERSON_OF', - toPartyRef: ref(toPartyId), - validFrom: null, - validTo: null, + relationshipIt.effect( + 'update refines an unknown historical validFrom through the persistence service', + () => + Effect.gen(function* testProgram2() { + const refinedAt = '2025-01-01T00:00:00.000Z'; + const validTo = DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')); + const current = relationshipRow({ validTo }); + const updated = relationshipRow({ + revision: 2, + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe(refinedAt)), + validTo, + }); + const harness = transactionHarness( + [[current], ...canonicalEndpointReads, []], + [], + [[updated]], + ); + + const result = yield* updatePartyRelationshipRecord( + harness.transaction, + tenantId, + principalId, + actionInvocationId, + decodeUpdatePayload({ + changeReason: 'Reliable engagement evidence established the relationship start', + expectedRevision: 1, + provenance: { method: 'DOCUMENT_REVIEW', source: 'ENGAGEMENT_RECORD' }, + relationshipRef, + validFrom: refinedAt, + }), + ); + + expect(result.outcome).toBe('CHANGED'); + expect(DateTime.formatIso(Option.getOrThrow(result.relationship.validFrom))).toBe( + refinedAt, + ); + expect(result.relationship.state).toBe('HISTORICAL'); + expect(harness.updateSets[0]?.['validFrom']).toEqual( + DateTime.toDateUtc(DateTime.makeUnsafe(refinedAt)), + ); + expect(harness.updateSets[0]?.['revision']).toBe(2); }), - ), ); - assert.equal(result.outcome, 'CREATED'); - assert.equal(result.relationship.state, 'CURRENT'); - assert.equal(harness.insertValues[0]?.['assertionState'], 'ACTIVE'); - assert.equal(harness.insertValues[0]?.['validFrom'], null); - assert.equal('state' in (harness.insertValues[0] ?? {}), false); - assert.equal('isCurrent' in (harness.insertValues[0] ?? {}), false); -}); - -test('update refines an unknown historical validFrom through the persistence service', async () => { - const refinedAt = '2025-01-01T00:00:00.000Z'; - const validTo = new Date('2026-01-01T00:00:00.000Z'); - const current = relationshipRow({ validTo }); - const updated = relationshipRow({ revision: 2, validFrom: new Date(refinedAt), validTo }); - const harness = transactionHarness([[current], ...canonicalEndpointReads, []], [], [[updated]]); - - const result = await runEffectTestPromise( - updatePartyRelationshipRecord( - harness.transaction, - tenantId, - principalId, - actionInvocationId, - decodeUpdatePayload({ - changeReason: 'Reliable engagement evidence established the relationship start', - expectedRevision: 1, - provenance: { method: 'DOCUMENT_REVIEW', source: 'ENGAGEMENT_RECORD' }, - relationshipRef, - validFrom: refinedAt, + relationshipIt.effect( + 'end keeps a future-ended relationship current and exposes bounded end history', + () => + Effect.gen(function* testProgram3() { + const effectiveAt = '2099-01-01T00:00:00.000Z'; + const survivorId = '70000000-0000-4000-8000-000000000001'; + const current = relationshipRow({ + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2025-01-01T00:00:00.000Z')), + }); + const ended = relationshipRow({ + endProvenanceMethod: 'MANUAL_CONFIRMATION', + endProvenanceSource: 'ENGAGEMENT_REVIEW', + endReason: 'A successor contact takes responsibility', + endedByActionInvocationId: actionInvocationId, + endedByPrincipalId: principalId, + endedRecordedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-09-03T00:00:00.000Z')), + revision: 2, + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2025-01-01T00:00:00.000Z')), + validTo: DateTime.toDateUtc(DateTime.makeUnsafe(effectiveAt)), + }); + const harness = transactionHarness( + [ + [current], + [{ aliasPartyId: fromPartyId, canonicalPartyId: survivorId, tenantId }], + [], + [{ partyId: survivorId }], + [], + [{ partyId: toPartyId }], + ], + [], + [[ended]], + ); + + const result = yield* endPartyRelationshipRecord( + harness.transaction, + tenantId, + principalId, + actionInvocationId, + decodeEndPayload({ + effectiveAt, + expectedRevision: 1, + provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, + reason: 'A successor contact takes responsibility', + relationshipRef, + }), + ); + + expect(result.relationship.state).toBe('CURRENT'); + expect(result.relationship.from.canonicalPartyRef.resourceId).toBe(survivorId); + expect(result.relationship.from.storedPartyRef.resourceId).toBe(fromPartyId); + expect(result.relationship.endHistory.length).toBe(1); + const [endEvidence] = result.relationship.endHistory; + expect(endEvidence).toBeDefined(); + if (endEvidence === undefined) { + throw new Error('Expected endEvidence'); + } + expect(DateTime.formatIso(endEvidence.effectiveAt)).toBe(effectiveAt); + expect(Option.getOrThrow(endEvidence.reason)).toBe( + 'A successor contact takes responsibility', + ); + expect('state' in (harness.updateSets[0] ?? {})).toBe(false); + expect('isCurrent' in (harness.updateSets[0] ?? {})).toBe(false); }), - ), ); - assert.equal(result.outcome, 'CHANGED'); - assert.equal(DateTime.formatIso(Option.getOrThrow(result.relationship.validFrom)), refinedAt); - assert.equal(result.relationship.state, 'HISTORICAL'); - assert.deepEqual(harness.updateSets[0]?.['validFrom'], new Date(refinedAt)); - assert.equal(harness.updateSets[0]?.['revision'], 2); -}); + relationshipIt.effect( + 'detail derives scheduled state and resolves stored endpoint aliases independently', + () => + Effect.gen(function* testProgram4() { + const canonicalFrom = '70000000-0000-4000-8000-000000000001'; + const middleAlias = '80000000-0000-4000-8000-000000000001'; + const scheduled = relationshipRow({ + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2099-01-01T00:00:00.000Z')), + }); + const harness = transactionHarness([ + [scheduled], + [{ aliasPartyId: fromPartyId, canonicalPartyId: middleAlias, tenantId }], + [{ aliasPartyId: middleAlias, canonicalPartyId: canonicalFrom, tenantId }], + [], + [{ partyId: canonicalFrom }], + [], + [{ partyId: toPartyId }], + ]); + + const detail = yield* findPartyRelationshipRecord( + harness.transaction, + tenantId, + relationshipId, + ); + + expect(detail?.state).toBe('SCHEDULED'); + expect(detail?.from.storedPartyRef.resourceId).toBe(fromPartyId); + expect(detail?.from.canonicalPartyRef.resourceId).toBe(canonicalFrom); + expect(detail).toBeDefined(); + if (detail === undefined || detail === null) { + throw new Error('Expected detail'); + } + expect(Option.getOrThrow(detail.from.requestedAlias).resourceId).toBe(fromPartyId); + expect(Option.isNone(detail.to.requestedAlias)).toBe(true); + }), + ); -test('end keeps a future-ended relationship current and exposes bounded end history', async () => { - const effectiveAt = '2099-01-01T00:00:00.000Z'; - const survivorId = '70000000-0000-4000-8000-000000000001'; - const current = relationshipRow({ validFrom: new Date('2025-01-01T00:00:00.000Z') }); - const ended = relationshipRow({ - endProvenanceMethod: 'MANUAL_CONFIRMATION', - endProvenanceSource: 'ENGAGEMENT_REVIEW', - endReason: 'A successor contact takes responsibility', - endedByActionInvocationId: actionInvocationId, - endedByPrincipalId: principalId, - endedRecordedAt: new Date('2026-09-03T00:00:00.000Z'), - revision: 2, - validFrom: new Date('2025-01-01T00:00:00.000Z'), - validTo: new Date(effectiveAt), - }); - const harness = transactionHarness( - [ - [current], - [{ aliasPartyId: fromPartyId, canonicalPartyId: survivorId, tenantId }], - [], - [{ partyId: survivorId }], - [], - [{ partyId: toPartyId }], - ], - [], - [[ended]], + relationshipIt.effect( + 'non-active assertions never read as current even with an open effective interval', + () => + Effect.all( + ['RETRACTED', 'SUPERSEDED', 'DISPUTED'].map((assertionState) => + Effect.gen(function* testProgram6() { + const harness = transactionHarness([ + [relationshipRow({ assertionState })], + ...canonicalEndpointReads, + ]); + const detail = yield* findPartyRelationshipRecord( + harness.transaction, + tenantId, + relationshipId, + ); + + expect(detail?.assertionState).toBe(assertionState); + expect(detail?.state).toBe('HISTORICAL'); + }), + ), + ), ); - const result = await runEffectTestPromise( - endPartyRelationshipRecord( - harness.transaction, - tenantId, - principalId, - actionInvocationId, - decodeEndPayload({ - effectiveAt, - expectedRevision: 1, - provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, - reason: 'A successor contact takes responsibility', - relationshipRef, + relationshipIt.effect( + 'durable relationship update resolves alias-backed stored endpoints without rewriting them', + () => + Effect.gen(function* testProgram7() { + const survivorId = '70000000-0000-4000-8000-000000000001'; + const updated = relationshipRow({ + revision: 2, + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2099-01-01T00:00:00.000Z')), + }); + const harness = transactionHarness( + [ + [relationshipRow()], + [{ aliasPartyId: fromPartyId, canonicalPartyId: survivorId, tenantId }], + [], + [{ partyId: survivorId }], + [], + [{ partyId: toPartyId }], + [], + ], + [], + [[updated]], + ); + const result = yield* updatePartyRelationshipRecord( + harness.transaction, + tenantId, + principalId, + actionInvocationId, + decodeUpdatePayload({ + changeReason: 'A revised planned start', + expectedRevision: 1, + provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, + relationshipRef, + validFrom: '2099-01-01T00:00:00.000Z', + }), + ); + + expect(result.outcome).toBe('CHANGED'); + expect(result.relationship.from.canonicalPartyRef.resourceId).toBe(survivorId); + expect(result.relationship.from.storedPartyRef.resourceId).toBe(fromPartyId); + expect('fromPartyId' in (harness.updateSets[0] ?? {})).toBe(false); }), - ), ); - assert.equal(result.relationship.state, 'CURRENT'); - assert.equal(result.relationship.from.canonicalPartyRef.resourceId, survivorId); - assert.equal(result.relationship.from.storedPartyRef.resourceId, fromPartyId); - assert.equal(result.relationship.endHistory.length, 1); - const [endEvidence] = result.relationship.endHistory; - assert.ok(endEvidence); - assert.equal(DateTime.formatIso(endEvidence.effectiveAt), effectiveAt); - assert.equal(Option.getOrThrow(endEvidence.reason), 'A successor contact takes responsibility'); - assert.equal('state' in (harness.updateSets[0] ?? {}), false); - assert.equal('isCurrent' in (harness.updateSets[0] ?? {}), false); -}); - -test('detail derives scheduled state and resolves stored endpoint aliases independently', async () => { - const canonicalFrom = '70000000-0000-4000-8000-000000000001'; - const middleAlias = '80000000-0000-4000-8000-000000000001'; - const scheduled = relationshipRow({ validFrom: new Date('2099-01-01T00:00:00.000Z') }); - const harness = transactionHarness([ - [scheduled], - [{ aliasPartyId: fromPartyId, canonicalPartyId: middleAlias, tenantId }], - [{ aliasPartyId: middleAlias, canonicalPartyId: canonicalFrom, tenantId }], - [], - [{ partyId: canonicalFrom }], - [], - [{ partyId: toPartyId }], - ]); - - const detail = await runEffectTestPromise( - findPartyRelationshipRecord(harness.transaction, tenantId, relationshipId), + relationshipIt.effect( + 'create rejects an explicit alias endpoint with canonical survivor guidance', + () => + Effect.gen(function* testProgram8() { + const survivorId = '70000000-0000-4000-8000-000000000001'; + const harness = transactionHarness([ + [ + { + archivedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), + currentType: 'PERSON', + partyId: fromPartyId, + }, + { archivedAt: null, currentType: 'ORGANIZATION', partyId: toPartyId }, + ], + [{ aliasPartyId: fromPartyId, canonicalPartyId: survivorId, tenantId }], + [], + [{ partyId: survivorId }], + ]); + const rejection = yield* createPartyRelationshipRecord( + harness.transaction, + tenantId, + principalId, + actionInvocationId, + decodeCreatePayload({ + fromPartyRef: ref(fromPartyId), + provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, + relationshipType: 'CONTACT_PERSON_OF', + toPartyRef: ref(toPartyId), + validFrom: null, + validTo: null, + }), + ).pipe(Effect.flip); + + expect(Predicate.isTagged(rejection, 'PartyAliasWriteRejected')).toBe(true); + if (Schema.is(PartyAliasWriteRejected)(rejection)) { + expect(rejection.canonicalPartyRef.resourceId).toBe(survivorId); + } + expect(harness.insertValues.length).toBe(0); + }), ); - assert.equal(detail?.state, 'SCHEDULED'); - assert.equal(detail?.from.storedPartyRef.resourceId, fromPartyId); - assert.equal(detail?.from.canonicalPartyRef.resourceId, canonicalFrom); - assert.ok(detail); - assert.equal(Option.getOrThrow(detail.from.requestedAlias).resourceId, fromPartyId); - assert.ok(Option.isNone(detail.to.requestedAlias)); -}); - -test('non-active assertions never read as current even with an open effective interval', async () => { - await Promise.all( - ['RETRACTED', 'SUPERSEDED', 'DISPUTED'].map(async (assertionState) => { + relationshipIt.effect('a known historical start cannot be rewritten by ordinary update', () => + Effect.gen(function* testProgram9() { const harness = transactionHarness([ - [relationshipRow({ assertionState })], + [ + relationshipRow({ + validFrom: DateTime.toDateUtc(DateTime.makeUnsafe('2025-01-01T00:00:00.000Z')), + }), + ], ...canonicalEndpointReads, ]); - const detail = await runEffectTestPromise( - findPartyRelationshipRecord(harness.transaction, tenantId, relationshipId), - ); - - assert.equal(detail?.assertionState, assertionState); - assert.equal(detail?.state, 'HISTORICAL'); + const rejection = yield* updatePartyRelationshipRecord( + harness.transaction, + tenantId, + principalId, + actionInvocationId, + decodeUpdatePayload({ + changeReason: 'The previous start was wrong', + expectedRevision: 1, + provenance: { method: 'DOCUMENT_REVIEW', source: 'ENGAGEMENT_RECORD' }, + relationshipRef, + validFrom: '2025-02-01T00:00:00.000Z', + }), + ).pipe(Effect.flip); + + expect(Predicate.isTagged(rejection, 'PartyRelationshipCorrectionRequired')).toBe(true); + if (Schema.is(PartyRelationshipCorrectionRequired)(rejection)) { + expect(rejection.fact).toBe('validFrom'); + } + expect(harness.updateSets.length).toBe(0); }), ); -}); - -test('durable relationship update resolves alias-backed stored endpoints without rewriting them', async () => { - const survivorId = '70000000-0000-4000-8000-000000000001'; - const updated = relationshipRow({ - revision: 2, - validFrom: new Date('2099-01-01T00:00:00.000Z'), - }); - const harness = transactionHarness( - [ - [relationshipRow()], - [{ aliasPartyId: fromPartyId, canonicalPartyId: survivorId, tenantId }], - [], - [{ partyId: survivorId }], - [], - [{ partyId: toPartyId }], - [], - ], - [], - [[updated]], - ); - const result = await runEffectTestPromise( - updatePartyRelationshipRecord( - harness.transaction, - tenantId, - principalId, - actionInvocationId, - decodeUpdatePayload({ - changeReason: 'A revised planned start', - expectedRevision: 1, - provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, - relationshipRef, - validFrom: '2099-01-01T00:00:00.000Z', - }), - ), - ); - - assert.equal(result.outcome, 'CHANGED'); - assert.equal(result.relationship.from.canonicalPartyRef.resourceId, survivorId); - assert.equal(result.relationship.from.storedPartyRef.resourceId, fromPartyId); - assert.equal('fromPartyId' in (harness.updateSets[0] ?? {}), false); -}); - -test('create rejects an explicit alias endpoint with canonical survivor guidance', async () => { - const survivorId = '70000000-0000-4000-8000-000000000001'; - const harness = transactionHarness([ - [ - { - archivedAt: new Date('2026-01-01T00:00:00.000Z'), - currentType: 'PERSON', - partyId: fromPartyId, - }, - { archivedAt: null, currentType: 'ORGANIZATION', partyId: toPartyId }, - ], - [{ aliasPartyId: fromPartyId, canonicalPartyId: survivorId, tenantId }], - [], - [{ partyId: survivorId }], - ]); - const rejection = await runEffectTestPromise( - createPartyRelationshipRecord( - harness.transaction, - tenantId, - principalId, - actionInvocationId, - decodeCreatePayload({ - fromPartyRef: ref(fromPartyId), - provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, - relationshipType: 'CONTACT_PERSON_OF', - toPartyRef: ref(toPartyId), - validFrom: null, - validTo: null, - }), - ).pipe(Effect.flip), - ); - assert.equal(rejection._tag, 'PartyAliasWriteRejected'); - if (Schema.is(PartyAliasWriteRejected)(rejection)) { - assert.equal(rejection.canonicalPartyRef.resourceId, survivorId); - } - assert.equal(harness.insertValues.length, 0); -}); - -test('a known historical start cannot be rewritten by ordinary update', async () => { - const harness = transactionHarness([ - [relationshipRow({ validFrom: new Date('2025-01-01T00:00:00.000Z') })], - ...canonicalEndpointReads, - ]); - const rejection = await runEffectTestPromise( - updatePartyRelationshipRecord( - harness.transaction, - tenantId, - principalId, - actionInvocationId, - decodeUpdatePayload({ - changeReason: 'The previous start was wrong', - expectedRevision: 1, - provenance: { method: 'DOCUMENT_REVIEW', source: 'ENGAGEMENT_RECORD' }, - relationshipRef, - validFrom: '2025-02-01T00:00:00.000Z', + relationshipIt.effect( + 'removing a future planned end clears its current evidence and retains prior audit detail', + () => + Effect.gen(function* testProgram10() { + const current = relationshipRow({ + endProvenanceMethod: 'MANUAL_CONFIRMATION', + endProvenanceSource: 'ENGAGEMENT_REVIEW', + endReason: 'A planned contact handover', + endedByActionInvocationId: actionInvocationId, + endedByPrincipalId: principalId, + endedRecordedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-01-01T00:00:00.000Z')), + validTo: DateTime.toDateUtc(DateTime.makeUnsafe('2099-01-01T00:00:00.000Z')), + }); + const updated = relationshipRow({ revision: 2 }); + const harness = transactionHarness( + [[current], ...canonicalEndpointReads, []], + [], + [[updated]], + ); + const result = yield* updatePartyRelationshipRecord( + harness.transaction, + tenantId, + principalId, + actionInvocationId, + decodeUpdatePayload({ + changeReason: 'The planned handover was canceled', + expectedRevision: 1, + provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, + relationshipRef, + validTo: null, + }), + ); + + expect(result.outcome).toBe('CHANGED'); + expect(Option.isNone(result.relationship.validTo)).toBe(true); + expect(result.relationship.endHistory).toEqual([]); + expect(harness.updateSets[0]?.['endReason']).toBe(null); + expect(harness.updateSets[0]?.['endedRecordedAt']).toBe(null); + if (result.outcome === 'CHANGED') { + const [previousEndEvidence] = result.previous.endHistory; + expect(previousEndEvidence).toBeDefined(); + if (previousEndEvidence === undefined) { + throw new Error('Expected previousEndEvidence'); + } + expect(Option.getOrThrow(previousEndEvidence.reason)).toBe('A planned contact handover'); + } }), - ).pipe(Effect.flip), ); - assert.equal(rejection._tag, 'PartyRelationshipCorrectionRequired'); - if (Schema.is(PartyRelationshipCorrectionRequired)(rejection)) { - assert.equal(rejection.fact, 'validFrom'); - } - assert.equal(harness.updateSets.length, 0); -}); - -test('removing a future planned end clears its current evidence and retains prior audit detail', async () => { - const current = relationshipRow({ - endProvenanceMethod: 'MANUAL_CONFIRMATION', - endProvenanceSource: 'ENGAGEMENT_REVIEW', - endReason: 'A planned contact handover', - endedByActionInvocationId: actionInvocationId, - endedByPrincipalId: principalId, - endedRecordedAt: new Date('2026-01-01T00:00:00.000Z'), - validTo: new Date('2099-01-01T00:00:00.000Z'), - }); - const updated = relationshipRow({ revision: 2 }); - const harness = transactionHarness([[current], ...canonicalEndpointReads, []], [], [[updated]]); - const result = await runEffectTestPromise( - updatePartyRelationshipRecord( - harness.transaction, - tenantId, - principalId, - actionInvocationId, - decodeUpdatePayload({ - changeReason: 'The planned handover was canceled', - expectedRevision: 1, - provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, - relationshipRef, - validTo: null, + relationshipIt.effect( + 'update can shorten a future planned end to a valid retrospective end with new evidence', + () => + Effect.gen(function* testProgram11() { + const validFrom = DateTime.toDateUtc(DateTime.makeUnsafe('2025-01-01T00:00:00.000Z')); + const effectiveAt = '2026-02-01T00:00:00.000Z'; + const current = relationshipRow({ + validFrom, + validTo: DateTime.toDateUtc(DateTime.makeUnsafe('2099-01-01T00:00:00.000Z')), + }); + const updated = relationshipRow({ + endProvenanceMethod: 'DOCUMENT_REVIEW', + endProvenanceSource: 'ENGAGEMENT_RECORD', + endReason: 'The handover actually completed earlier', + endedByActionInvocationId: actionInvocationId, + endedByPrincipalId: principalId, + endedRecordedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-09-03T00:00:00.000Z')), + revision: 2, + validFrom, + validTo: DateTime.toDateUtc(DateTime.makeUnsafe(effectiveAt)), + }); + const harness = transactionHarness( + [[current], ...canonicalEndpointReads, []], + [], + [[updated]], + ); + const result = yield* updatePartyRelationshipRecord( + harness.transaction, + tenantId, + principalId, + actionInvocationId, + decodeUpdatePayload({ + changeReason: 'The handover actually completed earlier', + expectedRevision: 1, + provenance: { method: 'DOCUMENT_REVIEW', source: 'ENGAGEMENT_RECORD' }, + relationshipRef, + validTo: effectiveAt, + }), + ); + + expect(result.outcome).toBe('CHANGED'); + expect(result.relationship.state).toBe('HISTORICAL'); + const [endEvidence] = result.relationship.endHistory; + expect(endEvidence).toBeDefined(); + if (endEvidence === undefined) { + throw new Error('Expected endEvidence'); + } + expect(DateTime.formatIso(endEvidence.effectiveAt)).toBe(effectiveAt); + expect(harness.updateSets[0]?.['endProvenanceSource']).toBe('ENGAGEMENT_RECORD'); + expect(harness.updateSets[0]?.['endedByActionInvocationId']).toBe(actionInvocationId); }), - ), ); - assert.equal(result.outcome, 'CHANGED'); - assert.ok(Option.isNone(result.relationship.validTo)); - assert.deepEqual(result.relationship.endHistory, []); - assert.equal(harness.updateSets[0]?.['endReason'], null); - assert.equal(harness.updateSets[0]?.['endedRecordedAt'], null); - if (result.outcome === 'CHANGED') { - const [previousEndEvidence] = result.previous.endHistory; - assert.ok(previousEndEvidence); - assert.equal(Option.getOrThrow(previousEndEvidence.reason), 'A planned contact handover'); - } -}); - -test('update can shorten a future planned end to a valid retrospective end with new evidence', async () => { - const validFrom = new Date('2025-01-01T00:00:00.000Z'); - const effectiveAt = '2026-02-01T00:00:00.000Z'; - const current = relationshipRow({ validFrom, validTo: new Date('2099-01-01T00:00:00.000Z') }); - const updated = relationshipRow({ - endProvenanceMethod: 'DOCUMENT_REVIEW', - endProvenanceSource: 'ENGAGEMENT_RECORD', - endReason: 'The handover actually completed earlier', - endedByActionInvocationId: actionInvocationId, - endedByPrincipalId: principalId, - endedRecordedAt: new Date('2026-09-03T00:00:00.000Z'), - revision: 2, - validFrom, - validTo: new Date(effectiveAt), - }); - const harness = transactionHarness([[current], ...canonicalEndpointReads, []], [], [[updated]]); - const result = await runEffectTestPromise( - updatePartyRelationshipRecord( - harness.transaction, - tenantId, - principalId, - actionInvocationId, - decodeUpdatePayload({ - changeReason: 'The handover actually completed earlier', - expectedRevision: 1, - provenance: { method: 'DOCUMENT_REVIEW', source: 'ENGAGEMENT_RECORD' }, - relationshipRef, - validTo: effectiveAt, - }), - ), - ); - - assert.equal(result.outcome, 'CHANGED'); - assert.equal(result.relationship.state, 'HISTORICAL'); - const [endEvidence] = result.relationship.endHistory; - assert.ok(endEvidence); - assert.equal(DateTime.formatIso(endEvidence.effectiveAt), effectiveAt); - assert.equal(harness.updateSets[0]?.['endProvenanceSource'], 'ENGAGEMENT_RECORD'); - assert.equal(harness.updateSets[0]?.['endedByActionInvocationId'], actionInvocationId); -}); - -test('an evidence-backed end without a generic reason stays visible and retries exactly', async () => { - const effectiveAt = '2026-02-01T00:00:00.000Z'; - const ended = relationshipRow({ - endProvenanceMethod: 'DOCUMENT_REVIEW', - endProvenanceSource: 'ENGAGEMENT_RECORD', - endedByActionInvocationId: actionInvocationId, - endedByPrincipalId: principalId, - endedRecordedAt: new Date('2026-09-03T00:00:00.000Z'), - revision: 2, - validTo: new Date(effectiveAt), - }); - const harness = transactionHarness( - [[relationshipRow()], ...canonicalEndpointReads], - [], - [[ended]], - ); - const payload = { - effectiveAt, - expectedRevision: 1, - provenance: { method: 'DOCUMENT_REVIEW', source: 'ENGAGEMENT_RECORD' }, - relationshipRef, - }; - const result = await runEffectTestPromise( - endPartyRelationshipRecord( - harness.transaction, - tenantId, - principalId, - actionInvocationId, - decodeEndPayload(payload), - ), - ); - assert.equal(result.relationship.endHistory.length, 1); - const [endEvidence] = result.relationship.endHistory; - assert.ok(endEvidence); - assert.ok(Option.isNone(endEvidence.reason)); - assert.equal(harness.updateSets[0]?.['endReason'], null); - - const retryHarness = transactionHarness([[ended], ...canonicalEndpointReads]); - const retry = await runEffectTestPromise( - endPartyRelationshipRecord( - retryHarness.transaction, - tenantId, - principalId, - actionInvocationId, - decodeEndPayload({ - ...payload, - expectedRevision: 2, + relationshipIt.effect( + 'an evidence-backed end without a generic reason stays visible and retries exactly', + () => + Effect.gen(function* testProgram12() { + const effectiveAt = '2026-02-01T00:00:00.000Z'; + const ended = relationshipRow({ + endProvenanceMethod: 'DOCUMENT_REVIEW', + endProvenanceSource: 'ENGAGEMENT_RECORD', + endedByActionInvocationId: actionInvocationId, + endedByPrincipalId: principalId, + endedRecordedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-09-03T00:00:00.000Z')), + revision: 2, + validTo: DateTime.toDateUtc(DateTime.makeUnsafe(effectiveAt)), + }); + const harness = transactionHarness( + [[relationshipRow()], ...canonicalEndpointReads], + [], + [[ended]], + ); + const payload = { + effectiveAt, + expectedRevision: 1, + provenance: { method: 'DOCUMENT_REVIEW', source: 'ENGAGEMENT_RECORD' }, + relationshipRef, + }; + const result = yield* endPartyRelationshipRecord( + harness.transaction, + tenantId, + principalId, + actionInvocationId, + decodeEndPayload(payload), + ); + expect(result.relationship.endHistory.length).toBe(1); + const [endEvidence] = result.relationship.endHistory; + expect(endEvidence).toBeDefined(); + if (endEvidence === undefined) { + throw new Error('Expected endEvidence'); + } + expect(Option.isNone(endEvidence.reason)).toBe(true); + expect(harness.updateSets[0]?.['endReason']).toBe(null); + + const retryHarness = transactionHarness([[ended], ...canonicalEndpointReads]); + const retry = yield* endPartyRelationshipRecord( + retryHarness.transaction, + tenantId, + principalId, + actionInvocationId, + decodeEndPayload({ + ...payload, + expectedRevision: 2, + }), + ); + expect(retry.outcome).toBe('UNCHANGED'); + expect(retryHarness.updateSets.length).toBe(0); }), - ), ); - assert.equal(retry.outcome, 'UNCHANGED'); - assert.equal(retryHarness.updateSets.length, 0); }); diff --git a/app/verticals/party-registry/tests/unit/runtime-locales.test.ts b/app/verticals/party-registry/tests/unit/runtime-locales.test.ts index 5333bd08b..50ea417a0 100644 --- a/app/verticals/party-registry/tests/unit/runtime-locales.test.ts +++ b/app/verticals/party-registry/tests/unit/runtime-locales.test.ts @@ -1,21 +1,24 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; + import { Predicate } from 'effect'; import runtime from '../../src/modern.runtime.ts'; import csResource from '../../locales/cs/translation.json' with { type: 'json' }; import enResource from '../../locales/en/translation.json' with { type: 'json' }; import { partyRegistryI18nResources } from '../../src/i18n/resources.ts'; -test('runtime registers the Party Registry page namespace alongside shared translations', () => { +it('runtime registers the Party Registry page namespace alongside shared translations', () => { const configuration = Predicate.isFunction(runtime) ? runtime('index') : runtime; const { i18n } = configuration; - assert.ok(i18n?.i18nInstance); - assert.deepEqual(i18n.initOptions?.resources, { + expect(i18n?.i18nInstance).toBeTruthy(); + if (i18n?.i18nInstance === undefined) { + throw new Error('Expected value to be present'); + } + expect(i18n.initOptions?.resources).toEqual({ cs: { ...partyRegistryI18nResources.cs, translation: csResource }, en: { ...partyRegistryI18nResources.en, translation: enResource }, }); - assert.deepEqual(i18n.initOptions?.supportedLngs, ['en', 'cs']); - assert.deepEqual(i18n.initOptions?.ns, ['party-registry', 'translation']); - assert.equal(i18n.initOptions?.defaultNS, 'party-registry'); - assert.equal(i18n.initOptions?.fallbackLng, 'en'); + expect(i18n.initOptions?.supportedLngs).toEqual(['en', 'cs']); + expect(i18n.initOptions?.ns).toEqual(['party-registry', 'translation']); + expect(i18n.initOptions?.defaultNS).toBe('party-registry'); + expect(i18n.initOptions?.fallbackLng).toBe('en'); }); diff --git a/app/verticals/party-registry/tests/unit/schema-contract.test.ts b/app/verticals/party-registry/tests/unit/schema-contract.test.ts index 2c35a3956..5ca77f23a 100644 --- a/app/verticals/party-registry/tests/unit/schema-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/schema-contract.test.ts @@ -1,7 +1,9 @@ -// @effect-diagnostics asyncFunction:off nodeBuiltinImport:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; +// @effect-diagnostics nodeBuiltinImport:off -- Filesystem migration contract verifies actual checked-in SQL files; expires: 2026-12-31. +import { assert, expect, it } from 'effect-rstest'; +import { Effect } from 'effect'; + import { readdir, readFile } from 'node:fs/promises'; -import test from 'node:test'; + import { getTableName, isTable } from 'drizzle-orm'; import { getTableConfig, PgDialect } from 'drizzle-orm/pg-core'; import * as schemaExports from '../../src/db/schema.ts'; @@ -54,18 +56,21 @@ const uniqueColumns = (table: (typeof configuredTables)[number], name: string) = const config = configOf(table); const constraint = config.uniqueConstraints.find((candidate) => candidate.name === name); const tableIndex = config.indexes.find((candidate) => candidate.config.name === name); - assert.ok(constraint ?? tableIndex, `Expected unique key ${name}`); + expect(constraint ?? tableIndex, `Expected unique key ${name}`).toBeTruthy(); const columns = constraint?.columns ?? tableIndex?.config.columns ?? []; return columns.map((column) => ('name' in column ? column.name : false)); }; const foreignKey = (table: (typeof configuredTables)[number], name: string) => { const result = configOf(table).foreignKeys.find((candidate) => candidate.getName() === name); - assert.ok(result, `Expected foreign key ${name}`); + expect(result, `Expected foreign key ${name}`).toBeTruthy(); + if (result === undefined) { + throw new Error(`Expected foreign key ${name}`); + } return result; }; -test('owns the complete Party Registry operational catalog in the party schema', () => { +it('owns the complete Party Registry operational catalog in the party schema', () => { const exportedTables = Object.values(schemaExports).flatMap((value) => isTable(value) ? [value] : [], ); @@ -76,8 +81,8 @@ test('owns the complete Party Registry operational catalog in the party schema', }) .toSorted(); - assert.equal(PARTY_SCHEMA_NAME, 'party'); - assert.deepEqual(PARTY_TABLE_INVENTORY, [ + expect(PARTY_SCHEMA_NAME).toBe('party'); + expect(PARTY_TABLE_INVENTORY).toEqual([ 'counterparties', 'counterparty_admin_read_models', 'counterparty_role_admin_read_models', @@ -96,13 +101,10 @@ test('owns the complete Party Registry operational catalog in the party schema', 'party_official_identifiers', 'party_relationships', ]); - assert.deepEqual( - qualifiedNames, - PARTY_TABLE_INVENTORY.map((name) => `party.${name}`), - ); + expect(qualifiedNames).toEqual(PARTY_TABLE_INVENTORY.map((name) => `party.${name}`)); }); -test('keeps tenant-admin Counterparty reads on an atomic owner-local projection', () => { +it('keeps tenant-admin Counterparty reads on an atomic owner-local projection', () => { const snapshotConfig = configOf(counterpartyAdminReadModels); const roleConfig = configOf(counterpartyRoleAdminReadModels); for (const column of [ @@ -113,10 +115,10 @@ test('keeps tenant-admin Counterparty reads on an atomic owner-local projection' 'created_at', 'archived_at', ]) { - assert.ok( + expect( snapshotConfig.columns.some((candidate) => candidate.name === column), column, - ); + ).toBeTruthy(); } for (const column of [ 'tenant_id', @@ -136,78 +138,75 @@ test('keeps tenant-admin Counterparty reads on an atomic owner-local projection' 'provenance_source', 'provenance_method', ]) { - assert.ok( + expect( roleConfig.columns.some((candidate) => candidate.name === column), column, - ); + ).toBeTruthy(); } - assert.equal( + expect( getTableName( foreignKey( counterpartyAdminReadModels, 'party_counterparty_admin_model_source_fk', ).reference().foreignTable, ), - getTableName(counterparties), - ); - assert.equal( + ).toBe(getTableName(counterparties)); + expect( getTableName( foreignKey( counterpartyRoleAdminReadModels, 'party_counterparty_role_admin_model_source_fk', ).reference().foreignTable, ), - getTableName(counterpartyRolePeriods), - ); - assert.ok( + ).toBe(getTableName(counterpartyRolePeriods)); + expect( roleConfig.indexes.some( (candidate) => candidate.config.name === 'party_counterparty_role_admin_models_history_idx', ), - ); + ).toBeTruthy(); for (const config of [snapshotConfig, roleConfig]) { - assert.deepEqual( - config.policies.map((policy) => policy.name), - [ - `${config.name}_tenant_select`, - `${config.name}_tenant_insert`, - `${config.name}_tenant_update`, - `${config.name}_tenant_delete`, - ], - ); + expect(config.policies.map((policy) => policy.name)).toEqual([ + `${config.name}_tenant_select`, + `${config.name}_tenant_insert`, + `${config.name}_tenant_update`, + `${config.name}_tenant_delete`, + ]); } }); -test('gives every tenant-owned record a tenant-qualified identity and forced-RLS policy shape', () => { +it('gives every tenant-owned record a tenant-qualified identity and forced-RLS policy shape', () => { for (const table of configuredTables) { const config = configOf(table); - assert.equal(config.enableRLS, true, `${config.name} must enable RLS`); - assert.ok( + expect(config.enableRLS, `${config.name} must enable RLS`).toBe(true); + expect( config.columns.some((column) => column.name === 'tenant_id' && column.notNull), `${config.name} needs a required tenant_id`, - ); - assert.deepEqual( + ).toBeTruthy(); + expect( config.policies.map((policy) => policy.for), - ['select', 'insert', 'update', 'delete'], `${config.name} needs complete CRUD policies`, - ); + ).toEqual(['select', 'insert', 'update', 'delete']); for (const policy of config.policies) { - assert.equal(policy.to, 'ontos_runtime'); + expect(policy.to).toBe('ontos_runtime'); } const tenantIdentity = [...config.uniqueConstraints, ...config.indexes].find((candidate) => { const columns = 'columns' in candidate ? candidate.columns : (candidate.config.columns ?? []); return columns.some((column) => 'name' in column && column.name === 'tenant_id'); }); - assert.ok(tenantIdentity, `${config.name} needs a tenant-qualified unique key`); + expect(tenantIdentity, `${config.name} needs a tenant-qualified unique key`).toBeTruthy(); + if (tenantIdentity === undefined) { + throw new Error(`${config.name} needs a tenant-qualified unique key`); + } } }); -test('models Party identity and assertion history without conflating effective and recorded time', () => { +it('models Party identity and assertion history without conflating effective and recorded time', () => { const partyConfig = configOf(parties); - assert.ok( + expect( partyConfig.columns.some((column) => column.name === 'current_display_name' && !column.notNull), - ); - assert.deepEqual(uniqueColumns(parties, 'party_parties_tenant_id_uk'), ['tenant_id', 'party_id']); - assert.deepEqual(partyConfig.checks.map((candidate) => candidate.name).toSorted(), [ + ).toBeTruthy(); + expect(uniqueColumns(parties, 'party_parties_tenant_id_uk')).toEqual(['tenant_id', 'party_id']); + expect(partyConfig.checks.map((candidate) => candidate.name).toSorted()).toEqual([ 'party_parties_display_name_ck', 'party_parties_revision_ck', 'party_parties_type_ck', @@ -227,49 +226,47 @@ test('models Party identity and assertion history without conflating effective a 'supersedes_assertion_id', 'retracts_assertion_id', ]) { - assert.ok( + expect( assertionConfig.columns.some((candidate) => candidate.name === column), column, - ); + ).toBeTruthy(); } - assert.equal( + expect( getTableName( foreignKey(partyFactAssertions, 'party_fact_assertions_tenant_party_fk').reference() .foreignTable, ), - getTableName(parties), - ); + ).toBe(getTableName(parties)); }); -test('keeps official assertions historical while exclusive claims own exact matching uniqueness', () => { - assert.deepEqual(uniqueColumns(partyIdentifierClaims, 'party_identifier_claims_exact_claim_uk'), [ +it('keeps official assertions historical while exclusive claims own exact matching uniqueness', () => { + expect(uniqueColumns(partyIdentifierClaims, 'party_identifier_claims_exact_claim_uk')).toEqual([ 'tenant_id', 'identifier_type_key', 'namespace', 'normalized_value', ]); - assert.deepEqual( + expect( configOf(partyOfficialIdentifiers) .checks.map((candidate) => candidate.name) .toSorted(), - [ - 'party_official_identifiers_external_evidence_ck', - 'party_official_identifiers_interval_ck', - 'party_official_identifiers_normalized_value_ck', - 'party_official_identifiers_state_ck', - 'party_official_identifiers_type_ck', - 'party_official_identifiers_verification_ck', - ], - ); + ).toEqual([ + 'party_official_identifiers_external_evidence_ck', + 'party_official_identifiers_interval_ck', + 'party_official_identifiers_normalized_value_ck', + 'party_official_identifiers_state_ck', + 'party_official_identifiers_type_ck', + 'party_official_identifiers_verification_ck', + ]); const claims = configOf(partyIdentifierClaims); - assert.ok( + expect( claims.indexes.some( (candidate) => candidate.config.name === 'party_identifier_claims_party_lookup_idx', ), - ); + ).toBeTruthy(); }); -test('preserves bounded external observation evidence separately from trusted actor and effective time', () => { +it('preserves bounded external observation evidence separately from trusted actor and effective time', () => { for (const table of [ partyFactAssertions, partyOfficialIdentifiers, @@ -277,20 +274,23 @@ test('preserves bounded external observation evidence separately from trusted ac partyContactPointPurposes, ]) { const config = configOf(table); - assert.ok( + expect( config.columns.some((column) => column.name === 'external_evidence' && !column.notNull), - ); + ).toBeTruthy(); const evidenceCheck = config.checks.find( (candidate) => candidate.name === `${config.name}_external_evidence_ck`, ); - assert.ok(evidenceCheck); + expect(evidenceCheck).toBeTruthy(); + if (evidenceCheck === undefined) { + throw new Error('Expected value to be present'); + } const evidenceSql = dialect.sqlToQuery(evidenceCheck.value).sql; - assert.match(evidenceSql, /observedAt/u); - assert.match(evidenceSql, /decidedAt/u); - assert.match(evidenceSql, /authorityPolicyKey/u); - assert.match(evidenceSql, /party_registry\.ares_enrichment/u); - assert.match(evidenceSql, /4096/u); - assert.doesNotMatch(evidenceSql, /validFrom|principalId|rawPayload/u); + expect(evidenceSql).toMatch(/observedAt/u); + expect(evidenceSql).toMatch(/decidedAt/u); + expect(evidenceSql).toMatch(/authorityPolicyKey/u); + expect(evidenceSql).toMatch(/party_registry\.ares_enrichment/u); + expect(evidenceSql).toMatch(/4096/u); + expect(evidenceSql).not.toMatch(/validFrom|principalId|rawPayload/u); } }); @@ -304,7 +304,7 @@ const checksOf = (table: (typeof configuredTables)[number]) => const checkSql = (checks: Readonly>, name: string) => checks[name] ?? ''; -test('models typed contact point lifecycles with owner-local references', () => { +it('models typed contact point lifecycles with owner-local references', () => { const contactChecks = checksOf(partyContactPoints); assert.match(checkSql(contactChecks, 'party_contact_points_shape_ck'), /EMAIL/u); assert.match(checkSql(contactChecks, 'party_contact_points_shape_ck'), /PHONE/u); @@ -351,7 +351,7 @@ test('models typed contact point lifecycles with owner-local references', () => assert.ok(preferredIndex?.config.where); }); -test('models contact point purpose lifecycles with owner-local references', () => { +it('models contact point purpose lifecycles with owner-local references', () => { const purposeConfig = configOf(partyContactPointPurposes); const purposeChecks = Object.fromEntries( purposeConfig.checks.map((candidate) => [ @@ -408,10 +408,10 @@ test('models contact point purpose lifecycles with owner-local references', () = ); }); -test('models relationship lifecycles with owner-local references', () => { +it('models relationship lifecycles with owner-local references', () => { const relationshipChecks = checksOf(partyRelationships); assert.match(checkSql(relationshipChecks, 'party_relationships_type_ck'), /CONTACT_PERSON_OF/u); - assert.doesNotMatch( + assert.notMatch( checkSql(relationshipChecks, 'party_relationships_type_ck'), /EMPLOYEE_OF|BRANCH_OF|OTHER/u, ); @@ -446,10 +446,7 @@ test('models relationship lifecycles with owner-local references', () => { checkSql(relationshipChecks, 'party_relationships_assertion_state_ck'), /ACTIVE.*SUPERSEDED.*RETRACTED.*DISPUTED/u, ); - assert.doesNotMatch( - checkSql(relationshipChecks, 'party_relationships_assertion_state_ck'), - /ENDED/u, - ); + assert.notMatch(checkSql(relationshipChecks, 'party_relationships_assertion_state_ck'), /ENDED/u); const relationshipIntervalIndex = configOf(partyRelationships).indexes.find( (candidate) => candidate.config.name === 'party_relationships_interval_idx', ); @@ -479,7 +476,7 @@ test('models relationship lifecycles with owner-local references', () => { ); }); -test('models Counterparty lifecycles with owner-local references', () => { +it('models Counterparty lifecycles with owner-local references', () => { assert.deepEqual(uniqueColumns(counterparties, 'party_counterparties_context_uk'), [ 'tenant_id', 'party_id', @@ -494,7 +491,7 @@ test('models Counterparty lifecycles with owner-local references', () => { const roleChecks = checksOf(counterpartyRolePeriods); assert.match(checkSql(roleChecks, 'party_counterparty_role_periods_type_ck'), /CUSTOMER/u); assert.match(checkSql(roleChecks, 'party_counterparty_role_periods_type_ck'), /SUPPLIER/u); - assert.doesNotMatch( + assert.notMatch( checkSql(roleChecks, 'party_counterparty_role_periods_type_ck'), /BUSINESS_PARTNER/u, ); @@ -530,83 +527,98 @@ test('models Counterparty lifecycles with owner-local references', () => { false, 'effective intervals, not an is_current unique index, own role-period uniqueness', ); - assert.doesNotMatch( + assert.notMatch( checkSql(roleChecks, 'party_counterparty_role_periods_state_ck'), /ACTIVE' and [^)]*is_current/u, ); }); -test('persists one recoverable match decision per Action and bounded duplicate review state', () => { - assert.deepEqual( - uniqueColumns(partyMatchDecisions, 'party_match_decisions_action_invocation_uk'), - ['tenant_id', 'action_invocation_id'], - ); +it('persists one recoverable match decision per Action and bounded duplicate review state', () => { + expect(uniqueColumns(partyMatchDecisions, 'party_match_decisions_action_invocation_uk')).toEqual([ + 'tenant_id', + 'action_invocation_id', + ]); const decisionChecks = checksOf(partyMatchDecisions); - assert.match(decisionChecks['party_match_decisions_outcome_ck'] ?? '', /CREATED/u); - assert.match(decisionChecks['party_match_decisions_outcome_ck'] ?? '', /MATCHED/u); - assert.match(decisionChecks['party_match_decisions_outcome_ck'] ?? '', /AMBIGUOUS/u); - assert.match(decisionChecks['party_match_decisions_outcome_ck'] ?? '', /NO_MATCH/u); - assert.ok(configOf(duplicateCandidateCases).columns.some((column) => column.name === 'revision')); + expect(decisionChecks['party_match_decisions_outcome_ck'] ?? '').toMatch(/CREATED/u); + expect(decisionChecks['party_match_decisions_outcome_ck'] ?? '').toMatch(/MATCHED/u); + expect(decisionChecks['party_match_decisions_outcome_ck'] ?? '').toMatch(/AMBIGUOUS/u); + expect(decisionChecks['party_match_decisions_outcome_ck'] ?? '').toMatch(/NO_MATCH/u); + expect( + configOf(duplicateCandidateCases).columns.some((column) => column.name === 'revision'), + ).toBeTruthy(); const activeCaseIndex = configOf(duplicateCandidateCases).indexes.find( (candidate) => candidate.config.name === 'party_duplicate_cases_fingerprint_uk', ); - assert.equal(activeCaseIndex?.config.unique, true); - assert.deepEqual(uniqueColumns(duplicateCandidateCases, 'party_duplicate_cases_fingerprint_uk'), [ + expect(activeCaseIndex?.config.unique).toBe(true); + expect(uniqueColumns(duplicateCandidateCases, 'party_duplicate_cases_fingerprint_uk')).toEqual([ 'tenant_id', 'evaluation_fingerprint', 'match_rule_version', ]); - assert.equal( + expect( getTableName( foreignKey(duplicateCandidateCases, 'party_duplicate_cases_prior_case_fk').reference() .foreignTable, ), - 'duplicate_candidate_cases', - ); - assert.ok(activeCaseIndex?.config.where); - assert.match(dialect.sqlToQuery(activeCaseIndex.config.where).sql, /OPEN.*NEEDS_EVIDENCE/u); + ).toBe('duplicate_candidate_cases'); + expect(activeCaseIndex?.config.where).toBeTruthy(); + if (activeCaseIndex?.config.where === undefined) { + throw new Error('Expected active case predicate'); + } + expect(dialect.sqlToQuery(activeCaseIndex.config.where).sql).toMatch(/OPEN.*NEEDS_EVIDENCE/u); const snapshotCheck = configOf(duplicateCandidateCases).checks.find( (candidate) => candidate.name === 'party_duplicate_cases_snapshot_ck', ); - assert.ok(snapshotCheck); - assert.match( - dialect.sqlToQuery(snapshotCheck.value).sql, + expect(snapshotCheck).toBeTruthy(); + if (snapshotCheck === undefined) { + throw new Error('Expected value to be present'); + } + expect(dialect.sqlToQuery(snapshotCheck.value).sql).toMatch( /provenance.*source.*method.*validFrom/u, ); - assert.equal( + expect( getTableName( foreignKey( duplicateCandidateCaseParties, 'party_duplicate_candidate_case_parties_tenant_party_fk', ).reference().foreignTable, ), - getTableName(parties), - ); + ).toBe(getTableName(parties)); }); -test('prepares append-only correction and non-executable merge records with safe aliases', () => { +it('prepares append-only correction and non-executable merge records with safe aliases', () => { const correctionConfig = configOf(partyCorrections); - assert.ok(correctionConfig.columns.some((column) => column.name === 'reason')); - assert.ok(correctionConfig.columns.some((column) => column.name === 'evidence_refs')); - assert.ok(correctionConfig.columns.some((column) => column.name === 'acting_principal_id')); - assert.ok(correctionConfig.columns.some((column) => column.name === 'approving_principal_id')); - assert.ok(correctionConfig.columns.some((column) => column.name === 'policy_version')); - assert.ok( + expect(correctionConfig.columns.some((column) => column.name === 'reason')).toBeTruthy(); + expect(correctionConfig.columns.some((column) => column.name === 'evidence_refs')).toBeTruthy(); + expect( + correctionConfig.columns.some((column) => column.name === 'acting_principal_id'), + ).toBeTruthy(); + expect( + correctionConfig.columns.some((column) => column.name === 'approving_principal_id'), + ).toBeTruthy(); + expect(correctionConfig.columns.some((column) => column.name === 'policy_version')).toBeTruthy(); + expect( correctionConfig.checks.some((candidate) => candidate.name === 'party_corrections_target_ck'), - ); + ).toBeTruthy(); const mergeStatus = configOf(partyMerges).checks.find( (candidate) => candidate.name === 'party_merges_status_ck', ); - assert.ok(mergeStatus); + expect(mergeStatus).toBeTruthy(); + if (mergeStatus === undefined) { + throw new Error('Expected value to be present'); + } const mergeStatusSql = dialect.sqlToQuery(mergeStatus.value).sql; - assert.match(mergeStatusSql, /PREPARED/u); - assert.match(mergeStatusSql, /BLOCKED/u); - assert.doesNotMatch(mergeStatusSql, /APPLIED|COMPLETED|EXECUTED/u); + expect(mergeStatusSql).toMatch(/PREPARED/u); + expect(mergeStatusSql).toMatch(/BLOCKED/u); + expect(mergeStatusSql).not.toMatch(/APPLIED|COMPLETED|EXECUTED/u); const preparedEvidence = configOf(partyMerges).checks.find( (candidate) => candidate.name === 'party_merges_prepared_evidence_ck', ); - assert.ok(preparedEvidence); + expect(preparedEvidence).toBeTruthy(); + if (preparedEvidence === undefined) { + throw new Error('Expected value to be present'); + } for (const field of [ 'version', 'confirmedDuplicateDecisionId', @@ -616,79 +628,90 @@ test('prepares append-only correction and non-executable merge records with safe 'selectionReason', 'selectionEvidenceChain', ]) { - assert.ok(dialect.sqlToQuery(preparedEvidence.value).sql.includes(field), field); + expect(dialect.sqlToQuery(preparedEvidence.value).sql.includes(field), field).toBeTruthy(); } - assert.deepEqual(uniqueColumns(partyAliases, 'party_aliases_alias_uk'), [ + expect(uniqueColumns(partyAliases, 'party_aliases_alias_uk')).toEqual([ 'tenant_id', 'alias_party_id', ]); - assert.ok( + expect( configOf(partyAliases).checks.some( (candidate) => candidate.name === 'party_aliases_not_self_ck', ), - ); + ).toBeTruthy(); }); -test('ships an independent Party migration with forced RLS and append-only correction evidence', async () => { - const drizzleConfig = await readFile( - new URL('../../drizzle.config.ts', import.meta.url), - 'utf-8', - ); - assert.match(drizzleConfig, /__drizzle_migrations_party/u); - assert.match(drizzleConfig, /\.\/src\/db\/schema\.ts/u); +it.effect( + 'ships an independent Party migration with forced RLS and append-only correction evidence', + () => + Effect.gen(function* testScenario() { + const drizzleConfig = yield* Effect.promise(() => + readFile(new URL('../../drizzle.config.ts', import.meta.url), 'utf-8'), + ); + expect(drizzleConfig).toMatch(/__drizzle_migrations_party/u); + expect(drizzleConfig).toMatch(/\.\/src\/db\/schema\.ts/u); - const migrationDirectory = new URL('../../drizzle/', import.meta.url); - const migrationDirectoryEntries = await readdir(migrationDirectory, { withFileTypes: true }); - const migrationFolders = migrationDirectoryEntries - .filter((entry) => entry.isDirectory()) - .map((entry) => entry.name) - .toSorted(); - assert.ok(migrationFolders.length >= 2); - const remediationFolder = migrationFolders.find((name) => name.endsWith('_nebulous_cardiac')); - assert.ok(remediationFolder); - const remediation = await readFile( - new URL(`${remediationFolder}/migration.sql`, migrationDirectory), - 'utf-8', - ); - assert.match(remediation, /party_match_decisions_create_result_ck/u); - assert.match(remediation, /committed_create_outcome/u); - const migration = await readFile( - new URL(`${migrationFolders[0] ?? ''}/migration.sql`, migrationDirectory), - 'utf-8', - ); - assert.equal( - migration.match(/ALTER TABLE "party"\."[^"]+" ENABLE ROW LEVEL SECURITY;/gu)?.length, - PARTY_TABLE_INVENTORY.length, - ); - assert.equal( - migration.match(/ALTER TABLE "party"\."[^"]+" FORCE ROW LEVEL SECURITY;/gu)?.length, - PARTY_TABLE_INVENTORY.length, - ); - assert.doesNotMatch(migration, /REFERENCES "(?:core|auth|contacts)"\./u); - assert.match(migration, /party_reject_correction_mutation/u); - assert.match(migration, /before update or delete on "party"\."party_corrections"/iu); - assert.match(migration, /CREATE EXTENSION IF NOT EXISTS btree_gist/iu); - assert.match( - migration, - /party_relationships_no_overlap_excl[\s\S]*EXCLUDE USING gist[\s\S]*tstzrange[\s\S]*-infinity[\s\S]*assertion_state[\s\S]*ACTIVE/iu, - ); - assert.match( - migration, - /party_counterparty_role_periods_no_overlap_excl[\s\S]*EXCLUDE USING gist[\s\S]*tstzrange/iu, - ); -}); + const migrationDirectory = new URL('../../drizzle/', import.meta.url); + const migrationDirectoryEntries = yield* Effect.promise(() => + readdir(migrationDirectory, { withFileTypes: true }), + ); + const migrationFolders = migrationDirectoryEntries + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .toSorted(); + expect(migrationFolders.length >= 2).toBeTruthy(); + const remediationFolder = migrationFolders.find((name) => name.endsWith('_nebulous_cardiac')); + expect(remediationFolder).toBeTruthy(); + if (remediationFolder === undefined) { + throw new Error('Expected value to be present'); + } + const remediation = yield* Effect.promise(() => + readFile(new URL(`${remediationFolder}/migration.sql`, migrationDirectory), 'utf-8'), + ); + expect(remediation).toMatch(/party_match_decisions_create_result_ck/u); + expect(remediation).toMatch(/committed_create_outcome/u); + const migration = yield* Effect.promise(() => + readFile( + new URL(`${migrationFolders[0] ?? ''}/migration.sql`, migrationDirectory), + 'utf-8', + ), + ); + expect( + migration.match(/ALTER TABLE "party"\."[^"]+" ENABLE ROW LEVEL SECURITY;/gu)?.length, + ).toBe(PARTY_TABLE_INVENTORY.length); + expect( + migration.match(/ALTER TABLE "party"\."[^"]+" FORCE ROW LEVEL SECURITY;/gu)?.length, + ).toBe(PARTY_TABLE_INVENTORY.length); + expect(migration).not.toMatch(/REFERENCES "(?:core|auth|contacts)"\./u); + expect(migration).toMatch(/party_reject_correction_mutation/u); + expect(migration).toMatch(/before update or delete on "party"\."party_corrections"/iu); + expect(migration).toMatch(/CREATE EXTENSION IF NOT EXISTS btree_gist/iu); + expect(migration).toMatch( + /party_relationships_no_overlap_excl[\s\S]*EXCLUDE USING gist[\s\S]*tstzrange[\s\S]*-infinity[\s\S]*assertion_state[\s\S]*ACTIVE/iu, + ); + expect(migration).toMatch( + /party_counterparty_role_periods_no_overlap_excl[\s\S]*EXCLUDE USING gist[\s\S]*tstzrange/iu, + ); + }), +); -test('registers Party ownership in application database grants and exact verification', async () => { - const bootstrap = await readFile( - new URL('../../../../scripts/postgres/bootstrap-runtime-role.mts', import.meta.url), - 'utf-8', - ); - const verifier = await readFile( - new URL('../../../../scripts/verify-application-db-schema.mts', import.meta.url), - 'utf-8', - ); - assert.match(bootstrap, /\['core', 'auth', 'contacts', 'party'\]/u); - assert.match(verifier, /\['auth', 'contacts', 'core', 'party'\]/u); - assert.match(verifier, /__drizzle_migrations_party/u); - assert.match(verifier, /verticals\/party-registry\/scripts\/verify-db-schema\.mts/u); -}); +it.effect('registers Party ownership in application database grants and exact verification', () => + Effect.gen(function* testScenario() { + const bootstrap = yield* Effect.promise(() => + readFile( + new URL('../../../../scripts/postgres/bootstrap-runtime-role.mts', import.meta.url), + 'utf-8', + ), + ); + const verifier = yield* Effect.promise(() => + readFile( + new URL('../../../../scripts/verify-application-db-schema.mts', import.meta.url), + 'utf-8', + ), + ); + expect(bootstrap).toMatch(/\['core', 'auth', 'contacts', 'party'\]/u); + expect(verifier).toMatch(/\['auth', 'contacts', 'core', 'party'\]/u); + expect(verifier).toMatch(/__drizzle_migrations_party/u); + expect(verifier).toMatch(/verticals\/party-registry\/scripts\/verify-db-schema\.mts/u); + }), +); diff --git a/app/verticals/party-registry/tests/unit/search-contract.test.ts b/app/verticals/party-registry/tests/unit/search-contract.test.ts index fb6471a3c..d180d5d97 100644 --- a/app/verticals/party-registry/tests/unit/search-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/search-contract.test.ts @@ -1,6 +1,6 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; + +import { Effect, Schema } from 'effect'; import { CounterpartiesProviderRequestSchema, CounterpartiesProviderResponseSchema, @@ -14,115 +14,123 @@ import { PARTY_SEARCH_SEMANTICS, } from '../../shared/domain/search-descriptor.ts'; -test('Party-owned search semantics expose only current approved V1 facts', () => { +it('Party-owned search semantics expose only current approved V1 facts', () => { const searchableFacts: readonly string[] = PARTY_SEARCH_SEMANTICS.searchableFacts; - assert.deepEqual(PARTY_SEARCH_SEMANTICS.searchableFacts, [ + expect(PARTY_SEARCH_SEMANTICS.searchableFacts).toEqual([ 'DISPLAY_NAME', 'ACTIVE_OFFICIAL_IDENTIFIER', 'ACTIVE_EMAIL', 'ACTIVE_PHONE', ]); - assert.equal(searchableFacts.includes('ADDRESS'), false); - assert.equal(searchableFacts.includes('HISTORICAL_IDENTIFIER'), false); - assert.equal(PARTY_SEARCH_SEMANTICS.contactPointIdentityAuthority, 'NON_UNIQUE'); - assert.equal(PARTY_SEARCH_SEMANTICS.resultMatchAuthority, 'NONE'); -}); - -test('Counterparty semantics retain Legal Entity and current-period role boundaries', () => { - assert.equal(COUNTERPARTY_SEARCH_SEMANTICS.legalEntityScope, 'REQUIRED_TRUSTED_CONTEXT'); - assert.deepEqual(COUNTERPARTY_SEARCH_SEMANTICS.roleFilters, ['CUSTOMER', 'SUPPLIER']); - assert.equal(COUNTERPARTY_SEARCH_SEMANTICS.rolePeriodSemantics, 'CURRENT_AT_EFFECTIVE_TIME'); - assert.equal(COUNTERPARTY_SEARCH_SEMANTICS.deduplicateBy, 'COUNTERPARTY_IDENTITY'); + expect(searchableFacts.includes('ADDRESS')).toBe(false); + expect(searchableFacts.includes('HISTORICAL_IDENTIFIER')).toBe(false); + expect(PARTY_SEARCH_SEMANTICS.contactPointIdentityAuthority).toBe('NON_UNIQUE'); + expect(PARTY_SEARCH_SEMANTICS.resultMatchAuthority).toBe('NONE'); }); -test('Party Search accepts a bounded query and an explicit archived switch', () => { - assert.deepEqual( - Schema.decodeUnknownSync(PartiesProviderRequestSchema)({ - includeArchived: true, - query: ' ACME ', - }), - { includeArchived: true, query: 'ACME' }, - ); - assert.throws(() => Schema.decodeUnknownSync(PartiesProviderRequestSchema)({ query: ' ' })); - assert.throws(() => - Schema.decodeUnknownSync(PartiesProviderRequestSchema)({ query: 'a'.repeat(201) }), - ); +it('Counterparty semantics retain Legal Entity and current-period role boundaries', () => { + expect(COUNTERPARTY_SEARCH_SEMANTICS.legalEntityScope).toBe('REQUIRED_TRUSTED_CONTEXT'); + expect(COUNTERPARTY_SEARCH_SEMANTICS.roleFilters).toEqual(['CUSTOMER', 'SUPPLIER']); + expect(COUNTERPARTY_SEARCH_SEMANTICS.rolePeriodSemantics).toBe('CURRENT_AT_EFFECTIVE_TIME'); + expect(COUNTERPARTY_SEARCH_SEMANTICS.deduplicateBy).toBe('COUNTERPARTY_IDENTITY'); }); -test('Counterparty Search exposes only the closed current-role filter', () => { - assert.deepEqual( - Schema.decodeUnknownSync(CounterpartiesProviderRequestSchema)({ - includeArchived: false, - query: 'ACME', - role: 'CUSTOMER', - }), - { includeArchived: false, query: 'ACME', role: 'CUSTOMER' }, - ); - assert.throws(() => - Schema.decodeUnknownSync(CounterpartiesProviderRequestSchema)({ - query: 'ACME', - role: 'BUSINESS_PARTNER', - }), - ); -}); +it.effect('Party Search accepts a bounded query and an explicit archived switch', () => + Effect.gen(function* testScenario() { + expect( + yield* Schema.decodeUnknownEffect(PartiesProviderRequestSchema)({ + includeArchived: true, + query: ' ACME ', + }), + ).toEqual({ includeArchived: true, query: 'ACME' }); + expect(() => + Schema.decodeUnknownSync(PartiesProviderRequestSchema)({ query: ' ' }), + ).toThrow(); + expect(() => + Schema.decodeUnknownSync(PartiesProviderRequestSchema)({ query: 'a'.repeat(201) }), + ).toThrow(); + }), +); -test('Party Search result is a minimal canonical projection without PII match evidence', () => { - const result = Schema.decodeUnknownSync(PartiesProviderResponseSchema)([ - { - archived: false, - matchedViaAlias: true, - ref: { - moduleId: 'party.registry', - resourceId: 'party-1', - resourceType: 'party.registry.party', - tenantId: '10000000-0000-4000-8000-000000000001', - }, - title: 'ACME', - }, - ]); +it.effect('Counterparty Search exposes only the closed current-role filter', () => + Effect.gen(function* testScenario() { + expect( + yield* Schema.decodeUnknownEffect(CounterpartiesProviderRequestSchema)({ + includeArchived: false, + query: 'ACME', + role: 'CUSTOMER', + }), + ).toEqual({ includeArchived: false, query: 'ACME', role: 'CUSTOMER' }); + expect(() => + Schema.decodeUnknownSync(CounterpartiesProviderRequestSchema)({ + query: 'ACME', + role: 'BUSINESS_PARTNER', + }), + ).toThrow(); + }), +); - assert.deepEqual(Object.keys(result[0] ?? {}).toSorted(), [ - 'archived', - 'matchedViaAlias', - 'ref', - 'title', - ]); - assert.equal('email' in (result[0] ?? {}), false); - assert.equal('identifier' in (result[0] ?? {}), false); - assert.equal('matchedValue' in (result[0] ?? {}), false); -}); - -test('Counterparty Search result distinguishes Counterparty and canonical Party', () => { - const tenantId = '10000000-0000-4000-8000-000000000001'; - const result = Schema.decodeUnknownSync(CounterpartiesProviderResponseSchema)([ - { - currentRoles: ['CUSTOMER', 'SUPPLIER'], - legalEntity: { - legalEntityId: '20000000-0000-4000-8000-000000000002', - tenantId, - }, - party: { +it.effect('Party Search result is a minimal canonical projection without PII match evidence', () => + Effect.gen(function* testScenario() { + const result = yield* Schema.decodeUnknownEffect(PartiesProviderResponseSchema)([ + { archived: false, - matchedViaAlias: false, + matchedViaAlias: true, ref: { moduleId: 'party.registry', resourceId: 'party-1', resourceType: 'party.registry.party', - tenantId, + tenantId: '10000000-0000-4000-8000-000000000001', }, title: 'ACME', }, - ref: { - moduleId: 'party.registry', - resourceId: 'counterparty-1', - resourceType: 'party.registry.counterparty', - tenantId, + ]); + + expect(Object.keys(result[0] ?? {}).toSorted()).toEqual([ + 'archived', + 'matchedViaAlias', + 'ref', + 'title', + ]); + expect('email' in (result[0] ?? {})).toBe(false); + expect('identifier' in (result[0] ?? {})).toBe(false); + expect('matchedValue' in (result[0] ?? {})).toBe(false); + }), +); + +it.effect('Counterparty Search result distinguishes Counterparty and canonical Party', () => + Effect.gen(function* testScenario() { + const tenantId = '10000000-0000-4000-8000-000000000001'; + const result = yield* Schema.decodeUnknownEffect(CounterpartiesProviderResponseSchema)([ + { + currentRoles: ['CUSTOMER', 'SUPPLIER'], + legalEntity: { + legalEntityId: '20000000-0000-4000-8000-000000000002', + tenantId, + }, + party: { + archived: false, + matchedViaAlias: false, + ref: { + moduleId: 'party.registry', + resourceId: 'party-1', + resourceType: 'party.registry.party', + tenantId, + }, + title: 'ACME', + }, + ref: { + moduleId: 'party.registry', + resourceId: 'counterparty-1', + resourceType: 'party.registry.counterparty', + tenantId, + }, }, - }, - ]); + ]); - assert.equal(result[0]?.ref.resourceType, 'party.registry.counterparty'); - assert.equal(result[0]?.party.ref.resourceType, 'party.registry.party'); - assert.equal('email' in (result[0]?.party ?? {}), false); - assert.equal('phone' in (result[0]?.party ?? {}), false); -}); + expect(result[0]?.ref.resourceType).toBe('party.registry.counterparty'); + expect(result[0]?.party.ref.resourceType).toBe('party.registry.party'); + expect('email' in (result[0]?.party ?? {})).toBe(false); + expect('phone' in (result[0]?.party ?? {})).toBe(false); + }), +); diff --git a/app/verticals/party-registry/tests/unit/search-core-adapter.test.ts b/app/verticals/party-registry/tests/unit/search-core-adapter.test.ts index 7a27178c3..3d56c6066 100644 --- a/app/verticals/party-registry/tests/unit/search-core-adapter.test.ts +++ b/app/verticals/party-registry/tests/unit/search-core-adapter.test.ts @@ -1,7 +1,6 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; + +import { Effect, Schema, Predicate } from 'effect'; import { CoreSearchProjectionHitSchema } from '@app/core-runtime'; import type { CoreSearchQueryRuntimeService } from '@app/core-runtime'; import { makePartySearchProjectionGateway } from '../../src/search/parties.provider.ts'; @@ -59,42 +58,42 @@ const wrongResourceHit = Schema.decodeUnknownSync(CoreSearchProjectionHitSchema) title: 'Wrong', }); -test('Party adapter queries only the Core-owned Party projection and maps alias context', () => - runEffectTestPromise( - Effect.gen(function* partyAdapterQuery() { - const calls: unknown[] = []; - const core: CoreSearchQueryRuntimeService = { - search: (input) => { - calls.push(input); - return Effect.succeed([partyAliasHit]); - }, - }; +it.effect('Party adapter queries only the Core-owned Party projection and maps alias context', () => + Effect.gen(function* partyAdapterQuery() { + const calls: unknown[] = []; + const core: CoreSearchQueryRuntimeService = { + search: (input) => { + calls.push(input); + return Effect.succeed([partyAliasHit]); + }, + }; - const gateway = makePartySearchProjectionGateway(core); - const hits = yield* gateway.searchParties({ includeArchived: true, query: 'ACME', tenantId }); + const gateway = makePartySearchProjectionGateway(core); + const hits = yield* gateway.searchParties({ includeArchived: true, query: 'ACME', tenantId }); - assert.deepEqual(calls, [ - { - includeArchived: true, - moduleId: 'party.registry', - query: 'ACME', - resourceType: 'party.registry.party', - tenantId, - }, - ]); - assert.deepEqual(hits, [ - { - archived: false, - canonicalPartyRef: partyRef('survivor'), - matchedPartyRef: partyRef('absorbed'), - title: 'ACME', - }, - ]); - }), - )); + expect(calls).toEqual([ + { + includeArchived: true, + moduleId: 'party.registry', + query: 'ACME', + resourceType: 'party.registry.party', + tenantId, + }, + ]); + expect(hits).toEqual([ + { + archived: false, + canonicalPartyRef: partyRef('survivor'), + matchedPartyRef: partyRef('absorbed'), + title: 'ACME', + }, + ]); + }), +); -test('Counterparty adapter uses trusted Legal Entity, effective time, role facet and safe periods', () => - runEffectTestPromise( +it.effect( + 'Counterparty adapter uses trusted Legal Entity, effective time, role facet and safe periods', + () => Effect.gen(function* counterpartyAdapterQuery() { const calls: unknown[] = []; const core: CoreSearchQueryRuntimeService = { @@ -114,7 +113,7 @@ test('Counterparty adapter uses trusted Legal Entity, effective time, role facet tenantId, }); - assert.deepEqual(calls, [ + expect(calls).toEqual([ { effectiveAt, facets: [{ key: 'current-role', values: ['CUSTOMER'] }], @@ -126,7 +125,7 @@ test('Counterparty adapter uses trusted Legal Entity, effective time, role facet tenantId, }, ]); - assert.deepEqual(hits, [ + expect(hits).toEqual([ { canonicalPartyRef: partyRef('survivor'), counterpartyRef: counterpartyRef('cp-1'), @@ -144,10 +143,11 @@ test('Counterparty adapter uses trusted Legal Entity, effective time, role facet }, ]); }), - )); +); -test('Party adapter fails closed when a generic projection returns the wrong resource contract', () => - runEffectTestPromise( +it.effect( + 'Party adapter fails closed when a generic projection returns the wrong resource contract', + () => Effect.gen(function* invalidProjectionContract() { const core: CoreSearchQueryRuntimeService = { search: () => Effect.succeed([wrongResourceHit]), @@ -160,6 +160,6 @@ test('Party adapter fails closed when a generic projection returns the wrong res tenantId, }), ); - assert.equal(failure._tag, 'Failure'); + expect(Predicate.isTagged(failure, 'Failure')).toBeTruthy(); }), - )); +); diff --git a/app/verticals/party-registry/tests/unit/search-identifier-sync.test.ts b/app/verticals/party-registry/tests/unit/search-identifier-sync.test.ts index baed0f539..11027f8e0 100644 --- a/app/verticals/party-registry/tests/unit/search-identifier-sync.test.ts +++ b/app/verticals/party-registry/tests/unit/search-identifier-sync.test.ts @@ -1,6 +1,5 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; + import { DateTime, Effect, Option, Schema } from 'effect'; import { makeCoreSearchIngestion, @@ -101,8 +100,8 @@ const makeSearchFixture = (identifiers: readonly PartySearchSourceValue[]) => { { load: (context, target) => Effect.sync(() => { - assert.equal(context.tenantId, tenantId); - assert.deepEqual(target, { partyId: partyRef.resourceId }); + expect(context.tenantId).toBe(tenantId); + expect(target).toEqual({ partyId: partyRef.resourceId }); return canonical; }), }, @@ -130,7 +129,7 @@ const makeSearchFixture = (identifiers: readonly PartySearchSourceValue[]) => { workerKey: descriptor.workerKey, }).pipe(Effect.provideService(PartySearchProjector, projector)); } else { - assert.equal(message.topic, 'party.registry.official-identifier-updated.v1'); + expect(message.topic).toBe('party.registry.official-identifier-updated.v1'); const { descriptor } = projectOfficialIdentifierUpdatedToSearchWorker; const payload = yield* Schema.decodeUnknownEffect(descriptor.payloadSchema)( message.payloadJson, @@ -159,42 +158,46 @@ const makeSearchFixture = (identifiers: readonly PartySearchSourceValue[]) => { }; const assertIdentifierOutbox = ( - harness: ReturnType, + harness: Effect.Success>, eventType: string, ) => { const [commit] = harness.snapshot().committed; - assert.ok(commit); - assert.equal(commit.evidence.outboxMessages.length, 1); + expect(commit).toBeTruthy(); + if (commit === undefined) { + throw new Error('Expected value to be present'); + } + expect(commit.evidence.outboxMessages.length).toBe(1); const [outbox] = commit.evidence.outboxMessages; - assert.ok(outbox); - assert.equal(commit.evidence.domainEvents[outbox.domainEventIndex]?.eventType, eventType); - assert.deepEqual(outbox.message.payloadJson, { officialIdentifierRef, partyRef }); + expect(outbox).toBeTruthy(); + if (outbox === undefined) { + throw new Error('Expected value to be present'); + } + expect(commit.evidence.domainEvents[outbox.domainEventIndex]?.eventType).toBe(eventType); + expect(outbox.message.payloadJson).toEqual({ officialIdentifierRef, partyRef }); return outbox; }; const assertAttachedIdentifierDelivery = ( - harness: ReturnType, + harness: Effect.Success>, search: ReturnType, ) => Effect.gen(function* verifyCommittedIdentifierDelivery() { const outbox = assertIdentifierOutbox(harness, 'party.registry.official-identifier-added.v1'); - assert.deepEqual(yield* search.query(), []); + expect(yield* search.query()).toEqual([]); yield* search.deliver(outbox.message); const hits = yield* search.query(); - assert.deepEqual( - hits.map((hit) => hit.ref), - [partyRef], - ); + expect(hits.map((hit) => hit.ref)).toEqual([partyRef]); yield* search.deliver(outbox.message); - assert.deepEqual(yield* search.query(), hits); + expect(yield* search.query()).toEqual(hits); }); -test('CreateParty MATCHED_EXISTING publishes an attached identifier and indexes it after delivery only', () => - runEffectTestPromise( +it.effect( + 'CreateParty MATCHED_EXISTING publishes an attached identifier and indexes it after delivery only', + () => Effect.gen(function* matchedExistingCreateScenario() { const search = makeSearchFixture([]); yield* search.seed; - const harness = makeActionTestHarness({ + const harness = yield* makeActionTestHarness({ actionPermission: 'allowed', services: [ bindActionTestServices(createPartyAction, { @@ -228,17 +231,18 @@ test('CreateParty MATCHED_EXISTING publishes an attached identifier and indexes registration: createPartyAction, transport: { correlationId: 'identifier-sync', idempotencyKey: 'match-identifier-1' }, }); - assert.deepEqual(result, { decisionRef, outcome: 'MATCHED_EXISTING', partyRef }); + expect(result).toEqual({ decisionRef, outcome: 'MATCHED_EXISTING', partyRef }); yield* assertAttachedIdentifierDelivery(harness, search); }), - )); +); -test('reviewed MATCH_EXISTING publishes an attached identifier and indexes it after delivery only', () => - runEffectTestPromise( +it.effect( + 'reviewed MATCH_EXISTING publishes an attached identifier and indexes it after delivery only', + () => Effect.gen(function* reviewedMatchScenario() { const search = makeSearchFixture([]); yield* search.seed; - const harness = makeActionTestHarness({ + const harness = yield* makeActionTestHarness({ actionPermission: 'allowed', services: [ bindActionTestServices(resolveDuplicateCandidateMatchAction, { @@ -271,7 +275,7 @@ test('reviewed MATCH_EXISTING publishes an attached identifier and indexes it af idempotencyKey: 'review-match-identifier-1', }, }); - assert.deepEqual(result, { + expect(result).toEqual({ caseRef, decisionRef, lifecycleState: 'RESOLVED', @@ -280,10 +284,11 @@ test('reviewed MATCH_EXISTING publishes an attached identifier and indexes it af }); yield* assertAttachedIdentifierDelivery(harness, search); }), - )); +); -test('END_VALIDITY refreshes search only after its committed identifier message and remains replay-safe', () => - runEffectTestPromise( +it.effect( + 'END_VALIDITY refreshes search only after its committed identifier message and remains replay-safe', + () => Effect.gen(function* endIdentifierSearchScenario() { const search = makeSearchFixture([identifier]); yield* search.seed; @@ -296,7 +301,7 @@ test('END_VALIDITY refreshes search only after its committed identifier message verifiedByPrincipalId: null, } as const; const after = { ...before, state: 'ENDED', validTo } as const; - const harness = makeActionTestHarness({ + const harness = yield* makeActionTestHarness({ actionPermission: 'allowed', services: [ bindActionTestServices(updatePartyOfficialIdentifierAction, { @@ -333,10 +338,10 @@ test('END_VALIDITY refreshes search only after its committed identifier message harness, 'party.registry.official-identifier-updated.v1', ); - assert.equal((yield* search.query()).length, 1); + expect((yield* search.query()).length).toBe(1); yield* search.deliver(outbox.message); - assert.deepEqual(yield* search.query(), []); + expect(yield* search.query()).toEqual([]); yield* search.deliver(outbox.message); - assert.deepEqual(yield* search.query(), []); + expect(yield* search.query()).toEqual([]); }), - )); +); diff --git a/app/verticals/party-registry/tests/unit/search-projector.test.ts b/app/verticals/party-registry/tests/unit/search-projector.test.ts index da8018fa6..4c56cd152 100644 --- a/app/verticals/party-registry/tests/unit/search-projector.test.ts +++ b/app/verticals/party-registry/tests/unit/search-projector.test.ts @@ -1,7 +1,7 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Exit, Match } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { TestClock } from 'effect/testing'; + +import { Effect, Exit, Match, Predicate } from 'effect'; import { createCoreSearchQueryRuntime, makeCoreSearchIngestion, @@ -68,8 +68,9 @@ const snapshot: PartySearchSourceSnapshot = { removedRefs: [], tenantId, }; -test('post-commit projection makes only active permission-safe identity evidence searchable', () => - runEffectTestPromise( +it.effect( + 'post-commit projection makes only active permission-safe identity evidence searchable', + () => Effect.gen(function* testScenario() { const documents = yield* buildPartySearchDocuments(snapshot); const store = makeInMemoryCoreSearchProjectionStore(); @@ -91,20 +92,24 @@ test('post-commit projection makes only active permission-safe identity evidence }); const publicHits = yield* query('public@example.test'); const identifierHits = yield* query('12345678'); - assert.equal(publicHits.length, 1); - assert.equal(identifierHits.length, 1); - assert.deepEqual(yield* query('private@example.test'), []); - assert.deepEqual(yield* query('+420123456789'), []); - assert.deepEqual(publicHits, [ + expect(publicHits.length).toBe(1); + expect(identifierHits.length).toBe(1); + expect(yield* query('private@example.test')).toEqual([]); + expect(yield* query('+420123456789')).toEqual([]); + expect(publicHits).toEqual([ { archived: false, facets: [], metadata: [], ref: partyRef, title: 'ACME' }, ]); }), - )); -test('aliases collapse to canonical identity and only alias-only evidence labels the match', () => - runEffectTestPromise( +); +it.effect( + 'aliases collapse to canonical identity and only alias-only evidence labels the match', + () => Effect.gen(function* testScenario() { const [party] = snapshot.parties; - assert.ok(party); + expect(party).toBeTruthy(); + if (party === undefined) { + throw new Error('Expected value to be present'); + } const store = makeInMemoryCoreSearchProjectionStore(); const documents = yield* buildPartySearchDocuments({ ...snapshot, @@ -139,13 +144,13 @@ test('aliases collapse to canonical identity and only alias-only evidence labels tenantId, }); const alias = yield* query('Old Company'); - assert.equal(alias.length, 1); - assert.deepEqual(alias[0]?.ref, partyRef); - assert.equal(alias[0]?.matchedRef?.resourceId, 'absorbed'); + expect(alias.length).toBe(1); + expect(alias[0]?.ref).toEqual(partyRef); + expect(alias[0]?.matchedRef?.resourceId).toBe('absorbed'); const canonicalHits = yield* query('ACME'); - assert.equal(canonicalHits[0]?.matchedRef, undefined); + expect(canonicalHits[0]?.matchedRef).toBe(undefined); }), - )); +); const context: OutboxWorkerHandlerContext = { attemptNumber: 1, claimId: 'claim', @@ -158,8 +163,9 @@ const context: OutboxWorkerHandlerContext = { topic: 'party.registry.party-updated.v1', workerKey: 'party.registry.project-party-updated-to-search', }; -test('snapshot-generation replay is idempotent, archive/unarchive refreshes and older delivery cannot resurrect a tombstone', () => - runEffectTestPromise( +it.effect( + 'snapshot-generation replay is idempotent, archive/unarchive refreshes and older delivery cannot resurrect a tombstone', + () => Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); let current = snapshot; @@ -185,7 +191,7 @@ test('snapshot-generation replay is idempotent, archive/unarchive refreshes and yield* deliver(); yield* deliver(); const replayHits = yield* query(); - assert.equal(replayHits.length, 1); + expect(replayHits.length).toBe(1); current = { ...snapshot, parties: snapshot.parties.map((party) => ({ @@ -195,16 +201,16 @@ test('snapshot-generation replay is idempotent, archive/unarchive refreshes and projectionVersion: '8', }; yield* deliver(); - assert.deepEqual(yield* query(), []); + expect(yield* query()).toEqual([]); const archivedHits = yield* query(true); - assert.equal(archivedHits[0]?.archived, true); + expect(archivedHits[0]?.archived).toBe(true); current = { ...snapshot, projectionVersion: '9', }; yield* deliver(); const unarchivedHits = yield* query(); - assert.equal(unarchivedHits.length, 1); + expect(unarchivedHits.length).toBe(1); current = { ...snapshot, parties: [], @@ -214,14 +220,18 @@ test('snapshot-generation replay is idempotent, archive/unarchive refreshes and yield* deliver(); current = snapshot; yield* deliver(); - assert.deepEqual(yield* query(true), []); + expect(yield* query(true)).toEqual([]); }), - )); -test('future-ended contact disappears at its period boundary without another lifecycle message', () => - runEffectTestPromise( +); +it.effect( + 'future-ended contact disappears at its period boundary without another lifecycle message', + () => Effect.gen(function* testScenario() { const [party] = snapshot.parties; - assert.ok(party); + expect(party).toBeTruthy(); + if (party === undefined) { + throw new Error('Expected value to be present'); + } const store = makeInMemoryCoreSearchProjectionStore(); const documents = yield* buildPartySearchDocuments({ ...snapshot, @@ -257,16 +267,20 @@ test('future-ended contact disappears at its period boundary without another lif tenantId, }); const currentHits = yield* query('2026-09-03T00:00:00.000Z'); - assert.equal(currentHits.length, 1); - assert.deepEqual(yield* query('2026-09-04T00:00:00.000Z'), []); + expect(currentHits.length).toBe(1); + expect(yield* query('2026-09-04T00:00:00.000Z')).toEqual([]); }), - )); -test('Counterparty identity survives aliases, current-role expiry and canonical-party collisions', () => - runEffectTestPromise( +); +it.effect( + 'Counterparty identity survives aliases, current-role expiry and canonical-party collisions', + () => Effect.gen(function* testScenario() { const legalEntityId = '20000000-0000-4000-8000-000000000002'; const [party] = snapshot.parties; - assert.ok(party); + expect(party).toBeTruthy(); + if (party === undefined) { + throw new Error('Expected value to be present'); + } const aliasRef = { ...partyRef, resourceId: 'absorbed', @@ -322,36 +336,37 @@ test('Counterparty identity survives aliases, current-role expiry and canonical- tenantId, }; const hits = yield* gateway.searchCounterparties(input); - assert.equal(hits.length, 2); - assert.deepEqual( - hits.map((hit) => hit.counterpartyRef.resourceId), - ['cp-1', 'cp-2'], - ); + expect(hits.length).toBe(2); + expect(hits.map((hit) => hit.counterpartyRef.resourceId)).toEqual(['cp-1', 'cp-2']); const normalized = normalizeCounterpartySearchHits(input, hits); const normalizedItems = Match.value(normalized).pipe( Match.tag('SearchResults', ({ items }) => items), Match.tag('SearchProjectionViolation', ({ reason }) => - assert.fail(`Expected normalized search results: ${reason}`), + expect.unreachable(`Expected normalized search results: ${reason}`), ), Match.exhaustive, ); - assert.equal(normalizedItems[0]?.collision?.kind, 'CANONICAL_PARTY_COUNTERPARTY_COLLISION'); - assert.equal(normalizedItems[0]?.party.matchedViaAlias, true); - assert.deepEqual( + expect(normalizedItems[0]?.collision?.kind).toBe('CANONICAL_PARTY_COUNTERPARTY_COLLISION'); + expect(normalizedItems[0]?.party.matchedViaAlias).toBe(true); + expect( yield* gateway.searchCounterparties({ ...input, effectiveAt: '2026-10-01T00:00:00.000Z', }), - [], - ); + ).toEqual([]); }), - )); -test('shared public contact returns multiple Parties without uniqueness or matching authority', () => - runEffectTestPromise( +); +it.effect( + 'shared public contact returns multiple Parties without uniqueness or matching authority', + () => Effect.gen(function* testScenario() { + yield* TestClock.setTime(Date.parse('2026-09-03T00:00:00.000Z')); const store = makeInMemoryCoreSearchProjectionStore(); const [party] = snapshot.parties; - assert.ok(party); + expect(party).toBeTruthy(); + if (party === undefined) { + throw new Error('Expected value to be present'); + } const documents = yield* buildPartySearchDocuments({ ...snapshot, parties: [ @@ -378,14 +393,12 @@ test('shared public contact returns multiple Parties without uniqueness or match resourceType: 'party.registry.party', tenantId, }); - assert.deepEqual( - hits.map((hit) => hit.ref.resourceId), - ['party-1', 'party-2'], - ); + expect(hits.map((hit) => hit.ref.resourceId)).toEqual(['party-1', 'party-2']); }), - )); -test('rebuild reconciles omitted documents and preserves tombstones against stale lifecycle delivery', () => - runEffectTestPromise( +); +it.effect( + 'rebuild reconciles omitted documents and preserves tombstones against stale lifecycle delivery', + () => Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); let current = snapshot; @@ -414,7 +427,7 @@ test('rebuild reconciles omitted documents and preserves tombstones against stal yield* projector.project(context, { partyId: 'party-1', }); - assert.deepEqual( + expect( yield* createCoreSearchQueryRuntime(store).search({ includeArchived: true, moduleId: 'party.registry', @@ -422,12 +435,12 @@ test('rebuild reconciles omitted documents and preserves tombstones against stal resourceType: 'party.registry.party', tenantId, }), - [], - ); + ).toEqual([]); }), - )); -test('source failure is sanitized and leaves previously searchable state intact for retry', () => - runEffectTestPromise( +); +it.effect( + 'source failure is sanitized and leaves previously searchable state intact for retry', + () => Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); let fail = false; @@ -462,15 +475,19 @@ test('source failure is sanitized and leaves previously searchable state intact resourceType: 'party.registry.party', tenantId, }); - assert.equal(failure._tag, 'Failure'); - assert.equal(priorHits.length, 1); + expect(Predicate.isTagged(failure, 'Failure')).toBeTruthy(); + expect(priorHits.length).toBe(1); }), - )); -test('zero-length cancelled periods are never searchable and do not poison projection delivery', () => - runEffectTestPromise( +); +it.effect( + 'zero-length cancelled periods are never searchable and do not poison projection delivery', + () => Effect.gen(function* testScenario() { const [party] = snapshot.parties; - assert.ok(party); + expect(party).toBeTruthy(); + if (party === undefined) { + throw new Error('Expected value to be present'); + } const result = yield* Effect.exit( buildPartySearchDocuments({ ...snapshot, @@ -510,53 +527,58 @@ test('zero-length cancelled periods are never searchable and do not poison proje ], }), ); - assert.ok(Exit.isSuccess(result)); - assert.deepEqual( + expect(Exit.isSuccess(result)).toBeTruthy(); + if (!Exit.isSuccess(result)) { + throw new Error('Expected value to be present'); + } + expect( result.value[0]?.temporalSearchableText?.filter((entry) => entry.value === 'cancelled'), - [], - ); - assert.deepEqual(result.value[1]?.temporalFacets, []); - }), - )); -test('projection generation is independent of an out-of-order business event sequence', () => - runEffectTestPromise( - Effect.gen(function* testScenario() { - const store = makeInMemoryCoreSearchProjectionStore(); - const projector = makePartySearchProjector( - { - load: () => - Effect.succeed({ - ...snapshot, - projectionVersion: '1', - }), - }, - makeCoreSearchIngestion(store), - store, - ); - yield* projector.project( - { - ...context, - tenantSequenceNo: 999n, - }, - { - partyId: 'party-1', - }, - ); - const hits = yield* createCoreSearchQueryRuntime(store).search({ - includeArchived: false, - moduleId: 'party.registry', - query: 'ACME', - resourceType: 'party.registry.party', - tenantId, - }); - assert.equal(hits.length, 1); + ).toEqual([]); + expect(result.value[1]?.temporalFacets).toEqual([]); }), - )); -test('correction and identifier/contact changes replace obsolete evidence instead of accumulating history', () => - runEffectTestPromise( +); +it.effect('projection generation is independent of an out-of-order business event sequence', () => + Effect.gen(function* testScenario() { + const store = makeInMemoryCoreSearchProjectionStore(); + const projector = makePartySearchProjector( + { + load: () => + Effect.succeed({ + ...snapshot, + projectionVersion: '1', + }), + }, + makeCoreSearchIngestion(store), + store, + ); + yield* projector.project( + { + ...context, + tenantSequenceNo: 999n, + }, + { + partyId: 'party-1', + }, + ); + const hits = yield* createCoreSearchQueryRuntime(store).search({ + includeArchived: false, + moduleId: 'party.registry', + query: 'ACME', + resourceType: 'party.registry.party', + tenantId, + }); + expect(hits.length).toBe(1); + }), +); +it.effect( + 'correction and identifier/contact changes replace obsolete evidence instead of accumulating history', + () => Effect.gen(function* testScenario() { const [party] = snapshot.parties; - assert.ok(party); + expect(party).toBeTruthy(); + if (party === undefined) { + throw new Error('Expected value to be present'); + } const store = makeInMemoryCoreSearchProjectionStore(); let current = snapshot; const projector = makePartySearchProjector( @@ -598,15 +620,16 @@ test('correction and identifier/contact changes replace obsolete evidence instea resourceType: 'party.registry.party', tenantId, }); - assert.deepEqual(yield* query('ACME'), []); - assert.deepEqual(yield* query('12345678'), []); - assert.deepEqual(yield* query('public@example.test'), []); + expect(yield* query('ACME')).toEqual([]); + expect(yield* query('12345678')).toEqual([]); + expect(yield* query('public@example.test')).toEqual([]); const corrected = yield* query('Corrected Company'); - assert.equal(corrected.length, 1); + expect(corrected.length).toBe(1); }), - )); -test('a complete empty rebuild also rejects delayed evidence for a never-before-indexed Party', () => - runEffectTestPromise( +); +it.effect( + 'a complete empty rebuild also rejects delayed evidence for a never-before-indexed Party', + () => Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); let current: PartySearchSourceSnapshot = { @@ -635,6 +658,6 @@ test('a complete empty rebuild also rejects delayed evidence for a never-before- resourceType: 'party.registry.party', tenantId, }); - assert.deepEqual(hits, []); + expect(hits).toEqual([]); }), - )); +); diff --git a/app/verticals/party-registry/tests/unit/search-provider.test.ts b/app/verticals/party-registry/tests/unit/search-provider.test.ts index 64d9276fb..5f5a4f4ef 100644 --- a/app/verticals/party-registry/tests/unit/search-provider.test.ts +++ b/app/verticals/party-registry/tests/unit/search-provider.test.ts @@ -1,6 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { assert, expect, it } from 'effect-rstest'; import { Effect } from 'effect'; import type { PartySearchProjectionGatewayService } from '../../shared/domain/search-projection-gateway.ts'; import { @@ -12,49 +10,51 @@ import { loadPartySearch, partiesRead } from '../../src/search/parties.provider. const tenantId = '10000000-0000-4000-8000-000000000001'; const legalEntityId = '20000000-0000-4000-8000-000000000002'; -test('Party provider declares optional Legal Entity context and tenant Party-read authority', () => { - assert.equal(partiesRead.descriptor.accessKind, 'search'); - assert.equal(partiesRead.descriptor.legalEntityScope, 'optional'); - assert.equal(partiesRead.descriptor.permissionTarget, 'tenant'); +it('Party provider declares optional Legal Entity context and tenant Party-read authority', () => { + expect(partiesRead.descriptor.accessKind).toBe('search'); + expect(partiesRead.descriptor.legalEntityScope).toBe('optional'); + expect(partiesRead.descriptor.permissionTarget).toBe('tenant'); }); -test('Counterparty provider requires trusted Legal Entity context and candidate authorization', () => { - assert.equal(counterpartiesRead.descriptor.accessKind, 'search'); - assert.equal(counterpartiesRead.descriptor.legalEntityScope, 'required'); - assert.equal(counterpartiesRead.descriptor.permissionTarget, 'legal_entity'); +it('Counterparty provider requires trusted Legal Entity context and candidate authorization', () => { + expect(counterpartiesRead.descriptor.accessKind).toBe('search'); + expect(counterpartiesRead.descriptor.legalEntityScope).toBe('required'); + expect(counterpartiesRead.descriptor.permissionTarget).toBe('legal_entity'); }); -test('Party provider sends only trusted tenant scope to the Core projection gateway', () => - runEffectTestPromise( - Effect.gen(function* trustedPartyScope() { - const calls: unknown[] = []; - const gateway: PartySearchProjectionGatewayService = { - searchCounterparties: () => Effect.succeed([]), - searchParties: (input) => { +it.effect('Party provider sends only trusted tenant scope to the Core projection gateway', () => + Effect.gen(function* trustedPartyScope() { + const calls: unknown[] = []; + const gateway: PartySearchProjectionGatewayService = { + searchCounterparties: () => Effect.succeed([]), + searchParties: (input) => + Effect.sync(() => { calls.push(input); - return Effect.succeed([]); - }, - }; + return []; + }), + }; - const result = yield* loadPartySearch( - gateway, - { tenantId }, - { includeArchived: true, query: 'ACME' }, - ); - assert.deepEqual(result, []); - assert.deepEqual(calls, [{ includeArchived: true, query: 'ACME', tenantId }]); - }), - )); + const result = yield* loadPartySearch( + gateway, + { tenantId }, + { includeArchived: true, query: 'ACME' }, + ); + expect(result).toEqual([]); + expect(calls).toEqual([{ includeArchived: true, query: 'ACME', tenantId }]); + }), +); -test('Counterparty provider derives Legal Entity from trusted scope and never from payload', () => - runEffectTestPromise( +it.effect( + 'Counterparty provider derives Legal Entity from trusted scope and never from payload', + () => Effect.gen(function* trustedCounterpartyScope() { const calls: unknown[] = []; const gateway: PartySearchProjectionGatewayService = { - searchCounterparties: (input) => { - calls.push(input); - return Effect.succeed([]); - }, + searchCounterparties: (input) => + Effect.sync(() => { + calls.push(input); + return []; + }), searchParties: () => Effect.succeed([]), }; @@ -64,8 +64,8 @@ test('Counterparty provider derives Legal Entity from trusted scope and never fr { includeArchived: false, query: 'ACME', role: 'SUPPLIER' }, '2026-09-03T12:00:00.000Z', ); - assert.deepEqual(result, []); - assert.deepEqual(calls, [ + expect(result).toEqual([]); + expect(calls).toEqual([ { effectiveAt: '2026-09-03T12:00:00.000Z', includeArchived: false, @@ -76,10 +76,11 @@ test('Counterparty provider derives Legal Entity from trusted scope and never fr }, ]); }), - )); +); -test('Counterparty provider preserves typed normalization failures and omits an absent role', () => - runEffectTestPromise( +it.effect( + 'Counterparty provider preserves typed normalization failures and omits an absent role', + () => Effect.gen(function* invalidCounterpartyInstant() { const calls: unknown[] = []; const gateway: PartySearchProjectionGatewayService = { @@ -98,4 +99,4 @@ test('Counterparty provider preserves typed normalization failures and omits an { effectiveAt: 'invalid', includeArchived: false, legalEntityId, query: 'ACME', tenantId }, ]); }), - )); +); diff --git a/app/verticals/party-registry/tests/unit/search-rebuild-request.test.ts b/app/verticals/party-registry/tests/unit/search-rebuild-request.test.ts index 7e3ba5dc8..0f8974b10 100644 --- a/app/verticals/party-registry/tests/unit/search-rebuild-request.test.ts +++ b/app/verticals/party-registry/tests/unit/search-rebuild-request.test.ts @@ -1,7 +1,5 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Effect, Schema } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Effect, Schema, Predicate } from 'effect'; import type { OutboxWorkerHandlerContext } from '@app/core-runtime'; import { makeActionTestHarness } from '@app/core-runtime/testing/actions'; import { requestSearchRebuildAction } from '../../src/actions/request-search-rebuild.action.ts'; @@ -28,34 +26,37 @@ const request = { transport: { correlationId: 'search-rebuild-test', idempotencyKey: 'rebuild-1' }, }; -test('tenant rebuild requests require Party administration and canonical idempotency', () => { - const { descriptor } = requestSearchRebuildAction; - assert.equal(descriptor.actionKey, 'party.registry.request-search-rebuild'); - assert.equal(descriptor.tenantPermission?.({}), 'manage_party_identity'); - assert.equal(descriptor.idempotency, 'required'); - assert.equal(descriptor.legalEntityScope, 'optional'); - assert.equal(descriptor.entrypoint.scope, 'tenant'); - assert.deepEqual(Schema.decodeUnknownSync(descriptor.payloadSchema)({}), {}); - assert.deepEqual(Object.keys(descriptor.domainEvents), [ - 'party.registry.search-rebuild-requested.v1', - ]); -}); +it.effect('tenant rebuild requests require Party administration and canonical idempotency', () => + Effect.gen(function* rebuildDescriptor() { + const { descriptor } = requestSearchRebuildAction; + expect(descriptor.actionKey).toBe('party.registry.request-search-rebuild'); + expect(descriptor.tenantPermission?.({})).toBe('manage_party_identity'); + expect(descriptor.idempotency).toBe('required'); + expect(descriptor.legalEntityScope).toBe('optional'); + expect(descriptor.entrypoint.scope).toBe('tenant'); + expect(yield* Schema.decodeUnknownEffect(descriptor.payloadSchema)({})).toEqual({}); + expect(Object.keys(descriptor.domainEvents)).toEqual([ + 'party.registry.search-rebuild-requested.v1', + ]); + }), +); -test('authorized rebuild commits one linked request without reading identity or running the projector', () => { - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - tenantPermission: 'allowed', - }); - return runEffectTestPromise( +it.effect( + 'authorized rebuild commits one linked request without reading identity or running the projector', + () => Effect.gen(function* authorizedRebuildRequest() { + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'allowed', + }); const result = yield* harness.runtime.runAction(request); - assert.equal(result.status, 'QUEUED'); - assert.equal(Schema.is(Schema.String.check(Schema.isUUID()))(result.requestId), true); + expect(result.status).toBe('QUEUED'); + expect(Schema.is(Schema.String.check(Schema.isUUID()))(result.requestId)).toBe(true); const { committed, permissionDenials } = harness.snapshot(); - assert.equal(committed.length, 1); - assert.deepEqual(permissionDenials, []); - assert.deepEqual(committed[0]?.evidence.dataAccessEvents, []); - assert.deepEqual(committed[0]?.evidence.domainEvents, [ + expect(committed.length).toBe(1); + expect(permissionDenials).toEqual([]); + expect(committed[0]?.evidence.dataAccessEvents).toEqual([]); + expect(committed[0]?.evidence.domainEvents).toEqual([ { eventType: 'party.registry.search-rebuild-requested.v1', payloadJson: { requestId: result.requestId }, @@ -65,7 +66,7 @@ test('authorized rebuild commits one linked request without reading identity or subjectResourceType: 'tenant', }, ]); - assert.deepEqual(committed[0]?.evidence.outboxMessages, [ + expect(committed[0]?.evidence.outboxMessages).toEqual([ { domainEventIndex: 0, message: { @@ -76,44 +77,41 @@ test('authorized rebuild commits one linked request without reading identity or }, ]); }), - ); -}); +); -test('denied Party administration cannot queue a rebuild even with Action execution permission', () => { - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - tenantPermission: 'denied', - }); - return runEffectTestPromise( +it.effect( + 'denied Party administration cannot queue a rebuild even with Action execution permission', + () => Effect.gen(function* deniedRebuildRequest() { + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'denied', + }); const error = yield* harness.runtime.runAction(request).pipe(Effect.flip); - assert.equal(error._tag, 'ActionPermissionDenied'); + expect(Predicate.isTagged(error, 'ActionPermissionDenied')).toBe(true); const snapshot = harness.snapshot(); - assert.deepEqual(snapshot.committed, []); - assert.equal(snapshot.permissionDenials.length, 1); - assert.equal(snapshot.stages.includes('handler_executed'), false); + expect(snapshot.committed).toEqual([]); + expect(snapshot.permissionDenials.length).toBe(1); + expect(snapshot.stages.includes('handler_executed')).toBe(false); }), - ); -}); +); -test('replaying the same authorized rebuild request queues only once', () => { - const harness = makeActionTestHarness({ - actionPermission: 'allowed', - tenantPermission: 'allowed', - }); - return runEffectTestPromise( - Effect.gen(function* replayRebuildRequest() { - yield* harness.runtime.runAction(request); - const replay = yield* harness.runtime.runAction(request).pipe(Effect.flip); - assert.equal(replay._tag, 'ActionAlreadyCommitted'); - const snapshot = harness.snapshot(); - assert.equal(snapshot.committed.length, 1); - assert.equal(snapshot.committed[0]?.evidence.domainEvents.length, 1); - assert.equal(snapshot.committed[0]?.evidence.outboxMessages.length, 1); - assert.equal(snapshot.invocations.length, 1); - }), - ); -}); +it.effect('replaying the same authorized rebuild request queues only once', () => + Effect.gen(function* replayRebuildRequest() { + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'allowed', + }); + yield* harness.runtime.runAction(request); + const replay = yield* harness.runtime.runAction(request).pipe(Effect.flip); + expect(Predicate.isTagged(replay, 'ActionAlreadyCommitted')).toBe(true); + const snapshot = harness.snapshot(); + expect(snapshot.committed.length).toBe(1); + expect(snapshot.committed[0]?.evidence.domainEvents.length).toBe(1); + expect(snapshot.committed[0]?.evidence.outboxMessages.length).toBe(1); + expect(snapshot.invocations.length).toBe(1); + }), +); const workerContext: OutboxWorkerHandlerContext = { attemptNumber: 1, @@ -128,29 +126,29 @@ const workerContext: OutboxWorkerHandlerContext = { workerKey: 'party.registry.rebuild-search', }; -test('rebuild worker uses its trusted committed context, and failures remain retryable', () => { - const unavailable = new PartySearchProjectionUnavailable({ - code: 'party_search_projection_unavailable', - reason: 'Party search projection is temporarily unavailable', - }); - return runEffectTestPromise( - Effect.gen(function* rebuildWorkerFailure() { +it.effect( + 'rebuild worker uses its trusted committed context, and failures remain retryable', + () => { + const unavailable = new PartySearchProjectionUnavailable({ + code: 'party_search_projection_unavailable', + reason: 'Party search projection is temporarily unavailable', + }); + return Effect.gen(function* rebuildWorkerFailure() { const failure = yield* handleRebuildSearch({ requestId }, workerContext).pipe( Effect.provideService(PartySearchProjector, { project: (context, target) => { - assert.equal(context, workerContext); - assert.deepEqual(target, { rebuild: true }); + expect(context).toBe(workerContext); + expect(target).toEqual({ rebuild: true }); return Effect.fail(unavailable); }, }), Effect.flip, ); - assert.equal(failure, unavailable); - assert.equal(rebuildSearchWorker.descriptor.workerKey, 'party.registry.rebuild-search'); - assert.equal( - rebuildSearchWorker.descriptor.topic, + expect(failure).toBe(unavailable); + expect(rebuildSearchWorker.descriptor.workerKey).toBe('party.registry.rebuild-search'); + expect(rebuildSearchWorker.descriptor.topic).toBe( 'party.registry.search-rebuild-requested.v1', ); - }), - ); -}); + }); + }, +); diff --git a/app/verticals/party-registry/tests/unit/search-semantics.test.ts b/app/verticals/party-registry/tests/unit/search-semantics.test.ts index b0afb63d8..b2a8a190d 100644 --- a/app/verticals/party-registry/tests/unit/search-semantics.test.ts +++ b/app/verticals/party-registry/tests/unit/search-semantics.test.ts @@ -1,6 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { Match } from 'effect'; +import { expect, it } from 'effect-rstest'; +import { Match, Predicate, Struct } from 'effect'; import { normalizeCounterpartySearchHits, normalizePartySearchHits, @@ -34,31 +33,34 @@ const expectSearchResults = ( ): SearchResults => Match.value(result).pipe( Match.tag('SearchResults', (results) => results), - Match.tag('SearchProjectionViolation', ({ reason }) => - assert.fail(`Expected normalized search results, but the projection was invalid: ${reason}`), - ), + Match.tag('SearchProjectionViolation', ({ reason }) => { + throw new Error( + `Expected normalized search results, but the projection was invalid: ${reason}`, + ); + }), Match.exhaustive, ); -test('Party Search hides archived hits by default and explicitly labels included archived hits', () => { +it('Party Search hides archived hits by default and explicitly labels included archived hits', () => { const hits: readonly PartySearchProjectionHit[] = [ { archived: false, canonicalPartyRef: partyRef('active'), title: 'Active' }, { archived: true, canonicalPartyRef: partyRef('archived'), title: 'Archived' }, ]; - assert.deepEqual(normalizePartySearchHits({ includeArchived: false, tenantId }, hits), { - _tag: 'SearchResults', + const activeResults = normalizePartySearchHits({ includeArchived: false, tenantId }, hits); + expect(Predicate.isTagged(activeResults, 'SearchResults')).toBe(true); + expect(Struct.omit(activeResults, ['_tag'])).toEqual({ items: [{ archived: false, matchedViaAlias: false, ref: partyRef('active'), title: 'Active' }], }); const included = normalizePartySearchHits({ includeArchived: true, tenantId }, hits); - assert.equal(included._tag, 'SearchResults'); - assert.deepEqual( - expectSearchResults(included).items.map(({ archived }) => archived), - [false, true], - ); + expect(Predicate.isTagged(included, 'SearchResults')).toBe(true); + expect(expectSearchResults(included).items.map(({ archived }) => archived)).toEqual([ + false, + true, + ]); }); -test('Party aliases collapse to one survivor while shared contact queries may retain multiple Parties', () => { +it('Party aliases collapse to one survivor while shared contact queries may retain multiple Parties', () => { const survivor = partyRef('survivor'); const result = normalizePartySearchHits({ includeArchived: false, tenantId }, [ { archived: false, canonicalPartyRef: survivor, title: 'ACME' }, @@ -71,33 +73,34 @@ test('Party aliases collapse to one survivor while shared contact queries may re { archived: false, canonicalPartyRef: partyRef('shared-2'), title: 'Other person' }, ]); - assert.equal(result._tag, 'SearchResults'); + expect(Predicate.isTagged(result, 'SearchResults')).toBe(true); const { items } = expectSearchResults(result); - assert.deepEqual( - items.map(({ ref }) => ref.resourceId), - ['survivor', 'shared-2'], - ); - assert.equal(items[0]?.matchedViaAlias, true); + expect(items.map(({ ref }) => ref.resourceId)).toEqual(['survivor', 'shared-2']); + expect(items[0]?.matchedViaAlias).toBe(true); }); -test('Party Search fails closed when Core returns a cross-tenant or inconsistent projection', () => { +it('Party Search fails closed when Core returns a cross-tenant or inconsistent projection', () => { const wrongTenant = { ...partyRef('wrong'), tenantId: '90000000-0000-4000-8000-000000000009', }; - assert.equal( - normalizePartySearchHits({ includeArchived: true, tenantId }, [ - { archived: false, canonicalPartyRef: wrongTenant, title: 'Wrong' }, - ])._tag, - 'SearchProjectionViolation', - ); - assert.equal( - normalizePartySearchHits({ includeArchived: true, tenantId }, [ - { archived: false, canonicalPartyRef: partyRef('same'), title: 'One' }, - { archived: true, canonicalPartyRef: partyRef('same'), title: 'Two' }, - ])._tag, - 'SearchProjectionViolation', - ); + expect( + Predicate.isTagged( + normalizePartySearchHits({ includeArchived: true, tenantId }, [ + { archived: false, canonicalPartyRef: wrongTenant, title: 'Wrong' }, + ]), + 'SearchProjectionViolation', + ), + ).toBe(true); + expect( + Predicate.isTagged( + normalizePartySearchHits({ includeArchived: true, tenantId }, [ + { archived: false, canonicalPartyRef: partyRef('same'), title: 'One' }, + { archived: true, canonicalPartyRef: partyRef('same'), title: 'Two' }, + ]), + 'SearchProjectionViolation', + ), + ).toBe(true); }); const baseCounterpartyHit = ( @@ -113,7 +116,7 @@ const baseCounterpartyHit = ( rolePeriods, }); -test('Counterparty Search evaluates only current role periods at the exclusive time boundary', () => { +it('Counterparty Search evaluates only current role periods at the exclusive time boundary', () => { const effectiveAt = '2026-09-03T12:00:00.000Z'; const hits: readonly CounterpartySearchProjectionHit[] = [ baseCounterpartyHit('ended', 'p1', [ @@ -139,16 +142,13 @@ test('Counterparty Search evaluates only current role periods at the exclusive t hits, ); - assert.equal(result._tag, 'SearchResults'); + expect(Predicate.isTagged(result, 'SearchResults')).toBe(true); const { items } = expectSearchResults(result); - assert.deepEqual( - items.map(({ ref }) => ref.resourceId), - ['future-ended', 'dual'], - ); - assert.deepEqual(items[1]?.currentRoles, ['CUSTOMER', 'SUPPLIER']); + expect(items.map(({ ref }) => ref.resourceId)).toEqual(['future-ended', 'dual']); + expect(items[1]?.currentRoles).toEqual(['CUSTOMER', 'SUPPLIER']); }); -test('Counterparty Search without a role retains durable Counterparties with no current role', () => { +it('Counterparty Search without a role retains durable Counterparties with no current role', () => { const result = normalizeCounterpartySearchHits( { effectiveAt: '2026-09-03T12:00:00.000Z', @@ -159,11 +159,11 @@ test('Counterparty Search without a role retains durable Counterparties with no [baseCounterpartyHit('no-role', 'p1')], ); - assert.equal(result._tag, 'SearchResults'); - assert.deepEqual(expectSearchResults(result).items[0]?.currentRoles, []); + expect(Predicate.isTagged(result, 'SearchResults')).toBe(true); + expect(expectSearchResults(result).items[0]?.currentRoles).toEqual([]); }); -test('Counterparty identity dedupes independently and survivor collisions are surfaced', () => { +it('Counterparty identity dedupes independently and survivor collisions are surfaced', () => { const hits = [ baseCounterpartyHit('cp-1', 'survivor'), baseCounterpartyHit('cp-1', 'survivor'), @@ -179,22 +179,18 @@ test('Counterparty identity dedupes independently and survivor collisions are su hits, ); - assert.equal(result._tag, 'SearchResults'); + expect(Predicate.isTagged(result, 'SearchResults')).toBe(true); const { items } = expectSearchResults(result); - assert.deepEqual( - items.map(({ ref }) => ref.resourceId), - ['cp-1', 'cp-2'], - ); - assert.deepEqual( + expect(items.map(({ ref }) => ref.resourceId)).toEqual(['cp-1', 'cp-2']); + expect( items.map(({ collision }) => collision?.counterpartyRefs.map(({ resourceId }) => resourceId)), - [ - ['cp-1', 'cp-2'], - ['cp-1', 'cp-2'], - ], - ); + ).toEqual([ + ['cp-1', 'cp-2'], + ['cp-1', 'cp-2'], + ]); }); -test('Counterparty Search fails closed on the wrong Legal Entity instead of broadening scope', () => { +it('Counterparty Search fails closed on the wrong Legal Entity instead of broadening scope', () => { const result = normalizeCounterpartySearchHits( { effectiveAt: '2026-09-03T12:00:00.000Z', @@ -213,5 +209,5 @@ test('Counterparty Search fails closed on the wrong Legal Entity instead of broa ], ); - assert.equal(result._tag, 'SearchProjectionViolation'); + expect(Predicate.isTagged(result, 'SearchProjectionViolation')).toBe(true); }); diff --git a/app/verticals/party-registry/tests/unit/search-source.test.ts b/app/verticals/party-registry/tests/unit/search-source.test.ts index 718081662..08a46710a 100644 --- a/app/verticals/party-registry/tests/unit/search-source.test.ts +++ b/app/verticals/party-registry/tests/unit/search-source.test.ts @@ -1,15 +1,13 @@ +import { expect, it } from 'effect-rstest'; import type { CoreSearchSnapshotReadExecutor, CoreSearchWorkerSnapshotService, OutboxWorkerHandlerContext, } from '@app/core-runtime'; -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import type { AnyColumn, Query, SQL, Table } from 'drizzle-orm'; import { getTableName } from 'drizzle-orm'; import { PgDialect } from 'drizzle-orm/pg-core'; -import { DateTime, Effect, Result } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { DateTime, Effect, Result, Predicate } from 'effect'; import { makePartySearchProjectionSource } from '../../src/services/party-search-projection-source.service.ts'; const tenantId = '10000000-0000-4000-8000-000000000001'; @@ -80,72 +78,72 @@ const harness = ( return { columns, filters, scopes, source: makePartySearchProjectionSource(snapshot) }; }; -test('canonical snapshot preserves alias identity and legal-entity Counterparty context', () => - runEffectTestPromise( - Effect.gen(function* canonicalAliasSnapshot() { - const { source } = harness({ - counterparties: [{ counterpartyId, legalEntityId, partyId: aliasId, tenantId }], - counterparty_role_periods: [ - { - counterpartyId, - legalEntityId, - role: 'CUSTOMER', - state: 'ACTIVE', - tenantId, - validFrom: from, - validTo: null, - }, - ], - parties: [ - { archivedAt: null, displayName: 'Canonical', partyId, tenantId }, - { archivedAt: from, displayName: 'Former name', partyId: aliasId, tenantId }, - ], - party_aliases: [{ aliasPartyId: aliasId, canonicalPartyId: partyId, tenantId }], - party_contact_points: [], - party_official_identifiers: [ - { - isCurrent: true, - partyId, - state: 'ACTIVE', - tenantId, - validFrom: from, - validTo: null, - value: '27074358', - }, - ], - }); - const result = yield* source.load(context, { partyId: aliasId }); - assert.deepEqual(result, { - counterparties: [ - { - legalEntityId, - partyRef: ref(partyId), - ref: { ...ref(counterpartyId), resourceType: 'party.registry.counterparty' }, - rolePeriods: [{ role: 'CUSTOMER', state: 'ACTIVE', validFrom: from.toISOString() }], - storedPartyRef: ref(aliasId), - }, - ], - parties: [ - { - aliases: [ - { contacts: [], displayName: 'Former name', identifiers: [], ref: ref(aliasId) }, - ], - archived: false, - contacts: [], - displayName: 'Canonical', - identifiers: [{ state: 'ACTIVE', validFrom: from.toISOString(), value: '27074358' }], - ref: ref(partyId), - }, - ], - projectionVersion: '9', - removedRefs: [ref(aliasId)], - tenantId, - }); - }), - )); +it.effect('canonical snapshot preserves alias identity and legal-entity Counterparty context', () => + Effect.gen(function* canonicalAliasSnapshot() { + const { source } = harness({ + counterparties: [{ counterpartyId, legalEntityId, partyId: aliasId, tenantId }], + counterparty_role_periods: [ + { + counterpartyId, + legalEntityId, + role: 'CUSTOMER', + state: 'ACTIVE', + tenantId, + validFrom: from, + validTo: null, + }, + ], + parties: [ + { archivedAt: null, displayName: 'Canonical', partyId, tenantId }, + { archivedAt: from, displayName: 'Former name', partyId: aliasId, tenantId }, + ], + party_aliases: [{ aliasPartyId: aliasId, canonicalPartyId: partyId, tenantId }], + party_contact_points: [], + party_official_identifiers: [ + { + isCurrent: true, + partyId, + state: 'ACTIVE', + tenantId, + validFrom: from, + validTo: null, + value: '27074358', + }, + ], + }); + const result = yield* source.load(context, { partyId: aliasId }); + expect(result).toEqual({ + counterparties: [ + { + legalEntityId, + partyRef: ref(partyId), + ref: { ...ref(counterpartyId), resourceType: 'party.registry.counterparty' }, + rolePeriods: [{ role: 'CUSTOMER', state: 'ACTIVE', validFrom: from.toISOString() }], + storedPartyRef: ref(aliasId), + }, + ], + parties: [ + { + aliases: [ + { contacts: [], displayName: 'Former name', identifiers: [], ref: ref(aliasId) }, + ], + archived: false, + contacts: [], + displayName: 'Canonical', + identifiers: [{ state: 'ACTIVE', validFrom: from.toISOString(), value: '27074358' }], + ref: ref(partyId), + }, + ], + projectionVersion: '9', + removedRefs: [ref(aliasId)], + tenantId, + }); + }), +); -test('source exposes only current public email and phone search evidence, never ADDRESS or raw contact fields', () => - runEffectTestPromise( +it.effect( + 'source exposes only current public email and phone search evidence, never ADDRESS or raw contact fields', + () => Effect.gen(function* privateSearchEvidence() { const contact = { isCurrent: true, @@ -176,7 +174,7 @@ test('source exposes only current public email and phone search evidence, never ], }); const result = yield* source.load(context, { partyId }); - assert.deepEqual(result.parties[0]?.contacts, [ + expect(result.parties[0]?.contacts).toEqual([ { privacy: 'PUBLIC', state: 'ACTIVE', @@ -192,8 +190,8 @@ test('source exposes only current public email and phone search evidence, never value: '+420123456789', }, ]); - assert.equal(result.parties[0]?.displayName, null); - assert.deepEqual(filters['party_contact_points']?.params, [ + expect(result.parties[0]?.displayName).toBe(null); + expect(filters['party_contact_points']?.params).toEqual([ tenantId, partyId, 'EMAIL', @@ -202,9 +200,8 @@ test('source exposes only current public email and phone search evidence, never 'ACTIVE', true, ]); - assert.match(filters['party_contact_points']?.sql ?? '', /privacy_classification/u); - assert.deepEqual( - columns['party_contact_points']?.toSorted(), + expect(filters['party_contact_points']?.sql ?? '').toMatch(/privacy_classification/u); + expect(columns['party_contact_points']?.toSorted()).toEqual( [ 'partyId', 'tenantId', @@ -218,29 +215,29 @@ test('source exposes only current public email and phone search evidence, never ].toSorted(), ); }), - )); +); -test('missing Party and Counterparty targets produce explicit versioned tombstone refs', () => - runEffectTestPromise( - Effect.gen(function* missingTargetTombstones() { - const { source } = harness({}); - const party = yield* source.load(context, { partyId }); - const counterparty = yield* source.load(context, { counterpartyId }); - assert.deepEqual(party, { - counterparties: [], - parties: [], - projectionVersion: '9', - removedRefs: [ref(partyId)], - tenantId, - }); - assert.deepEqual(counterparty.removedRefs, [ - { ...ref(counterpartyId), resourceType: 'party.registry.counterparty' }, - ]); - }), - )); +it.effect('missing Party and Counterparty targets produce explicit versioned tombstone refs', () => + Effect.gen(function* missingTargetTombstones() { + const { source } = harness({}); + const party = yield* source.load(context, { partyId }); + const counterparty = yield* source.load(context, { counterpartyId }); + expect(party).toEqual({ + counterparties: [], + parties: [], + projectionVersion: '9', + removedRefs: [ref(partyId)], + tenantId, + }); + expect(counterparty.removedRefs).toEqual([ + { ...ref(counterpartyId), resourceType: 'party.registry.counterparty' }, + ]); + }), +); -test('full rebuild reads each Core-enumerated legal entity in the same snapshot and keeps distinct Counterparties', () => - runEffectTestPromise( +it.effect( + 'full rebuild reads each Core-enumerated legal entity in the same snapshot and keeps distinct Counterparties', + () => Effect.gen(function* rebuildSnapshot() { const secondLegalEntityId = '30000000-0000-4000-8000-000000000002'; const secondCounterpartyId = '40000000-0000-4000-8000-000000000002'; @@ -260,18 +257,19 @@ test('full rebuild reads each Core-enumerated legal entity in the same snapshot [legalEntityId, secondLegalEntityId], ); const result = yield* source.load(context, { rebuild: true }); - assert.deepEqual(scopes, [undefined, legalEntityId, secondLegalEntityId, undefined]); - assert.deepEqual( - result.counterparties.map((row) => row.ref.resourceId), - [counterpartyId, secondCounterpartyId], - ); - assert.equal(result.parties[0]?.archived, true); - assert.equal(result.projectionVersion, '9'); + expect(scopes).toEqual([undefined, legalEntityId, secondLegalEntityId, undefined]); + expect(result.counterparties.map((row) => row.ref.resourceId)).toEqual([ + counterpartyId, + secondCounterpartyId, + ]); + expect(result.parties[0]?.archived).toBe(true); + expect(result.projectionVersion).toBe('9'); }), - )); +); -test('Counterparty-only refresh emits only its canonical family and selected Counterparty', () => - runEffectTestPromise( +it.effect( + 'Counterparty-only refresh emits only its canonical family and selected Counterparty', + () => Effect.gen(function* targetedCounterpartySnapshot() { const otherId = '20000000-0000-4000-8000-000000000009'; const { source } = harness({ @@ -290,19 +288,14 @@ test('Counterparty-only refresh emits only its canonical family and selected Cou ], }); const result = yield* source.load(context, { counterpartyId }); - assert.deepEqual( - result.parties.map((party) => party.ref.resourceId), - [partyId], - ); - assert.deepEqual( - result.counterparties.map((row) => row.ref.resourceId), - [counterpartyId], - ); + expect(result.parties.map((party) => party.ref.resourceId)).toEqual([partyId]); + expect(result.counterparties.map((row) => row.ref.resourceId)).toEqual([counterpartyId]); }), - )); +); -test('alias cycles and cross-tenant source rows fail closed with sanitized typed failures', () => - runEffectTestPromise( +it.effect( + 'alias cycles and cross-tenant source rows fail closed with sanitized typed failures', + () => Effect.gen(function* rejectedSourceSnapshot() { for (const rows of [ { @@ -317,11 +310,13 @@ test('alias cycles and cross-tenant source rows fail closed with sanitized typed ]) { const { source } = harness(rows); const outcome = yield* source.load(context, { rebuild: true }).pipe(Effect.result); - assert.ok(Result.isFailure(outcome)); + expect(Result.isFailure(outcome)).toBe(true); if (Result.isFailure(outcome)) { - assert.equal(outcome.failure._tag, 'PartySearchProjectionUnavailable'); - assert.doesNotMatch(outcome.failure.reason, /Secret name|foreign-tenant/u); + expect(Predicate.isTagged(outcome.failure, 'PartySearchProjectionUnavailable')).toBe( + true, + ); + expect(outcome.failure.reason).not.toMatch(/Secret name|foreign-tenant/u); } } }), - )); +); diff --git a/app/verticals/party-registry/tests/unit/search-worker-registration.test.ts b/app/verticals/party-registry/tests/unit/search-worker-registration.test.ts index b33698644..4ccfec1ed 100644 --- a/app/verticals/party-registry/tests/unit/search-worker-registration.test.ts +++ b/app/verticals/party-registry/tests/unit/search-worker-registration.test.ts @@ -1,21 +1,23 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from 'effect-rstest'; import { CORE_SEARCH_INGESTION_REGISTRATIONS } from '@app/core-runtime'; import { outboxWorkers } from '../../src/workers/index.ts'; -test('every accepted Party search lifecycle and explicit rebuild topic has its exact generated self-consumer', () => { +it('every accepted Party search lifecycle and explicit rebuild topic has its exact generated self-consumer', () => { for (const registration of CORE_SEARCH_INGESTION_REGISTRATIONS) { const matches = outboxWorkers.filter( ({ descriptor }) => descriptor.workerKey === registration.workerKey, ); - assert.equal(matches.length, 1, registration.workerKey); + expect(matches.length, registration.workerKey).toBe(1); const [worker] = matches; - assert.ok(worker); - assert.equal(worker.descriptor.topic, registration.topic); - assert.equal(worker.descriptor.producerModuleKey, 'party.registry'); - assert.equal(worker.descriptor.consumerModuleKey, 'party.registry'); - assert.equal(worker.descriptor.entrypoint.access, 'background'); - assert.equal(worker.descriptor.entrypoint.scope, 'tenant'); + expect(worker).toBeDefined(); + if (worker === undefined) { + throw new Error('Expected registered worker'); + } + expect(worker.descriptor.topic).toBe(registration.topic); + expect(worker.descriptor.producerModuleKey).toBe('party.registry'); + expect(worker.descriptor.consumerModuleKey).toBe('party.registry'); + expect(worker.descriptor.entrypoint.access).toBe('background'); + expect(worker.descriptor.entrypoint.scope).toBe('tenant'); } - assert.equal(CORE_SEARCH_INGESTION_REGISTRATIONS.length, 15); + expect(CORE_SEARCH_INGESTION_REGISTRATIONS.length).toBe(15); }); diff --git a/app/verticals/party-registry/tests/unit/timeline-resource-contract.test.ts b/app/verticals/party-registry/tests/unit/timeline-resource-contract.test.ts index 38fc6a1f5..0e8691a1c 100644 --- a/app/verticals/party-registry/tests/unit/timeline-resource-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/timeline-resource-contract.test.ts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { assert, it } from 'effect-rstest'; import { Schema } from 'effect'; import * as duplicateCase from '../../shared/resources/duplicate-candidate-case.ts'; import * as correction from '../../shared/resources/party-correction.ts'; @@ -34,7 +33,7 @@ const resources = [ ]; for (const { descriptor, makeRef, schema, slug } of resources) { - test(`${slug} retains its own resource identity and timeline-only capabilities`, () => { + it(`${slug} retains its own resource identity and timeline-only capabilities`, () => { const tenantId = '10000000-0000-4000-8000-000000000001'; const reference = makeRef(tenantId, 'resource-1'); assert.deepEqual(reference, {