From bfe3256ee36ae5590654153caf68a67de76b00db Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Mon, 7 Sep 2026 23:16:51 +0200 Subject: [PATCH 01/38] Use native Effect predicates in runtime and test assertions --- .../tests/integration/auth-runtime.test.ts | 12 +- .../identity-modes-runtime.test.ts | 8 +- .../tests/unit/auth-config.test.ts | 8 +- .../tests/unit/auth-contract.test.ts | 10 +- .../tests/unit/identity-lifecycle.test.ts | 10 +- .../tests/unit/layout.test.tsx | 6 +- .../tests/unit/legal-entity-selection.test.ts | 4 +- .../core-runtime/src/actions/repository.ts | 2 +- .../core-runtime/src/outbox/runtime.ts | 17 +- .../src/testing/live-operations.ts | 1 - .../integration/action-permission.test.ts | 18 +- .../tests/integration/action-runtime.test.ts | 73 ++++---- .../integration/identity-runtime.test.ts | 2 +- .../integration/legal-entity-context.test.ts | 6 +- .../integration/module-state-gate.test.ts | 12 +- .../integration/principal-management.test.ts | 6 +- .../integration/principal-resolver.test.ts | 10 +- .../integration/search-persistence.test.ts | 11 +- .../tests/unit/action-collector.test.ts | 26 +-- .../tests/unit/action-definition.test.ts | 10 +- .../tests/unit/action-errors.test.ts | 10 +- .../tests/unit/action-policy.test.ts | 4 +- .../tests/unit/action-runtime.test.ts | 142 +++++++-------- .../tests/unit/action-testing-harness.test.ts | 16 +- .../unit/commit-recovery-metadata.test.ts | 14 +- .../core-runtime/tests/unit/config.test.ts | 14 +- .../unit/database-driver-failure.test.ts | 20 +-- .../tests/unit/legal-entity-context.test.ts | 83 ++++----- .../tests/unit/module-state-gate.test.ts | 12 +- .../tests/unit/outbox-health.test.ts | 16 +- .../tests/unit/pool-configuration.test.ts | 14 +- .../tests/unit/principal-management.test.ts | 18 +- .../tests/unit/principal-resolver.test.ts | 128 ++++++++------ .../tests/unit/read-runtime.test.ts | 60 +++---- .../tests/unit/scoped-transaction.test.ts | 4 +- .../tests/unit/search-ingestion.test.ts | 6 +- .../tests/unit/search-projection.test.ts | 8 +- .../unit/system-principal-context.test.ts | 10 +- .../tests/unit/tenant-module-state.test.ts | 12 +- .../tests/integration/ares-governed.test.ts | 17 +- .../integration/governed-identity.test.ts | 162 +++++++++++------- .../api-integration-ares-application.test.ts | 56 +++--- .../api-integration-command-runtime.test.ts | 73 ++++---- .../tests/unit/ares-lookup-read.test.ts | 14 +- .../tests/unit/ares-subject.service.test.ts | 34 ++-- .../contact-point-persistence.service.test.ts | 46 ++--- .../tests/unit/correction-contract.test.ts | 38 ++-- .../counterparty-persistence.service.test.ts | 38 ++-- .../tests/unit/database-client.test.ts | 8 +- ...gement-profile-persistence-service.test.ts | 20 +-- .../identifier-persistence.service.test.ts | 34 ++-- .../unit/identity-action-evidence.test.ts | 12 +- .../unit/identity-party-detail-alias.test.ts | 14 +- .../unit/identity-persistence.service.test.ts | 72 ++++---- .../tests/unit/matching-persistence.test.ts | 58 +++---- .../merge-alias-resolution-service.test.ts | 16 +- .../tests/unit/merge-alias-resolution.test.ts | 48 +++--- .../unit/merge-survivor-selection.test.ts | 42 ++--- .../relationship-persistence.service.test.ts | 28 +-- .../tests/unit/search-core-adapter.test.ts | 10 +- .../tests/unit/search-projector.test.ts | 28 +-- .../tests/unit/search-rebuild-request.test.ts | 16 +- .../tests/unit/search-semantics.test.ts | 54 +++--- .../tests/unit/search-source.test.ts | 16 +- 64 files changed, 933 insertions(+), 864 deletions(-) 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 ffd9c4ec5..3a96a5ede 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 @@ -213,7 +213,7 @@ const installedPageCatalog = (): InstalledModuleCatalog => }, ]); -test('creates, resolves, persists, revokes, and signs out a Better Auth session', async () => { +void 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( @@ -370,7 +370,7 @@ test('creates, resolves, persists, revokes, and signs out a Better Auth session' const invalid = await runEffectTestPromise( Effect.flip(authentication.signIn(email, 'wrong-password', requestHeaders)), ); - assert.equal(invalid._tag, 'InvalidCredentialsError'); + assert.ok(Predicate.isTagged(invalid, 'InvalidCredentialsError')); const anonymousRuntime = makeShellAuthenticationApiRuntime( authenticationLayer, @@ -1013,7 +1013,7 @@ test('creates, resolves, persists, revokes, and signs out a Better Auth session' .pipe(Effect.provide(authenticationContextLayer)), ), ); - assert.equal(revoked._tag, 'OntosIdentityForbiddenError'); + assert.ok(Predicate.isTagged(revoked, 'OntosIdentityForbiddenError')); const forbiddenModulesResponse = await unavailableHandler.handler( new Request(`${configuration.baseUrl}/shell/composition`, { headers: authenticatedHeaders, @@ -1066,7 +1066,7 @@ test('creates, resolves, persists, revokes, and signs out a Better Auth session' } }); -test('selects, lists, switches, revalidates, and upgrades a multi-tenant session', async () => { +void 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'; @@ -1738,7 +1738,7 @@ test('selects, lists, switches, revalidates, and upgrades a multi-tenant session .pipe(Effect.provide(multiAuthenticationContextLayer)), ), ); - assert.equal(revokedSession._tag, 'OntosIdentityForbiddenError'); + assert.ok(Predicate.isTagged(revokedSession, 'OntosIdentityForbiddenError')); await runEffectTestPromise( coreDatabase .update(principalAuthBindings) @@ -1769,7 +1769,7 @@ test('selects, lists, switches, revalidates, and upgrades a multi-tenant session .pipe(Effect.provide(multiAuthenticationContextLayer)), ), ); - assert.equal(sessionWithRemovedBinding._tag, 'OntosIdentityForbiddenError'); + assert.ok(Predicate.isTagged(sessionWithRemovedBinding, 'OntosIdentityForbiddenError')); } finally { await Promise.all(handlers.map(async ({ dispose }) => await dispose())); await cleanup(); 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 9f480a544..205224005 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 @@ -463,7 +463,7 @@ void test('verifies provider keys and completes live support impersonation with ); await runEffectTestPromise(keys.setEnabled(verified.providerKeyId, false)); const invalidKey = await runEffectTestPromise(Effect.flip(keys.verify(issued.secret))); - assert.equal(invalidKey._tag, 'ApiKeyCredentialInvalidError'); + assert.ok(Predicate.isTagged(invalidKey, 'ApiKeyCredentialInvalidError')); const managedPrincipal = await runEffectTestPromise( providePrincipalManagementRepository( @@ -580,7 +580,7 @@ void test('verifies provider keys and completes live support impersonation with const incompleteImpersonation = await runEffectTestPromise( Effect.flip(provideContextAccess(authentication.resolveTenantContext(impersonatedHeaders))), ); - assert.equal(incompleteImpersonation._tag, 'OntosIdentityForbiddenError'); + assert.ok(Predicate.isTagged(incompleteImpersonation, 'OntosIdentityForbiddenError')); await runEffectTestPromise( authDatabase .update(session) @@ -596,7 +596,7 @@ void test('verifies provider keys and completes live support impersonation with const mismatchedImpersonationReason = await runEffectTestPromise( Effect.flip(provideContextAccess(authentication.resolveTenantContext(impersonatedHeaders))), ); - assert.equal(mismatchedImpersonationReason._tag, 'OntosIdentityForbiddenError'); + assert.ok(Predicate.isTagged(mismatchedImpersonationReason, 'OntosIdentityForbiddenError')); await runEffectTestPromise( authDatabase .update(session) @@ -626,7 +626,7 @@ void test('verifies provider keys and completes live support impersonation with const revokedImpersonation = await runEffectTestPromise( Effect.flip(provideContextAccess(authentication.resolveTenantContext(impersonatedHeaders))), ); - assert.equal(revokedImpersonation._tag, 'OntosIdentityForbiddenError'); + assert.ok(Predicate.isTagged(revokedImpersonation, 'OntosIdentityForbiddenError')); const stopped = await runEffectTestPromise( provideContextAccess( providePrincipalManagementRepository( 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..b7494d9ed 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,6 +1,6 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import { expect, test } from '@rstest/core'; -import { Effect } from 'effect'; +import { Effect, Predicate } from 'effect'; import { parseAuthConfig } from '../../api/auth/config.ts'; import { parseGatewayIssuerConfig } from '../../api/auth/gateway-issuer-config.ts'; @@ -39,8 +39,8 @@ test('requires a strong secret and PostgreSQL URL in the typed error channel', a ), ), ]).then(([secretError, databaseError]) => { - expect(secretError._tag).toBe('AuthConfigError'); - expect(databaseError._tag).toBe('AuthConfigError'); + expect(Predicate.isTagged(secretError, 'AuthConfigError')).toBe(true); + expect(Predicate.isTagged(databaseError, 'AuthConfigError')).toBe(true); })); test('keeps gateway signing configuration independent from Better Auth configuration', async () => { @@ -48,5 +48,5 @@ test('keeps gateway signing configuration independent from Better Auth configura const gatewayError = await runEffectTestPromise(Effect.flip(parseGatewayIssuerConfig({}))); expect(authentication.baseUrl).toBe('http://localhost:3020'); - expect(gatewayError._tag).toBe('GatewayIssuerConfigError'); + expect(Predicate.isTagged(gatewayError, 'GatewayIssuerConfigError')).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 fc9e40e55..aaf48fc2e 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,6 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import { expect, test } from '@rstest/core'; -import { DateTime, Effect, Schema } from 'effect'; +import { DateTime, Effect, Schema, Predicate } from 'effect'; import { CurrentSessionSchema, AvailableLegalEntitiesResponseSchema, @@ -233,7 +233,7 @@ test('validates tenant UUIDs and strips all non-contract fields', async () => { const invalidPayload = await runEffectTestPromise( Effect.flip(Schema.decodeUnknownEffect(SwitchTenantPayloadSchema)({ tenantId: 'not-a-uuid' })), ); - expect(invalidPayload._tag).toBe('SchemaError'); + expect(Predicate.isTagged(invalidPayload, 'SchemaError')).toBe(true); }); test('rejects malformed credentials through Effect Schema', async () => { @@ -245,7 +245,7 @@ test('rejects malformed credentials through Effect Schema', async () => { }), ), ); - expect(error._tag).toBe('SchemaError'); + expect(Predicate.isTagged(error, 'SchemaError')).toBe(true); }); test('requires lifecycle reasons and strips provider-private API key identifiers', async () => { @@ -270,8 +270,8 @@ test('requires lifecycle reasons and strips provider-private API key identifiers }), ), ); - 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)({ 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..909ec358e 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 @@ -6,7 +6,7 @@ import { IdentityTargetInvalidError, PrincipalBindingMissingError, } from '@app/core-runtime'; -import { Effect, Redacted } from 'effect'; +import { Effect, Redacted, Predicate } from 'effect'; import { ApiKeyProviderUnavailableError, ApiKeyStateInconsistentError, @@ -89,7 +89,7 @@ test('compensates a failed Core bind and never exposes the provider key identifi ), ); expect(failure).toBe(bindFailure); - expect(failure._tag).toBe('IdentityTargetInvalidError'); + expect(Predicate.isTagged(failure, 'IdentityTargetInvalidError')).toBe(true); expect(disabled).toEqual(['private-provider-key-id']); }); @@ -117,7 +117,7 @@ test('preserves resolver lifecycle failures instead of rewriting them as an outa ); expect(failure).toBe(resolverFailure); - expect(failure._tag).toBe('PrincipalBindingMissingError'); + expect(Predicate.isTagged(failure, 'PrincipalBindingMissingError')).toBe(true); }); test('preserves a typed Core status-transition failure before touching provider state', async () => { @@ -151,7 +151,7 @@ test('preserves a typed Core status-transition failure before touching provider ); expect(failure).toBe(actionFailure); - expect(failure._tag).toBe('IdentityTargetInvalidError'); + expect(Predicate.isTagged(failure, 'IdentityTargetInvalidError')).toBe(true); expect(providerCalls).toBe(0); }); @@ -456,7 +456,7 @@ test('cleans one bounded pending batch and requires a retry before issuing anoth ), ); - expect(failure._tag).toBe('IdentityLifecycleOperationError'); + expect(Predicate.isTagged(failure, 'IdentityLifecycleOperationError')).toBe(true); expect(disabled).toEqual(['bounded-orphan']); expect(issueCalls).toBe(0); }); 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 79cb6ef44..114610fa8 100644 --- a/app/apps/shell-super-app/tests/unit/layout.test.tsx +++ b/app/apps/shell-super-app/tests/unit/layout.test.tsx @@ -305,10 +305,8 @@ test('renders the account Menu last and dispatches only the logout command by ke 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', - ); + const accountMenu = header?.querySelector(':scope > :last-child'); + expect(accountMenu?.dataset['position']).toBe('end'); trigger.focus(); await user.keyboard('{Enter}'); 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..c630d75e4 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 @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import { test } from '@rstest/core'; 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, @@ -110,7 +110,7 @@ test('fails closed for authorization uncertainty and validates a switch independ ), ), ); - assert.equal(unavailable._tag, 'LegalEntitySelectionUnavailableError'); + assert.ok(Predicate.isTagged(unavailable, 'LegalEntitySelectionUnavailableError')); assert.deepEqual( await runEffectTestPromise( provideSelectionServices( diff --git a/app/packages/core-runtime/src/actions/repository.ts b/app/packages/core-runtime/src/actions/repository.ts index e7c6f737b..034215bde 100644 --- a/app/packages/core-runtime/src/actions/repository.ts +++ b/app/packages/core-runtime/src/actions/repository.ts @@ -106,7 +106,7 @@ const normalizeForHash = (value: Value, seen: WeakSet): Canonical if (Predicate.isBigInt(value)) { return ['bigint', value.toString(10)]; } - if (value instanceof Date) { + if (Predicate.isDate(value)) { return ['date', value.toISOString()]; } if (Array.isArray(value)) { diff --git a/app/packages/core-runtime/src/outbox/runtime.ts b/app/packages/core-runtime/src/outbox/runtime.ts index a7fe9bc42..3917d2317 100644 --- a/app/packages/core-runtime/src/outbox/runtime.ts +++ b/app/packages/core-runtime/src/outbox/runtime.ts @@ -17,10 +17,9 @@ import { import { OutboxHandlerExecutionError, OutboxPayloadDecodeError, - OutboxPersistenceError, OutboxWorkerDescriptorError, } from './errors.ts'; -import type { OutboxClaimLostError } from './errors.ts'; +import type { OutboxClaimLostError, OutboxPersistenceError } from './errors.ts'; import { OutboxRepository } from './repository.ts'; import type { OutboxClaim, OutboxRepositoryService as OutboxRepositoryPort } from './repository.ts'; @@ -101,7 +100,7 @@ const validateCycleInput = Effect.fn('OutboxRuntime.validateCycleInput')( } const registrations = yield* Effect.try({ catch: (error) => - error instanceof OutboxWorkerDescriptorError + Schema.is(OutboxWorkerDescriptorError)(error) ? error : descriptorFailure('The Outbox Worker descriptor set is invalid'), try: () => validateOutboxWorkerRegistrations(input.registrations), @@ -302,9 +301,7 @@ const processNextOutboxDelivery = Effect.fn('makeOutboxRuntime.processNextDelive reason: 'The Outbox Message payload does not match its published schema', }); const status = yield* repository.fail(claim, decodeError.reason, execution.now).pipe( - Effect.tapError((error) => - error instanceof OutboxPersistenceError ? logUnexpectedPersistence(claim) : Effect.void, - ), + Effect.tapErrorTag('OutboxPersistenceError', () => logUnexpectedPersistence(claim)), (effect) => withOutcomeSpan(effect, claim, 'payload_decode_failure'), ); return { @@ -342,9 +339,7 @@ const processNextOutboxDelivery = Effect.fn('makeOutboxRuntime.processNextDelive : 'The Outbox Worker handler returned a declared failure', }); const status = yield* repository.fail(claim, executionError.reason, execution.now).pipe( - Effect.tapError((error) => - error instanceof OutboxPersistenceError ? logUnexpectedPersistence(claim) : Effect.void, - ), + Effect.tapErrorTag('OutboxPersistenceError', () => logUnexpectedPersistence(claim)), (effect) => withOutcomeSpan(effect, claim, 'handler_failure'), ); return { @@ -356,9 +351,7 @@ const processNextOutboxDelivery = Effect.fn('makeOutboxRuntime.processNextDelive } yield* repository.complete(claim, execution.now).pipe( - Effect.tapError((error) => - error instanceof OutboxPersistenceError ? logUnexpectedPersistence(claim) : Effect.void, - ), + 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/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 e0e80af38..a1a66d813 100644 --- a/app/packages/core-runtime/tests/integration/action-permission.test.ts +++ b/app/packages/core-runtime/tests/integration/action-permission.test.ts @@ -1,7 +1,7 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import { v1 } from '@authzed/authzed-node'; import { and, eq } from 'drizzle-orm'; -import { Effect, Exit, Schema, flow } from 'effect'; +import { Effect, Exit, Schema, flow, Predicate } from 'effect'; import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import test, { after, before } from 'node:test'; @@ -578,7 +578,7 @@ effectTest( .where(eq(outboxMessages.tenantId, tenantId)), ]); - assert.equal(failure._tag, 'ActionPermissionDenied'); + assert.ok(Predicate.isTagged(failure, 'ActionPermissionDenied')); assert.equal(failure.reason, 'The principal is not permitted to execute this Action'); assert.equal(executions.value, 0); assert.equal(invocation.status, 'rejected'); @@ -637,7 +637,7 @@ effectTest( ), ); - assert.equal(failure._tag, 'ActionPermissionDenied', kind); + assert.ok(Predicate.isTagged(failure, 'ActionPermissionDenied'), kind); assert.equal(executions.value, 0, kind); }), { concurrency: 1, discard: true }, @@ -678,10 +678,10 @@ effectTest( .from(auditEvents) .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)); - assert.deepEqual( - results.map((result) => result._tag), - ['ActionPermissionDenied', 'ActionPermissionDenied'], - ); + assert.equal(results.length, 2); + for (const result of results) { + assert.ok(Predicate.isTagged(result, 'ActionPermissionDenied')); + } assert.equal(executions.value, 0); assert.equal(invocation.status, 'rejected'); assert.equal(audits.length, 1); @@ -773,7 +773,7 @@ effectTest( .from(auditEvents) .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)); - assert.equal(failure._tag, 'ActionTransactionError', stage); + assert.ok(Predicate.isTagged(failure, 'ActionTransactionError'), stage); assert.equal(executions.value, 0, stage); assert.equal(invocation.status, 'received', stage); assert.equal(invocation.completedAt, null, stage); @@ -823,7 +823,7 @@ effectTest( ), ); - assert.equal(failure._tag, 'ActionPermissionCheckError'); + assert.ok(Predicate.isTagged(failure, 'ActionPermissionCheckError')); assert.equal(failure.reason.includes('invalid-integration-key'), false); assert.equal(executions.value, 0); assert.equal(invocation.status, 'received'); 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 88e29e822..4be2144a7 100644 --- a/app/packages/core-runtime/tests/integration/action-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/action-runtime.test.ts @@ -3,7 +3,7 @@ import { ConnectionError, SqlError, UnknownError } from 'effect/unstable/sql/Sql 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, 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'; @@ -71,8 +71,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, @@ -456,17 +454,8 @@ const makeRegistration = ({ (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 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) => { @@ -559,7 +548,7 @@ void test('rechecks business module state under the tenant lock and retries afte ); await runEffectTestPromise(Deferred.succeed(continuePolicy, null)); const denied = await firstAttempt; - assert.equal(failureTag(denied), 'ModuleStateDeniedError'); + assert.ok(hasFailure(denied, 'ModuleStateDeniedError')); assert.equal(handlerExecutions, 0); const [openInvocation] = await runEffectTestPromise( @@ -854,7 +843,7 @@ void test('atomically rejects denied global and same-owner MicroVertical Policie const failure = Exit.isFailure(exit) ? Option.flatMap(Cause.findErrorOption(exit.cause), decodeActionPolicyDeniedFailure) : Option.none(); - assert.equal(failureTag(exit), 'ActionPolicyDenied'); + assert.ok(hasFailure(exit, 'ActionPolicyDenied')); if (Option.isSome(failure)) { assert.equal(failure.value.reason, scenario.reason); assert.equal(failure.value.policyReasonCode, scenario.reasonCode); @@ -968,9 +957,8 @@ void test('rolls back every denied-Policy finalization persistence failure', asy .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)), ); - assert.equal( - failureTag(exit), - 'ActionInvocationPersistenceError', + assert.ok( + hasFailure(exit, 'ActionInvocationPersistenceError'), Exit.isFailure(exit) ? Cause.pretty(exit.cause) : 'success', ); assert.equal(handlerExecutions, 0); @@ -1077,7 +1065,7 @@ void test('rolls back domain rejection, evidence persistence failure, and orphan .where(eq(domainEvents.actionInvocationId, invocationId)), ); - assert.equal(failureTag(exit), scenario.expectedTag); + assert.ok(hasFailure(exit, scenario.expectedTag)); assert.equal(states.length, 0); assert.equal(invocations[0]?.status, 'running'); assert.equal(invocations[0]?.completedAt, null); @@ -1174,7 +1162,7 @@ void test('rolls back every individual success-evidence persistence failure', as ]), ); - assert.equal(failureTag(exit), 'ActionTransactionError', stage); + assert.ok(hasFailure(exit, 'ActionTransactionError'), stage); assert.equal(states.length, 0, stage); assert.equal(invocation?.status, 'running', stage); assert.equal(invocation?.completedAt, null, stage); @@ -1237,8 +1225,8 @@ void test('keeps Policy rejection terminal and deduplicates repeated and concurr .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)), ); - assert.equal(failureTag(first), 'ActionPolicyDenied'); - assert.equal(failureTag(retry), 'ActionInvocationStateError'); + assert.ok(hasFailure(first, 'ActionPolicyDenied')); + assert.ok(hasFailure(retry, 'ActionInvocationStateError')); assert.equal(evaluations, 1); assert.equal(handlerExecutions, 0); assert.equal(invocation.status, 'rejected'); @@ -1290,7 +1278,10 @@ void test('keeps Policy rejection terminal and deduplicates repeated and concurr .where(eq(auditEvents.actionInvocationId, concurrentInvocation.actionInvocationId)), ); - assert.deepEqual(concurrent.map(failureTag), ['ActionPolicyDenied', 'ActionPolicyDenied']); + assert.equal(concurrent.length, 2); + for (const outcome of concurrent) { + assert.ok(hasFailure(outcome, 'ActionPolicyDenied')); + } assert.equal(concurrentEvaluations, 2); assert.equal(handlerExecutions, 0); assert.equal(concurrentInvocation.status, 'rejected'); @@ -1369,7 +1360,7 @@ void test('never lets a losing Policy denial replace a running or successful inv ); assert.equal(successResult.value, 'same'); - assert.equal(failureTag(rejectedExit), 'ActionInvocationPersistenceError'); + assert.ok(hasFailure(rejectedExit, 'ActionInvocationPersistenceError')); assert.equal(invocation.status, 'succeeded'); assert.equal(audits.filter((row) => row.eventType === 'action.rejected').length, 0); }); @@ -1433,9 +1424,11 @@ void test('serializes concurrent requests and enforces committed, open-retry, an assert.equal(executions, 1); assert.equal(concurrentResults.filter(Exit.isSuccess).length, 1); - assert.deepEqual(concurrentResults.filter(Exit.isFailure).map(failureTag), [ - 'ActionAlreadyCommitted', - ]); + const failedResults = concurrentResults.filter(Exit.isFailure); + assert.equal(failedResults.length, 1); + for (const outcome of failedResults) { + assert.ok(hasFailure(outcome, 'ActionAlreadyCommitted')); + } const committedRetry = yield* Effect.exit( runtime.runAction({ @@ -1447,7 +1440,7 @@ void test('serializes concurrent requests and enforces committed, open-retry, an }, }), ); - assert.equal(failureTag(committedRetry), 'ActionAlreadyCommitted'); + assert.ok(hasFailure(committedRetry, 'ActionAlreadyCommitted')); assert.equal(executions, 1); const conflict = yield* Effect.exit( @@ -1456,7 +1449,7 @@ void test('serializes concurrent requests and enforces committed, open-retry, an payload: { value: 'different' }, }), ); - assert.equal(failureTag(conflict), 'ActionRequestHashConflict'); + assert.ok(hasFailure(conflict, 'ActionRequestHashConflict')); const openKey = 'open-retry'; const openModule = `test.open-retry.${tenantId}`; @@ -1472,7 +1465,7 @@ void test('serializes concurrent requests and enforces committed, open-retry, an transport: transport(openKey, openModule), }), ); - assert.equal(failureTag(rejected), 'TestDomainRejected'); + assert.ok(hasFailure(rejected, 'TestDomainRejected')); const retried = yield* runtime.runAction({ payload: { value: 'retryable' }, @@ -1619,7 +1612,7 @@ void test('resolves a lost commit acknowledgement from the durable succeeded mar }), ), ); - assert.equal(failureTag(first), 'ActionCommitIndeterminate'); + assert.ok(hasFailure(first, 'ActionCommitIndeterminate')); const resolvingRuntime = makeActionRuntime( database, @@ -1696,10 +1689,10 @@ void test('resolves a lost commit acknowledgement from the durable succeeded mar .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.ok(hasFailure(committedResolution, 'ActionAlreadyCommitted')); + assert.ok(hasFailure(unauthorizedResolution, 'ActionInvocationNotFound')); + assert.ok(hasFailure(unavailableResolution, 'ActionCommitIndeterminate')); + assert.ok(hasFailure(resolved, 'ActionAlreadyCommitted')); assert.equal(invocations[0]?.status, 'succeeded'); assert.equal(states.length, 1); @@ -1740,7 +1733,7 @@ void test('resolves a lost commit acknowledgement from the durable succeeded mar }), ), ); - assert.equal(failureTag(openFirst), 'ActionCommitIndeterminate'); + assert.ok(hasFailure(openFirst, 'ActionCommitIndeterminate')); const openInvocations = await runEffectTestPromise( database.executor @@ -1771,7 +1764,7 @@ void test('resolves a lost commit acknowledgement from the durable succeeded mar .where(eq(tenantModuleStates.moduleKey, openModuleStateKey)), ); - assert.equal(openResolution._tag, 'ActionCommitOpen'); + assert.ok(Predicate.isTagged(openResolution, 'ActionCommitOpen')); assert.equal(openResolved.value, 'rolled-back-with-lost-ack'); assert.equal(openStates.length, 1); }); @@ -1869,7 +1862,7 @@ void test('persists no invocation or evidence for every non-writable business mo }), ), ); - assert.equal(failureTag(exit), 'ModuleStateDeniedError', state); + assert.ok(hasFailure(exit, 'ModuleStateDeniedError'), state); const invocations = await runEffectTestPromise( database.executor .select() @@ -1901,7 +1894,7 @@ void test('persists no invocation or evidence for every non-writable business mo }), ), ); - assert.equal(failureTag(missingExit), 'ModuleStateDeniedError'); + assert.ok(hasFailure(missingExit, 'ModuleStateDeniedError')); assert.equal(handlerExecutions, 1); }); }); 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..adeeafca4 100644 --- a/app/packages/core-runtime/tests/integration/identity-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/identity-runtime.test.ts @@ -467,7 +467,7 @@ effectTest( }), ), ); - assert.equal(systemDenied._tag, 'ActionPermissionDenied'); + assert.ok(Predicate.isTagged(systemDenied, 'ActionPermissionDenied')); const systemTenantMember = relationship( 'tenant', tenantId, 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 c8fb46bad..fe65078e5 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 @@ -4,7 +4,7 @@ import { } from '@app/core-runtime/testing/effect-runtime'; import { eq } from 'drizzle-orm'; -import { Effect, Exit as NativeExit, Scope as NativeScope } from 'effect'; +import { Effect, Exit as NativeExit, Scope as NativeScope, Predicate } from 'effect'; import assert from 'node:assert/strict'; import test, { after as afterNativeDatabase } from 'node:test'; import { Pool } from 'pg'; @@ -110,9 +110,9 @@ effectTest( legalName: 'Zeta entity', }); const inactiveError = yield* Effect.flip(context.validateSelection(tenantOne, suspended)); - assert.equal(inactiveError._tag, 'LegalEntityContextInactiveError'); + assert.ok(Predicate.isTagged(inactiveError, 'LegalEntityContextInactiveError')); const missingError = yield* Effect.flip(context.validateSelection(tenantOne, foreign)); - assert.equal(missingError._tag, 'LegalEntityContextMissingError'); + assert.ok(Predicate.isTagged(missingError, 'LegalEntityContextMissingError')); }).pipe( Effect.ensuring(cleanup.pipe(Effect.orDie)), Effect.ensuring(databaseEffect(pool.end.bind(pool)).pipe(Effect.orDie)), 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 9f9cce005..c96678f80 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,7 +1,7 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; // @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. import { and, eq } from 'drizzle-orm'; -import { Effect, Exit } from 'effect'; +import { Effect, Exit, Predicate } from 'effect'; import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import test from 'node:test'; @@ -163,7 +163,7 @@ void test('batches tenant-isolated states once, rejects malformed/unavailable re const quarantined = await runEffectTestPromise( Effect.flip(gate.check(tenantTwoSnapshot, read)), ); - assert.equal(quarantined._tag, 'ModuleStateDeniedError'); + assert.ok(Predicate.isTagged(quarantined, 'ModuleStateDeniedError')); const missingDescriptor = defineTenantModuleEntrypoint({ access: 'read', @@ -178,7 +178,7 @@ void test('batches tenant-isolated states once, rejects malformed/unavailable re const missing = await runEffectTestPromise( Effect.flip(gate.check(missingSnapshot, missingDescriptor)), ); - assert.equal(missing._tag, 'ModuleStateDeniedError'); + assert.ok(Predicate.isTagged(missing, 'ModuleStateDeniedError')); await runEffectTestPromise( database.executor.transaction((transaction) => @@ -201,7 +201,7 @@ void test('batches tenant-isolated states once, rejects malformed/unavailable re Effect.flip(gate.recheckWrite(transaction, tenantOne, write)), ), ); - assert.equal(lockedDenial._tag, 'ModuleStateDeniedError'); + assert.ok(Predicate.isTagged(lockedDenial, 'ModuleStateDeniedError')); const unavailable = await runEffectTestPromise( Effect.flip( @@ -211,7 +211,7 @@ void test('batches tenant-isolated states once, rejects malformed/unavailable re ), ), ); - assert.equal(unavailable._tag, 'ModuleStateCheckUnavailableError'); + assert.ok(Predicate.isTagged(unavailable, 'ModuleStateCheckUnavailableError')); assert.doesNotMatch(unavailable.reason, /secret|db failure/u); const malformed = await runEffectTestPromise( @@ -222,7 +222,7 @@ void test('batches tenant-isolated states once, rejects malformed/unavailable re ), ), ); - assert.equal(malformed._tag, 'ModuleStateCheckUnavailableError'); + assert.ok(Predicate.isTagged(malformed, 'ModuleStateCheckUnavailableError')); assert.doesNotMatch(malformed.reason, /corrupt|storage/u); } finally { await runEffectTestPromise( 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 e945ded5d..1da0e2a30 100644 --- a/app/packages/core-runtime/tests/integration/principal-management.test.ts +++ b/app/packages/core-runtime/tests/integration/principal-management.test.ts @@ -5,7 +5,7 @@ import { // @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. import { eq } from 'drizzle-orm'; -import { Effect, Exit as NativeExit, Scope as NativeScope } from 'effect'; +import { Effect, Exit as NativeExit, Scope as NativeScope, Predicate } from 'effect'; import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import test, { after as afterNativeDatabase } from 'node:test'; @@ -118,7 +118,7 @@ void test('persists managed key lifecycle without credential material and enforc ), ), ); - assert.equal(duplicate._tag, 'IdentityLifecycleConflictError'); + assert.ok(Predicate.isTagged(duplicate, 'IdentityLifecycleConflictError')); const missingReason = await runEffectTestPromise( database.transaction((transaction) => @@ -139,7 +139,7 @@ void test('persists managed key lifecycle without credential material and enforc ), ), ); - assert.equal(missingReason._tag, 'IdentityTargetInvalidError'); + assert.ok(Predicate.isTagged(missingReason, 'IdentityTargetInvalidError')); await runEffectTestPromise( database.transaction((transaction) => 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 8d5adeab2..61a728070 100644 --- a/app/packages/core-runtime/tests/integration/principal-resolver.test.ts +++ b/app/packages/core-runtime/tests/integration/principal-resolver.test.ts @@ -4,7 +4,7 @@ import { } from '@app/core-runtime/testing/effect-runtime'; import { and, eq } from 'drizzle-orm'; -import { DateTime, Effect, Exit as NativeExit, Scope as NativeScope } from 'effect'; +import { DateTime, Effect, Exit as NativeExit, Scope as NativeScope, Predicate } from 'effect'; import assert from 'node:assert/strict'; import test, { after as afterNativeDatabase } from 'node:test'; import { Pool } from 'pg'; @@ -118,7 +118,7 @@ effectTest( const foreignResolution = yield* Effect.flip( resolver.resolveBetterAuthUserForTenant('foreign-better-auth-subject', tenantOne), ); - assert.equal(foreignResolution._tag, 'PrincipalBindingMissingError'); + assert.ok(Predicate.isTagged(foreignResolution, 'PrincipalBindingMissingError')); yield* database .update(principalAuthBindings) @@ -133,7 +133,7 @@ effectTest( const revokedResolution = yield* Effect.flip( resolver.resolveBetterAuthUserForTenant(subject, tenantOne), ); - assert.equal(revokedResolution._tag, 'PrincipalBindingInactiveError'); + assert.ok(Predicate.isTagged(revokedResolution, 'PrincipalBindingInactiveError')); yield* database .update(principalAuthBindings) @@ -146,7 +146,7 @@ effectTest( const inactivePrincipal = yield* Effect.flip( resolver.resolveBetterAuthUserForTenant(subject, tenantOne), ); - assert.equal(inactivePrincipal._tag, 'PrincipalInactiveError'); + assert.ok(Predicate.isTagged(inactivePrincipal, 'PrincipalInactiveError')); yield* database .update(principals) @@ -159,7 +159,7 @@ effectTest( const inactiveTenant = yield* Effect.flip( resolver.resolveBetterAuthUserForTenant(subject, tenantOne), ); - assert.equal(inactiveTenant._tag, 'TenantInactiveError'); + assert.ok(Predicate.isTagged(inactiveTenant, 'TenantInactiveError')); }).pipe( Effect.ensuring(cleanup.pipe(Effect.orDie)), Effect.ensuring(databaseEffect(pool.end.bind(pool)).pipe(Effect.orDie)), 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 4cd1c0c22..9052c6bf8 100644 --- a/app/packages/core-runtime/tests/integration/search-persistence.test.ts +++ b/app/packages/core-runtime/tests/integration/search-persistence.test.ts @@ -3,7 +3,14 @@ import { makeEffectTestCallback, } from '@app/core-runtime/testing/effect-runtime'; -import { Effect, Function as Fn, Exit as NativeExit, Scope as NativeScope, Schema } from 'effect'; +import { + Effect, + Function as Fn, + Exit as NativeExit, + Scope as NativeScope, + Schema, + Predicate, +} from 'effect'; import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import test, { after as afterNativeDatabase } from 'node:test'; @@ -245,7 +252,7 @@ effectTest( const divergence = yield* Effect.flip( restarted.replace({ ...emptyRebuild, documents: [staleDocument] }), ); - assert.equal(divergence._tag, 'CoreSearchProjectionInvalid'); + assert.ok(Predicate.isTagged(divergence, 'CoreSearchProjectionInvalid')); yield* restarted.apply({ document: { ...staleDocument, projectionVersion: '3' }, kind: 'upsert', 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..445eed263 100644 --- a/app/packages/core-runtime/tests/unit/action-collector.test.ts +++ b/app/packages/core-runtime/tests/unit/action-collector.test.ts @@ -2,7 +2,7 @@ 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 { Effect, Schema, Predicate } from 'effect'; import { createActionCollector } from '../../src/actions/collector.ts'; const event = (id: string) => @@ -85,8 +85,8 @@ void test('rejects orphan and foreign Domain Event references', async () => { Effect.flip(second.addOutboxMessageInput({}, message('counter.project'))), ); - assert.equal(foreignError._tag, 'ActionCollectorError'); - assert.equal(orphanError._tag, 'ActionCollectorError'); + assert.ok(Predicate.isTagged(foreignError, 'ActionCollectorError')); + assert.ok(Predicate.isTagged(orphanError, 'ActionCollectorError')); }); void test('does not expose externally mutable collector arrays or captured payloads', async () => { @@ -148,10 +148,10 @@ void test('captures one immutable JSON audit-evidence object and rejects invalid }).recordAuditEvidence({ checkpoint: 'started' }), ), ); - assert.equal(repeated._tag, 'ActionCollectorError'); - assert.equal(invalid._tag, 'ActionCollectorError'); - assert.equal(undeclared._tag, 'ActionCollectorError'); - assert.equal(missingSchema._tag, 'ActionCollectorError'); + assert.ok(Predicate.isTagged(repeated, 'ActionCollectorError')); + assert.ok(Predicate.isTagged(invalid, 'ActionCollectorError')); + assert.ok(Predicate.isTagged(undeclared, 'ActionCollectorError')); + assert.ok(Predicate.isTagged(missingSchema, 'ActionCollectorError')); }); void test('applies descriptor evidence policy and rejects incompatible evidence', async () => { @@ -171,7 +171,7 @@ void test('applies descriptor evidence policy and rejects incompatible evidence' ), ); - assert.equal(error._tag, 'ActionCollectorError'); + assert.ok(Predicate.isTagged(error, 'ActionCollectorError')); const metadataCollector = makeCollector(); await runEffectTestPromise( @@ -207,7 +207,7 @@ void test('rejects an Outbox producer that differs from its registered Domain Ev ), ); - assert.equal(error._tag, 'ActionCollectorError'); + assert.ok(Predicate.isTagged(error, 'ActionCollectorError')); }); void test('enforces Action-declared event payloads and producer ownership', async () => { @@ -245,9 +245,9 @@ void test('enforces Action-declared event payloads and producer ownership', asyn ), ); - assert.equal(invalidPayload._tag, 'ActionCollectorError'); - assert.equal(invalidProducer._tag, 'ActionCollectorError'); - assert.equal(undeclared._tag, 'ActionCollectorError'); - assert.equal(inheritedName._tag, 'ActionCollectorError'); + assert.ok(Predicate.isTagged(invalidPayload, 'ActionCollectorError')); + assert.ok(Predicate.isTagged(invalidProducer, 'ActionCollectorError')); + assert.ok(Predicate.isTagged(undeclared, 'ActionCollectorError')); + assert.ok(Predicate.isTagged(inheritedName, 'ActionCollectorError')); assert.equal(collector.snapshot().domainEvents.length, 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 41b624853..aa403c136 100644 --- a/app/packages/core-runtime/tests/unit/action-definition.test.ts +++ b/app/packages/core-runtime/tests/unit/action-definition.test.ts @@ -56,7 +56,7 @@ void test('defines an immutable typed descriptor and decodes typed payloads and assert.equal(Object.isFrozen(registration.descriptor.policies), true); }); -test('keeps the Resource permission resolver private behind an immutable declaration', () => { +void test('keeps the Resource permission resolver private behind an immutable declaration', () => { const permission = defineActionResourcePermission<{ readonly counterpartyId: string }>( ({ counterpartyId }) => ({ permission: 'write', @@ -74,7 +74,7 @@ test('keeps the Resource permission resolver private behind an immutable declara assert.equal('resolver' in permission, false); }); -test('requires trusted Legal Entity scope for a Counterparty permission declaration', () => { +void test('requires trusted Legal Entity scope for a Counterparty permission declaration', () => { const entrypoint = defineTenantModuleEntrypoint({ access: 'write', authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, @@ -102,7 +102,7 @@ test('requires trusted Legal Entity scope for a Counterparty permission declarat ); }); -test('uses Schema.Void for a no-payload Action', async () => { +void test('uses Schema.Void for a no-payload Action', async () => { const registration = defineAction( { accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'cache.read.v1' }, @@ -137,7 +137,7 @@ test('uses Schema.Void for a no-payload Action', async () => { ); assert.equal(payload, undefined); - assert.equal(invalid._tag, 'ActionPayloadValidationError'); + assert.ok(Predicate.isTagged(invalid, 'ActionPayloadValidationError')); }); void test('keeps the private handler outside the public Action registration', () => { @@ -175,7 +175,7 @@ void test('rejects invalid declared results through a typed error', async () => Effect.flip(decodeActionResult(Schema.Struct({ id: Schema.String }), { id: 1 })), ); - assert.equal(error._tag, 'ActionResultValidationError'); + assert.ok(Predicate.isTagged(error, 'ActionResultValidationError')); assert.equal(error.code, 'action_result_invalid'); }); 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..917da6b2b 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,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { Schema } from 'effect'; +import { Schema, Predicate } from 'effect'; import { ACTION_CORE_ERROR_TAGS, ActionAlreadyCommitted, @@ -109,10 +109,10 @@ void test('publishes the exhaustive stable Core Action error tags', () => { }), ]; - assert.deepEqual( - errors.map((error) => error._tag), - ACTION_CORE_ERROR_TAGS, - ); + assert.equal(errors.length, ACTION_CORE_ERROR_TAGS.length); + for (const [index, tag] of ACTION_CORE_ERROR_TAGS.entries()) { + assert.ok(Predicate.isTagged(errors[index], tag)); + } for (const error of errors) { assert.equal(error.reason.includes('postgresql://'), false); assert.equal(error.reason.includes('ontos-local-development-key'), false); 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..e00e5113e 100644 --- a/app/packages/core-runtime/tests/unit/action-policy.test.ts +++ b/app/packages/core-runtime/tests/unit/action-policy.test.ts @@ -2,7 +2,7 @@ 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 { Effect, Predicate } from 'effect'; import { defineGlobalPolicy, defineMicroverticalPolicy, @@ -84,7 +84,7 @@ void test('evaluates typed allow and safe denial outcomes', async () => { const denial = await runEffectTestPromise(Effect.flip(denied.evaluate(input))); assert.deepEqual(observed, [input]); - assert.equal(denial._tag, 'PolicyDenied'); + assert.ok(Predicate.isTagged(denial, 'PolicyDenied')); assert.equal(denial.reasonCode, 'stock_unavailable'); assert.equal(denial.reason, 'Requested stock is unavailable — retry later'); assert.equal(Object.isFrozen(denial), true); 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 eb353548c..9b5b8da92 100644 --- a/app/packages/core-runtime/tests/unit/action-runtime.test.ts +++ b/app/packages/core-runtime/tests/unit/action-runtime.test.ts @@ -450,7 +450,7 @@ const makeRepositoryFailures = async () => { return { cause, persistenceFailure, transactionFailure }; }; -test('repository constructors retain original causes across Effect Cause propagation', async () => { +void 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))), @@ -467,14 +467,14 @@ test('repository constructors retain original causes across Effect Cause propaga ); }); -test('public error classes expose no retained-cause accessors', () => { +void test('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); } }); -test('repository causes are absent from reflection, JSON, and Schema encoding', async () => { +void test('repository causes are absent from reflection, JSON, and Schema encoding', async () => { const { persistenceFailure, transactionFailure } = await makeRepositoryFailures(); const publicTransaction = new ActionTransactionError({ code: transactionFailure.code, @@ -502,7 +502,7 @@ test('repository causes are absent from reflection, JSON, and Schema encoding', }); }); -test('repository cause readers reject foreign objects carrying the former cause property', () => { +void test('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( @@ -574,7 +574,7 @@ const registration = () => }), ); -test('executes the complete stage order with transaction ownership and success evidence', async () => { +void test('executes the complete stage order with transaction ownership and success evidence', async () => { const harness = makeHarness(); const result = await runEffectTestPromise( harness.runtime.runAction({ @@ -612,7 +612,7 @@ test('executes the complete stage order with transaction ownership and success e }); }); -test('hashes the encoded representation of decoded DateTime and Option values', async () => { +void 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), @@ -679,7 +679,7 @@ test('hashes the encoded representation of decoded DateTime and Option values', ); }); -test('uses a resolver-branded recovery only for the exact support-stop Action and still checks permission', async () => { +void test('uses a resolver-branded recovery only for the exact support-stop Action and still checks permission', async () => { const recoveryPrincipal = await runEffectTestPromise( supportRecoveryPrincipalContextResolverFromRepository({ load: () => @@ -743,7 +743,7 @@ test('uses a resolver-branded recovery only for the exact support-stop Action an .pipe(providePrincipalManagementRepository), ), ); - assert.equal(denied._tag, 'ActionPermissionDenied'); + assert.ok(Predicate.isTagged(denied, 'ActionPermissionDenied')); assert.deepEqual(deniedHarness.permissionCounts(), { permissionCheckCount: 1, rejectionCount: 1, @@ -766,7 +766,7 @@ test('uses a resolver-branded recovery only for the exact support-stop Action an .pipe(providePrincipalManagementRepository), ), ); - assert.equal(wrongCheckpoint._tag, 'ActionTrustedContextValidationError'); + assert.ok(Predicate.isTagged(wrongCheckpoint, 'ActionTrustedContextValidationError')); const wrongAction = await runEffectTestPromise( Effect.flip( @@ -778,10 +778,10 @@ test('uses a resolver-branded recovery only for the exact support-stop Action an }), ), ); - assert.equal(wrongAction._tag, 'ActionTrustedContextValidationError'); + assert.ok(Predicate.isTagged(wrongAction, 'ActionTrustedContextValidationError')); }); -test('fails business Actions closed before invocation, permission, Policy, or handler access', async () => { +void test('fails business Actions closed before invocation, permission, Policy, or handler access', async () => { await forEachSequential( ( [ @@ -840,7 +840,7 @@ test('fails business Actions closed before invocation, permission, Policy, or ha }), ), ); - assert.equal(failure._tag, 'ModuleStateDeniedError', state); + assert.ok(Predicate.isTagged(failure, 'ModuleStateDeniedError'), state); assert.equal(handlerCalls, 0); assert.equal(policyCalls, 0); assert.deepEqual(harness.counts(), { @@ -862,7 +862,7 @@ test('fails business Actions closed before invocation, permission, Policy, or ha ); }); -test('distinguishes unavailable early checks and rolls back a denied locked recheck', async () => { +void test('distinguishes unavailable early checks and rolls back a denied locked recheck', async () => { const action = defineAction( { accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'stock.read.v1' }, @@ -899,7 +899,7 @@ test('distinguishes unavailable early checks and rolls back a denied locked rech }), ), ); - assert.equal(unavailableFailure._tag, 'ModuleStateCheckUnavailableError'); + assert.ok(Predicate.isTagged(unavailableFailure, 'ModuleStateCheckUnavailableError')); assert.equal(unavailable.counts().createCount, 0); const locked = makeHarness({ lockedModuleState: 'denied' }); @@ -913,7 +913,7 @@ test('distinguishes unavailable early checks and rolls back a denied locked rech }), ), ); - assert.equal(lockedFailure._tag, 'ModuleStateDeniedError'); + assert.ok(Predicate.isTagged(lockedFailure, 'ModuleStateDeniedError')); assert.deepEqual(locked.gateCounts(), { handlerResolutionCount: 0, moduleStateReadCount: 1, @@ -927,7 +927,7 @@ test('distinguishes unavailable early checks and rolls back a denied locked rech }); }); -test('allows an explicitly authorized Action before Policy evaluation', async () => { +void test('allows an explicitly authorized Action before Policy evaluation', async () => { const harness = makeHarness({ permissionDecision: 'allowed' }); const result = await runEffectTestPromise( harness.runtime.runAction({ @@ -946,7 +946,7 @@ test('allows an explicitly authorized Action before Policy evaluation', async () assert.equal(harness.counts().transactionCount, 1); }); -test('requires a declared tenant role independently from the Action executor relation', async () => { +void test('requires a declared tenant role independently from the Action executor relation', async () => { const tenantAuthorizedRegistration = defineAction( { accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'identity.read.v1' }, @@ -993,7 +993,7 @@ test('requires a declared tenant role independently from the Action executor rel }), ), ); - assert.equal(failure._tag, expectedTag); + assert.ok(Predicate.isTagged(failure, expectedTag)); assert.equal(harness.counts().transitionCount, 0); }, ); @@ -1013,7 +1013,7 @@ test('requires a declared tenant role independently from the Action executor rel assert.equal(allowed.counts().transitionCount, 1); }); -test('accepts every Party write authority as an explicit tenant permission', async () => { +void test('accepts every Party write authority as an explicit tenant permission', async () => { const partyPermissions = [ 'manage_party_identity', 'manage_party_relationships', @@ -1073,7 +1073,7 @@ test('accepts every Party write authority as an explicit tenant permission', asy ); }); -test('canonicalizes every resolved tenant permission target for hash and evidence', async () => { +void test('canonicalizes every resolved tenant permission target for hash and evidence', async () => { const action = defineAction( { accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'identity.read.v1' }, @@ -1136,7 +1136,7 @@ test('canonicalizes every resolved tenant permission target for hash and evidenc assert.deepEqual(second.flushed[0]?.transport, first.flushed[0]?.transport); }); -test('authorizes Counterparty creation against the trusted Legal Entity before Policy and transaction', async () => { +void test('authorizes Counterparty creation against the trusted Legal Entity before Policy and transaction', async () => { let handlerCalls = 0; let policyCalls = 0; const action = defineAction( @@ -1194,7 +1194,7 @@ test('authorizes Counterparty creation against the trusted Legal Entity before P }), ), ); - assert.equal(failure._tag, 'ActionPermissionDenied'); + assert.ok(Predicate.isTagged(failure, 'ActionPermissionDenied')); assert.equal(policyCalls, 0); assert.equal(handlerCalls, 0); assert.equal(denied.counts().transactionCount, 0); @@ -1223,7 +1223,7 @@ test('authorizes Counterparty creation against the trusted Legal Entity before P }), ), ); - assert.equal(unavailableFailure._tag, 'ActionPermissionCheckError'); + assert.ok(Predicate.isTagged(unavailableFailure, 'ActionPermissionCheckError')); assert.equal(unavailable.rejections.length, 0); assert.equal(unavailable.counts().transactionCount, 0); assert.equal(unavailable.stages.includes('permission_checked'), false); @@ -1251,7 +1251,7 @@ test('authorizes Counterparty creation against the trusted Legal Entity before P ); }); -test('authorizes the resolved Resource target before Policy, transaction, and handler', async () => { +void test('authorizes the resolved Resource target before Policy, transaction, and handler', async () => { let handlerCalls = 0; let policyCalls = 0; const action = defineAction( @@ -1325,7 +1325,7 @@ test('authorizes the resolved Resource target before Policy, transaction, and ha ), ); - assert.equal(failure._tag, 'ActionPermissionDenied'); + assert.ok(Predicate.isTagged(failure, 'ActionPermissionDenied')); assert.equal(policyCalls, 0); assert.equal(handlerCalls, 0); assert.equal(denied.counts().transactionCount, 0); @@ -1363,12 +1363,12 @@ test('authorizes the resolved Resource target before Policy, transaction, and ha }), ), ); - assert.equal(unavailableFailure._tag, 'ActionPermissionCheckError'); + assert.ok(Predicate.isTagged(unavailableFailure, 'ActionPermissionCheckError')); assert.equal(unavailable.rejections.length, 0); assert.equal(unavailable.counts().transactionCount, 0); }); -test('persists a definite permission denial before returning it and never evaluates Policies', async () => { +void test('persists a definite permission denial before returning it and never evaluates Policies', async () => { let handlerCount = 0; let policyCount = 0; let serviceFactoryCount = 0; @@ -1424,7 +1424,7 @@ test('persists a definite permission denial before returning it and never evalua ), ); - assert.equal(failure._tag, 'ActionPermissionDenied'); + assert.ok(Predicate.isTagged(failure, 'ActionPermissionDenied')); assert.equal(failure.code, 'action_permission_denied'); assert.equal(handlerCount, 0); assert.equal(policyCount, 0); @@ -1458,7 +1458,7 @@ test('persists a definite permission denial before returning it and never evalua ]); }); -test('fails closed before Policy evaluation when permission cannot be determined', async () => { +void test('fails closed before Policy evaluation when permission cannot be determined', async () => { const harness = makeHarness({ permissionFailure: true }); const failure = await runEffectTestPromise( Effect.flip( @@ -1471,7 +1471,7 @@ test('fails closed before Policy evaluation when permission cannot be determined ), ); - assert.equal(failure._tag, 'ActionPermissionCheckError'); + assert.ok(Predicate.isTagged(failure, 'ActionPermissionCheckError')); assert.deepEqual(harness.permissionCounts(), { permissionCheckCount: 1, rejectionCount: 0, @@ -1491,7 +1491,7 @@ test('fails closed before Policy evaluation when permission cannot be determined ]); }); -test('does not claim permission denial when terminal evidence persistence rolls back', async () => { +void 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( @@ -1504,7 +1504,7 @@ test('does not claim permission denial when terminal evidence persistence rolls ), ); - assert.equal(failure._tag, 'ActionTransactionError'); + assert.ok(Predicate.isTagged(failure, 'ActionTransactionError')); assert.deepEqual(harness.permissionCounts(), { permissionCheckCount: 1, rejectionCount: 1, @@ -1513,7 +1513,7 @@ test('does not claim permission denial when terminal evidence persistence rolls assert.equal(harness.counts().transactionCount, 0); }); -test('evaluates Policies in order before running and hands allowed checkpoints to success', async () => { +void test('evaluates Policies in order before running and hands allowed checkpoints to success', async () => { const observed: string[] = []; const globalPolicy = defineGlobalPolicy<{ readonly amount: number }>({ evaluate: () => { @@ -1584,7 +1584,7 @@ test('evaluates Policies in order before running and hands allowed checkpoints t ]); }); -test('short-circuits the first Policy denial, finalizes it, and never starts execution', async () => { +void test('short-circuits the first Policy denial, finalizes it, and never starts execution', async () => { const observed: string[] = []; let handlerExecutions = 0; const policies = [ @@ -1650,7 +1650,7 @@ test('short-circuits the first Policy denial, finalizes it, and never starts exe ), ); - assert.equal(denial._tag, 'ActionPolicyDenied'); + assert.ok(Predicate.isTagged(denial, 'ActionPolicyDenied')); assert.equal(denial.policyReasonCode, 'counter_locked'); assert.equal(denial.reason, 'Counter changes are locked — try later'); assert.deepEqual(observed, ['first', 'denied']); @@ -1682,7 +1682,7 @@ test('short-circuits the first Policy denial, finalizes it, and never starts exe assert.equal(harness.flushed.length, 0); }); -test('sanitizes Policy defects and interrupts without finalizing', async () => { +void test('sanitizes Policy defects and interrupts without finalizing', async () => { const evaluators = [() => Effect.die('secret evaluator defect'), () => Effect.interrupt] as const; await forEachSequential( @@ -1732,7 +1732,7 @@ test('sanitizes Policy defects and interrupts without finalizing', async () => { ), ); - assert.equal(error._tag, 'ActionPolicyEvaluationError'); + assert.ok(Predicate.isTagged(error, 'ActionPolicyEvaluationError')); assert.equal(error.reason.includes('secret'), false); assert.equal(handlerExecutions, 0); assert.equal(harness.finalized.length, 0); @@ -1746,7 +1746,7 @@ test('sanitizes Policy defects and interrupts without finalizing', async () => { ); }); -test('returns persistence failure when denial evidence cannot be finalized', async () => { +void 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')), @@ -1792,13 +1792,13 @@ test('returns persistence failure when denial evidence cannot be finalized', asy ), ); - assert.equal(error._tag, 'ActionInvocationPersistenceError'); + assert.ok(Predicate.isTagged(error, 'ActionInvocationPersistenceError')); assert.equal(handlerExecutions, 0); assert.equal(harness.finalized.length, 0); assert.equal(harness.counts().transactionCount, 0); }); -test('creates fresh collectors for every execution', async () => { +void test('creates fresh collectors for every execution', async () => { const harness = makeHarness(); await forEachSequential( [ @@ -1825,7 +1825,7 @@ test('creates fresh collectors for every execution', async () => { assert.notEqual(harness.flushed[0]?.evidence, harness.flushed[1]?.evidence); }); -test('evaluates Policies afresh for separate invocations', async () => { +void test('evaluates Policies afresh for separate invocations', async () => { let evaluations = 0; const policy = defineGlobalPolicy<{ readonly amount: number }>({ evaluate: () => { @@ -1880,7 +1880,7 @@ test('evaluates Policies afresh for separate invocations', async () => { assert.equal(evaluations, 2); }); -test('rejects structural payloads, trusted context, and missing idempotency before invocation', async () => { +void test('rejects structural payloads, trusted context, and missing idempotency before invocation', async () => { const harness = makeHarness(); const invalidPayload = await runEffectTestPromise( Effect.flip( @@ -1928,14 +1928,14 @@ test('rejects structural payloads, trusted context, and missing idempotency befo ), ); - assert.equal(invalidPayload._tag, 'ActionPayloadValidationError'); - assert.equal(invalidPrincipal._tag, 'ActionTrustedContextValidationError'); - assert.equal(missingKey._tag, 'ActionIdempotencyKeyRequired'); - assert.equal(forgedSystemPrincipal._tag, 'ActionTrustedContextValidationError'); + assert.ok(Predicate.isTagged(invalidPayload, 'ActionPayloadValidationError')); + assert.ok(Predicate.isTagged(invalidPrincipal, 'ActionTrustedContextValidationError')); + assert.ok(Predicate.isTagged(missingKey, 'ActionIdempotencyKeyRequired')); + assert.ok(Predicate.isTagged(forgedSystemPrincipal, 'ActionTrustedContextValidationError')); assert.equal(harness.counts().createCount, 0); }); -test('preserves declared domain rejections and rolls back collected evidence', async () => { +void test('preserves declared domain rejections and rolls back collected evidence', async () => { const DomainRejectedContract = Schema.TaggedStruct('DomainRejected', { reason: Schema.String, }); @@ -2001,13 +2001,13 @@ test('preserves declared domain rejections and rolls back collected evidence', a ), ); - assert.equal(error._tag, 'DomainRejected'); + assert.ok(Predicate.isTagged(error, 'DomainRejected')); assert.equal(error.reason, 'counter_locked'); assert.equal(policyEvaluations, 1); assert.equal(harness.flushed.length, 0); }); -test('sanitizes unexpected defects and rejects invalid typed results', async () => { +void test('sanitizes unexpected defects and rejects invalid typed results', async () => { const defectHarness = makeHarness(); const defective = defineAction( { @@ -2084,14 +2084,14 @@ test('sanitizes unexpected defects and rejects invalid typed results', async () ), ); - assert.equal(defect._tag, 'ActionHandlerExecutionError'); + assert.ok(Predicate.isTagged(defect, 'ActionHandlerExecutionError')); assert.equal(defect.reason.includes('secret'), false); - assert.equal(resultError._tag, 'ActionResultValidationError'); + assert.ok(Predicate.isTagged(resultError, 'ActionResultValidationError')); assert.equal(defectHarness.flushed.length, 0); assert.equal(resultHarness.flushed.length, 0); }); -test('sanitizes undeclared handler failures instead of widening the domain error contract', async () => { +void test('sanitizes undeclared handler failures instead of widening the domain error contract', async () => { const DeclaredDomainErrorContract = Schema.TaggedStruct('DeclaredDomainError', { reason: Schema.String, }); @@ -2141,12 +2141,12 @@ test('sanitizes undeclared handler failures instead of widening the domain error ), ); - assert.equal(error._tag, 'ActionHandlerExecutionError'); + assert.ok(Predicate.isTagged(error, 'ActionHandlerExecutionError')); assert.equal(error.reason.includes('secret'), false); assert.equal(harness.flushed.length, 0); }); -test('handles committed, conflict, definite rollback, and indeterminate commit branches', async () => { +void test('handles committed, conflict, definite rollback, and indeterminate commit branches', async () => { const committed = makeHarness({ createRecord: { actionInvocationId: 'committed', @@ -2238,23 +2238,23 @@ test('handles committed, conflict, definite rollback, and indeterminate commit b }), ); - assert.equal(committedError._tag, 'ActionAlreadyCommitted'); + assert.ok(Predicate.isTagged(committedError, 'ActionAlreadyCommitted')); assert.equal(committed.counts().transactionCount, 0); assert.equal(committed.permissionCounts().permissionCheckCount, 0); - assert.equal(conflictError._tag, 'ActionRequestHashConflict'); + assert.ok(Predicate.isTagged(conflictError, '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.ok(Predicate.isTagged(definiteError, 'ActionTransactionError')); + assert.ok(Predicate.isTagged(definiteCommitError, 'ActionTransactionError')); + assert.ok(Predicate.isTagged(uncertainError, 'ActionCommitIndeterminate')); assert.equal(uncertain.flushed.length, 1); - assert.deepEqual( - acknowledgementErrors.map((error) => error._tag), - acknowledgementFailureCodes.map(() => 'ActionCommitIndeterminate'), - ); + assert.equal(acknowledgementErrors.length, acknowledgementFailureCodes.length); + for (const error of acknowledgementErrors) { + assert.ok(Predicate.isTagged(error, 'ActionCommitIndeterminate')); + } }); -test('interruption during commit waits for native commit settlement', async () => { +void test('interruption during commit waits for native commit settlement', async () => { const commitStarted = Deferred.makeUnsafe(); const commitSettlement = Deferred.makeUnsafe(); const harness = makeHarness({ @@ -2287,7 +2287,7 @@ test('interruption during commit waits for native commit settlement', async () = assert.equal(harness.flushed.length, 1); }); -test('resolves commit state explicitly and keeps unavailable outcomes indeterminate', async () => { +void test('resolves commit state explicitly and keeps unavailable outcomes indeterminate', async () => { const invocationId = '00000000-0000-4000-8000-000000000099'; const open = makeHarness({ createRecord: { @@ -2330,12 +2330,12 @@ test('resolves commit state explicitly and keeps unavailable outcomes indetermin _tag: 'ActionCommitOpen', invocationId, }); - assert.equal(committedResolution._tag, 'ActionAlreadyCommitted'); - assert.equal(unavailableResolution._tag, 'ActionCommitIndeterminate'); + assert.ok(Predicate.isTagged(committedResolution, 'ActionAlreadyCommitted')); + assert.ok(Predicate.isTagged(unavailableResolution, 'ActionCommitIndeterminate')); assert.equal(unavailableResolution.invocationId, invocationId); }); -test('rejects terminal invocation states before handler execution', async () => { +void test('rejects terminal invocation states before handler execution', async () => { const terminal = makeHarness({ createRecord: { actionInvocationId: 'terminal', @@ -2355,12 +2355,12 @@ test('rejects terminal invocation states before handler execution', async () => ), ); - assert.equal(error._tag, 'ActionInvocationStateError'); + assert.ok(Predicate.isTagged(error, 'ActionInvocationStateError')); assert.equal(terminal.counts().transitionCount, 0); assert.equal(terminal.counts().transactionCount, 0); }); -test('uses one runtime contract for Shell/Core and MicroVertical-shaped registrations', async () => { +void test('uses one runtime contract for Shell/Core and MicroVertical-shaped registrations', async () => { const shell = makeHarness(); const microvertical = makeHarness(); const moduleRegistration = defineAction( @@ -2412,6 +2412,6 @@ test('uses one runtime contract for Shell/Core and MicroVertical-shaped registra assert.deepEqual(moduleResult, { reserved: true }); }); -test('the Core database service identity remains server-only', () => { +void test('the Core database service identity remains server-only', () => { assert.equal(Predicate.isFunction(CoreDatabase), 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 b388d83bd..7632d1247 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 @@ -2,7 +2,7 @@ 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 { Effect, Schema, Predicate } from 'effect'; import { defineAction } from '../../src/actions/definition.ts'; import { ACTION_RUNTIME_STAGES } from '../../src/actions/runtime.ts'; import { defineTenantModuleEntrypoint } from '../../src/modules/module-entrypoint.ts'; @@ -65,7 +65,7 @@ const request = { transport: { correlationId: 'action-harness-test', idempotencyKey: 'increment-once' }, } as const; -test('runs the real Action lifecycle and preserves committed replay semantics', async () => { +void test('runs the real Action lifecycle and preserves committed replay semantics', async () => { const harness = makeActionTestHarness({ actionPermission: 'allowed', tenantPermission: 'allowed', @@ -75,7 +75,7 @@ test('runs the real Action lifecycle and preserves committed replay semantics', const replay = await runEffectTestPromise(harness.runtime.runAction(request).pipe(Effect.flip)); const snapshot = harness.snapshot(); - assert.equal(replay._tag, 'ActionAlreadyCommitted'); + assert.ok(Predicate.isTagged(replay, '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'); @@ -85,12 +85,12 @@ test('runs the real Action lifecycle and preserves committed replay semantics', assert.equal(snapshot.committed[0]?.evidence.outboxMessages.length, 1); }); -test('defaults authorization closed and never starts a transaction for a denial', async () => { +void 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.ok(Predicate.isTagged(denied, 'ActionPermissionDenied')); assert.equal(snapshot.invocations.length, 1); assert.equal(snapshot.invocations[0]?.status, 'rejected'); assert.equal(snapshot.permissionDenials.length, 1); @@ -98,7 +98,7 @@ test('defaults authorization closed and never starts a transaction for a denial' assert.equal(snapshot.stages.includes('handler_executed'), false); }); -test('substitutes typed owner services without replacing the private handler', async () => { +void test('substitutes typed owner services without replacing the private handler', async () => { interface CounterServices { readonly increment: (amount: number) => Effect.Effect; } @@ -156,7 +156,7 @@ test('substitutes typed owner services without replacing the private handler', a assert.equal(harness.snapshot().committed.length, 1); }); -test('rejects missing idempotency before creating an invocation', async () => { +void test('rejects missing idempotency before creating an invocation', async () => { const harness = makeActionTestHarness({ actionPermission: 'allowed', tenantPermission: 'allowed', @@ -172,6 +172,6 @@ test('rejects missing idempotency before creating an invocation', async () => { .pipe(Effect.flip), ); - assert.equal(failure._tag, 'ActionIdempotencyKeyRequired'); + assert.ok(Predicate.isTagged(failure, 'ActionIdempotencyKeyRequired')); assert.equal(harness.snapshot().invocations.length, 0); }); 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..e4413c5ff 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 @@ -3,7 +3,7 @@ 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 { Effect, Schema, Predicate } 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,7 +17,7 @@ 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 () => { +void test('committed retry and explicit recovery return the same invocation without rerunning or replaying the result', async () => { let executions = 0; const registration = defineAction( { @@ -64,7 +64,7 @@ test('committed retry and explicit recovery return the same invocation without r ); for (const outcome of [replay, recovered]) { - assert.equal(outcome._tag, 'ActionAlreadyCommitted'); + assert.ok(Predicate.isTagged(outcome, 'ActionAlreadyCommitted')); assert.equal('invocationId' in outcome ? outcome.invocationId : undefined, invocationId); assert.equal('total' in outcome, false); assert.equal('result' in outcome, false); @@ -74,7 +74,7 @@ test('committed retry and explicit recovery return the same invocation without r assert.equal(harness.snapshot().transactionCount, 1); }); -test('committed error schema requires and preserves the recovery invocation identifier', async () => { +void test('committed error schema requires and preserves the recovery invocation identifier', async () => { const encoded = { _tag: 'ActionAlreadyCommitted', code: 'action_already_committed', @@ -100,7 +100,7 @@ test('committed error schema requires and preserves the recovery invocation iden assert.equal('status' in decoded, false); }); -test('lost commit acknowledgement recovers the committed invocation and faults only once', async () => { +void test('lost commit acknowledgement recovers the committed invocation and faults only once', async () => { let executions = 0; const registration = defineAction( { @@ -144,7 +144,7 @@ test('lost commit acknowledgement recovers the committed invocation and faults o const uncertain = await runEffectTestPromise( harness.runtime.runAction(request).pipe(Effect.flip), ); - assert.equal(uncertain._tag, 'ActionCommitIndeterminate'); + assert.ok(Predicate.isTagged(uncertain, 'ActionCommitIndeterminate')); assert.ok('invocationId' in uncertain); assert.equal(uncertain.invocationId, harness.snapshot().invocations[0]?.actionInvocationId); assert.equal(harness.snapshot().invocations[0]?.status, 'succeeded'); @@ -156,7 +156,7 @@ test('lost commit acknowledgement recovers the committed invocation and faults o ); const replay = await runEffectTestPromise(harness.runtime.runAction(request).pipe(Effect.flip)); for (const outcome of [recovered, replay]) { - assert.equal(outcome._tag, 'ActionAlreadyCommitted'); + assert.ok(Predicate.isTagged(outcome, 'ActionAlreadyCommitted')); assert.ok('invocationId' in outcome); assert.equal(outcome.invocationId, uncertain.invocationId); } diff --git a/app/packages/core-runtime/tests/unit/config.test.ts b/app/packages/core-runtime/tests/unit/config.test.ts index 6dd59706c..5c68dc752 100644 --- a/app/packages/core-runtime/tests/unit/config.test.ts +++ b/app/packages/core-runtime/tests/unit/config.test.ts @@ -2,7 +2,7 @@ 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 { Effect, Predicate } from 'effect'; import { acquirePoolResource } from '../../src/db/client.ts'; import { ROOT_ENV_PATH, @@ -60,8 +60,8 @@ void test('keeps missing and malformed configuration in the typed error channel' ), ); - assert.equal(missing._tag, 'DatabaseConfigError'); - assert.equal(malformed._tag, 'DatabaseConfigError'); + assert.ok(Predicate.isTagged(missing, 'DatabaseConfigError')); + assert.ok(Predicate.isTagged(malformed, 'DatabaseConfigError')); }); void test('requires distinct administrative and least-privilege runtime identities', async () => { @@ -113,10 +113,10 @@ void test('requires distinct administrative and least-privilege runtime identiti 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'); + assert.ok(Predicate.isTagged(missing, 'DatabaseConfigError')); + assert.ok(Predicate.isTagged(identical, 'DatabaseConfigError')); + assert.ok(Predicate.isTagged(queryParameterCollision, 'DatabaseConfigError')); + assert.ok(Predicate.isTagged(superuserCompatible, 'DatabaseConfigError')); }); void test('finalizes the pool resource when its Effect scope closes', async () => { 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..f4e060cad 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,5 +1,5 @@ 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'; @@ -145,17 +145,17 @@ void test('distinguishes commit ambiguity from definite transaction failures', ( const administrativeShutdown = decodeDatabaseDriverFailure({ code: '57P01' }); const serializationFailure = decodeDatabaseDriverFailure({ code: '40001' }); - assert.equal( - Option.isSome(connectionFailure) && connectionFailure.value._tag, - 'DatabaseCommitAcknowledgementAmbiguous', + assert.ok( + Option.isSome(connectionFailure) && + Predicate.isTagged(connectionFailure.value, 'DatabaseCommitAcknowledgementAmbiguous'), ); - assert.equal( - Option.isSome(administrativeShutdown) && administrativeShutdown.value._tag, - 'DatabaseCommitAcknowledgementAmbiguous', + assert.ok( + Option.isSome(administrativeShutdown) && + Predicate.isTagged(administrativeShutdown.value, 'DatabaseCommitAcknowledgementAmbiguous'), ); - assert.equal( - Option.isSome(serializationFailure) && serializationFailure.value._tag, - 'DatabaseTransactionFailure', + assert.ok( + Option.isSome(serializationFailure) && + Predicate.isTagged(serializationFailure.value, 'DatabaseTransactionFailure'), ); assert.equal(isDatabaseCommitAcknowledgementAmbiguous({ code: '40001' }), false); assert.equal(isDatabaseCommitAcknowledgementAmbiguous({ code: '57014' }), false); 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..3938c4130 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 { Effect, flow, 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, @@ -28,14 +25,6 @@ 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', Effect.gen(function* listsActiveLegalEntities() { @@ -94,25 +83,29 @@ effectTest( yield* classifySelectedLegalEntity([activeRecord], tenantId, activeRecord.legalEntityId), { legalEntityId: activeRecord.legalEntityId, legalName: activeRecord.legalName }, ); - assert.equal( - yield* failureTag( - classifySelectedLegalEntity( - [activeRecord], - tenantId, - '20000000-0000-4000-8000-000000000099', + assert.ok( + 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, + assert.ok( + Predicate.isTagged( + yield* Effect.flip( + classifySelectedLegalEntity( + [{ ...activeRecord, status: 'suspended' }], + tenantId, + activeRecord.legalEntityId, + ), ), + 'LegalEntityContextInactiveError', ), - 'LegalEntityContextInactiveError', ); }), ); @@ -120,24 +113,32 @@ effectTest( effectTest( 'rejects cross-tenant, malformed, and duplicate records', Effect.gen(function* rejectsInvalidLegalEntityRecords() { - assert.equal( - yield* failureTag( - classifyActiveLegalEntities( - [{ ...activeRecord, tenantId: '10000000-0000-4000-8000-000000000002' }], - tenantId, + assert.ok( + Predicate.isTagged( + yield* Effect.flip( + classifyActiveLegalEntities( + [{ ...activeRecord, tenantId: '10000000-0000-4000-8000-000000000002' }], + tenantId, + ), ), + 'LegalEntityContextInvalidError', ), - 'LegalEntityContextInvalidError', ); - assert.equal( - yield* failureTag( - classifyActiveLegalEntities([{ ...activeRecord, legalName: '' }], tenantId), + assert.ok( + Predicate.isTagged( + yield* Effect.flip( + classifyActiveLegalEntities([{ ...activeRecord, legalName: '' }], tenantId), + ), + 'LegalEntityContextInvalidError', ), - 'LegalEntityContextInvalidError', ); - assert.equal( - yield* failureTag(classifyActiveLegalEntities([activeRecord, { ...activeRecord }], tenantId)), - 'LegalEntityContextAmbiguousError', + assert.ok( + Predicate.isTagged( + yield* Effect.flip( + classifyActiveLegalEntities([activeRecord, { ...activeRecord }], tenantId), + ), + 'LegalEntityContextAmbiguousError', + ), ); }), ); @@ -155,7 +156,7 @@ effectTest( ), }); const error = yield* Effect.flip(context.listActiveForTenant(tenantId)); - assert.equal(error._tag, 'LegalEntityContextUnavailableError'); + assert.ok(Predicate.isTagged(error, 'LegalEntityContextUnavailableError')); assert.doesNotMatch(error.reason, /secret database diagnostic/u); }), ); 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..262834474 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 @@ -226,7 +226,7 @@ void test('deduplicates one batch, reuses an immutable snapshot, and fails undec const failure = await runEffectTestPromise( Effect.flip(checkModuleEntrypoint(snapshot, undeclared)), ); - assert.equal(failure._tag, 'ModuleStateCheckUnavailableError'); + assert.ok(Predicate.isTagged(failure, 'ModuleStateCheckUnavailableError')); const undeclaredSameModule = defineTenantModuleEntrypoint({ access: 'write', authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, @@ -237,7 +237,7 @@ void test('deduplicates one batch, reuses an immutable snapshot, and fails undec const sameModuleFailure = await runEffectTestPromise( Effect.flip(checkModuleEntrypoint(snapshot, undeclaredSameModule)), ); - assert.equal(sameModuleFailure._tag, 'ModuleStateCheckUnavailableError'); + assert.ok(Predicate.isTagged(sameModuleFailure, 'ModuleStateCheckUnavailableError')); assert.equal(reads, 1); }); @@ -334,7 +334,7 @@ void test('the gateway rejects missing trusted principal context before state ac const failure = await runEffectTestPromise( Effect.flip(makeModuleEntrypointGateway(gate).prepareSnapshotInput({}, [descriptor])), ); - assert.equal(failure._tag, 'ModuleStateCheckUnavailableError'); + assert.ok(Predicate.isTagged(failure, 'ModuleStateCheckUnavailableError')); assert.equal(reads, 0); }); @@ -476,7 +476,7 @@ void test('gates every future entrypoint category before its fake implementation denied.map(async (entrypoint) => await runEffectTestPromise(Effect.flip(run(entrypoint)))), ); for (const failure of deniedFailures) { - assert.equal(failure._tag, 'ModuleStateDeniedError'); + assert.ok(Predicate.isTagged(failure, 'ModuleStateDeniedError')); } assert.equal(authorizationCalls, allowed.length); assert.equal(loadCalls, allowed.length); @@ -522,7 +522,7 @@ void test('the gateway never evaluates authorization or lazy implementation on d }), ), ); - assert.equal(failure._tag, 'ModuleStateDeniedError'); + assert.ok(Predicate.isTagged(failure, 'ModuleStateDeniedError')); assert.equal(authorizationCalls, 0); assert.equal(loadFactoryCalls, 0); assert.equal(loadCalls, 0); @@ -540,5 +540,5 @@ void test('a missing row is a definite denial rather than an unavailable read', const failure = await runEffectTestPromise( Effect.flip(checkModuleEntrypoint(snapshot, descriptor)), ); - assert.equal(failure._tag, 'ModuleStateDeniedError'); + assert.ok(Predicate.isTagged(failure, 'ModuleStateDeniedError')); }); 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..2911194d8 100644 --- a/app/packages/core-runtime/tests/unit/outbox-health.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-health.test.ts @@ -3,13 +3,13 @@ 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 { ConfigProvider, Effect, Layer, Result } from 'effect'; +import { ConfigProvider, Effect, Layer, Result, 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 () => +void test('production health binds all IPv4 interfaces for external-container probes', async () => runEffectTestPromise( Effect.scoped( Effect.gen(function* externallyReachableHealth() { @@ -20,7 +20,7 @@ test('production health binds all IPv4 interfaces for external-container probes' ), )); -test('readiness starts false, follows successful/failing cycles, expires, and closes on shutdown', async () => { +void test('readiness starts false, follows successful/failing cycles, expires, and closes on shutdown', async () => { let now = 1000; return runEffectTestPromise( Effect.scoped( @@ -54,7 +54,7 @@ test('readiness starts false, follows successful/failing cycles, expires, and cl ); }); -test('closing the health scope marks it unavailable and releases its dynamically allocated port', async () => +void test('closing the health scope marks it unavailable and releases its dynamically allocated port', async () => runEffectTestPromise( Effect.gen(function* releasedPort() { const health = yield* createOutboxWorkerHealth({ staleAfterMs: 5000 }); @@ -66,7 +66,7 @@ test('closing the health scope marks it unavailable and releases its dynamically }), )); -test('a health port already in use produces a typed server startup failure', async () => +void test('a health port already in use produces a typed server startup failure', async () => runEffectTestPromise( Effect.scoped( Effect.gen(function* occupiedPort() { @@ -76,12 +76,12 @@ test('a health port already in use produces a typed server startup failure', asy Effect.scoped(serveOutboxWorkerHealth(health, { port: server.port })), ); assert.ok(Result.isFailure(result)); - assert.equal(result.failure._tag, 'ServeError'); + assert.ok(Predicate.isTagged(result.failure, 'ServeError')); }), ), )); -test('invalid configured health ports fail startup with a typed configuration error before polling', async () => +void test('invalid configured health ports fail startup with a typed configuration error before polling', async () => runEffectTestPromise( Effect.gen(function* invalidPortConfiguration() { for (const port of ['0', '65536', '4102.5', 'invalid']) { @@ -103,7 +103,7 @@ test('invalid configured health ports fail startup with a typed configuration er ), ); assert.ok(Result.isFailure(result)); - assert.equal(result.failure._tag, 'ConfigError'); + assert.ok(Predicate.isTagged(result.failure, 'ConfigError')); } }), )); 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..3d863f477 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,7 @@ 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 { Effect, Redacted, Predicate } from 'effect'; import { DEFAULT_DATABASE_POOL_DEADLINES, configureDatabasePool, @@ -9,7 +9,7 @@ import { const runtimeUrl = 'postgresql://runtime:secret@localhost:5432/ontos'; -test( +void test( 'uses acquisition and statement deadlines without opting into a lock deadline', makeEffectTestCallback( Effect.gen(function* verifyDefaults() { @@ -30,7 +30,7 @@ test( ), ); -test( +void test( 'includes an explicitly opted-in lock deadline', makeEffectTestCallback( Effect.gen(function* verifyLockDeadline() { @@ -44,7 +44,7 @@ test( ), ); -test( +void test( 'rejects URL deadline overrides with a typed configuration failure', makeEffectTestCallback( Effect.forEach( @@ -60,7 +60,7 @@ test( Effect.gen(function* verifyParameter() { const connectionString = Redacted.make(`${runtimeUrl}?${parameter}`); const error = yield* Effect.flip(configureDatabasePool(connectionString)); - assert.equal(error._tag, 'DatabaseConnectionError'); + assert.ok(Predicate.isTagged(error, 'DatabaseConnectionError')); assert.equal( error.reason, 'Database URL deadline parameters and startup options are unsupported; use poolDeadlines', @@ -71,7 +71,7 @@ test( ), ); -test( +void test( 'rejects invalid deadline values with a typed configuration failure', makeEffectTestCallback( Effect.gen(function* verifyInvalidDeadline() { @@ -80,7 +80,7 @@ test( configureDatabasePool(connectionString, { statement_timeout: 0 }), ); - assert.equal(error._tag, 'DatabaseConnectionError'); + assert.ok(Predicate.isTagged(error, 'DatabaseConnectionError')); assert.equal( error.reason, '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..d7d182e56 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,7 @@ 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 { Effect, Option, flow, Predicate } from 'effect'; import type { PrincipalManagementPersistence, PrincipalManagementRepositoryService, @@ -92,8 +92,8 @@ effectTest( }).pipe(provideRepository(transaction)), ); - assert.equal(principalError._tag, 'IdentityTargetInvalidError'); - assert.equal(bindingError._tag, 'IdentityTargetInvalidError'); + assert.ok(Predicate.isTagged(principalError, 'IdentityTargetInvalidError')); + assert.ok(Predicate.isTagged(bindingError, 'IdentityTargetInvalidError')); }), ); @@ -157,9 +157,9 @@ effectTest( ), ); - assert.equal(conflictError._tag, 'IdentityLifecycleConflictError'); - assert.equal(terminalError._tag, 'IdentityLifecycleConflictError'); - assert.equal(reasonError._tag, 'IdentityTargetInvalidError'); + assert.ok(Predicate.isTagged(conflictError, 'IdentityLifecycleConflictError')); + assert.ok(Predicate.isTagged(terminalError, 'IdentityLifecycleConflictError')); + assert.ok(Predicate.isTagged(reasonError, 'IdentityTargetInvalidError')); }), ); @@ -183,7 +183,7 @@ effectTest( tenantId, }).pipe(provideRepository(selectingBinding(record))), ); - assert.equal(error._tag, 'IdentityTargetInvalidError'); + assert.ok(Predicate.isTagged(error, 'IdentityTargetInvalidError')); }), ), ); @@ -234,7 +234,7 @@ effectTest( }).pipe(provideRepository(transaction)), ); - assert.equal(error._tag, 'IdentityLifecycleConflictError'); + assert.ok(Predicate.isTagged(error, 'IdentityLifecycleConflictError')); }), ); @@ -260,7 +260,7 @@ effectTest( ), ); - assert.equal(error._tag, 'IdentityTargetInvalidError'); + assert.ok(Predicate.isTagged(error, 'IdentityTargetInvalidError')); yield* validateSupportImpersonation({ ...input, 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..1a9dc72fb 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 { DateTime, Effect, flow, 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, @@ -35,14 +34,6 @@ const activeRecord: PrincipalResolutionRecord = { 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', Effect.gen(function* listsEligibleTenants() { @@ -162,9 +153,11 @@ effectTest( principalKind: 'human', tenantId: 'tenant-2', }); - assert.equal( - yield* failureTag(classifySelectedPrincipal([activeRecord, selected], 'foreign-tenant')), - 'PrincipalBindingMissingError', + assert.ok( + Predicate.isTagged( + yield* Effect.flip(classifySelectedPrincipal([activeRecord, selected], 'foreign-tenant')), + 'PrincipalBindingMissingError', + ), ); }), ); @@ -175,17 +168,23 @@ effectTest( (['service', 'integration', 'agent', 'system'] as const).map((principalKind) => Effect.gen(function* rejectsNonHumanPrincipal() { const record = { ...activeRecord, principalKind }; - assert.equal( - yield* failureTag(classifyDefaultPrincipal([record])), - 'PrincipalInactiveError', + assert.ok( + Predicate.isTagged( + yield* Effect.flip(classifyDefaultPrincipal([record])), + 'PrincipalInactiveError', + ), ); - assert.equal( - yield* failureTag(classifySelectedPrincipal([record], record.tenantId)), - 'PrincipalInactiveError', + assert.ok( + Predicate.isTagged( + yield* Effect.flip(classifySelectedPrincipal([record], record.tenantId)), + 'PrincipalInactiveError', + ), ); - assert.equal( - yield* failureTag(classifyAvailableTenants([record])), - 'PrincipalInactiveError', + assert.ok( + Predicate.isTagged( + yield* Effect.flip(classifyAvailableTenants([record])), + 'PrincipalInactiveError', + ), ); }), ), @@ -204,11 +203,13 @@ effectTest( }), ), ); - assert.equal( - yield* failureTag( - classifyApiKeyPrincipal([activeRecord, { ...activeRecord, tenantId: 't-2' }]), + assert.ok( + Predicate.isTagged( + yield* Effect.flip( + classifyApiKeyPrincipal([activeRecord, { ...activeRecord, tenantId: 't-2' }]), + ), + 'PrincipalBindingAmbiguousError', ), - 'PrincipalBindingAmbiguousError', ); }), ); @@ -216,40 +217,59 @@ effectTest( effectTest( '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.ok( + Predicate.isTagged( + yield* Effect.flip(classifyAvailableTenants([])), + 'PrincipalBindingMissingError', + ), ); - assert.equal( - yield* failureTag( - classifyAvailableTenants([ - { - ...activeRecord, - bindingRevokedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-03-01T00:00:00.000Z')), - }, - ]), + assert.ok( + Predicate.isTagged( + yield* Effect.flip( + classifyAvailableTenants([{ ...activeRecord, bindingStatus: 'revoked' }]), + ), + 'PrincipalBindingInactiveError', + ), + ); + assert.ok( + Predicate.isTagged( + yield* Effect.flip( + classifyAvailableTenants([ + { + ...activeRecord, + bindingRevokedAt: DateTime.toDateUtc(DateTime.makeUnsafe('2026-03-01T00:00:00.000Z')), + }, + ]), + ), + 'PrincipalBindingInactiveError', ), - 'PrincipalBindingInactiveError', ); - assert.equal( - yield* failureTag( - classifyAvailableTenants([{ ...activeRecord, principalStatus: 'disabled' }]), + assert.ok( + Predicate.isTagged( + yield* Effect.flip( + classifyAvailableTenants([{ ...activeRecord, principalStatus: 'disabled' }]), + ), + 'PrincipalInactiveError', ), - 'PrincipalInactiveError', ); - assert.equal( - yield* failureTag(classifyAvailableTenants([{ ...activeRecord, tenantStatus: 'suspended' }])), - 'TenantInactiveError', + assert.ok( + Predicate.isTagged( + yield* Effect.flip( + classifyAvailableTenants([{ ...activeRecord, tenantStatus: 'suspended' }]), + ), + 'TenantInactiveError', + ), ); - assert.equal( - yield* failureTag( - classifyAvailableTenants([ - activeRecord, - { ...activeRecord, principalId: 'duplicate-principal' }, - ]), + assert.ok( + Predicate.isTagged( + yield* Effect.flip( + classifyAvailableTenants([ + activeRecord, + { ...activeRecord, principalId: 'duplicate-principal' }, + ]), + ), + 'PrincipalBindingAmbiguousError', ), - 'PrincipalBindingAmbiguousError', ); }), ); @@ -268,7 +288,7 @@ effectTest( ), }).listAvailableTenants('subject'), ); - assert.equal(error._tag, 'PrincipalResolverUnavailableError'); + assert.ok(Predicate.isTagged(error, 'PrincipalResolverUnavailableError')); assert.doesNotMatch(error.reason, /secret database error/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 7dd1cf610..105951fc5 100644 --- a/app/packages/core-runtime/tests/unit/read-runtime.test.ts +++ b/app/packages/core-runtime/tests/unit/read-runtime.test.ts @@ -256,7 +256,7 @@ void test('uses each denying Policy reference own declared HTTP status', async ( }), ), ); - assert.equal(error._tag, 'ReadPolicyDenied'); + assert.ok(Predicate.isTagged(error, 'ReadPolicyDenied')); assert.equal(Schema.decodeUnknownSync(ReadPolicyDenied)(error).httpStatus, denialStatus); }), ); @@ -322,7 +322,7 @@ void test('rejects invalid input before opening a transaction or executing a han }), ), ); - assert.equal(error._tag, 'ReadInputValidationError'); + assert.ok(Predicate.isTagged(error, 'ReadInputValidationError')); assert.equal(harness.evidence(), 0); }); @@ -355,7 +355,7 @@ void test('preserves typed result-validation failure across transaction rollback }), ), ); - assert.equal(error._tag, 'ReadResultValidationError'); + assert.ok(Predicate.isTagged(error, 'ReadResultValidationError')); assert.equal(harness.evidence(), 0); }); @@ -371,7 +371,7 @@ void test('never releases an allowed result when required evidence persistence f }), ), ); - assert.equal(error._tag, 'ReadEvidencePersistenceError'); + assert.ok(Predicate.isTagged(error, 'ReadEvidencePersistenceError')); assert.equal(harness.evidence(), 0); }); @@ -403,7 +403,7 @@ void test('preserves scoped service-factory unavailability and never invokes the }), ), ); - assert.equal(error._tag, 'OperationContextUnavailable'); + assert.ok(Predicate.isTagged(error, 'OperationContextUnavailable')); assert.equal(handlerCalls, 0); assert.equal(harness.evidence(), 0); }); @@ -447,13 +447,13 @@ void test('persists sanitized permission denial and never invokes the private ha }), ), ); - assert.equal(error._tag, 'ReadPermissionDenied'); + assert.ok(Predicate.isTagged(error, '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 () => { +void test('fails closed when explicit Counterparty read authority is unavailable', async () => { const legalEntityId = '00000000-0000-4000-8000-000000000004'; let handlerCalls = 0; const harness = makeHarness({ @@ -490,12 +490,12 @@ test('fails closed when explicit Counterparty read authority is unavailable', as }), ), ); - assert.equal(error._tag, 'ReadPermissionUnavailable'); + assert.ok(Predicate.isTagged(error, 'ReadPermissionUnavailable')); assert.equal(handlerCalls, 0); assert.equal(harness.evidence(), 0); }); -test('derives the authorized resource from decoded input and ignores conflicting transport hints', async () => { +void 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({ @@ -545,7 +545,7 @@ test('derives the authorized resource from decoded input and ignores conflicting assert.deepEqual(authorizedTarget, target); }); -test('authorizes a canonical Resource through explicit tenant Party administration alternatives', async () => { +void test('authorizes a canonical Resource through explicit tenant Party administration alternatives', async () => { const legalEntityId = '00000000-0000-4000-8000-000000000004'; const target = { moduleId: 'party.registry', @@ -653,7 +653,7 @@ test('authorizes a canonical Resource through explicit tenant Party administrati }), ), ); - assert.equal(unavailable._tag, 'ReadPermissionUnavailable'); + assert.ok(Predicate.isTagged(unavailable, 'ReadPermissionUnavailable')); assert.equal(indeterminate.evidence(), 0); const denied = makeHarness({ @@ -671,12 +671,12 @@ test('authorizes a canonical Resource through explicit tenant Party administrati }), ), ); - assert.equal(denial._tag, 'ReadPermissionDenied'); + assert.ok(Predicate.isTagged(denial, 'ReadPermissionDenied')); assert.equal(denied.evidence(), 1); assert.equal(handlerCalls, 2); }); -test('rejects generic tenant access as an alternative permission target', async () => { +void test('rejects generic tenant access as an alternative permission target', async () => { const legalEntityId = '00000000-0000-4000-8000-000000000004'; let handlerCalls = 0; const invalid = defineRead( @@ -723,11 +723,11 @@ test('rejects generic tenant access as an alternative permission target', async }), ), ); - assert.equal(failure._tag, 'ReadHandlerExecutionError'); + assert.ok(Predicate.isTagged(failure, 'ReadHandlerExecutionError')); assert.equal(handlerCalls, 0); }); -test('never treats missing Legal Entity scope as an allowed alternative', async () => { +void test('never treats missing Legal Entity scope as an allowed alternative', async () => { let handlerCalls = 0; const composed = defineRead( { @@ -757,11 +757,11 @@ test('never treats missing Legal Entity scope as an allowed alternative', async }), ), ); - assert.equal(failure._tag, 'ReadPermissionUnavailable'); + assert.ok(Predicate.isTagged(failure, 'ReadPermissionUnavailable')); assert.equal(handlerCalls, 0); }); -test('rejects alternative targets whenever result authorization cannot preserve them', async () => { +void test('rejects alternative targets whenever result authorization cannot preserve them', async () => { let handlerCalls = 0; const search = defineRead( { @@ -792,11 +792,11 @@ test('rejects alternative targets whenever result authorization cannot preserve }), ), ); - assert.equal(failure._tag, 'ReadHandlerExecutionError'); + assert.ok(Predicate.isTagged(failure, 'ReadHandlerExecutionError')); assert.equal(handlerCalls, 0); }); -test('rejects handler-controlled hashes in metadata-only evidence', async () => { +void test('rejects handler-controlled hashes in metadata-only evidence', async () => { const harness = makeHarness(); const unboundedEvidence = defineRead( registration().descriptor, @@ -814,7 +814,7 @@ test('rejects handler-controlled hashes in metadata-only evidence', async () => }), ), ); - assert.equal(error._tag, 'ReadEvidenceValidationError'); + assert.ok(Predicate.isTagged(error, 'ReadEvidenceValidationError')); assert.equal(harness.evidence(), 0); }); @@ -842,7 +842,7 @@ void test('persists late definite denial after rolling back the owner transactio }), ), ); - assert.equal(error._tag, 'ReadPermissionDenied'); + assert.ok(Predicate.isTagged(error, 'ReadPermissionDenied')); assert.equal(harness.evidence(), 1); }); @@ -885,11 +885,11 @@ void test('does not release generated search candidates denied by result-level a }), ), ); - assert.equal(error._tag, 'ReadPermissionDenied'); + assert.ok(Predicate.isTagged(error, 'ReadPermissionDenied')); assert.equal(harness.evidence(), 1); }); -test('authorizes tenant-scoped Party search results without fabricating a Legal Entity', async () => { +void test('authorizes tenant-scoped Party search results without fabricating a Legal Entity', async () => { const candidate = Schema.decodeUnknownSync(ResourceTargetSchema)({ moduleId: 'party.registry', resourceId: 'party-1', @@ -933,7 +933,7 @@ test('authorizes tenant-scoped Party search results without fabricating a Legal assert.equal(resourceChecks, 0); }); -test('fails closed when tenant-scoped Party result authorization becomes unavailable', async () => { +void test('fails closed when tenant-scoped Party result authorization becomes unavailable', async () => { const candidate = Schema.decodeUnknownSync(ResourceTargetSchema)({ moduleId: 'party.registry', resourceId: 'party-1', @@ -966,10 +966,10 @@ test('fails closed when tenant-scoped Party result authorization becomes unavail }), ), ); - assert.equal(failure._tag, 'ReadPermissionUnavailable'); + assert.ok(Predicate.isTagged(failure, 'ReadPermissionUnavailable')); }); -test('preserves declared owner read availability and not-found failures but sanitizes defects', async () => { +void test('preserves declared owner read availability and not-found failures but sanitizes defects', async () => { const failures = [ new ReadHandlerUnavailable({ code: 'read_handler_unavailable', @@ -1005,7 +1005,9 @@ test('preserves declared owner read availability and not-found failures but sani }), ), ); - assert.equal(error._tag, expectedTags[index]); + const expectedTag = expectedTags[index]; + assert.ok(expectedTag !== undefined); + assert.ok(Predicate.isTagged(error, expectedTag)); assert.doesNotMatch(error.reason, /secret/u); assert.equal(harness.evidence(), 0); }), @@ -1075,7 +1077,7 @@ void test('prioritizes failed denial evidence while retaining permission denial 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.ok(Predicate.isTagged(failures[0], 'ReadEvidencePersistenceError')); assert.equal(failures[1], denied); - assert.equal(failures[1]._tag, 'ReadPermissionDenied'); + assert.ok(Predicate.isTagged(failures[1], 'ReadPermissionDenied')); }); 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..d38001d97 100644 --- a/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts +++ b/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts @@ -2,7 +2,7 @@ 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 { Effect, Option, Predicate } from 'effect'; import { OperationalScopeTransaction, installOperationalScopeFromTransactionService, @@ -72,7 +72,7 @@ void test('fails closed when transaction settings do not match', async () => { }).pipe(Effect.provideService(OperationalScopeTransaction, transaction)), ), ); - assert.equal(error._tag, 'OperationContextUnavailable'); + assert.ok(Predicate.isTagged(error, 'OperationContextUnavailable')); }); void test('creates complete CRUD RLS policies with update using and with-check predicates', () => { 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 97aba69fd..375fdae48 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,7 @@ 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 { Effect, Predicate } from 'effect'; import { CORE_SEARCH_INGESTION_REGISTRATIONS, CORE_SEARCH_PARTY_LIFECYCLE_TOPICS, @@ -104,7 +104,7 @@ effectTest('identifier updates accept only their generated self-consumer worker' workerKey: 'party.registry.project-official-identifier-added-to-search', }), ); - assert.equal(denied._tag, 'CoreSearchProjectionInvalid'); + assert.ok(Predicate.isTagged(denied, 'CoreSearchProjectionInvalid')); }); }); @@ -138,7 +138,7 @@ effectTest('rejects undeclared topics and sequence/document identity mismatches' Effect.tap((failures) => Effect.sync(() => { for (const failure of failures) { - assert.equal(failure._tag, 'CoreSearchProjectionInvalid'); + assert.ok(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')); } }), ), 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 0f5d817d8..d4ddd6630 100644 --- a/app/packages/core-runtime/tests/unit/search-projection.test.ts +++ b/app/packages/core-runtime/tests/unit/search-projection.test.ts @@ -1,7 +1,7 @@ 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 { Effect, Schema, Predicate } from 'effect'; import { makeCoreSearchQueryRuntime, makeInMemoryCoreSearchProjectionStore, @@ -87,7 +87,7 @@ effectTest( ); yield* store.replace(rebuild); const divergent = yield* Effect.flip(store.replace({ ...rebuild, documents: [party()] })); - assert.equal(divergent._tag, 'CoreSearchProjectionInvalid'); + assert.ok(Predicate.isTagged(divergent, 'CoreSearchProjectionInvalid')); yield* store.apply({ document: party({ projectionVersion: '3' }), kind: 'upsert' }); yield* store.replace(rebuild); const searchResults = yield* runtime.search({ @@ -192,7 +192,7 @@ effectTest( Effect.tap((failures) => Effect.sync(() => { for (const failure of failures) { - assert.equal(failure._tag, 'CoreSearchProjectionInvalid'); + assert.ok(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')); } }), ), @@ -435,7 +435,7 @@ effectTest( tenantId, }), ); - assert.equal(failure._tag, 'CoreSearchProjectionInvalid'); + assert.ok(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')); const result = yield* runtime.search({ includeArchived: false, moduleId: 'party.registry', 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..3930dd590 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,6 +1,6 @@ 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 { Effect, Option, Schema, Predicate } from 'effect'; import assert from 'node:assert/strict'; import test from 'node:test'; import { TrustedPrincipalContextSchema } from '../../src/actions/principal-context.ts'; @@ -80,9 +80,9 @@ void test('rejects forged registrations, unsafe refs, wrong kinds, and inactive ), ); - assert.equal(invalid._tag, 'SystemPrincipalContextInvalidError'); - assert.equal(wrongKind._tag, 'SystemPrincipalContextDeniedError'); - assert.equal(inactive._tag, 'SystemPrincipalContextDeniedError'); + assert.ok(Predicate.isTagged(invalid, 'SystemPrincipalContextInvalidError')); + assert.ok(Predicate.isTagged(wrongKind, 'SystemPrincipalContextDeniedError')); + assert.ok(Predicate.isTagged(inactive, 'SystemPrincipalContextDeniedError')); assert.throws(() => registerSystemWorkload({ jobKey: 'unsafe:key' }), TypeError); }); @@ -106,7 +106,7 @@ void test('permits service principals only when the trusted registration opts in }), ); - assert.equal(denied._tag, 'SystemPrincipalContextDeniedError'); + assert.ok(Predicate.isTagged(denied, 'SystemPrincipalContextDeniedError')); assert.equal(allowed.authMethod, 'system'); }); 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 6b189a010..b5e194d89 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 @@ -2,7 +2,7 @@ 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 { Effect, Schema, Predicate } from 'effect'; import { changeTenantModuleStateAction } from '../../src/modules/actions/change-tenant-module-state.action.ts'; import type { InstalledModuleCatalog, OntosModuleDeploymentContract } from '../../src/index.ts'; import { @@ -101,7 +101,7 @@ void test('uses one canonical tenant module state schema', async () => { const failure = await runEffectTestPromise( Effect.flip(Schema.decodeUnknownEffect(TenantModuleStateSchema)('enabled')), ); - assert.equal(failure._tag, 'SchemaError'); + assert.ok(Predicate.isTagged(failure, 'SchemaError')); }); void test('maps only trusted supported authentication methods to history sources', async () => { @@ -118,7 +118,7 @@ void test('maps only trusted supported authentication methods to history sources const unsupported = await runEffectTestPromise( Effect.flip(resolveTenantModuleStateChangeSource('api_key')), ); - assert.equal(unsupported._tag, 'TenantModuleStateUnsupportedChangeSourceError'); + assert.ok(Predicate.isTagged(unsupported, 'TenantModuleStateUnsupportedChangeSourceError')); assert.equal(unsupported.code, 'tenant_module_state_change_source_unsupported'); }); @@ -129,7 +129,7 @@ void test('rejects a no-op transition without changing first-state semantics', a const unchanged = await runEffectTestPromise( Effect.flip(rejectUnchangedTenantModuleState('active', 'active')), ); - assert.equal(unchanged._tag, 'TenantModuleStateUnchangedError'); + assert.ok(Predicate.isTagged(unchanged, 'TenantModuleStateUnchangedError')); assert.equal(unchanged.code, 'tenant_module_state_unchanged'); }); @@ -187,11 +187,11 @@ void test('validates only installed membership and the target module supported s const unknown = await runEffectTestPromise( Effect.flip(validateTenantModuleStateTransition(installed, 'unknown.module', 'active')), ); - assert.equal(unknown._tag, 'TenantModuleStateUnknownModuleError'); + assert.ok(Predicate.isTagged(unknown, 'TenantModuleStateUnknownModuleError')); const unsupported = await runEffectTestPromise( Effect.flip(validateTenantModuleStateTransition(installed, 'property.registry', 'archived')), ); - assert.equal(unsupported._tag, 'TenantModuleStateUnsupportedStateError'); + assert.ok(Predicate.isTagged(unsupported, 'TenantModuleStateUnsupportedStateError')); await runEffectTestPromise( validateTenantModuleStateTransition(installed, 'property.registry', 'active'), ); 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 5956001de..96776cba5 100644 --- a/app/verticals/party-registry/tests/integration/ares-governed.test.ts +++ b/app/verticals/party-registry/tests/integration/ares-governed.test.ts @@ -20,6 +20,7 @@ import { Option, Redacted, Schema, + Predicate, } from 'effect'; import { FetchHttpClient, HttpClient, HttpClientResponse } from 'effect/unstable/http'; import { SignJWT, exportJWK, generateKeyPair } from 'jose'; @@ -108,7 +109,7 @@ const lookupIco = Schema.decodeUnknownSync(AresSubjectLookupIcoSchema)('27074358 const endPool = (pool: Pool) => pool.end(); const promiseEffect = (operation: () => PromiseLike) => Effect.promise(operation); -test('exported ARES coordinator uses real authorized HTTP commands, canonical persistence and reviewed correction', () => +void test('exported ARES coordinator uses real authorized HTTP commands, canonical persistence and reviewed correction', () => runEffectTestPromise( Effect.scoped( Effect.gen(function* aresGovernedTestEffect() { @@ -373,9 +374,9 @@ test('exported ARES coordinator uses real authorized HTTP commands, canonical pe Effect.result, ), ); - assert.equal( - 'failure' in unconfirmed && unconfirmed.failure._tag, - 'AresApplySelectionInvalid', + assert.ok( + 'failure' in unconfirmed && + Predicate.isTagged(unconfirmed.failure, 'AresApplySelectionInvalid'), ); const afterUnconfirmed = yield* state(); assert.equal( @@ -386,7 +387,7 @@ test('exported ARES coordinator uses real authorized HTTP commands, canonical pe const appliedMessage = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))( applied, ); - assert.equal(applied._tag, 'AresApplyCompleted', appliedMessage); + assert.ok(Predicate.isTagged(applied, 'AresApplyCompleted'), appliedMessage); assert.equal(applied.completed.length, 3); const persisted = yield* state(); assert.equal(persisted.claims.length, 1); @@ -410,7 +411,7 @@ test('exported ARES coordinator uses real authorized HTTP commands, canonical pe 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.equal(replay._tag, 'AresApplyCompleted'); + assert.ok(Predicate.isTagged(replay, 'AresApplyCompleted')); assert.equal(replay.completed.length, 0); assert.equal(replay.skipped.length, 3); const afterReplay = yield* state(); @@ -423,7 +424,9 @@ test('exported ARES coordinator uses real authorized HTTP commands, canonical pe const denied = yield* runHttpEffect( applyAresObservation(request, { gateway: deniedGateway, baseUrl }).pipe(Effect.result), ); - assert.equal('failure' in denied && denied.failure._tag, 'AresLookupForbiddenProblem'); + assert.ok( + 'failure' in denied && Predicate.isTagged(denied.failure, 'AresLookupForbiddenProblem'), + ); const afterDenied = yield* state(); assert.equal(afterDenied.core.invocations.length, persisted.core.invocations.length); 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..00bcf2048 100644 --- a/app/verticals/party-registry/tests/integration/governed-identity.test.ts +++ b/app/verticals/party-registry/tests/integration/governed-identity.test.ts @@ -14,7 +14,15 @@ 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 { + Effect, + Exit, + Layer, + Exit as NativeExit, + Scope as NativeScope, + Redacted, + Predicate, +} from 'effect'; import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import test, { after as afterNativeDatabase } from 'node:test'; @@ -82,13 +90,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) => @@ -103,7 +104,7 @@ const readPartyDetail = (partyRef: PartyRef, principal: TrustedPrincipalContext) const endPool = (pool: Pool) => pool.end(); const promiseEffect = (operation: () => PromiseLike) => Effect.promise(operation); -test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims, recovery and temporal authorization', () => +void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims, recovery and temporal authorization', () => runEffectTestPromise( Effect.scoped( Effect.gen(function* governedIdentityTestEffect() { @@ -250,18 +251,25 @@ 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.ok( + 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.ok( + Predicate.isTagged( + yield* run( + resolveActionCommit({ + invocationId: invocation.actionInvocationId, + principal: fixture.manager, + }).pipe(Effect.flip), + ), + 'ActionAlreadyCommitted', ), - 'ActionAlreadyCommitted', ); const recovered = yield* run( ReadRuntime.pipe( @@ -285,31 +293,40 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims 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.ok( + 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', + assert.ok( + 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.ok( + 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); 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); @@ -325,19 +342,27 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims ); 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.ok( + 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.ok( + 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.ok( + Predicate.isTagged( + yield* run(readPartyDetail(partyRef, fixture.legalEntityOnly).pipe(Effect.flip)), + 'ReadPermissionDenied', + ), ); const searchLayer = PartySearchProjectionGatewayLive.pipe( Layer.provide(CoreSearchQueryRuntimeLive), @@ -356,22 +381,26 @@ 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.ok( + Predicate.isTagged(yield* run(deniedSearch.pipe(Effect.flip)), 'ReadPermissionDenied'), + ); + assert.ok( + 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 = { @@ -419,16 +448,13 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims '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, @@ -519,13 +545,19 @@ 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.ok( + 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.ok( + Predicate.isTagged( + yield* run(readPartyDetail(partyRef, fixture.legalEntityOnly).pipe(Effect.flip)), + 'ReadPermissionDenied', + ), ); const updatedRelationship = yield* run( runAction({ @@ -623,7 +655,7 @@ test('governed Party identity uses real PostgreSQL and SpiceDB for atomic claims assert.ok(archivedParty.party.archivedAt); const archivedCounterparty = yield* readCounterparty(); assert.equal(archivedCounterparty.party.archived, true); - assert.notEqual(yield* run(counterparty().pipe(tag)), 'SUCCESS'); + yield* run(counterparty().pipe(Effect.flip)); const unarchive = yield* run( runAction({ registration: unarchivePartyAction, 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 f36099099..ee1c2558f 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 @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { DateTime, Effect, ManagedRuntime, Match, Option, Result, Schema } from 'effect'; +import { DateTime, Effect, ManagedRuntime, Match, Option, Result, Schema, Predicate } from 'effect'; import { TestClock } from 'effect/testing'; import { AresAppliedEvidenceSchema, @@ -210,7 +210,7 @@ const makeReads = (displayName: string | null = null): AresApplyReads => ({ }), }); -test('runs only explicitly selected standard Actions and preserves every result', async () => { +void 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() }), @@ -220,14 +220,14 @@ test('runs only explicitly selected standard Actions and preserves every result' 'update-party|Bearer signed-gateway-token', 'add-party-official-identifier|Bearer signed-gateway-token', ]); - assert.equal(outcome._tag, 'AresApplyCompleted'); + assert.ok(Predicate.isTagged(outcome, 'AresApplyCompleted')); assert.deepEqual( outcome.completed.map(({ route }) => route), ['PARTY_UPDATE', 'IDENTIFIER_ADD'], ); }); -test('propagates bounded evidence and independent command delivery keys', async () => { +void test('propagates bounded evidence and independent command delivery keys', async () => { const calls: string[] = []; const recorded: { readonly evidenceRef: string | undefined; @@ -272,7 +272,7 @@ test('propagates bounded evidence and independent command delivery keys', async ); }); -test('stops after the first failed Action and returns a typed partial outcome', async () => { +void test('stops after the first failed Action and returns a typed partial outcome', async () => { const calls: string[] = []; const outcome = await runAresEffectTestPromise( applyAresObservation( @@ -295,10 +295,10 @@ test('stops after the first failed Action and returns a typed partial outcome', ['PARTY_UPDATE'], ); assert.equal(partial.failed.route, 'IDENTIFIER_ADD'); - assert.equal(partial.failed.error._tag, 'TestFailure'); + assert.ok(Predicate.isTagged(partial.failed.error, 'TestFailure')); }); -test('resumes a replay after a prior selected fact is already satisfied', async () => { +void test('resumes a replay after a prior selected fact is already satisfied', async () => { const calls: string[] = []; const outcome = await runAresEffectTestPromise( applyAresObservation(request, makeInvoker(calls), { @@ -308,7 +308,7 @@ test('resumes a replay after a prior selected fact is already satisfied', async ); assert.deepEqual(calls, ['add-party-official-identifier|Bearer signed-gateway-token']); - assert.equal(outcome._tag, 'AresApplyCompleted'); + assert.ok(Predicate.isTagged(outcome, 'AresApplyCompleted')); assert.deepEqual(outcome.skipped, [ { fact: 'BUSINESS_NAME', reason: 'ALREADY_SATISFIED', route: 'PARTY_UPDATE' }, ]); @@ -318,7 +318,7 @@ test('resumes a replay after a prior selected fact is already satisfied', async ); }); -test('defers when canonical revision or refreshed evidence changed', async () => { +void test('defers when canonical revision or refreshed evidence changed', async () => { const calls: string[] = []; const revisionRequest: AresApplyRequest = { ...request, @@ -348,12 +348,12 @@ test('defers when canonical revision or refreshed evidence changed', async () => ), ]); - assert.equal(revisionOutcome._tag, 'AresApplyDeferred'); - assert.equal(changedOutcome._tag, 'AresApplyDeferred'); + assert.ok(Predicate.isTagged(revisionOutcome, 'AresApplyDeferred')); + assert.ok(Predicate.isTagged(changedOutcome, 'AresApplyDeferred')); assert.deepEqual(calls, []); }); -test('rejects unconfirmed or observation-mismatched selections before invoking an Action', async () => { +void test('rejects unconfirmed or observation-mismatched selections before invoking an Action', async () => { const calls: string[] = []; const invalidRequests: readonly AresApplyRequest[] = [ { @@ -396,13 +396,13 @@ test('rejects unconfirmed or observation-mismatched selections before invoking a for (const result of results) { assert.equal('failure' in result, true); if ('failure' in result) { - assert.equal(result.failure instanceof AresApplySelectionInvalid, true); + assert.equal(Schema.is(AresApplySelectionInvalid)(result.failure), true); } } assert.deepEqual(calls, []); }); -test('does not accept a different street number as the observed registered address', async () => { +void test('does not accept a different street number as the observed registered address', async () => { const calls: string[] = []; const invalidRequest: AresApplyRequest = { correlationId: request.correlationId, @@ -492,7 +492,7 @@ const correctionSelection: AresApplyRequest['selections'][number] = { route: 'PARTY_CORRECTION', }; -test('review-authorized assertion context returns explicit Correction handoff without a write', async () => { +void test('review-authorized assertion context returns explicit Correction handoff without a write', async () => { const calls: string[] = []; const reads = makeReads('Wrong name'); let reviewed = false; @@ -542,7 +542,7 @@ test('review-authorized assertion context returns explicit Correction handoff wi assert.deepEqual(calls, []); }); -test('governed identifier history supports ICO correction suspicion without claiming the identifier', async () => { +void test('governed identifier history supports ICO correction suspicion without claiming the identifier', async () => { const calls: string[] = []; const [, selection] = request.selections; assert.ok(selection); @@ -588,7 +588,7 @@ test('governed identifier history supports ICO correction suspicion without clai assert.deepEqual(calls, []); }); -test('every governed read and selected Action receives fresh audience-scoped authorization', async () => { +void test('every governed read and selected Action receives fresh audience-scoped authorization', async () => { const tokens: string[] = []; const calls: string[] = []; const delegate = makeReads(); @@ -632,7 +632,7 @@ test('every governed read and selected Action receives fresh audience-scoped aut ]); }); -test('read denial fails before writes and preserves its declared error', async () => { +void test('read denial fails before writes and preserves its declared error', async () => { const calls: string[] = []; const denied = { _tag: 'PartyDetailForbiddenProblem' as const, @@ -651,7 +651,7 @@ test('read denial fails before writes and preserves its declared error', async ( assert.deepEqual(calls, []); }); -test('alias and archived targets never dispatch selected writes', async () => { +void test('alias and archived targets never dispatch selected writes', async () => { await Promise.all( (['ALIAS', 'ARCHIVED'] as const).map(async (kind) => { const calls: string[] = []; @@ -694,7 +694,7 @@ test('alias and archived targets never dispatch selected writes', async () => { ); }); -test('provider revision change alone invalidates the earlier confirmation', async () => { +void test('provider revision change alone invalidates the earlier confirmation', async () => { const calls: string[] = []; const reads = makeReads(); const outcome = await runAresEffectTestPromise( @@ -712,11 +712,11 @@ test('provider revision change alone invalidates the earlier confirmation', asyn }, }), ); - assert.equal(outcome._tag, 'AresApplyDeferred'); + assert.ok(Predicate.isTagged(outcome, 'AresApplyDeferred')); assert.deepEqual(calls, []); }); -test('retry preserves exact command payload and reports required standard recovery', async () => { +void 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'); @@ -746,7 +746,7 @@ test('retry preserves exact command payload and reports required standard recove ]); }); -test('failed second Action stops the following supported address and retains prior commit receipt', async () => { +void 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', @@ -795,7 +795,7 @@ test('failed second Action stops the following supported address and retains pri { gateway, reads: makeReads() }, ), ); - assert.equal(outcome._tag, 'AresApplyPartiallyCompleted'); + assert.ok(Predicate.isTagged(outcome, 'AresApplyPartiallyCompleted')); assert.equal(outcome.completed.length, 1); assert.deepEqual(calls, [ 'update-party|Bearer signed-gateway-token', @@ -803,7 +803,7 @@ test('failed second Action stops the following supported address and retains pri ]); }); -test('stale refreshed evidence and missing canonical target cannot execute enrichment', async () => { +void test('stale refreshed evidence and missing canonical target cannot execute enrichment', async () => { const calls: string[] = []; const stale = await runAresEffectTestPromise( applyAresObservation(request, makeInvoker(calls), { @@ -819,7 +819,7 @@ test('stale refreshed evidence and missing canonical target cannot execute enric }, }), ); - assert.equal(stale._tag, 'AresApplyDeferred'); + assert.ok(Predicate.isTagged(stale, 'AresApplyDeferred')); const absent = await runAresEffectTestPromise( applyAresObservation({ ...request, partyRef: null }, makeInvoker(calls), { gateway, @@ -830,7 +830,7 @@ test('stale refreshed evidence and missing canonical target cannot execute enric assert.deepEqual(calls, []); }); -test('fresh identical refresh cannot revive an expired original confirmation', async () => { +void test('fresh identical refresh cannot revive an expired original confirmation', async () => { const calls: string[] = []; const outcome = await runAresEffectTestPromise( applyAresObservation( @@ -855,7 +855,7 @@ test('fresh identical refresh cannot revive an expired original confirmation', a assert.deepEqual(calls, []); }); -test('a correction route is never historical-error evidence by itself', async () => { +void 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), { 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 1b1d48410..76e9bb0fb 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 @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import test from 'node:test'; -import { ConfigProvider, Context, Effect, Layer, Schema } from 'effect'; +import { ConfigProvider, Context, Effect, Layer, Schema, Predicate } from 'effect'; import { ReadRuntime, ReadHandlerNotFound, @@ -273,7 +273,7 @@ const commandRequest = ( }); }; -test('every registered command is mounted and rejects missing structural input or authentication before the lifecycle', async () => { +void test('every registered command is mounted and rejects missing structural input or authentication before the lifecycle', async () => { const assertion = await makeAssertion(); const harness = makeActionTestHarness(); const app = mounted(harness, assertion.environment); @@ -310,10 +310,13 @@ test('every registered command is mounted and rejects missing structural input o assert.match(response.headers.get('content-type') ?? '', /application\/problem\+json/u); const body = await response.json(); assert.equal( - body._tag, - response.status === 400 - ? 'PartyCommandInvalidRequestProblem' - : 'PartyCommandAuthenticationProblem', + Predicate.isTagged( + body, + response.status === 400 + ? 'PartyCommandInvalidRequestProblem' + : 'PartyCommandAuthenticationProblem', + ), + true, ); assert.equal(body.status, response.status); }, @@ -332,14 +335,14 @@ test('every registered command is mounted and rejects missing structural input o assert.equal(malformed.status, 400); assert.match(malformed.headers.get('content-type') ?? '', /application\/problem\+json/u); const malformedBody = await malformed.json(); - assert.equal(malformedBody._tag, 'PartyCommandInvalidRequestProblem'); + assert.equal(Predicate.isTagged(malformedBody, 'PartyCommandInvalidRequestProblem'), true); assert.equal(harness.snapshot().invocations.length, 0); } finally { await app.dispose(); } }); -test('missing, malformed, and wrong-audience assertions are challenged without creating invocations', async () => { +void test('missing, malformed, and wrong-audience assertions are challenged without creating invocations', async () => { await forEachSequential(['party-registry', 'contacts'], async (audience) => { const assertion = await makeAssertion(audience); const harness = makeActionTestHarness(); @@ -357,7 +360,7 @@ test('missing, malformed, and wrong-audience assertions are challenged without c 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.equal(body._tag, 'PartyCommandAuthenticationProblem'); + assert.equal(Predicate.isTagged(body, 'PartyCommandAuthenticationProblem'), true); assert.equal(body.status, 401); assert.equal(JSON.stringify(body).includes(assertion.token), false); }); @@ -368,7 +371,7 @@ test('missing, malformed, and wrong-audience assertions are challenged without c }); }); -test('verification configuration unavailability is retryable and never reaches the lifecycle', async () => { +void test('verification configuration unavailability is retryable and never reaches the lifecycle', async () => { const assertion = await makeAssertion(); const harness = makeActionTestHarness(); const app = mounted(harness, {}); @@ -382,7 +385,7 @@ test('verification configuration unavailability is retryable and never reaches t assert.equal(response.status, 503); assert.equal(response.headers.get('www-authenticate'), null); const body = await response.json(); - assert.equal(body._tag, 'PartyCommandUnavailableProblem'); + assert.equal(Predicate.isTagged(body, 'PartyCommandUnavailableProblem'), true); assert.equal(body.retryable, true); assert.equal(harness.snapshot().invocations.length, 0); } finally { @@ -390,7 +393,7 @@ test('verification configuration unavailability is retryable and never reaches t } }); -test('correlation and idempotency are mandatory before the Core Action lifecycle', async () => { +void test('correlation and idempotency are mandatory before the Core Action lifecycle', async () => { const assertion = await makeAssertion(); const harness = makeActionTestHarness(); const app = mounted(harness, assertion.environment); @@ -401,7 +404,10 @@ test('correlation and idempotency are mandatory before the Core Action lifecycle ); assert.equal(missingKey.status, 428); const missingKeyBody = await missingKey.json(); - assert.equal(missingKeyBody._tag, 'PartyCommandPreconditionRequiredProblem'); + assert.equal( + Predicate.isTagged(missingKeyBody, 'PartyCommandPreconditionRequiredProblem'), + true, + ); const missingCorrelation = await handle( app, commandRequest('request-search-rebuild', {}, assertion.token, { @@ -411,14 +417,17 @@ test('correlation and idempotency are mandatory before the Core Action lifecycle ); assert.equal(missingCorrelation.status, 400); const missingCorrelationBody = await missingCorrelation.json(); - assert.equal(missingCorrelationBody._tag, 'PartyCommandInvalidRequestProblem'); + assert.equal( + Predicate.isTagged(missingCorrelationBody, 'PartyCommandInvalidRequestProblem'), + true, + ); assert.equal(harness.snapshot().invocations.length, 0); } finally { await app.dispose(); } }); -test('real Core permission denial is a durable 403 and does not execute the command', async () => { +void 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', @@ -434,7 +443,7 @@ test('real Core permission denial is a durable 403 and does not execute the comm ); assert.equal(response.status, 403); const body = await response.json(); - assert.equal(body._tag, 'PartyCommandForbiddenProblem'); + assert.equal(Predicate.isTagged(body, 'PartyCommandForbiddenProblem'), true); assert.equal(harness.snapshot().invocations.length, 1); assert.equal(harness.snapshot().permissionDenials.length, 1); } finally { @@ -442,7 +451,7 @@ test('real Core permission denial is a durable 403 and does not execute the comm } }); -test('the real handler translates domain conflicts and rolls back without successful evidence', async () => { +void test('the real handler translates domain conflicts and rolls back without successful evidence', async () => { const assertion = await makeAssertion(); const harness = makeActionTestHarness({ actionPermission: 'allowed', @@ -463,7 +472,7 @@ test('the real handler translates domain conflicts and rolls back without succes ); assert.equal(response.status, 409); const body = await response.json(); - assert.equal(body._tag, 'PartyCommandConflictProblem'); + assert.equal(Predicate.isTagged(body, 'PartyCommandConflictProblem'), true); assert.equal(body.code, 'party_lifecycle_conflict'); assert.equal(harness.snapshot().invocations.length, 1); assert.equal(harness.snapshot().committed.length, 0); @@ -472,7 +481,7 @@ test('the real handler translates domain conflicts and rolls back without succes } }); -test('alias conflicts preserve only safe canonical recovery metadata', async () => { +void 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({ @@ -502,7 +511,7 @@ test('alias conflicts preserve only safe canonical recovery metadata', async () ); assert.equal(response.status, 409); const body = await response.json(); - assert.equal(body._tag, 'PartyCommandAliasWriteRejectedProblem'); + assert.equal(Predicate.isTagged(body, 'PartyCommandAliasWriteRejectedProblem'), true); assert.deepEqual(body.aliasPartyRef, partyRef); assert.deepEqual(body.canonicalPartyRef, canonicalPartyRef); assert.equal(JSON.stringify(body).includes('Private diagnostic'), false); @@ -512,7 +521,7 @@ test('alias conflicts preserve only safe canonical recovery metadata', async () } }); -test('committed request replay stays a terminal 409 and does not execute or emit twice', async () => { +void 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', @@ -539,7 +548,7 @@ test('committed request replay stays a terminal 409 and does not execute or emit ); assert.equal(replay.status, 409); const body = await replay.json(); - assert.equal(body._tag, 'PartyCommandAlreadyCommittedProblem'); + assert.equal(Predicate.isTagged(body, 'PartyCommandAlreadyCommittedProblem'), true); assert.equal(body.code, 'action_already_committed'); assert.equal(body.invocationId, harness.snapshot().invocations[0]?.actionInvocationId); assert.equal(body.retryCommand, false); @@ -551,7 +560,7 @@ test('committed request replay stays a terminal 409 and does not execute or emit } }); -test('declared not-found, capability-unavailable and unexpected defects retain safe distinct HTTP statuses', async () => { +void test('declared not-found, capability-unavailable and unexpected defects retain safe distinct HTTP statuses', async () => { const assertion = await makeAssertion(); const cases = [ { @@ -599,7 +608,7 @@ test('declared not-found, capability-unavailable and unexpected defects retain s assert.equal(response.status, item.status); assert.match(response.headers.get('content-type') ?? '', /application\/problem\+json/u); const body = await response.json(); - assert.equal(body._tag, item.tag); + assert.equal(Predicate.isTagged(body, item.tag), true); assert.equal(body.status, item.status); assert.equal(JSON.stringify(body).includes('private'), false); if (item.status === 503) { @@ -612,7 +621,7 @@ test('declared not-found, capability-unavailable and unexpected defects retain s }); }); -test('semantically insufficient Party evidence is a declared 422, not a server defect', async () => { +void test('semantically insufficient Party evidence is a declared 422, not a server defect', async () => { const assertion = await makeAssertion(); const harness = makeActionTestHarness({ actionPermission: 'allowed', @@ -648,7 +657,7 @@ test('semantically insufficient Party evidence is a declared 422, not a server d } }); -test('the Core request hash rejects reuse of an idempotency key for a different command payload', async () => { +void 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({ @@ -699,7 +708,7 @@ test('the Core request hash rejects reuse of an idempotency key for a different } }); -test('commit resolution requires authentication and a valid invocation without creating an Action', async () => { +void test('commit resolution requires authentication and a valid invocation without creating an Action', async () => { const assertion = await makeAssertion(); const harness = makeActionTestHarness(); const app = mounted(harness, assertion.environment); @@ -710,7 +719,7 @@ test('commit resolution requires authentication and a valid invocation without c const malformed = await handle(app, recoveryRequest('not-an-id', assertion.token)); assert.equal(malformed.status, 400); const malformedBody = await malformed.json(); - assert.equal(malformedBody._tag, 'PartyCommandInvalidRequestProblem'); + assert.equal(Predicate.isTagged(malformedBody, 'PartyCommandInvalidRequestProblem'), true); const absent = await handle(app, recoveryRequest(randomUUID(), assertion.token)); assert.equal(absent.status, 404); assert.equal(harness.snapshot().invocations.length, 0); @@ -719,7 +728,7 @@ test('commit resolution requires authentication and a valid invocation without c } }); -test('an open invocation resolves explicitly without authorizing automatic command retry', async () => { +void test('an open invocation resolves explicitly without authorizing automatic command retry', async () => { const assertion = await makeAssertion(); const harness = makeActionTestHarness({ actionPermission: 'allowed', @@ -756,7 +765,7 @@ test('an open invocation resolves explicitly without authorizing automatic comma } }); -test('actual Core commit acknowledgement loss resolves and the mounted governed Read returns the original decision without rerunning the Action', async () => { +void 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; @@ -859,7 +868,7 @@ test('actual Core commit acknowledgement loss resolves and the mounted governed ); assert.equal(uncertain.status, 503); const body = await uncertain.json(); - assert.equal(body._tag, 'PartyCommandCommitIndeterminateProblem'); + assert.equal(Predicate.isTagged(body, 'PartyCommandCommitIndeterminateProblem'), true); assert.equal(body.resolution, 'RESOLVE_COMMIT'); assert.equal(body.retryCommand, false); const invocationId = harness.snapshot().invocations[0]?.actionInvocationId; @@ -892,7 +901,7 @@ test('actual Core commit acknowledgement loss resolves and the mounted governed ); assert.equal(replay.status, 409); const replayBody = await replay.json(); - assert.equal(replayBody._tag, 'PartyCommandAlreadyCommittedProblem'); + assert.equal(Predicate.isTagged(replayBody, 'PartyCommandAlreadyCommittedProblem'), true); assert.equal(replayBody.invocationId, invocationId); assert.equal(replayBody.retryCommand, false); assert.equal(executions, 1); 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 71addc2c0..3e61cae9c 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 @@ -3,7 +3,7 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import assert from 'node:assert/strict'; import { readFile, readdir } from 'node:fs/promises'; import test from 'node:test'; -import { Effect, Schema } from 'effect'; +import { Effect, Schema, Predicate } from 'effect'; import { getReadHandler } from '../../../../packages/core-runtime/src/reads/definition.ts'; import { AresLookupApi, @@ -59,7 +59,7 @@ const scope = Object.freeze({ }); const request = Schema.decodeUnknownSync(AresLookupRequestSchema)({ ico: '48039101' }); -test('declares a tenant-authorized Party evidence Read with optional Legal Entity context', () => { +void 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'); @@ -68,7 +68,7 @@ test('declares a tenant-authorized Party evidence Read with optional Legal Entit assert.equal(aresLookupRead.descriptor.evidencePolicy.captureMode, 'metadata_only'); }); -test('passes trusted correlation to the private adapter and returns exactly one evidence result', async () => { +void 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, { @@ -91,7 +91,7 @@ test('passes trusted correlation to the private adapter and returns exactly one assert.deepEqual(result, { evidence: { resultCount: 1 }, result: evidence }); }); -test('maps provider failures to the closed governed Read error vocabulary without leaking details', async () => { +void 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' }), @@ -141,12 +141,12 @@ test('maps provider failures to the closed governed Read error vocabulary withou ), ); for (const { error, expectedTag } of errors) { - assert.equal(error._tag, expectedTag); + assert.ok(Predicate.isTagged(error, expectedTag)); assert.equal(JSON.stringify(error).includes('private'), false); } }); -test('publishes safe status-matched Problem Details and no provider payload schema', () => { +void test('publishes safe status-matched Problem Details and no provider payload schema', () => { interface ProblemFixture { readonly _tag: string; readonly detail: string; @@ -190,7 +190,7 @@ test('publishes safe status-matched Problem Details and no provider payload sche assert.equal(AresLookupApi.identifier, 'AresLookupApi'); }); -test('keeps the ARES integration read-only and exposes no ARES Action', async () => { +void 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), 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..bf1bd0c53 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 @@ -2,7 +2,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'; +import { DateTime, Effect, Fiber, Layer, Logger, Option, Predicate } from 'effect'; import { TestClock } from 'effect/testing'; import { HttpClient, HttpClientError, HttpClientResponse } from 'effect/unstable/http'; import type { HttpClientRequest } from 'effect/unstable/http'; @@ -70,7 +70,7 @@ const capturedLoggerLayer = (entries: string[]) => }), ]); -test('maps a bounded ARES observation and sends an exact credential-free JSON request', async () => { +void 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) => { @@ -111,7 +111,7 @@ test('maps a bounded ARES observation and sends an exact credential-free JSON re assert.equal(requests[0]?.request.headers['cookie'], undefined); }); -test('rejects malformed IČOs before provider I/O', async () => { +void test('rejects malformed IČOs before provider I/O', async () => { let requests = 0; const client = clientFrom((request) => { requests += 1; @@ -121,13 +121,13 @@ test('rejects malformed IČOs before provider I/O', async () => { 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.ok(Predicate.isTagged(error, 'AresSubjectInvalidIco')); }), ); assert.equal(requests, 0); }); -test('represents absent optional provider facts explicitly without inventing Party facts', async () => { +void test('represents absent optional provider facts explicitly without inventing Party facts', async () => { const client = clientFrom((request) => Effect.succeed( jsonResponse(request, 200, { @@ -152,7 +152,7 @@ test('represents absent optional provider facts explicitly without inventing Par assert.ok(Option.isNone(result.providerRecordRef)); }); -test('keeps not-found, denial, throttling, timeout, and unavailable failures distinct and safe', async () => { +void test('keeps not-found, denial, throttling, timeout, and unavailable failures distinct and safe', async () => { const statusCases = [ [400, 'AresSubjectResponseInvalid', 1], [401, 'AresSubjectDenied', 1], @@ -183,7 +183,7 @@ test('keeps not-found, denial, throttling, timeout, and unavailable failures dis return yield* Fiber.join(fiber); }).pipe(Effect.provide(TestClock.layer())); const error = await runEffectTestPromise(expectedAttempts === 3 ? fiberProgram : program); - assert.equal(error._tag, tag); + assert.ok(Predicate.isTagged(error, 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); @@ -191,7 +191,7 @@ test('keeps not-found, denial, throttling, timeout, and unavailable failures dis ); }); -test('retries transport faults with bounded backoff without exposing diagnostics', async () => { +void test('retries transport faults with bounded backoff without exposing diagnostics', async () => { const logs: string[] = []; let attempts = 0; const client = clientFrom((request) => { @@ -216,14 +216,14 @@ test('retries transport faults with bounded backoff without exposing diagnostics }).pipe(Effect.provide(Layer.mergeAll(TestClock.layer(), capturedLoggerLayer(logs)))); const error = await runEffectTestPromise(program); - assert.equal(error._tag, 'AresSubjectUnavailable'); + assert.ok(Predicate.isTagged(error, '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); }); -test('times out and aborts each of the three bounded attempts', async () => { +void test('times out and aborts each of the three bounded attempts', async () => { const signals: AbortSignal[] = []; const client = clientFrom((_request, _url, signal) => { signals.push(signal); @@ -237,7 +237,7 @@ test('times out and aborts each of the three bounded attempts', async () => { }).pipe(Effect.provide(TestClock.layer())); const error = await runEffectTestPromise(program); - assert.equal(error._tag, 'AresSubjectTimeout'); + assert.ok(Predicate.isTagged(error, 'AresSubjectTimeout')); assert.equal(signals.length, 3); assert.equal( signals.every((signal) => signal.aborted), @@ -245,7 +245,7 @@ test('times out and aborts each of the three bounded attempts', async () => { ); }); -test('bounds stalled response bodies with the same three-attempt timeout policy', async () => { +void test('bounds stalled response bodies with the same three-attempt timeout policy', async () => { let attempts = 0; const client = clientFrom((request) => { attempts += 1; @@ -268,11 +268,11 @@ test('bounds stalled response bodies with the same three-attempt timeout policy' return yield* Fiber.join(fiber); }).pipe(Effect.provide(TestClock.layer())); const error = await runEffectTestPromise(program); - assert.equal(error._tag, 'AresSubjectTimeout'); + assert.ok(Predicate.isTagged(error, 'AresSubjectTimeout')); assert.equal(attempts, 3); }); -test('rejects malformed JSON, schema drift, mismatched IČO, and oversized text without partial evidence', async () => { +void test('rejects malformed JSON, schema drift, mismatched IČO, and oversized text without partial evidence', async () => { const responses: readonly (( request: HttpClientRequest.HttpClientRequest, ) => HttpClientResponse.HttpClientResponse)[] = [ @@ -290,13 +290,13 @@ test('rejects malformed JSON, schema drift, mismatched IČO, and oversized text return Effect.succeed(response(request)); }); const error = await runEffectTestPromise(Effect.flip(lookup(client))); - assert.equal(error._tag, 'AresSubjectResponseInvalid'); + assert.ok(Predicate.isTagged(error, 'AresSubjectResponseInvalid')); assert.equal(requests, 1); }), ); }); -test('coalesces identical requests and exposes cache age without changing observedAt', async () => { +void test('coalesces identical requests and exposes cache age without changing observedAt', async () => { let requests = 0; const client = clientFrom((request) => { requests += 1; @@ -334,7 +334,7 @@ test('coalesces identical requests and exposes cache age without changing observ assert.notEqual(result.cached.servedAt, result.cached.observedAt); }); -test('bounds distinct upstream lookups to four concurrent requests', async () => { +void test('bounds distinct upstream lookups to four concurrent requests', async () => { let active = 0; let maximumActive = 0; let requests = 0; 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..9a3a84419 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,8 +1,8 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; /* 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 { DateTime, Effect, Option, Schema, Predicate } from 'effect'; import assert from 'node:assert/strict'; import test from 'node:test'; import { AresAppliedEvidenceSchema } from '../../shared/domain/ares-application.ts'; @@ -225,7 +225,7 @@ const wholeEndCommand = (effectiveEnd: string, reason = 'Party retired this mail target: { type: 'WHOLE_CONTACT_POINT' as const }, }); -test('stores future end provenance while keeping the contact current until the boundary', () => +void 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'; @@ -264,7 +264,7 @@ test('stores future end provenance while keeping the contact current until the b }), )); -test('stores end provenance on both a last ADDRESS purpose and its owning address', () => +void test('stores end provenance on both a last ADDRESS purpose and its owning address', () => runEffectTestPromise( Effect.gen(function* contactPointScenario() { const effectiveEnd = '2026-02-01T00:00:00.000Z'; @@ -332,7 +332,7 @@ test('stores end provenance on both a last ADDRESS purpose and its owning addres }), )); -test('reuses only an exact end request and rejects changed evidence at the same boundary', () => +void 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'; @@ -366,12 +366,12 @@ 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.ok(Predicate.isTagged(changed, 'Failure')); assert.equal(changedHarness.updateSets.length, 0); }), )); -test('stores correction end provenance on the preserved original Contact Point', () => +void test('stores correction end provenance on the preserved original Contact Point', () => runEffectTestPromise( Effect.gen(function* contactPointScenario() { const original = contactRow(); @@ -443,7 +443,7 @@ const updateCommand = (change: Parameters[2]['c }, }); -test('re-adds a scheduled-ended purpose as a new period without reopening its history', () => +void test('re-adds a scheduled-ended purpose as a new period without reopening its history', () => runEffectTestPromise( Effect.gen(function* contactPointScenario() { const stalePurpose = purposeRow({ @@ -491,7 +491,7 @@ test('re-adds a scheduled-ended purpose as a new period without reopening its hi }), )); -test('rejects a REGISTERED context collision as a typed domain conflict before mutation', () => +void test('rejects a REGISTERED context collision as a typed domain conflict before mutation', () => runEffectTestPromise( Effect.gen(function* contactPointScenario() { const address = addressRow(); @@ -516,7 +516,7 @@ test('rejects a REGISTERED context collision as a typed domain conflict before m }), ), ); - assert.equal(error._tag, 'PartyContactPointAlreadyExists'); + assert.ok(Predicate.isTagged(error, 'PartyContactPointAlreadyExists')); assert.equal(harness.updateSets.length, 0); assert.equal(harness.insertValues.length, 0); const condition = harness.selectWheres.at(3); @@ -560,13 +560,13 @@ test('rejects a REGISTERED context collision as a typed domain conflict before m verification: { state: 'UNVERIFIED' }, }), ); - assert.equal(addError._tag, 'PartyContactPointAlreadyExists'); + assert.ok(Predicate.isTagged(addError, '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', () => +void test('advances revisions on both the transferred purpose and its owning address', () => runEffectTestPromise( Effect.gen(function* contactPointScenario() { const address = addressRow(); @@ -595,14 +595,14 @@ test('advances revisions on both the transferred purpose and its owning address' 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.ok(is(revision, 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', () => +void test('preserves original provenance evidence and appends deduplicated enrichment', () => runEffectTestPromise( Effect.gen(function* contactPointScenario() { const row = contactRow({ @@ -641,7 +641,7 @@ test('preserves original provenance evidence and appends deduplicated enrichment }), )); -test('rejects invalid E.164 and oversized extensions through the service typed-error path', () => +void test('rejects invalid E.164 and oversized extensions through the service typed-error path', () => runEffectTestPromise( Effect.all( [ @@ -676,7 +676,7 @@ test('rejects invalid E.164 and oversized extensions through the service typed-e verification: { state: 'UNVERIFIED' }, }), ); - assert.equal(error._tag, 'PartyContactPointInvalid'); + assert.ok(Predicate.isTagged(error, 'PartyContactPointInvalid')); assert.equal(harness.selectWheres.length, 0); assert.equal(harness.insertValues.length, 0); }), @@ -684,7 +684,7 @@ test('rejects invalid E.164 and oversized extensions through the service typed-e ).pipe(Effect.asVoid), )); -test('rejects an explicit alias Party add but keeps durable ContactPoint updates readable through the full chain', () => +void test('rejects an explicit alias Party add but keeps durable ContactPoint updates readable through the full chain', () => runEffectTestPromise( Effect.gen(function* contactPointScenario() { const intermediatePartyId = '20000000-0000-4000-8000-000000000002'; @@ -804,7 +804,7 @@ test('rejects an explicit alias Party add but keeps durable ContactPoint updates }), )); -test('advances the replaced channel preference revision as well as the selected contact', () => +void test('advances the replaced channel preference revision as well as the selected contact', () => runEffectTestPromise( Effect.gen(function* contactPointScenario() { const row = contactRow({ preferred: false }); @@ -821,13 +821,13 @@ test('advances the replaced channel preference revision as well as the selected updateCommand({ preferred: true, type: 'SET_CHANNEL_PREFERRED' }), ); const revision = harness.updateSets[0]?.['revision']; - assert.ok(revision instanceof SQL); + assert.ok(is(revision, 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', () => +void 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)({ @@ -906,7 +906,7 @@ test('persists bounded ARES provenance on the address and purpose without using }), )); -test('treats PHONE extensions as distinct endpoints while rejecting an exact duplicate extension', () => +void test('treats PHONE extensions as distinct endpoints while rejecting an exact duplicate extension', () => runEffectTestPromise( Effect.gen(function* contactPointScenario() { const existing = contactRow({ @@ -950,12 +950,12 @@ test('treats PHONE extensions as distinct endpoints while rejecting an exact dup const duplicate = yield* Effect.flip( addContactPointRecord(duplicateHarness.transaction, scope, command('101')), ); - assert.equal(duplicate._tag, 'PartyContactPointAlreadyExists'); + assert.ok(Predicate.isTagged(duplicate, 'PartyContactPointAlreadyExists')); assert.equal(duplicateHarness.insertValues.length, 0); }), )); -test('whole ADDRESS end preserves an earlier purpose end and its independent accepted evidence', () => +void test('whole ADDRESS end preserves an earlier purpose end and its independent accepted evidence', () => runEffectTestPromise( Effect.gen(function* contactPointScenario() { const address = addressRow(); 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 823c57d50..a20611dca 100644 --- a/app/verticals/party-registry/tests/unit/correction-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/correction-contract.test.ts @@ -1,7 +1,7 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; // @effect-diagnostics asyncFunction:off globalDate:off -- Existing compatibility boundary; expires: 2026-12-31. /* 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 { Effect, Match, Option, Schema, Predicate } from 'effect'; import assert from 'node:assert/strict'; import test from 'node:test'; import { @@ -55,7 +55,7 @@ const relationshipCommand = decode(SupersedeRelationshipCorrectionCommandSchema) relationshipCommandEncoded, ); -test('correction is closed to Party type, display name, and official identifier assertions', () => { +void test('correction is closed to Party type, display name, and official identifier assertions', () => { for (const factKind of ['PARTY_TYPE', 'DISPLAY_NAME', 'OFFICIAL_IDENTIFIER']) { assert.doesNotThrow(() => decode(PartyCorrectionCommandSchema)({ @@ -80,7 +80,7 @@ test('correction is closed to Party type, display name, and official identifier ); }); -test('correction follow-up is typed and duplicate confirmation remains readiness-only', () => { +void 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'); @@ -124,7 +124,7 @@ test('correction follow-up is typed and duplicate confirmation remains readiness ); }); -test('relationship correction is closed, revisioned, interval checked, and has no caller authority hints', () => { +void test('relationship correction is closed, revisioned, interval checked, and has no caller authority hints', () => { const strictDecode = Schema.decodeUnknownSync(PartyCorrectionCommandSchema, { onExcessProperty: 'error', }); @@ -227,7 +227,7 @@ const transactionHarness = ( return { insertValues, transaction, updateSets }; }; -test('relationship supersession preserves endpoint/type identity and stores trusted actor plus old/new links', async () => { +void 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( @@ -255,7 +255,7 @@ test('relationship supersession preserves endpoint/type identity and stores trus assert.equal(Option.getOrThrow(result.replacementRelationshipRef).resourceId, replacementId); }); -test('relationship retraction retains original effective validity and creates no replacement', async () => { +void test('relationship retraction retains original effective validity and creates no replacement', async () => { const command = decode(PartyCorrectionCommandSchema)({ ...evidence, correctionMode: 'RETRACT', @@ -277,7 +277,7 @@ test('relationship retraction retains original effective validity and creates no assert.ok(Option.isNone(result.replacementAssertionId)); }); -test('stale revision and foreign-tenant relationship correction fail before business writes', async () => { +void test('stale revision and foreign-tenant relationship correction fail before business writes', async () => { await Promise.all( [ decode(SupersedeRelationshipCorrectionCommandSchema)({ @@ -298,14 +298,14 @@ test('stale revision and foreign-tenant relationship correction fail before busi }), ), ); - assert.equal(error._tag, 'PartyCorrectionConflict'); + assert.ok(Predicate.isTagged(error, 'PartyCorrectionConflict')); assert.equal(h.updateSets.length, 0); assert.equal(h.insertValues.length, 0); }), ); }); -test('UNRESOLVED Party Type enrichment is rejected before mutation by correction', async () => { +void test('UNRESOLVED Party Type enrichment is rejected before mutation by correction', async () => { const h = transactionHarness([ [], [{ partyId }], @@ -344,12 +344,12 @@ test('UNRESOLVED Party Type enrichment is rejected before mutation by correction correctPartyFactRecord(h.transaction, tenantId, command, { actionInvocationId, principalId }), ), ); - assert.equal(error._tag, 'PartyCorrectionConflict'); + assert.ok(Predicate.isTagged(error, 'PartyCorrectionConflict')); assert.match(error.reason, /enrichment/u); assert.equal(h.updateSets.length, 0); }); -test('detail exposes immutable original/result semantics, governance, and source distinct from actor', async () => { +void test('detail exposes immutable original/result semantics, governance, and source distinct from actor', async () => { const h = transactionHarness([ [ { @@ -417,7 +417,7 @@ test('detail exposes immutable original/result semantics, governance, and source assert.equal(detail.governance.policyVersion, detail.policyVersion); }); -test('relationship overlap is a typed conflict and no correction journal is written after failed replacement', async () => { +void 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 }]], @@ -433,14 +433,14 @@ test('relationship overlap is a typed conflict and no correction journal is writ }), ), ); - assert.equal(error._tag, 'PartyCorrectionConflict'); + assert.ok(Predicate.isTagged(error, '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 () => { +void test('correction of a durable relationship preserves stored alias endpoints', async () => { const canonicalId = '90000000-0000-4000-8000-000000000001'; const original = relationshipRow(); const h = transactionHarness( @@ -466,13 +466,13 @@ test('correction of a durable relationship preserves stored alias endpoints', as assert.notEqual(h.insertValues[0]?.['fromPartyId'], canonicalId); }); -test('correction history requires reviewer authority; ordinary identity read permission is insufficient', () => { +void test('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' }); }); -test('Party Type correction reconciles newly eligible claims before superseding the original fact', async () => { +void test('Party Type correction reconciles newly eligible claims before superseding the original fact', async () => { const h = transactionHarness([ [], [{ partyId }], @@ -523,13 +523,13 @@ test('Party Type correction reconciles newly eligible claims before superseding correctPartyFactRecord(h.transaction, tenantId, command, { actionInvocationId, principalId }), ), ); - assert.equal(error._tag, 'PartyCorrectionConflict'); + assert.ok(Predicate.isTagged(error, 'PartyCorrectionConflict')); assert.match(error.reason, /exclusive identifier claims/u); assert.equal(h.updateSets.length, 0); assert.equal(h.insertValues.length, 0); }); -test('type Correction cannot treat a reviewer decision or source label as subject evidence', async () => { +void test('type Correction cannot treat a reviewer decision or source label as subject evidence', async () => { const h = transactionHarness([ [], [{ partyId }], @@ -558,7 +558,7 @@ test('type Correction cannot treat a reviewer decision or source label as subjec correctPartyFactRecord(h.transaction, tenantId, command, { actionInvocationId, principalId }), ), ); - assert.equal(error._tag, 'PartyCorrectionConflict'); + assert.ok(Predicate.isTagged(error, 'PartyCorrectionConflict')); assert.equal(error.reason, 'subject_evidence_required'); assert.equal(h.insertValues.length, 0); assert.equal(h.updateSets.length, 0); 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..190023ac6 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,5 +1,5 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { DateTime, Effect } from 'effect'; +import { DateTime, Effect, Predicate } 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'; @@ -146,7 +146,7 @@ const endInput = (validTo: string, method: string) => ({ validTo, }); -test('keeps a future-ended role active until its exclusive effective end', () => { +void 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'], @@ -166,7 +166,7 @@ test('keeps a future-ended role active until its exclusive effective end', () => endInput(futureEnd, 'CONFIRMED_CUSTOMER_RELATIONSHIP_END'), ), ).then((result) => { - assert.equal(result._tag, 'found'); + assert.ok(Predicate.isTagged(result, 'found')); assert.equal(harness.updateSets[0]?.['state'], 'ACTIVE'); assert.equal(harness.updateSets[0]?.['isCurrent'], true); assert.equal(harness.updateSets[0]?.['endProvenanceSource'], 'contracts.core'); @@ -182,7 +182,7 @@ test('keeps a future-ended role active until its exclusive effective end', () => }); }); -test('records a retrospective end as historical without deleting the role period', () => { +void 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'], @@ -205,7 +205,7 @@ test('records a retrospective end as historical without deleting the role period }); }); -test('rejects inactivity evidence before persisting a CUSTOMER end', () => { +void test('rejects inactivity evidence before persisting a CUSTOMER end', () => { const harness = transactionHarness([[counterpartyRow], [roleRow()]]); return runEffectTestPromise( @@ -223,7 +223,7 @@ test('rejects inactivity evidence before persisting a CUSTOMER end', () => { }); }); -test('reuses an exactly repeated end without another write', () => { +void test('reuses an exactly repeated end without another write', () => { const validTo = '2025-01-01T00:00:00.000Z'; const ended = roleRow({ endEvidenceRefs: ['contract:end'], @@ -241,13 +241,13 @@ test('reuses an exactly repeated end without another write', () => { endInput(validTo, 'CONFIRMED_CUSTOMER_RELATIONSHIP_END'), ), ).then((result) => { - assert.equal(result._tag, 'found'); + assert.ok(Predicate.isTagged(result, 'found')); assert.equal(result.changed, false); assert.equal(harness.updateSets.length, 0); }); }); -test('reads end provenance independently from the role-add provenance', () => { +void test('reads end provenance independently from the role-add provenance', () => { const ended = roleRow({ endEvidenceRefs: ['contract:end'], endProvenanceMethod: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', @@ -261,7 +261,7 @@ test('reads end provenance independently from the role-add provenance', () => { return runEffectTestPromise( listCounterpartyRoleHistory(harness.transaction, tenantId, legalEntityId, counterpartyId), ).then((result) => { - assert.equal(result._tag, 'found'); + assert.ok(Predicate.isTagged(result, 'found')); assert.deepEqual(result.value[0]?.endProvenance, { evidenceReference: 'contract:end', method: 'CONFIRMED_CUSTOMER_RELATIONSHIP_END', @@ -271,7 +271,7 @@ test('reads end provenance independently from the role-add provenance', () => { }); }); -test('allows the authorized tenant-admin path to read history without payload Legal Entity data', () => { +void test('allows the authorized tenant-admin path to read history without payload Legal Entity data', () => { const harness = transactionHarness([ [{ ...counterpartyRow, storedPartyId: partyId }], [roleRow()], @@ -280,7 +280,7 @@ test('allows the authorized tenant-admin path to read history without payload Le return runEffectTestPromise( listCounterpartyRoleHistory(harness.transaction, tenantId, undefined, counterpartyId), ).then((result) => { - assert.equal(result._tag, 'found'); + assert.ok(Predicate.isTagged(result, 'found')); assert.equal(result.value[0]?.roleType, 'CUSTOMER'); assert.deepEqual(harness.selectedTables, [ 'counterparty_admin_read_models', @@ -289,7 +289,7 @@ test('allows the authorized tenant-admin path to read history without payload Le }); }); -test('rejects an alias Party create target with canonical survivor guidance', () => { +void 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 }], @@ -313,13 +313,13 @@ test('rejects an alias Party create target with canonical survivor guidance', () tenantId, }), ).then((result) => { - assert.equal(result._tag, 'party_alias'); + assert.ok(Predicate.isTagged(result, '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', () => { +void 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([ @@ -343,7 +343,7 @@ test('admin detail follows a complete Party alias chain while retaining the stor return runEffectTestPromise( findCounterpartyRecord(harness.transaction, tenantId, undefined, counterpartyId), ).then((result) => { - assert.equal(result._tag, 'found'); + assert.ok(Predicate.isTagged(result, 'found')); assert.equal(result.value.party.storedPartyRef.resourceId, partyId); assert.equal(result.value.party.canonicalPartyRef.resourceId, survivorId); assert.equal(result.value.legalEntityRef.resourceId, legalEntityId); @@ -352,7 +352,7 @@ test('admin detail follows a complete Party alias chain while retaining the stor }); }); -test('creates the tenant-admin snapshot atomically without creating an implicit role', () => { +void test('creates the tenant-admin snapshot atomically without creating an implicit role', () => { const party = { archivedAt: null, partyId, tenantId }; const harness = transactionHarness( [[], [{ partyId }], [], [{ partyId }], [party]], @@ -376,13 +376,13 @@ test('creates the tenant-admin snapshot atomically without creating an implicit tenantId, }), ).then((result) => { - assert.equal(result._tag, 'found'); + assert.ok(Predicate.isTagged(result, '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', () => { +void 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( @@ -409,7 +409,7 @@ test('adds a future role and its admin history projection in the same transactio validTo: null, }), ).then((result) => { - assert.equal(result._tag, 'found'); + assert.ok(Predicate.isTagged(result, 'found')); assert.equal(harness.insertValues[0]?.['isCurrent'], false); assert.deepEqual(harness.insertedTables, [ 'counterparty_role_periods', 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..df0c2ffa8 100644 --- a/app/verticals/party-registry/tests/unit/database-client.test.ts +++ b/app/verticals/party-registry/tests/unit/database-client.test.ts @@ -2,10 +2,10 @@ 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 { Effect, Predicate } from 'effect'; import { acquirePoolResource, makePartyDatabase } from '../../src/db/client.ts'; -test('finalizes the Party Registry pool when its Effect scope closes', async () => { +void test('finalizes the Party Registry pool when its Effect scope closes', async () => { let finalized = false; await runEffectTestPromise( Effect.scoped( @@ -20,7 +20,7 @@ test('finalizes the Party Registry pool when its Effect scope closes', async () assert.equal(finalized, true); }); -test('keeps Party Registry pool acquisition failure in the typed error channel', async () => { +void test('keeps Party Registry pool acquisition failure in the typed error channel', async () => { const error = await runEffectTestPromise( Effect.flip( Effect.scoped( @@ -39,6 +39,6 @@ test('keeps Party Registry pool acquisition failure in the typed error channel', ), ), ); - assert.equal(error._tag, 'PartyDatabaseConnectionError'); + assert.ok(Predicate.isTagged(error, 'PartyDatabaseConnectionError')); assert.equal(error.reason, 'Unable to initialize the Party Registry PostgreSQL connection pool'); }); 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 5a7d3c3a0..28160e670 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,7 +1,7 @@ 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 { DateTime, Effect, Predicate } from 'effect'; import assert from 'node:assert/strict'; import test from 'node:test'; import type { OrganizationEngagementProfileRecord } from '../../src/db/engagement-schema.ts'; @@ -45,7 +45,7 @@ const rejectingMutationTransaction = (failure: Failure) => }), }) as unknown as Parameters[0]; -test('reconstructs typed references from the owner-local persistence record', () => { +void test('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); @@ -58,7 +58,7 @@ test('reconstructs typed references from the owner-local persistence record', () ); }); -test('fails closed when a caller-supplied ref crosses the trusted tenant', async () => { +void test('fails closed when a caller-supplied ref crosses the trusted tenant', async () => { const failure = await runEffectTestPromise( Effect.flip( ensureReferencesBelongToTenant(tenantId, { @@ -67,11 +67,11 @@ test('fails closed when a caller-supplied ref crosses the trusted tenant', async }), ), ); - assert.equal(failure._tag, 'EngagementProfileConflict'); + assert.ok(Predicate.isTagged(failure, 'EngagementProfileConflict')); assert.equal(failure.code, 'contacts_party_counterparty_mismatch'); }); -test('maps a wrapped owner uniqueness constraint to the declared engagement conflict', async () => { +void test('maps a wrapped owner uniqueness constraint to the declared engagement conflict', async () => { const failure = await runEffectTestPromise( Effect.flip( createOrganizationEngagementProfile( @@ -88,7 +88,7 @@ test('maps a wrapped owner uniqueness constraint to the declared engagement conf ), ); - assert.equal(failure._tag, 'EngagementProfileConflict'); + assert.ok(Predicate.isTagged(failure, 'EngagementProfileConflict')); assert.equal(failure.code, 'contacts_engagement_profile_already_exists'); assert.equal( failure.reason, @@ -96,7 +96,7 @@ test('maps a wrapped owner uniqueness constraint to the declared engagement conf ); }); -test('continues past an unrelated wrapper code to the owner uniqueness constraint', async () => { +void test('continues past an unrelated wrapper code to the owner uniqueness constraint', async () => { const failure = await runEffectTestPromise( Effect.flip( createOrganizationEngagementProfile( @@ -112,11 +112,11 @@ test('continues past an unrelated wrapper code to the owner uniqueness constrain ), ); - assert.equal(failure._tag, 'EngagementProfileConflict'); + assert.ok(Predicate.isTagged(failure, 'EngagementProfileConflict')); assert.equal(failure.code, 'contacts_engagement_profile_already_exists'); }); -test('maps an unrelated uniqueness constraint to the existing persistence fallback', async () => { +void test('maps an unrelated uniqueness constraint to the existing persistence fallback', async () => { const failure = await runEffectTestPromise( Effect.flip( createOrganizationEngagementProfile( @@ -129,7 +129,7 @@ test('maps an unrelated uniqueness constraint to the existing persistence fallba ), ); - assert.equal(failure._tag, 'EngagementProfilePersistenceUnavailable'); + assert.ok(Predicate.isTagged(failure, 'EngagementProfilePersistenceUnavailable')); assert.equal(failure.code, 'contacts_engagement_profile_persistence_unavailable'); assert.equal( failure.reason, 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..8d4a4d79d 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,5 +1,5 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { DateTime, Effect, Match, Schema } from 'effect'; +import { DateTime, Effect, Match, Schema, Predicate } from 'effect'; // @effect-diagnostics asyncFunction:off globalDate:off -- Existing compatibility boundary; expires: 2026-12-31. /* 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'; @@ -148,7 +148,7 @@ const harness = ( return { deleted: () => deletes, inserts, lockedTables, transaction, updates }; }; -test('Add reuses a current same-Party identifier instead of duplicating an assertion', async () => { +void 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, { @@ -165,7 +165,7 @@ test('Add reuses a current same-Party identifier instead of duplicating an asser assert.equal(db.inserts.length, 0); }); -test('Add retains ARES evidence separately from the accepting actor and only claims eligible Party types', async () => { +void 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', @@ -214,7 +214,7 @@ test('Add retains ARES evidence separately from the accepting actor and only cla ); }); -test('ending an identifier preserves its fact and releases its current claim', async () => { +void 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') }), @@ -227,14 +227,14 @@ test('ending an identifier preserves its fact and releases its current claim', a '2026-02-01T00:00:00.000Z', ), ); - assert.equal(result._tag, 'found'); + assert.ok(Predicate.isTagged(result, '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]); }); -test('a future end does not release a presently valid claim', async () => { +void test('a future end does not release a presently valid claim', async () => { const db = harness(); const result = await runEffectTestPromise( endOfficialIdentifierRecord( @@ -244,7 +244,7 @@ test('a future end does not release a presently valid claim', async () => { '2099-01-01T00:00:00.000Z', ), ); - assert.equal(result._tag, 'conflict'); + assert.ok(Predicate.isTagged(result, 'conflict')); assert.equal(db.updates.length, 0); assert.equal(db.deleted(), 0); }); @@ -256,7 +256,7 @@ const verificationCommand = { verification: 'VERIFIED', } as const; -test('verification collision changes neither metadata nor claim ownership', async () => { +void test('verification collision changes neither metadata nor claim ownership', async () => { const db = harness({ claimOwner: 'another-party' }); const result = await runEffectTestPromise( updateOfficialIdentifierVerificationRecord( @@ -266,12 +266,12 @@ test('verification collision changes neither metadata nor claim ownership', asyn verificationCommand, ), ); - assert.equal(result._tag, 'claim_conflict'); + assert.ok(Predicate.isTagged(result, 'claim_conflict')); assert.equal(db.updates.length, 0); assert.equal(db.inserts.length, 0); }); -test('verification preserves before-state and immutable identity/provenance while acquiring an eligible claim', async () => { +void test('verification preserves before-state and immutable identity/provenance while acquiring an eligible claim', async () => { const db = harness(); const result = await runEffectTestPromise( updateOfficialIdentifierVerificationRecord( @@ -281,7 +281,7 @@ test('verification preserves before-state and immutable identity/provenance whil verificationCommand, ), ); - assert.equal(result._tag, 'found'); + assert.ok(Predicate.isTagged(result, 'found')); const found = Match.value(result).pipe( Match.tag('found', (value) => value), Match.orElse(() => assert.fail('Expected the identifier verification update to succeed')), @@ -297,7 +297,7 @@ test('verification preserves before-state and immutable identity/provenance whil assert.equal(db.inserts[0]?.table, partyIdentifierClaims); }); -test('PERSON verification cannot acquire an implicit strong identifier claim', async () => { +void test('PERSON verification cannot acquire an implicit strong identifier claim', async () => { const db = harness({ partyType: 'PERSON' }); const result = await runEffectTestPromise( updateOfficialIdentifierVerificationRecord( @@ -307,11 +307,11 @@ test('PERSON verification cannot acquire an implicit strong identifier claim', a verificationCommand, ), ); - assert.equal(result._tag, 'found'); + assert.ok(Predicate.isTagged(result, 'found')); assert.equal(db.inserts.length, 0); }); -test('verification downgrade releases its claim without erasing the previous verification evidence', async () => { +void 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, @@ -324,7 +324,7 @@ test('verification downgrade releases its claim without erasing the previous ver verification: 'REJECTED', }), ); - assert.equal(result._tag, 'found'); + assert.ok(Predicate.isTagged(result, 'found')); const found = Match.value(result).pipe( Match.tag('found', (value) => value), Match.orElse(() => assert.fail('Expected the identifier verification downgrade to succeed')), @@ -335,7 +335,7 @@ test('verification downgrade releases its claim without erasing the previous ver assert.equal(db.deleted(), 1); }); -test('archived Party and stale verification updates are rejected before mutation', async () => { +void test('archived Party and stale verification updates are rejected before mutation', async () => { await Promise.all( [harness({ archived: true }), harness({ current: row({ verificationState: 'REJECTED' }) })].map( async (db) => { @@ -347,7 +347,7 @@ test('archived Party and stale verification updates are rejected before mutation verificationCommand, ), ); - assert.equal(result._tag, 'conflict'); + assert.ok(Predicate.isTagged(result, 'conflict')); assert.equal(db.updates.length, 0); }, ), 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..674856377 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,7 @@ 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 { 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'; @@ -44,7 +44,7 @@ const assertInvariantEvidence = (snapshot: ActionEvidenceSnapshot) => { assert.equal(snapshot.outboxMessages.length, 1); }; -test('Update Party records metadata-only invariant evidence with its event and outbox', () => +void test('Update Party records metadata-only invariant evidence with its event and outbox', () => runEffectTestPromise( Effect.gen(function* verifyUpdateEvidence() { const collector = createActionCollector( @@ -72,7 +72,7 @@ test('Update Party records metadata-only invariant evidence with its event and o }), )); -test('Archive Party records metadata-only invariant evidence with its event and outbox', () => +void test('Archive Party records metadata-only invariant evidence with its event and outbox', () => runEffectTestPromise( Effect.gen(function* verifyArchiveEvidence() { const collector = createActionCollector( @@ -93,7 +93,7 @@ test('Archive Party records metadata-only invariant evidence with its event and }), )); -test('Unarchive Party records metadata-only invariant evidence with its event and outbox', () => +void test('Unarchive Party records metadata-only invariant evidence with its event and outbox', () => runEffectTestPromise( Effect.gen(function* verifyUnarchiveEvidence() { const collector = createActionCollector( @@ -114,7 +114,7 @@ test('Unarchive Party records metadata-only invariant evidence with its event an }), )); -test('Unarchive review outcome commits once and replays without an unarchive event or outbox', () => +void test('Unarchive review outcome commits once and replays without an unarchive event or outbox', () => runEffectTestPromise( Effect.gen(function* verifyUnarchiveConflictEvidence() { let calls = 0; @@ -155,7 +155,7 @@ test('Unarchive review outcome commits once and replays without an unarchive eve }; assert.deepEqual(yield* harness.runtime.runAction(request), blocked); const replay = yield* harness.runtime.runAction(request).pipe(Effect.flip); - assert.equal(replay._tag, 'ActionAlreadyCommitted'); + assert.ok(Predicate.isTagged(replay, 'ActionAlreadyCommitted')); assert.equal(calls, 1); const snapshot = harness.snapshot(); assert.equal(snapshot.committed.length, 1); 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..378f732c9 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,7 @@ 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 { 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,7 +63,7 @@ const makeServices = ( }; }; -test('Party Detail reads the final canonical Party after the complete historical alias chain', () => { +void 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'), @@ -90,7 +90,7 @@ test('Party Detail reads the final canonical Party after the complete historical assert.deepEqual(encoded.party, canonicalPartyWire); }); -test('Party Detail preserves archived lifecycle independently of direct resolution metadata', () => { +void 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, @@ -110,7 +110,7 @@ test('Party Detail preserves archived lifecycle independently of direct resoluti }); }); -test('Party Detail fails closed for cycles and broken historical chains without reading an alias Party', () => { +void 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')], @@ -119,12 +119,12 @@ test('Party Detail fails closed for cycles and broken historical chains without const error = runEffectTestSync( Effect.flip(readPartyDetailFromServices(partyRef('party-a'), tenantId, services)), ); - assert.equal(error._tag, 'ReadHandlerUnavailable'); + assert.ok(Predicate.isTagged(error, 'ReadHandlerUnavailable')); assert.deepEqual(lookups, []); } }); -test('Party Detail hides a missing direct Party and a cross-tenant requested reference', () => { +void test('Party Detail hides a missing direct Party and a cross-tenant requested reference', () => { const { lookups, services } = makeServices([]); for (const requested of [ partyRef('missing'), @@ -133,7 +133,7 @@ test('Party Detail hides a missing direct Party and a cross-tenant requested ref const error = runEffectTestSync( Effect.flip(readPartyDetailFromServices(requested, tenantId, services)), ); - assert.equal(error._tag, 'ReadHandlerNotFound'); + assert.ok(Predicate.isTagged(error, 'ReadHandlerNotFound')); } assert.deepEqual(lookups, []); }); 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 f6ba39732..0bb8dfd71 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,7 +1,7 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import type { SQL } from 'drizzle-orm'; import { PgDialect } from 'drizzle-orm/pg-core'; -import { DateTime, Effect, Match, Option, Result, Schema } from 'effect'; +import { DateTime, Effect, Match, Option, Result, Schema, Predicate } from 'effect'; import assert from 'node:assert/strict'; import test from 'node:test'; import type { AresAppliedEvidence } from '../../shared/domain/ares-application.ts'; @@ -150,11 +150,11 @@ const assertTenantLockIsFirst = (harness: ReturnType) assert.deepEqual(query.params, [tenantIdentityWriteLockKey(tenantId)]); }; -test('ended Party facts are made non-current as part of the same transition', () => { +void test('ended Party facts are made non-current as part of the same transition', () => { assert.deepEqual(endedPartyFactTransition, { isCurrent: false, state: 'ENDED' }); }); -test('unnamed Party insertion persists no fabricated display-name assertion', () => +void test('unnamed Party insertion persists no fabricated display-name assertion', () => runEffectTestPromise( Effect.gen(function* verifyIdentityPersistence() { const candidateEvidence: AresAppliedEvidence = { @@ -218,7 +218,7 @@ test('unnamed Party insertion persists no fabricated display-name assertion', () }), )); -test('identity updates close the preceding assertion before accepting its replacement', () => +void test('identity updates close the preceding assertion before accepting its replacement', () => runEffectTestPromise( Effect.gen(function* verifyIdentityPersistence() { const current = partyRow({ archivedAt: null }); @@ -240,7 +240,7 @@ test('identity updates close the preceding assertion before accepting its replac provenanceSource: 'test', validFrom: '2026-01-01T00:00:00.000Z', }); - assert.equal(result._tag, 'found'); + assert.ok(Predicate.isTagged(result, 'found')); assertTenantLockIsFirst(harness); assert.deepEqual(harness.updateSets[1], { isCurrent: false, @@ -255,7 +255,7 @@ test('identity updates close the preceding assertion before accepting its replac }), )); -test('unarchive owner classification distinguishes conflict from ambiguity deterministically', () => { +void test('unarchive owner classification distinguishes conflict from ambiguity deterministically', () => { assert.deepEqual(classifyUnarchiveClaimOwners(partyId, [{}, { partyId }]), { _tag: 'available', }); @@ -276,7 +276,7 @@ test('unarchive owner classification distinguishes conflict from ambiguity deter ); }); -test('future-effective identity updates do not replace current facts early', () => +void test('future-effective identity updates do not replace current facts early', () => runEffectTestPromise( Effect.gen(function* verifyIdentityPersistence() { const harness = transactionHarness([[partyRow({ archivedAt: null })], [], [{ partyId }]]); @@ -290,13 +290,13 @@ 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.ok(Predicate.isTagged(result, 'conflict')); assert.deepEqual(harness.insertedValues, []); assert.deepEqual(harness.updateSets, []); }), )); -test('unarchive keeps the Party archived when an exact claim belongs to another Party', () => +void test('unarchive keeps the Party archived when an exact claim belongs to another Party', () => runEffectTestPromise( Effect.gen(function* verifyIdentityPersistence() { const harness = transactionHarness([ @@ -321,7 +321,7 @@ test('unarchive keeps the Party archived when an exact claim belongs to another }), )); -test('blocked unarchive persists a case and decision without mutating Party, then reuses the case on a fresh attempt', () => +void 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'; @@ -362,7 +362,7 @@ test('blocked unarchive persists a case and decision without mutating Party, the 4, decisionId, ); - assert.equal(result._tag, 'blocked'); + assert.ok(Predicate.isTagged(result, 'blocked')); const blocked = Match.value(result).pipe( Match.tag('blocked', ({ value }) => value), Match.orElse(() => assert.fail('Expected unarchive to be blocked')), @@ -414,7 +414,7 @@ test('blocked unarchive persists a case and decision without mutating Party, the 4, secondDecisionId, ); - assert.equal(retry._tag, 'blocked'); + assert.ok(Predicate.isTagged(retry, 'blocked')); const retryBlocked = Match.value(retry).pipe( Match.tag('blocked', ({ value }) => value), Match.orElse(() => assert.fail('Expected retry to be blocked')), @@ -427,7 +427,7 @@ test('blocked unarchive persists a case and decision without mutating Party, the }), )); -test('unresolved unnamed unarchive review persists no invented display-name evidence', () => +void test('unresolved unnamed unarchive review persists no invented display-name evidence', () => runEffectTestPromise( Effect.gen(function* verifyUnresolvedUnarchiveReview() { const current = partyRow({ currentDisplayName: null, currentType: 'UNRESOLVED' }); @@ -449,7 +449,7 @@ test('unresolved unnamed unarchive review persists no invented display-name evid 4, secondOwnerId, ); - assert.equal(result._tag, 'blocked'); + assert.ok(Predicate.isTagged(result, 'blocked')); const blocked = Match.value(result).pipe( Match.tag('blocked', ({ value }) => value), Match.orElse(() => assert.fail('Expected unarchive to be blocked')), @@ -464,7 +464,7 @@ test('unresolved unnamed unarchive review persists no invented display-name evid }), )); -test('archive acquires the tenant identity lock before any Party row lock', () => +void test('archive acquires the tenant identity lock before any Party row lock', () => runEffectTestPromise( Effect.gen(function* verifyIdentityPersistence() { const harness = transactionHarness([[]]); @@ -475,12 +475,12 @@ test('archive acquires the tenant identity lock before any Party row lock', () = 4, 'ARCHIVED', ); - assert.equal(result._tag, 'not_found'); + assert.ok(Predicate.isTagged(result, 'not_found')); assertTenantLockIsFirst(harness); }), )); -test('unarchive restores an unclaimed eligible identifier before activating the Party', () => +void test('unarchive restores an unclaimed eligible identifier before activating the Party', () => runEffectTestPromise( Effect.gen(function* verifyIdentityPersistence() { const activeParty = partyRow({ archivedAt: null, revision: 5 }); @@ -491,7 +491,7 @@ test('unarchive restores an unclaimed eligible identifier before activating the const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); - assert.equal(result._tag, 'found'); + assert.ok(Predicate.isTagged(result, 'found')); assert.deepEqual(harness.insertedValues, [ [ { @@ -517,7 +517,7 @@ test('unarchive restores an unclaimed eligible identifier before activating the }), )); -test('unarchive rejects an alias rather than forwarding the write to its survivor', () => +void test('unarchive rejects an alias rather than forwarding the write to its survivor', () => runEffectTestPromise( Effect.gen(function* verifyIdentityPersistence() { const harness = transactionHarness([ @@ -530,13 +530,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.ok(Predicate.isTagged(error, 'PartyAliasWriteRejected')); assert.deepEqual(harness.insertedValues, []); assert.deepEqual(harness.updateSets, []); }), )); -test('unarchive reports ambiguous exact claims without changing archived state', () => +void test('unarchive reports ambiguous exact claims without changing archived state', () => runEffectTestPromise( Effect.gen(function* verifyIdentityPersistence() { const harness = transactionHarness([ @@ -568,7 +568,7 @@ test('unarchive reports ambiguous exact claims without changing archived state', }), )); -test('unarchive does not promote a PERSON ICO into an exclusive strong claim', () => +void test('unarchive does not promote a PERSON ICO into an exclusive strong claim', () => runEffectTestPromise( Effect.gen(function* verifyIdentityPersistence() { const currentParty = partyRow({ currentType: 'PERSON' }); @@ -578,13 +578,13 @@ 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.ok(Predicate.isTagged(result, 'found')); assert.deepEqual(harness.insertedValues, []); assert.equal(harness.updateSets.length, 1); }), )); -test('unarchive requires review while a duplicate case involving the Party remains open', () => +void 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'; @@ -605,7 +605,7 @@ test('unarchive requires review while a duplicate case involving the Party remai }), )); -test('unarchive requires review for unresolved identity without any eligible strong claim', () => +void test('unarchive requires review for unresolved identity without any eligible strong claim', () => runEffectTestPromise( Effect.gen(function* verifyIdentityPersistence() { const harness = transactionHarness([ @@ -626,7 +626,7 @@ test('unarchive requires review for unresolved identity without any eligible str }), )); -test('reviewed UNRESOLVED Party can unarchive using retained accepted creation evidence', () => +void test('reviewed UNRESOLVED Party can unarchive using retained accepted creation evidence', () => runEffectTestPromise( Effect.gen(function* restoreReviewedUnresolved() { const current = partyRow({ currentType: 'UNRESOLVED' }); @@ -643,13 +643,13 @@ test('reviewed UNRESOLVED Party can unarchive using retained accepted creation e [[{ ...current, archivedAt: null, revision: 5 }]], ); const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); - assert.equal(result._tag, 'found'); + assert.ok(Predicate.isTagged(result, 'found')); assert.equal(harness.updateSets.length, 1); assert.deepEqual(harness.insertedValues, []); }), )); -test('Party type enrichment refuses another owner of a newly eligible identifier', () => +void test('Party type enrichment refuses another owner of a newly eligible identifier', () => runEffectTestPromise( Effect.gen(function* preventEnrichmentCollision() { const current = partyRow({ archivedAt: null, currentType: 'UNRESOLVED' }); @@ -682,14 +682,14 @@ test('Party type enrichment refuses another owner of a newly eligible identifier ], validFrom: '2026-01-01T00:00:00.000Z', }); - assert.equal(result._tag, 'conflict'); + assert.ok(Predicate.isTagged(result, 'conflict')); assert.deepEqual(harness.insertedValues, []); assert.deepEqual(harness.updateSets, []); assert.deepEqual(harness.deletedTargets, []); }), )); -test('Party type enrichment atomically claims identifiers that newly qualify', () => +void test('Party type enrichment atomically claims identifiers that newly qualify', () => runEffectTestPromise( Effect.gen(function* claimEnrichedIdentifier() { const current = partyRow({ archivedAt: null, currentType: 'UNRESOLVED' }); @@ -717,7 +717,7 @@ test('Party type enrichment atomically claims identifiers that newly qualify', ( ], validFrom: '2026-01-01T00:00:00.000Z', }); - assert.equal(result._tag, 'found'); + assert.ok(Predicate.isTagged(result, 'found')); assert.deepEqual(harness.insertedValues[0], [ { identifierTypeKey: 'ICO', @@ -732,7 +732,7 @@ test('Party type enrichment atomically claims identifiers that newly qualify', ( }), )); -test('type correction reconciliation releases an ICO claim no longer eligible for a PERSON', () => +void test('type correction reconciliation releases an ICO claim no longer eligible for a PERSON', () => runEffectTestPromise( Effect.gen(function* releaseIneligibleClaim() { const harness = transactionHarness([ @@ -762,7 +762,7 @@ test('type correction reconciliation releases an ICO claim no longer eligible fo }), )); -test('identity updates reject a historical end earlier than the assertion being replaced', () => +void test('identity updates reject a historical end earlier than the assertion being replaced', () => runEffectTestPromise( Effect.gen(function* rejectInvalidHistoricalInterval() { const harness = transactionHarness([ @@ -781,14 +781,14 @@ test('identity updates reject a historical end earlier than the assertion being provenanceSource: 'test', validFrom: '2026-01-01T00:00:00.000Z', }); - assert.equal(result._tag, 'conflict'); + assert.ok(Predicate.isTagged(result, 'conflict')); assert.deepEqual(harness.insertedValues, []); assert.deepEqual(harness.updateSets, []); assert.deepEqual(harness.deletedTargets, []); }), )); -test('type enrichment rejects unevidenced type before accepting facts or claims', () => +void test('type enrichment rejects unevidenced type before accepting facts or claims', () => runEffectTestPromise( Effect.gen(function* rejectUnsupportedType() { const current = partyRow({ archivedAt: null, currentType: 'UNRESOLVED' }); @@ -805,7 +805,7 @@ test('type enrichment rejects unevidenced type before accepting facts or claims' validFrom: '2026-01-01T00:00:00.000Z', }), ); - assert.equal(error._tag, 'PartyEvidenceInsufficient'); + assert.ok(Predicate.isTagged(error, 'PartyEvidenceInsufficient')); assert.deepEqual(harness.insertedValues, []); assert.deepEqual(harness.updateSets, []); }), 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 71191029d..5219cdeb3 100644 --- a/app/verticals/party-registry/tests/unit/matching-persistence.test.ts +++ b/app/verticals/party-registry/tests/unit/matching-persistence.test.ts @@ -1,7 +1,7 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; /* 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 { DateTime, Effect, Schema, Predicate } from 'effect'; import assert from 'node:assert/strict'; import test from 'node:test'; import { createActionCollector } from '../../../../packages/core-runtime/src/actions/collector.ts'; @@ -196,7 +196,7 @@ const ambiguousHarness = (existingCase: boolean) => ]), ); -test('durable Party Match commits an ambiguity decision, complete case references, and original evidence without mutating a Party', () => +void 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); @@ -239,7 +239,7 @@ test('durable Party Match commits an ambiguity decision, complete case reference }), )); -test('an unchanged open ambiguity reuses its immutable evaluated case without rewriting candidate links', () => +void test('an unchanged open ambiguity reuses its immutable evaluated case without rewriting candidate links', () => runEffectTestPromise( Effect.gen(function* anUnchangedOpenAmbiguityReusesItsImmutable() { const subject = ambiguousHarness(true); @@ -256,7 +256,7 @@ test('an unchanged open ambiguity reuses its immutable evaluated case without re }), )); -test('PERSON IČO cannot acquire organization auto-match authority and NO_MATCH still records a decision', () => +void 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(); @@ -309,7 +309,7 @@ const resolutionInput = { tenantId, }; -test('an unarchive review cannot create a replacement Party or attach its facts through Candidate matching', () => +void 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']) { @@ -337,7 +337,7 @@ test('an unarchive review cannot create a replacement Party or attach its facts : yield* Effect.flip( resolveDuplicateCandidateMatch(subject.transaction, resolutionInput), ); - assert.equal(error._tag, 'DuplicateCandidateConflict'); + assert.ok(Predicate.isTagged(error, 'DuplicateCandidateConflict')); assert.match(error.reason, /unarchive/iu); assert.deepEqual(subject.inserts, []); assert.deepEqual(subject.updates, []); @@ -362,7 +362,7 @@ const actionScope = { tenantId, }; -test('Create Party matching an existing subject publishes each newly accepted identifier without fabricating Party Created', () => +void test('Create Party matching an existing subject publishes each newly accepted identifier without fabricating Party Created', () => runEffectTestPromise( Effect.gen(function* createPartyMatchingAnExistingSubjectPublishes() { const subject = harness( @@ -424,7 +424,7 @@ test('Create Party matching an existing subject publishes each newly accepted id }), )); -test('reviewed matching publishes the accepted identifier through its declared Action event and linked outbox', () => +void test('reviewed matching publishes the accepted identifier through its declared Action event and linked outbox', () => runEffectTestPromise( Effect.gen(function* reviewedMatchingPublishesTheAcceptedIdentifierThrough() { const subject = harness( @@ -477,7 +477,7 @@ test('reviewed matching publishes the accepted identifier through its declared A }), )); -test('matched Create reusing an existing identifier does not republish an acceptance event', () => +void test('matched Create reusing an existing identifier does not republish an acceptance event', () => runEffectTestPromise( Effect.gen(function* matchedCreateReusingAnExistingIdentifierDoes() { const subject = harness( @@ -537,7 +537,7 @@ test('matched Create reusing an existing identifier does not republish an accept }), )); -test('repeated Candidate facts accepted in one matching transaction publish one identifier event', () => +void test('repeated Candidate facts accepted in one matching transaction publish one identifier event', () => runEffectTestPromise( Effect.gen(function* repeatedCandidateFactsAcceptedInOneMatching() { const subject = harness( @@ -603,7 +603,7 @@ test('repeated Candidate facts accepted in one matching transaction publish one }), )); -test('reviewed matching with already-owned claims creates no duplicate identifier notifications', () => +void test('reviewed matching with already-owned claims creates no duplicate identifier notifications', () => runEffectTestPromise( Effect.gen(function* reviewedMatchingWithAlreadyOwnedClaimsCreates() { const subject = harness( @@ -641,7 +641,7 @@ test('reviewed matching with already-owned claims creates no duplicate identifie }), )); -test('reviewed matching locks and rejects an archived canonical target before any attachment or resolution', () => +void test('reviewed matching locks and rejects an archived canonical target before any attachment or resolution', () => runEffectTestPromise( Effect.gen(function* reviewedMatchingLocksAndRejectsAnArchived() { const subject = harness( @@ -666,7 +666,7 @@ test('reviewed matching locks and rejects an archived canonical target before an const failure = yield* Effect.flip( resolveDuplicateCandidateMatch(subject.transaction, resolutionInput), ); - assert.equal(failure._tag, 'DuplicateCandidateConflict'); + assert.ok(Predicate.isTagged(failure, 'DuplicateCandidateConflict')); assert.equal( subject.reads.some(({ table, locked }) => table === parties && locked), true, @@ -676,7 +676,7 @@ test('reviewed matching locks and rejects an archived canonical target before an }), )); -test('reviewed matching rejects a cross-tenant selected reference without resolving its identity', () => +void test('reviewed matching rejects a cross-tenant selected reference without resolving its identity', () => runEffectTestPromise( Effect.gen(function* reviewedMatchingRejectsACrossTenantSelected() { const subject = harness(); @@ -686,7 +686,7 @@ test('reviewed matching rejects a cross-tenant selected reference without resolv selectedPartyTenantId: '90000000-0000-4000-8000-000000000001', }), ); - assert.equal(failure._tag, 'DuplicateCandidateConflict'); + assert.ok(Predicate.isTagged(failure, 'DuplicateCandidateConflict')); assert.equal( subject.reads.length, 1, @@ -696,7 +696,7 @@ test('reviewed matching rejects a cross-tenant selected reference without resolv }), )); -test('reviewed matching rejects an absorbed target with the full-chain canonical survivor reference', () => +void test('reviewed matching rejects an absorbed target with the full-chain canonical survivor reference', () => runEffectTestPromise( Effect.gen(function* reviewedMatchingRejectsAnAbsorbedTargetWith() { const subject = harness( @@ -726,7 +726,7 @@ test('reviewed matching rejects an absorbed target with the full-chain canonical }), )); -test('future-effective evidence is rejected before a current decision or Party can be persisted', () => +void test('future-effective evidence is rejected before a current decision or Party can be persisted', () => runEffectTestPromise( Effect.gen(function* futureEffectiveEvidenceIsRejectedBeforeA() { const subject = harness(); @@ -737,12 +737,12 @@ test('future-effective evidence is rejected before a current decision or Party c tenantId, }), ); - assert.equal(failure._tag, 'PartyEvidenceInsufficient'); + assert.ok(Predicate.isTagged(failure, 'PartyEvidenceInsufficient')); assert.deepEqual(subject.inserts, []); }), )); -test('durable matching is an idempotent identity Action and the separate UX preview remains a governed read', () => { +void 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() }), @@ -752,7 +752,7 @@ test('durable matching is an idempotent identity Action and the separate UX prev assert.equal(partyMatchRead.descriptor.accessKind, 'detail'); }); -test('weak exact canonical evidence produces review rather than automatic identity or NO_MATCH', () => +void test('weak exact canonical evidence produces review rather than automatic identity or NO_MATCH', () => runEffectTestPromise( Effect.gen(function* weakExactCanonicalEvidenceProducesReviewRather() { const subject = harness( @@ -795,7 +795,7 @@ test('weak exact canonical evidence produces review rather than automatic identi }), )); -test('initial no-strong Create review captures relevant same-name canonical Parties in its immutable snapshot', () => +void test('initial no-strong Create review captures relevant same-name canonical Parties in its immutable snapshot', () => runEffectTestPromise( Effect.gen(function* initialNoStrongCreateReviewCapturesRelevant() { const subject = harness( @@ -830,7 +830,7 @@ test('initial no-strong Create review captures relevant same-name canonical Part }), )); -test('a new material evaluation creates a linked successor without rewriting the prior case', () => +void 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'; @@ -871,7 +871,7 @@ test('a new material evaluation creates a linked successor without rewriting the }), )); -test('explicit prior-case continuation rejects foreign or missing review references', () => +void test('explicit prior-case continuation rejects foreign or missing review references', () => runEffectTestPromise( Effect.forEach( [tenantId, '90000000-0000-4000-8000-000000000001'], @@ -887,14 +887,14 @@ test('explicit prior-case continuation rejects foreign or missing review referen tenantId, }), ); - assert.equal(failure._tag, 'PartyEvidenceInsufficient'); + assert.ok(Predicate.isTagged(failure, 'PartyEvidenceInsufficient')); assert.deepEqual(subject.inserts, []); }), { concurrency: 'unbounded', discard: true }, ), )); -test('equivalent Candidate property and evidence ordering has one deterministic fingerprint', () => { +void test('equivalent Candidate property and evidence ordering has one deterministic fingerprint', () => { const original = candidate({ displayName: 'Northwind', evidenceRefs: ['evidence:b', 'evidence:a'], @@ -911,7 +911,7 @@ test('equivalent Candidate property and evidence ordering has one deterministic assert.equal(candidateFingerprint(original), candidateFingerprint(reordered)); }); -test('insufficient typed evidence cannot persist a case or decision even with a verified identifier', () => +void 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) { @@ -925,13 +925,13 @@ test('insufficient typed evidence cannot persist a case or decision even with a const failure = yield* operation === 'CREATE' ? Effect.flip(createOrMatchParty(subject.transaction, input)) : Effect.flip(matchParty(subject.transaction, input)); - assert.equal(failure._tag, 'PartyEvidenceInsufficient'); + assert.ok(Predicate.isTagged(failure, 'PartyEvidenceInsufficient')); assert.equal(subject.inserts.length, 0); } }), )); -test('reviewer selection cannot waive missing subject/type evidence from a retained case', () => +void test('reviewer selection cannot waive missing subject/type evidence from a retained case', () => runEffectTestPromise( Effect.gen(function* denyUnevidencedReview() { const row = caseRow(); @@ -955,7 +955,7 @@ test('reviewer selection cannot waive missing subject/type evidence from a retai tenantId, }), ); - assert.equal(failure._tag, 'DuplicateCandidateConflict'); + assert.ok(Predicate.isTagged(failure, 'DuplicateCandidateConflict')); assert.equal(subject.inserts.length, 0); assert.equal(subject.updates.length, 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..bfd2837e8 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,7 @@ 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 { Effect, 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,7 +20,7 @@ const lookup = (overrides: Partial = {}): PartyAliasLookup => ...overrides, }); -test('central resolution service walks the complete canonical alias chain in one scoped transaction seam', () => { +void 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')); @@ -32,7 +32,7 @@ test('central resolution service walks the complete canonical alias chain in one }); }); -test('central resolution fails closed for cycles, cross-tenant targets, and broken chains', () => { +void test('central resolution fails closed for cycles, cross-tenant targets, and broken chains', () => { const cycle = makePartyAliasResolutionService( lookup({ findAlias: (_requestedTenantId, aliasPartyId) => @@ -46,7 +46,7 @@ test('central resolution fails closed for cycles, cross-tenant targets, and brok }), ); const cycleError = runEffectTestSync(Effect.flip(cycle.resolvePartyAlias(tenantId, 'party-a'))); - assert.equal(cycleError._tag, 'PartyAliasResolutionCycle'); + assert.ok(Predicate.isTagged(cycleError, 'PartyAliasResolutionCycle')); const crossTenant = makePartyAliasResolutionService( lookup({ @@ -63,7 +63,7 @@ test('central resolution fails closed for cycles, cross-tenant targets, and brok const crossTenantError = runEffectTestSync( Effect.flip(crossTenant.resolvePartyAlias(tenantId, 'party-b')), ); - assert.equal(crossTenantError._tag, 'PartyAliasResolutionCrossTenant'); + assert.ok(Predicate.isTagged(crossTenantError, 'PartyAliasResolutionCrossTenant')); const broken = makePartyAliasResolutionService( lookup({ @@ -72,16 +72,16 @@ test('central resolution fails closed for cycles, cross-tenant targets, and brok }), ); const brokenError = runEffectTestSync(Effect.flip(broken.resolvePartyAlias(tenantId, 'missing'))); - assert.equal(brokenError._tag, 'PartyAliasResolutionBrokenChain'); + assert.ok(Predicate.isTagged(brokenError, 'PartyAliasResolutionBrokenChain')); }); -test('central write guard returns typed canonical-survivor guidance and never forwards', () => { +void 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')), ); - assert.equal(rejection._tag, 'PartyAliasWriteRejected'); + assert.ok(Predicate.isTagged(rejection, 'PartyAliasWriteRejected')); assert.deepEqual(rejection.aliasPartyRef, { moduleId: 'party.registry', resourceId: 'party-b', 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..85f3f712b 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 { DateTime, Predicate } from 'effect'; import type { PartyRef } from '../../shared/resources/party.ts'; import { assertCanonicalWriteTarget, @@ -26,7 +26,7 @@ const alias = (aliasPartyId: string, survivorPartyId: string, tenant = tenantId) survivorPartyRef: party(survivorPartyId, tenant), }); -test('resolves an historical alias chain to one final canonical Party', () => { +void test('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'), @@ -40,30 +40,36 @@ test('resolves an historical alias chain to one final canonical Party', () => { }); }); -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', +void test('rejects alias cycles, self aliases, and cross-tenant targets', () => { + assert.ok( + Predicate.isTagged( + resolveCanonicalPartyRef(party('party-a'), [ + alias('party-a', 'party-b'), + alias('party-b', 'party-a'), + ]), + 'PartyAliasCycleRejected', + ), ); - assert.equal( - resolveCanonicalPartyRef(party('party-a'), [alias('party-a', 'party-a')])._tag, - 'PartyAliasSelfReferenceRejected', + assert.ok( + Predicate.isTagged( + resolveCanonicalPartyRef(party('party-a'), [alias('party-a', 'party-a')]), + 'PartyAliasSelfReferenceRejected', + ), ); - assert.equal( - resolveCanonicalPartyRef(party('party-a'), [ - { - ...alias('party-a', 'party-b'), - survivorPartyRef: party('party-b', '22222222-2222-4222-8222-222222222222'), - }, - ])._tag, - 'PartyAliasCrossTenantRejected', + assert.ok( + Predicate.isTagged( + resolveCanonicalPartyRef(party('party-a'), [ + { + ...alias('party-a', 'party-b'), + survivorPartyRef: party('party-b', '22222222-2222-4222-8222-222222222222'), + }, + ]), + 'PartyAliasCrossTenantRejected', + ), ); }); -test('rejects new writes addressed to an absorbed alias instead of forwarding them', () => { +void 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', aliasPartyRef: party('party-b'), 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..4a8e6a001 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,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { Match } from 'effect'; +import { Match, Predicate } from 'effect'; import type { PartyRef } from '../../shared/resources/party.ts'; import type { MergeSurvivorCandidate, @@ -51,7 +51,7 @@ const expectSelected = (result: CanonicalSurvivorSelection) => Match.exhaustive, ); -test('blocks survivor selection when authoritative identity truth is unresolved', () => { +void test('blocks survivor selection when authoritative identity truth is unresolved', () => { const result = selectCanonicalSurvivor( confirmedSelection([ candidate('party-a'), @@ -66,7 +66,7 @@ test('blocks survivor selection when authoritative identity truth is unresolved' }); }); -test('uses the governed hierarchy before reference count, lifecycle, completeness, or age', () => { +void test('uses the governed hierarchy before reference count, lifecycle, completeness, or age', () => { const result = selectCanonicalSurvivor( confirmedSelection([ candidate('well-established', { @@ -84,7 +84,7 @@ test('uses the governed hierarchy before reference count, lifecycle, completenes ]), ); - assert.equal(result._tag, 'CanonicalSurvivorSelected'); + assert.ok(Predicate.isTagged(result, 'CanonicalSurvivorSelected')); const selected = expectSelected(result); assert.deepEqual(selected.survivorPartyRef, party('authoritative')); assert.equal(selected.decidingCriterion, 'AUTHORITATIVE_EVIDENCE'); @@ -96,7 +96,7 @@ test('uses the governed hierarchy before reference count, lifecycle, completenes ); }); -test('uses reference stability, lifecycle, completeness, age, then resource identity deterministically', () => { +void test('uses reference stability, lifecycle, completeness, age, then resource identity deterministically', () => { const referenceWinner = selectCanonicalSurvivor( confirmedSelection([ candidate('a', { referenceStabilityRank: 1 }), @@ -113,7 +113,7 @@ test('uses reference stability, lifecycle, completeness, age, then resource iden assert.equal(selected.decidingCriterion, 'STABLE_RESOURCE_IDENTITY'); }); -test('rejects a cross-tenant merge set before selection', () => { +void test('rejects a cross-tenant merge set before selection', () => { const result = selectCanonicalSurvivor( confirmedSelection([ candidate('party-a'), @@ -133,35 +133,37 @@ test('rejects a cross-tenant merge set before selection', () => { }); }); -test('rejects selection without an explicit confirmed duplicate decision and matching evidence set', () => { +void test('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', 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', + assert.ok( + Predicate.isTagged( + selectCanonicalSurvivor({ + candidates, + confirmation: { + confirmedDuplicateDecisionId: ConfirmedDuplicateDecisionIdSchema.make('decision-1'), + confirmedPartyRefs: [party('party-a')], + decisionActorPrincipalId: DecisionActorPrincipalIdSchema.make('principal-1'), + evidenceRefs: ['evidence-1'], + }, + }), + 'SurvivorSelectionBlocked', + ), ); }); -test('retains immutable evaluated values and explains progressive elimination for three candidates', () => { +void test('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'); + assert.ok(Predicate.isTagged(result, 'CanonicalSurvivorSelected')); const selected = expectSelected(result); const authority = selected.evidenceChain.find( ({ criterion }) => criterion === 'AUTHORITATIVE_EVIDENCE', 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..3e6428060 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,7 +1,7 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; // @effect-diagnostics asyncFunction:off 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 { DateTime, Effect, Option, Schema, Predicate } from 'effect'; import assert from 'node:assert/strict'; import test from 'node:test'; import { PartyAliasWriteRejected } from '../../shared/domain/merge-alias-resolution.ts'; @@ -132,7 +132,7 @@ const transactionHarness = ( return { insertValues, transaction, updateSets }; }; -test('create persists an active assertion with unknown start and derives current state', async () => { +void test('create persists an active assertion with unknown start and derives current state', async () => { const created = relationshipRow(); const harness = transactionHarness( [ @@ -171,7 +171,7 @@ test('create persists an active assertion with unknown start and derives current assert.equal('isCurrent' in (harness.insertValues[0] ?? {}), false); }); -test('update refines an unknown historical validFrom through the persistence service', async () => { +void 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 }); @@ -201,7 +201,7 @@ test('update refines an unknown historical validFrom through the persistence ser assert.equal(harness.updateSets[0]?.['revision'], 2); }); -test('end keeps a future-ended relationship current and exposes bounded end history', async () => { +void 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') }); @@ -257,7 +257,7 @@ test('end keeps a future-ended relationship current and exposes bounded end hist assert.equal('isCurrent' in (harness.updateSets[0] ?? {}), false); }); -test('detail derives scheduled state and resolves stored endpoint aliases independently', async () => { +void 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') }); @@ -283,7 +283,7 @@ test('detail derives scheduled state and resolves stored endpoint aliases indepe assert.ok(Option.isNone(detail.to.requestedAlias)); }); -test('non-active assertions never read as current even with an open effective interval', async () => { +void test('non-active assertions never read as current even with an open effective interval', async () => { await Promise.all( ['RETRACTED', 'SUPERSEDED', 'DISPUTED'].map(async (assertionState) => { const harness = transactionHarness([ @@ -300,7 +300,7 @@ test('non-active assertions never read as current even with an open effective in ); }); -test('durable relationship update resolves alias-backed stored endpoints without rewriting them', async () => { +void 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, @@ -341,7 +341,7 @@ test('durable relationship update resolves alias-backed stored endpoints without assert.equal('fromPartyId' in (harness.updateSets[0] ?? {}), false); }); -test('create rejects an explicit alias endpoint with canonical survivor guidance', async () => { +void test('create rejects an explicit alias endpoint with canonical survivor guidance', async () => { const survivorId = '70000000-0000-4000-8000-000000000001'; const harness = transactionHarness([ [ @@ -373,14 +373,14 @@ test('create rejects an explicit alias endpoint with canonical survivor guidance ).pipe(Effect.flip), ); - assert.equal(rejection._tag, 'PartyAliasWriteRejected'); + assert.ok(Predicate.isTagged(rejection, '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 () => { +void 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, @@ -401,14 +401,14 @@ test('a known historical start cannot be rewritten by ordinary update', async () ).pipe(Effect.flip), ); - assert.equal(rejection._tag, 'PartyRelationshipCorrectionRequired'); + assert.ok(Predicate.isTagged(rejection, '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 () => { +void 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', @@ -448,7 +448,7 @@ test('removing a future planned end clears its current evidence and retains prio } }); -test('update can shorten a future planned end to a valid retrospective end with new evidence', async () => { +void 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') }); @@ -489,7 +489,7 @@ test('update can shorten a future planned end to a valid retrospective end with assert.equal(harness.updateSets[0]?.['endedByActionInvocationId'], actionInvocationId); }); -test('an evidence-backed end without a generic reason stays visible and retries exactly', async () => { +void 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', 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..2ef551659 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,7 @@ 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 { 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,7 +59,7 @@ const wrongResourceHit = Schema.decodeUnknownSync(CoreSearchProjectionHitSchema) title: 'Wrong', }); -test('Party adapter queries only the Core-owned Party projection and maps alias context', () => +void test('Party adapter queries only the Core-owned Party projection and maps alias context', () => runEffectTestPromise( Effect.gen(function* partyAdapterQuery() { const calls: unknown[] = []; @@ -93,7 +93,7 @@ test('Party adapter queries only the Core-owned Party projection and maps alias }), )); -test('Counterparty adapter uses trusted Legal Entity, effective time, role facet and safe periods', () => +void test('Counterparty adapter uses trusted Legal Entity, effective time, role facet and safe periods', () => runEffectTestPromise( Effect.gen(function* counterpartyAdapterQuery() { const calls: unknown[] = []; @@ -146,7 +146,7 @@ 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', () => +void test('Party adapter fails closed when a generic projection returns the wrong resource contract', () => runEffectTestPromise( Effect.gen(function* invalidProjectionContract() { const core: CoreSearchQueryRuntimeService = { @@ -160,6 +160,6 @@ test('Party adapter fails closed when a generic projection returns the wrong res tenantId, }), ); - assert.equal(failure._tag, 'Failure'); + assert.ok(Predicate.isTagged(failure, 'Failure')); }), )); 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 b1ddadb5a..e45a72000 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 { Effect, Exit, Match, Predicate } from 'effect'; import { makeCoreSearchQueryRuntime, makeCoreSearchIngestion, @@ -68,7 +68,7 @@ const snapshot: PartySearchSourceSnapshot = { removedRefs: [], tenantId, }; -test('post-commit projection makes only active permission-safe identity evidence searchable', () => +void test('post-commit projection makes only active permission-safe identity evidence searchable', () => runEffectTestPromise( Effect.gen(function* testScenario() { const documents = yield* buildPartySearchDocuments(snapshot); @@ -100,7 +100,7 @@ test('post-commit projection makes only active permission-safe identity evidence ]); }), )); -test('aliases collapse to canonical identity and only alias-only evidence labels the match', () => +void test('aliases collapse to canonical identity and only alias-only evidence labels the match', () => runEffectTestPromise( Effect.gen(function* testScenario() { const [party] = snapshot.parties; @@ -158,7 +158,7 @@ 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', () => +void test('snapshot-generation replay is idempotent, archive/unarchive refreshes and older delivery cannot resurrect a tombstone', () => runEffectTestPromise( Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); @@ -217,7 +217,7 @@ test('snapshot-generation replay is idempotent, archive/unarchive refreshes and assert.deepEqual(yield* query(true), []); }), )); -test('future-ended contact disappears at its period boundary without another lifecycle message', () => +void test('future-ended contact disappears at its period boundary without another lifecycle message', () => runEffectTestPromise( Effect.gen(function* testScenario() { const [party] = snapshot.parties; @@ -261,7 +261,7 @@ test('future-ended contact disappears at its period boundary without another lif assert.deepEqual(yield* query('2026-09-04T00:00:00.000Z'), []); }), )); -test('Counterparty identity survives aliases, current-role expiry and canonical-party collisions', () => +void test('Counterparty identity survives aliases, current-role expiry and canonical-party collisions', () => runEffectTestPromise( Effect.gen(function* testScenario() { const legalEntityId = '20000000-0000-4000-8000-000000000002'; @@ -346,7 +346,7 @@ test('Counterparty identity survives aliases, current-role expiry and canonical- ); }), )); -test('shared public contact returns multiple Parties without uniqueness or matching authority', () => +void test('shared public contact returns multiple Parties without uniqueness or matching authority', () => runEffectTestPromise( Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); @@ -384,7 +384,7 @@ test('shared public contact returns multiple Parties without uniqueness or match ); }), )); -test('rebuild reconciles omitted documents and preserves tombstones against stale lifecycle delivery', () => +void test('rebuild reconciles omitted documents and preserves tombstones against stale lifecycle delivery', () => runEffectTestPromise( Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); @@ -426,7 +426,7 @@ test('rebuild reconciles omitted documents and preserves tombstones against stal ); }), )); -test('source failure is sanitized and leaves previously searchable state intact for retry', () => +void test('source failure is sanitized and leaves previously searchable state intact for retry', () => runEffectTestPromise( Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); @@ -462,11 +462,11 @@ test('source failure is sanitized and leaves previously searchable state intact resourceType: 'party.registry.party', tenantId, }); - assert.equal(failure._tag, 'Failure'); + assert.ok(Predicate.isTagged(failure, 'Failure')); assert.equal(priorHits.length, 1); }), )); -test('zero-length cancelled periods are never searchable and do not poison projection delivery', () => +void test('zero-length cancelled periods are never searchable and do not poison projection delivery', () => runEffectTestPromise( Effect.gen(function* testScenario() { const [party] = snapshot.parties; @@ -518,7 +518,7 @@ test('zero-length cancelled periods are never searchable and do not poison proje assert.deepEqual(result.value[1]?.temporalFacets, []); }), )); -test('projection generation is independent of an out-of-order business event sequence', () => +void test('projection generation is independent of an out-of-order business event sequence', () => runEffectTestPromise( Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); @@ -552,7 +552,7 @@ test('projection generation is independent of an out-of-order business event seq assert.equal(hits.length, 1); }), )); -test('correction and identifier/contact changes replace obsolete evidence instead of accumulating history', () => +void test('correction and identifier/contact changes replace obsolete evidence instead of accumulating history', () => runEffectTestPromise( Effect.gen(function* testScenario() { const [party] = snapshot.parties; @@ -605,7 +605,7 @@ test('correction and identifier/contact changes replace obsolete evidence instea assert.equal(corrected.length, 1); }), )); -test('a complete empty rebuild also rejects delayed evidence for a never-before-indexed Party', () => +void test('a complete empty rebuild also rejects delayed evidence for a never-before-indexed Party', () => runEffectTestPromise( Effect.gen(function* testScenario() { const store = makeInMemoryCoreSearchProjectionStore(); 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..d739a1c9a 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,7 @@ 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 { 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,7 +28,7 @@ const request = { transport: { correlationId: 'search-rebuild-test', idempotencyKey: 'rebuild-1' }, }; -test('tenant rebuild requests require Party administration and canonical idempotency', () => { +void 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'); @@ -41,7 +41,7 @@ test('tenant rebuild requests require Party administration and canonical idempot ]); }); -test('authorized rebuild commits one linked request without reading identity or running the projector', () => { +void test('authorized rebuild commits one linked request without reading identity or running the projector', () => { const harness = makeActionTestHarness({ actionPermission: 'allowed', tenantPermission: 'allowed', @@ -79,7 +79,7 @@ test('authorized rebuild commits one linked request without reading identity or ); }); -test('denied Party administration cannot queue a rebuild even with Action execution permission', () => { +void test('denied Party administration cannot queue a rebuild even with Action execution permission', () => { const harness = makeActionTestHarness({ actionPermission: 'allowed', tenantPermission: 'denied', @@ -87,7 +87,7 @@ test('denied Party administration cannot queue a rebuild even with Action execut return runEffectTestPromise( Effect.gen(function* deniedRebuildRequest() { const error = yield* harness.runtime.runAction(request).pipe(Effect.flip); - assert.equal(error._tag, 'ActionPermissionDenied'); + assert.ok(Predicate.isTagged(error, 'ActionPermissionDenied')); const snapshot = harness.snapshot(); assert.deepEqual(snapshot.committed, []); assert.equal(snapshot.permissionDenials.length, 1); @@ -96,7 +96,7 @@ test('denied Party administration cannot queue a rebuild even with Action execut ); }); -test('replaying the same authorized rebuild request queues only once', () => { +void test('replaying the same authorized rebuild request queues only once', () => { const harness = makeActionTestHarness({ actionPermission: 'allowed', tenantPermission: 'allowed', @@ -105,7 +105,7 @@ test('replaying the same authorized rebuild request queues only once', () => { Effect.gen(function* replayRebuildRequest() { yield* harness.runtime.runAction(request); const replay = yield* harness.runtime.runAction(request).pipe(Effect.flip); - assert.equal(replay._tag, 'ActionAlreadyCommitted'); + assert.ok(Predicate.isTagged(replay, 'ActionAlreadyCommitted')); const snapshot = harness.snapshot(); assert.equal(snapshot.committed.length, 1); assert.equal(snapshot.committed[0]?.evidence.domainEvents.length, 1); @@ -128,7 +128,7 @@ const workerContext: OutboxWorkerHandlerContext = { workerKey: 'party.registry.rebuild-search', }; -test('rebuild worker uses its trusted committed context, and failures remain retryable', () => { +void 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', 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..868d646ee 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,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { Match } from 'effect'; +import { Match, Predicate } from 'effect'; import { normalizeCounterpartySearchHits, normalizePartySearchHits, @@ -40,7 +40,7 @@ const expectSearchResults = ( Match.exhaustive, ); -test('Party Search hides archived hits by default and explicitly labels included archived hits', () => { +void test('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' }, @@ -51,14 +51,14 @@ test('Party Search hides archived hits by default and explicitly labels included items: [{ archived: false, matchedViaAlias: false, ref: partyRef('active'), title: 'Active' }], }); const included = normalizePartySearchHits({ includeArchived: true, tenantId }, hits); - assert.equal(included._tag, 'SearchResults'); + assert.ok(Predicate.isTagged(included, 'SearchResults')); assert.deepEqual( expectSearchResults(included).items.map(({ archived }) => archived), [false, true], ); }); -test('Party aliases collapse to one survivor while shared contact queries may retain multiple Parties', () => { +void test('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,7 +71,7 @@ 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'); + assert.ok(Predicate.isTagged(result, 'SearchResults')); const { items } = expectSearchResults(result); assert.deepEqual( items.map(({ ref }) => ref.resourceId), @@ -80,23 +80,27 @@ test('Party aliases collapse to one survivor while shared contact queries may re assert.equal(items[0]?.matchedViaAlias, true); }); -test('Party Search fails closed when Core returns a cross-tenant or inconsistent projection', () => { +void test('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.ok( + Predicate.isTagged( + normalizePartySearchHits({ includeArchived: true, tenantId }, [ + { archived: false, canonicalPartyRef: wrongTenant, title: 'Wrong' }, + ]), + 'SearchProjectionViolation', + ), ); - assert.equal( - normalizePartySearchHits({ includeArchived: true, tenantId }, [ - { archived: false, canonicalPartyRef: partyRef('same'), title: 'One' }, - { archived: true, canonicalPartyRef: partyRef('same'), title: 'Two' }, - ])._tag, - 'SearchProjectionViolation', + assert.ok( + Predicate.isTagged( + normalizePartySearchHits({ includeArchived: true, tenantId }, [ + { archived: false, canonicalPartyRef: partyRef('same'), title: 'One' }, + { archived: true, canonicalPartyRef: partyRef('same'), title: 'Two' }, + ]), + 'SearchProjectionViolation', + ), ); }); @@ -113,7 +117,7 @@ const baseCounterpartyHit = ( rolePeriods, }); -test('Counterparty Search evaluates only current role periods at the exclusive time boundary', () => { +void test('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,7 +143,7 @@ test('Counterparty Search evaluates only current role periods at the exclusive t hits, ); - assert.equal(result._tag, 'SearchResults'); + assert.ok(Predicate.isTagged(result, 'SearchResults')); const { items } = expectSearchResults(result); assert.deepEqual( items.map(({ ref }) => ref.resourceId), @@ -148,7 +152,7 @@ test('Counterparty Search evaluates only current role periods at the exclusive t assert.deepEqual(items[1]?.currentRoles, ['CUSTOMER', 'SUPPLIER']); }); -test('Counterparty Search without a role retains durable Counterparties with no current role', () => { +void test('Counterparty Search without a role retains durable Counterparties with no current role', () => { const result = normalizeCounterpartySearchHits( { effectiveAt: '2026-09-03T12:00:00.000Z', @@ -159,11 +163,11 @@ test('Counterparty Search without a role retains durable Counterparties with no [baseCounterpartyHit('no-role', 'p1')], ); - assert.equal(result._tag, 'SearchResults'); + assert.ok(Predicate.isTagged(result, 'SearchResults')); assert.deepEqual(expectSearchResults(result).items[0]?.currentRoles, []); }); -test('Counterparty identity dedupes independently and survivor collisions are surfaced', () => { +void test('Counterparty identity dedupes independently and survivor collisions are surfaced', () => { const hits = [ baseCounterpartyHit('cp-1', 'survivor'), baseCounterpartyHit('cp-1', 'survivor'), @@ -179,7 +183,7 @@ test('Counterparty identity dedupes independently and survivor collisions are su hits, ); - assert.equal(result._tag, 'SearchResults'); + assert.ok(Predicate.isTagged(result, 'SearchResults')); const { items } = expectSearchResults(result); assert.deepEqual( items.map(({ ref }) => ref.resourceId), @@ -194,7 +198,7 @@ test('Counterparty identity dedupes independently and survivor collisions are su ); }); -test('Counterparty Search fails closed on the wrong Legal Entity instead of broadening scope', () => { +void test('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 +217,5 @@ test('Counterparty Search fails closed on the wrong Legal Entity instead of broa ], ); - assert.equal(result._tag, 'SearchProjectionViolation'); + assert.ok(Predicate.isTagged(result, 'SearchProjectionViolation')); }); 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..0ba7b9e72 100644 --- a/app/verticals/party-registry/tests/unit/search-source.test.ts +++ b/app/verticals/party-registry/tests/unit/search-source.test.ts @@ -7,7 +7,7 @@ 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 { DateTime, Effect, Result, Predicate } from 'effect'; import assert from 'node:assert/strict'; import test from 'node:test'; import { makePartySearchProjectionSource } from '../../src/services/party-search-projection-source.service.ts'; @@ -80,7 +80,7 @@ const harness = ( return { columns, filters, scopes, source: makePartySearchProjectionSource(snapshot) }; }; -test('canonical snapshot preserves alias identity and legal-entity Counterparty context', () => +void test('canonical snapshot preserves alias identity and legal-entity Counterparty context', () => runEffectTestPromise( Effect.gen(function* canonicalAliasSnapshot() { const { source } = harness({ @@ -144,7 +144,7 @@ test('canonical snapshot preserves alias identity and legal-entity Counterparty }), )); -test('source exposes only current public email and phone search evidence, never ADDRESS or raw contact fields', () => +void test('source exposes only current public email and phone search evidence, never ADDRESS or raw contact fields', () => runEffectTestPromise( Effect.gen(function* privateSearchEvidence() { const contact = { @@ -220,7 +220,7 @@ test('source exposes only current public email and phone search evidence, never }), )); -test('missing Party and Counterparty targets produce explicit versioned tombstone refs', () => +void test('missing Party and Counterparty targets produce explicit versioned tombstone refs', () => runEffectTestPromise( Effect.gen(function* missingTargetTombstones() { const { source } = harness({}); @@ -239,7 +239,7 @@ test('missing Party and Counterparty targets produce explicit versioned tombston }), )); -test('full rebuild reads each Core-enumerated legal entity in the same snapshot and keeps distinct Counterparties', () => +void test('full rebuild reads each Core-enumerated legal entity in the same snapshot and keeps distinct Counterparties', () => runEffectTestPromise( Effect.gen(function* rebuildSnapshot() { const secondLegalEntityId = '30000000-0000-4000-8000-000000000002'; @@ -270,7 +270,7 @@ test('full rebuild reads each Core-enumerated legal entity in the same snapshot }), )); -test('Counterparty-only refresh emits only its canonical family and selected Counterparty', () => +void test('Counterparty-only refresh emits only its canonical family and selected Counterparty', () => runEffectTestPromise( Effect.gen(function* targetedCounterpartySnapshot() { const otherId = '20000000-0000-4000-8000-000000000009'; @@ -301,7 +301,7 @@ test('Counterparty-only refresh emits only its canonical family and selected Cou }), )); -test('alias cycles and cross-tenant source rows fail closed with sanitized typed failures', () => +void test('alias cycles and cross-tenant source rows fail closed with sanitized typed failures', () => runEffectTestPromise( Effect.gen(function* rejectedSourceSnapshot() { for (const rows of [ @@ -319,7 +319,7 @@ test('alias cycles and cross-tenant source rows fail closed with sanitized typed const outcome = yield* source.load(context, { rebuild: true }).pipe(Effect.result); assert.ok(Result.isFailure(outcome)); if (Result.isFailure(outcome)) { - assert.equal(outcome.failure._tag, 'PartySearchProjectionUnavailable'); + assert.ok(Predicate.isTagged(outcome.failure, 'PartySearchProjectionUnavailable')); assert.doesNotMatch(outcome.failure.reason, /Secret name|foreign-tenant/u); } } From bb7f2918d5a9ecbfa5dddf4bd8d4778263e32c18 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Mon, 7 Sep 2026 23:13:39 +0200 Subject: [PATCH 02/38] Enforce native Effect interfaces and remove generator Promise bridges --- .../api/modules/installed-module-catalog.ts | 11 +- .../shell-super-app/tests/e2e/auth-fixture.ts | 378 +++++++++--------- .../shell-super-app/tests/e2e/login.spec.ts | 12 +- .../integration/generated-owner-fixture.ts | 217 +++++----- .../generated-owner-isolation.test.ts | 46 ++- .../module-catalog-runtime.test.ts | 12 +- .../support/impersonation-service-doubles.ts | 4 + .../unit/module-entrypoint-loader.test.ts | 86 ++-- .../EFFECT_V4_ANTIPATTERN_AUDIT.md | 5 + app/oxlint.config.ts | 30 +- .../search-worker-snapshot.test.ts | 112 +++--- .../tests/unit/search-worker-snapshot.test.ts | 24 +- .../audit-database-trust-boundaries.mts | 4 +- app/scripts/bootstrap-agent-skills.mts | 20 +- app/scripts/check-authorization-readiness.mts | 2 +- .../check-database-access-boundaries.mts | 15 +- app/scripts/check-ontos-module-contracts.mts | 18 +- .../database-trust-audit/collect-snapshot.mts | 4 +- app/scripts/database-trust-audit/report.mts | 11 +- .../generate-ontos-module-contract.mts | 36 +- app/scripts/initialize-local-development.mts | 16 +- .../migrate-contacts-authorization.mts | 15 +- app/scripts/prepare-dev-module-contract.mts | 30 +- app/scripts/proof-node-backend-federation.mts | 4 +- app/scripts/proof-workerd-ssr.mts | 1 + ...provision-current-action-authorization.mts | 20 +- app/scripts/scaffolding/cli.mts | 66 ++- .../external-http-adapter/scaffold.mts | 6 +- app/scripts/scaffolding/generator-adapter.mts | 59 +-- .../scaffold.mts | 6 +- .../microvertical-page/scaffold.mts | 61 ++- .../scaffolding/module-contract/scaffold.mts | 6 +- .../scaffolding/outbox-message/scaffold.mts | 4 +- .../scaffolding/outbox-worker/scaffold.mts | 4 +- app/scripts/scaffolding/policy/scaffold.mts | 15 +- .../retire-contribution/scaffold.mts | 4 +- .../search-provider-access/scaffold.mts | 6 +- app/scripts/scaffolding/shared.mts | 50 +-- .../tests/module-contract-generator.test.mts | 98 +++-- .../tests/resource-generator.test.mts | 98 ++++- .../tests/retire-contribution.test.mts | 23 +- .../tests/scaffold-generators.test.mts | 268 +++++++------ app/scripts/tests/api-only-tooling.test.mts | 15 +- .../tests/database-access-boundaries.test.mts | 7 +- .../initialize-local-development.test.mts | 18 +- .../module-entrypoint-boundaries.test.mts | 57 ++- .../tests/outbox-worker-delivery.test.mts | 14 +- .../tests/plan-deployment-impact.test.mts | 54 ++- ...sion-current-action-authorization.test.mts | 57 +-- .../tests/quality-audit-model.test.mts | 8 +- .../quality-audit-runtime-model.test.mts | 4 +- .../tests/quality-audit-test-support.mts | 49 +-- .../validate-ultramodern-workspace.mts | 1 + app/tools/oxlint/effect-native/README.md | 19 +- app/tools/oxlint/effect-native/index.ts | 2 + .../effect-native/repository-policy.config.ts | 22 + .../effect-native/rules/no-instanceof.ts | 23 ++ .../rules/no-manual-tag-comparison.ts | 78 +++- .../rules/no-promise-shaped-port.ts | 168 +++++++- .../fixtures/no-instanceof/.oxlintrc.json | 5 + .../invalid/scripts/every-constructor.mts | 9 + .../valid/scripts/native-guards.mts | 8 + .../packages/core-runtime/src/runtime.ts | 2 +- .../packages/core-runtime/src/tag-identity.ts | 3 + .../tests/unit/assertions.test.ts | 13 + .../packages/shared-contracts/src/switch.ts | 1 - .../core-runtime/src/native-assertions.ts | 8 + .../shared-contracts/src/edge-lookalikes.ts | 1 - .../src/indirect-lookalikes.ts | 1 - .../src/db/evasion-then-built-store.ts | 2 +- .../src/edge-callback-parameters.ts | 3 +- .../src/generic-operation-ports.ts | 12 + .../src/unused-recursive-operation.ts | 3 + .../core-runtime/tests/unit/store.test.ts | 2 +- .../invalid/scripts/overloaded-operation.mts | 5 + .../contacts/src/routes/edge-generic-jsx.tsx | 2 +- .../verticals/contacts/src/routes/page.tsx | 2 +- .../api/modules/fp-third-party-sdk-mirror.ts | 10 +- .../core-runtime/src/db/driver-edge.ts | 3 +- .../core-runtime/src/generic-effect-ports.ts | 9 + .../contacts/src/effect-native-service.ts | 4 +- .../tests/repository-policy.test.mts | 19 + .../scripts/prepare-contacts-migration.mts | 17 +- 83 files changed, 1473 insertions(+), 1174 deletions(-) create mode 100644 app/tools/oxlint/effect-native/repository-policy.config.ts create mode 100644 app/tools/oxlint/effect-native/rules/no-instanceof.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-instanceof/.oxlintrc.json create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-instanceof/invalid/scripts/every-constructor.mts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-instanceof/valid/scripts/native-guards.mts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/tag-identity.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/tests/unit/assertions.test.ts rename app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/{valid => invalid}/packages/shared-contracts/src/switch.ts (89%) create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/native-assertions.ts rename app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/{valid => invalid}/packages/core-runtime/src/edge-callback-parameters.ts (83%) create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/generic-operation-ports.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/unused-recursive-operation.ts rename app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/{valid => invalid}/packages/core-runtime/tests/unit/store.test.ts (70%) create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/scripts/overloaded-operation.mts rename app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/{valid => invalid}/verticals/contacts/src/routes/edge-generic-jsx.tsx (76%) rename app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/{valid => invalid}/verticals/contacts/src/routes/page.tsx (68%) create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/src/generic-effect-ports.ts create mode 100644 app/tools/oxlint/effect-native/tests/repository-policy.test.mts 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 68af80ca8..140c58667 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,7 @@ const readResponseChunks = ( > => Effect.tryPromise({ catch: unavailable, - try: invokePromiseWithoutSignal(reader.read.bind(reader)), + try: async () => await reader.read(), }).pipe( Effect.timeout(timeout), Effect.flatMap((next) => { @@ -199,10 +195,7 @@ 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), + (bodyReader) => Effect.promise(async () => await bodyReader.cancel()).pipe(Effect.ignore), ); return yield* decodeContractDocument(text).pipe(Effect.mapError((cause) => invalid(cause))); }, 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 b8fcb69d2..869398ecd 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,9 @@ -import { setTimeout as delay } from 'node:timers/promises'; +import { Effect } from 'effect'; +import { acquirePoolResource, makeAuthDatabase } from '../../api/auth/db/client.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,13 +15,7 @@ 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', @@ -44,190 +37,191 @@ export const e2eTenants = { }, } as const; -export const createAuthenticationFixture = async () => { - const { - baseUrl: baseURL, - connectionString, - secret, - } = await runEffectTestPromise(loadAuthConfig({ envPath: APP_ENV_PATH })); +export const createAuthenticationFixture = Effect.fn('createAuthenticationFixture')( + function* createAuthenticationFixtureEffect() { + const { + baseUrl: baseURL, + connectionString, + secret, + } = yield* loadAuthConfig({ envPath: APP_ENV_PATH }); - 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'], - }); + const corePool = yield* acquirePoolResource(() => new Pool({ connectionString })); + 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 = 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)); + const cleanup = Effect.fn('cleanupAuthenticationFixture')( + function* cleanupAuthenticationFixtureEffect() { + // 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, [ + e2eTenants.first.principalId, + e2eTenants.second.principalId, + ]), + ); + const existingUsers = yield* authDatabase + .select({ id: user.id }) + .from(user) + .where(eq(user.email, e2eCredentials.email)); - 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)); - }), - ); - // 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, - ]), - ); - await Promise.all( - existingUsers.map((existingUser) => - coreDatabase + 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, [ + e2eTenants.first.principalId, + e2eTenants.second.principalId, + ]), + ); + 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(eq(principalAuthBindings.principalId, e2eTenants.first.principalId)); + yield* coreDatabase + .delete(principalAuthBindings) + .where(eq(principalAuthBindings.principalId, e2eTenants.second.principalId)); + yield* coreDatabase + .delete(tenantModuleStates) + .where(eq(tenantModuleStates.tenantId, e2eTenants.first.tenantId)); + yield* coreDatabase + .delete(tenantModuleStates) + .where(eq(tenantModuleStates.tenantId, e2eTenants.second.tenantId)); + yield* coreDatabase + .delete(legalEntities) + .where(eq(legalEntities.tenantId, e2eTenants.first.tenantId)); + yield* coreDatabase + .delete(legalEntities) + .where(eq(legalEntities.tenantId, e2eTenants.second.tenantId)); + yield* coreDatabase + .delete(principals) + .where(eq(principals.principalId, e2eTenants.first.principalId)); + yield* coreDatabase + .delete(principals) + .where(eq(principals.principalId, e2eTenants.second.principalId)); + yield* coreDatabase.delete(tenants).where(eq(tenants.tenantId, e2eTenants.first.tenantId)); + yield* coreDatabase.delete(tenants).where(eq(tenants.tenantId, e2eTenants.second.tenantId)); + }, ); - await coreDatabase - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.principalId, e2eTenants.first.principalId)); - await coreDatabase - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.principalId, e2eTenants.second.principalId)); - await coreDatabase - .delete(tenantModuleStates) - .where(eq(tenantModuleStates.tenantId, e2eTenants.first.tenantId)); - await coreDatabase - .delete(tenantModuleStates) - .where(eq(tenantModuleStates.tenantId, e2eTenants.second.tenantId)); - await coreDatabase - .delete(legalEntities) - .where(eq(legalEntities.tenantId, e2eTenants.first.tenantId)); - await coreDatabase - .delete(legalEntities) - .where(eq(legalEntities.tenantId, e2eTenants.second.tenantId)); - await coreDatabase - .delete(principals) - .where(eq(principals.principalId, e2eTenants.first.principalId)); - await coreDatabase - .delete(principals) - .where(eq(principals.principalId, e2eTenants.second.principalId)); - await coreDatabase.delete(tenants).where(eq(tenants.tenantId, e2eTenants.first.tenantId)); - await coreDatabase.delete(tenants).where(eq(tenants.tenantId, e2eTenants.second.tenantId)); - }; - 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)); + yield* cleanup(); + 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-tenant', + status: 'active', + tenantId: e2eTenants.first.tenantId, + }, + { + defaultLocale: 'en', + name: e2eTenants.second.name, + slug: 'e2e-zeta-tenant', + 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: '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 }, + ]); + }, +); 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 81f191650..2ff6f6aaa 100644 --- a/app/apps/shell-super-app/tests/e2e/login.spec.ts +++ b/app/apps/shell-super-app/tests/e2e/login.spec.ts @@ -1,4 +1,5 @@ -import { Predicate } from 'effect'; +import { Exit, Predicate, Scope } from 'effect'; +import { runEffectTestPromise, runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; import { expect, test } from '@playwright/test'; import type { Page } from '@playwright/test'; import { shellAuthenticationApiContract } from '../../shared/api.ts'; @@ -37,16 +38,13 @@ const gotoHydratedLogin = async (page: Page, language: 'cs' | 'en') => { }); }; -let cleanupFixture: (() => Promise) | undefined; +const fixtureScope = runEffectTestSync(Scope.make()); test.beforeAll( async () => - await createAuthenticationFixture().then((cleanup) => { - cleanupFixture = cleanup; - }), + await runEffectTestPromise(createAuthenticationFixture().pipe(Scope.provide(fixtureScope))), ); - -test.afterAll(async () => await cleanupFixture?.()); +test.afterAll(async () => await runEffectTestPromise(Scope.close(fixtureScope, Exit.void))); test('renders the exact anonymous English and Czech home states', async ({ page }) => await page 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 408e568a8..192ce3252 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,12 +75,12 @@ const createWorkspace = async (root: string): Promise => { version: '0.0.0', }), ); - 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({ @@ -96,48 +97,51 @@ const createWorkspace = async (root: string): Promise => { ], }), ); -}; +}); -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, '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, '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} @@ -177,7 +181,7 @@ const addResourceType = async (root: string): Promise => { }, ],`, ); - await writeFile( + yield* fileSystem.writeFileString( manifestPath, replaceRequired( withResourceDetail, @@ -198,18 +202,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, @@ -221,8 +225,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. @@ -278,7 +282,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'; @@ -329,7 +333,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'; @@ -384,7 +388,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'; @@ -486,9 +490,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()) })', @@ -497,7 +504,7 @@ const adaptGeneratedOwner = async (root: string, schemaName: string): Promise Promise; readonly root: string; readonly verticalRoot: string; } -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', @@ -562,7 +573,7 @@ export const createGeneratedOwnerFixture = async ( ], { workspaceRoot: root }, ); - await runScaffold( + yield* runScaffoldEffect( 'module-api', [ '--vertical', @@ -576,7 +587,7 @@ export const createGeneratedOwnerFixture = async ( ], { workspaceRoot: root }, ); - await runScaffold( + yield* runScaffoldEffect( 'module-api', [ '--vertical', @@ -590,7 +601,7 @@ export const createGeneratedOwnerFixture = async ( ], { workspaceRoot: root }, ); - await runScaffold( + yield* runScaffoldEffect( 'search-provider', [ '--vertical', @@ -606,15 +617,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 dde5f2983..995d827f0 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,3 +1,4 @@ +import { NodeServices } from '@effect/platform-node'; import { makeFaultInjectableCoreDatabase, TestQueryHook, @@ -138,10 +139,7 @@ const testGatewayAssertionRedemption: GatewayAssertionRedemption = { consume: () => Effect.void, }; -interface OwnerHttpHandler { - readonly dispose: () => Promise; - readonly handler: (request: Request) => Promise; -} +type OwnerHttpHandler = ReturnType['createHandler']>; const OwnerDetailSchema = Schema.Struct({ fields: Schema.Array(Schema.Struct({ label: Schema.String, value: Schema.String })), @@ -509,14 +507,19 @@ const principal = ( tenantId, }); -test('Codesmith composes the disposable owner Action and receiving read BFFs', async () => { - const fixture = await createGeneratedOwnerFixture( - `generated_owner_${randomUUID().replaceAll('-', '')}`, +void test('Codesmith composes the disposable owner Action and receiving read BFFs', async () => { + const fixture = await runEffectTestPromise( + createGeneratedOwnerFixture(`generated_owner_${randomUUID().replaceAll('-', '')}`).pipe( + Effect.provide(NodeServices.layer), + NativeScope.provide(nativeDatabaseScope), + ), + ); + const contract = await runEffectTestPromise( + deriveOntosModuleDeploymentContract({ + vertical: GENERATED_OWNER.slug, + workspaceRoot: fixture.root, + }).pipe(Effect.provide(NodeServices.layer)), ); - 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')), }; @@ -543,11 +546,10 @@ test('Codesmith composes the disposable owner Action and receiving read BFFs', a 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 () => { +void 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(); @@ -574,11 +576,18 @@ test('generated owner enforces tenant and legal-entity isolation through Shell, NativeScope.provide(nativeDatabaseScope), ), ); - const fixture = await createGeneratedOwnerFixture(schemaName); - const contract = await deriveOntosModuleDeploymentContract({ - vertical: GENERATED_OWNER.slug, - workspaceRoot: fixture.root, - }); + const fixture = await runEffectTestPromise( + createGeneratedOwnerFixture(schemaName).pipe( + Effect.provide(NodeServices.layer), + NativeScope.provide(nativeDatabaseScope), + ), + ); + const contract = await runEffectTestPromise( + deriveOntosModuleDeploymentContract({ + vertical: GENERATED_OWNER.slug, + workspaceRoot: fixture.root, + }).pipe(Effect.provide(NodeServices.layer)), + ); const capturedLogs: string[] = []; const loggerLayer = capturedLoggerLayer(capturedLogs); const testSpiceDb = await effectRuntime.runPromise(TestSpiceDbConfig); @@ -1490,7 +1499,6 @@ test('generated owner enforces tenant and legal-entity isolation through Shell, ); await runtimePool.end(); await admin.end(); - await fixture.dispose(); await effectRuntime.dispose(); } }); 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..e0b100180 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,4 +1,7 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; +import { + makeEffectTestCallback, + runEffectTestPromise, +} from '@app/core-runtime/testing/effect-runtime'; import assert from 'node:assert/strict'; import test from 'node:test'; import { @@ -193,11 +196,6 @@ const propertySafeRuntime = extractVerticalRuntimeSafeDescriptors(propertyRuntim 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, @@ -223,7 +221,7 @@ const makeContractFetch = ( void test( 'keeps discovered metadata separate from one complete owner-local runtime', - runEffectTest( + makeEffectTestCallback( 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'; 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 59544336b..1c15b04c1 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 @@ -8,6 +8,7 @@ import type { AuthenticationServiceContract } from '../../api/auth/service.ts'; const unconfiguredEffect = (operation: string) => Effect.die(`${operation} is not configured in this test`); +// oxlint-disable-next-line effect-native/no-promise-shaped-port -- Rejection belongs to the Better Auth SDK fixture API. const unconfiguredPromise = async (operation: string) => { throw new Error(`${operation} is not configured in this test`); }; @@ -25,8 +26,11 @@ const authenticationDefaults: AuthenticationServiceContract = { }; const providerDefaults: SupportAuthProvider['api'] = { + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. getSession: async () => await unconfiguredPromise('getSession'), + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. impersonateUser: async () => await unconfiguredPromise('impersonateUser'), + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. stopImpersonating: async () => await unconfiguredPromise('stopImpersonating'), }; 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..006edb262 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 @@ -117,9 +117,7 @@ const component = defineTenantModuleEntrypoint({ const compatibleRemoteModule = (value: { readonly default: unknown }) => Predicate.isFunction(value.default); -type EffectTestCallback = () => Promise; - -const runEffectTest = (effect: Effect.Effect): EffectTestCallback => +const runEffectTest = (effect: Effect.Effect) => Fn.flow(Fn.constant(effect), runEffectTestPromise); test( @@ -500,44 +498,52 @@ test( ), ); -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([ - { - identity: 'late-rejection/page', - isCompatible: compatibleRemoteModule, - load: async () => { - try { - return await pending.promise; - } finally { - loadSettled.resolve(null); - } +test( + 'does not surface a late remote rejection after a timeout', + runEffectTest( + 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: async () => { + loadStarted.resolve(null); + try { + return await pending.promise; + } finally { + loadSettled.resolve(null); + } + }, + timeoutMs: 10, + }, + ]), + ); + yield* Effect.promise(async () => await loadStarted.promise); + yield* TestClock.adjust('10 millis'); + const result = yield* Fiber.join(resultFiber); + expect(result).toEqual([ + { + identity: 'late-rejection/page', + reason: 'timeout', + state: 'unavailable', }, - timeoutMs: 10, - }, - ]), - ); - - 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(async () => await 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)( 'never invokes a remote loader after a %s target resolution', 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/oxlint.config.ts b/app/oxlint.config.ts index ad4a4108a..8a6e38f1c 100644 --- a/app/oxlint.config.ts +++ b/app/oxlint.config.ts @@ -183,7 +183,10 @@ export default defineConfig({ { // 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', }, @@ -240,28 +243,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: [ @@ -760,7 +741,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', @@ -803,7 +783,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/packages/core-runtime/tests/integration/search-worker-snapshot.test.ts b/app/packages/core-runtime/tests/integration/search-worker-snapshot.test.ts index 12052114c..b709f6b47 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 @@ -14,7 +14,7 @@ import { } from 'effect'; import assert from 'node:assert/strict'; import test, { after as afterNativeDatabase } from 'node:test'; -import type { PoolClient, QueryResult, QueryResultRow } from 'pg'; +import type { PoolClient } from 'pg'; import { Pool } from 'pg'; import { loadDatabaseConnectionPair } from '../../src/db/config.ts'; import { coreRelations, domainEvents } from '../../src/db/schema.ts'; @@ -32,21 +32,11 @@ 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 .select({ @@ -76,10 +66,10 @@ const readSnapshotPosition = ( ); const beginTransaction = (client: PoolClient) => - databaseEffect(() => queryPromise(client, 'begin')); + Effect.tryPromise(async () => await client.query('begin')); const commitTransaction = (client: PoolClient) => - databaseEffect(() => queryPromise(client, 'commit')); + Effect.tryPromise(async () => await client.query('commit')); const insertPendingEvent = ( client: PoolClient, @@ -87,12 +77,12 @@ 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( + async () => + await 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() { @@ -118,52 +108,58 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { 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 result = yield* Effect.tryPromise( + async () => + await 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] = result.rows; assert.ok(row); 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* Effect.tryPromise( + async () => + await admin.query('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* Effect.tryPromise( + async () => + await admin.query('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* Effect.tryPromise( + async () => + await admin.query('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( + async () => await admin.query('delete from core.tenants where tenant_id = $1', [tenantId]), ); yield* Effect.all( - [databaseEffect(() => endPromise(admin)), databaseEffect(() => endPromise(runtimePool))], + [ + Effect.tryPromise(async () => await admin.end()), + Effect.tryPromise(async () => await 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* Effect.tryPromise( + async () => + await admin.query( + `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.tryPromise( + async () => + await 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( @@ -241,14 +237,14 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { (blocked) => blocked ? Effect.succeed(true) - : databaseEffect(() => queryPromise(admin, 'select pg_sleep(0.01)')).pipe( + : Effect.tryPromise(async () => await 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( + async () => + await 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), @@ -288,10 +284,10 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { }); }); yield* Effect.acquireUseRelease( - databaseEffect(() => connectPromise(admin)), + Effect.tryPromise(async () => await admin.connect()), lateCommitSnapshot, (pending) => - databaseEffect(() => queryPromise(pending, 'rollback')).pipe( + Effect.tryPromise(async () => await pending.query('rollback')).pipe( Effect.orDie, Effect.ensuring(Effect.sync(() => pending.release())), ), @@ -299,7 +295,7 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { }).pipe(Effect.ensuring(cleanup)); }); -test('worker projection uses independent generations and one repeatable snapshot across tenant and Legal Entity scopes', (_context, done) => { +void 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)), 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..39eeeae8f 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,7 @@ 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 { Effect, Schema, Predicate } from 'effect'; import { attestOutboxWorkerHandlerContext } from '../../src/outbox/definition.ts'; import { CoreSearchProjectionUnavailable } from '../../src/search/projection.ts'; import { @@ -91,14 +91,10 @@ effectTest( (candidate) => Effect.flip(snapshot.read(candidate, () => Effect.succeed('unreachable'))), { concurrency: 'unbounded' }, ); - assert.deepEqual( - failures.map(({ _tag }) => _tag), - [ - 'CoreSearchProjectionInvalid', - 'CoreSearchProjectionInvalid', - 'CoreSearchProjectionInvalid', - ], - ); + assert.equal(failures.length, 3); + for (const failure of failures) { + assert.ok(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')); + } assert.equal(calls, 0); }); }, @@ -202,7 +198,7 @@ effectTest( view.forLegalEntity('20000000-0000-4000-8000-000000000002', () => Effect.succeed('no')), ), ); - assert.equal(invalidScope._tag, 'CoreSearchProjectionInvalid'); + assert.ok(Predicate.isTagged(invalidScope, 'CoreSearchProjectionInvalid')); assert.deepEqual(installedScopes, []); const failure = yield* Effect.flip( snapshot.read(verified, (view) => @@ -230,7 +226,7 @@ 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.ok(Predicate.isTagged(failure, 'CoreSearchProjectionUnavailable')); assert.doesNotMatch(failure.reason, /private database/u); }); }); @@ -292,7 +288,7 @@ 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'); + assert.ok(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')); }); }); @@ -310,8 +306,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'); + assert.ok(Predicate.isTagged(first, 'CoreSearchProjectionInvalid')); + assert.ok(Predicate.isTagged(second, 'CoreSearchProjectionInvalid')); }), ), ); 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/bootstrap-agent-skills.mts b/app/scripts/bootstrap-agent-skills.mts index e68e13beb..7bd23e912 100644 --- a/app/scripts/bootstrap-agent-skills.mts +++ b/app/scripts/bootstrap-agent-skills.mts @@ -1,6 +1,17 @@ #!/usr/bin/env node import { NodeServices } from '@effect/platform-node'; -import { Config, ConfigProvider, Console, Effect, Exit, Option, Path, Schema, Stdio } from 'effect'; +import { + Config, + ConfigProvider, + Console, + Effect, + Exit, + Option, + Path, + Schema, + Stdio, + Predicate, +} from 'effect'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; class AgentSkillsBootstrapError extends Schema.TaggedError()( @@ -63,10 +74,9 @@ const program = Effect.gen(function* bootstrapAgentSkills() { return Effect.succeed(1); } const launchCause = error.reason.cause; - const causeMessage = - launchCause instanceof Error - ? launchCause.message.replace(/^spawn /u, 'spawnSync ') - : error.message; + const causeMessage = Predicate.isError(launchCause) + ? launchCause.message.replace(/^spawn /u, 'spawnSync ') + : error.message; return Effect.fail( failure( `Failed to launch ${launch.target} for UltraModern command "${ultramodernArgs diff --git a/app/scripts/check-authorization-readiness.mts b/app/scripts/check-authorization-readiness.mts index a9dc0b687..355b80722 100644 --- a/app/scripts/check-authorization-readiness.mts +++ b/app/scripts/check-authorization-readiness.mts @@ -577,7 +577,7 @@ const authorizationReadinessCommand = Command.make( const nowEpochMs = yield* Clock.currentTimeMillis; const evidence = yield* Effect.try({ catch: (error) => - error instanceof AuthorizationReadinessError + Schema.is(AuthorizationReadinessError)(error) ? error : new AuthorizationReadinessError({ reason: 'authorization evidence is invalid' }), try: () => diff --git a/app/scripts/check-database-access-boundaries.mts b/app/scripts/check-database-access-boundaries.mts index f357b4e8f..d744be2e3 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, @@ -373,7 +372,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; @@ -419,15 +418,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 }, @@ -435,7 +425,7 @@ class DatabaseAccessBoundaryCheckFailed extends Schema.TaggedError 0) { yield* Effect.all( violations.map((violation) => @@ -452,6 +442,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 0cd3ce1f0..f5653c5bb 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, @@ -366,14 +366,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`, @@ -440,6 +438,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 eb6dd37f0..ffe480671 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'; @@ -214,7 +215,7 @@ export const getDatabaseTrustBoundaryFailureMessage = ( cause: Cause.Cause, ): string => { const failure = Cause.findErrorOption(cause); - return Option.isSome(failure) && failure.value instanceof DatabaseTrustBoundaryAuditError + return Option.isSome(failure) && Schema.is(DatabaseTrustBoundaryAuditError)(failure.value) ? failure.value.reason : genericAuditFailureMessage; }; diff --git a/app/scripts/generate-ontos-module-contract.mts b/app/scripts/generate-ontos-module-contract.mts index 164955305..1d1a44408 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 }); @@ -464,7 +452,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( @@ -478,14 +466,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; @@ -497,7 +479,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)), ); @@ -550,20 +532,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})`), ), @@ -574,6 +549,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 297332c84..7374f72fe 100644 --- a/app/scripts/initialize-local-development.mts +++ b/app/scripts/initialize-local-development.mts @@ -1,7 +1,7 @@ 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'; @@ -326,14 +326,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); @@ -816,7 +816,7 @@ export const initializeLocalDevelopment = ( ): Effect.Effect< LocalDevelopmentInitializationResult, LocalDevelopmentInitializationError, - FileSystem.FileSystem | Path.Path + NodeServices.NodeServices > => Effect.gen(function* initialize() { const configuration = @@ -893,9 +893,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/migrate-contacts-authorization.mts b/app/scripts/migrate-contacts-authorization.mts index a366f4e69..94fdf6ced 100644 --- a/app/scripts/migrate-contacts-authorization.mts +++ b/app/scripts/migrate-contacts-authorization.mts @@ -6,7 +6,6 @@ import { Console, Effect, Exit, - flow, ManagedRuntime, Number as EffectNumber, Redacted, @@ -492,7 +491,7 @@ const acquireSpiceDbClient = (configuration: SpiceDbConfigValue) => (client) => Effect.sync(() => client.close()), ); -const migrateContactsAuthorizationEffect = ( +export const migrateContactsAuthorization = ( mode: ContactsAuthorizationMigrationMode, ): Effect.Effect => Effect.gen(function* migrateContactsAuthorizationProgram() { @@ -521,21 +520,12 @@ const migrateContactsAuthorizationEffect = ( }; }).pipe(Effect.scoped); -const migrationRuntime = ManagedRuntime.make(NodeServices.layer); - -export const migrateContactsAuthorization: ( - mode: ContactsAuthorizationMigrationMode, -) => Promise = flow( - migrateContactsAuthorizationEffect, - migrationRuntime.runPromise, -); - 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( @@ -546,6 +536,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 a665fecc3..7931a9277 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 }); @@ -60,7 +60,7 @@ const prepareDevModuleContractCommand = Command.make( ); }).pipe( Effect.mapError((cause) => - cause instanceof ModuleContractPreparationError + Schema.is(ModuleContractPreparationError)(cause) ? cause : new ModuleContractPreparationError({ cause, @@ -88,7 +88,7 @@ const { NodeServices } = Result.getOrThrow( const exit = await Effect.runPromiseExit( Command.run(prepareDevModuleContractCommand, { version: '1.0.0' }).pipe( Effect.tapError((failure) => - failure instanceof ModuleContractPreparationError + Schema.is(ModuleContractPreparationError)(failure) ? Console.error(failure.message) : Effect.void, ), diff --git a/app/scripts/proof-node-backend-federation.mts b/app/scripts/proof-node-backend-federation.mts index 47bc3ca28..5e9bb3dac 100644 --- a/app/scripts/proof-node-backend-federation.mts +++ b/app/scripts/proof-node-backend-federation.mts @@ -1,6 +1,6 @@ #!/usr/bin/env node import { NodeServices } from '@effect/platform-node'; -import { Config, Console, Effect, Exit, Option, Path, Schema, Stdio } from 'effect'; +import { Config, Console, Effect, Exit, Option, Path, Schema, Stdio, Predicate } from 'effect'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; class BackendFederationProofLaunchError extends Schema.TaggedError()( @@ -54,7 +54,7 @@ const program = Effect.gen(function* backendFederationProofProgram() { return Effect.succeed(1); } const launchCause = cause.reason.cause; - const causeMessage = launchCause instanceof Error ? launchCause.message : cause.message; + const causeMessage = Predicate.isError(launchCause) ? launchCause.message : cause.message; return Effect.fail( new BackendFederationProofLaunchError({ cause, diff --git a/app/scripts/proof-workerd-ssr.mts b/app/scripts/proof-workerd-ssr.mts index d303fd9b8..9bea3fa9a 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 cc749e277..984c5c2e0 100644 --- a/app/scripts/provision-current-action-authorization.mts +++ b/app/scripts/provision-current-action-authorization.mts @@ -163,7 +163,7 @@ const discoverCurrentActionsEffect = ( ): Effect.Effect< readonly ActionAuthorizationProvisioningAction[], ActionAuthorizationProvisioningError, - FileSystem.FileSystem | Path.Path + NodeServices.NodeServices > => Effect.gen(function* discoverCurrentActionsEffectGenerator() { const path = yield* Path.Path; @@ -192,10 +192,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 verticalActions: ActionAuthorizationProvisioningAction[] = []; @@ -240,7 +240,7 @@ const discoverCurrentActionsProgram = ( ): Effect.Effect< readonly ActionAuthorizationProvisioningAction[], ActionAuthorizationProvisioningError, - FileSystem.FileSystem | Path.Path + NodeServices.NodeServices > => discoverCurrentActionsEffect(workspaceRoot, deriveContract); export const discoverCurrentActions = flow( @@ -254,7 +254,7 @@ const discoverCurrentActionKeysProgram = ( ): Effect.Effect< readonly string[], ActionAuthorizationProvisioningError, - FileSystem.FileSystem | Path.Path + NodeServices.NodeServices > => discoverCurrentActionsEffect(workspaceRoot, deriveContract).pipe( Effect.map((actions) => actions.map(({ actionKey }) => actionKey)), @@ -329,7 +329,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) { @@ -368,7 +368,7 @@ export function runCurrentActionAuthorizationProvisioning( ): Effect.Effect< ActionAuthorizationProvisioningResult & { readonly environment: 'development' | 'stage' }, ActionAuthorizationProvisioningError, - FileSystem.FileSystem | Path.Path + NodeServices.NodeServices >; export function runCurrentActionAuthorizationProvisioning( workspaceRoot: string, @@ -376,7 +376,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/scaffolding/cli.mts b/app/scripts/scaffolding/cli.mts index 4d4b99fc5..4eca2bb44 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; }, ); @@ -476,14 +487,7 @@ Options: if (options.routeRefresh === undefined) { return defaultRouteRefresh(input); } - return Effect.tryPromise({ - catch: (cause) => - new ScaffoldingError({ - cause, - message: cause instanceof Error ? 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 }); @@ -923,11 +927,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' }; @@ -942,18 +946,6 @@ const runScaffoldEffect = Effect.fn('runScaffold')(function* runScaffoldEffectGe return { kind: 'generated', result }; }); -const makeScaffoldProgram = ( - command: ScaffoldCommand, - rawArguments: readonly string[], - options: RunScaffoldOptions = {}, -) => runScaffoldEffect(command, rawArguments, options); - -export const runScaffold: ( - command: ScaffoldCommand, - rawArguments: readonly string[], - options?: RunScaffoldOptions, -) => Promise = flow(makeScaffoldProgram, scaffoldingRuntime.runPromise); - const optionalTextFlag = (name: string) => Flag.string(name).pipe(Flag.optional); const forwardedArguments = Argument.variadic(Argument.string('forwarded flags')); const cliFlags = { @@ -1213,6 +1205,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/external-http-adapter/scaffold.mts b/app/scripts/scaffolding/external-http-adapter/scaffold.mts index 3a692fb55..a646d2c6c 100644 --- a/app/scripts/scaffolding/external-http-adapter/scaffold.mts +++ b/app/scripts/scaffolding/external-http-adapter/scaffold.mts @@ -1,5 +1,5 @@ import { createCodesmithGenerator } from '../generator-adapter.mts'; -import { Effect } from 'effect'; +import { Effect, Predicate } from 'effect'; import { createMutationEffect, discoverOntosModuleEffect, @@ -20,8 +20,8 @@ import type { const preserveFileSystemCause = (failure: ScaffoldFailure): ScaffoldFailure => { const { cause } = failure; - const underlying = cause instanceof Error ? cause.cause : undefined; - return underlying instanceof Error + const underlying = Predicate.isError(cause) ? cause.cause : undefined; + return Predicate.isError(underlying) ? scaffoldFailure(`${failure.message}: ${underlying.message}`, cause) : failure; }; 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-action-boundary/scaffold.mts b/app/scripts/scaffolding/microvertical-action-boundary/scaffold.mts index 7e93e94f1..d0c7b941f 100644 --- a/app/scripts/scaffolding/microvertical-action-boundary/scaffold.mts +++ b/app/scripts/scaffolding/microvertical-action-boundary/scaffold.mts @@ -1,4 +1,4 @@ -import { Array as EffectArray, Effect, FileSystem, Option, Schema } from 'effect'; +import { Array as EffectArray, Effect, FileSystem, Option, Schema, Predicate } from 'effect'; import { createMutationEffect, discoverOntosModuleEffect, @@ -33,10 +33,10 @@ const scaffoldError = (message: string, cause?: unknown): ActionBoundaryScaffold const trySync = (operation: () => Value) => Effect.try({ catch: (cause) => - cause instanceof ActionBoundaryScaffoldError + Schema.is(ActionBoundaryScaffoldError)(cause) ? cause : scaffoldError( - cause instanceof Error ? cause.message : 'action boundary update failed', + Predicate.isError(cause) ? cause.message : 'action boundary update failed', cause, ), try: operation, diff --git a/app/scripts/scaffolding/microvertical-page/scaffold.mts b/app/scripts/scaffolding/microvertical-page/scaffold.mts index 212481a8a..020c7f068 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, @@ -227,7 +214,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) { @@ -475,7 +462,7 @@ const renderReadAuthorization = ( const validateReadAuthorization = ( config: Pick, -): Effect.Effect => +): Effect.Effect => Effect.gen(function* validateReadAuthorizationEffect() { if ( config.authorization === 'context_permission' && @@ -790,7 +777,7 @@ const patchLocale = ( vertical: PageVerticalMetadata, locale: string, page: string, -): Effect.Effect => +): Effect.Effect => Effect.gen(function* patchLocaleEffect() { const localePath = resolveContainedPath( workspaceRoot, @@ -835,7 +822,7 @@ const migrateLegacyLocale = ( vertical: PageVerticalMetadata, locale: string, page: string, -): Effect.Effect => +): Effect.Effect => Effect.gen(function* migrateLegacyLocaleEffect() { const localePath = resolveContainedPath( workspaceRoot, @@ -1193,7 +1180,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/module-contract/scaffold.mts b/app/scripts/scaffolding/module-contract/scaffold.mts index c21f68460..6f119ed00 100644 --- a/app/scripts/scaffolding/module-contract/scaffold.mts +++ b/app/scripts/scaffolding/module-contract/scaffold.mts @@ -1,4 +1,4 @@ -import { Array as EffectArray, Effect, FileSystem, Option, Schema } from 'effect'; +import { Array as EffectArray, Effect, FileSystem, Option, Schema, Predicate } from 'effect'; import { createCodesmithGenerator } from '../generator-adapter.mts'; import { MODULE_CONTRACT_GENERATOR_HEADER, @@ -89,10 +89,10 @@ const scaffoldError = (message: string, cause?: unknown): ModuleContractScaffold const trySync = (operation: () => Value) => Effect.try({ catch: (cause) => - cause instanceof ModuleContractScaffoldError + Schema.is(ModuleContractScaffoldError)(cause) ? cause : scaffoldError( - cause instanceof Error ? cause.message : 'module contract update failed', + Predicate.isError(cause) ? cause.message : 'module contract update failed', cause, ), try: operation, diff --git a/app/scripts/scaffolding/outbox-message/scaffold.mts b/app/scripts/scaffolding/outbox-message/scaffold.mts index 3d8424b28..58c37fa96 100644 --- a/app/scripts/scaffolding/outbox-message/scaffold.mts +++ b/app/scripts/scaffolding/outbox-message/scaffold.mts @@ -1,4 +1,4 @@ -import { Cause, Effect, FileSystem, Result, Schema } from 'effect'; +import { Cause, Effect, FileSystem, Result, Schema, Predicate } from 'effect'; import type { PlatformError } from 'effect'; import { createCodesmithGenerator } from '../generator-adapter.mts'; import { @@ -41,7 +41,7 @@ const planningFailure = (reason: string, cause?: unknown): OutboxMessageScaffold : new OutboxMessageScaffoldError({ cause, reason }); const failureFromCause = (cause: unknown): OutboxMessageScaffoldError => - planningFailure(cause instanceof Error ? cause.message : String(cause), cause); + planningFailure(Predicate.isError(cause) ? cause.message : String(cause), cause); const fromLegacySync = ( operation: () => Value, diff --git a/app/scripts/scaffolding/outbox-worker/scaffold.mts b/app/scripts/scaffolding/outbox-worker/scaffold.mts index f6848c5da..e5223a293 100644 --- a/app/scripts/scaffolding/outbox-worker/scaffold.mts +++ b/app/scripts/scaffolding/outbox-worker/scaffold.mts @@ -1,4 +1,4 @@ -import { Effect, FileSystem, Match, Option, Schema } from 'effect'; +import { Effect, FileSystem, Match, Option, Schema, Predicate } from 'effect'; import { createCodesmithGenerator } from '../generator-adapter.mts'; import { MODULE_REGISTRATION_IMPORT_SLOT_END, @@ -46,7 +46,7 @@ class OutboxWorkerScaffoldError extends Schema.TaggedError new OutboxWorkerScaffoldError({ cause, - message: message ?? (cause instanceof Error ? cause.message : String(cause)), + message: message ?? (Predicate.isError(cause) ? cause.message : String(cause)), }); const trySync = (operation: () => Value) => diff --git a/app/scripts/scaffolding/policy/scaffold.mts b/app/scripts/scaffolding/policy/scaffold.mts index ccaea5c45..8e3242c31 100644 --- a/app/scripts/scaffolding/policy/scaffold.mts +++ b/app/scripts/scaffolding/policy/scaffold.mts @@ -1,9 +1,9 @@ -import { Effect, FileSystem, Match, Schema } from 'effect'; +import { Effect, FileSystem, Match, Schema, Predicate } from 'effect'; import { createCodesmithGenerator } from '../generator-adapter.mts'; import { CORE_POLICY_SLOT_END, CORE_POLICY_SLOT_START, - discoverOntosModule, + discoverOntosModuleEffect, ensureUniqueMutationPaths, insertSortedSlot, requireCanonicalSlug, @@ -30,18 +30,13 @@ export class PolicyScaffoldError extends Schema.TaggedError } const planningFailure = (cause: unknown): PolicyScaffoldError => - new PolicyScaffoldError({ reason: cause instanceof Error ? cause.message : String(cause) }); + new PolicyScaffoldError({ reason: Predicate.isError(cause) ? cause.message : String(cause) }); const fromLegacySync = ( operation: () => Value, ): 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, @@ -168,8 +163,8 @@ export 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/retire-contribution/scaffold.mts b/app/scripts/scaffolding/retire-contribution/scaffold.mts index aca074a99..779136da1 100644 --- a/app/scripts/scaffolding/retire-contribution/scaffold.mts +++ b/app/scripts/scaffolding/retire-contribution/scaffold.mts @@ -1,4 +1,4 @@ -import { Effect, FileSystem, Schema } from 'effect'; +import { Effect, FileSystem, Schema, Predicate } from 'effect'; import { createCodesmithGenerator } from '../generator-adapter.mts'; import { ACTION_GENERATOR_HEADER, @@ -58,7 +58,7 @@ const scaffoldError = (message: string, cause?: unknown): RetireContributionScaf const trySync = (operation: () => Value, fallback: string) => Effect.try({ - catch: (cause) => scaffoldError(cause instanceof Error ? cause.message : fallback, cause), + catch: (cause) => scaffoldError(Predicate.isError(cause) ? cause.message : fallback, cause), try: operation, }); diff --git a/app/scripts/scaffolding/search-provider-access/scaffold.mts b/app/scripts/scaffolding/search-provider-access/scaffold.mts index 95a082c40..bd17e6e90 100644 --- a/app/scripts/scaffolding/search-provider-access/scaffold.mts +++ b/app/scripts/scaffolding/search-provider-access/scaffold.mts @@ -1,4 +1,4 @@ -import { Effect, FileSystem, Schema } from 'effect'; +import { Effect, FileSystem, Schema, Predicate } from 'effect'; import { discoverOntosModuleEffect, ensureUniqueMutationPaths, @@ -35,10 +35,10 @@ const scaffoldError = (message: string, cause?: unknown): SearchProviderAccessSc const trySync = (operation: () => Value) => Effect.try({ catch: (cause) => - cause instanceof SearchProviderAccessScaffoldError + Schema.is(SearchProviderAccessScaffoldError)(cause) ? cause : scaffoldError( - cause instanceof Error ? cause.message : 'search provider access update failed', + Predicate.isError(cause) ? cause.message : 'search provider access update failed', cause, ), try: operation, diff --git a/app/scripts/scaffolding/shared.mts b/app/scripts/scaffolding/shared.mts index c8484c48e..293464318 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, Path, Predicate, Result, Schema } from 'effect'; +import { Effect, FileSystem, 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. */ @@ -400,11 +400,6 @@ const pathExistsEffect = (targetPath: string) => .pipe(Effect.mapError((cause) => scaffoldFailure(`failed to inspect ${targetPath}`, cause))); }); -export const pathExists: (targetPath: string) => Promise = flow( - pathExistsEffect, - scaffoldingRuntime.runPromise, -); - const regexMayStartAt = (content: string, index: number): boolean => { const prefix = content.slice(0, index).trimEnd(); if (prefix.length === 0) { @@ -765,14 +760,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; @@ -1110,11 +1097,6 @@ export const discoverVerticalEffect = ( return { ...vertical, topologyEntry }; }); -export const discoverVertical: ( - workspaceRoot: string, - requestedVertical: string, -) => Promise = flow(discoverVerticalEffect, scaffoldingRuntime.runPromise); - const readGeneratedModuleOwnerEffect = ( filePath: string, vertical: VerticalMetadata, @@ -1200,14 +1182,6 @@ export const discoverOntosModuleEffect = ( }; }); -export const discoverOntosModule: ( - workspaceRoot: string, - requestedVertical: string, -) => Promise = flow( - discoverOntosModuleEffect, - scaffoldingRuntime.runPromise, -); - export const createMutationEffect = ( filePath: string, content: string, @@ -1235,11 +1209,6 @@ export const createMutationEffect = ( return { content: formatted.code, kind: 'create', path: filePath }; }); -export const createMutation: (filePath: string, content: string) => Promise = flow( - createMutationEffect, - scaffoldingRuntime.runPromise, -); - export const updateMutation = ( filePath: string, previous: string, @@ -1257,11 +1226,6 @@ export const deleteMutationEffect = ( return { kind: 'delete', path: filePath }; }); -export const deleteMutation: (filePath: string) => Promise = flow( - deleteMutationEffect, - scaffoldingRuntime.runPromise, -); - const CORE_RUNTIME_PACKAGE = '@app/core-runtime'; const WORKSPACE_DEPENDENCY_VERSION = 'workspace:*'; @@ -1373,7 +1337,7 @@ export const applyMutationPlanEffect = ( mutation.content, 'utf-8', ), - }), + }).pipe(Effect.uninterruptible), { concurrency: 'unbounded', discard: true }, ); const fileSystem = yield* FileSystem.FileSystem; @@ -1386,14 +1350,6 @@ export const applyMutationPlanEffect = ( return plan.result; }); -const makeApplyMutationPlanEffect = (core: GeneratorCore, plan: ScaffoldPlan) => - applyMutationPlanEffect(core, plan); - -export const applyMutationPlan: ( - core: GeneratorCore, - plan: ScaffoldPlan, -) => Promise = flow(makeApplyMutationPlanEffect, scaffoldingRuntime.runPromise); - const dedentGeneratedSlotBody = (slotBody: string): string => { const lines = slotBody.split('\n'); while (lines[0]?.trim().length === 0) { diff --git a/app/scripts/scaffolding/tests/module-contract-generator.test.mts b/app/scripts/scaffolding/tests/module-contract-generator.test.mts index 5638d1670..524a80911 100644 --- a/app/scripts/scaffolding/tests/module-contract-generator.test.mts +++ b/app/scripts/scaffolding/tests/module-contract-generator.test.mts @@ -1,15 +1,15 @@ +import { NodeServices } from '@effect/platform-node'; import assert from 'node:assert/strict'; import { mkdir, mkdtemp, readFile, rm, symlink, 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 } from '../../ultramodern-api-boundary-rules.mts'; import { generateOntosModuleContract } from '../../generate-ontos-module-contract.mts'; -import { getHelpText, runScaffold } from '../cli.mts'; +import { getHelpText, runScaffoldEffect } from '../cli.mts'; import type { JsonValue } from '../shared.mts'; const APP_ID = 'property-registry'; @@ -196,15 +196,19 @@ const withFixture = async (run: (root: string) => Promise): Promise }; const scaffold = async (root: string, vertical = APP_ID, module = MODULE_ID) => - await runScaffold(MODULE_CONTRACT_COMMAND, [VERTICAL_FLAG, vertical, '--module', module], { - workspaceRoot: root, - }); + await runEffectTestPromise( + runScaffoldEffect(MODULE_CONTRACT_COMMAND, [VERTICAL_FLAG, vertical, '--module', module], { + workspaceRoot: root, + }).pipe(Effect.provide(NodeServices.layer)), + ); 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, - }); + const result = await runEffectTestPromise( + runScaffoldEffect(MODULE_CONTRACT_COMMAND, ['--help'], { + workspaceRoot: missingRoot, + }).pipe(Effect.provide(NodeServices.layer)), + ); assert.deepEqual(result, { help: getHelpText(MODULE_CONTRACT_COMMAND), kind: 'help' }); assert.match(result.help, /--vertical --module /u); }); @@ -269,7 +273,11 @@ void test('business generators fail closed before the mandatory module contract commands.map( async ([command, flags]) => await assert.rejects( - runScaffold(command, flags, { workspaceRoot: root }), + runEffectTestPromise( + runScaffoldEffect(command, flags, { workspaceRoot: root }).pipe( + Effect.provide(NodeServices.layer), + ), + ), /requires scaffold:module-contract/u, ), ), @@ -373,11 +381,13 @@ void test('emits deterministic deployment-safe JSON and rejects damaged owner sl ), 'utf-8', ); - const first = await generateOntosModuleContract({ - target: 'dist', - vertical: APP_ID, - workspaceRoot: root, - }); + const first = await runEffectTestPromise( + generateOntosModuleContract({ + target: 'dist', + vertical: APP_ID, + workspaceRoot: root, + }).pipe(Effect.provide(NodeServices.layer)), + ); const firstContent = await readFile(first.path, 'utf-8'); const packagePath = path.join(root, PROPERTY_PACKAGE_PATH); const packageContent = await readFile(packagePath, 'utf-8'); @@ -391,20 +401,24 @@ void test('emits deterministic deployment-safe JSON and rejects damaged owner sl }; await writeFile(packagePath, json(incompatiblePackage), 'utf-8'); await assert.rejects( + runEffectTestPromise( + generateOntosModuleContract({ + target: 'dist', + vertical: APP_ID, + workspaceRoot: root, + }).pipe(Effect.provide(NodeServices.layer)), + ), + /module marker does not match/u, + ); + assert.equal(await readFile(first.path, 'utf-8'), firstContent); + await writeFile(packagePath, packageContent, 'utf-8'); + const second = await runEffectTestPromise( generateOntosModuleContract({ target: 'dist', vertical: APP_ID, workspaceRoot: root, - }), - /module marker does not match/u, + }).pipe(Effect.provide(NodeServices.layer)), ); - 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); @@ -421,11 +435,13 @@ void test('emits deterministic deployment-safe JSON and rejects damaged owner sl 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 secondDeployment = await runEffectTestPromise( + generateOntosModuleContract({ + target: 'dist', + vertical: DOCUMENTS_APP_ID, + workspaceRoot: root, + }).pipe(Effect.provide(NodeServices.layer)), + ); 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); @@ -438,11 +454,13 @@ void test('emits deterministic deployment-safe JSON and rejects damaged owner sl 'utf-8', ); await assert.rejects( - generateOntosModuleContract({ - target: 'dist', - vertical: APP_ID, - workspaceRoot: root, - }), + runEffectTestPromise( + generateOntosModuleContract({ + target: 'dist', + vertical: APP_ID, + workspaceRoot: root, + }).pipe(Effect.provide(NodeServices.layer)), + ), /exactly one.*slot/u, ); }); @@ -452,17 +470,19 @@ void test('maps Cloudflare emission to the Modern output root and validates auth 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, - workspaceRoot: root, - }); + const emitted = await runEffectTestPromise( + generateOntosModuleContract({ + target: 'cloudflare-dist', + vertical: APP_ID, + workspaceRoot: root, + }).pipe(Effect.provide(NodeServices.layer)), + ); assert.match( emitted.path, /verticals\/property-registry\/dist-cloudflare\/public\/\.well-known\/ontos-module-manifest\.json$/u, ); await runEffectTestPromise( - checkOntosModuleContracts(root).pipe(Effect.provide(NodeFileSystem.layer)), + checkOntosModuleContracts(root).pipe(Effect.provide(NodeServices.layer)), ); }); }); diff --git a/app/scripts/scaffolding/tests/resource-generator.test.mts b/app/scripts/scaffolding/tests/resource-generator.test.mts index b65017d28..e2655da7f 100644 --- a/app/scripts/scaffolding/tests/resource-generator.test.mts +++ b/app/scripts/scaffolding/tests/resource-generator.test.mts @@ -1,3 +1,8 @@ +import { runEffectTestPromise } from '../../../packages/core-runtime/src/testing/effect-runtime.ts'; +import { Effect, Fiber, FileSystem, Schema } from 'effect'; +import { CodeSmith, GeneratorCore } from '@modern-js/codesmith'; +import { applyMutationPlanEffect } from '../shared.mts'; +import { NodeServices } from '@effect/platform-node'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { randomUUID } from 'node:crypto'; @@ -6,8 +11,7 @@ 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'; const appRoot = path.resolve(import.meta.dirname, '..', '..', '..'); const tscPath = path.join(appRoot, 'node_modules', '.bin', 'tsc'); @@ -156,9 +160,11 @@ export declare const ShellSearchContributionSchema: Schema.Codec Promise): Promise }; const scaffoldResource = async (root: string, resource = resourceName) => - await runScaffold('resource', ['--vertical', verticalName, '--resource', resource], { - workspaceRoot: root, - }); + await runEffectTestPromise( + runScaffoldEffect('resource', ['--vertical', verticalName, '--resource', resource], { + workspaceRoot: root, + }).pipe(Effect.provide(NodeServices.layer)), + ); 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, - }); + const result = await runEffectTestPromise( + runScaffoldEffect('resource', ['--help'], { + workspaceRoot: missingRoot, + }).pipe(Effect.provide(NodeServices.layer)), + ); 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); @@ -354,3 +364,69 @@ await test('resource scaffold upgrades the previous generated empty resourceType assert.match(upgraded, /rentalUnitResourceDescriptor,/u); }); }); + +void test('waits for an interrupted Codesmith write before removing its scoped output', async (context) => { + const root = await mkdtemp(path.join(tmpdir(), 'ontos-scaffold-cancellation-')); + context.after(async () => await rm(root, { 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[] = []; + context.mock.method(core.output, 'fs', async () => { + started.resolve(); + await release.promise; + await mkdir(root, { recursive: true }); + await writeFile(path.join(root, 'generated.ts'), 'export {};'); + events.push('write'); + }); + await runEffectTestPromise( + 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, { recursive: true, force: true }) + .pipe(Effect.orDie, Effect.andThen(Effect.sync(() => events.push('cleanup')))), + ); + yield* applyMutationPlanEffect(core, { + mutations: [ + { kind: 'create', path: path.join(root, 'generated.ts'), content: 'export {};' }, + ], + result: undefined, + }); + }), + ), + ); + yield* Effect.promise(async () => await started.promise); + const interruption = yield* Effect.forkChild(Fiber.interrupt(worker)); + yield* Effect.yieldNow; + assert.deepEqual(events, []); + release.resolve(); + yield* Fiber.join(interruption); + assert.deepEqual(events, ['write', 'cleanup']); + assert.equal(yield* fileSystem.exists(root), false); + }).pipe(Effect.provide(NodeServices.layer)), + ); +}); + +void test('reports synchronous malformed-owner validation through the typed command channel', async () => { + await withFixture(async (root) => { + await writeFile(path.join(root, verticalPackagePath), '[]'); + const before = await snapshotTree(root); + const failure = await runEffectTestPromise( + runScaffoldEffect('resource', ['--vertical', verticalName, '--resource', resourceName], { + workspaceRoot: root, + }).pipe(Effect.flip, Effect.provide(NodeServices.layer)), + ); + assert.ok(Schema.is(ScaffoldingError)(failure)); + assert.match(failure.message, /JSON object/u); + assert.deepEqual(await snapshotTree(root), before); + }); +}); diff --git a/app/scripts/scaffolding/tests/retire-contribution.test.mts b/app/scripts/scaffolding/tests/retire-contribution.test.mts index fe4d11ce6..b23d01dfd 100644 --- a/app/scripts/scaffolding/tests/retire-contribution.test.mts +++ b/app/scripts/scaffolding/tests/retire-contribution.test.mts @@ -1,9 +1,12 @@ +import { runEffectTestPromise } from '../../../packages/core-runtime/src/testing/effect-runtime.ts'; +import { Effect } from 'effect'; +import { NodeServices } from '@effect/platform-node'; import assert from 'node:assert/strict'; import { access, mkdir, mkdtemp, readFile, readdir, rm, 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 json = (value: JsonValue): string => `${JSON.stringify(value, null, 2)}\n`; @@ -197,16 +200,20 @@ const withFixture = async (run: (root: string) => Promise): Promise }; const retire = async (root: string, kind: 'action' | 'api' | 'page', name: string) => - await runScaffold( - RETIRE_CONTRIBUTION_COMMAND, - ['--vertical', 'inventory', '--kind', kind, '--name', name], - { workspaceRoot: root }, + await runEffectTestPromise( + runScaffoldEffect( + RETIRE_CONTRIBUTION_COMMAND, + ['--vertical', 'inventory', '--kind', kind, '--name', name], + { workspaceRoot: root }, + ).pipe(Effect.provide(NodeServices.layer)), ); 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'), - }); + const result = await runEffectTestPromise( + runScaffoldEffect(RETIRE_CONTRIBUTION_COMMAND, ['--help'], { + workspaceRoot: path.join(tmpdir(), 'retire-help-missing'), + }).pipe(Effect.provide(NodeServices.layer)), + ); assert.deepEqual(result, { help: getHelpText(RETIRE_CONTRIBUTION_COMMAND), kind: 'help' }); assert.match(result.help, /--kind /u); }); diff --git a/app/scripts/scaffolding/tests/scaffold-generators.test.mts b/app/scripts/scaffolding/tests/scaffold-generators.test.mts index 31c474561..63a6b2b19 100644 --- a/app/scripts/scaffolding/tests/scaffold-generators.test.mts +++ b/app/scripts/scaffolding/tests/scaffold-generators.test.mts @@ -1,3 +1,4 @@ +import { NodeServices } from '@effect/platform-node'; import { runEffectTestPromise } from '../../../packages/core-runtime/src/testing/effect-runtime.ts'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; @@ -6,7 +7,7 @@ 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 test from 'node:test'; import { Predicate } from 'effect'; import { TrustedPrincipalContextSchema } from '../../../packages/core-runtime/src/actions/principal-context.ts'; import type { TrustedPrincipalContext } from '../../../packages/core-runtime/src/actions/principal-context.ts'; @@ -34,7 +35,7 @@ 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 { getHelpText, runScaffoldEffect, ScaffoldingError } from '../cli.mts'; import type { ScaffoldCommand } from '../cli.mts'; import type { JsonValue } from '../shared.mts'; import { @@ -56,10 +57,6 @@ const GeneratedPrincipalErrorTagSchema = Schema.Literals([ ]); type GeneratedPrincipalErrorTag = typeof GeneratedPrincipalErrorTagSchema.Type; -const test = (name: string, handler: () => void | Promise): void => { - void nodeTest(name, handler); -}; - const isGeneratedPrincipalError = (tag: GeneratedPrincipalErrorTag) => Schema.is(Schema.Struct({ _tag: Schema.Literal(tag) })); @@ -515,12 +512,14 @@ export const coreActionCatalog = [ await Promise.all( [inventoryVertical, billingVertical, hrVertical, contactsVertical].map( async (vertical) => - await runScaffold( - 'module-contract', - [scaffoldFlag.vertical, vertical.slug, '--module', vertical.moduleId], - { - workspaceRoot: root, - }, + await runEffectTestPromise( + runScaffoldEffect( + 'module-contract', + [scaffoldFlag.vertical, vertical.slug, '--module', vertical.moduleId], + { + workspaceRoot: root, + }, + ).pipe(Effect.provide(NodeServices.layer)), ), ), ); @@ -566,50 +565,52 @@ const run = async ( scaffoldArguments: readonly string[], routeRefresh?: (appId: string) => void, ) => - await runScaffold( - command, - (() => { - let flags = [...scaffoldArguments]; - if ( - command === 'action' && - flags.includes('--action') && - !flags.includes(scaffoldFlag.legalEntityScope) - ) { - flags = [...flags, scaffoldFlag.legalEntityScope, 'optional']; - } - if (!flags.includes(scaffoldFlag.authorization)) { - if (command === 'action') { - flags = [ - ...flags, - scaffoldFlag.authorization, - 'action_execution', - '--provisioning', - 'tenant_membership_default', - ]; - } else if (command === scaffoldCommand.outboxWorker) { - flags = [...flags, scaffoldFlag.authorization, 'owner_local_background']; - } else if ( - command === scaffoldCommand.microverticalPage || - command === scaffoldCommand.moduleApi || - command === scaffoldCommand.publicComponent || - command === 'report' || - command === scaffoldCommand.searchProvider + await runEffectTestPromise( + runScaffoldEffect( + command, + (() => { + let flags = [...scaffoldArguments]; + if ( + command === 'action' && + flags.includes('--action') && + !flags.includes(scaffoldFlag.legalEntityScope) ) { - flags = [ - ...flags, - scaffoldFlag.authorization, - 'context_permission', - '--permission', - 'module.access', - ]; + flags = [...flags, scaffoldFlag.legalEntityScope, 'optional']; } - } - return flags; - })(), - { - routeRefresh: ({ appId }) => routeRefresh?.(appId), - workspaceRoot: fixture.root, - }, + if (!flags.includes(scaffoldFlag.authorization)) { + if (command === 'action') { + flags = [ + ...flags, + scaffoldFlag.authorization, + 'action_execution', + '--provisioning', + 'tenant_membership_default', + ]; + } else if (command === scaffoldCommand.outboxWorker) { + flags = [...flags, scaffoldFlag.authorization, 'owner_local_background']; + } else if ( + command === scaffoldCommand.microverticalPage || + command === scaffoldCommand.moduleApi || + command === scaffoldCommand.publicComponent || + command === 'report' || + command === scaffoldCommand.searchProvider + ) { + flags = [ + ...flags, + scaffoldFlag.authorization, + 'context_permission', + '--permission', + 'module.access', + ]; + } + } + return flags; + })(), + { + routeRefresh: ({ appId }) => Effect.sync(() => routeRefresh?.(appId)), + workspaceRoot: fixture.root, + }, + ).pipe(Effect.provide(NodeServices.layer)), ); const addInventoryItemResourceType = async (fixture: Fixture): Promise => { @@ -639,7 +640,7 @@ const addInventoryItemResourceType = async (fixture: Fixture): Promise => ); }; -test('documents every command and treats --help as a write-free operation', async () => { +void test('documents every command and treats --help as a write-free operation', async () => { await Promise.all( ( [ @@ -659,9 +660,11 @@ test('documents every command and treats --help as a write-free operation', asyn scaffoldCommand.searchProvider, ] as const ).map(async (command) => { - const result = await runScaffold(command, ['--', '--help'], { - workspaceRoot: path.join(tmpdir(), 'does-not-need-to-exist'), - }); + const result = await runEffectTestPromise( + runScaffoldEffect(command, ['--', '--help'], { + workspaceRoot: path.join(tmpdir(), 'does-not-need-to-exist'), + }).pipe(Effect.provide(NodeServices.layer)), + ); assert.deepEqual(result, { help: getHelpText(command), kind: 'help' }); assert.match(result.help, new RegExp(`scaffold:${command}`, 'u')); }), @@ -686,7 +689,7 @@ test('documents every command and treats --help as a write-free operation', asyn ); }); -test('search-provider access updates only generated access metadata and fails atomically on drift', async () => { +void 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); @@ -772,7 +775,7 @@ test('search-provider access updates only generated access metadata and fails at }); }); -test('generated API owner slots sort property keys before suffix variants', async () => { +void test('generated API owner slots sort property keys before suffix variants', async () => { await withFixture(async (fixture) => { await run(fixture, scaffoldCommand.moduleApi, [ scaffoldFlag.vertical, @@ -797,7 +800,7 @@ test('generated API owner slots sort property keys before suffix variants', asyn }); }); -test('generated read clients fetch mounted owner URLs and support separately deployed hosts', async () => { +void 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, [ @@ -908,7 +911,7 @@ test('generated read clients fetch mounted owner URLs and support separately dep }); }); -test('governed contribution generators patch owner contracts and lazy adapters atomically', async () => { +void 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); @@ -1145,7 +1148,7 @@ void ignored; }); }); -test('recognizes only exact schema-only Outbox package subpaths as cross-vertical contracts', () => { +void test('recognizes only exact schema-only Outbox package subpaths as cross-vertical contracts', () => { const producerPackage = { exports: { '.': './src/index.ts', @@ -1181,23 +1184,25 @@ test('recognizes only exact schema-only Outbox package subpaths as cross-vertica ); }); -test('rejects malformed command contracts and leaves the fixture unchanged', async () => { +void 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 }, + runEffectTestPromise( + runScaffoldEffect( + 'action', + [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + scaffoldFlag.authorization, + 'action_execution', + '--provisioning', + 'tenant_membership_default', + ], + { workspaceRoot: fixture.root }, + ).pipe(Effect.provide(NodeServices.layer)), ), /missing required flag --legal-entity-scope/u, ); @@ -1325,7 +1330,7 @@ test('rejects malformed command contracts and leaves the fixture unchanged', asy }); }); -test('generates one immutable Action identity boundary and exact direct dependencies', async () => { +void 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); @@ -1371,7 +1376,7 @@ test('generates one immutable Action identity boundary and exact direct dependen }); }); -test('Action identity boundary preflight refuses unsafe writes', async () => { +void test('Action identity boundary preflight refuses unsafe writes', async () => { await withFixture(async (fixture) => { await run(fixture, scaffoldCommand.microverticalActionBoundary, [ scaffoldFlag.vertical, @@ -1407,7 +1412,7 @@ test('Action identity boundary preflight refuses unsafe writes', async () => { }); }); -test('generated verifier executes real Shell assertions and overlapping Ed25519 rotation', async () => { +void test('generated verifier executes real Shell assertions and overlapping Ed25519 rotation', async () => { await withFixture(async (fixture) => { await run(fixture, scaffoldCommand.microverticalActionBoundary, [ scaffoldFlag.vertical, @@ -1758,7 +1763,7 @@ test('generated verifier executes real Shell assertions and overlapping Ed25519 }); }); -test('generates one self-contained typed fail-closed Action and preserves package metadata', async () => { +void test('generates one self-contained typed fail-closed Action and preserves package metadata', async () => { await withFixture(async (fixture) => { await run(fixture, 'action', [ scaffoldFlag.vertical, @@ -1849,7 +1854,7 @@ export const createOrder2Action = defineAction( }); }); -test('generates an owner-local Action service without overwriting business logic', async () => { +void test('generates an owner-local Action service without overwriting business logic', async () => { await withFixture(async (fixture) => { await run(fixture, scaffoldCommand.actionService, [ scaffoldFlag.vertical, @@ -1883,7 +1888,7 @@ export const inventoryPersistenceService = () => Effect.succeed({}); }); }); -test('generates exactly one private owner-local external HTTP adapter', async () => { +void 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, [ @@ -1976,7 +1981,7 @@ export const AresSubjectServiceLive = Layer.effect(AresSubjectService, makeAresS }); }); -test('rejects unsafe external HTTP adapter command input without writing', async () => { +void 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][] = [ @@ -2113,7 +2118,7 @@ test('rejects unsafe external HTTP adapter command input without writing', async }); }); -test('external HTTP adapter planner rejects malformed OntOS ownership atomically', async () => { +void 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'); @@ -2162,7 +2167,7 @@ test('external HTTP adapter planner rejects malformed OntOS ownership atomically }); }); -test('Action generation rejects unrelated imports in its governed owner slots', async () => { +void 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'); @@ -2184,7 +2189,7 @@ import { fakeRead } from './src/api/fake.read.ts';`, }); }); -test('generates Core-owned Actions only through the Core owner slot with atomic preflight', async () => { +void test('generates Core-owned Actions only through the Core owner slot with atomic preflight', async () => { await withFixture(async (fixture) => { await run(fixture, 'action', [ '--scope', @@ -2329,7 +2334,7 @@ test('generates Core-owned Actions only through the Core owner slot with atomic }); }); -test('preflights the Action dependency patch before creating a file', async () => { +void 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')); @@ -2355,7 +2360,7 @@ test('preflights the Action dependency patch before creating a file', async () = }); }); -test('rejects Action generation when a vertical app identity is duplicated', async () => { +void 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')); @@ -2382,7 +2387,7 @@ test('rejects Action generation when a vertical app identity is duplicated', asy }); }); -test('rejects Action generation when the target identity is absent from topology', async () => { +void 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')); @@ -2409,7 +2414,7 @@ test('rejects Action generation when the target identity is absent from topology }); }); -test('preserves owner JSON document style while patching the Core dependency', async () => { +void 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')); @@ -2431,7 +2436,7 @@ test('preserves owner JSON document style while patching the Core dependency', a }); }); -test('generates Action-owned Outbox Messages and sorts only the owned export slot', async () => { +void test('generates Action-owned Outbox Messages and sorts only the owned export slot', async () => { await withFixture(async (fixture) => { await run(fixture, 'action', [ scaffoldFlag.vertical, @@ -2546,7 +2551,7 @@ export const outboxProducerModuleKey = 'inventory.stock' as const; }); }); -test('rejects missing, handwritten, duplicate, and normalized-collision Outbox targets without partial writes', async () => { +void test('rejects missing, handwritten, duplicate, and normalized-collision Outbox targets without partial writes', async () => { await withFixture(async (fixture) => { const beforeMissing = await snapshotTree(fixture.root); await assert.rejects( @@ -2636,7 +2641,7 @@ test('rejects missing, handwritten, duplicate, and normalized-collision Outbox t }); }); -test('generates isolated Outbox Workers from published contracts and composes a stable registry', async () => { +void test('generates isolated Outbox Workers from published contracts and composes a stable registry', async () => { await withFixture(async (fixture) => { await run(fixture, 'action', [ scaffoldFlag.vertical, @@ -2871,7 +2876,7 @@ export const startBillingOutboxWorker = (): void => }); }); -test('generates self-consuming Outbox Workers without circular project or package dependencies', async () => { +void test('generates self-consuming Outbox Workers without circular project or package dependencies', async () => { await withFixture(async (fixture) => { await run(fixture, 'action', [ scaffoldFlag.vertical, @@ -2979,7 +2984,7 @@ test('generates self-consuming Outbox Workers without circular project or packag }); }); -test('refuses unpublished or malformed Outbox contracts without partial consumer writes', async () => { +void test('refuses unpublished or malformed Outbox contracts without partial consumer writes', async () => { await withFixture(async (fixture) => { const beforeUnpublished = await snapshotTree(fixture.root); await assert.rejects( @@ -3039,7 +3044,7 @@ test('refuses unpublished or malformed Outbox contracts without partial consumer }); }); -test('generates fail-closed global and owner-local Policies with narrow exports', async () => { +void 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']); @@ -3125,7 +3130,7 @@ export { tenantActivePolicy } from './policies/tenant-active.policy.ts'; }); }); -test('generates a title-only authenticated page at the default MicroVertical URL', async () => { +void 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); @@ -3280,7 +3285,7 @@ export { routeMeta }; }); }); -test('allows a two-letter MicroVertical slug in a derived default page URL', async () => { +void test('allows a two-letter MicroVertical slug in a derived default page URL', async () => { await withFixture(async (fixture) => { await run(fixture, scaffoldCommand.microverticalPage, [ scaffoldFlag.vertical, @@ -3296,7 +3301,7 @@ test('allows a two-letter MicroVertical slug in a derived default page URL', asy }); }); -test('renders a newly generated federated page with English and Czech owner resources', async () => { +void 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, @@ -3396,7 +3401,7 @@ process.stdout.write(renderToStaticMarkup()); }); }); -test('adds further pages after generated owner files have been formatted', async () => { +void test('adds further pages after generated owner files have been formatted', async () => { await withFixture(async (fixture) => { const formattedOwnerPaths = [ inventoryManifestFile, @@ -3460,7 +3465,7 @@ test('adds further pages after generated owner files have been formatted', async }); }); -test('supports an explicit nested page URL and rejects unsafe URL inputs atomically', async () => { +void 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, @@ -3567,7 +3572,7 @@ test('supports an explicit nested page URL and rejects unsafe URL inputs atomica ); }); -test('generates a non-navigational dynamic page with canonical parameters and router directories', async () => { +void test('generates a non-navigational dynamic page with canonical parameters and router directories', async () => { await withFixture(async (fixture) => { await writeFixtureFile( fixture.root, @@ -3649,7 +3654,7 @@ test('generates a non-navigational dynamic page with canonical parameters and ro }); }); -test('generates the Contacts Contact-detail two-parameter page atomically and safely reruns it', async () => { +void test('generates the Contacts Contact-detail two-parameter page atomically and safely reruns it', async () => { const generatorArguments = [ scaffoldFlag.vertical, inventorySlug, @@ -3724,7 +3729,7 @@ test('generates the Contacts Contact-detail two-parameter page atomically and sa }); }); -test('rejects unsafe dynamic parameters and dynamic route collisions without writing', async () => { +void test('rejects unsafe dynamic parameters and dynamic route collisions without writing', async () => { await Promise.all( [ '/inventory/customers/:1id/edit', @@ -3882,7 +3887,7 @@ test('rejects unsafe dynamic parameters and dynamic route collisions without wri ]); }); -test('extends an existing dynamic route branch without reclassifying an existing static sibling', async () => { +void 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, @@ -3916,7 +3921,7 @@ test('extends an existing dynamic route branch without reclassifying an existing }); }); -test('rejects reserved, dynamic, and cross-owner page URLs before writing', async () => { +void test('rejects reserved, dynamic, and cross-owner page URLs before writing', async () => { await Promise.all([ withFixture(async (fixture) => { await writeFixtureFile( @@ -3987,7 +3992,7 @@ test('rejects reserved, dynamic, and cross-owner page URLs before writing', asyn ]); }); -test('uses exact page identities and rejects edited generated wiring', async () => { +void test('uses exact page identities and rejects edited generated wiring', async () => { await withFixture(async (fixture) => { await run(fixture, scaffoldCommand.microverticalPage, [ scaffoldFlag.vertical, @@ -4114,7 +4119,7 @@ test('uses exact page identities and rejects edited generated wiring', async () ]); }); -test('migrates only exact legacy generated page output and then reruns as a no-op', async () => { +void test('migrates only exact legacy generated page output and then reruns as a no-op', async () => { await withFixture(async (fixture) => { const generatorArguments = [ scaffoldFlag.vertical, @@ -4232,7 +4237,7 @@ export const loader = ({ request }: ShellPageLoaderArguments) => }); }); -test('rejects page generation when an owning locale has no truthful starter translation', async () => { +void 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')); @@ -4264,7 +4269,7 @@ test('rejects page generation when an owning locale has no truthful starter tran }); }); -test('page prerequisite and nested-route failures are preflighted, while refresh failure is safely rerunnable', async () => { +void 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'), @@ -4303,24 +4308,25 @@ test('page prerequisite and nested-route failures are preflighted, while refresh 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'); + runEffectTestPromise( + 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, }, - workspaceRoot: fixture.root, - }, + ).pipe(Effect.provide(NodeServices.layer)), ), /route refresh fixture failure/u, ); @@ -4429,7 +4435,7 @@ const runCombinedScenario = async (fixture: Fixture): Promise { +void test('all generators compose deterministically without crossing owner boundaries', async () => { const first = await createFixture(); const second = await createFixture(); try { @@ -4457,7 +4463,7 @@ test('all generators compose deterministically without crossing owner boundaries } }); -test('every generated TypeScript file is already formatter-stable', async () => { +void test('every generated TypeScript file is already formatter-stable', async () => { await withFixture(async (fixture) => { await runCombinedScenario(fixture); await run(fixture, scaffoldCommand.outboxWorker, [ @@ -4518,7 +4524,7 @@ test('every generated TypeScript file is already formatter-stable', async () => }); }); -test('all generated files typecheck against the real workspace contracts', async () => { +void test('all generated files typecheck against the real workspace contracts', async () => { await withFixture(async (fixture) => { await runCombinedScenario(fixture); await run(fixture, scaffoldCommand.outboxWorker, [ diff --git a/app/scripts/tests/api-only-tooling.test.mts b/app/scripts/tests/api-only-tooling.test.mts index 883f7b26a..537f6c34c 100644 --- a/app/scripts/tests/api-only-tooling.test.mts +++ b/app/scripts/tests/api-only-tooling.test.mts @@ -61,16 +61,20 @@ interface ReleaseEnvelope { } interface ReleaseFramework { + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- Structural mirror of the installed Modern.js release-envelope SDK. readonly emitFrameworkMicroVerticalReleaseEnvelope: (input: { readonly apiOnly: boolean; readonly distDirectory: string; readonly target: string; }) => Promise; + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- Structural mirror of the installed Modern.js release-envelope SDK. readonly emitNodeStagedReleaseEnvelope: (input: { readonly distDirectory: string; readonly outputDirectory: string; }) => Promise; + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- Structural mirror of the installed Modern.js release-envelope SDK. readonly verifyBuildOutputReleaseEnvelope: (root: string, target: string) => Promise; + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- Structural mirror of the installed Modern.js release-envelope SDK. readonly verifyNodeReleaseEnvelopeStaging: (input: { readonly outputDirectory: string; }) => Promise; @@ -85,6 +89,7 @@ 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; +// oxlint-disable-next-line effect-native/no-promise-shaped-port -- The dynamically loaded Modern.js validator owns this Promise signature. type ValidateCloudflareApp = ( app: ApiOnlyAppFixture, applicationPublicUrl: string, @@ -301,11 +306,11 @@ 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'), -); const releaseFixture = async (context: TestContext) => { + const releaseFramework = await loadReleaseFramework( + path.join(releaseFrameworkRoot, 'esm-node/ultramodern-release-envelope/framework-output.mjs'), + ); const root = await mkdtemp(path.join(os.tmpdir(), 'ontos-empty-producer-')); context.after(async (): Promise => { await rm(root, { force: true, recursive: true }); @@ -359,7 +364,7 @@ const releaseFixture = async (context: TestContext) => { distDirectory: root, target: 'node', }); - return { artifact, emit, manifest, putJson, putText, root }; + return { artifact, emit, framework: releaseFramework, manifest, putJson, putText, root }; }; void test('empty MF producers retain complete build and Node staged release evidence in every framework format', async (context) => { @@ -395,7 +400,7 @@ void test('empty MF producers retain complete build and Node staged release evid await fixture.emit(); await fixture.putText(compiledUiAssetPath, 'console.log("tampered");'); await assert.rejects( - async () => await releaseFramework.verifyBuildOutputReleaseEnvelope(fixture.root, 'node'), + async () => await fixture.framework.verifyBuildOutputReleaseEnvelope(fixture.root, 'node'), /digest|hash|size/iu, ); }); diff --git a/app/scripts/tests/database-access-boundaries.test.mts b/app/scripts/tests/database-access-boundaries.test.mts index 27e770710..367058a1a 100644 --- a/app/scripts/tests/database-access-boundaries.test.mts +++ b/app/scripts/tests/database-access-boundaries.test.mts @@ -1,3 +1,6 @@ +import { runEffectTestPromise } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; +import { Effect } from 'effect'; +import { NodeServices } from '@effect/platform-node'; import assert from 'node:assert/strict'; import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; @@ -62,7 +65,9 @@ void test('allows owner database factories and rejects Action, read, nested BFF, await writeFile(file, source); }), ); - const violations = await checkDatabaseAccessBoundaries(root); + const violations = await runEffectTestPromise( + checkDatabaseAccessBoundaries(root).pipe(Effect.provide(NodeServices.layer)), + ); assert.deepEqual( violations.map(({ file, line }) => `${file}:${line}`), [ diff --git a/app/scripts/tests/initialize-local-development.test.mts b/app/scripts/tests/initialize-local-development.test.mts index fb1dc93c8..5a369e2b2 100644 --- a/app/scripts/tests/initialize-local-development.test.mts +++ b/app/scripts/tests/initialize-local-development.test.mts @@ -4,8 +4,8 @@ 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 } 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'; @@ -43,7 +43,7 @@ const topology = JSON.stringify({ verticals: [{ id: 'party-registry' }, { id: 'i const moduleContract = ( moduleId: string, -): Awaited> => ({ +): Effect.Success> => ({ deployment: { appId: 'test-module', buildMarker: 'test-build' }, manifest: { activation: { @@ -174,13 +174,11 @@ void test('derives only configured Party Registry through its generated owner co 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`); + const deriveContract = ({ vertical }: { readonly vertical: string }) => + Effect.succeed(moduleContract(`${vertical}.core`)); assert.deepEqual( await runEffectTestPromise( - deriveActivatedModuleIds(root, deriveContract).pipe( - Effect.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)), - ), + deriveActivatedModuleIds(root, deriveContract).pipe(Effect.provide(NodeServices.layer)), ), ['party-registry.core'], ); @@ -190,11 +188,11 @@ 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'); + const deriveContract = () => Effect.succeed(moduleContract('duplicate.core')); await assert.rejects( runEffectTestPromise( deriveActivatedModuleIds(root, deriveContract, ['party-registry', 'inventory']).pipe( - Effect.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)), + Effect.provide(NodeServices.layer), ), ), LocalDevelopmentInitializationError, diff --git a/app/scripts/tests/module-entrypoint-boundaries.test.mts b/app/scripts/tests/module-entrypoint-boundaries.test.mts index 64402c981..b782da01f 100644 --- a/app/scripts/tests/module-entrypoint-boundaries.test.mts +++ b/app/scripts/tests/module-entrypoint-boundaries.test.mts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import nodeTest from 'node:test'; +import test from 'node:test'; import { NodeServices } from '@effect/platform-node'; import { ManagedRuntime } from 'effect'; import { checkModuleEntrypointBoundaries as checkModuleEntrypointBoundariesEffect } from '../check-module-entrypoint-boundaries.mts'; @@ -18,11 +18,8 @@ import { 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); -}; -nodeTest.after(async () => { +test.after(async () => { await boundaryCheckRuntime.dispose(); }); @@ -119,7 +116,7 @@ export const routeMeta = { moduleId: 'inventory.stock', ownerAppId: 'inventory-s return root; }; -test('accepts governed generated Actions, pages, Workers, catalogs, and route manifests', async () => { +void test('accepts governed generated Actions, pages, Workers, catalogs, and route manifests', async () => { const root = await makeFixture(); try { await checkModuleEntrypointBoundaries(root); @@ -191,7 +188,7 @@ export const manifest = { api: { 'stock-list': StockListApi, } };`, ); }; -test('accepts only a complete generated governed module API seam', async () => { +void test('accepts only a complete generated governed module API seam', async () => { const root = await makeFixture(); try { await writeGovernedModuleApi(root); @@ -307,7 +304,7 @@ const violations = [ ] as const; for (const violation of violations) { - test(`rejects bypass ${violation.file}`, async () => { + void test(`rejects bypass ${violation.file}`, async () => { const root = await makeFixture(); try { await write(root, violation.file, violation.source); @@ -326,7 +323,7 @@ for (const violation of violations) { }); } -test('rejects missing headers, spoofed metadata, and stale generated descriptors', async () => { +void test('rejects missing headers, spoofed metadata, and stale generated descriptors', async () => { const root = await makeFixture(); try { await write( @@ -446,12 +443,12 @@ const assertPublishedPartyUsage = ( ...overrides, }); -test('accepts an exact generated PartyRef contract import', () => { +void test('accepts an exact generated PartyRef contract import', () => { assert.doesNotThrow(() => assertPublishedPartyUsage()); assert.deepEqual(publishedResourceRefContractExports(partyPackage), ['./resources/party']); }); -test('rejects a ResourceRef contract with a spoofed Codesmith header', () => { +void test('rejects a ResourceRef contract with a spoofed Codesmith header', () => { assert.throws( () => assertPublishedPartyUsage({ @@ -462,7 +459,7 @@ test('rejects a ResourceRef contract with a spoofed Codesmith header', () => { ); }); -test('rejects a ResourceRef export whose target is not its exact generated shared path', () => { +void test('rejects a ResourceRef export whose target is not its exact generated shared path', () => { assert.throws( () => assertPublishedPartyUsage({ @@ -475,7 +472,7 @@ test('rejects a ResourceRef export whose target is not its exact generated share ); }); -test('rejects a ResourceRef import whose public path is not resources/', () => { +void test('rejects a ResourceRef import whose public path is not resources/', () => { assert.throws( () => assertPublishedPartyUsage({ @@ -489,7 +486,7 @@ test('rejects a ResourceRef import whose public path is not resources/', ( ); }); -test('rejects owner runtime imports hidden in a generated ResourceRef file', () => { +void test('rejects owner runtime imports hidden in a generated ResourceRef file', () => { assert.throws( () => assertPublishedPartyUsage({ @@ -500,21 +497,21 @@ test('rejects owner runtime imports hidden in a generated ResourceRef file', () ); }); -test('rejects a ResourceRef import without the consuming workspace dependency', () => { +void test('rejects a ResourceRef import without the consuming workspace dependency', () => { assert.throws( () => assertPublishedPartyUsage({ dependencyDeclared: false }), /must declare @app\/party-registry as a workspace dependency/u, ); }); -test('rejects a ResourceRef import without the consuming TypeScript project reference', () => { +void test('rejects a ResourceRef import without the consuming TypeScript project reference', () => { assert.throws( () => assertPublishedPartyUsage({ projectReferenceDeclared: false }), /must project-reference @app\/party-registry/u, ); }); -test('rejects barrels and arbitrary exported owner subpaths', () => { +void test('rejects barrels and arbitrary exported owner subpaths', () => { for (const specifier of [ PARTY_PACKAGE_NAME, '@app/party-registry/shared/domain/party', @@ -528,7 +525,7 @@ test('rejects barrels and arbitrary exported owner subpaths', () => { } }); -test('preserves existing exact Outbox contract dependency behavior', () => { +void test('preserves existing exact Outbox contract dependency behavior', () => { assert.deepEqual(publishedOutboxContractExports(partyPackage), ['./outbox/party-created']); assert.doesNotThrow(() => assertPublishedOutboxDependencyUsage({ @@ -563,7 +560,7 @@ test('preserves existing exact Outbox contract dependency behavior', () => { ); }); -test('accepts an exact published Party Registry Effect client aggregate', () => { +void test('accepts an exact published Party Registry Effect client aggregate', () => { assert.doesNotThrow(() => assertPublishedPartyUsage({ moduleSpecifiers: [PARTY_API_CLIENT_SPECIFIER], @@ -580,7 +577,7 @@ test('accepts an exact published Party Registry Effect client aggregate', () => ); }); -test('rejects a published client export that points at backend implementation', () => { +void test('rejects a published client export that points at backend implementation', () => { assert.throws( () => assertPublishedPartyUsage({ @@ -594,7 +591,7 @@ test('rejects a published client export that points at backend implementation', ); }); -test('rejects a client aggregate that imports provider backend or private source', () => { +void test('rejects a client aggregate that imports provider backend or private source', () => { for (const forbiddenImport of [ '../../api/index.ts', '../db/repository.ts', @@ -617,7 +614,7 @@ test('rejects a client aggregate that imports provider backend or private source } }); -test('rejects an aggregate whose client leaf has no Codesmith metadata', () => { +void test('rejects an aggregate whose client leaf has no Codesmith metadata', () => { assert.throws( () => assertPublishedPartyUsage({ @@ -631,7 +628,7 @@ test('rejects an aggregate whose client leaf has no Codesmith metadata', () => { ); }); -test('accepts an exact generated MicroVertical command client leaf', () => { +void test('accepts an exact generated MicroVertical command client leaf', () => { assert.doesNotThrow(() => assertPublishedPartyUsage({ moduleSpecifiers: [PARTY_API_CLIENT_SPECIFIER], @@ -651,7 +648,7 @@ test('accepts an exact generated MicroVertical command client leaf', () => { ); }); -test('rejects a command client leaf whose generated owner marker names another deployment', () => { +void test('rejects a command client leaf whose generated owner marker names another deployment', () => { assert.throws( () => assertPublishedPartyUsage({ @@ -673,7 +670,7 @@ test('rejects a command client leaf whose generated owner marker names another d ); }); -test('rejects API barrels and arbitrary client subpaths', () => { +void test('rejects API barrels and arbitrary client subpaths', () => { for (const specifier of [ '@app/party-registry/api', '@app/party-registry/api/client/internal', @@ -702,7 +699,7 @@ 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', () => { +void test('validates Outbox producer module identity when deployment appId differs from moduleId', () => { const moduleId = resolvePublishedContractModuleId({ dependencyPackageJson: partyPackage, dependencyPackageName: PARTY_PACKAGE_NAME, @@ -719,7 +716,7 @@ test('validates Outbox producer module identity when deployment appId differs fr ); }); -test('rejects an Outbox producer that substitutes deployment appId for moduleId', () => { +void test('rejects an Outbox producer that substitutes deployment appId for moduleId', () => { assert.throws( () => assertPublishedOutboxContractSource({ @@ -734,7 +731,7 @@ test('rejects an Outbox producer that substitutes deployment appId for moduleId' ); }); -test('rejects mismatched package and generated manifest ownership for published contracts', () => { +void test('rejects mismatched package and generated manifest ownership for published contracts', () => { assert.throws( () => resolvePublishedContractModuleId({ @@ -747,7 +744,7 @@ test('rejects mismatched package and generated manifest ownership for published ); }); -test('keeps executable owner behavior out of published Outbox contracts', () => { +void test('keeps executable owner behavior out of published Outbox contracts', () => { assert.throws( () => assertPublishedOutboxContractSource({ @@ -758,7 +755,7 @@ test('keeps executable owner behavior out of published Outbox contracts', () => /must remain a generated schema-only Outbox contract/u, ); }); -test('rejects missing, orphaned, and cross-owner route manifest entries', async () => { +void test('rejects missing, orphaned, and cross-owner route manifest entries', async () => { const root = await makeFixture(); try { await write( diff --git a/app/scripts/tests/outbox-worker-delivery.test.mts b/app/scripts/tests/outbox-worker-delivery.test.mts index ab89b91e4..bb05b450d 100644 --- a/app/scripts/tests/outbox-worker-delivery.test.mts +++ b/app/scripts/tests/outbox-worker-delivery.test.mts @@ -4,7 +4,7 @@ import { once } from 'node:events'; 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 { test } from 'node:test'; import { Option, Schema } from 'effect'; import { generateOutboxWorkerDeployment } from '../generate-outbox-worker-deployment.mjs'; import { materializeOutboxWorker } from '../materialize-outbox-worker.mjs'; @@ -23,10 +23,6 @@ const decodeExitEvent = Schema.decodeUnknownSync( ); const decodeDataEvent = Schema.decodeUnknownSync(Schema.Tuple([Schema.Unknown])); -const test = (name: string, run: () => void | Promise): void => { - void registerNodeTest(name, run); -}; - 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 }); @@ -59,7 +55,7 @@ const makeFixture = async () => { return root; }; -test('generates a separate supervised worker setup without changing owner configuration', async () => { +void test('generates a separate supervised worker setup without changing owner configuration', async () => { const root = await makeFixture(); try { 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`; @@ -77,7 +73,7 @@ test('generates a separate supervised worker setup without changing owner config } }); -test('materializes and starts a relocatable production worker artifact', async () => { +void test('materializes and starts a relocatable production worker artifact', async () => { const root = await makeFixture(); try { const command = path.resolve('scripts/materialize-zerops-runtime.mjs'); @@ -138,7 +134,7 @@ test('materializes and starts a relocatable production worker artifact', async ( } }); -test('keeps the live Party Registry worker deployment generated and independently supervised', async () => { +void 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); @@ -149,7 +145,7 @@ test('keeps the live Party Registry worker deployment generated and independentl assert.doesNotMatch(worker, /(?:^|\s)&(?:\s|$)/u); }); -test('bundles the real Party host including the production Effect HTTP health adapter', async () => { +void 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({ diff --git a/app/scripts/tests/plan-deployment-impact.test.mts b/app/scripts/tests/plan-deployment-impact.test.mts index 485a4ca76..44781b7a1 100644 --- a/app/scripts/tests/plan-deployment-impact.test.mts +++ b/app/scripts/tests/plan-deployment-impact.test.mts @@ -5,7 +5,7 @@ import { execFileSync } from 'node:child_process'; import { 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 { test } from 'node:test'; import { NodeServices } from '@effect/platform-node'; import { ManagedRuntime } from 'effect'; import { hashAuthorizationEvidence } from '../check-authorization-readiness.mts'; @@ -77,15 +77,11 @@ 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 () => { +test.after(async () => { await deploymentImpactRuntime.dispose(); }); @@ -168,7 +164,7 @@ const withFixture = async ( } }; -test('deploys a generated owner worker immediately after its provider', async () => { +void test('deploys a generated owner worker immediately after its provider', async () => { await withFixture( async (root) => { const plan = await planDeploymentImpact({ @@ -196,7 +192,7 @@ const runGit = (root: string, argumentsList: readonly string[]): string => }, ).trim(); -test('plans current Contacts owner-local changes without a hard-coded owner registry', async () => { +void 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'], @@ -215,7 +211,7 @@ test('plans current Contacts owner-local changes without a hard-coded owner regi }); }); -test('orders authorization schema and replay migration before every affected consumer', async () => { +void 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'], @@ -228,7 +224,7 @@ test('orders authorization schema and replay migration before every affected con }); }); -test('plans Shell-only changes for the topology-derived Shell owner', async () => { +void 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'], @@ -242,7 +238,7 @@ test('plans Shell-only changes for the topology-derived Shell owner', async () = }); }); -test('adds the migrator before an owner whose schema or migration contract changed', async () => { +void 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'], @@ -260,7 +256,7 @@ for (const changedPath of [ 'scripts/verify-application-db-schema.mts', 'scripts/postgres/bootstrap-runtime-role.mts', ]) { - test(`includes the migrator for root migration contract ${changedPath}`, async () => { + void test(`includes the migrator for root migration contract ${changedPath}`, async () => { await withFixture(async (root) => { const plan = await planDeploymentImpact({ changedPaths: [changedPath], rootDirectory: root }); assert.deepEqual( @@ -275,7 +271,7 @@ for (const changedPath of [ 'scripts/postgres/bootstrap-spicedb-database.mts', 'packages/core-runtime/src/install/spicedb-database-config.ts', ]) { - test(`includes the migrator, SpiceDB, and every consumer for SpiceDB database bootstrap change ${changedPath}`, async () => { + void test(`includes the migrator, SpiceDB, and every consumer for SpiceDB database bootstrap change ${changedPath}`, async () => { await withFixture(async (root) => { const plan = await planDeploymentImpact({ changedPaths: [changedPath], rootDirectory: root }); assert.deepEqual( @@ -286,7 +282,7 @@ for (const changedPath of [ }); } -test('expands shared-package changes to every consumer in dependency order', async () => { +void 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'], @@ -299,7 +295,7 @@ test('expands shared-package changes to every consumer in dependency order', asy }); }); -test('expands a provider public-contract change to the dependent Shell', async () => { +void 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'], @@ -312,7 +308,7 @@ test('expands a provider public-contract change to the dependent Shell', async ( }); }); -test('orders SpiceDB before all consumers for authorization runtime changes', async () => { +void 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'], @@ -337,7 +333,7 @@ for (const changedPath of [ 'zerops.yaml', 'topology/reference-topology.json', ]) { - test(`conservatively deploys every phase for ${changedPath}`, async () => { + void test(`conservatively deploys every phase for ${changedPath}`, async () => { await withFixture(async (root) => { const plan = await planDeploymentImpact({ changedPaths: [changedPath], rootDirectory: root }); assert.deepEqual( @@ -348,7 +344,7 @@ for (const changedPath of [ }); } -test('produces a reviewed no-op for documentation-only changes', async () => { +void test('produces a reviewed no-op for documentation-only changes', async () => { await withFixture(async (root) => { const plan = await planDeploymentImpact({ changedPaths: ['docs/architecture/DEPLOYMENT.md'], @@ -359,7 +355,7 @@ test('produces a reviewed no-op for documentation-only changes', async () => { }); }); -test('fails closed for the unknown destination of a renamed application directory', async () => { +void test('fails closed for the unknown destination of a renamed application directory', async () => { await withFixture(async (root) => { await assert.rejects( planDeploymentImpact({ @@ -371,7 +367,7 @@ test('fails closed for the unknown destination of a renamed application director }); }); -test('fails closed when a topology delivery unit has no ownership entry', async () => { +void test('fails closed when a topology delivery unit has no ownership entry', async () => { await withFixture( async (root) => { await assert.rejects( @@ -383,7 +379,7 @@ test('fails closed when a topology delivery unit has no ownership entry', async ); }); -test('fails closed when topology and ownership identities disagree', async () => { +void test('fails closed when topology and ownership identities disagree', async () => { await withFixture(async (root) => { await writeJson(root, OWNERSHIP_PATH, { owners: [ @@ -400,7 +396,7 @@ test('fails closed when topology and ownership identities disagree', async () => }); }); -test('fails closed when shared-package topology and ownership identities disagree', async () => { +void test('fails closed when shared-package topology and ownership identities disagree', async () => { await withFixture(async (root) => { await writeJson(root, OWNERSHIP_PATH, { owners: [ @@ -417,7 +413,7 @@ test('fails closed when shared-package topology and ownership identities disagre }); }); -test('fails closed when a topology unit has no supported stage setup', async () => { +void test('fails closed when a topology unit has no supported stage setup', async () => { await withFixture( async (root) => { await assert.rejects( @@ -429,7 +425,7 @@ test('fails closed when a topology unit has no supported stage setup', async () ); }); -test('uses a safe full deployment for an all-zero comparison base', async () => { +void test('uses a safe full deployment for an all-zero comparison base', async () => { await withFixture(async (root) => { const plan = await planDeploymentImpact({ baseRevision: '0000000000000000000000000000000000000000', @@ -445,7 +441,7 @@ test('uses a safe full deployment for an all-zero comparison base', async () => }); }); -test('uses a safe full deployment for an unavailable comparison base', async () => { +void test('uses a safe full deployment for an unavailable comparison base', async () => { await withFixture(async (root) => { const plan = await planDeploymentImpact({ baseRevision: 'missing-base-revision', @@ -460,7 +456,7 @@ test('uses a safe full deployment for an unavailable comparison base', async () }); }); -test('uses a safe full deployment when the comparison base is not an ancestor', async () => { +void 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', '.']); @@ -485,7 +481,7 @@ test('uses a safe full deployment when the comparison base is not an ancestor', }); }); -test('changing a topology identity changes the plan without editing planner source', async () => { +void test('changing a topology identity changes the plan without editing planner source', async () => { await withFixture( async (root) => { const plan = await planDeploymentImpact({ @@ -596,7 +592,7 @@ const withoutReadinessEvidence = ( return remaining; }; -test('requires exact impact, readiness, and negative-smoke evidence for enforced promotion', () => { +void test('requires exact impact, readiness, and negative-smoke evidence for enforced promotion', () => { assert.deepEqual(validateAuthorizationPromotionGate(promotionFixture()), { environment: 'stage', mode: 'enforced', @@ -622,7 +618,7 @@ test('requires exact impact, readiness, and negative-smoke evidence for enforced ); }); -test('report-only promotion is bounded, explicit-baseline-only, and never allowed in production', () => { +void test('report-only promotion is bounded, explicit-baseline-only, and never allowed in production', () => { const enforced = promotionFixture(); const withoutRequiredEvidence = withoutReadinessEvidence( withoutNegativeSmokeEvidence(withoutImpactEvidence(enforced)), diff --git a/app/scripts/tests/provision-current-action-authorization.test.mts b/app/scripts/tests/provision-current-action-authorization.test.mts index 623d0ba58..274234c80 100644 --- a/app/scripts/tests/provision-current-action-authorization.test.mts +++ b/app/scripts/tests/provision-current-action-authorization.test.mts @@ -1,3 +1,4 @@ +import { NodeServices } from '@effect/platform-node'; import { runEffectTestPromise } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; import assert from 'node:assert/strict'; import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; @@ -6,7 +7,7 @@ import path from 'node:path'; import { test } from 'node:test'; import { pathToFileURL } from 'node:url'; import { v1 } from '@authzed/authzed-node'; -import { Effect, Option, Schema } from 'effect'; +import { Effect, Option, Schema, Predicate } from 'effect'; import { ACTION_AUTHORIZATION_DENIED_PRINCIPAL_ID, ActionAuthorizationProvisioningError, @@ -105,7 +106,7 @@ const rejectionOf = async (promise: Promise): Promise => { try { await promise; } catch (error) { - if (error instanceof Error) { + if (Predicate.isError(error)) { return error; } return assert.fail('Expected the Promise to reject with an Error'); @@ -634,41 +635,45 @@ const writeInventory = async ( 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, - }); + const currentContract = await runEffectTestPromise( + deriveOntosModuleDeploymentContract({ + vertical: 'party-registry', + workspaceRoot, + }).pipe(Effect.provide(NodeServices.layer)), + ); const [currentPublicAction] = currentContract.manifest.publicSurface.actions; assert.ok(currentPublicAction !== undefined); const root = await mkdtemp(path.join(os.tmpdir(), 'ontos-action-discovery-')); try { 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 incomplete: typeof deriveOntosModuleDeploymentContract = () => + Effect.succeed({ + ...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.ok(Schema.is(ActionAuthorizationProvisioningError)(incompleteError)); 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' }], + 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 = await rejectionOf(discoverCurrentActionKeys(root, duplicate)); - assert.ok(duplicateError instanceof ActionAuthorizationProvisioningError); + assert.ok(Schema.is(ActionAuthorizationProvisioningError)(duplicateError)); assert.equal(duplicateError.code, 'action_authorization_discovery_failed'); await writeInventory(root, [vertical, vertical]); diff --git a/app/scripts/tests/quality-audit-model.test.mts b/app/scripts/tests/quality-audit-model.test.mts index 0a0ffc0f5..39cabace6 100644 --- a/app/scripts/tests/quality-audit-model.test.mts +++ b/app/scripts/tests/quality-audit-model.test.mts @@ -215,7 +215,9 @@ await test('real pinned Knip models exact consumers and preserves neighboring fi const model = await runEffectTestPromise( buildKnipModel(root, base, consumerPath).pipe(Effect.provide(NodeServices.layer)), ); - const run = await runPinnedKnip(root, consumerPath, model); + const run = await runEffectTestPromise( + runPinnedKnip(root, consumerPath, model).pipe(Effect.provide(NodeServices.layer)), + ); assert.equal(run.status, 1, `${run.stdout}\n${run.stderr}`); assert.equal(run.stderr, ''); const report = await runEffectTestPromise( @@ -461,7 +463,9 @@ await test('vendor ownership rejects a different installed copy and accepts the ); 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); + const run = await runEffectTestPromise( + runPinnedKnip(root, consumerPath, differentCopies).pipe(Effect.provide(NodeServices.layer)), + ); assert.equal(run.status, 1, run.stderr); const report = Schema.decodeUnknownSync(Schema.fromJsonString(ReportSchema))(run.stdout); assert.ok( diff --git a/app/scripts/tests/quality-audit-runtime-model.test.mts b/app/scripts/tests/quality-audit-runtime-model.test.mts index 4dea1cae8..89ec658ac 100644 --- a/app/scripts/tests/quality-audit-runtime-model.test.mts +++ b/app/scripts/tests/quality-audit-runtime-model.test.mts @@ -275,7 +275,9 @@ await test('real Knip keeps unused neighboring files, dependency names and expor consumerPath, ).pipe(Effect.provide(NodeServices.layer)), ); - const run = await runPinnedKnip(root, consumerPath, model); + const run = await runEffectTestPromise( + runPinnedKnip(root, consumerPath, model).pipe(Effect.provide(NodeServices.layer)), + ); assert.equal(run.error, undefined); assert.ok(run.status === 0 || run.status === 1, run.stderr); const report = await runEffectTestPromise( 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/validate-ultramodern-workspace.mts b/app/scripts/validate-ultramodern-workspace.mts index 0eb0f4677..e9eb24dcc 100644 --- a/app/scripts/validate-ultramodern-workspace.mts +++ b/app/scripts/validate-ultramodern-workspace.mts @@ -2593,6 +2593,7 @@ interface TsConfig { const BuildArtifactSchema = Schema.Struct({ deliveryUnit: Schema.optionalKey(DeliveryUnitSchema), }); +// oxlint-disable-next-line effect-native/no-promise-shaped-port -- The dynamically loaded @vercel/nft SDK owns this Promise signature. type NodeFileTrace = ( files: readonly string[], options: { readonly base: string; readonly log: boolean; readonly processCwd: string }, diff --git a/app/tools/oxlint/effect-native/README.md b/app/tools/oxlint/effect-native/README.md index 6b3e09236..53e102faf 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 @@ -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,15 @@ 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. 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..5572b22a9 --- /dev/null +++ b/app/tools/oxlint/effect-native/repository-policy.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'oxlint'; + +/** The operator/discriminant policy also covers tooling and root configuration files. */ +export default defineConfig({ + jsPlugins: [{ name: 'effect-native', specifier: './index.ts' }], + categories: { correctness: 'off' }, + ignorePatterns: [ + '**/node_modules/**', + '**/dist/**', + '**/build/**', + '**/.output/**', + '**/dist-cloudflare/**', + '**/.modern-js/**', + '**/@mf-types/**', + '**/repos/**', + '**/tools/oxlint/**/tests/fixtures/**', + ], + rules: { + 'effect-native/no-instanceof': 'error', + 'effect-native/no-manual-tag-comparison': ['error', { include: ['**'], adtTags: [] }], + }, +}); 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 5742b2dad..736540654 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`/ @@ -631,6 +627,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 ' + @@ -707,6 +705,29 @@ 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); + // Follow projections and boolean guards; object literals remain complete contract assertions. + if (expression.type === 'ObjectExpression' || expression.type === 'TemplateLiteral') + return null; + for (const [key, value] of Object.entries(expression)) { + if (key === 'parent' || key === 'typeAnnotation') continue; + const children = Array.isArray(value) ? value : [value]; + for (const child of children) { + if (typeof child !== 'object' || child === null || !('type' in child)) continue; + const found = comparedTag(child as ESTree.Node, 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); @@ -731,6 +752,10 @@ export const rule = defineRule({ }; return { + SwitchStatement(node) { + if (tagOf(node.discriminant) === null || suppressed(node)) return; + context.report({ node, messageId: 'tagSwitch' }); + }, BinaryExpression(node) { if ((node.left as ESTree.Node).type === 'PrivateIdentifier') return; @@ -770,8 +795,6 @@ export const rule = defineRule({ other = node.left; } if (reference === null) return; - // `a._tag === b._tag` is an identity test, not a case analysis over a closed vocabulary. - if (tagOf(other) !== null) return; const tag = asStringLiteral(other); // Effect's own ADT tags belong to `no-raw-effect-adt-tag-check`; `allowTags` is the escape hatch. @@ -825,7 +848,6 @@ export const rule = defineRule({ other = first; } if (reference === null) return; - if (tagOf(other) !== null) return; const literal = asStringLiteral(other); if (literal !== null && exempt.has(literal)) return; if (suppressed(node)) return; @@ -846,6 +868,48 @@ export const rule = defineRule({ if (method === null) return; const receiver = callee.object as ESTree.Node; + // Assertions are comparisons too, including tag projections in arrays and aliased values. + const assertionMethods = new Set([ + 'equal', + 'strictEqual', + 'notEqual', + 'notStrictEqual', + 'deepEqual', + 'deepStrictEqual', + 'notDeepEqual', + 'notDeepStrictEqual', + 'toBe', + 'toEqual', + 'toStrictEqual', + 'toContain', + 'toContainEqual', + 'toMatch', + 'match', + 'doesNotMatch', + ]); + if (assertionMethods.has(method)) { + const compared = [...node.arguments]; + let subject = unwrap(receiver); + while (subject.type === 'MemberExpression') + subject = unwrap(subject.object as ESTree.Node); + if (subject.type === 'CallExpression') compared.push(...subject.arguments); + for (const argument of compared) { + if (argument.type === 'SpreadElement') continue; + const reference = comparedTag(argument); + if (reference === null) continue; + if (suppressed(node)) return; + context.report({ + node, + messageId: 'tagEqualityCall', + data: { + callee: describe(context, node.callee as ESTree.Node), + text: referenceText(context, reference), + }, + }); + return; + } + } + // `error._tag.startsWith('Contacts')`, `String(error._tag).endsWith('Problem')`. if (STRING_PROBES.has(method)) { const reference = tagOf(receiver); 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 d6106ca7d..be173ad8a 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 @@ -22,17 +22,15 @@ import { globToRegExp, isTestFile, normalisePath } from '../shared/paths.ts'; /** Fixture files live at `tools/oxlint//tests/fixtures//{valid,invalid}/`. */ const FIXTURE_PREFIX = /^tools\/oxlint\/[^/]+\/tests\/fixtures\/[^/]+\/(?:valid|invalid)\//u; -const DEFAULT_INCLUDE = ['apps/**', 'verticals/**', 'packages/**']; +const DEFAULT_INCLUDE = ['apps/**', 'verticals/**', 'packages/**', 'scripts/**']; 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']; @@ -93,8 +91,8 @@ function readOptions(context: Context): RuleOptions { 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), @@ -253,8 +251,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, @@ -460,6 +458,58 @@ export const rule = defineRule({ return false; }; + /** 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) { + 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)(?:\.|$))/u.test( + imported(call.callee) ?? '', + ) + ) + return true; + } + return false; + }; + + /** Only the actual thunk forwarded to Effect's foreign Promise conversion has that contract. */ + const isForeignThunkParameter = (node: any): boolean => { + let current = node; + while (current?.parent && current.parent.type !== 'Identifier') current = current.parent; + const parameter = current?.parent; + if (!parameter || !FUNCTION_TYPES.has(parameter.parent?.type)) 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((ref: any) => { + let value = ref.identifier; + if ( + value.parent?.type === 'CallExpression' && + value.parent.callee === value && + atDriverEdge(value.parent) + ) + return true; + if ( + value.parent?.type === 'Property' && + value.parent.value === value && + value.parent.key.name === 'try' + ) + value = value.parent.parent; + const call = value.parent; + return ( + call?.type === 'CallExpression' && + call.arguments.includes(value) && + isPromiseBoundaryCall(call) + ); + }) + ); + }; + /** Better Auth owns this exact hook signature, not arbitrary services nested in its options. */ const atAuthHook = (node: any): boolean => { let current = node; @@ -517,14 +567,27 @@ export const rule = defineRule({ annotation: ESTree.TSTypeAnnotation | null | undefined, ): string | null => { if (annotation === null || annotation === undefined) return null; - const resolve = (raw: any, seen = new Set()): string | null => { + interface TypeBinding { + readonly node: any; + readonly substitutions: ReadonlyMap; + } + const resolve = ( + raw: any, + seen = new Set(), + substitutions: ReadonlyMap = new Map(), + ): string | null => { if (!raw || seen.has(raw)) return null; seen.add(raw); if (raw.type === 'TSTypeAnnotation' || raw.type === 'TSParenthesizedType') - return resolve(raw.typeAnnotation, seen); + return resolve(raw.typeAnnotation, seen, substitutions); + if (raw.type === 'TSTypeParameter') + return ( + resolve(raw.constraint, new Set(seen), substitutions) ?? + resolve(raw.default, new Set(seen), substitutions) + ); if (raw.type === 'TSUnionType' || raw.type === 'TSIntersectionType') { for (const item of raw.types) { - const result = resolve(item, new Set(seen)); + const result = resolve(item, new Set(seen), substitutions); if (result) return result; } return null; @@ -542,10 +605,28 @@ export const rule = defineRule({ return `${name}<…>`; if (names.length !== 1) return null; const variable = variableFor(raw.typeName, name); + const bound = substitutions.get(variable); + if (bound) return resolve(bound.node, seen, bound.substitutions); const alias = variable?.defs.find( (d: any) => d.node.type === 'TSTypeAliasDeclaration', )?.node; - if (alias) return resolve(alias.typeAnnotation, seen); + if (alias) { + const applied = new Map(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 ? substitutions : applied, + }); + } + return resolve(alias.typeAnnotation, seen, applied); + } + const parameter = variable?.defs.find((d: any) => d.node.type === 'TSTypeParameter')?.node; + if (parameter) return resolve(parameter, seen, substitutions); if (variable?.defs.length || !options.promiseTypes.includes(name)) return null; return `${name}<…>`; }; @@ -605,9 +686,11 @@ 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); }; @@ -620,7 +703,17 @@ export const rule = defineRule({ let parent = parentOf(current); while ( parent && - ['TSUnionType', 'TSIntersectionType', 'TSParenthesizedType'].includes(parent.type) + [ + 'TSUnionType', + 'TSIntersectionType', + 'TSParenthesizedType', + 'TSTypeParameterInstantiation', + 'TSTypeReference', + 'TSArrayType', + 'TSOptionalType', + 'TSRestType', + 'TSTypeOperator', + ].includes(parent.type) ) { current = parent; parent = parentOf(current); @@ -632,7 +725,27 @@ export const rule = defineRule({ 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'; + if (owner.type === 'Identifier' || owner.type === 'RestElement') { + let declaration = parentOf(owner); + if ( + declaration?.type === 'AssignmentPattern' || + declaration?.type === 'TSParameterProperty' || + declaration?.type === 'RestElement' + ) + declaration = parentOf(declaration); + return ( + declaration !== null && + (declaration.type === 'VariableDeclarator' || + FUNCTION_TYPES.has(declaration.type) || + [ + 'TSDeclareFunction', + 'TSEmptyBodyFunctionExpression', + 'TSFunctionType', + 'TSMethodSignature', + 'TSCallSignatureDeclaration', + ].includes(declaration.type)) + ); + } return ( owner.type === 'TSPropertySignature' || owner.type === 'TSIndexSignature' || @@ -719,21 +832,34 @@ export const rule = defineRule({ return true; const owner = namedOwner(fn); const id = (owner as any).id; - if (!id || id.type !== 'Identifier') return false; + if (!id || id.type !== 'Identifier') { + for (let current = fn.parent; current; current = current.parent) { + if (FUNCTION_TYPES.has(current.type)) return exemptHelper(current, new Set(seen)); + } + return false; + } if ( (owner as any).parent?.type === 'ExportNamedDeclaration' || (owner as any).parent?.parent?.type === 'ExportNamedDeclaration' ) return false; const variable = variableFor(id, id.name); - const refs = variable?.references.filter((r: any) => r.isRead()) ?? []; + const refs = (variable?.references.filter((r: any) => r.isRead()) ?? []).filter( + (ref: any) => { + for (let current = ref.identifier.parent; current; current = current.parent) { + if (current === fn) return false; + } + return true; + }, + ); if (!refs.length) return false; return refs.every((ref: any) => { 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) + if (atDriverEdge(ref.identifier) || atTestBoundary(ref.identifier)) return true; + for (let current = call; current; current = current.parent) { + if (current === fn) return true; if (FUNCTION_TYPES.has(current.type)) return exemptHelper(current, new Set(seen)); + } return false; }); }; @@ -750,7 +876,8 @@ export const rule = defineRule({ if (!options.includeFunctionDeclarations) return; if (!isModuleScopeFunction(node)) return; } else if (!isImplementationPosition(node)) return; - if (atDriverEdge(node) || atAuthHook(node) || exemptHelper(node)) return; + if (atDriverEdge(node) || atAuthHook(node) || atTestBoundary(node) || exemptHelper(node)) + return; if (insideReportedFunction(node)) return; const member = nameOf(node); // Framework router entrypoints (`loader`, `action`, ...) are forced to return a Promise. @@ -787,6 +914,7 @@ export const rule = defineRule({ const wrapper = promiseReference(node.returnType); if (wrapper === null) return; if (!isPortFunctionTypePosition(node as unknown as AnyNode)) return; + if (isForeignThunkParameter(node) || atTestBoundary(node)) return; report(node, 'promisePort', { member: nameOf(node as unknown as AnyNode), wrapper }); }, TSPropertySignature: (node: ESTree.TSPropertySignature) => { 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/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/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..8a82f3eae --- /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,13 @@ +// expect-count: 9 +import assert, { strictEqual as equal } from 'node:assert/strict'; +import { expect } from '@rstest/core'; +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(option._tag, 'Some'); +assert.equal(guard && failure._tag, '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/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..7fdcb19da --- /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,8 @@ +import { Predicate, Schema, Exit, Match } 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(error, { _tag: 'Missing', message: 'gone' }); +assert.fail(`Unexpected failure ${error._tag}`); +expect(Predicate.isTagged(error, 'Missing')).toBe(true); 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/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-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..2fde48c8c --- /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,12 @@ +// expect-count: 7 +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; 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/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/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/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..517869754 --- /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,9 @@ +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; } 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/repository-policy.test.mts b/app/tools/oxlint/effect-native/tests/repository-policy.test.mts new file mode 100644 index 000000000..662a56e6e --- /dev/null +++ b/app/tools/oxlint/effect-native/tests/repository-policy.test.mts @@ -0,0 +1,19 @@ +import assert from 'node:assert/strict'; +import { join } from 'node:path'; +import { test } from 'node:test'; +import { appRoot, pluginDirectory, runOxlint } from './oxlint.mts'; + +test('all repository source, including tools and root configuration, follows the Effect discrimination policy', () => { + const run = runOxlint( + join(pluginDirectory, 'repository-policy.config.ts'), + ['.', '--ignore-pattern', 'tools/oxlint/**/tests/fixtures/**'], + appRoot, + ); + assert.deepEqual( + run.diagnostics.map( + ({ filename, labels, code }) => `${filename}:${labels[0]?.span.line} ${code}`, + ), + [], + ); + assert.equal(run.exitCode, 0); +}); diff --git a/app/verticals/party-registry/scripts/prepare-contacts-migration.mts b/app/verticals/party-registry/scripts/prepare-contacts-migration.mts index 9787e5980..de88d81bf 100644 --- a/app/verticals/party-registry/scripts/prepare-contacts-migration.mts +++ b/app/verticals/party-registry/scripts/prepare-contacts-migration.mts @@ -44,26 +44,19 @@ 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)), + try: async () => await 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 +67,7 @@ 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)), + try: async () => await client.connect(), }).pipe( Effect.timeoutOrElse({ duration: POSTGRES_OPERATION_TIMEOUT, @@ -88,7 +81,7 @@ 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)), + try: async () => await client.end(), }).pipe( Effect.timeoutOrElse({ duration: POSTGRES_OPERATION_TIMEOUT, From 0ca264e244c71436fb5c51112b6ebbf9f622e28c Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Mon, 7 Sep 2026 23:32:10 +0200 Subject: [PATCH 03/38] Keep foreign SDK callbacks compatible with Effect diagnostics --- .../api/modules/installed-module-catalog.ts | 6 +- .../search-worker-snapshot.test.ts | 131 ++++++++++++------ .../tests/resource-generator.test.mts | 21 +-- .../scripts/prepare-contacts-migration.mts | 9 +- 4 files changed, 107 insertions(+), 60 deletions(-) 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 140c58667..fd46dba05 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 @@ -137,7 +137,8 @@ const readResponseChunks = ( > => Effect.tryPromise({ catch: unavailable, - try: async () => await reader.read(), + // 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) => { @@ -195,7 +196,8 @@ const readBoundedContract = Effect.fn('ShellInstalledModuleCatalog.readBoundedCo const text = yield* Effect.acquireUseRelease( Effect.succeed(reader), (bodyReader) => collectResponseBody(bodyReader, maxBytes, timeout), - (bodyReader) => Effect.promise(async () => await bodyReader.cancel()).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/packages/core-runtime/tests/integration/search-worker-snapshot.test.ts b/app/packages/core-runtime/tests/integration/search-worker-snapshot.test.ts index b709f6b47..f491e5cb2 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 @@ -66,10 +66,18 @@ const readSnapshotPosition = ( ); const beginTransaction = (client: PoolClient) => - Effect.tryPromise(async () => await client.query('begin')); + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => client.query('begin'), + }); const commitTransaction = (client: PoolClient) => - Effect.tryPromise(async () => await client.query('commit')); + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => client.query('commit'), + }); const insertPendingEvent = ( client: PoolClient, @@ -77,13 +85,15 @@ const insertPendingEvent = ( tenantId: string, pendingSubjectId: string, ) => - Effect.tryPromise( - async () => - await client.query( + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + 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; @@ -108,59 +118,79 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { const insertEvent = (id: string) => Effect.gen(function* insertDomainEvent() { const subjectId = yield* crypto.randomUUIDv4; - const result = yield* Effect.tryPromise( - async () => - await admin.query<{ tenant_sequence_no: string }>( + const result = yield* Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + 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] = result.rows; assert.ok(row); return row.tenant_sequence_no; }); const cleanup = Effect.gen(function* cleanupWorkerSnapshot() { - yield* Effect.tryPromise( - async () => - await admin.query('delete from core.search_projection_generations where tenant_id = $1', [ + yield* Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => + admin.query('delete from core.search_projection_generations where tenant_id = $1', [ tenantId, ]), - ); - yield* Effect.tryPromise( - async () => - await admin.query('delete from core.domain_events where tenant_id = $1', [tenantId]), - ); - yield* Effect.tryPromise( - async () => - await admin.query('delete from core.legal_entities where tenant_id = $1', [tenantId]), - ); - yield* Effect.tryPromise( - async () => await admin.query('delete from core.tenants where tenant_id = $1', [tenantId]), - ); + }); + yield* Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => admin.query('delete from core.domain_events where tenant_id = $1', [tenantId]), + }); + yield* Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => admin.query('delete from core.legal_entities where tenant_id = $1', [tenantId]), + }); + yield* Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => admin.query('delete from core.tenants where tenant_id = $1', [tenantId]), + }); yield* Effect.all( [ - Effect.tryPromise(async () => await admin.end()), - Effect.tryPromise(async () => await runtimePool.end()), + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => admin.end(), + }), + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => runtimePool.end(), + }), ], { concurrency: 'unbounded' }, ); }).pipe(Effect.orDie); yield* Effect.gen(function* exerciseWorkerSnapshots() { - yield* Effect.tryPromise( - async () => - await admin.query( + yield* Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + 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( - async () => - await admin.query( + }); + yield* Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + 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], @@ -237,15 +267,21 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { (blocked) => blocked ? Effect.succeed(true) - : Effect.tryPromise(async () => await admin.query('select pg_sleep(0.01)')).pipe( + : Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => admin.query('select pg_sleep(0.01)'), + }).pipe( Effect.andThen( - Effect.tryPromise( - async () => - await admin.query<{ count: number }>( + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + 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), ), @@ -284,13 +320,18 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { }); }); yield* Effect.acquireUseRelease( - Effect.tryPromise(async () => await admin.connect()), + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => admin.connect(), + }), lateCommitSnapshot, (pending) => - Effect.tryPromise(async () => await pending.query('rollback')).pipe( - Effect.orDie, - Effect.ensuring(Effect.sync(() => pending.release())), - ), + Effect.tryPromise({ + catch: (cause) => new Cause.UnknownError(cause), + // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => pending.query('rollback'), + }).pipe(Effect.orDie, Effect.ensuring(Effect.sync(() => pending.release()))), ); }).pipe(Effect.ensuring(cleanup)); }); diff --git a/app/scripts/scaffolding/tests/resource-generator.test.mts b/app/scripts/scaffolding/tests/resource-generator.test.mts index e2655da7f..6d94e7176 100644 --- a/app/scripts/scaffolding/tests/resource-generator.test.mts +++ b/app/scripts/scaffolding/tests/resource-generator.test.mts @@ -18,6 +18,7 @@ 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}`; @@ -161,7 +162,7 @@ export declare const ShellSearchContributionSchema: Schema.Codec Promise): Promise const scaffoldResource = async (root: string, resource = resourceName) => await runEffectTestPromise( - runScaffoldEffect('resource', ['--vertical', verticalName, '--resource', resource], { + runScaffoldEffect('resource', [verticalFlag, verticalName, '--resource', resource], { workspaceRoot: root, }).pipe(Effect.provide(NodeServices.layer)), ); @@ -374,11 +375,11 @@ void test('waits for an interrupted Codesmith write before removing its scoped o materialsManager: smith.materialsManager, outputPath: root, }); - const started = Promise.withResolvers(); - const release = Promise.withResolvers(); + const started = Promise.withResolvers(); + const release = Promise.withResolvers(); const events: string[] = []; context.mock.method(core.output, 'fs', async () => { - started.resolve(); + started.resolve(null); await release.promise; await mkdir(root, { recursive: true }); await writeFile(path.join(root, 'generated.ts'), 'export {};'); @@ -392,14 +393,14 @@ void test('waits for an interrupted Codesmith write before removing its scoped o Effect.gen(function* writeScopedOutput() { yield* Effect.addFinalizer(() => fileSystem - .remove(root, { recursive: true, force: true }) + .remove(root, { force: true, recursive: true }) .pipe(Effect.orDie, Effect.andThen(Effect.sync(() => events.push('cleanup')))), ); yield* applyMutationPlanEffect(core, { mutations: [ - { kind: 'create', path: path.join(root, 'generated.ts'), content: 'export {};' }, + { content: 'export {};', kind: 'create', path: path.join(root, 'generated.ts') }, ], - result: undefined, + result: null, }); }), ), @@ -408,7 +409,7 @@ void test('waits for an interrupted Codesmith write before removing its scoped o const interruption = yield* Effect.forkChild(Fiber.interrupt(worker)); yield* Effect.yieldNow; assert.deepEqual(events, []); - release.resolve(); + release.resolve(null); yield* Fiber.join(interruption); assert.deepEqual(events, ['write', 'cleanup']); assert.equal(yield* fileSystem.exists(root), false); @@ -421,7 +422,7 @@ void test('reports synchronous malformed-owner validation through the typed comm await writeFile(path.join(root, verticalPackagePath), '[]'); const before = await snapshotTree(root); const failure = await runEffectTestPromise( - runScaffoldEffect('resource', ['--vertical', verticalName, '--resource', resourceName], { + runScaffoldEffect('resource', [verticalFlag, verticalName, '--resource', resourceName], { workspaceRoot: root, }).pipe(Effect.flip, Effect.provide(NodeServices.layer)), ); diff --git a/app/verticals/party-registry/scripts/prepare-contacts-migration.mts b/app/verticals/party-registry/scripts/prepare-contacts-migration.mts index de88d81bf..a5c35d355 100644 --- a/app/verticals/party-registry/scripts/prepare-contacts-migration.mts +++ b/app/verticals/party-registry/scripts/prepare-contacts-migration.mts @@ -50,7 +50,8 @@ const query = ( ): Effect.Effect, ContactsMigrationError> => Effect.tryPromise({ catch: (cause) => databaseFailure(`PostgreSQL query failed: ${text}`, cause), - try: async () => await client.query(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, @@ -67,7 +68,8 @@ const connect = Effect.fn('ContactsMigration.connect')(function* connectEffect( }); yield* Effect.tryPromise({ catch: (cause) => databaseFailure('Unable to connect to PostgreSQL', cause), - try: async () => await client.connect(), + // 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, @@ -81,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: async () => await client.end(), + // 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, From e7aff4cb408e9f2a2597ea1cec7e3080da718cc1 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Mon, 7 Sep 2026 23:39:52 +0200 Subject: [PATCH 04/38] Close test-local Promise and imported tag assertion bypasses --- .../tests/integration/auth-runtime.test.ts | 8 ++- .../tests/unit/auth-db-client.test.ts | 1 + .../tests/unit/impersonation-service.test.ts | 10 +++ .../unit/module-entrypoint-loader.test.ts | 6 ++ .../tests/unit/routes/home/loader.test.ts | 1 + .../core-runtime/tests/unit/config.test.ts | 1 + app/scripts/tests/api-only-tooling.test.mts | 72 +++++++++---------- .../rules/no-manual-tag-comparison.ts | 46 ++++++++++-- .../rules/no-promise-shaped-port.ts | 9 ++- .../tests/unit/assertions.test.ts | 8 ++- .../core-runtime/src/native-assertions.ts | 4 ++ .../tests/unit/test-local-ports.test.ts | 11 +++ 12 files changed, 125 insertions(+), 52 deletions(-) create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/tests/unit/test-local-ports.test.ts 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 3a96a5ede..a6e624644 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 @@ -51,6 +51,10 @@ import { AuthenticationService, makeAuthenticationService } from '../../api/auth import { makeShellAuthenticationApiRuntime } from '../../api/index.ts'; import { renderActionPrincipalServer } from '../../../../scripts/scaffolding/microvertical-action-boundary/scaffold.mts'; +type AuthenticationRuntimeHandler = ReturnType< + ReturnType['createHandler'] +>; + const nativeDatabaseScope = runNativeSync(NativeScope.make()); const email = 'better-auth-runtime@example.test'; @@ -240,7 +244,7 @@ void test('creates, resolves, persists, revokes, and signs out a Better Auth ses TenantModuleStateService, makeTenantModuleStateService({ executor: coreDatabase }), ); - const handlers: { readonly dispose: () => Promise }[] = []; + const handlers: AuthenticationRuntimeHandler[] = []; const generatedFixtureRoot = await mkdtemp(path.join(tmpdir(), 'ontos-auth-runtime-')); const cleanup = async () => { @@ -1138,7 +1142,7 @@ void test('selects, lists, switches, revalidates, and upgrades a multi-tenant se TenantModuleStateService, makeTenantModuleStateService({ executor: coreDatabase }), ); - const handlers: { readonly dispose: () => Promise }[] = []; + const handlers: AuthenticationRuntimeHandler[] = []; const cleanup = async () => { await runEffectTestPromise( 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..aa8a73ffb 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 @@ -7,6 +7,7 @@ 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 = { + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements pg Pool.end's foreign Promise API. async end(...args: []) { recorded.push(args.length); }, 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 ba81be304..5cc2aadd2 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 @@ -140,6 +140,7 @@ const supportRecoveryPrincipal: SupportRecoveryPrincipalContextResolverService = const provider = (impersonated: boolean): SupportAuthProvider => ({ api: { + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. getSession: async () => ({ headers: new Headers(), response: { @@ -159,9 +160,11 @@ const provider = (impersonated: boolean): SupportAuthProvider => ({ user: { id: 'original-provider-user' }, }, }), + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. impersonateUser: async () => { throw new Error('not used'); }, + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. stopImpersonating: async () => { const headers = new Headers(); headers.append('set-cookie', 'session=restored; Path=/; HttpOnly'); @@ -223,6 +226,7 @@ test('preserves definite requested-checkpoint errors for their declared HTTP map }), configuration, provider: makeSupportAuthProviderDouble({ + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. impersonateUser: async () => { providerCalls += 1; throw new Error('must not create a session'); @@ -293,6 +297,7 @@ test('removes the provider session and recovery when started evidence cannot com }), configuration, provider: makeSupportAuthProviderDouble({ + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. impersonateUser: async () => ({ headers: new Headers(), response: { session: { id: impersonationSessionId } }, @@ -531,6 +536,7 @@ test('persists and completes stopped evidence on the first stop after impersonat authentication: makeAuthenticationServiceDouble(), configuration, provider: makeSupportAuthProviderDouble({ + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. getSession: async () => ({ headers: new Headers(), response: null }), }), resolver: makePrincipalResolverDouble(), @@ -625,6 +631,7 @@ test('restores the original session and stopped checkpoint after the provider re authentication: makeAuthenticationServiceDouble(), configuration, provider: makeSupportAuthProviderDouble({ + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. getSession: async () => ({ headers: new Headers(), response: null }), }), resolver: makePrincipalResolverDouble(), @@ -702,6 +709,7 @@ test('completes stopped recovery when a lost response leaves only an expired ori authentication: makeAuthenticationServiceDouble(), configuration, provider: makeSupportAuthProviderDouble({ + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. getSession: async () => ({ headers: new Headers(), response: null }), }), resolver: makePrincipalResolverDouble(), @@ -757,6 +765,7 @@ test('clears a mismatched restored session and completes recovery from the recor configuration, provider: makeSupportAuthProviderDouble({ ...provider(true).api, + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. stopImpersonating: async () => { const headers = new Headers(); headers.append('set-cookie', 'better-auth.session_token=unexpected; Path=/; HttpOnly'); @@ -807,6 +816,7 @@ test('deletes the impersonation session and clears cookies when original restora configuration, provider: makeSupportAuthProviderDouble({ ...provider(true).api, + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. stopImpersonating: async () => { throw new Error('admin session expired'); }, 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 006edb262..3bb33605c 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 @@ -277,6 +277,7 @@ test( { identity: 'property-registry/page', isCompatible: compatibleRemoteModule, + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements the Module Federation import Promise boundary. load: async () => { throw new Error('remote unavailable'); }, @@ -326,6 +327,7 @@ test('never starts a queued load whose deadline expired before a permit became a Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ identity: `module-${index}/page`, isCompatible: compatibleRemoteModule, + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements the Module Federation import Promise boundary. load: async () => { const pending = Promise.withResolvers(); pendingLoads.push(pending); @@ -373,6 +375,7 @@ test('never starts an expired queued load when synchronous work delays deadline Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ identity: `module-${index}/page`, isCompatible: compatibleRemoteModule, + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements the Module Federation import Promise boundary. load: async () => { started.push(index); if (index === 0) { @@ -413,6 +416,7 @@ test( Array.from({ length: 12 }, (_, index) => ({ identity: `module-${index}/page`, isCompatible: compatibleRemoteModule, + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements the Module Federation import Promise boundary. load: async () => { started.push(index); if (started.length === MODULE_LOAD_CONCURRENCY) { @@ -456,6 +460,7 @@ test( Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ identity: `module-${index}/page`, isCompatible: compatibleRemoteModule, + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements the Module Federation import Promise boundary. load: async () => { events.push(`started-${index}`); if (index === MODULE_LOAD_CONCURRENCY - 1) { @@ -510,6 +515,7 @@ test( { identity: 'late-rejection/page', isCompatible: compatibleRemoteModule, + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements the Module Federation import Promise boundary. load: async () => { loadStarted.resolve(null); try { 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..2515e5a90 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 @@ -29,6 +29,7 @@ rstest.mock('../../../../src/api/auth-client.ts', () => ({ })); rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This double implements ManagedRuntime.runPromise at the browser execution boundary. runBrowserEffect: async (effect: Effect.Effect) => await runEffectTestPromise( effect.pipe( diff --git a/app/packages/core-runtime/tests/unit/config.test.ts b/app/packages/core-runtime/tests/unit/config.test.ts index 5c68dc752..df3d0570a 100644 --- a/app/packages/core-runtime/tests/unit/config.test.ts +++ b/app/packages/core-runtime/tests/unit/config.test.ts @@ -125,6 +125,7 @@ void test('finalizes the pool resource when its Effect scope closes', async () = await runEffectTestPromise( Effect.scoped( acquirePoolResource(() => ({ + // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements pg Pool.end's foreign Promise API. end: async () => { finalized = true; }, diff --git a/app/scripts/tests/api-only-tooling.test.mts b/app/scripts/tests/api-only-tooling.test.mts index 537f6c34c..55b15871d 100644 --- a/app/scripts/tests/api-only-tooling.test.mts +++ b/app/scripts/tests/api-only-tooling.test.mts @@ -52,13 +52,7 @@ 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[]; - }; -} +type ReleaseEnvelope = typeof ReleaseEnvelopeSchema.Type; interface ReleaseFramework { // oxlint-disable-next-line effect-native/no-promise-shaped-port -- Structural mirror of the installed Modern.js release-envelope SDK. @@ -255,20 +249,10 @@ const loadReleaseFramework = async (modulePath: string): Promise { - 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); - }, + emitFrameworkMicroVerticalReleaseEnvelope, + emitNodeStagedReleaseEnvelope, + verifyBuildOutputReleaseEnvelope, + verifyNodeReleaseEnvelopeStaging, }; }; @@ -359,11 +343,13 @@ const releaseFixture = async (context: TestContext) => { ), ); const emit = async () => - await releaseFramework.emitFrameworkMicroVerticalReleaseEnvelope({ - apiOnly: false, - distDirectory: root, - target: 'node', - }); + Schema.decodeUnknownSync(ReleaseEnvelopeSchema)( + await releaseFramework.emitFrameworkMicroVerticalReleaseEnvelope({ + apiOnly: false, + distDirectory: root, + target: 'node', + }), + ); return { artifact, emit, framework: releaseFramework, manifest, putJson, putText, root }; }; @@ -379,19 +365,23 @@ void test('empty MF producers retain complete build and Node staged release evid `ultramodern-release-envelope/framework-output.${extension}`, ), ); - const envelope = await framework.emitFrameworkMicroVerticalReleaseEnvelope({ - apiOnly: false, - distDirectory: fixture.root, - target: 'node', - }); + const envelope = Schema.decodeUnknownSync(ReleaseEnvelopeSchema)( + 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, - }); + const staged = Schema.decodeUnknownSync(ReleaseEnvelopeSchema)( + await framework.emitNodeStagedReleaseEnvelope({ + distDirectory: fixture.root, + outputDirectory: fixture.root, + }), + ); assert.ok(staged.surfaces.uiClient.includes(compiledUiAssetPath)); await framework.verifyNodeReleaseEnvelopeStaging({ outputDirectory: fixture.root }); }), @@ -422,11 +412,13 @@ void test('empty MF producers bind root-relative route assets when publicPath is `ultramodern-release-envelope/framework-output.${extension}`, ), ); - const envelope = await framework.emitFrameworkMicroVerticalReleaseEnvelope({ - apiOnly: false, - distDirectory: fixture.root, - target: 'node', - }); + const envelope = Schema.decodeUnknownSync(ReleaseEnvelopeSchema)( + await framework.emitFrameworkMicroVerticalReleaseEnvelope({ + apiOnly: false, + distDirectory: fixture.root, + target: 'node', + }), + ); assert.ok(envelope.surfaces.uiClient.includes(compiledUiAssetPath)); }), ); 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 736540654..643f78c5d 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 @@ -862,11 +862,41 @@ export const rule = defineRule({ return; } - const callee = unwrap(node.callee); - if (callee.type !== 'MemberExpression') return; - const method = memberPropertyName(callee); + let callee = unwrap(node.callee); + const seenCallees = new Set(); + while (callee.type === 'Identifier' && !seenCallees.has(callee)) { + seenCallees.add(callee); + const initialiser = constInitialiser(context, callee); + if (initialiser === null) break; + callee = unwrap(initialiser); + } + let method: string | null = null; + let receiver: ESTree.Node | null = null; + if (callee.type === 'MemberExpression') { + method = memberPropertyName(callee); + receiver = callee.object as ESTree.Node; + } else if (callee.type === 'Identifier') { + const variable = resolveVariable(context, callee.name, callee); + const definition = variable?.defs.find((entry) => entry.type === 'ImportBinding'); + const specifier = definition?.node as ESTree.Node | undefined; + if (specifier?.type === 'ImportSpecifier') { + const declaration = context.sourceCode.ast.body.find( + (statement) => + statement.type === 'ImportDeclaration' && + statement.specifiers.some((entry) => entry === specifier), + ); + if ( + declaration?.type === 'ImportDeclaration' && + /^(?:node:)?assert(?:\/strict)?$/u.test(declaration.source.value) + ) { + method = + specifier.imported.type === 'Identifier' + ? specifier.imported.name + : specifier.imported.value; + } + } + } if (method === null) return; - const receiver = callee.object as ESTree.Node; // Assertions are comparisons too, including tag projections in arrays and aliased values. const assertionMethods = new Set([ @@ -889,10 +919,10 @@ export const rule = defineRule({ ]); if (assertionMethods.has(method)) { const compared = [...node.arguments]; - let subject = unwrap(receiver); - while (subject.type === 'MemberExpression') + let subject = receiver === null ? null : unwrap(receiver); + while (subject?.type === 'MemberExpression') subject = unwrap(subject.object as ESTree.Node); - if (subject.type === 'CallExpression') compared.push(...subject.arguments); + if (subject?.type === 'CallExpression') compared.push(...subject.arguments); for (const argument of compared) { if (argument.type === 'SpreadElement') continue; const reference = comparedTag(argument); @@ -910,6 +940,8 @@ export const rule = defineRule({ } } + if (receiver === null) return; + // `error._tag.startsWith('Contacts')`, `String(error._tag).endsWith('Problem')`. if (STRING_PROBES.has(method)) { const reference = tagOf(receiver); 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 be173ad8a..8c51c0156 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 @@ -462,6 +462,7 @@ export const rule = defineRule({ 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' && @@ -823,6 +824,7 @@ export const rule = defineRule({ const exemptHelper = (fn: any, seen = new Set()): boolean => { if (seen.has(fn)) return false; seen.add(fn); + if (atTestBoundary(fn)) return true; const body = functionBody(fn); if (body?.type === 'ImportExpression') return true; if ( @@ -833,8 +835,11 @@ export const rule = defineRule({ const owner = namedOwner(fn); const id = (owner as any).id; if (!id || id.type !== 'Identifier') { - for (let current = fn.parent; current; current = current.parent) { - if (FUNCTION_TYPES.has(current.type)) return exemptHelper(current, new Set(seen)); + const call = fn.parent; + if (call?.type === 'CallExpression' && call.arguments.includes(fn)) { + for (let current = call.parent; current; current = current.parent) { + if (FUNCTION_TYPES.has(current.type)) return exemptHelper(current, new Set(seen)); + } } return false; } 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 index 8a82f3eae..f375f5b81 100644 --- 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 @@ -1,4 +1,4 @@ -// expect-count: 9 +// expect-count: 12 import assert, { strictEqual as equal } from 'node:assert/strict'; import { expect } from '@rstest/core'; assert.equal(error._tag, 'Missing'); @@ -11,3 +11,9 @@ expect(error._tag).not.toEqual('Missing'); expect(error._tag).resolves.toStrictEqual('Missing'); assert.equal(option._tag, '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'); 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 index 7fdcb19da..a0de06dad 100644 --- 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 @@ -6,3 +6,7 @@ assert.ok(Schema.is(Missing)(error)); assert.deepEqual(error, { _tag: 'Missing', 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'); +} 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; +}); From 0b735d1ec90a6cf4c11f886cb927b711dcf489a9 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Mon, 7 Sep 2026 23:51:37 +0200 Subject: [PATCH 05/38] Respect tag exemptions in assertion comparisons --- .../rules/no-manual-tag-comparison.ts | 13 +++-- .../no-manual-tag-comparison/.oxlintrc.json | 49 ++++++++++++++++--- .../core-runtime/src/configured-assertions.ts | 6 +++ .../tests/unit/assertions.test.ts | 5 +- .../valid/packages/core-runtime/src/adt.ts | 7 +++ .../core-runtime/src/configured-assertions.ts | 6 +++ 6 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/configured-assertions.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/configured-assertions.ts 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 643f78c5d..a761700a8 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 @@ -918,15 +918,22 @@ export const rule = defineRule({ 'doesNotMatch', ]); if (assertionMethods.has(method)) { - const compared = [...node.arguments]; + let compared = node.arguments.slice(0, 2); let subject = receiver === null ? null : unwrap(receiver); while (subject?.type === 'MemberExpression') subject = unwrap(subject.object as ESTree.Node); - if (subject?.type === 'CallExpression') compared.push(...subject.arguments); - for (const argument of compared) { + if (subject?.type === 'CallExpression') + compared = [...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) continue; + const other = compared[index === 0 ? 1 : 0]; + if (other !== undefined && other.type !== 'SpreadElement') { + const literal = asStringLiteral(other); + if (literal !== null && exempt.has(literal)) continue; + if (containerIsAdtOnly(other)) continue; + } if (suppressed(node)) return; context.report({ node, 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/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..8186aca8b --- /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,6 @@ +// expect-count: 3 +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']); 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 index f375f5b81..936afdf1d 100644 --- 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 @@ -1,4 +1,4 @@ -// expect-count: 12 +// expect-count: 13 import assert, { strictEqual as equal } from 'node:assert/strict'; import { expect } from '@rstest/core'; assert.equal(error._tag, 'Missing'); @@ -9,7 +9,8 @@ assert.deepEqual(tags, ['Missing']); expect(error._tag).toBe('Missing'); expect(error._tag).not.toEqual('Missing'); expect(error._tag).resolves.toStrictEqual('Missing'); -assert.equal(option._tag, 'Some'); +assert.equal(error._tag, 'Missing', 'Some'); +expect(error._tag).toBe('Missing', 'Some'); assert.equal(guard && failure._tag, 'Missing'); equal(error._tag, 'Missing'); 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..afd22a0e9 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,8 @@ 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']); 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..6f264780b --- /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,6 @@ +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']); From be56a69f326e86b20f4839bf3ad77ea716ee461c Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 00:13:03 +0200 Subject: [PATCH 06/38] Verify assertion imports and honor switch tag exemptions --- .../rules/no-manual-tag-comparison.ts | 121 +++++++++++++----- .../core-runtime/src/configured-assertions.ts | 7 +- .../tests/unit/assertions.test.ts | 12 +- .../valid/packages/core-runtime/src/adt.ts | 6 + .../core-runtime/src/configured-assertions.ts | 5 + .../core-runtime/src/native-assertions.ts | 9 ++ 6 files changed, 124 insertions(+), 36 deletions(-) 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 a761700a8..4f0c1ecee 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 @@ -293,6 +293,77 @@ function constInitialiser(context: Context, node: ESTree.Node): ESTree.Node | nu return declarator.init ?? null; } +/** Resolve assertion imports through lexical bindings, aliases, and matcher modifiers. */ +function assertionCall( + context: Context, + call: ESTree.CallExpression, +): { method: string; subject: ESTree.CallExpression | null } | 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 initialiser = constInitialiser(context, expression); + if (initialiser !== null) { + expression = unwrap(initialiser); + 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') { + if (subject !== null) return null; + subject = expression; + expression = unwrap(expression.callee); + continue; + } + 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; + if (specifier.type === 'ImportSpecifier') + members.unshift( + specifier.imported.type === 'Identifier' + ? specifier.imported.name + : specifier.imported.value, + ); + const source = declaration.source.value; + if (/^(?:node:)?assert(?:\/strict)?$/u.test(source)) { + 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; + } + if ( + !['@rstest/core', '@app/effect-rstest', 'vitest', '@jest/globals', 'expect'].includes(source) + ) + return null; + if (specifier.type === 'ImportDefaultSpecifier' && source === 'expect') + members.unshift('expect'); + if (subject === null || 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 }; + } + 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. @@ -754,6 +825,15 @@ export const rule = defineRule({ 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(node) { @@ -870,33 +950,11 @@ export const rule = defineRule({ if (initialiser === null) break; callee = unwrap(initialiser); } - let method: string | null = null; - let receiver: ESTree.Node | null = null; - if (callee.type === 'MemberExpression') { - method = memberPropertyName(callee); - receiver = callee.object as ESTree.Node; - } else if (callee.type === 'Identifier') { - const variable = resolveVariable(context, callee.name, callee); - const definition = variable?.defs.find((entry) => entry.type === 'ImportBinding'); - const specifier = definition?.node as ESTree.Node | undefined; - if (specifier?.type === 'ImportSpecifier') { - const declaration = context.sourceCode.ast.body.find( - (statement) => - statement.type === 'ImportDeclaration' && - statement.specifiers.some((entry) => entry === specifier), - ); - if ( - declaration?.type === 'ImportDeclaration' && - /^(?:node:)?assert(?:\/strict)?$/u.test(declaration.source.value) - ) { - method = - specifier.imported.type === 'Identifier' - ? specifier.imported.name - : specifier.imported.value; - } - } - } - if (method === null) return; + 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; // Assertions are comparisons too, including tag projections in arrays and aliased values. const assertionMethods = new Set([ @@ -917,13 +975,10 @@ export const rule = defineRule({ 'match', 'doesNotMatch', ]); - if (assertionMethods.has(method)) { + if (assertion !== null && assertionMethods.has(assertion.method)) { let compared = node.arguments.slice(0, 2); - let subject = receiver === null ? null : unwrap(receiver); - while (subject?.type === 'MemberExpression') - subject = unwrap(subject.object as ESTree.Node); - if (subject?.type === 'CallExpression') - compared = [...subject.arguments.slice(0, 1), ...node.arguments.slice(0, 1)]; + if (assertion.subject !== null) + compared = [...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); 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 index 8186aca8b..fd2d74014 100644 --- 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 @@ -1,6 +1,11 @@ -// expect-count: 3 +// expect-count: 4 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; +} 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 index 936afdf1d..f6314e7c2 100644 --- 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 @@ -1,6 +1,9 @@ -// expect-count: 13 +// expect-count: 17 import assert, { strictEqual as equal } from 'node:assert/strict'; -import { expect } from '@rstest/core'; +import { expect, expect as check } from '@rstest/core'; +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']); @@ -18,3 +21,8 @@ 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'); 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 afd22a0e9..f8d05b9cf 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 @@ -20,3 +20,9 @@ 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; +} 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 index 6f264780b..1f2e0cf30 100644 --- 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 @@ -4,3 +4,8 @@ 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; +} 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 index a0de06dad..d3e5fc0e6 100644 --- 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 @@ -10,3 +10,12 @@ 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'); +} From 536d650f0bf72b0306b3dc02ac07cd3df1cd6303 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 00:15:40 +0200 Subject: [PATCH 07/38] Preserve tag checks through Rstest assertion exports --- .../effect-native/rules/no-manual-tag-comparison.ts | 4 ++++ .../packages/core-runtime/tests/unit/assertions.test.ts | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) 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 4f0c1ecee..00c52ae02 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 @@ -352,6 +352,10 @@ function assertionCall( return null; if (specifier.type === 'ImportDefaultSpecifier' && source === 'expect') members.unshift('expect'); + if (subject === null && members.length === 2 && members[0] === 'assert') { + const method = members[1]; + return method === undefined ? null : { method, subject: null }; + } if (subject === null || members.shift() !== 'expect') return null; const method = members.pop(); if ( 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 index f6314e7c2..aef9108d3 100644 --- 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 @@ -1,6 +1,7 @@ -// expect-count: 17 +// expect-count: 19 import assert, { strictEqual as equal } from 'node:assert/strict'; -import { expect, expect as check } from '@rstest/core'; +import { assert as rstestAssert, expect, expect as check } from '@rstest/core'; +import { assert as effectAssert } from '@app/effect-rstest'; import * as testing from '@rstest/core'; import * as assertions from 'node:assert/strict'; import { strict as strictAssert } from 'node:assert'; @@ -26,3 +27,6 @@ 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'); From 48c5d41c0b26cca0a10a665b723a04a0cff6501d Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 00:13:25 +0200 Subject: [PATCH 08/38] test(rstest): vendor @app/effect-rstest and run pilot suites through Rstest projects Vendor the community port of @effect/vitest for Rstest as a workspace package, define unit/integration/component Rstest projects for every app package, and migrate five pilot suites to it.effect / it.live / it.layer with no Promise bridges. Co-Authored-By: Claude Fable 5.1 --- app/apps/shell-super-app/package.json | 7 +- app/apps/shell-super-app/rstest.config.ts | 43 +- .../tests/unit/auth-config.test.ts | 77 +- .../tests/unit/stage-demo-bootstrap.test.ts | 200 +-- app/oxlint.config.ts | 88 ++ app/packages/core-runtime/package.json | 18 +- app/packages/core-runtime/rstest.config.ts | 13 + .../tests/integration/outbox-runtime.test.ts | 872 +++++------ .../tests/unit/outbox-definition.test.ts | 267 ++-- app/packages/effect-rstest/LICENSE | 22 + app/packages/effect-rstest/README.md | 5 + app/packages/effect-rstest/package.json | 23 + app/packages/effect-rstest/rstest.config.ts | 6 + app/packages/effect-rstest/src/index.ts | 282 ++++ .../effect-rstest/src/internal/internal.ts | 347 +++++ app/packages/effect-rstest/src/utils.ts | 335 +++++ .../effect-rstest/tests/index.test.ts | 268 ++++ .../effect-rstest/tests/isolation.test.ts | 117 ++ .../tests/nested-isolation.test.ts | 203 +++ .../effect-rstest/tests/support/bar.ts | 3 + .../effect-rstest/tests/support/child.ts | 5 + .../effect-rstest/tests/support/foo.ts | 3 + .../effect-rstest/tests/support/parent.ts | 5 + .../effect-rstest/tests/support/scoped.ts | 5 + .../tests/support/shared-child.ts | 5 + .../effect-rstest/tests/support/sleeper.ts | 9 + .../effect-rstest/tests/support/state.ts | 7 + .../tests/support/todo-service.ts | 12 + app/packages/effect-rstest/tsconfig.json | 21 + app/packages/shared-contracts/package.json | 6 +- .../shared-contracts/rstest.config.ts | 5 + app/pnpm-lock.yaml | 34 + app/tsconfig.json | 3 + app/verticals/party-registry/package.json | 9 +- app/verticals/party-registry/rstest.config.ts | 32 +- .../api-integration-ares-application.test.ts | 1326 +++++++++-------- 36 files changed, 3250 insertions(+), 1433 deletions(-) create mode 100644 app/packages/core-runtime/rstest.config.ts create mode 100644 app/packages/effect-rstest/LICENSE create mode 100644 app/packages/effect-rstest/README.md create mode 100644 app/packages/effect-rstest/package.json create mode 100644 app/packages/effect-rstest/rstest.config.ts create mode 100644 app/packages/effect-rstest/src/index.ts create mode 100644 app/packages/effect-rstest/src/internal/internal.ts create mode 100644 app/packages/effect-rstest/src/utils.ts create mode 100644 app/packages/effect-rstest/tests/index.test.ts create mode 100644 app/packages/effect-rstest/tests/isolation.test.ts create mode 100644 app/packages/effect-rstest/tests/nested-isolation.test.ts create mode 100644 app/packages/effect-rstest/tests/support/bar.ts create mode 100644 app/packages/effect-rstest/tests/support/child.ts create mode 100644 app/packages/effect-rstest/tests/support/foo.ts create mode 100644 app/packages/effect-rstest/tests/support/parent.ts create mode 100644 app/packages/effect-rstest/tests/support/scoped.ts create mode 100644 app/packages/effect-rstest/tests/support/shared-child.ts create mode 100644 app/packages/effect-rstest/tests/support/sleeper.ts create mode 100644 app/packages/effect-rstest/tests/support/state.ts create mode 100644 app/packages/effect-rstest/tests/support/todo-service.ts create mode 100644 app/packages/effect-rstest/tsconfig.json create mode 100644 app/packages/shared-contracts/rstest.config.ts diff --git a/app/apps/shell-super-app/package.json b/app/apps/shell-super-app/package.json index 991286351..c75fbde34 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": { @@ -77,7 +77,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", + "@app/effect-rstest": "workspace:*" }, "modernjs": { "preset": "presetUltramodern", diff --git a/app/apps/shell-super-app/rstest.config.ts b/app/apps/shell-super-app/rstest.config.ts index df0ea039e..2860aa3bf 100644 --- a/app/apps/shell-super-app/rstest.config.ts +++ b/app/apps/shell-super-app/rstest.config.ts @@ -53,21 +53,32 @@ const encodedSiteUrl = Result.getOrThrow( ); 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, }, - }, - testEnvironment: 'happy-dom', + ], }); 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 b7494d9ed..a576ee0ef 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, Predicate } from 'effect'; -import { parseAuthConfig } from '../../api/auth/config.ts'; -import { parseGatewayIssuerConfig } from '../../api/auth/gateway-issuer-config.ts'; +import { expect, it } from '@app/effect-rstest'; +import { Effect, Schema } from 'effect'; +import { AuthConfigError, parseAuthConfig } from '../../api/auth/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(Predicate.isTagged(secretError, 'AuthConfigError')).toBe(true); - expect(Predicate.isTagged(databaseError, 'AuthConfigError')).toBe(true); - })); - -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(Predicate.isTagged(gatewayError, 'GatewayIssuerConfigError')).toBe(true); -}); + }), +); +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(Schema.is(AuthConfigError)(secretError)).toBe(true); + expect(Schema.is(AuthConfigError)(databaseError)).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/stage-demo-bootstrap.test.ts b/app/apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts index 842363f40..ad15f882a 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 '@app/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/oxlint.config.ts b/app/oxlint.config.ts index 8a6e38f1c..cdbf2c0b3 100644 --- a/app/oxlint.config.ts +++ b/app/oxlint.config.ts @@ -180,6 +180,94 @@ export default defineConfig({ typeCheck: true, }, overrides: [ + { + // Sonar S2187 has a hard-coded API list excluding Effect's it.effect/it.live/it.layer. + // Keep scoped to verified pilot files until the upstream detector supports these APIs. + files: [ + 'packages/core-runtime/tests/unit/outbox-definition.test.ts', + 'packages/core-runtime/tests/integration/outbox-runtime.test.ts', + 'verticals/party-registry/tests/unit/api-integration-ares-application.test.ts', + 'apps/shell-super-app/tests/unit/auth-config.test.ts', + 'apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts', + ], + rules: { 'sonarjs/no-empty-test-file': 'off' }, + }, + { + // These native Promise APIs are lifted directly into Effect; async thunks add no behavior. + files: [ + 'packages/core-runtime/tests/integration/outbox-runtime.test.ts', + 'apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts', + ], + rules: { 'typescript/promise-function-async': 'off' }, + }, + { + // vendored port of @effect/vitest; keep diffable against upstream + files: ['packages/effect-rstest/src/**'], + rules: { + 'anti-slop/no-known-value-widening': 'off', + 'anti-slop/no-reflect-apply': 'off', + 'anti-slop/no-reflect-get': 'off', + 'anti-slop/no-runtime-typeof': 'off', + 'anti-slop/no-unknown-parameters': 'off', + 'anti-slop/require-safety-comment-for-type-assertion': 'off', + 'effect-native/no-bare-effect-run': 'off', + 'effect-native/no-dependency-parameters': 'off', + 'effect-native/no-effect-provide-in-library': 'off', + 'effect-native/no-imperative-loop-in-effect-gen': 'off', + 'effect-native/no-layer-provide-in-library': 'off', + 'effect-native/no-local-defect-seam': 'off', + 'effect-native/no-native-error-construction': 'off', + 'effect-native/no-nested-effect-run': 'off', + 'effect-native/no-promise-shaped-port': 'off', + 'effect-native/no-refinement-outside-schema': 'off', + 'eslint/func-names': 'off', + 'eslint/func-style': 'off', + 'eslint/no-inline-comments': 'off', + 'eslint/no-negated-condition': 'off', + 'eslint/no-plusplus': 'off', + 'eslint/no-shadow': 'off', + 'eslint/no-unused-vars': 'off', + 'eslint/no-use-before-define': 'off', + 'eslint/prefer-arrow-callback': 'off', + 'import/export': 'off', + 'import/no-namespace': 'off', + 'jsdoc/check-tag-names': 'off', + 'perfectionist/sort-interfaces': 'off', + 'perfectionist/sort-object-types': 'off', + 'perfectionist/sort-objects': 'off', + 'sonarjs/no-built-in-override': 'off', + 'sonarjs/no-collapsible-if': 'off', + 'sonarjs/no-exclusive-tests': 'off', + 'sonarjs/no-nested-functions': 'off', + 'sonarjs/no-wildcard-import': 'off', + 'sonarjs/variable-name': 'off', + 'typescript/array-type': 'off', + 'typescript/ban-ts-comment': 'off', + 'typescript/consistent-indexed-object-style': 'off', + 'typescript/no-confusing-void-expression': 'off', + 'typescript/no-duplicate-type-constituents': 'off', + 'typescript/no-explicit-any': 'off', + 'typescript/no-namespace': 'off', + 'typescript/no-unnecessary-qualifier': 'off', + 'typescript/no-unnecessary-type-arguments': 'off', + 'typescript/no-unnecessary-type-assertion': 'off', + 'typescript/no-unsafe-argument': 'off', + 'typescript/no-unsafe-return': 'off', + 'typescript/no-unsafe-type-assertion': 'off', + 'typescript/non-nullable-type-assertion-style': 'off', + 'typescript/prefer-for-of': 'off', + 'typescript/prefer-function-type': 'off', + 'typescript/prefer-reduce-type-parameter': 'off', + 'typescript/prefer-ts-expect-error': 'off', + 'typescript/promise-function-async': 'off', + 'typescript/strict-boolean-expressions': 'off', + 'unicorn/catch-error-name': 'off', + 'unicorn/no-array-method-this-argument': 'off', + 'unicorn/no-array-reduce': 'off', + 'unicorn/no-lonely-if': 'off', + 'unicorn/no-negated-condition': 'off', + }, + }, { // This guarded test-only entrypoint composes real services with boundary fakes. // database-access:check rejects imports of it from production source. diff --git a/app/packages/core-runtime/package.json b/app/packages/core-runtime/package.json index e1d2ed1c2..325deaa91 100644 --- a/app/packages/core-runtime/package.json +++ b/app/packages/core-runtime/package.json @@ -17,14 +17,14 @@ "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": { @@ -39,6 +39,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", + "@app/effect-rstest": "workspace:*", + "@rstest/core": "0.11.10" } } 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/tests/integration/outbox-runtime.test.ts b/app/packages/core-runtime/tests/integration/outbox-runtime.test.ts index 4317de310..6744f97d8 100644 --- a/app/packages/core-runtime/tests/integration/outbox-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/outbox-runtime.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 { and, asc, eq } from 'drizzle-orm'; -import { DateTime, Effect, Option, Schema } from 'effect'; -import assert from 'node:assert/strict'; +import { expect, it } from '@app/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 { @@ -17,42 +14,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: { @@ -85,238 +57,201 @@ 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 +) => + 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], + ), + ); + 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 }; + }); +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)), - ); - await forEachSequential(messageRows, async ({ messageId }) => { - const deliveries = await runEffectTestPromise( - database - .select({ deliveryId: outboxDeliveries.outboxDeliveryId }) - .from(outboxDeliveries) - .where(eq(outboxDeliveries.outboxMessageId, messageId)), + .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), + ), ); - await forEachSequential(deliveries, async ({ deliveryId }) => { - await runEffectTestPromise( - database.delete(outboxAttempts).where(eq(outboxAttempts.outboxDeliveryId, deliveryId)), - ); + yield* database + .delete(outboxDeliveries) + .where(inArray(outboxDeliveries.outboxMessageId, messageIds)); + yield* database.delete(outboxMessages).where(eq(outboxMessages.tenantId, tenantId)); + yield* database.delete(domainEvents).where(eq(domainEvents.tenantId, tenantId)); + yield* database.delete(tenantModuleStates).where(eq(tenantModuleStates.tenantId, tenantId)); + yield* database.delete(tenants).where(eq(tenants.tenantId, tenantId)); + }); +it.live('matches zero, one, or multiple exact workers once without historical backfill', () => + Effect.gen(function* matchesZeroOneOrMultiple() { + const configuration = yield* loadDatabaseConfig(); + const { executor: database } = yield* makeCoreDatabase(configuration); + const tenantId = yield* Effect.acquireRelease(insertTenant(database), (id) => + cleanupTenant(database, id).pipe(Effect.orDie), + ); + yield* insertMessage(database, tenantId); + yield* insertMessage(database, tenantId, 'producer.unmatched'); + const repository = makeOutboxRepository(database); + const workers = [makeWorker('consumer.alpha'), makeWorker('consumer.beta')]; + const firstMatch = yield* repository.matchUnmatched( + workers.map(subscriptionOf), + dateAt('2026-08-03T10:00:00Z'), + ); + expect(firstMatch.deliveriesCreated).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 = 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 configuration = yield* loadDatabaseConfig(); + const { executor: database } = yield* makeCoreDatabase(configuration); + const tenantId = yield* Effect.acquireRelease(insertTenant(database), (id) => + cleanupTenant(database, id).pipe(Effect.orDie), + ); + 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', }); - await runEffectTestPromise( - database.delete(outboxDeliveries).where(eq(outboxDeliveries.outboxMessageId, messageId)), + const repository = makeOutboxRepository(database); + const subscriptions = [consumerWorker, reportingWorker].map(subscriptionOf); + const matched = yield* repository.matchUnmatched(subscriptions, dateAt('2026-08-03T10:00:00Z')); + expect(matched.deliveriesCreated).toBe(2); + const claimAt = yield* DateTime.nowAsDate; + const consumerClaim = Option.getOrNull( + yield* repository.claimNext([consumerWorker], 'consumer-process', claimAt), ); - }); - await runEffectTestPromise( - database.delete(outboxMessages).where(eq(outboxMessages.tenantId, tenantId)), - ); - await runEffectTestPromise( - database.delete(domainEvents).where(eq(domainEvents.tenantId, tenantId)), - ); - await runEffectTestPromise( - database.delete(tenantModuleStates).where(eq(tenantModuleStates.tenantId, tenantId)), - ); - await runEffectTestPromise(database.delete(tenants).where(eq(tenants.tenantId, tenantId))); -}; - -void test('matches zero, one, or multiple exact workers once without historical backfill', async () => { - await withDatabase(async (database) => { - const tenantId = await insertTenant(database); - try { - await insertMessage(database, tenantId); - await 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(), [ - '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, - ); - } finally { - await cleanupTenant(database, tenantId); - } - }); -}); - -void test('matches the complete subscription catalog before owner-local processes claim work', async () => { - await withDatabase(async (database) => { - const tenantId = await insertTenant(database); - try { - await runEffectTestPromise(activateConsumer(database, tenantId)); - await runEffectTestPromise( - database.insert(tenantModuleStates).values({ - moduleKey: 'reporting', - state: 'active', - tenantId, - }), - ); - await 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 consumerClaim = Option.getOrNull( - await claimNext(repository, [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'); - } finally { - await cleanupTenant(database, tenantId); - } - }); -}); - -void test('gates claims on every non-active consumer state and permits one concurrent live claim', async () => { - await withDatabase(async (database) => { - const tenantId = await insertTenant(database); - try { - await insertMessage(database, tenantId); + const reportingClaim = Option.getOrNull( + 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 configuration = yield* loadDatabaseConfig(); + const { executor: database } = yield* makeCoreDatabase(configuration); + const tenantId = yield* Effect.acquireRelease(insertTenant(database), (id) => + cleanupTenant(database, id).pipe(Effect.orDie), + ); + yield* 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( + yield* repository.matchUnmatched( + [subscriptionOf(registration)], + dateAt('2026-08-03T11:00:00Z'), + ); + 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, - async (state) => { - await runEffectTestPromise( - database + Effect.forEach((state) => + Effect.gen(function* checksInactiveState() { + yield* database .update(tenantModuleStates) .set({ state }) .where( @@ -324,264 +259,249 @@ void test('gates claims on every non-active consumer state and permits one concu 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, - ); - }, + ).toBe(null); + }), + ), ); - await runEffectTestPromise( - database - .update(tenantModuleStates) - .set({ state: 'active' }) - .where( - and( - eq(tenantModuleStates.tenantId, tenantId), - eq(tenantModuleStates.moduleKey, 'consumer'), - ), + yield* database + .update(tenantModuleStates) + .set({ state: 'active' }) + .where( + and( + 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 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)), + 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], + ), ); - assert.ok(attempt); - assert.equal(attempt.finishedAt, null); - } finally { - await cleanupTenant(database, tenantId); - } - }); -}); - -void test('reclaims only expired leases, abandons the old attempt, and rejects stale finalization', async () => { - await withDatabase(async (database) => { - const tenantId = await insertTenant(database); - try { - await runEffectTestPromise(activateConsumer(database, tenantId)); - await insertMessage(database, tenantId); + 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 configuration = yield* loadDatabaseConfig(); + const { executor: database } = yield* makeCoreDatabase(configuration); + const tenantId = yield* Effect.acquireRelease(insertTenant(database), (id) => + cleanupTenant(database, id).pipe(Effect.orDie), + ); + yield* activateConsumer(database, tenantId); + yield* insertMessage(database, tenantId); const registration = makeWorker('consumer.lease-proof'); const repository = makeOutboxRepository(database); - const started = advanceDate(await runEffectTestPromise(DateTime.nowAsDate), 1000); - await runEffectTestPromise( - repository.matchUnmatched([subscriptionOf(registration)], started), + const started = advanceDate(yield* DateTime.nowAsDate, 1000); + yield* repository.matchUnmatched([subscriptionOf(registration)], started); + const first = Option.getOrThrow( + yield* repository.claimNext([registration], 'runtime-a', started), ); - const first = Option.getOrNull( - await claimNext(repository, [registration], 'runtime-a', started), - ); - assert.ok(first); - assert.equal( + expect(first).toBeDefined(); + expect( Option.getOrNull( - await claimNext(repository, [registration], 'runtime-b', advanceDate(started, 999)), + yield* repository.claimNext([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 - .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); - } finally { - await cleanupTenant(database, tenantId); - } - }); -}); - -void test('finishes an abandoned final attempt before dead-lettering its expired delivery', async () => { - await withDatabase(async (database) => { - const tenantId = await insertTenant(database); - try { - await runEffectTestPromise(activateConsumer(database, tenantId)); - await insertMessage(database, tenantId); - const registration = makeWorker('consumer.final-lease', { maxAttempts: 1 }); - const repository = makeOutboxRepository(database); - const started = advanceDate(await runEffectTestPromise(DateTime.nowAsDate), 1000); - await runEffectTestPromise( - repository.matchUnmatched([subscriptionOf(registration)], started), - ); - const claim = Option.getOrNull( - await claimNext(repository, [registration], 'runtime-a', started), - ); - assert.ok(claim); - - assert.equal( - Option.getOrNull( - await claimNext(repository, [registration], 'runtime-b', advanceDate(started, 1001)), + ).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))), ), - null, - ); - const [delivery] = await runEffectTestPromise( - database - .select() - .from(outboxDeliveries) - .where(eq(outboxDeliveries.outboxDeliveryId, claim.deliveryId)), - ); - const [attempt] = await runEffectTestPromise( - database + ).toBe(true); + const attempts = yield* database + .select() + .from(outboxAttempts) + .where(eq(outboxAttempts.outboxDeliveryId, first.deliveryId)) + .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 configuration = yield* loadDatabaseConfig(); + const { executor: database } = yield* makeCoreDatabase(configuration); + const tenantId = yield* Effect.acquireRelease(insertTenant(database), (id) => + cleanupTenant(database, id).pipe(Effect.orDie), + ); + yield* activateConsumer(database, tenantId); + yield* insertMessage(database, tenantId); + const registration = makeWorker('consumer.final-lease', { maxAttempts: 1 }); + const repository = makeOutboxRepository(database); + const started = advanceDate(yield* DateTime.nowAsDate, 1000); + yield* repository.matchUnmatched([subscriptionOf(registration)], started); + const claim = Option.getOrThrow( + yield* repository.claimNext([registration], 'runtime-a', started), + ); + expect(claim).toBeDefined(); + expect( + Option.getOrNull( + yield* repository.claimNext([registration], 'runtime-b', advanceDate(started, 1001)), + ), + ).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)), - ); - assert.equal(delivery?.status, 'dead'); - assert.equal(attempt?.errorMessage, 'Outbox Worker lease expired before completion'); - assert.ok(attempt?.finishedAt); - } finally { - await cleanupTenant(database, tenantId); - } - }); -}); - -void test('finalizes success atomically and advances only through contiguous done deliveries', async () => { - await withDatabase(async (database) => { - const tenantId = await insertTenant(database); - try { - await runEffectTestPromise(activateConsumer(database, tenantId)); - const firstMessage = await insertMessage(database, tenantId); - const secondMessage = await insertMessage(database, tenantId); - const registration = makeWorker('consumer.checkpoint-proof'); - const repository = makeOutboxRepository(database); - const now = advanceDate(await runEffectTestPromise(DateTime.nowAsDate), 1000); - await runEffectTestPromise(repository.matchUnmatched([subscriptionOf(registration)], now)); - 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)), - ), - [], - ); - 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 + .where(eq(outboxAttempts.outboxDeliveryId, claim.deliveryId)))[0], + ), + ); + 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 configuration = yield* loadDatabaseConfig(); + const { executor: database } = yield* makeCoreDatabase(configuration); + const tenantId = yield* Effect.acquireRelease(insertTenant(database), (id) => + cleanupTenant(database, id).pipe(Effect.orDie), + ); + yield* activateConsumer(database, tenantId); + const firstMessage = yield* insertMessage(database, tenantId); + const secondMessage = yield* insertMessage(database, tenantId); + const registration = makeWorker('consumer.checkpoint-proof'); + const repository = makeOutboxRepository(database); + const now = advanceDate(yield* DateTime.nowAsDate, 1000); + yield* repository.matchUnmatched([subscriptionOf(registration)], now); + 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(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, - ); - } finally { - await cleanupTenant(database, tenantId); - } - }); -}); - -void test('schedules bounded retry, dead-letters exhaustion, stores safe errors, and never checkpoints failure', async () => { - await withDatabase(async (database) => { - const tenantId = await insertTenant(database); - try { - await runEffectTestPromise(activateConsumer(database, tenantId)); - await insertMessage(database, tenantId); + .from(workerCheckpoints) + .where(eq(workerCheckpoints.tenantId, tenantId)))[0], + ), + ); + 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 configuration = yield* loadDatabaseConfig(); + const { executor: database } = yield* makeCoreDatabase(configuration); + const tenantId = yield* Effect.acquireRelease(insertTenant(database), (id) => + cleanupTenant(database, id).pipe(Effect.orDie), + ); + yield* activateConsumer(database, tenantId); + yield* insertMessage(database, tenantId); const registration = makeWorker('consumer.retry-proof', { maxAttempts: 2 }); const repository = makeOutboxRepository(database); - const now = advanceDate(await runEffectTestPromise(DateTime.nowAsDate), 1000); - await runEffectTestPromise(repository.matchUnmatched([subscriptionOf(registration)], now)); - 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)), - ), + const now = advanceDate(yield* DateTime.nowAsDate, 1000); + yield* repository.matchUnmatched([subscriptionOf(registration)], now); + 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', ); - assert.equal( + expect( Option.getOrNull( - await claimNext(repository, [registration], 'runtime-b', advanceDate(now, 999)), + yield* repository.claimNext([registration], 'runtime-b', advanceDate(now, 999)), ), - null, + ).toBe(null); + const second = Option.getOrThrow( + yield* repository.claimNext([registration], 'runtime-b', advanceDate(now, 1001)), ); - 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)), - ), + expect(second).toBeDefined(); + expect(yield* repository.fail(second, 'terminal safe failure', advanceDate(now, 1002))).toBe( 'dead', ); - const [delivery] = await runEffectTestPromise( - 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 + const [delivery] = yield* database + .select() + .from(outboxDeliveries) + .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)); + expect(attempts.map(({ errorMessage }) => errorMessage)).toEqual([ + 'safe retry message', + 'terminal safe failure', + ]); + expect( + 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)), - ), - [], - ); - } finally { - await cleanupTenant(database, tenantId); - } - }); -}); - -void test('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'); -}); + .from(workerCheckpoints) + .where(eq(workerCheckpoints.tenantId, tenantId)), + ).toEqual([]); + }), +); +it.live('keeps test descriptor arrays compatible with the erased startup registry surface', () => + Effect.sync(() => { + const registry: readonly AnyOutboxWorkerRegistration[] = [ + makeWorker('consumer.registry-proof'), + ]; + expect(registry[0]?.descriptor.workerKey).toBe('consumer.registry-proof'); + }), +); 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..a816a40b7 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 '@app/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,149 +35,149 @@ 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, { +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', + }); + }), +); +it.effect('preserves schema inference for a typed handler payload', () => + Effect.sync(() => { + defineOutboxWorker( + { consumerModuleKey: 'consumer', - entrypoint: { + entrypoint: defineTenantModuleEntrypoint({ access: 'background', authorization: { kind: 'owner_local_background' }, - entrypointKey: 'consumer.message-logger', + entrypointKey: 'consumer.inference-proof', moduleKey: 'consumer', role: 'worker', - scope: 'tenant', - }, - leaseDurationMs: 30_000, + }), + leaseDurationMs: 1000, payloadSchema, producerModuleKey: 'producer', retryPolicy: { - initialBackoffMs: 1000, - maxAttempts: 5, - maxBackoffMs: 10_000, - multiplier: 2, + initialBackoffMs: 0, + maxAttempts: 1, + maxBackoffMs: 0, + multiplier: 1, }, 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', - }); - }), - ), + workerKey: 'consumer.inference-proof', + }, + (payload) => { + const key: string = payload.messageKey; + return Effect.sync(() => expect(key).toBe(payload.messageKey)); + }, + ); + }), ); - -void test('preserves schema inference for a typed handler payload', () => { - defineOutboxWorker( - { - consumerModuleKey: 'consumer', - entrypoint: defineTenantModuleEntrypoint({ - access: 'background', - authorization: { kind: 'owner_local_background' }, - entrypointKey: 'consumer.inference-proof', - moduleKey: 'consumer', - role: 'worker', - }), - leaseDurationMs: 1000, - payloadSchema, - producerModuleKey: 'producer', - retryPolicy: { - initialBackoffMs: 0, - maxAttempts: 1, - maxBackoffMs: 0, - multiplier: 1, +it.effect('rejects invalid identities, retry policies, and lease policies', () => + Effect.sync(() => { + const valid = makeWorker().descriptor; + const invalidDescriptors = [ + { ...valid, workerKey: 'producer.foreign-worker' }, + { + ...valid, + entrypoint: defineTenantModuleEntrypoint({ + access: 'background', + authorization: { kind: 'owner_local_background' }, + entrypointKey: valid.workerKey, + moduleKey: 'foreign', + role: 'worker', + }), }, - topic: 'producer.message-created', - workerKey: 'consumer.inference-proof', - }, - (payload) => { - const key: string = payload.messageKey; - return Effect.sync(() => assert.equal(key, payload.messageKey)); - }, - ); -}); - -void test('rejects invalid identities, retry policies, and lease policies', () => { - const valid = makeWorker().descriptor; - const invalidDescriptors = [ - { ...valid, workerKey: 'producer.foreign-worker' }, - { - ...valid, - entrypoint: defineTenantModuleEntrypoint({ - access: 'background', - authorization: { kind: 'owner_local_background' }, - entrypointKey: valid.workerKey, - moduleKey: 'foreign', - role: 'worker', + { ...valid, topic: 'Invalid' }, + { ...valid, leaseDurationMs: 999 }, + { ...valid, retryPolicy: { ...valid.retryPolicy, maxAttempts: 0 } }, + { + ...valid, + retryPolicy: { ...valid.retryPolicy, initialBackoffMs: 11_000 }, + }, + { ...valid, retryPolicy: { ...valid.retryPolicy, multiplier: 0 } }, + ]; + for (const descriptor of invalidDescriptors) { + expect(() => defineOutboxWorker(descriptor, () => Effect.void)).toThrow( + OutboxWorkerDescriptorError, + ); + } + }), +); +it.effect('rejects duplicate worker keys and calculates bounded exponential backoff', () => + Effect.sync(() => { + const worker = makeWorker(); + expect(() => validateOutboxWorkerRegistrations([worker, worker])).toThrow( + expect.objectContaining({ + name: 'OutboxWorkerDescriptorError', + reason: expect.stringMatching(/duplicate Outbox Worker key/u), }), - }, - { ...valid, topic: 'Invalid' }, - { ...valid, leaseDurationMs: 999 }, - { ...valid, retryPolicy: { ...valid.retryPolicy, maxAttempts: 0 } }, - { - ...valid, - retryPolicy: { ...valid.retryPolicy, initialBackoffMs: 11_000 }, - }, - { ...valid, retryPolicy: { ...valid.retryPolicy, multiplier: 0 } }, - ]; - - for (const descriptor of invalidDescriptors) { - assert.throws( - () => defineOutboxWorker(descriptor, () => Effect.void), - Schema.is(OutboxWorkerDescriptorError), ); - } -}); - -void test('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); -}); - -void test('validates and freezes the schema-free installed subscription catalog', () => { - const worker = makeWorker(); - const subscription = { - consumerModuleKey: worker.descriptor.consumerModuleKey, - entrypoint: worker.descriptor.entrypoint, - producerModuleKey: worker.descriptor.producerModuleKey, - topic: worker.descriptor.topic, - 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(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); + }), +); +it.effect('validates and freezes the schema-free installed subscription catalog', () => + Effect.sync(() => { + const worker = makeWorker(); + const subscription = { + consumerModuleKey: worker.descriptor.consumerModuleKey, + entrypoint: worker.descriptor.entrypoint, + producerModuleKey: worker.descriptor.producerModuleKey, + topic: worker.descriptor.topic, + workerKey: worker.descriptor.workerKey, + }; + const validated = validateOutboxWorkerSubscriptions([subscription]); + 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/effect-rstest/LICENSE b/app/packages/effect-rstest/LICENSE new file mode 100644 index 000000000..d5aad8163 --- /dev/null +++ b/app/packages/effect-rstest/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc +Copyright (c) 2026 ScriptedAlchemy (effect-rstest port) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/packages/effect-rstest/README.md b/app/packages/effect-rstest/README.md new file mode 100644 index 000000000..a764729b9 --- /dev/null +++ b/app/packages/effect-rstest/README.md @@ -0,0 +1,5 @@ +# @app/effect-rstest + +Vendored community port of `@effect/vitest` to Rstest by ScriptedAlchemy, commit `79abbf6`. Source: https://github.com/ScriptedAlchemy/effect-rstest. MIT licensed; the original copyright and permission notice are preserved in [LICENSE](./LICENSE). + +Workspace exports use TypeScript source; local changes adapt imports and repository diagnostics. diff --git a/app/packages/effect-rstest/package.json b/app/packages/effect-rstest/package.json new file mode 100644 index 000000000..614501b5c --- /dev/null +++ b/app/packages/effect-rstest/package.json @@ -0,0 +1,23 @@ +{ + "name": "@app/effect-rstest", + "version": "0.1.0", + "private": true, + "type": "module", + "license": "MIT", + "exports": { + ".": "./src/index.ts", + "./utils": "./src/utils.ts" + }, + "scripts": { + "test:unit": "rstest", + "typecheck": "node ../../scripts/ultramodern-typecheck.mts --project tsconfig.json" + }, + "dependencies": { + "effect": "4.0.0-beta.107", + "@rstest/core": "0.11.10" + }, + "devDependencies": { + "@effect/tsgo": "0.19.0", + "@types/node": "20.19.43" + } +} diff --git a/app/packages/effect-rstest/rstest.config.ts b/app/packages/effect-rstest/rstest.config.ts new file mode 100644 index 000000000..b3d0280f9 --- /dev/null +++ b/app/packages/effect-rstest/rstest.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + include: ['tests/**/*.test.ts'], + testEnvironment: 'node', +}); diff --git a/app/packages/effect-rstest/src/index.ts b/app/packages/effect-rstest/src/index.ts new file mode 100644 index 000000000..30f51988f --- /dev/null +++ b/app/packages/effect-rstest/src/index.ts @@ -0,0 +1,282 @@ +/** + * Helpers for testing Effect (v4) code with [Rstest](https://rstest.rs). + * + * This module is a port of `@effect/vitest` to the Rstest runner. The public + * API mirrors `@effect/vitest`: an enhanced `it` with `effect`, `live`, + * `layer`, `prop` and `flakyTest`, plus `addEqualityTesters` and + * `describeWrapped`. + * + * @since 0.1.0 + */ +import type * as Duration from 'effect/Duration'; +import type * as Effect from 'effect/Effect'; +import type * as Layer from 'effect/Layer'; +import type * as Schema from 'effect/Schema'; +import type * as Scope from 'effect/Scope'; +import type * as FC from 'effect/testing/FastCheck'; +import * as Rs from '@rstest/core'; +import * as internal from './internal/internal.ts'; + +/** + * Re-exports everything from `@rstest/core` (`describe`, `expect`, `assert`, + * hooks, `rs`, ...). + * + * @since 0.1.0 + */ +export * from '@rstest/core'; + +/** + * @since 0.1.0 + */ +export type API = Rs.TestAPIs; + +/** + * Type namespace retained from `@effect/vitest` for source compatibility. + * + * @since 0.1.0 + */ +export namespace Vitest { + /** + * @since 0.1.0 + */ + export interface TestFunction> { + (...args: TestArgs): Effect.Effect; + } + + /** + * @since 0.1.0 + */ + export interface Test { + ( + name: string, + self: TestFunction, + timeout?: number | Rs.TestOptions, + ): void; + } + + /** + * @since 0.1.0 + */ + export type Arbitraries = + | Array | FC.Arbitrary> + | { [K in string]: Schema.Schema | FC.Arbitrary }; + + /** + * @since 0.1.0 + */ + export interface Tester extends Vitest.Test { + skip: Vitest.Test; + skipIf: (condition: unknown) => Vitest.Test; + runIf: (condition: unknown) => Vitest.Test; + only: Vitest.Test; + each: ( + cases: ReadonlyArray, + ) => ( + name: string, + self: TestFunction>, + timeout?: number | Rs.TestOptions, + ) => void; + fails: Vitest.Test; + + /** + * @since 0.1.0 + */ + prop: ( + name: string, + arbitraries: Arbs, + self: TestFunction< + A, + E, + R, + [ + { + [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary + ? T + : Arbs[K] extends Schema.Schema + ? T + : never; + }, + Rs.TestContext, + ] + >, + timeout?: + | number + | (Rs.TestOptions & { + fastCheck?: FC.Parameters<{ + [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary + ? T + : Arbs[K] extends Schema.Schema + ? T + : never; + }>; + }), + ) => void; + } + + /** + * @since 0.1.0 + */ + export interface MethodsNonLive extends API { + readonly effect: Vitest.Tester; + readonly describe: Rs.Describe; + readonly flakyTest: ( + self: Effect.Effect, + timeout?: Duration.Input, + ) => Effect.Effect; + readonly layer: ( + layer: Layer.Layer, + options?: { + readonly timeout?: Duration.Input; + }, + ) => { + (f: (it: Vitest.MethodsNonLive) => void): void; + (name: string, f: (it: Vitest.MethodsNonLive) => void): void; + }; + + /** + * @since 0.1.0 + */ + readonly prop: ( + name: string, + arbitraries: Arbs, + self: ( + properties: { + [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary + ? T + : Arbs[K] extends Schema.Schema + ? T + : never; + }, + ctx: Rs.TestContext, + ) => void, + timeout?: + | number + | (Rs.TestOptions & { + fastCheck?: FC.Parameters<{ + [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary + ? T + : Arbs[K] extends Schema.Schema + ? T + : never; + }>; + }), + ) => void; + } + + /** + * @since 0.1.0 + */ + export interface Methods extends MethodsNonLive { + readonly live: Vitest.Tester; + readonly layer: ( + layer: Layer.Layer, + options?: { + readonly memoMap?: Layer.MemoMap; + readonly timeout?: Duration.Input; + readonly excludeTestServices?: boolean; + }, + ) => { + (f: (it: Vitest.MethodsNonLive) => void): void; + (name: string, f: (it: Vitest.MethodsNonLive) => void): void; + }; + } +} + +/** + * @since 0.1.0 + */ +export const addEqualityTesters: () => void = internal.addEqualityTesters; + +/** + * @since 0.1.0 + */ +export const effect: Vitest.Tester = internal.effect; + +/** + * @since 0.1.0 + */ +export const live: Vitest.Tester = internal.live; + +/** + * Share a `Layer` between multiple tests, optionally wrapping + * the tests in a `describe` block if a name is provided. + * + * @since 0.1.0 + * + * ```ts + * import { assert, layer } from "effect-rstest" + * import { Effect, Layer, Context } from "effect" + * + * class Foo extends Context.Service()("Foo") { + * static layer = Layer.succeed(Foo, "foo") + * } + * + * class Bar extends Context.Service()("Bar") { + * static layer = Layer.effect( + * Bar, + * Effect.map(Foo, () => "bar" as const) + * ) + * } + * + * layer(Foo.layer)("layer", (it) => { + * it.effect("adds context", () => + * Effect.gen(function*() { + * const foo = yield* Foo + * assert.strictEqual(foo, "foo") + * })) + * + * it.layer(Bar.layer)("nested", (it) => { + * it.effect("adds context", () => + * Effect.gen(function*() { + * const foo = yield* Foo + * const bar = yield* Bar + * assert.strictEqual(foo, "foo") + * assert.strictEqual(bar, "bar") + * })) + * }) + * }) + * ``` + */ +export const layer: ( + layer_: Layer.Layer, + options?: { + readonly memoMap?: Layer.MemoMap; + readonly timeout?: Duration.Input; + readonly excludeTestServices?: boolean; + }, +) => { + (f: (it: Vitest.MethodsNonLive) => void): void; + (name: string, f: (it: Vitest.MethodsNonLive) => void): void; +} = internal.layer; + +/** + * @since 0.1.0 + */ +export const flakyTest: ( + self: Effect.Effect, + timeout?: Duration.Input, +) => Effect.Effect = internal.flakyTest; + +/** + * @since 0.1.0 + */ +export const prop: Vitest.Methods['prop'] = internal.prop; + +/** + * @since 0.1.0 + */ +export const it: Vitest.Methods = internal.makeMethods(Rs.it); + +/** + * @since 0.1.0 + */ +export const makeMethods: (it: Rs.TestAPIs) => Vitest.Methods = internal.makeMethods; + +/** + * Unlike `@effect/vitest`, this returns `void` because Rstest's `describe` + * does not return a `SuiteCollector`. + * + * @since 0.1.0 + */ +export const describeWrapped: (name: string, f: (it: Vitest.Methods) => void) => void = + internal.describeWrapped; diff --git a/app/packages/effect-rstest/src/internal/internal.ts b/app/packages/effect-rstest/src/internal/internal.ts new file mode 100644 index 000000000..ac66607ef --- /dev/null +++ b/app/packages/effect-rstest/src/internal/internal.ts @@ -0,0 +1,347 @@ +// @effect-diagnostics missedPipeableOpportunity:off strictEffectProvide:off -- vendored port of @effect/vitest; remove-when: upstream removes this runtime boundary +/** + * @since 0.1.0 + */ + +import * as Cause from 'effect/Cause'; +import * as Duration from 'effect/Duration'; +import * as Effect from 'effect/Effect'; +import * as Exit from 'effect/Exit'; +import { flow, pipe } from 'effect/Function'; +import * as Layer from 'effect/Layer'; +import { isObject } from 'effect/Predicate'; +import * as Rec from 'effect/Record'; +import * as Schedule from 'effect/Schedule'; +import * as Schema from 'effect/Schema'; +import * as Scope from 'effect/Scope'; +import * as fc from 'effect/testing/FastCheck'; +import * as TestClock from 'effect/testing/TestClock'; +import * as TestConsole from 'effect/testing/TestConsole'; +import * as Rs from '@rstest/core'; +import type * as EffectRstest from '../index.ts'; + +const runPromise: ( + _: Effect.Effect, + ctx?: Rs.TestContext | undefined, +) => Promise = Effect.fnUntraced( + function* (effect: Effect.Effect, _ctx?: Rs.TestContext) { + const exit = yield* Effect.exit(effect); + if (Exit.isFailure(exit)) { + const errors = Cause.prettyErrors(exit.cause); + for (let i = 0; i < errors.length; i++) { + yield* Effect.logError(errors[i]); + } + } + return yield* exit; + }, + (effect, _, ctx) => Effect.runPromise(effect, { signal: ctx?.signal }), +); + +/** @internal */ +const runTest = + (ctx?: Rs.TestContext) => + (effect: Effect.Effect) => + runPromise(effect, ctx); + +/** @internal */ +export type TestContext = TestConsole.TestConsole | TestClock.TestClock; + +const TestEnv = Layer.mergeAll(TestConsole.layer, TestClock.layer()); + +/** @internal */ +export const addEqualityTesters = () => { + Rs.expect.addEqualityTesters([]); +}; + +/** @internal */ +const testOptions = (timeout?: number | Rs.TestOptions): Rs.TestOptions => + typeof timeout === 'number' ? { timeout } : (timeout ?? {}); + +const hookTimeout = (timeout?: Duration.Input) => + timeout === undefined ? undefined : Duration.toMillis(Duration.fromInputUnsafe(timeout)); + +const makeItProxy = ( + it: Rs.TestAPIs, + overrides: Methods, +): Methods & Rs.TestAPIs => + new Proxy(it as Methods & Rs.TestAPIs, { + apply(target, thisArg, argArray) { + return Reflect.apply(target, thisArg, argArray); + }, + get(target, property, receiver) { + if (Object.hasOwn(overrides, property)) { + return Reflect.get(overrides, property); + } + // do not bind: binding would strip rstest's static helpers (e.g. `it.each`) + return Reflect.get(target, property, receiver); + }, + }); + +/** @internal */ +const makeTester = ( + mapEffect: (self: Effect.Effect) => Effect.Effect, + it: Rs.TestAPIs = Rs.it, +): EffectRstest.Vitest.Tester => { + // rstest's test callbacks must return `MaybePromise`, so the test + // value is intentionally discarded here (vitest accepts any return value) + const run = >( + ctx: Rs.TestContext & object, + args: TestArgs, + self: EffectRstest.Vitest.TestFunction, + ): Promise => + pipe( + Effect.suspend(() => self(...args)), + mapEffect, + runTest(ctx), + ); + + const f: EffectRstest.Vitest.Test = (name, self, timeout) => + it(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)); + + const skip: EffectRstest.Vitest.Tester['only'] = (name, self, timeout) => + it.skip(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)); + + const skipIf: EffectRstest.Vitest.Tester['skipIf'] = (condition) => (name, self, timeout) => + it.skipIf(Boolean(condition))(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)); + + const runIf: EffectRstest.Vitest.Tester['runIf'] = (condition) => (name, self, timeout) => + it.runIf(Boolean(condition))(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)); + + const only: EffectRstest.Vitest.Tester['only'] = (name, self, timeout) => + it.only(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)); + + const each: EffectRstest.Vitest.Tester['each'] = (cases) => (name, self, timeout) => + it.for(cases)(name, testOptions(timeout), (args, ctx) => run(ctx, [args], self) as any); + + const fails: EffectRstest.Vitest.Tester['fails'] = (name, self, timeout) => + it.fails(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)); + + const prop: EffectRstest.Vitest.Tester['prop'] = (name, arbitraries, self, timeout) => { + if (Array.isArray(arbitraries)) { + const arbs = arbitraries.map((arbitrary) => { + if (Schema.isSchema(arbitrary)) { + return Schema.toArbitrary(arbitrary)(fc); + } + return arbitrary as fc.Arbitrary; + }); + return it(name, testOptions(timeout), (ctx) => + // @ts-ignore + fc.assert( + // @ts-ignore + fc.asyncProperty(...arbs, (...as) => run(ctx, [as as any, ctx], self)), + // @ts-ignore + // @ts-ignore -- upstream variadic FastCheck options + isObject(timeout) ? timeout?.['fastCheck'] : {}, + ), + ); + } + + const arbs = fc.record( + Object.keys(arbitraries).reduce( + function (result, key) { + const arb: any = arbitraries[key]; + Rec.assignProperty(result, key, Schema.isSchema(arb) ? Schema.toArbitrary(arb)(fc) : arb); + return result; + }, + {} as Record>, + ), + ); + + return it(name, testOptions(timeout), (ctx) => + // @ts-ignore + fc.assert( + fc.asyncProperty(arbs, (...as) => + // @ts-ignore + run(ctx, [as[0] as any, ctx], self), + ), + // @ts-ignore + // @ts-ignore -- upstream variadic FastCheck options + isObject(timeout) ? timeout?.['fastCheck'] : {}, + ), + ); + }; + + return Object.assign(f, { skip, skipIf, runIf, only, each, fails, prop }); +}; + +/** @internal */ +export const prop: EffectRstest.Vitest.Methods['prop'] = (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'); + } + return arbitrary; + }); + return Rs.it( + name, + testOptions(timeout), + // @ts-ignore + (ctx) => + fc.assert( + // @ts-ignore -- upstream variadic FastCheck typing + fc.property(...arbs, (...as) => self(as, ctx)), + // @ts-ignore -- upstream variadic FastCheck options + isObject(timeout) ? timeout?.['fastCheck'] : {}, + ), + ); + } + + 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); + return result; + }, + {} as Record>, + ), + ); + + return Rs.it( + name, + testOptions(timeout), + // @ts-ignore + (ctx) => + fc.assert( + // @ts-ignore -- upstream variadic FastCheck typing + fc.property(arbs, (as) => self(as, ctx)), + // @ts-ignore -- upstream variadic FastCheck options + isObject(timeout) ? timeout?.['fastCheck'] : {}, + ), + ); +}; + +/** @internal */ +export const layer = + ( + layer_: Layer.Layer, + options?: { + readonly memoMap?: Layer.MemoMap; + readonly timeout?: Duration.Input; + readonly excludeTestServices?: boolean; + }, + ): { + (f: (it: EffectRstest.Vitest.MethodsNonLive) => void): void; + (name: string, f: (it: EffectRstest.Vitest.MethodsNonLive) => void): void; + } => + ( + ...args: + | [name: string, f: (it: EffectRstest.Vitest.MethodsNonLive) => void] + | [f: (it: EffectRstest.Vitest.MethodsNonLive) => void] + ) => { + const excludeTestServices = options?.excludeTestServices ?? false; + const withTestEnv = excludeTestServices + ? (layer_ as Layer.Layer) + : Layer.provideMerge(layer_, TestEnv); + 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 closed = false; + const closeScope = (ctx?: Rs.TestContext) => { + if (closed) { + return Promise.resolve(); + } + closed = true; + return runPromise(Scope.close(scope, Exit.void), ctx); + }; + + const makeIt = (it: Rs.TestAPIs): EffectRstest.Vitest.MethodsNonLive => + makeItProxy(it, { + effect: makeTester( + (effect) => + Effect.flatMap(contextEffect, (context) => + effect.pipe(Effect.scoped, Effect.provide(context)), + ), + it, + ), + describe: Rs.describe, + prop, + flakyTest, + layer( + nestedLayer: Layer.Layer, + options?: { + readonly timeout?: Duration.Input; + }, + ) { + return layer(Layer.provideMerge(nestedLayer, withTestEnv), { + ...options, + memoMap: Layer.forkMemoMapUnsafe(memoMap), + excludeTestServices, + }); + }, + }); + + if (args.length === 1) { + // Rstest has no `getCurrentSuite`, so use an empty nested suite as the + // lifecycle boundary for an unnamed layer block. Rstest omits empty suite + // names from test paths, while its beforeAll/afterAll hooks ensure the + // scope closes before later tests in the enclosing suite run. + return Rs.describe('', () => { + Rs.beforeAll(() => runPromise(Effect.asVoid(contextEffect)), 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.afterAll(() => closeScope(), hookTimeout(options?.timeout)); + return args[1](makeIt(Rs.it)); + }); + }; + +/** @internal */ +export const flakyTest = ( + self: Effect.Effect, + timeout: Duration.Input = Duration.seconds(30), +) => + pipe( + self, + Effect.scoped, + Effect.sandbox, + Effect.retry( + pipe( + Schedule.recurs(10), + Schedule.while((_) => + Effect.succeed( + Duration.isLessThanOrEqualTo( + Duration.fromInputUnsafe(_.elapsed), + Duration.fromInputUnsafe(timeout), + ), + ), + ), + ), + ), + Effect.orDie, + ); + +/** @internal */ +export const makeMethods = (it: Rs.TestAPIs): EffectRstest.Vitest.Methods => + makeItProxy(it, { + effect: makeTester(flow(Effect.scoped, Effect.provide(TestEnv)), it), + live: makeTester(Effect.scoped, it), + describe: Rs.describe, + flakyTest, + layer, + prop, + }); + +/** @internal */ +export const { + /** @internal */ + effect, + /** @internal */ + live, +} = makeMethods(Rs.it); + +/** @internal */ +export const describeWrapped = (name: string, f: (it: EffectRstest.Vitest.Methods) => void): void => + Rs.describe(name, () => f(makeMethods(Rs.it))); diff --git a/app/packages/effect-rstest/src/utils.ts b/app/packages/effect-rstest/src/utils.ts new file mode 100644 index 000000000..487aa7390 --- /dev/null +++ b/app/packages/effect-rstest/src/utils.ts @@ -0,0 +1,335 @@ +// @effect-diagnostics asyncFunction:off -- vendored port of @effect/vitest; remove-when: upstream removes this runtime boundary +/** + * Provides assertion helpers used by `effect-rstest` tests. + * + * This module defines small assertion functions built on Node's `assert`, + * Rstest's instance checks, and Effect's equality support. The helpers cover + * basic equality, thrown errors, defined and undefined values, strings, regular + * expressions, class instances, `Option`, `Result`, and `Exit`. Most helpers are + * synchronous; `throwsAsync` handles rejected promises. + * + * @since 0.1.0 + */ +import type * as Cause from 'effect/Cause'; +import * as Equal from 'effect/Equal'; +import * as Exit from 'effect/Exit'; +import * as Option from 'effect/Option'; +import * as Predicate from 'effect/Predicate'; +import * as Result from 'effect/Result'; +import * as assert from 'node:assert'; +import { assert as rassert } from '@rstest/core'; + +// ---------------------------- +// Primitives +// ---------------------------- + +/** + * Fails the current test with the provided error message. + * + * @category testing + * @since 0.1.0 + */ +export function fail(message: string) { + assert.fail(message); +} + +/** + * Asserts that `actual` is deeply strictly equal to `expected` using Node's `assert.deepStrictEqual`. + * + * @category testing + * @since 0.1.0 + */ +export function deepStrictEqual(actual: A, expected: A, message?: string, ..._: Array) { + assert.deepStrictEqual(actual, expected, message as string); +} + +/** + * Asserts that `actual` is not deeply strictly equal to `expected` using Node's `assert.notDeepStrictEqual`. + * + * @category testing + * @since 0.1.0 + */ +export function notDeepStrictEqual( + actual: A, + expected: A, + message?: string, + ..._: Array +) { + assert.notDeepStrictEqual(actual, expected, message as string); +} + +/** + * Asserts that `actual` is strictly equal to `expected` using Node's `assert.strictEqual`. + * + * @category testing + * @since 0.1.0 + */ +export function strictEqual(actual: A, expected: A, message?: string, ..._: Array) { + if (message !== undefined) { + assert.strictEqual(actual, expected, message); + } else { + assert.strictEqual(actual, expected); + } +} + +/** + * Asserts that `actual` is equal to `expected` using the `Equal.equals` trait. + * + * @category testing + * @since 0.1.0 + */ +export function assertEquals(actual: A, expected: A, message?: string, ..._: Array) { + if (!Equal.equals(actual, expected)) { + deepStrictEqual(actual, expected, message); // show diff + fail(message ?? 'Expected values to be Equal.equals'); + } +} + +/** + * Asserts that `thunk` does not throw an error. + * + * @category testing + * @since 0.1.0 + */ +export function doesNotThrow(thunk: () => void, message?: string, ..._: Array) { + assert.doesNotThrow(thunk, message); +} + +// ---------------------------- +// Derived +// ---------------------------- + +/** + * Asserts that `value` is an instance of `constructor`. + * + * @category testing + * @since 0.1.0 + */ +export function assertInstanceOf any>( + value: unknown, + constructor: C, + message?: string, + ..._: Array +): asserts value is InstanceType { + rassert.instanceOf(value, constructor as any, message); +} + +/** + * Asserts that `self` is `true`. + * + * @category testing + * @since 0.1.0 + */ +export function assertTrue(self: unknown, message?: string, ..._: Array): asserts self { + strictEqual(self, true, message); +} + +/** + * Asserts that `self` is `false`. + * + * @category testing + * @since 0.1.0 + */ +export function assertFalse(self: boolean, message?: string, ..._: Array) { + strictEqual(self, false, message); +} + +/** + * Asserts that `actual` includes `expected`. + * + * @category testing + * @since 0.1.0 + */ +export function assertInclude(actual: string | undefined, expected: string, ..._: Array) { + if (typeof expected === 'string') { + if (actual?.includes(expected) !== true) { + fail(`Expected\n\n${actual}\n\nto include\n\n${expected}`); + } + } +} + +/** + * Asserts that `actual` matches `regExp`. + * + * @category testing + * @since 0.1.0 + */ +export function assertMatch(actual: string, regExp: RegExp, ..._: Array) { + if (!regExp.test(actual)) { + fail(`Expected\n\n${actual}\n\nto match\n\n${regExp}`); + } +} + +/** + * Asserts that `thunk` throws, optionally checking the thrown value against an expected `Error` or validation function. + * + * @category testing + * @since 0.1.0 + */ +export function throws( + thunk: () => void, + error?: Error | ((u: unknown) => undefined), + ..._: Array +) { + try { + thunk(); + } catch (e) { + if (error !== undefined) { + if (Predicate.isFunction(error)) { + error(e); + } else { + deepStrictEqual(e, error); + } + } + return; + } + fail('Expected to throw an error'); +} + +/** + * Asserts that `thunk` throws or returns a rejected promise, optionally checking the failure value against an expected `Error` or validation function. + * + * @category testing + * @since 0.1.0 + */ +export async function throwsAsync( + thunk: () => Promise, + error?: Error | ((u: unknown) => undefined), + ..._: Array +) { + try { + await thunk(); + } catch (e) { + if (error !== undefined) { + if (Predicate.isFunction(error)) { + error(e); + } else { + deepStrictEqual(e, error); + } + } + return; + } + fail('Expected to throw an error'); +} + +// ---------------------------- +// Option +// ---------------------------- + +/** + * Asserts that `option` is `None`. + * + * @category testing + * @since 0.1.0 + */ +export function assertNone( + option: Option.Option, + ..._: Array +): asserts option is Option.None { + deepStrictEqual(option, Option.none()); +} + +/** + * Asserts that `a` is not `undefined`. + * + * @category testing + * @since 0.1.0 + */ +export function assertDefined( + a: A | undefined, + ..._: Array +): asserts a is Exclude { + if (a === undefined) { + fail('Expected value to be defined'); + } +} + +/** + * Asserts that `a` is `undefined`. + * + * @category testing + * @since 0.1.0 + */ +export function assertUndefined(a: A | undefined, ..._: Array): asserts a is undefined { + if (a !== undefined) { + fail('Expected value to be undefined'); + } +} + +/** + * Asserts that `option` is `Some` and contains a value equal to `expected`. + * + * @category testing + * @since 0.1.0 + */ +export function assertSome( + option: Option.Option, + expected: A, + ..._: Array +): asserts option is Option.Some { + deepStrictEqual(option, Option.some(expected)); +} + +// ---------------------------- +// Result +// ---------------------------- + +/** + * Asserts that `result` is `Success` and contains a value equal to `expected`. + * + * @category testing + * @since 0.1.0 + */ +export function assertSuccess( + result: Result.Result, + expected: A, + ..._: Array +): asserts result is Result.Success { + deepStrictEqual(result, Result.succeed(expected)); +} + +/** + * Asserts that `result` is `Failure` and contains an error equal to `expected`. + * + * @category testing + * @since 0.1.0 + */ +export function assertFailure( + result: Result.Result, + expected: E, + ..._: Array +): asserts result is Result.Failure { + deepStrictEqual(result, Result.fail(expected)); +} + +// ---------------------------- +// Exit +// ---------------------------- + +/** + * Asserts that `exit` is a failure with a cause equal to `expected`. + * + * @category testing + * @since 0.1.0 + */ +export function assertExitFailure( + exit: Exit.Exit, + expected: Cause.Cause, + ..._: Array +): asserts exit is Exit.Failure { + deepStrictEqual(exit, Exit.failCause(expected)); +} + +/** + * Asserts that `exit` is a success with a value equal to `expected`. + * + * @category testing + * @since 0.1.0 + */ +export function assertExitSuccess( + exit: Exit.Exit, + expected: A, + ..._: Array +): asserts exit is Exit.Success { + deepStrictEqual(exit, Exit.succeed(expected)); +} diff --git a/app/packages/effect-rstest/tests/index.test.ts b/app/packages/effect-rstest/tests/index.test.ts new file mode 100644 index 000000000..e75076264 --- /dev/null +++ b/app/packages/effect-rstest/tests/index.test.ts @@ -0,0 +1,268 @@ +import { Sleeper } from './support/sleeper.ts'; +import { Scoped } from './support/scoped.ts'; +import { Foo } from './support/foo.ts'; +import { Bar } from './support/bar.ts'; +import { afterAll, assert, describe, describeWrapped, expect, it, layer } from '@app/effect-rstest'; +import { throws, throwsAsync } from '@app/effect-rstest/utils'; +import { Clock, Duration, Effect, Exit, Fiber, Layer, Schema } from 'effect'; +import { FastCheck, TestClock } from 'effect/testing'; + +const realNumber = FastCheck.float({ noDefaultInfinity: true, noNaN: true }); + +it.effect('effect', () => + Effect.acquireRelease( + Effect.sync(() => expect(1).toEqual(1)), + () => Effect.void, + ), +); +it.live('live', () => + Effect.acquireRelease( + Effect.sync(() => expect(1).toEqual(1)), + () => Effect.void, + ), +); + +describeWrapped('describeWrapped', (suiteIt0) => { + suiteIt0.effect('provides the enhanced test API', () => + Effect.sync(() => expect(suiteIt0.layer).toBeTypeOf('function')), + ); +}); + +it('throws fails when the thunk does not throw', () => { + expect(() => throws(() => {})).toThrow(); +}); + +const resolvedPromiseThrows: () => Promise = throwsAsync.bind( + undefined, + Promise.resolve.bind(Promise), + undefined, +); + +it.effect('throwsAsync fails when the promise resolves', () => + Effect.gen(function* throwsResolved() { + const result = yield* Effect.exit(Effect.tryPromise(resolvedPromiseThrows)); + expect(Exit.isFailure(result)).toBe(true); + }), +); + +// each + +it.effect.each([1, 2, 3])('effect each %s', (n) => + Effect.acquireRelease( + Effect.sync(() => expect(n).toEqual(n)), + () => Effect.void, + ), +); +it.live.each([1, 2, 3])('live each %s', (n) => + Effect.acquireRelease( + Effect.sync(() => expect(n).toEqual(n)), + () => Effect.void, + ), +); + +// skip + +it.live.skip('live skipped', () => Effect.die('skipped anyway')); +it.effect.skip('effect skipped', () => Effect.die('skipped anyway')); + +// skipIf + +it.effect.skipIf(true)('effect skipIf (true)', () => Effect.die('skipped anyway')); +it.effect.skipIf(false)('effect skipIf (false)', () => Effect.sync(() => expect(1).toEqual(1))); + +// runIf + +it.effect.runIf(true)('effect runIf (true)', () => Effect.sync(() => expect(1).toEqual(1))); +it.effect.runIf(false)('effect runIf (false)', () => Effect.die('not run anyway')); + +// chained helpers + +it.describe.each(['foo', 'bar'] as const)('describe.each %s', (text) => { + it.effect('runs an Effect test', () => + Effect.sync(() => { + assert.include(['foo', 'bar'], text); + }), + ); +}); + +it.skip.each([1])('skip.each %s', () => assert.fail('skipped anyway')); + +// The following test is expected to fail because it simulates a test timeout. +// Be aware that eventual 'failure' of the test is only logged out. +it.live.fails( + 'interrupts on timeout', + (ctx) => + Effect.gen(function* testEffect() { + let acquired = false; + + ctx.onTestFailed(() => { + expect(acquired).toBe(false); + }); + + yield* Effect.acquireRelease( + Effect.sync(() => (acquired = true)), + () => Effect.sync(() => (acquired = false)), + ); + yield* Effect.sleep(1000); + }), + 1, +); + +const fooLayer = Layer.succeed(Foo)('foo'); + +const barLayer = Layer.effect(Bar)(Foo.pipe(Effect.as('bar' as const))); + +const sleeperLayer = Layer.effect(Sleeper)( + Effect.gen(function* testEffect() { + const clock = yield* Clock.Clock; + + return { + sleep: (ms: number) => clock.sleep(Duration.millis(ms)), + }; + }), +); + +describe('layer', () => { + layer(fooLayer)((suiteIt1) => { + suiteIt1.effect('adds context', () => + Effect.gen(function* testEffect() { + const foo = yield* Foo; + expect(foo).toEqual('foo'); + }), + ); + + suiteIt1.layer(barLayer)('nested', (suiteIt2) => { + suiteIt2.effect('adds context', () => + Effect.gen(function* testEffect() { + const foo = yield* Foo; + const bar = yield* Bar; + expect(foo).toEqual('foo'); + expect(bar).toEqual('bar'); + }), + ); + }); + + suiteIt1.layer(barLayer)((suiteIt3) => { + suiteIt3.effect('without name', () => + Effect.gen(function* testEffect() { + const foo = yield* Foo; + const bar = yield* Bar; + expect(foo).toEqual('foo'); + expect(bar).toEqual('bar'); + }), + ); + }); + + describe('release', () => { + let released = false; + afterAll(() => { + expect(released).toEqual(true); + }); + + const scopedLayer = Layer.effect(Scoped)( + Effect.acquireRelease(Effect.succeed('scoped' as const), () => + Effect.sync(() => (released = true)), + ), + ); + + suiteIt1.layer(scopedLayer)((suiteIt4) => { + suiteIt4.effect('adds context', () => + Effect.gen(function* testEffect() { + const foo = yield* Foo; + const scoped = yield* Scoped; + expect(foo).toEqual('foo'); + expect(scoped).toEqual('scoped'); + }), + ); + }); + + suiteIt1.effect.prop( + 'adds context', + [realNumber], + ([num]) => + Effect.gen(function* testEffect() { + const foo = yield* Foo; + expect(foo).toEqual('foo'); + return !Number.isNaN(num); + }), + { fastCheck: { numRuns: 200 } }, + ); + }); + }); + + layer(sleeperLayer)('test services', (suiteIt5) => { + suiteIt5.effect('TestClock', () => + Effect.gen(function* testEffect() { + const sleeper = yield* Sleeper; + const fiber = yield* Effect.forkChild(sleeper.sleep(100_000)); + yield* Effect.yieldNow; + yield* TestClock.adjust(100_000); + yield* Fiber.join(fiber); + }), + ); + }); + + layer(fooLayer)('with a name', (suiteIt6) => { + describe('with a nested describe', () => { + suiteIt6.effect('adds context', () => + Effect.gen(function* testEffect() { + const foo = yield* Foo; + expect(foo).toEqual('foo'); + }), + ); + }); + suiteIt6.effect('adds context', () => + Effect.gen(function* testEffect() { + const foo = yield* Foo; + expect(foo).toEqual('foo'); + }), + ); + }); + + layer(sleeperLayer, { excludeTestServices: true })('live services', (suiteIt7) => { + suiteIt7.effect('Clock', () => + Effect.gen(function* testEffect() { + const sleeper = yield* Sleeper; + yield* sleeper.sleep(1); + }), + ); + }); +}); + +// property testing + +it.prop('symmetry', [realNumber, FastCheck.integer()], ([a, b]) => { + expect(a + b).toBe(b + a); +}); + +it.prop('symmetry with object', { a: realNumber, b: FastCheck.integer() }, ({ a, b }) => { + expect(a + b).toBe(b + a); +}); + +it.live.prop('schema with object', { value: Schema.Int }, ({ value }) => + Effect.sync(() => assert.isTrue(Number.isInteger(value))), +); + +it.effect.prop('symmetry', [realNumber, FastCheck.integer()], ([a, b]) => + Effect.gen(function* testEffect() { + yield* Effect.void; + assert.isTrue(a + b === b + a); + }), +); + +it.effect.prop('symmetry with object', { a: realNumber, b: FastCheck.integer() }, ({ a, b }) => + Effect.gen(function* testEffect() { + yield* Effect.void; + assert.strictEqual(a + b, b + a); + }), +); + +it.effect.prop( + 'should detect the substring', + { a: FastCheck.string(), b: FastCheck.string(), c: FastCheck.string() }, + ({ a, b, c }) => + Effect.gen(function* testEffect() { + yield* Effect.scope; + assert.include(a + b + c, b); + }), +); diff --git a/app/packages/effect-rstest/tests/isolation.test.ts b/app/packages/effect-rstest/tests/isolation.test.ts new file mode 100644 index 000000000..b077d662a --- /dev/null +++ b/app/packages/effect-rstest/tests/isolation.test.ts @@ -0,0 +1,117 @@ +import { TodoService } from './support/todo-service.ts'; +import { State } from './support/state.ts'; +import { Scoped } from './support/scoped.ts'; +import { afterAll, assert, describe, it } from '@app/effect-rstest'; +import { Effect, Layer, Ref } from 'effect'; + +describe('top-level it.layer isolation', () => { + let nextId = 0; + const observedStateIds: number[] = []; + + const baseLayer = Layer.effect(State)( + Effect.gen(function* testEffect() { + nextId += 1; + const id = nextId; + const todos = yield* Ref.make([]); + const migrated = yield* Ref.make(false); + return { id, migrated, todos }; + }), + ); + + const migrationLayer = Layer.effectDiscard( + Effect.gen(function* testEffect() { + const state = yield* State; + yield* Ref.set(state.migrated, true); + }), + ); + + const migratedLayer = Layer.merge(baseLayer, migrationLayer.pipe(Layer.provide(baseLayer))); + + const inMemoryLayer = Layer.effect(TodoService)( + Effect.gen(function* testEffect() { + const state = yield* State; + return { + add: (title: string) => Ref.update(state.todos, (todos) => [...todos, title]), + list: Ref.get(state.todos), + migrated: Ref.get(state.migrated), + stateId: Effect.succeed(state.id), + } as const; + }), + ).pipe(Layer.provide(migratedLayer)); + + it.layer(inMemoryLayer)((suiteIt0) => { + suiteIt0.effect('first block mutates isolated state', () => + Effect.gen(function* testEffect() { + const service = yield* TodoService; + const stateId = yield* service.stateId; + const migrated = yield* service.migrated; + + observedStateIds.push(stateId); + yield* service.add('write tests'); + + assert.isTrue(migrated); + assert.deepStrictEqual(yield* service.list, ['write tests']); + }), + ); + }); + + it.layer(inMemoryLayer)((suiteIt1) => { + suiteIt1.effect('second block starts fresh', () => + Effect.gen(function* testEffect() { + const service = yield* TodoService; + const stateId = yield* service.stateId; + const migrated = yield* service.migrated; + + observedStateIds.push(stateId); + + assert.isTrue(migrated); + assert.deepStrictEqual(yield* service.list, []); + + yield* service.add('ship feature'); + assert.deepStrictEqual(yield* service.list, ['ship feature']); + }), + ); + }); + + it.layer(inMemoryLayer)((suiteIt2) => { + suiteIt2.effect('third block also starts fresh', () => + Effect.gen(function* testEffect() { + const service = yield* TodoService; + const stateId = yield* service.stateId; + const migrated = yield* service.migrated; + + observedStateIds.push(stateId); + + assert.isTrue(migrated); + assert.deepStrictEqual(yield* service.list, []); + }), + ); + }); + + afterAll(() => { + assert.deepStrictEqual(observedStateIds, [1, 2, 3]); + }); +}); + +describe('unnamed layer release boundary', () => { + let released = false; + + const scopedLayer = Layer.effect(Scoped)( + Effect.acquireRelease(Effect.succeed('scoped' as const), () => + Effect.sync(() => (released = true)), + ), + ); + + it.layer(scopedLayer)((suiteIt3) => { + suiteIt3.effect('uses resource', () => + Effect.gen(function* usesResource() { + const value = yield* Scoped; + assert.strictEqual(value, 'scoped'); + }), + ); + }); + + it('later test sees released resource', () => { + assert.isTrue(released); + }); +}); diff --git a/app/packages/effect-rstest/tests/nested-isolation.test.ts b/app/packages/effect-rstest/tests/nested-isolation.test.ts new file mode 100644 index 000000000..5fb3c5217 --- /dev/null +++ b/app/packages/effect-rstest/tests/nested-isolation.test.ts @@ -0,0 +1,203 @@ +import { TodoService } from './support/todo-service.ts'; +import { State } from './support/state.ts'; +import { SharedChild } from './support/shared-child.ts'; +import { Parent } from './support/parent.ts'; +import { Child } from './support/child.ts'; +import { afterAll, assert, beforeAll, describe, expect, it, layer } from '@app/effect-rstest'; +import { Effect, Layer, Ref } from 'effect'; + +describe('nested sibling layers', () => { + let nextChildId = 0; + let firstChildId = -1; + let secondChildId = -1; + const releasedChildIds: number[] = []; + + const parentLayer = Layer.succeed(Parent)('parent'); + + const childLayer = Layer.effect(Child)( + Parent.pipe( + Effect.flatMap(() => { + nextChildId += 1; + const id = nextChildId; + return Effect.acquireRelease(Effect.succeed({ id }), () => + Effect.sync(() => { + releasedChildIds.push(id); + }), + ); + }), + ), + ); + + layer(parentLayer)('parent', (suiteIt0) => { + suiteIt0.layer(childLayer)('first sibling', (suiteIt1) => { + suiteIt1.effect('allocates child', () => + Effect.gen(function* testEffect() { + const child = yield* Child; + firstChildId = child.id; + + assert.strictEqual(child.id, 1); + assert.deepStrictEqual(releasedChildIds, []); + }), + ); + }); + + suiteIt0.layer(childLayer)('second sibling', (suiteIt2) => { + beforeAll(() => { + expect(releasedChildIds).toEqual([firstChildId]); + }); + + suiteIt2.effect('allocates a fresh child', () => + Effect.gen(function* testEffect() { + const child = yield* Child; + secondChildId = child.id; + + assert.strictEqual(child.id, 2); + assert.isTrue(child.id !== firstChildId); + assert.deepStrictEqual(releasedChildIds, [firstChildId]); + }), + ); + }); + + afterAll(() => { + expect(firstChildId).toEqual(1); + expect(secondChildId).toEqual(2); + expect(releasedChildIds).toEqual([1, 2]); + }); + }); +}); + +describe.concurrent('nested sibling layers in concurrent suites', () => { + let nextSharedId = 0; + let firstSharedId: number | undefined; + let secondSharedId: number | undefined; + const releasedSharedIds: number[] = []; + + const parentLayer = Layer.succeed(Parent)('parent'); + + const sharedChildLayer = Layer.effect(SharedChild)( + Parent.pipe( + Effect.flatMap(() => + Effect.gen(function* testEffect() { + yield* Effect.yieldNow; + + nextSharedId += 1; + const id = nextSharedId; + return yield* Effect.acquireRelease(Effect.succeed({ id }), () => + Effect.sync(() => { + releasedSharedIds.push(id); + }), + ); + }), + ), + ), + ); + + layer(parentLayer)('parent', (suiteIt3) => { + describe.concurrent('concurrent siblings', () => { + suiteIt3.layer(sharedChildLayer)('first sibling', (suiteIt4) => { + suiteIt4.effect('captures shared child', () => + Effect.gen(function* testEffect() { + const child = yield* SharedChild; + firstSharedId = child.id; + assert.isTrue(child.id === 1 || child.id === 2); + }), + ); + }); + + suiteIt3.layer(sharedChildLayer)('second sibling', (suiteIt5) => { + suiteIt5.effect('allocates an isolated child', () => + Effect.gen(function* testEffect() { + const child = yield* SharedChild; + secondSharedId = child.id; + assert.isTrue(child.id === 1 || child.id === 2); + }), + ); + }); + }); + + afterAll(() => { + expect(firstSharedId).not.toEqual(secondSharedId); + expect(nextSharedId).toEqual(2); + expect(releasedSharedIds.toSorted((a, b) => a - b)).toEqual([1, 2]); + }); + }); +}); + +describe('nested sibling isolation with provided state graph', () => { + const parentLayer = Layer.succeed(Parent)('parent'); + + let nextId = 0; + let firstStateId = -1; + let secondStateId = -1; + + const baseLayer = Layer.effect(State)( + Effect.gen(function* testEffect() { + nextId += 1; + const id = nextId; + const todos = yield* Ref.make([]); + const migrated = yield* Ref.make(false); + return { id, migrated, todos }; + }), + ); + + const migrationLayer = Layer.effectDiscard( + Effect.gen(function* testEffect() { + const state = yield* State; + yield* Ref.set(state.migrated, true); + }), + ); + + const migratedLayer = Layer.merge(baseLayer, migrationLayer.pipe(Layer.provide(baseLayer))); + + const inMemoryLayer = Layer.effect(TodoService)( + Effect.gen(function* testEffect() { + const state = yield* State; + return { + add: (title: string) => Ref.update(state.todos, (todos) => [...todos, title]), + list: Ref.get(state.todos), + migrated: Ref.get(state.migrated), + stateId: Effect.succeed(state.id), + } as const; + }), + ).pipe(Layer.provide(migratedLayer)); + + layer(parentLayer)('parent', (suiteIt6) => { + suiteIt6.layer(inMemoryLayer)('first sibling', (suiteIt7) => { + suiteIt7.effect('mutates isolated provided state', () => + Effect.gen(function* testEffect() { + const service = yield* TodoService; + firstStateId = yield* service.stateId; + + assert.isTrue(yield* service.migrated); + yield* service.add('write tests'); + assert.deepStrictEqual(yield* service.list, ['write tests']); + }), + ); + }); + + suiteIt6.layer(inMemoryLayer)('second sibling', (suiteIt8) => { + suiteIt8.effect('starts fresh with a new provided state', () => + Effect.gen(function* testEffect() { + const service = yield* TodoService; + secondStateId = yield* service.stateId; + + assert.isTrue(yield* service.migrated); + assert.deepStrictEqual(yield* service.list, []); + + yield* service.add('ship feature'); + assert.deepStrictEqual(yield* service.list, ['ship feature']); + }), + ); + }); + + afterAll(() => { + expect(firstStateId).toEqual(1); + expect(secondStateId).toEqual(2); + expect(nextId).toEqual(2); + }); + }); +}); + +it('exposes nested layer API', () => { + expect(it.layer).toBeTypeOf('function'); +}); diff --git a/app/packages/effect-rstest/tests/support/bar.ts b/app/packages/effect-rstest/tests/support/bar.ts new file mode 100644 index 000000000..823005bf2 --- /dev/null +++ b/app/packages/effect-rstest/tests/support/bar.ts @@ -0,0 +1,3 @@ +import { Context } from 'effect'; + +export class Bar extends Context.Service()('@app/effect-rstest/tests/support/bar') {} diff --git a/app/packages/effect-rstest/tests/support/child.ts b/app/packages/effect-rstest/tests/support/child.ts new file mode 100644 index 000000000..af4bee949 --- /dev/null +++ b/app/packages/effect-rstest/tests/support/child.ts @@ -0,0 +1,5 @@ +import { Context } from 'effect'; + +export class Child extends Context.Service()( + '@app/effect-rstest/tests/support/child', +) {} diff --git a/app/packages/effect-rstest/tests/support/foo.ts b/app/packages/effect-rstest/tests/support/foo.ts new file mode 100644 index 000000000..fbfda9fb9 --- /dev/null +++ b/app/packages/effect-rstest/tests/support/foo.ts @@ -0,0 +1,3 @@ +import { Context } from 'effect'; + +export class Foo extends Context.Service()('@app/effect-rstest/tests/support/foo') {} diff --git a/app/packages/effect-rstest/tests/support/parent.ts b/app/packages/effect-rstest/tests/support/parent.ts new file mode 100644 index 000000000..6eb27db44 --- /dev/null +++ b/app/packages/effect-rstest/tests/support/parent.ts @@ -0,0 +1,5 @@ +import { Context } from 'effect'; + +export class Parent extends Context.Service()( + '@app/effect-rstest/tests/support/parent', +) {} diff --git a/app/packages/effect-rstest/tests/support/scoped.ts b/app/packages/effect-rstest/tests/support/scoped.ts new file mode 100644 index 000000000..3040e3a1e --- /dev/null +++ b/app/packages/effect-rstest/tests/support/scoped.ts @@ -0,0 +1,5 @@ +import { Context } from 'effect'; + +export class Scoped extends Context.Service()( + '@app/effect-rstest/tests/support/scoped', +) {} diff --git a/app/packages/effect-rstest/tests/support/shared-child.ts b/app/packages/effect-rstest/tests/support/shared-child.ts new file mode 100644 index 000000000..58bd2add0 --- /dev/null +++ b/app/packages/effect-rstest/tests/support/shared-child.ts @@ -0,0 +1,5 @@ +import { Context } from 'effect'; + +export class SharedChild extends Context.Service()( + '@app/effect-rstest/tests/support/shared-child/SharedChild', +) {} diff --git a/app/packages/effect-rstest/tests/support/sleeper.ts b/app/packages/effect-rstest/tests/support/sleeper.ts new file mode 100644 index 000000000..809afaf9c --- /dev/null +++ b/app/packages/effect-rstest/tests/support/sleeper.ts @@ -0,0 +1,9 @@ +import { Context } from 'effect'; +import type { Effect } from 'effect'; + +export class Sleeper extends Context.Service< + Sleeper, + { + readonly sleep: (ms: number) => Effect.Effect; + } +>()('@app/effect-rstest/tests/support/sleeper') {} diff --git a/app/packages/effect-rstest/tests/support/state.ts b/app/packages/effect-rstest/tests/support/state.ts new file mode 100644 index 000000000..0db192f47 --- /dev/null +++ b/app/packages/effect-rstest/tests/support/state.ts @@ -0,0 +1,7 @@ +import { Context } from 'effect'; +import type { Ref } from 'effect'; + +export class State extends Context.Service< + State, + { readonly id: number; readonly migrated: Ref.Ref; readonly todos: Ref.Ref } +>()('@app/effect-rstest/tests/support/state') {} diff --git a/app/packages/effect-rstest/tests/support/todo-service.ts b/app/packages/effect-rstest/tests/support/todo-service.ts new file mode 100644 index 000000000..1c62973b7 --- /dev/null +++ b/app/packages/effect-rstest/tests/support/todo-service.ts @@ -0,0 +1,12 @@ +import { Context } from 'effect'; +import type { Effect } from 'effect'; + +export class TodoService extends Context.Service< + TodoService, + { + readonly add: (title: string) => Effect.Effect; + readonly list: Effect.Effect; + readonly migrated: Effect.Effect; + readonly stateId: Effect.Effect; + } +>()('@app/effect-rstest/tests/support/todo-service/TodoService') {} diff --git a/app/packages/effect-rstest/tsconfig.json b/app/packages/effect-rstest/tsconfig.json new file mode 100644 index 000000000..f6e6427b1 --- /dev/null +++ b/app/packages/effect-rstest/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": false, + "emitDeclarationOnly": true, + "incremental": true, + "noEmit": false, + "outDir": "../../node_modules/.cache/tsgo/declarations/packages__effect-rstest", + "tsBuildInfoFile": "../../node_modules/.cache/tsgo/packages__effect-rstest.tsbuildinfo", + "types": [ + "node" + ] + }, + "include": [ + "src", + "tests", + "rstest.config.ts" + ] +} diff --git a/app/packages/shared-contracts/package.json b/app/packages/shared-contracts/package.json index e51a45bc9..96d0b18fa 100644 --- a/app/packages/shared-contracts/package.json +++ b/app/packages/shared-contracts/package.json @@ -10,7 +10,7 @@ }, "scripts": { "test:types": "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": { @@ -20,6 +20,8 @@ }, "devDependencies": { "@effect/tsgo": "0.19.0", - "@types/node": "20.19.43" + "@types/node": "20.19.43", + "@app/effect-rstest": "workspace:*", + "@rstest/core": "0.11.10" } } 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/pnpm-lock.yaml b/app/pnpm-lock.yaml index 54f90eb5c..a51e91f3a 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -242,6 +242,9 @@ importers: specifier: 7.18.1 version: 7.18.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) devDependencies: + '@app/effect-rstest': + specifier: workspace:* + version: link:../../packages/effect-rstest '@cloudflare/workers-types': specifier: 5.20260810.1 version: 5.20260810.1 @@ -333,6 +336,12 @@ importers: specifier: 8.22.0 version: 8.22.0 devDependencies: + '@app/effect-rstest': + specifier: workspace:* + version: link:../effect-rstest + '@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) '@types/node': specifier: ^20.19.43 version: 20.19.43 @@ -365,6 +374,22 @@ importers: specifier: 20.19.43 version: 20.19.43 + packages/effect-rstest: + dependencies: + '@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) + effect: + specifier: 4.0.0-beta.107 + version: 4.0.0-beta.107(patch_hash=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f) + devDependencies: + '@effect/tsgo': + specifier: 0.19.0 + version: 0.19.0 + '@types/node': + specifier: 20.19.43 + version: 20.19.43 + packages/shared-contracts: dependencies: '@app/core-runtime': @@ -377,9 +402,15 @@ importers: specifier: 4.0.0-beta.107 version: 4.0.0-beta.107(patch_hash=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f) devDependencies: + '@app/effect-rstest': + specifier: workspace:* + version: link:../effect-rstest '@effect/tsgo': specifier: 0.19.0 version: 0.19.0 + '@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) '@types/node': specifier: 20.19.43 version: 20.19.43 @@ -471,6 +502,9 @@ importers: specifier: 7.18.1 version: 7.18.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) devDependencies: + '@app/effect-rstest': + specifier: workspace:* + version: link:../../packages/effect-rstest '@effect/tsgo': specifier: 0.19.0 version: 0.19.0 diff --git a/app/tsconfig.json b/app/tsconfig.json index e500a6889..5611403cb 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -18,6 +18,9 @@ }, { "path": "verticals/party-registry" + }, + { + "path": "packages/effect-rstest" } ] } diff --git a/app/verticals/party-registry/package.json b/app/verticals/party-registry/package.json index b63d863d9..d14b18d0a 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" }, @@ -108,7 +108,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", + "@app/effect-rstest": "workspace:*" }, "modernjs": { "preset": "presetUltramodern", diff --git a/app/verticals/party-registry/rstest.config.ts b/app/verticals/party-registry/rstest.config.ts index 5159e7131..708482553 100644 --- a/app/verticals/party-registry/rstest.config.ts +++ b/app/verticals/party-registry/rstest.config.ts @@ -5,14 +5,26 @@ import { defineConfig } from '@rstest/core'; Object.assign(globalThis, { require: createRequire(import.meta.url) }); 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'] }, + ], }); 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 ee1c2558f..4bc0ee366 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, Predicate } from 'effect'; +import { expect, it } from '@app/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 = makeActionGateway(() => 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,727 @@ const makeReads = (displayName: string | null = null): AresApplyReads => ({ }, }), }); - -void 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.ok(Predicate.isTagged(outcome, 'AresApplyCompleted')); - assert.deepEqual( - outcome.completed.map(({ route }) => route), - ['PARTY_UPDATE', 'IDENTIFIER_ADD'], - ); -}); - -void 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, - ); -}); - -void 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.ok(Predicate.isTagged(partial.failed.error, 'TestFailure')); -}); - -void 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.ok(Predicate.isTagged(outcome, '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([]); + }), ); -}); - -void 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.ok(Predicate.isTagged(revisionOutcome, 'AresApplyDeferred')); - assert.ok(Predicate.isTagged(changedOutcome, 'AresApplyDeferred')); - assert.deepEqual(calls, []); -}); - -void 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(Schema.is(AresApplySelectionInvalid)(result.failure), true); - } - } - assert.deepEqual(calls, []); -}); - -void 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', -}; - -void 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 outcome = yield* 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, + }, + ], + }), + }, + }, + ); + 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 = makeActionGateway(() => { + 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, []); -}); - -void 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, []); -}); - -void test('every governed read and selected Action receives fresh audience-scoped authorization', async () => { - const tokens: string[] = []; - const calls: string[] = []; - const delegate = makeReads(); - const issued = makeActionGateway(() => { - 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', - ]); -}); - -void 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, []); -}); - -void 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', + ]); + }), ); -}); - -void 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.ok(Predicate.isTagged(outcome, 'AresApplyDeferred')); - assert.deepEqual(calls, []); -}); - -void 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', - ]); -}); - -void 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.ok(Predicate.isTagged(outcome, 'AresApplyPartiallyCompleted')); - assert.equal(outcome.completed.length, 1); - assert.deepEqual(calls, [ - 'update-party|Bearer signed-gateway-token', - 'add-party-official-identifier|Bearer signed-gateway-token', - ]); -}); - -void 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.ok(Predicate.isTagged(stale, '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, []); -}); - -void 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, []); -}); - -void 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, []); }); From 02ed2745cbbd15aafd65e774aa1e33c5cc349abe Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 00:21:47 +0200 Subject: [PATCH 09/38] Limit assertion tag detection to compared value projections --- .../rules/no-manual-tag-comparison.ts | 59 +++++++++++++++---- .../tests/unit/assertions.test.ts | 7 ++- .../valid/packages/core-runtime/src/adt.ts | 2 + .../core-runtime/src/native-assertions.ts | 4 ++ 4 files changed, 61 insertions(+), 11 deletions(-) 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 00c52ae02..a273294fe 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 @@ -788,17 +788,56 @@ export const rule = defineRule({ const expression = unwrap(node); const initialiser = constInitialiser(context, expression); if (initialiser !== null) return comparedTag(initialiser, seen); - // Follow projections and boolean guards; object literals remain complete contract assertions. - if (expression.type === 'ObjectExpression' || expression.type === 'TemplateLiteral') - return null; - for (const [key, value] of Object.entries(expression)) { - if (key === 'parent' || key === 'typeAnnotation') continue; - const children = Array.isArray(value) ? value : [value]; - for (const child of children) { - if (typeof child !== 'object' || child === null || !('type' in child)) continue; - const found = comparedTag(child as ESTree.Node, seen); - if (found !== null) return found; + // Follow values that reach the comparison. Reading a tag inside an arbitrary + // callback or predicate does not make that function or its result a tag value. + let values: readonly ESTree.Node[]; + switch (expression.type) { + case 'ArrayExpression': + values = expression.elements.filter((element) => element !== null); + break; + case 'SpreadElement': + values = [expression.argument]; + break; + case 'LogicalExpression': + values = [expression.left, expression.right]; + break; + case 'ConditionalExpression': + values = [expression.consequent, expression.alternate]; + break; + case 'SequenceExpression': + values = expression.expressions.slice(-1); + break; + case 'CallExpression': { + const callee = unwrap(expression.callee); + if (callee.type !== 'MemberExpression' || memberPropertyName(callee) !== 'map') + return null; + const first = expression.arguments[0]; + if (first === undefined || first.type === 'SpreadElement') return null; + const callback = unwrap(constInitialiser(context, first) ?? first); + if (callback.type !== 'ArrowFunctionExpression' && callback.type !== 'FunctionExpression') + return null; + if (callback.body === null) return null; + values = [callback.body]; + break; } + case 'BlockStatement': + values = expression.body; + break; + case 'ReturnStatement': + values = expression.argument === null ? [] : [expression.argument]; + break; + case 'IfStatement': + values = + expression.alternate === null + ? [expression.consequent] + : [expression.consequent, expression.alternate]; + break; + default: + return null; + } + for (const value of values) { + const found = comparedTag(value, seen); + if (found !== null) return found; } return null; }; 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 index aef9108d3..0c534e283 100644 --- 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 @@ -1,4 +1,4 @@ -// expect-count: 19 +// 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 '@app/effect-rstest'; @@ -30,3 +30,8 @@ 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/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 f8d05b9cf..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 @@ -26,3 +26,5 @@ switch (option._tag) { 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/native-assertions.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/native-assertions.ts index d3e5fc0e6..031ad0643 100644 --- 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 @@ -19,3 +19,7 @@ function shadowedAssertion(assert: typeof import('node:assert/strict')) { 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); From 095e1ae802914750d314494d8fff9d2091d11d9e Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 00:41:41 +0200 Subject: [PATCH 10/38] Resolve Promise-returning generic function aliases at port declarations --- .../http-principal-authentication.test.ts | 10 +++-- .../rules/no-promise-shaped-port.ts | 39 ++++++++++++++----- .../src/generic-operation-ports.ts | 14 ++++++- .../core-runtime/src/generic-effect-ports.ts | 8 ++++ ...teway-assertion-redemption-runtime.test.ts | 6 +-- 5 files changed, 59 insertions(+), 18 deletions(-) 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 ee35fcd35..eb0f6abdc 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 @@ -3,7 +3,7 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; 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, @@ -140,9 +140,11 @@ test('mounted HTTP authentication maps verifier classes, challenges unusable cre expectedStatus === 401 ? authenticationProblem() : unavailableProblem(), ); const body = yield* Schema.decodeUnknownEffect(ProblemResponseSchema)(rawBody); - assert.equal( - body._tag, - expectedStatus === 401 ? 'FixtureAuthenticationProblem' : 'FixtureUnavailableProblem', + assert.ok( + Predicate.isTagged( + body, + expectedStatus === 401 ? 'FixtureAuthenticationProblem' : 'FixtureUnavailableProblem', + ), ); assert.equal(body.status, expectedStatus); } 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 8c51c0156..185550be2 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 @@ -565,7 +565,8 @@ export const rule = defineRule({ /** The `Promise` / `PromiseLike` reference of a return/value annotation, if any. */ const promiseReference = ( - annotation: ESTree.TSTypeAnnotation | null | undefined, + annotation: ESTree.TSTypeAnnotation | ESTree.TSTypeReference | null | undefined, + functionAliasOnly = false, ): string | null => { if (annotation === null || annotation === undefined) return null; interface TypeBinding { @@ -576,19 +577,26 @@ export const rule = defineRule({ raw: any, seen = new Set(), substitutions: ReadonlyMap = new Map(), + insideFunction = false, ): string | null => { if (!raw || seen.has(raw)) return null; seen.add(raw); if (raw.type === 'TSTypeAnnotation' || raw.type === 'TSParenthesizedType') - return resolve(raw.typeAnnotation, seen, substitutions); + return resolve(raw.typeAnnotation, seen, substitutions, insideFunction); + // Direct function types have their own visitor. Applied aliases need this + // traversal here because that visitor cannot see the use-site substitutions. + if (raw.type === 'TSFunctionType') + return substitutions.size === 0 + ? null + : resolve(raw.returnType, seen, substitutions, true); if (raw.type === 'TSTypeParameter') return ( - resolve(raw.constraint, new Set(seen), substitutions) ?? - resolve(raw.default, new Set(seen), substitutions) + resolve(raw.constraint, new Set(seen), substitutions, insideFunction) ?? + resolve(raw.default, new Set(seen), substitutions, insideFunction) ); if (raw.type === 'TSUnionType' || raw.type === 'TSIntersectionType') { for (const item of raw.types) { - const result = resolve(item, new Set(seen), substitutions); + const result = resolve(item, new Set(seen), substitutions, insideFunction); if (result) return result; } return null; @@ -603,11 +611,11 @@ export const rule = defineRule({ !variableFor(raw, 'globalThis')?.defs.length && options.promiseTypes.includes(name) ) - return `${name}<…>`; + return functionAliasOnly && !insideFunction ? null : `${name}<…>`; if (names.length !== 1) return null; const variable = variableFor(raw.typeName, name); const bound = substitutions.get(variable); - if (bound) return resolve(bound.node, seen, bound.substitutions); + if (bound) return resolve(bound.node, seen, bound.substitutions, insideFunction); const alias = variable?.defs.find( (d: any) => d.node.type === 'TSTypeAliasDeclaration', )?.node; @@ -624,12 +632,12 @@ export const rule = defineRule({ substitutions: argument ? substitutions : applied, }); } - return resolve(alias.typeAnnotation, seen, applied); + return resolve(alias.typeAnnotation, seen, applied, insideFunction); } const parameter = variable?.defs.find((d: any) => d.node.type === 'TSTypeParameter')?.node; - if (parameter) return resolve(parameter, seen, substitutions); + if (parameter) return resolve(parameter, seen, substitutions, insideFunction); if (variable?.defs.length || !options.promiseTypes.includes(name)) return null; - return `${name}<…>`; + return functionAliasOnly && !insideFunction ? null : `${name}<…>`; }; return resolve(annotation); }; @@ -900,6 +908,17 @@ export const rule = defineRule({ }; return { + TSTypeReference: (node: ESTree.TSTypeReference) => { + const annotation = parentOf(node as unknown as AnyNode); + if (annotation?.type !== 'TSTypeAnnotation') return; + const owner = parentOf(annotation); + if (owner?.type !== 'Identifier' && owner?.type !== 'RestElement') 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; 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 index 2fde48c8c..98ca30e31 100644 --- 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 @@ -1,4 +1,4 @@ -// expect-count: 7 +// expect-count: 16 import type { Effect } from 'effect'; type Operation = PromiseLike | Effect.Effect; interface Constrained = Operation> { run(): R; } @@ -10,3 +10,15 @@ 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; } 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 index 517869754..c3fe79417 100644 --- 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 @@ -7,3 +7,11 @@ 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; } 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 a53e281ae..60746fcd9 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 @@ -2,7 +2,7 @@ import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; import { GATEWAY_ASSERTION_CLOCK_SKEW_SECONDS } from '@app/shared-contracts'; import { PgClient } from '@effect/sql-pg'; import { makeWithDefaults } from 'drizzle-orm/effect-postgres'; -import { Cause, Clock, Effect, Exit, Schema, Stream } from 'effect'; +import { Cause, Clock, Effect, Exit, Schema, Stream, Predicate } from 'effect'; import { TestClock } from 'effect/testing'; import { Reactivity } from 'effect/unstable/reactivity'; import type { Connection } from 'effect/unstable/sql/SqlConnection'; @@ -73,7 +73,7 @@ for (const settlement of ['COMMIT', 'ROLLBACK']) { const failure = yield* fixture.redemption .consume(assertion) .pipe(Effect.provideService(Clock.Clock, fixture.clock), Effect.flip); - assert.equal(failure._tag, 'GatewayAssertionRedemptionUnavailableError'); + assert.ok(Predicate.isTagged(failure, 'GatewayAssertionRedemptionUnavailableError')); const serializedFailure = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))( failure, ); @@ -120,7 +120,7 @@ test('rejects assertions crossing expiry before redemption without deleting repl const failure = yield* fixture.redemption .consume(assertion) .pipe(Effect.provideService(Clock.Clock, fixture.clock), Effect.flip); - assert.equal(failure._tag, 'GatewayAssertionReplayError'); + assert.ok(Predicate.isTagged(failure, 'GatewayAssertionReplayError')); } assert.deepEqual(statements, [], 'expired assertions cannot run replay-evidence cleanup'); }).pipe(Effect.scoped), From 7e60d5514a0c99f59d5ad545b18c2bfd9137241e Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 02:17:35 +0200 Subject: [PATCH 11/38] test(rstest): migrate every test to Rstest + @app/effect-rstest and delete the Promise bridges - all app, tooling, scaffolding and lint-rule tests run through Rstest projects (it.effect / it.live / it.layer) - remove packages/core-runtime testing/effect-runtime bridges; fixture factories return Effects - lint: restrict node:test / node:assert / @rstest/core / effect-runtime imports in tests, lint tooling tests - rstest configs: SWC .mts parser override, core-runtime externals for natively imported generated modules - no-promise-shaped-port: resolve substitutions through generic object and interface aliases Co-Authored-By: Claude Fable 5.1 --- app/apps/shell-super-app/api/auth/service.ts | 27 +- app/apps/shell-super-app/rstest.config.ts | 32 + .../shell-super-app/tests/e2e/login.spec.ts | 10 +- .../tests/integration/auth-runtime.test.ts | 2145 +++-- .../generated-owner-isolation.test.ts | 1688 ++-- .../identity-modes-runtime.test.ts | 959 +- .../module-catalog-runtime.test.ts | 251 +- .../module-federation-i18n-runtime.test.ts | 50 +- .../integration/stage-demo-bootstrap.test.ts | 156 +- .../tests/unit/api-index.test.ts | 2 +- .../tests/unit/auth-boundary.test.ts | 2 +- .../tests/unit/auth-contract.test.ts | 287 +- .../tests/unit/auth-db-client.test.ts | 33 +- .../tests/unit/auth-schema.test.ts | 2 +- .../tests/unit/browser-effect-runtime.test.ts | 2 +- .../tests/unit/deployment-allowlist.test.ts | 109 +- .../tests/unit/gateway-issuer.test.ts | 608 +- .../tests/unit/identity-lifecycle.test.ts | 828 +- .../tests/unit/impersonation-service.test.ts | 1183 +-- .../unit/installed-module-catalog.test.ts | 420 +- .../unit/installed-outbox-matcher.test.ts | 61 +- .../tests/unit/installed-verticals.test.ts | 100 +- .../tests/unit/layout.test.tsx | 2 +- .../tests/unit/legal-entity-selection.test.ts | 73 +- .../unit/module-entrypoint-loader.test.ts | 594 +- .../tests/unit/routes/home/loader.test.ts | 307 +- .../tests/unit/routes/home/page.test.tsx | 125 +- .../tests/unit/routes/login/locales.test.ts | 2 +- .../tests/unit/routes/login/page.test.tsx | 270 +- .../tests/unit/routes/modules/loader.test.ts | 139 +- .../tests/unit/routes/modules/page.test.tsx | 391 +- .../tests/unit/shell-composition.test.ts | 412 +- .../unit/shell-governed-read-schemas.test.ts | 2 +- .../tests/unit/shell-resources.test.ts | 899 +- .../EFFECT_V4_LINT_ENFORCEMENT.md | 4 +- app/oxlint.config.ts | 115 +- app/package.json | 16 +- app/packages/core-runtime/package.json | 1 - app/packages/core-runtime/rstest.config.ts | 4 + .../core-runtime/src/testing/actions.ts | 39 +- .../src/testing/effect-runtime.ts | 6 - .../integration/action-permission.test.ts | 973 +- .../tests/integration/action-runtime.test.ts | 2296 +++-- .../contacts-identity-migration.test.ts | 69 +- .../tests/integration/context-access.test.ts | 50 +- .../integration/identity-runtime.test.ts | 1102 ++- .../integration/legal-entity-context.test.ts | 159 +- .../integration/module-state-gate.test.ts | 223 +- .../tests/integration/pool-deadlines.test.ts | 151 +- .../integration/principal-management.test.ts | 124 +- .../integration/principal-resolver.test.ts | 90 +- .../tests/integration/read-runtime.test.ts | 199 +- .../integration/search-persistence.test.ts | 268 +- .../search-worker-snapshot.test.ts | 109 +- .../integration/tenant-isolation.test.ts | 155 +- .../integration/tenant-module-state.test.ts | 837 +- .../core-runtime/tests/support/database.ts | 49 +- .../tests/support/effect-runtime.ts | 34 - .../unit/action-authorization-rollout.test.ts | 94 +- .../tests/unit/action-collector.test.ts | 296 +- .../tests/unit/action-definition.test.ts | 273 +- .../tests/unit/action-errors.test.ts | 23 +- .../tests/unit/action-identity.test.ts | 239 +- .../tests/unit/action-permission.test.ts | 532 +- .../tests/unit/action-policy.test.ts | 101 +- .../tests/unit/action-public-surface.test.ts | 87 +- .../tests/unit/action-runtime.test.ts | 3230 +++---- .../tests/unit/action-testing-harness.test.ts | 247 +- .../unit/application-composition.test.ts | 478 +- .../tests/unit/catalog-contract.test.ts | 19 +- .../unit/commit-recovery-metadata.test.ts | 317 +- .../core-runtime/tests/unit/config.test.ts | 160 +- .../tests/unit/context-access.test.ts | 228 +- .../unit/database-driver-failure.test.ts | 136 +- .../unit/entrypoint-classification.test.ts | 36 +- .../tests/unit/legal-entity-context.test.ts | 89 +- .../tests/unit/module-catalog.test.ts | 82 +- .../tests/unit/module-manifest.test.ts | 342 +- .../tests/unit/module-state-gate.test.ts | 707 +- .../unit/native-transaction-context.test.ts | 200 +- .../tests/unit/native-transaction.test.ts | 304 +- .../tests/unit/operation-context.test.ts | 239 +- .../tests/unit/outbox-health.test.ts | 107 +- .../tests/unit/outbox-poller.test.ts | 158 +- .../tests/unit/outbox-process.test.ts | 16 +- .../tests/unit/outbox-runtime.test.ts | 410 +- .../tests/unit/permission-client.test.ts | 180 +- .../tests/unit/pool-configuration.test.ts | 122 +- .../tests/unit/principal-management.test.ts | 152 +- .../tests/unit/principal-resolver.test.ts | 143 +- .../tests/unit/read-definition.test.ts | 56 +- .../tests/unit/read-runtime.test.ts | 1470 ++- .../tests/unit/schema-contract.test.ts | 113 +- .../tests/unit/scoped-transaction.test.ts | 82 +- .../tests/unit/search-ingestion.test.ts | 60 +- .../tests/unit/search-projection.test.ts | 203 +- .../tests/unit/search-schema.test.ts | 144 +- .../tests/unit/search-worker-snapshot.test.ts | 83 +- .../tests/unit/service-public-surface.test.ts | 7 +- .../tests/unit/shell-contribution.test.ts | 60 +- .../tests/unit/spicedb-client.test.ts | 26 +- .../unit/spicedb-database-bootstrap.test.ts | 141 +- .../unit/stage-context-bootstrap.test.ts | 7 +- .../unit/system-principal-context.test.ts | 121 +- .../tests/unit/tenant-module-state.test.ts | 186 +- app/packages/effect-rstest/README.md | 45 + .../effect-rstest/tests/index.test.ts | 8 +- .../gateway-principal-verifier/package.json | 6 +- .../rstest.config.ts | 5 + .../unit/gateway-principal-verifier.test.ts | 272 +- .../tests/unit/client-runtime.test.ts | 316 +- .../tests/unit/gateway-context.test.ts | 179 +- app/pnpm-lock.yaml | 39 +- app/pnpm-workspace.yaml | 2 - app/rstest.config.ts | 28 + app/scripts/local-environment-values.test.mts | 126 +- .../tests/module-contract-generator.test.mts | 905 +- .../tests/resource-generator.test.mts | 736 +- .../tests/retire-contribution.test.mts | 444 +- .../tests/scaffold-generators.test.mts | 8057 +++++++++-------- app/scripts/tests/api-only-tooling.test.mts | 1341 +-- .../audit-database-trust-boundaries.test.mts | 337 +- .../authorization-rollout-contract.test.mts | 86 +- .../check-authorization-readiness.test.mts | 152 +- .../tests/database-access-boundaries.test.mts | 177 +- .../initialize-local-development.test.mts | 356 +- app/scripts/tests/locki-feature.test.mts | 294 +- .../migrate-contacts-authorization.test.mts | 36 +- .../module-entrypoint-boundaries.test.mts | 807 +- .../tests/outbox-worker-delivery.test.mts | 316 +- .../tests/plan-deployment-impact.test.mts | 921 +- .../protected-entrypoint-inventory.test.mts | 70 +- ...sion-current-action-authorization.test.mts | 1098 ++- .../tests/quality-audit-model.test.mts | 882 +- .../quality-audit-runtime-model.test.mts | 534 +- app/scripts/tests/quality-audit.test.mts | 1139 ++- ...-fail-closed-authorization-impact.test.mts | 40 +- app/scripts/tests/root-environment.test.mts | 51 +- .../typecheck-project-references.test.mts | 249 +- .../validate-ultramodern-workspace.mts | 13 +- app/tools/oxlint/effect-native/README.md | 22 +- .../effect-native/repository-policy.config.ts | 34 +- .../rules/no-effect-run-in-tests.ts | 29 +- .../rules/no-promise-shaped-port.ts | 60 +- .../tests/discover-rules.test.mts | 61 +- .../effect-native/tests/fixtures.test.mts | 53 +- .../invalid/tests/harness/it-layer.ts | 5 + .../invalid/tests/support/effect-harness.ts | 5 + .../invalid/tests/support/it-effect.mts | 5 + .../valid/harness-usage.test.tsx | 10 +- .../valid/tests/harness/it-layer.ts | 11 +- .../valid/tests/support/effect-harness.ts | 21 +- .../src/generic-operation-ports.ts | 24 +- .../core-runtime/src/generic-effect-ports.ts | 20 + .../effect-native/tests/launcher.test.mts | 87 +- .../oxlint/effect-native/tests/oxlint.mts | 7 +- .../effect-native/tests/oxlint.test.mts | 43 +- .../oxlint/effect-native/tests/paths.test.mts | 15 +- .../tests/production-options.test.mts | 65 +- .../effect-native/tests/registration.test.mts | 127 +- .../tests/repository-policy.test.mts | 18 +- .../effect-native/tests/script-scope.test.mts | 49 +- .../tests/temporary-workspace.test.mts | 58 +- app/verticals/party-registry/rstest.config.ts | 10 +- .../tests/components/contacts-page.test.tsx | 2 +- .../tests/integration/ares-governed.test.ts | 828 +- .../integration/database-boundary.test.ts | 1532 ++-- .../engagement-database-boundary.test.ts | 200 +- .../integration/governed-identity.test.ts | 225 +- .../integration/identity-concurrency.test.ts | 281 +- .../unit/api-integration-client-url.test.ts | 62 +- .../api-integration-command-client.test.ts | 206 +- .../api-integration-command-contract.test.ts | 128 +- .../api-integration-command-recovery.test.ts | 485 +- .../api-integration-command-runtime.test.ts | 1305 +-- .../unit/api-integration-contract.test.ts | 169 +- .../api-integration-correction-client.test.ts | 310 +- .../unit/api-integration-runtime.test.ts | 76 +- .../unit/ares-application-policy.test.ts | 193 +- .../tests/unit/ares-evidence-contract.test.ts | 280 +- .../tests/unit/ares-lookup-read.test.ts | 300 +- .../tests/unit/ares-subject.service.test.ts | 627 +- .../unit/audit-evidence-contract.test.ts | 48 +- .../tests/unit/catalog-contract.test.ts | 15 +- .../tests/unit/contact-point-contract.test.ts | 151 +- .../contact-point-correction-action.test.ts | 22 +- .../contact-point-persistence.service.test.ts | 892 +- .../tests/unit/correction-contract.test.ts | 741 +- .../tests/unit/cors-origin.test.ts | 7 +- .../tests/unit/counterparty-contract.test.ts | 647 +- .../counterparty-persistence.service.test.ts | 468 +- .../unit/counterparty-role-lifecycle.test.ts | 88 +- .../tests/unit/database-client.test.ts | 37 +- .../unit/engagement-catalog-contract.test.ts | 9 +- .../engagement-profile-api-contract.test.ts | 210 +- ...gement-profile-persistence-service.test.ts | 98 +- .../engagement-reference-validation.test.ts | 32 +- .../unit/engagement-schema-contract.test.ts | 74 +- .../tests/unit/identifier-contract.test.ts | 89 +- .../identifier-persistence.service.test.ts | 370 +- .../unit/identifier-update-outbox.test.ts | 181 +- .../unit/identity-action-evidence.test.ts | 144 +- .../tests/unit/identity-contract.test.ts | 181 +- ...y-create-without-strong-identifier.test.ts | 67 +- .../unit/identity-party-detail-alias.test.ts | 150 +- .../identity-party-detail-history.test.ts | 169 +- .../unit/identity-persistence.service.test.ts | 861 +- .../tests/unit/matching-contract.test.ts | 81 +- .../tests/unit/matching-persistence.test.ts | 1413 ++- .../merge-alias-resolution-service.test.ts | 159 +- .../tests/unit/merge-alias-resolution.test.ts | 28 +- .../unit/merge-collision-reference.test.ts | 74 +- .../unit/merge-readiness-contract.test.ts | 468 +- .../unit/merge-survivor-selection.test.ts | 118 +- .../unit/prepare-contacts-migration.test.ts | 117 +- .../unit/relationship-domain-contract.test.ts | 364 +- .../relationship-operation-contract.test.ts | 182 +- .../relationship-persistence.service.test.ts | 849 +- .../tests/unit/runtime-locales.test.ts | 21 +- .../tests/unit/schema-contract.test.ts | 515 +- .../tests/unit/search-contract.test.ts | 194 +- .../tests/unit/search-core-adapter.test.ts | 86 +- .../tests/unit/search-identifier-sync.test.ts | 93 +- .../tests/unit/search-projector.test.ts | 267 +- .../tests/unit/search-provider.test.ts | 78 +- .../tests/unit/search-rebuild-request.test.ts | 150 +- .../tests/unit/search-semantics.test.ts | 87 +- .../tests/unit/search-source.test.ts | 235 +- .../unit/search-worker-registration.test.ts | 24 +- 229 files changed, 36442 insertions(+), 34777 deletions(-) delete mode 100644 app/packages/core-runtime/src/testing/effect-runtime.ts delete mode 100644 app/packages/core-runtime/tests/support/effect-runtime.ts create mode 100644 app/packages/gateway-principal-verifier/rstest.config.ts create mode 100644 app/rstest.config.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/tests/harness/it-layer.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/tests/support/effect-harness.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/tests/support/it-effect.mts diff --git a/app/apps/shell-super-app/api/auth/service.ts b/app/apps/shell-super-app/api/auth/service.ts index c4ae21ce6..54bd599c3 100644 --- a/app/apps/shell-super-app/api/auth/service.ts +++ b/app/apps/shell-super-app/api/auth/service.ts @@ -1040,18 +1040,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/rstest.config.ts b/app/apps/shell-super-app/rstest.config.ts index 2860aa3bf..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,6 +55,34 @@ 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({ projects: [ { @@ -79,6 +110,7 @@ export default defineConfig({ name: 'integration', testEnvironment: 'node', testTimeout: 30_000, + tools: { rspack: { externals: [externalizeCoreRuntime] }, swc }, }, ], }); 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 2ff6f6aaa..bbdcaab75 100644 --- a/app/apps/shell-super-app/tests/e2e/login.spec.ts +++ b/app/apps/shell-super-app/tests/e2e/login.spec.ts @@ -1,5 +1,4 @@ -import { Exit, Predicate, Scope } from 'effect'; -import { runEffectTestPromise, runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; +import { Effect, Exit, Predicate, Scope } from 'effect'; import { expect, test } from '@playwright/test'; import type { Page } from '@playwright/test'; import { shellAuthenticationApiContract } from '../../shared/api.ts'; @@ -38,13 +37,14 @@ const gotoHydratedLogin = async (page: Page, language: 'cs' | 'en') => { }); }; -const fixtureScope = runEffectTestSync(Scope.make()); +// Playwright hooks are the Promise boundary for this scoped Effect fixture. +const fixtureScope = Effect.runSync(Scope.make()); test.beforeAll( async () => - await runEffectTestPromise(createAuthenticationFixture().pipe(Scope.provide(fixtureScope))), + await Effect.runPromise(createAuthenticationFixture().pipe(Scope.provide(fixtureScope))), ); -test.afterAll(async () => await runEffectTestPromise(Scope.close(fixtureScope, Exit.void))); +test.afterAll(async () => await Effect.runPromise(Scope.close(fixtureScope, Exit.void))); test('renders the exact anonymous English and Czech home states', async ({ page }) => await page 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 a20a5375f..f0fe51059 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,24 +1,16 @@ +import { expect, it } from '@app/effect-rstest'; import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.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 { 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, @@ -42,7 +34,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'; @@ -54,9 +46,6 @@ import { renderActionPrincipalServer } from '../../../../scripts/scaffolding/mic type AuthenticationRuntimeHandler = ReturnType< ReturnType['createHandler'] >; - -const nativeDatabaseScope = runNativeSync(NativeScope.make()); - const email = 'better-auth-runtime@example.test'; const password = 'correct-horse-battery-staple'; const tenantId = '30000000-0000-4000-8000-000000000001'; @@ -66,7 +55,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 }), }); @@ -77,7 +65,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[] }) => @@ -96,17 +83,14 @@ 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 installedCatalog = (moduleIds: readonly string[]): InstalledModuleCatalog => Object.freeze({ contracts: Object.freeze([]), @@ -117,7 +101,6 @@ const installedCatalog = (moduleIds: readonly string[]): InstalledModuleCatalog moduleIds: Object.freeze([...moduleIds]), outboxSubscriptions: Object.freeze([]), }); - const installedPageCatalog = (): InstalledModuleCatalog => buildInstalledModuleCatalog([ { @@ -216,166 +199,142 @@ const installedPageCatalog = (): InstalledModuleCatalog => expectedAppId: 'inventory-stock', }, ]); - -void 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: AuthenticationRuntimeHandler[] = []; - const generatedFixtureRoot = await mkdtemp(path.join(tmpdir(), 'ontos-auth-runtime-')); - - const cleanup = async () => { - await runEffectTestPromise( - coreDatabase.delete(dataAccessEvents).where(eq(dataAccessEvents.tenantId, tenantId)), - ); - await runEffectTestPromise( - coreDatabase.delete(auditEvents).where(eq(auditEvents.tenantId, tenantId)), - ); - await runEffectTestPromise( - 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( - coreDatabase - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.providerSubjectId, existingUser.id)), - ); - await runEffectTestPromise( - authDatabase.delete(session).where(eq(session.userId, existingUser.id)), - ); - await runEffectTestPromise( - authDatabase.delete(account).where(eq(account.userId, existingUser.id)), - ); - await runEffectTestPromise(authDatabase.delete(user).where(eq(user.id, existingUser.id))); - }), - ); - - await runEffectTestPromise( - coreDatabase + }).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* coreDatabase.delete(dataAccessEvents).where(eq(dataAccessEvents.tenantId, tenantId)); + yield* coreDatabase.delete(auditEvents).where(eq(auditEvents.tenantId, tenantId)); + yield* coreDatabase.delete(actionInvocations).where(eq(actionInvocations.tenantId, tenantId)); + 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* coreDatabase + .delete(principalAuthBindings) + .where(eq(principalAuthBindings.providerSubjectId, existingUser.id)); + 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)); + }), + ), + ); + yield* coreDatabase .delete(principalAuthBindings) - .where(eq(principalAuthBindings.principalId, principalId)), - ); - await runEffectTestPromise( - coreDatabase.delete(tenantModuleStates).where(eq(tenantModuleStates.tenantId, tenantId)), - ); - await runEffectTestPromise( - coreDatabase + .where(eq(principalAuthBindings.principalId, principalId)); + yield* coreDatabase .delete(tenantModuleStates) - .where(eq(tenantModuleStates.tenantId, foreignTenantId)), - ); - await runEffectTestPromise( - coreDatabase.delete(principals).where(eq(principals.principalId, principalId)), - ); - await runEffectTestPromise( - coreDatabase + .where(eq(tenantModuleStates.tenantId, tenantId)); + yield* coreDatabase + .delete(tenantModuleStates) + .where(eq(tenantModuleStates.tenantId, foreignTenantId)); + yield* coreDatabase.delete(principals).where(eq(principals.principalId, principalId)); + yield* coreDatabase .delete(legalEntities) - .where(eq(legalEntities.legalEntityId, fixtureLegalEntityId)), - ); - await runEffectTestPromise(coreDatabase.delete(tenants).where(eq(tenants.tenantId, tenantId))); - await runEffectTestPromise( - 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 }, - ]), - ); - + .where(eq(legalEntities.legalEntityId, fixtureLegalEntityId)); + yield* coreDatabase.delete(tenants).where(eq(tenants.tenantId, tenantId)); + yield* coreDatabase.delete(tenants).where(eq(tenants.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(Predicate.isTagged(invalid, 'InvalidCredentialsError')); - + expect(Predicate.isTagged(invalid, 'InvalidCredentialsError')).toBe(true); const anonymousRuntime = makeShellAuthenticationApiRuntime( authenticationLayer, makeGatewayIssuerLayer({ @@ -391,50 +350,57 @@ void test('creates, resolves, persists, revokes, and signs out a Better Auth ses ); 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(anonymousGatewayResponse.headers.get('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(anonymousGatewayResponse.headers.get('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(anonymousModulesResponse.headers.get('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(anonymousModulesResponse.headers.get('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(anonymousPageResponse.headers.get('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(anonymousPageResponse.headers.get('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, @@ -449,14 +415,11 @@ void test('creates, resolves, persists, revokes, and signs out a Better Auth ses }), method: 'POST', }); - const current = await runEffectTestPromise( - authentication - .currentSession(authenticatedHeaders) - .pipe(Effect.provide(authenticationContextLayer)), - ); - assert.equal(current.identity?.tenantId, tenantId); - assert.notEqual(current.identity, undefined); - + const current = yield* authentication + .currentSession(authenticatedHeaders) + .pipe(Effect.provide(authenticationContextLayer)); + expect(current.identity?.tenantId).toBe(tenantId); + expect(current.identity).not.toBe(undefined); const pageRuntime = makeShellAuthenticationApiRuntime( authenticationLayer, makeGatewayIssuerLayer({ @@ -471,26 +434,25 @@ void test('creates, resolves, persists, revokes, and signs out a Better Auth ses 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'); } @@ -523,15 +485,15 @@ void test('creates, resolves, persists, revokes, and signs out a Better Auth ses 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({ @@ -550,38 +512,42 @@ void test('creates, resolves, persists, revokes, and signs out a Better Auth ses }), ).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: authenticatedHeaders.get('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: authenticatedHeaders.get('cookie') ?? '', + origin: configuration.baseUrl, + }, + method: 'POST', + }), + ), ); - assert.equal(missingIdempotencyResponse.status, 428); - + expect(missingIdempotencyResponse.status).toBe(428); const deniedIdentityAdministrationRuntime = makeShellAuthenticationApiRuntime( authenticationLayer, makeGatewayIssuerLayer({ @@ -620,78 +586,75 @@ void test('creates, resolves, persists, revokes, and signs out a Better Auth ses }), ).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: authenticatedHeaders.get('cookie') ?? '', - 'idempotency-key': 'denied-managed-identity', - origin: configuration.baseUrl, - }, - method: 'POST', - }), + 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: authenticatedHeaders.get('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), - ); - assert.equal(deniedIdentityInvocation?.status, 'rejected'); + 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); + expect(deniedIdentityInvocation?.status).toBe('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', @@ -699,7 +662,6 @@ void test('creates, resolves, persists, revokes, and signs out a Better Auth ses queryHash: null, resultCount: 0, }); - const refreshingRuntime = makeShellAuthenticationApiRuntime( Layer.succeed(AuthenticationService, { ...authentication, @@ -750,16 +712,15 @@ void test('creates, resolves, persists, revokes, and signs out a Better Auth ses 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( @@ -799,46 +760,54 @@ void test('creates, resolves, persists, revokes, and signs out a Better Auth ses () => 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( - unavailableGatewayResponse.headers.get('content-type') ?? '', + expect(unavailableGatewayResponse.status).toBe(503); + expect(unavailableGatewayResponse.headers.get('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'), @@ -865,111 +834,147 @@ void test('creates, resolves, persists, revokes, and signs out a Better Auth ses 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', + ), + ); + expect( + assertionResponse.status, + yield* Effect.tryPromise(() => assertionResponse.clone().text()), + ).toBe(200); + const assertion = Schema.decodeUnknownSync(TokenResponseSchema)( + yield* Effect.tryPromise(() => assertionResponse.json()), + ); + const verifiedAssertion = yield* Effect.tryPromise(() => + jwtVerify(assertion.token, pair.publicKey, { + algorithms: ['EdDSA'], + audience: 'inventory-stock', + currentDate: new Date(1_700_000_001_000), + issuer: 'https://shell.example.test', }), ); - assert.equal(assertionResponse.status, 200, await assertionResponse.clone().text()); - const assertion = Schema.decodeUnknownSync(TokenResponseSchema)(await assertionResponse.json()); - const verifiedAssertion = await jwtVerify(assertion.token, pair.publicKey, { - algorithms: ['EdDSA'], - audience: 'inventory-stock', - currentDate: new Date(1_700_000_001_000), - issuer: 'https://shell.example.test', - }); const verifiedPrincipal = Schema.decodeUnknownSync(TrustedPrincipalContextSchema)( verifiedAssertion.payload['principal'], ); - assert.equal(verifiedPrincipal.authBindingId, fixtureAuthBindingId); - assert.match(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(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 ${assertion.token}`, { - 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(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 ${assertion.token}`, { + 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(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({ @@ -982,338 +987,309 @@ void test('creates, resolves, persists, revokes, and signs out a Better Auth ses 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(defectResponse.headers.get('content-type') ?? '', /application\/problem\+json/u); + expect(defectResponse.status).toBe(500); + expect(defectResponse.headers.get('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( + 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)); + expect(stillAuthenticated.identity?.principalId).toBe(principalId); + yield* coreDatabase + .update(principalAuthBindings) + .set({ revokedAt: new Date('2026-09-01T00:00:00.000Z'), status: 'revoked' }) + .where(eq(principalAuthBindings.providerSubjectId, betterAuthUserId)); + const revoked = yield* Effect.flip( authentication .currentSession(authenticatedHeaders) .pipe(Effect.provide(authenticationContextLayer)), ); - assert.equal(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)), - ); - const revoked = await runEffectTestPromise( - Effect.flip( - authentication - .currentSession(authenticatedHeaders) - .pipe(Effect.provide(authenticationContextLayer)), + expect(Predicate.isTagged(revoked, 'OntosIdentityForbiddenError')).toBe(true); + const forbiddenModulesResponse = yield* Effect.tryPromise(() => + unavailableHandler.handler( + new Request(`${configuration.baseUrl}/shell/composition`, { + headers: authenticatedHeaders, + }), ), ); - assert.ok(Predicate.isTagged(revoked, 'OntosIdentityForbiddenError')); - const forbiddenModulesResponse = await unavailableHandler.handler( - new Request(`${configuration.baseUrl}/shell/composition`, { - headers: authenticatedHeaders, - }), - ); - assert.equal(forbiddenModulesResponse.status, 401); - assert.match(forbiddenModulesResponse.headers.get('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 signOutResponse = await unavailableHandler.handler( - new Request(`${configuration.baseUrl}/auth/sign-out`, { - headers: authenticatedHeaders, - method: 'POST', - }), + expect(forbiddenModulesResponse.status).toBe(401); + expect(forbiddenModulesResponse.headers.get('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(); - } -}); - -void 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, - { + } as const; + const multiContextAccessLayer = Layer.succeed( + ContextAccess, + multiLegalEntitySelectionOptions.contextAccess, + ); + 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, - runResolverEffect: multiLegalEntitySelectionOptions.runResolverEffect, - }, - ); - const moduleStateLayer = Layer.succeed( - TenantModuleStateService, - makeTenantModuleStateService({ executor: coreDatabase }), - ); - const handlers: AuthenticationRuntimeHandler[] = []; - - const cleanup = async () => { - await runEffectTestPromise( - coreDatabase + }).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 cleanup = Effect.fnUntraced(function* runIntegration7() { + yield* coreDatabase .delete(dataAccessEvents) - .where(inArray(dataAccessEvents.tenantId, [firstTenantId, secondTenantId])), - ); - 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( - coreDatabase + .where(inArray(dataAccessEvents.tenantId, [firstTenantId, secondTenantId])); + 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* coreDatabase .delete(principalAuthBindings) - .where(inArray(principalAuthBindings.providerSubjectId, existingUserIds)), - ); - await runEffectTestPromise( - authDatabase.delete(session).where(inArray(session.userId, existingUserIds)), - ); - await runEffectTestPromise( - authDatabase.delete(account).where(inArray(account.userId, existingUserIds)), - ); - await runEffectTestPromise( - authDatabase.delete(user).where(inArray(user.id, existingUserIds)), - ); - } - await runEffectTestPromise( - coreDatabase.delete(tenantModuleStates).where(eq(tenantModuleStates.tenantId, firstTenantId)), - ); - await runEffectTestPromise( - coreDatabase + .where(inArray(principalAuthBindings.providerSubjectId, existingUserIds)); + yield* authDatabase.delete(session).where(inArray(session.userId, existingUserIds)); + yield* authDatabase.delete(account).where(inArray(account.userId, existingUserIds)); + yield* authDatabase.delete(user).where(inArray(user.id, existingUserIds)); + } + yield* coreDatabase .delete(tenantModuleStates) - .where(eq(tenantModuleStates.tenantId, secondTenantId)), - ); - await runEffectTestPromise( - coreDatabase.delete(principals).where(eq(principals.principalId, firstPrincipalId)), - ); - await runEffectTestPromise( - coreDatabase.delete(principals).where(eq(principals.principalId, secondPrincipalId)), - ); - await runEffectTestPromise( - coreDatabase + .where(eq(tenantModuleStates.tenantId, firstTenantId)); + yield* coreDatabase + .delete(tenantModuleStates) + .where(eq(tenantModuleStates.tenantId, secondTenantId)); + yield* coreDatabase.delete(principals).where(eq(principals.principalId, firstPrincipalId)); + yield* coreDatabase.delete(principals).where(eq(principals.principalId, secondPrincipalId)); + yield* coreDatabase .delete(legalEntities) - .where(inArray(legalEntities.legalEntityId, [firstLegalEntityId, secondLegalEntityId])), - ); - await runEffectTestPromise( - coreDatabase.delete(tenants).where(eq(tenants.tenantId, firstTenantId)), - ); - await runEffectTestPromise( - coreDatabase.delete(tenants).where(eq(tenants.tenantId, secondTenantId)), - ); - }; - - 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 }, - ]), + .where(inArray(legalEntities.legalEntityId, [firstLegalEntityId, secondLegalEntityId])); + yield* coreDatabase.delete(tenants).where(eq(tenants.tenantId, firstTenantId)); + yield* coreDatabase.delete(tenants).where(eq(tenants.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, }); - const initialSessions = await runEffectTestPromise( - authDatabase - .select({ activeTenantId: session.activeTenantId }) - .from(session) - .where(eq(session.userId, betterAuthUserId)), - ); - assert.equal(initialSessions[0]?.activeTenantId, firstTenantId); - - const pair = await generateKeyPair('EdDSA', { crv: 'Ed25519', extractable: true }); - const privateJwk = await exportJWK(pair.privateKey); + const initialSessions = yield* authDatabase + .select({ activeTenantId: session.activeTenantId }) + .from(session) + .where(eq(session.userId, betterAuthUserId)); + expect(initialSessions[0]?.activeTenantId).toBe(firstTenantId); + 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({ @@ -1339,95 +1315,95 @@ void test('selects, lists, switches, revalidates, and upgrades a multi-tenant se 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(anonymousAvailableResponse.headers.get('www-authenticate') ?? '', /^Bearer /u); - - const availableResponse = await runtime.handler( - new Request(`${configuration.baseUrl}/auth/tenants`, { headers: authenticatedHeaders }), + expect(anonymousAvailableResponse.status).toBe(401); + expect(anonymousAvailableResponse.headers.get('www-authenticate') ?? '').toMatch(/^Bearer /u); + const availableResponse = yield* Effect.tryPromise(() => + runtime.handler( + new Request(`${configuration.baseUrl}/auth/tenants`, { headers: authenticatedHeaders }), + ), ); - assert.equal(availableResponse.status, 200); - assert.deepEqual(await availableResponse.json(), { + 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( - new Request(`${configuration.baseUrl}/auth/tenant/switch`, { - body: JSON.stringify({ tenantId: '31000000-0000-4000-8000-000000000099' }), - headers: new Headers({ - 'content-type': 'application/json', - cookie: authenticatedCookie, - origin: configuration.baseUrl, + const forbiddenResponse = yield* Effect.tryPromise(() => + runtime.handler( + new Request(`${configuration.baseUrl}/auth/tenant/switch`, { + body: JSON.stringify({ tenantId: '31000000-0000-4000-8000-000000000099' }), + headers: new Headers({ + 'content-type': 'application/json', + cookie: authenticatedCookie, + origin: configuration.baseUrl, + }), + method: 'POST', }), - method: 'POST', - }), - ); - assert.equal(forbiddenResponse.status, 403); - const sessionsAfterForbiddenSwitch = await runEffectTestPromise( - authDatabase - .select({ activeTenantId: session.activeTenantId }) - .from(session) - .where(eq(session.userId, betterAuthUserId)), + ), ); - assert.equal(sessionsAfterForbiddenSwitch[0]?.activeTenantId, firstTenantId); - - await runEffectTestPromise( - coreDatabase - .update(principals) - .set({ status: 'disabled' }) - .where(eq(principals.principalId, secondPrincipalId)), - ); - const inactiveTargetResponse = await runtime.handler( - new Request(`${configuration.baseUrl}/auth/tenant/switch`, { - body: JSON.stringify({ tenantId: secondTenantId }), - headers: new Headers({ - 'content-type': 'application/json', - cookie: authenticatedCookie, - origin: configuration.baseUrl, + expect(forbiddenResponse.status).toBe(403); + const sessionsAfterForbiddenSwitch = yield* authDatabase + .select({ activeTenantId: session.activeTenantId }) + .from(session) + .where(eq(session.userId, betterAuthUserId)); + expect(sessionsAfterForbiddenSwitch[0]?.activeTenantId).toBe(firstTenantId); + yield* coreDatabase + .update(principals) + .set({ status: 'disabled' }) + .where(eq(principals.principalId, secondPrincipalId)); + const inactiveTargetResponse = yield* Effect.tryPromise(() => + runtime.handler( + new Request(`${configuration.baseUrl}/auth/tenant/switch`, { + body: JSON.stringify({ tenantId: secondTenantId }), + headers: new Headers({ + 'content-type': 'application/json', + cookie: authenticatedCookie, + origin: configuration.baseUrl, + }), + method: 'POST', }), - method: 'POST', - }), - ); - assert.equal(inactiveTargetResponse.status, 403); - const sessionsAfterInactiveSwitch = await runEffectTestPromise( - authDatabase - .select({ activeTenantId: session.activeTenantId }) - .from(session) - .where(eq(session.userId, betterAuthUserId)), - ); - assert.equal(sessionsAfterInactiveSwitch[0]?.activeTenantId, firstTenantId); - await runEffectTestPromise( - coreDatabase - .update(principals) - .set({ status: 'active' }) - .where(eq(principals.principalId, secondPrincipalId)), + ), ); - - const resolverUnavailableAuthentication = makeAuthenticationService( - configuration, - authPersistence.adapter, - { + expect(inactiveTargetResponse.status).toBe(403); + const sessionsAfterInactiveSwitch = yield* authDatabase + .select({ activeTenantId: session.activeTenantId }) + .from(session) + .where(eq(session.userId, betterAuthUserId)); + expect(sessionsAfterInactiveSwitch[0]?.activeTenantId).toBe(firstTenantId); + 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 +1411,7 @@ void test('selects, lists, switches, revalidates, and upgrades a multi-tenant se new PrincipalResolverUnavailableError({ reason: 'Injected resolver outage' }), ) : resolver.resolveBetterAuthUserForTenant(userId, selectedTenantId), - }, - { runResolverEffect: multiLegalEntitySelectionOptions.runResolverEffect }, + }), ); const resolverUnavailableRuntime = makeShellAuthenticationApiRuntime( Layer.succeed(AuthenticationService, resolverUnavailableAuthentication), @@ -1449,31 +1424,29 @@ void test('selects, lists, switches, revalidates, and upgrades a multi-tenant se moduleStateLayer, ).createHandler(); handlers.push(resolverUnavailableRuntime); - const resolverUnavailableResponse = await resolverUnavailableRuntime.handler( - new Request(`${configuration.baseUrl}/auth/tenant/switch`, { - body: JSON.stringify({ tenantId: secondTenantId }), - headers: new Headers({ - 'content-type': 'application/json', - cookie: authenticatedCookie, - origin: configuration.baseUrl, + const resolverUnavailableResponse = yield* Effect.tryPromise(() => + resolverUnavailableRuntime.handler( + new Request(`${configuration.baseUrl}/auth/tenant/switch`, { + body: JSON.stringify({ tenantId: secondTenantId }), + headers: new Headers({ + 'content-type': 'application/json', + cookie: authenticatedCookie, + origin: configuration.baseUrl, + }), + method: 'POST', }), - method: 'POST', - }), - ); - assert.equal(resolverUnavailableResponse.status, 503); - const sessionsAfterResolverFailure = await runEffectTestPromise( - authDatabase - .select({ activeTenantId: session.activeTenantId }) - .from(session) - .where(eq(session.userId, betterAuthUserId)), + ), ); - assert.equal(sessionsAfterResolverFailure[0]?.activeTenantId, firstTenantId); - + expect(resolverUnavailableResponse.status).toBe(503); + const sessionsAfterResolverFailure = yield* authDatabase + .select({ activeTenantId: session.activeTenantId }) + .from(session) + .where(eq(session.userId, betterAuthUserId)); + expect(sessionsAfterResolverFailure[0]?.activeTenantId).toBe(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 @@ -1486,20 +1459,60 @@ void test('selects, lists, switches, revalidates, and upgrades a multi-tenant se 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( + 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 + `), + ); + yield* adminAuthDatabase.execute( + sql.raw(` + DROP FUNCTION IF EXISTS auth.tenant_switch_test_fail_persistence() + `), + ); + }, Effect.orDie), + ); + const persistenceUnavailableResponse = yield* Effect.tryPromise(() => + runtime.handler( + new Request(`${configuration.baseUrl}/auth/tenant/switch`, { + body: JSON.stringify({ tenantId: secondTenantId }), + headers: new Headers({ + 'content-type': 'application/json', + cookie: authenticatedCookie, + origin: configuration.baseUrl, + }), + method: 'POST', + }), + ), + ); + expect(persistenceUnavailableResponse.status).toBe(503); + const sessionsAfterPersistenceFailure = yield* authDatabase + .select({ activeTenantId: session.activeTenantId }) + .from(session) + .where(eq(session.userId, betterAuthUserId)); + expect(sessionsAfterPersistenceFailure[0]?.activeTenantId).toBe(firstTenantId); + }), + ); + const sessionsBeforeSwitch = yield* authDatabase + .select({ activeLegalEntityId: session.activeLegalEntityId }) + .from(session) + .where(eq(session.userId, betterAuthUserId)); + expect(sessionsBeforeSwitch[0]?.activeLegalEntityId).toBe(firstLegalEntityId); + const switchResponse = yield* Effect.tryPromise(() => + runtime.handler( new Request(`${configuration.baseUrl}/auth/tenant/switch`, { body: JSON.stringify({ tenantId: secondTenantId }), headers: new Headers({ @@ -1509,118 +1522,78 @@ void test('selects, lists, switches, revalidates, and upgrades a multi-tenant se }), method: 'POST', }), - ); - assert.equal(persistenceUnavailableResponse.status, 503); - const sessionsAfterPersistenceFailure = await runEffectTestPromise( - authDatabase - .select({ activeTenantId: session.activeTenantId }) - .from(session) - .where(eq(session.userId, betterAuthUserId)), - ); - assert.equal(sessionsAfterPersistenceFailure[0]?.activeTenantId, firstTenantId); - } finally { - await runEffectTestPromise( - adminAuthDatabase.execute( - sql.raw(` - DROP TRIGGER IF EXISTS tenant_switch_test_persistence_failure ON auth.session - `), - ), - ); - await runEffectTestPromise( - 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)), + ), ); - assert.equal(sessionsBeforeSwitch[0]?.activeLegalEntityId, firstLegalEntityId); - - const switchResponse = await runtime.handler( - new Request(`${configuration.baseUrl}/auth/tenant/switch`, { - body: JSON.stringify({ tenantId: secondTenantId }), - headers: new Headers({ - 'content-type': 'application/json', - cookie: authenticatedCookie, - origin: configuration.baseUrl, + 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)); + expect(sessionsAfterSwitch[0]?.activeTenantId).toBe(secondTenantId); + expect(sessionsAfterSwitch[0]?.activeLegalEntityId).toBe(null); + const currentSessionAfterSwitch = yield* authentication + .currentSession(authenticatedHeaders) + .pipe(Effect.provide(multiAuthenticationContextLayer)); + expect(currentSessionAfterSwitch.identity?.principalId).toBe(secondPrincipalId); + 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, }), - method: 'POST', - }), - ); - 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)), - ); - assert.equal(sessionsAfterSwitch[0]?.activeTenantId, secondTenantId); - assert.equal(sessionsAfterSwitch[0]?.activeLegalEntityId, null); - const currentSessionAfterSwitch = await runEffectTestPromise( - authentication - .currentSession(authenticatedHeaders) - .pipe(Effect.provide(multiAuthenticationContextLayer)), - ); - assert.equal(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 }), + ), ); - 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 assertion = Schema.decodeUnknownSync(TokenResponseSchema)( + yield* Effect.tryPromise(() => assertionResponse.json()), + ); + const verified = yield* Effect.tryPromise(() => + jwtVerify(assertion.token, pair.publicKey, { + algorithms: ['EdDSA'], + audience: 'inventory-stock', + currentDate: new Date(1_700_000_001_000), + issuer: 'https://shell.example.test', }), ); - const assertion = Schema.decodeUnknownSync(TokenResponseSchema)(await assertionResponse.json()); - const verified = await jwtVerify(assertion.token, pair.publicKey, { - algorithms: ['EdDSA'], - audience: 'inventory-stock', - currentDate: new Date(1_700_000_001_000), - issuer: 'https://shell.example.test', - }); const verifiedPrincipal = Schema.decodeUnknownSync(TrustedPrincipalContextSchema)( verified.payload['principal'], ); - assert.equal(verifiedPrincipal.authBindingId, secondAuthBindingId); - assert.match(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(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 @@ -1633,158 +1606,124 @@ void test('selects, lists, switches, revalidates, and upgrades a multi-tenant se 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( - new Request(`${configuration.baseUrl}/auth/tenant/switch`, { - body: JSON.stringify({ tenantId: firstTenantId }), - headers: new Headers({ - 'content-type': 'application/json', - cookie: authenticatedCookie, - origin: configuration.baseUrl, - 'x-correlation-id': 'unexpected-switch-persistence-test', - }), - method: 'POST', - }), - ); - assert.equal(unexpectedSwitchResponse.status, 500); - assert.doesNotMatch( - await unexpectedSwitchResponse.text(), - /secret auth persistence defect|P0001/u, - ); - const sessionsAfterUnexpectedSwitchFailure = await runEffectTestPromise( - authDatabase + 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 + `), + ); + yield* adminAuthDatabase.execute( + sql.raw(` + DROP FUNCTION IF EXISTS auth.tenant_switch_test_fail_internal_persistence() + `), + ); + }, Effect.orDie), + ); + const unexpectedSwitchResponse = yield* Effect.tryPromise(() => + runtime.handler( + new Request(`${configuration.baseUrl}/auth/tenant/switch`, { + body: JSON.stringify({ tenantId: firstTenantId }), + headers: new Headers({ + 'content-type': 'application/json', + cookie: authenticatedCookie, + origin: configuration.baseUrl, + 'x-correlation-id': 'unexpected-switch-persistence-test', + }), + method: 'POST', + }), + ), + ); + expect(unexpectedSwitchResponse.status).toBe(500); + expect(yield* Effect.tryPromise(() => unexpectedSwitchResponse.text())).not.toMatch( + /secret auth persistence defect|P0001/u, + ); + const sessionsAfterUnexpectedSwitchFailure = yield* authDatabase .select({ activeTenantId: session.activeTenantId }) .from(session) - .where(eq(session.userId, betterAuthUserId)), - ); - assert.equal(sessionsAfterUnexpectedSwitchFailure[0]?.activeTenantId, secondTenantId); - - await runEffectTestPromise( - authDatabase + .where(eq(session.userId, betterAuthUserId)); + expect(sessionsAfterUnexpectedSwitchFailure[0]?.activeTenantId).toBe(secondTenantId); + yield* 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 runEffectTestPromise( - authDatabase + .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* authDatabase .select({ activeTenantId: session.activeTenantId }) .from(session) - .where(eq(session.userId, betterAuthUserId)), - ); - assert.equal(sessionsAfterUnexpectedLegacyUpgrade[0]?.activeTenantId, null); - } finally { - await runEffectTestPromise( - adminAuthDatabase.execute( - sql.raw(` - DROP TRIGGER IF EXISTS tenant_switch_test_internal_persistence_failure ON auth.session - `), - ), - ); - await runEffectTestPromise( - 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)), - ); - assert.equal(upgradedSession.identity?.tenantId, firstTenantId); - const upgradedSessionRows = await runEffectTestPromise( - authDatabase - .select({ activeTenantId: session.activeTenantId }) - .from(session) - .where(eq(session.userId, betterAuthUserId)), - ); - assert.equal(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)), - ); - const revokedSession = await runEffectTestPromise( - Effect.flip( - authentication - .currentSession(authenticatedHeaders) - .pipe(Effect.provide(multiAuthenticationContextLayer)), - ), - ); - assert.ok(Predicate.isTagged(revokedSession, 'OntosIdentityForbiddenError')); - await runEffectTestPromise( - coreDatabase - .update(principalAuthBindings) - .set({ revokedAt: null, status: 'active' }) - .where(eq(principalAuthBindings.tenantId, secondTenantId)), + .where(eq(session.userId, betterAuthUserId)); + expect(sessionsAfterUnexpectedLegacyUpgrade[0]?.activeTenantId).toBe(null); + }), ); - const restoredSession = await runEffectTestPromise( + const upgradedSession = yield* authentication + .currentSession(authenticatedHeaders) + .pipe(Effect.provide(multiAuthenticationContextLayer)); + expect(upgradedSession.identity?.tenantId).toBe(firstTenantId); + const upgradedSessionRows = yield* authDatabase + .select({ activeTenantId: session.activeTenantId }) + .from(session) + .where(eq(session.userId, betterAuthUserId)); + expect(upgradedSessionRows[0]?.activeTenantId).toBe(firstTenantId); + 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)); + const revokedSession = yield* Effect.flip( authentication .currentSession(authenticatedHeaders) .pipe(Effect.provide(multiAuthenticationContextLayer)), ); - assert.equal(restoredSession.identity?.tenantId, secondTenantId); - + expect(Predicate.isTagged(revokedSession, 'OntosIdentityForbiddenError')).toBe(true); + yield* coreDatabase + .update(principalAuthBindings) + .set({ revokedAt: null, status: 'active' }) + .where(eq(principalAuthBindings.tenantId, secondTenantId)); + const restoredSession = yield* authentication + .currentSession(authenticatedHeaders) + .pipe(Effect.provide(multiAuthenticationContextLayer)); + expect(restoredSession.identity?.tenantId).toBe(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( - coreDatabase.delete(dataAccessEvents).where(eq(dataAccessEvents.tenantId, secondTenantId)), - ); - await runEffectTestPromise( - coreDatabase - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.tenantId, secondTenantId)), - ); - const sessionWithRemovedBinding = await runEffectTestPromise( - Effect.flip( - authentication - .currentSession(authenticatedHeaders) - .pipe(Effect.provide(multiAuthenticationContextLayer)), - ), + yield* coreDatabase + .delete(dataAccessEvents) + .where(eq(dataAccessEvents.tenantId, secondTenantId)); + yield* coreDatabase + .delete(principalAuthBindings) + .where(eq(principalAuthBindings.tenantId, secondTenantId)); + const sessionWithRemovedBinding = yield* Effect.flip( + authentication + .currentSession(authenticatedHeaders) + .pipe(Effect.provide(multiAuthenticationContextLayer)), ); - assert.ok(Predicate.isTagged(sessionWithRemovedBinding, 'OntosIdentityForbiddenError')); - } 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), + expect(Predicate.isTagged(sessionWithRemovedBinding, 'OntosIdentityForbiddenError')).toBe(true); + }), ); 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 995d827f0..c26cc18ac 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,25 @@ +import { TestClock } from 'effect/testing'; +import { expect, it } from '@app/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, @@ -54,15 +44,12 @@ 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 { 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 +93,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 +105,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,13 +118,17 @@ const TestSpiceDbConfig = Config.all({ preSharedKey: Redacted.value(preSharedKey), })), ); - const testGatewayAssertionRedemption: GatewayAssertionRedemption = { consume: () => Effect.void, }; - 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, @@ -158,12 +146,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; @@ -181,11 +173,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( @@ -205,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') }), ); @@ -222,7 +210,6 @@ const isCreateRecordRejected = Schema.is( const isActionHandlerExecutionError = Schema.is( Schema.Struct({ _tag: Schema.Literal('ActionHandlerExecutionError') }), ); - const relationship = ( resourceType: string, resourceId: string, @@ -237,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, @@ -266,19 +251,18 @@ const makeOwnerHandler = ( const handler: OwnerHttpHandler = bff.createHandler(); return handler; }; - -const loadGeneratedOwner = async ( +const loadGeneratedOwner = Effect.fnUntraced(function* runIntegration1( verticalRoot: string, runtime: ReadRuntimeService, loggerLayer: Layer.Layer, 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); - }; + }); const [ detailApi, detailServer, @@ -289,27 +273,33 @@ 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'), - ]); + ] = 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 = Schema.decodeUnknownSync(VerticalRuntimeRegistrationSchema)( registrationOwner['isolationOwnerRegistration'], ); const actions = getVerticalRuntimeActions(registration); const entrypoints = getVerticalRuntimeEntrypoints(registration); - const [detailClient, listClient, searchClient] = await Promise.all([ - entrypoints.api['resource-detail']?.(), - entrypoints.api['resource-list']?.(), - entrypoints.search['records']?.(), - ]); + 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' }, + ); const generatedAction = actions.find( ({ descriptor }) => descriptor.actionKey === GENERATED_OWNER.actionKey, ); @@ -365,46 +355,53 @@ const loadGeneratedOwner = async ( ), }, }; -}; - -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 Schema.decodeUnknownSync(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, @@ -412,48 +409,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) => @@ -482,17 +501,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, @@ -506,151 +523,157 @@ const principal = ( principalId, tenantId, }); - -void test('Codesmith composes the disposable owner Action and receiving read BFFs', async () => { - const fixture = await runEffectTestPromise( - createGeneratedOwnerFixture(`generated_owner_${randomUUID().replaceAll('-', '')}`).pipe( - Effect.provide(NodeServices.layer), - NativeScope.provide(nativeDatabaseScope), - ), - ); - const contract = await runEffectTestPromise( - deriveOntosModuleDeploymentContract({ +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 = 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, { + }).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(), - ]); - } -}); - -void 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 runEffectTestPromise( - createGeneratedOwnerFixture(schemaName).pipe( + }), +); +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), - NativeScope.provide(nativeDatabaseScope), - ), - ); - const contract = await runEffectTestPromise( - deriveOntosModuleDeploymentContract({ + ); + 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 = 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({ + }).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( @@ -668,134 +691,269 @@ void test('generated owner enforces tenant and legal-entity isolation through Sh }), ), 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 = 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', + ), + ], + [ + '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', @@ -839,185 +997,174 @@ void test('generated owner enforces tenant and legal-entity isolation through Sh 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( @@ -1032,128 +1179,107 @@ void test('generated owner enforces tenant and legal-entity isolation through Sh ), ), ); - 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', @@ -1161,7 +1287,6 @@ void test('generated owner enforces tenant and legal-entity isolation through Sh result_count: 0, }, ]); - const unavailableContextAccess: ContextAccessService = { legalEntities: ({ legalEntityIds }) => Effect.succeed(legalEntityIds.map((key) => ({ decision: 'unavailable' as const, key }))), @@ -1187,7 +1312,7 @@ void test('generated owner enforces tenant and legal-entity isolation through Sh unavailableResolver, unavailableContextAccess, ); - const unavailableOwner = await loadGeneratedOwner( + const unavailableOwner = yield* loadGeneratedOwner( fixture.verticalRoot, unavailableRuntime, loggerLayer, @@ -1195,27 +1320,25 @@ void test('generated owner enforces tenant and legal-entity isolation through Sh ); 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, @@ -1226,16 +1349,17 @@ void test('generated owner enforces tenant and legal-entity isolation through Sh 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(), @@ -1247,7 +1371,7 @@ void test('generated owner enforces tenant and legal-entity isolation through Sh 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; @@ -1256,34 +1380,32 @@ void test('generated owner enforces tenant and legal-entity isolation through Sh readonly title: string; }, idempotencyKey: string, - ) => { - const authorization = await issueAuthorization(trustedPrincipal); - const verified = await effectRuntime.runPromise( - generated.verifyOperationPrincipal(authorization, { + ) { + const authorization = yield* issueAuthorization(trustedPrincipal); + const verified = yield* generated + .verifyOperationPrincipal(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, @@ -1293,9 +1415,8 @@ void test('generated owner enforces tenant and legal-entity isolation through Sh }, randomUUID(), ), - { created: true }, - ); - await Promise.all( + ).toEqual({ created: true }); + yield* Effect.all( [ { legalEntityId: entityA2, @@ -1310,198 +1431,107 @@ void test('generated owner enforces tenant and legal-entity isolation through Sh 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, /verifyOperationPrincipal/u); - assert.match(generatedServerSource, /yield\* ReadRuntime/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 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(/verifyOperationPrincipal/u); + expect(generatedServerSource).toMatch(/yield\* ReadRuntime/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 205224005..39835c0f6 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,17 +1,9 @@ +import { expect, it } from '@app/effect-rstest'; import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.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 { 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 { @@ -65,8 +57,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) { @@ -78,449 +68,407 @@ 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( - authDatabase + 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* authDatabase .delete(supportImpersonationRecovery) - .where(eq(supportImpersonationRecovery.tenantId, tenantId)), - ); - await runEffectTestPromise( - authDatabase.delete(apikey).where(inArray(apikey.referenceId, ids)), - ); - await runEffectTestPromise(authDatabase.delete(session).where(inArray(session.userId, ids))); - await runEffectTestPromise(authDatabase.delete(account).where(inArray(account.userId, ids))); - await runEffectTestPromise(authDatabase.delete(user).where(inArray(user.id, ids))); - } - await runEffectTestPromise( - coreDatabase.delete(dataAccessEvents).where(eq(dataAccessEvents.tenantId, tenantId)), - ); - await runEffectTestPromise( - coreDatabase.delete(auditEvents).where(eq(auditEvents.tenantId, tenantId)), - ); - await runEffectTestPromise( - coreDatabase.delete(actionInvocations).where(eq(actionInvocations.tenantId, tenantId)), - ); - await runEffectTestPromise( - coreDatabase + .where(eq(supportImpersonationRecovery.tenantId, tenantId)); + yield* authDatabase.delete(apikey).where(inArray(apikey.referenceId, ids)); + yield* authDatabase.delete(session).where(inArray(session.userId, ids)); + yield* authDatabase.delete(account).where(inArray(account.userId, ids)); + yield* authDatabase.delete(user).where(inArray(user.id, ids)); + } + yield* coreDatabase.delete(dataAccessEvents).where(eq(dataAccessEvents.tenantId, tenantId)); + yield* coreDatabase.delete(auditEvents).where(eq(auditEvents.tenantId, tenantId)); + yield* coreDatabase.delete(actionInvocations).where(eq(actionInvocations.tenantId, tenantId)); + yield* coreDatabase .delete(principalAuthBindings) - .where(eq(principalAuthBindings.tenantId, tenantId)), - ); - await runEffectTestPromise( - coreDatabase.delete(principals).where(eq(principals.tenantId, tenantId)), - ); - await runEffectTestPromise(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}`, + .where(eq(principalAuthBindings.tenantId, tenantId)); + yield* coreDatabase.delete(principals).where(eq(principals.tenantId, tenantId)); + yield* coreDatabase.delete(tenants).where(eq(tenants.tenantId, 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, }); @@ -538,116 +486,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) && @@ -658,20 +590,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' && @@ -679,8 +608,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' && @@ -688,16 +617,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 e0b100180..f6bbf3dc0 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,9 +1,4 @@ -import { - makeEffectTestCallback, - runEffectTestPromise, -} from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/effect-rstest'; import { OntosModuleDeploymentContractSchema, defineAction, @@ -15,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'; @@ -87,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')), ); @@ -95,7 +89,6 @@ const PropertyRegistryUnitIdSchema = Schema.String.pipe(Schema.brand('PropertyRe const DocumentsCenterDocumentIdSchema = Schema.String.pipe( Schema.brand('DocumentsCenterDocumentId'), ); - const PropertyAction = defineAction( { accessEvidencePolicy: { @@ -123,7 +116,6 @@ const PropertyAction = defineAction( }, () => Effect.succeed({ renamed: true }), ); - const PropertyOutboxWorker = defineOutboxWorker( { consumerModuleKey: 'property.registry', @@ -148,9 +140,7 @@ const PropertyOutboxWorker = defineOutboxWorker( }, () => Effect.void, ); - const PropertyDashboard = () => null; - const propertyManifest = defineOntosModuleManifest({ activation: { defaultState: 'inactive', @@ -185,148 +175,119 @@ const propertyManifest = defineOntosModuleManifest({ }, }, }); - const propertyRuntimeRegistration = defineVerticalRuntimeRegistration({ actions: [PropertyAction], manifest: propertyManifest, outboxWorkers: [PropertyOutboxWorker], }); - const propertySafeRuntime = extractVerticalRuntimeSafeDescriptors(propertyRuntimeRegistration); - const ContractDocumentJsonSchema = Schema.fromJsonString(OntosModuleDeploymentContractSchema); - -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', - makeEffectTestCallback( - 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, - }, - topology: { - verticals: [ - { id: 'property-registry', kind: 'vertical' }, - { id: 'documents-center', kind: 'vertical' }, - ], + ontosModuleManifests: { + 'documents-center': documentsUrl, + 'property-registry': propertyUrl, }, - }); - 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..23df4ea02 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 '@app/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..41cc6b700 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 '@app/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/unit/api-index.test.ts b/app/apps/shell-super-app/tests/unit/api-index.test.ts index 65a0f4f84..32e048bb6 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 '@app/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..347321c60 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 '@app/effect-rstest'; import { Schema } from 'effect'; const workspaceRoot = new URL('../../../../', import.meta.url); 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 aaf48fc2e..16e96f116 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,5 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { expect, test } from '@rstest/core'; +import { expect, it } from '@app/effect-rstest'; import { DateTime, Effect, Schema, Predicate } from 'effect'; import { CurrentSessionSchema, @@ -27,7 +26,7 @@ const statuses = (endpoint: TenantEndpoint) => .map((schema) => schema.ast.annotations?.['httpApiStatus']) .toSorted((left, right) => Number(left) - Number(right)); -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(); @@ -97,86 +96,82 @@ test('publishes authentication, identity lifecycle, and gateway operations', () ); }); -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', @@ -190,7 +185,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']) @@ -200,11 +195,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', @@ -216,65 +211,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(Predicate.isTagged(invalidPayload, 'SchemaError')).toBe(true); -}); + ).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(Predicate.isTagged(error, 'SchemaError')).toBe(true); -}); + ); + 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(Predicate.isTagged(missingPrincipalReason, 'SchemaError')).toBe(true); - expect(Predicate.isTagged(missingRevocationReason, 'SchemaError')).toBe(true); + ); + 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, @@ -284,33 +273,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', @@ -322,17 +309,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 aa8a73ffb..01b470509 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,28 +1,27 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { expect, test } from '@rstest/core'; +import { expect, it } from '@app/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 = { - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements pg Pool.end's foreign Promise API. - 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 ac4238b95..bc8c30f57 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 '@app/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..3710d9690 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,4 +1,4 @@ -import { expect, test } from '@rstest/core'; +import { expect, test } from '@app/effect-rstest'; import { Effect } from 'effect'; import { runBrowserEffect } from '../../src/runtime/browser-effect-runtime.ts'; 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..c7d143ff7 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 '@app/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..3f52d1a4d 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 '@app/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 909ec358e..aa0488436 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,6 +1,4 @@ -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 '@app/effect-rstest'; import { ActionTransactionError, IdentityTargetInvalidError, @@ -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(Predicate.isTagged(failure, 'IdentityTargetInvalidError')).toBe(true); - 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(Predicate.isTagged(failure, 'PrincipalBindingMissingError')).toBe(true); -}); - -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(Predicate.isTagged(failure, 'IdentityTargetInvalidError')).toBe(true); - 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(Predicate.isTagged(failure, 'IdentityLifecycleOperationError')).toBe(true); - 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 5cc2aadd2..b6f708572 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,6 +1,5 @@ -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 '@app/effect-rstest'; import { ActionRuntime, ActionAlreadyCommitted, @@ -140,60 +139,56 @@ const supportRecoveryPrincipal: SupportRecoveryPrincipalContextResolverService = const provider = (impersonated: boolean): SupportAuthProvider => ({ api: { - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise 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' }, - }, - }), - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. - impersonateUser: async () => { - throw new Error('not used'); - }, - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. - 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() { @@ -226,10 +221,9 @@ test('preserves definite requested-checkpoint errors for their declared HTTP map }), configuration, provider: makeSupportAuthProviderDouble({ - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. - 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({ @@ -259,70 +253,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({ - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. - 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', @@ -336,515 +330,534 @@ 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, - }); +); + +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 = await runEffectTestPromise( - service + 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({ - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. - getSession: async () => ({ headers: new Headers(), response: null }), - }), - resolver: makePrincipalResolverDouble(), - store: makeSupportImpersonationStoreDouble({ - deleteRecovery: () => - 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 }), }), - deleteSession: () => - Effect.sync(() => { - deleteCalls += 1; - }), - 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); -}); +); -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, - }; - let deleted = false; - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ checkpoint: 'stopped', recorded: true }), - ]); - const service = makeService({ - actionRuntime: actionRuntime.runtime, - authentication: makeAuthenticationServiceDouble(), - configuration, - provider: makeSupportAuthProviderDouble({ - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. - getSession: async () => ({ headers: new Headers(), response: null }), - }), - resolver: makePrincipalResolverDouble(), - store: makeSupportImpersonationStoreDouble({ - deleteRecovery: () => - Effect.sync(() => { - deleted = true; +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, + }; + let deleted = false; + 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 }), }), - deleteSession: () => Effect.void, - loadOriginalSession: () => - Effect.succeed( - Option.some({ - expiresAt: new Date('2099-01-01T00:00:00.000Z'), - id: restoredSessionId, - }), + resolver: makePrincipalResolverDouble(), + store: makeSupportImpersonationStoreDouble({ + deleteRecovery: () => + Effect.sync(() => { + deleted = true; + }), + deleteSession: () => Effect.void, + loadOriginalSession: () => + Effect.succeed( + Option.some({ + expiresAt: new Date('2099-01-01T00:00:00.000Z'), + id: restoredSessionId, + }), + ), + loadRecoveries: () => Effect.succeed([recovery]), + }), + supportRecoveryPrincipal, + }); + + 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'), ), - loadRecoveries: () => Effect.succeed([recovery]), + ).toBe(true); }), - supportRecoveryPrincipal, - }); - - 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, - }; - let deleted = false; - const actionRuntime = makeActionRuntimeDouble([ - actionSuccess({ checkpoint: 'stopped', recorded: true }), - ]); - const service = makeService({ - actionRuntime: actionRuntime.runtime, - authentication: makeAuthenticationServiceDouble(), - configuration, - provider: makeSupportAuthProviderDouble({ - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. - getSession: async () => ({ headers: new Headers(), response: null }), - }), - resolver: makePrincipalResolverDouble(), - store: makeSupportImpersonationStoreDouble({ - deleteRecovery: () => - Effect.sync(() => { - deleted = 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, + }; + let deleted = false; + 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 }), }), - deleteSession: () => Effect.void, - loadOriginalSession: () => - Effect.succeed( - Option.some({ - expiresAt: new Date('2000-01-01T00:00:00.000Z'), - id: restoredSessionId, + resolver: makePrincipalResolverDouble(), + store: makeSupportImpersonationStoreDouble({ + deleteRecovery: () => + Effect.sync(() => { + deleted = true; + }), + deleteSession: () => Effect.void, + loadOriginalSession: () => + Effect.succeed( + Option.some({ + expiresAt: new Date('2000-01-01T00:00:00.000Z'), + id: restoredSessionId, + }), + ), + loadRecoveries: () => Effect.succeed([recovery]), + }), + supportRecoveryPrincipal, + }); + + const result = yield* service + .stop({ + idempotencyKey: 'stop-expired-lost-response', + requestHeaders: new Headers({ + cookie: `better-auth.admin_session=${adminCookie}`, }), - ), - loadRecoveries: () => Effect.succeed([recovery]), + }) + .pipe( + Effect.provideService( + SupportImpersonationCorrelationId, + 'correlation-expired-lost-response', + ), + ); + + 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); }), - supportRecoveryPrincipal, - }); +); - const result = await runEffectTestPromise( - service - .stop({ - idempotencyKey: 'stop-expired-lost-response', - requestHeaders: new Headers({ - cookie: `better-auth.admin_session=${adminCookie}`, +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' } }, + }); + }, }), - }) - .pipe( - Effect.provideService( - SupportImpersonationCorrelationId, - 'correlation-expired-lost-response', - ), - ), - ); - - 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); -}); - -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, - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise 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' } }, - }; - }, - }), - resolver: makePrincipalResolverDouble(), - store: makeSupportImpersonationStoreDouble({ - deleteRecovery: () => - Effect.sync(() => { - deleted = true; + resolver: makePrincipalResolverDouble(), + store: makeSupportImpersonationStoreDouble({ + deleteRecovery: () => + Effect.sync(() => { + deleted = true; + }), + insertRecovery: () => Effect.void, }), - insertRecovery: () => Effect.void, - }), - supportRecoveryPrincipal, - }); + supportRecoveryPrincipal, + }); - const result = await runEffectTestPromise( - service - .stop({ - idempotencyKey: 'stop-mismatched-restore', - requestHeaders: new Headers(), - }) - .pipe( - Effect.provideService(SupportImpersonationCorrelationId, 'correlation-mismatched-restore'), - ), - ); - - 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, - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise 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 9e898a4e9..4ac3dab4e 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,6 +1,4 @@ -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 '@app/effect-rstest'; import { Effect, Predicate } from 'effect'; import type { DeploymentAllowlist } from '../../api/modules/deployment-allowlist.ts'; import { @@ -71,101 +69,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([ { @@ -173,135 +254,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 degraded = yield* loader; + const recovered = yield* loader; + const cached = yield* loader; - 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 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 778f3c44d..ada046976 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,5 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { expect, test } from '@rstest/core'; +import { expect, it } from '@app/effect-rstest'; import { buildInstalledModuleCatalog } from '@app/core-runtime'; import { Effect } from 'effect'; import { matchInstalledOutboxMessagesOnce } from '../../api/modules/installed-outbox-matcher.ts'; @@ -48,35 +47,35 @@ const contract = ( schemaVersion: '2', }); -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..2d0005646 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 '@app/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 114610fa8..cdbd57501 100644 --- a/app/apps/shell-super-app/tests/unit/layout.test.tsx +++ b/app/apps/shell-super-app/tests/unit/layout.test.tsx @@ -1,4 +1,4 @@ -import { afterEach, expect, rstest, test } from '@rstest/core'; +import { afterEach, expect, rstest, test } from '@app/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' }; 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 c630d75e4..6c358773a 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,6 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import { test } from '@rstest/core'; +import { expect, it } from '@app/effect-rstest'; import { ContextAccess, LegalEntityContext } from '@app/core-runtime'; import type { ContextAccessService, LegalEntityContextService } from '@app/core-runtime'; import { Effect, Layer, Predicate } from 'effect'; @@ -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.ok(Predicate.isTagged(unavailable, '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 3bb33605c..8837d611d 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 '@app/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,69 +118,60 @@ const component = defineTenantModuleEntrypoint({ const compatibleRemoteModule = (value: { readonly default: unknown }) => Predicate.isFunction(value.default); -const runEffectTest = (effect: Effect.Effect) => - 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(error).toMatchObject({ _tag: 'ModuleStateDeniedError' }); + expect(authorizations).toBe(0); + expect(loads).toBe(0); + }), ); class RemoteLoadUnavailable extends Schema.TaggedError()( @@ -202,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, ), @@ -260,123 +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, - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements the Module Federation import Promise boundary. - 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', + load: () => Promise.reject(new Error('remote unavailable')), + timeoutMs: 50, + }, + { + identity: 'throwing-validator/page', + isCompatible: () => { + throw new TypeError('malformed runtime value'); }, - { - identity: 'throwing-validator/page', - reason: 'incompatible', - state: 'unavailable', - }, - ]); - }), - ), + 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, - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements the Module Federation import Promise boundary. - 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, - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements the Module Federation import Promise boundary. - load: async () => { + + load: () => { started.push(index); if (index === 0) { const unblockAt = clock.currentTimeMillisUnsafe() + 40; @@ -385,69 +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, - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements the Module Federation import Promise boundary. - 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(), @@ -460,8 +446,8 @@ test( Array.from({ length: MODULE_LOAD_CONCURRENCY + 1 }, (_, index) => ({ identity: `module-${index}/page`, isCompatible: compatibleRemoteModule, - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements the Module Federation import Promise boundary. - load: async () => { + + load: () => { events.push(`started-${index}`); if (index === MODULE_LOAD_CONCURRENCY - 1) { firstWindowStarted.resolve(null); @@ -469,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 }); @@ -500,60 +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', - runEffectTest( - 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, - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements the Module Federation import Promise boundary. - load: async () => { - loadStarted.resolve(null); - try { - return await pending.promise; - } finally { - loadSettled.resolve(null); - } - }, - timeoutMs: 10, - }, - ]), - ); - yield* Effect.promise(async () => await loadStarted.promise); - yield* TestClock.adjust('10 millis'); - const result = yield* Fiber.join(resultFiber); - expect(result).toEqual([ +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', - reason: 'timeout', - state: 'unavailable', - }, - ]); - pending.reject(new Error('remote unavailable')); - yield* Effect.promise(async () => await loadSettled.promise); - expect(result).toEqual([ - { - identity: 'late-rejection/page', - reason: 'timeout', - state: 'unavailable', + isCompatible: compatibleRemoteModule, + + load: () => { + loadStarted.resolve(null); + return Promise.resolve( + pending.promise.finally(() => { + loadSettled.resolve(null); + }), + ); + }, + timeoutMs: 10, }, - ]); - }).pipe(Effect.provide(TestClock.layer())), - ), + ]), + ); + yield* Effect.promise(() => loadStarted.promise); + yield* TestClock.adjust('10 millis'); + const result = yield* Fiber.join(resultFiber); + 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( @@ -567,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/home/loader.test.ts b/app/apps/shell-super-app/tests/unit/routes/home/loader.test.ts index 2515e5a90..85c03717f 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,5 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { beforeEach, expect, rstest, test } from '@rstest/core'; +import { beforeEach, expect, rstest, it } from '@app/effect-rstest'; import { ConfigProvider, Effect } from 'effect'; import * as actualAuthClient from '../../../../src/api/auth-client.ts' with { rstest: 'importActual', @@ -10,12 +9,14 @@ const { availableLegalEntitiesMock, availableTenantsMock, browserConfigValuesMock, + browserEffectMock, currentSessionMock, shellCompositionMock, } = rstest.hoisted(() => ({ availableLegalEntitiesMock: rstest.fn(), availableTenantsMock: rstest.fn(), browserConfigValuesMock: rstest.fn<() => { readonly BETTER_AUTH_URL?: string }>(), + browserEffectMock: rstest.fn(), currentSessionMock: rstest.fn(), shellCompositionMock: rstest.fn(), })); @@ -29,16 +30,7 @@ rstest.mock('../../../../src/api/auth-client.ts', () => ({ })); rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This double implements ManagedRuntime.runPromise at the browser execution boundary. - runBrowserEffect: async (effect: Effect.Effect) => - await runEffectTestPromise( - effect.pipe( - Effect.provideService( - ConfigProvider.ConfigProvider, - ConfigProvider.fromUnknown(browserConfigValuesMock()), - ), - ), - ), + runBrowserEffect: browserEffectMock, })); const identity = { @@ -68,13 +60,34 @@ const request = () => headers: { cookie: 'session=test-session' }, }); -const withBetterAuthUrl = async ( +const loadModel = (input: Parameters[0]) => + Effect.gen(function* loadRouteModel() { + const boundary = Promise.withResolvers>>(); + let captured: Effect.Effect>, unknown> | undefined; + browserEffectMock.mockImplementationOnce( + (effect: Effect.Effect>, unknown>) => { + captured = effect.pipe( + Effect.provideService( + ConfigProvider.ConfigProvider, + ConfigProvider.fromUnknown(browserConfigValuesMock()), + ), + ); + return boundary.promise; + }, + ); + const result = loader(input); + const model = yield* captured ?? Effect.die('Route did not invoke the browser Effect boundary'); + boundary.resolve(model); + return yield* Effect.promise(() => result); + }); +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({}); @@ -99,132 +112,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, - ); -}); +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, + ); + }), +); -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(); -}); +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(); + }), +); -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('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, - }), - ); - 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(); -}); + 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 })); +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 })); - await withBetterAuthUrl( - 'https://shell.stage.example.test', - async () => await loader({ request: new Request('http://shell.stage.example.test/en') }), - ); + 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', - ); -}); + 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 })); +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 })); - await withBetterAuthUrl( - 'http://localhost:3020', - async () => await loader({ request: new Request('http://localhost:3020/en') }), - ); + 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', - ); -}); + 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', - }); -}); +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', + }); + }), +); -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' }, - }); -}); +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' }, + }); + }), +); -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', - }); -}); +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', + }); + }), +); -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' }); -}); +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' }); + }), +); -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('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 1697c3b8e..81331ec93 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 { runBrowserEffect } from '../../../../src/runtime/browser-effect-runtime.ts' with { + rstest: 'importActual', +}; +import { afterEach, beforeEach, expect, rstest, it } from '@app/effect-rstest'; import { cleanup, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Effect, Schema } from 'effect'; @@ -148,9 +150,7 @@ const authenticatedModel = (): HomePageModel => ({ beforeEach(() => { navigateMock.mockResolvedValue(undefined); - runBrowserEffectMock.mockImplementation( - async (effect: Effect.Effect) => await runEffectTestPromise(effect), - ); + runBrowserEffectMock.mockImplementation(runBrowserEffect); signOutMock.mockReturnValue(Effect.succeed({ signedOut: true })); switchTenantMock.mockReturnValue(Effect.succeed({ selectedTenantId: tenantId2 })); switchLegalEntityMock.mockReturnValue(Effect.succeed({ selectedLegalEntityId: legalEntityId2 })); @@ -161,13 +161,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', @@ -177,45 +177,78 @@ 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' }), + ), + ); + }), +); 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 e122b2fd7..816f06c20 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 '@app/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 1c63ded68..d0cb90352 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,5 +1,7 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { afterEach, beforeEach, expect, rstest, test } from '@rstest/core'; +import { runBrowserEffect } from '../../../../src/runtime/browser-effect-runtime.ts' with { + rstest: 'importActual', +}; +import { afterEach, beforeEach, expect, rstest, it } from '@app/effect-rstest'; import { Effect, Redacted } from 'effect'; import { cleanup, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -7,15 +9,13 @@ 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 () => {}), + navigateMock: rstest.fn(), runBrowserEffectMock: rstest.fn(), signInMock: rstest.fn(), })); beforeEach(() => { - runBrowserEffectMock.mockImplementation( - async (effect: Effect.Effect) => await runEffectTestPromise(effect), - ); + runBrowserEffectMock.mockImplementation(runBrowserEffect); signInMock.mockReturnValue( Effect.succeed({ identity: { @@ -81,7 +81,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,11 +101,12 @@ test('shows the required login controls through the UI kit', () => { ); }); -test('shows both field errors and one Toast when both values are missing', async () => { - const user = userEvent.setup(); - renderLogin(); +it.live('shows both field errors and one Toast when both values are missing', () => + Effect.gen(function* showsBothFieldErrorsAndOne() { + const user = userEvent.setup(); + renderLogin(); - await user.click(getSubmit()).then(() => { + yield* Effect.promise(() => user.click(getSubmit())); const login = getLogin(); const password = getPassword(); @@ -116,139 +117,132 @@ test('shows both field errors and one Toast when both values are missing', async expect(screen.getAllByText('Login details are incomplete')).toHaveLength(1); expect(screen.getByText('Fill in both required fields.')).toBeTruthy(); expect(document.activeElement).toBe(login); - }); -}); + }), +); -test('creates one Toast per repeated invalid submission', async () => { - const user = userEvent.setup(); - renderLogin(); - - await user - .click(getSubmit()) - .then(async () => await user.click(getSubmit())) - .then(() => { - expect(screen.getAllByText('Login details are incomplete')).toHaveLength(2); - expect(screen.getAllByText('Fill in both required fields.')).toHaveLength(2); - }); -}); +it.live('creates one Toast per repeated invalid submission', () => + Effect.gen(function* createsOneToastPerRepeatedInvalid() { + const user = userEvent.setup(); + renderLogin(); -test('shows only the Login error when the password is present', async () => { - const user = userEvent.setup(); - renderLogin(); - - await user - .type(getPassword(), 'secret') - .then(async () => await user.click(getSubmit())) - .then(() => { - expect(getLogin().getAttribute('aria-invalid')).toBe('true'); - expect(getPassword().getAttribute('aria-invalid')).toBeNull(); - expect(screen.getByText('Enter your login.')).toBeTruthy(); - expect(screen.queryByText('Enter your password.')).toBeNull(); - expect(screen.getAllByText('Login details are incomplete')).toHaveLength(1); - expect(document.activeElement).toBe(getLogin()); - }); -}); + yield* Effect.promise(() => user.click(getSubmit())); + 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('shows only the Password error when the login is present', async () => { - const user = userEvent.setup(); - renderLogin(); - - await user - .type(getLogin(), 'admin') - .then(async () => await user.click(getSubmit())) - .then(() => { - expect(getLogin().getAttribute('aria-invalid')).toBeNull(); - expect(getPassword().getAttribute('aria-invalid')).toBe('true'); - expect(screen.queryByText('Enter your login.')).toBeNull(); - expect(screen.getByText('Enter your password.')).toBeTruthy(); - expect(screen.getAllByText('Login details are incomplete')).toHaveLength(1); - expect(document.activeElement).toBe(getPassword()); - }); -}); +it.live('shows only the Login error when the password is present', () => + Effect.gen(function* showsOnlyTheLoginErrorWhen() { + const user = userEvent.setup(); + renderLogin(); -test('treats a whitespace-only Login as missing', async () => { - const user = userEvent.setup(); - renderLogin(); - - await user - .type(getLogin(), ' ') - .then(async () => await user.type(getPassword(), 'secret')) - .then(async () => await user.click(getSubmit())) - .then(() => { - expect(getLogin().getAttribute('aria-invalid')).toBe('true'); - expect(screen.getByText('Enter your login.')).toBeTruthy(); - expect(document.activeElement).toBe(getLogin()); - }); -}); + yield* Effect.promise(() => user.type(getPassword(), 'secret')); + yield* Effect.promise(() => user.click(getSubmit())); + expect(getLogin().getAttribute('aria-invalid')).toBe('true'); + expect(getPassword().getAttribute('aria-invalid')).toBeNull(); + expect(screen.getByText('Enter your login.')).toBeTruthy(); + expect(screen.queryByText('Enter your password.')).toBeNull(); + expect(screen.getAllByText('Login details are incomplete')).toHaveLength(1); + expect(document.activeElement).toBe(getLogin()); + }), +); -test('accepts a non-empty whitespace Password', async () => { - const user = userEvent.setup(); - renderLogin(); - - await user - .type(getLogin(), 'admin') - .then(async () => await user.type(getPassword(), ' ')) - .then(async () => await user.click(getSubmit())) - .then(() => { - expect(getLogin().getAttribute('aria-invalid')).toBeNull(); - expect(getPassword().getAttribute('aria-invalid')).toBeNull(); - expect(screen.queryByText('Login details are incomplete')).toBeNull(); - }); -}); +it.live('shows only the Password error when the login is present', () => + Effect.gen(function* showsOnlyThePasswordErrorWhen() { + const user = userEvent.setup(); + renderLogin(); -test('clears stale errors after both fields are corrected', async () => { - const user = userEvent.setup(); - renderLogin(); - - await user - .click(getSubmit()) - .then(async () => await user.type(getLogin(), 'admin')) - .then(async () => await user.type(getPassword(), 'secret')) - .then(async () => await user.click(getSubmit())) - .then(() => { - 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(); - }); -}); + yield* Effect.promise(() => user.type(getLogin(), 'admin')); + yield* Effect.promise(() => user.click(getSubmit())); + expect(getLogin().getAttribute('aria-invalid')).toBeNull(); + expect(getPassword().getAttribute('aria-invalid')).toBe('true'); + expect(screen.queryByText('Enter your login.')).toBeNull(); + expect(screen.getByText('Enter your password.')).toBeTruthy(); + expect(screen.getAllByText('Login details are incomplete')).toHaveLength(1); + expect(document.activeElement).toBe(getPassword()); + }), +); -test('runs the same validation when submitted with Enter', async () => { - const user = userEvent.setup(); - renderLogin(); - - await user - .click(getLogin()) - .then(async () => await user.keyboard('{Enter}')) - .then(() => { - 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()); - }); -}); +it.live('treats a whitespace-only Login as missing', () => + Effect.gen(function* treatsAWhitespaceOnlyLoginAs() { + const user = userEvent.setup(); + renderLogin(); + + yield* Effect.promise(() => user.type(getLogin(), ' ')); + yield* Effect.promise(() => user.type(getPassword(), 'secret')); + yield* Effect.promise(() => user.click(getSubmit())); + expect(getLogin().getAttribute('aria-invalid')).toBe('true'); + expect(screen.getByText('Enter your login.')).toBeTruthy(); + expect(document.activeElement).toBe(getLogin()); + }), +); -test('submits valid values through the Shell authentication client and navigates home', async () => { - const user = userEvent.setup(); - renderLogin(); - - await user - .type(getLogin(), 'admin') - .then(async () => await user.type(getPassword(), 'secret')) - .then(async () => await user.click(getSubmit())) - .then( - async () => - await waitFor(() => { - expect(signInMock).toHaveBeenCalledWith( - { - email: 'admin', - password: Redacted.make('secret'), - }, - { locale: 'en' }, - ); - expect(runBrowserEffectMock).toHaveBeenCalledTimes(1); - expect(navigateMock).toHaveBeenCalledWith({ to: '/en/' }); - expect(screen.queryByText('Login details are incomplete')).toBeNull(); - }), +it.live('accepts a non-empty whitespace Password', () => + Effect.gen(function* acceptsANonEmptyWhitespacePassword() { + const user = userEvent.setup(); + renderLogin(); + + yield* Effect.promise(() => user.type(getLogin(), 'admin')); + yield* Effect.promise(() => user.type(getPassword(), ' ')); + yield* Effect.promise(() => user.click(getSubmit())); + expect(getLogin().getAttribute('aria-invalid')).toBeNull(); + expect(getPassword().getAttribute('aria-invalid')).toBeNull(); + expect(screen.queryByText('Login details are incomplete')).toBeNull(); + }), +); + +it.live('clears stale errors after both fields are corrected', () => + Effect.gen(function* clearsStaleErrorsAfterBothFields() { + const user = userEvent.setup(); + renderLogin(); + + yield* Effect.promise(() => 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(); + }), +); + +it.live('runs the same validation when submitted with Enter', () => + Effect.gen(function* runsTheSameValidationWhenSubmitted() { + 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()); + }), +); + +it.live('submits valid values through the Shell authentication client and navigates home', () => + Effect.gen(function* submitsValidValuesThroughTheShell() { + const user = userEvent.setup(); + renderLogin(); + + yield* Effect.promise(() => user.type(getLogin(), 'admin')); + yield* Effect.promise(() => user.type(getPassword(), 'secret')); + yield* Effect.promise(() => user.click(getSubmit())); + yield* Effect.promise(() => + waitFor(() => { + expect(signInMock).toHaveBeenCalledWith( + { + email: 'admin', + password: Redacted.make('secret'), + }, + { locale: 'en' }, + ); + 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..fd1fb5c8c 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,5 +1,4 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { beforeEach, expect, rstest, test } from '@rstest/core'; +import { beforeEach, expect, rstest, it } from '@app/effect-rstest'; import { Effect } from 'effect'; import * as actualAuthClient from '../../../../src/api/auth-client.ts' with { rstest: 'importActual', @@ -19,10 +18,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, })); @@ -65,7 +60,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 +74,86 @@ 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: { +it.live('retains only declared bounded route parameters outside the resolved target identity', () => + Effect.gen(function* retainsOnlyDeclaredBoundedRouteParameters() { + expect( + yield* Effect.promise(() => + loader({ + 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', }, - request: request(), - routeParams: { id: 'party-1' }, - }), - ).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), - ); -}); + }); + 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.live('retains module landing behavior when no exact page entrypoint is supplied', () => + Effect.gen(function* retainsModuleLandingBehaviorWhenNo() { + expect( + yield* Effect.promise(() => + loader({ 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.live('does not request or load a private target before authentication', () => + Effect.gen(function* doesNotRequestOrLoadA() { + loadHomePageModelMock.mockResolvedValueOnce({ state: 'anonymous' }); + expect( + yield* Effect.promise(() => + loader({ + params: { + entrypointKey: 'party.registry.page.contacts', + moduleId: 'party.registry', + }, + request: request(), + }), + ), + ).toMatchObject({ state: 'selection_required' }); + expect(resolveModuleTargetMock).not.toHaveBeenCalled(); + }), +); -test.each([ +it.live.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* Effect.promise(() => + loader({ + params: { + entrypointKey: 'party.registry.page.contacts', + moduleId: 'party.registry', + }, + request: request(), + }), + ), + ).toMatchObject({ state }); + }), +); 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 1fab832ab..1ee6369e4 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,7 +1,6 @@ -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 '@app/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, @@ -38,10 +37,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, })); @@ -121,7 +116,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); @@ -131,15 +126,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}"`); @@ -154,176 +153,216 @@ 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('loads the generated Customers list page as a static exact target', async () => { - const customersListModel: ResolvedPageModel = { - ...resolvedModel, - routeParams: {}, - target: targetFixture('contacts.core.page-customers-list', 'contacts.core.page.customers-list'), - }; - useLoaderDataMock.mockReturnValue(customersListModel); - render(); - expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(customersListModel.target); - await waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)); - expect(await screen.findByText('contacts.core.page-customers-list:static')).toBeTruthy(); -}); +it.live('loads the generated Customers list page as a static exact target', () => + Effect.gen(function* loadsTheGeneratedCustomersListPage() { + const customersListModel: ResolvedPageModel = { + ...resolvedModel, + routeParams: {}, + target: targetFixture( + 'contacts.core.page-customers-list', + 'contacts.core.page.customers-list', + ), + }; + useLoaderDataMock.mockReturnValue(customersListModel); + render(); + expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(customersListModel.target); + yield* Effect.promise(() => waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1))); + expect( + yield* Effect.promise(() => screen.findByText('contacts.core.page-customers-list:static')), + ).toBeTruthy(); + }), +); -test('loads the approved Customer-detail remote once with the exact declared Customer ID', async () => { - const customerDetailModel: ResolvedPageModel = { - ...resolvedModel, - routeParams: { id: '11111111-1111-4111-8111-111111111111' }, - target: targetFixture( - 'contacts.core.page-customer-detail', - 'contacts.core.page.customer-detail', - ), - }; - useLoaderDataMock.mockReturnValue(customerDetailModel); - render(); - expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(customerDetailModel.target); - await waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)); - expect( - await screen.findByText( - 'contacts.core.page-customer-detail:11111111-1111-4111-8111-111111111111', - ), - ).toBeTruthy(); -}); +it.live('loads the approved Customer-detail remote once with the exact declared Customer ID', () => + Effect.gen(function* loadsTheApprovedCustomerDetailRemote() { + const customerDetailModel: ResolvedPageModel = { + ...resolvedModel, + routeParams: { id: '11111111-1111-4111-8111-111111111111' }, + target: targetFixture( + 'contacts.core.page-customer-detail', + 'contacts.core.page.customer-detail', + ), + }; + useLoaderDataMock.mockReturnValue(customerDetailModel); + render(); + expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(customerDetailModel.target); + yield* Effect.promise(() => waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1))); + expect( + yield* Effect.promise(() => + screen.findByText( + 'contacts.core.page-customer-detail:11111111-1111-4111-8111-111111111111', + ), + ), + ).toBeTruthy(); + }), +); -test('loads the approved Contact-detail remote once with both exact hierarchical IDs', async () => { - const contactDetailModel: ResolvedPageModel = { - ...resolvedModel, - routeParams: { - contactId: '33333333-3333-4333-8333-333333333333', - id: '11111111-1111-4111-8111-111111111111', - }, - target: targetFixture('contacts.core.page-contact-detail', 'contacts.core.page.contact-detail'), - }; - useLoaderDataMock.mockReturnValue(contactDetailModel); - render(); - - expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(contactDetailModel.target); - await waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)); - expect(remotePropsMock).toHaveBeenCalledWith({ - routeParams: contactDetailModel.routeParams, - target: contactDetailModel.target, - }); -}); +it.live('loads the approved Contact-detail remote once with both exact hierarchical IDs', () => + Effect.gen(function* loadsTheApprovedContactDetailRemote() { + const contactDetailModel: ResolvedPageModel = { + ...resolvedModel, + routeParams: { + contactId: '33333333-3333-4333-8333-333333333333', + id: '11111111-1111-4111-8111-111111111111', + }, + target: targetFixture( + 'contacts.core.page-contact-detail', + 'contacts.core.page.contact-detail', + ), + }; + useLoaderDataMock.mockReturnValue(contactDetailModel); + render(); -test('passes ContactEdit both hierarchical IDs and the resolved fail-closed target', async () => { - const contactEditModel: ResolvedPageModel = { - ...resolvedModel, - routeParams: { - contactId: '33333333-3333-4333-8333-333333333333', - id: '11111111-1111-4111-8111-111111111111', - }, - target: targetFixture( - 'contacts.core.page-contact-edit', - 'contacts.core.page.contact-edit', - false, - ), - }; - useLoaderDataMock.mockReturnValue(contactEditModel); - render(); - - expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(contactEditModel.target); - await waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)); - expect(remotePropsMock).toHaveBeenCalledWith({ - routeParams: contactEditModel.routeParams, - target: contactEditModel.target, - }); -}); + expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(contactDetailModel.target); + yield* Effect.promise(() => waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1))); + expect(remotePropsMock).toHaveBeenCalledWith({ + routeParams: contactDetailModel.routeParams, + target: contactDetailModel.target, + }); + }), +); -test('passes CustomerEdit its exact ID and fail-closed writable target', async () => { - const customerEditModel: ResolvedPageModel = { - ...resolvedModel, - routeParams: { id: 'customer-1' }, - target: targetFixture( - 'contacts.core.page-customer-edit', - 'contacts.core.page.customer-edit', - false, - ), - }; - useLoaderDataMock.mockReturnValue(customerEditModel); - render(); - - expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(customerEditModel.target); - await waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)); - expect(remotePropsMock).toHaveBeenCalledWith({ - routeParams: { id: 'customer-1' }, - target: customerEditModel.target, - }); - expect(await screen.findByText('contacts.core.page-customer-edit:customer-1')).toBeTruthy(); -}); +it.live('passes ContactEdit both hierarchical IDs and the resolved fail-closed target', () => + Effect.gen(function* passesContactEditBothHierarchicalIDsAnd() { + const contactEditModel: ResolvedPageModel = { + ...resolvedModel, + routeParams: { + contactId: '33333333-3333-4333-8333-333333333333', + id: '11111111-1111-4111-8111-111111111111', + }, + target: targetFixture( + 'contacts.core.page-contact-edit', + 'contacts.core.page.contact-edit', + false, + ), + }; + useLoaderDataMock.mockReturnValue(contactEditModel); + render(); -test('passes CustomerCreate its bounded route context and resolved writable target', async () => { - const customerCreateModel: ResolvedPageModel = { - ...resolvedModel, - routeParams: { id: 'untrusted-route-context' }, - target: targetFixture( - 'contacts.core.page-customer-create', - 'contacts.core.page.customer-create', - ), - }; - useLoaderDataMock.mockReturnValue(customerCreateModel); - render(); - - expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(customerCreateModel.target); - await waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)); - expect(remotePropsMock).toHaveBeenCalledWith({ - routeParams: { id: 'untrusted-route-context' }, - target: customerCreateModel.target, - }); - expect( - await screen.findByText('contacts.core.page-customer-create:untrusted-route-context'), - ).toBeTruthy(); -}); + expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(contactEditModel.target); + yield* Effect.promise(() => waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1))); + expect(remotePropsMock).toHaveBeenCalledWith({ + routeParams: contactEditModel.routeParams, + target: contactEditModel.target, + }); + }), +); -test('passes ContactCreate its exact ID and fail-closed writable target', async () => { - const contactCreateModel: ResolvedPageModel = { - ...resolvedModel, - routeParams: { id: '11111111-1111-4111-8111-111111111111' }, - target: targetFixture( - 'contacts.core.page-contact-create', - 'contacts.core.page.contact-create', - false, - ), - }; - useLoaderDataMock.mockReturnValue(contactCreateModel); - render(); - - expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(contactCreateModel.target); - await waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)); - expect(remotePropsMock).toHaveBeenCalledWith({ - routeParams: { id: '11111111-1111-4111-8111-111111111111' }, - target: contactCreateModel.target, - }); - expect( - await screen.findByText( - 'contacts.core.page-contact-create:11111111-1111-4111-8111-111111111111', - ), - ).toBeTruthy(); -}); +it.live('passes CustomerEdit its exact ID and fail-closed writable target', () => + Effect.gen(function* passesCustomerEditItsExactIDAnd() { + const customerEditModel: ResolvedPageModel = { + ...resolvedModel, + routeParams: { id: 'customer-1' }, + target: targetFixture( + 'contacts.core.page-customer-edit', + 'contacts.core.page.customer-edit', + false, + ), + }; + useLoaderDataMock.mockReturnValue(customerEditModel); + render(); + + expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(customerEditModel.target); + yield* Effect.promise(() => waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1))); + expect(remotePropsMock).toHaveBeenCalledWith({ + routeParams: { id: 'customer-1' }, + target: customerEditModel.target, + }); + expect( + yield* Effect.promise(() => screen.findByText('contacts.core.page-customer-edit:customer-1')), + ).toBeTruthy(); + }), +); + +it.live('passes CustomerCreate its bounded route context and resolved writable target', () => + Effect.gen(function* passesCustomerCreateItsBoundedRouteContext() { + const customerCreateModel: ResolvedPageModel = { + ...resolvedModel, + routeParams: { id: 'untrusted-route-context' }, + target: targetFixture( + 'contacts.core.page-customer-create', + 'contacts.core.page.customer-create', + ), + }; + useLoaderDataMock.mockReturnValue(customerCreateModel); + render(); + + expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(customerCreateModel.target); + yield* Effect.promise(() => waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1))); + expect(remotePropsMock).toHaveBeenCalledWith({ + routeParams: { id: 'untrusted-route-context' }, + target: customerCreateModel.target, + }); + expect( + yield* Effect.promise(() => + screen.findByText('contacts.core.page-customer-create:untrusted-route-context'), + ), + ).toBeTruthy(); + }), +); + +it.live('passes ContactCreate its exact ID and fail-closed writable target', () => + Effect.gen(function* passesContactCreateItsExactIDAnd() { + const contactCreateModel: ResolvedPageModel = { + ...resolvedModel, + routeParams: { id: '11111111-1111-4111-8111-111111111111' }, + target: targetFixture( + 'contacts.core.page-contact-create', + 'contacts.core.page.contact-create', + false, + ), + }; + useLoaderDataMock.mockReturnValue(contactCreateModel); + render(); + + expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(contactCreateModel.target); + yield* Effect.promise(() => waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1))); + expect(remotePropsMock).toHaveBeenCalledWith({ + routeParams: { id: '11111111-1111-4111-8111-111111111111' }, + target: contactCreateModel.target, + }); + expect( + yield* Effect.promise(() => + screen.findByText('contacts.core.page-contact-create:11111111-1111-4111-8111-111111111111'), + ), + ).toBeTruthy(); + }), +); 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..ec7bcab6a 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 '@app/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..131fca654 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 '@app/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..3f3ef85cf 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 '@app/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/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md b/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md index 266a5523c..e114796eb 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 certified `@app/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 cdbf2c0b3..123aa5a39 100644 --- a/app/oxlint.config.ts +++ b/app/oxlint.config.ts @@ -155,6 +155,8 @@ export default defineConfig({ 'dist', 'node_modules', 'repos/**', + // vendored port of @effect/vitest; kept diffable against upstream + 'packages/effect-rstest/src/**', '.modern', '.modernjs', '**/modern-tanstack/**', @@ -181,91 +183,44 @@ export default defineConfig({ }, overrides: [ { - // Sonar S2187 has a hard-coded API list excluding Effect's it.effect/it.live/it.layer. - // Keep scoped to verified pilot files until the upstream detector supports these APIs. - files: [ - 'packages/core-runtime/tests/unit/outbox-definition.test.ts', - 'packages/core-runtime/tests/integration/outbox-runtime.test.ts', - 'verticals/party-registry/tests/unit/api-integration-ares-application.test.ts', - 'apps/shell-super-app/tests/unit/auth-config.test.ts', - 'apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts', - ], - rules: { 'sonarjs/no-empty-test-file': 'off' }, + 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', + }, }, { - // These native Promise APIs are lifted directly into Effect; async thunks add no behavior. + excludeFiles: ['**/tests/e2e/**'], files: [ - 'packages/core-runtime/tests/integration/outbox-runtime.test.ts', - 'apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts', + 'apps/**/tests/**', + 'verticals/**/tests/**', + 'packages/**/tests/**', + 'scripts/**/*.test.mts', + 'tools/**/tests/*.test.mts', ], - rules: { 'typescript/promise-function-async': 'off' }, - }, - { - // vendored port of @effect/vitest; keep diffable against upstream - files: ['packages/effect-rstest/src/**'], rules: { - 'anti-slop/no-known-value-widening': 'off', - 'anti-slop/no-reflect-apply': 'off', - 'anti-slop/no-reflect-get': 'off', - 'anti-slop/no-runtime-typeof': 'off', - 'anti-slop/no-unknown-parameters': 'off', - 'anti-slop/require-safety-comment-for-type-assertion': 'off', - 'effect-native/no-bare-effect-run': 'off', - 'effect-native/no-dependency-parameters': 'off', - 'effect-native/no-effect-provide-in-library': 'off', - 'effect-native/no-imperative-loop-in-effect-gen': 'off', - 'effect-native/no-layer-provide-in-library': 'off', - 'effect-native/no-local-defect-seam': 'off', - 'effect-native/no-native-error-construction': 'off', - 'effect-native/no-nested-effect-run': 'off', - 'effect-native/no-promise-shaped-port': 'off', - 'effect-native/no-refinement-outside-schema': 'off', - 'eslint/func-names': 'off', - 'eslint/func-style': 'off', - 'eslint/no-inline-comments': 'off', - 'eslint/no-negated-condition': 'off', - 'eslint/no-plusplus': 'off', - 'eslint/no-shadow': 'off', - 'eslint/no-unused-vars': 'off', - 'eslint/no-use-before-define': 'off', - 'eslint/prefer-arrow-callback': 'off', - 'import/export': 'off', - 'import/no-namespace': 'off', - 'jsdoc/check-tag-names': 'off', - 'perfectionist/sort-interfaces': 'off', - 'perfectionist/sort-object-types': 'off', - 'perfectionist/sort-objects': 'off', - 'sonarjs/no-built-in-override': 'off', - 'sonarjs/no-collapsible-if': 'off', - 'sonarjs/no-exclusive-tests': 'off', - 'sonarjs/no-nested-functions': 'off', - 'sonarjs/no-wildcard-import': 'off', - 'sonarjs/variable-name': 'off', - 'typescript/array-type': 'off', - 'typescript/ban-ts-comment': 'off', - 'typescript/consistent-indexed-object-style': 'off', - 'typescript/no-confusing-void-expression': 'off', - 'typescript/no-duplicate-type-constituents': 'off', - 'typescript/no-explicit-any': 'off', - 'typescript/no-namespace': 'off', - 'typescript/no-unnecessary-qualifier': 'off', - 'typescript/no-unnecessary-type-arguments': 'off', - 'typescript/no-unnecessary-type-assertion': 'off', - 'typescript/no-unsafe-argument': 'off', - 'typescript/no-unsafe-return': 'off', - 'typescript/no-unsafe-type-assertion': 'off', - 'typescript/non-nullable-type-assertion-style': 'off', - 'typescript/prefer-for-of': 'off', - 'typescript/prefer-function-type': 'off', - 'typescript/prefer-reduce-type-parameter': 'off', - 'typescript/prefer-ts-expect-error': 'off', - 'typescript/promise-function-async': 'off', - 'typescript/strict-boolean-expressions': 'off', - 'unicorn/catch-error-name': 'off', - 'unicorn/no-array-method-this-argument': 'off', - 'unicorn/no-array-reduce': 'off', - 'unicorn/no-lonely-if': 'off', - 'unicorn/no-negated-condition': 'off', + 'eslint/no-restricted-imports': [ + 'error', + { + paths: [ + { message: 'Import test APIs from @app/effect-rstest instead.', name: 'node:test' }, + { + message: 'Import assertions from @app/effect-rstest instead.', + name: 'node:assert', + }, + { + message: 'Import assertions from @app/effect-rstest instead.', + name: 'node:assert/strict', + }, + { + message: 'Import test APIs from @app/effect-rstest instead.', + name: '@rstest/core', + }, + ], + }, + ], }, }, { diff --git a/app/package.json b/app/package.json index ea17f10e4..b99cfea25 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/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/root-environment.test.mts scripts/tests/typecheck-project-references.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", @@ -85,7 +85,7 @@ "authorization:impact:report": "node ./scripts/report-fail-closed-authorization-impact.mts", "authorization:readiness:check": "node ./scripts/check-authorization-readiness.mts", "quality:audit": "node ./scripts/quality-audit.mts", - "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" + "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" }, "dependencies": { "@authzed/authzed-node": "1.6.1", @@ -97,6 +97,8 @@ "@effect/sql-pg": "4.0.0-beta.107" }, "devDependencies": { + "@app/effect-rstest": "workspace:*", + "@rstest/core": "0.11.10", "@effect/platform-node": "4.0.0-beta.107", "@effect/tsgo": "0.19.0", "@noble/hashes": "2.2.0", diff --git a/app/packages/core-runtime/package.json b/app/packages/core-runtime/package.json index 325deaa91..80f38de48 100644 --- a/app/packages/core-runtime/package.json +++ b/app/packages/core-runtime/package.json @@ -10,7 +10,6 @@ "./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": { diff --git a/app/packages/core-runtime/rstest.config.ts b/app/packages/core-runtime/rstest.config.ts index 671f8b7a8..b82ae3431 100644 --- a/app/packages/core-runtime/rstest.config.ts +++ b/app/packages/core-runtime/rstest.config.ts @@ -1,6 +1,10 @@ import { defineConfig } from '@rstest/core'; export default defineConfig({ + // Integration repositories match the shared database outbox globally. + // Rstest only supports a root pool, so the whole package runs serially to prevent + // independent subscription catalogs from consuming each other. + pool: { maxWorkers: 1 }, projects: [ { include: ['tests/unit/**/*.test.ts'], name: 'unit', testEnvironment: 'node' }, { diff --git a/app/packages/core-runtime/src/testing/actions.ts b/app/packages/core-runtime/src/testing/actions.ts index 8e8dbd7b9..920d3baf7 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[] = []; @@ -331,23 +332,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 }) => @@ -491,7 +490,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/tests/integration/action-permission.test.ts b/app/packages/core-runtime/tests/integration/action-permission.test.ts index a1a66d813..18a6ae45b 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 '@app/effect-rstest'; import { v1 } from '@authzed/authzed-node'; import { and, eq } from 'drizzle-orm'; -import { Effect, Exit, Schema, flow, Predicate } from 'effect'; -import assert from 'node:assert/strict'; +import { Context, Effect, Layer, Exit, Schema, flow, 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,13 +109,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 encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); const promiseEffect = (promise: PromiseLike): Effect.Effect => @@ -185,233 +175,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 +210,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* promiseEffect( + 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* 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, + }), + ), + }), + ), + ); + }); + + const cleanup = 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; + }); + + yield* Effect.acquireRelease(prepare, () => cleanup.pipe(Effect.orDie)); + return adminClient; + }), +); + interface ExecutionCounter { value: number; } @@ -475,64 +482,99 @@ 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()), + ), + ); + +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']); -effectTest( - 'allows direct Principal and Tenant-membership executor grants', +const testProgram1 = () => 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); - }), + 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 }, ), - ), -); + ); -effectTest( - 'persists one normalized terminal denial and no business or collected evidence', - withDatabase((database) => - Effect.gen(function* verifyTerminalDenial() { +const testProgram2 = () => + withDatabase( + Effect.fn(function* verifyTerminalDenial(database) { const executions: ExecutionCounter = { value: 0 }; const key = 'missing'; const moduleStateKey = `${actionPrefix}.state.missing`; @@ -554,7 +596,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() @@ -578,39 +623,34 @@ effectTest( .where(eq(outboxMessages.tenantId, tenantId)), ]); - assert.ok(Predicate.isTagged(failure, '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( [ @@ -618,37 +658,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.ok(Predicate.isTagged(failure, '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`; @@ -672,122 +709,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.equal(results.length, 2); + expect(results.length).toBe(2); for (const result of results) { - assert.ok(Predicate.isTagged(result, 'ActionPermissionDenied')); + expect(Predicate.isTagged(result, 'ActionPermissionDenied')).toBe(true); } - assert.equal(executions.value, 0); - assert.equal(invocation.status, 'rejected'); - assert.equal(audits.length, 1); + 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* promiseEffect( + 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.ok(Predicate.isTagged(failure, '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`; @@ -806,13 +810,16 @@ effectTest( transport: transport(key, moduleStateKey), }), ), - { ...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) @@ -823,12 +830,40 @@ effectTest( ), ); - assert.ok(Predicate.isTagged(failure, '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 4be2144a7..665ca79cd 100644 --- a/app/packages/core-runtime/tests/integration/action-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/action-runtime.test.ts @@ -1,11 +1,8 @@ -import { runEffectTestPromise, runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from '@app/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, Predicate } 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'; @@ -165,8 +162,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() { @@ -226,11 +223,6 @@ const withEvidencePersistenceFailure = ( return { executor }; }; -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 { @@ -240,38 +232,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', @@ -279,59 +265,41 @@ 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( - database.executor.delete(outboxMessages).where(eq(outboxMessages.tenantId, tenantId)), - ); - await runEffectTestPromise( - database.executor.delete(domainEvents).where(eq(domainEvents.tenantId, tenantId)), - ); - await runEffectTestPromise( - database.executor.delete(dataAccessEvents).where(eq(dataAccessEvents.tenantId, tenantId)), - ); - await runEffectTestPromise( - database.executor.delete(auditEvents).where(eq(auditEvents.tenantId, tenantId)), - ); - await runEffectTestPromise( - database.executor + }); + }), + ))(); + +const cleanup = (() => + withDatabase( + Effect.fn(function* integrationProgram2(database) { + yield* database.executor.delete(outboxMessages).where(eq(outboxMessages.tenantId, tenantId)); + yield* database.executor.delete(domainEvents).where(eq(domainEvents.tenantId, tenantId)); + yield* database.executor + .delete(dataAccessEvents) + .where(eq(dataAccessEvents.tenantId, tenantId)); + yield* database.executor.delete(auditEvents).where(eq(auditEvents.tenantId, tenantId)); + yield* database.executor .delete(tenantModuleStateChanges) - .where(eq(tenantModuleStateChanges.tenantId, tenantId)), - ); - await runEffectTestPromise( - database.executor.delete(tenantModuleStates).where(eq(tenantModuleStates.tenantId, tenantId)), - ); - await runEffectTestPromise( - database.executor.delete(actionInvocations).where(eq(actionInvocations.tenantId, tenantId)), - ); - await runEffectTestPromise( - database.executor + .where(eq(tenantModuleStateChanges.tenantId, tenantId)); + yield* database.executor + .delete(tenantModuleStates) + .where(eq(tenantModuleStates.tenantId, tenantId)); + yield* database.executor + .delete(actionInvocations) + .where(eq(actionInvocations.tenantId, tenantId)); + yield* database.executor .delete(principalAuthBindings) - .where(eq(principalAuthBindings.tenantId, tenantId)), - ); - await runEffectTestPromise( - database.executor.delete(principals).where(eq(principals.tenantId, tenantId)), - ); - await runEffectTestPromise( - database.executor.delete(legalEntities).where(eq(legalEntities.tenantId, tenantId)), - ); - await runEffectTestPromise( - database.executor.delete(tenants).where(eq(tenants.tenantId, tenantId)), - ); - }); -}); + .where(eq(principalAuthBindings.tenantId, tenantId)); + yield* database.executor.delete(principals).where(eq(principals.tenantId, tenantId)); + yield* database.executor.delete(legalEntities).where(eq(legalEntities.tenantId, tenantId)); + yield* database.executor.delete(tenants).where(eq(tenants.tenantId, tenantId)); + }), + ))(); const TestDomainEvents = { 'test-state.changed': Schema.Struct({ value: Schema.String }), @@ -348,6 +316,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'>[]; } @@ -357,6 +326,7 @@ const makeRegistration = ({ mode = 'success', moduleStateKey, onExecute, + onExecuteEffect, policies = [], }: RegistrationOptions) => defineAction( @@ -384,150 +354,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 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', @@ -544,31 +516,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.ok(hasFailure(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', @@ -585,34 +554,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({ @@ -620,11 +586,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) @@ -646,32 +610,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[] = []; @@ -683,16 +645,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({ @@ -702,38 +664,37 @@ void test('commits allowed Policy checkpoints atomically before handler success policies: [policy], }), transport: transport(key, moduleStateKey), - }), - ); + }); - const [invocation] = await runEffectTestPromise( - database.executor + const [invocation] = yield* database.executor .select() .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, key)), - ); - assert.ok(invocation); - const audits = await runEffectTestPromise( - database.executor + .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)), - ); - - 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); - }); + .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)); + + 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', @@ -793,7 +754,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) => { @@ -811,167 +775,159 @@ 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.ok(hasFailure(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 [invocation] = await runEffectTestPromise( - database.executor - .select() - .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, key)), - ); - assert.ok(invocation); - const audits = await runEffectTestPromise( - database.executor - .select() - .from(auditEvents) - .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)), - ); - - assert.ok( - hasFailure(exit, 'ActionInvocationPersistenceError'), - Exit.isFailure(exit) ? Cause.pretty(exit.cause) : 'success', + ); + 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( + 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', @@ -993,24 +949,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, @@ -1021,64 +974,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.ok(hasFailure(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', @@ -1091,29 +1033,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, @@ -1124,25 +1061,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) @@ -1159,85 +1090,83 @@ void test('rolls back every individual success-evidence persistence failure', as .select() .from(outboxMessages) .where(eq(outboxMessages.tenantId, tenantId)), - ]), - ); + ]); - assert.ok(hasFailure(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 [invocation] = await runEffectTestPromise( - database.executor +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 [invocation] = yield* database.executor .select() .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, key)), - ); - assert.ok(invocation); - const audits = await runEffectTestPromise( - database.executor + .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)), - ); - - assert.ok(hasFailure(first, 'ActionPolicyDenied')); - assert.ok(hasFailure(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() { + .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)); + + 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); @@ -1245,392 +1174,398 @@ 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.equal(concurrent.length, 2); - for (const outcome of concurrent) { - assert.ok(hasFailure(outcome, '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 [invocation] = await runEffectTestPromise( - database.executor + 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); + }), + ); + +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), + }; + + 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 [invocation] = yield* database.executor .select() .from(actionInvocations) - .where(eq(actionInvocations.idempotencyKey, key)), - ); - assert.ok(invocation); - const audits = await runEffectTestPromise( - database.executor + .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)), - ); + .where(eq(auditEvents.actionInvocationId, invocation.actionInvocationId)); - assert.equal(successResult.value, 'same'); - assert.ok(hasFailure(rejectedExit, 'ActionInvocationPersistenceError')); - assert.equal(invocation.status, 'succeeded'); - assert.equal(audits.filter((row) => row.eventType === 'action.rejected').length, 0); - }); -}); - -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), - ]); - - assert.equal(executions, 1); - assert.equal(concurrentResults.filter(Exit.isSuccess).length, 1); - const failedResults = concurrentResults.filter(Exit.isFailure); - assert.equal(failedResults.length, 1); - for (const outcome of failedResults) { - assert.ok(hasFailure(outcome, 'ActionAlreadyCommitted')); - } + 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 committedRetry = yield* Effect.exit( - runtime.runAction({ - ...concurrentInput, - transport: { - ...concurrentInput.transport, - correlationId: 'integration-concurrent-retry', - traceId: 'retry-trace', - }, - }), - ); - assert.ok(hasFailure(committedRetry, 'ActionAlreadyCommitted')); - assert.equal(executions, 1); +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 conflict = yield* Effect.exit( - runtime.runAction({ - ...concurrentInput, - payload: { value: 'different' }, - }), - ); - assert.ok(hasFailure(conflict, 'ActionRequestHashConflict')); + 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 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.ok(hasFailure(rejected, 'TestDomainRejected')); + 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 uncertainExecutor: ContextServiceContract['executor'] = Object.assign( - Object.create(database.executor), - uncertainTransaction, - ); - const uncertainRuntime = makeActionRuntime( - { executor: uncertainExecutor }, - 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 uncertainExecutor: ContextServiceContract['executor'] = Object.assign( + Object.create(database.executor), + uncertainTransaction, + ); + const uncertainRuntime = makeActionRuntime( + { executor: uncertainExecutor }, + 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.ok(hasFailure(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: { @@ -1638,263 +1573,292 @@ 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.ok(hasFailure(committedResolution, 'ActionAlreadyCommitted')); - assert.ok(hasFailure(unauthorizedResolution, 'ActionInvocationNotFound')); - assert.ok(hasFailure(unavailableResolution, 'ActionCommitIndeterminate')); - assert.ok(hasFailure(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 uncertainRollbackExecutor: ContextServiceContract['executor'] = Object.assign( - Object.create(database.executor), - uncertainRollbackTransaction, - ); - const uncertainOpenRuntime = makeActionRuntime( - { executor: uncertainRollbackExecutor }, - 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 uncertainRollbackExecutor: ContextServiceContract['executor'] = Object.assign( + Object.create(database.executor), + uncertainRollbackTransaction, + ); + const uncertainOpenRuntime = makeActionRuntime( + { executor: uncertainRollbackExecutor }, + 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.ok(hasFailure(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.ok(Predicate.isTagged(openResolution, '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.ok(hasFailure(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.ok(hasFailure(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..fe6e0a17c 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 '@app/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,18 +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)); @@ -194,15 +181,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,17 +196,21 @@ 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`)); @@ -233,16 +223,14 @@ const contactsIdentityMigrationProgram = Effect.gen(function* contactsIdentityMi [legacyModule, contactsModule], ), ); - yield* databaseEffect(assert.rejects(pool.query(statements[0] ?? ''), /would collide/u)); + const collisionError = yield* Effect.flip(databaseEffect(pool.query(statements[0] ?? ''))); + expect(String(collisionError.cause)).toMatch(/would collide/u); const collisionRows = yield* databaseEffect( 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(() => @@ -252,7 +240,12 @@ const contactsIdentityMigrationProgram = Effect.gen(function* contactsIdentityMi ); }).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..0c8af3649 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 '@app/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 adeeafca4..6d5e4d62b 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 '@app/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, @@ -101,581 +83,579 @@ 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), + () => + 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), ); - 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, - ), - ); - 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* promiseEffect( + spiceDbClient.promises.writeRelationships.bind( + spiceDbClient.promises, + 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* promiseEffect( + spiceDbClient.promises.writeRelationships.bind( + spiceDbClient.promises, + 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.ok(Predicate.isTagged(systemDenied, '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* promiseEffect( + spiceDbClient.promises.writeRelationships.bind( + spiceDbClient.promises, + 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, + }), ), + }); + return promiseEffect( + spiceDbClient.promises.writeRelationships.bind(spiceDbClient.promises, request), ); - 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, + }); + 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.acquireRelease(Effect.void, () => 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 fe65078e5..48d975903 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,25 +1,12 @@ -import { - makeEffectTestCallback as nativeTestCallback, - makeEffectTestCallback, -} from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from '@app/effect-rstest'; import { eq } from 'drizzle-orm'; -import { Effect, Exit as NativeExit, Scope as NativeScope, Predicate } from 'effect'; -import assert from 'node:assert/strict'; -import test, { after as afterNativeDatabase } from 'node:test'; +import { Effect, Predicate } from 'effect'; import { Pool } from 'pg'; import { makeLegalEntityContext } from '../../src/auth/legal-entity-context.ts'; import { loadDatabaseConfig } from '../../src/db/config.ts'; import { coreRelations, legalEntities, tenants } from '../../src/db/schema.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.promise(() => operation()); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), -); const tenantOne = '11000000-0000-4000-8000-000000000001'; const tenantTwo = '11000000-0000-4000-8000-000000000002'; @@ -28,18 +15,14 @@ const activeTwo = '21000000-0000-4000-8000-000000000002'; const suspended = '21000000-0000-4000-8000-000000000003'; const foreign = '21000000-0000-4000-8000-000000000004'; -const effectTest = (name: string, effect: Effect.Effect): void => { - test(name, makeEffectTestCallback(effect)); -}; - -effectTest( - '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 pool = new Pool({ connectionString: configuration.connectionString }); - const database = yield* makeTestDatabaseFromPool(pool, coreRelations).pipe( - NativeScope.provide(nativeDatabaseScope), + const pool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: configuration.connectionString })), + (resource) => Effect.promise(() => resource.end()).pipe(Effect.orDie), ); + const database = yield* makeTestDatabaseFromPool(pool, coreRelations); const context = makeLegalEntityContext({ executor: database }); const cleanup = Effect.gen(function* cleanLegalEntityContextFixtures() { yield* database.delete(legalEntities).where(eq(legalEntities.tenantId, tenantOne)); @@ -48,74 +31,70 @@ effectTest( 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.ok(Predicate.isTagged(inactiveError, 'LegalEntityContextInactiveError')); - const missingError = yield* Effect.flip(context.validateSelection(tenantOne, foreign)); - assert.ok(Predicate.isTagged(missingError, 'LegalEntityContextMissingError')); - }).pipe( - Effect.ensuring(cleanup.pipe(Effect.orDie)), - Effect.ensuring(databaseEffect(pool.end.bind(pool)).pipe(Effect.orDie)), - ); + 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 c96678f80..c9427ef91 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 '@app/effect-rstest'; import { and, eq } from 'drizzle-orm'; -import { Effect, Exit, Predicate } 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.acquireRelease(Effect.void, () => + Effect.gen(function* moduleStateGate2() { + yield* database.executor + .delete(tenantModuleStates) + .where(eq(tenantModuleStates.tenantId, tenantOne)); + yield* database.executor + .delete(tenantModuleStates) + .where(eq(tenantModuleStates.tenantId, tenantTwo)); + yield* database.executor.delete(tenants).where(eq(tenants.tenantId, tenantOne)); + yield* database.executor.delete(tenants).where(eq(tenants.tenantId, tenantTwo)); + }).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]), - ); - 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)), + const firstPersistedDescriptor = Option.getOrThrow( + Option.fromNullishOr(persistedStateDescriptors[0]), + ); + 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.ok(Predicate.isTagged(quarantined, '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,75 +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.ok(Predicate.isTagged(missing, '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), - ), - ); - await runEffectTestPromise( - database.executor - .update(tenantModuleStates) - .set({ state: 'read_only' }) - .where( - and( - eq(tenantModuleStates.tenantId, tenantOne), - eq(tenantModuleStates.moduleKey, moduleKey), - ), + yield* database.executor.transaction((transaction) => + gate.recheckWrite(transaction, tenantOne, write), + ); + 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.ok(Predicate.isTagged(lockedDenial, '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.ok(Predicate.isTagged(unavailable, '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], - ), + const malformed = yield* Effect.flip( + makeModuleStateGate(unavailableStateService('corrupt-storage-value')).prepareSnapshot( + tenantOne, + [read], ), ); - assert.ok(Predicate.isTagged(malformed, 'ModuleStateCheckUnavailableError')); - assert.doesNotMatch(malformed.reason, /corrupt|storage/u); - } finally { - await runEffectTestPromise( - database.executor - .delete(tenantModuleStates) - .where(eq(tenantModuleStates.tenantId, tenantOne)), - ); - await runEffectTestPromise( - database.executor - .delete(tenantModuleStates) - .where(eq(tenantModuleStates.tenantId, tenantTwo)), - ); - await runEffectTestPromise( - database.executor.delete(tenants).where(eq(tenants.tenantId, tenantOne)), - ); - await runEffectTestPromise( - database.executor.delete(tenants).where(eq(tenants.tenantId, tenantTwo)), - ); - } - }); -}); + expect(Predicate.isTagged(malformed, 'ModuleStateCheckUnavailableError')).toBe(true); + expect(malformed.reason).not.toMatch(/corrupt|storage/u); + }), +); 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..0e151d21e 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 '@app/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 1da0e2a30..5d83f2f31 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,8 @@ -import { - makeEffectTestCallback as nativeTestCallback, - runEffectTestPromise, -} from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from '@app/effect-rstest'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. import { eq } from 'drizzle-orm'; -import { Effect, Exit as NativeExit, Scope as NativeScope, Predicate } 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,46 +14,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 { 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( - database - .delete(principalAuthBindings) - .where(eq(principalAuthBindings.providerSubjectId, providerKeyId)), - ); - await runEffectTestPromise( - database.delete(principals).where(eq(principals.tenantId, tenantId)), - ); - await runEffectTestPromise(database.delete(tenants).where(eq(tenants.tenantId, tenantId))); - }; +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 = Effect.gen(function* principalManagement2() { + yield* database + .delete(principalAuthBindings) + .where(eq(principalAuthBindings.providerSubjectId, providerKeyId)); + yield* database.delete(principals).where(eq(principals.tenantId, tenantId)); + yield* 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', @@ -70,10 +54,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', @@ -84,10 +66,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, @@ -99,10 +79,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, @@ -116,12 +94,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, @@ -137,12 +113,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', @@ -157,18 +131,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 61a728070..a907a4f05 100644 --- a/app/packages/core-runtime/tests/integration/principal-resolver.test.ts +++ b/app/packages/core-runtime/tests/integration/principal-resolver.test.ts @@ -1,25 +1,12 @@ -import { - makeEffectTestCallback as nativeTestCallback, - makeEffectTestCallback, -} from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from '@app/effect-rstest'; import { and, eq } from 'drizzle-orm'; -import { DateTime, Effect, Exit as NativeExit, Scope as NativeScope, Predicate } from 'effect'; -import assert from 'node:assert/strict'; -import test, { after as afterNativeDatabase } from 'node:test'; +import { DateTime, Effect, Predicate } from 'effect'; import { Pool } from 'pg'; import { makePrincipalResolver } from '../../src/auth/principal-resolver.ts'; import { loadDatabaseConfig } from '../../src/db/config.ts'; import { coreRelations, principalAuthBindings, principals, tenants } from '../../src/db/schema.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.promise(() => operation()); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), -); const tenantOne = '10000000-0000-4000-8000-000000000001'; const tenantTwo = '10000000-0000-4000-8000-000000000002'; @@ -27,35 +14,32 @@ const principalOne = '20000000-0000-4000-8000-000000000001'; const principalTwo = '20000000-0000-4000-8000-000000000002'; const subject = 'better-auth-integration-subject'; -const effectTest = (name: string, effect: Effect.Effect): void => { - test(name, makeEffectTestCallback(effect)); -}; - -effectTest( +it.live( 'lists and selects multiple tenant-scoped principals and fails closed after access changes', - Effect.gen(function* principalResolverIntegration() { - const configuration = yield* loadDatabaseConfig(); - const pool = new Pool({ connectionString: configuration.connectionString }); - const database = yield* makeTestDatabaseFromPool(pool, coreRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ); - 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 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 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', @@ -107,18 +91,18 @@ effectTest( }, ]); - 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.ok(Predicate.isTagged(foreignResolution, 'PrincipalBindingMissingError')); + expect(Predicate.isTagged(foreignResolution, 'PrincipalBindingMissingError')).toBe(true); yield* database .update(principalAuthBindings) @@ -127,13 +111,13 @@ effectTest( 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.ok(Predicate.isTagged(revokedResolution, 'PrincipalBindingInactiveError')); + expect(Predicate.isTagged(revokedResolution, 'PrincipalBindingInactiveError')).toBe(true); yield* database .update(principalAuthBindings) @@ -146,7 +130,7 @@ effectTest( const inactivePrincipal = yield* Effect.flip( resolver.resolveBetterAuthUserForTenant(subject, tenantOne), ); - assert.ok(Predicate.isTagged(inactivePrincipal, 'PrincipalInactiveError')); + expect(Predicate.isTagged(inactivePrincipal, 'PrincipalInactiveError')).toBe(true); yield* database .update(principals) @@ -159,10 +143,6 @@ effectTest( const inactiveTenant = yield* Effect.flip( resolver.resolveBetterAuthUserForTenant(subject, tenantOne), ); - assert.ok(Predicate.isTagged(inactiveTenant, 'TenantInactiveError')); - }).pipe( - Effect.ensuring(cleanup.pipe(Effect.orDie)), - Effect.ensuring(databaseEffect(pool.end.bind(pool)).pipe(Effect.orDie)), - ); - }), + 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..03f8695e6 100644 --- a/app/packages/core-runtime/tests/integration/read-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/read-runtime.test.ts @@ -1,14 +1,8 @@ -import { - makeEffectTestCallback as nativeTestCallback, - runEffectTestPromise, -} from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from '@app/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, @@ -24,69 +18,83 @@ import { 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), -); - -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 connections = yield* loadDatabaseConnectionPair(); + const admin = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), + (ownedPool) => Effect.promise(() => ownedPool.end()).pipe(Effect.orDie), + ); + const runtimePool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.runtime.connectionString })), + (ownedPool) => Effect.promise(() => ownedPool.end()).pipe(Effect.orDie), + ); + 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.acquireRelease(Effect.void, () => + 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 +102,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 +119,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 +148,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 9052c6bf8..931b750ba 100644 --- a/app/packages/core-runtime/tests/integration/search-persistence.test.ts +++ b/app/packages/core-runtime/tests/integration/search-persistence.test.ts @@ -1,19 +1,7 @@ -import { - makeEffectTestCallback as nativeTestCallback, - makeEffectTestCallback, -} from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from '@app/effect-rstest'; -import { - Effect, - Function as Fn, - Exit as NativeExit, - Scope as NativeScope, - Schema, - Predicate, -} 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'; @@ -21,12 +9,6 @@ import { coreRelations } from '../../src/db/schema.ts'; import { makePostgresCoreSearchProjectionStore } from '../../src/search/persistence.ts'; import { makeCoreSearchQueryRuntime } 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), -); const queryEffect = ( client: Pool, @@ -36,106 +18,108 @@ 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 = makeCoreSearchQueryRuntime(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 connections = yield* loadDatabaseConnectionPair(); + const admin = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.admin.connectionString })), + (ownedPool) => Effect.promise(() => ownedPool.end()).pipe(Effect.orDie), + ); + const runtimePool = yield* Effect.acquireRelease( + Effect.sync(() => new Pool({ connectionString: connections.runtime.connectionString })), + (ownedPool) => Effect.promise(() => ownedPool.end()).pipe(Effect.orDie), + ); + 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 = makeCoreSearchQueryRuntime(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.acquireRelease(Effect.void, () => 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')`, @@ -184,11 +168,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, @@ -199,19 +180,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' }; @@ -229,9 +209,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 = () => makeCoreSearchQueryRuntime(restarted).search({ @@ -247,25 +225,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.ok(Predicate.isTagged(divergence, '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', @@ -274,11 +252,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', @@ -286,28 +261,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 f491e5cb2..e8f120346 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,19 +1,8 @@ -import { makeEffectTestCallback as nativeTestCallback } from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from '@app/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 { Cause, Crypto, Deferred, Effect, Fiber, Option } from 'effect'; import type { PoolClient } from 'pg'; import { Pool } from 'pg'; import { loadDatabaseConnectionPair } from '../../src/db/config.ts'; @@ -29,14 +18,7 @@ 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()); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), -); - -const workerSnapshotRuntime = ManagedRuntime.make(NodeServices.layer); const readLegalEntitySettings = (executor: CoreSearchSnapshotReadExecutor, eventId: string) => executor .select({ @@ -68,14 +50,14 @@ const readSnapshotPosition = ( const beginTransaction = (client: PoolClient) => Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => client.query('begin'), }); const commitTransaction = (client: PoolClient) => Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => client.query('commit'), }); @@ -87,7 +69,7 @@ const insertPendingEvent = ( ) => Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + 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)`, @@ -110,9 +92,7 @@ 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) => @@ -120,21 +100,21 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { const subjectId = yield* crypto.randomUUIDv4; const result = yield* Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + 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] = result.rows; - assert.ok(row); + const row = Option.getOrThrow(Option.fromNullishOr(result.rows[0])); + expect(row).toBeDefined(); return row.tenant_sequence_no; }); const cleanup = Effect.gen(function* cleanupWorkerSnapshot() { yield* Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => admin.query('delete from core.search_projection_generations where tenant_id = $1', [ tenantId, @@ -142,29 +122,29 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { }); yield* Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => admin.query('delete from core.domain_events where tenant_id = $1', [tenantId]), }); yield* Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => admin.query('delete from core.legal_entities where tenant_id = $1', [tenantId]), }); yield* Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => admin.query('delete from core.tenants where tenant_id = $1', [tenantId]), }); yield* Effect.all( [ Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => admin.end(), }), Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => runtimePool.end(), }), ], @@ -172,10 +152,11 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { ); }).pipe(Effect.orDie); - yield* Effect.gen(function* exerciseWorkerSnapshots() { + yield* Effect.acquireRelease(Effect.void, () => cleanup); + const exercise = Effect.gen(function* exerciseWorkerSnapshots() { yield* Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => admin.query( `insert into core.tenants (tenant_id, slug, name, status, default_locale) values ($1, $2, 'Snapshot tenant', 'active', 'en')`, @@ -184,7 +165,7 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { }); yield* Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + 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')`, @@ -215,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); @@ -224,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, @@ -232,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 @@ -269,13 +249,13 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { ? Effect.succeed(true) : Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => admin.query('select pg_sleep(0.01)'), }).pipe( Effect.andThen( Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => admin.query<{ count: number }>( `select count(*)::int as count from pg_stat_activity where application_name = $1 and wait_event_type = 'Lock'`, @@ -286,15 +266,15 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { 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. @@ -310,11 +290,11 @@ 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', }); @@ -322,27 +302,24 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { yield* Effect.acquireUseRelease( Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => admin.connect(), }), lateCommitSnapshot, (pending) => Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), - // oxlint-disable-next-line typescript/promise-function-async -- Effect owns this foreign pg SDK Promise boundary. + try: () => pending.query('rollback'), }).pipe(Effect.orDie, Effect.ensuring(Effect.sync(() => pending.release()))), ); - }).pipe(Effect.ensuring(cleanup)); -}); - -void 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(), - }), }); + 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..d4ec5e576 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 '@app/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 ac84a1dc2..c21696130 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,9 +1,8 @@ +import { expect, it } from '@app/effect-rstest'; 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'; @@ -141,13 +140,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() { yield* database.executor @@ -171,69 +163,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), @@ -293,8 +285,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) @@ -303,277 +295,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) => @@ -602,57 +570,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( @@ -683,7 +644,7 @@ effectTest( }), { concurrency: 'unbounded' }, ); - assert.ok(exits.every(Exit.isSuccess)); + expect(exits.every(Exit.isSuccess)).toBe(true); yield* withDatabase((database) => Effect.gen(function* verifySerializedTransitions() { @@ -695,57 +656,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 85c0e5a74..663f4daac 100644 --- a/app/packages/core-runtime/tests/support/database.ts +++ b/app/packages/core-runtime/tests/support/database.ts @@ -7,37 +7,34 @@ import { Reactivity } from 'effect/unstable/reactivity'; import type { Connection } from 'effect/unstable/sql/SqlConnection'; import type { SqlError } from 'effect/unstable/sql/SqlError'; import { coreRelations } from '../../src/db/schema.ts'; -import { runEffectTestSync } from './effect-runtime.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 values = (sql: string, params: readonly unknown[]) => - execute(sql, params).pipe(Effect.map((rows) => rows.map(Object.values))); - const connection: Connection = { - execute, - executeRaw: execute, - executeStream: (sql, params) => Stream.fromIterableEffect(execute(sql, params)), - executeUnprepared: execute, - executeValues: values, - executeValuesUnprepared: values, - }; - 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 values = (sql: string, params: readonly unknown[]) => + execute(sql, params).pipe(Effect.map((rows) => rows.map(Object.values))); + const connection: Connection = { + execute, + executeRaw: execute, + executeStream: (sql, params) => Stream.fromIterableEffect(execute(sql, params)), + executeUnprepared: execute, + executeValues: values, + executeValuesUnprepared: values, + }; + 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. */ 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..32af05c70 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 '@app/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 445eed263..eadc43450 100644 --- a/app/packages/core-runtime/tests/unit/action-collector.test.ts +++ b/app/packages/core-runtime/tests/unit/action-collector.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 '@app/effect-rstest'; import { Effect, Schema, Predicate } from 'effect'; import { createActionCollector } from '../../src/actions/collector.ts'; @@ -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.ok(Predicate.isTagged(foreignError, 'ActionCollectorError')); - assert.ok(Predicate.isTagged(orphanError, '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.ok(Predicate.isTagged(repeated, 'ActionCollectorError')); - assert.ok(Predicate.isTagged(invalid, 'ActionCollectorError')); - assert.ok(Predicate.isTagged(undeclared, 'ActionCollectorError')); - assert.ok(Predicate.isTagged(missingSchema, '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.ok(Predicate.isTagged(error, '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.ok(Predicate.isTagged(error, '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.ok(Predicate.isTagged(invalidPayload, 'ActionCollectorError')); - assert.ok(Predicate.isTagged(invalidProducer, 'ActionCollectorError')); - assert.ok(Predicate.isTagged(undeclared, 'ActionCollectorError')); - assert.ok(Predicate.isTagged(inheritedName, '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 aa403c136..1bf5ba775 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 '@app/effect-rstest'; import { DateTime, Effect, Option, Schema, Predicate } from 'effect'; import { decodeActionPayload, @@ -16,47 +13,49 @@ import { defineTenantModuleEntrypoint, } from '../../src/modules/module-entrypoint.ts'; -void test('defines an immutable typed descriptor and decodes typed payloads and results', async () => { - const registration = defineAction( - { - accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, - actionKey: 'shell.counter.change', - auditProfile: 'standard', - domainErrorSchema: Schema.Never, - domainEvents: {}, - entrypoint: defineSystemModuleEntrypoint({ - access: 'write', - authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, - entrypointKey: 'shell.counter.change', - moduleKey: 'core.shell', - role: 'action', - }), - idempotency: 'required', - legalEntityScope: 'optional', - owningModuleKey: 'core.shell', - payloadSchema: Schema.Struct({ amount: Schema.Finite }), - policies: [], - 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( + { + accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, + actionKey: 'shell.counter.change', + auditProfile: 'standard', + domainErrorSchema: Schema.Never, + domainEvents: {}, + entrypoint: defineSystemModuleEntrypoint({ + access: 'write', + authorization: { kind: 'action_execution', provisioning: 'tenant_membership_default' }, + entrypointKey: 'shell.counter.change', + moduleKey: 'core.shell', + role: 'action', + }), + idempotency: 'required', + legalEntityScope: 'optional', + owningModuleKey: 'core.shell', + payloadSchema: Schema.Struct({ amount: Schema.Finite }), + policies: [], + 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); + }), +); -void 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', @@ -68,13 +67,13 @@ void test('keeps the Resource permission resolver private behind an immutable de }), ); - 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); }); -void 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' }, @@ -82,7 +81,7 @@ void test('requires trusted Legal Entity scope for a Counterparty permission dec moduleKey: 'party.registry', role: 'action', }); - assert.throws(() => + expect(() => validateActionDescriptorInput({ entrypoint, legalEntityPermission: 'manage_counterparty', @@ -90,8 +89,8 @@ void test('requires trusted Legal Entity scope for a Counterparty permission dec owningModuleKey: 'party.registry', policies: [], }), - ); - assert.doesNotThrow(() => + ).toThrow(); + expect(() => validateActionDescriptorInput({ entrypoint, legalEntityPermission: 'manage_counterparty', @@ -99,48 +98,48 @@ void test('requires trusted Legal Entity scope for a Counterparty permission dec owningModuleKey: 'party.registry', policies: [], }), - ); + ).not.toThrow(); }); -void 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.ok(Predicate.isTagged(invalid, '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( { accessEvidencePolicy: { captureMode: 'metadata_only', policyKey: 'counter.read.v1' }, @@ -166,35 +165,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.ok(Predicate.isTagged(error, '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', @@ -231,13 +236,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', @@ -286,7 +291,7 @@ void test('rejects cross-owner, string, copied, and missing Policy references at ); }; - assert.throws(() => + expect(() => defineAction( { ...descriptor, @@ -295,21 +300,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' }, @@ -334,7 +339,7 @@ void test('rejects Action entrypoint owner, scope, role/access, and forged immut }, () => Effect.void, ); - assert.throws(() => + expect(() => validateActionDescriptorInput({ ...registration.descriptor, entrypoint: defineTenantModuleEntrypoint({ @@ -345,8 +350,8 @@ void test('rejects Action entrypoint owner, scope, role/access, and forged immut role: 'action', }), }), - ); - assert.throws(() => + ).toThrow(); + expect(() => validateActionDescriptorInput({ ...registration.descriptor, entrypoint: defineSystemModuleEntrypoint({ @@ -357,8 +362,8 @@ void test('rejects Action entrypoint owner, scope, role/access, and forged immut role: 'action', }), }), - ); - assert.throws(() => + ).toThrow(); + expect(() => validateActionDescriptorInput({ ...registration.descriptor, entrypoint: defineTenantModuleEntrypoint({ @@ -370,17 +375,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 917da6b2b..dcc160b61 100644 --- a/app/packages/core-runtime/tests/unit/action-errors.test.ts +++ b/app/packages/core-runtime/tests/unit/action-errors.test.ts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/effect-rstest'; import { Schema, Predicate } from 'effect'; import { ACTION_CORE_ERROR_TAGS, @@ -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.equal(errors.length, ACTION_CORE_ERROR_TAGS.length); + expect(errors.length).toBe(ACTION_CORE_ERROR_TAGS.length); for (const [index, tag] of ACTION_CORE_ERROR_TAGS.entries()) { - assert.ok(Predicate.isTagged(errors[index], tag)); + 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-identity.test.ts b/app/packages/core-runtime/tests/unit/action-identity.test.ts index 9bcb80e3a..a06ade87c 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 '@app/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..de79fffb6 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 '@app/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 e00e5113e..c9f72809e 100644 --- a/app/packages/core-runtime/tests/unit/action-policy.test.ts +++ b/app/packages/core-runtime/tests/unit/action-policy.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 '@app/effect-rstest'; import { Effect, Predicate } from 'effect'; import { defineGlobalPolicy, @@ -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.ok(Predicate.isTagged(denial, '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..9c8828a9d 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 '@app/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 9b5b8da92..60a696f77 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,6 @@ -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 { expect, it } from '@app/effect-rstest'; import { Cause, DateTime, 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 { defineAction, defineActionResourcePermission, @@ -82,23 +78,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 +105,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 +120,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[] = []; @@ -243,47 +237,46 @@ const makeHarness = (options: HarnessOptions = {}) => { let installedTenantId: string = principal.tenantId; let installedLegalEntityId: string = principal.legalEntityId; - 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; - } - } - if (text === 'begin') { - transactionCount += 1; - if (options.transactionMode === 'definite-failure') { - return yield* new SqlError({ - reason: new ConnectionError({ cause: new Error('transaction unavailable') }), - }); - } + 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 === 'commit') { - const defaultCommitCodes = { 'commit-definite': '40001', uncertain: '08007' }; - const defaultCode = - options.transactionMode === 'uncertain' || options.transactionMode === 'commit-definite' - ? defaultCommitCodes[options.transactionMode] - : undefined; - const code = options.commitFailureCode ?? defaultCode; - if (code !== undefined) { - return yield* new SqlError({ reason: new ConnectionError({ cause: { code } }) }); - } - if (options.commit !== undefined) { - return yield* options.commit; - } + } + if (text === 'begin') { + transactionCount += 1; + if (options.transactionMode === 'definite-failure') { + return yield* new SqlError({ + reason: new ConnectionError({ cause: new Error('transaction unavailable') }), + }); } - if (text.includes('current_setting')) { - return [{ legal_entity_id: installedLegalEntityId, tenant_id: installedTenantId }]; + } + if (text === 'commit') { + const defaultCommitCodes = { 'commit-definite': '40001', uncertain: '08007' }; + const defaultCode = + options.transactionMode === 'uncertain' || options.transactionMode === 'commit-definite' + ? defaultCommitCodes[options.transactionMode] + : undefined; + const code = options.commitFailureCode ?? defaultCode; + if (code !== undefined) { + return yield* new SqlError({ reason: new ConnectionError({ cause: { code } }) }); } - if (text.startsWith('select')) { - return [{ authBindingId: principal.authBindingId }]; + if (options.commit !== undefined) { + return yield* options.commit; } - return []; - }); - const database = { executor: makeTestDatabase(query) }; + } + 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) => { @@ -418,13 +411,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', @@ -433,80 +426,92 @@ 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 }; -}; - -void 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), - ); }); -void 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); } }); -void 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); + expect(yield* Schema.encodeEffect(ActionTransactionError)(transactionFailure)).toEqual({ + _tag: 'ActionTransactionError', + code: transactionFailure.code, + reason: transactionFailure.reason, + }); + expect( + yield* Schema.encodeEffect(ActionInvocationPersistenceError)(persistenceFailure), + ).toEqual({ + _tag: 'ActionInvocationPersistenceError', + code: persistenceFailure.code, + reason: persistenceFailure.reason, + }); + }), +); -void 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( @@ -516,8 +521,8 @@ void test('repository cause readers reject foreign objects carrying the former c }), { [formerCauseProperty]: cause }, ); - assert.equal(getActionTransactionFailureCause(transactionFailure), undefined); - assert.equal(getActionInvocationPersistenceFailureCause(persistenceFailure), undefined); + expect(getActionTransactionFailureCause(transactionFailure)).toBe(undefined); + expect(getActionInvocationPersistenceFailureCause(persistenceFailure)).toBe(undefined); }); const registration = () => @@ -545,143 +550,144 @@ 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 }; + }), ); -void 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, + }); + }), +); -void 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 }), + ); + }), +); -void 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({ @@ -697,15 +703,13 @@ void test('uses a resolver-branded recovery only for the exact support-stop Acti 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', @@ -718,15 +722,13 @@ void test('uses a resolver-branded recovery only for the exact support-stop Acti 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: { @@ -741,16 +743,15 @@ void test('uses a resolver-branded recovery only for the exact support-stop Acti transport: transport('support-recovery-denied'), }) .pipe(providePrincipalManagementRepository), - ), - ); - assert.ok(Predicate.isTagged(denied, '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: { @@ -764,345 +765,355 @@ void test('uses a resolver-branded recovery only for the exact support-stop Acti transport: transport('support-recovery-wrong-checkpoint'), }) .pipe(providePrincipalManagementRepository), - ), - ); - assert.ok(Predicate.isTagged(wrongCheckpoint, '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.ok(Predicate.isTagged(wrongAction, 'ActionTrustedContextValidationError')); -}); + ); + expect(Predicate.isTagged(wrongAction, 'ActionTrustedContextValidationError')).toBe(true); + }), +); -void 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.ok(Predicate.isTagged(failure, '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, - }); - }, - ); -}); - -void 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.ok(Predicate.isTagged(unavailableFailure, 'ModuleStateCheckUnavailableError')); - assert.equal(unavailable.counts().createCount, 0); + ); + expect(Predicate.isTagged(unavailableFailure, 'ModuleStateCheckUnavailableError')).toBe(true); - const locked = makeHarness({ lockedModuleState: 'denied' }); - const lockedFailure = await runEffectTestPromise( - Effect.flip( + expect(unavailable.counts().createCount).toBe(0); + + 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.ok(Predicate.isTagged(lockedFailure, '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, + }); + }), +); -void 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); + }), +); -void 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.ok(Predicate.isTagged(failure, 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); + }), +); -void 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, - }); - }, - ); -}); - -void 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, @@ -1112,10 +1123,8 @@ void test('canonicalizes every resolved tenant permission target for hash and ev targetResourceId: 'forged-one', targetResourceType: 'first', }, - }), - ); - await runEffectTestPromise( - second.runtime.runAction({ + }); + yield* second.runtime.runAction({ payload: undefined, principal, registration: action, @@ -1125,112 +1134,111 @@ void test('canonicalizes every resolved tenant permission target for hash and ev 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); + }), +); -void 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; - }, - policyKey: 'party.registry.counterparty-create.v1', +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' }, + 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.ok(Predicate.isTagged(failure, '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.ok(Predicate.isTagged(unavailableFailure, '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, @@ -1238,79 +1246,80 @@ void test('authorizes Counterparty creation against the trusted Legal Entity bef ...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); + }), +); -void 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; - }, - policyKey: 'party.registry.role-end.v1', +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' }, + 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, @@ -1322,598 +1331,618 @@ void test('authorizes the resolved Resource target before Policy, transaction, a targetResourceType: 'forged-type', }, }), - ), - ); + ); - assert.ok(Predicate.isTagged(failure, '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.ok(Predicate.isTagged(unavailableFailure, 'ActionPermissionCheckError')); - assert.equal(unavailable.rejections.length, 0); - assert.equal(unavailable.counts().transactionCount, 0); -}); + ); + expect(Predicate.isTagged(unavailableFailure, 'ActionPermissionCheckError')).toBe(true); -void 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; - }, - policyKey: 'global.unreachable-after-permission-denial.v1', + 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' }, + 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.ok(Predicate.isTagged(failure, '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'), + }, + ]); + }), +); -void 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.ok(Predicate.isTagged(failure, '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); + + 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', + ]); + }), +); -void 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( +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.ok(Predicate.isTagged(failure, '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); -void 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', - }, - ]); -}); + }); -void 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.ok(Predicate.isTagged(denial, '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); -}); + ); -void 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.ok(Predicate.isTagged(error, '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); -void 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 }); + ); + }), +); + +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 = await runEffectTestPromise( - Effect.flip( + const error = yield* Effect.flip( harness.runtime.runAction({ payload: undefined, principal, registration: action, transport: transport('policy-finalization-failure'), }), - ), - ); + ); - assert.ok(Predicate.isTagged(error, '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); + }), +); -void 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); + }), +); -void 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); + }), +); -void 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: { @@ -1925,59 +1954,64 @@ void test('rejects structural payloads, trusted context, and missing idempotency registration: registration(), transport: transport('forged-system'), }), - ), - ); + ); + + expect(Predicate.isTagged(invalidPayload, 'ActionPayloadValidationError')).toBe(true); - assert.ok(Predicate.isTagged(invalidPayload, 'ActionPayloadValidationError')); - assert.ok(Predicate.isTagged(invalidPrincipal, 'ActionTrustedContextValidationError')); - assert.ok(Predicate.isTagged(missingKey, 'ActionIdempotencyKeyRequired')); - assert.ok(Predicate.isTagged(forgedSystemPrincipal, 'ActionTrustedContextValidationError')); - assert.equal(harness.counts().createCount, 0); -}); + expect(Predicate.isTagged(invalidPrincipal, 'ActionTrustedContextValidationError')).toBe(true); + expect(Predicate.isTagged(missingKey, 'ActionIdempotencyKeyRequired')).toBe(true); -void 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(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: {}, @@ -1988,416 +2022,432 @@ void test('preserves declared domain rejections and rolls back collected evidenc }); 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.ok(Predicate.isTagged(error, 'DomainRejected')); - assert.equal(error.reason, 'counter_locked'); - assert.equal(policyEvaluations, 1); - assert.equal(harness.flushed.length, 0); -}); + expect(Predicate.isTagged(error, 'DomainRejected')).toBe(true); -void 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.ok(Predicate.isTagged(defect, 'ActionHandlerExecutionError')); - assert.equal(defect.reason.includes('secret'), false); - assert.ok(Predicate.isTagged(resultError, 'ActionResultValidationError')); - assert.equal(defectHarness.flushed.length, 0); - assert.equal(resultHarness.flushed.length, 0); -}); + expect(Predicate.isTagged(defect, 'ActionHandlerExecutionError')).toBe(true); -void 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.ok(Predicate.isTagged(error, 'ActionHandlerExecutionError')); - assert.equal(error.reason.includes('secret'), false); - assert.equal(harness.flushed.length, 0); -}); + expect(Predicate.isTagged(error, 'ActionHandlerExecutionError')).toBe(true); -void 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.ok(Predicate.isTagged(committedError, 'ActionAlreadyCommitted')); - assert.equal(committed.counts().transactionCount, 0); - assert.equal(committed.permissionCounts().permissionCheckCount, 0); - assert.ok(Predicate.isTagged(conflictError, 'ActionRequestHashConflict')); - assert.equal(conflict.counts().transactionCount, 0); - assert.equal(conflict.permissionCounts().permissionCheckCount, 0); - assert.ok(Predicate.isTagged(definiteError, 'ActionTransactionError')); - assert.ok(Predicate.isTagged(definiteCommitError, 'ActionTransactionError')); - assert.ok(Predicate.isTagged(uncertainError, 'ActionCommitIndeterminate')); - assert.equal(uncertain.flushed.length, 1); - assert.equal(acknowledgementErrors.length, acknowledgementFailureCodes.length); - for (const error of acknowledgementErrors) { - assert.ok(Predicate.isTagged(error, 'ActionCommitIndeterminate')); - } -}); + expect(Predicate.isTagged(committedError, 'ActionAlreadyCommitted')).toBe(true); -void 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); -void 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.ok(Predicate.isTagged(committedResolution, 'ActionAlreadyCommitted')); - assert.ok(Predicate.isTagged(unavailableResolution, '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); + } + }), +); -void 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(openResolution).toEqual({ + _tag: 'ActionCommitOpen', + 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.ok(Predicate.isTagged(error, 'ActionInvocationStateError')); - assert.equal(terminal.counts().transitionCount, 0); - assert.equal(terminal.counts().transactionCount, 0); -}); + expect(Predicate.isTagged(error, 'ActionInvocationStateError')).toBe(true); -void 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, @@ -2405,13 +2455,13 @@ void test('uses one runtime contract for Shell/Core and MicroVertical-shaped reg ...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 }); + }), +); -void 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 7632d1247..a540a4bd5 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,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 '@app/effect-rstest'; import { Effect, Schema, Predicate } from 'effect'; import { defineAction } from '../../src/actions/definition.ts'; import { ACTION_RUNTIME_STAGES } from '../../src/actions/runtime.ts'; @@ -39,23 +36,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 = { @@ -65,113 +61,124 @@ const request = { transport: { correlationId: 'action-harness-test', idempotencyKey: 'increment-once' }, } as const; -void 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.ok(Predicate.isTagged(replay, '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); -}); - -void 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.ok(Predicate.isTagged(denied, 'ActionPermissionDenied')); - assert.equal(snapshot.invocations.length, 1); - assert.equal(snapshot.invocations[0]?.status, 'rejected'); - assert.equal(snapshot.permissionDenials.length, 1); - assert.equal(snapshot.transactionCount, 0); - assert.equal(snapshot.stages.includes('handler_executed'), false); -}); - -void 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.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); -}); - -void 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.ok(Predicate.isTagged(failure, 'ActionIdempotencyKeyRequired')); - assert.equal(harness.snapshot().invocations.length, 0); -}); + expect(Predicate.isTagged(failure, 'ActionIdempotencyKeyRequired')).toBe(true); + + expect(harness.snapshot().invocations.length).toBe(0); + }), +); 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..f12cc1db8 100644 --- a/app/packages/core-runtime/tests/unit/application-composition.test.ts +++ b/app/packages/core-runtime/tests/unit/application-composition.test.ts @@ -1,6 +1,4 @@ -import { runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/effect-rstest'; import { Effect, Schema } from 'effect'; import { canonicalizeApplicationComposition, @@ -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,16 @@ 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' }); + expect(yield* Schema.encodeEffect(ApplicationCompositionValidationError)(error)).toEqual({ + _tag: 'ApplicationCompositionValidationError', + code: 'application_composition_invalid', + reason: 'Invalid candidate', + }); + }), +); const addModuleCopy = ( input: Candidate, @@ -140,232 +146,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..879a27096 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 '@app/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 e4413c5ff..e2dd7a9a5 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,8 +1,7 @@ -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 { expect, it } from '@app/effect-rstest'; + +/* oxlint-disable sonarjs/no-undefined-assignment -- Existing compatibility boundary; expires: 2026-12-31. */ + import { Effect, Schema, Predicate } from 'effect'; import { defineAction } from '../../src/actions/definition.ts'; import { ActionAlreadyCommitted } from '../../src/actions/errors.ts'; @@ -17,161 +16,173 @@ const principal = { tenantId: '30000000-0000-4000-8000-000000000001', } as const; -void 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.ok(Predicate.isTagged(outcome, '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); + }), +); -void 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); -}); - -void 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); + expect(yield* decoded.pipe(Schema.encodeEffect(ActionAlreadyCommitted))).toEqual(encoded); + 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.ok(Predicate.isTagged(uncertain, '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.ok(Predicate.isTagged(outcome, '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 df3d0570a..298d856c6 100644 --- a/app/packages/core-runtime/tests/unit/config.test.ts +++ b/app/packages/core-runtime/tests/unit/config.test.ts @@ -1,7 +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 { expect, it } from '@app/effect-rstest'; + import { Effect, Predicate } from 'effect'; import { acquirePoolResource } from '../../src/db/client.ts'; import { @@ -11,127 +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.ok(Predicate.isTagged(missing, 'DatabaseConfigError')); - assert.ok(Predicate.isTagged(malformed, '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.ok(Predicate.isTagged(missing, 'DatabaseConfigError')); - assert.ok(Predicate.isTagged(identical, 'DatabaseConfigError')); - assert.ok(Predicate.isTagged(queryParameterCollision, 'DatabaseConfigError')); - assert.ok(Predicate.isTagged(superuserCompatible, '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(() => ({ - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements pg Pool.end's foreign Promise API. - 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 13d4bb107..40fe9b2ba 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 '@app/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( @@ -108,13 +101,12 @@ 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']); }), ); -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: string[] = []; const service = makeContextAccess( @@ -135,17 +127,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: string[] = []; const service = makeContextAccess( @@ -166,31 +157,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', @@ -198,8 +186,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( @@ -213,11 +200,8 @@ 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, @@ -227,63 +211,67 @@ effectTest( ], 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 f4e060cad..cd2fdaa9c 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 '@app/effect-rstest'; import { EffectDrizzleQueryError } from 'drizzle-orm/effect-core'; 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.ok( + expect( Option.isSome(connectionFailure) && Predicate.isTagged(connectionFailure.value, 'DatabaseCommitAcknowledgementAmbiguous'), - ); - assert.ok( + ).toBe(true); + expect( Option.isSome(administrativeShutdown) && Predicate.isTagged(administrativeShutdown.value, 'DatabaseCommitAcknowledgementAmbiguous'), - ); - assert.ok( + ).toBe(true); + expect( Option.isSome(serializationFailure) && Predicate.isTagged(serializationFailure.value, 'DatabaseTransactionFailure'), - ); - assert.equal(isDatabaseCommitAcknowledgementAmbiguous({ code: '40001' }), false); - assert.equal(isDatabaseCommitAcknowledgementAmbiguous({ code: '57014' }), false); + ).toBe(true); + expect(isDatabaseCommitAcknowledgementAmbiguous({ code: '40001' })).toBe(false); + expect(isDatabaseCommitAcknowledgementAmbiguous({ code: '57014' })).toBe(false); }); -void test('classifies the exact commit-acknowledgement socket vocabulary', () => { +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..9eff1c3cd 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 '@app/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/legal-entity-context.test.ts b/app/packages/core-runtime/tests/unit/legal-entity-context.test.ts index 3938c4130..84b481216 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,8 +1,8 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { Effect, flow, Predicate } from 'effect'; +import { expect, it } from '@app/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 { LegalEntityContextRecord } from '../../src/auth/legal-entity-context.ts'; import { classifyActiveLegalEntities, @@ -12,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.', @@ -25,14 +20,13 @@ const activeRecord: LegalEntityContextRecord = { tenantId, }; -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, @@ -61,29 +55,26 @@ 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.ok( + ).toEqual({ legalEntityId: activeRecord.legalEntityId, legalName: activeRecord.legalName }); + expect( Predicate.isTagged( yield* Effect.flip( classifySelectedLegalEntity( @@ -94,8 +85,8 @@ effectTest( ), 'LegalEntityContextMissingError', ), - ); - assert.ok( + ).toBe(true); + expect( Predicate.isTagged( yield* Effect.flip( classifySelectedLegalEntity( @@ -106,14 +97,13 @@ effectTest( ), '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.ok( + expect( Predicate.isTagged( yield* Effect.flip( classifyActiveLegalEntities( @@ -123,31 +113,30 @@ effectTest( ), 'LegalEntityContextInvalidError', ), - ); - assert.ok( + ).toBe(true); + expect( Predicate.isTagged( yield* Effect.flip( classifyActiveLegalEntities([{ ...activeRecord, legalName: '' }], tenantId), ), 'LegalEntityContextInvalidError', ), - ); - assert.ok( + ).toBe(true); + expect( Predicate.isTagged( yield* Effect.flip( 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') }), @@ -156,7 +145,9 @@ effectTest( ), }); const error = yield* Effect.flip(context.listActiveForTenant(tenantId)); - assert.ok(Predicate.isTagged(error, '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 5b406ecb9..a6621e05b 100644 --- a/app/packages/core-runtime/tests/unit/module-catalog.test.ts +++ b/app/packages/core-runtime/tests/unit/module-catalog.test.ts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/effect-rstest'; import { buildInstalledModuleCatalog, resolveInstalledModuleCatalog, @@ -50,7 +49,7 @@ const contract = ( schemaVersion: '2', }); -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'), @@ -62,19 +61,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: { @@ -95,10 +93,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: { @@ -113,15 +111,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', [ @@ -133,10 +131,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', [ @@ -177,19 +175,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'), @@ -200,8 +198,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'), @@ -212,21 +210,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'), @@ -247,8 +245,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' }, @@ -257,7 +255,7 @@ void test('resolves healthy, incompatible, and unreachable deployments independe ]); }); -void test('excludes every contradictory claimant while preserving unrelated deployments', () => { +it('excludes every contradictory claimant while preserving unrelated deployments', () => { const catalog = resolveInstalledModuleCatalog([ { contract: contract('documents-center', 'shared.module'), @@ -276,15 +274,15 @@ void test('excludes every contradictory claimant while preserving unrelated depl }, ]); - assert.deepEqual(catalog.moduleIds, ['reporting.center']); - assert.deepEqual(catalog.deploymentStatuses, [ + expect(catalog.moduleIds).toEqual(['reporting.center']); + expect(catalog.deploymentStatuses).toEqual([ { appId: 'documents-center', reason: 'incompatible', status: 'unavailable' }, { appId: 'property-registry', reason: 'incompatible', status: 'unavailable' }, { appId: 'reporting-center', moduleId: 'reporting.center', status: 'available' }, ]); }); -void test('rejects duplicate deployment identities from tolerant candidate promotion', () => { +it('rejects duplicate deployment identities from tolerant candidate promotion', () => { const catalog = resolveInstalledModuleCatalog([ { contract: contract('property-registry', 'property.registry'), @@ -303,14 +301,14 @@ void test('rejects duplicate deployment identities from tolerant candidate promo }, ]); - 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', reason: 'incompatible', status: 'unavailable' }, ]); }); -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'), @@ -326,8 +324,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..1a263df83 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 '@app/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 262834474..926a83892 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 '@app/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.ok(Predicate.isTagged(failure, '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.ok(Predicate.isTagged(sameModuleFailure, '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.ok(Predicate.isTagged(failure, '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.ok(Predicate.isTagged(failure, '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.ok(Predicate.isTagged(failure, '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.ok(Predicate.isTagged(failure, '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..e898c9918 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 '@app/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..46e070c1c 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 '@app/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..c9ea91a72 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 '@app/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-health.test.ts b/app/packages/core-runtime/tests/unit/outbox-health.test.ts index 2911194d8..ef8bd2b3c 100644 --- a/app/packages/core-runtime/tests/unit/outbox-health.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-health.test.ts @@ -1,29 +1,25 @@ -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, Predicate } from 'effect'; +import { expect, it } from '@app/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'; -void 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.scoped( + 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'); + }), + ), +); -void test('readiness starts false, follows successful/failing cycles, expires, and closes on shutdown', async () => { - let now = 1000; - return runEffectTestPromise( - Effect.scoped( +it.live( + 'readiness starts false, follows successful/failing cycles, expires, and closes on shutdown', + () => { + let now = 1000; + return Effect.scoped( Effect.gen(function* healthLifecycle() { const services = yield* Layer.build(FetchHttpClient.layer); const client = yield* Effect.provide(HttpClient.HttpClient, services); @@ -34,58 +30,58 @@ void test('readiness starts false, follows successful/failing cycles, expires, a 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); + 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; - assert.equal(readyResponse.status, 200); - assert.deepEqual(yield* readyResponse.json, { ready: true }); + expect(readyResponse.status).toBe(200); + expect(yield* readyResponse.json).toEqual({ ready: true }); now = 1101; - assert.equal((yield* ready).status, 503); + expect((yield* ready).status).toBe(503); yield* health.cycleSucceeded; yield* health.cycleFailed; - assert.equal((yield* ready).status, 503); + expect((yield* ready).status).toBe(503); yield* health.cycleSucceeded; yield* health.shuttingDown; - assert.equal((yield* ready).status, 503); + expect((yield* ready).status).toBe(503); }), - ), - ); -}); + ); + }, +); -void 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); }), - )); +); -void 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.ok(Predicate.isTagged(result.failure, 'ServeError')); - }), - ), - )); +it.live('a health port already in use produces a typed server startup failure', () => + Effect.scoped( + 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); + }), + ), +); -void 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 +98,7 @@ void test('invalid configured health ports fail startup with a typed configurati }), ), ); - assert.ok(Result.isFailure(result)); - assert.ok(Predicate.isTagged(result.failure, '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..5a04682ac 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 '@app/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..e96cfc122 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 '@app/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..3eb7b041a 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 '@app/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..25633515b 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,109 @@ -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 '@app/effect-rstest'; import { v1 } from '@authzed/authzed-node'; -import { Cause, Effect, flow, Schema } from 'effect'; +import { Cause, Effect, Predicate, Schema } from 'effect'; 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.live('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 failure = yield* Effect.flip( + client.checkPermission(v1.CheckPermissionRequest.create({})), + ); + 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 3d863f477..26dfb63dc 100644 --- a/app/packages/core-runtime/tests/unit/pool-configuration.test.ts +++ b/app/packages/core-runtime/tests/unit/pool-configuration.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 '@app/effect-rstest'; import { Effect, Redacted, Predicate } from 'effect'; import { DEFAULT_DATABASE_POOL_DEADLINES, @@ -9,82 +7,64 @@ import { const runtimeUrl = 'postgresql://runtime:secret@localhost:5432/ontos'; -void 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`); + }), ); -void 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); + }), ); -void 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.ok(Predicate.isTagged(error, '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' }, ), ); -void 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.ok(Predicate.isTagged(error, '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 d7d182e56..1a406a4c5 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, Predicate } from 'effect'; +import { expect, it } from '@app/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.ok(Predicate.isTagged(principalError, 'IdentityTargetInvalidError')); - assert.ok(Predicate.isTagged(bindingError, '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.ok(Predicate.isTagged(conflictError, 'IdentityLifecycleConflictError')); - assert.ok(Predicate.isTagged(terminalError, 'IdentityLifecycleConflictError')); - assert.ok(Predicate.isTagged(reasonError, '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.ok(Predicate.isTagged(error, '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.ok(Predicate.isTagged(error, '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.ok(Predicate.isTagged(error, '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 1a9dc72fb..f4df12e85 100644 --- a/app/packages/core-runtime/tests/unit/principal-resolver.test.ts +++ b/app/packages/core-runtime/tests/unit/principal-resolver.test.ts @@ -1,8 +1,8 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import { DateTime, Effect, flow, Predicate } from 'effect'; +import { expect, it } from '@app/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 { PrincipalResolutionRecord } from '../../src/auth/principal-resolver.ts'; import { classifyApiKeyPrincipal, @@ -13,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')), @@ -33,9 +26,7 @@ const activeRecord: PrincipalResolutionRecord = { tenantName: 'Zeta tenant', tenantStatus: 'active', }; - -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, @@ -77,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', @@ -107,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, @@ -127,7 +114,7 @@ effectTest( }, ]); - assert.deepEqual(result, { + expect(result).toEqual({ authBindingId: 'binding-1', displayName: 'Tie winner', principalId: 'principal-0', @@ -136,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, @@ -146,92 +131,88 @@ 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.ok( + 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.ok( + expect( Predicate.isTagged( yield* Effect.flip(classifyDefaultPrincipal([record])), 'PrincipalInactiveError', ), - ); - assert.ok( + ).toBe(true); + expect( Predicate.isTagged( yield* Effect.flip(classifySelectedPrincipal([record], record.tenantId)), 'PrincipalInactiveError', ), - ); - assert.ok( + ).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.ok( - Predicate.isTagged( - yield* Effect.flip( - classifyApiKeyPrincipal([activeRecord, { ...activeRecord, tenantId: 't-2' }]), + () => + 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); + }), ), - 'PrincipalBindingAmbiguousError', - ), - ); - }), + ); + 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.ok( + expect( Predicate.isTagged( yield* Effect.flip(classifyAvailableTenants([])), 'PrincipalBindingMissingError', ), - ); - assert.ok( + ).toBe(true); + expect( Predicate.isTagged( yield* Effect.flip( classifyAvailableTenants([{ ...activeRecord, bindingStatus: 'revoked' }]), ), 'PrincipalBindingInactiveError', ), - ); - assert.ok( + ).toBe(true); + expect( Predicate.isTagged( yield* Effect.flip( classifyAvailableTenants([ @@ -243,24 +224,24 @@ effectTest( ), 'PrincipalBindingInactiveError', ), - ); - assert.ok( + ).toBe(true); + expect( Predicate.isTagged( yield* Effect.flip( classifyAvailableTenants([{ ...activeRecord, principalStatus: 'disabled' }]), ), 'PrincipalInactiveError', ), - ); - assert.ok( + ).toBe(true); + expect( Predicate.isTagged( yield* Effect.flip( classifyAvailableTenants([{ ...activeRecord, tenantStatus: 'suspended' }]), ), 'TenantInactiveError', ), - ); - assert.ok( + ).toBe(true); + expect( Predicate.isTagged( yield* Effect.flip( classifyAvailableTenants([ @@ -270,16 +251,14 @@ effectTest( ), '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') }), @@ -288,7 +267,9 @@ effectTest( ), }).listAvailableTenants('subject'), ); - assert.ok(Predicate.isTagged(error, '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..d42522157 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 '@app/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 105951fc5..52a126268 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 '@app/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.ok(Predicate.isTagged(error, '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], + ); + const error = yield* Effect.flip( + harness.runtime.runRead({ + input: {}, + principal: scope, + registration: governed, + transport: { correlationId: scope.correlationId }, + }), ); - assert.deepEqual( - await runEffectTestPromise( - harness.runtime.runRead({ + 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.ok(Predicate.isTagged(error, '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,85 +346,82 @@ void test('preserves typed result-validation failure across transaction rollback registration: invalidRegistration, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.ok(Predicate.isTagged(error, '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.ok(Predicate.isTagged(error, '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.ok(Predicate.isTagged(error, 'OperationContextUnavailable')); - assert.equal(handlerCalls, 0); - assert.equal(harness.evidence(), 0); -}); - -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 = defineRead( - { - ...registration().descriptor, - legalEntityScope: 'required', - permissionTarget: 'legal_entity', - }, - () => { - handlerCalls += 1; - return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); - }, - () => Effect.succeed({}), - () => ({ kind: 'legal_entity', permission: 'read_counterparty' }), - ); - const error = await runEffectTestPromise( - Effect.flip( + ); + expect(Predicate.isTagged(error, 'OperationContextUnavailable')).toBe(true); + expect(handlerCalls).toBe(0); + expect(harness.evidence()).toBe(0); + }), +); +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 = defineRead( + { + ...registration().descriptor, + legalEntityScope: 'required', + permissionTarget: 'legal_entity', + }, + () => { + handlerCalls += 1; + return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); + }, + () => Effect.succeed({}), + () => ({ kind: 'legal_entity', permission: 'read_counterparty' }), + ); + const error = yield* Effect.flip( harness.runtime.runRead({ input: {}, principal: { @@ -445,36 +435,35 @@ void test('persists sanitized permission denial and never invokes the private ha registration: deniedRegistration, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.ok(Predicate.isTagged(error, 'ReadPermissionDenied')); - assert.deepEqual(legalEntityPermissions, ['read_counterparty']); - assert.equal(handlerCalls, 0); - assert.equal(harness.evidence(), 1); -}); - -void 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 = defineRead( - { - ...registration().descriptor, - legalEntityScope: 'optional', - permissionTarget: 'legal_entity', - }, - () => { - handlerCalls += 1; - return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); - }, - () => Effect.succeed({}), - () => ({ kind: 'legal_entity', permission: 'read_counterparty' }), - ); - 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 = defineRead( + { + ...registration().descriptor, + legalEntityScope: 'optional', + permissionTarget: 'legal_entity', + }, + () => { + handlerCalls += 1; + return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); + }, + () => Effect.succeed({}), + () => ({ kind: 'legal_entity', permission: 'read_counterparty' }), + ); + const error = yield* Effect.flip( harness.runtime.runRead({ input: {}, principal: { @@ -488,228 +477,223 @@ void test('fails closed when explicit Counterparty read authority is unavailable registration: counterpartyRead, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.ok(Predicate.isTagged(error, 'ReadPermissionUnavailable')); - assert.equal(handlerCalls, 0); - assert.equal(harness.evidence(), 0); -}); - -void 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); -}); - -void 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.ok(Predicate.isTagged(unavailable, '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.ok(Predicate.isTagged(denial, '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); -void 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, @@ -721,156 +705,152 @@ void test('rejects generic tenant access as an alternative permission target', a registration: invalid, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.ok(Predicate.isTagged(failure, 'ReadHandlerExecutionError')); - assert.equal(handlerCalls, 0); -}); - -void test('never treats missing Legal Entity scope as an allowed alternative', async () => { - let handlerCalls = 0; - const composed = defineRead( - { - ...registration().descriptor, - permissionTarget: 'tenant', - }, - () => { - handlerCalls += 1; - return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); - }, - () => Effect.succeed({}), - () => ({ - kind: 'any_of', - targets: [ - { kind: 'tenant', permission: 'manage_party_identity' }, - { kind: 'module', moduleId: 'party.registry' }, - ], - }), - ); - const failure = await runEffectTestPromise( - Effect.flip( - makeHarness({ tenantPermissionDecision: 'denied' }).runtime.runRead({ + ); + expect(Predicate.isTagged(failure, 'ReadHandlerExecutionError')).toBe(true); + expect(handlerCalls).toBe(0); + }), +); +it.effect('never treats missing Legal Entity scope as an allowed alternative', () => + Effect.gen(function* migratedTest16() { + let handlerCalls = 0; + const composed = defineRead( + { + ...registration().descriptor, + permissionTarget: 'tenant', + }, + () => { + handlerCalls += 1; + return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); + }, + () => Effect.succeed({}), + () => ({ + kind: 'any_of', + targets: [ + { kind: 'tenant', permission: 'manage_party_identity' }, + { kind: 'module', moduleId: 'party.registry' }, + ], + }), + ); + const failure = yield* Effect.flip( + (yield* makeHarness({ tenantPermissionDecision: 'denied' })).runtime.runRead({ input: {}, principal: scope, registration: composed, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.ok(Predicate.isTagged(failure, 'ReadPermissionUnavailable')); - assert.equal(handlerCalls, 0); -}); - -void test('rejects alternative targets whenever result authorization cannot preserve them', async () => { - let handlerCalls = 0; - const search = defineRead( - { - ...registration().descriptor, - permissionTarget: 'tenant', - }, - () => { - handlerCalls += 1; - return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); - }, - () => Effect.succeed({}), - () => ({ - kind: 'any_of', - targets: [ - { kind: 'tenant', permission: 'read_party_identity' }, - { kind: 'module', moduleId: 'party.registry' }, - ], - }), - () => [], - ); - const failure = await runEffectTestPromise( - Effect.flip( - makeHarness({ tenantPermissionDecision: 'allowed' }).runtime.runRead({ + ); + expect(Predicate.isTagged(failure, 'ReadPermissionUnavailable')).toBe(true); + expect(handlerCalls).toBe(0); + }), +); +it.effect('rejects alternative targets whenever result authorization cannot preserve them', () => + Effect.gen(function* migratedTest17() { + let handlerCalls = 0; + const search = defineRead( + { + ...registration().descriptor, + permissionTarget: 'tenant', + }, + () => { + handlerCalls += 1; + return Effect.succeed({ evidence: { resultCount: 0 }, result: [] }); + }, + () => Effect.succeed({}), + () => ({ + kind: 'any_of', + targets: [ + { kind: 'tenant', permission: 'read_party_identity' }, + { kind: 'module', moduleId: 'party.registry' }, + ], + }), + () => [], + ); + const failure = yield* Effect.flip( + (yield* makeHarness({ tenantPermissionDecision: 'allowed' })).runtime.runRead({ input: {}, principal: scope, registration: search, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.ok(Predicate.isTagged(failure, 'ReadHandlerExecutionError')); - assert.equal(handlerCalls, 0); -}); - -void test('rejects handler-controlled hashes in metadata-only evidence', async () => { - const harness = makeHarness(); - const unboundedEvidence = defineRead( - registration().descriptor, - () => Effect.succeed({ evidence: { queryHash: 'raw query text', resultCount: 1 }, result: [] }), - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }), - ); - const error = await runEffectTestPromise( - Effect.flip( + ); + expect(Predicate.isTagged(failure, 'ReadHandlerExecutionError')).toBe(true); + expect(handlerCalls).toBe(0); + }), +); +it.effect('rejects handler-controlled hashes in metadata-only evidence', () => + Effect.gen(function* migratedTest18() { + const harness = yield* makeHarness(); + const unboundedEvidence = defineRead( + registration().descriptor, + () => + Effect.succeed({ evidence: { queryHash: 'raw query text', resultCount: 1 }, result: [] }), + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); + const error = yield* Effect.flip( harness.runtime.runRead({ input: {}, principal: scope, registration: unboundedEvidence, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.ok(Predicate.isTagged(error, 'ReadEvidenceValidationError')); - assert.equal(harness.evidence(), 0); -}); - -void test('persists late definite denial after rolling back the owner transaction', async () => { - const harness = makeHarness(); - const lateDenial = defineRead( - registration().descriptor, - () => - Effect.fail( - new ReadPermissionDenied({ - code: 'read_permission_denied', - reason: 'A late provider target check denied this read', - }), - ), - () => Effect.succeed({}), - () => ({ kind: 'module', moduleId: 'core.shell' }), - ); - const error = await runEffectTestPromise( - Effect.flip( + ); + expect(Predicate.isTagged(error, 'ReadEvidenceValidationError')).toBe(true); + expect(harness.evidence()).toBe(0); + }), +); +it.effect('persists late definite denial after rolling back the owner transaction', () => + Effect.gen(function* migratedTest19() { + const harness = yield* makeHarness(); + const lateDenial = defineRead( + registration().descriptor, + () => + Effect.fail( + new ReadPermissionDenied({ + code: 'read_permission_denied', + reason: 'A late provider target check denied this read', + }), + ), + () => Effect.succeed({}), + () => ({ kind: 'module', moduleId: 'core.shell' }), + ); + const error = yield* Effect.flip( harness.runtime.runRead({ input: {}, principal: scope, registration: lateDenial, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.ok(Predicate.isTagged(error, 'ReadPermissionDenied')); - assert.equal(harness.evidence(), 1); -}); - -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( + ); + expect(Predicate.isTagged(error, 'ReadPermissionDenied')).toBe(true); + expect(harness.evidence()).toBe(1); + }), +); +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: { @@ -883,201 +863,205 @@ void test('does not release generated search candidates denied by result-level a registration: searchRegistration, transport: { correlationId: scope.correlationId }, }), - ), - ); - assert.ok(Predicate.isTagged(error, 'ReadPermissionDenied')); - assert.equal(harness.evidence(), 1); -}); - -void 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); -}); - -void 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.ok(Predicate.isTagged(failure, 'ReadPermissionUnavailable')); -}); - -void 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' }, ); - const expectedTag = expectedTags[index]; - assert.ok(expectedTag !== undefined); - assert.ok(Predicate.isTagged(error, expectedTag)); - assert.doesNotMatch(error.reason, /secret/u); - assert.equal(harness.evidence(), 0); - }), - ); -}); - -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); -}); +); +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('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.ok(Predicate.isTagged(failures[0], 'ReadEvidencePersistenceError')); - assert.equal(failures[1], denied); - assert.ok(Predicate.isTagged(failures[1], '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..52e16bab9 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 '@app/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 d38001d97..65073ddc4 100644 --- a/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts +++ b/app/packages/core-runtime/tests/unit/scoped-transaction.test.ts @@ -1,7 +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 { expect, it } from '@app/effect-rstest'; + import { Effect, Option, Predicate } from 'effect'; import { OperationalScopeTransaction, @@ -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.ok(Predicate.isTagged(error, '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 375fdae48..c60f35f58 100644 --- a/app/packages/core-runtime/tests/unit/search-ingestion.test.ts +++ b/app/packages/core-runtime/tests/unit/search-ingestion.test.ts @@ -1,6 +1,4 @@ -import { runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/effect-rstest'; import { Effect, Predicate } from 'effect'; import { CORE_SEARCH_INGESTION_REGISTRATIONS, @@ -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 = makeCoreSearchQueryRuntime(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.ok(Predicate.isTagged(denied, '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.ok(Predicate.isTagged(failure, '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 d4ddd6630..df560ca15 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 { expect, it } from '@app/effect-rstest'; import { Effect, Schema, Predicate } from 'effect'; +import { TestClock } from 'effect/testing'; import { makeCoreSearchQueryRuntime, 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.ok(Predicate.isTagged(divergent, '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.ok(Predicate.isTagged(failure, '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 = makeCoreSearchQueryRuntime(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 = makeCoreSearchQueryRuntime(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 = makeCoreSearchQueryRuntime(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.ok(Predicate.isTagged(failure, '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 = makeCoreSearchQueryRuntime(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..3cccb558e 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 '@app/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 39eeeae8f..9b09e71c6 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,6 +1,4 @@ -import { runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/effect-rstest'; import { Effect, Schema, Predicate } from 'effect'; import { attestOutboxWorkerHandlerContext } from '../../src/outbox/definition.ts'; import { CoreSearchProjectionUnavailable } from '../../src/search/projection.ts'; @@ -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,16 +80,16 @@ effectTest( (candidate) => Effect.flip(snapshot.read(candidate, () => Effect.succeed('unreachable'))), { concurrency: 'unbounded' }, ); - assert.equal(failures.length, 3); + expect(failures.length).toBe(3); for (const failure of failures) { - assert.ok(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')); + expect(Predicate.isTagged(failure, 'CoreSearchProjectionInvalid')).toBe(true); } - assert.equal(calls, 0); + 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( @@ -123,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)[] = []; @@ -151,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)[] = []; @@ -198,20 +187,20 @@ effectTest( view.forLegalEntity('20000000-0000-4000-8000-000000000002', () => Effect.succeed('no')), ), ); - assert.ok(Predicate.isTagged(invalidScope, '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( @@ -226,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.ok(Predicate.isTagged(failure, '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; @@ -247,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( @@ -257,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( @@ -267,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( @@ -288,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.ok(Predicate.isTagged(failure, '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( @@ -306,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.ok(Predicate.isTagged(first, 'CoreSearchProjectionInvalid')); - assert.ok(Predicate.isTagged(second, '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..f27c09267 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 '@app/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..2b62df33e 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 '@app/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..57489e822 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 '@app/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..41b563aa4 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 '@app/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..9aca37712 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 '@app/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 3930dd590..ade723dde 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 { expect, it } from '@app/effect-rstest'; import { Effect, Option, Schema, Predicate } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; 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.ok(Predicate.isTagged(invalid, 'SystemPrincipalContextInvalidError')); - assert.ok(Predicate.isTagged(wrongKind, 'SystemPrincipalContextDeniedError')); - assert.ok(Predicate.isTagged(inactive, '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.ok(Predicate.isTagged(denied, '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 b5e194d89..6820dbea1 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,7 +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'; +// @effect-diagnostics preferSchemaOverJson:off -- Verifies native JSON serialization of errors and schema AST metadata; expires: 2026-12-31. +import { expect, it } from '@app/effect-rstest'; import { Effect, Schema, Predicate } from 'effect'; import { changeTenantModuleStateAction } from '../../src/modules/actions/change-tenant-module-state.action.ts'; import type { InstalledModuleCatalog, OntosModuleDeploymentContract } from '../../src/index.ts'; @@ -89,51 +87,46 @@ const catalog = ( }); }; -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.ok(Predicate.isTagged(failure, '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.ok(Predicate.isTagged(unsupported, '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.ok(Predicate.isTagged(unchanged, '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', @@ -175,73 +168,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.ok(Predicate.isTagged(unknown, 'TenantModuleStateUnknownModuleError')); - const unsupported = await runEffectTestPromise( - Effect.flip(validateTenantModuleStateTransition(installed, 'property.registry', 'archived')), - ); - assert.ok(Predicate.isTagged(unsupported, '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/effect-rstest/README.md b/app/packages/effect-rstest/README.md index a764729b9..a312aecbf 100644 --- a/app/packages/effect-rstest/README.md +++ b/app/packages/effect-rstest/README.md @@ -3,3 +3,48 @@ Vendored community port of `@effect/vitest` to Rstest by ScriptedAlchemy, commit `79abbf6`. Source: https://github.com/ScriptedAlchemy/effect-rstest. MIT licensed; the original copyright and permission notice are preserved in [LICENSE](./LICENSE). Workspace exports use TypeScript source; local changes adapt imports and repository diagnostics. + +## Usage + +Import `it` and `expect` from `@app/effect-rstest`. Choose the smallest runner that fits: + +- **Plain `it`**: synchronous assertions with no Effect program. +- **`it.effect`**: return an Effect directly. It supplies a test clock and test console; use `TestClock.adjust` from `effect/testing` to advance Effect sleeps deterministically. +- **`it.live`**: return an Effect using live services instead of the test clock/console. Use it when real elapsed time is necessary, such as integration tests coordinating Effect deadlines with external database or network I/O. Ordinary deterministic Effect tests should use `it.effect`. + +```ts +import { expect, it } from '@app/effect-rstest'; +import { Effect } from 'effect'; + +it('adds numbers', () => { + expect(1 + 1).toBe(2); +}); + +it.effect('reads an Effect value', () => + Effect.gen(function* readsValue() { + const value = yield* Effect.succeed(42); + expect(value).toBe(42); + }), +); +``` + +### Scoped cleanup + +Both `it.effect` and `it.live` automatically own and close a per-test scope. Register cleanup with `Effect.acquireRelease` (or `Effect.addFinalizer` for an already-acquired resource); no extra `Effect.scoped` or Promise bridge is needed. Finalizers run on success, failure, and interruption. + +```ts +it.effect('cleans up its fixture', () => + Effect.gen(function* scopedFixture() { + const cache = yield* Effect.acquireRelease( + Effect.sync(() => new Map()), + (resource) => Effect.sync(() => resource.clear()), + ); + cache.set('answer', 42); + expect(cache.get('answer')).toBe(42); + }), +); +``` + +For an existing resource, register its Effect cleanup before using it: `yield* Effect.addFinalizer(() => release(resource))`. Use `acquireRelease` when acquisition and registration must be interruption-safe together. + +**Do not use JavaScript `try/finally` for Effect cleanup.** A failed yielded Effect short-circuits the generator; JavaScript `finally` does not finalize failed yielded Effects. Register an Effect finalizer instead, so cleanup also runs when a yield fails or the test is interrupted. diff --git a/app/packages/effect-rstest/tests/index.test.ts b/app/packages/effect-rstest/tests/index.test.ts index e75076264..60ab36083 100644 --- a/app/packages/effect-rstest/tests/index.test.ts +++ b/app/packages/effect-rstest/tests/index.test.ts @@ -32,15 +32,11 @@ it('throws fails when the thunk does not throw', () => { expect(() => throws(() => {})).toThrow(); }); -const resolvedPromiseThrows: () => Promise = throwsAsync.bind( - undefined, - Promise.resolve.bind(Promise), - undefined, -); +const resolvedPromiseThrows = throwsAsync.bind(undefined, Promise.resolve.bind(Promise), undefined); it.effect('throwsAsync fails when the promise resolves', () => Effect.gen(function* throwsResolved() { - const result = yield* Effect.exit(Effect.tryPromise(resolvedPromiseThrows)); + const result = yield* Effect.exit(Effect.tryPromise(() => resolvedPromiseThrows())); expect(Exit.isFailure(result)).toBe(true); }), ); diff --git a/app/packages/gateway-principal-verifier/package.json b/app/packages/gateway-principal-verifier/package.json index 90a228b15..f670e2454 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": { @@ -19,6 +19,8 @@ }, "devDependencies": { "@effect/tsgo": "0.19.0", - "@types/node": "20.19.43" + "@types/node": "20.19.43", + "@app/effect-rstest": "workspace:*", + "@rstest/core": "0.11.10" } } 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 19a3c32c1..290f4c286 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 '@app/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,67 +25,75 @@ 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('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}`), { @@ -97,71 +101,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), @@ -171,31 +172,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/tests/unit/client-runtime.test.ts b/app/packages/shared-contracts/tests/unit/client-runtime.test.ts index 546852832..43a841cce 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 '@app/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 { Predicate, Result } from 'effect'; +import { Predicate } 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,78 +203,80 @@ 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(outcome).toEqual(problem); + }), +); -test('keeps transport failures in the typed Effect error channel', async () => { - const outcome = await Effect.runPromise( - makeEffectBffClient({ +it.effect('keeps transport failures in the typed Effect error channel', () => + Effect.gen(function* testScenario7() { + 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, controlledTransportFailureFetch), - ), - ); + ); - assert.ok(Result.isFailure(outcome)); - assert.ok(Predicate.isTagged(outcome.failure, 'HttpClientError')); -}); + expect(Predicate.isTagged(outcome, 'HttpClientError')).toBe(true); + }), +); -test('keeps response decoding failures in the typed Effect error channel', async () => { - const outcome = await Effect.runPromise( - makeEffectBffClient({ +it.effect('keeps response decoding failures in the typed Effect error channel', () => + Effect.gen(function* testScenario8() { + 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, invalidResponseFetch), - ), - ); + ); - assert.ok(Result.isFailure(outcome)); - assert.ok(Predicate.isTagged(outcome.failure, 'SchemaError')); -}); + expect(Predicate.isTagged(outcome, 'SchemaError')).toBe(true); + }), +); 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 090519426..5c85749c3 100644 --- a/app/packages/shared-contracts/tests/unit/gateway-context.test.ts +++ b/app/packages/shared-contracts/tests/unit/gateway-context.test.ts @@ -1,8 +1,6 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/effect-rstest'; import { TrustedPrincipalContextSchema } from '@app/core-runtime/actions/principal-context'; -import { Schema } from 'effect'; +import { Effect, Schema } from 'effect'; import { ApiKeyGatewayHeadersSchema, GatewayContextApiGroup, @@ -40,93 +38,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( + Schema.decodeUnknownSync(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( + Schema.decodeUnknownSync(GatewayContextRequestSchema)({ + audience: 'inventory-stock', }), - ), - ); -}); + ).toEqual({ audience: 'inventory-stock' }); + expect( + Schema.decodeUnknownSync(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', @@ -136,17 +134,16 @@ 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, + ]); }); diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index a51e91f3a..486afaa2e 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -9,7 +9,6 @@ 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 @@ -52,6 +51,9 @@ importers: specifier: 8.22.0 version: 8.22.0 devDependencies: + '@app/effect-rstest': + specifier: workspace:* + version: link:packages/effect-rstest '@effect/platform-node': specifier: 4.0.0-beta.107 version: 4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f))(ioredis@5.11.1(supports-color@10.2.2)) @@ -79,6 +81,9 @@ importers: '@oxlint/plugins': specifier: 1.79.0 version: 1.79.0 + '@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) '@types/node': specifier: 20.19.43 version: 20.19.43 @@ -352,6 +357,22 @@ importers: specifier: 1.0.0-rc.5-ab785fc version: 1.0.0-rc.5-ab785fc + packages/effect-rstest: + dependencies: + '@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) + effect: + specifier: 4.0.0-beta.107 + version: 4.0.0-beta.107(patch_hash=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f) + devDependencies: + '@effect/tsgo': + specifier: 0.19.0 + version: 0.19.0 + '@types/node': + specifier: 20.19.43 + version: 20.19.43 + packages/gateway-principal-verifier: dependencies: '@app/core-runtime': @@ -367,25 +388,15 @@ importers: specifier: 6.2.5 version: 6.2.5 devDependencies: + '@app/effect-rstest': + specifier: workspace:* + version: link:../effect-rstest '@effect/tsgo': specifier: 0.19.0 version: 0.19.0 - '@types/node': - specifier: 20.19.43 - version: 20.19.43 - - packages/effect-rstest: - dependencies: '@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) - effect: - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(patch_hash=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f) - devDependencies: - '@effect/tsgo': - specifier: 0.19.0 - version: 0.19.0 '@types/node': specifier: 20.19.43 version: 20.19.43 diff --git a/app/pnpm-workspace.yaml b/app/pnpm-workspace.yaml index 6eba6e313..a33d6f187 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: 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/local-environment-values.test.mts b/app/scripts/local-environment-values.test.mts index 867a83a71..ab5b48df1 100644 --- a/app/scripts/local-environment-values.test.mts +++ b/app/scripts/local-environment-values.test.mts @@ -1,73 +1,81 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/effect-rstest'; +import { Effect } from 'effect'; 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', () => { - const values = localSpiceDbValues( - [ - `SPICEDB_ENDPOINT=${spiceDbEndpoint}`, - `SPICEDB_GRPC_PORT=${spiceDbGrpcPort}`, - `SPICEDB_HTTP_PORT=${spiceDbHttpPort}`, - 'SPICEDB_INSECURE=true', - 'SPICEDB_PRESHARED_KEY=existing-key', - ], - {}, - ); +it.effect('preserves canonical SpiceDB values when no local override is supplied', () => + Effect.sync(() => { + const values = localSpiceDbValues( + [ + `SPICEDB_ENDPOINT=${spiceDbEndpoint}`, + `SPICEDB_GRPC_PORT=${spiceDbGrpcPort}`, + `SPICEDB_HTTP_PORT=${spiceDbHttpPort}`, + 'SPICEDB_INSECURE=true', + 'SPICEDB_PRESHARED_KEY=existing-key', + ], + {}, + ); - assert.deepEqual(values, { - SPICEDB_ENDPOINT: spiceDbEndpoint, - SPICEDB_GRPC_PORT: spiceDbGrpcPort, - SPICEDB_HTTP_PORT: spiceDbHttpPort, - SPICEDB_INSECURE: 'true', - SPICEDB_PRESHARED_KEY: 'existing-key', - }); -}); + expect(values).toEqual({ + SPICEDB_ENDPOINT: spiceDbEndpoint, + SPICEDB_GRPC_PORT: spiceDbGrpcPort, + SPICEDB_HTTP_PORT: spiceDbHttpPort, + SPICEDB_INSECURE: 'true', + SPICEDB_PRESHARED_KEY: 'existing-key', + }); + }), +); -await test('applies explicit local port overrides as one consistent endpoint', () => { - const values = localSpiceDbValues( - ['SPICEDB_ENDPOINT=localhost:50051', 'SPICEDB_GRPC_PORT=50051'], - { grpcPort: spiceDbGrpcPort, httpPort: spiceDbHttpPort }, - ); +it.effect('applies explicit local port overrides as one consistent endpoint', () => + Effect.sync(() => { + 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( - localPublicClientValues([], { - partyRegistryApiBaseUrl: 'http://localhost:4199/party-api', - shellId: 'staff-shell', - shellPort: 3099, +it.effect( + 'derives local public-client URLs from configured Shell identity/port and Party API URL', + () => + Effect.sync(() => { + expect( + localPublicClientValues([], { + partyRegistryApiBaseUrl: 'http://localhost:4199/party-api', + shellId: 'staff-shell', + shellPort: 3099, + }), + ).toEqual({ + ONTOS_PARTY_REGISTRY_API_BASE_URL: 'http://localhost:4199/party-api', + ONTOS_SHELL_GATEWAY_BASE_URL: 'http://localhost:3099/staff-shell-api', + }); }), - { - 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( - localPublicClientValues( - [ - 'ONTOS_SHELL_GATEWAY_BASE_URL=https://gateway.example.test/shell-super-app-api', - 'ONTOS_PARTY_REGISTRY_API_BASE_URL=https://party.example.test/party-registry-api', - ], - { - partyRegistryApiBaseUrl: 'http://localhost:4102/party-registry-api', - shellId: 'shell-super-app', - shellPort: 3020, - }, - ), - { +it.effect('preserves explicitly configured public-client URLs', () => + Effect.sync(() => { + expect( + localPublicClientValues( + [ + 'ONTOS_SHELL_GATEWAY_BASE_URL=https://gateway.example.test/shell-super-app-api', + 'ONTOS_PARTY_REGISTRY_API_BASE_URL=https://party.example.test/party-registry-api', + ], + { + partyRegistryApiBaseUrl: 'http://localhost:4102/party-registry-api', + shellId: 'shell-super-app', + shellPort: 3020, + }, + ), + ).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/scaffolding/tests/module-contract-generator.test.mts b/app/scripts/scaffolding/tests/module-contract-generator.test.mts index 524a80911..36bf525da 100644 --- a/app/scripts/scaffolding/tests/module-contract-generator.test.mts +++ b/app/scripts/scaffolding/tests/module-contract-generator.test.mts @@ -1,17 +1,35 @@ +import { Cause, Effect, Schema } from 'effect'; +import { expect, it } from '@app/effect-rstest'; import { NodeServices } from '@effect/platform-node'; -import assert from 'node:assert/strict'; + import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import test from 'node:test'; -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 } from '../../ultramodern-api-boundary-rules.mts'; -import { generateOntosModuleContract } from '../../generate-ontos-module-contract.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'; @@ -63,462 +81,517 @@ const decodeModuleContract = (source: string) => 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 write = (root: string, relative: string, content: string): Effect.Effect => + Effect.gen(function* scenario1() { + const target = path.join(root, relative); + yield* Effect.promise(() => mkdir(path.dirname(target), { recursive: true })); + yield* Effect.promise(() => writeFile(target, content, 'utf-8')); + }); -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 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', +const createFixture = (): Effect.Effect => + Effect.gen(function* scenario2() { + const root = yield* Effect.promise(() => + mkdtemp(path.join(tmpdir(), 'ontos-module-contract-')), + ); + 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', }, - { - 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', + 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', }, - ], - }), - ); - 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 runEffectTestPromise( - runScaffoldEffect(MODULE_CONTRACT_COMMAND, [VERTICAL_FLAG, vertical, '--module', module], { - workspaceRoot: root, - }).pipe(Effect.provide(NodeServices.layer)), - ); - -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 runEffectTestPromise( - runScaffoldEffect(MODULE_CONTRACT_COMMAND, ['--help'], { - workspaceRoot: missingRoot, - }).pipe(Effect.provide(NodeServices.layer)), - ); - 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', + 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/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', + }), + ); + yield* write( + root, + 'verticals/documents-center/tsconfig.json', + json({ compilerOptions: { composite: true }, include: ['src'], references: [] }), + ); + yield* write( + root, + 'verticals/documents-center/module-federation.config.ts', + 'export default {};\n', + ); + 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', + }, ], - ], - [ - 'policy', - ['--scope', 'microvertical', VERTICAL_FLAG, APP_ID, '--policy', 'property-visible'], - ], - ] as const; - await Promise.all( - commands.map( - async ([command, flags]) => - await assert.rejects( - runEffectTestPromise( - runScaffoldEffect(command, flags, { workspaceRoot: root }).pipe( - Effect.provide(NodeServices.layer), - ), - ), - /requires scaffold:module-contract/u, - ), + }), + ); + 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, + }), + ); + yield* Effect.promise(() => + mkdir(path.join(root, 'node_modules', '@app'), { recursive: true }), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime'), + path.join(root, 'node_modules/@app/core-runtime'), + 'dir', ), ); + yield* Effect.promise(() => + symlink(path.join(appRoot, 'node_modules/effect'), path.join(root, 'node_modules/effect')), + ); + return root; }); -}); -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 withFixture = ( + run: (root: string) => Effect.Effect, +): Effect.Effect => + Effect.gen(function* scenario3() { + const root = yield* createFixture(); + yield* run(root).pipe( + Effect.ensuring(Effect.promise(() => rm(root, { force: true, recursive: true }))), ); - await assert.rejects(scaffold(root, DOCUMENTS_APP_ID, MODULE_ID), /duplicate OntOS module ID/u); }); + +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, + }, + ).pipe(Effect.provide(NodeServices.layer)); }); -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', - ); - 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); +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'); } - 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'), - ); - 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, - ); - 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'), - ), + 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' }, + ); + }), ); - 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', +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), + ); + }), ); - const first = await runEffectTestPromise( - generateOntosModuleContract({ - target: 'dist', - vertical: APP_ID, - workspaceRoot: root, - }).pipe(Effect.provide(NodeServices.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 = 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 = Schema.decodeUnknownSync(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', + ]); + }), ); - 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( - runEffectTestPromise( - generateOntosModuleContract({ + }), +); + +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)), - ), - /module marker does not match/u, - ); - assert.equal(await readFile(first.path, 'utf-8'), firstContent); - await writeFile(packagePath, packageContent, 'utf-8'); - const second = await runEffectTestPromise( - generateOntosModuleContract({ - target: 'dist', - vertical: APP_ID, - workspaceRoot: root, - }).pipe(Effect.provide(NodeServices.layer)), - ); - 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', - ); - 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 runEffectTestPromise( - generateOntosModuleContract({ - target: 'dist', - vertical: DOCUMENTS_APP_ID, - workspaceRoot: root, - }).pipe(Effect.provide(NodeServices.layer)), - ); - 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', - ); - await assert.rejects( - runEffectTestPromise( - generateOntosModuleContract({ + }).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 = 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)), - ), - /exactly one.*slot/u, - ); - }); -}); + }).pipe(Effect.provide(NodeServices.layer)); + expect(yield* Effect.promise(() => readFile(second.path, 'utf-8'))).toBe(firstContent); + expect(second.etag).toBe(first.etag); + const document = 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 = 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); -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 runEffectTestPromise( - generateOntosModuleContract({ - target: 'cloudflare-dist', - vertical: APP_ID, - workspaceRoot: root, - }).pipe(Effect.provide(NodeServices.layer)), - ); - assert.match( - emitted.path, - /verticals\/property-registry\/dist-cloudflare\/public\/\.well-known\/ontos-module-manifest\.json$/u, + 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), + ); + }), ); - await runEffectTestPromise( - checkOntosModuleContracts(root).pipe(Effect.provide(NodeServices.layer)), + }), +); + +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)); + }), ); - }); -}); + }), +); -void test('permits owner-local registration imports but rejects cross-deployment owner imports', () => { +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); }); diff --git a/app/scripts/scaffolding/tests/resource-generator.test.mts b/app/scripts/scaffolding/tests/resource-generator.test.mts index 6d94e7176..7ba604171 100644 --- a/app/scripts/scaffolding/tests/resource-generator.test.mts +++ b/app/scripts/scaffolding/tests/resource-generator.test.mts @@ -1,18 +1,23 @@ -import { runEffectTestPromise } from '../../../packages/core-runtime/src/testing/effect-runtime.ts'; -import { Effect, Fiber, FileSystem, Schema } from 'effect'; +import { Cause, Effect, Fiber, FileSystem, Schema } from 'effect'; +import { afterEach, expect, it, rs } from '@app/effect-rstest'; + import { CodeSmith, GeneratorCore } from '@modern-js/codesmith'; import { applyMutationPlanEffect } from '../shared.mts'; import { NodeServices } from '@effect/platform-node'; -import assert from 'node:assert/strict'; + import { spawnSync } from 'node:child_process'; import { randomUUID } from 'node:crypto'; import { mkdir, mkdtemp, readFile, readdir, rm, symlink, 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 { 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'; @@ -41,96 +46,112 @@ type JsonValue = const json = (value: JsonValue): string => `${JSON.stringify(value, null, 2)}\n`; -const write = async (root: string, relativePath: string, content: string): Promise => { - const target = path.join(root, relativePath); - await mkdir(path.dirname(target), { recursive: true }); - await writeFile(target, content, 'utf-8'); -}; +const write = (root: string, relativePath: string, content: string): Effect.Effect => + Effect.gen(function* scenario1() { + const target = path.join(root, relativePath); + yield* Effect.promise(() => mkdir(path.dirname(target), { recursive: true })); + yield* Effect.promise(() => writeFile(target, content, 'utf-8')); + }); -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.map(async (entry) => { - const entryPath = path.join(directory, entry.name); - if (entry.isDirectory() && entry.name !== 'node_modules') { - await visit(entryPath); - } else if (entry.isFile()) { - snapshot[path.relative(root, entryPath)] = await readFile(entryPath, 'utf-8'); - } - }), +const visitTree = ( + root: string, + snapshot: Record, + directory: string, +): Effect.Effect => + Effect.gen(function* scenario3() { + const entries = yield* Effect.promise(() => readdir(directory, { withFileTypes: true })); + yield* Effect.all( + entries.map( + Effect.fn(function* scenario4(entry) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory() && entry.name !== 'node_modules') { + yield* visitTree(root, snapshot, entryPath); + } else if (entry.isFile()) { + snapshot[path.relative(root, entryPath)] = yield* Effect.promise(() => + readFile(entryPath, 'utf-8'), + ); + } + }), + ), + { concurrency: 'unbounded' }, ); - }; - await visit(root); + }); + +const snapshotTree = Effect.fn(function* scenario2(root: string) { + const snapshot: Record = {}; + + yield* visitTree(root, snapshot, root); return snapshot; -}; +}); -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, - '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', +const createFixture = (): Effect.Effect => + Effect.gen(function* scenario5() { + 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', }, - ], - }), - ); - await write( - root, - 'types/core-runtime.d.ts', - `import type { Schema } from 'effect'; + 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, + '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', + }, + ], + }), + ); + yield* write( + root, + 'types/core-runtime.d.ts', + `import type { Schema } from 'effect'; export interface OntosResourceType { readonly capabilities: { @@ -153,240 +174,312 @@ 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 runEffectTestPromise( - runScaffoldEffect('module-contract', [verticalFlag, verticalName, '--module', moduleId], { - workspaceRoot: root, - }).pipe(Effect.provide(NodeServices.layer)), - ); - return root; -}; + ); + yield* Effect.promise(() => + mkdir(path.join(root, 'node_modules', '@app'), { recursive: true }), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime'), + path.join(root, 'node_modules/@app/core-runtime'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink(path.join(appRoot, 'node_modules/effect'), path.join(root, 'node_modules/effect')), + ); + yield* runScaffoldEffect( + 'module-contract', + [verticalFlag, verticalName, '--module', moduleId], + { + workspaceRoot: root, + }, + ).pipe(Effect.provide(NodeServices.layer)); + 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 = ( + run: (root: string) => Effect.Effect, +): Effect.Effect => + Effect.gen(function* scenario6() { + const root = yield* createFixture(); + yield* run(root).pipe( + Effect.ensuring(Effect.promise(() => rm(root, { force: true, recursive: true }))), + ); + }); -const scaffoldResource = async (root: string, resource = resourceName) => - await runEffectTestPromise( - runScaffoldEffect('resource', [verticalFlag, verticalName, '--resource', resource], { +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)), - ); + }, + ).pipe(Effect.provide(NodeServices.layer)); +}); -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 runEffectTestPromise( - runScaffoldEffect('resource', ['--help'], { +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)), - ); - 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); -}); + }).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, + ); + expect(result.help).toMatch(/lower-kebab-case/u); + }), +); -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'); +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 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, - ); - assert.match(resource, /satisfies OntosResourceType/u); + 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); - 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', - ); + expect(manifest).toMatch( + /import \{ rentalUnitResourceDescriptor \} from '\.\/shared\/resources\/rental-unit\.ts';/u, + ); + expect(manifest).toMatch(/resourceTypes: \[[\s\S]*rentalUnitResourceDescriptor,/u); + const modulePackage = Schema.decodeUnknownSync(packageJsonSchema, { + onExcessProperty: 'preserve', + })(JSON.parse(packageSource)); + expect(modulePackage.exports['./resources/rental-unit']).toBe( + './shared/resources/rental-unit.ts', + ); - 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 generatedModule = Schema.decodeUnknownSync(generatedResourceModuleSchema)( + yield* Effect.promise( + () => import(`${pathToFileURL(resourcePath).href}?test=${randomUUID()}`), + ), + ); + const rentalUnitRefSchema = Schema.make>( + generatedModule.RentalUnitRefSchema.ast, + ); + const reference = Schema.decodeUnknownSync(rentalUnitRefSchema)({ moduleId, - resourceId: '', + resourceId: 'unit-42', resourceType, tenantId, - }), - /length of at least 1/u, - ); + }); + expect(reference).toEqual({ + moduleId, + resourceId: 'unit-42', + resourceType, + tenantId, + }); + expect(() => + Schema.decodeUnknownSync(rentalUnitRefSchema)({ + moduleId, + resourceId: '', + resourceType, + tenantId, + }), + ).toThrow(/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); - await assert.rejects(scaffoldResource(root, '../unsafe'), /lower-kebab-case/u); - assert.deepEqual(await snapshotTree(root), 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); + const failureCause1 = yield* Effect.flip( + Effect.sandbox(scaffoldResource(root, '../unsafe')), + ); + expect(String(Cause.squash(failureCause1))).toMatch(/lower-kebab-case/u); + expect(yield* snapshotTree(root)).toEqual(beforeTraversal); - await scaffoldResource(root); - const afterFirstRun = await snapshotTree(root); - await assert.rejects(scaffoldResource(root), /refusing to overwrite existing business file/u); - assert.deepEqual(await snapshotTree(root), afterFirstRun); - }); -}); + yield* scaffoldResource(root); + const afterFirstRun = yield* snapshotTree(root); + const failureCause2 = yield* Effect.flip(Effect.sandbox(scaffoldResource(root))); + expect(String(Cause.squash(failureCause2))).toMatch( + /refusing to overwrite existing business file/u, + ); + expect(yield* snapshotTree(root)).toEqual(afterFirstRun); + }), + ); + }), +); -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', + ), + ); + const beforeMissingSlot = yield* snapshotTree(root); + const failureCause3 = yield* Effect.flip(Effect.sandbox(scaffoldResource(root))); + expect(String(Cause.squash(failureCause3))).toMatch(/generated owner file/u); + expect(yield* snapshotTree(root)).toEqual(beforeMissingSlot); + }), ); - const beforeMissingSlot = await snapshotTree(root); - await assert.rejects(scaffoldResource(root), /generated owner file/u); - assert.deepEqual(await snapshotTree(root), beforeMissingSlot); - }); - 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'); - const beforeExportCollision = await snapshotTree(root); - await assert.rejects(scaffoldResource(root), /resource contract export .* already exists/u); - assert.deepEqual(await snapshotTree(root), beforeExportCollision); - }); -}); + yield* withFixture( + Effect.fn(function* scenario15(root) { + const packagePath = path.join(root, verticalPackagePath); + const packageValue = Schema.decodeUnknownSync(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'), + ); + const beforeExportCollision = yield* snapshotTree(root); + const failureCause4 = yield* Effect.flip(Effect.sandbox(scaffoldResource(root))); + expect(String(Cause.squash(failureCause4))).toMatch( + /resource contract export .* already exists/u, + ); + expect(yield* snapshotTree(root)).toEqual(beforeExportCollision); + }), + ); + }), +); -await test('resource scaffold upgrades the previous generated empty resourceTypes field safely', 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 upgrades the previous generated empty resourceTypes field safely', + Effect.fn(function* scenario16() { + yield* withFixture( + Effect.fn(function* scenario17(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', + ), + ); - await scaffoldResource(root); - const upgraded = await readFile(manifestPath, 'utf-8'); - assert.match(upgraded, /\/\/ /u); - assert.match(upgraded, /rentalUnitResourceDescriptor,/u); - }); -}); + yield* scaffoldResource(root); + const upgraded = yield* Effect.promise(() => readFile(manifestPath, 'utf-8')); + expect(upgraded).toMatch(/\/\/ /u); + expect(upgraded).toMatch(/rentalUnitResourceDescriptor,/u); + }), + ); + }), +); -void test('waits for an interrupted Codesmith write before removing its scoped output', async (context) => { - const root = await mkdtemp(path.join(tmpdir(), 'ontos-scaffold-cancellation-')); - context.after(async () => await rm(root, { 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[] = []; - context.mock.method(core.output, 'fs', async () => { - started.resolve(null); - await release.promise; - await mkdir(root, { recursive: true }); - await writeFile(path.join(root, 'generated.ts'), 'export {};'); - events.push('write'); - }); - await runEffectTestPromise( - Effect.gen(function* verifyWriteCleanupOrder() { +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(() => { + started.resolve(null); + // Codesmith requires a Promise-returning output callback. + // oxlint-disable promise/prefer-await-to-then -- Keep this external SDK mock Promise-shaped without an async test body. + return release.promise + .then(() => mkdir(root, { recursive: true })) + .then(() => writeFile(path.join(root, 'generated.ts'), 'export {};')) + .then(() => { + events.push('write'); + }); + // oxlint-enable promise/prefer-await-to-then + }); + const recordCleanup = () => events.push('cleanup'); + yield* Effect.gen(function* verifyWriteCleanupOrder() { const fileSystem = yield* FileSystem.FileSystem; const worker = yield* Effect.forkChild( Effect.scoped( @@ -394,7 +487,7 @@ void test('waits for an interrupted Codesmith write before removing its scoped o yield* Effect.addFinalizer(() => fileSystem .remove(root, { force: true, recursive: true }) - .pipe(Effect.orDie, Effect.andThen(Effect.sync(() => events.push('cleanup')))), + .pipe(Effect.orDie, Effect.andThen(Effect.sync(recordCleanup))), ); yield* applyMutationPlanEffect(core, { mutations: [ @@ -405,29 +498,36 @@ void test('waits for an interrupted Codesmith write before removing its scoped o }), ), ); - yield* Effect.promise(async () => await started.promise); + yield* Effect.promise(() => started.promise); const interruption = yield* Effect.forkChild(Fiber.interrupt(worker)); yield* Effect.yieldNow; - assert.deepEqual(events, []); + expect(events).toEqual([]); release.resolve(null); yield* Fiber.join(interruption); - assert.deepEqual(events, ['write', 'cleanup']); - assert.equal(yield* fileSystem.exists(root), false); - }).pipe(Effect.provide(NodeServices.layer)), - ); -}); + expect(events).toEqual(['write', 'cleanup']); + expect(yield* fileSystem.exists(root)).toBe(false); + }).pipe(Effect.provide(NodeServices.layer)); + }), +); -void test('reports synchronous malformed-owner validation through the typed command channel', async () => { - await withFixture(async (root) => { - await writeFile(path.join(root, verticalPackagePath), '[]'); - const before = await snapshotTree(root); - const failure = await runEffectTestPromise( - runScaffoldEffect('resource', [verticalFlag, verticalName, '--resource', resourceName], { - workspaceRoot: root, - }).pipe(Effect.flip, 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); + 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)).toEqual(before); + }), ); - assert.ok(Schema.is(ScaffoldingError)(failure)); - assert.match(failure.message, /JSON object/u); - assert.deepEqual(await snapshotTree(root), before); - }); -}); + }), +); diff --git a/app/scripts/scaffolding/tests/retire-contribution.test.mts b/app/scripts/scaffolding/tests/retire-contribution.test.mts index b23d01dfd..4d290ab37 100644 --- a/app/scripts/scaffolding/tests/retire-contribution.test.mts +++ b/app/scripts/scaffolding/tests/retire-contribution.test.mts @@ -1,14 +1,24 @@ -import { runEffectTestPromise } from '../../../packages/core-runtime/src/testing/effect-runtime.ts'; -import { Effect } from 'effect'; +import { Cause, Effect } from 'effect'; +import { expect, it } from '@app/effect-rstest'; + import { NodeServices } from '@effect/platform-node'; -import assert from 'node:assert/strict'; + import { access, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import test from 'node:test'; + 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 RETIRE_CONTRIBUTION_COMMAND = 'retire-contribution'; @@ -16,30 +26,43 @@ const ARCHIVE_ITEM = 'archive-item'; const ITEM_DETAIL = 'item-detail'; const ARCHIVE_ITEM_ACTION_PATH = 'verticals/inventory/src/actions/archive-item.action.ts'; -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 write = (root: string, relative: string, content: string): Effect.Effect => + Effect.gen(function* scenario1() { + const target = path.join(root, relative); + yield* Effect.promise(() => mkdir(path.dirname(target), { recursive: true })); + yield* Effect.promise(() => writeFile(target, content, 'utf-8')); + }); -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.map(async (entry) => { - const target = path.join(directory, entry.name); - if (entry.isDirectory()) { - await visit(target); - } else if (entry.isFile()) { - snapshot[path.relative(root, target)] = await readFile(target, 'utf-8'); - } - }), +const visitTree = ( + root: string, + snapshot: Record, + directory: string, +): Effect.Effect => + Effect.gen(function* scenario3() { + const entries = yield* Effect.promise(() => readdir(directory, { withFileTypes: true })); + yield* Effect.all( + entries.map( + Effect.fn(function* scenario4(entry) { + const target = path.join(directory, entry.name); + if (entry.isDirectory()) { + yield* visitTree(root, snapshot, target); + } else if (entry.isFile()) { + snapshot[path.relative(root, target)] = yield* Effect.promise(() => + readFile(target, 'utf-8'), + ); + } + }), + ), + { concurrency: 'unbounded' }, ); - }; - await visit(root); + }); + +const snapshotTree = Effect.fn(function* scenario2(root: string) { + const snapshot: Record = {}; + + yield* visitTree(root, snapshot, root); return snapshot; -}; +}); const manifest = `// @generated by OntOS Codesmith Module Contract v1 // @ontos-deployment-app-id inventory @@ -109,182 +132,235 @@ 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 = ( + run: (root: string) => Effect.Effect, +): Effect.Effect => + Effect.gen(function* scenario6() { + const root = yield* createFixture(); + yield* run(root).pipe( + Effect.ensuring(Effect.promise(() => rm(root, { force: true, recursive: true }))), + ); + }); -const retire = async (root: string, kind: 'action' | 'api' | 'page', name: string) => - await runEffectTestPromise( - runScaffoldEffect( - RETIRE_CONTRIBUTION_COMMAND, - ['--vertical', 'inventory', '--kind', kind, '--name', name], - { workspaceRoot: root }, - ).pipe(Effect.provide(NodeServices.layer)), - ); +const retire = Effect.fn(function* scenario7( + root: string, + kind: 'action' | 'api' | 'page', + name: string, +) { + return yield* runScaffoldEffect( + RETIRE_CONTRIBUTION_COMMAND, + ['--vertical', 'inventory', '--kind', kind, '--name', name], + { workspaceRoot: root }, + ).pipe(Effect.provide(NodeServices.layer)); +}); -await test('retire-contribution help is write-free and documents the narrow kinds', async () => { - const result = await runEffectTestPromise( - runScaffoldEffect(RETIRE_CONTRIBUTION_COMMAND, ['--help'], { +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)), - ); - assert.deepEqual(result, { help: getHelpText(RETIRE_CONTRIBUTION_COMMAND), kind: 'help' }); - assert.match(result.help, /--kind /u); -}); + }).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 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( + '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, '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))), - ), - ); + 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' }, + ); - 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, '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); + }), + ); + }), +); diff --git a/app/scripts/scaffolding/tests/scaffold-generators.test.mts b/app/scripts/scaffolding/tests/scaffold-generators.test.mts index adfbee0e5..79aa4e581 100644 --- a/app/scripts/scaffolding/tests/scaffold-generators.test.mts +++ b/app/scripts/scaffolding/tests/scaffold-generators.test.mts @@ -1,14 +1,14 @@ +import { Cause, Predicate } from 'effect'; +import { expect, it } from '@app/effect-rstest'; import { NodeServices } from '@effect/platform-node'; -import { runEffectTestPromise } from '../../../packages/core-runtime/src/testing/effect-runtime.ts'; -import assert from 'node:assert/strict'; + 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 test from 'node:test'; -import { Predicate } from 'effect'; + import { TrustedPrincipalContextSchema } from '../../../packages/core-runtime/src/actions/principal-context.ts'; import type { TrustedPrincipalContext } from '../../../packages/core-runtime/src/actions/principal-context.ts'; import { @@ -43,6 +43,15 @@ import { publishedOutboxContractExports, } from '../../published-outbox-contracts.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'); + }), + }); + interface Fixture { readonly root: string; } @@ -314,113 +323,123 @@ 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' }, - }; -}; + publicJwk: { ...publicJwk, alg: 'EdDSA', kid, use: 'sig' }, + }; + }); -const writeFixtureFile = async ( +const writeFixtureFile = ( 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'); -}; +): Effect.Effect => + Effect.gen(function* scenario2() { + const filePath = path.join(root, relativePath); + yield* Effect.promise(() => mkdir(path.dirname(filePath), { recursive: true })); + yield* Effect.promise(() => writeFile(filePath, content, 'utf-8')); + }); -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`, - `export const existingApiRuntime = '${vertical.moduleId}';\n`, - ); - 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'; +const createVertical = (root: string, vertical: FixtureVertical): Effect.Effect => + Effect.gen(function* scenario3() { + yield* writeFixtureFile( + root, + `verticals/${vertical.slug}/module-federation.config.ts`, + 'export default { exposes: {} };\n', + ); + yield* writeFixtureFile( + root, + `verticals/${vertical.slug}/tsconfig.json`, + json({ + compilerOptions: { composite: true }, + include: ['src', 'shared'], + references: [], + }), + ); + yield* 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', + }), + ); + yield* writeFixtureFile( + root, + `verticals/${vertical.slug}/api/index.ts`, + `export const existingApiRuntime = '${vertical.moduleId}';\n`, + ); + yield* Effect.all( + ['cs', 'en'].map( + Effect.fn(function* scenario4(locale) { + return yield* writeFixtureFile( + 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* writeFixtureFile( + 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 }; @@ -445,26 +464,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* writeFixtureFile( + 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* writeFixtureFile(root, rootPackageFile, json({ name: 'fixture', private: true })); + yield* writeFixtureFile( + root, + coreRuntimeIndexFile, + `export const existingCoreSurface = true;\n\n// \n// \n\n// \n// \n`, + ); + yield* writeFixtureFile( + root, + coreActionCatalogFile, + `export const existingCatalogSurface = true; // // @@ -474,153 +494,169 @@ export const coreActionCatalog = [ // ]; `, - ); - await writeFixtureFile(root, shellSentinelFile, 'export const shell = true;\n'); - await writeFixtureFile( - root, - shellVerticalClientsFile, - `export const ultramodernVerticalClients = [ + ); + yield* writeFixtureFile(root, shellSentinelFile, 'export const shell = true;\n'); + yield* writeFixtureFile( + 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}`, - }), - ), - }), - ); - await Promise.all( - [inventoryVertical, billingVertical, hrVertical, contactsVertical].map( - async (vertical) => - await runEffectTestPromise( - runScaffoldEffect( + ); + yield* createVertical(root, inventoryVertical); + yield* createVertical(root, billingVertical); + yield* createVertical(root, hrVertical); + yield* createVertical(root, contactsVertical); + yield* 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* 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)), - ), - ), - ); - return { root }; -}; + ).pipe(Effect.provide(NodeServices.layer)); + }), + ), + { concurrency: 'unbounded' }, + ); + 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 }); - } -}; +const withFixture = ( + run: (fixture: Fixture) => Effect.Effect, +): Effect.Effect => + Effect.gen(function* scenario7() { + const fixture = yield* createFixture(); + yield* run(fixture).pipe( + Effect.ensuring(Effect.promise(() => rm(fixture.root, { force: true, recursive: true }))), + ); + }); -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( +const visitTree = ( + root: string, + snapshot: Record, + directory: string, +): Effect.Effect => + Effect.gen(function* scenario9() { + const entries = yield* Effect.promise(() => readdir(directory, { withFileTypes: true })); + yield* Effect.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'); - } - }), + .map( + Effect.fn(function* scenario10(entry) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + yield* visitTree(root, snapshot, entryPath); + } else if (entry.isFile()) { + snapshot[path.relative(root, entryPath)] = yield* Effect.promise(() => + readFile(entryPath, 'utf-8'), + ); + } + }), + ), + { concurrency: 'unbounded' }, ); - }; - await visit(root); + }); + +const snapshotTree = Effect.fn(function* scenario8(root: string) { + const snapshot: Record = {}; + + yield* visitTree(root, snapshot, 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 run = async ( +const run = Effect.fn(function* scenario12( fixture: Fixture, command: ScaffoldCommand, scaffoldArguments: readonly string[], routeRefresh?: (appId: string) => void, -) => - await runEffectTestPromise( - runScaffoldEffect( - command, - (() => { - let flags = [...scaffoldArguments]; - if ( - command === 'action' && - flags.includes('--action') && - !flags.includes(scaffoldFlag.legalEntityScope) +) { + return yield* runScaffoldEffect( + command, + (() => { + let flags = [...scaffoldArguments]; + if ( + command === 'action' && + flags.includes('--action') && + !flags.includes(scaffoldFlag.legalEntityScope) + ) { + flags = [...flags, scaffoldFlag.legalEntityScope, 'optional']; + } + if (!flags.includes(scaffoldFlag.authorization)) { + if (command === 'action') { + flags = [ + ...flags, + scaffoldFlag.authorization, + 'action_execution', + '--provisioning', + 'tenant_membership_default', + ]; + } else if (command === scaffoldCommand.outboxWorker) { + flags = [...flags, scaffoldFlag.authorization, 'owner_local_background']; + } else if ( + command === scaffoldCommand.microverticalPage || + command === scaffoldCommand.moduleApi || + command === scaffoldCommand.publicComponent || + command === 'report' || + command === scaffoldCommand.searchProvider ) { - flags = [...flags, scaffoldFlag.legalEntityScope, 'optional']; - } - if (!flags.includes(scaffoldFlag.authorization)) { - if (command === 'action') { - flags = [ - ...flags, - scaffoldFlag.authorization, - 'action_execution', - '--provisioning', - 'tenant_membership_default', - ]; - } else if (command === scaffoldCommand.outboxWorker) { - flags = [...flags, scaffoldFlag.authorization, 'owner_local_background']; - } else if ( - command === scaffoldCommand.microverticalPage || - command === scaffoldCommand.moduleApi || - command === scaffoldCommand.publicComponent || - command === 'report' || - command === scaffoldCommand.searchProvider - ) { - flags = [ - ...flags, - scaffoldFlag.authorization, - 'context_permission', - '--permission', - 'module.access', - ]; - } + flags = [ + ...flags, + scaffoldFlag.authorization, + 'context_permission', + '--permission', + 'module.access', + ]; } - return flags; - })(), - { - routeRefresh: ({ appId }) => Effect.sync(() => routeRefresh?.(appId)), - workspaceRoot: fixture.root, - }, - ).pipe(Effect.provide(NodeServices.layer)), - ); + } + return flags; + })(), + { + routeRefresh: ({ appId }) => Effect.sync(() => routeRefresh?.(appId)), + workspaceRoot: fixture.root, + }, + ).pipe(Effect.provide(NodeServices.layer)); +}); -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, @@ -635,484 +671,534 @@ const addInventoryItemResourceType = async (fixture: Fixture): Promise => owningModuleId: 'inventory.stock', }, ],`, - ), - 'utf-8', - ); -}; - -void 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 runEffectTestPromise( - runScaffoldEffect(command, ['--', '--help'], { - workspaceRoot: path.join(tmpdir(), 'does-not-need-to-exist'), - }).pipe(Effect.provide(NodeServices.layer)), - ); - 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, - ); -}); - -void 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, - ); - 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 providerPath = path.join(fixture.root, inventorySearchProviderFile); - await writeFile( - providerPath, - `${provider}\n// Owner-customized searchable semantics remain untouched.\n`, - ); - 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 '), - ); - const beforeRejectedUpdate = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.searchProviderAccess, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - fixtureName.inventoryItems, - scaffoldFlag.legalEntityScope, - 'required', - scaffoldFlag.accessFiltering, - 'resource_permission', - scaffoldFlag.requestFilters, - 'includeArchived,role', - ]), - /Codesmith-owned provider/u, + ), + 'utf-8', + ), ); - assert.deepEqual(await snapshotTree(fixture.root), beforeRejectedUpdate); }); -}); -void 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), +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' }, ); - for (const source of sources) { - assert.ok(source.indexOf("'party-match':") < source.indexOf("'party-match-decision':")); - } - }); -}); - -void 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, 'packages/shared-contracts'), - path.join(fixture.root, 'node_modules/@app/shared-contracts'), - 'dir', + 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 symlink( - path.join(appRoot, effectNodeModulePath), - path.join(fixture.root, effectNodeModulePath), - 'dir', + expect(getHelpText(scaffoldCommand.microverticalPage)).toMatch(/:parameter/u); + expect(getHelpText(scaffoldCommand.microverticalPage)).toMatch( + /\/contacts\/customers\/:id\/edit/u, ); - await symlink( - path.join(appRoot, pluginBffNodeModulePath), - path.join(fixture.root, pluginBffNodeModulePath), - 'dir', + expect(getHelpText(scaffoldCommand.externalHttpAdapter)).toMatch( + /scaffold:external-http-adapter -- --vertical --provider --operation /u, ); - const result = spawnSync( - process.execPath, - [ - '--input-type=module', - '--eval', - ` - import { Effect } 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 = []; - 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) => { - 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); - }; - 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))); - } - 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) => { - 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); - }))); - } - 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) => { - if (String(url) === 'https://shell.example.test/shell-super-app-api/auth/gateway-context') { - return 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); - }))); - } - console.log(JSON.stringify(calls)); - `, - ], - { cwd: fixture.root, encoding: 'utf-8' }, + expect(getHelpText(scaffoldCommand.externalHttpAdapter)).toMatch( + /--vertical contacts --provider ares --operation subject/u, ); - assert.equal(result.status, 0, result.stderr || result.error?.message); - assert.deepEqual( - JSON.parse(result.stdout), - [ - '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(getHelpText(scaffoldCommand.searchProviderAccess)).toMatch( + /--tenant-permission read_party_identity/u, ); - }); -}); - -void 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, '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, - 'verticals/inventory-stock/src/api/stock-levels-report-client.ts', - ); - const moduleApiClient = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/api/resource-detail-client.ts', - ); - const moduleApiContract = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/shared/apis/resource-detail.ts', - ); - 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, - 'verticals/inventory-stock/src/reports/stock-levels.provider.ts', - ); - const moduleApiRead = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/api/resource-detail.read.ts', - ); - const searchServer = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/api/inventory-items-search-server.ts', - ); - const reportServer = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/api/stock-levels-report-server.ts', - ); - const moduleApiServer = await readFixtureFile( - fixture.root, - 'verticals/inventory-stock/api/resource-detail-read-server.ts', - ); - const operationBoundary = await readFixtureFile(fixture.root, inventoryActionPrincipalFile); - assert.match(searchClient, /makeEffectHttpApiClient\(InventoryItemsSearchApi, \{/u); - assert.match(reportClient, /makeEffectHttpApiClient\(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); - for (const client of [moduleApiClient, searchClient, reportClient]) { - assert.match(client, /Context\.Reference/u); - assert.match(client, /WithAuthorization/u); - assert.match( - client, - /setHeaders\(\{\s+authorization,\s+'x-correlation-id': correlationId,?\s+\}\)/u, - ); - } - assert.doesNotMatch(searchClient, /\.provider\.ts|import\(/u); - assert.doesNotMatch(reportClient, /\.provider\.ts|import\(/u); - for (const provider of [searchProvider, reportProvider]) { - 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); - } - assert.match(searchProvider, /result\.map\(\(\{ ref \}\) => ref\)/u); - assert.match(moduleApiRead, /defineRead\(/u); - assert.match(moduleApiRead, /legalEntityScope: 'required'/u); - for (const server of [moduleApiServer, searchServer, reportServer]) { - assert.match(server, /verifyOperationPrincipal\(\s*request\.headers\.authorization,/u); - assert.match(server, /yield\* ReadRuntime/u); - assert.match(server, /\.runRead\(\{/u); - assert.match(server, /HttpEffect\.appendPreResponseHandler/u); - assert.match(server, /'www-authenticate', 'Bearer'/u); - assert.match(server, /Match\.tags\(\{/u); - assert.match(server, /ReadHandlerNotFound: notFoundProblem/u); - assert.match( - server, - /ReadPolicyDenied: \(failure\) => policyProblem\(failure\.httpStatus\)/u, - ); - assert.match(server, /Effect\.catchTags\(\{/u); - assert.doesNotMatch(server, /switch \(error\._tag\)|error\._tag ===/u); - assert.match(server, /problem\.status === 401\s+\?\s+bearerChallenge/u); - assert.doesNotMatch(server, /tenantId|legalEntityId|principalId|CoreDatabase|from 'pg'/u); - } - assert.match( - operationBoundary, - /export const verifyOperationPrincipal = verifyActionPrincipal/u, - ); - const searchContract = await readFixtureFile(fixture.root, inventorySearchContractFile); - 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, /Schema\.Literal\(409\)/u); - - const beforeRepeat = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.publicComponent, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - 'inventory-summary', - ]), - /refusing to overwrite/u, +it.live( + 'search-provider access updates only generated access metadata and fails atomically on drift', + Effect.fn(function* scenario16() { + yield* withFixture( + Effect.fn(function* scenario17(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); + + 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 '), + ), + ); + const beforeRejectedUpdate = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.searchProviderAccess, [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.inventoryItems, + scaffoldFlag.legalEntityScope, + 'required', + scaffoldFlag.accessFiltering, + 'resource_permission', + scaffoldFlag.requestFilters, + 'includeArchived,role', + ]), + (error) => expect(String(error)).toMatch(/Codesmith-owned provider/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeRejectedUpdate); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), beforeRepeat); - await assert.rejects( - run(fixture, scaffoldCommand.moduleApi, [ - scaffoldFlag.vertical, - inventorySlug, - '--name', - '../unsafe', - ]), - /lower-kebab-case/u, + }), +); + +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, + ); + } + }), ); - assert.deepEqual(await snapshotTree(fixture.root), beforeRepeat); - const billingFederationPath = path.join( - fixture.root, - 'verticals/billing/module-federation.config.ts', + }), +); + +it.live( + 'generated read clients fetch mounted owner URLs and support separately deployed hosts', + Effect.fn(function* scenario21() { + yield* withFixture( + Effect.fn(function* scenario22(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* Effect.promise(() => + mkdir(path.join(fixture.root, 'node_modules/@app'), { recursive: true }), + ); + yield* Effect.promise(() => + mkdir(path.join(fixture.root, 'node_modules/@modern-js'), { recursive: true }), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/shared-contracts'), + path.join(fixture.root, 'node_modules/@app/shared-contracts'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, effectNodeModulePath), + path.join(fixture.root, effectNodeModulePath), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, pluginBffNodeModulePath), + path.join(fixture.root, pluginBffNodeModulePath), + 'dir', + ), + ); + const result = spawnSync( + process.execPath, + [ + '--input-type=module', + '--eval', + ` + import { Effect } 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'; + const calls = []; + 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) => { + 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); + }; + await Effect.runPromise(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 Effect.runPromise(invoke(payload, 'Bearer proof', 'correlation-proof').pipe(Effect.provideService(FetchHttpClient.Fetch, async (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); + }))); + } + for (const [, payload, response, invoke] of cases) { + await Effect.runPromise(invoke(payload, 'correlation-proof', { baseUrl: 'https://inventory.example.test/custom/inventory-stock-api' }).pipe(Effect.provideService(FetchHttpClient.Fetch, async (url, init) => { + if (String(url) === 'https://shell.example.test/shell-super-app-api/auth/gateway-context') { + return 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); + }))); + } + console.log(JSON.stringify(calls)); + `, + ], + { cwd: fixture.root, encoding: 'utf-8' }, + ); + expect(result.status, result.stderr || result.error?.message).toBe(0); + expect(JSON.parse(result.stdout)).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, + })), + ); + }), ); - await writeFile( - billingFederationPath, - `const ignored = /exposes: \\{\\}/u; + }), +); + +it.live( + 'governed contribution generators patch owner contracts and lazy adapters atomically', + Effect.fn(function* scenario23() { + yield* withFixture( + Effect.fn(function* scenario24(fixture) { + const manifestPath = path.join(fixture.root, inventoryManifestFile); + yield* addInventoryItemResourceType(fixture); + + 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, 'report', [ + scaffoldFlag.vertical, + inventorySlug, + '--name', + fixtureName.stockLevels, + scaffoldFlag.resource, + 'item', + ]); + + 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, + 'verticals/inventory-stock/src/api/stock-levels-report-client.ts', + ); + const moduleApiClient = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/api/resource-detail-client.ts', + ); + const moduleApiContract = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/shared/apis/resource-detail.ts', + ); + 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, + 'verticals/inventory-stock/src/reports/stock-levels.provider.ts', + ); + const moduleApiRead = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/api/resource-detail.read.ts', + ); + const searchServer = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/api/inventory-items-search-server.ts', + ); + const reportServer = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/api/stock-levels-report-server.ts', + ); + const moduleApiServer = yield* readFixtureFile( + fixture.root, + 'verticals/inventory-stock/api/resource-detail-read-server.ts', + ); + const operationBoundary = yield* readFixtureFile( + fixture.root, + inventoryActionPrincipalFile, + ); + expect(searchClient).toMatch(/makeEffectHttpApiClient\(InventoryItemsSearchApi, \{/u); + expect(reportClient).toMatch(/makeEffectHttpApiClient\(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); + for (const client of [moduleApiClient, searchClient, reportClient]) { + expect(client).toMatch(/Context\.Reference/u); + expect(client).toMatch(/WithAuthorization/u); + expect(client).toMatch( + /setHeaders\(\{\s+authorization,\s+'x-correlation-id': correlationId,?\s+\}\)/u, + ); + } + expect(searchClient).not.toMatch(/\.provider\.ts|import\(/u); + expect(reportClient).not.toMatch(/\.provider\.ts|import\(/u); + for (const provider of [searchProvider, reportProvider]) { + 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); + } + expect(searchProvider).toMatch(/result\.map\(\(\{ ref \}\) => ref\)/u); + expect(moduleApiRead).toMatch(/defineRead\(/u); + expect(moduleApiRead).toMatch(/legalEntityScope: 'required'/u); + for (const server of [moduleApiServer, searchServer, reportServer]) { + expect(server).toMatch(/verifyOperationPrincipal\(\s*request\.headers\.authorization,/u); + expect(server).toMatch(/yield\* ReadRuntime/u); + expect(server).toMatch(/\.runRead\(\{/u); + expect(server).toMatch(/HttpEffect\.appendPreResponseHandler/u); + expect(server).toMatch(/'www-authenticate', 'Bearer'/u); + expect(server).toMatch(/Match\.tags\(\{/u); + expect(server).toMatch(/ReadHandlerNotFound: notFoundProblem/u); + expect(server).toMatch( + /ReadPolicyDenied: \(failure\) => policyProblem\(failure\.httpStatus\)/u, + ); + expect(server).toMatch(/Effect\.catchTags\(\{/u); + expect(server).not.toMatch(/switch \(error\._tag\)|error\._tag ===/u); + expect(server).toMatch(/problem\.status === 401\s+\?\s+bearerChallenge/u); + expect(server).not.toMatch(/tenantId|legalEntityId|principalId|CoreDatabase|from 'pg'/u); + } + expect(operationBoundary).toMatch( + /export const verifyOperationPrincipal = verifyActionPrincipal/u, + ); + const searchContract = yield* readFixtureFile(fixture.root, inventorySearchContractFile); + 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(/Schema\.Literal\(409\)/u); + + const beforeRepeat = yield* snapshotTree(fixture.root); + 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); + 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: {}, @@ -1122,33 +1208,40 @@ 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'); - const beforeUnpatchable = await snapshotTree(fixture.root); - await assert.rejects( - run(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'), + ); + const beforeUnpatchable = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.publicComponent, [ + scaffoldFlag.vertical, + 'billing', + '--name', + 'billing-details', + ]), + (error) => expect(String(error)).toMatch(/exposes object is missing/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeUnpatchable); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), beforeUnpatchable); - }); -}); + }), +); -void 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', @@ -1156,671 +1249,763 @@ void test('recognizes only exact schema-only Outbox package subpaths as cross-ve './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, - ); + ).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); }); -void test('rejects malformed command contracts and leaves the fixture unchanged', async () => { - await withFixture(async (fixture) => { - const before = await snapshotTree(fixture.root); - await assert.rejects( - runEffectTestPromise( - runScaffoldEffect( - 'action', +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), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + const invalidCalls: readonly [ScaffoldCommand, readonly string[], RegExp][] = [ + ['action', [scaffoldFlag.vertical, inventorySlug], /missing required flag --action/u], [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - scaffoldFlag.authorization, - 'action_execution', - '--provisioning', - 'tenant_membership_default', + 'action', + [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + scaffoldFlag.legalEntityScope, + 'invalid', + ], + /must be required, optional, or forbidden/u, ], - { workspaceRoot: fixture.root }, - ).pipe(Effect.provide(NodeServices.layer)), - ), - /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', - [ + [ + '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); + }), + ), + { concurrency: 'unbounded' }, + ); + }), + ); + }), +); + +it.live( + 'generates one immutable Action identity boundary and exact direct dependencies', + Effect.fn(function* scenario28() { + yield* withFixture( + Effect.fn(function* scenario29(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, - '--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, - [ + ]); + expect(result.kind).toBe('generated'); + const server = yield* readFixtureFile(fixture.root, inventoryActionPrincipalFile); + 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(/acquire\(\{ audience: ACTION_GATEWAY_AUDIENCE \}/u); + expect(client).not.toMatch(/localStorage|sessionStorage/u); + expect(server).toMatch(/verifyAndRedeem/u); + expect(redemption).toMatch(/GatewayAssertionRedemptionUnavailableError/u); + const packageJson = 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); + }), + ); + }), +); + +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, - '--action', - fixtureName.action, - '--topic', - 'Not.Safe', - ], - /dot-separated/u, - ], - [ - scaffoldCommand.outboxWorker, - [ + ]); + const afterFirstRun = yield* snapshotTree(fixture.root); + yield* run(fixture, scaffoldCommand.microverticalActionBoundary, [ 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); + ]); + expect(yield* snapshotTree(fixture.root)).toEqual(afterFirstRun); }), ); - }); -}); - -void 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 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, /acquire\(\{ audience: ACTION_GATEWAY_AUDIENCE \}/u); - assert.doesNotMatch(client, /localStorage|sessionStorage/u); - assert.match(server, /verifyAndRedeem/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); - }); -}); - -void 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 + yield* withFixture( + Effect.fn(function* scenario32(fixture) { + yield* writeFixtureFile( + fixture.root, + inventoryActionPrincipalFile, + `// Owner-authored identity adapter export const ownerCode = true; `, + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalActionBoundary, [ + scaffoldFlag.vertical, + inventorySlug, + ]), + (error) => expect(String(error)).toMatch(/refusing to overwrite existing business file/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.microverticalActionBoundary, [ - scaffoldFlag.vertical, - inventorySlug, - ]), - /refusing to overwrite existing business file/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }); -}); + }), +); -void 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, 'packages/shared-contracts'), - path.join(fixture.root, 'node_modules/@app/shared-contracts'), - '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.doesNotMatch(edgeInputs, /core-runtime\/src\/(?:auth|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(GeneratedActionGatewayModuleSchema)( - await import(pathToFileURL(path.join(fixture.root, inventoryActionGatewayFile)).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), - }), +it.live( + 'generated verifier executes real Shell assertions and overlapping Ed25519 rotation', + Effect.fn(function* scenario33() { + yield* withFixture( + Effect.fn(function* scenario34(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', ), - ), - ); - 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, - }), - ); + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/shared-contracts'), + path.join(fixture.root, 'node_modules/@app/shared-contracts'), + '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( + Schema.decodeUnknownSync(EsbuildMetafileSchema)( + JSON.parse(yield* Effect.promise(() => readFile(edgeMetafile, 'utf-8'))), + ).inputs, + ).join('\n'); + expect(edgeInputs).not.toMatch( + /core-runtime\/src\/(?:auth|db)|node:(?:crypto|path)|\/pg\//u, + ); + const generatedModule = Schema.decodeUnknownSync(GeneratedPrincipalModuleSchema)( + yield* Effect.promise( + () => import(pathToFileURL(path.join(fixture.root, inventoryActionPrincipalFile)).href), + ), + ); + const billingGeneratedModule = Schema.decodeUnknownSync(GeneratedPrincipalModuleSchema)( + yield* Effect.promise( + () => + import( + pathToFileURL( + path.join(fixture.root, 'verticals/billing/api/auth/action-principal.ts'), + ).href + ), + ), + ); + const generatedClientModule = Schema.decodeUnknownSync(GeneratedActionGatewayModuleSchema)( + yield* Effect.promise( + () => import(pathToFileURL(path.join(fixture.root, inventoryActionGatewayFile)).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* scenario35( + 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, + }); - 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* 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* scenario37(jwks) { + return yield* expectFailure( + verify(currentAssertion.token, { + ...environment, + ONTOS_GATEWAY_PUBLIC_JWKS: JSON.stringify(jwks), + }), + (error) => + expect( + isGeneratedPrincipalError('ActionPrincipalConfigurationError')(error), + ).toBe(true), + ); }), - isGeneratedPrincipalError('ActionPrincipalConfigurationError'), ), - ), - ); - await assert.rejects( - verify(currentAssertion.token, { - ...environment, - ONTOS_GATEWAY_ISSUER: 'file:///not-an-http-issuer', - }), - 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'), - ); - await assert.rejects( - runEffectTestPromise( - generatedModule.verifyActionPrincipal(`Bearer ${currentAssertion.token}`, { - currentTimeSeconds: Effect.succeed(1_700_000_001), - environment: {}, - redemption: testRedemption, - }), - ), - isGeneratedPrincipalError('ActionPrincipalConfigurationError'), - ); - let acquisitions = 0; - const authorizations: string[] = []; - const idempotencyKey = 'caller-owned-idempotency-key'; - const actionGateway = generatedClientModule.makeActionGateway(({ 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(actionGateway.invoke(attempt)), idempotencyKey); - assert.equal(await runEffectTestPromise(actionGateway.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, - }), - ), - ); - 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'], { + { 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: endpointEnvironment, + environment, redemption: testRedemption, - }) - .pipe(Effect.tap(markActionReached), Effect.catchTags(generatedPrincipalErrorHandlers)), - ), + }), + (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 actionGateway = generatedClientModule.makeActionGateway(({ 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* actionGateway.invoke(attempt)).toBe(idempotencyKey); + expect(yield* actionGateway.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.gen(function* useResource2() { + 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( + Schema.decodeUnknownSync(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); + }).pipe(Effect.ensuring(Effect.promise(() => actionHandler.dispose()))); + }), ); - 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); - - 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(); - } - }); -}); + }), +); -void 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'; @@ -1878,90 +2063,99 @@ 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); - const beforeRerun = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, 'action', [scaffoldFlag.vertical, inventorySlug, '--action', 'create-order2']), - /refusing to overwrite/u, +`); + const packageJson = decodeFixturePackage( + yield* readFixtureFile(fixture.root, inventoryPackageFile), + ); + expect(packageJson.dependencies).toEqual({ + '@app/core-runtime': workspaceVersion, + zeta: '1.0.0', + }); + expect(packageJson.scripts['existing']).toBe(preservedFixtureValue); + const beforeRerun = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, 'action', [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + 'create-order2', + ]), + (error) => expect(String(error)).toMatch(/refusing to overwrite/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeRerun); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), beforeRerun); - }); -}); + }), +); -void 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 +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({}); -`, - ); - const beforeRerun = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.actionService, [ - scaffoldFlag.vertical, - inventorySlug, - '--service', - 'inventory-persistence', - ]), - /refusing to overwrite/u, +`); + const beforeRerun = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.actionService, [ + scaffoldFlag.vertical, + inventorySlug, + '--service', + 'inventory-persistence', + ]), + (error) => expect(String(error)).toMatch(/refusing to overwrite/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeRerun); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), beforeRerun); - }); -}); + }), +); -void 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'], - ); - 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'; @@ -1998,525 +2192,606 @@ 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, + ); - const beforeOverwrite = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.externalHttpAdapter, [ - scaffoldFlag.vertical, - 'contacts', - scaffoldFlag.provider, - 'ares', - scaffoldFlag.operation, - 'subject', - ]), - /refusing to overwrite/u, + const beforeOverwrite = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.externalHttpAdapter, [ + scaffoldFlag.vertical, + 'contacts', + scaffoldFlag.provider, + 'ares', + scaffoldFlag.operation, + 'subject', + ]), + (error) => expect(String(error)).toMatch(/refusing to overwrite/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeOverwrite); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), beforeOverwrite); - }); -}); - -void 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(); - }); -}); + }), +); -void 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', - ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(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); + } + }), ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }); + }), +); - await withFixture(async (fixture) => { - await writeFixtureFile( - fixture.root, - 'verticals/contacts/src/integrations', - 'planner fixture blocks the required directory\n', - ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(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', + ), + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.externalHttpAdapter, [ + scaffoldFlag.vertical, + 'contacts', + scaffoldFlag.provider, + 'ares', + scaffoldFlag.operation, + 'subject', + ]), + (error) => expect(String(error)).toMatch(/is not a generated module owner/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }); -}); -void 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( - '// ', - `// + yield* withFixture( + Effect.fn(function* scenario49(fixture) { + yield* writeFixtureFile( + fixture.root, + 'verticals/contacts/src/integrations', + 'planner fixture blocks the required directory\n', + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.externalHttpAdapter, [ + scaffoldFlag.vertical, + 'contacts', + scaffoldFlag.provider, + 'ares', + scaffoldFlag.operation, + 'subject', + ]), + (error) => expect(String(error)).toMatch(/ENOTDIR|not a directory/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ); + }), +); + +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', - ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, 'action', [scaffoldFlag.vertical, inventorySlug, '--action', 'create-order3']), - /generated owner slot contains unsupported developer content/u, + ), + 'utf-8', + ), + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, 'action', [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + 'create-order3', + ]), + (error) => + expect(String(error)).toMatch( + /generated owner slot contains unsupported developer content/u, + ), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }); -}); + }), +); -void 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', - ]); +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', + ]); - 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,'), - ); - assert.match(coreCatalog, /export const existingCatalogSurface = true/u); - - const beforeOverwrite = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, 'action', [ - '--scope', - 'core', - '--module', - fixtureName.actionModule, - '--action', - 'account-change', - ]), - /refusing to overwrite/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); + + const beforeOverwrite = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, 'action', [ + '--scope', + 'core', + '--module', + fixtureName.actionModule, + '--action', + 'account-change', + ]), + (error) => expect(String(error)).toMatch(/refusing to overwrite/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeOverwrite); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), beforeOverwrite); - }); - await withFixture(async (fixture) => { - const indexPath = path.join(fixture.root, coreRuntimeIndexFile); - await writeFile( - indexPath, - `export const existingCoreSurface = true;\n\n// \n// \n`, - 'utf-8', - ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, 'action', [ - '--scope', - 'core', - '--module', - fixtureName.actionModule, - '--action', - fixtureName.action, - ]), - /generated owner file does not contain one valid/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', + ), + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, 'action', [ + '--scope', + 'core', + '--module', + fixtureName.actionModule, + '--action', + fixtureName.action, + ]), + (error) => + expect(String(error)).toMatch(/generated owner file does not contain one valid/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }); - 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', - ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(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', + ), + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, 'action', [ + '--scope', + 'core', + '--module', + fixtureName.actionModule, + '--action', + fixtureName.action, + ]), + (error) => expect(String(error)).toMatch(/unsupported developer content/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }); - 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', - ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, 'action', [ - '--scope', - 'core', - '--module', - fixtureName.actionModule, - '--action', - fixtureName.action, - ]), - /unsupported developer content/u, + 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', + ), + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, 'action', [ + '--scope', + 'core', + '--module', + fixtureName.actionModule, + '--action', + fixtureName.action, + ]), + (error) => expect(String(error)).toMatch(/unsupported developer content/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }); -}); + }), +); -void 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' }, +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 = 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', + ), + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, 'action', [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + ]), + (error) => expect(String(error)).toMatch(/incompatible/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); }), - 'utf-8', ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, 'action', [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - ]), - /incompatible/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }); -}); + }), +); -void 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( + '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 = decodeFixturePackage( + yield* Effect.promise(() => readFile(billingPackagePath, 'utf-8')), + ); + yield* Effect.promise(() => + writeFile( + billingPackagePath, + json({ + ...billingPackage, + modernjs: { ...billingPackage.modernjs, appId: inventoryVertical.appId }, + }), + 'utf-8', + ), + ); + const before = yield* snapshotTree(fixture.root); + + yield* expectFailure( + run(fixture, 'action', [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + ]), + (error) => expect(String(error)).toMatch(/duplicate generated appId inventory-stock/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); }), - 'utf-8', - ); - const before = await snapshotTree(fixture.root); - - await assert.rejects( - run(fixture, 'action', [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - ]), - /duplicate generated appId inventory-stock/u, ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }); -}); + }), +); -void 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 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 = decodeFixturePackage( + yield* Effect.promise(() => readFile(packagePath, 'utf-8')), + ); + yield* Effect.promise(() => + writeFile( + packagePath, + json({ + ...packageJson, + modernjs: { ...packageJson.modernjs, appId: 'inventory-shadow' }, + }), + 'utf-8', + ), + ); + const before = yield* snapshotTree(fixture.root); + + yield* expectFailure( + run(fixture, 'action', [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + ]), + (error) => + expect(String(error)).toMatch( + /must have exactly one matching generated topology entry/u, + ), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); }), - 'utf-8', ); - const before = await snapshotTree(fixture.root); - - await assert.rejects( - run(fixture, 'action', [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - ]), - /must have exactly one matching generated topology entry/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }); -}); - -void 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'); + }), +); - 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 = 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, @@ -2536,11 +2811,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'; @@ -2552,177 +2825,194 @@ 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 = 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', - ]); - const beforeIdentifierCollision = await snapshotTree(fixture.root); - await assert.rejects( - run(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', + ]); + const beforeIdentifierCollision = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.outboxMessage, [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + '--topic', + 'events.foo1-bar', + ]), + (error) => + expect(String(error)).toMatch( + /Outbox identifier CreateOrderEventsFoo1BarOutbox already exists/u, + ), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeIdentifierCollision); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), beforeIdentifierCollision); - }); -}); + }), +); -void test('rejects missing, handwritten, duplicate, and normalized-collision Outbox targets without partial writes', async () => { - await withFixture(async (fixture) => { - const beforeMissing = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.outboxMessage, [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - 'missing-action', - '--topic', - fixtureName.ordersCreated, - ]), - /requires the generated Action/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), beforeMissing); +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) { + const beforeMissing = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.outboxMessage, [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + 'missing-action', + '--topic', + fixtureName.ordersCreated, + ]), + (error) => expect(String(error)).toMatch(/requires the generated Action/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeMissing); - await writeFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/actions/handwritten.action.ts', - `// \n// \n`, - ); - const beforeHandwritten = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.outboxMessage, [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - 'handwritten', - '--topic', - fixtureName.ordersCreated, - ]), - /only the matching generated Action/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), beforeHandwritten); + yield* writeFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/actions/handwritten.action.ts', + `// \n// \n`, + ); + const beforeHandwritten = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.outboxMessage, [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + 'handwritten', + '--topic', + fixtureName.ordersCreated, + ]), + (error) => expect(String(error)).toMatch(/only the matching generated Action/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeHandwritten); - 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',"), - ); - const beforeMismatchedEntrypoint = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.outboxMessage, [ - scaffoldFlag.vertical, - inventorySlug, - '--action', - fixtureName.action, - '--topic', - fixtureName.ordersCreated, - ]), - /matching generated Action with its governed write entrypoint/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), beforeMismatchedEntrypoint); - 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( + yield* run(fixture, 'action', [ + scaffoldFlag.vertical, + inventorySlug, + '--action', + fixtureName.action, + ]); + const governedActionPath = inventoryActionFile; + const governedAction = yield* readFixtureFile(fixture.root, governedActionPath); + yield* writeFixtureFile( + fixture.root, + governedActionPath, + governedAction.replace(" access: 'write',", " access: 'read',"), + ); + const beforeMismatchedEntrypoint = yield* snapshotTree(fixture.root); + yield* expectFailure( run(fixture, scaffoldCommand.outboxMessage, [ scaffoldFlag.vertical, inventorySlug, '--action', fixtureName.action, '--topic', - topic, + fixtureName.ordersCreated, ]), - /already exists/u, + (error) => + expect(String(error)).toMatch( + /matching generated Action with its governed write entrypoint/u, + ), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeMismatchedEntrypoint); + yield* writeFixtureFile(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); }), ); - }); -}); + }), +); -void test('generates isolated Outbox Workers from published contracts and composes a stable registry', 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 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* scenario70() { + yield* withFixture( + Effect.fn(function* scenario71(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 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 @@ -2775,11 +3065,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'; @@ -2790,11 +3078,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'; @@ -2810,20 +3096,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 { @@ -2853,259 +3135,277 @@ export const startBillingOutboxWorker = (): void => registrations: outboxWorkers, subscriptions: outboxSubscriptions, }); -`, - ); - assert.equal( - await readFixtureFile(fixture.root, billingApiIndexFile), - "export const existingApiRuntime = 'billing.core';\n", - ); - 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( + "export const existingApiRuntime = 'billing.core';\n", + ); + const consumerPackage = 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 = Schema.decodeUnknownSync(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'), - ); - const beforeRerun = await snapshotTree(fixture.root); - await assert.rejects( - run(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'), + ).toBe(true); + const beforeRerun = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.outboxWorker, [ + scaffoldFlag.vertical, + 'billing', + '--worker', + fixtureName.ordersCreatedLogger, + scaffoldFlag.producer, + inventorySlug, + '--topic', + fixtureName.ordersCreated, + ]), + (error) => expect(String(error)).toMatch(/refusing to overwrite/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeRerun); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), beforeRerun); - }); -}); + }), +); -void 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); - } - const beforeRerun = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.outboxWorker, args), - /refusing to overwrite/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), beforeRerun); - 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' }] }), - ); - const beforeCircularReference = await snapshotTree(fixture.root); - await assert.rejects( - run(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 = 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); + } + const beforeRerun = yield* snapshotTree(fixture.root); + yield* expectFailure(run(fixture, scaffoldCommand.outboxWorker, args), (error) => + expect(String(error)).toMatch(/refusing to overwrite/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeRerun); + 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* writeFixtureFile( + fixture.root, + inventoryTsconfigFile, + JSON.stringify({ references: [{ path: '../inventory-stock' }] }), + ); + const beforeCircularReference = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.outboxWorker, [ + scaffoldFlag.vertical, + inventorySlug, + '--worker', + 'orders-created-audit', + scaffoldFlag.producer, + inventorySlug, + '--topic', + fixtureName.ordersCreated, + ]), + (error) => expect(String(error)).toMatch(/circular self project reference/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeCircularReference); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), beforeCircularReference); - }); -}); + }), +); -void test('refuses unpublished or malformed Outbox contracts without partial consumer writes', async () => { - await withFixture(async (fixture) => { - const beforeUnpublished = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.outboxWorker, [ - scaffoldFlag.vertical, - 'billing', - '--worker', - fixtureName.ordersLogger, - scaffoldFlag.producer, - inventorySlug, - '--topic', - 'orders.missing', - ]), - /published producer Outbox contract is missing/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), beforeUnpublished); +it.live( + 'refuses unpublished or malformed Outbox contracts without partial consumer writes', + Effect.fn(function* scenario74() { + yield* withFixture( + Effect.fn(function* scenario75(fixture) { + const beforeUnpublished = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.outboxWorker, [ + scaffoldFlag.vertical, + 'billing', + '--worker', + fixtureName.ordersLogger, + scaffoldFlag.producer, + inventorySlug, + '--topic', + 'orders.missing', + ]), + (error) => + expect(String(error)).toMatch(/published producer Outbox contract is missing/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeUnpublished); - 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', - ); - const beforeMalformed = await snapshotTree(fixture.root); - await assert.rejects( - run(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', + ), + ); + const beforeMalformed = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.outboxWorker, [ + scaffoldFlag.vertical, + 'billing', + '--worker', + fixtureName.ordersLogger, + scaffoldFlag.producer, + inventorySlug, + '--topic', + fixtureName.ordersCreated, + ]), + (error) => expect(String(error)).toMatch(/owner\/topic\/schema mismatch/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeMalformed); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), beforeMalformed); - }); -}); + }), +); -void 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({ @@ -3115,14 +3415,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({ @@ -3133,12 +3432,9 @@ export const stockAvailablePolicy = defineMicroverticalPolicy // @@ -3147,58 +3443,61 @@ export const stockAvailablePolicy = defineMicroverticalPolicy -`, - ); - assert.doesNotMatch(coreIndex, /stockAvailablePolicy/u); - assert.equal( - decodeFixturePackage(await readFixtureFile(fixture.root, inventoryPackageFile)).dependencies[ - '@app/core-runtime' - ], - workspaceVersion, - ); - const beforeDuplicate = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, 'policy', ['--scope', 'global', '--policy', fixtureName.policy]), - /refusing to overwrite/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), beforeDuplicate); - - await run(fixture, 'policy', ['--scope', 'global', '--policy', 'foo-1-bar']); - const beforeIdentifierCollision = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, 'policy', ['--scope', 'global', '--policy', 'foo1-bar']), - /Policy identifier foo1BarPolicy already exists/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), beforeIdentifierCollision); - }); -}); - -void 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', +`); + expect(coreIndex).not.toMatch(/stockAvailablePolicy/u); + expect( + decodeFixturePackage(yield* readFixtureFile(fixture.root, inventoryPackageFile)) + .dependencies['@app/core-runtime'], + ).toBe(workspaceVersion); + const beforeDuplicate = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, 'policy', ['--scope', 'global', '--policy', fixtureName.policy]), + (error) => expect(String(error)).toMatch(/refusing to overwrite/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeDuplicate); + + yield* run(fixture, 'policy', ['--scope', 'global', '--policy', 'foo-1-bar']); + const beforeIdentifierCollision = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, 'policy', ['--scope', 'global', '--policy', 'foo1-bar']), + (error) => + expect(String(error)).toMatch(/Policy identifier foo1BarPolicy already exists/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeIdentifierCollision); + }), ); - 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 = () => { @@ -3224,58 +3523,52 @@ export const PurchaseOrdersPage = () => { }; 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'; +`); + 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', @@ -3304,78 +3597,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 = decodeInventoryLocale(englishContent); + const czech = 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, + ); + }), ); - }); -}); + }), +); -void 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* writeFixtureFile( + fixture.root, + 'node_modules/@modern-js/plugin-i18n/package.json', + json({ + exports: { './runtime': './runtime.tsx' }, + name: '@modern-js/plugin-i18n', + type: 'module', + }), + ); + yield* writeFixtureFile( + fixture.root, + 'node_modules/@modern-js/plugin-i18n/runtime.tsx', + `import { createContext, useContext } from 'react'; import type { ReactNode } from 'react'; interface BoundaryValue { @@ -3404,223 +3716,247 @@ 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); - }); -}); - -void 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' }, ); }), ); - }); -}); + }), +); -void 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) => { - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(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) { + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'orders', + '--url', + url, + ]), + (error) => expect(String(error)).toMatch(/--url/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), ); - assert.deepEqual(await snapshotTree(fixture.root), before); }), - ), - ); -}); + ), + { concurrency: 'unbounded' }, + ); + }), +); -void 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* writeFixtureFile( + fixture.root, + inventoryFederationConfigFile, + `export default { exposes: { './PageInventoryStock': './src/federation-entry.tsx', }, @@ -3629,554 +3965,643 @@ void test('generates a non-navigational dynamic page with canonical parameters a }, }; `, + ); + 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); -void 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* writeFixtureFile( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/contacts/customers/[id]/contacts/[contactId]/page.tsx', + 'export default function DeveloperOwnedPage() { return null; }\n', + ); + const before = yield* snapshotTree(fixture.root); + + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, generatorArguments), + (error) => expect(String(error)).toMatch(/refusing to overwrite|already exists/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), ); - const before = await snapshotTree(fixture.root); + }), +); - await assert.rejects( - run(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) { + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.customerEditPage, + '--url', + url, + ]), + (error) => expect(String(error)).toMatch(/--url/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ); + }), + ), + { concurrency: 'unbounded' }, ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }); -}); -void 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) => { - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(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, + ]); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.customerEditPage, + '--url', + '/inventory/customers/:customerId', + ]), + (error) => + expect(String(error)).toMatch(/routing collision|already registered|collides/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ), + withFixture( + Effect.fn(function* scenario104(fixture) { + const generatorArguments = [ scaffoldFlag.vertical, inventorySlug, '--page', fixtureName.customerEditPage, '--url', - url, - ]), - /--url/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - ), - ); + 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'), + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, generatorArguments), + (error) => expect(String(error)).toMatch(/collides/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ), + withFixture( + Effect.fn(function* scenario105(fixture) { + yield* writeFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/routes/[lang]/inventory/customers/[id]/edit/page.tsx', + 'export default function PartialPage() { return null; }\n', + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.customerEditPage, + '--url', + customerEditUrl, + ]), + (error) => expect(String(error)).toMatch(/collides with nested content/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ), + withFixture( + Effect.fn(function* scenario106(fixture) { + yield* run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'customer-new', + '--url', + '/inventory/customers/new', + ]); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.customerEditPage, + '--url', + customerDetailUrl, + ]), + (error) => expect(String(error)).toMatch(/static route segment|collides/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ), + 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 }, + ), + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + fixtureName.customerEditPage, + '--url', + '/shared/customers/:id/edit', + ]), + (error) => expect(String(error)).toMatch(/already registered by billing/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ), + ], + { concurrency: 'unbounded' }, + ); + }), +); - await Promise.all([ - withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'customer-detail', - '--url', - customerDetailUrl, - ]); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - fixtureName.customerEditPage, - '--url', - '/inventory/customers/:customerId', - ]), - /routing collision|already registered|collides/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - 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', - ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.microverticalPage, generatorArguments), - /collides/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - 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', - ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - fixtureName.customerEditPage, - '--url', - customerEditUrl, - ]), - /collides with nested content/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'customer-new', - '--url', - '/inventory/customers/new', - ]); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(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, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - withFixture(async (fixture) => { - await run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - 'billing', - '--page', - fixtureName.customerEditPage, - '--url', - '/shared/customers/:id/edit', - ]); - await rm( - path.join( + ]); + yield* writeFixtureFile( fixture.root, - 'apps/shell-super-app/src/routes/[lang]/shared/customers/[id]/edit', - ), - { recursive: true }, - ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(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, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - ]); -}); - -void 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* writeFixtureFile( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/modules/[moduleId]/page.tsx', + 'export default function ModulePage() { return null; }\n', + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'customers', + '--url', + '/modules/customers', + ]), + (error) => + expect(String(error)).toMatch(/collides with dynamic route segment \[moduleId\]/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ), + withFixture( + Effect.fn(function* scenario112(fixture) { + yield* writeFixtureFile( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/login/page.tsx', + 'export default function LoginPage() { return null; }\n', + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'customers', + '--url', + '/login/customers', + ]), + (error) => expect(String(error)).toMatch(/reserved route prefix \/login/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ), + 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, + }, + ), + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'customer-list', + '--url', + '/shared/customers', + ]), + (error) => expect(String(error)).toMatch(/already registered by billing/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ), + ], + { concurrency: 'unbounded' }, ); - }); -}); + }), +); -void 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', - ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'customers', - '--url', - '/modules/customers', - ]), - /collides with dynamic route segment \[moduleId\]/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - withFixture(async (fixture) => { - await writeFixtureFile( - fixture.root, - 'apps/shell-super-app/src/routes/[lang]/login/page.tsx', - 'export default function LoginPage() { return null; }\n', - ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(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', - '/login/customers', - ]), - /reserved route prefix \/login/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - 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, - }); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.microverticalPage, [ + 'order-lines', + ]); + yield* run(fixture, scaffoldCommand.microverticalPage, [ scaffoldFlag.vertical, inventorySlug, '--page', - 'customer-list', - '--url', - '/shared/customers', - ]), - /already registered by billing/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - ]); -}); + 'order', + ]); + yield* Effect.promise(() => + stat( + path.join( + fixture.root, + 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/order/page.tsx', + ), + ), + ); + }), + ); -void 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', - ), + 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', + ), + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'orders', + '--url', + '/second/orders', + ]), + (error) => + expect(String(error)).toMatch( + /page identity inventory\.stock\.page\.orders already exists/u, + ), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ), + 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'), + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, generatorArguments), + (error) => expect(String(error)).toMatch(/already exists|collides/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ), + 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', + ), + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, generatorArguments), + (error) => expect(String(error)).toMatch(/already exists|collides/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ), + 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', + ), + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, generatorArguments), + (error) => expect(String(error)).toMatch(/already exists|collides/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ), + withFixture( + Effect.fn(function* scenario120(fixture) { + const generatorArguments = [scaffoldFlag.vertical, inventorySlug, '--page', 'orders']; + yield* run(fixture, scaffoldCommand.microverticalPage, generatorArguments); + yield* writeFixtureFile( + fixture.root, + 'apps/shell-super-app/src/routes/[lang]/inventory-stock/orders/developer-note.ts', + 'export const developerNote = true;\n', + ); + const before = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, generatorArguments), + (error) => expect(String(error)).toMatch(/already exists|collides/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), + ), + ], + { concurrency: 'unbounded' }, ); - }); + }), +); - 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', - ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.microverticalPage, [ +it.live( + 'migrates only exact legacy generated page output and then reruns as a no-op', + Effect.fn(function* scenario121() { + yield* withFixture( + Effect.fn(function* scenario122(fixture) { + const generatorArguments = [ scaffoldFlag.vertical, inventorySlug, '--page', 'orders', '--url', - '/second/orders', - ]), - /page identity inventory\.stock\.page\.orders already exists/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - 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'); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.microverticalPage, generatorArguments), - /already exists|collides/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - 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' }, - // `, - ), - 'utf-8', - ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.microverticalPage, generatorArguments), - /already exists|collides/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - 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'", - ), - 'utf-8', - ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.microverticalPage, generatorArguments), - /already exists|collides/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - 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', - ); - const before = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.microverticalPage, generatorArguments), - /already exists|collides/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), before); - }), - ]); -}); - -void test('migrates only exact legacy generated page output and then reruns as a no-op', 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'; + '/orders', + ]; + yield* run(fixture, scaffoldCommand.microverticalPage, generatorArguments); + yield* writeFixtureFile( + fixture.root, + inventoryOrdersRouteFile, + `import { useModernI18n } from '@modern-js/plugin-i18n/runtime'; import { UltramodernRouteHead } from '../../ultramodern-route-head'; export const OrdersPage = () => { @@ -4213,11 +4638,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* writeFixtureFile( + 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; @@ -4226,551 +4651,665 @@ 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 run(fixture, scaffoldCommand.microverticalPage, generatorArguments); - const migratedPage = await readFixtureFile(fixture.root, inventoryOrdersRouteFile); - assert.doesNotMatch(migratedPage, /\.description|\.empty|
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 }, + }, + }); + yield* Effect.promise(() => writeFile(localePath, json(nextCatalog), 'utf-8')); + }), + ), + { concurrency: 'unbounded' }, + ); -void 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* run(fixture, scaffoldCommand.microverticalPage, generatorArguments); + const migratedPage = yield* readFixtureFile(fixture.root, inventoryOrdersRouteFile); + expect(migratedPage).not.toMatch(/\.description|\.empty|
{ - await withFixture(async (fixture) => { - await rm( - path.join(fixture.root, 'verticals/inventory-stock/src/routes/ultramodern-route-head.tsx'), - ); - const beforeMissingHead = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'orders', - ]), - /UltramodernRouteHead is missing/u, ); - assert.deepEqual(await snapshotTree(fixture.root), beforeMissingHead); - }); + }), +); - await withFixture(async (fixture) => { - await writeFixtureFile( - fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/orders/nested.ts', - 'export {};\n', - ); - const beforeCollision = await snapshotTree(fixture.root); - await assert.rejects( - run(fixture, scaffoldCommand.microverticalPage, [ - scaffoldFlag.vertical, - inventorySlug, - '--page', - 'orders', - ]), - /collides with nested content/u, - ); - assert.deepEqual(await snapshotTree(fixture.root), beforeCollision); - }); +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 = 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* writeFixtureFile( + fixture.root, + 'verticals/inventory-stock/locales/de/inventory.json', + json({ inventory: { existing: 'de-preserved' } }), + ); + const before = yield* snapshotTree(fixture.root); - await withFixture(async (fixture) => { - await assert.rejects( - runEffectTestPromise( - runScaffoldEffect( - scaffoldCommand.microverticalPage, - [ + yield* expectFailure( + run(fixture, 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)), - ), - /route refresh fixture failure/u, - ); - await stat( - path.join( - fixture.root, - 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/orders/page.tsx', - ), + fixtureName.purchaseOrdersPage, + ]), + (error) => expect(String(error)).toMatch(/no starter translation for locale de/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(before); + }), ); - 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', + ), + ), + ); + const beforeMissingHead = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'orders', + ]), + (error) => expect(String(error)).toMatch(/UltramodernRouteHead is missing/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeMissingHead); + }), ); - 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); -}; - -void 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* writeFixtureFile( + fixture.root, + 'verticals/inventory-stock/src/routes/[lang]/inventory-stock/orders/nested.ts', + 'export {};\n', + ); + const beforeCollision = yield* snapshotTree(fixture.root); + yield* expectFailure( + run(fixture, scaffoldCommand.microverticalPage, [ + scaffoldFlag.vertical, + inventorySlug, + '--page', + 'orders', + ]), + (error) => expect(String(error)).toMatch(/collides with nested content/u), + ); + expect(yield* snapshotTree(fixture.root)).toEqual(beforeCollision); + }), ); - 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 }); - } -}); + }), +); -void 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, - inventoryActionGatewayFile, - 'verticals/inventory-stock/shared/apis/resource-detail.ts', - 'verticals/inventory-stock/src/api/resource-detail.read.ts', - 'verticals/inventory-stock/src/api/resource-detail-client.ts', - 'verticals/inventory-stock/api/resource-detail-read-server.ts', - inventorySearchContractFile, - inventorySearchProviderFile, - 'verticals/inventory-stock/src/api/inventory-items-search-client.ts', - 'verticals/inventory-stock/api/inventory-items-search-server.ts', - 'verticals/inventory-stock/shared/apis/stock-levels-report.ts', - 'verticals/inventory-stock/src/reports/stock-levels.provider.ts', - 'verticals/inventory-stock/src/api/stock-levels-report-client.ts', - 'verticals/inventory-stock/api/stock-levels-report-server.ts', - ]; - - 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`); - }), - ); - }); -}); - -void 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 }); - await symlink( - path.join(appRoot, 'packages/core-runtime/node_modules/effect'), - path.join(fixture.root, effectNodeModulePath), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/node_modules/@effect/sql-pg'), - path.join(fixture.root, 'node_modules/@effect/sql-pg'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/node_modules/@effect/platform-node'), - path.join(fixture.root, 'node_modules/@effect/platform-node'), - 'dir', - ); - await symlink( - path.join(appRoot, 'apps/shell-super-app/node_modules/jose'), - path.join(fixture.root, 'node_modules/jose'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/node_modules/drizzle-orm'), - path.join(fixture.root, 'node_modules/drizzle-orm'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/node_modules/dotenv'), - path.join(fixture.root, 'node_modules/dotenv'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/node_modules/pg'), - path.join(fixture.root, 'node_modules/pg'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/node_modules/@authzed/authzed-node'), - path.join(fixture.root, 'node_modules/@authzed/authzed-node'), - 'dir', - ); - await symlink( - path.join(appRoot, 'apps/shell-super-app/node_modules/@modern-js/plugin-i18n'), - path.join(fixture.root, 'node_modules/@modern-js/plugin-i18n'), - 'dir', - ); - await symlink( - path.join(appRoot, 'apps/shell-super-app/node_modules/@modern-js/plugin-bff'), - path.join(fixture.root, pluginBffNodeModulePath), - 'dir', - ); - await symlink( - path.join(appRoot, 'apps/shell-super-app/node_modules/@types/react'), - path.join(fixture.root, 'node_modules/@types/react'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/node_modules/@types/pg'), - path.join(fixture.root, 'node_modules/@types/pg'), - 'dir', - ); - await symlink( - path.join(appRoot, 'node_modules/@types/node'), - path.join(fixture.root, 'node_modules/@types/node'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/src/actions'), - path.join(fixture.root, 'packages/core-runtime/src/actions'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/src/db'), - path.join(fixture.root, 'packages/core-runtime/src/db'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/src/operations'), - path.join(fixture.root, 'packages/core-runtime/src/operations'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/src/database'), - path.join(fixture.root, 'packages/core-runtime/src/database'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/src/environment'), - path.join(fixture.root, 'packages/core-runtime/src/environment'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/src/permissions'), - path.join(fixture.root, 'packages/core-runtime/src/permissions'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/src/auth'), - path.join(fixture.root, 'packages/core-runtime/src/auth'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/src/authorization'), - path.join(fixture.root, 'packages/core-runtime/src/authorization'), - 'dir', - ); - await symlink( - path.join(appRoot, 'packages/core-runtime/src/modules/module-entrypoint.ts'), - path.join(fixture.root, 'packages/core-runtime/src/modules/module-entrypoint.ts'), - 'file', + 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/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')], - }, - 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* scenario132() { + yield* withFixture( + Effect.fn(function* scenario133(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, + inventoryActionGatewayFile, + 'verticals/inventory-stock/shared/apis/resource-detail.ts', + 'verticals/inventory-stock/src/api/resource-detail.read.ts', + 'verticals/inventory-stock/src/api/resource-detail-client.ts', + 'verticals/inventory-stock/api/resource-detail-read-server.ts', + inventorySearchContractFile, + inventorySearchProviderFile, + 'verticals/inventory-stock/src/api/inventory-items-search-client.ts', + 'verticals/inventory-stock/api/inventory-items-search-server.ts', + 'verticals/inventory-stock/shared/apis/stock-levels-report.ts', + 'verticals/inventory-stock/src/reports/stock-levels.provider.ts', + 'verticals/inventory-stock/src/api/stock-levels-report-client.ts', + 'verticals/inventory-stock/api/stock-levels-report-server.ts', + ]; + + yield* Effect.all( + generatedFiles.map( + Effect.fn(function* scenario134(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* scenario135() { + yield* withFixture( + Effect.fn(function* scenario136(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 }), + ); + 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, 'packages/core-runtime/node_modules/@effect/sql-pg'), + path.join(fixture.root, 'node_modules/@effect/sql-pg'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/node_modules/@effect/platform-node'), + path.join(fixture.root, 'node_modules/@effect/platform-node'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'apps/shell-super-app/node_modules/jose'), + path.join(fixture.root, 'node_modules/jose'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/node_modules/drizzle-orm'), + path.join(fixture.root, 'node_modules/drizzle-orm'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/node_modules/dotenv'), + path.join(fixture.root, 'node_modules/dotenv'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/node_modules/pg'), + path.join(fixture.root, 'node_modules/pg'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/node_modules/@authzed/authzed-node'), + path.join(fixture.root, 'node_modules/@authzed/authzed-node'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'apps/shell-super-app/node_modules/@modern-js/plugin-i18n'), + path.join(fixture.root, 'node_modules/@modern-js/plugin-i18n'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'apps/shell-super-app/node_modules/@modern-js/plugin-bff'), + path.join(fixture.root, pluginBffNodeModulePath), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'apps/shell-super-app/node_modules/@types/react'), + path.join(fixture.root, 'node_modules/@types/react'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/node_modules/@types/pg'), + path.join(fixture.root, 'node_modules/@types/pg'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'node_modules/@types/node'), + path.join(fixture.root, 'node_modules/@types/node'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/src/actions'), + path.join(fixture.root, 'packages/core-runtime/src/actions'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/src/db'), + path.join(fixture.root, 'packages/core-runtime/src/db'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/src/operations'), + path.join(fixture.root, 'packages/core-runtime/src/operations'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/src/database'), + path.join(fixture.root, 'packages/core-runtime/src/database'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/src/environment'), + path.join(fixture.root, 'packages/core-runtime/src/environment'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/src/permissions'), + path.join(fixture.root, 'packages/core-runtime/src/permissions'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/src/auth'), + path.join(fixture.root, 'packages/core-runtime/src/auth'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/src/authorization'), + path.join(fixture.root, 'packages/core-runtime/src/authorization'), + 'dir', + ), + ); + yield* Effect.promise(() => + symlink( + path.join(appRoot, 'packages/core-runtime/src/modules/module-entrypoint.ts'), + path.join(fixture.root, 'packages/core-runtime/src/modules/module-entrypoint.ts'), + 'file', + ), + ); + 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* scenario137(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/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'), + ], + }, + 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); + }), + ); + }), +); diff --git a/app/scripts/tests/api-only-tooling.test.mts b/app/scripts/tests/api-only-tooling.test.mts index 55b15871d..454f3ebf6 100644 --- a/app/scripts/tests/api-only-tooling.test.mts +++ b/app/scripts/tests/api-only-tooling.test.mts @@ -1,20 +1,22 @@ -/// +import { Cause, Effect, Predicate, Schema } from 'effect'; +import { afterEach, expect, it, rs } from '@app/effect-rstest'; -import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; 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 { transform } from 'esbuild'; import { format } from 'oxfmt'; +afterEach(() => { + rs.restoreAllMocks(); +}); + const workspaceRoot = fileURLToPath(new URL('../..', import.meta.url)); const partyId = 'party-registry'; const mfManifestPath = '/mf-manifest.json'; @@ -240,41 +242,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, - emitNodeStagedReleaseEnvelope, - verifyBuildOutputReleaseEnvelope, - verifyNodeReleaseEnvelopeStaging, - }; -}; +const loadReleaseFramework = (modulePath: string): Effect.Effect => + Effect.gen(function* scenario1() { + const source: unknown = yield* Effect.promise(() => 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, + 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 Schema.decodeUnknownSync(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[], @@ -291,15 +310,15 @@ const releaseFrameworkRoot = path.join( 'verticals/party-registry/node_modules/@modern-js/app-tools/dist', ); -const releaseFixture = async (context: TestContext) => { - const releaseFramework = await loadReleaseFramework( +const releaseFixture = Effect.fn(function* scenario5() { + const releaseFramework = yield* loadReleaseFramework( path.join(releaseFrameworkRoot, 'esm-node/ultramodern-release-envelope/framework-output.mjs'), ); - 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 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'), ); @@ -320,208 +339,272 @@ 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 () => - Schema.decodeUnknownSync(ReleaseEnvelopeSchema)( - await releaseFramework.emitFrameworkMicroVerticalReleaseEnvelope({ - apiOnly: false, - distDirectory: root, - target: 'node', - }), - ); - return { artifact, emit, framework: releaseFramework, 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 = Schema.decodeUnknownSync(ReleaseEnvelopeSchema)( - await framework.emitFrameworkMicroVerticalReleaseEnvelope({ + const emit = Effect.fn(function* scenario9() { + return Schema.decodeUnknownSync(ReleaseEnvelopeSchema)( + yield* Effect.promise(() => + releaseFramework.emitFrameworkMicroVerticalReleaseEnvelope({ apiOnly: false, - distDirectory: fixture.root, + distDirectory: 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 = Schema.decodeUnknownSync(ReleaseEnvelopeSchema)( - 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 fixture.framework.verifyBuildOutputReleaseEnvelope(fixture.root, 'node'), - /digest|hash|size/iu, - ); + ), + ); + }); + 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 = Schema.decodeUnknownSync(ReleaseEnvelopeSchema)( - await framework.emitFrameworkMicroVerticalReleaseEnvelope({ - apiOnly: false, - distDirectory: fixture.root, - target: 'node', +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'].map( + Effect.fn(function* scenario11(moduleFormat) { + const fixture = yield* releaseFixture(); + const extension = moduleFormat === 'cjs' ? 'js' : 'mjs'; + const framework = yield* loadReleaseFramework( + path.join( + releaseFrameworkRoot, + moduleFormat, + `ultramodern-release-envelope/framework-output.${extension}`, + ), + ); + const envelope = Schema.decodeUnknownSync(ReleaseEnvelopeSchema)( + yield* Effect.promise(() => + framework.emitFrameworkMicroVerticalReleaseEnvelope({ + apiOnly: false, + distDirectory: fixture.root, + target: 'node', + }), + ), + ); + expect(envelope.surfaces.uiClient.includes(compiledUiAssetPath)).toBe(true); + expect(envelope.surfaces.ssr).toEqual([ssrBundlePath]); + expect(envelope.surfaces.apiBackend).toEqual([apiBundlePath]); + yield* Effect.promise(() => + framework.verifyBuildOutputReleaseEnvelope(fixture.root, 'node'), + ); + const staged = Schema.decodeUnknownSync(ReleaseEnvelopeSchema)( + yield* Effect.promise(() => + framework.emitNodeStagedReleaseEnvelope({ + distDirectory: fixture.root, + outputDirectory: fixture.root, + }), + ), + ); + expect(staged.surfaces.uiClient.includes(compiledUiAssetPath)).toBe(true); + yield* Effect.promise(() => + framework.verifyNodeReleaseEnvelopeStaging({ outputDirectory: fixture.root }), + ); }), - ); - assert.ok(envelope.surfaces.uiClient.includes(compiledUiAssetPath)); - }), - ); -}); + ), + { 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(() => + 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'].map( + Effect.fn(function* scenario14(moduleFormat) { + const fixture = yield* releaseFixture(); + fixture.manifest.metaData.publicPath = 'auto'; + yield* fixture.putJson(mfManifestFile, fixture.manifest); + yield* fixture.putJson(routesManifestFile, { + routeAssets: { index: { assets: [`/${compiledUiAssetPath}`] } }, + }); + const extension = moduleFormat === 'cjs' ? 'js' : 'mjs'; + const framework = yield* loadReleaseFramework( + path.join( + releaseFrameworkRoot, + moduleFormat, + `ultramodern-release-envelope/framework-output.${extension}`, + ), + ); + const envelope = Schema.decodeUnknownSync(ReleaseEnvelopeSchema)( + yield* Effect.promise(() => + framework.emitFrameworkMicroVerticalReleaseEnvelope({ + apiOnly: false, + distDirectory: fixture.root, + target: 'node', + }), + ), + ); + 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 { runInNewContext } from 'node:vm'; const framework = { appTools: () => ({}), @@ -544,183 +627,219 @@ 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); + }).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, 'verticals/party-registry'); - const source = await readFile(path.join(partyRoot, 'api/index.ts'), '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 rspackModule: unknown = require(require.resolve('@rspack/core', { paths: [appToolsPath] })); - 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`, +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, 'verticals/party-registry'); + const source = yield* Effect.promise(() => + readFile(path.join(partyRoot, 'api/index.ts'), 'utf-8'), ); - const definePlugin = new rspackFixture.DefinePlugin( - Object.fromEntries( - Object.entries(globalVars).map(([key, value]) => [key, JSON.stringify(value)]), - ), + 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 rspackModule: unknown = require( + require.resolve('@rspack/core', { paths: [appToolsPath] }), + ); + const rspackFixture = Schema.decodeUnknownSync(RspackModuleFixtureSchema)(rspackModule); + const temporaryRoot = yield* Effect.promise(() => + mkdtemp(path.join(partyRoot, 'node_modules/.ontos-compiled-cors-')), ); - 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' } } }, + 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 = Schema.decodeUnknownSync(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 = Schema.decodeUnknownSync(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 = Schema.decodeUnknownSync(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, ''); -}; +}); -void test('all published scaffold formats retain lint-safe Party infrastructure 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), - ), - 'modern.config.ts': Schema.decodeUnknownSync(Schema.String)( - createAppModernConfig('app', app), - ), - 'shared/ultramodern-build.ts': Schema.decodeUnknownSync(Schema.String)( - createUltramodernBuildModule('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, 'verticals/party-registry', fileName), - 'utf-8', +it.live( + 'all published scaffold formats retain lint-safe Party infrastructure parity', + Effect.fn(function* scenario24() { + yield* Effect.all( + ['esm', 'esm-node', 'cjs'].map( + Effect.fn(function* scenario25(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) + : yield* Effect.promise(() => import(pathToFileURL(descriptorPath).href)); + const descriptorModule = + Schema.decodeUnknownSync(DescriptorModuleSchema)(descriptorSource); + const componentPath = generatorModulePath('demo-components'); + const componentSource: unknown = + moduleFormat === 'cjs' + ? require(componentPath) + : yield* Effect.promise(() => import(pathToFileURL(componentPath).href)); + const componentModule = Schema.decodeUnknownSync(ComponentModuleSchema)(componentSource); + const federationPath = generatorModulePath('module-federation/config'); + const federationSource: unknown = + moduleFormat === 'cjs' + ? require(federationPath) + : yield* Effect.promise(() => import(pathToFileURL(federationPath).href)); + const federationModule = Schema.decodeUnknownSync(FederationConfigModuleSchema)( + federationSource, ); - assert.equal( - await normalizedGeneratedSource(fileName, source), - await normalizedGeneratedSource(fileName, actual), - `${moduleFormat}: ${fileName} must match the controlled scaffold`, + const buildModulePath = generatorModulePath('module-federation/reexport-module'); + 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), + ), + 'modern.config.ts': Schema.decodeUnknownSync(Schema.String)( + createAppModernConfig('app', app), + ), + 'shared/ultramodern-build.ts': Schema.decodeUnknownSync(Schema.String)( + createUltramodernBuildModule('app', app), + ), + 'src/routes/layout.tsx': Schema.decodeUnknownSync(Schema.String)(createLayout(app.id)), + }; + yield* Effect.all( + Object.entries(generated).map( + Effect.fn(function* scenario26([fileName, source]) { + const actual = yield* Effect.promise(() => + readFile(path.join(workspaceRoot, 'verticals/party-registry', fileName), 'utf-8'), + ); + expect( + yield* normalizedGeneratedSource(fileName, source), + `${moduleFormat}: ${fileName} must match the controlled scaffold`, + ).toBe(yield* normalizedGeneratedSource(fileName, actual)); + }), + ), + { concurrency: 'unbounded' }, ); }), - ); - }), - ); -}); + ), + { concurrency: 'unbounded' }, + ); + }), +); -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'), @@ -730,13 +849,16 @@ 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* scenario28() { + const output: unknown = yield* Effect.promise(() => + validateCloudflareApp(app, applicationPublicUrl), + ); + return Schema.decodeUnknownSync(CloudflareEvidenceSchema)(output); + }); const federationValidationModule: unknown = await import( pathToFileURL( path.join(generatorRoot, 'dist/esm-node/ultramodern-workspace/mf-validation/validate.js'), @@ -795,41 +917,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], @@ -837,167 +973,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 = Schema.decodeUnknownSync(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, 'topology/reference-topology.json'), - ); - 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, 'topology/reference-topology.json'), + ); + 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, 'topology/reference-topology.json'), - ); - 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, 'topology/reference-topology.json'), + ); + 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)}; @@ -1027,31 +1210,33 @@ globalThis.fetch = async input => { return 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); + }), +); diff --git a/app/scripts/tests/audit-database-trust-boundaries.test.mts b/app/scripts/tests/audit-database-trust-boundaries.test.mts index 37fa6e226..5c08ec822 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 '@app/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..aecc28b9d 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 '@app/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/check-authorization-readiness.test.mts b/app/scripts/tests/check-authorization-readiness.test.mts index 05bb222c8..f534e48ba 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 '@app/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/database-access-boundaries.test.mts b/app/scripts/tests/database-access-boundaries.test.mts index 367058a1a..627a80d0d 100644 --- a/app/scripts/tests/database-access-boundaries.test.mts +++ b/app/scripts/tests/database-access-boundaries.test.mts @@ -1,94 +1,97 @@ -import { runEffectTestPromise } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; import { Effect } from 'effect'; import { NodeServices } from '@effect/platform-node'; -import assert from 'node:assert/strict'; +import { expect, it } from '@app/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 runEffectTestPromise( - checkDatabaseAccessBoundaries(root).pipe(Effect.provide(NodeServices.layer)), - ); - assert.deepEqual( - violations.map(({ file, line }) => `${file}:${line}`), - [ - 'apps/shell/api/routes/private.ts:1', - 'verticals/stock/api/index.ts:1', - 'verticals/stock/api/routes/export.ts:1', - 'verticals/stock/src/actions/package-root.action.ts:1', - 'verticals/stock/src/actions/reserve.action.ts:1', - 'verticals/stock/src/actions/reserve.action.ts:2', - 'verticals/stock/src/actions/side-effect.action.ts:1', - 'verticals/stock/src/db/billing-leak.ts:1', - 'verticals/stock/src/db/cross-owner.ts:1', - 'verticals/stock/src/db/dynamic-core.ts:1', - 'verticals/stock/src/reads/list.read.ts:1', - 'verticals/stock/src/reads/side-effect.read.ts:1', - '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 }); - } -}); +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.tryPromise(() => + 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* 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', + 'verticals/stock/src/actions/package-root.action.ts:1', + 'verticals/stock/src/actions/reserve.action.ts:1', + 'verticals/stock/src/actions/reserve.action.ts:2', + 'verticals/stock/src/actions/side-effect.action.ts:1', + 'verticals/stock/src/db/billing-leak.ts:1', + 'verticals/stock/src/db/cross-owner.ts:1', + 'verticals/stock/src/db/dynamic-core.ts:1', + 'verticals/stock/src/reads/list.read.ts:1', + 'verticals/stock/src/reads/side-effect.read.ts:1', + '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 { + yield* Effect.tryPromise(() => rm(root, { force: true, recursive: true })); + } + }), +); diff --git a/app/scripts/tests/initialize-local-development.test.mts b/app/scripts/tests/initialize-local-development.test.mts index 5a369e2b2..14b0a4728 100644 --- a/app/scripts/tests/initialize-local-development.test.mts +++ b/app/scripts/tests/initialize-local-development.test.mts @@ -1,11 +1,9 @@ -import { runEffectTestPromise } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; -import assert from 'node:assert/strict'; +import { expect, it } from '@app/effect-rstest'; import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import { test } from 'node:test'; import { NodeServices } from '@effect/platform-node'; -import { Effect, Exit } from 'effect'; +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'; @@ -83,189 +81,199 @@ const moduleContract = ( schemaVersion: '2', }); -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); +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); - 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, + 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, + { + ...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('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 = ({ vertical }: { readonly vertical: string }) => - Effect.succeed(moduleContract(`${vertical}.core`)); - assert.deepEqual( - await runEffectTestPromise( - deriveActivatedModuleIds(root, deriveContract).pipe(Effect.provide(NodeServices.layer)), - ), - ['party-registry.core'], - ); -}); +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); + }), +); + +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); + }), +); -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 = () => Effect.succeed(moduleContract('duplicate.core')); - await assert.rejects( - runEffectTestPromise( - deriveActivatedModuleIds(root, deriveContract, ['party-registry', 'inventory']).pipe( +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), ), - ), - LocalDevelopmentInitializationError, - ); -}); + ).toEqual(['party-registry.core']); + }), +); -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); -}); +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); + }), +); -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('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); + }), +); - const conflict = await runEffectTestPromise( - reconcileCoreContext(database, LOCAL_AUTH_USER_ID, [PARTY_REGISTRY_MODULE_ID]).pipe( - Effect.flip, - ), - ); +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"') ? [{}, {}] : []; + }), + ); - 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')); -}); + const conflict = yield* reconcileCoreContext(database, LOCAL_AUTH_USER_ID, [ + PARTY_REGISTRY_MODULE_ID, + ]).pipe(Effect.flip); -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([]), - ); + 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); + }), +); - const error = await runEffectTestPromise( - reconcileCoreContext(database, LOCAL_AUTH_USER_ID, []).pipe(Effect.flip), - ); - assert.equal(error.code, 'local_persistence_failed'); -}); +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([]), + ); -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([]); - }), - ); + const error = yield* reconcileCoreContext(database, LOCAL_AUTH_USER_ID, []).pipe(Effect.flip); + expect(error.code).toBe('local_persistence_failed'); + }), +); - 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('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..76735f0cb 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 '@app/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..1dcfbd963 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 '@app/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 b782da01f..f45a93fec 100644 --- a/app/scripts/tests/module-entrypoint-boundaries.test.mts +++ b/app/scripts/tests/module-entrypoint-boundaries.test.mts @@ -1,10 +1,9 @@ -import assert from 'node:assert/strict'; +import { expect, it } from '@app/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 { NodeServices } from '@effect/platform-node'; -import { ManagedRuntime } from 'effect'; +import { Effect } from 'effect'; import { checkModuleEntrypointBoundaries as checkModuleEntrypointBoundariesEffect } from '../check-module-entrypoint-boundaries.mts'; import { assertPublishedCrossMicroVerticalContractUsage, @@ -15,13 +14,8 @@ import { resolvePublishedContractModuleId, } from '../published-outbox-contracts.mts'; -const boundaryCheckRuntime = ManagedRuntime.make(NodeServices.layer); -const checkModuleEntrypointBoundaries = async (root: string): Promise => - await boundaryCheckRuntime.runPromise(checkModuleEntrypointBoundariesEffect(root)); - -test.after(async () => { - await boundaryCheckRuntime.dispose(); -}); +const checkModuleEntrypointBoundaries = (root: string) => + checkModuleEntrypointBoundariesEffect(root).pipe(Effect.provide(NodeServices.layer)); const ACTION_HEADER_FOR_TEST = '// @generated by OntOS Codesmith Action v1'; const ACTION_FILE = 'verticals/inventory-stock/src/actions/reserve.action.ts'; @@ -47,180 +41,188 @@ 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 makeFixture = async (): Promise => { - const root = await mkdtemp(path.join(os.tmpdir(), 'ontos-module-entrypoints-')); - await write( - root, - '.modernjs/ultramodern.json', - JSON.stringify({ - topology: { - apps: [ - { id: 'shell-super-app', path: 'apps/shell-super-app' }, - { id: 'inventory-stock', path: 'verticals/inventory-stock' }, - ], - }, - }), - ); - await write( - root, - 'apps/shell-super-app/src/routes/home/route.meta.ts', - ` +const makeFixture = () => + Effect.gen(function* testEffect2() { + const root = yield* Effect.tryPromise(() => + mkdtemp(path.join(os.tmpdir(), 'ontos-module-entrypoints-')), + ); + yield* write( + root, + '.modernjs/ultramodern.json', + JSON.stringify({ + topology: { + apps: [ + { id: 'shell-super-app', path: 'apps/shell-super-app' }, + { id: 'inventory-stock', path: 'verticals/inventory-stock' }, + ], + }, + }), + ); + yield* write( + root, + 'apps/shell-super-app/src/routes/home/route.meta.ts', + ` 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( - root, - 'apps/shell-super-app/src/routes/ultramodern-route-metadata.ts', - `export const routes = [{ entrypoint: { entrypointKey: 'shell-super-app.page.home' } }];`, - ); - await write( - root, - 'apps/shell-super-app/shared/api.ts', - `export const api = HttpApi.make('shell').add(GatewayContextApiGroup); + ); + yield* write( + root, + 'apps/shell-super-app/src/routes/ultramodern-route-metadata.ts', + `export const routes = [{ entrypoint: { entrypointKey: 'shell-super-app.page.home' } }];`, + ); + yield* write( + root, + 'apps/shell-super-app/shared/api.ts', + `export const api = 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( - root, - 'apps/shell-super-app/api/index.ts', - `export const issueGatewayContext = true; + ); + yield* write( + root, + 'apps/shell-super-app/api/index.ts', + `export const issueGatewayContext = true; export const issueApiKeyGatewayContext = true;`, - ); - await write( - root, - 'verticals/inventory-stock/src/routes/orders/route.meta.ts', - ` + ); + yield* write( + root, + 'verticals/inventory-stock/src/routes/orders/route.meta.ts', + ` 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( - 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( - root, - 'packages/shared-contracts/src/gateway-context.ts', - `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;`); - return root; -}; - -void 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 }); - } -}); + ); + yield* write( + root, + 'verticals/inventory-stock/src/routes/ultramodern-route-metadata.ts', + `export const routes = [{ entrypoint: { entrypointKey: 'inventory.stock.page.orders' } }];`, + ); + yield* write(root, ACTION_FILE, validAction); + yield* write(root, WORKER_FILE, validWorker); + yield* write( + root, + 'packages/shared-contracts/src/gateway-context.ts', + `export const shellGatewayContextContract = { issueGatewayContextPath: '/shell-super-app-api/auth/gateway-context', issueApiKeyGatewayContextPath: '/shell-super-app-api/auth/api-key/gateway-context' };`, + ); + yield* write(root, 'packages/core-runtime/src/index.ts', `export const core = true;`); + return root; + }); -const writeGovernedModuleApi = async (root: string): Promise => { - const vertical = 'verticals/inventory-stock'; - const header = '// @generated by OntOS Codesmith module-api v1\n'; - await write( - root, - `${vertical}/shared/api.ts`, - `import { StockListApi } from './apis/stock-list.ts'; +it.live('accepts governed generated Actions, pages, Workers, catalogs, and route manifests', () => + Effect.gen(function* testEffect3() { + const root = yield* makeFixture(); + try { + yield* checkModuleEntrypointBoundaries(root); + } finally { + yield* Effect.tryPromise(() => rm(root, { force: true, recursive: true })); + } + }), +); + +const writeGovernedModuleApi = (root: string) => + Effect.gen(function* testEffect4() { + const vertical = 'verticals/inventory-stock'; + const header = '// @generated by OntOS Codesmith module-api v1\n'; + yield* write( + root, + `${vertical}/shared/api.ts`, + `import { StockListApi } from './apis/stock-list.ts'; export const endpoint = HttpApiEndpoint.post('listStock', '/stock/list'); export const api = HttpApi.make('InventoryApi').addHttpApi(StockListApi);`, - ); - await write( - root, - `${vertical}/shared/apis/stock-list.ts`, - `${header}export const StockListApi = HttpApi.make('StockListApi');`, - ); - await write( - root, - `${vertical}/src/api/stock-list.read.ts`, - `${header}export const stockListEntrypoint = defineTenantModuleEntrypoint({ access: 'read', authorization: { kind: 'context_permission', permission: 'module.access' }, entrypointKey: 'inventory.stock.api.stock-list', moduleKey: 'inventory.stock', role: 'api' }); + ); + yield* write( + root, + `${vertical}/shared/apis/stock-list.ts`, + `${header}export const StockListApi = HttpApi.make('StockListApi');`, + ); + yield* write( + root, + `${vertical}/src/api/stock-list.read.ts`, + `${header}export const stockListEntrypoint = defineTenantModuleEntrypoint({ access: 'read', authorization: { kind: 'context_permission', permission: 'module.access' }, entrypointKey: 'inventory.stock.api.stock-list', moduleKey: 'inventory.stock', role: 'api' }); export const stockListRead = defineRead({ entrypoint: stockListEntrypoint, legalEntityScope: 'optional', permissionTarget: 'tenant', policies: [] });`, - ); - await write( - root, - `${vertical}/src/api/stock-list-client.ts`, - `${header}const client = makeEffectHttpApiClient(StockListApi); + ); + yield* write( + root, + `${vertical}/src/api/stock-list-client.ts`, + `${header}const client = makeEffectHttpApiClient(StockListApi); export const execute = () => operationGateway.invoke((authorization) => client.pipe(setHeaders({ authorization, 'x-correlation-id': 'required' })));`, - ); - await write( - root, - `${vertical}/api/stock-list-read-server.ts`, - `${header}export const stockListReadApiLive = HttpApiBuilder.group(StockListApi, 'reads', () => verifyOperationPrincipal(authorization).pipe(Effect.flatMap(() => ReadRuntime), Effect.flatMap((runtime) => runtime.runRead({ registration: stockListRead }))));`, - ); - await write( - root, - `${vertical}/api/auth/action-principal.ts`, - `// @generated by OntOS Codesmith MicroVertical Action Boundary v1 + ); + yield* write( + root, + `${vertical}/api/stock-list-read-server.ts`, + `${header}export const stockListReadApiLive = HttpApiBuilder.group(StockListApi, 'reads', () => verifyOperationPrincipal(authorization).pipe(Effect.flatMap(() => ReadRuntime), Effect.flatMap((runtime) => runtime.runRead({ registration: stockListRead }))));`, + ); + yield* write( + root, + `${vertical}/api/auth/action-principal.ts`, + `// @generated by OntOS Codesmith MicroVertical Action Boundary v1 export const verifyOperationPrincipal = true;`, - ); - await write( - root, - `${vertical}/src/api/action-gateway.ts`, - `// @generated by OntOS Codesmith MicroVertical Action Boundary v1 + ); + yield* write( + root, + `${vertical}/src/api/action-gateway.ts`, + `// @generated by OntOS Codesmith MicroVertical Action Boundary v1 export const operationGateway = true;`, - ); - await write( - root, - `${vertical}/vertical.manifest.ts`, - `import { StockListApi } from './shared/apis/stock-list.ts'; + ); + yield* write( + root, + `${vertical}/vertical.manifest.ts`, + `import { StockListApi } from './shared/apis/stock-list.ts'; export const manifest = { api: { 'stock-list': StockListApi, } };`, - ); - await write( - root, - `${vertical}/vertical.registration.ts`, - `export const registration = { api: { 'stock-list': () => import('./src/api/stock-list-client.ts'), } }; + ); + yield* write( + root, + `${vertical}/vertical.registration.ts`, + `export const registration = { api: { 'stock-list': () => import('./src/api/stock-list-client.ts'), } }; // // // // // // `, - ); -}; + ); + }); -void test('accepts only a complete generated governed module API seam', async () => { - const root = await makeFixture(); - try { - await writeGovernedModuleApi(root); - await write( - root, - 'verticals/inventory-stock/shared/apis/inventory-search.ts', - `// @generated by OntOS Codesmith Governed Contribution v1 +it.live('accepts only a complete generated governed module API seam', () => + Effect.gen(function* testEffect5() { + const root = yield* makeFixture(); + try { + yield* writeGovernedModuleApi(root); + yield* write( + root, + 'verticals/inventory-stock/shared/apis/inventory-search.ts', + `// @generated by OntOS Codesmith Governed Contribution v1 // @ontos-contribution-kind search-provider export const InventorySearchApi = HttpApi.make('InventorySearchApi');`, - ); - await write( - root, - 'verticals/inventory-stock/src/search/inventory.provider.ts', - `// @generated by OntOS Codesmith Governed Contribution v1 + ); + yield* write( + root, + 'verticals/inventory-stock/src/search/inventory.provider.ts', + `// @generated by OntOS Codesmith Governed Contribution v1 // @ontos-contribution-kind search-provider export const inventoryProvider = true;`, - ); - await checkModuleEntrypointBoundaries(root); + ); + yield* checkModuleEntrypointBoundaries(root); - await write( - root, - 'verticals/inventory-stock/src/api/stock-list.read.ts', - `// @generated by OntOS Codesmith module-api v1 + yield* write( + root, + 'verticals/inventory-stock/src/api/stock-list.read.ts', + `// @generated by OntOS Codesmith module-api v1 export const stockListEntrypoint = defineTenantModuleEntrypoint({ access: 'historical_read', authorization: { kind: 'context_permission', permission: 'module.access' }, entrypointKey: 'inventory.stock.api.stock-list', moduleKey: 'inventory.stock', role: 'api' }); export const stockListRead = defineRead({ entrypoint: stockListEntrypoint, legalEntityScope: 'optional', permissionTarget: 'tenant', policies: [] });`, - ); - await checkModuleEntrypointBoundaries(root); + ); + yield* checkModuleEntrypointBoundaries(root); - await write( - root, - 'verticals/inventory-stock/vertical.registration.ts', - `export const registration = { api: { 'stock-list': () => + yield* write( + root, + 'verticals/inventory-stock/vertical.registration.ts', + `export const registration = { api: { 'stock-list': () => import('./src/api/stock-list-client.ts'), } }; // // @@ -228,22 +230,22 @@ export const stockListRead = defineRead({ entrypoint: stockListEntrypoint, legal // // // `, - ); - await checkModuleEntrypointBoundaries(root); - - await write( - root, - 'verticals/inventory-stock/api/stock-list-read-server.ts', - `// @generated by OntOS Codesmith module-api v1\nexport 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* checkModuleEntrypointBoundaries(root); + + yield* write( + root, + 'verticals/inventory-stock/api/stock-list-read-server.ts', + `// @generated by OntOS Codesmith module-api v1\nexport const server = true;`, + ); + expect(String(yield* Effect.flip(checkModuleEntrypointBoundaries(root)))).toMatch( + /module APIs require an approved Codesmith generator/u, + ); + } finally { + yield* Effect.tryPromise(() => rm(root, { force: true, recursive: true })); + } + }), +); const violations = [ { @@ -304,68 +306,83 @@ const violations = [ ] as const; for (const violation of violations) { - void 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(); + try { + 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); + } finally { + yield* Effect.tryPromise(() => rm(root, { force: true, recursive: true })); + } + }), + ); } -void test('rejects missing headers, spoofed metadata, and stale generated descriptors', async () => { - const root = await makeFixture(); - try { - await write( - root, - ACTION_FILE, - `${ACTION_HEADER_FOR_TEST} +it.live('rejects missing headers, spoofed metadata, and stale generated descriptors', () => + Effect.gen(function* testEffect7() { + const root = yield* makeFixture(); + try { + yield* write( + root, + ACTION_FILE, + `${ACTION_HEADER_FOR_TEST} // @ontos-action-owner inventory.stock // @ontos-action-slug reserve 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( - root, - ACTION_FILE, - `${ACTION_HEADER_FOR_TEST} + ); + 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} // @ontos-action-owner inventory.stock // @ontos-action-slug reserve // defineTenantModuleEntrypoint({ access: 'write', entrypointKey: 'inventory.stock.reserve', moduleKey: 'inventory.stock', role: 'action' }); export const spoofed = true; `, - ); - await assert.rejects(checkModuleEntrypointBoundaries(root), /scaffold:action/u); - await write(root, ACTION_FILE, validAction); - await write( - root, - WORKER_FILE, - `// @generated by OntOS Codesmith Outbox Worker v1 + ); + 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 // @ontos-outbox-worker-key inventory.stock.project // @ontos-outbox-worker-owner inventory.stock 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, + ); + } finally { + yield* Effect.tryPromise(() => rm(root, { force: true, recursive: true })); + } + }), +); const partyResourceSource = `// @generated by OntOS Codesmith Resource v1 // @ontos-resource-owner party.registry @@ -443,125 +460,110 @@ const assertPublishedPartyUsage = ( ...overrides, }); -void 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']); }); -void 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); }); -void 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); }); -void 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); }); -void 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); }); -void 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, ); }); -void 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, ); }); -void 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, ); } }); -void 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); }); -void 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) => { @@ -574,62 +576,56 @@ void test('accepts an exact published Party Registry Effect client aggregate', ( throw new Error(`unexpected provider source ${target}`); }, }), - ); + ).not.toThrow(); }); -void 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); }); -void 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, - ); - } -}); - -void 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); }); -void 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) => { @@ -645,32 +641,30 @@ void test('accepts an exact generated MicroVertical command client leaf', () => throw new Error(`unexpected provider source ${target}`); }, }), - ); + ).not.toThrow(); }); -void 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); }); -void 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', @@ -678,8 +672,7 @@ void 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, ); } @@ -699,72 +692,70 @@ export const OutboxPayloadSchema = Schema.Struct({ partyId: Schema.String }); export const outboxProducerModuleKey = '${PARTY_MODULE_ID}' as const; `; -void 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(); }); -void 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); }); -void 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); }); -void 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, - ); -}); -void test('rejects missing, orphaned, and cross-owner route manifest entries', async () => { - const root = await makeFixture(); - try { - await 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 }); - } +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); }); +it.live('rejects missing, orphaned, and cross-owner route manifest entries', () => + Effect.gen(function* testEffect8() { + const root = yield* makeFixture(); + try { + yield* write( + root, + 'apps/shell-super-app/src/routes/ultramodern-route-metadata.ts', + `export const routes = [{ entrypoint: { entrypointKey: 'inventory.stock.page.orders' } }];`, + ); + expect(String(yield* Effect.flip(checkModuleEntrypointBoundaries(root)))).toMatch( + /manifest is stale/u, + ); + } finally { + yield* Effect.tryPromise(() => rm(root, { force: true, recursive: true })); + } + }), +); diff --git a/app/scripts/tests/outbox-worker-delivery.test.mts b/app/scripts/tests/outbox-worker-delivery.test.mts index bb05b450d..d9b227331 100644 --- a/app/scripts/tests/outbox-worker-delivery.test.mts +++ b/app/scripts/tests/outbox-worker-delivery.test.mts @@ -1,11 +1,10 @@ -import assert from 'node:assert/strict'; +import { expect, it } from '@app/effect-rstest'; import { spawn } from 'node:child_process'; import { once } from 'node:events'; import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import { test } from 'node:test'; -import { Option, Schema } from 'effect'; +import { Effect, Option, Schema } from 'effect'; import { generateOutboxWorkerDeployment } from '../generate-outbox-worker-deployment.mjs'; import { materializeOutboxWorker } from '../materialize-outbox-worker.mjs'; @@ -23,147 +22,184 @@ const decodeExitEvent = Schema.decodeUnknownSync( ); const decodeDataEvent = Schema.decodeUnknownSync(Schema.Tuple([Schema.Unknown])); -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; -}; - -void test('generates a separate supervised worker setup without changing owner configuration', async () => { - const root = await makeFixture(); - try { - 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 }); - } -}); - -void test('materializes and starts a relocatable production worker artifact', async () => { - const root = await makeFixture(); - try { - 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 makeFixture = () => + Effect.gen(function* testEffect1() { + const root = yield* Effect.tryPromise(() => + mkdtemp(path.join(os.tmpdir(), 'ontos-worker-artifact-')), + ); + 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', + }), + ), ); - let diagnostics = ''; - child.stdout.on('data', (data) => { - diagnostics += String(data); - }); - 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); - const artifact = decodeWorkerArtifact( - await readFile( - path.join(root, '.zerops/runtime/ledger-worker/worker-artifact.json'), - 'utf-8', + 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', ), ); - assert.equal(artifact.serviceId, 'ledger-worker'); - assert.equal( - artifact.sourceInputs.some((input: string) => input.endsWith(WORKER_HOST_ENTRY)), - true, + 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, + }, + ], + }), + ), ); - 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 }); - } -}); + return root; + }); -void 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: \$\{party-registry_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('generates a separate supervised worker setup without changing owner configuration', () => + Effect.gen(function* testEffect2() { + const root = yield* makeFixture(); + try { + 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 = 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, + ); + } finally { + yield* Effect.tryPromise(() => rm(root, { force: true, recursive: true })); + } + }), +); + +it.live('materializes and starts a relocatable production worker artifact', () => + Effect.gen(function* testEffect3() { + const root = yield* makeFixture(); + try { + 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'], + }, + ); + let diagnostics = ''; + child.stdout.on('data', (data) => { + diagnostics += String(data); + }); + child.stderr.on('data', (data) => { + diagnostics += String(data); + }); + const exitEvent: unknown = yield* Effect.tryPromise(() => once(child, 'exit')); + const [statusOption] = decodeExitEvent(exitEvent); + const status = Option.getOrNull(statusOption); + expect(status, diagnostics).toBe(0); + const artifact = decodeWorkerArtifact( + yield* Effect.tryPromise(() => + readFile(path.join(root, '.zerops/runtime/ledger-worker/worker-artifact.json'), 'utf-8'), + ), + ); + 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 = yield* Effect.tryPromise(() => + once(runtime.stdout, 'data', { + signal: AbortSignal.timeout(3000), + }), + ); + const [workerOutput] = decodeDataEvent(dataEvent); + const output = String(workerOutput); + expect(output).toMatch(/worker-started/u); + runtime.kill('SIGTERM'); + } finally { + yield* Effect.tryPromise(() => rm(root, { force: true, recursive: true })); + } + }), +); + +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: \$\{party-registry_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); + }), +); -void 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'); - const artifact = decodeWorkerArtifact( - await readFile(path.join(runtimeDir, 'worker-artifact.json'), 'utf-8'), +it.live('bundles the real Party host including the production Effect HTTP health adapter', () => + Effect.gen(function* testEffect5() { + const runtimeDir = yield* Effect.tryPromise(() => + mkdtemp(path.join(os.tmpdir(), 'ontos-party-worker-bundle-')), ); - 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 }); - } -}); + try { + 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( + 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'); + } finally { + yield* Effect.tryPromise(() => rm(runtimeDir, { force: true, recursive: true })); + } + }), +); diff --git a/app/scripts/tests/plan-deployment-impact.test.mts b/app/scripts/tests/plan-deployment-impact.test.mts index 44781b7a1..b549c4c6a 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 '@app/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 } 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,110 +87,130 @@ const SHELL_OWNER = { const OWNERSHIP_PATH = 'topology/ownership.json'; const DOCUMENTATION_PATH = 'docs/README.md'; -const deploymentImpactRuntime = ManagedRuntime.make(NodeServices.layer); -const planDeploymentImpact = async (options: PlanDeploymentImpactOptions) => - await deploymentImpactRuntime.runPromise(planDeploymentImpactEffect(options)); +const planDeploymentImpact = (options: PlanDeploymentImpactOptions) => + planDeploymentImpactEffect(options).pipe(Effect.provide(NodeServices.layer)); -test.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 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 }); - } -}; - -void 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( @@ -192,134 +222,161 @@ const runGit = (root: string, argumentsList: readonly string[]): string => }, ).trim(); -void 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']); + }), ); - }); -}); - -void 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, + ]); + }), ); - }); -}); - -void 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); - }); -}); - -void 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', ]) { - void 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']); + }), ); - }); - }); + }), + ); } for (const changedPath of [ 'scripts/postgres/bootstrap-spicedb-database.mts', 'packages/core-runtime/src/install/spicedb-database-config.ts', ]) { - void test(`includes the migrator, SpiceDB, and every consumer for SpiceDB database bootstrap change ${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( + `includes the migrator, SpiceDB, and every consumer for SpiceDB database bootstrap change ${changedPath}`, + () => + Effect.gen(function* testEffect16() { + yield* withFixture((root) => + Effect.gen(function* testEffect17() { + const plan = yield* planDeploymentImpact({ + changedPaths: [changedPath], + rootDirectory: root, + }); + expect(plan.phases.map((phase) => phase.id)).toEqual([ + 'migrator', + 'spicedb', + 'contacts', + SHELL_ID, + ]); + }), + ); + }), + ); } -void 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]); + }), ); - }); -}); - -void 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]); + }), ); - }); -}); - -void 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 [ 'pnpm-lock.yaml', @@ -333,171 +390,229 @@ for (const changedPath of [ 'zerops.yaml', 'topology/reference-topology.json', ]) { - void 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, + ]); + }), ); - }); - }); + }), + ); } -void 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, []); - }); -}); - -void 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, ); - }); -}); - -void 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 }, - ); -}); - -void 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, + ); + }), ); - }); -}); - -void 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 }, ); - }); -}); - -void 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'] }, - ); -}); - -void 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); + }), ); - }); -}); - -void 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); + }), ); - }); -}); - -void 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); - }); -}); - -void 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 = { @@ -572,7 +687,7 @@ const withoutImpactEvidence = ( input: AuthorizationPromotionGateInput, ): AuthorizationPromotionGateInput => { const { impact, ...remaining } = input; - assert.ok(impact); + expect(impact !== undefined).toBe(true); return remaining; }; @@ -580,7 +695,7 @@ const withoutNegativeSmokeEvidence = ( input: AuthorizationPromotionGateInput, ): AuthorizationPromotionGateInput => { const { negativeSmoke, ...remaining } = input; - assert.ok(negativeSmoke); + expect(negativeSmoke !== undefined).toBe(true); return remaining; }; @@ -588,12 +703,12 @@ const withoutReadinessEvidence = ( input: AuthorizationPromotionGateInput, ): AuthorizationPromotionGateInput => { const { readiness, ...remaining } = input; - assert.ok(readiness); + expect(readiness !== undefined).toBe(true); return remaining; }; -void 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', @@ -603,22 +718,23 @@ void test('requires exact impact, readiness, and negative-smoke evidence for enf 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); }); -void 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)), @@ -628,28 +744,23 @@ void test('report-only promotion is bounded, explicit-baseline-only, and never a 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..855d935b7 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 '@app/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 274234c80..6c95a61d7 100644 --- a/app/scripts/tests/provision-current-action-authorization.test.mts +++ b/app/scripts/tests/provision-current-action-authorization.test.mts @@ -1,13 +1,15 @@ +import type { deriveOntosModuleDeploymentContract as DeriveModuleContract } from '../generate-ontos-module-contract.mts'; + +import { expect, it } from '@app/effect-rstest'; import { NodeServices } from '@effect/platform-node'; -import { runEffectTestPromise } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; -import assert from 'node:assert/strict'; + 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, Schema, Predicate } from 'effect'; +import { Cause, Effect, Option, Schema } from 'effect'; import { ACTION_AUTHORIZATION_DENIED_PRINCIPAL_ID, ActionAuthorizationProvisioningError, @@ -20,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'; @@ -98,239 +100,293 @@ 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'); -}; - -void test('selects only exact source-controlled development and stage targets', async () => { - const development = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget(developmentConfiguration), - ); - 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); +const failureOf = (effect: Effect.Effect) => + Effect.gen(function* testEffect1() { + return yield* Effect.flip(effect); + }); - const { deploymentEnvironment: _environment, ...withoutEnvironment } = developmentConfiguration; - const implicitDevelopment = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget(withoutEnvironment), +const rejectionOf = ( + effect: Effect.Effect, +) => + effect.pipe( + Effect.matchCause({ + onFailure: Cause.squash, + onSuccess: () => { + throw new Error('Expected the Effect to fail'); + }, + }), ); - assert.deepEqual(implicitDevelopment.contexts, development.contexts); - assert.equal(implicitDevelopment.environment, 'development'); - const ipv6Development = await runEffectTestPromise( - selectActionAuthorizationProvisioningTarget({ +it.live( + '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 stage = yield* selectActionAuthorizationProvisioningTarget(stageConfiguration); + expect(stage.environment).toBe('stage'); + expect(stage.contexts.length).toBe(2); + + 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.live( + '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.live( + 'workspace validation rejects both provisioning spellings in every automatic startup path', + Effect.fn(function* testEffect5() { + const source = yield* Effect.tryPromise({ + catch: (error) => error, + try: () => + 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.tryPromise({ + catch: (error) => error, + try: () => mkdtemp(path.join(os.tmpdir(), 'ontos-workspace-validation-')), + }); + 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({ + catch: (error) => error, + try: () => + 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({ + catch: (error) => error, + try: () => import(pathToFileURL(modulePath).href), + }); + }); - try { - await validate({}); - const validationPromises: Promise[] = []; - for (const command of [ - 'node ./scripts/provision-current-action-authorization.mts', - 'pnpm authorization:provision-current-actions', - ]) { - for (const file of [ - 'scripts/initialize-local-development.mts', - 'scripts/locki-feature.sh', - 'docker-compose.yml', - 'scripts/run-zerops-spicedb.sh', + try { + yield* validate({}); + const validationPromises: Effect.Effect[] = []; + for (const command of [ + 'node ./scripts/provision-current-action-authorization.mts', + 'pnpm authorization:provision-current-actions', ]) { + for (const file of [ + 'scripts/initialize-local-development.mts', + 'scripts/locki-feature.sh', + 'docker-compose.yml', + 'scripts/run-zerops-spicedb.sh', + ]) { + validationPromises.push( + validate({ [file]: command }).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/must not provision Action authorization/u), + ), + ), + ); + } + for (const automaticScript of ['dev', 'build', 'cloudflare:build', 'cloudflare:deploy']) { + validationPromises.push( + validate({}, { [automaticScript]: command }).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/must not invoke Action authorization provisioning/u), + ), + ), + ); + } validationPromises.push( - assert.rejects(validate({ [file]: command }), /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, - ), - ); - } - validationPromises.push( - assert.rejects( validate( {}, { 'local:initialize': `${scripts['local:initialize']} && ${command}`, }, + ).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/must not provision Action authorization/u), + ), ), - /must not provision Action authorization/u, - ), - ); + ); + } + yield* Effect.all(validationPromises, { concurrency: 'unbounded' }); + } finally { + yield* Effect.tryPromise({ + catch: (error) => error, + try: () => rm(validationRoot, { recursive: true }), + }); } - 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, - ); + }), +); + +it.live( + '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* Effect.tryPromise({ + catch: (error) => error, + try: () => discoverCurrentActionKeys(workspaceRoot), + }), + ).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.live( + '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; @@ -405,46 +461,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.live( + '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.live( + '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, @@ -462,237 +518,329 @@ 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.live( + '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.live( + '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.live( + '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.live( + '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.live( + '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 })), - ]); -}; +) => + Effect.gen(function* testEffect18() { + yield* Effect.tryPromise({ + catch: (error) => error, + try: () => 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' }, + ); + }); -void test('rejects incomplete and duplicate public Action discovery', async () => { - const workspaceRoot = path.resolve(import.meta.dirname, '../..'); - const currentContract = await runEffectTestPromise( - deriveOntosModuleDeploymentContract({ +it.live( + '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; - assert.ok(currentPublicAction !== undefined); - const root = await mkdtemp(path.join(os.tmpdir(), 'ontos-action-discovery-')); - try { - const vertical = { id: 'example', package: '@app/example', path: 'verticals/example' }; - await writeInventory(root, [vertical]); - const incomplete: typeof deriveOntosModuleDeploymentContract = () => - Effect.succeed({ - ...currentContract, - deployment: { ...currentContract.deployment, appId: 'example' }, - manifest: { - ...currentContract.manifest, - publicSurface: { ...currentContract.manifest.publicSurface, actions: [] }, - }, - }); - const incompleteError = await rejectionOf(discoverCurrentActionKeys(root, incomplete)); - assert.ok(Schema.is(ActionAuthorizationProvisioningError)(incompleteError)); - assert.equal(incompleteError.code, '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' }], + }).pipe(Effect.provide(NodeServices.layer)); + const [currentPublicAction] = currentContract.manifest.publicSurface.actions; + expect(currentPublicAction !== undefined).toBe(true); + const root = yield* Effect.tryPromise({ + catch: (error) => error, + try: () => mkdtemp(path.join(os.tmpdir(), 'ontos-action-discovery-')), + }); + try { + const vertical = { id: 'example', package: '@app/example', path: 'verticals/example' }; + 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 incompleteError = yield* rejectionOf( + Effect.tryPromise({ + catch: (error) => error, + try: () => discoverCurrentActionKeys(root, incomplete), + }), + ); + 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( + Effect.tryPromise({ + catch: (error) => error, + try: () => discoverCurrentActionKeys(root, duplicate), + }), + ); + expect(Schema.is(NativeProvisioningError)(duplicateError)).toBe(true); + expect(Schema.decodeUnknownSync(NativeProvisioningError)(duplicateError).code).toBe( + 'action_authorization_discovery_failed', + ); + + yield* writeInventory(root, [vertical, vertical]); + yield* Effect.tryPromise({ + catch: (error) => error, + try: () => discoverCurrentActionKeys(root, duplicate), + }).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(NativeProvisioningError), + ), + ); + } finally { + yield* Effect.tryPromise({ + catch: (error) => error, + try: () => rm(root, { recursive: true }), }); - const duplicateError = await rejectionOf(discoverCurrentActionKeys(root, duplicate)); - assert.ok(Schema.is(ActionAuthorizationProvisioningError)(duplicateError)); - assert.equal(duplicateError.code, 'action_authorization_discovery_failed'); - - await writeInventory(root, [vertical, vertical]); - await assert.rejects( - discoverCurrentActionKeys(root, duplicate), - ActionAuthorizationProvisioningError, + } + }), +); + +it.live( + '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', + ]), ); - } 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(error.code).toBe('action_authorization_configuration_invalid'); + expect(error.reason).toMatch(/no command-line arguments/u); + }), +); diff --git a/app/scripts/tests/quality-audit-model.test.mts b/app/scripts/tests/quality-audit-model.test.mts index 39cabace6..9cb71de68 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 '@app/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'; @@ -44,297 +45,165 @@ 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: { effect: '4.0.0-beta.107' }, - name: 'remote-controls', - private: true, - type: 'module', - }), - ); - 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 = realpathSync(mkdtempSync(path.join(tmpdir(), 'ontos-knip-model-'))); + 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/*'], }), ); - const consumerPath = path.join(root, '.codex/knip-model/consumers.mts'); - const model = await runEffectTestPromise( - buildKnipModel(root, base, consumerPath).pipe(Effect.provide(NodeServices.layer)), + 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 run = await runEffectTestPromise( - runPinnedKnip(root, consumerPath, model).pipe(Effect.provide(NodeServices.layer)), + 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', + }), ); - 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), + write(root, `${resolverOwner}/index.js`, fixtureModuleSource); + write( + root, + `${resolverTarget}/package.json`, + yield* stringify({ main: 'index.js', name: rspackPackageName }), ); - 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( - !findings('exports').includes( - 'verticals/remote/shared/ultramodern-build.ts#declaredBuildIdentity', - ), + 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'), ); - assert.ok( - findings('exports').includes( - 'verticals/remote/shared/ultramodern-build.ts#unusedBuildNeighbor', - ), + write( + root, + 'src/own-resolver.ts', + [ + ...requirePrelude, + `require.resolve('oxc-parser', { paths: [${JSON.stringify(root)}] });`, + ].join('\n'), ); - assert.ok(!findings('exports').includes('tools/oxlint/effect-native/report.config.ts#default')); - assert.ok( - findings('exports').includes( - 'tools/oxlint/effect-native/report.config.ts#unusedConfigNeighbor', - ), + write( + root, + 'verticals/remote/package.json', + yield* stringify({ + dependencies: { effect: '4.0.0-beta.107' }, + name: 'remote-controls', + private: true, + type: 'module', + }), ); - 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.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( - !model.evidence.some( - (item) => item.kind === 'resolver' && item.source === 'src/own-resolver.ts', - ), + write( + root, + 'verticals/remote/src/index.ts', + "void import('childRemote/Public'); void import('misspelledChild/Public'); void import('declaredRemote/Public');", ); - assert.ok( - model.evidence.some( - (item) => - item.kind === 'resolver' && - item.source === resolverFile && - item.target === rspackPackageName && - item.line === 3 && - item.column === 17 && - item.resolved !== undefined, - ), + write( + root, + 'verticals/remote/module-federation.config.ts', + "export default {remotes: {childRemote:'child@https://example.test/remote.js'}};", ); - assert.ok( - !model.evidence.some((item) => item.kind === 'resolver' && item.source === directFile), + 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'), ); - assert.ok( - model.evidence.some( - (item) => item.target === 'src/schema.ts#registeredSchema' && item.kind === 'export', - ), + write( + root, + 'verticals/remote/shared/ultramodern-build.ts', + 'export const declaredBuildIdentity = 1; export const unusedBuildNeighbor = 2;', ); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); - -await test('modeling fails on invalid source instead of silently losing consumer evidence', async () => { - const root = await fixture(); - try { - 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 }, + write( + root, + 'tools/oxlint/effect-native/report.mts', + "runOxlint(join(pluginDirectory, 'report.config.ts'), []);", ); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); - -await test('the model regression harness uses the repository-pinned Knip version', async () => { - const manifest = await runEffectTestPromise( - 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'); -}); - -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 { write( root, - 'quality-audit/scope.json', - await stringify({ - exclude: [], - patterns: [sourcePattern, configurationFiles, 'verticals/**/*.{ts,mts}'], - }), + '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, - 'quality-audit/knip.json', - await stringify({ + '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(); + try { + const base = yield* Schema.decodeUnknownEffect(KnipConfigSchema)({ workspaces: { '.': { drizzle: { config: [] }, @@ -345,159 +214,328 @@ await test('runner calibrates only the proven resolver record and retains the di }, 'verticals/*': { entry: [indexFile, configurationFiles], project: ['**/*.{ts,mts}'] }, }, - }), - ); - write( - root, - '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); - const summary = Schema.decodeUnknownSync( - Schema.fromJsonString( - Schema.Struct({ - results: Schema.Array( - Schema.Struct({ - coverage: Schema.Struct({ - findingCounts: Schema.Record(Schema.String, Schema.Number), - modeledUsages: Schema.Number, - nativeFindingCounts: Schema.Record(Schema.String, Schema.Number), - }), - name: Schema.String, - }), + }); + const consumerPath = path.join(root, '.codex/knip-model/consumers.mts'); + 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); + 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}`), + ); + 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', + ), + ).toBe(true); + expect( + findings('exports').includes( + 'verticals/remote/shared/ultramodern-build.ts#unusedBuildNeighbor', + ), + ).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, + ); + 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, + ); + expect(findings('unlisted').includes('verticals/remote/src/index.ts#declaredRemote')).toBe( + true, + ); + expect(findings('dependencies').includes('verticals/remote/package.json#effect')).toBe(true); + 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', + ), + ).toBe(true); + expect( + model.evidence.some( + (item) => + item.kind === 'resolver' && + item.source === resolverFile && + item.target === rspackPackageName && + item.line === 3 && + item.column === 17 && + item.resolved !== undefined, + ), + ).toBe(true); + expect( + !model.evidence.some((item) => item.kind === 'resolver' && item.source === directFile), + ).toBe(true); + expect( + model.evidence.some( + (item) => item.target === 'src/schema.ts#registeredSchema' && item.kind === 'export', + ), + ).toBe(true); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }), +); + +it.live( + 'modeling fails on invalid source instead of silently losing consumer evidence', + Effect.fn(function* testEffect4() { + const root = yield* fixture(); + try { + write(root, 'module-federation.config.ts', 'export default { broken: ;'); + 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), ), - runDirectory: Schema.String, - status: Schema.Literal('reported'), + ); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }), +); + +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')); + expect(manifest.version).toBe('6.34.0'); + }), +); + +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 = realpathSync(mkdtempSync(path.join(tmpdir(), 'ontos-knip-model-output-'))); + try { + write( + root, + 'quality-audit/scope.json', + yield* stringify({ + exclude: [], + patterns: [sourcePattern, configurationFiles, 'verticals/**/*.{ts,mts}'], }), - ), - )(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, - (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 }]); - const raw = Schema.decodeUnknownSync(Schema.fromJsonString(ReportSchema))( - readFileSync(path.join(summary.runDirectory, 'knip/report.ndjson'), 'utf-8') - .trim() - .split('\n')[0], - ); - assert.ok( - 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 }); - } -}); + ); + write( + root, + 'quality-audit/knip.json', + yield* stringify({ + workspaces: { + '.': { + drizzle: { config: [] }, + entry: [indexFile, configurationFiles], + lefthook: false, + node: false, + project: [sourcePattern, configurationFiles, 'tools/**/*.{ts,mts}'], + }, + 'verticals/*': { entry: [indexFile, configurationFiles], project: ['**/*.{ts,mts}'] }, + }, + }), + ); + write( + root, + 'quality-audit/knip-reporter.mts', + readFileSync(path.join(appRoot, 'quality-audit/knip-reporter.mts'), 'utf-8'), + ); + 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({ + results: Schema.Array( + Schema.Struct({ + coverage: Schema.Struct({ + findingCounts: Schema.Record(Schema.String, Schema.Number), + modeledUsages: Schema.Number, + nativeFindingCounts: Schema.Record(Schema.String, Schema.Number), + }), + name: Schema.String, + }), + ), + runDirectory: Schema.String, + status: Schema.Literal('reported'), + }), + ), + )(readFileSync(path.join(output, 'summary.json'), 'utf-8')); + const result = summary.results.find((entry) => entry.name === 'knip'); + 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')); + 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], + ); + expect( + raw.issues.some( + (issue) => + issue.file === directFile && + issue.unlisted.some((entry) => entry.name === rspackPackageName), + ), + ).toBe(true); + } finally { + rmSync(root, { force: true, recursive: true }); + rmSync(output, { force: true, recursive: 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 })); - 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( - ( +it.live( + 'vendor ownership rejects a different installed copy and accepts the same canonical target', + Effect.fn(function* testEffect7() { + const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'ontos-knip-copy-controls-'))); + try { + 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`; + yield* Effect.all( + ( + [ + [owner, 'owner', { producer: '1.0.0' }], + [producer, 'producer', { target: '1.0.0' }], + [ownerTarget, 'target', {}], + [producerTarget, 'target', {}], + ] as const + ).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, + indexFile, [ - [owner, 'owner', { producer: '1.0.0' }], - [producer, 'producer', { target: '1.0.0' }], - [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); - }), - ); - write( - root, - indexFile, - [ - ...requirePrelude, - `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( - root, - { entry: [indexFile], node: false, project: [sourcePattern] }, - consumerPath, - ).pipe(Effect.provide(NodeServices.layer)), + ...requirePrelude, + `require.resolve('target', { paths: [${JSON.stringify(path.join(root, owner, 'index.js'))}] });`, + ].join('\n'), ); - const differentCopies = await build(); - assert.ok( - !differentCopies.evidence.some( - (item) => item.kind === 'resolver' && item.target === 'target', - ), - ); - 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, - 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 runEffectTestPromise( - runPinnedKnip(root, consumerPath, differentCopies).pipe(Effect.provide(NodeServices.layer)), - ); - assert.equal(run.status, 1, run.stderr); - const report = Schema.decodeUnknownSync(Schema.fromJsonString(ReportSchema))(run.stdout); - assert.ok( - report.issues.some( - (issue) => - issue.file === indexFile && issue.unlisted.some((item) => item.name === 'target'), - ), - ); - 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( - sameCopy.evidence.some( - (item) => - item.kind === 'resolver' && - item.target === 'target' && - item.owningManifest === path.join(root, producer, packageFile), - ), - ); - write( - root, - indexFile, - [ - ...requirePrelude, - `require.resolve('target', { paths: [${await stringify(path.join(root, owner, 'missing'))}] });`, - ].join('\n'), - ); - const missingAnchor = await build(); - assert.ok( - !missingAnchor.evidence.some((item) => item.kind === 'resolver' && item.target === 'target'), - ); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + const consumerPath = path.join(root, '.audit/consumers.mts'); + const build = () => + Effect.gen(function* testEffect9() { + return yield* buildKnipModel( + root, + { entry: [indexFile], node: false, project: [sourcePattern] }, + consumerPath, + ).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', + ); + 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')), + ); + 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); + 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 = 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: [${yield* stringify(path.join(root, owner, 'missing'))}] });`, + ].join('\n'), + ); + const missingAnchor = yield* build(); + expect( + !missingAnchor.evidence.some( + (item) => item.kind === 'resolver' && item.target === 'target', + ), + ).toBe(true); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }), +); diff --git a/app/scripts/tests/quality-audit-runtime-model.test.mts b/app/scripts/tests/quality-audit-runtime-model.test.mts index 89ec658ac..8a4920d6b 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 '@app/effect-rstest'; import { runPinnedKnip } from './quality-audit-test-support.mts'; -import assert from 'node:assert/strict'; + import { mkdirSync, mkdtempSync, 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'; @@ -29,238 +30,265 @@ 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; -}; - -await test('runtime consumers require the exact CSS, shell, deployment and compiler contracts', async () => { - const root = await fixture(); - try { - const initial = await facts(root); - for (const target of [ - cssUsed, - launchedFile, - resetFile, - '@effect/tsgo', - pluginName, - readinessConfig, - ]) { - assert.ok( - initial.some((fact) => fact.target === target), - target, - ); - } - for (const target of ['@fixture/css-dead', '@fixture/css-comment', 'scripts/dead.mts']) { - assert.ok(!initial.some((fact) => fact.target === target), target); - } - assert.equal(initial.find((fact) => fact.target === pluginName)?.kind, compilerOptionKind); - assert.ok( - initial.some( - (fact) => fact.target === `${readinessConfig}#default` && fact.kind === 'export', - ), +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 = 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, layoutFile, emptyLayout); - write(root, `${shellRoot}/package.json`, '{"name":"@fixture/shell"}'); - write(root, 'zerops.yaml', '# - cd app && node scripts/reset.mjs'); + write(root, tsgoPackage, '{"name":"@effect/tsgo","version":"0.19.0"}'); write( root, - compilerConfig, - await stringify({ - compilerOptions: { plugins: [{ name: pluginName }], types: [pluginName] }, + `${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, - `${vendorRoot}/ultramodern-performance-readiness.mjs`, - `const configPath = '${readinessConfig}'; void configPath;`, + 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')) });", ); - const changed = await facts(root); - for (const target of [cssUsed, launchedFile, resetFile, pluginName, readinessConfig]) { - assert.ok(!changed.some((fact) => fact.target === target), target); - } - } 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 { - write(root, tsgoReadme, '"name": "@effect/language-service"'); - const nameOnly = await facts(root); - assert.ok(!nameOnly.some((fact) => fact.kind === compilerOptionKind)); 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)); - write(root, tsgoPackage, '{"name":"@effect/tsgo","version":"0.19.0"}'); write( root, compilerConfig, - await stringify({ - compilerOptions: { - plugins: [{ name: pluginName }], - types: [`${pluginName}/types`], - }, - }), + yield* stringify({ compilerOptions: { plugins: [{ name: pluginName }] } }), ); - const typeImport = await facts(root); - assert.ok(!typeImport.some((fact) => fact.kind === compilerOptionKind)); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); - -await test('compiler configuration accepts JSONC but rejects malformed and schema-invalid input', async () => { - const root = await fixture(); - try { write( root, - compilerConfig, - `{ + '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; + }); + +it.live( + 'runtime consumers require the exact CSS, shell, deployment and compiler contracts', + Effect.fn(function* testEffect4() { + const root = yield* fixture(); + try { + const initial = yield* facts(root); + for (const target of [ + cssUsed, + launchedFile, + resetFile, + '@effect/tsgo', + pluginName, + readinessConfig, + ]) { + expect( + initial.some((fact) => fact.target === target), + target, + ).toBe(true); + } + for (const target of ['@fixture/css-dead', '@fixture/css-comment', 'scripts/dead.mts']) { + expect(!initial.some((fact) => fact.target === target), target).toBe(true); + } + 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, + yield* stringify({ + compilerOptions: { plugins: [{ name: pluginName }], types: [pluginName] }, + }), + ); + write( + root, + `${vendorRoot}/ultramodern-performance-readiness.mjs`, + `const configPath = '${readinessConfig}'; void configPath;`, + ); + const changed = yield* facts(root); + for (const target of [cssUsed, launchedFile, resetFile, pluginName, readinessConfig]) { + expect(!changed.some((fact) => fact.target === target), target).toBe(true); + } + } finally { + rmSync(root, { force: true, recursive: true }); + } + }), +); + +it.live( + 'compiler-option proof requires the installed pinned compiler and built-in plugin documentation', + Effect.fn(function* testEffect5() { + const root = yield* fixture(); + try { + write(root, tsgoReadme, '"name": "@effect/language-service"'); + 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 = 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, + yield* stringify({ + compilerOptions: { + plugins: [{ name: pluginName }], + types: [`${pluginName}/types`], + }, + }), + ); + const typeImport = yield* facts(root); + expect(!typeImport.some((fact) => fact.kind === compilerOptionKind)).toBe(true); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }), +); + +it.live( + 'compiler configuration accepts JSONC but rejects malformed and schema-invalid input', + Effect.fn(function* testEffect6() { + const root = yield* fixture(); + try { + write( + root, + compilerConfig, + `{ // TypeScript permits comments and trailing commas. "compilerOptions": { "plugins": [{ "name": "${pluginName}", }], }, }`, - ); - const modeled = await facts(root); - assert.ok(modeled.some((fact) => fact.kind === compilerOptionKind)); - write(root, compilerConfig, '{ "compilerOptions": {'); - await assert.rejects(facts(root), /InvalidTsconfig/u); - write(root, compilerConfig, '{ "compilerOptions": { "plugins": false } }'); - await assert.rejects(facts(root), /plugins/u); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + ); + const modeled = yield* facts(root); + expect(modeled.some((fact) => fact.kind === compilerOptionKind)).toBe(true); + write(root, compilerConfig, '{ "compilerOptions": {'); + yield* facts(root).pipe( + Effect.flip, + Effect.map((error) => expect(String(error)).toMatch(/InvalidTsconfig/u)), + ); + write(root, compilerConfig, '{ "compilerOptions": { "plugins": false } }'); + yield* facts(root).pipe( + Effect.flip, + Effect.map((error) => expect(String(error)).toMatch(/plugins/u)), + ); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }), +); -await test('DTS compiler resolution belongs to the invoking workspace and excludes commented lookalikes', async () => { - const root = await fixture(); - try { - 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 ['@effect/tsgo', '@typescript/native']) { - assert.ok(initial.some((fact) => fact.target === target && fact.workspace === shellRoot)); +it.live( + 'DTS compiler resolution belongs to the invoking workspace and excludes commented lookalikes', + Effect.fn(function* testEffect7() { + const root = yield* fixture(); + try { + 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 = 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 = yield* facts(root); + expect(!commented.some((fact) => fact.source === configFile)).toBe(true); + } finally { + rmSync(root, { force: true, recursive: 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 }); - } -}); + }), +); -await test('Lefthook configuration proves only intended tool usage and ignores commented hook text', async () => { - const root = await fixture(); - try { - const source = 'pre-commit:\n commands:\n format:\n run: pnpm format\n'; - write(root, 'lefthook.yml', source); - const configured = await 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); - write( - root, - 'lefthook.yml', - source - .split('\n') - .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 }); - } -}); +it.live( + 'Lefthook configuration proves only intended tool usage and ignores commented hook text', + Effect.fn(function* testEffect8() { + const root = yield* fixture(); + try { + const source = 'pre-commit:\n commands:\n format:\n run: pnpm format\n'; + write(root, 'lefthook.yml', source); + const configured = yield* facts(root); + const tool = configured.find((fact) => fact.target === 'lefthook'); + expect(tool?.kind).toBe('dependency'); + expect(tool?.reason ?? '').toMatch(/does not establish hook activation/u); + write( + root, + 'lefthook.yml', + source + .split('\n') + .map((line) => `# ${line}`) + .join('\n'), + ); + const commented = yield* facts(root); + expect(!commented.some((fact) => fact.target === 'lefthook')).toBe(true); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }), +); -await test('real Knip keeps unused neighboring files, dependency names and exports after runtime modeling', async () => { - const root = await fixture(); - try { - const consumerPath = path.join(root, '.audit/consumers.mts'); - const model = await runEffectTestPromise( - buildKnipModel( +it.live( + 'real Knip keeps unused neighboring files, dependency names and exports after runtime modeling', + Effect.fn(function* testEffect9() { + const root = yield* fixture(); + try { + const consumerPath = path.join(root, '.audit/consumers.mts'); + const model = yield* buildKnipModel( root, { workspaces: { @@ -273,15 +301,13 @@ await test('real Knip keeps unused neighboring files, dependency names and expor }, }, consumerPath, - ).pipe(Effect.provide(NodeServices.layer)), - ); - const run = await runEffectTestPromise( - runPinnedKnip(root, consumerPath, model).pipe(Effect.provide(NodeServices.layer)), - ); - assert.equal(run.error, undefined); - assert.ok(run.status === 0 || run.status === 1, run.stderr); - const report = await runEffectTestPromise( - Schema.decodeUnknownEffect( + ).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( @@ -294,35 +320,37 @@ await test('real Knip keeps unused neighboring files, dependency names and expor ), }), ), - )(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( - !unusedFiles.some( - (file) => - file.endsWith('/scripts/launched.mts') || file === resetFile || file === readinessConfig, - ), - ); - 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)); - const exports = new Set( - report.issues.flatMap((issue) => issue.exports.map((item) => item.name)), - ); - assert.ok( - !report.issues.some( - (issue) => - issue.file === readinessConfig && issue.exports.some((item) => item.name === 'default'), - ), - ); - for (const name of ['unusedLauncherExport', 'unusedResetExport', 'unusedConfigExport']) { - assert.ok(exports.has(name), name); + )(run.stdout); + const unusedFiles = report.issues.flatMap((issue) => issue.files.map((item) => item.name)); + 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)), + ); + 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)), + ); + expect( + !report.issues.some( + (issue) => + issue.file === readinessConfig && issue.exports.some((item) => item.name === 'default'), + ), + ).toBe(true); + for (const name of ['unusedLauncherExport', 'unusedResetExport', 'unusedConfigExport']) { + expect(exports.has(name), name).toBe(true); + } + } finally { + rmSync(root, { force: true, recursive: true }); } - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); + }), +); diff --git a/app/scripts/tests/quality-audit.test.mts b/app/scripts/tests/quality-audit.test.mts index 3a3d2b85b..55c50da3d 100644 --- a/app/scripts/tests/quality-audit.test.mts +++ b/app/scripts/tests/quality-audit.test.mts @@ -1,4 +1,5 @@ -import assert from 'node:assert/strict'; +import { expect, it } from '@app/effect-rstest'; + import { spawnSync } from 'node:child_process'; import { copyFileSync, @@ -12,11 +13,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 { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; -import { runEffectTestPromise } from '../../packages/core-runtime/src/testing/effect-runtime.ts'; + import { auditSteps, runQualityAudit, validateReport } from '../quality-audit.mts'; const FALLOW_CLONES = 'fallow-clones'; @@ -33,165 +34,230 @@ const ProvenanceSchema = Schema.fromJsonString( }), ); const appRoot = path.resolve(import.meta.dirname, '../..'); -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 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)); +}; -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, +const reportSchema = Schema.fromJsonString(Schema.Unknown); +const stringify = (value: Schema.Json) => + Effect.gen(function* testEffect1() { + return yield* Schema.encodeEffect(reportSchema)(value); }); - await assert.rejects(validate('jscpd', '{}'), /Malformed analyzer report/u); - await assert.rejects(validate('jscpd', '{broken'), /Malformed analyzer report/u); - await assert.rejects( - validate( +const validate = (name: string, source: string) => + Effect.gen(function* testEffect2() { + return yield* validateReport(name, source); + }); + +it.live( + '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* validate('jscpd', yield* stringify(report))).toEqual({ + coverage: { tokenEligibleFiles: 2 }, + files: 2, + findings: 1, + }); + yield* validate('jscpd', '{}').pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/Malformed analyzer report/u), + ), + ); + yield* validate('jscpd', '{broken').pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/Malformed analyzer report/u), + ), + ); + yield* validate( 'jscpd', - await stringify({ duplicates: [], statistics: { total: { clones: 0, sources: 0 } } }), - ), - /no files/u, - ); - await assert.rejects( - validate( + yield* stringify({ duplicates: [], statistics: { total: { clones: 0, sources: 0 } } }), + ).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/no files/u), + ), + ); + yield* validate( 'jscpd', - await stringify({ ...report, statistics: { total: { clones: 0, sources: 2 } } }), - ), - /count disagrees/u, - ); -}); + yield* stringify({ ...report, statistics: { total: { clones: 0, sources: 2 } } }), + ).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/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.live( + 'Knip coverage is mandatory and findings count categories rather than files', + Effect.fn(function* testEffect4() { + const findings = yield* stringify({ issues: [] }); + const coverage = yield* stringify({ + 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( + }); + expect(yield* validate('knip', `${findings}\n${coverage}`)).toEqual({ + coverage: { + findingCounts: { exports: 4, files: 2 }, + processed: 12, + total: 12, + workspaces: ['.'], + }, + files: 12, + findings: 6, + }); + yield* validate('knip', findings).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/coverage records/u), + ), + ); + yield* validate( 'knip', - `${findings}\n${await stringify({ coverage: { processed: 0, total: 0 }, findingCounts: {}, workspaces: ['.'] })}`, - ), - /no files/u, - ); -}); + `${findings}\n${yield* stringify({ coverage: { processed: 0, total: 0 }, findingCounts: {}, workspaces: ['.'] })}`, + ).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/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( +it.live( + '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* validate(FALLOW_CLONES, yield* stringify(report))).toEqual({ + coverage: { tokenEligibleFiles: 2 }, + files: 2, + findings: 0, + }); + yield* validate(FALLOW_CLONES, yield* stringify({ ...report, schema_version: 10 })).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/Malformed analyzer report/u), + ), + ); + yield* validate( FALLOW_CLONES, - await stringify({ + yield* 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, - ); -}); + ).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/incomplete workspace/u), + ), + ); + yield* validate('fallow-files', yield* stringify({ file_count: 2, files: ['a.ts'] })).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/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), +const createFixture = () => + Effect.gen(function* testEffect6() { + 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'), + yield* stringify({ name: 'quality-test', private: true, type: 'module' }), ); - } - 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; -}; + 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* 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 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') => + Effect.gen(function* testEffect7() { + return yield* runQualityAudit(root, output, tool).pipe(Effect.provide(NodeServices.layer)); + }); const SummarySchema = Schema.Struct({ mode: Schema.Literal('report-only'), @@ -209,25 +275,27 @@ const SummarySchema = Schema.Struct({ runDirectory: Schema.String, status: Schema.String, }); -const summary = async (output: string) => - await runEffectTestPromise( - Schema.decodeUnknownEffect(Schema.fromJsonString(SummarySchema))( +const summary = (output: string) => + Effect.gen(function* testEffect8() { + return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(SummarySchema))( readFileSync(path.join(output, 'summary.json'), '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 { - writeFileSync( - path.join(root, 'scripts/metric-example.tsx'), - `import { useState } from 'react'; +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'); + try { + writeFileSync( + path.join(root, 'scripts/metric-example.tsx'), + `import { useState } from 'react'; export function Panel({ ${props} }: Record) { useState('one'); useState('two'); @@ -239,12 +307,11 @@ export function branchHeavy(value: number) { return 0; } `, - ); - await runFixture(root, output, 'fallow'); - const result = await summary(output); - const healthDirectory = path.join(result.runDirectory, FALLOW_HEALTH); - const rows = await runEffectTestPromise( - Schema.decodeUnknownEffect( + ); + yield* runFixture(root, output, 'fallow'); + const result = yield* summary(output); + const healthDirectory = path.join(result.runDirectory, FALLOW_HEALTH); + const rows = yield* Schema.decodeUnknownEffect( Schema.fromJsonString( Schema.Array( Schema.Struct({ @@ -255,74 +322,91 @@ export function branchHeavy(value: number) { }), ), ), - )(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); - 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); - 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 }); - } -}); - -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 { - for (const [index, file] of policyFiles.entries()) { - const policy = Array.from( - { length: 16 }, - (_, field) => - `decision${field}: subject === '${index === 0 ? 'role' : 'admin'}-${field}' ? '${index === 0 ? 'allow' : 'audit'}-${field}' : '${index === 0 ? 'deny' : 'defer'}-${field}'`, - ).join(',\n'); - writeFileSync( - path.join(root, 'scripts', file), - `export function selectPolicy(subject: string) {\nreturn {\n${policy}\n};\n}\n`, + )(readFileSync(path.join(healthDirectory, 'complexity.json'), 'utf-8')); + const panel = rows.find((row) => row.name === 'Panel'); + const branchHeavy = rows.find((row) => row.name === 'branchHeavy'); + 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`, ); - } - await runFixture(root, output, 'all'); - const result = await summary(output); - const schema = Schema.fromJsonString( - Schema.Struct({ - clone_groups: Schema.Array( - Schema.Struct({ instances: Schema.Array(Schema.Struct({ file: Schema.String })) }), + expect(corrupted).not.toBe(raw); + yield* validate(FALLOW_HEALTH, corrupted).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/contributions disagree/u), ), - }), - ); - await Promise.all( - [FALLOW_CLONES, FALLOW_SIMILARITY].map(async (name) => { - const report = await runEffectTestPromise( - Schema.decodeUnknownEffect(schema)( - readFileSync(path.join(result.runDirectory, name, 'report.json'), 'utf-8'), - ), + ); + const wrongCount = raw.replace( + /"functions_above_threshold"\s*:\s*\d+/u, + '"functions_above_threshold": 0', + ); + yield* validate(FALLOW_HEALTH, wrongCount).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/count disagrees/u), + ), + ); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }), +); + +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']; + try { + for (const [index, file] of policyFiles.entries()) { + const policy = Array.from( + { length: 16 }, + (_, field) => + `decision${field}: subject === '${index === 0 ? 'role' : 'admin'}-${field}' ? '${index === 0 ? 'allow' : 'audit'}-${field}' : '${index === 0 ? 'deny' : 'defer'}-${field}'`, + ).join(',\n'); + writeFileSync( + path.join(root, 'scripts', file), + `export function selectPolicy(subject: string) {\nreturn {\n${policy}\n};\n}\n`, ); - const matchesDistinctPolicies = report.clone_groups.some((group) => - policyFiles.every((file) => - group.instances.some((instance) => path.basename(instance.file) === file), + } + yield* runFixture(root, output, 'all'); + const result = yield* summary(output); + const schema = Schema.fromJsonString( + Schema.Struct({ + clone_groups: Schema.Array( + Schema.Struct({ instances: Schema.Array(Schema.Struct({ file: Schema.String })) }), ), - ); - assert.equal(matchesDistinctPolicies, name === FALLOW_SIMILARITY, name); - }), - ); - const jscpd = await runEffectTestPromise( - Schema.decodeUnknownEffect( + }), + ); + 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) => + includesPolicyFiles(group.instances, policyFiles), + ); + expect(matchesDistinctPolicies, name).toBe(name === FALLOW_SIMILARITY); + }), + ), + { concurrency: 'unbounded' }, + ); + const jscpd = yield* Schema.decodeUnknownEffect( Schema.fromJsonString( Schema.Struct({ duplicates: Schema.Array( @@ -333,209 +417,259 @@ await test('primary clone detectors preserve policy literals while semantic simi ), }), ), - )(readFileSync(path.join(result.runDirectory, 'jscpd/report.json'), 'utf-8')), - ); - assert.equal( - jscpd.duplicates.some((pair) => - policyFiles.every((file) => - [pair.firstFile.name, pair.secondFile.name].some((name) => path.basename(name) === file), + )(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); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }), +); -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); - for (const name of ['knip', 'jscpd', FALLOW_CLONES, FALLOW_HEALTH]) { - assert.ok( - first.results.some((row) => row.name === name && row.findings > 0), - `${name} must report injected debt`, +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); + try { + 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]) { + 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'); + yield* runFixture(root, output, 'knip'); + writeFileSync(path.join(root, KNIP_CONFIG), '{invalid'); + yield* runFixture(root, output, 'knip').pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/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, ); + expect( + readFileSync(path.join(first.runDirectory, 'knip/report.ndjson'), 'utf-8').length > 0, + ).toBe(true); + } finally { + rmSync(root, { force: true, recursive: true }); } - writeFileSync(path.join(root, 'quality-audit/jscpd.json'), '{invalid unrelated config'); - await 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'), - /Malformed .*configs\/knip\.json/u, - ); - assert.ok( - readFileSync(path.join(first.runDirectory, 'knip/report.ndjson'), 'utf-8').length > 0, - ); - } finally { - rmSync(root, { force: true, recursive: 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 { - 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 failedDirectory = path.join(missing.runDirectory, 'knip'); - assert.deepEqual(missing.results, [ - { - coverage: {}, - diagnostic: readFileSync( - path.join(failedDirectory, 'validation-error.txt'), - 'utf-8', - ).trimEnd(), - directory: failedDirectory, - files: 0, - findings: 0, - name: 'knip', - status: 'error', - }, - ]); - assert.match( - readFileSync(path.join(missing.runDirectory, 'knip/metadata.json'), 'utf-8'), - /Missing pinned local binary/u, - ); - writeFileSync( - path.join(root, 'quality-audit/scope.json'), - await stringify({ exclude: [], patterns: ['absent/**/*.ts'] }), - ); - await assert.rejects(runFixture(root, output, 'jscpd'), /analysis failed/u); - const empty = await summary(output); - assert.deepEqual(empty.results, [ - { - coverage: {}, - diagnostic: 'QualityAuditError: Source inventory: analysis contains no files', - directory: empty.runDirectory, - files: 0, - findings: 0, - name: 'setup', - status: 'error', - }, - ]); - assert.match( - readFileSync(path.join(output, 'summary.json'), 'utf-8'), - /Source inventory: analysis contains no files/u, - ); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); +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); + try { + rmSync(path.join(root, 'node_modules')); + yield* runFixture(root, output, 'knip').pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/analysis failed/u), + ), + ); + const missing = yield* summary(output); + expect(missing.status).toBe('error'); + const failedDirectory = path.join(missing.runDirectory, 'knip'); + expect(missing.results).toEqual([ + { + coverage: {}, + diagnostic: readFileSync( + path.join(failedDirectory, 'validation-error.txt'), + 'utf-8', + ).trimEnd(), + directory: failedDirectory, + files: 0, + findings: 0, + name: 'knip', + status: 'error', + }, + ]); + 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'), + yield* stringify({ exclude: [], patterns: ['absent/**/*.ts'] }), + ); + yield* runFixture(root, output, 'jscpd').pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/analysis failed/u), + ), + ); + const empty = yield* summary(output); + expect(empty.results).toEqual([ + { + coverage: {}, + diagnostic: 'QualityAuditError: Source inventory: analysis contains no files', + directory: empty.runDirectory, + files: 0, + findings: 0, + name: 'setup', + status: 'error', + }, + ]); + expect(readFileSync(path.join(output, 'summary.json'), '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() { +it.live( + 'the CLI handles escaped paths, foreign cwd and untracked source provenance', + Effect.fn(function* testEffect14() { + const root = yield* createFixture(); + const output = path.join(root, REPORT_DIRECTORY); + try { + yield* Effect.gen(function* initializeFixtureRepository() { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; yield* spawner.string(ChildProcess.make('git', ['init', '-q', root])); - }).pipe(Effect.provide(NodeServices.layer)), - ); - const executable = path.join(root, 'scripts/quality audit.mts'); - copyFileSync(path.join(appRoot, 'scripts/quality-audit.mts'), executable); - for (const file of ['knip-model.mts', 'knip-runtime-model.mts']) { - copyFileSync( - path.join(appRoot, CONFIG_DIRECTORY, file), - path.join(root, CONFIG_DIRECTORY, file), + }).pipe(Effect.provide(NodeServices.layer)); + const executable = path.join(root, 'scripts/quality audit.mts'); + copyFileSync(path.join(appRoot, 'scripts/quality-audit.mts'), executable); + for (const file of ['knip-model.mts', 'knip-runtime-model.mts']) { + copyFileSync( + path.join(appRoot, CONFIG_DIRECTORY, file), + path.join(root, CONFIG_DIRECTORY, file), + ); + } + const result = spawnSync( + process.execPath, + [executable, '--tool', 'knip', '--output', output], + { + cwd: tmpdir(), + encoding: 'utf-8', + timeout: 60_000, + }, ); - } - 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)( + expect( + result.error, + `CLI spawn failed: ${String(result.error)}\n${result.stdout}\n${result.stderr}`, + ).toBe(undefined); + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + const report = yield* summary(output); + expect(report.status).toBe('reported'); + 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 }); - } -}); + ); + 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, + ); + } finally { + rmSync(root, { force: true, recursive: 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'), - /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 }); - } -}); +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'); + try { + yield* runFixture(root, output, 'all').pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/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, + ); + expect(readdirSync(report.runDirectory)).toEqual([]); + expect(readdirSync(path.join(root, '.codex'))).toEqual([CALLER_OWNED_FILE]); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }), +); -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 { - 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 ?? '', - /output directory outside configured source roots/u, - ); - assert.deepEqual(readdirSync(rejected.runDirectory), []); - assert.deepEqual(readdirSync(path.join(root, '.codex')), [CALLER_OWNED_FILE]); +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); + try { + mkdirSync(sourceOutput); + symlinkSync(sourceOutput, output, 'dir'); + yield* runFixture(root, output, 'all').pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/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, + ); + 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 }); - } -}); + rmSync(output); + mkdirSync(safeOutput); + symlinkSync(safeOutput, output, 'dir'); + yield* runFixture(root, output, 'jscpd'); + const accepted = yield* summary(output); + expect(accepted.status).toBe('reported'); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }), +); -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 { - writeFileSync(path.join(root, '.gitignore'), 'node_modules\n.codex\n'); - await runEffectTestPromise( - Effect.gen(function* commitFixture() { +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); + try { + writeFileSync(path.join(root, '.gitignore'), 'node_modules\n.codex\n'); + yield* Effect.gen(function* commitFixture() { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const commands = [ ['init', '-q'], @@ -559,80 +693,105 @@ await test('custom output does not mark clean source provenance as modified', as (args) => spawner .exitCode(ChildProcess.make('git', args, { cwd: root })) - .pipe(Effect.tap((code) => Effect.sync(() => assert.equal(Number(code), 0)))), + .pipe(Effect.map((code) => expect(Number(code)).toBe(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)( + }).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([]); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }), +); -await test('narrowed workspace and Fallow source discovery produce coverage errors', async () => { - const root = await createFixture(); - const output = path.join(root, REPORT_DIRECTORY); - try { - 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'), - ); - writeFileSync( - path.join(root, 'quality-audit/fallow.json'), - await stringify({ ignorePatterns: ['scripts/**', 'node_modules/**', 'packages/**'] }), - ); - await assert.rejects(runFixture(root, output, 'fallow'), /analysis failed/u); - const omitted = await summary(output); - assert.ok( - omitted.results.some((result) => result.name === 'coverage' && result.status === 'error'), - ); - assert.match( - readFileSync(path.join(omitted.runDirectory, 'coverage.json'), 'utf-8'), - /scripts\/index.ts/u, - ); - } finally { - rmSync(root, { force: true, recursive: true }); - } -}); +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); + try { + mkdirSync(path.join(root, 'packages/omitted'), { recursive: true }); + writeFileSync( + path.join(root, 'packages/omitted/package.json'), + yield* stringify({ name: 'omitted', private: true }), + ); + yield* runFixture(root, output, 'knip').pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/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'), + yield* stringify({ ignorePatterns: ['scripts/**', 'node_modules/**', 'packages/**'] }), + ); + yield* runFixture(root, output, 'fallow').pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/analysis failed/u), + ), + ); + const omitted = yield* summary(output); + expect( + omitted.results.some((result) => result.name === 'coverage' && result.status === 'error'), + ).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 { - // 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' }), - ); - 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'), - /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 }); - } -}); +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); + try { + // 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'), + yield* stringify({ version: '0.0.0' }), + ); + yield* runFixture(root, output, 'knip').pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/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, + ); + expect(readFileSync(path.join(mismatch.runDirectory, 'knip/stdout.txt'), 'utf-8')).toBe(''); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }), +); 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..6092429b2 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 '@app/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 593b249a6..27c616738 100644 --- a/app/scripts/tests/root-environment.test.mts +++ b/app/scripts/tests/root-environment.test.mts @@ -1,16 +1,18 @@ -import assert from 'node:assert/strict'; +import { expect, it } from '@app/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 { pathToFileURL } 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 +28,30 @@ 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({ + catch: (error) => error, + try: () => 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 databaseConfigUrl = pathToFileURL( path.join(appRoot, 'packages/core-runtime/src/db/config.ts'), ).href; @@ -81,15 +88,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( @@ -104,7 +111,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/typecheck-project-references.test.mts b/app/scripts/tests/typecheck-project-references.test.mts index a06cccede..816b75d27 100644 --- a/app/scripts/tests/typecheck-project-references.test.mts +++ b/app/scripts/tests/typecheck-project-references.test.mts @@ -1,19 +1,14 @@ -import assert from 'node:assert/strict'; +import { expect, it } from '@app/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 { Config, Effect, Predicate, Schema, Stream } from 'effect'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; -interface TypecheckResult { - readonly status: number; - readonly stderr: string; - readonly stdout: string; -} - interface WorkspaceScriptPlan { readonly typecheck: string; } @@ -39,14 +34,10 @@ 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 runTypecheck = (fixture: string, commandArguments: readonly string[]) => + Effect.gen(function* testEffect1() { + return yield* Effect.gen(function* runTypecheckEffect() { const inheritedPath = yield* Config.string('PATH').pipe(Config.withDefault('')); const processSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; return yield* Effect.scoped( @@ -75,119 +66,127 @@ const runTypecheck = async ( return { status, stderr, stdout }; }), ); - }), - ); - -test.after(async () => { - await typecheckRuntime.dispose(); -}); - -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', - ); -}); + }).pipe(Effect.provide(NodeServices.layer)); + }); -void test('Drizzle consumer surface compiles in both ESM and CommonJS projects', async () => { - const fixture = mkdtempSync(path.join(os.tmpdir(), 'ontos-drizzle-declarations-')); - try { - symlinkSync( - path.join(workspaceRoot, 'node_modules'), - path.join(fixture, 'node_modules'), - 'dir', - ); - writeFileSync(path.join(fixture, packageJsonFile), '{"private":true,"type":"module"}\n'); - writeFileSync( - path.join(fixture, tsconfigFile), - JSON.stringify({ - compilerOptions: { - exactOptionalPropertyTypes: true, - module: 'NodeNext', - moduleResolution: 'NodeNext', - noEmit: true, - skipLibCheck: true, - strict: true, - target: 'ESNext', - types: ['node'], - }, - files: ['./consumer.mts', './consumer.cts'], +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({ + catch: (error) => error, + try: () => + import( + pathToFileURL( + path.join( + workspaceRoot, + 'node_modules/@modern-js/create/dist/esm-node/ultramodern-workspace/workspace-script-plan.js', + ), + ).href + ), }), ); - for (const extension of ['mts', 'cts']) { + const scriptPlan = Schema.decodeUnknownSync(WorkspaceScriptPlanSchema)( + generator.createWorkspaceRootScriptPlan([]), + ); + expect(scriptPlan.typecheck).toBe( + 'node ./scripts/ultramodern-typecheck.mts --build tsconfig.json', + ); + }), +); + +it.live( + 'Drizzle consumer surface compiles in both ESM and CommonJS projects', + Effect.fn(function* testEffect3() { + const fixture = mkdtempSync(path.join(os.tmpdir(), 'ontos-drizzle-declarations-')); + try { + symlinkSync( + path.join(workspaceRoot, 'node_modules'), + path.join(fixture, 'node_modules'), + 'dir', + ); + writeFileSync(path.join(fixture, packageJsonFile), '{"private":true,"type":"module"}\n'); writeFileSync( - path.join(fixture, `consumer.${extension}`), - 'import { pgTable, uuid } from "drizzle-orm/pg-core";\n' + - 'export const fixtureTable = pgTable("declaration_fixture", { id: uuid("id") });\n', + path.join(fixture, tsconfigFile), + JSON.stringify({ + compilerOptions: { + exactOptionalPropertyTypes: true, + module: 'NodeNext', + moduleResolution: 'NodeNext', + noEmit: true, + skipLibCheck: true, + strict: true, + target: 'ESNext', + types: ['node'], + }, + files: ['./consumer.mts', './consumer.cts'], + }), ); + for (const extension of ['mts', 'cts']) { + writeFileSync( + path.join(fixture, `consumer.${extension}`), + 'import { pgTable, uuid } from "drizzle-orm/pg-core";\n' + + 'export const fixtureTable = pgTable("declaration_fixture", { id: uuid("id") });\n', + ); + } + const result = yield* runTypecheck(fixture, ['--project', tsconfigFile]); + expect(result.status, result.stdout + result.stderr).toBe(0); + } finally { + rmSync(fixture, { force: true, recursive: true }); } - const result = await runTypecheck(fixture, ['--project', tsconfigFile]); - assert.equal(result.status, 0, result.stdout + result.stderr); - } finally { - rmSync(fixture, { force: true, recursive: true }); - } -}); + }), +); -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 { - mkdirSync(path.join(fixture, 'referenced')); - symlinkSync( - path.join(workspaceRoot, 'node_modules'), - path.join(fixture, 'node_modules'), - 'dir', - ); - writeFileSync(path.join(fixture, packageJsonFile), '{"private":true,"type":"module"}\n'); - writeFileSync( - path.join(fixture, tsconfigFile), - JSON.stringify({ files: [], references: [{ path: './referenced' }] }), - ); - writeFileSync( - path.join(fixture, 'referenced/tsconfig.json'), - JSON.stringify({ - compilerOptions: { - composite: true, - declaration: true, - emitDeclarationOnly: true, - outDir: './output', - strict: true, - types: [], - }, - files: ['./index.ts'], - }), - ); - 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( - 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', - ); - 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 }); - } -}); +it.live( + 'root typecheck checks referenced projects and rejects a newly introduced type error', + Effect.fn(function* testEffect4() { + const fixture = mkdtempSync(path.join(os.tmpdir(), 'ontos-typecheck-references-')); + try { + mkdirSync(path.join(fixture, 'referenced')); + symlinkSync( + path.join(workspaceRoot, 'node_modules'), + path.join(fixture, 'node_modules'), + 'dir', + ); + writeFileSync(path.join(fixture, packageJsonFile), '{"private":true,"type":"module"}\n'); + writeFileSync( + path.join(fixture, tsconfigFile), + JSON.stringify({ files: [], references: [{ path: './referenced' }] }), + ); + writeFileSync( + path.join(fixture, 'referenced/tsconfig.json'), + JSON.stringify({ + compilerOptions: { + composite: true, + declaration: true, + emitDeclarationOnly: true, + outDir: './output', + strict: true, + types: [], + }, + files: ['./index.ts'], + }), + ); + const sourceFile = path.join(fixture, 'referenced/index.ts'); + writeFileSync(sourceFile, 'export const referenceGateFixture: number = 1;\n'); + const [runtime, wrapper, ...args] = packageJson.scripts.typecheck.split(' '); + 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 = 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); + } finally { + rmSync(fixture, { force: true, recursive: true }); + } + }), +); diff --git a/app/scripts/validate-ultramodern-workspace.mts b/app/scripts/validate-ultramodern-workspace.mts index 33e71f5df..35ccd882a 100644 --- a/app/scripts/validate-ultramodern-workspace.mts +++ b/app/scripts/validate-ultramodern-workspace.mts @@ -292,12 +292,10 @@ const workspaceValidationContractDefinition = { 'action:test:integration': 'pnpm --filter @app/core-runtime action:test:integration', 'deployment-impact:plan': 'node ./scripts/plan-deployment-impact.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': - '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/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/root-environment.test.mts scripts/tests/typecheck-project-references.test.mts', + 'test:scripts': 'rstest --project scripts', 'test:unit': 'pnpm -r --if-present run test:unit && pnpm -r --if-present run test:component', }, cloudflareSecurity: { @@ -5071,6 +5069,7 @@ const assertTsConfigReferenceGraph = () => { SHARED_VALIDATOR_STRING_047, ...fullStackVerticals.map((vertical) => vertical.path), ...additionalShellPaths, + 'packages/effect-rstest', ].map((referencePath) => ({ path: referencePath })); const expectedShellReferences = [ SHARED_VALIDATOR_STRING_092, @@ -7014,11 +7013,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 53e102faf..aa545354d 100644 --- a/app/tools/oxlint/effect-native/README.md +++ b/app/tools/oxlint/effect-native/README.md @@ -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 @@ -137,3 +137,23 @@ references establish that boundary; an exported Promise helper remains an owned 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 `@app/effect-rstest` so the runner owns services, scopes, +test time, and configuration. There is no harness-path allowlist: the runner implementation in +`packages/effect-rstest/src/**` is already outside test-file scope. That vendored upstream port is +ignored by workspace lint; `packages/effect-rstest/tests/**` remains linted. + +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`, +`@rstest/core`, and the retired `@app/core-runtime/testing/effect-runtime` 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/repository-policy.config.ts b/app/tools/oxlint/effect-native/repository-policy.config.ts index 5572b22a9..abe01e40c 100644 --- a/app/tools/oxlint/effect-native/repository-policy.config.ts +++ b/app/tools/oxlint/effect-native/repository-policy.config.ts @@ -1,8 +1,7 @@ import { defineConfig } from 'oxlint'; -/** The operator/discriminant policy also covers tooling and root configuration files. */ +/** Repository policies also cover tooling tests and root configuration files. */ export default defineConfig({ - jsPlugins: [{ name: 'effect-native', specifier: './index.ts' }], categories: { correctness: 'off' }, ignorePatterns: [ '**/node_modules/**', @@ -15,8 +14,37 @@ export default defineConfig({ '**/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: [ + { message: 'Import test APIs from @app/effect-rstest instead.', name: 'node:test' }, + { + message: 'Import assertions from @app/effect-rstest instead.', + name: 'node:assert', + }, + { + message: 'Import assertions from @app/effect-rstest instead.', + name: 'node:assert/strict', + }, + { + message: 'Import test APIs from @app/effect-rstest instead.', + name: '@rstest/core', + }, + ], + }, + ], + }, + }, + ], rules: { 'effect-native/no-instanceof': 'error', - 'effect-native/no-manual-tag-comparison': ['error', { include: ['**'], adtTags: [] }], + '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 230adeace..e1e1b778b 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 @@ -37,8 +37,8 @@ * * ## 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 runner implementation in `packages/effect-rstest/src/**` is outside test-file scope and + * 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. @@ -76,12 +76,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/**', @@ -102,7 +96,6 @@ const DEFAULT_EFFECT_MODULE_SOURCES: readonly string[] = [ ]; interface RuleOptions { - readonly harnessPaths?: readonly string[]; readonly ignorePaths?: readonly string[]; readonly testPaths?: readonly string[]; readonly effectModules?: readonly string[]; @@ -122,7 +115,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, @@ -215,34 +207,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, ' + + 'repository-owned @app/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 @app/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 @app/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 @app/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 @app/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 @app/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' } }, @@ -253,7 +244,6 @@ export const rule = defineRule({ ], defaultOptions: [ { - harnessPaths: [...DEFAULT_HARNESS_PATHS], ignorePaths: [...DEFAULT_IGNORE_PATHS], testPaths: [], effectModules: [...DEFAULT_EFFECT_MODULES], @@ -264,8 +254,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-promise-shaped-port.ts b/app/tools/oxlint/effect-native/rules/no-promise-shaped-port.ts index 8c51c0156..9b75f291f 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 @@ -565,7 +565,8 @@ export const rule = defineRule({ /** The `Promise` / `PromiseLike` reference of a return/value annotation, if any. */ const promiseReference = ( - annotation: ESTree.TSTypeAnnotation | null | undefined, + annotation: ESTree.TSTypeAnnotation | ESTree.TSTypeReference | null | undefined, + functionAliasOnly = false, ): string | null => { if (annotation === null || annotation === undefined) return null; interface TypeBinding { @@ -576,19 +577,43 @@ export const rule = defineRule({ raw: any, seen = new Set(), substitutions: ReadonlyMap = new Map(), + insideFunction = false, ): string | null => { if (!raw || seen.has(raw)) return null; seen.add(raw); if (raw.type === 'TSTypeAnnotation' || raw.type === 'TSParenthesizedType') - return resolve(raw.typeAnnotation, seen, substitutions); + return resolve(raw.typeAnnotation, seen, substitutions, insideFunction); + // Direct function types have their own visitor. Applied aliases need this + // traversal here because that visitor cannot see the use-site substitutions. + if (raw.type === 'TSFunctionType') + return substitutions.size === 0 + ? null + : resolve(raw.returnType, seen, substitutions, true); if (raw.type === 'TSTypeParameter') return ( - resolve(raw.constraint, new Set(seen), substitutions) ?? - resolve(raw.default, new Set(seen), substitutions) + resolve(raw.constraint, new Set(seen), substitutions, insideFunction) ?? + resolve(raw.default, new Set(seen), substitutions, insideFunction) ); if (raw.type === 'TSUnionType' || raw.type === 'TSIntersectionType') { for (const item of raw.types) { - const result = resolve(item, new Set(seen), substitutions); + const result = resolve(item, new Set(seen), substitutions, insideFunction); + if (result) return result; + } + return null; + } + // Object members have their own visitors too; only an applied generic alias + // (`type Service = { run: () => T }` used as `Service>`) needs this. + if (raw.type === 'TSTypeLiteral' || raw.type === 'TSInterfaceBody') { + if (substitutions.size === 0) 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, + new Set(seen), + substitutions, + insideFunction || !isValue, + ); if (result) return result; } return null; @@ -603,13 +628,13 @@ export const rule = defineRule({ !variableFor(raw, 'globalThis')?.defs.length && options.promiseTypes.includes(name) ) - return `${name}<…>`; + return functionAliasOnly && !insideFunction ? null : `${name}<…>`; if (names.length !== 1) return null; const variable = variableFor(raw.typeName, name); const bound = substitutions.get(variable); - if (bound) return resolve(bound.node, seen, bound.substitutions); - const alias = variable?.defs.find( - (d: any) => d.node.type === 'TSTypeAliasDeclaration', + if (bound) return resolve(bound.node, seen, bound.substitutions, insideFunction); + const alias = variable?.defs.find((d: any) => + ['TSTypeAliasDeclaration', 'TSInterfaceDeclaration'].includes(d.node.type), )?.node; if (alias) { const applied = new Map(substitutions); @@ -624,12 +649,12 @@ export const rule = defineRule({ substitutions: argument ? substitutions : applied, }); } - return resolve(alias.typeAnnotation, seen, applied); + return resolve(alias.typeAnnotation ?? alias.body, seen, applied, insideFunction); } const parameter = variable?.defs.find((d: any) => d.node.type === 'TSTypeParameter')?.node; - if (parameter) return resolve(parameter, seen, substitutions); + if (parameter) return resolve(parameter, seen, substitutions, insideFunction); if (variable?.defs.length || !options.promiseTypes.includes(name)) return null; - return `${name}<…>`; + return functionAliasOnly && !insideFunction ? null : `${name}<…>`; }; return resolve(annotation); }; @@ -900,6 +925,17 @@ export const rule = defineRule({ }; return { + TSTypeReference: (node: ESTree.TSTypeReference) => { + const annotation = parentOf(node as unknown as AnyNode); + if (annotation?.type !== 'TSTypeAnnotation') return; + const owner = parentOf(annotation); + if (owner?.type !== 'Identifier' && owner?.type !== 'RestElement') 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; 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 5a65fb222..c7cf01ba5 100644 --- a/app/tools/oxlint/effect-native/tests/discover-rules.test.mts +++ b/app/tools/oxlint/effect-native/tests/discover-rules.test.mts @@ -1,39 +1,52 @@ -import assert from 'node:assert/strict'; +import { expect, it } from '@app/effect-rstest'; +import { Effect, Predicate } from 'effect'; + import { spawnSync } from 'node:child_process'; -import { copyFileSync, mkdirSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { test } from 'node:test'; +import { copyFileSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; + import { pathToFileURL } from 'node:url'; 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({ + catch: (cause) => cause, + try: () => discoverRules(['not-a-rule']), + }), + ); + const message: unknown = expect.stringMatching(/Unknown fixture rule: not-a-rule/u); + expect(error).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 moduleUrl = pathToFileURL(join(shared, 'discover-rules.ts')).href; + const moduleUrl = pathToFileURL(realpathSync(path.join(shared, discoveryFile))).href; const result = spawnSync( process.execPath, [ @@ -63,10 +76,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 de08e95ac..98ca1944d 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures.test.mts +++ b/app/tools/oxlint/effect-native/tests/fixtures.test.mts @@ -1,7 +1,6 @@ -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 '@app/effect-rstest'; import { fixtureConfigPath, @@ -15,45 +14,45 @@ 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); + }); } 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: 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}`); @@ -63,10 +62,12 @@ for (const rule of rules) { } } 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})`); + } } - 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/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/harness-usage.test.tsx b/app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/harness-usage.test.tsx index 36ce8b362..d3df3a7ef 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 "@app/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..7a589e265 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 "@app/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..da4c2cb4a 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 "@app/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-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 index 2fde48c8c..2859026d7 100644 --- 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 @@ -1,4 +1,4 @@ -// expect-count: 7 +// expect-count: 21 import type { Effect } from 'effect'; type Operation = PromiseLike | Effect.Effect; interface Constrained = Operation> { run(): R; } @@ -10,3 +10,25 @@ 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/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 index 517869754..b7bc3e81e 100644 --- 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 @@ -7,3 +7,23 @@ 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/launcher.test.mts b/app/tools/oxlint/effect-native/tests/launcher.test.mts index 6effbcc92..1ca14e251 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 '@app/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 4be57463c..8c383c257 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, relative, 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', ); export interface Diagnostic { diff --git a/app/tools/oxlint/effect-native/tests/oxlint.test.mts b/app/tools/oxlint/effect-native/tests/oxlint.test.mts index 07e9a6ab3..c07991021 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 '@app/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..4108e8607 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 '@app/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..62a3bcd59 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 '@app/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..b5a8e5eef 100644 --- a/app/tools/oxlint/effect-native/tests/registration.test.mts +++ b/app/tools/oxlint/effect-native/tests/registration.test.mts @@ -1,77 +1,100 @@ -import assert from 'node:assert/strict'; +import { expect, it } from '@app/effect-rstest'; +import { Schema } from 'effect'; import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { test } from 'node:test'; +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'; -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), + }), + ), + 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); } }); diff --git a/app/tools/oxlint/effect-native/tests/repository-policy.test.mts b/app/tools/oxlint/effect-native/tests/repository-policy.test.mts index 662a56e6e..47e620312 100644 --- a/app/tools/oxlint/effect-native/tests/repository-policy.test.mts +++ b/app/tools/oxlint/effect-native/tests/repository-policy.test.mts @@ -1,19 +1,17 @@ -import assert from 'node:assert/strict'; -import { join } from 'node:path'; -import { test } from 'node:test'; +import { expect, it } from '@app/effect-rstest'; +import nodePath from 'node:path'; import { appRoot, pluginDirectory, runOxlint } from './oxlint.mts'; -test('all repository source, including tools and root configuration, follows the Effect discrimination policy', () => { +it('all repository source, including tools and root configuration, follows the Effect discrimination policy', () => { const run = runOxlint( - join(pluginDirectory, 'repository-policy.config.ts'), + nodePath.join(pluginDirectory, 'repository-policy.config.ts'), ['.', '--ignore-pattern', 'tools/oxlint/**/tests/fixtures/**'], appRoot, ); - assert.deepEqual( + expect( run.diagnostics.map( - ({ filename, labels, code }) => `${filename}:${labels[0]?.span.line} ${code}`, + ({ code, filename, labels }) => `${filename}:${labels[0]?.span.line} ${code}`, ), - [], - ); - assert.equal(run.exitCode, 0); + ).toEqual([]); + expect(run.exitCode).toBe(0); }); 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 87c2ab531..1974344b7 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,6 @@ -import assert from 'node:assert/strict'; +import { expect, it } from '@app/effect-rstest'; import { mkdirSync, writeFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { test } from 'node:test'; +import nodePath from 'node:path'; import { runOxlint, testsDirectory } from './oxlint.mts'; import { withTemporaryWorkspace } from './temporary-workspace.mts'; @@ -26,26 +25,29 @@ 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((directory) => { const workspaces = ['apps/shell-super-app', 'verticals/contacts', 'packages/core-runtime']; const sources = workspaces.map((workspace) => `${workspace}/src/operation.ts`); 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 }] @@ -56,28 +58,29 @@ for (const { rule, source } of cases) { for (const absolute of [false, true]) { const run = runOxlint( config, - absolute ? paths.map((path) => join(directory, path)) : paths, + absolute ? paths.map((path) => nodePath.join(directory, path)) : paths, 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( - run.diagnostics.map((diagnostic) => diagnostic.filename.replaceAll('\\', '/')), + run.diagnostics.map((diagnostic) => + (nodePath.isAbsolute(diagnostic.filename) + ? nodePath.relative(directory, diagnostic.filename) + : diagnostic.filename + ).replaceAll('\\', '/'), + ), ), ]; - assert.deepEqual( - reported.sort(), - (includeScripts ? paths : sources).toSorted(), + expect( + reported.toSorted(), `${rule}: includeScripts=${includeScripts}, absolute=${absolute}`, - ); + ).toEqual((includeScripts ? paths : sources).toSorted()); } } }); 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..ec089cf63 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 '@app/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/rstest.config.ts b/app/verticals/party-registry/rstest.config.ts index 708482553..2a0274fbc 100644 --- a/app/verticals/party-registry/rstest.config.ts +++ b/app/verticals/party-registry/rstest.config.ts @@ -4,6 +4,9 @@ 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({ projects: [ { @@ -25,6 +28,11 @@ export default defineConfig({ include: ['tests/integration/**/*.test.ts'], testTimeout: 30_000, }, - { name: 'unit', testEnvironment: 'node', include: ['tests/unit/**/*.test.ts'] }, + { + name: 'unit', + testEnvironment: 'node', + include: ['tests/unit/**/*.test.ts'], + tools: { swc }, + }, ], }); 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 265aac85a..f7aef5817 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 '@app/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 96776cba5..05fd76e99 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 '@app/effect-rstest'; + import { loadDatabaseConnectionPair } from '@app/core-runtime'; import { makeLiveOperationFixture } from '@app/core-runtime/testing/actions'; @@ -15,8 +12,6 @@ import { Effect, Layer, Match, - Exit as NativeExit, - Scope as NativeScope, Option, Redacted, Schema, @@ -24,9 +19,7 @@ import { } 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'; @@ -73,11 +66,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, @@ -106,46 +94,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()); -void 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) @@ -154,398 +138,396 @@ void test('exported ARES coordinator uses real authorized HTTP commands, canonic .setIssuedAt() .setExpirationTime('5m') .setJti(randomUUID()) - .sign(privateKey); - const token = yield* promiseEffect(sign.bind(undefined, fixture.manager)); - const authorization = `Bearer ${token}`; - const gateway = makeActionGateway(() => - 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 token = yield* sign(fixture.manager); + const authorization = `Bearer ${token}`; + const gateway = makeActionGateway(() => + 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 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(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 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(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'), - }, - }, - 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', - }, - authorization, - options(), - ), - ); - assert.ok(reviewed.partyRef); - return reviewed.partyRef; - }); - const lookup = () => - runHttpEffect( - executeAresLookupWithAuthorization({ ico: lookupIco }, authorization, randomUUID(), { - baseUrl, - }), - ); - const detail = (partyRef: PartyRef) => - runHttpEffect( - executePartyDetailWithAuthorization( - { partyRef, includeFactHistory: true }, - 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 }, + authorization, + options(), ), ); - const partyRef = yield* create(); - 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( - 'failure' in unconfirmed && - Predicate.isTagged(unconfirmed.failure, 'AresApplySelectionInvalid'), - ); - 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(Predicate.isTagged(applied, 'AresApplyCompleted'), 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.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(Predicate.isTagged(replay, 'AresApplyCompleted')); - 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 = makeActionGateway(() => - promiseEffect(sign.bind(undefined, fixture.denied)).pipe( - Effect.map((signedToken) => ({ expiresAt: 0, token: signedToken })), + authorization, + options(), ), ); - const denied = yield* runHttpEffect( - applyAresObservation(request, { gateway: deniedGateway, baseUrl }).pipe(Effect.result), - ); - assert.ok( - 'failure' in denied && Predicate.isTagged(denied.failure, 'AresLookupForbiddenProblem'), - ); - 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 }), + return reviewed.partyRef; + }); + const lookup = () => + runHttpEffect( + executeAresLookupWithAuthorization({ ico: lookupIco }, authorization, randomUUID(), { + baseUrl, + }), ); - const collisionOutcome = Match.value(collision).pipe( - Match.tag('AresApplyPartiallyCompleted', (outcome) => outcome), - Match.orElse(() => - assert.fail(`Expected partial completion, received ${collision._tag}`), + const detail = (partyRef: PartyRef) => + runHttpEffect( + executePartyDetailWithAuthorization( + { partyRef, includeFactHistory: true }, + 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 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 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), }, - 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, 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; + 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 = makeActionGateway(() => + 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), + }, + 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, 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 2bc0e81b6..7317165ca 100644 --- a/app/verticals/party-registry/tests/integration/database-boundary.test.ts +++ b/app/verticals/party-registry/tests/integration/database-boundary.test.ts @@ -1,16 +1,10 @@ -import { - makeEffectTestCallback as nativeTestCallback, - runEffectTestPromise, - runEffectTestSync as runNativeSync, -} from '@app/core-runtime/testing/effect-runtime'; +import { assert, expect, it } from '@app/effect-rstest'; + import { findPostgresFailure, loadDatabaseConnectionPair } from '@app/core-runtime'; -import { DateTime, Effect, Exit as NativeExit, Scope as NativeScope, Option } from 'effect'; -// @effect-diagnostics asyncFunction:off globalDate:off -- Existing compatibility boundary; expires: 2026-12-31. +import { DateTime, Effect, Option, 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 { Pool } from 'pg'; import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.ts'; import { RuleKeySchema } from '../../shared/domain/matching-contracts.ts'; @@ -36,11 +30,6 @@ 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), -); - const tenantA = 'a1000000-0000-4000-8000-000000000001'; const tenantB = 'a1000000-0000-4000-8000-000000000002'; const legalEntityA = 'a2000000-0000-4000-8000-000000000001'; @@ -69,100 +58,73 @@ const hasPostgreSqlCode = (error: Parameters[0]): boolean => Option.exists(findPostgresFailure(error), ({ code }) => code === expected); -test('enforces Party owner invariants, tenant isolation, and independent fact lifecycles', async () => { - const connections = await runEffectTestPromise(loadDatabaseConnectionPair()); - const adminPool = new Pool({ connectionString: connections.admin.connectionString }); - const runtimePool = new Pool({ connectionString: connections.runtime.connectionString, max: 1 }); - const admin = await runEffectTestPromise( - makeTestDatabaseFromPool(adminPool, partyRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ), - ); - const runtime = await runEffectTestPromise( - makeTestDatabaseFromPool(runtimePool, partyRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ), - ); - - const cleanup = async () => { - await runEffectTestPromise( - admin.delete(partyCorrections).where(inArray(partyCorrections.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin.delete(partyAliases).where(inArray(partyAliases.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin.delete(partyMerges).where(inArray(partyMerges.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin - .delete(partyMatchDecisions) - .where(inArray(partyMatchDecisions.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin - .delete(duplicateCandidateCaseParties) - .where(inArray(duplicateCandidateCaseParties.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin - .delete(duplicateCandidateCases) - .where(inArray(duplicateCandidateCases.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin - .delete(counterpartyRoleAdminReadModels) - .where(inArray(counterpartyRoleAdminReadModels.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin - .delete(counterpartyAdminReadModels) - .where(inArray(counterpartyAdminReadModels.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin - .delete(counterpartyRolePeriods) - .where(inArray(counterpartyRolePeriods.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin.delete(counterparties).where(inArray(counterparties.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin.delete(partyRelationships).where(inArray(partyRelationships.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin - .delete(partyContactPointPurposes) - .where(inArray(partyContactPointPurposes.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin.delete(partyContactPoints).where(inArray(partyContactPoints.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin - .delete(partyIdentifierClaims) - .where(inArray(partyIdentifierClaims.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin - .delete(partyOfficialIdentifiers) - .where(inArray(partyOfficialIdentifiers.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin - .delete(partyFactAssertions) - .where(inArray(partyFactAssertions.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin.delete(parties).where(inArray(parties.tenantId, fixtureTenants)), +it.live('enforces Party owner invariants, tenant isolation, and independent fact lifecycles', () => + Effect.gen(function* testEffect1() { + 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: 1 }), + ), + (pool) => Effect.promise(() => pool.end()).pipe(Effect.orDie), ); - }; + const admin = yield* makeTestDatabaseFromPool(adminPool, partyRelations); + const runtime = yield* makeTestDatabaseFromPool(runtimePool, partyRelations); - const withTenant = ( - tenantId: string, - operation: (transaction: PartyTransaction) => Effect.Effect, - ): Promise => - runEffectTestPromise( + const cleanup = () => + Effect.gen(function* testEffect2() { + yield* admin + .delete(partyCorrections) + .where(inArray(partyCorrections.tenantId, fixtureTenants)); + yield* admin.delete(partyAliases).where(inArray(partyAliases.tenantId, fixtureTenants)); + yield* admin.delete(partyMerges).where(inArray(partyMerges.tenantId, fixtureTenants)); + yield* admin + .delete(partyMatchDecisions) + .where(inArray(partyMatchDecisions.tenantId, fixtureTenants)); + yield* admin + .delete(duplicateCandidateCaseParties) + .where(inArray(duplicateCandidateCaseParties.tenantId, fixtureTenants)); + yield* admin + .delete(duplicateCandidateCases) + .where(inArray(duplicateCandidateCases.tenantId, fixtureTenants)); + yield* admin + .delete(counterpartyRoleAdminReadModels) + .where(inArray(counterpartyRoleAdminReadModels.tenantId, fixtureTenants)); + yield* admin + .delete(counterpartyAdminReadModels) + .where(inArray(counterpartyAdminReadModels.tenantId, fixtureTenants)); + yield* admin + .delete(counterpartyRolePeriods) + .where(inArray(counterpartyRolePeriods.tenantId, fixtureTenants)); + yield* admin.delete(counterparties).where(inArray(counterparties.tenantId, fixtureTenants)); + yield* admin + .delete(partyRelationships) + .where(inArray(partyRelationships.tenantId, fixtureTenants)); + yield* admin + .delete(partyContactPointPurposes) + .where(inArray(partyContactPointPurposes.tenantId, fixtureTenants)); + yield* admin + .delete(partyContactPoints) + .where(inArray(partyContactPoints.tenantId, fixtureTenants)); + yield* admin + .delete(partyIdentifierClaims) + .where(inArray(partyIdentifierClaims.tenantId, fixtureTenants)); + yield* admin + .delete(partyOfficialIdentifiers) + .where(inArray(partyOfficialIdentifiers.tenantId, fixtureTenants)); + yield* admin + .delete(partyFactAssertions) + .where(inArray(partyFactAssertions.tenantId, fixtureTenants)); + yield* admin.delete(parties).where(inArray(parties.tenantId, fixtureTenants)); + }); + + const withTenant = ( + tenantId: string, + operation: (transaction: PartyTransaction) => Effect.Effect, + ) => runtime.transaction((transaction) => Effect.gen(function* transactionTestBody() { yield* transaction.execute( @@ -171,66 +133,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, @@ -263,88 +214,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, @@ -355,52 +301,50 @@ 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, + }, + ]); const purposeEvidence = { acceptedByActionInvocationId: actionA, acceptedByPrincipalId: principalA, @@ -412,134 +356,123 @@ 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', - }), - ), - 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.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', + yield* admin.insert(partyContactPointPurposes).values([ + { ...purposeEvidence, contactPointId: addressA, purposeKey: 'BILLING' }, + { ...purposeEvidence, contactPointId: addressA, purposeKey: 'DELIVERY' }, ]); - assert.deepEqual(scheduledContactEnd?.endEvidenceRefs, []); - const [scheduledPurposeEnd] = await runEffectTestPromise( - 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')), + expect( + hasPostgreSqlCode('23505')( + yield* Effect.flip( + admin.insert(partyContactPointPurposes).values({ + ...purposeEvidence, + contactPointId: addressA2, + 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); + 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(eq(partyContactPointPurposes.purposeKey, 'DELIVERY')); + const [scheduledContactEnd] = yield* admin + .select() + .from(partyContactPoints) + .where(eq(partyContactPoints.contactPointId, emailA2)); + 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(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(eq(partyContactPointPurposes.purposeKey, 'BILLING')), + ), ), - 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', + 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')), + }), + ), + ), + ).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( @@ -550,138 +483,137 @@ 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(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(eq(counterpartyRolePeriods.roleType, 'CUSTOMER')); const futureRoleEvidence = { acceptedByActionInvocationId: actionA, acceptedByPrincipalId: principalA, @@ -697,252 +629,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, @@ -950,8 +863,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, @@ -966,52 +881,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 4a68bcade..8956f3401 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,16 +1,8 @@ -import { - makeEffectTestCallback as nativeTestCallback, - runEffectTestPromise, - runEffectTestSync as runNativeSync, -} from '@app/core-runtime/testing/effect-runtime'; +import { expect, it } from '@app/effect-rstest'; import { findPostgresFailure, loadDatabaseConnectionPair } from '@app/core-runtime'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. - import { eq, inArray, sql } from 'drizzle-orm'; -import { Effect, Exit as NativeExit, Scope as NativeScope, Option } from 'effect'; -import assert from 'node:assert/strict'; -import test, { after as afterNativeDatabase } from 'node:test'; +import { Effect, Option } from 'effect'; import { Pool } from 'pg'; import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.ts'; import { @@ -19,11 +11,6 @@ import { personEngagementProfiles, } from '../../src/db/engagement-schema.ts'; -const nativeDatabaseScope = runNativeSync(NativeScope.make()); -afterNativeDatabase( - NativeScope.close(nativeDatabaseScope, NativeExit.void).pipe(nativeTestCallback), -); - const tenantA = 'c1000000-0000-4000-8000-000000000001'; const tenantB = 'c1000000-0000-4000-8000-000000000002'; const fixtureTenants = [tenantA, tenantB] as const; @@ -33,52 +20,48 @@ const hasPostgreSqlCode = (error: Parameters[0]): boolean => Option.exists(findPostgresFailure(error), ({ code }) => code === expected); -test('enforces tenant isolation and canonical-reference uniqueness without cross-vertical FKs', async () => { - const connections = await runEffectTestPromise(loadDatabaseConnectionPair()); - const adminPool = new Pool({ connectionString: connections.admin.connectionString }); - const runtimePool = new Pool({ connectionString: connections.runtime.connectionString, max: 1 }); - const admin = await runEffectTestPromise( - makeTestDatabaseFromPool(adminPool, contactsRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ), - ); - const runtime = await runEffectTestPromise( - makeTestDatabaseFromPool(runtimePool, contactsRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ), - ); - const cleanup = async () => { - await runEffectTestPromise( - admin - .delete(personEngagementProfiles) - .where(inArray(personEngagementProfiles.tenantId, fixtureTenants)), - ); - await runEffectTestPromise( - admin - .delete(organizationEngagementProfiles) - .where(inArray(organizationEngagementProfiles.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 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: 1 }), + ), + (pool) => Effect.promise(() => pool.end()).pipe(Effect.orDie), + ); + const admin = yield* makeTestDatabaseFromPool(adminPool, contactsRelations); + const runtime = yield* makeTestDatabaseFromPool(runtimePool, contactsRelations); + const cleanup = () => + Effect.gen(function* testEffect2() { + yield* admin + .delete(personEngagementProfiles) + .where(inArray(personEngagementProfiles.tenantId, fixtureTenants)); + yield* admin + .delete(organizationEngagementProfiles) + .where(inArray(organizationEngagementProfiles.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)`, @@ -101,7 +84,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({ @@ -109,50 +92,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)`, @@ -163,19 +143,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 00bcf2048..1e60e05c7 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 '@app/effect-rstest'; + import type { TrustedPrincipalContext } from '@app/core-runtime'; import { CoreSearchQueryRuntimeLive, @@ -14,18 +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, - Predicate, -} 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'; @@ -58,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, @@ -101,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()); -void 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(); @@ -137,11 +119,9 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c ); 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>) => @@ -188,23 +168,28 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c 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', { @@ -215,15 +200,15 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c }), ), ); - 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' }, @@ -232,16 +217,16 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c }); 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]; @@ -251,16 +236,18 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c function* verifyCommitRecoveryEffect(value) { const key = randomUUID(); fixture.faultNextTransaction('lost-ack'); - assert.ok( + 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.ok( + assert.isOk(invocation); + + assert.isOk( Predicate.isTagged( yield* run( resolveActionCommit({ @@ -271,6 +258,7 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c 'ActionAlreadyCommitted', ), ); + const recovered = yield* run( ReadRuntime.pipe( Effect.flatMap((runtime) => @@ -286,25 +274,28 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c 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.ok( + 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.ok( + 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', @@ -315,40 +306,43 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c { concurrency: 1, discard: true }, ); const beforeDenied = yield* snapshot(); - assert.ok( + 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'); 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.ok( + 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.ok( + + assert.isOk( Predicate.isTagged( yield* run( create(candidate('00006947'), randomUUID(), fixture.legalEntityOnly).pipe( @@ -358,12 +352,14 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c 'ActionPermissionDenied', ), ); - assert.ok( + + assert.isOk( Predicate.isTagged( yield* run(readPartyDetail(partyRef, fixture.legalEntityOnly).pipe(Effect.flip)), 'ReadPermissionDenied', ), ); + const searchLayer = PartySearchProjectionGatewayLive.pipe( Layer.provide(CoreSearchQueryRuntimeLive), ); @@ -381,10 +377,11 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c ), Effect.provideContext(searchContext), ); - assert.ok( + assert.isOk( Predicate.isTagged(yield* run(deniedSearch.pipe(Effect.flip)), 'ReadPermissionDenied'), ); - assert.ok( + + assert.isOk( Predicate.isTagged( yield* run( ReadRuntime.pipe( @@ -419,10 +416,11 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c 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( @@ -438,10 +436,11 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c ); // 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', @@ -491,10 +490,9 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c }), ); 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( @@ -515,7 +513,8 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c }), ), ); - assert.ok(person.outcome === 'AMBIGUOUS'); + assert.isOk(person.outcome === 'AMBIGUOUS'); + const reviewedPerson = yield* run( runAction({ registration: resolveDuplicateCandidateCreateAction, @@ -528,7 +527,8 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c transport: transport(), }), ); - assert.ok(reviewedPerson.partyRef); + assert.isOk(reviewedPerson.partyRef); + const relationship = yield* run( runAction({ registration: createPartyRelationshipAction, @@ -545,7 +545,7 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c }), ); // Domain relationships never provision access to Party records. - assert.ok( + assert.isOk( Predicate.isTagged( yield* run( readPartyDetail(reviewedPerson.partyRef, fixture.legalEntityOnly).pipe(Effect.flip), @@ -553,12 +553,14 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c 'ReadPermissionDenied', ), ); - assert.ok( + + assert.isOk( Predicate.isTagged( yield* run(readPartyDetail(partyRef, fixture.legalEntityOnly).pipe(Effect.flip)), 'ReadPermissionDenied', ), ); + const updatedRelationship = yield* run( runAction({ registration: updatePartyRelationshipAction, @@ -573,7 +575,7 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c transport: transport(), }), ); - assert.equal(updatedRelationship.outcome, 'CHANGED'); + expect(updatedRelationship.outcome).toBe('CHANGED'); const endedRelationship = yield* run( runAction({ registration: endPartyRelationshipAction, @@ -588,12 +590,13 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c 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, @@ -616,7 +619,8 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c ), ) .limit(1); - assert.ok(identifierTemplate); + assert.isOk(identifierTemplate); + yield* admin.insert(partyOfficialIdentifiers).values({ ...identifierTemplate, officialIdentifierId: randomUUID(), @@ -634,7 +638,7 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c transport: transport(), }), ); - assert.ok( + assert.isOk( collision.outcome === 'BLOCKED' && collision.reasonCode === 'EXACT_CLAIM_CONFLICT', ); @@ -652,9 +656,10 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c }), ); 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); + expect(archivedCounterparty.party.archived).toBe(true); yield* run(counterparty().pipe(Effect.flip)); const unarchive = yield* run( runAction({ @@ -668,10 +673,10 @@ void test('governed Party identity uses real PostgreSQL and SpiceDB for atomic c 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 c6010707b..cf46315b6 100644 --- a/app/verticals/party-registry/tests/integration/identity-concurrency.test.ts +++ b/app/verticals/party-registry/tests/integration/identity-concurrency.test.ts @@ -1,15 +1,7 @@ -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 { expect, it } from '@app/effect-rstest'; import { loadDatabaseConnectionPair } from '@app/core-runtime'; 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 { DateTime, Effect, Option } from 'effect'; import { Pool } from 'pg'; import { makeTestDatabaseFromPool } from '../../../../packages/core-runtime/tests/support/database.ts'; import { normalizeOfficialIdentifier } from '../../shared/domain/identifier-contracts.ts'; @@ -32,155 +24,134 @@ 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), -); - 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 connections = await runEffectTestPromise(loadDatabaseConnectionPair()); - const adminPool = new Pool({ connectionString: connections.admin.connectionString }); - const runtimePool = new Pool({ connectionString: connections.runtime.connectionString, max: 2 }); - const admin = await runEffectTestPromise( - makeTestDatabaseFromPool(adminPool, partyRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ), - ); - const runtime = await runEffectTestPromise( - makeTestDatabaseFromPool(runtimePool, partyRelations).pipe( - NativeScope.provide(nativeDatabaseScope), - ), - ); - const cleanup = async () => { - await runEffectTestPromise( - admin.delete(partyMatchDecisions).where(eq(partyMatchDecisions.tenantId, tenantId)), - ); - await runEffectTestPromise( - admin - .delete(duplicateCandidateCaseParties) - .where(eq(duplicateCandidateCaseParties.tenantId, tenantId)), - ); - await runEffectTestPromise( - admin.delete(duplicateCandidateCases).where(eq(duplicateCandidateCases.tenantId, tenantId)), - ); - await runEffectTestPromise( - admin.delete(partyIdentifierClaims).where(eq(partyIdentifierClaims.tenantId, tenantId)), - ); - await runEffectTestPromise( - admin.delete(partyOfficialIdentifiers).where(eq(partyOfficialIdentifiers.tenantId, tenantId)), - ); - await runEffectTestPromise( - admin.delete(partyFactAssertions).where(eq(partyFactAssertions.tenantId, tenantId)), - ); - await runEffectTestPromise(admin.delete(parties).where(eq(parties.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, { +it.live( + 'real PostgreSQL identity locks serialize concurrent exact creates and repeated identifier acceptance', + () => + Effect.gen(function* identityConcurrencyTest() { + const connections = yield* loadDatabaseConnectionPair(); + const adminPool = 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, max: 2 }), + ), + (pool) => Effect.promise(() => pool.end()), + ); + const admin = yield* makeTestDatabaseFromPool(adminPool, partyRelations); + const runtime = yield* makeTestDatabaseFromPool(runtimePool, partyRelations); + const cleanup = Effect.gen(function* cleanupIdentityRecords() { + yield* admin.delete(partyMatchDecisions).where(eq(partyMatchDecisions.tenantId, tenantId)); + yield* admin + .delete(duplicateCandidateCaseParties) + .where(eq(duplicateCandidateCaseParties.tenantId, tenantId)); + yield* admin + .delete(duplicateCandidateCases) + .where(eq(duplicateCandidateCases.tenantId, tenantId)); + yield* admin + .delete(partyIdentifierClaims) + .where(eq(partyIdentifierClaims.tenantId, tenantId)); + yield* admin + .delete(partyOfficialIdentifiers) + .where(eq(partyOfficialIdentifiers.tenantId, tenantId)); + yield* admin.delete(partyFactAssertions).where(eq(partyFactAssertions.tenantId, tenantId)); + yield* admin.delete(parties).where(eq(parties.tenantId, tenantId)); + }); + 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/unit/api-integration-client-url.test.ts b/app/verticals/party-registry/tests/unit/api-integration-client-url.test.ts index 72cfecfa1..24da8f856 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 '@app/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 cc95fa23b..7cffd034f 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,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 '@app/effect-rstest'; import { Effect, Result } from 'effect'; import { FetchHttpClient } from 'effect/unstable/http'; import { @@ -9,115 +6,120 @@ import { requestSearchRebuildWithAuthorization, } from '../../src/api/party-command-client.ts'; -test('fresh assertions and command metadata reach the independent owner deployment', async () => { - const requests: Request[] = []; - let assertions = 0; - const fakeFetch: typeof fetch = (input, init) => { - const request = new Request(input, init); - requests.push(request); - if (new URL(request.url).hostname === 'shell.example') { - assertions += 1; +it.effect('fresh assertions and command metadata reach the independent owner deployment', () => + Effect.gen(function* testProgram1() { + const requests: Request[] = []; + let assertions = 0; + const fakeFetch: typeof fetch = (input, init) => { + const request = new Request(input, init); + requests.push(request); + if (new URL(request.url).hostname === 'shell.example') { + assertions += 1; + return Promise.resolve( + Response.json({ expiresAt: 2_000_000_000, token: `token-${assertions}` }), + ); + } return Promise.resolve( - Response.json({ expiresAt: 2_000_000_000, token: `token-${assertions}` }), + Response.json({ requestId: '10000000-0000-4000-8000-000000000001', status: 'QUEUED' }), ); - } - return Promise.resolve( - Response.json({ requestId: '10000000-0000-4000-8000-000000000001', status: 'QUEUED' }), - ); - }; - 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( + }; + 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('decodes declared errors without weakening their tag or stable conflict code', async () => { - const problem = { - _tag: 'PartyCommandConflictProblem', - code: 'action_request_hash_conflict', - detail: 'This key was used with a different command payload.', - status: 409, - title: 'Idempotency conflict', - type: 'urn:ontos:action:request-hash-conflict', - }; - const fakeFetch: typeof fetch = () => - Promise.resolve( - Response.json(problem, { - headers: { 'content-type': 'application/problem+json' }, - status: 409, - }), - ); - const outcome = await runEffectTestPromise( - requestSearchRebuildWithAuthorization({}, 'Bearer test', { +it.effect('decodes declared errors without weakening their tag or stable conflict code', () => + Effect.gen(function* testProgram2() { + const problem = { + _tag: 'PartyCommandConflictProblem', + code: 'action_request_hash_conflict', + detail: 'This key was used with a different command payload.', + status: 409, + title: 'Idempotency conflict', + type: 'urn:ontos:action:request-hash-conflict', + }; + const fakeFetch: typeof fetch = () => + Promise.resolve( + Response.json(problem, { + headers: { 'content-type': 'application/problem+json' }, + status: 409, + }), + ); + const outcome = yield* requestSearchRebuildWithAuthorization({}, 'Bearer test', { baseUrl: 'https://party.example/party-registry-api', correlationId: 'conflict', idempotencyKey: 'rebuild-1', - }).pipe(Effect.result, Effect.provideService(FetchHttpClient.Fetch, fakeFetch)), - ); - assert.ok(Result.isFailure(outcome)); - assert.deepEqual(outcome.failure, problem); -}); + }).pipe(Effect.result, Effect.provideService(FetchHttpClient.Fetch, fakeFetch)); + expect(Result.isFailure(outcome)).toBe(true); + if (!Result.isFailure(outcome)) { + throw new Error('Expected truthy value'); + } + expect(outcome.failure).toEqual(problem); + }), +); -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..e1eac5fe5 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 '@app/effect-rstest'; import { readFile, readdir } from 'node:fs/promises'; -import test from 'node:test'; -import { Schema } from 'effect'; +import { Effect, Schema } from 'effect'; import { partyRegistryCommandsApi, PartyCommandAliasWriteRejectedProblemSchema, @@ -16,61 +15,74 @@ 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', + }; + expect( + yield* Schema.decodeUnknownEffect(PartyCommandAliasWriteRejectedProblemSchema)(input), + ).toEqual(input); + }), +); -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 8e7ece6fe..d4dcbdda1 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,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 '@app/effect-rstest'; import { Effect, Match, Result, Schema } from 'effect'; import { FetchHttpClient } from 'effect/unstable/http'; import { @@ -23,257 +20,287 @@ const invocationId = Schema.decodeUnknownSync(ActionInvocationIdSchema)( '10000000-0000-4000-8000-000000000001', ); -test('already committed is terminal and carries the invocation for governed refresh', () => { - const problem = { - _tag: 'PartyCommandAlreadyCommittedProblem', - code: 'action_already_committed', - detail: 'Refresh the authoritative governed reads.', - invocationId, - resolution: 'REFRESH_GOVERNED_READS', - retryCommand: false, - status: 409, - title: 'Already committed', - type: 'urn:ontos:party:already-committed', - }; - assert.deepEqual( - Schema.decodeUnknownSync(PartyCommandAlreadyCommittedProblemSchema)(problem), - problem, - ); - for (const endpoint of Object.values(partyRegistryCommandsApi.groups.partyCommands.endpoints)) { - assert.ok([...endpoint.error].some((schema) => Schema.is(schema)(problem))); - } - assert.throws(() => - Schema.decodeUnknownSync(PartyCommandAlreadyCommittedProblemSchema)({ - ...problem, - retryCommand: true, - }), - ); -}); - -test('commit uncertainty retains a resolution handle and never instructs blind command retry', () => { - const problem = { - _tag: 'PartyCommandCommitIndeterminateProblem', - detail: 'Resolve the invocation before deciding the next step.', - invocationId, - resolution: 'RESOLVE_COMMIT', - retryCommand: false, - status: 503, - title: 'Commit outcome unknown', - type: 'urn:ontos:party:commit-indeterminate', - }; - assert.deepEqual( - Schema.decodeUnknownSync(PartyCommandCommitIndeterminateProblemSchema)(problem), - problem, - ); - for (const endpoint of Object.values(partyRegistryCommandsApi.groups.partyCommands.endpoints)) { - assert.ok([...endpoint.error].some((schema) => Schema.is(schema)(problem))); - } - assert.throws(() => - Schema.decodeUnknownSync(PartyCommandCommitIndeterminateProblemSchema)({ - ...problem, - retryCommand: true, - }), - ); - assert.throws(() => - Schema.decodeUnknownSync(ResolvePartyCommandCommitPayloadSchema)({ invocationId: 'invalid' }), - ); -}); +it.effect('already committed is terminal and carries the invocation for governed refresh', () => + Effect.gen(function* decodeContract1() { + const problem = { + _tag: 'PartyCommandAlreadyCommittedProblem', + code: 'action_already_committed', + detail: 'Refresh the authoritative governed reads.', + invocationId, + resolution: 'REFRESH_GOVERNED_READS', + retryCommand: false, + status: 409, + title: 'Already committed', + type: 'urn:ontos:party:already-committed', + }; + expect( + yield* Schema.decodeUnknownEffect(PartyCommandAlreadyCommittedProblemSchema)(problem), + ).toEqual(problem); + for (const endpoint of Object.values(partyRegistryCommandsApi.groups.partyCommands.endpoints)) { + expect([...endpoint.error].some((schema) => Schema.is(schema)(problem))).toBe(true); + } + expect(() => + Schema.decodeUnknownSync(PartyCommandAlreadyCommittedProblemSchema)({ + ...problem, + retryCommand: true, + }), + ).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)({ - _tag: 'PartyCommandCommitResolution', +it.effect( + 'commit uncertainty retains a resolution handle and never instructs blind command retry', + () => + Effect.gen(function* decodeContract2() { + const problem = { + _tag: 'PartyCommandCommitIndeterminateProblem', + detail: 'Resolve the invocation before deciding the next step.', invocationId, + resolution: 'RESOLVE_COMMIT', retryCommand: false, - state, - }), - { _tag: 'PartyCommandCommitResolution', invocationId, retryCommand: false, state }, - ); - } -}); - -test('the command client decodes indeterminate commits without losing recovery metadata', async () => { - const problem = { - _tag: 'PartyCommandCommitIndeterminateProblem', - detail: 'Resolve first.', - invocationId, - resolution: 'RESOLVE_COMMIT', - retryCommand: false, - status: 503, - title: 'Unknown commit', - type: 'urn:ontos:party:commit-indeterminate', - }; - const fakeFetch: typeof fetch = () => - Promise.resolve( - Response.json(problem, { - headers: { 'content-type': 'application/problem+json' }, status: 503, - }), - ); - const result = await runEffectTestPromise( - requestSearchRebuildWithAuthorization({}, 'Bearer test', { + title: 'Commit outcome unknown', + type: 'urn:ontos:party:commit-indeterminate', + }; + expect( + yield* Schema.decodeUnknownEffect(PartyCommandCommitIndeterminateProblemSchema)(problem), + ).toEqual(problem); + for (const endpoint of Object.values( + partyRegistryCommandsApi.groups.partyCommands.endpoints, + )) { + expect([...endpoint.error].some((schema) => Schema.is(schema)(problem))).toBe(true); + } + expect(() => + Schema.decodeUnknownSync(PartyCommandCommitIndeterminateProblemSchema)({ + ...problem, + retryCommand: true, + }), + ).toThrow(); + expect(() => + Schema.decodeUnknownSync(ResolvePartyCommandCommitPayloadSchema)({ + invocationId: 'invalid', + }), + ).toThrow(); + }), +); + +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']) { + expect( + yield* Schema.decodeUnknownEffect(ResolvePartyCommandCommitResultSchema)({ + _tag: 'PartyCommandCommitResolution', + invocationId, + retryCommand: false, + state, + }), + ).toEqual({ _tag: 'PartyCommandCommitResolution', invocationId, retryCommand: false, state }); + } + }), +); + +it.effect('the command client decodes indeterminate commits without losing recovery metadata', () => + Effect.gen(function* testProgram1() { + const problem = { + _tag: 'PartyCommandCommitIndeterminateProblem', + detail: 'Resolve first.', + invocationId, + resolution: 'RESOLVE_COMMIT', + retryCommand: false, + status: 503, + title: 'Unknown commit', + type: 'urn:ontos:party:commit-indeterminate', + }; + const fakeFetch: typeof fetch = () => + Promise.resolve( + Response.json(problem, { + headers: { 'content-type': 'application/problem+json' }, + status: 503, + }), + ); + const result = yield* requestSearchRebuildWithAuthorization({}, 'Bearer test', { baseUrl: 'https://party.example/party-registry-api', correlationId: 'uncertain', idempotencyKey: 'same-key', - }).pipe(Effect.result, Effect.provideService(FetchHttpClient.Fetch, fakeFetch)), - ); - assert.ok(Result.isFailure(result)); - assert.deepEqual(result.failure, problem); -}); + }).pipe(Effect.result, Effect.provideService(FetchHttpClient.Fetch, fakeFetch)); + expect(Result.isFailure(result)).toBe(true); + if (!Result.isFailure(result)) { + throw new Error('Expected truthy value'); + } + expect(result.failure).toEqual(problem); + }), +); -test('the command client preserves committed invocation metadata across HTTP', async () => { - const problem = { - _tag: 'PartyCommandAlreadyCommittedProblem', - code: 'action_already_committed', - detail: 'Refresh the authoritative governed reads.', - invocationId, - resolution: 'REFRESH_GOVERNED_READS', - retryCommand: false, - status: 409, - title: 'Already committed', - type: 'urn:ontos:party:already-committed', - }; - const fakeFetch: typeof fetch = () => - Promise.resolve( - Response.json(problem, { - headers: { 'content-type': 'application/problem+json' }, - status: 409, - }), - ); - const result = await runEffectTestPromise( - requestSearchRebuildWithAuthorization({}, 'Bearer test', { +it.effect('the command client preserves committed invocation metadata across HTTP', () => + Effect.gen(function* testProgram2() { + const problem = { + _tag: 'PartyCommandAlreadyCommittedProblem', + code: 'action_already_committed', + detail: 'Refresh the authoritative governed reads.', + invocationId, + resolution: 'REFRESH_GOVERNED_READS', + retryCommand: false, + status: 409, + title: 'Already committed', + type: 'urn:ontos:party:already-committed', + }; + const fakeFetch: typeof fetch = () => + Promise.resolve( + Response.json(problem, { + headers: { 'content-type': 'application/problem+json' }, + status: 409, + }), + ); + const result = yield* requestSearchRebuildWithAuthorization({}, 'Bearer test', { baseUrl: 'https://party.example/party-registry-api', correlationId: 'committed', 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: Request[] = []; - let assertions = 0; - const fakeFetch: typeof fetch = (input, init) => { - const request = new Request(input, init); - requests.push(request); - if (new URL(request.url).hostname === 'shell.example') { - assertions += 1; - return Promise.resolve( - Response.json({ expiresAt: 2_000_000_000, token: `fresh-${assertions}` }), - ); + }).pipe(Effect.result, Effect.provideService(FetchHttpClient.Fetch, fakeFetch)); + expect(Result.isFailure(result)).toBe(true); + if (!Result.isFailure(result)) { + throw new Error('Expected truthy value'); } - return Promise.resolve( - Response.json({ - _tag: 'PartyCommandCommitResolution', - invocationId, - retryCommand: false, - state: 'COMMITTED', - }), - ); - }; - 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', - ); - 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 }); -}); + expect(result.failure).toEqual(problem); + }), +); -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) => { +it.effect( + 'recovery acquires a fresh assertion without submitting an idempotency key or re-running a command', + () => + Effect.gen(function* testProgram3() { const requests: Request[] = []; let assertions = 0; - 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 fakeFetch: typeof fetch = (input, init) => { const request = new Request(input, init); requests.push(request); if (new URL(request.url).hostname === 'shell.example') { + assertions += 1; return Promise.resolve( - Response.json({ expiresAt: 2_000_000_000, token: `fresh-${(assertions += 1)}` }), - ); - } - if (request.url.endsWith('/resolve')) { - return Promise.resolve( - Response.json({ - _tag: 'PartyCommandCommitResolution', - invocationId, - retryCommand: false, - state: 'COMMITTED', - }), + Response.json({ expiresAt: 2_000_000_000, token: `fresh-${assertions}` }), ); } return Promise.resolve( 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, + _tag: 'PartyCommandCommitResolution', + invocationId, + retryCommand: false, + state: 'COMMITTED', }), ); }; - 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)), - ); - 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 requests: Request[] = []; + let assertions = 0; + 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 fakeFetch: typeof fetch = (input, init) => { + const request = new Request(input, init); + requests.push(request); + if (new URL(request.url).hostname === 'shell.example') { + return Promise.resolve( + Response.json({ expiresAt: 2_000_000_000, token: `fresh-${(assertions += 1)}` }), + ); + } + if (request.url.endsWith('/resolve')) { + return Promise.resolve( + Response.json({ + _tag: 'PartyCommandCommitResolution', + invocationId, + retryCommand: false, + state: 'COMMITTED', + }), + ); + } + return Promise.resolve( + 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, + }), + ); + }; + 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 76e9bb0fb..84a90cb4f 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,7 +1,5 @@ -// @effect-diagnostics asyncFunction:off nodeBuiltinImport:off -- Existing compatibility boundary; expires: 2026-12-31. -import assert from 'node:assert/strict'; +import { expect, it } from '@app/effect-rstest'; import { randomUUID } from 'node:crypto'; -import test from 'node:test'; import { ConfigProvider, Context, Effect, Layer, Schema, Predicate } from 'effect'; import { ReadRuntime, @@ -129,45 +127,54 @@ const endpointNames = [ 'updatePartyRelationship', ] as const; -const makeAssertion = async (audience = 'party-registry') => { - 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(issuer) - .setAudience(audience) - .setSubject(principal.principalId) - .setIssuedAt() - .setExpirationTime('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, - }; -}; +const makeAssertion = (audience = 'party-registry') => + Effect.gen(function* testProgram1() { + 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(issuer) + .setAudience(audience) + .setSubject(principal.principalId) + .setIssuedAt() + .setExpirationTime('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 mounted = ( - harness: ReturnType, + harness: Effect.Success>, environment: Readonly>, readRuntime?: ReadRuntimeService, ) => { @@ -208,19 +215,12 @@ const mounted = ( // still requires an explicitly empty per-request context. const emptyRequestContext = Context.makeUnsafe(new Map()); const handle = (app: ReturnType, request: Request) => - app.handler(request, emptyRequestContext); + 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({ @@ -273,641 +273,720 @@ const commandRequest = ( }); }; -void test('every registered command is mounted and rejects missing structural input or authentication before the lifecycle', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness(); - const app = mounted(harness, assertion.environment); - try { - 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', - }, +it.live( + 'every registered command is mounted and rejects missing structural input or authentication before the lifecycle', + () => + Effect.gen(function* testProgram2() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness(); + const app = yield* Effect.acquireRelease( + Effect.sync(() => mounted(harness, assertion.environment)), + (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), + ); + + 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* testProgram3() { + 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, + 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', + }), + ); + 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); + }), +); + +it.live( + 'missing, malformed, and wrong-audience assertions are challenged without creating invocations', + () => + forEachSequential(['party-registry', 'contacts'], (audience) => + Effect.gen(function* testProgram5() { + const assertion = yield* makeAssertion(audience); + const harness = yield* makeActionTestHarness(); + const app = yield* Effect.acquireRelease( + Effect.sync(() => mounted(harness, assertion.environment)), + (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), ); - 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.equal( - Predicate.isTagged( - body, - response.status === 400 - ? 'PartyCommandInvalidRequestProblem' - : 'PartyCommandAuthenticationProblem', - ), - true, - ); - 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.equal(Predicate.isTagged(malformedBody, 'PartyCommandInvalidRequestProblem'), true); - assert.equal(harness.snapshot().invocations.length, 0); - } finally { - await app.dispose(); - } -}); -void test('missing, malformed, and wrong-audience assertions are challenged without creating invocations', async () => { - await forEachSequential(['party-registry', 'contacts'], async (audience) => { - const assertion = await makeAssertion(audience); - const harness = makeActionTestHarness(); - const app = mounted(harness, assertion.environment); - try { - const tokens = audience === 'contacts' ? [assertion.token] : [undefined, 'not-a-jwt']; - await forEachSequential(tokens, async (token) => { - const response = await handle( - app, - commandRequest('request-search-rebuild', {}, token, { - 'idempotency-key': 'authentication-test', + const tokens = audience === 'contacts' ? [assertion.token] : [undefined, 'not-a-jwt']; + yield* forEachSequential(tokens, (token) => + Effect.gen(function* testProgram6() { + const response = yield* handle( + app, + commandRequest('request-search-rebuild', {}, token, { + 'idempotency-key': 'authentication-test', + }), + ); + 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); }), ); - 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.equal(Predicate.isTagged(body, 'PartyCommandAuthenticationProblem'), true); - assert.equal(body.status, 401); - assert.equal(JSON.stringify(body).includes(assertion.token), false); - }); - assert.equal(harness.snapshot().invocations.length, 0); - } finally { - await app.dispose(); - } - }); -}); - -void test('verification configuration unavailability is retryable and never reaches the lifecycle', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness(); - const app = mounted(harness, {}); - try { - const response = await handle( - app, - commandRequest('request-search-rebuild', {}, assertion.token, { - 'idempotency-key': 'configuration-test', + expect(harness.snapshot().invocations.length).toBe(0); }), + ), +); + +it.live( + 'verification configuration unavailability is retryable and never reaches the lifecycle', + () => + Effect.gen(function* testProgram7() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness(); + const app = yield* Effect.acquireRelease( + Effect.sync(() => mounted(harness, {})), + (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), + ); + + 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); + 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); + }), +); + +it.live('correlation and idempotency are mandatory before the Core Action lifecycle', () => + Effect.gen(function* testProgram8() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness(); + const app = yield* Effect.acquireRelease( + Effect.sync(() => mounted(harness, assertion.environment)), + (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), ); - assert.equal(response.status, 503); - assert.equal(response.headers.get('www-authenticate'), null); - const body = await response.json(); - assert.equal(Predicate.isTagged(body, 'PartyCommandUnavailableProblem'), true); - assert.equal(body.retryable, true); - assert.equal(harness.snapshot().invocations.length, 0); - } finally { - await app.dispose(); - } -}); -void test('correlation and idempotency are mandatory before the Core Action lifecycle', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness(); - const app = mounted(harness, assertion.environment); - try { - const missingKey = await handle( + const missingKey = yield* handle( app, commandRequest('request-search-rebuild', {}, assertion.token), ); - assert.equal(missingKey.status, 428); - const missingKeyBody = await missingKey.json(); - assert.equal( - Predicate.isTagged(missingKeyBody, 'PartyCommandPreconditionRequiredProblem'), + expect(missingKey.status).toBe(428); + const missingKeyBody = yield* Effect.promise(() => missingKey.json()); + expect(Predicate.isTagged(missingKeyBody, 'PartyCommandPreconditionRequiredProblem')).toBe( true, ); - const missingCorrelation = await handle( + const missingCorrelation = yield* handle( app, commandRequest('request-search-rebuild', {}, assertion.token, { 'idempotency-key': 'correlation-test', 'x-correlation-id': '', }), ); - assert.equal(missingCorrelation.status, 400); - const missingCorrelationBody = await missingCorrelation.json(); - assert.equal( - Predicate.isTagged(missingCorrelationBody, 'PartyCommandInvalidRequestProblem'), + expect(missingCorrelation.status).toBe(400); + const missingCorrelationBody = yield* Effect.promise(() => missingCorrelation.json()); + expect(Predicate.isTagged(missingCorrelationBody, 'PartyCommandInvalidRequestProblem')).toBe( true, ); - assert.equal(harness.snapshot().invocations.length, 0); - } finally { - await app.dispose(); - } -}); + expect(harness.snapshot().invocations.length).toBe(0); + }), +); -void 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', - }); - const app = mounted(harness, assertion.environment); - try { - const response = await handle( +it.live('real Core permission denial is a durable 403 and does not execute the command', () => + Effect.gen(function* testProgram9() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness({ + actionPermission: 'denied', + tenantPermission: 'allowed', + }); + const app = yield* Effect.acquireRelease( + Effect.sync(() => mounted(harness, assertion.environment)), + (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), + ); + + 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.equal(Predicate.isTagged(body, 'PartyCommandForbiddenProblem'), true); - assert.equal(harness.snapshot().invocations.length, 1); - assert.equal(harness.snapshot().permissionDenials.length, 1); - } finally { - await app.dispose(); - } -}); + 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); + }), +); -void 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 }), - }), - ], - }); - const app = mounted(harness, assertion.environment); - try { - const response = await handle( - app, - commandRequest('archive-party', archivePayload, assertion.token, { - 'idempotency-key': 'conflict-test', - }), +it.live( + 'the real handler translates domain conflicts and rolls back without successful evidence', + () => + Effect.gen(function* testProgram10() { + 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* Effect.acquireRelease( + Effect.sync(() => mounted(harness, assertion.environment)), + (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), + ); + + 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* testProgram11() { + 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* Effect.acquireRelease( + Effect.sync(() => mounted(harness, assertion.environment)), + (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), ); - assert.equal(response.status, 409); - const body = await response.json(); - assert.equal(Predicate.isTagged(body, 'PartyCommandConflictProblem'), true); - assert.equal(body.code, 'party_lifecycle_conflict'); - assert.equal(harness.snapshot().invocations.length, 1); - assert.equal(harness.snapshot().committed.length, 0); - } finally { - await app.dispose(); - } -}); -void 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', - }), - ), - }), - ], - }); - const app = mounted(harness, assertion.environment); - try { - const response = await handle( + 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.equal(Predicate.isTagged(body, 'PartyCommandAliasWriteRejectedProblem'), true); - 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); - } finally { - await app.dispose(); - } -}); + 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); + }), +); -void 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', - }); - const app = mounted(harness, assertion.environment); - try { - const first = await handle( +it.live('committed request replay stays a terminal 409 and does not execute or emit twice', () => + Effect.gen(function* testProgram12() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'allowed', + }); + const app = yield* Effect.acquireRelease( + Effect.sync(() => mounted(harness, assertion.environment)), + (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), + ); + + 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.equal(Predicate.isTagged(body, 'PartyCommandAlreadyCommittedProblem'), true); - 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); - } finally { - await app.dispose(); - } -}); + 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); + }), +); -void 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* testProgram13() { + 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* testProgram14() { + const harness = yield* makeActionTestHarness({ + actionPermission: 'allowed', + tenantPermission: 'allowed', + services: [item.service], + }); + const app = yield* Effect.acquireRelease( + Effect.sync(() => mounted(harness, assertion.environment)), + (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), + ); + + 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* testProgram15() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness({ actionPermission: 'allowed', tenantPermission: 'allowed', - services: [item.service], - }); - const app = mounted(harness, assertion.environment); - try { - 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.equal(Predicate.isTagged(body, item.tag), true); - 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); - } finally { - await app.dispose(); - } - }); -}); + ], + }); + const app = yield* Effect.acquireRelease( + Effect.sync(() => mounted(harness, assertion.environment)), + (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), + ); -void 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', - }), - ), - }), - ], - }); - const app = mounted(harness, assertion.environment); - try { - 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); - } finally { - await app.dispose(); - } -}); + 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); + }), +); -void 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* testProgram16() { + 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, + }, + }; + }), }), - }), - ], - }); - const app = mounted(harness, assertion.environment); - try { - 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); - } finally { - await app.dispose(); - } -}); + ], + }); + const app = yield* Effect.acquireRelease( + Effect.sync(() => mounted(harness, assertion.environment)), + (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), + ); -void test('commit resolution requires authentication and a valid invocation without creating an Action', async () => { - const assertion = await makeAssertion(); - const harness = makeActionTestHarness(); - const app = mounted(harness, assertion.environment); - try { - 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.equal(Predicate.isTagged(malformedBody, 'PartyCommandInvalidRequestProblem'), true); - const absent = await handle(app, recoveryRequest(randomUUID(), assertion.token)); - assert.equal(absent.status, 404); - assert.equal(harness.snapshot().invocations.length, 0); - } finally { - await app.dispose(); - } -}); + 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); + }), +); -void 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', - }); - const app = mounted(harness, assertion.environment); - try { - const failed = await handle( +it.live( + 'commit resolution requires authentication and a valid invocation without creating an Action', + () => + Effect.gen(function* testProgram17() { + const assertion = yield* makeAssertion(); + const harness = yield* makeActionTestHarness(); + const app = yield* Effect.acquireRelease( + Effect.sync(() => mounted(harness, assertion.environment)), + (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), + ); + + 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* testProgram18() { + 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* Effect.acquireRelease( + Effect.sync(() => mounted(harness, assertion.environment)), + (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), + ); + + 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(), { + 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); + expect(yield* Effect.promise(() => resolution.json())).toEqual({ _tag: 'PartyCommandCommitResolution', invocationId, retryCommand: false, state: 'OPEN', }); - assert.equal(harness.snapshot().invocations.length, 1); - assert.equal(harness.snapshot().committed.length, 0); - } finally { - await app.dispose(); - } -}); + expect(harness.snapshot().invocations.length).toBe(1); + expect(harness.snapshot().committed.length).toBe(0); + }), +); -void 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* testProgram19() { + 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', - }), - ), - ); - }), - }; - const app = mounted(harness, assertion.environment, reads); - try { - 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.equal(Predicate.isTagged(body, 'PartyCommandCommitIndeterminateProblem'), true); - 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.equal(Predicate.isTagged(replayBody, 'PartyCommandAlreadyCommittedProblem'), true); - 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); - } finally { - await app.dispose(); - } -}); + 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* Effect.acquireRelease( + Effect.sync(() => mounted(harness, assertion.environment, reads)), + (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), + ); + + 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); + expect(yield* Effect.promise(() => resolution.json())).toEqual({ + _tag: 'PartyCommandCommitResolution', + 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 23eabce14..2eacbc870 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 '@app/effect-rstest'; import { readFile } from 'node:fs/promises'; -import test from 'node:test'; -import { Schema } from 'effect'; +import { Effect, Schema } from 'effect'; import { partyRegistryApi, @@ -60,9 +59,9 @@ const serverFiles = [ 'person-engagement-profile-read-server', ] as const; -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', @@ -72,21 +71,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', @@ -98,72 +93,84 @@ test('keeps readiness tied to the immutable build marker', () => { status: 'ready', versionSkew: 'none', }), - true, - ); + ).toBe(true); }); -test('composes generated governed servers through the Core read runtime', async () => { - const serverSources = await Promise.all([ - readFile(new URL('../../api/index.ts', import.meta.url), 'utf-8'), - readFile(new URL('../../api/engagement-profile-server.ts', import.meta.url), 'utf-8'), - ]); - const source = serverSources.join('\n'); +it.effect('composes generated governed servers through the Core read runtime', () => + Effect.gen(function* testProgram1() { + const serverSources = yield* Effect.promise(() => + Promise.all([ + readFile(new URL('../../api/index.ts', import.meta.url), 'utf-8'), + readFile(new URL('../../api/engagement-profile-server.ts', import.meta.url), 'utf-8'), + ]), + ); + const source = serverSources.join('\n'); - for (const serverFile of serverFiles) { - assert.match(source, new RegExp(serverFile.replaceAll('-', '[-]'), 'u')); - } - assert.match(source, /ReadRuntimeLive/u); - assert.match(source, /ContextAccessLive/u); - assert.match(source, /Layer\.provide\(CorePersistenceLive\)/u); - assert.doesNotMatch(source, /partyRegistryItems|Wire a real|generated-party-registry/u); - assert.doesNotMatch(source, /\.handle\(['"]create['"]/u); - assert.match(source, /ActionRuntimeLive/u); - assert.match(source, /partyRegistryCommandsLive/u); -}); + for (const serverFile of serverFiles) { + expect(source).toMatch(new RegExp(serverFile.replaceAll('-', '[-]'), 'u')); + } + expect(source).toMatch(/ReadRuntimeLive/u); + expect(source).toMatch(/ContextAccessLive/u); + expect(source).toMatch(/Layer\.provide\(CorePersistenceLive\)/u); + expect(source).not.toMatch(/partyRegistryItems|Wire a real|generated-party-registry/u); + expect(source).not.toMatch(/\.handle\(['"]create['"]/u); + expect(source).toMatch(/ActionRuntimeLive/u); + expect(source).toMatch(/partyRegistryCommandsLive/u); + }), +); -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..46d89b75a 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 '@app/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 8ec0170d8..7c762b90b 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,43 +1,49 @@ -// @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 '@app/effect-rstest'; import { HttpApi, HttpApiBuilder, HttpRouter, HttpServer } from '@modern-js/plugin-bff/effect-edge'; -import { Context, Layer, Schema } from 'effect'; +import { Effect, Context, Layer, Schema } from 'effect'; import { partyRegistryFoundationLive } from '../../api/index.ts'; import { partyRegistryApi, partyRegistryReadinessSchema } from '../../shared/api.ts'; import { ultramodernApiMarker } from '../../shared/ultramodern-build.ts'; -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(), - ); - assert.equal(response.status, 200); - const readiness = Schema.decodeUnknownSync(partyRegistryReadinessSchema)(await response.json()); - assert.deepEqual(readiness.marker, ultramodernApiMarker); - assert.equal(readiness.status, 'ready'); +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 = 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', - }), - Context.empty(), - ); - assert.equal(removedWrite.status, 404); - } finally { - await server.dispose(); - } -}); + 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); + }), +); 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..f444aed9f 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 '@app/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..53a65e6b5 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 '@app/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 3e61cae9c..9524b9c62 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,8 +1,7 @@ -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 { expect, it } from '@app/effect-rstest'; import { readFile, readdir } from 'node:fs/promises'; -import test from 'node:test'; + import { Effect, Schema, Predicate } from 'effect'; import { getReadHandler } from '../../../../packages/core-runtime/src/reads/definition.ts'; import { @@ -59,149 +58,170 @@ const scope = Object.freeze({ }); const request = Schema.decodeUnknownSync(AresLookupRequestSchema)({ ico: '48039101' }); -void 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'); }); -void 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'), - }), - ), - ); + }).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 }); -}); + expect(calls).toEqual([{ correlationId: scope.correlationId, ico: '48039101' }]); + expect(result).toEqual({ evidence: { resultCount: 1 }, result: evidence }); + }), +); -void 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; - - 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.ok(Predicate.isTagged(error, 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); + } + }), +); -void 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], - [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.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], + [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); + } + expect(yield* Schema.decodeUnknownEffect(AresLookupRequestSchema)({ ico: '48039101' })).toEqual( + { + ico: '48039101', + }, + ); + expect(yield* Schema.decodeUnknownEffect(AresLookupResponseSchema)(evidenceWire)).toEqual( + evidence, + ); + expect(AresLookupApi.identifier).toBe('AresLookupApi'); + }), +); -void 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 bf1bd0c53..80a4b1de3 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, Predicate } from 'effect'; +// @effect-diagnostics strictEffectProvide:off -- Tests intentionally provide isolated adapter and logger layers. expires: 2026-12-31. +import { expect, it } from '@app/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[]) => }), ]); -void 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())); + }); -void 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.ok(Predicate.isTagged(error, '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); -void 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); + }), +); -void 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.ok(Predicate.isTagged(error, 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' }, + ); }), - ); -}); +); -void 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.ok(Predicate.isTagged(error, '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); + }), +); -void 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.ok(Predicate.isTagged(error, '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); + }), +); -void 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.ok(Predicate.isTagged(error, '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); + }), +); -void 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.ok(Predicate.isTagged(error, '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' }, + ); }), - ); -}); +); -void 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); + }), +); -void 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/audit-evidence-contract.test.ts b/app/verticals/party-registry/tests/unit/audit-evidence-contract.test.ts index 81261e8bf..ea29775e9 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 '@app/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 724d03a3c..753f6ee0b 100644 --- a/app/verticals/party-registry/tests/unit/catalog-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/catalog-contract.test.ts @@ -1,16 +1,15 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/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'], }); 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..32280ed3f 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 '@app/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..122925cd2 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 '@app/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 9a3a84419..cde2c523c 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 '@app/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 { is, SQL } from 'drizzle-orm'; import { PgDialect } from 'drizzle-orm/pg-core'; import { DateTime, Effect, Option, Schema, Predicate } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; 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 }, }); - -void 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', - ]); - }), - )); - -void 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')), ); - }), - )); - -void 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 @@ void test('reuses only an exact end request and rejects changed evidence at the 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 @@ void test('reuses only an exact end request and rejects changed evidence at the wholeEndCommand(effectiveEnd, 'A different reason'), ), ); - assert.ok(Predicate.isTagged(changed, 'Failure')); - assert.equal(changedHarness.updateSets.length, 0); + expect(Predicate.isTagged(changed, 'Failure')).toBe(true); + expect(changedHarness.updateSets.length).toBe(0); }), - )); - -void 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, }, }); - -void 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); - } - }), - )); - -void 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.ok(Predicate.isTagged(error, '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.ok(Predicate.isTagged(addError, 'PartyContactPointAlreadyExists')); - assert.equal(addHarness.insertValues.length, 0); - assert.equal(addHarness.updateSets.length, 0); - }), - )); - -void 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(is(revision, 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); - }), - )); - -void 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', - ]); - }), - )); - -void 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 @@ void test('rejects invalid E.164 and oversized extensions through the service ty verification: { state: 'UNVERIFIED' }, }), ); - assert.ok(Predicate.isTagged(error, '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), - )); - -void 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 @@ void test('rejects an explicit alias Party add but keeps durable ContactPoint up 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 @@ void test('rejects an explicit alias Party add but keeps durable ContactPoint up }), 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 @@ void test('rejects an explicit alias Party add but keeps durable ContactPoint up 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 @@ void test('rejects an explicit alias Party add but keeps durable ContactPoint up 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); }), - )); - -void 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(is(revision, SQL)); - assert.match(new PgDialect().sqlToQuery(revision).sql, /revision.*\+ 1/u); - assert.equal(harness.updateSets[1]?.['revision'], 2); - }), - )); - -void 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 @@ void test('persists bounded ARES provenance on the address and purpose without u 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); } }), - )); - -void 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 @@ void test('treats PHONE extensions as distinct endpoints while rejecting an exac }); 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.ok(Predicate.isTagged(duplicate, 'PartyContactPointAlreadyExists')); - assert.equal(duplicateHarness.insertValues.length, 0); + expect(Predicate.isTagged(duplicate, 'PartyContactPointAlreadyExists')).toBe(true); + expect(duplicateHarness.insertValues.length).toBe(0); }), - )); - -void 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 @@ void test('whole ADDRESS end preserves an earlier purpose end and its independen 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 a20611dca..af2a62dc0 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 '@app/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, Predicate } 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, ); - -void 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 @@ void test('correction is closed to Party type, display name, and official identi 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 @@ void test('correction is closed to Party type, display name, and official identi replacementValue: 'x', targetAssertionId: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb', }), - ); + ).toThrow(); }); - -void 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 @@ void test('correction follow-up is typed and duplicate confirmation remains read ], 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', ); }); - -void 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 @@ void test('relationship correction is closed, revisioned, interval checked, and '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,340 +218,363 @@ const transactionHarness = ( } as unknown as Parameters[0]; return { insertValues, transaction, updateSets }; }; - -void 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, - }), - ); - 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); -}); - -void 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)); -}); - -void 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, { - 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]], ); - assert.ok(Predicate.isTagged(error, 'PartyCorrectionConflict')); - assert.equal(h.updateSets.length, 0); - assert.equal(h.insertValues.length, 0); - }), - ); -}); - -void test('UNRESOLVED Party Type enrichment is rejected before mutation by correction', async () => { - const h = transactionHarness([ - [], - [{ partyId }], - [], - [{ partyId }], - [ - { - assertionId, - factKind: 'PARTY_TYPE', - isCurrent: true, - normalizedValue: 'UNRESOLVED', - partyId, - state: 'ACTIVE', - }, - ], - ]); - const command = decode(PartyCorrectionCommandSchema)({ - ...evidence, - factKind: 'PARTY_TYPE', - partyId, - replacementValue: 'PERSON', - subjectEvidence: [ - { - basis: 'REVIEWED_DOCUMENT', - evidenceRef: 'record/42', - kind: 'ACTOR_ATTESTATION', - observedSubject: 'PERSON', - statement: 'Reviewed this external organization', - subjectKey: 'one-subject', - }, - ], - targetAssertionId: assertionId, - }); - const error = await runEffectTestPromise( - Effect.flip( - correctPartyFactRecord(h.transaction, tenantId, command, { actionInvocationId, principalId }), - ), - ); - assert.ok(Predicate.isTagged(error, 'PartyCorrectionConflict')); - assert.match(error.reason, /enrichment/u); - assert.equal(h.updateSets.length, 0); -}); - -void test('detail exposes immutable original/result semantics, governance, and source distinct from actor', async () => { - const h = transactionHarness([ - [ - { - actingPrincipalId: principalId, + const result = yield* correctPartyFactRecord(h.transaction, tenantId, relationshipCommand, { 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); -}); - -void 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, { + 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); + }), +); +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 }, ), - ); - assert.ok(Predicate.isTagged(error, '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); -}); - -void 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 }], +); +it.effect('UNRESOLVED Party Type enrichment is rejected before mutation by correction', () => + Effect.gen(function* correctionScenario5() { + const h = transactionHarness([ [], - [{ partyId: canonicalId }], + [{ partyId }], [], - [{ partyId: organizationId }], - ], - [[relationshipRow({ relationshipId: replacementId })], [{ correctionId }]], - [[original]], - ); - await runEffectTestPromise( - correctPartyFactRecord(h.transaction, tenantId, relationshipCommand, { + [{ partyId }], + [ + { + assertionId, + factKind: 'PARTY_TYPE', + isCurrent: true, + normalizedValue: 'UNRESOLVED', + partyId, + state: 'ACTIVE', + }, + ], + ]); + const command = decode(PartyCorrectionCommandSchema)({ + ...evidence, + factKind: 'PARTY_TYPE', + partyId, + replacementValue: 'PERSON', + subjectEvidence: [ + { + basis: 'REVIEWED_DOCUMENT', + evidenceRef: 'record/42', + kind: 'ACTOR_ATTESTATION', + observedSubject: 'PERSON', + statement: 'Reviewed this external organization', + subjectKey: 'one-subject', + }, + ], + targetAssertionId: assertionId, + }); + const error = yield* Effect.flip( + correctPartyFactRecord(h.transaction, tenantId, command, { actionInvocationId, principalId }), + ); + expect(Predicate.isTagged(error, 'PartyCorrectionConflict')).toBe(true); + expect(error.reason).toMatch(/enrichment/u); + expect(h.updateSets.length).toBe(0); + }), +); +it.effect( + 'detail exposes immutable original/result semantics, governance, and source distinct from actor', + () => + Effect.gen(function* correctionScenario6() { + const h = transactionHarness([ + [ + { + actingPrincipalId: principalId, + actionInvocationId, + 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, + ); + expect(detail.governance.legalHolds).toBe('HONOR_GOVERNED_LEGAL_HOLDS'); + expect(detail.governance.policyVersion).toBe(detail.policyVersion); + }), +); +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); -}); - -void 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' }); }); - -void test('Party Type correction reconciles newly eligible claims before superseding the original fact', async () => { - const h = transactionHarness([ - [], - [{ partyId }], - [], - [{ partyId }], - [ - { - assertionId, +it.effect( + 'Party Type correction reconciles newly eligible claims before superseding the original fact', + () => + Effect.gen(function* correctionScenario9() { + const h = transactionHarness([ + [], + [{ partyId }], + [], + [{ partyId }], + [ + { + assertionId, + factKind: 'PARTY_TYPE', + isCurrent: true, + normalizedValue: 'PERSON', + partyId, + state: 'ACTIVE', + }, + ], + [], + [ + { + identifierTypeKey: 'ICO', + namespace: 'CZ:ICO', + normalizedValue: '27074358', + officialIdentifierId: replacementId, + verificationState: 'VERIFIED', + }, + ], + [], + [{ partyId: organizationId }], + ]); + const command = decode(PartyCorrectionCommandSchema)({ + ...evidence, factKind: 'PARTY_TYPE', - isCurrent: true, - normalizedValue: 'PERSON', partyId, - state: 'ACTIVE', - }, - ], - [], - [ - { - identifierTypeKey: 'ICO', - namespace: 'CZ:ICO', - normalizedValue: '27074358', - officialIdentifierId: replacementId, - verificationState: 'VERIFIED', - }, - ], - [], - [{ partyId: organizationId }], - ]); - const command = decode(PartyCorrectionCommandSchema)({ - ...evidence, - factKind: 'PARTY_TYPE', - partyId, - replacementValue: 'ORGANIZATION', - subjectEvidence: [ - { - basis: 'REVIEWED_DOCUMENT', - evidenceRef: 'record/42', - kind: 'ACTOR_ATTESTATION', - observedSubject: 'ORGANIZATION', - statement: 'Reviewed this external organization', - subjectKey: 'one-subject', - }, - ], - targetAssertionId: assertionId, - }); - const error = await runEffectTestPromise( - Effect.flip( - correctPartyFactRecord(h.transaction, tenantId, command, { actionInvocationId, principalId }), - ), - ); - assert.ok(Predicate.isTagged(error, 'PartyCorrectionConflict')); - assert.match(error.reason, /exclusive identifier claims/u); - assert.equal(h.updateSets.length, 0); - assert.equal(h.insertValues.length, 0); -}); - -void test('type Correction cannot treat a reviewer decision or source label as subject evidence', async () => { - const h = transactionHarness([ - [], - [{ partyId }], - [], - [{ partyId }], - [ - { - assertionId, + replacementValue: 'ORGANIZATION', + subjectEvidence: [ + { + basis: 'REVIEWED_DOCUMENT', + evidenceRef: 'record/42', + kind: 'ACTOR_ATTESTATION', + observedSubject: 'ORGANIZATION', + statement: 'Reviewed this external organization', + subjectKey: 'one-subject', + }, + ], + targetAssertionId: assertionId, + }); + const error = yield* Effect.flip( + correctPartyFactRecord(h.transaction, tenantId, command, { + actionInvocationId, + principalId, + }), + ); + expect(Predicate.isTagged(error, 'PartyCorrectionConflict')).toBe(true); + expect(error.reason).toMatch(/exclusive identifier claims/u); + expect(h.updateSets.length).toBe(0); + expect(h.insertValues.length).toBe(0); + }), +); +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.ok(Predicate.isTagged(error, '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..9afaa6864 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 '@app/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..045ac5665 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 '@app/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 190023ac6..764fce4d3 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 { TestClock } from 'effect/testing'; +import { expect, it } from '@app/effect-rstest'; import { DateTime, Effect, Predicate } 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, }); -void 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.ok(Predicate.isTagged(result, '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]]); -void 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); - }); -}); + ); -void 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, { + ); + + expect(result).toEqual({ _tag: 'evidence_insufficient', method: 'ENGAGEMENT_INACTIVITY', roleType: 'CUSTOMER', }); - assert.equal(harness.updateSets.length, 0); - }); -}); + expect(harness.updateSets.length).toBe(0); + }), +); -void 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.ok(Predicate.isTagged(result, '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]]); -void 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.ok(Predicate.isTagged(result, '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', }); - }); -}); + }), +); -void 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.ok(Predicate.isTagged(result, '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', + ]); + }), +); -void 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 @@ void test('rejects an alias Party create target with canonical survivor guidance source: 'contracts.core', }, tenantId, - }), - ).then((result) => { - assert.ok(Predicate.isTagged(result, 'party_alias')); - assert.equal(result.canonicalPartyRef.resourceId, survivorId); - assert.equal(harness.insertValues.length, 0); - }); -}); + }); -void 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.ok(Predicate.isTagged(result, '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); + }), +); -void 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 @@ void test('creates the tenant-admin snapshot atomically without creating an impl source: 'contracts.core', }, tenantId, - }), - ).then((result) => { - assert.ok(Predicate.isTagged(result, 'found')); - assert.deepEqual(harness.insertedTables, ['counterparties', 'counterparty_admin_read_models']); - assert.equal(harness.insertValues[1]?.['storedPartyId'], partyId); - }); -}); + }); -void 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 @@ void test('adds a future role and its admin history projection in the same trans tenantId, validFrom: futureStart, validTo: null, - }), - ).then((result) => { - assert.ok(Predicate.isTagged(result, '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-role-lifecycle.test.ts b/app/verticals/party-registry/tests/unit/counterparty-role-lifecycle.test.ts index 10ca16527..03339cb25 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 '@app/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 df0c2ffa8..266753156 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 { expect, it } from '@app/effect-rstest'; import { Effect, Predicate } from 'effect'; import { acquirePoolResource, makePartyDatabase } from '../../src/db/client.ts'; -void 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); + }), +); -void 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 @@ void test('keeps Party Registry pool acquisition failure in the typed error chan }, ), ), - ), - ); - assert.ok(Predicate.isTagged(error, '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 a084e9881..2cd912df6 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,17 +1,16 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/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: [], }); 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 8dbeeb07c..9196f60ee 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,8 +1,6 @@ -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 '@app/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 { FetchHttpClient } from 'effect/unstable/http'; import { @@ -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 preserve owner request context at the HTTP boundary', 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(() => + Schema.decodeUnknownSync(schema, { onExcessProperty: 'error' })({ + ...payload, + customerId: 'd4000000-0000-4000-8000-000000000001', + }), + ).toThrow(); } - 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, - }); - }; + }), +); - await runEffectTestPromise( - attachOrganizationEngagement( +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, + }), + ); + }; + + yield* attachOrganizationEngagement( { counterpartyRef, partyRef }, { baseUrl: 'https://party.example/party-registry-api', @@ -113,40 +113,44 @@ test('public engagement mutations preserve owner request context at the HTTP bou 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/')); - assert.ok(mutationRequest); - 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, - ); -}); + const mutationRequest = requests.find(({ url }) => url.includes('/contacts/engagement/')); + expect(mutationRequest).toBeDefined(); + 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, + ); + expect( + yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))( + mutationRequest?.headers.get('x-modernjs-bff-operation-context') ?? '', + ), + ).toEqual(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'), - ]); +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]) { - assert.doesNotMatch( - source, - /\b(?:createCustomer|editCustomer|archiveCustomer|unarchiveCustomer)\b/u, - ); - assert.doesNotMatch( - source, - /\b(?:createContact|editContact|archiveContact|unarchiveContact)\b/u, - ); - assert.doesNotMatch(source, /CustomerAresLookup|customerId|contactId/u); - } -}); + 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 28160e670..f37a5225c 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,9 +1,6 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -// @effect-diagnostics asyncFunction:off -- Existing compatibility boundary; expires: 2026-12-31. +import { expect, it } from '@app/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, Predicate } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; import type { OrganizationEngagementProfileRecord } from '../../src/db/engagement-schema.ts'; import { createOrganizationEngagementProfile, @@ -45,35 +42,34 @@ const rejectingMutationTransaction = (failure: Failure) => }), }) as unknown as Parameters[0]; -void 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); }); -void 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.ok(Predicate.isTagged(failure, 'EngagementProfileConflict')); - assert.equal(failure.code, 'contacts_party_counterparty_mismatch'); -}); + ); + expect(Predicate.isTagged(failure, 'EngagementProfileConflict')).toBe(true); + expect(failure.code).toBe('contacts_party_counterparty_mismatch'); + }), +); -void 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: { @@ -85,20 +81,19 @@ void test('maps a wrapped owner uniqueness constraint to the declared engagement }), { ...refs, tenantId }, ), - ), - ); + ); - assert.ok(Predicate.isTagged(failure, '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', + ); + }), +); -void 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', @@ -109,16 +104,16 @@ void test('continues past an unrelated wrapper code to the owner uniqueness cons }), { ...refs, tenantId }, ), - ), - ); + ); - assert.ok(Predicate.isTagged(failure, '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'); + }), +); -void 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', @@ -126,13 +121,12 @@ void test('maps an unrelated uniqueness constraint to the existing persistence f }), { ...refs, tenantId }, ), - ), - ); + ); - assert.ok(Predicate.isTagged(failure, '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', + ); + }), +); 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..3e852626d 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 '@app/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..2c1dcd7f9 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 '@app/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/identifier-contract.test.ts b/app/verticals/party-registry/tests/unit/identifier-contract.test.ts index 88ef9e0b7..1e784414a 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 '@app/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 8d4a4d79d..94974a8b0 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 { TestClock } from 'effect/testing'; +import { expect, it } from '@app/effect-rstest'; import { DateTime, Effect, Match, Schema, Predicate } from 'effect'; -// @effect-diagnostics asyncFunction:off globalDate:off -- Existing compatibility boundary; expires: 2026-12-31. /* 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 }; }; -void 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); + }), +); -void 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, + ); + }), + ), ); }), - ); -}); +); -void 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.ok(Predicate.isTagged(result, '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]); + }), +); -void 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.ok(Predicate.isTagged(result, '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; -void 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.ok(Predicate.isTagged(result, '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); + }), +); -void 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.ok(Predicate.isTagged(result, '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); + }), +); -void 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.ok(Predicate.isTagged(result, 'found')); - assert.equal(db.inserts.length, 0); -}); + ); + expect(Predicate.isTagged(result, 'found')).toBe(true); + expect(db.inserts.length).toBe(0); + }), +); -void 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.ok(Predicate.isTagged(result, '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); -}); +); -void 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.ok(Predicate.isTagged(result, '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..c3c2d13ee 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 '@app/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 674856377..4bb348b25 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,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 '@app/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'; @@ -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); }; -void 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()); + }), +); -void 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()); + }), +); -void 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 @@ void test('Unarchive Party records metadata-only invariant evidence with its eve ); assertInvariantEvidence(collector.snapshot()); }), - )); +); -void 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 @@ void test('Unarchive review outcome commits once and replays without an unarchiv }, reasonCode: 'EXACT_CLAIM_CONFLICT' as const, }; - const harness = makeActionTestHarness({ + const harness = yield* makeActionTestHarness({ actionPermission: 'allowed', services: [ bindActionTestServices(unarchivePartyAction, { @@ -153,15 +151,15 @@ void test('Unarchive review outcome commits once and replays without an unarchiv 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.ok(Predicate.isTagged(replay, '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..82281b376 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 '@app/effect-rstest'; +import { Effect, DateTime, Option, Schema } 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,111 @@ 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', - }); -}); + expect(yield* Schema.encodeEffect(PartyNotFound)(failure)).toEqual({ + _tag: 'PartyNotFound', + 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 +161,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..9c6c33b0f 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 '@app/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 378f732c9..321e5b57f 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,6 +1,4 @@ -import { runEffectTestSync } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/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'; @@ -63,77 +61,85 @@ const makeServices = ( }; }; -void 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); + }), +); -void 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'), + }); + }), +); -void 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.ok(Predicate.isTagged(error, '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([]); + } + }), +); -void 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.ok(Predicate.isTagged(error, '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 d11ba2deb..f20fd19dd 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 '@app/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,94 +34,102 @@ 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], ]); - assert.match(queries[0] ?? '', /"tenant_id" = \$1/u); - assert.match(queries[0] ?? '', /"party_id" = \$2/u); - assert.doesNotMatch(queries[0] ?? '', /provenance|principal|invocation|verification/u); - assert.doesNotMatch(queries[1] ?? '', /external_evidence/u); - assert.match(queries[1] ?? '', /"state" = \$3/u); - assert.match(queries[1] ?? '', /"is_current" = \$4/u); + expect(queries[0] ?? '').toMatch(/"tenant_id" = \$1/u); + expect(queries[0] ?? '').toMatch(/"party_id" = \$2/u); + expect(queries[0] ?? '').not.toMatch(/provenance|principal|invocation|verification/u); + expect(queries[1] ?? '').not.toMatch(/external_evidence/u); + expect(queries[1] ?? '').toMatch(/"state" = \$3/u); + expect(queries[1] ?? '').toMatch(/"is_current" = \$4/u); const detail = yield* readPartyDetailFromServices( partyRef, tenantId, @@ -153,10 +159,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 0bb8dfd71..6e0587a38 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 '@app/effect-rstest'; + import type { SQL } from 'drizzle-orm'; import { PgDialect } from 'drizzle-orm/pg-core'; -import { DateTime, Effect, Match, Option, Result, Schema, Predicate } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { DateTime, Effect, Layer, Match, Option, Result, Schema, Predicate } 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'; @@ -146,16 +147,20 @@ 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)]); }; -void 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' }); + }); -void 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, @@ -200,84 +205,79 @@ void 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); }), - )); - -void 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.ok(Predicate.isTagged(result, '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); + }), + ); -void 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, [ - { partyId: secondOwnerId }, - { partyId: firstOwnerId }, - { partyId: secondOwnerId }, - ]), - { + it('unarchive owner classification distinguishes conflict from ambiguity deterministically', () => { + expect(classifyUnarchiveClaimOwners(partyId, [{}, { partyId }])).toEqual({ + _tag: 'available', + }); + expect(classifyUnarchiveClaimOwners(partyId, [{ partyId: firstOwnerId }])).toEqual({ + _tag: 'identity_conflict', + conflictingPartyId: firstOwnerId, + }); + expect( + classifyUnarchiveClaimOwners(partyId, [ + { partyId: secondOwnerId }, + { partyId: firstOwnerId }, + { partyId: secondOwnerId }, + ]), + ).toEqual({ _tag: 'identity_ambiguous', candidatePartyIds: [firstOwnerId, secondOwnerId], - }, - ); -}); + }); + }); -void 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, { @@ -290,56 +290,17 @@ void test('future-effective identity updates do not replace current facts early' provenanceSource: 'test', validFrom: '2999-01-01T00:00:00.000Z', }); - assert.ok(Predicate.isTagged(result, 'conflict')); - assert.deepEqual(harness.insertedValues, []); - assert.deepEqual(harness.updateSets, []); + expect(Predicate.isTagged(result, 'conflict')).toBe(true); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets).toEqual([]); }), - )); - -void 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, []); - }), - )); - -void 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 }], @@ -347,125 +308,173 @@ void test('blocked unarchive persists a case and decision without mutating Party [identifierRow()], [{}], [{ partyId: firstOwnerId }], - [partyRow()], - [identifierRow()], + ]); + + const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); + + expect(result).toEqual({ + _tag: 'identity_conflict', + 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.ok(Predicate.isTagged(result, '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.ok(Predicate.isTagged(retry, '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); - }), - )); - -void 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.ok(Predicate.isTagged(result, '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'); + }), + ); -void 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( @@ -475,50 +484,49 @@ void test('archive acquires the tenant identity lock before any Party row lock', 4, 'ARCHIVED', ); - assert.ok(Predicate.isTagged(result, 'not_found')); + expect(Predicate.isTagged(result, 'not_found')).toBe(true); assertTenantLockIsFirst(harness); }), - )); - -void 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.ok(Predicate.isTagged(result, '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 }); + }), + ); -void 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()], @@ -530,14 +538,13 @@ void test('unarchive rejects an alias rather than forwarding the write to its su const error = yield* Effect.flip( unarchivePartyRecord(harness.transaction, tenantId, partyId, 4), ); - assert.ok(Predicate.isTagged(error, 'PartyAliasWriteRejected')); - assert.deepEqual(harness.insertedValues, []); - assert.deepEqual(harness.updateSets, []); + expect(Predicate.isTagged(error, 'PartyAliasWriteRejected')).toBe(true); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets).toEqual([]); }), - )); + ); -void 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()], @@ -559,17 +566,16 @@ void test('unarchive reports ambiguous exact claims without changing archived st ]); const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); - assert.deepEqual(result, { + expect(result).toEqual({ _tag: 'identity_ambiguous', candidatePartyIds: [firstOwnerId, secondOwnerId], }); - assert.deepEqual(harness.insertedValues, []); - assert.deepEqual(harness.updateSets, []); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets).toEqual([]); }), - )); + ); -void 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( @@ -578,79 +584,81 @@ void test('unarchive does not promote a PERSON ICO into an exclusive strong clai ); const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); - assert.ok(Predicate.isTagged(result, 'found')); - assert.deepEqual(harness.insertedValues, []); - assert.equal(harness.updateSets.length, 1); - }), - )); - -void 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, []); + expect(Predicate.isTagged(result, 'found')).toBe(true); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets.length).toBe(1); }), - )); - -void 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, []); - }), - )); + ); -void 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(result).toEqual({ + _tag: 'review_required', + 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.ok(Predicate.isTagged(result, 'found')); - assert.equal(harness.updateSets.length, 1); - assert.deepEqual(harness.insertedValues, []); - }), - )); + ]); + const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); + expect(result).toEqual({ + _tag: 'review_required', + caseIds: [], + reasonCode: 'UNRESOLVED_IDENTITY', + }); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets).toEqual([]); + }), + ); -void 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([ @@ -682,15 +690,14 @@ void test('Party type enrichment refuses another owner of a newly eligible ident ], validFrom: '2026-01-01T00:00:00.000Z', }); - assert.ok(Predicate.isTagged(result, 'conflict')); - assert.deepEqual(harness.insertedValues, []); - assert.deepEqual(harness.updateSets, []); - assert.deepEqual(harness.deletedTargets, []); + expect(Predicate.isTagged(result, 'conflict')).toBe(true); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets).toEqual([]); + expect(harness.deletedTargets).toEqual([]); }), - )); + ); -void 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( @@ -717,8 +724,8 @@ void test('Party type enrichment atomically claims identifiers that newly qualif ], validFrom: '2026-01-01T00:00:00.000Z', }); - assert.ok(Predicate.isTagged(result, 'found')); - assert.deepEqual(harness.insertedValues[0], [ + expect(Predicate.isTagged(result, 'found')).toBe(true); + expect(harness.insertedValues[0]).toEqual([ { identifierTypeKey: 'ICO', namespace: 'CZ:ICO', @@ -728,68 +735,69 @@ void test('Party type enrichment atomically claims identifiers that newly qualif tenantId, }, ]); - assert.equal(harness.updateSets.length, 2); + expect(harness.updateSets.length).toBe(2); }), - )); + ); -void 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(result).toEqual({ _tag: 'available', eligibleClaimCount: 0 }); + expect(harness.deletedTargets.length).toBe(1); + expect(harness.insertedValues).toEqual([]); + assertTenantLockIsFirst(harness); + }), + ); -void 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.ok(Predicate.isTagged(result, 'conflict')); - assert.deepEqual(harness.insertedValues, []); - assert.deepEqual(harness.updateSets, []); - assert.deepEqual(harness.deletedTargets, []); - }), - )); + 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); + expect(harness.insertedValues).toEqual([]); + expect(harness.updateSets).toEqual([]); + expect(harness.deletedTargets).toEqual([]); + }), + ); -void 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 }]]); @@ -805,8 +813,9 @@ void test('type enrichment rejects unevidenced type before accepting facts or cl validFrom: '2026-01-01T00:00:00.000Z', }), ); - assert.ok(Predicate.isTagged(error, '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..5b5fe4a71 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 '@app/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 5219cdeb3..c09982d58 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 '@app/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, Predicate } 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,767 +197,753 @@ const ambiguousHarness = (existingCase: boolean) => ]), ); -void 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); + }), + ); -void 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); + }), + ); + + 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, + }; + + 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' }, + }, + ], + ], + ], + ]), + ); + 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([]); + } + }), + ); -void 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'), + 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 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, + }), }, - ], - }), - 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, - ); - }), - )); + }, + ); + 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), + }); + }), + ); -const resolutionInput = { - actionInvocationId, - candidateCaseId, - expectedRevision: 1, - principalId, - reason: 'Reviewed authoritative evidence', - selectedPartyId: partyC, - selectedPartyTenantId: tenantId, - tenantId, -}; + 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 = 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), + }, + }, + ); + 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, + ); + }), + ); -void 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(); + 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)]]], [ - duplicateCandidateCases, + partyOfficialIdentifiers, [ [ { - ...original, - candidateSnapshot: { ...original.candidateSnapshot, intent: 'UNARCHIVE' }, + acceptedByActionInvocationId: 'prior-acceptance', + officialIdentifierId, + partyId: partyA, }, ], ], ], ]), ); - const error = - resolution === 'CREATE' - ? yield* Effect.flip( - resolveDuplicateCandidateCreate(subject.transaction, resolutionInput), - ) - : yield* Effect.flip( - resolveDuplicateCandidateMatch(subject.transaction, resolutionInput), - ); - assert.ok(Predicate.isTagged(error, '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({ + 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, + }), + }, + }, + ); + 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); + }), + ); -void 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 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, - }), + 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, + }, + ], + ], + ], + ]), + ); + 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(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), - }); - }), - )); + { + ...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); + }), + ); -void 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 = 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), + 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 = 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), }, - }, - ); - 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, - ); - }), - )); + { + ...collector, + actionInvocationId, + scope: actionScope, + services: { + resolve: () => resolveDuplicateCandidateMatch(subject.transaction, resolutionInput), + }, + }, + ); + expect(result.outcome).toBe('MATCH_EXISTING'); + expect(collector.snapshot().domainEvents).toEqual([]); + expect(collector.snapshot().outboxMessages).toEqual([]); + }), + ); -void 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( + '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, [[]]], [ + parties, [ - { - acceptedByActionInvocationId: 'prior-acceptance', - officialIdentifierId, - partyId: partyA, - }, + [{ partyId: partyC }], + [ + { + archivedAt: DateTime.toDateUtc(DateTime.makeUnsafe(instant)), + currentType: 'ORGANIZATION', + partyId: partyC, + }, + ], ], ], - ], - ]), - ); - 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' }, - ], + ]), + ); + 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([]); + }), + ); + + 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', }), - }, - { - ...collector, - actionInvocationId, - scope: actionScope, - services: { - createOrMatch: (value, invocationId) => - createOrMatchParty(subject.transaction, { - actionInvocationId: invocationId, - candidate: value, - principalId, - tenantId, - }), - }, - }, - ); - 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, - ); - }), - )); + ); + 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([]); + }), + ); -void 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( + 'reviewed matching rejects an absorbed target with the full-chain canonical survivor reference', + () => + Effect.gen(function* reviewedMatchingRejectsAnAbsorbedTargetWith() { + const subject = harness( + new Map([ + [duplicateCandidateCases, [[caseRow()]]], [ - [], + partyAliases, [ - { - acceptedByActionInvocationId: actionInvocationId, - officialIdentifierId, - partyId: partyA, - }, + [{ aliasPartyId: partyB, canonicalPartyId: partyA, tenantId }], + [{ aliasPartyId: partyA, canonicalPartyId: partyC, 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)( - { + [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([]); + }), + ); + + 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({ officialIdentifiers: [ - { identifierType: 'ICO', value: '27074358', verification: 'VERIFIED' }, - identifier, - identifier, + { 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'), + }, ], }), - }, - { - ...collector, - actionInvocationId, - scope: actionScope, - services: { - createOrMatch: (value, invocationId) => - createOrMatchParty(subject.transaction, { - actionInvocationId: invocationId, - candidate: value, - principalId, - tenantId, - }), - }, - }, - ); - assert.equal( - subject.inserts.filter(({ table }) => table === partyOfficialIdentifiers).length, - 1, - ); - assert.equal(collector.snapshot().domainEvents.length, 1); - assert.equal(collector.snapshot().outboxMessages.length, 1); - }), - )); + tenantId, + }); + 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); + }), + ); -void 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 = 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, + 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, - scope: actionScope, - services: { - resolve: () => resolveDuplicateCandidateMatch(subject.transaction, resolutionInput), - }, - }, - ); - assert.equal(result.outcome, 'MATCH_EXISTING'); - assert.deepEqual(collector.snapshot().domainEvents, []); - assert.deepEqual(collector.snapshot().outboxMessages, []); - }), - )); + 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); + }), + ); -void 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( + '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([ [ - [{ partyId: partyC }], + partyIdentifierClaims, [ - { - archivedAt: DateTime.toDateUtc(DateTime.makeUnsafe(instant)), - currentType: 'ORGANIZATION', - partyId: partyC, - }, + [{ officialIdentifierId: '70000000-0000-4000-8000-000000000001', partyId: partyA }], + [{ partyId: partyB }], ], ], - ], - ]), - ); - const failure = yield* Effect.flip( - resolveDuplicateCandidateMatch(subject.transaction, resolutionInput), - ); - assert.ok(Predicate.isTagged(failure, 'DuplicateCandidateConflict')); - assert.equal( - subject.reads.some(({ table, locked }) => table === parties && locked), - true, - ); - assert.deepEqual(subject.inserts, []); - assert.deepEqual(subject.updates, []); - }), - )); - -void 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.ok(Predicate.isTagged(failure, 'DuplicateCandidateConflict')); - assert.equal( - subject.reads.length, - 1, - 'only the trusted tenant serialization lock is acquired', - ); - assert.deepEqual(subject.inserts, []); - }), - )); - -void 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, + [partyAliases, [[], []]], + [parties, [[{ partyId: partyA }], [{ partyId: partyB }]]], [ - [{ aliasPartyId: partyB, canonicalPartyId: partyA, tenantId }], - [{ aliasPartyId: partyA, canonicalPartyId: partyC, tenantId }], - [], + duplicateCandidateCases, + [[], [{ ...caseRow(), candidateCaseId: priorId, lifecycleState: 'RESOLVED' }]], ], - ], - [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, []); - }), - )); - -void 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, { + ]), + ); + const result = yield* matchParty(subject.transaction, { actionInvocationId, - candidate: candidate({ validFrom: DateTime.makeUnsafe('2099-01-01T00:00:00.000Z') }), + candidate: candidate(), tenantId, - }), - ); - assert.ok(Predicate.isTagged(failure, 'PartyEvidenceInsufficient')); - assert.deepEqual(subject.inserts, []); - }), - )); + }); + 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([]); + }), + ); -void 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', + 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 }, + ), ); - assert.equal(matchPartyAction.descriptor.legalEntityScope, 'optional'); - assert.equal(partyMatchRead.descriptor.accessKind, 'detail'); -}); -void 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, - ); - }), - )); + 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)); + }); -void 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( + '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); + } + }), + ); -void 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( + 'reviewer selection cannot waive missing subject/type evidence from a retained case', + () => + Effect.gen(function* denyUnevidencedReview() { + const row = caseRow(); + const subject = harness( + new Map([ [ - [{ officialIdentifierId: '70000000-0000-4000-8000-000000000001', partyId: partyA }], - [{ partyId: partyB }], + duplicateCandidateCases, + [[{ ...row, candidateSnapshot: { ...row.candidateSnapshot, subjectEvidence: [] } }]], ], - ], - [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, []); - }), - )); - -void 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.ok(Predicate.isTagged(failure, 'PartyEvidenceInsufficient')); - assert.deepEqual(subject.inserts, []); - }), - { concurrency: 'unbounded', discard: true }, - ), - )); - -void test('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, - }; - assert.equal(candidateFingerprint(original), candidateFingerprint(reordered)); + ]), + ); + 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); + }), + ); }); - -void 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.ok(Predicate.isTagged(failure, 'PartyEvidenceInsufficient')); - assert.equal(subject.inserts.length, 0); - } - }), - )); - -void 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.ok(Predicate.isTagged(failure, 'DuplicateCandidateConflict')); - assert.equal(subject.inserts.length, 0); - assert.equal(subject.updates.length, 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 bfd2837e8..31e3b9b19 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, Predicate } from 'effect'; +import { expect, it } from '@app/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, }); -void 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'); -void 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.ok(Predicate.isTagged(cycleError, '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.ok(Predicate.isTagged(crossTenantError, '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.ok(Predicate.isTagged(brokenError, 'PartyAliasResolutionBrokenChain')); -}); +); -void 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.ok(Predicate.isTagged(rejection, '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 85f3f712b..45a53e831 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,5 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/effect-rstest'; + import { DateTime, Predicate } from 'effect'; import type { PartyRef } from '../../shared/resources/party.ts'; import { @@ -26,13 +26,13 @@ const alias = (aliasPartyId: string, survivorPartyId: string, tenant = tenantId) survivorPartyRef: party(survivorPartyId, tenant), }); -void 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, { + expect(result).toEqual({ _tag: 'CanonicalPartyResolved', canonicalPartyRef: party('party-c'), requestedAlias: party('party-b'), @@ -40,8 +40,8 @@ void test('resolves an historical alias chain to one final canonical Party', () }); }); -void test('rejects alias cycles, self aliases, and cross-tenant targets', () => { - assert.ok( +it('rejects alias cycles, self aliases, and cross-tenant targets', () => { + expect( Predicate.isTagged( resolveCanonicalPartyRef(party('party-a'), [ alias('party-a', 'party-b'), @@ -49,14 +49,14 @@ void test('rejects alias cycles, self aliases, and cross-tenant targets', () => ]), 'PartyAliasCycleRejected', ), - ); - assert.ok( + ).toBe(true); + expect( Predicate.isTagged( resolveCanonicalPartyRef(party('party-a'), [alias('party-a', 'party-a')]), 'PartyAliasSelfReferenceRejected', ), - ); - assert.ok( + ).toBe(true); + expect( Predicate.isTagged( resolveCanonicalPartyRef(party('party-a'), [ { @@ -66,17 +66,17 @@ void test('rejects alias cycles, self aliases, and cross-tenant targets', () => ]), 'PartyAliasCrossTenantRejected', ), - ); + ).toBe(true); }); -void test('rejects new writes addressed to an absorbed alias instead of forwarding them', () => { - assert.deepEqual(assertCanonicalWriteTarget(party('party-b'), [alias('party-b', 'party-a')]), { +it('rejects new writes addressed to an absorbed alias instead of forwarding them', () => { + expect(assertCanonicalWriteTarget(party('party-b'), [alias('party-b', 'party-a')])).toEqual({ _tag: 'AliasWriteRejected', aliasPartyRef: party('party-b'), canonicalPartyRef: party('party-a'), code: 'ALIAS_WRITE_FORBIDDEN', }); - assert.deepEqual(assertCanonicalWriteTarget(party('party-a'), [alias('party-b', 'party-a')]), { + expect(assertCanonicalWriteTarget(party('party-a'), [alias('party-b', 'party-a')])).toEqual({ _tag: 'CanonicalWriteTargetAccepted', 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..f52553c15 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,5 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/effect-rstest'; + import { DateTime, Match } from 'effect'; import type { PartyRef } from '../../shared/resources/party.ts'; import { analyzeMergeCollisions } from '../../src/merge/merge-collision-analysis.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,7 +295,7 @@ test('blocks readiness for unsupported references and incomplete retry contracts ], }); - assert.deepEqual(result, { + expect(result).toEqual({ _tag: 'ReferencePreservationBlocked', blockers: [ { code: 'UNSUPPORTED_REFERENCE_CLASS', ownerKey: 'custom-module' }, @@ -308,13 +304,13 @@ 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, { + expect(result).toEqual({ _tag: 'ReferencePreservationBlocked', 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..8a596b2c0 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 '@app/effect-rstest'; import { readFile } from 'node:fs/promises'; -import test from 'node:test'; -import { Match, Schema } from 'effect'; +import { Effect, Match, Schema } 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(rejection).toEqual({ + _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); + 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 4a8e6a001..e8ae735e2 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,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/effect-rstest'; import { Match, Predicate } from 'effect'; import type { PartyRef } from '../../shared/resources/party.ts'; import type { @@ -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, ); -void 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 @@ void test('blocks survivor selection when authoritative identity truth is unreso ]), ); - assert.deepEqual(result, { + expect(result).toEqual({ _tag: 'SurvivorSelectionBlocked', blocker: 'AUTHORITATIVE_IDENTITY_CONFLICT', conflictingPartyRefs: [party('party-b')], }); }); -void 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 @@ void test('uses the governed hierarchy before reference count, lifecycle, comple ]), ); - assert.ok(Predicate.isTagged(result, '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', + ]); }); -void 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'); }); -void 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,7 +125,7 @@ void test('rejects a cross-tenant merge set before selection', () => { ]), ); - assert.deepEqual(result, { + expect(result).toEqual({ _tag: 'SurvivorSelectionBlocked', blocker: 'CROSS_TENANT_MERGE_SET', conflictingPartyRefs: [ @@ -133,14 +135,14 @@ void test('rejects a cross-tenant merge set before selection', () => { }); }); -void 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 }), { + expect(selectCanonicalSurvivor({ candidates, confirmation: null })).toEqual({ _tag: 'SurvivorSelectionBlocked', blocker: 'DUPLICATE_SET_NOT_CONFIRMED', conflictingPartyRefs: [party('party-a'), party('party-b')], }); - assert.ok( + expect( Predicate.isTagged( selectCanonicalSurvivor({ candidates, @@ -153,17 +155,17 @@ void test('rejects selection without an explicit confirmed duplicate decision an }), 'SurvivorSelectionBlocked', ), - ); + ).toBe(true); }); -void 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.ok(Predicate.isTagged(result, 'CanonicalSurvivorSelected')); + expect(Predicate.isTagged(result, 'CanonicalSurvivorSelected')).toBe(true); const selected = expectSelected(result); const authority = selected.evidenceChain.find( ({ criterion }) => criterion === 'AUTHORITATIVE_EVIDENCE', @@ -171,45 +173,55 @@ void test('retains immutable evaluated values and explains progressive eliminati 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/prepare-contacts-migration.test.ts b/app/verticals/party-registry/tests/unit/prepare-contacts-migration.test.ts index 9accf91a2..ca6f7c9a3 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 '@app/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/relationship-domain-contract.test.ts b/app/verticals/party-registry/tests/unit/relationship-domain-contract.test.ts index 598c6369e..d3e1ae06a 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 '@app/effect-rstest'; +import { Effect, DateTime, Option, Schema } from 'effect'; import { ContactPersonOfRelationshipType, CreatePartyRelationshipPayloadSchema, @@ -40,206 +39,212 @@ 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 }), { + expect(decideRelationshipCreate([existing], { ...existing })).toEqual({ _tag: 'reuse', relationshipId: relationshipRef.resourceId, }); - assert.deepEqual( + expect( decideRelationshipCreate([existing], { relationshipId: 'ignored', validFrom: presentInstant('2026-09-15T10:00:00.000Z'), validTo: absentInstant, }), - { _tag: 'overlap', relationshipId: relationshipRef.resourceId }, - ); - assert.deepEqual( + ).toEqual({ _tag: 'overlap', relationshipId: relationshipRef.resourceId }); + expect( decideRelationshipCreate([existing], { relationshipId: 'ignored', validFrom: presentInstant('2026-10-01T10:00:00.000Z'), validTo: absentInstant, }), - { _tag: 'create' }, - ); + ).toEqual({ _tag: 'create' }); }); -test('only a still-future validity plan is ordinarily updateable', () => { - assert.deepEqual( +it('only a still-future validity plan is ordinarily updateable', () => { + expect( decideRelationshipUpdate( { revision: 2, @@ -253,9 +258,8 @@ test('only a still-future validity plan is ordinarily updateable', () => { }, instant('2026-09-03T00:00:00.000Z'), ), - { _tag: 'update' }, - ); - assert.deepEqual( + ).toEqual({ _tag: 'update' }); + expect( decideRelationshipUpdate( { revision: 2, @@ -269,9 +273,8 @@ test('only a still-future validity plan is ordinarily updateable', () => { }, instant('2026-09-03T00:00:00.000Z'), ), - { _tag: 'correction_required', fact: 'validTo' }, - ); - assert.deepEqual( + ).toEqual({ _tag: 'correction_required', fact: 'validTo' }); + expect( decideRelationshipUpdate( { revision: 2, @@ -284,9 +287,8 @@ test('only a still-future validity plan is ordinarily updateable', () => { }, instant('2026-09-03T00:00:00.000Z'), ), - { _tag: 'end_required' }, - ); - assert.deepEqual( + ).toEqual({ _tag: 'end_required' }); + expect( decideRelationshipUpdate( { revision: 2, @@ -296,9 +298,8 @@ test('only a still-future validity plan is ordinarily updateable', () => { { expectedRevision: 1, validFrom: undefined, validTo: absentInstant }, instant('2026-09-03T00:00:00.000Z'), ), - { _tag: 'revision_conflict', actualRevision: 2 }, - ); - assert.deepEqual( + ).toEqual({ _tag: 'revision_conflict', actualRevision: 2 }); + expect( decideRelationshipUpdate( { revision: 2, validFrom: absentInstant, validTo: absentInstant }, { @@ -308,9 +309,8 @@ test('only a still-future validity plan is ordinarily updateable', () => { }, instant('2026-09-03T00:00:00.000Z'), ), - { _tag: 'update' }, - ); - assert.deepEqual( + ).toEqual({ _tag: 'update' }); + expect( decideRelationshipUpdate( { revision: 2, @@ -324,9 +324,8 @@ test('only a still-future validity plan is ordinarily updateable', () => { }, instant('2026-09-03T00:00:00.000Z'), ), - { _tag: 'update' }, - ); - assert.deepEqual( + ).toEqual({ _tag: 'update' }); + expect( decideRelationshipUpdate( { revision: 2, @@ -340,11 +339,10 @@ test('only a still-future validity plan is ordinarily updateable', () => { }, instant('2026-09-03T00:00:00.000Z'), ), - { _tag: 'correction_required', fact: 'validFrom' }, - ); + ).toEqual({ _tag: 'correction_required', 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 +357,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')), { + expect(decideRelationshipEnd(current, exact, instant('2026-09-03T00:00:00.000Z'))).toEqual({ _tag: 'unchanged', }); - assert.deepEqual( + expect( decideRelationshipEnd( current, { ...exact, reason: 'A different historical explanation' }, instant('2026-09-03T00:00:00.000Z'), ), - { _tag: 'correction_required', fact: 'validTo' }, - ); - assert.deepEqual( + ).toEqual({ _tag: 'correction_required', fact: 'validTo' }); + expect( decideRelationshipEnd( { ...current, endProvenanceMethod: null, endProvenanceSource: null, endReason: null }, exact, instant('2026-09-03T00:00:00.000Z'), ), - { _tag: 'attach_end_evidence' }, - ); + ).toEqual({ _tag: 'attach_end_evidence' }); }); 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 46dabf04f..de494b8ec 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,6 +1,5 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { DateTime, Option, Schema } from 'effect'; +import { expect, it } from '@app/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'; @@ -25,108 +24,113 @@ 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.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(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 3e6428060..8620bafbf 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 '@app/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, Predicate } 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 }; }; -void 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); -}); - -void 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); + }), + ); -void 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); -}); - -void 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)); -}); - -void 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); }), ); -}); - -void 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); -}); - -void 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.ok(Predicate.isTagged(rejection, 'PartyAliasWriteRejected')); - if (Schema.is(PartyAliasWriteRejected)(rejection)) { - assert.equal(rejection.canonicalPartyRef.resourceId, survivorId); - } - assert.equal(harness.insertValues.length, 0); -}); - -void 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.ok(Predicate.isTagged(rejection, 'PartyRelationshipCorrectionRequired')); - if (Schema.is(PartyRelationshipCorrectionRequired)(rejection)) { - assert.equal(rejection.fact, 'validFrom'); - } - assert.equal(harness.updateSets.length, 0); -}); - -void 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'); - } -}); - -void 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); -}); - -void 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..3af4f6055 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 '@app/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 3a87f43e1..e60222856 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 { expect, it } from '@app/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,34 +274,37 @@ 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); } }); // eslint-disable-next-line complexity -- One schema-boundary matrix keeps all related family invariants visible. -test('models typed contact, relationship, and Counterparty lifecycles with owner-local references', () => { +it('models typed contact, relationship, and Counterparty lifecycles with owner-local references', () => { const contactChecks = Object.fromEntries( configOf(partyContactPoints).checks.map((candidate) => [ candidate.name, dialect.sqlToQuery(candidate.value).sql, ]), ); - assert.match(contactChecks['party_contact_points_shape_ck'] ?? '', /EMAIL/u); - assert.match(contactChecks['party_contact_points_shape_ck'] ?? '', /PHONE/u); - assert.match(contactChecks['party_contact_points_shape_ck'] ?? '', /ADDRESS/u); + expect(contactChecks['party_contact_points_shape_ck'] ?? '').toMatch(/EMAIL/u); + expect(contactChecks['party_contact_points_shape_ck'] ?? '').toMatch(/PHONE/u); + expect(contactChecks['party_contact_points_shape_ck'] ?? '').toMatch(/ADDRESS/u); for (const column of [ 'display_value', 'normalization_version', @@ -324,27 +324,25 @@ test('models typed contact, relationship, and Counterparty lifecycles with owner 'ended_recorded_at', 'revision', ]) { - assert.ok( + expect( configOf(partyContactPoints).columns.some((candidate) => candidate.name === column), column, - ); + ).toBeTruthy(); } - assert.match(contactChecks['party_contact_points_revision_ck'] ?? '', /> 0/u); - assert.match( - contactChecks['party_contact_points_additional_evidence_ck'] ?? '', + expect(contactChecks['party_contact_points_revision_ck'] ?? '').toMatch(/> 0/u); + expect(contactChecks['party_contact_points_additional_evidence_ck'] ?? '').toMatch( /additional_evidence_refs.*array.*additional_evidence_refs.*32/u, ); - assert.match( - contactChecks['party_contact_points_end_evidence_ck'] ?? '', + expect(contactChecks['party_contact_points_end_evidence_ck'] ?? '').toMatch( /valid_to.*end_reason.*end_provenance_source.*end_provenance_method.*end_evidence_refs.*ended_by_action_invocation_id.*ended_by_principal_id.*ended_recorded_at/u, ); - assert.match(contactChecks['party_contact_points_shape_ck'] ?? '', /num_nonnulls/u); - assert.match(contactChecks['party_contact_points_shape_ck'] ?? '', /\^\\\+/u); + expect(contactChecks['party_contact_points_shape_ck'] ?? '').toMatch(/num_nonnulls/u); + expect(contactChecks['party_contact_points_shape_ck'] ?? '').toMatch(/\^\\\+/u); const preferredIndex = configOf(partyContactPoints).indexes.find( (candidate) => candidate.config.name === 'party_contact_points_current_preferred_uk', ); - assert.equal(preferredIndex?.config.unique, true); - assert.ok(preferredIndex?.config.where); + expect(preferredIndex?.config.unique).toBe(true); + expect(preferredIndex?.config.where).toBeTruthy(); const purposeConfig = configOf(partyContactPointPurposes); const purposeChecks = Object.fromEntries( purposeConfig.checks.map((candidate) => [ @@ -352,10 +350,10 @@ test('models typed contact, relationship, and Counterparty lifecycles with owner dialect.sqlToQuery(candidate.value).sql, ]), ); - assert.match(purposeChecks['party_contact_point_purposes_key_ck'] ?? '', /REGISTERED/u); - assert.match(purposeChecks['party_contact_point_purposes_key_ck'] ?? '', /BILLING/u); - assert.match(purposeChecks['party_contact_point_purposes_key_ck'] ?? '', /DELIVERY/u); - assert.match(purposeChecks['party_contact_point_purposes_key_ck'] ?? '', /CORRESPONDENCE/u); + expect(purposeChecks['party_contact_point_purposes_key_ck'] ?? '').toMatch(/REGISTERED/u); + expect(purposeChecks['party_contact_point_purposes_key_ck'] ?? '').toMatch(/BILLING/u); + expect(purposeChecks['party_contact_point_purposes_key_ck'] ?? '').toMatch(/DELIVERY/u); + expect(purposeChecks['party_contact_point_purposes_key_ck'] ?? '').toMatch(/CORRESPONDENCE/u); for (const column of [ 'registry_context', 'jurisdiction', @@ -373,32 +371,30 @@ test('models typed contact, relationship, and Counterparty lifecycles with owner 'ended_recorded_at', 'revision', ]) { - assert.ok( + expect( purposeConfig.columns.some((candidate) => candidate.name === column), column, - ); + ).toBeTruthy(); } - assert.match( - purposeChecks['party_contact_point_purposes_end_evidence_ck'] ?? '', + expect(purposeChecks['party_contact_point_purposes_end_evidence_ck'] ?? '').toMatch( /valid_to.*end_reason.*end_provenance_source.*end_provenance_method.*end_evidence_refs.*ended_by_action_invocation_id.*ended_by_principal_id.*ended_recorded_at/u, ); - assert.equal( + expect( getTableName( foreignKey(partyContactPointPurposes, 'party_contact_point_purposes_contact_fk').reference() .foreignTable, ), - getTableName(partyContactPoints), - ); + ).toBe(getTableName(partyContactPoints)); const preferredPurpose = purposeConfig.indexes.find( (candidate) => candidate.config.name === 'party_contact_point_purposes_current_preferred_uk', ); - assert.equal(preferredPurpose?.config.unique, true); - assert.ok(preferredPurpose?.config.where); - assert.ok( + expect(preferredPurpose?.config.unique).toBe(true); + expect(preferredPurpose?.config.where).toBeTruthy(); + expect( purposeConfig.indexes.find( (candidate) => candidate.config.name === 'party_contact_point_purposes_current_registered_uk', )?.config.where, - ); + ).toBeTruthy(); const relationshipChecks = Object.fromEntries( configOf(partyRelationships).checks.map((candidate) => [ @@ -406,49 +402,44 @@ test('models typed contact, relationship, and Counterparty lifecycles with owner dialect.sqlToQuery(candidate.value).sql, ]), ); - assert.match(relationshipChecks['party_relationships_type_ck'] ?? '', /CONTACT_PERSON_OF/u); - assert.doesNotMatch( - relationshipChecks['party_relationships_type_ck'] ?? '', + expect(relationshipChecks['party_relationships_type_ck'] ?? '').toMatch(/CONTACT_PERSON_OF/u); + expect(relationshipChecks['party_relationships_type_ck'] ?? '').not.toMatch( /EMPLOYEE_OF|BRANCH_OF|OTHER/u, ); - assert.ok( + expect( configOf(partyRelationships).columns.some( (column) => column.name === 'revision' && column.notNull && column.hasDefault, ), - ); - assert.ok( + ).toBeTruthy(); + expect( configOf(partyRelationships).columns.some( (column) => column.name === 'valid_from' && !column.notNull, ), - ); - assert.ok( + ).toBeTruthy(); + expect( configOf(partyRelationships).columns.some( (column) => column.name === 'assertion_state' && column.notNull && column.hasDefault, ), - ); - assert.equal( - configOf(partyRelationships).columns.some((column) => column.name === 'is_current'), + ).toBeTruthy(); + expect(configOf(partyRelationships).columns.some((column) => column.name === 'is_current')).toBe( false, ); - assert.equal( - configOf(partyRelationships).columns.some((column) => column.name === 'state'), + expect(configOf(partyRelationships).columns.some((column) => column.name === 'state')).toBe( false, ); - assert.match( - relationshipChecks['party_relationships_interval_ck'] ?? '', + expect(relationshipChecks['party_relationships_interval_ck'] ?? '').toMatch( /valid_to.*is null.*valid_from.*is null.*valid_to.*>.*valid_from/u, ); - assert.match( - relationshipChecks['party_relationships_assertion_state_ck'] ?? '', + expect(relationshipChecks['party_relationships_assertion_state_ck'] ?? '').toMatch( /ACTIVE.*SUPERSEDED.*RETRACTED.*DISPUTED/u, ); - assert.doesNotMatch(relationshipChecks['party_relationships_assertion_state_ck'] ?? '', /ENDED/u); + expect(relationshipChecks['party_relationships_assertion_state_ck'] ?? '').not.toMatch(/ENDED/u); const relationshipIntervalIndex = configOf(partyRelationships).indexes.find( (candidate) => candidate.config.name === 'party_relationships_interval_idx', ); - assert.equal(relationshipIntervalIndex?.config.unique, false); - assert.equal(relationshipIntervalIndex?.config.where, undefined); - assert.match(relationshipChecks['party_relationships_revision_ck'] ?? '', /> 0/u); + expect(relationshipIntervalIndex?.config.unique).toBe(false); + expect(relationshipIntervalIndex?.config.where).toBe(undefined); + expect(relationshipChecks['party_relationships_revision_ck'] ?? '').toMatch(/> 0/u); for (const column of [ 'end_reason', 'end_provenance_source', @@ -458,29 +449,28 @@ test('models typed contact, relationship, and Counterparty lifecycles with owner 'ended_by_principal_id', 'ended_recorded_at', ]) { - assert.ok( + expect( configOf(partyRelationships).columns.some((candidate) => candidate.name === column), column, - ); + ).toBeTruthy(); } - assert.equal( + expect( getTableName( foreignKey(partyRelationships, 'party_relationships_tenant_from_party_fk').reference() .foreignTable, ), - getTableName(parties), - ); + ).toBe(getTableName(parties)); - assert.deepEqual(uniqueColumns(counterparties, 'party_counterparties_context_uk'), [ + expect(uniqueColumns(counterparties, 'party_counterparties_context_uk')).toEqual([ 'tenant_id', 'party_id', 'legal_entity_id', ]); for (const column of ['creation_reason', 'evidence_refs', 'source_record_refs', 'recorded_at']) { - assert.ok( + expect( configOf(counterparties).columns.some((candidate) => candidate.name === column), column, - ); + ).toBeTruthy(); } const roleChecks = Object.fromEntries( configOf(counterpartyRolePeriods).checks.map((candidate) => [ @@ -488,10 +478,9 @@ test('models typed contact, relationship, and Counterparty lifecycles with owner dialect.sqlToQuery(candidate.value).sql, ]), ); - assert.match(roleChecks['party_counterparty_role_periods_type_ck'] ?? '', /CUSTOMER/u); - assert.match(roleChecks['party_counterparty_role_periods_type_ck'] ?? '', /SUPPLIER/u); - assert.doesNotMatch( - roleChecks['party_counterparty_role_periods_type_ck'] ?? '', + expect(roleChecks['party_counterparty_role_periods_type_ck'] ?? '').toMatch(/CUSTOMER/u); + expect(roleChecks['party_counterparty_role_periods_type_ck'] ?? '').toMatch(/SUPPLIER/u); + expect(roleChecks['party_counterparty_role_periods_type_ck'] ?? '').not.toMatch( /BUSINESS_PARTNER/u, ); for (const column of [ @@ -505,109 +494,120 @@ test('models typed contact, relationship, and Counterparty lifecycles with owner 'ended_by_principal_id', 'ended_recorded_at', ]) { - assert.ok( + expect( configOf(counterpartyRolePeriods).columns.some((candidate) => candidate.name === column), column, - ); + ).toBeTruthy(); } const roleEndEvidence = roleChecks['party_counterparty_role_periods_end_evidence_ck'] ?? ''; - assert.match( - roleEndEvidence, + expect(roleEndEvidence).toMatch( /valid_to[^)]*is null[^)]*end_provenance_source[^)]*is null[^)]*end_provenance_method[^)]*is null/u, ); - assert.match( - roleEndEvidence, + expect(roleEndEvidence).toMatch( /valid_to.*is not null.*end_provenance_source.*btrim.*end_provenance_method.*btrim/u, ); - assert.equal( + expect( configOf(counterpartyRolePeriods).indexes.some( (candidate) => candidate.config.name === 'party_counterparty_role_periods_current_uk', ), - false, 'effective intervals, not an is_current unique index, own role-period uniqueness', - ); - assert.doesNotMatch( - roleChecks['party_counterparty_role_periods_state_ck'] ?? '', + ).toBe(false); + expect(roleChecks['party_counterparty_role_periods_state_ck'] ?? '').not.toMatch( /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 = Object.fromEntries( configOf(partyMatchDecisions).checks.map((candidate) => [ candidate.name, dialect.sqlToQuery(candidate.value).sql, ]), ); - 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', @@ -617,79 +617,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..946f5bf70 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 '@app/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 2ef551659..c0774b73f 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,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 '@app/effect-rstest'; + import { Effect, Schema, Predicate } from 'effect'; import { CoreSearchProjectionHitSchema } from '@app/core-runtime'; import type { CoreSearchQueryRuntimeService } from '@app/core-runtime'; @@ -59,42 +58,42 @@ const wrongResourceHit = Schema.decodeUnknownSync(CoreSearchProjectionHitSchema) title: 'Wrong', }); -void 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', + }, + ]); + }), +); -void 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 @@ void test('Counterparty adapter uses trusted Legal Entity, effective time, role tenantId, }); - assert.deepEqual(calls, [ + expect(calls).toEqual([ { effectiveAt, facets: [{ key: 'current-role', values: ['CUSTOMER'] }], @@ -126,7 +125,7 @@ void test('Counterparty adapter uses trusted Legal Entity, effective time, role tenantId, }, ]); - assert.deepEqual(hits, [ + expect(hits).toEqual([ { canonicalPartyRef: partyRef('survivor'), counterpartyRef: counterpartyRef('cp-1'), @@ -144,10 +143,11 @@ void test('Counterparty adapter uses trusted Legal Entity, effective time, role }, ]); }), - )); +); -void 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 @@ void test('Party adapter fails closed when a generic projection returns the wron tenantId, }), ); - assert.ok(Predicate.isTagged(failure, '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 47529628d..f5e52b58a 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 '@app/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,37 +158,40 @@ const makeSearchFixture = (identifiers: readonly PartySearchSourceValue[]) => { }; const assertAttachedIdentifierDelivery = ( - harness: ReturnType, + harness: Effect.Success>, search: ReturnType, ) => Effect.gen(function* verifyCommittedIdentifierDelivery() { 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, + expect(outbox).toBeTruthy(); + if (outbox === undefined) { + throw new Error('Expected value to be present'); + } + expect(commit.evidence.domainEvents[outbox.domainEventIndex]?.eventType).toBe( 'party.registry.official-identifier-added.v1', ); - assert.deepEqual(outbox.message.payloadJson, { officialIdentifierRef, partyRef }); - assert.deepEqual(yield* search.query(), []); + expect(outbox.message.payloadJson).toEqual({ officialIdentifierRef, partyRef }); + 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, { @@ -223,17 +225,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, { @@ -266,7 +269,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', @@ -275,10 +278,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; @@ -291,7 +295,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, { @@ -325,19 +329,24 @@ test('END_VALIDITY refreshes search only after its committed identifier message transport: { correlationId: 'identifier-sync', idempotencyKey: 'end-identifier-1' }, }); 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, + expect(outbox).toBeTruthy(); + if (outbox === undefined) { + throw new Error('Expected value to be present'); + } + expect(commit.evidence.domainEvents[outbox.domainEventIndex]?.eventType).toBe( 'party.registry.official-identifier-updated.v1', ); - assert.deepEqual(outbox.message.payloadJson, { officialIdentifierRef, partyRef }); - assert.equal((yield* search.query()).length, 1); + expect(outbox.message.payloadJson).toEqual({ officialIdentifierRef, partyRef }); + 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 e45a72000..d4317c448 100644 --- a/app/verticals/party-registry/tests/unit/search-projector.test.ts +++ b/app/verticals/party-registry/tests/unit/search-projector.test.ts @@ -1,6 +1,6 @@ -import { runEffectTestPromise } from '@app/core-runtime/testing/effect-runtime'; -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/effect-rstest'; +import { TestClock } from 'effect/testing'; + import { Effect, Exit, Match, Predicate } from 'effect'; import { makeCoreSearchQueryRuntime, @@ -68,8 +68,9 @@ const snapshot: PartySearchSourceSnapshot = { removedRefs: [], tenantId, }; -void 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 @@ void test('post-commit projection makes only active permission-safe identity evi }); 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' }, ]); }), - )); -void 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 @@ void test('aliases collapse to canonical identity and only alias-only evidence l 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', }; -void 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 @@ void test('snapshot-generation replay is idempotent, archive/unarchive refreshes 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 @@ void test('snapshot-generation replay is idempotent, archive/unarchive refreshes 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 @@ void test('snapshot-generation replay is idempotent, archive/unarchive refreshes yield* deliver(); current = snapshot; yield* deliver(); - assert.deepEqual(yield* query(true), []); + expect(yield* query(true)).toEqual([]); }), - )); -void 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 @@ void test('future-ended contact disappears at its period boundary without anothe 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([]); }), - )); -void 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 @@ void test('Counterparty identity survives aliases, current-role expiry and canon 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([]); }), - )); -void 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 @@ void test('shared public contact returns multiple Parties without uniqueness or 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']); }), - )); -void 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 @@ void test('rebuild reconciles omitted documents and preserves tombstones against yield* projector.project(context, { partyId: 'party-1', }); - assert.deepEqual( + expect( yield* makeCoreSearchQueryRuntime(store).search({ includeArchived: true, moduleId: 'party.registry', @@ -422,12 +435,12 @@ void test('rebuild reconciles omitted documents and preserves tombstones against resourceType: 'party.registry.party', tenantId, }), - [], - ); + ).toEqual([]); }), - )); -void 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 @@ void test('source failure is sanitized and leaves previously searchable state in resourceType: 'party.registry.party', tenantId, }); - assert.ok(Predicate.isTagged(failure, 'Failure')); - assert.equal(priorHits.length, 1); + expect(Predicate.isTagged(failure, 'Failure')).toBeTruthy(); + expect(priorHits.length).toBe(1); }), - )); -void 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 @@ void test('zero-length cancelled periods are never searchable and do not poison ], }), ); - 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, []); - }), - )); -void 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* makeCoreSearchQueryRuntime(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([]); }), - )); -void 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* makeCoreSearchQueryRuntime(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 @@ void test('correction and identifier/contact changes replace obsolete evidence i 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); }), - )); -void 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 @@ void test('a complete empty rebuild also rejects delayed evidence for a never-be 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 9b7079625..9e45f13e8 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 { expect, it } from '@app/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,4 +76,4 @@ test('Counterparty provider derives Legal Entity from trusted scope and never fr }, ]); }), - )); +); 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 d739a1c9a..40bcfae19 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,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 '@app/effect-rstest'; import { Effect, Schema, Predicate } from 'effect'; import type { OutboxWorkerHandlerContext } from '@app/core-runtime'; import { makeActionTestHarness } from '@app/core-runtime/testing/actions'; @@ -28,34 +26,37 @@ const request = { transport: { correlationId: 'search-rebuild-test', idempotencyKey: 'rebuild-1' }, }; -void 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', + ]); + }), +); -void 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 @@ void test('authorized rebuild commits one linked request without reading identit subjectResourceType: 'tenant', }, ]); - assert.deepEqual(committed[0]?.evidence.outboxMessages, [ + expect(committed[0]?.evidence.outboxMessages).toEqual([ { domainEventIndex: 0, message: { @@ -76,44 +77,41 @@ void test('authorized rebuild commits one linked request without reading identit }, ]); }), - ); -}); +); -void 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.ok(Predicate.isTagged(error, '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); }), - ); -}); +); -void 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.ok(Predicate.isTagged(replay, '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', }; -void 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 868d646ee..e650cd4e6 100644 --- a/app/verticals/party-registry/tests/unit/search-semantics.test.ts +++ b/app/verticals/party-registry/tests/unit/search-semantics.test.ts @@ -1,5 +1,4 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; +import { expect, it } from '@app/effect-rstest'; import { Match, Predicate } from 'effect'; import { normalizeCounterpartySearchHits, @@ -34,31 +33,33 @@ 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, ); -void 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), { + expect(normalizePartySearchHits({ includeArchived: false, tenantId }, hits)).toEqual({ _tag: 'SearchResults', items: [{ archived: false, matchedViaAlias: false, ref: partyRef('active'), title: 'Active' }], }); const included = normalizePartySearchHits({ includeArchived: true, tenantId }, hits); - assert.ok(Predicate.isTagged(included, '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, + ]); }); -void 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,29 +72,26 @@ void test('Party aliases collapse to one survivor while shared contact queries m { archived: false, canonicalPartyRef: partyRef('shared-2'), title: 'Other person' }, ]); - assert.ok(Predicate.isTagged(result, '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); }); -void 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.ok( + expect( Predicate.isTagged( normalizePartySearchHits({ includeArchived: true, tenantId }, [ { archived: false, canonicalPartyRef: wrongTenant, title: 'Wrong' }, ]), 'SearchProjectionViolation', ), - ); - assert.ok( + ).toBe(true); + expect( Predicate.isTagged( normalizePartySearchHits({ includeArchived: true, tenantId }, [ { archived: false, canonicalPartyRef: partyRef('same'), title: 'One' }, @@ -101,7 +99,7 @@ void test('Party Search fails closed when Core returns a cross-tenant or inconsi ]), 'SearchProjectionViolation', ), - ); + ).toBe(true); }); const baseCounterpartyHit = ( @@ -117,7 +115,7 @@ const baseCounterpartyHit = ( rolePeriods, }); -void 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', [ @@ -143,16 +141,13 @@ void test('Counterparty Search evaluates only current role periods at the exclus hits, ); - assert.ok(Predicate.isTagged(result, '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']); }); -void 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', @@ -163,11 +158,11 @@ void test('Counterparty Search without a role retains durable Counterparties wit [baseCounterpartyHit('no-role', 'p1')], ); - assert.ok(Predicate.isTagged(result, 'SearchResults')); - assert.deepEqual(expectSearchResults(result).items[0]?.currentRoles, []); + expect(Predicate.isTagged(result, 'SearchResults')).toBe(true); + expect(expectSearchResults(result).items[0]?.currentRoles).toEqual([]); }); -void 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'), @@ -183,22 +178,18 @@ void test('Counterparty identity dedupes independently and survivor collisions a hits, ); - assert.ok(Predicate.isTagged(result, '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'], + ]); }); -void 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', @@ -217,5 +208,5 @@ void test('Counterparty Search fails closed on the wrong Legal Entity instead of ], ); - assert.ok(Predicate.isTagged(result, '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 0ba7b9e72..da408b112 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 '@app/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, Predicate } from 'effect'; -import assert from 'node:assert/strict'; -import test from 'node:test'; 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) }; }; -void 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, + }); + }), +); -void 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 @@ void test('source exposes only current public email and phone search evidence, n ], }); 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 @@ void test('source exposes only current public email and phone search evidence, n 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 @@ void test('source exposes only current public email and phone search evidence, n '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 @@ void test('source exposes only current public email and phone search evidence, n ].toSorted(), ); }), - )); +); -void 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' }, + ]); + }), +); -void 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 @@ void test('full rebuild reads each Core-enumerated legal entity in the same snap [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'); }), - )); +); -void 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 @@ void test('Counterparty-only refresh emits only its canonical family and selecte ], }); 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]); }), - )); +); -void 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 @@ void test('alias cycles and cross-tenant source rows fail closed with sanitized ]) { 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.ok(Predicate.isTagged(outcome.failure, '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..1a0d55fea 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 '@app/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); }); From 9786dd317957e27ef632d24ad3741deb3bf9ff00 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 09:33:56 +0200 Subject: [PATCH 12/38] test(rstest): close enforcement gaps and verify scoped cleanup Cover Promise-returning owned test callbacks, preserve synchronous tests, and reap child processes on failure and interruption. Canonicalize temporary fixture paths for macOS lint overrides. Co-Authored-By: Claude Fable 5.1 --- .../support/impersonation-service-doubles.ts | 9 +- .../tests/unit/browser-effect-runtime.test.ts | 52 +- .../tests/unit/layout.test.tsx | 195 ++-- .../tests/unit/routes/login/page.test.tsx | 34 +- app/oxlint.config.ts | 17 +- .../integration/identity-runtime.test.ts | 2 +- .../integration/module-state-gate.test.ts | 2 +- .../tests/integration/outbox-runtime.test.ts | 12 +- .../tests/integration/read-runtime.test.ts | 2 +- .../integration/search-persistence.test.ts | 2 +- .../search-worker-snapshot.test.ts | 2 +- .../tests/unit/outbox-definition.test.ts | 184 ++-- .../tests/unit/outbox-health.test.ts | 84 +- .../tests/unit/permission-client.test.ts | 11 +- app/packages/effect-rstest/README.md | 18 + app/scripts/local-environment-values.test.mts | 125 ++- .../tests/database-access-boundaries.test.mts | 161 ++-- .../module-entrypoint-boundaries.test.mts | 241 ++--- .../tests/outbox-worker-delivery.test.mts | 276 ++++-- ...sion-current-action-authorization.test.mts | 252 +++-- .../tests/quality-audit-model.test.mts | 578 ++++++------ .../quality-audit-runtime-model.test.mts | 359 ++++---- app/scripts/tests/quality-audit.test.mts | 858 +++++++----------- .../typecheck-project-references.test.mts | 160 ++-- app/tools/oxlint/effect-native/README.md | 2 +- .../effect-native/repository-policy.config.ts | 17 +- .../rules/no-effect-run-in-tests.ts | 1 + .../rules/no-promise-shaped-port.ts | 130 ++- .../shared/test-restricted-imports.ts | 16 + .../invalid/effect-client.test.ts | 8 + .../valid/effect-client.test.ts | 7 + .../tests/unit/async-test-callback.test.ts | 13 + .../unit/inferred-promise-callback.test.ts | 18 + .../tests/unit/layer-test-callback.test.ts | 13 + .../tools/example/tests/async-tooling.test.ts | 4 + .../tests/unit/async-test-boundaries.test.ts | 17 + .../example/tests/synchronous-tooling.test.ts | 23 + .../effect-native/tests/registration.test.mts | 35 +- .../tests/temporary-workspace.mts | 7 +- .../api-integration-command-runtime.test.ts | 116 +-- 40 files changed, 2095 insertions(+), 1968 deletions(-) create mode 100644 app/tools/oxlint/effect-native/shared/test-restricted-imports.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/invalid/effect-client.test.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-effect-run-in-tests/valid/effect-client.test.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/async-test-callback.test.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/inferred-promise-callback.test.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/layer-test-callback.test.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/tools/example/tests/async-tooling.test.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/async-test-boundaries.test.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/tools/example/tests/synchronous-tooling.test.ts 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 1c15b04c1..bd164b587 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 @@ -26,12 +26,9 @@ const authenticationDefaults: AuthenticationServiceContract = { }; const providerDefaults: SupportAuthProvider['api'] = { - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. - getSession: async () => await unconfiguredPromise('getSession'), - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. - impersonateUser: async () => await unconfiguredPromise('impersonateUser'), - // oxlint-disable-next-line effect-native/no-promise-shaped-port -- This fixture implements Better Auth's foreign Promise API. - stopImpersonating: async () => await unconfiguredPromise('stopImpersonating'), + getSession: unconfiguredPromise.bind(undefined, 'getSession'), + impersonateUser: unconfiguredPromise.bind(undefined, 'impersonateUser'), + stopImpersonating: unconfiguredPromise.bind(undefined, 'stopImpersonating'), }; const storeDefaults: SupportImpersonationStore = { 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 3710d9690..d07346614 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,38 @@ -import { expect, test } from '@app/effect-rstest'; +import { expect, it } from '@app/effect-rstest'; 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; +it.effect('preserves Effect success and failure behavior at the browser boundary', () => + Effect.gen(function* browserBoundaryResults() { + const failure = { _tag: 'ExpectedFailure' } as const; - await expect(runBrowserEffect(Effect.succeed('ready'))).resolves.toBe('ready'); - await expect(runBrowserEffect(Effect.fail(failure))).rejects.toBe(failure); -}); + yield* Effect.promise(() => + expect(runBrowserEffect(Effect.succeed('ready'))).resolves.toBe('ready'), + ); + yield* Effect.promise(() => + 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; - }), +it.effect('interrupts the running Effect when its AbortSignal is aborted', () => + Effect.gen(function* browserBoundaryInterruption() { + const controller = new AbortController(); + let finalized = false; + const request = runBrowserEffect( + Effect.never.pipe( + Effect.ensuring( + Effect.sync(() => { + finalized = true; + }), + ), ), - ), - { signal: controller.signal }, - ); + { signal: controller.signal }, + ); - controller.abort(); + controller.abort(); - await expect(request).rejects.toBeTruthy(); - expect(finalized).toBe(true); -}); + yield* Effect.promise(() => expect(request).rejects.toBeTruthy()); + expect(finalized).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 cdbd57501..b2e2ded88 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 '@app/effect-rstest'; +import { afterEach, expect, it, rstest, test } from '@app/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'; @@ -286,98 +287,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?.querySelector(':scope > :last-child'); - expect(accountMenu?.dataset['position']).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); - - 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 - , - ); - - 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(); -}); - -test('renders complete ordered tenant items and dispatches keyboard selection once', async () => { - 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); -}); +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 + , + ); + + 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); + }), +); + +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' }); + 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(); + }), +); + +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' }); + 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/routes/login/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/login/page.test.tsx index d0cb90352..0470cd1fa 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 @@ -101,8 +101,8 @@ it('shows the required login controls through the UI kit', () => { ); }); -it.live('shows both field errors and one Toast when both values are missing', () => - Effect.gen(function* showsBothFieldErrorsAndOne() { +it.effect('shows both field errors and one Toast when both values are missing', () => + Effect.gen(function* showsBothFieldErrorsAndOneToast() { const user = userEvent.setup(); renderLogin(); @@ -120,8 +120,8 @@ it.live('shows both field errors and one Toast when both values are missing', () }), ); -it.live('creates one Toast per repeated invalid submission', () => - Effect.gen(function* createsOneToastPerRepeatedInvalid() { +it.effect('creates one Toast per repeated invalid submission', () => + Effect.gen(function* createsOneToastPerRepeatedInvalidSubmission() { const user = userEvent.setup(); renderLogin(); @@ -132,8 +132,8 @@ it.live('creates one Toast per repeated invalid submission', () => }), ); -it.live('shows only the Login error when the password is present', () => - Effect.gen(function* showsOnlyTheLoginErrorWhen() { +it.effect('shows only the Login error when the password is present', () => + Effect.gen(function* showsOnlyTheLoginErrorWhenPasswordPresent() { const user = userEvent.setup(); renderLogin(); @@ -148,8 +148,8 @@ it.live('shows only the Login error when the password is present', () => }), ); -it.live('shows only the Password error when the login is present', () => - Effect.gen(function* showsOnlyThePasswordErrorWhen() { +it.effect('shows only the Password error when the login is present', () => + Effect.gen(function* showsOnlyThePasswordErrorWhenLoginPresent() { const user = userEvent.setup(); renderLogin(); @@ -164,8 +164,8 @@ it.live('shows only the Password error when the login is present', () => }), ); -it.live('treats a whitespace-only Login as missing', () => - Effect.gen(function* treatsAWhitespaceOnlyLoginAs() { +it.effect('treats a whitespace-only Login as missing', () => + Effect.gen(function* treatsAWhitespaceOnlyLoginAsMissing() { const user = userEvent.setup(); renderLogin(); @@ -178,7 +178,7 @@ it.live('treats a whitespace-only Login as missing', () => }), ); -it.live('accepts a non-empty whitespace Password', () => +it.effect('accepts a non-empty whitespace Password', () => Effect.gen(function* acceptsANonEmptyWhitespacePassword() { const user = userEvent.setup(); renderLogin(); @@ -192,8 +192,8 @@ it.live('accepts a non-empty whitespace Password', () => }), ); -it.live('clears stale errors after both fields are corrected', () => - Effect.gen(function* clearsStaleErrorsAfterBothFields() { +it.effect('clears stale errors after both fields are corrected', () => + Effect.gen(function* clearsStaleErrorsAfterBothFieldsCorrected() { const user = userEvent.setup(); renderLogin(); @@ -208,8 +208,8 @@ it.live('clears stale errors after both fields are corrected', () => }), ); -it.live('runs the same validation when submitted with Enter', () => - Effect.gen(function* runsTheSameValidationWhenSubmitted() { +it.effect('runs the same validation when submitted with Enter', () => + Effect.gen(function* runsTheSameValidationWhenSubmittedWithEnter() { const user = userEvent.setup(); renderLogin(); @@ -222,8 +222,8 @@ it.live('runs the same validation when submitted with Enter', () => }), ); -it.live('submits valid values through the Shell authentication client and navigates home', () => - Effect.gen(function* submitsValidValuesThroughTheShell() { +it.effect('submits valid values through the Shell authentication client and navigates home', () => + Effect.gen(function* submitsValidValuesThroughShellAuthClient() { const user = userEvent.setup(); renderLogin(); diff --git a/app/oxlint.config.ts b/app/oxlint.config.ts index 123aa5a39..c84a9c717 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'; @@ -204,21 +205,7 @@ export default defineConfig({ 'eslint/no-restricted-imports': [ 'error', { - paths: [ - { message: 'Import test APIs from @app/effect-rstest instead.', name: 'node:test' }, - { - message: 'Import assertions from @app/effect-rstest instead.', - name: 'node:assert', - }, - { - message: 'Import assertions from @app/effect-rstest instead.', - name: 'node:assert/strict', - }, - { - message: 'Import test APIs from @app/effect-rstest instead.', - name: '@rstest/core', - }, - ], + paths: testRestrictedImports, }, ], }, 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 6d5e4d62b..b23c6787c 100644 --- a/app/packages/core-runtime/tests/integration/identity-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/identity-runtime.test.ts @@ -655,7 +655,7 @@ it.live( }), ), ); - yield* Effect.acquireRelease(Effect.void, () => release.pipe(Effect.orDie)); + yield* Effect.addFinalizer(() => release.pipe(Effect.orDie)); yield* exercise; }), ); 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 c9427ef91..f0ced9c3d 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 @@ -43,7 +43,7 @@ it.live( `${moduleKey}.${state.replaceAll('_', '-')}`; const configuration = yield* loadDatabaseConfig(); const database = yield* makeCoreDatabase(configuration); - yield* Effect.acquireRelease(Effect.void, () => + yield* Effect.addFinalizer(() => Effect.gen(function* moduleStateGate2() { yield* database.executor .delete(tenantModuleStates) 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 6744f97d8..8bec0821c 100644 --- a/app/packages/core-runtime/tests/integration/outbox-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/outbox-runtime.test.ts @@ -497,11 +497,7 @@ it.live( ).toEqual([]); }), ); -it.live('keeps test descriptor arrays compatible with the erased startup registry surface', () => - Effect.sync(() => { - const registry: readonly AnyOutboxWorkerRegistration[] = [ - makeWorker('consumer.registry-proof'), - ]; - expect(registry[0]?.descriptor.workerKey).toBe('consumer.registry-proof'); - }), -); +it('keeps test descriptor arrays compatible with the erased startup registry surface', () => { + const registry: readonly AnyOutboxWorkerRegistration[] = [makeWorker('consumer.registry-proof')]; + expect(registry[0]?.descriptor.workerKey).toBe('consumer.registry-proof'); +}); 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 03f8695e6..1615fe69c 100644 --- a/app/packages/core-runtime/tests/integration/read-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/read-runtime.test.ts @@ -70,7 +70,7 @@ it.live('commits live allowed evidence before releasing a governed read result', () => ({ kind: 'module', moduleId: 'core.shell' }), ); - yield* Effect.acquireRelease(Effect.void, () => + yield* Effect.addFinalizer(() => Effect.gen(function* readRuntime2() { yield* Effect.promise(() => admin.query('delete from core.data_access_events where tenant_id = $1', [tenantId]), 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 931b750ba..8bef0da0f 100644 --- a/app/packages/core-runtime/tests/integration/search-persistence.test.ts +++ b/app/packages/core-runtime/tests/integration/search-persistence.test.ts @@ -119,7 +119,7 @@ it.live( ]); }).pipe(Effect.orDie); - yield* Effect.acquireRelease(Effect.void, () => cleanup); + 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')`, 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 e8f120346..acdcaf4fd 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 @@ -152,7 +152,7 @@ const workerSnapshotProgram = Effect.gen(function* workerSnapshotIntegration() { ); }).pipe(Effect.orDie); - yield* Effect.acquireRelease(Effect.void, () => cleanup); + yield* Effect.addFinalizer(() => cleanup); const exercise = Effect.gen(function* exerciseWorkerSnapshots() { yield* Effect.tryPromise({ catch: (cause) => new Cause.UnknownError(cause), 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 a816a40b7..c99959d0e 100644 --- a/app/packages/core-runtime/tests/unit/outbox-definition.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-definition.test.ts @@ -82,102 +82,94 @@ it.effect('defines an exact immutable registration while keeping the handler opa }); }), ); -it.effect('preserves schema inference for a typed handler payload', () => - Effect.sync(() => { - defineOutboxWorker( - { - consumerModuleKey: 'consumer', - entrypoint: defineTenantModuleEntrypoint({ - access: 'background', - authorization: { kind: 'owner_local_background' }, - entrypointKey: 'consumer.inference-proof', - moduleKey: 'consumer', - role: 'worker', - }), - leaseDurationMs: 1000, - payloadSchema, - producerModuleKey: 'producer', - retryPolicy: { - initialBackoffMs: 0, - maxAttempts: 1, - maxBackoffMs: 0, - multiplier: 1, - }, - topic: 'producer.message-created', - workerKey: 'consumer.inference-proof', - }, - (payload) => { - const key: string = payload.messageKey; - return Effect.sync(() => expect(key).toBe(payload.messageKey)); - }, - ); - }), -); -it.effect('rejects invalid identities, retry policies, and lease policies', () => - Effect.sync(() => { - const valid = makeWorker().descriptor; - const invalidDescriptors = [ - { ...valid, workerKey: 'producer.foreign-worker' }, - { - ...valid, - entrypoint: defineTenantModuleEntrypoint({ - access: 'background', - authorization: { kind: 'owner_local_background' }, - entrypointKey: valid.workerKey, - moduleKey: 'foreign', - role: 'worker', - }), - }, - { ...valid, topic: 'Invalid' }, - { ...valid, leaseDurationMs: 999 }, - { ...valid, retryPolicy: { ...valid.retryPolicy, maxAttempts: 0 } }, - { - ...valid, - retryPolicy: { ...valid.retryPolicy, initialBackoffMs: 11_000 }, - }, - { ...valid, retryPolicy: { ...valid.retryPolicy, multiplier: 0 } }, - ]; - for (const descriptor of invalidDescriptors) { - expect(() => defineOutboxWorker(descriptor, () => Effect.void)).toThrow( - OutboxWorkerDescriptorError, - ); - } - }), -); -it.effect('rejects duplicate worker keys and calculates bounded exponential backoff', () => - Effect.sync(() => { - const worker = makeWorker(); - expect(() => validateOutboxWorkerRegistrations([worker, worker])).toThrow( - expect.objectContaining({ - name: 'OutboxWorkerDescriptorError', - reason: expect.stringMatching(/duplicate Outbox Worker key/u), +it('preserves schema inference for a typed handler payload', () => { + defineOutboxWorker( + { + consumerModuleKey: 'consumer', + entrypoint: defineTenantModuleEntrypoint({ + access: 'background', + authorization: { kind: 'owner_local_background' }, + entrypointKey: 'consumer.inference-proof', + moduleKey: 'consumer', + role: 'worker', }), - ); - 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); - }), -); -it.effect('validates and freezes the schema-free installed subscription catalog', () => - Effect.sync(() => { - const worker = makeWorker(); - const subscription = { - consumerModuleKey: worker.descriptor.consumerModuleKey, - entrypoint: worker.descriptor.entrypoint, - producerModuleKey: worker.descriptor.producerModuleKey, - topic: worker.descriptor.topic, - workerKey: worker.descriptor.workerKey, - }; - const validated = validateOutboxWorkerSubscriptions([subscription]); - 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), + leaseDurationMs: 1000, + payloadSchema, + producerModuleKey: 'producer', + retryPolicy: { + initialBackoffMs: 0, + maxAttempts: 1, + maxBackoffMs: 0, + multiplier: 1, + }, + topic: 'producer.message-created', + workerKey: 'consumer.inference-proof', + }, + (payload) => { + const key: string = payload.messageKey; + return Effect.sync(() => expect(key).toBe(payload.messageKey)); + }, + ); +}); +it('rejects invalid identities, retry policies, and lease policies', () => { + const valid = makeWorker().descriptor; + const invalidDescriptors = [ + { ...valid, workerKey: 'producer.foreign-worker' }, + { + ...valid, + entrypoint: defineTenantModuleEntrypoint({ + access: 'background', + authorization: { kind: 'owner_local_background' }, + entrypointKey: valid.workerKey, + moduleKey: 'foreign', + role: 'worker', }), + }, + { ...valid, topic: 'Invalid' }, + { ...valid, leaseDurationMs: 999 }, + { ...valid, retryPolicy: { ...valid.retryPolicy, maxAttempts: 0 } }, + { + ...valid, + retryPolicy: { ...valid.retryPolicy, initialBackoffMs: 11_000 }, + }, + { ...valid, retryPolicy: { ...valid.retryPolicy, multiplier: 0 } }, + ]; + for (const descriptor of invalidDescriptors) { + expect(() => defineOutboxWorker(descriptor, () => Effect.void)).toThrow( + OutboxWorkerDescriptorError, ); - }), -); + } +}); +it('rejects duplicate worker keys and calculates bounded exponential backoff', () => { + const worker = makeWorker(); + 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); +}); +it('validates and freezes the schema-free installed subscription catalog', () => { + const worker = makeWorker(); + const subscription = { + consumerModuleKey: worker.descriptor.consumerModuleKey, + entrypoint: worker.descriptor.entrypoint, + producerModuleKey: worker.descriptor.producerModuleKey, + topic: worker.descriptor.topic, + workerKey: worker.descriptor.workerKey, + }; + const validated = validateOutboxWorkerSubscriptions([subscription]); + 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-health.test.ts b/app/packages/core-runtime/tests/unit/outbox-health.test.ts index ef8bd2b3c..cb6ec058c 100644 --- a/app/packages/core-runtime/tests/unit/outbox-health.test.ts +++ b/app/packages/core-runtime/tests/unit/outbox-health.test.ts @@ -6,47 +6,43 @@ import { runOutboxWorkerProcess } from '../../src/outbox/process.ts'; import { OutboxRuntime } from '../../src/outbox/runtime.ts'; it.live('production health binds all IPv4 interfaces for external-container probes', () => - Effect.scoped( - 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'); - }), - ), + 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'); + }), ); it.live( 'readiness starts false, follows successful/failing cycles, expires, and closes on shutdown', () => { let now = 1000; - return 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; - 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); - }), - ); + 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); + }); }, ); @@ -64,16 +60,14 @@ it.live( ); it.live('a health port already in use produces a typed server startup failure', () => - Effect.scoped( - 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); - }), - ), + 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); + }), ); it.effect( 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 25633515b..052aad0d2 100644 --- a/app/packages/core-runtime/tests/unit/permission-client.test.ts +++ b/app/packages/core-runtime/tests/unit/permission-client.test.ts @@ -1,6 +1,7 @@ import { expect, it, rstest } from '@app/effect-rstest'; import { v1 } from '@authzed/authzed-node'; -import { Cause, Effect, Predicate, Schema } from 'effect'; +import { Cause, Effect, Fiber, Predicate, Schema } from 'effect'; +import { TestClock } from 'effect/testing'; import { SpiceDbPermissionClientError, createSpiceDbPermissionClient, @@ -86,7 +87,7 @@ it.effect('SDK rejections become typed permission failures without leaking diagn }), ); -it.live('an SDK call that never replies is bounded by the permission deadline', () => +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 @@ -98,9 +99,11 @@ it.live('an SDK call that never replies is bounded by the permission deadline', Effect.sync(() => createSpiceDbPermissionClient(configuration, SPICEDB_CHECK_TIMEOUT_MS)), (acquiredClient) => Effect.sync(() => acquiredClient.close()), ); - const failure = yield* Effect.flip( + 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/effect-rstest/README.md b/app/packages/effect-rstest/README.md index a312aecbf..c9c7f189b 100644 --- a/app/packages/effect-rstest/README.md +++ b/app/packages/effect-rstest/README.md @@ -28,6 +28,24 @@ it.effect('reads an Effect value', () => ); ``` +### Shared layers + +Use `it.layer` to share a layer across a suite; its resources are released when the suite ends. Set `excludeTestServices: true` when the suite needs live services instead of the test clock/console (the default is `false`). Replace `Layer.empty` below with your fixture layer: + +```ts +import { expect, it } from '@app/effect-rstest'; +import { Effect, Layer } from 'effect'; + +it.layer(Layer.empty, { excludeTestServices: true })('live fixture suite', (it) => { + it.effect('reads an Effect value', () => + Effect.gen(function* readsValue() { + const value = yield* Effect.succeed(42); + expect(value).toBe(42); + }), + ); +}); +``` + ### Scoped cleanup Both `it.effect` and `it.live` automatically own and close a per-test scope. Register cleanup with `Effect.acquireRelease` (or `Effect.addFinalizer` for an already-acquired resource); no extra `Effect.scoped` or Promise bridge is needed. Finalizers run on success, failure, and interruption. diff --git a/app/scripts/local-environment-values.test.mts b/app/scripts/local-environment-values.test.mts index ab5b48df1..1e7d4b5f8 100644 --- a/app/scripts/local-environment-values.test.mts +++ b/app/scripts/local-environment-values.test.mts @@ -1,81 +1,70 @@ import { expect, it } from '@app/effect-rstest'; -import { Effect } from 'effect'; import { localPublicClientValues, localSpiceDbValues } from './local-environment-values.mts'; const spiceDbGrpcPort = '50052'; const spiceDbHttpPort = '8444'; const spiceDbEndpoint = `localhost:${spiceDbGrpcPort}`; -it.effect('preserves canonical SpiceDB values when no local override is supplied', () => - Effect.sync(() => { - const values = localSpiceDbValues( - [ - `SPICEDB_ENDPOINT=${spiceDbEndpoint}`, - `SPICEDB_GRPC_PORT=${spiceDbGrpcPort}`, - `SPICEDB_HTTP_PORT=${spiceDbHttpPort}`, - 'SPICEDB_INSECURE=true', - 'SPICEDB_PRESHARED_KEY=existing-key', - ], - {}, - ); +it('preserves canonical SpiceDB values when no local override is supplied', () => { + const values = localSpiceDbValues( + [ + `SPICEDB_ENDPOINT=${spiceDbEndpoint}`, + `SPICEDB_GRPC_PORT=${spiceDbGrpcPort}`, + `SPICEDB_HTTP_PORT=${spiceDbHttpPort}`, + 'SPICEDB_INSECURE=true', + 'SPICEDB_PRESHARED_KEY=existing-key', + ], + {}, + ); - expect(values).toEqual({ - SPICEDB_ENDPOINT: spiceDbEndpoint, - SPICEDB_GRPC_PORT: spiceDbGrpcPort, - SPICEDB_HTTP_PORT: spiceDbHttpPort, - SPICEDB_INSECURE: 'true', - SPICEDB_PRESHARED_KEY: 'existing-key', - }); - }), -); + expect(values).toEqual({ + SPICEDB_ENDPOINT: spiceDbEndpoint, + SPICEDB_GRPC_PORT: spiceDbGrpcPort, + SPICEDB_HTTP_PORT: spiceDbHttpPort, + SPICEDB_INSECURE: 'true', + SPICEDB_PRESHARED_KEY: 'existing-key', + }); +}); -it.effect('applies explicit local port overrides as one consistent endpoint', () => - Effect.sync(() => { - const values = localSpiceDbValues( - ['SPICEDB_ENDPOINT=localhost:50051', 'SPICEDB_GRPC_PORT=50051'], - { grpcPort: spiceDbGrpcPort, httpPort: spiceDbHttpPort }, - ); +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 }, + ); - expect(values.SPICEDB_ENDPOINT).toBe(spiceDbEndpoint); - expect(values.SPICEDB_GRPC_PORT).toBe(spiceDbGrpcPort); - expect(values.SPICEDB_HTTP_PORT).toBe(spiceDbHttpPort); - }), -); + expect(values.SPICEDB_ENDPOINT).toBe(spiceDbEndpoint); + expect(values.SPICEDB_GRPC_PORT).toBe(spiceDbGrpcPort); + expect(values.SPICEDB_HTTP_PORT).toBe(spiceDbHttpPort); +}); -it.effect( - 'derives local public-client URLs from configured Shell identity/port and Party API URL', - () => - Effect.sync(() => { - expect( - localPublicClientValues([], { - partyRegistryApiBaseUrl: 'http://localhost:4199/party-api', - shellId: 'staff-shell', - shellPort: 3099, - }), - ).toEqual({ - ONTOS_PARTY_REGISTRY_API_BASE_URL: 'http://localhost:4199/party-api', - ONTOS_SHELL_GATEWAY_BASE_URL: 'http://localhost:3099/staff-shell-api', - }); +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, }), -); + ).toEqual({ + ONTOS_PARTY_REGISTRY_API_BASE_URL: 'http://localhost:4199/party-api', + ONTOS_SHELL_GATEWAY_BASE_URL: 'http://localhost:3099/staff-shell-api', + }); +}); -it.effect('preserves explicitly configured public-client URLs', () => - Effect.sync(() => { - expect( - localPublicClientValues( - [ - 'ONTOS_SHELL_GATEWAY_BASE_URL=https://gateway.example.test/shell-super-app-api', - 'ONTOS_PARTY_REGISTRY_API_BASE_URL=https://party.example.test/party-registry-api', - ], - { - partyRegistryApiBaseUrl: 'http://localhost:4102/party-registry-api', - shellId: 'shell-super-app', - shellPort: 3020, - }, - ), - ).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', - }); - }), -); +it('preserves explicitly configured public-client URLs', () => { + expect( + localPublicClientValues( + [ + 'ONTOS_SHELL_GATEWAY_BASE_URL=https://gateway.example.test/shell-super-app-api', + 'ONTOS_PARTY_REGISTRY_API_BASE_URL=https://party.example.test/party-registry-api', + ], + { + partyRegistryApiBaseUrl: 'http://localhost:4102/party-registry-api', + shellId: 'shell-super-app', + shellPort: 3020, + }, + ), + ).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/tests/database-access-boundaries.test.mts b/app/scripts/tests/database-access-boundaries.test.mts index 627a80d0d..861a763a3 100644 --- a/app/scripts/tests/database-access-boundaries.test.mts +++ b/app/scripts/tests/database-access-boundaries.test.mts @@ -10,88 +10,85 @@ 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.tryPromise(() => - mkdtemp(path.join(os.tmpdir(), 'ontos-db-boundary-')), + const root = yield* Effect.acquireRelease( + Effect.tryPromise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-db-boundary-'))), + (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), ); - 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* 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', - 'verticals/stock/src/actions/package-root.action.ts:1', - 'verticals/stock/src/actions/reserve.action.ts:1', - 'verticals/stock/src/actions/reserve.action.ts:2', - 'verticals/stock/src/actions/side-effect.action.ts:1', - 'verticals/stock/src/db/billing-leak.ts:1', - 'verticals/stock/src/db/cross-owner.ts:1', - 'verticals/stock/src/db/dynamic-core.ts:1', - 'verticals/stock/src/reads/list.read.ts:1', - 'verticals/stock/src/reads/side-effect.read.ts:1', - '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 { - yield* Effect.tryPromise(() => rm(root, { 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', + 'verticals/stock/src/actions/package-root.action.ts:1', + 'verticals/stock/src/actions/reserve.action.ts:1', + 'verticals/stock/src/actions/reserve.action.ts:2', + 'verticals/stock/src/actions/side-effect.action.ts:1', + 'verticals/stock/src/db/billing-leak.ts:1', + 'verticals/stock/src/db/cross-owner.ts:1', + 'verticals/stock/src/db/dynamic-core.ts:1', + 'verticals/stock/src/reads/list.read.ts:1', + 'verticals/stock/src/reads/side-effect.read.ts:1', + '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', + ]); }), ); diff --git a/app/scripts/tests/module-entrypoint-boundaries.test.mts b/app/scripts/tests/module-entrypoint-boundaries.test.mts index f45a93fec..e93b6643d 100644 --- a/app/scripts/tests/module-entrypoint-boundaries.test.mts +++ b/app/scripts/tests/module-entrypoint-boundaries.test.mts @@ -1,9 +1,10 @@ import { expect, it } from '@app/effect-rstest'; +import { existsSync } from 'node:fs'; import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { NodeServices } from '@effect/platform-node'; -import { Effect } from 'effect'; +import { Effect, Exit } from 'effect'; import { checkModuleEntrypointBoundaries as checkModuleEntrypointBoundariesEffect } from '../check-module-entrypoint-boundaries.mts'; import { assertPublishedCrossMicroVerticalContractUsage, @@ -48,11 +49,15 @@ const write = (root: string, file: string, source: string) => yield* Effect.tryPromise(() => writeFile(target, source, 'utf-8')); }); -const makeFixture = () => +const makeFixture = ( + onAcquired: (root: string) => Effect.Effect = () => Effect.void, +) => Effect.gen(function* testEffect2() { - const root = yield* Effect.tryPromise(() => - mkdtemp(path.join(os.tmpdir(), 'ontos-module-entrypoints-')), + const root = yield* Effect.acquireRelease( + Effect.tryPromise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-module-entrypoints-'))), + (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), ); + yield* onAcquired(root); yield* write( root, '.modernjs/ultramodern.json', @@ -117,11 +122,7 @@ export const routeMeta = { moduleId: 'inventory.stock', ownerAppId: 'inventory-s it.live('accepts governed generated Actions, pages, Workers, catalogs, and route manifests', () => Effect.gen(function* testEffect3() { const root = yield* makeFixture(); - try { - yield* checkModuleEntrypointBoundaries(root); - } finally { - yield* Effect.tryPromise(() => rm(root, { force: true, recursive: true })); - } + yield* checkModuleEntrypointBoundaries(root); }), ); @@ -192,37 +193,36 @@ export const manifest = { api: { 'stock-list': StockListApi, } };`, it.live('accepts only a complete generated governed module API seam', () => Effect.gen(function* testEffect5() { const root = yield* makeFixture(); - try { - yield* writeGovernedModuleApi(root); - yield* write( - root, - 'verticals/inventory-stock/shared/apis/inventory-search.ts', - `// @generated by OntOS Codesmith Governed Contribution v1 + yield* writeGovernedModuleApi(root); + yield* write( + root, + 'verticals/inventory-stock/shared/apis/inventory-search.ts', + `// @generated by OntOS Codesmith Governed Contribution v1 // @ontos-contribution-kind search-provider export const InventorySearchApi = HttpApi.make('InventorySearchApi');`, - ); - yield* write( - root, - 'verticals/inventory-stock/src/search/inventory.provider.ts', - `// @generated by OntOS Codesmith Governed Contribution v1 + ); + yield* write( + root, + 'verticals/inventory-stock/src/search/inventory.provider.ts', + `// @generated by OntOS Codesmith Governed Contribution v1 // @ontos-contribution-kind search-provider export const inventoryProvider = true;`, - ); - yield* checkModuleEntrypointBoundaries(root); + ); + yield* checkModuleEntrypointBoundaries(root); - yield* write( - root, - 'verticals/inventory-stock/src/api/stock-list.read.ts', - `// @generated by OntOS Codesmith module-api v1 + yield* write( + root, + 'verticals/inventory-stock/src/api/stock-list.read.ts', + `// @generated by OntOS Codesmith module-api v1 export const stockListEntrypoint = defineTenantModuleEntrypoint({ access: 'historical_read', authorization: { kind: 'context_permission', permission: 'module.access' }, entrypointKey: 'inventory.stock.api.stock-list', moduleKey: 'inventory.stock', role: 'api' }); export const stockListRead = defineRead({ entrypoint: stockListEntrypoint, legalEntityScope: 'optional', permissionTarget: 'tenant', policies: [] });`, - ); - yield* checkModuleEntrypointBoundaries(root); + ); + yield* checkModuleEntrypointBoundaries(root); - yield* write( - root, - 'verticals/inventory-stock/vertical.registration.ts', - `export const registration = { api: { 'stock-list': () => + yield* write( + root, + 'verticals/inventory-stock/vertical.registration.ts', + `export const registration = { api: { 'stock-list': () => import('./src/api/stock-list-client.ts'), } }; // // @@ -230,20 +230,17 @@ export const stockListRead = defineRead({ entrypoint: stockListEntrypoint, legal // // // `, - ); - yield* checkModuleEntrypointBoundaries(root); - - yield* write( - root, - 'verticals/inventory-stock/api/stock-list-read-server.ts', - `// @generated by OntOS Codesmith module-api v1\nexport const server = true;`, - ); - expect(String(yield* Effect.flip(checkModuleEntrypointBoundaries(root)))).toMatch( - /module APIs require an approved Codesmith generator/u, - ); - } finally { - yield* Effect.tryPromise(() => rm(root, { force: true, recursive: true })); - } + ); + yield* checkModuleEntrypointBoundaries(root); + + yield* write( + root, + 'verticals/inventory-stock/api/stock-list-read-server.ts', + `// @generated by OntOS Codesmith module-api v1\nexport const server = true;`, + ); + expect(String(yield* Effect.flip(checkModuleEntrypointBoundaries(root)))).toMatch( + /module APIs require an approved Codesmith generator/u, + ); }), ); @@ -309,21 +306,17 @@ for (const violation of violations) { it.live(`rejects bypass ${violation.file}`, () => Effect.gen(function* testEffect6() { const root = yield* makeFixture(); - try { - 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); - } finally { - yield* Effect.tryPromise(() => rm(root, { force: true, recursive: true })); - } + 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); }), ); } @@ -331,56 +324,52 @@ for (const violation of violations) { it.live('rejects missing headers, spoofed metadata, and stale generated descriptors', () => Effect.gen(function* testEffect7() { const root = yield* makeFixture(); - try { - yield* write( - root, - ACTION_FILE, - `${ACTION_HEADER_FOR_TEST} + yield* write( + root, + ACTION_FILE, + `${ACTION_HEADER_FOR_TEST} // @ontos-action-owner inventory.stock // @ontos-action-slug reserve export const stale = true; `, - ); - 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} + ); + 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} // @ontos-action-owner inventory.stock // @ontos-action-slug reserve // defineTenantModuleEntrypoint({ access: 'write', entrypointKey: 'inventory.stock.reserve', moduleKey: 'inventory.stock', role: 'action' }); export const spoofed = true; `, - ); - 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 + ); + 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 // @ontos-outbox-worker-key inventory.stock.project // @ontos-outbox-worker-owner inventory.stock export const stale = 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, - ); - } finally { - yield* Effect.tryPromise(() => 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, + ); }), ); @@ -745,17 +734,49 @@ it('keeps executable owner behavior out of published Outbox contracts', () => { it.live('rejects missing, orphaned, and cross-owner route manifest entries', () => Effect.gen(function* testEffect8() { const root = yield* makeFixture(); - try { - yield* write( - root, - 'apps/shell-super-app/src/routes/ultramodern-route-metadata.ts', - `export const routes = [{ entrypoint: { entrypointKey: 'inventory.stock.page.orders' } }];`, - ); - expect(String(yield* Effect.flip(checkModuleEntrypointBoundaries(root)))).toMatch( - /manifest is stale/u, - ); - } finally { - yield* Effect.tryPromise(() => rm(root, { force: true, recursive: true })); - } + yield* write( + root, + 'apps/shell-super-app/src/routes/ultramodern-route-metadata.ts', + `export const routes = [{ entrypoint: { entrypointKey: 'inventory.stock.page.orders' } }];`, + ); + expect(String(yield* Effect.flip(checkModuleEntrypointBoundaries(root)))).toMatch( + /manifest is stale/u, + ); }), ); + +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((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) { + expect(exit).toEqual(Exit.fail(failure)); + } + }), + ); +} diff --git a/app/scripts/tests/outbox-worker-delivery.test.mts b/app/scripts/tests/outbox-worker-delivery.test.mts index d9b227331..55f22e607 100644 --- a/app/scripts/tests/outbox-worker-delivery.test.mts +++ b/app/scripts/tests/outbox-worker-delivery.test.mts @@ -1,10 +1,13 @@ import { expect, it } from '@app/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 { Effect, 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'; @@ -17,15 +20,50 @@ 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 waitForExit = (child: ChildProcess) => + Effect.gen(function* awaitChildExit() { + while (child.exitCode === null && child.signalCode === null && child.pid !== undefined) { + yield* Effect.sleep('10 millis'); + } + }); + +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.tryPromise(() => - mkdtemp(path.join(os.tmpdir(), 'ontos-worker-artifact-')), + 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 }), @@ -68,31 +106,27 @@ const makeFixture = () => it.live('generates a separate supervised worker setup without changing owner configuration', () => Effect.gen(function* testEffect2() { const root = yield* makeFixture(); - try { - 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 = 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, - ); - } finally { - yield* Effect.tryPromise(() => rm(root, { force: true, recursive: true })); - } + 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 = 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, + ); }), ); it.live('materializes and starts a relocatable production worker artifact', () => Effect.gen(function* testEffect3() { const root = yield* makeFixture(); - try { - const command = path.resolve('scripts/materialize-zerops-runtime.mjs'); - const child = spawn( + const command = path.resolve('scripts/materialize-zerops-runtime.mjs'); + const child = yield* scopedChild(() => + spawn( '/usr/bin/env', [ `ULTRAMODERN_WORKSPACE_ROOT=${root}`, @@ -110,43 +144,35 @@ it.live('materializes and starts a relocatable production worker artifact', () = cwd: root, stdio: ['ignore', 'pipe', 'pipe'], }, - ); - let diagnostics = ''; - child.stdout.on('data', (data) => { - diagnostics += String(data); - }); - child.stderr.on('data', (data) => { - diagnostics += String(data); - }); - const exitEvent: unknown = yield* Effect.tryPromise(() => once(child, 'exit')); - const [statusOption] = decodeExitEvent(exitEvent); - const status = Option.getOrNull(statusOption); - expect(status, diagnostics).toBe(0); - const artifact = decodeWorkerArtifact( - yield* Effect.tryPromise(() => - readFile(path.join(root, '.zerops/runtime/ledger-worker/worker-artifact.json'), 'utf-8'), - ), - ); - 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'], { + ), + ); + let diagnostics = ''; + child.stdout.on('data', (data) => { + diagnostics += String(data); + }); + child.stderr.on('data', (data) => { + diagnostics += String(data); + }); + yield* Effect.tryPromise((signal) => once(child, 'exit', { signal })); + const status = child.exitCode; + expect(status, diagnostics).toBe(0); + const artifact = decodeWorkerArtifact( + yield* Effect.tryPromise(() => + readFile(path.join(root, '.zerops/runtime/ledger-worker/worker-artifact.json'), 'utf-8'), + ), + ); + expect(artifact.serviceId).toBe('ledger-worker'); + expect(artifact.sourceInputs.some((input: string) => input.endsWith(WORKER_HOST_ENTRY))).toBe( + true, + ); + const runtime = yield* scopedChild(() => + spawn(process.execPath, ['worker.mjs'], { cwd: path.join(root, '.zerops/runtime/ledger-worker'), stdio: ['ignore', 'pipe', 'pipe'], - }); - const dataEvent: unknown = yield* Effect.tryPromise(() => - once(runtime.stdout, 'data', { - signal: AbortSignal.timeout(3000), - }), - ); - const [workerOutput] = decodeDataEvent(dataEvent); - const output = String(workerOutput); - expect(output).toMatch(/worker-started/u); - runtime.kill('SIGTERM'); - } finally { - yield* Effect.tryPromise(() => rm(root, { force: true, recursive: true })); - } + }), + ); + const output = yield* readWorkerOutput(runtime); + expect(output).toMatch(/worker-started/u); }), ); @@ -171,35 +197,105 @@ it.live( it.live('bundles the real Party host including the production Effect HTTP health adapter', () => Effect.gen(function* testEffect5() { - const runtimeDir = yield* Effect.tryPromise(() => - mkdtemp(path.join(os.tmpdir(), 'ontos-party-worker-bundle-')), - ); - try { - 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( - 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'); - } finally { - yield* Effect.tryPromise(() => rm(runtimeDir, { force: true, recursive: true })); - } + 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( + 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/provision-current-action-authorization.test.mts b/app/scripts/tests/provision-current-action-authorization.test.mts index 6c95a61d7..f15502c81 100644 --- a/app/scripts/tests/provision-current-action-authorization.test.mts +++ b/app/scripts/tests/provision-current-action-authorization.test.mts @@ -117,7 +117,7 @@ const rejectionOf = ( }), ); -it.live( +it.effect( 'selects only exact source-controlled development and stage targets', Effect.fn(function* testEffect2() { const development = @@ -172,7 +172,7 @@ it.live( }), ); -it.live( +it.effect( 'reports expected provisioning failures and sanitizes unexpected Promise rejections', Effect.fn(function* testEffect4() { const expected = new ActionAuthorizationProvisioningError({ @@ -202,7 +202,7 @@ it.live( }), ); -it.live( +it.effect( 'workspace validation rejects both provisioning spellings in every automatic startup path', Effect.fn(function* testEffect5() { const source = yield* Effect.tryPromise({ @@ -220,10 +220,13 @@ it.live( 'node ./scripts/provision-current-action-authorization.mts', 'local:initialize': 'node ./scripts/initialize-local-development.mts', }; - const validationRoot = yield* Effect.tryPromise({ - catch: (error) => error, - try: () => mkdtemp(path.join(os.tmpdir(), 'ontos-workspace-validation-')), - }); + const validationRoot = yield* Effect.acquireRelease( + Effect.tryPromise({ + catch: (error) => error, + try: () => mkdtemp(path.join(os.tmpdir(), 'ontos-workspace-validation-')), + }), + (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), + ); let validationIndex = 0; const validate = ( sources: Readonly>, @@ -258,49 +261,20 @@ it.live( }); }); - try { - yield* validate({}); - const validationPromises: Effect.Effect[] = []; - for (const command of [ - 'node ./scripts/provision-current-action-authorization.mts', - 'pnpm authorization:provision-current-actions', + yield* validate({}); + const validationPromises: Effect.Effect[] = []; + for (const command of [ + 'node ./scripts/provision-current-action-authorization.mts', + 'pnpm authorization:provision-current-actions', + ]) { + for (const file of [ + 'scripts/initialize-local-development.mts', + 'scripts/locki-feature.sh', + 'docker-compose.yml', + 'scripts/run-zerops-spicedb.sh', ]) { - for (const file of [ - 'scripts/initialize-local-development.mts', - 'scripts/locki-feature.sh', - 'docker-compose.yml', - 'scripts/run-zerops-spicedb.sh', - ]) { - validationPromises.push( - validate({ [file]: command }).pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/must not provision Action authorization/u), - ), - ), - ); - } - for (const automaticScript of ['dev', 'build', 'cloudflare:build', 'cloudflare:deploy']) { - validationPromises.push( - validate({}, { [automaticScript]: command }).pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/must not invoke Action authorization provisioning/u), - ), - ), - ); - } validationPromises.push( - validate( - {}, - { - 'local:initialize': `${scripts['local:initialize']} && ${command}`, - }, - ).pipe( + validate({ [file]: command }).pipe( Effect.flip, Effect.map((error) => expect(() => { @@ -310,17 +284,39 @@ it.live( ), ); } - yield* Effect.all(validationPromises, { concurrency: 'unbounded' }); - } finally { - yield* Effect.tryPromise({ - catch: (error) => error, - try: () => rm(validationRoot, { recursive: true }), - }); + for (const automaticScript of ['dev', 'build', 'cloudflare:build', 'cloudflare:deploy']) { + validationPromises.push( + validate({}, { [automaticScript]: command }).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/must not invoke Action authorization provisioning/u), + ), + ), + ); + } + validationPromises.push( + validate( + {}, + { + 'local:initialize': `${scripts['local:initialize']} && ${command}`, + }, + ).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(/must not provision Action authorization/u), + ), + ), + ); } + yield* Effect.all(validationPromises, { concurrency: 'unbounded' }); }), ); -it.live( +it.effect( 'discovers exactly the current generated Core and Party Registry Action baseline', Effect.fn(function* testEffect7() { const workspaceRoot = path.resolve(import.meta.dirname, '../..'); @@ -344,7 +340,7 @@ it.live( }), ); -it.live( +it.effect( 'builds lossless, deterministic Tenant-membership grants for development and stage', Effect.fn(function* testEffect8() { const development = @@ -461,7 +457,7 @@ const makeProvisioningClient = ( }; }; -it.live( +it.effect( 'provisions with TOUCH, verifies both outcomes, and is safe to rerun', Effect.fn(function* testEffect9() { const target = yield* selectActionAuthorizationProvisioningTarget(developmentConfiguration); @@ -486,7 +482,7 @@ it.live( }), ); -it.live( +it.effect( 'never grants explicit Actions through Tenant membership and verifies recorded policy outcomes', Effect.fn(function* testEffect10() { const target = yield* selectActionAuthorizationProvisioningTarget(developmentConfiguration); @@ -528,7 +524,7 @@ it.live( }), ); -it.live( +it.effect( 'rejects missing or mismatched explicit Action verification assertions', Effect.fn(function* testEffect11() { const target = yield* selectActionAuthorizationProvisioningTarget(developmentConfiguration); @@ -576,7 +572,7 @@ it.live( }), ); -it.live( +it.effect( 'fails promotion when an explicit Action policy contradicts a recorded assertion', Effect.fn(function* testEffect13() { const target = yield* selectActionAuthorizationProvisioningTarget(developmentConfiguration); @@ -611,7 +607,7 @@ it.live( }), ); -it.live( +it.effect( 'rejects invalid input and missing membership before writing grants', Effect.fn(function* testEffect15() { const target = yield* selectActionAuthorizationProvisioningTarget(developmentConfiguration); @@ -646,7 +642,7 @@ it.live( }), ); -it.live( +it.effect( 'fails closed when authorization returns no permission response', Effect.fn(function* testEffect16() { const target = yield* selectActionAuthorizationProvisioningTarget(developmentConfiguration); @@ -665,7 +661,7 @@ it.live( }), ); -it.live( +it.effect( 'sanitizes authorization service failures', Effect.fn(function* testEffect17() { const secret = 'super-secret-credential'; @@ -719,7 +715,7 @@ const writeInventory = ( ); }); -it.live( +it.effect( 'rejects incomplete and duplicate public Action discovery', Effect.fn(function* testEffect19() { const workspaceRoot = path.resolve(import.meta.dirname, '../..'); @@ -758,80 +754,74 @@ it.live( }).pipe(Effect.provide(NodeServices.layer)); const [currentPublicAction] = currentContract.manifest.publicSurface.actions; expect(currentPublicAction !== undefined).toBe(true); - const root = yield* Effect.tryPromise({ - catch: (error) => error, - try: () => mkdtemp(path.join(os.tmpdir(), 'ontos-action-discovery-')), - }); - try { - const vertical = { id: 'example', package: '@app/example', path: 'verticals/example' }; - 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 incompleteError = yield* rejectionOf( - Effect.tryPromise({ - catch: (error) => error, - try: () => discoverCurrentActionKeys(root, incomplete), - }), - ); - expect(Schema.is(NativeProvisioningError)(incompleteError)).toBe(true); - expect(Schema.decodeUnknownSync(NativeProvisioningError)(incompleteError).code).toBe( - 'action_authorization_discovery_failed', - ); + const root = yield* Effect.acquireRelease( + Effect.tryPromise({ + catch: (error) => error, + try: () => 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' }; + 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 incompleteError = yield* rejectionOf( + Effect.tryPromise({ + catch: (error) => error, + try: () => discoverCurrentActionKeys(root, incomplete), + }), + ); + 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 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( - Effect.tryPromise({ - catch: (error) => error, - try: () => discoverCurrentActionKeys(root, duplicate), - }), - ); - expect(Schema.is(NativeProvisioningError)(duplicateError)).toBe(true); - expect(Schema.decodeUnknownSync(NativeProvisioningError)(duplicateError).code).toBe( - 'action_authorization_discovery_failed', - ); - - yield* writeInventory(root, [vertical, vertical]); - yield* Effect.tryPromise({ + }, + }); + const duplicateError = yield* rejectionOf( + Effect.tryPromise({ catch: (error) => error, try: () => discoverCurrentActionKeys(root, duplicate), - }).pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(NativeProvisioningError), - ), - ); - } finally { - yield* Effect.tryPromise({ - catch: (error) => error, - try: () => rm(root, { recursive: true }), - }); - } + }), + ); + expect(Schema.is(NativeProvisioningError)(duplicateError)).toBe(true); + expect(Schema.decodeUnknownSync(NativeProvisioningError)(duplicateError).code).toBe( + 'action_authorization_discovery_failed', + ); + + yield* writeInventory(root, [vertical, vertical]); + yield* Effect.tryPromise({ + catch: (error) => error, + try: () => discoverCurrentActionKeys(root, duplicate), + }).pipe( + Effect.flip, + Effect.map((error) => + expect(() => { + throw error; + }).toThrow(NativeProvisioningError), + ), + ); }), ); -it.live( +it.effect( 'the operator entrypoint rejects every command-line argument before loading configuration', Effect.fn(function* testEffect20() { const error = yield* failureOf( diff --git a/app/scripts/tests/quality-audit-model.test.mts b/app/scripts/tests/quality-audit-model.test.mts index 9cb71de68..047ce0bb7 100644 --- a/app/scripts/tests/quality-audit-model.test.mts +++ b/app/scripts/tests/quality-audit-model.test.mts @@ -56,7 +56,10 @@ const write = (root: string, file: string, source: string) => { const fixture = () => Effect.gen(function* testEffect2() { - const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'ontos-knip-model-'))); + 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, @@ -202,111 +205,103 @@ it.live( 'real pinned Knip models exact consumers and preserves neighboring findings', Effect.fn(function* testEffect3() { const root = yield* fixture(); - try { - const base = yield* 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 base = yield* Schema.decodeUnknownEffect(KnipConfigSchema)({ + workspaces: { + '.': { + drizzle: { config: [] }, + entry: [indexFile, configurationFiles], + lefthook: false, + node: false, + project: [sourcePattern, configurationFiles, 'tools/**/*.{ts,mts}'], }, - }); - const consumerPath = path.join(root, '.codex/knip-model/consumers.mts'); - 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); - 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}`), - ); - 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', - ), - ).toBe(true); - expect( - findings('exports').includes( - 'verticals/remote/shared/ultramodern-build.ts#unusedBuildNeighbor', - ), - ).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, - ); - 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, - ); - expect(findings('unlisted').includes('verticals/remote/src/index.ts#declaredRemote')).toBe( - true, + 'verticals/*': { entry: [indexFile, configurationFiles], project: ['**/*.{ts,mts}'] }, + }, + }); + const consumerPath = path.join(root, '.codex/knip-model/consumers.mts'); + 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); + 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}`), ); - expect(findings('dependencies').includes('verticals/remote/package.json#effect')).toBe(true); - 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', - ), - ).toBe(true); - expect( - model.evidence.some( - (item) => - item.kind === 'resolver' && - item.source === resolverFile && - item.target === rspackPackageName && - item.line === 3 && - item.column === 17 && - item.resolved !== undefined, - ), - ).toBe(true); - expect( - !model.evidence.some((item) => item.kind === 'resolver' && item.source === directFile), - ).toBe(true); - expect( - model.evidence.some( - (item) => item.target === 'src/schema.ts#registeredSchema' && item.kind === 'export', - ), - ).toBe(true); - } finally { - rmSync(root, { force: true, recursive: true }); - } + 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', + ), + ).toBe(true); + expect( + findings('exports').includes( + 'verticals/remote/shared/ultramodern-build.ts#unusedBuildNeighbor', + ), + ).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, + ); + 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, + ); + expect(findings('unlisted').includes('verticals/remote/src/index.ts#declaredRemote')).toBe( + true, + ); + expect(findings('dependencies').includes('verticals/remote/package.json#effect')).toBe(true); + 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', + ), + ).toBe(true); + expect( + model.evidence.some( + (item) => + item.kind === 'resolver' && + item.source === resolverFile && + item.target === rspackPackageName && + item.line === 3 && + item.column === 17 && + item.resolved !== undefined, + ), + ).toBe(true); + expect( + !model.evidence.some((item) => item.kind === 'resolver' && item.source === directFile), + ).toBe(true); + expect( + model.evidence.some( + (item) => item.target === 'src/schema.ts#registeredSchema' && item.kind === 'export', + ), + ).toBe(true); }), ); @@ -314,21 +309,17 @@ it.live( 'modeling fails on invalid source instead of silently losing consumer evidence', Effect.fn(function* testEffect4() { const root = yield* fixture(); - try { - write(root, 'module-federation.config.ts', 'export default { broken: ;'); - 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), - ), - ); - } finally { - rmSync(root, { force: true, recursive: true }); - } + write(root, 'module-federation.config.ts', 'export default { broken: ;'); + 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), + ), + ); }), ); @@ -348,194 +339,189 @@ it.live( const root = yield* 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 { - write( - root, - 'quality-audit/scope.json', - yield* stringify({ - exclude: [], - patterns: [sourcePattern, configurationFiles, 'verticals/**/*.{ts,mts}'], - }), - ); - write( - root, - 'quality-audit/knip.json', - yield* stringify({ - workspaces: { - '.': { - drizzle: { config: [] }, - entry: [indexFile, configurationFiles], - lefthook: false, - node: false, - project: [sourcePattern, configurationFiles, 'tools/**/*.{ts,mts}'], - }, - 'verticals/*': { entry: [indexFile, configurationFiles], project: ['**/*.{ts,mts}'] }, + 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', + yield* stringify({ + exclude: [], + patterns: [sourcePattern, configurationFiles, 'verticals/**/*.{ts,mts}'], + }), + ); + write( + root, + 'quality-audit/knip.json', + yield* stringify({ + workspaces: { + '.': { + drizzle: { config: [] }, + entry: [indexFile, configurationFiles], + lefthook: false, + node: false, + project: [sourcePattern, configurationFiles, 'tools/**/*.{ts,mts}'], }, - }), - ); - write( - root, - 'quality-audit/knip-reporter.mts', - readFileSync(path.join(appRoot, 'quality-audit/knip-reporter.mts'), 'utf-8'), - ); - 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({ - results: Schema.Array( - Schema.Struct({ - coverage: Schema.Struct({ - findingCounts: Schema.Record(Schema.String, Schema.Number), - modeledUsages: Schema.Number, - nativeFindingCounts: Schema.Record(Schema.String, Schema.Number), - }), - name: Schema.String, + 'verticals/*': { entry: [indexFile, configurationFiles], project: ['**/*.{ts,mts}'] }, + }, + }), + ); + write( + root, + 'quality-audit/knip-reporter.mts', + readFileSync(path.join(appRoot, 'quality-audit/knip-reporter.mts'), 'utf-8'), + ); + 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({ + results: Schema.Array( + Schema.Struct({ + coverage: Schema.Struct({ + findingCounts: Schema.Record(Schema.String, Schema.Number), + modeledUsages: Schema.Number, + nativeFindingCounts: Schema.Record(Schema.String, Schema.Number), }), - ), - runDirectory: Schema.String, - status: Schema.Literal('reported'), - }), - ), - )(readFileSync(path.join(output, 'summary.json'), 'utf-8')); - const result = summary.results.find((entry) => entry.name === 'knip'); - 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')); - 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], - ); - expect( - raw.issues.some( - (issue) => - issue.file === directFile && - issue.unlisted.some((entry) => entry.name === rspackPackageName), - ), - ).toBe(true); - } finally { - rmSync(root, { force: true, recursive: true }); - rmSync(output, { force: true, recursive: true }); + name: Schema.String, + }), + ), + runDirectory: Schema.String, + status: Schema.Literal('reported'), + }), + ), + )(readFileSync(path.join(output, 'summary.json'), 'utf-8')); + const result = summary.results.find((entry) => entry.name === 'knip'); + 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')); + 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], + ); + expect( + raw.issues.some( + (issue) => + issue.file === directFile && + issue.unlisted.some((entry) => entry.name === rspackPackageName), + ), + ).toBe(true); }), ); it.live( 'vendor ownership rejects a different installed copy and accepts the same canonical target', Effect.fn(function* testEffect7() { - const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'ontos-knip-copy-controls-'))); - try { - 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`; - yield* Effect.all( - ( - [ - [owner, 'owner', { producer: '1.0.0' }], - [producer, 'producer', { target: '1.0.0' }], - [ownerTarget, 'target', {}], - [producerTarget, 'target', {}], - ] as const - ).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, - indexFile, + 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`; + yield* Effect.all( + ( [ - ...requirePrelude, - `require.resolve('target', { paths: [${JSON.stringify(path.join(root, owner, 'index.js'))}] });`, - ].join('\n'), - ); - const consumerPath = path.join(root, '.audit/consumers.mts'); - const build = () => - Effect.gen(function* testEffect9() { - return yield* buildKnipModel( + [owner, 'owner', { producer: '1.0.0' }], + [producer, 'producer', { target: '1.0.0' }], + [ownerTarget, 'target', {}], + [producerTarget, 'target', {}], + ] as const + ).map(([directory, name, dependencies]) => + Effect.gen(function* testEffect8() { + write( root, - { entry: [indexFile], node: false, project: [sourcePattern] }, - consumerPath, - ).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', - ); - 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')), - ); - 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); - 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 = 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: [${yield* stringify(path.join(root, owner, 'missing'))}] });`, - ].join('\n'), - ); - const missingAnchor = yield* build(); - expect( - !missingAnchor.evidence.some( - (item) => item.kind === 'resolver' && item.target === 'target', - ), - ).toBe(true); - } finally { - rmSync(root, { force: true, recursive: true }); + `${directory}/package.json`, + yield* stringify({ dependencies, main: 'index.js', name }), + ); + write(root, `${directory}/index.js`, fixtureModuleSource); + }), + ), + { concurrency: 'unbounded' }, + ); + write( + root, + indexFile, + [ + ...requirePrelude, + `require.resolve('target', { paths: [${JSON.stringify(path.join(root, owner, 'index.js'))}] });`, + ].join('\n'), + ); + const consumerPath = path.join(root, '.audit/consumers.mts'); + const build = () => + Effect.gen(function* testEffect9() { + return yield* buildKnipModel( + root, + { entry: [indexFile], node: false, project: [sourcePattern] }, + consumerPath, + ).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', + ); + 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')), + ); + 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); + 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 = 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: [${yield* stringify(path.join(root, owner, 'missing'))}] });`, + ].join('\n'), + ); + const missingAnchor = yield* build(); + expect( + !missingAnchor.evidence.some((item) => item.kind === 'resolver' && item.target === 'target'), + ).toBe(true); }), ); diff --git a/app/scripts/tests/quality-audit-runtime-model.test.mts b/app/scripts/tests/quality-audit-runtime-model.test.mts index 8a4920d6b..719ff2540 100644 --- a/app/scripts/tests/quality-audit-runtime-model.test.mts +++ b/app/scripts/tests/quality-audit-runtime-model.test.mts @@ -40,7 +40,10 @@ const facts = (root: string) => }); const fixture = () => Effect.gen(function* testEffect3() { - const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'ontos-knip-runtime-'))); + 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', @@ -119,51 +122,47 @@ it.live( 'runtime consumers require the exact CSS, shell, deployment and compiler contracts', Effect.fn(function* testEffect4() { const root = yield* fixture(); - try { - const initial = yield* facts(root); - for (const target of [ - cssUsed, - launchedFile, - resetFile, - '@effect/tsgo', - pluginName, - readinessConfig, - ]) { - expect( - initial.some((fact) => fact.target === target), - target, - ).toBe(true); - } - for (const target of ['@fixture/css-dead', '@fixture/css-comment', 'scripts/dead.mts']) { - expect(!initial.some((fact) => fact.target === target), target).toBe(true); - } - expect(initial.find((fact) => fact.target === pluginName)?.kind).toBe(compilerOptionKind); + const initial = yield* facts(root); + for (const target of [ + cssUsed, + launchedFile, + resetFile, + '@effect/tsgo', + pluginName, + readinessConfig, + ]) { expect( - initial.some( - (fact) => fact.target === `${readinessConfig}#default` && fact.kind === 'export', - ), + initial.some((fact) => fact.target === target), + target, ).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, - yield* stringify({ - compilerOptions: { plugins: [{ name: pluginName }], types: [pluginName] }, - }), - ); - write( - root, - `${vendorRoot}/ultramodern-performance-readiness.mjs`, - `const configPath = '${readinessConfig}'; void configPath;`, - ); - const changed = yield* facts(root); - for (const target of [cssUsed, launchedFile, resetFile, pluginName, readinessConfig]) { - expect(!changed.some((fact) => fact.target === target), target).toBe(true); - } - } finally { - rmSync(root, { force: true, recursive: true }); + } + for (const target of ['@fixture/css-dead', '@fixture/css-comment', 'scripts/dead.mts']) { + expect(!initial.some((fact) => fact.target === target), target).toBe(true); + } + 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, + yield* stringify({ + compilerOptions: { plugins: [{ name: pluginName }], types: [pluginName] }, + }), + ); + write( + root, + `${vendorRoot}/ultramodern-performance-readiness.mjs`, + `const configPath = '${readinessConfig}'; void configPath;`, + ); + const changed = yield* facts(root); + for (const target of [cssUsed, launchedFile, resetFile, pluginName, readinessConfig]) { + expect(!changed.some((fact) => fact.target === target), target).toBe(true); } }), ); @@ -172,30 +171,26 @@ it.live( 'compiler-option proof requires the installed pinned compiler and built-in plugin documentation', Effect.fn(function* testEffect5() { const root = yield* fixture(); - try { - write(root, tsgoReadme, '"name": "@effect/language-service"'); - 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 = 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, - yield* stringify({ - compilerOptions: { - plugins: [{ name: pluginName }], - types: [`${pluginName}/types`], - }, - }), - ); - const typeImport = yield* facts(root); - expect(!typeImport.some((fact) => fact.kind === compilerOptionKind)).toBe(true); - } finally { - rmSync(root, { force: true, recursive: true }); - } + write(root, tsgoReadme, '"name": "@effect/language-service"'); + 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 = 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, + yield* stringify({ + compilerOptions: { + plugins: [{ name: pluginName }], + types: [`${pluginName}/types`], + }, + }), + ); + const typeImport = yield* facts(root); + expect(!typeImport.some((fact) => fact.kind === compilerOptionKind)).toBe(true); }), ); @@ -203,30 +198,26 @@ it.live( 'compiler configuration accepts JSONC but rejects malformed and schema-invalid input', Effect.fn(function* testEffect6() { const root = yield* fixture(); - try { - write( - root, - compilerConfig, - `{ - // TypeScript permits comments and trailing commas. - "compilerOptions": { "plugins": [{ "name": "${pluginName}", }], }, + write( + root, + compilerConfig, + `{ + // TypeScript permits comments and trailing commas. + "compilerOptions": { "plugins": [{ "name": "${pluginName}", }], }, }`, - ); - const modeled = yield* facts(root); - expect(modeled.some((fact) => fact.kind === compilerOptionKind)).toBe(true); - write(root, compilerConfig, '{ "compilerOptions": {'); - yield* facts(root).pipe( - Effect.flip, - Effect.map((error) => expect(String(error)).toMatch(/InvalidTsconfig/u)), - ); - write(root, compilerConfig, '{ "compilerOptions": { "plugins": false } }'); - yield* facts(root).pipe( - Effect.flip, - Effect.map((error) => expect(String(error)).toMatch(/plugins/u)), - ); - } finally { - rmSync(root, { force: true, recursive: true }); - } + ); + const modeled = yield* facts(root); + expect(modeled.some((fact) => fact.kind === compilerOptionKind)).toBe(true); + write(root, compilerConfig, '{ "compilerOptions": {'); + yield* facts(root).pipe( + Effect.flip, + Effect.map((error) => expect(String(error)).toMatch(/InvalidTsconfig/u)), + ); + write(root, compilerConfig, '{ "compilerOptions": { "plugins": false } }'); + yield* facts(root).pipe( + Effect.flip, + Effect.map((error) => expect(String(error)).toMatch(/plugins/u)), + ); }), ); @@ -234,24 +225,20 @@ it.live( 'DTS compiler resolution belongs to the invoking workspace and excludes commented lookalikes', Effect.fn(function* testEffect7() { const root = yield* fixture(); - try { - 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 = 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 = yield* facts(root); - expect(!commented.some((fact) => fact.source === configFile)).toBe(true); - } finally { - rmSync(root, { force: true, recursive: true }); + 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 = 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 = yield* facts(root); + expect(!commented.some((fact) => fact.source === configFile)).toBe(true); }), ); @@ -259,26 +246,22 @@ it.live( 'Lefthook configuration proves only intended tool usage and ignores commented hook text', Effect.fn(function* testEffect8() { const root = yield* fixture(); - try { - const source = 'pre-commit:\n commands:\n format:\n run: pnpm format\n'; - write(root, 'lefthook.yml', source); - const configured = yield* facts(root); - const tool = configured.find((fact) => fact.target === 'lefthook'); - expect(tool?.kind).toBe('dependency'); - expect(tool?.reason ?? '').toMatch(/does not establish hook activation/u); - write( - root, - 'lefthook.yml', - source - .split('\n') - .map((line) => `# ${line}`) - .join('\n'), - ); - const commented = yield* facts(root); - expect(!commented.some((fact) => fact.target === 'lefthook')).toBe(true); - } finally { - rmSync(root, { force: true, recursive: true }); - } + const source = 'pre-commit:\n commands:\n format:\n run: pnpm format\n'; + write(root, 'lefthook.yml', source); + const configured = yield* facts(root); + const tool = configured.find((fact) => fact.target === 'lefthook'); + expect(tool?.kind).toBe('dependency'); + expect(tool?.reason ?? '').toMatch(/does not establish hook activation/u); + write( + root, + 'lefthook.yml', + source + .split('\n') + .map((line) => `# ${line}`) + .join('\n'), + ); + const commented = yield* facts(root); + expect(!commented.some((fact) => fact.target === 'lefthook')).toBe(true); }), ); @@ -286,71 +269,65 @@ it.live( 'real Knip keeps unused neighboring files, dependency names and exports after runtime modeling', Effect.fn(function* testEffect9() { const root = yield* fixture(); - try { - const consumerPath = path.join(root, '.audit/consumers.mts'); - 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'], - }, + const consumerPath = path.join(root, '.audit/consumers.mts'); + 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 = 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)); - 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)), - ); - 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)), - ); - expect( - !report.issues.some( - (issue) => - issue.file === readinessConfig && issue.exports.some((item) => item.name === 'default'), - ), - ).toBe(true); - for (const name of ['unusedLauncherExport', 'unusedResetExport', 'unusedConfigExport']) { - expect(exports.has(name), name).toBe(true); - } - } finally { - rmSync(root, { force: true, recursive: true }); + }, + 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)); + 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)), + ); + 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)), + ); + expect( + !report.issues.some( + (issue) => + issue.file === readinessConfig && issue.exports.some((item) => item.name === 'default'), + ), + ).toBe(true); + for (const name of ['unusedLauncherExport', 'unusedResetExport', 'unusedConfigExport']) { + expect(exports.has(name), name).toBe(true); } }), ); diff --git a/app/scripts/tests/quality-audit.test.mts b/app/scripts/tests/quality-audit.test.mts index 55c50da3d..ea9d88b54 100644 --- a/app/scripts/tests/quality-audit.test.mts +++ b/app/scripts/tests/quality-audit.test.mts @@ -43,16 +43,9 @@ const includesPolicyFiles = ( }; const reportSchema = Schema.fromJsonString(Schema.Unknown); -const stringify = (value: Schema.Json) => - Effect.gen(function* testEffect1() { - return yield* Schema.encodeEffect(reportSchema)(value); - }); -const validate = (name: string, source: string) => - Effect.gen(function* testEffect2() { - return yield* validateReport(name, source); - }); +const encodeReport = Schema.encodeEffect(reportSchema); -it.live( +it.effect( 'report-only analysis accepts findings and rejects empty or malformed reports', Effect.fn(function* testEffect3() { const report = { @@ -66,62 +59,42 @@ it.live( ], statistics: { total: { clones: 1, sources: 2 } }, }; - expect(yield* validate('jscpd', yield* stringify(report))).toEqual({ + expect(yield* validateReport('jscpd', yield* encodeReport(report))).toEqual({ coverage: { tokenEligibleFiles: 2 }, files: 2, findings: 1, }); - yield* validate('jscpd', '{}').pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/Malformed analyzer report/u), - ), - ); - yield* validate('jscpd', '{broken').pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/Malformed analyzer report/u), + 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 } } }), ), ); - yield* validate( - 'jscpd', - yield* stringify({ duplicates: [], statistics: { total: { clones: 0, sources: 0 } } }), - ).pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/no files/u), - ), - ); - yield* validate( - 'jscpd', - yield* stringify({ ...report, statistics: { total: { clones: 0, sources: 2 } } }), - ).pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/count disagrees/u), + 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); }), ); -it.live( +it.effect( 'Knip coverage is mandatory and findings count categories rather than files', Effect.fn(function* testEffect4() { - const findings = yield* stringify({ issues: [] }); - const coverage = yield* stringify({ + const findings = yield* encodeReport({ issues: [] }); + const coverage = yield* encodeReport({ coverage: { processed: 12, total: 12 }, findingCounts: { exports: 4, files: 2 }, workspaces: ['.'], }); - expect(yield* validate('knip', `${findings}\n${coverage}`)).toEqual({ + expect(yield* validateReport('knip', `${findings}\n${coverage}`)).toEqual({ coverage: { findingCounts: { exports: 4, files: 2 }, processed: 12, @@ -131,29 +104,19 @@ it.live( files: 12, findings: 6, }); - yield* validate('knip', findings).pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/coverage records/u), - ), - ); - yield* validate( - 'knip', - `${findings}\n${yield* stringify({ coverage: { processed: 0, total: 0 }, findingCounts: {}, workspaces: ['.'] })}`, - ).pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/no files/u), + 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); }), ); -it.live( +it.effect( 'Fallow rejects missing discovery, unsupported schema and incomplete workspaces', Effect.fn(function* testEffect5() { const report = { @@ -163,43 +126,31 @@ it.live( stats: { clone_groups: 0, total_files: 2 }, version: '3.22.0', }; - expect(yield* validate(FALLOW_CLONES, yield* stringify(report))).toEqual({ + expect(yield* validateReport(FALLOW_CLONES, yield* encodeReport(report))).toEqual({ coverage: { tokenEligibleFiles: 2 }, files: 2, findings: 0, }); - yield* validate(FALLOW_CLONES, yield* stringify({ ...report, schema_version: 10 })).pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/Malformed analyzer report/u), - ), + const unsupportedSchemaError = yield* Effect.flip( + validateReport(FALLOW_CLONES, yield* encodeReport({ ...report, schema_version: 10 })), ); - yield* validate( - FALLOW_CLONES, - yield* stringify({ - ...report, - workspace_diagnostics: [ - { kind: 'invalid-package-json', message: 'invalid package', path: 'packages/broken' }, - ], - }), - ).pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/incomplete workspace/u), + 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' }, + ], + }), ), ); - yield* validate('fallow-files', yield* stringify({ file_count: 2, files: ['a.ts'] })).pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/count disagrees/u), - ), + 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); }), ); @@ -216,7 +167,10 @@ it('tool selection preserves the complete Fallow group', () => { const createFixture = () => Effect.gen(function* testEffect6() { - const root = mkdtempSync(path.join(tmpdir(), 'ontos-quality-test-')); + 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')); @@ -224,7 +178,7 @@ const createFixture = () => symlinkSync(path.join(appRoot, 'node_modules'), path.join(root, 'node_modules'), 'dir'); writeFileSync( path.join(root, 'package.json'), - yield* stringify({ name: 'quality-test', private: true, type: 'module' }), + yield* encodeReport({ name: 'quality-test', private: true, type: 'module' }), ); for (const name of ['scope.json', 'fallow.json', 'jscpd.json', 'knip-reporter.mts']) { copyFileSync( @@ -234,7 +188,7 @@ const createFixture = () => } writeFileSync( path.join(root, KNIP_CONFIG), - yield* stringify({ + yield* encodeReport({ entry: ['scripts/index.ts'], lefthook: false, node: false, @@ -255,9 +209,7 @@ const createFixture = () => }); const runFixture = (root: string, output: string, tool: 'all' | 'knip' | 'jscpd' | 'fallow') => - Effect.gen(function* testEffect7() { - return yield* runQualityAudit(root, output, tool).pipe(Effect.provide(NodeServices.layer)); - }); + runQualityAudit(root, output, tool).pipe(Effect.provide(NodeServices.layer)); const SummarySchema = Schema.Struct({ mode: Schema.Literal('report-only'), @@ -276,11 +228,9 @@ const SummarySchema = Schema.Struct({ status: Schema.String, }); const summary = (output: string) => - Effect.gen(function* testEffect8() { - return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(SummarySchema))( - readFileSync(path.join(output, 'summary.json'), 'utf-8'), - ); - }); + Schema.decodeUnknownEffect(Schema.fromJsonString(SummarySchema))( + readFileSync(path.join(output, 'summary.json'), 'utf-8'), + ); it.live( 'real Fallow separates UI penalties from control-flow complexity without hiding branches', @@ -292,10 +242,9 @@ it.live( { length: 11 }, (_, index) => `if (value === ${index + 1}) return ${index + 1};`, ).join('\n'); - try { - writeFileSync( - path.join(root, 'scripts/metric-example.tsx'), - `import { useState } from 'react'; + writeFileSync( + path.join(root, 'scripts/metric-example.tsx'), + `import { useState } from 'react'; export function Panel({ ${props} }: Record) { useState('one'); useState('two'); @@ -307,61 +256,46 @@ export function branchHeavy(value: number) { return 0; } `, - ); - yield* runFixture(root, output, 'fallow'); - const result = yield* summary(output); - const healthDirectory = path.join(result.runDirectory, FALLOW_HEALTH); - 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')); - const panel = rows.find((row) => row.name === 'Panel'); - const branchHeavy = rows.find((row) => row.name === 'branchHeavy'); - 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`, - ); - expect(corrupted).not.toBe(raw); - yield* validate(FALLOW_HEALTH, corrupted).pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/contributions disagree/u), - ), - ); - const wrongCount = raw.replace( - /"functions_above_threshold"\s*:\s*\d+/u, - '"functions_above_threshold": 0', - ); - yield* validate(FALLOW_HEALTH, wrongCount).pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/count disagrees/u), + ); + yield* runFixture(root, output, 'fallow'); + const result = yield* summary(output); + const healthDirectory = path.join(result.runDirectory, FALLOW_HEALTH); + const rows = yield* Schema.decodeUnknownEffect( + Schema.fromJsonString( + Schema.Array( + Schema.Struct({ + controlFlowCognitive: Schema.Number, + exceedsControlFlowLimits: Schema.Boolean, + name: Schema.String, + weightedCognitive: Schema.Number, + }), ), - ); - } finally { - rmSync(root, { force: true, recursive: true }); + ), + )(readFileSync(path.join(healthDirectory, 'complexity.json'), 'utf-8')); + const panel = rows.find((row) => row.name === 'Panel'); + const branchHeavy = rows.find((row) => row.name === 'branchHeavy'); + 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`, + ); + 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', + ); + const thresholdCountError = yield* Effect.flip(validateReport(FALLOW_HEALTH, wrongCount)); + expect(thresholdCountError.message).toMatch(/count disagrees/u); }), ); @@ -371,65 +305,59 @@ it.live( const root = yield* createFixture(); const output = path.join(root, REPORT_DIRECTORY); const policyFiles = ['policy-read.ts', 'policy-write.ts']; - try { - for (const [index, file] of policyFiles.entries()) { - const policy = Array.from( - { length: 16 }, - (_, field) => - `decision${field}: subject === '${index === 0 ? 'role' : 'admin'}-${field}' ? '${index === 0 ? 'allow' : 'audit'}-${field}' : '${index === 0 ? 'deny' : 'defer'}-${field}'`, - ).join(',\n'); - writeFileSync( - path.join(root, 'scripts', file), - `export function selectPolicy(subject: string) {\nreturn {\n${policy}\n};\n}\n`, - ); - } - yield* runFixture(root, output, 'all'); - const result = yield* summary(output); - const schema = Schema.fromJsonString( - Schema.Struct({ - clone_groups: Schema.Array( - Schema.Struct({ instances: Schema.Array(Schema.Struct({ file: Schema.String })) }), - ), - }), - ); - 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) => - includesPolicyFiles(group.instances, policyFiles), - ); - expect(matchesDistinctPolicies, name).toBe(name === FALLOW_SIMILARITY); - }), - ), - { concurrency: 'unbounded' }, + for (const [index, file] of policyFiles.entries()) { + const policy = Array.from( + { length: 16 }, + (_, field) => + `decision${field}: subject === '${index === 0 ? 'role' : 'admin'}-${field}' ? '${index === 0 ? 'allow' : 'audit'}-${field}' : '${index === 0 ? 'deny' : 'defer'}-${field}'`, + ).join(',\n'); + writeFileSync( + path.join(root, 'scripts', file), + `export function selectPolicy(subject: string) {\nreturn {\n${policy}\n};\n}\n`, ); - 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 }), - }), - ), - }), + } + yield* runFixture(root, output, 'all'); + const result = yield* summary(output); + const schema = Schema.fromJsonString( + Schema.Struct({ + clone_groups: Schema.Array( + Schema.Struct({ instances: Schema.Array(Schema.Struct({ file: 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, - ), + }), + ); + 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) => + includesPolicyFiles(group.instances, policyFiles), + ); + expect(matchesDistinctPolicies, name).toBe(name === FALLOW_SIMILARITY); + }), + ), + { concurrency: 'unbounded' }, + ); + 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), ), - ).toBe(false); - } finally { - rmSync(root, { force: true, recursive: true }); - } + ), + ).toBe(false); }), ); @@ -438,44 +366,34 @@ it.live( Effect.fn(function* testEffect12() { const root = yield* createFixture(); const output = path.join(root, REPORT_DIRECTORY); - try { - 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]) { - 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'); - yield* runFixture(root, output, 'knip'); - writeFileSync(path.join(root, KNIP_CONFIG), '{invalid'); - yield* runFixture(root, output, 'knip').pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/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, - ); + 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]) { expect( - readFileSync(path.join(first.runDirectory, 'knip/report.ndjson'), 'utf-8').length > 0, + first.results.some((row) => row.name === name && row.findings > 0), + `${name} must report injected debt`, ).toBe(true); - } finally { - rmSync(root, { force: true, recursive: true }); } + writeFileSync(path.join(root, 'quality-audit/jscpd.json'), '{invalid unrelated config'); + yield* runFixture(root, output, 'knip'); + writeFileSync(path.join(root, KNIP_CONFIG), '{invalid'); + 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, + ); + expect( + readFileSync(path.join(first.runDirectory, 'knip/report.ndjson'), 'utf-8').length > 0, + ).toBe(true); }), ); @@ -484,66 +402,50 @@ it.live( Effect.fn(function* testEffect13() { const root = yield* createFixture(); const output = path.join(root, REPORT_DIRECTORY); - try { - rmSync(path.join(root, 'node_modules')); - yield* runFixture(root, output, 'knip').pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/analysis failed/u), - ), - ); - const missing = yield* summary(output); - expect(missing.status).toBe('error'); - const failedDirectory = path.join(missing.runDirectory, 'knip'); - expect(missing.results).toEqual([ - { - coverage: {}, - diagnostic: readFileSync( - path.join(failedDirectory, 'validation-error.txt'), - 'utf-8', - ).trimEnd(), - directory: failedDirectory, - files: 0, - findings: 0, - name: 'knip', - status: 'error', - }, - ]); - 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'), - yield* stringify({ exclude: [], patterns: ['absent/**/*.ts'] }), - ); - yield* runFixture(root, output, 'jscpd').pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/analysis failed/u), - ), - ); - const empty = yield* summary(output); - expect(empty.results).toEqual([ - { - coverage: {}, - diagnostic: 'QualityAuditError: Source inventory: analysis contains no files', - directory: empty.runDirectory, - files: 0, - findings: 0, - name: 'setup', - status: 'error', - }, - ]); - expect(readFileSync(path.join(output, 'summary.json'), 'utf-8')).toMatch( - /Source inventory: analysis contains no files/u, - ); - } finally { - rmSync(root, { force: true, recursive: true }); - } + rmSync(path.join(root, 'node_modules')); + 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'); + expect(missing.results).toEqual([ + { + coverage: {}, + diagnostic: readFileSync( + path.join(failedDirectory, 'validation-error.txt'), + 'utf-8', + ).trimEnd(), + directory: failedDirectory, + files: 0, + findings: 0, + name: 'knip', + status: 'error', + }, + ]); + 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'), + yield* encodeReport({ exclude: [], patterns: ['absent/**/*.ts'] }), + ); + 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', + directory: empty.runDirectory, + files: 0, + findings: 0, + name: 'setup', + status: 'error', + }, + ]); + expect(readFileSync(path.join(output, 'summary.json'), 'utf-8')).toMatch( + /Source inventory: analysis contains no files/u, + ); }), ); @@ -552,48 +454,38 @@ it.live( Effect.fn(function* testEffect14() { const root = yield* createFixture(); const output = path.join(root, REPORT_DIRECTORY); - try { - yield* Effect.gen(function* initializeFixtureRepository() { - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - yield* spawner.string(ChildProcess.make('git', ['init', '-q', root])); - }).pipe(Effect.provide(NodeServices.layer)); - const executable = path.join(root, 'scripts/quality audit.mts'); - copyFileSync(path.join(appRoot, 'scripts/quality-audit.mts'), executable); - for (const file of ['knip-model.mts', 'knip-runtime-model.mts']) { - copyFileSync( - path.join(appRoot, CONFIG_DIRECTORY, file), - path.join(root, CONFIG_DIRECTORY, file), - ); - } - const result = spawnSync( - process.execPath, - [executable, '--tool', 'knip', '--output', output], - { - cwd: tmpdir(), - encoding: 'utf-8', - timeout: 60_000, - }, - ); - expect( - result.error, - `CLI spawn failed: ${String(result.error)}\n${result.stdout}\n${result.stderr}`, - ).toBe(undefined); - expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); - const report = yield* summary(output); - expect(report.status).toBe('reported'); - const provenance = yield* Schema.decodeUnknownEffect(ProvenanceSchema)( - readFileSync(path.join(report.runDirectory, 'provenance.json'), 'utf-8'), - ); - 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, + yield* Effect.gen(function* initializeFixtureRepository() { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + yield* spawner.string(ChildProcess.make('git', ['init', '-q', root])); + }).pipe(Effect.provide(NodeServices.layer)); + const executable = path.join(root, 'scripts/quality audit.mts'); + copyFileSync(path.join(appRoot, 'scripts/quality-audit.mts'), executable); + for (const file of ['knip-model.mts', 'knip-runtime-model.mts']) { + copyFileSync( + path.join(appRoot, CONFIG_DIRECTORY, file), + path.join(root, CONFIG_DIRECTORY, file), ); - } finally { - rmSync(root, { force: true, recursive: true }); } + const result = spawnSync(process.execPath, [executable, '--tool', 'knip', '--output', output], { + cwd: tmpdir(), + encoding: 'utf-8', + timeout: 60_000, + }); + expect( + result.error, + `CLI spawn failed: ${String(result.error)}\n${result.stdout}\n${result.stderr}`, + ).toBe(undefined); + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + const report = yield* summary(output); + expect(report.status).toBe('reported'); + const provenance = yield* Schema.decodeUnknownEffect(ProvenanceSchema)( + readFileSync(path.join(report.runDirectory, 'provenance.json'), 'utf-8'), + ); + 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, + ); }), ); @@ -602,25 +494,15 @@ it.live( Effect.fn(function* testEffect15() { const root = yield* createFixture(); const output = path.join(root, 'scripts/reports'); - try { - yield* runFixture(root, output, 'all').pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/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, - ); - expect(readdirSync(report.runDirectory)).toEqual([]); - expect(readdirSync(path.join(root, '.codex'))).toEqual([CALLER_OWNED_FILE]); - } finally { - rmSync(root, { force: true, recursive: true }); - } + 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, + ); + expect(readdirSync(report.runDirectory)).toEqual([]); + expect(readdirSync(path.join(root, '.codex'))).toEqual([CALLER_OWNED_FILE]); }), ); @@ -631,34 +513,24 @@ it.live( const output = path.join(root, 'reports-link'); const sourceOutput = path.join(root, 'scripts/reports'); const safeOutput = path.join(root, REPORT_DIRECTORY); - try { - mkdirSync(sourceOutput); - symlinkSync(sourceOutput, output, 'dir'); - yield* runFixture(root, output, 'all').pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/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, - ); - expect(readdirSync(rejected.runDirectory)).toEqual([]); - expect(readdirSync(path.join(root, '.codex'))).toEqual([CALLER_OWNED_FILE]); + mkdirSync(sourceOutput); + symlinkSync(sourceOutput, output, 'dir'); + 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, + ); + expect(readdirSync(rejected.runDirectory)).toEqual([]); + expect(readdirSync(path.join(root, '.codex'))).toEqual([CALLER_OWNED_FILE]); - rmSync(output); - mkdirSync(safeOutput); - symlinkSync(safeOutput, output, 'dir'); - yield* runFixture(root, output, 'jscpd'); - const accepted = yield* summary(output); - expect(accepted.status).toBe('reported'); - } finally { - rmSync(root, { force: true, recursive: true }); - } + rmSync(output); + mkdirSync(safeOutput); + symlinkSync(safeOutput, output, 'dir'); + yield* runFixture(root, output, 'jscpd'); + const accepted = yield* summary(output); + expect(accepted.status).toBe('reported'); }), ); @@ -667,46 +539,42 @@ it.live( Effect.fn(function* testEffect17() { const root = yield* createFixture(); const output = path.join(root, REPORT_DIRECTORY); - try { - writeFileSync(path.join(root, '.gitignore'), 'node_modules\n.codex\n'); - yield* Effect.gen(function* commitFixture() { - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const commands = [ - ['init', '-q'], - ['add', '.gitignore', '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'), - ); - expect(provenance.sourceState).toBe('clean'); - expect(provenance.workingTreeChanges).toEqual([]); - } finally { - rmSync(root, { force: true, recursive: true }); - } + writeFileSync(path.join(root, '.gitignore'), 'node_modules\n.codex\n'); + yield* Effect.gen(function* commitFixture() { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const commands = [ + ['init', '-q'], + ['add', '.gitignore', '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'), + ); + expect(provenance.sourceState).toBe('clean'); + expect(provenance.workingTreeChanges).toEqual([]); }), ); @@ -715,51 +583,33 @@ it.live( Effect.fn(function* testEffect18() { const root = yield* createFixture(); const output = path.join(root, REPORT_DIRECTORY); - try { - mkdirSync(path.join(root, 'packages/omitted'), { recursive: true }); - writeFileSync( - path.join(root, 'packages/omitted/package.json'), - yield* stringify({ name: 'omitted', private: true }), - ); - yield* runFixture(root, output, 'knip').pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/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'), - yield* stringify({ ignorePatterns: ['scripts/**', 'node_modules/**', 'packages/**'] }), - ); - yield* runFixture(root, output, 'fallow').pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/analysis failed/u), - ), - ); - const omitted = yield* summary(output); - expect( - omitted.results.some((result) => result.name === 'coverage' && result.status === 'error'), - ).toBe(true); - expect(readFileSync(path.join(omitted.runDirectory, 'coverage.json'), 'utf-8')).toMatch( - /scripts\/index.ts/u, - ); - } finally { - rmSync(root, { force: true, recursive: true }); - } + mkdirSync(path.join(root, 'packages/omitted'), { recursive: true }); + writeFileSync( + path.join(root, 'packages/omitted/package.json'), + 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'), + yield* encodeReport({ ignorePatterns: ['scripts/**', 'node_modules/**', 'packages/**'] }), + ); + 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'), + ).toBe(true); + expect(readFileSync(path.join(omitted.runDirectory, 'coverage.json'), 'utf-8')).toMatch( + /scripts\/index.ts/u, + ); }), ); @@ -768,30 +618,20 @@ it.live( Effect.fn(function* testEffect19() { const root = yield* createFixture(); const output = path.join(root, REPORT_DIRECTORY); - try { - // 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'), - yield* stringify({ version: '0.0.0' }), - ); - yield* runFixture(root, output, 'knip').pipe( - Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(/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, - ); - expect(readFileSync(path.join(mismatch.runDirectory, 'knip/stdout.txt'), 'utf-8')).toBe(''); - } finally { - rmSync(root, { force: true, recursive: true }); - } + // 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'), + yield* encodeReport({ version: '0.0.0' }), + ); + 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, + ); + expect(readFileSync(path.join(mismatch.runDirectory, 'knip/stdout.txt'), 'utf-8')).toBe(''); }), ); diff --git a/app/scripts/tests/typecheck-project-references.test.mts b/app/scripts/tests/typecheck-project-references.test.mts index 816b75d27..f0397bf2b 100644 --- a/app/scripts/tests/typecheck-project-references.test.mts +++ b/app/scripts/tests/typecheck-project-references.test.mts @@ -98,95 +98,93 @@ it.live( it.live( 'Drizzle consumer surface compiles in both ESM and CommonJS projects', Effect.fn(function* testEffect3() { - const fixture = mkdtempSync(path.join(os.tmpdir(), 'ontos-drizzle-declarations-')); - try { - symlinkSync( - path.join(workspaceRoot, 'node_modules'), - path.join(fixture, 'node_modules'), - 'dir', - ); - writeFileSync(path.join(fixture, packageJsonFile), '{"private":true,"type":"module"}\n'); + 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'), + 'dir', + ); + writeFileSync(path.join(fixture, packageJsonFile), '{"private":true,"type":"module"}\n'); + writeFileSync( + path.join(fixture, tsconfigFile), + JSON.stringify({ + compilerOptions: { + exactOptionalPropertyTypes: true, + module: 'NodeNext', + moduleResolution: 'NodeNext', + noEmit: true, + skipLibCheck: true, + strict: true, + target: 'ESNext', + types: ['node'], + }, + files: ['./consumer.mts', './consumer.cts'], + }), + ); + for (const extension of ['mts', 'cts']) { writeFileSync( - path.join(fixture, tsconfigFile), - JSON.stringify({ - compilerOptions: { - exactOptionalPropertyTypes: true, - module: 'NodeNext', - moduleResolution: 'NodeNext', - noEmit: true, - skipLibCheck: true, - strict: true, - target: 'ESNext', - types: ['node'], - }, - files: ['./consumer.mts', './consumer.cts'], - }), + path.join(fixture, `consumer.${extension}`), + 'import { pgTable, uuid } from "drizzle-orm/pg-core";\n' + + 'export const fixtureTable = pgTable("declaration_fixture", { id: uuid("id") });\n', ); - for (const extension of ['mts', 'cts']) { - writeFileSync( - path.join(fixture, `consumer.${extension}`), - 'import { pgTable, uuid } from "drizzle-orm/pg-core";\n' + - 'export const fixtureTable = pgTable("declaration_fixture", { id: uuid("id") });\n', - ); - } - const result = yield* runTypecheck(fixture, ['--project', tsconfigFile]); - expect(result.status, result.stdout + result.stderr).toBe(0); - } finally { - rmSync(fixture, { force: true, recursive: true }); } + const result = yield* runTypecheck(fixture, ['--project', tsconfigFile]); + expect(result.status, result.stdout + result.stderr).toBe(0); }), ); it.live( 'root typecheck checks referenced projects and rejects a newly introduced type error', Effect.fn(function* testEffect4() { - const fixture = mkdtempSync(path.join(os.tmpdir(), 'ontos-typecheck-references-')); - try { - mkdirSync(path.join(fixture, 'referenced')); - symlinkSync( - path.join(workspaceRoot, 'node_modules'), - path.join(fixture, 'node_modules'), - 'dir', - ); - writeFileSync(path.join(fixture, packageJsonFile), '{"private":true,"type":"module"}\n'); - writeFileSync( - path.join(fixture, tsconfigFile), - JSON.stringify({ files: [], references: [{ path: './referenced' }] }), - ); - writeFileSync( - path.join(fixture, 'referenced/tsconfig.json'), - JSON.stringify({ - compilerOptions: { - composite: true, - declaration: true, - emitDeclarationOnly: true, - outDir: './output', - strict: true, - types: [], - }, - files: ['./index.ts'], - }), - ); - const sourceFile = path.join(fixture, 'referenced/index.ts'); - writeFileSync(sourceFile, 'export const referenceGateFixture: number = 1;\n'); - const [runtime, wrapper, ...args] = packageJson.scripts.typecheck.split(' '); - 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 = 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); - } finally { - rmSync(fixture, { force: true, recursive: true }); - } + 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'), + path.join(fixture, 'node_modules'), + 'dir', + ); + writeFileSync(path.join(fixture, packageJsonFile), '{"private":true,"type":"module"}\n'); + writeFileSync( + path.join(fixture, tsconfigFile), + JSON.stringify({ files: [], references: [{ path: './referenced' }] }), + ); + writeFileSync( + path.join(fixture, 'referenced/tsconfig.json'), + JSON.stringify({ + compilerOptions: { + composite: true, + declaration: true, + emitDeclarationOnly: true, + outDir: './output', + strict: true, + types: [], + }, + files: ['./index.ts'], + }), + ); + const sourceFile = path.join(fixture, 'referenced/index.ts'); + writeFileSync(sourceFile, 'export const referenceGateFixture: number = 1;\n'); + const [runtime, wrapper, ...args] = packageJson.scripts.typecheck.split(' '); + 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 = 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/tools/oxlint/effect-native/README.md b/app/tools/oxlint/effect-native/README.md index aa545354d..a36bedf0e 100644 --- a/app/tools/oxlint/effect-native/README.md +++ b/app/tools/oxlint/effect-native/README.md @@ -153,7 +153,7 @@ Effect re-entry is diagnosed by `no-nested-effect-run`. Additional rule options `effectModules`, and `effectModuleSources`; there is no fixer or suggestion. The workspace import policy rejects `node:test`, `node:assert`, `node:assert/strict`, -`@rstest/core`, and the retired `@app/core-runtime/testing/effect-runtime` in application, +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/repository-policy.config.ts b/app/tools/oxlint/effect-native/repository-policy.config.ts index abe01e40c..7809ede16 100644 --- a/app/tools/oxlint/effect-native/repository-policy.config.ts +++ b/app/tools/oxlint/effect-native/repository-policy.config.ts @@ -1,3 +1,4 @@ +import { testRestrictedImports } from './shared/test-restricted-imports.ts'; import { defineConfig } from 'oxlint'; /** Repository policies also cover tooling tests and root configuration files. */ @@ -23,21 +24,7 @@ export default defineConfig({ 'eslint/no-restricted-imports': [ 'error', { - paths: [ - { message: 'Import test APIs from @app/effect-rstest instead.', name: 'node:test' }, - { - message: 'Import assertions from @app/effect-rstest instead.', - name: 'node:assert', - }, - { - message: 'Import assertions from @app/effect-rstest instead.', - name: 'node:assert/strict', - }, - { - message: 'Import test APIs from @app/effect-rstest instead.', - name: '@rstest/core', - }, - ], + paths: testRestrictedImports, }, ], }, 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 e1e1b778b..2ebe5f1a2 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 @@ -93,6 +93,7 @@ const DEFAULT_EFFECT_MODULE_SOURCES: readonly string[] = [ 'effect', 'effect/**', '@modern-js/plugin-bff/effect-edge', + '@modern-js/plugin-bff/effect-client', ]; interface RuleOptions { 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 9b75f291f..3ad896cb4 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 @@ -8,8 +8,9 @@ * 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'; @@ -22,7 +23,13 @@ import { globToRegExp, isTestFile, normalisePath } from '../shared/paths.ts'; /** Fixture files live at `tools/oxlint//tests/fixtures//{valid,invalid}/`. */ const FIXTURE_PREFIX = /^tools\/oxlint\/[^/]+\/tests\/fixtures\/[^/]+\/(?:valid|invalid)\//u; -const DEFAULT_INCLUDE = ['apps/**', 'verticals/**', 'packages/**', 'scripts/**']; +const DEFAULT_INCLUDE = [ + 'apps/**', + 'verticals/**', + 'packages/**', + 'scripts/**', + 'tools/**/tests/**', +]; const DEFAULT_IGNORE = [ '**/dist/**', '**/build/**', @@ -332,6 +339,24 @@ export const rule = defineRule({ if (source === 'effect/Effect') return `effect:Effect${name ? `.${name}` : ''}`; return `${source}:${name ?? '*'}`; } + if (def.type === 'Parameter' && def.node.params?.[0] === def.name) { + let callback = def.node; + while (callback.parent && wrappers.has(callback.parent.type)) callback = callback.parent; + const registration = callback.parent; + if ( + registration?.type === 'CallExpression' && + registration.arguments.includes(callback) + ) { + let factory = unwrap(registration.callee); + while (factory?.type === 'CallExpression') factory = unwrap(factory.callee); + if ( + /^@app\/effect-rstest:(?:\*\.)?(?:(?:it|test)\.)?layer$/u.test( + imported(factory, seen) ?? '', + ) + ) + return '@app/effect-rstest:it'; + } + } if ( def.type === 'Variable' && def.parent?.kind === 'const' && @@ -458,6 +483,21 @@ 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; + let current = node; + while (current.parent && wrappers.has(current.parent.type)) current = current.parent; + const call = current.parent; + if (call?.type !== 'CallExpression' || !call.arguments.includes(current)) return false; + let callee = call.callee; + // Parameterized registrations, e.g. test.each(rows)(name, callback). + while (callee.type === 'CallExpression') callee = callee.callee; + return /^(?:@app\/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; @@ -659,6 +699,79 @@ export const rule = defineRule({ return resolve(annotation); }; + /** 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 ( + node.async === true || + promiseReference(node.returnType) !== null || + returnsKnownPromise(node.body, seen) + ); + if (node.type === 'Identifier') { + const variable = variableFor(node, node.name); + return (variable?.defs ?? []).some( + (def: any) => + def.type === 'Variable' && + def.parent?.kind === 'const' && + !FUNCTION_TYPES.has(unwrap(def.node.init)?.type) && + !variable.references.some((ref: any) => ref.isWrite() && !ref.init) && + returnsKnownPromise(def.node.init, seen), + ); + } + if (node.type === 'BlockStatement') { + 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 visit(node); + } + if (node.type === 'ConditionalExpression') + return ( + returnsKnownPromise(node.consequent, new Set(seen)) || + returnsKnownPromise(node.alternate, new Set(seen)) + ); + if (node.type === 'CallExpression' || node.type === 'NewExpression') { + const segments = memberSegments(unwrap(node.callee)); + const globalSegments = segments?.[0] === 'globalThis' ? segments.slice(1) : segments; + if ( + segments && + !variableFor(node.callee, segments[0]!)?.defs.length && + globalSegments?.[0] === 'Promise' + ) { + if (node.type === 'NewExpression' && globalSegments.length === 1) return true; + if ( + node.type === 'CallExpression' && + globalSegments.length === 2 && + ['resolve', 'reject', 'all', 'allSettled', 'any', 'race'].includes(globalSegments[1]!) + ) + return true; + } + if (node.type === 'CallExpression') { + if (/^effect:(?:root\.)?Effect\.runPromise(?:Exit)?$/u.test(imported(node.callee) ?? '')) + return true; + if (node.callee.type === 'Identifier') { + const variable = variableFor(node.callee, node.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) + ); + }); + } + } + } + return false; + }; + /** Human-readable name for the reported member, used in the message. */ const memberName = (node: AnyNode): string => { const key = (node as { readonly key?: ESTree.Node | null }).key ?? null; @@ -901,7 +1014,16 @@ export const rule = defineRule({ }; const wrapper = promiseReference(fn.returnType); const isAsync = fn.async === true; - if (!isAsync && wrapper === null) return; + const ownedTest = atOwnedTestCallback(node); + if (!isAsync && wrapper === null && !(ownedTest && returnsKnownPromise(node))) return; + if (ownedTest) { + reportedFunctions.add(node.start); + report(node as ESTree.Node, isAsync ? 'asyncPort' : 'promiseReturningImplementation', { + member: 'test callback', + wrapper: wrapper ?? 'Promise', + }); + return; + } if (node.type === 'FunctionDeclaration') { if (!options.includeFunctionDeclarations) return; if (!isModuleScopeFunction(node)) return; 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..6fd158089 --- /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 @app/effect-rstest instead.', name: 'node:test' }, + { + message: 'Import assertions from @app/effect-rstest instead.', + name: 'node:assert', + }, + { + message: 'Import assertions from @app/effect-rstest instead.', + name: 'node:assert/strict', + }, + { + message: 'Import test APIs from @app/effect-rstest instead.', + name: '@rstest/core', + }, +]; 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/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-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..53f951918 --- /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 '@app/effect-rstest'; +import * as suite from '@app/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/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..778373a0d --- /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 '@app/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..a6f550855 --- /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 '@app/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/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..073ad05b6 --- /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 '@app/effect-rstest'; + +it('owned tooling callback', async () => {}); 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..1e9151462 --- /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 '@app/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/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..5b9e1eaac --- /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 '@app/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/registration.test.mts b/app/tools/oxlint/effect-native/tests/registration.test.mts index b5a8e5eef..8c40d075a 100644 --- a/app/tools/oxlint/effect-native/tests/registration.test.mts +++ b/app/tools/oxlint/effect-native/tests/registration.test.mts @@ -1,12 +1,13 @@ import { expect, it } from '@app/effect-rstest'; import { Schema } from 'effect'; -import { readFileSync } from 'node:fs'; +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import nodePath from 'node:path'; import { pathToFileURL } from 'node:url'; 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 RuleSetting = Schema.Union([Schema.String, Schema.Array(Schema.Unknown)]); // The production config is typed against oxlint's own definitions; only the fields this suite @@ -21,6 +22,13 @@ const ProductionConfigModule = Schema.Struct({ 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)), }), }); @@ -98,3 +106,26 @@ it('fixture configs enable only their owned rule without file-ignore shortcuts', ).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/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/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 84a90cb4f..45152d25c 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 @@ -128,7 +128,7 @@ const endpointNames = [ ] as const; const makeAssertion = (audience = 'party-registry') => - Effect.gen(function* testProgram1() { + Effect.gen(function* signPrincipalAssertions() { const { privateKey, publicKey } = yield* Effect.promise(() => generateKeyPair('Ed25519')); const publicJwk = { ...(yield* Effect.promise(() => exportJWK(publicKey))), @@ -211,6 +211,16 @@ const mounted = ( ); }; +const mountApp = ( + harness: Effect.Success>, + environment: Readonly>, + readRuntime?: ReadRuntimeService, +) => + Effect.acquireRelease( + Effect.sync(() => mounted(harness, environment, readRuntime)), + (app) => Effect.promise(() => app.dispose()).pipe(Effect.orDie), + ); + // 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()); @@ -276,13 +286,10 @@ const commandRequest = ( it.live( 'every registered command is mounted and rejects missing structural input or authentication before the lifecycle', () => - Effect.gen(function* testProgram2() { + Effect.gen(function* rejectInvalidMountedCommands() { const assertion = yield* makeAssertion(); const harness = yield* makeActionTestHarness(); - const app = yield* Effect.acquireRelease( - Effect.sync(() => mounted(harness, assertion.environment)), - (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), - ); + const app = yield* mountApp(harness, assertion.environment); expect(Object.keys(partyRegistryApi.groups.partyCommands.endpoints).length).toBe(24); expect(Object.keys(partyRegistryApi.groups.partyCommands.endpoints).toSorted()).toEqual( @@ -296,7 +303,7 @@ it.live( yield* forEachSequential( Object.values(partyRegistryApi.groups.partyCommands.endpoints), (endpoint) => - Effect.gen(function* testProgram3() { + Effect.gen(function* rejectInvalidEndpointRequest() { const response = yield* handle( app, new Request(`https://party.ontos.test${endpoint.path}`, { @@ -350,17 +357,14 @@ it.live( 'missing, malformed, and wrong-audience assertions are challenged without creating invocations', () => forEachSequential(['party-registry', 'contacts'], (audience) => - Effect.gen(function* testProgram5() { + Effect.gen(function* challengeInvalidAudienceAssertions() { const assertion = yield* makeAssertion(audience); const harness = yield* makeActionTestHarness(); - const app = yield* Effect.acquireRelease( - Effect.sync(() => mounted(harness, assertion.environment)), - (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), - ); + const app = yield* mountApp(harness, assertion.environment); const tokens = audience === 'contacts' ? [assertion.token] : [undefined, 'not-a-jwt']; yield* forEachSequential(tokens, (token) => - Effect.gen(function* testProgram6() { + Effect.gen(function* challengeInvalidToken() { const response = yield* handle( app, commandRequest('request-search-rebuild', {}, token, { @@ -390,13 +394,10 @@ it.live( it.live( 'verification configuration unavailability is retryable and never reaches the lifecycle', () => - Effect.gen(function* testProgram7() { + Effect.gen(function* reportUnavailableVerificationConfiguration() { const assertion = yield* makeAssertion(); const harness = yield* makeActionTestHarness(); - const app = yield* Effect.acquireRelease( - Effect.sync(() => mounted(harness, {})), - (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), - ); + const app = yield* mountApp(harness, {}); const response = yield* handle( app, @@ -414,13 +415,10 @@ it.live( ); it.live('correlation and idempotency are mandatory before the Core Action lifecycle', () => - Effect.gen(function* testProgram8() { + Effect.gen(function* requireCorrelationAndIdempotency() { const assertion = yield* makeAssertion(); const harness = yield* makeActionTestHarness(); - const app = yield* Effect.acquireRelease( - Effect.sync(() => mounted(harness, assertion.environment)), - (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), - ); + const app = yield* mountApp(harness, assertion.environment); const missingKey = yield* handle( app, @@ -448,16 +446,13 @@ it.live('correlation and idempotency are mandatory before the Core Action lifecy ); it.live('real Core permission denial is a durable 403 and does not execute the command', () => - Effect.gen(function* testProgram9() { + Effect.gen(function* persistPermissionDenial() { const assertion = yield* makeAssertion(); const harness = yield* makeActionTestHarness({ actionPermission: 'denied', tenantPermission: 'allowed', }); - const app = yield* Effect.acquireRelease( - Effect.sync(() => mounted(harness, assertion.environment)), - (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), - ); + const app = yield* mountApp(harness, assertion.environment); const response = yield* handle( app, @@ -476,7 +471,7 @@ it.live('real Core permission denial is a durable 403 and does not execute the c it.live( 'the real handler translates domain conflicts and rolls back without successful evidence', () => - Effect.gen(function* testProgram10() { + Effect.gen(function* rollBackDomainConflict() { const assertion = yield* makeAssertion(); const harness = yield* makeActionTestHarness({ actionPermission: 'allowed', @@ -487,10 +482,7 @@ it.live( }), ], }); - const app = yield* Effect.acquireRelease( - Effect.sync(() => mounted(harness, assertion.environment)), - (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), - ); + const app = yield* mountApp(harness, assertion.environment); const response = yield* handle( app, @@ -508,7 +500,7 @@ it.live( ); it.live('alias conflicts preserve only safe canonical recovery metadata', () => - Effect.gen(function* testProgram11() { + Effect.gen(function* preserveSafeAliasRecoveryMetadata() { const assertion = yield* makeAssertion(); const canonicalPartyRef = { ...partyRef, resourceId: 'a4000000-0000-4000-8000-000000000002' }; const harness = yield* makeActionTestHarness({ @@ -528,10 +520,7 @@ it.live('alias conflicts preserve only safe canonical recovery metadata', () => }), ], }); - const app = yield* Effect.acquireRelease( - Effect.sync(() => mounted(harness, assertion.environment)), - (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), - ); + const app = yield* mountApp(harness, assertion.environment); const response = yield* handle( app, @@ -554,16 +543,13 @@ it.live('alias conflicts preserve only safe canonical recovery metadata', () => ); it.live('committed request replay stays a terminal 409 and does not execute or emit twice', () => - Effect.gen(function* testProgram12() { + Effect.gen(function* rejectCommittedCommandReplay() { const assertion = yield* makeAssertion(); const harness = yield* makeActionTestHarness({ actionPermission: 'allowed', tenantPermission: 'allowed', }); - const app = yield* Effect.acquireRelease( - Effect.sync(() => mounted(harness, assertion.environment)), - (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), - ); + const app = yield* mountApp(harness, assertion.environment); const first = yield* handle( app, @@ -597,7 +583,7 @@ it.live('committed request replay stays a terminal 409 and does not execute or e it.live( 'declared not-found, capability-unavailable and unexpected defects retain safe distinct HTTP statuses', () => - Effect.gen(function* testProgram13() { + Effect.gen(function* preserveDistinctFailureStatuses() { const assertion = yield* makeAssertion(); const cases = [ { @@ -629,16 +615,13 @@ it.live( }, ]; yield* forEachSequential(cases, (item) => - Effect.gen(function* testProgram14() { + Effect.gen(function* verifySafeFailureResponse() { const harness = yield* makeActionTestHarness({ actionPermission: 'allowed', tenantPermission: 'allowed', services: [item.service], }); - const app = yield* Effect.acquireRelease( - Effect.sync(() => mounted(harness, assertion.environment)), - (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), - ); + const app = yield* mountApp(harness, assertion.environment); const response = yield* handle( app, @@ -666,7 +649,7 @@ it.live( ); it.live('semantically insufficient Party evidence is a declared 422, not a server defect', () => - Effect.gen(function* testProgram15() { + Effect.gen(function* rejectInsufficientPartyEvidence() { const assertion = yield* makeAssertion(); const harness = yield* makeActionTestHarness({ actionPermission: 'allowed', @@ -683,10 +666,7 @@ it.live('semantically insufficient Party evidence is a declared 422, not a serve }), ], }); - const app = yield* Effect.acquireRelease( - Effect.sync(() => mounted(harness, assertion.environment)), - (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), - ); + const app = yield* mountApp(harness, assertion.environment); const response = yield* handle( app, @@ -710,7 +690,7 @@ it.live('semantically insufficient Party evidence is a declared 422, not a serve it.live( 'the Core request hash rejects reuse of an idempotency key for a different command payload', () => - Effect.gen(function* testProgram16() { + Effect.gen(function* rejectIdempotencyPayloadMismatch() { const assertion = yield* makeAssertion(); let executions = 0; const harness = yield* makeActionTestHarness({ @@ -733,10 +713,7 @@ it.live( }), ], }); - const app = yield* Effect.acquireRelease( - Effect.sync(() => mounted(harness, assertion.environment)), - (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), - ); + const app = yield* mountApp(harness, assertion.environment); const first = yield* handle( app, @@ -765,13 +742,10 @@ it.live( it.live( 'commit resolution requires authentication and a valid invocation without creating an Action', () => - Effect.gen(function* testProgram17() { + Effect.gen(function* validateCommitResolutionRequest() { const assertion = yield* makeAssertion(); const harness = yield* makeActionTestHarness(); - const app = yield* Effect.acquireRelease( - Effect.sync(() => mounted(harness, assertion.environment)), - (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), - ); + const app = yield* mountApp(harness, assertion.environment); const missingAuth = yield* handle(app, recoveryRequest(randomUUID())); expect(missingAuth.status).toBe(401); @@ -787,7 +761,7 @@ it.live( ); it.live('an open invocation resolves explicitly without authorizing automatic command retry', () => - Effect.gen(function* testProgram18() { + Effect.gen(function* resolveOpenInvocationWithoutRetry() { const assertion = yield* makeAssertion(); const harness = yield* makeActionTestHarness({ actionPermission: 'allowed', @@ -798,10 +772,7 @@ it.live('an open invocation resolves explicitly without authorizing automatic co ], tenantPermission: 'allowed', }); - const app = yield* Effect.acquireRelease( - Effect.sync(() => mounted(harness, assertion.environment)), - (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), - ); + const app = yield* mountApp(harness, assertion.environment); const failed = yield* handle( app, @@ -831,7 +802,7 @@ it.live('an open invocation resolves explicitly without authorizing automatic co it.live( 'actual Core commit acknowledgement loss resolves and the mounted governed Read returns the original decision without rerunning the Action', () => - Effect.gen(function* testProgram19() { + Effect.gen(function* recoverOriginalDecisionAfterLostCommitAcknowledgement() { const assertion = yield* makeAssertion(); const decisions = new Map(); let executions = 0; @@ -930,10 +901,7 @@ it.live( ); }), }; - const app = yield* Effect.acquireRelease( - Effect.sync(() => mounted(harness, assertion.environment, reads)), - (mountedApp) => Effect.promise(() => mountedApp.dispose()).pipe(Effect.orDie), - ); + const app = yield* mountApp(harness, assertion.environment, reads); const uncertain = yield* handle( app, From fba58bcc9bd787a89e660f9e3cf26f29dfff02af Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 09:50:31 +0200 Subject: [PATCH 13/38] test(auth): distinguish successful navigation from router failure Restore the router mock's native Promise contract and assert completed submission without an internal-error toast. Cover rejected navigation separately. Co-Authored-By: Claude Fable 5.1 --- .../tests/unit/routes/login/page.test.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) 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 0470cd1fa..1cccadcde 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 @@ -15,6 +15,7 @@ const { navigateMock, runBrowserEffectMock, signInMock } = rstest.hoisted(() => })); beforeEach(() => { + navigateMock.mockImplementation(() => Promise.resolve()); runBrowserEffectMock.mockImplementation(runBrowserEffect); signInMock.mockReturnValue( Effect.succeed({ @@ -241,8 +242,29 @@ it.effect('submits valid values through the Shell authentication client and navi ); expect(runBrowserEffectMock).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(); }), ); }), ); + +it.effect('reports navigation failure and restores the login form after authentication', () => + Effect.gen(function* reportsNavigationFailure() { + navigateMock.mockRejectedValueOnce('Navigation failed'); + const user = userEvent.setup(); + renderLogin(); + yield* Effect.promise(() => user.type(getLogin(), 'admin')); + yield* Effect.promise(() => user.type(getPassword(), 'secret')); + yield* Effect.promise(() => user.click(getSubmit())); + 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()); + }), + ); + }), +); From d8d9dc98fd9b4f0b12449167c0e0ba27a42223bb Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 10:09:48 +0200 Subject: [PATCH 14/38] test(rstest): interrupt timed-out layers and verify generated checker scope Reproduce real hook timeout leakage. Await setup interruption before closing suite resources. Keep generated API validation aligned with source-only workspace boundaries with ignored-artifact and real-positive controls. Co-Authored-By: Claude Fable 5.1 --- app/packages/effect-rstest/package.json | 1 + .../effect-rstest/src/internal/internal.ts | 23 ++++- .../tests/fixtures/layer-lifetime.fixture.ts | 63 ++++++++++++++ .../tests/layer-lifetime.test.ts | 84 +++++++++++++++++++ ...odern-js-create@3.8.2-ultramodern.12.patch | 24 ++++-- app/pnpm-lock.yaml | 9 +- app/scripts/tests/api-only-tooling.test.mts | 30 ++++++- 7 files changed, 221 insertions(+), 13 deletions(-) create mode 100644 app/packages/effect-rstest/tests/fixtures/layer-lifetime.fixture.ts create mode 100644 app/packages/effect-rstest/tests/layer-lifetime.test.ts diff --git a/app/packages/effect-rstest/package.json b/app/packages/effect-rstest/package.json index 614501b5c..faac4cc64 100644 --- a/app/packages/effect-rstest/package.json +++ b/app/packages/effect-rstest/package.json @@ -17,6 +17,7 @@ "@rstest/core": "0.11.10" }, "devDependencies": { + "@effect/platform-node": "4.0.0-beta.107", "@effect/tsgo": "0.19.0", "@types/node": "20.19.43" } diff --git a/app/packages/effect-rstest/src/internal/internal.ts b/app/packages/effect-rstest/src/internal/internal.ts index ac66607ef..f5f8660a7 100644 --- a/app/packages/effect-rstest/src/internal/internal.ts +++ b/app/packages/effect-rstest/src/internal/internal.ts @@ -7,6 +7,7 @@ import * as Cause from 'effect/Cause'; import * as Duration from 'effect/Duration'; import * as Effect from 'effect/Effect'; 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'; @@ -244,13 +245,29 @@ 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: a timed-out beforeAll keeps running. + // Stop and await setup 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 => @@ -285,14 +302,14 @@ export const layer = // names from test paths, while its beforeAll/afterAll hooks ensure the // scope closes before later tests in the enclosing suite run. 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/app/packages/effect-rstest/tests/fixtures/layer-lifetime.fixture.ts b/app/packages/effect-rstest/tests/fixtures/layer-lifetime.fixture.ts new file mode 100644 index 000000000..ed0aeed07 --- /dev/null +++ b/app/packages/effect-rstest/tests/fixtures/layer-lifetime.fixture.ts @@ -0,0 +1,63 @@ +// oxlint-disable effect-native/no-native-timers -- real runner hook deadlines cannot be driven by TestClock; remove-when: Rstest exposes controllable hook timers +import { describe, expect, it, layer } from '@app/effect-rstest'; +import { Effect, Layer } from 'effect'; + +// Run only in the child runner: setup failures here are intentional. +for (const named of [true, false]) { + for (const mode of ['delayed', 'never', 'failure'] as const) { + describe(`${named ? 'named' : 'unnamed'} ${mode}`, () => { + const events: string[] = []; + const setup = Layer.effectDiscard( + Effect.gen(function* setupEffect() { + yield* Effect.acquireRelease( + Effect.sync(() => events.push('acquired')), + () => Effect.sync(() => events.push('released')), + ); + if (mode === 'failure') { + return yield* Effect.die('early-setup-failure'); + } + yield* (mode === 'never' ? Effect.never : Effect.sleep(400)).pipe( + Effect.onInterrupt(() => + Effect.gen(function* interruptSetup() { + yield* Effect.sleep(10); + events.push('interrupted'); + }), + ), + ); + events.push('late-effect'); + return yield* Effect.acquireRelease( + Effect.sync(() => events.push('late-acquired')), + () => Effect.sync(() => events.push('late-released')), + ); + }), + ); + // Named suites exercise an explicit timeout; unnamed suites inherit the + // runner's hookTimeout, which is also 100ms in the child configuration. + const withLayer = layer( + setup, + named ? { excludeTestServices: true, timeout: 100 } : { excludeTestServices: true }, + ); + if (named) { + withLayer('setup', (suiteIt) => { + suiteIt.effect('unreachable', () => Effect.sync(() => events.push('test-ran'))); + }); + } else { + withLayer((suiteIt) => { + suiteIt.effect('unreachable', () => Effect.sync(() => events.push('test-ran'))); + }); + } + + it.live('setup stops before resource release and later tests', () => + Effect.gen(function* observeSetupLifetime() { + const expected = + mode === 'failure' ? ['acquired', 'released'] : ['acquired', 'interrupted', 'released']; + expect(events).toEqual(expected); + if (mode === 'delayed') { + yield* Effect.sleep(600); + expect(events).toEqual(expected); + } + }), + ); + }); + } +} diff --git a/app/packages/effect-rstest/tests/layer-lifetime.test.ts b/app/packages/effect-rstest/tests/layer-lifetime.test.ts new file mode 100644 index 000000000..18c2dde96 --- /dev/null +++ b/app/packages/effect-rstest/tests/layer-lifetime.test.ts @@ -0,0 +1,84 @@ +import { NodeServices } from '@effect/platform-node'; +import { expect, it } from '@app/effect-rstest'; +import { Effect, Schema, Stream } from 'effect'; +import { ChildProcess } from 'effect/unstable/process'; + +const runnerReport = Schema.fromJsonString( + Schema.Struct({ + files: Schema.Array( + Schema.Struct({ + errors: Schema.Array(Schema.Struct({ message: Schema.String })), + }), + ), + summary: Schema.Struct({ + failedTests: Schema.Finite, + passedTests: Schema.Finite, + skippedTests: Schema.Finite, + tests: Schema.Finite, + }), + unhandledErrors: Schema.Array(Schema.Unknown), + }), +); + +it.layer(NodeServices.layer, { excludeTestServices: true })((suiteIt) => { + suiteIt.effect( + 'layer setup fibers stop on hook timeout and release resources on early failure', + () => + Effect.gen(function* runLayerLifetimeFixture() { + const child = yield* ChildProcess.make( + process.execPath, + [ + 'node_modules/@rstest/core/bin/rstest.js', + 'run', + '--include', + 'tests/fixtures/layer-lifetime.fixture.ts', + '--reporter', + 'json', + '--hookTimeout', + '100', + '--pool.maxWorkers', + '1', + ], + { + cwd: new URL('..', import.meta.url).pathname, + forceKillAfter: '1 second', + stderr: 'pipe', + stdin: 'ignore', + stdout: 'pipe', + }, + ); + const [status, stdout, stderr] = yield* Effect.all( + [ + child.exitCode, + child.stdout.pipe(Stream.decodeText(), Stream.mkString), + child.stderr.pipe(Stream.decodeText(), Stream.mkString), + ], + { concurrency: 'unbounded' }, + ).pipe( + // oxlint-disable-next-line effect-native/no-native-timers -- subprocess deadline uses real time; remove-when: Rstest can control child-process time + Effect.timeout('20 seconds'), + ); + // Failing hooks must still fail the runner, not become swallowed failures. + expect(Number(status), stderr).toBe(1); + const report = yield* Schema.decodeEffect(runnerReport)(stdout); + expect(report.summary).toEqual({ + failedTests: 0, + passedTests: 6, + skippedTests: 6, + tests: 12, + }); + expect(report.unhandledErrors).toEqual([]); + expect(report.files).toHaveLength(1); + const errors = report.files.flatMap((file) => file.errors); + expect(errors.map((error) => error.message)).toEqual([ + 'beforeAll hook timed out in 100ms', + 'beforeAll hook timed out in 100ms', + 'early-setup-failure', + 'beforeAll hook timed out in 100ms', + 'beforeAll hook timed out in 100ms', + 'early-setup-failure', + ]); + }), + 30_000, + ); +}); 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 81f450ab1..115ed462e 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 @@ -2845,7 +2845,7 @@ index 198beb018e1d6985953748cea5000448f381abe5..7d2acb37a8058c1c7b94c1df033d34e7 | 'always' | 'hourly' diff --git a/templates/workspace-scripts/check-ultramodern-api-boundaries.mts b/templates/workspace-scripts/check-ultramodern-api-boundaries.mts -index d606a2265cb438d32a612cfdd6ebb9561529d4f5..b31f281e7ab5bbea289fc2aae117fe39202023cf 100644 +index d606a2265cb438d32a612cfdd6ebb9561529d4f5..98dd4f8402d24c775c36eb639829fb07e0d03131 100644 --- a/templates/workspace-scripts/check-ultramodern-api-boundaries.mts +++ b/templates/workspace-scripts/check-ultramodern-api-boundaries.mts @@ -1,6 +1,10 @@ @@ -2859,10 +2859,22 @@ index d606a2265cb438d32a612cfdd6ebb9561529d4f5..b31f281e7ab5bbea289fc2aae117fe39 const workspaceRoot = process.env.ULTRAMODERN_WORKSPACE_ROOT ?? process.cwd(); const failures = []; -@@ -182,11 +186,1184 @@ for (const file of textFiles) { +@@ -11,6 +15,7 @@ const ignoredDirectories = new Set([ + '.output', + 'coverage', + 'dist', ++ 'dist-cloudflare', + 'node_modules', + 'repos', + ]); +@@ -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: [] }; @@ -4044,7 +4056,7 @@ index d606a2265cb438d32a612cfdd6ebb9561529d4f5..b31f281e7ab5bbea289fc2aae117fe39 function assertApiSurface(appPath) { const apiEntry = `${appPath}/api/index.ts`; const backendEffectExpose = `${appPath}/api/effect-api.ts`; -@@ -211,30 +1388,28 @@ function assertApiSurface(appPath) { +@@ -211,30 +1391,28 @@ function assertApiSurface(appPath) { if (exists(apiEntry)) { const entry = readText(apiEntry); @@ -4089,7 +4101,7 @@ index d606a2265cb438d32a612cfdd6ebb9561529d4f5..b31f281e7ab5bbea289fc2aae117fe39 } if (exists(backendEffectExpose)) { const backendExpose = readText(backendEffectExpose); -@@ -300,6 +1475,62 @@ function assertApiSurface(appPath) { +@@ -300,6 +1478,62 @@ function assertApiSurface(appPath) { /\bSchema\./u, 'must use Schema for request, response and error shapes.', ); @@ -4152,7 +4164,7 @@ index d606a2265cb438d32a612cfdd6ebb9561529d4f5..b31f281e7ab5bbea289fc2aae117fe39 } if (exists(modernConfig)) { -@@ -373,7 +1604,6 @@ if (exists('package.json')) { +@@ -373,7 +1607,6 @@ if (exists('package.json')) { } if (exists('topology/reference-topology.json')) { diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 719876216..acfffc925 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -17,7 +17,7 @@ patchedDependencies: '@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12': 92106508bb75f01d2d27454563d5030569bcf89a49155b2cc91a2b9111360d4c '@bleedingdev/modern-js-builder@3.8.2-ultramodern.12': c5bdbbf89a17e39cb43c17f66a1904ed29a1462b09fd19aaf05c7301cbe4601c '@bleedingdev/modern-js-code-tools@3.8.2-ultramodern.12': f47cc9b656ff270a7ec5d1407c048fa8ff807bd72b5c491490b4ae07ffffc595 - '@bleedingdev/modern-js-create@3.8.2-ultramodern.12': 94333aea6925d878a885616925eaefbc1668fcc0b91bf03f5e7caa12e79da131 + '@bleedingdev/modern-js-create@3.8.2-ultramodern.12': a9a2baf390d276f52298fb57d7e968739f2bdee0e7ffbba387baa085f40ee1be '@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 @@ -69,7 +69,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=94333aea6925d878a885616925eaefbc1668fcc0b91bf03f5e7caa12e79da131)(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=a9a2baf390d276f52298fb57d7e968739f2bdee0e7ffbba387baa085f40ee1be)(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)(02f8732f8d0ac3252ab424ea7a1b32c5)' @@ -367,6 +367,9 @@ importers: specifier: 4.0.0-beta.107 version: 4.0.0-beta.107(patch_hash=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f) devDependencies: + '@effect/platform-node': + specifier: 4.0.0-beta.107 + version: 4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f))(ioredis@5.11.1(supports-color@10.2.2)) '@effect/tsgo': specifier: 0.19.0 version: 0.19.0 @@ -9968,7 +9971,7 @@ snapshots: - react - react-dom - '@bleedingdev/modern-js-create@3.8.2-ultramodern.12(patch_hash=94333aea6925d878a885616925eaefbc1668fcc0b91bf03f5e7caa12e79da131)(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=a9a2baf390d276f52298fb57d7e968739f2bdee0e7ffbba387baa085f40ee1be)(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)' diff --git a/app/scripts/tests/api-only-tooling.test.mts b/app/scripts/tests/api-only-tooling.test.mts index d2ea3dbd8..f05e8d1e0 100644 --- a/app/scripts/tests/api-only-tooling.test.mts +++ b/app/scripts/tests/api-only-tooling.test.mts @@ -2727,12 +2727,40 @@ it.live( }, ], }); + yield* writeText( + checkoutWorkspace, + 'verticals/shopping/dist-cloudflare/api/index.js', + 'export const handler = () => new Response();', + ); + yield* writeText( + checkoutWorkspace, + 'verticals/retired/node_modules/cache.js', + 'export const cached = true;', + ); expect( runNode([path.join(formatRoot, checker.relativePath)], { env: { ULTRAMODERN_WORKSPACE_ROOT: checkoutWorkspace }, }), - `${moduleFormat} checkout workspace`, + `${moduleFormat} ignores generated output and retired package caches`, ).toMatch(/UltraModern API boundary check passed/u); + yield* writeText( + checkoutWorkspace, + 'verticals/shopping/api/unsafe.ts', + 'export const response = new Response();', + ); + expect(() => + runNode([path.join(formatRoot, checker.relativePath)], { + env: { ULTRAMODERN_WORKSPACE_ROOT: checkoutWorkspace }, + }), + ).toThrow(/API modules must not hand-build Response objects/u); + yield* writeJson(checkoutWorkspace, 'verticals/retired/package.json', { + name: '@generated-proof/retired', + }); + expect(() => + runNode([path.join(formatRoot, checker.relativePath)], { + env: { ULTRAMODERN_WORKSPACE_ROOT: checkoutWorkspace }, + }), + ).toThrow(/verticals\/retired\/api\/index\.ts is required/u); }), ), { concurrency: 'unbounded' }, From 9e1ec761c21352a3c9419753c70589692f647517 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 10:24:31 +0200 Subject: [PATCH 15/38] test(rstest): support schema properties and Effect semantic equality Co-Authored-By: Claude Fable 5.1 --- .../effect-rstest/src/internal/internal.ts | 12 ++--- .../effect-rstest/tests/equality.test.ts | 48 +++++++++++++++++++ .../tests/prop-schema-record.test.ts | 23 +++++++++ .../tests/prop-schema-tuple.test.ts | 23 +++++++++ 4 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 app/packages/effect-rstest/tests/equality.test.ts create mode 100644 app/packages/effect-rstest/tests/prop-schema-record.test.ts create mode 100644 app/packages/effect-rstest/tests/prop-schema-tuple.test.ts diff --git a/app/packages/effect-rstest/src/internal/internal.ts b/app/packages/effect-rstest/src/internal/internal.ts index f5f8660a7..50ff8ac34 100644 --- a/app/packages/effect-rstest/src/internal/internal.ts +++ b/app/packages/effect-rstest/src/internal/internal.ts @@ -6,6 +6,7 @@ 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'; @@ -51,7 +52,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 */ @@ -170,7 +173,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; }); @@ -192,10 +195,7 @@ export const prop: EffectRstest.Vitest.Methods['prop'] = (name, arbitraries, sel 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>, diff --git a/app/packages/effect-rstest/tests/equality.test.ts b/app/packages/effect-rstest/tests/equality.test.ts new file mode 100644 index 000000000..25001db9c --- /dev/null +++ b/app/packages/effect-rstest/tests/equality.test.ts @@ -0,0 +1,48 @@ +import { addEqualityTesters, expect, it } from '@app/effect-rstest'; +import { Equal, Hash } from 'effect'; + +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 { + // Deliberate collision: unequal values must reach the equality method. + return this.#key.length; + } +} + +addEqualityTesters(); + +it('uses semantic equality despite different enumerable representations', () => { + const left = new SemanticValue('same', 'left'); + const right = new SemanticValue('same', 'right'); + expect(left.representation).not.toBe(right.representation); + expect(Equal.equals(left, right)).toBe(true); + expect(left).toEqual(right); + expect({ value: left }).toEqual({ value: right }); +}); + +it('respects semantic inequality despite identical enumerable representations', () => { + const left = new SemanticValue('left', 'same'); + const right = new SemanticValue('next', 'same'); + expect(left.representation).toBe(right.representation); + expect(Equal.equals(left, right)).toBe(false); + expect(left).not.toEqual(right); + expect({ value: left }).not.toEqual({ value: right }); +}); + +it('preserves native plain-object deep equality and asymmetric matchers', () => { + expect({ nested: { value: 1 } }).toEqual({ nested: { value: 1 } }); + expect({ nested: { value: 1 } }).not.toEqual({ nested: { value: 2 } }); + expect({ nested: { value: 1 } }).toEqual({ nested: { value: expect.any(Number) } }); +}); diff --git a/app/packages/effect-rstest/tests/prop-schema-record.test.ts b/app/packages/effect-rstest/tests/prop-schema-record.test.ts new file mode 100644 index 000000000..803c95c9b --- /dev/null +++ b/app/packages/effect-rstest/tests/prop-schema-record.test.ts @@ -0,0 +1,23 @@ +import { expect, it } from '@app/effect-rstest'; +import { Schema } from 'effect'; +import { FastCheck } from 'effect/testing'; + +it.prop( + 'plain record properties generate schemas alongside arbitraries', + { count: FastCheck.integer({ max: 10, min: 1 }), label: Schema.Literal('schema') }, + ({ count, label }) => { + expect(label).toBe('schema'); + expect(Number.isInteger(count)).toBe(true); + expect(count).toBeGreaterThanOrEqual(1); + expect(count).toBeLessThanOrEqual(10); + }, + { fastCheck: { numRuns: 20 } }, +); + +it.prop( + 'plain record properties retain FastCheck-only support', + { value: FastCheck.constant(7) }, + ({ value }) => { + expect(value).toBe(7); + }, +); diff --git a/app/packages/effect-rstest/tests/prop-schema-tuple.test.ts b/app/packages/effect-rstest/tests/prop-schema-tuple.test.ts new file mode 100644 index 000000000..944e0a387 --- /dev/null +++ b/app/packages/effect-rstest/tests/prop-schema-tuple.test.ts @@ -0,0 +1,23 @@ +import { expect, it } from '@app/effect-rstest'; +import { Schema } from 'effect'; +import { FastCheck } from 'effect/testing'; + +it.prop( + 'plain tuple properties generate schemas alongside arbitraries', + [Schema.Literal('schema'), FastCheck.integer({ max: 10, min: 1 })], + ([label, count]) => { + expect(label).toBe('schema'); + expect(Number.isInteger(count)).toBe(true); + expect(count).toBeGreaterThanOrEqual(1); + expect(count).toBeLessThanOrEqual(10); + }, + { fastCheck: { numRuns: 20 } }, +); + +it.prop( + 'plain tuple properties retain FastCheck-only support', + [FastCheck.constant(7)], + ([value]) => { + expect(value).toBe(7); + }, +); From 1a6381173bed2114b2087b3517f32fe11c4af6d6 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 10:26:19 +0200 Subject: [PATCH 16/38] test(ci): isolate analyzer colors and retain wire codec requirements Co-Authored-By: Claude Fable 5.1 --- app/scripts/quality-audit.mts | 4 +- app/scripts/tests/quality-audit.test.mts | 94 ++++++++++++++++--- .../unit/api-integration-runtime.test.ts | 7 +- 3 files changed, 89 insertions(+), 16 deletions(-) diff --git a/app/scripts/quality-audit.mts b/app/scripts/quality-audit.mts index ee42396dd..7859d495f 100644 --- a/app/scripts/quality-audit.mts +++ b/app/scripts/quality-audit.mts @@ -710,7 +710,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/tests/quality-audit.test.mts b/app/scripts/tests/quality-audit.test.mts index ea9d88b54..afb5ad198 100644 --- a/app/scripts/tests/quality-audit.test.mts +++ b/app/scripts/tests/quality-audit.test.mts @@ -1,6 +1,5 @@ import { expect, it } from '@app/effect-rstest'; -import { spawnSync } from 'node:child_process'; import { copyFileSync, mkdirSync, @@ -15,7 +14,7 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; import { NodeServices } from '@effect/platform-node'; -import { Effect, Schema } from 'effect'; +import { Effect, Schema, Stream } from 'effect'; import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process'; import { auditSteps, runQualityAudit, validateReport } from '../quality-audit.mts'; @@ -26,6 +25,7 @@ const FALLOW_HEALTH = 'fallow-health'; const CONFIG_DIRECTORY = 'quality-audit'; const REPORT_DIRECTORY = 'reports'; 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({ @@ -177,7 +177,7 @@ const createFixture = () => 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'), + 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']) { @@ -450,7 +450,7 @@ it.live( ); it.live( - 'the CLI handles escaped paths, foreign cwd and untracked source provenance', + '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); @@ -466,18 +466,32 @@ it.live( path.join(root, CONFIG_DIRECTORY, file), ); } - const result = spawnSync(process.execPath, [executable, '--tool', 'knip', '--output', output], { - cwd: tmpdir(), - encoding: 'utf-8', - timeout: 60_000, - }); - expect( - result.error, - `CLI spawn failed: ${String(result.error)}\n${result.stdout}\n${result.stderr}`, - ).toBe(undefined); + const result = yield* Effect.gen(function* runColoredCli() { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const handle = yield* spawner.spawn( + 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', + }), + ); + 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 }; + }).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'), ); @@ -544,7 +558,7 @@ it.live( const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const commands = [ ['init', '-q'], - ['add', '.gitignore', 'package.json', CONFIG_DIRECTORY, 'scripts'], + ['add', '.gitignore', PACKAGE_JSON, CONFIG_DIRECTORY, 'scripts'], [ '-c', `core.hooksPath=${path.join(root, '.git/no-hooks')}`, @@ -635,3 +649,55 @@ it.live( 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, + ); + } + } + }), +); 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 fa25401da..fe15fb6eb 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 @@ -429,7 +429,12 @@ it.live( } 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 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]; From 5db9cac2ebaf8f77cc5af55a3a3c4c11410e9161 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 10:29:07 +0200 Subject: [PATCH 17/38] test(rstest): read subprocess reports independently of CLI banners Co-Authored-By: Claude Fable 5.1 --- .../tests/layer-lifetime.test.ts | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/app/packages/effect-rstest/tests/layer-lifetime.test.ts b/app/packages/effect-rstest/tests/layer-lifetime.test.ts index 18c2dde96..1dad62b37 100644 --- a/app/packages/effect-rstest/tests/layer-lifetime.test.ts +++ b/app/packages/effect-rstest/tests/layer-lifetime.test.ts @@ -1,6 +1,6 @@ import { NodeServices } from '@effect/platform-node'; import { expect, it } from '@app/effect-rstest'; -import { Effect, Schema, Stream } from 'effect'; +import { Effect, FileSystem, Schema, Stream } from 'effect'; import { ChildProcess } from 'effect/unstable/process'; const runnerReport = Schema.fromJsonString( @@ -25,6 +25,15 @@ it.layer(NodeServices.layer, { excludeTestServices: true })((suiteIt) => { 'layer setup fibers stop on hook timeout and release resources on early failure', () => Effect.gen(function* runLayerLifetimeFixture() { + const fs = yield* FileSystem.FileSystem; + const directory = yield* fs.makeTempDirectoryScoped({ prefix: 'effect-rstest-layer-' }); + const reportPath = `${directory}/report.json`; + const configPath = `${directory}/rstest.config.mjs`; + const config = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ + reporters: [['json', { outputPath: reportPath }]], + root: new URL('..', import.meta.url).pathname, + }); + yield* fs.writeFileString(configPath, `export default ${config};`); const child = yield* ChildProcess.make( process.execPath, [ @@ -32,8 +41,8 @@ it.layer(NodeServices.layer, { excludeTestServices: true })((suiteIt) => { 'run', '--include', 'tests/fixtures/layer-lifetime.fixture.ts', - '--reporter', - 'json', + '--config', + configPath, '--hookTimeout', '100', '--pool.maxWorkers', @@ -41,6 +50,8 @@ it.layer(NodeServices.layer, { excludeTestServices: true })((suiteIt) => { ], { cwd: new URL('..', import.meta.url).pathname, + // Exercise normal CLI mode: its banner makes stdout unsuitable for JSON. + env: { RSTEST_NO_AGENT: '1' }, forceKillAfter: '1 second', stderr: 'pipe', stdin: 'ignore', @@ -59,8 +70,10 @@ it.layer(NodeServices.layer, { excludeTestServices: true })((suiteIt) => { Effect.timeout('20 seconds'), ); // Failing hooks must still fail the runner, not become swallowed failures. - expect(Number(status), stderr).toBe(1); - const report = yield* Schema.decodeEffect(runnerReport)(stdout); + expect(Number(status), `${stdout}\n${stderr}`).toBe(1); + const report = yield* Schema.decodeEffect(runnerReport)( + yield* fs.readFileString(reportPath), + ); expect(report.summary).toEqual({ failedTests: 0, passedTests: 6, From a16a02ee0f1bbb4077a38e6f6200560b0a6626fa Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 10:31:17 +0200 Subject: [PATCH 18/38] docs(testing): document local adapter corrections and property APIs Co-Authored-By: Claude Fable 5.1 --- app/packages/effect-rstest/README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/packages/effect-rstest/README.md b/app/packages/effect-rstest/README.md index c9c7f189b..71f98b2de 100644 --- a/app/packages/effect-rstest/README.md +++ b/app/packages/effect-rstest/README.md @@ -2,7 +2,7 @@ Vendored community port of `@effect/vitest` to Rstest by ScriptedAlchemy, commit `79abbf6`. Source: https://github.com/ScriptedAlchemy/effect-rstest. MIT licensed; the original copyright and permission notice are preserved in [LICENSE](./LICENSE). -Workspace exports use TypeScript source; local changes adapt imports and repository diagnostics. +Workspace exports use TypeScript source. Local corrections cover Rstest lifecycle integration, schema-backed property tests, and Effect semantic equality, alongside import and diagnostic adaptations. ## Usage @@ -28,6 +28,25 @@ it.effect('reads an Effect value', () => ); ``` +### Properties and equality + +`it.prop` accepts FastCheck arbitraries and Effect schemas in either tuples or records. Use +`it.effect.prop` when the property itself returns an Effect. + +```ts +import { addEqualityTesters, expect, it } from '@app/effect-rstest'; +import { Schema } from 'effect'; + +addEqualityTesters(); + +it.prop('generates schema values', [Schema.String], ([value]) => { + expect(typeof value).toBe('string'); +}); +``` + +`addEqualityTesters` enables Effect's `Equal.equals` for values implementing its equality protocol. +Ordinary objects and asymmetric matchers retain Rstest's native equality behavior. + ### Shared layers Use `it.layer` to share a layer across a suite; its resources are released when the suite ends. Set `excludeTestServices: true` when the suite needs live services instead of the test clock/console (the default is `false`). Replace `Layer.empty` below with your fixture layer: From 88781a5f8bcbd2b3c8af9831ed949b0248439cb0 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 10:34:28 +0200 Subject: [PATCH 19/38] test(lint): track native test APIs inside wrapped suites Co-Authored-By: Claude Fable 5.1 --- .../rules/no-promise-shaped-port.ts | 2 +- .../describe-wrapped-test-callback.test.ts | 26 ++++++++++++++++ .../describe-wrapped-test-callback.test.ts | 30 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/describe-wrapped-test-callback.test.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/describe-wrapped-test-callback.test.ts 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 3ad896cb4..b2841f99f 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 @@ -350,7 +350,7 @@ export const rule = defineRule({ let factory = unwrap(registration.callee); while (factory?.type === 'CallExpression') factory = unwrap(factory.callee); if ( - /^@app\/effect-rstest:(?:\*\.)?(?:(?:it|test)\.)?layer$/u.test( + /^@app\/effect-rstest:(?:\*\.)?(?:describeWrapped|(?:(?:it|test)\.)?layer)$/u.test( imported(factory, seen) ?? '', ) ) 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..2e07d687a --- /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 '@app/effect-rstest'; +import * as suite from '@app/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/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..22751cefc --- /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 '@app/effect-rstest'; +import * as suite from '@app/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; From e6d2b739b9a8f646dc627da5d7faebe3d95b21d1 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 10:39:15 +0200 Subject: [PATCH 20/38] test(rstest): await timed-out test finalizers before suite release Also preserve typed startup defects and use the declared API readiness endpoint for browser startup. Co-Authored-By: Claude Fable 5.1 --- app/apps/shell-super-app/playwright.config.ts | 2 +- app/packages/effect-rstest/README.md | 8 ++ .../effect-rstest/src/internal/internal.ts | 17 ++- .../tests/fixtures/test-lifetime.fixture.ts | 105 +++++++++++++++++ .../effect-rstest/tests/test-lifetime.test.ts | 111 ++++++++++++++++++ .../tests/unit/effect-bff-runtime.test.ts | 9 +- 6 files changed, 244 insertions(+), 8 deletions(-) create mode 100644 app/packages/effect-rstest/tests/fixtures/test-lifetime.fixture.ts create mode 100644 app/packages/effect-rstest/tests/test-lifetime.test.ts diff --git a/app/apps/shell-super-app/playwright.config.ts b/app/apps/shell-super-app/playwright.config.ts index 42559bc7e..115835bfb 100644 --- a/app/apps/shell-super-app/playwright.config.ts +++ b/app/apps/shell-super-app/playwright.config.ts @@ -54,7 +54,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', diff --git a/app/packages/effect-rstest/README.md b/app/packages/effect-rstest/README.md index 71f98b2de..c9acd57bc 100644 --- a/app/packages/effect-rstest/README.md +++ b/app/packages/effect-rstest/README.md @@ -85,3 +85,11 @@ it.effect('cleans up its fixture', () => For an existing resource, register its Effect cleanup before using it: `yield* Effect.addFinalizer(() => release(resource))`. Use `acquireRelease` when acquisition and registration must be interruption-safe together. **Do not use JavaScript `try/finally` for Effect cleanup.** A failed yielded Effect short-circuits the generator; JavaScript `finally` does not finalize failed yielded Effects. Register an Effect finalizer instead, so cleanup also runs when a yield fails or the test is interrupted. + +On a runner timeout, the adapter interrupts the Effect and waits for its finalizers before the next +sequential test and enclosing layer teardown. Cleanup is not cut short by the runner's hook timeout, +so a finalizer that never settles can hold completion indefinitely. + +Rstest runs native `afterEach` hooks before its test-finished callbacks. Those hooks can therefore +run while timed-out Effect cleanup is still pending. Keep resource release in Effect finalizers, +not native hooks. This ordering guarantee does not serialize concurrent tests. diff --git a/app/packages/effect-rstest/src/internal/internal.ts b/app/packages/effect-rstest/src/internal/internal.ts index 50ff8ac34..ebbab9cab 100644 --- a/app/packages/effect-rstest/src/internal/internal.ts +++ b/app/packages/effect-rstest/src/internal/internal.ts @@ -42,8 +42,21 @@ const runPromise: ( /** @internal */ const runTest = (ctx?: Rs.TestContext) => - (effect: Effect.Effect) => - runPromise(effect, ctx); + (effect: Effect.Effect) => { + let settlement: Promise | undefined; + // Rstest aborts on timeout without awaiting the callback. Keep its outcome, + // but await Effect finalizers before the next test or suite teardown. Native + // afterEach hooks run earlier than onTestFinished and are not covered. + // Do not race cleanup against another hook timeout and reintroduce the leak. + ctx?.onTestFinished(() => settlement, 0); + const result = runPromise(effect, ctx); + // Cleanup must not rethrow a failure that Rstest already handled (e.g. fails). + settlement = result.then( + () => {}, + () => {}, + ); + return result; + }; /** @internal */ export type TestContext = TestConsole.TestConsole | TestClock.TestClock; diff --git a/app/packages/effect-rstest/tests/fixtures/test-lifetime.fixture.ts b/app/packages/effect-rstest/tests/fixtures/test-lifetime.fixture.ts new file mode 100644 index 000000000..fe75233b0 --- /dev/null +++ b/app/packages/effect-rstest/tests/fixtures/test-lifetime.fixture.ts @@ -0,0 +1,105 @@ +import { afterEach, describe, expect, it, layer } from '@app/effect-rstest'; +import { Effect, Fiber, Layer } from 'effect'; +import { TestClock } from 'effect/testing'; + +const suiteAcquired = 'suite acquired'; +const testAcquired = 'test acquired'; +const testReleased = 'test released'; + +const expectedTimeout = 'expected-timeout'; +const expectedFailure = 'expected-failure'; +// Run only in the child runner: ordinary failures/timeouts are intentional. +for (const mode of [ + 'timeout', + expectedTimeout, + 'success', + 'failure', + expectedFailure, + 'unexpected-success', + 'skipped', + 'runtime-skip', +] as const) { + describe(mode, () => { + const events: string[] = []; + const timedOut = mode === 'timeout' || mode === expectedTimeout; + const skipped = mode === 'skipped'; + const afterTest = skipped + ? [suiteAcquired] + : [ + suiteAcquired, + testAcquired, + ...(timedOut ? ['afterEach', testReleased] : [testReleased, 'afterEach']), + 'finished', + ]; + const resource = Layer.effectDiscard( + Effect.acquireRelease( + Effect.sync(() => events.push(suiteAcquired)), + () => Effect.sync(() => events.push('suite released')), + ), + ); + layer(resource, { excludeTestServices: true })('resource', (suiteIt) => { + afterEach((ctx) => { + if (ctx.task.name !== mode) { + return; + } + // Rstest runs native afterEach BEFORE onTestFinished. The barrier cannot + // order native hooks after timeout cleanup; record that boundary explicitly. + expect(events).toEqual([suiteAcquired, testAcquired, ...(timedOut ? [] : [testReleased])]); + events.push('afterEach'); + }); + const expectedToFail = + mode === expectedTimeout || mode === expectedFailure || mode === 'unexpected-success'; + const activeTest = expectedToFail ? suiteIt.effect.fails : suiteIt.effect; + const test = skipped ? suiteIt.effect.skip : activeTest; + test( + mode, + (ctx) => + Effect.gen(function* testLifetime() { + ctx.onTestFinished(() => { + expect(events).toEqual(afterTest.slice(0, -1)); + events.push('finished'); + }); + yield* Effect.acquireRelease( + Effect.sync(() => events.push(testAcquired)), + () => + // This layer intentionally uses live time: cleanup must outlast + // the runner's real deadline, not an Effect/TestClock deadline. + Effect.sleep(150).pipe( + Effect.andThen(Effect.sync(() => events.push(testReleased))), + ), + ); + if (timedOut) { + return yield* Effect.never; + } + if (mode === 'runtime-skip') { + ctx.skip(); + } + if (mode === 'failure' || mode === expectedFailure) { + return yield* Effect.die('intentional-test-failure'); + } + return false; + }), + timedOut ? 30 : 2000, + ); + suiteIt.effect('next test waits for cleanup', () => + Effect.sync(() => { + expect(events).toEqual(afterTest); + events.push('next test'); + }), + ); + }); + it.effect('parent releases after test cleanup', () => + Effect.sync(() => { + expect(events).toEqual([...afterTest, 'next test', 'suite released']); + }), + ); + }); +} + +it.effect('virtual-clock success still completes normally', () => + Effect.gen(function* virtualClockSuccess() { + const fiber = yield* Effect.forkChild(Effect.sleep('1 hour').pipe(Effect.as(42))); + yield* TestClock.adjust('1 hour'); + expect(yield* Fiber.join(fiber)).toBe(42); + }), +); diff --git a/app/packages/effect-rstest/tests/test-lifetime.test.ts b/app/packages/effect-rstest/tests/test-lifetime.test.ts new file mode 100644 index 000000000..23039a675 --- /dev/null +++ b/app/packages/effect-rstest/tests/test-lifetime.test.ts @@ -0,0 +1,111 @@ +import { NodeServices } from '@effect/platform-node'; +import { expect, it } from '@app/effect-rstest'; +import { Effect, FileSystem, Schema, Stream } from 'effect'; +import { ChildProcess } from 'effect/unstable/process'; + +const runnerReport = Schema.fromJsonString( + Schema.Struct({ + files: Schema.Array( + Schema.Struct({ errors: Schema.Array(Schema.Struct({ message: Schema.String })) }), + ), + summary: Schema.Struct({ + failedTests: Schema.Finite, + passedTests: Schema.Finite, + skippedTests: Schema.Finite, + tests: Schema.Finite, + }), + tests: Schema.Array( + Schema.Struct({ + errors: Schema.optional(Schema.Array(Schema.Struct({ message: Schema.String }))), + name: Schema.String, + status: Schema.String, + }), + ), + unhandledErrors: Schema.Array(Schema.Unknown), + }), +); + +it.layer(NodeServices.layer, { excludeTestServices: true })((suiteIt) => { + suiteIt.effect( + 'test timeout cleanup settles before later tests and suite release without changing outcomes', + () => + Effect.gen(function* runTestLifetimeFixture() { + const fs = yield* FileSystem.FileSystem; + const directory = yield* fs.makeTempDirectoryScoped({ + prefix: 'effect-rstest-test-lifetime-', + }); + const config = `${directory}/rstest.config.mjs`; + yield* fs.writeFileString( + config, + `export default { + testEnvironment: 'node', + reporters: [['json', { outputPath: new URL('./report.json', import.meta.url).pathname }]], + };`, + ); + const child = yield* ChildProcess.make( + process.execPath, + [ + 'node_modules/@rstest/core/bin/rstest.js', + 'run', + '--include', + 'tests/fixtures/test-lifetime.fixture.ts', + '--config', + config, + '--pool.maxWorkers', + '1', + '--hookTimeout', + '50', + ], + { + cwd: new URL('..', import.meta.url).pathname, + env: { RSTEST_NO_AGENT: '1' }, + forceKillAfter: '1 second', + stderr: 'pipe', + stdin: 'ignore', + stdout: 'pipe', + }, + ); + const [status, stdout, stderr] = yield* Effect.all( + [ + child.exitCode, + child.stdout.pipe(Stream.decodeText(), Stream.mkString), + child.stderr.pipe(Stream.decodeText(), Stream.mkString), + ], + { concurrency: 'unbounded' }, + ); + expect(Number(status), `${stdout}\n${stderr}`).toBe(1); + const report = yield* fs + .readFileString(`${directory}/report.json`) + .pipe(Effect.flatMap(Schema.decodeEffect(runnerReport))); + expect(report.summary).toEqual({ + failedTests: 3, + passedTests: 20, + skippedTests: 2, + tests: 25, + }); + expect(report.unhandledErrors).toEqual([]); + expect(report.files).toHaveLength(1); + expect(report.files.flatMap((file) => file.errors)).toEqual([]); + const failures = report.tests.filter((test) => test.status === 'fail'); + expect(failures.map((test) => test.name)).toEqual([ + 'timeout', + 'failure', + 'unexpected-success', + ]); + expect(failures.flatMap((test) => test.errors ?? []).map((error) => error.message)).toEqual( + [ + 'test timed out in 30ms (no expect assertions completed)', + 'intentional-test-failure', + 'Expect test to fail', + ], + ); + for (const name of ['expected-timeout', 'expected-failure', 'success']) { + expect(report.tests.find((test) => test.name === name)?.status).toBe('pass'); + } + for (const name of ['skipped', 'runtime-skip']) { + expect(report.tests.find((test) => test.name === name)?.status).toBe('skip'); + } + }), + 30_000, + ); +}); 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 e249e2d59..60b957e9f 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 @@ -117,10 +117,9 @@ it.live('preserves caller-owned Layer startup defects', () => Effect.sync(() => failingStartupRuntime.createHandler()), (runtimeServer) => Effect.promise(() => runtimeServer.dispose()), ); - const error = yield* Effect.tryPromise({ - catch: (cause) => cause, - try: () => server.handler(new Request('http://localhost/greet')), - }).pipe(Effect.flip); - expect(String(error)).toMatch(/fixture layer startup defect/u); + 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); }), ); From a46844c12ceca5b3b37413657d7cdfb7ca85c87d Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 10:47:24 +0200 Subject: [PATCH 21/38] test(playwright): load Core through native Node TypeScript boundary Use the supported build.external setting so private Core class identity is preserved and type-only declare fields bypass the incompatible Babel transform order. Co-Authored-By: Claude Fable 5.1 --- app/apps/shell-super-app/playwright.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/apps/shell-super-app/playwright.config.ts b/app/apps/shell-super-app/playwright.config.ts index 115835bfb..f146bd76d 100644 --- a/app/apps/shell-super-app/playwright.config.ts +++ b/app/apps/shell-super-app/playwright.config.ts @@ -30,6 +30,8 @@ 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, projects: [ { From 73def9ff0f71cfa055fb5fba34aefd46bc655d8d Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 11:08:48 +0200 Subject: [PATCH 22/38] test(effect): remove generic Promise adapter helpers Adapt real driver calls lazily in place and retain typed discovery rejection causes. Co-Authored-By: Claude Fable 5.1 --- .../integration/action-permission.test.ts | 15 ++++---- .../contacts-identity-migration.test.ts | 35 ++++++++++--------- .../integration/identity-runtime.test.ts | 28 ++++----------- .../tests/discover-rules.test.mts | 9 ++--- 4 files changed, 33 insertions(+), 54 deletions(-) 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 18a6ae45b..816d3ab24 100644 --- a/app/packages/core-runtime/tests/integration/action-permission.test.ts +++ b/app/packages/core-runtime/tests/integration/action-permission.test.ts @@ -2,7 +2,7 @@ import { expect, it } from '@app/effect-rstest'; import { v1 } from '@authzed/authzed-node'; import { and, eq } from 'drizzle-orm'; -import { Context, Effect, Layer, Exit, Schema, flow, Predicate } from 'effect'; +import { Context, Effect, Layer, Exit, Schema, Predicate } from 'effect'; import { randomUUID } from 'node:crypto'; import type { ActionHandlerContext } from '../../src/actions/context.ts'; import { defineAction } from '../../src/actions/definition.ts'; @@ -111,9 +111,6 @@ const withDatabase = ( const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); -const promiseEffect = (promise: PromiseLike): Effect.Effect => - Effect.promise(flow(() => promise)); - const relationshipActionKeys = new Set(); type ExecutorSubject = @@ -240,7 +237,7 @@ const PermissionFixture = Layer.effect( ); const prepare = Effect.gen(function* preparePermissionFixture() { - yield* promiseEffect( + yield* Effect.promise(() => adminClient.promises.writeSchema( v1.WriteSchemaRequest.create({ schema: ONTOS_SPICEDB_SCHEMA }), ), @@ -324,7 +321,7 @@ const PermissionFixture = Layer.effect( }), ); - yield* promiseEffect( + yield* Effect.promise(() => adminClient.promises.writeRelationships( v1.WriteRelationshipsRequest.create({ updates: [ @@ -364,7 +361,7 @@ const PermissionFixture = Layer.effect( const relationshipCleanupExit = yield* Effect.exit( Effect.all( [...relationshipActionKeys].map((actionKey) => - promiseEffect( + Effect.promise(() => adminClient.promises.deleteRelationships( v1.DeleteRelationshipsRequest.create({ relationshipFilter: v1.RelationshipFilter.create({ @@ -380,7 +377,7 @@ const PermissionFixture = Layer.effect( Effect.andThen( Effect.all( [tenantId, otherTenantId].map((membershipTenantId) => - promiseEffect( + Effect.promise(() => adminClient.promises.deleteRelationships( v1.DeleteRelationshipsRequest.create({ relationshipFilter: v1.RelationshipFilter.create({ @@ -738,7 +735,7 @@ const testProgram5 = () => const key = `denial-${stage}`; const actionKey = `${actionPrefix}.${stage}`; const moduleStateKey = `${actionPrefix}.state.${stage}`; - yield* promiseEffect( + yield* Effect.promise(() => adminClient.promises.writeRelationships( v1.WriteRelationshipsRequest.create({ updates: [ 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 fe6e0a17c..d14e5f7cf 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 @@ -53,7 +53,6 @@ const tableColumns = { type MigrationColumn = (typeof tableColumns)[keyof typeof tableColumns][number]; -const databaseEffect = (operation: PromiseLike) => Effect.tryPromise(() => operation); const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); const columnDefinitions = (columns: readonly MigrationColumn[]): string => @@ -65,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 }))); @@ -81,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, @@ -100,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, @@ -113,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) @@ -139,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})`, @@ -167,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; @@ -213,8 +212,8 @@ const contactsIdentityMigrationProgram = Effect.gen(function* contactsIdentityMi 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) @@ -223,9 +222,11 @@ const contactsIdentityMigrationProgram = Effect.gen(function* contactsIdentityMi [legacyModule, contactsModule], ), ); - const collisionError = yield* Effect.flip(databaseEffect(pool.query(statements[0] ?? ''))); + const collisionError = yield* Effect.flip( + Effect.tryPromise(() => pool.query(statements[0] ?? '')), + ); expect(String(collisionError.cause)).toMatch(/would collide/u); - const collisionRows = yield* databaseEffect( + const collisionRows = yield* Effect.tryPromise(() => pool.query<{ module_key: string }>( `select module_key from ${quotedSchema}.tenant_module_states order by module_key`, ), @@ -233,9 +234,9 @@ const contactsIdentityMigrationProgram = Effect.gen(function* contactsIdentityMi 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); 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 b23c6787c..4fd34a319 100644 --- a/app/packages/core-runtime/tests/integration/identity-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/identity-runtime.test.ts @@ -81,9 +81,6 @@ const relationship = ( }), }); -const promiseEffect = (operation: () => PromiseLike) => - Effect.promise(() => operation()); - it.live( 'runs identity mutations and tenant-isolated administration through live Action and Read runtimes', () => @@ -202,11 +199,8 @@ it.live( }), ), }); - yield* promiseEffect( - spiceDbClient.promises.writeRelationships.bind( - spiceDbClient.promises, - initialRelationshipsRequest, - ), + yield* Effect.promise(() => + spiceDbClient.promises.writeRelationships(initialRelationshipsRequest), ); yield* admin.insert(tenants).values([ { @@ -478,11 +472,8 @@ it.live( }), ), }); - yield* promiseEffect( - spiceDbClient.promises.writeRelationships.bind( - spiceDbClient.promises, - systemRelationshipsRequest, - ), + yield* Effect.promise(() => + spiceDbClient.promises.writeRelationships(systemRelationshipsRequest), ); const systemCreated = yield* runIdentityAction( actionRuntime.runAction({ @@ -546,11 +537,8 @@ it.live( }), ], }); - yield* promiseEffect( - spiceDbClient.promises.writeRelationships.bind( - spiceDbClient.promises, - removeSupportRelationshipRequest, - ), + yield* Effect.promise(() => + spiceDbClient.promises.writeRelationships(removeSupportRelationshipRequest), ); yield* admin .update(principalAuthBindings) @@ -642,9 +630,7 @@ it.live( }), ), }); - return promiseEffect( - spiceDbClient.promises.writeRelationships.bind(spiceDbClient.promises, request), - ); + return Effect.promise(() => spiceDbClient.promises.writeRelationships(request)); }); const release = cleanup.pipe( Effect.ensuring(cleanupRelationships.pipe(Effect.orDie)), 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 c7cf01ba5..b0019b91c 100644 --- a/app/tools/oxlint/effect-native/tests/discover-rules.test.mts +++ b/app/tools/oxlint/effect-native/tests/discover-rules.test.mts @@ -16,14 +16,9 @@ it.effect('rule discovery loads the selected production rule and rejects unknown 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({ - catch: (cause) => cause, - try: () => discoverRules(['not-a-rule']), - }), - ); + 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).toMatchObject({ message }); + expect(error.cause).toMatchObject({ message }); }), ); From 72f7f3fdd42d47a6694a6c7ff3338a09ff5401a3 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 11:20:00 +0200 Subject: [PATCH 23/38] test(effect): compose native programs and reject Promise round trips Expose action discovery as an Effect and assert typed failures directly. Reject explicit Effect runners and Promise matcher chains hidden inside test Promise adapters. Co-Authored-By: Claude Fable 5.1 --- ...provision-current-action-authorization.mts | 31 +---- ...sion-current-action-authorization.test.mts | 110 +++++++----------- app/scripts/tests/root-environment.test.mts | 7 +- .../typecheck-project-references.test.mts | 7 +- .../rules/no-promise-shaped-port.ts | 47 ++++++-- .../example/tests/promise-round-trip.test.ts | 32 +++++ .../example/tests/promise-round-trip.test.ts | 42 +++++++ 7 files changed, 163 insertions(+), 113 deletions(-) create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/example/tests/promise-round-trip.test.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/example/tests/promise-round-trip.test.ts diff --git a/app/scripts/provision-current-action-authorization.mts b/app/scripts/provision-current-action-authorization.mts index 984c5c2e0..f28b512cd 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,7 +155,7 @@ const decodeRepositoryInventory = (workspaceRoot: string) => return { ownership, topology }; }); -const discoverCurrentActionsEffect = ( +export const discoverCurrentActions = ( workspaceRoot: string, deriveContract: DeriveContract = deriveOntosModuleDeploymentContract, ): Effect.Effect< @@ -232,23 +230,7 @@ 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, - NodeServices.NodeServices -> => discoverCurrentActionsEffect(workspaceRoot, deriveContract); - -export const discoverCurrentActions = flow( - discoverCurrentActionsProgram, - repositoryRuntime.runPromise, -); - -const discoverCurrentActionKeysProgram = ( +export const discoverCurrentActionKeys = ( workspaceRoot: string, deriveContract: DeriveContract = deriveOntosModuleDeploymentContract, ): Effect.Effect< @@ -256,15 +238,10 @@ const discoverCurrentActionKeysProgram = ( ActionAuthorizationProvisioningError, 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; } @@ -347,7 +324,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, diff --git a/app/scripts/tests/provision-current-action-authorization.test.mts b/app/scripts/tests/provision-current-action-authorization.test.mts index f15502c81..e49005186 100644 --- a/app/scripts/tests/provision-current-action-authorization.test.mts +++ b/app/scripts/tests/provision-current-action-authorization.test.mts @@ -205,11 +205,9 @@ it.effect( it.effect( 'workspace validation rejects both provisioning spellings in every automatic startup path', Effect.fn(function* testEffect5() { - const source = yield* Effect.tryPromise({ - catch: (error) => error, - try: () => - readFile(new URL('../validate-ultramodern-workspace.mts', import.meta.url), 'utf-8'), - }); + 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); @@ -221,10 +219,7 @@ it.effect( 'local:initialize': 'node ./scripts/initialize-local-development.mts', }; const validationRoot = yield* Effect.acquireRelease( - Effect.tryPromise({ - catch: (error) => error, - try: () => mkdtemp(path.join(os.tmpdir(), 'ontos-workspace-validation-')), - }), + Effect.tryPromise(() => mkdtemp(path.join(os.tmpdir(), 'ontos-workspace-validation-'))), (directory) => Effect.promise(() => rm(directory, { force: true, recursive: true })), ); let validationIndex = 0; @@ -235,34 +230,29 @@ it.effect( Effect.gen(function* testEffect6() { const modulePath = path.join(validationRoot, `validation-${validationIndex}.mjs`); validationIndex += 1; - yield* Effect.tryPromise({ - catch: (error) => error, - try: () => - 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({ - catch: (error) => error, - try: () => import(pathToFileURL(modulePath).href), - }); + 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)); }); yield* validate({}); - const validationPromises: Effect.Effect[] = []; + const validations: Effect.Effect[] = []; for (const command of [ 'node ./scripts/provision-current-action-authorization.mts', 'pnpm authorization:provision-current-actions', @@ -273,30 +263,30 @@ it.effect( 'docker-compose.yml', 'scripts/run-zerops-spicedb.sh', ]) { - validationPromises.push( + validations.push( validate({ [file]: command }).pipe( Effect.flip, Effect.map((error) => expect(() => { - throw error; + throw error.cause; }).toThrow(/must not provision Action authorization/u), ), ), ); } for (const automaticScript of ['dev', 'build', 'cloudflare:build', 'cloudflare:deploy']) { - validationPromises.push( + validations.push( validate({}, { [automaticScript]: command }).pipe( Effect.flip, Effect.map((error) => expect(() => { - throw error; + throw error.cause; }).toThrow(/must not invoke Action authorization provisioning/u), ), ), ); } - validationPromises.push( + validations.push( validate( {}, { @@ -306,13 +296,13 @@ it.effect( Effect.flip, Effect.map((error) => expect(() => { - throw error; + throw error.cause; }).toThrow(/must not provision Action authorization/u), ), ), ); } - yield* Effect.all(validationPromises, { concurrency: 'unbounded' }); + yield* Effect.all(validations, { concurrency: 'unbounded' }); }), ); @@ -329,10 +319,7 @@ it.effect( ), ); expect( - yield* Effect.tryPromise({ - catch: (error) => error, - try: () => discoverCurrentActionKeys(workspaceRoot), - }), + 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); @@ -692,10 +679,7 @@ const writeInventory = ( verticals: readonly { readonly id: string; readonly package: string; readonly path: string }[], ) => Effect.gen(function* testEffect18() { - yield* Effect.tryPromise({ - catch: (error) => error, - try: () => mkdir(path.join(root, 'topology'), { recursive: true }), - }); + yield* Effect.tryPromise(() => mkdir(path.join(root, 'topology'), { recursive: true })); yield* Effect.all( [ Effect.promise(() => @@ -755,10 +739,7 @@ it.effect( const [currentPublicAction] = currentContract.manifest.publicSurface.actions; expect(currentPublicAction !== undefined).toBe(true); const root = yield* Effect.acquireRelease( - Effect.tryPromise({ - catch: (error) => error, - try: () => mkdtemp(path.join(os.tmpdir(), 'ontos-action-discovery-')), - }), + 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' }; @@ -773,10 +754,7 @@ it.effect( }, }); const incompleteError = yield* rejectionOf( - Effect.tryPromise({ - catch: (error) => error, - try: () => discoverCurrentActionKeys(root, incomplete), - }), + discoverCurrentActionKeys(root, incomplete).pipe(Effect.provide(NodeServices.layer)), ); expect(Schema.is(NativeProvisioningError)(incompleteError)).toBe(true); expect(Schema.decodeUnknownSync(NativeProvisioningError)(incompleteError).code).toBe( @@ -796,10 +774,7 @@ it.effect( }, }); const duplicateError = yield* rejectionOf( - Effect.tryPromise({ - catch: (error) => error, - try: () => discoverCurrentActionKeys(root, duplicate), - }), + discoverCurrentActionKeys(root, duplicate).pipe(Effect.provide(NodeServices.layer)), ); expect(Schema.is(NativeProvisioningError)(duplicateError)).toBe(true); expect(Schema.decodeUnknownSync(NativeProvisioningError)(duplicateError).code).toBe( @@ -807,17 +782,12 @@ it.effect( ); yield* writeInventory(root, [vertical, vertical]); - yield* Effect.tryPromise({ - catch: (error) => error, - try: () => discoverCurrentActionKeys(root, duplicate), - }).pipe( + const duplicateInventoryError = yield* discoverCurrentActionKeys(root, duplicate).pipe( + Effect.provide(NodeServices.layer), Effect.flip, - Effect.map((error) => - expect(() => { - throw error; - }).toThrow(NativeProvisioningError), - ), ); + expect(Schema.is(NativeProvisioningError)(duplicateInventoryError)).toBe(true); + expect(duplicateInventoryError.code).toBe('action_authorization_discovery_failed'); }), ); diff --git a/app/scripts/tests/root-environment.test.mts b/app/scripts/tests/root-environment.test.mts index 27c616738..e17ee16f5 100644 --- a/app/scripts/tests/root-environment.test.mts +++ b/app/scripts/tests/root-environment.test.mts @@ -35,10 +35,9 @@ it('apps contain no environment files that can override the app-root .env', () = it.live( 'workspace discovery resolves repository, app, shell, and microvertical directories', Effect.fn(function* testEffect1() { - const { resolveAppWorkspaceRoot } = yield* Effect.tryPromise({ - catch: (error) => error, - try: () => import('../../packages/core-runtime/src/environment/workspace-environment.ts'), - }); + const { resolveAppWorkspaceRoot } = yield* Effect.tryPromise( + () => import('../../packages/core-runtime/src/environment/workspace-environment.ts'), + ); for (const directory of [ repositoryRoot, diff --git a/app/scripts/tests/typecheck-project-references.test.mts b/app/scripts/tests/typecheck-project-references.test.mts index f0397bf2b..5edc4e9e7 100644 --- a/app/scripts/tests/typecheck-project-references.test.mts +++ b/app/scripts/tests/typecheck-project-references.test.mts @@ -73,9 +73,8 @@ 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({ - catch: (error) => error, - try: () => + yield* Effect.tryPromise( + () => import( pathToFileURL( path.join( @@ -84,7 +83,7 @@ it.live( ), ).href ), - }), + ), ); const scriptPlan = Schema.decodeUnknownSync(WorkspaceScriptPlanSchema)( generator.createWorkspaceRootScriptPlan([]), 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 b2841f99f..71cb4c2e1 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 @@ -235,6 +235,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: [ { @@ -310,20 +312,24 @@ export const rule = defineRule({ } return null; }; + const staticMemberKey = (node: any): string | null => { + const key = !node.computed + ? node.property.name + : node.property.type === 'Literal' + ? node.property.value + : node.property.type === 'TemplateLiteral' && !node.property.expressions.length + ? node.property.quasis[0]?.value.cooked + : null; + return typeof key === 'string' ? key : null; + }; const imported = (raw: any, seen = new Set()): string | null => { const node = unwrap(raw); if (!node || seen.has(node)) return null; seen.add(node); if (node.type === 'MemberExpression') { const left = imported(node.object, seen); - const key = !node.computed - ? node.property.name - : node.property.type === 'Literal' - ? node.property.value - : node.property.type === 'TemplateLiteral' && !node.property.expressions.length - ? node.property.quasis[0]?.value.cooked - : null; - return left && typeof key === 'string' ? `${left}.${key}` : null; + const key = staticMemberKey(node); + return left && key !== null ? `${left}.${key}` : null; } if (node.type !== 'Identifier') return null; const variable = variableFor(node, node.name); @@ -452,6 +458,30 @@ export const rule = defineRule({ ); }; + /** 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' && + /^effect:(?:root\.)?Effect\.runPromise(?:Exit)?$/u.test(imported(node.callee) ?? '') + ) + roundTrip = true; + if (node.type !== 'MemberExpression') return; + if (!['resolves', 'rejects'].includes(staticMemberKey(node) ?? '')) return; + const assertion = unwrap(node.object); + if ( + assertion?.type === 'CallExpression' && + /^(?:@app\/effect-rstest|@rstest\/core):(?:\*\.)?expect$/u.test( + imported(assertion.callee) ?? '', + ) + ) + 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); @@ -1047,6 +1077,7 @@ export const rule = defineRule({ }; return { + CallExpression: checkTestPromiseAdapter, TSTypeReference: (node: ESTree.TSTypeReference) => { const annotation = parentOf(node as unknown as AnyNode); if (annotation?.type !== 'TSTypeAnnotation') return; 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..0ba1e7dd6 --- /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 '@app/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/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..c744f4754 --- /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 '@app/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'))), +); From 13e692ac71e9b7a6e51960e2a70e71155eaa7147 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 11:23:34 +0200 Subject: [PATCH 24/38] test(rstest): isolate generated validator execution Exercise generated validator formats against their existing owned workspace instead of racing transient contract bundles in the live source tree. Preserve valid-output and negative enforcement proofs. Co-Authored-By: Claude Fable 5.1 --- app/scripts/tests/api-only-tooling.test.mts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/scripts/tests/api-only-tooling.test.mts b/app/scripts/tests/api-only-tooling.test.mts index f05e8d1e0..db2ec1a90 100644 --- a/app/scripts/tests/api-only-tooling.test.mts +++ b/app/scripts/tests/api-only-tooling.test.mts @@ -2635,12 +2635,8 @@ it.live( 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); + // Execute against the owned workspace below; parallel tests create transient source + // bundles in the real workspace that can disappear between enumeration and reads. const generatorModulePath = publishedGeneratorModulePath(moduleFormat); const descriptorSource: unknown = From 47186bde0a6009978b88bae3ab7010126d3db7a6 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 11:29:06 +0200 Subject: [PATCH 25/38] test(effect): remove browser runtime and loader Promise round trips Keep browser programs native until router or React execution edges. Exercise the configured runtime through scoped Fibers with deterministic abort/finalizer synchronization and test module timeouts with TestClock. Co-Authored-By: Claude Fable 5.1 --- .../src/routes/[lang]/login/page.tsx | 4 +- .../[lang]/modules/[moduleId]/page.data.ts | 81 ++++---- .../routes/[lang]/modules/[moduleId]/page.tsx | 4 +- .../src/routes/[lang]/page.data.ts | 192 +++++++++--------- .../[resourceType]/[resourceId]/page.data.ts | 7 +- .../[resourceType]/[resourceId]/page.tsx | 4 +- .../src/routes/[lang]/search/page.data.ts | 7 +- .../src/routes/use-shell-controls.ts | 8 +- .../src/runtime/browser-effect-runtime.ts | 4 +- .../tests/unit/browser-effect-runtime.test.ts | 61 ++++-- .../tests/unit/routes/home/loader.test.ts | 37 +--- .../tests/unit/routes/home/page.test.tsx | 27 ++- .../tests/unit/routes/login/page.test.tsx | 12 +- .../tests/unit/routes/modules/loader.test.ts | 117 ++++++----- 14 files changed, 293 insertions(+), 272 deletions(-) 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 110742c06..582c49908 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 { return Promise.resolve(); } setMediaState('pending'); - return runBrowserEffect( + return browserRuntime.runPromise( attachResourceMedia(model.resource.ref).pipe( Effect.matchEffect({ onFailure: (error) => diff --git a/app/apps/shell-super-app/src/routes/[lang]/search/page.data.ts b/app/apps/shell-super-app/src/routes/[lang]/search/page.data.ts index 248fbc45c..033ec34dc 100644 --- a/app/apps/shell-super-app/src/routes/[lang]/search/page.data.ts +++ b/app/apps/shell-super-app/src/routes/[lang]/search/page.data.ts @@ -2,7 +2,7 @@ import { Effect, Match, Option, Schema } from 'effect'; import { Url, UrlParams } from 'effect/unstable/http'; import type { ShellSearchResponse } from '../../../../shared/api.ts'; import { searchResources } 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'; @@ -37,8 +37,8 @@ const searchFromRequest = (request: Request): typeof SearchRouteSearch.Type => { 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 @@ -90,7 +90,7 @@ export const useShellControls = (model: AuthenticatedHomePageModel | undefined) } setLegalEntitySwitchPending(true); setLegalEntitySwitchFailed(false); - void runBrowserEffect( + void browserRuntime.runPromise( Schema.decodeUnknownEffect(SwitchLegalEntityPayloadSchema)({ legalEntityId }).pipe( Effect.flatMap((payload) => switchLegalEntity(payload, { locale: language })), Effect.matchEffect({ @@ -126,7 +126,7 @@ export const useShellControls = (model: AuthenticatedHomePageModel | undefined) } setTenantSwitchPending(true); setTenantSwitchFailed(false); - void runBrowserEffect( + void browserRuntime.runPromise( Schema.decodeUnknownEffect(SwitchTenantPayloadSchema)({ tenantId }).pipe( Effect.flatMap((payload) => switchTenant(payload, { locale: language })), Effect.matchEffect({ 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/unit/browser-effect-runtime.test.ts b/app/apps/shell-super-app/tests/unit/browser-effect-runtime.test.ts index d07346614..41930f124 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,38 +1,55 @@ import { expect, it } from '@app/effect-rstest'; -import { Effect } from 'effect'; -import { runBrowserEffect } from '../../src/runtime/browser-effect-runtime.ts'; +import { Deferred, Effect, Exit, Fiber, Schema } from 'effect'; +import { browserRuntime } from '../../src/runtime/browser-effect-runtime.ts'; -it.effect('preserves Effect success and failure behavior at the browser boundary', () => - Effect.gen(function* browserBoundaryResults() { - const failure = { _tag: 'ExpectedFailure' } as const; +class ExpectedFailure extends Schema.TaggedError()('ExpectedFailure', {}) {} - yield* Effect.promise(() => - expect(runBrowserEffect(Effect.succeed('ready'))).resolves.toBe('ready'), - ); - yield* Effect.promise(() => - expect(runBrowserEffect(Effect.fail(failure))).rejects.toBe(failure), - ); +/** 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), + ); + +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* browserBoundaryInterruption() { + Effect.gen(function* browserRuntimeInterruption() { const controller = new AbortController(); - let finalized = false; - const request = runBrowserEffect( - Effect.never.pipe( - Effect.ensuring( - Effect.sync(() => { - finalized = true; - }), - ), + 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); - yield* Effect.promise(() => 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/routes/home/loader.test.ts b/app/apps/shell-super-app/tests/unit/routes/home/loader.test.ts index 85c03717f..ddb1527e9 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 @@ -3,20 +3,18 @@ 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, availableTenantsMock, browserConfigValuesMock, - browserEffectMock, currentSessionMock, shellCompositionMock, } = rstest.hoisted(() => ({ availableLegalEntitiesMock: rstest.fn(), availableTenantsMock: rstest.fn(), browserConfigValuesMock: rstest.fn<() => { readonly BETTER_AUTH_URL?: string }>(), - browserEffectMock: rstest.fn(), currentSessionMock: rstest.fn(), shellCompositionMock: rstest.fn(), })); @@ -29,10 +27,6 @@ rstest.mock('../../../../src/api/auth-client.ts', () => ({ shellComposition: shellCompositionMock, })); -rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({ - runBrowserEffect: browserEffectMock, -})); - const identity = { displayName: 'Ada Lovelace', email: 'ada@example.test', @@ -60,26 +54,15 @@ const request = () => headers: { cookie: 'session=test-session' }, }); -const loadModel = (input: Parameters[0]) => - Effect.gen(function* loadRouteModel() { - const boundary = Promise.withResolvers>>(); - let captured: Effect.Effect>, unknown> | undefined; - browserEffectMock.mockImplementationOnce( - (effect: Effect.Effect>, unknown>) => { - captured = effect.pipe( - Effect.provideService( - ConfigProvider.ConfigProvider, - ConfigProvider.fromUnknown(browserConfigValuesMock()), - ), - ); - return boundary.promise; - }, - ); - const result = loader(input); - const model = yield* captured ?? Effect.die('Route did not invoke the browser Effect boundary'); - boundary.resolve(model); - return yield* Effect.promise(() => result); - }); +const loadModel = ({ request: input }: { readonly request: Request }) => + Effect.suspend(() => + loadHomePageModel(input).pipe( + Effect.provideService( + ConfigProvider.ConfigProvider, + ConfigProvider.fromUnknown(browserConfigValuesMock()), + ), + ), + ); const withBetterAuthUrl = ( baseUrl: string, operation: () => Effect.Effect, 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 81331ec93..3afcde321 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,4 +1,4 @@ -import { runBrowserEffect } from '../../../../src/runtime/browser-effect-runtime.ts' with { +import { browserRuntime } from '../../../../src/runtime/browser-effect-runtime.ts' with { rstest: 'importActual', }; import { afterEach, beforeEach, expect, rstest, it } from '@app/effect-rstest'; @@ -17,14 +17,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({ @@ -86,7 +91,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)( @@ -150,7 +155,7 @@ const authenticatedModel = (): HomePageModel => ({ beforeEach(() => { navigateMock.mockResolvedValue(undefined); - runBrowserEffectMock.mockImplementation(runBrowserEffect); + browserRunPromiseMock.mockImplementation(browserRuntime.runPromise); signOutMock.mockReturnValue(Effect.succeed({ signedOut: true })); switchTenantMock.mockReturnValue(Effect.succeed({ selectedTenantId: tenantId2 })); switchLegalEntityMock.mockReturnValue(Effect.succeed({ selectedLegalEntityId: legalEntityId2 })); 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 1cccadcde..bf1420939 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,4 +1,4 @@ -import { runBrowserEffect } from '../../../../src/runtime/browser-effect-runtime.ts' with { +import { browserRuntime } from '../../../../src/runtime/browser-effect-runtime.ts' with { rstest: 'importActual', }; import { afterEach, beforeEach, expect, rstest, it } from '@app/effect-rstest'; @@ -8,15 +8,15 @@ 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(() => ({ +const { browserRunPromiseMock, navigateMock, signInMock } = rstest.hoisted(() => ({ + browserRunPromiseMock: rstest.fn(), navigateMock: rstest.fn(), - runBrowserEffectMock: rstest.fn(), signInMock: rstest.fn(), })); beforeEach(() => { navigateMock.mockImplementation(() => Promise.resolve()); - runBrowserEffectMock.mockImplementation(runBrowserEffect); + browserRunPromiseMock.mockImplementation(browserRuntime.runPromise); signInMock.mockReturnValue( Effect.succeed({ identity: { @@ -66,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 *' }); @@ -240,7 +240,7 @@ it.effect('submits valid values through the Shell authentication client and navi }, { locale: 'en' }, ); - expect(runBrowserEffectMock).toHaveBeenCalledTimes(1); + expect(browserRunPromiseMock).toHaveBeenCalledTimes(1); expect(navigateMock).toHaveBeenCalledWith({ to: '/en/' }); expect(getSubmit().hasAttribute('disabled')).toBe(false); expect(screen.queryByText('shell.login.error.internal')).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 fd1fb5c8c..6f383fcd3 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,10 +1,11 @@ import { beforeEach, expect, rstest, it } from '@app/effect-rstest'; -import { Effect } from 'effect'; +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'; @@ -47,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', @@ -74,11 +81,12 @@ it('selects only declared safe route parameters and omits overlong values', () = ).toEqual({ id: 'party-1' }); }); -it.live('retains only declared bounded route parameters outside the resolved target identity', () => - Effect.gen(function* retainsOnlyDeclaredBoundedRouteParameters() { - expect( - yield* Effect.promise(() => - loader({ +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', @@ -86,30 +94,27 @@ it.live('retains only declared bounded route parameters outside the resolved tar 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), - ); - }), + ).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), + ); + }), ); -it.live('retains module landing behavior when no exact page entrypoint is supplied', () => +it.effect('retains module landing behavior when no exact page entrypoint is supplied', () => Effect.gen(function* retainsModuleLandingBehaviorWhenNo() { expect( - yield* Effect.promise(() => - loader({ params: { moduleId: 'party.registry' }, request: request() }), - ), + yield* moduleModel({ params: { moduleId: 'party.registry' }, request: request() }), ).toMatchObject({ routeParams: {} }); expect(resolveModuleTargetMock).toHaveBeenCalledWith( { moduleId: 'party.registry' }, @@ -118,25 +123,23 @@ it.live('retains module landing behavior when no exact page entrypoint is suppli }), ); -it.live('does not request or load a private target before authentication', () => +it.effect('does not request or load a private target before authentication', () => Effect.gen(function* doesNotRequestOrLoadA() { - loadHomePageModelMock.mockResolvedValueOnce({ state: 'anonymous' }); + loadHomePageModelMock.mockReturnValueOnce(Effect.succeed({ state: 'anonymous' })); expect( - yield* Effect.promise(() => - loader({ - params: { - entrypointKey: 'party.registry.page.contacts', - moduleId: 'party.registry', - }, - request: request(), - }), - ), + yield* moduleModel({ + params: { + entrypointKey: 'party.registry.page.contacts', + moduleId: 'party.registry', + }, + request: request(), + }), ).toMatchObject({ state: 'selection_required' }); expect(resolveModuleTargetMock).not.toHaveBeenCalled(); }), ); -it.live.each([ +it.effect.each([ ['ShellSelectionRequiredProblem', 'selection_required'], ['ShellTargetForbiddenProblem', 'forbidden'], ['ShellTargetNotFoundProblem', 'not_found'], @@ -145,15 +148,31 @@ it.live.each([ Effect.gen(function* ShellSelectionRequiredProblem() { resolveModuleTargetMock.mockReturnValueOnce(Effect.fail({ _tag })); expect( - yield* Effect.promise(() => - loader({ - params: { - entrypointKey: 'party.registry.page.contacts', - moduleId: 'party.registry', - }, - request: request(), - }), - ), + 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(); + }), +); From 87d10f5c4e9c3cf192362e0f3864f11ac590a641 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 11:31:17 +0200 Subject: [PATCH 26/38] test(rstest): remove forced Core serialization Restore Rstest's native N-1 worker default. Core integrations passed four explicit nine-worker stress runs, then all workspace integrations passed with nine workers after removing the override. Co-Authored-By: Claude Fable 5.1 --- app/packages/core-runtime/rstest.config.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/packages/core-runtime/rstest.config.ts b/app/packages/core-runtime/rstest.config.ts index b82ae3431..671f8b7a8 100644 --- a/app/packages/core-runtime/rstest.config.ts +++ b/app/packages/core-runtime/rstest.config.ts @@ -1,10 +1,6 @@ import { defineConfig } from '@rstest/core'; export default defineConfig({ - // Integration repositories match the shared database outbox globally. - // Rstest only supports a root pool, so the whole package runs serially to prevent - // independent subscription catalogs from consuming each other. - pool: { maxWorkers: 1 }, projects: [ { include: ['tests/unit/**/*.test.ts'], name: 'unit', testEnvironment: 'node' }, { From 2c7a151f22f90283cd23f032eb3bd999a1549acb Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 11:49:38 +0200 Subject: [PATCH 27/38] fix(lint): recognize genuine Playwright extended test bindings Co-Authored-By: Claude Fable 5.1 --- .../rules/no-promise-shaped-port.ts | 5 ++++ .../e2e/playwright-dynamic-extend.spec.ts | 11 ++++++++ .../playwright-extend-owned-service.spec.ts | 14 +++++++++++ .../tests/e2e/playwright-fake-factory.spec.ts | 10 ++++++++ .../e2e/playwright-mutable-extend.spec.ts | 11 ++++++++ .../tests/e2e/playwright-shadowed.spec.ts | 14 +++++++++++ .../tests/e2e/rstest-extend.spec.ts | 10 ++++++++ .../tests/unit/playwright-extended.test.ts | 25 +++++++++++++++++++ 8 files changed, 100 insertions(+) create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-dynamic-extend.spec.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-extend-owned-service.spec.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-fake-factory.spec.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-mutable-extend.spec.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/playwright-shadowed.spec.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/e2e/rstest-extend.spec.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/playwright-extended.test.ts 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 71cb4c2e1..733977b81 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 @@ -331,6 +331,11 @@ export const rule = defineRule({ 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; if (node.type !== 'Identifier') return null; const variable = variableFor(node, node.name); for (const def of variable?.defs ?? []) { 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..a329bdd78 --- /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 '@app/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/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))); +}); From 4bf1565605e1223a64c4a1a11f28e2a17ddc2957 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 12:02:10 +0200 Subject: [PATCH 28/38] fix(lint): close destructured and partial tag assertion gaps Co-Authored-By: Claude Fable 5.1 --- .../unit/module-entrypoint-loader.test.ts | 2 +- .../rules/no-manual-tag-comparison.ts | 84 +++++++++++++++++++ .../src/destructured-assertions.ts | 17 ++++ .../src/partial-object-assertions.ts | 10 +++ .../src/destructured-assertions.ts | 30 +++++++ 5 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/destructured-assertions.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/partial-object-assertions.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/destructured-assertions.ts 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 8837d611d..c78d6c32a 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 @@ -168,7 +168,7 @@ it.effect('checks the complete composition before authorizing or invoking any lo })), ), ); - expect(error).toMatchObject({ _tag: 'ModuleStateDeniedError' }); + expect(Schema.is(ModuleStateDeniedError)(error)).toBe(true); expect(authorizations).toBe(0); expect(loads).toBe(0); }), 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 a273294fe..b19f3b578 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 @@ -293,6 +293,42 @@ function constInitialiser(context: Context, node: ESTree.Node): ESTree.Node | nu return declarator.init ?? null; } +/** 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 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' || + declarator.id.type !== 'ObjectPattern' || + declarator.init === null || + declarator.parent.type !== 'VariableDeclaration' || + declarator.parent.kind !== 'const' + ) + return null; + for (const property of declarator.id.properties) { + if ( + property.type !== 'Property' || + property.value.type !== 'Identifier' || + property.value.name !== node.name + ) + continue; + const method = + !property.computed && property.key.type === 'Identifier' + ? property.key.name + : staticString(context, property.key); + return method === null ? null : { source: declarator.init, method }; + } + return null; +} + /** Resolve assertion imports through lexical bindings, aliases, and matcher modifiers. */ function assertionCall( context: Context, @@ -304,6 +340,12 @@ function assertionCall( let subject: ESTree.CallExpression | null = null; for (let depth = 0; depth < MAX_DEPTH && !seen.has(expression); depth += 1) { seen.add(expression); + const destructured = destructuredMethod(context, expression); + if (destructured !== null) { + members.unshift(destructured.method); + expression = unwrap(destructured.source); + continue; + } const initialiser = constInitialiser(context, expression); if (initialiser !== null) { expression = unwrap(initialiser); @@ -1015,6 +1057,7 @@ export const rule = defineRule({ 'toContain', 'toContainEqual', 'toMatch', + 'toMatchObject', 'match', 'doesNotMatch', ]); @@ -1022,6 +1065,47 @@ export const rule = defineRule({ let compared = node.arguments.slice(0, 2); if (assertion.subject !== null) compared = [...assertion.subject.arguments.slice(0, 1), ...node.arguments.slice(0, 1)]; + // Partial-object matchers discriminate by the expected shape, not a tag read. + // Only inspect the expected top-level discriminant; unrelated fields/fixtures are not probes. + if (assertion.subject !== null && assertion.method === 'toMatchObject') { + const expected = node.arguments[0]; + if (expected !== undefined && expected.type !== 'SpreadElement') { + let shape = unwrap(expected); + const seenShapes = new Set(); + for (let depth = 0; depth < MAX_DEPTH && !seenShapes.has(shape); depth += 1) { + seenShapes.add(shape); + const initialiser = constInitialiser(context, shape); + if (initialiser === null) break; + shape = unwrap(initialiser); + } + if (shape.type === 'ObjectExpression') { + const tag = shape.properties.find( + (property) => + property.type === 'Property' && + (!property.computed && property.key.type === 'Identifier' + ? property.key.name + : staticString(context, property.key)) === TAG_PROPERTY, + ); + if (tag?.type === 'Property') { + const literal = staticString(context, tag.value); + if ((literal === null || !exempt.has(literal)) && !suppressed(node)) { + context.report({ + node, + messageId: 'tagEqualityCall', + data: { + callee: describe(context, node.callee), + text: describe( + context, + assertion.subject.arguments[0] ?? assertion.subject, + ), + }, + }); + return; + } + } + } + } + } for (const [index, argument] of compared.entries()) { if (argument.type === 'SpreadElement') continue; const reference = comparedTag(argument); 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..62b2245a2 --- /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 '@app/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/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..9db5ea9e6 --- /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 '@app/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/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..fb95ed3ad --- /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,30 @@ +import assert from 'node:assert/strict'; +import { expect } from '@app/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('@app/effect-rstest').expect) { + expect(error).toMatchObject({ _tag: 'Missing' }); +} +export { shadow, shadowExpect }; + +const field = '_tag'; +expect(error).toMatchObject({ field: 'Missing' }); From 0b0dad1b427432e288a7ab68036d4146c0869e84 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 12:19:56 +0200 Subject: [PATCH 29/38] test(e2e): enforce isolated parallel browser coverage in CI Own authentication identities per worker; bound native acquisition without abandoning scoped cleanup. Wait for the real hydrated account menu before post-reload interaction. Run all browser tests with N-1 workers and no retries locally and in the integration gate. Co-Authored-By: Claude Fable 5.1 --- .../workflows/ultramodern-workspace-gates.yml | 8 + app/apps/shell-super-app/playwright.config.ts | 8 +- .../shell-super-app/tests/e2e/auth-fixture.ts | 160 +++++++++++++++--- .../shell-super-app/tests/e2e/login.spec.ts | 150 ++++++++++------ .../tests/e2e/worker-fixture-lifetime.spec.ts | 41 +++++ 5 files changed, 293 insertions(+), 74 deletions(-) create mode 100644 app/apps/shell-super-app/tests/e2e/worker-fixture-lifetime.spec.ts diff --git a/.github/workflows/ultramodern-workspace-gates.yml b/.github/workflows/ultramodern-workspace-gates.yml index fbc5d7094..aebcf5b35 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/playwright.config.ts b/app/apps/shell-super-app/playwright.config.ts index f146bd76d..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'; @@ -33,6 +35,7 @@ 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', @@ -40,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: [ { @@ -64,4 +67,5 @@ export default defineConfig({ url: `${origin}/en`, }, ], + workers: Math.max(1, availableParallelism() - 1), }); 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 869398ecd..c23946b40 100644 --- a/app/apps/shell-super-app/tests/e2e/auth-fixture.ts +++ b/app/apps/shell-super-app/tests/e2e/auth-fixture.ts @@ -1,5 +1,13 @@ -import { Effect } from 'effect'; +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 { betterAuth } from 'better-auth'; @@ -17,35 +25,135 @@ import { import { loadAuthConfig } from '../../api/auth/config.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', +// 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; + +class E2eAuthorizationFixtureError extends Schema.TaggedError()( + 'E2eAuthorizationFixtureError', + { reason: Schema.String }, +) {} + +interface FixtureTenant { + readonly legalEntityId: string; + readonly name: string; + readonly principalId: string; + readonly tenantId: string; +} + +interface FixtureTenants { + readonly first: FixtureTenant; + readonly second: FixtureTenant; +} + +// 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()), + ); + 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 })), + }), + ), + ); + // 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); }, -} as const; +); 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 corePool = yield* acquirePoolResource(() => new Pool({ connectionString })); + 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({ @@ -136,7 +244,6 @@ export const createAuthenticationFixture = Effect.fn('createAuthenticationFixtur ); yield* Effect.addFinalizer(() => cleanup().pipe(Effect.orDie)); - yield* cleanup(); const createdUser = yield* Effect.tryPromise( async () => await authentication.api.signUpEmail({ @@ -151,14 +258,14 @@ export const createAuthenticationFixture = Effect.fn('createAuthenticationFixtur { defaultLocale: 'en', name: e2eTenants.first.name, - slug: 'e2e-alpha-tenant', + slug: `e2e-alpha-${fixtureId}`, status: 'active', tenantId: e2eTenants.first.tenantId, }, { defaultLocale: 'en', name: e2eTenants.second.name, - slug: 'e2e-zeta-tenant', + slug: `e2e-zeta-${fixtureId}`, status: 'active', tenantId: e2eTenants.second.tenantId, }, @@ -218,10 +325,15 @@ export const createAuthenticationFixture = Effect.fn('createAuthenticationFixtur }, ]); yield* coreDatabase.insert(tenantModuleStates).values([ - { moduleKey: 'party.registry', state: 'active', tenantId: e2eTenants.first.tenantId }, - { moduleKey: 'party.registry', state: 'active', tenantId: e2eTenants.second.tenantId }, + { 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 bbdcaab75..963a2ce23 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 { Effect, Exit, Predicate, Scope } 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,14 +60,30 @@ const gotoHydratedLogin = async (page: Page, language: 'cs' | 'en') => { }); }; -// Playwright hooks are the Promise boundary for this scoped Effect fixture. -const fixtureScope = Effect.runSync(Scope.make()); - -test.beforeAll( - async () => - await Effect.runPromise(createAuthenticationFixture().pipe(Scope.provide(fixtureScope))), -); -test.afterAll(async () => await Effect.runPromise(Scope.close(fixtureScope, Exit.void))); +// 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 }) => await page @@ -91,13 +130,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 () => @@ -114,7 +153,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'); @@ -138,8 +180,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) => @@ -152,13 +194,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)); @@ -171,8 +216,8 @@ test('loads localized English and Czech Contacts pages only after login', async const form = hydratedLoginForm(page); await form .getByRole('textbox', { name: /^Přihlašovací jméno\s*\*$/u }) - .fill(e2eCredentials.email); - await form.getByLabel(/^Heslo/u).fill(e2eCredentials.password); + .fill(authentication.credentials.email); + await form.getByLabel(/^Heslo/u).fill(authentication.credentials.password); await form.getByRole('button', { name: 'Přihlásit se' }).click(); await expect(page).toHaveURL(/\/cs\/?$/u); await expect(page.getByText('Nasazení modulu je dočasně nedostupné.')).toHaveCount(0); @@ -221,12 +266,13 @@ 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 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); await form.getByRole('button', { name: 'Login' }).click(); await expect(page).toHaveURL(/\/en\/?$/u); @@ -248,6 +294,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') @@ -255,13 +302,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)) @@ -269,11 +316,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()) @@ -291,18 +339,19 @@ 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 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); await form.getByRole('button', { name: 'Login' }).click(); await expect(page).toHaveURL(/\/en\/?$/u); 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) => @@ -311,25 +360,26 @@ 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; @@ -337,8 +387,8 @@ test('retains Czech tenant context after one failed switch and supports keyboard const form = hydratedLoginForm(page); await form .getByRole('textbox', { name: /^Přihlašovací jméno\s*\*$/u }) - .fill(e2eCredentials.email); - await form.getByLabel(/^Heslo/u).fill(e2eCredentials.password); + .fill(authentication.credentials.email); + await form.getByLabel(/^Heslo/u).fill(authentication.credentials.password); await form.getByRole('button', { name: 'Přihlásit se' }).click(); await expect(page).toHaveURL(/\/cs\/?$/u); await page.route(`**${shellAuthenticationApiContract.switchTenantPath}`, async (route) => { @@ -351,16 +401,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'); @@ -372,11 +422,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; @@ -384,13 +435,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 () => @@ -459,13 +512,14 @@ 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 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); await form.getByRole('button', { name: 'Login' }).click(); await expect(page).toHaveURL(/\/en\/?$/u); await page.route( @@ -481,11 +535,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']); +}); From 1733f1b766f6b40a33e8b6538e39e1731e746534 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 12:31:37 +0200 Subject: [PATCH 30/38] test(effect): assert object variants through native schemas and predicates Reject expected object discriminants in equality and containment matchers, including Node deep assertions. Migrate 59 assertions across 21 suites while preserving exact payload and extra-field checks. Add ten failing provenance-aware matcher regressions and migrate the old positive fixture. Co-Authored-By: Claude Fable 5.1 --- .../tests/unit/auth-contract.test.ts | 14 +- .../tests/unit/action-runtime.test.ts | 40 ++- .../unit/application-composition.test.ts | 9 +- .../unit/commit-recovery-metadata.test.ts | 6 +- .../tests/unit/client-runtime.test.ts | 5 +- .../tests/unit/gateway-context.test.ts | 14 +- .../tests/unit/problem-details.test.ts | 26 +- .../rules/no-manual-tag-comparison.ts | 19 +- .../src/object-equality-assertions.ts | 17 ++ .../src/destructured-assertions.ts | 14 + .../core-runtime/src/native-assertions.ts | 4 +- .../api-integration-command-client.test.ts | 6 +- .../api-integration-command-contract.test.ts | 10 +- .../api-integration-command-recovery.test.ts | 44 +-- .../api-integration-command-runtime.test.ts | 17 +- .../tests/unit/ares-lookup-read.test.ts | 10 +- .../counterparty-persistence.service.test.ts | 6 +- .../tests/unit/identity-contract.test.ts | 7 +- .../unit/identity-persistence.service.test.ts | 47 ++-- .../tests/unit/merge-alias-resolution.test.ts | 21 +- .../unit/merge-collision-reference.test.ts | 10 +- .../unit/merge-readiness-contract.test.ts | 6 +- .../unit/merge-survivor-selection.test.ts | 20 +- .../unit/relationship-domain-contract.test.ts | 263 +++++++++--------- .../tests/unit/search-semantics.test.ts | 7 +- 25 files changed, 385 insertions(+), 257 deletions(-) create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/object-equality-assertions.ts 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 32d641e97..382d5abed 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,5 +1,5 @@ import { expect, it } from '@app/effect-rstest'; -import { DateTime, Effect, Schema, SchemaAST, Predicate } from 'effect'; +import { DateTime, Effect, Schema, SchemaAST, Predicate, Struct } from 'effect'; import { AuthenticationUnavailableProblemSchema, CurrentSessionSchema, @@ -136,13 +136,19 @@ it('preserves migrated Shell Problem Details wire shapes and ordered membership' 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', 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 60a696f77..df2756d42 100644 --- a/app/packages/core-runtime/tests/unit/action-runtime.test.ts +++ b/app/packages/core-runtime/tests/unit/action-runtime.test.ts @@ -1,5 +1,16 @@ import { expect, it } from '@app/effect-rstest'; -import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Option, Predicate, Schema } from 'effect'; +import { + Cause, + DateTime, + Deferred, + Effect, + Exit, + Fiber, + Option, + Predicate, + Schema, + Struct, +} from 'effect'; import { ConnectionError, SqlError } from 'effect/unstable/sql/SqlError'; import { defineAction, @@ -31,7 +42,11 @@ import { makeActionRepository, } from '../../src/actions/repository.ts'; import type { ActionRuntimeStage } from '../../src/actions/runtime.ts'; -import { ACTION_RUNTIME_STAGES, makeActionRuntime } from '../../src/actions/runtime.ts'; +import { + ACTION_RUNTIME_STAGES, + ActionCommitOpenSchema, + makeActionRuntime, +} from '../../src/actions/runtime.ts'; import type { PrincipalManagementRepositoryService } from '../../src/auth/principal-management.ts'; import { PrincipalManagementRepository } from '../../src/auth/principal-management.ts'; import { supportRecoveryPrincipalContextResolverFromRepository } from '../../src/auth/support-recovery-principal-context.ts'; @@ -489,15 +504,22 @@ it.effect( expect(Reflect.ownKeys(persistenceFailure)).toEqual(Reflect.ownKeys(publicPersistence)); expectSameJson(transactionFailure, publicTransaction); expectSameJson(persistenceFailure, publicPersistence); - expect(yield* Schema.encodeEffect(ActionTransactionError)(transactionFailure)).toEqual({ - _tag: 'ActionTransactionError', + 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( - yield* Schema.encodeEffect(ActionInvocationPersistenceError)(persistenceFailure), - ).toEqual({ - _tag: 'ActionInvocationPersistenceError', + Schema.is(Schema.toEncoded(ActionInvocationPersistenceError))(encodedPersistenceFailure), + ).toBe(true); + expect(Struct.omit(encodedPersistenceFailure, ['_tag'])).toEqual({ code: persistenceFailure.code, reason: persistenceFailure.reason, }); @@ -2370,8 +2392,8 @@ it.effect( unavailable.runtime.resolveActionCommit({ invocationId, principal }), ); - expect(openResolution).toEqual({ - _tag: 'ActionCommitOpen', + expect(Schema.is(ActionCommitOpenSchema)(openResolution)).toBe(true); + expect(Struct.omit(openResolution, ['_tag'])).toEqual({ invocationId, }); expect(Predicate.isTagged(committedResolution, 'ActionAlreadyCommitted')).toBe(true); 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 f12cc1db8..7f06d1d5c 100644 --- a/app/packages/core-runtime/tests/unit/application-composition.test.ts +++ b/app/packages/core-runtime/tests/unit/application-composition.test.ts @@ -1,5 +1,5 @@ import { expect, it } from '@app/effect-rstest'; -import { Effect, Schema } from 'effect'; +import { Effect, Schema, Struct } from 'effect'; import { canonicalizeApplicationComposition, ApplicationCompositionSchema, @@ -114,8 +114,11 @@ const federationManifest = (observations: Evidence) => it.effect('defaults the validation error code without changing its encoded contract', () => Effect.gen(function* encodeValidationError() { const error = new ApplicationCompositionValidationError({ reason: 'Invalid candidate' }); - expect(yield* Schema.encodeEffect(ApplicationCompositionValidationError)(error)).toEqual({ - _tag: 'ApplicationCompositionValidationError', + 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', }); 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 e2dd7a9a5..a63622011 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 @@ -2,7 +2,7 @@ import { expect, it } from '@app/effect-rstest'; /* oxlint-disable sonarjs/no-undefined-assignment -- Existing compatibility boundary; expires: 2026-12-31. */ -import { Effect, Schema, Predicate } from 'effect'; +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'; @@ -92,7 +92,9 @@ it.effect('committed error schema requires and preserves the recovery invocation reason: 'This idempotency key already committed successfully', } as const; const decoded = yield* Schema.decodeUnknownEffect(ActionAlreadyCommitted)(encoded); - expect(yield* decoded.pipe(Schema.encodeEffect(ActionAlreadyCommitted))).toEqual(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', 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 43a841cce..01f687994 100644 --- a/app/packages/shared-contracts/tests/unit/client-runtime.test.ts +++ b/app/packages/shared-contracts/tests/unit/client-runtime.test.ts @@ -9,7 +9,7 @@ import { HttpApiSchema, Schema, } from '@modern-js/plugin-bff/effect-client'; -import { Predicate } from 'effect'; +import { Predicate, Struct } from 'effect'; import { FetchHttpClient } from 'effect/unstable/http'; const RepresentativeConflictSchema = Schema.TaggedStruct('RepresentativeConflict', { @@ -247,7 +247,8 @@ it.effect('keeps declared backend failures in the typed Effect error channel', ( Effect.provideService(FetchHttpClient.Fetch, fakeFetch), ); - expect(outcome).toEqual(problem); + expect(Schema.is(RepresentativeConflictSchema)(outcome)).toBe(true); + expect(Struct.omit(outcome, ['_tag'])).toEqual(Struct.omit(problem, ['_tag'])); }), ); 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 a2e322230..4631d8da3 100644 --- a/app/packages/shared-contracts/tests/unit/gateway-context.test.ts +++ b/app/packages/shared-contracts/tests/unit/gateway-context.test.ts @@ -1,6 +1,6 @@ import { expect, it } from '@app/effect-rstest'; import { TrustedPrincipalContextSchema } from '@app/core-runtime/actions/principal-context'; -import { Effect, Schema, SchemaAST } from 'effect'; +import { Effect, Schema, SchemaAST, Struct } from 'effect'; import { ApiKeyGatewayHeadersSchema, GatewayContextApiGroup, @@ -176,12 +176,12 @@ it('preserves migrated gateway Problem Details shapes and ordered endpoint membe title: 'Gateway unavailable', type: 'https://ontos.dev/problems/gateway-unavailable', } as const; - expect(Schema.decodeUnknownSync(GatewayRateLimitedProblemSchema)(rateLimited)).toEqual( - rateLimited, - ); - expect(Schema.decodeUnknownSync(GatewayUnavailableProblemSchema)(unavailable)).toEqual( - unavailable, - ); + 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, 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 95ddbf96d..38598812c 100644 --- a/app/packages/shared-contracts/tests/unit/problem-details.test.ts +++ b/app/packages/shared-contracts/tests/unit/problem-details.test.ts @@ -8,7 +8,7 @@ import { HttpRouter, HttpServer, } from '@modern-js/plugin-bff/effect-edge'; -import { Context, Effect, Layer, Schema, SchemaAST } from 'effect'; +import { Context, Effect, Layer, Predicate, Schema, SchemaAST, Struct } from 'effect'; import { FetchHttpClient } from 'effect/unstable/http'; import { makeProblemDetailsSchema, @@ -32,11 +32,19 @@ for (const status of statuses) { type: `urn:ontos:test:problem:${status}`, } as const; - expect(Schema.decodeUnknownSync(schema)(problem)).toEqual(problem); - expect(Schema.encodeUnknownSync(schema)(problem)).toEqual(problem); + 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); - expect(schema.ast.annotations?.['~httpApiEncoding']).toEqual({ - _tag: 'Json', + 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(() => Schema.decodeUnknownSync(schema)({ ...problem, status: 418 })).toThrow(); @@ -63,7 +71,9 @@ it('adds only the deliberate retryable literal marker', () => { type: 'urn:ontos:test:retryable', } as const; - expect(Schema.decodeUnknownSync(schema)(problem)).toEqual(problem); + 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(); }); @@ -300,7 +310,9 @@ it('accepts concrete JSON literals and schemas with JSON-safe encodings', () => expect(decoded.attachment).toEqual(new Uint8Array([1, 2])); expect(decoded.count).toBe(7n); expect(decoded.occurredAt.toISOString()).toBe(encoded.occurredAt); - expect(Schema.encodeUnknownSync(schema)(decoded)).toEqual(encoded); + 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/tools/oxlint/effect-native/rules/no-manual-tag-comparison.ts b/app/tools/oxlint/effect-native/rules/no-manual-tag-comparison.ts index b19f3b578..9987112ff 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 @@ -1065,10 +1065,21 @@ export const rule = defineRule({ let compared = node.arguments.slice(0, 2); if (assertion.subject !== null) compared = [...assertion.subject.arguments.slice(0, 1), ...node.arguments.slice(0, 1)]; - // Partial-object matchers discriminate by the expected shape, not a tag read. + // Object matchers discriminate by the expected shape, not a tag read. // Only inspect the expected top-level discriminant; unrelated fields/fixtures are not probes. - if (assertion.subject !== null && assertion.method === 'toMatchObject') { - const expected = node.arguments[0]; + if ( + [ + 'toMatchObject', + 'toEqual', + 'toStrictEqual', + 'toContainEqual', + 'deepEqual', + 'deepStrictEqual', + 'notDeepEqual', + 'notDeepStrictEqual', + ].includes(assertion.method) + ) { + const expected = node.arguments[assertion.subject === null ? 1 : 0]; if (expected !== undefined && expected.type !== 'SpreadElement') { let shape = unwrap(expected); const seenShapes = new Set(); @@ -1096,7 +1107,7 @@ export const rule = defineRule({ callee: describe(context, node.callee), text: describe( context, - assertion.subject.arguments[0] ?? assertion.subject, + assertion.subject?.arguments[0] ?? node.arguments[0] ?? node, ), }, }); 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..b4c7b354e --- /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 '@app/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/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 index fb95ed3ad..57f67f5de 100644 --- 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 @@ -28,3 +28,17 @@ 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('@app/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/native-assertions.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/native-assertions.ts index 031ad0643..4c0739b09 100644 --- 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 @@ -1,9 +1,9 @@ -import { Predicate, Schema, Exit, Match } from 'effect'; +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(error, { _tag: 'Missing', message: 'gone' }); +assert.deepEqual(Struct.omit(error, ['_tag']), { message: 'gone' }); assert.fail(`Unexpected failure ${error._tag}`); expect(Predicate.isTagged(error, 'Missing')).toBe(true); 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 7cffd034f..b41bf25a1 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,5 +1,6 @@ import { expect, it } from '@app/effect-rstest'; -import { Effect, Result } from 'effect'; +import { Effect, Result, Schema, Struct } from 'effect'; +import { PartyCommandConflictProblemSchema } from '../../shared/command-api.ts'; import { FetchHttpClient } from 'effect/unstable/http'; import { requestSearchRebuild, @@ -87,7 +88,8 @@ it.effect('decodes declared errors without weakening their tag or stable conflic if (!Result.isFailure(outcome)) { throw new Error('Expected truthy value'); } - expect(outcome.failure).toEqual(problem); + expect(Schema.is(PartyCommandConflictProblemSchema)(outcome.failure)).toBe(true); + expect(Struct.omit(outcome.failure, ['_tag'])).toEqual(Struct.omit(problem, ['_tag'])); }), ); 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 e1eac5fe5..2b032df2b 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,7 +1,7 @@ // @effect-diagnostics nodeBuiltinImport:off -- Inspect source files through the Node filesystem boundary; expires: 2026-12-31. import { expect, it } from '@app/effect-rstest'; import { readFile, readdir } from 'node:fs/promises'; -import { Effect, Schema } from 'effect'; +import { Effect, Schema, Struct } from 'effect'; import { partyRegistryCommandsApi, PartyCommandAliasWriteRejectedProblemSchema, @@ -64,9 +64,11 @@ it.effect('alias conflict preserves both canonical and submitted references', () title: 'Canonical Party required', type: 'urn:ontos:party:alias-write-rejected', }; - expect( - yield* Schema.decodeUnknownEffect(PartyCommandAliasWriteRejectedProblemSchema)(input), - ).toEqual(input); + const decodedProblem = yield* Schema.decodeUnknownEffect( + PartyCommandAliasWriteRejectedProblemSchema, + )(input); + expect(Schema.is(PartyCommandAliasWriteRejectedProblemSchema)(decodedProblem)).toBe(true); + expect(Struct.omit(decodedProblem, ['_tag'])).toEqual(Struct.omit(input, ['_tag'])); }), ); 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 d4dcbdda1..013788ec4 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,5 +1,5 @@ import { expect, it } from '@app/effect-rstest'; -import { Effect, Match, Result, Schema } from 'effect'; +import { Effect, Match, Result, Schema, Struct } from 'effect'; import { FetchHttpClient } from 'effect/unstable/http'; import { PartyCommandCommitIndeterminateProblemSchema, @@ -33,9 +33,11 @@ it.effect('already committed is terminal and carries the invocation for governed title: 'Already committed', type: 'urn:ontos:party:already-committed', }; - expect( - yield* Schema.decodeUnknownEffect(PartyCommandAlreadyCommittedProblemSchema)(problem), - ).toEqual(problem); + const decodedProblem = yield* Schema.decodeUnknownEffect( + PartyCommandAlreadyCommittedProblemSchema, + )(problem); + expect(Schema.is(PartyCommandAlreadyCommittedProblemSchema)(decodedProblem)).toBe(true); + expect(Struct.omit(decodedProblem, ['_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); } @@ -62,9 +64,11 @@ it.effect( title: 'Commit outcome unknown', type: 'urn:ontos:party:commit-indeterminate', }; - expect( - yield* Schema.decodeUnknownEffect(PartyCommandCommitIndeterminateProblemSchema)(problem), - ).toEqual(problem); + const decodedProblem = yield* Schema.decodeUnknownEffect( + PartyCommandCommitIndeterminateProblemSchema, + )(problem); + expect(Schema.is(PartyCommandCommitIndeterminateProblemSchema)(decodedProblem)).toBe(true); + expect(Struct.omit(decodedProblem, ['_tag'])).toEqual(Struct.omit(problem, ['_tag'])); for (const endpoint of Object.values( partyRegistryCommandsApi.groups.partyCommands.endpoints, )) { @@ -90,14 +94,18 @@ it.effect('recovery is separate from the unchanged set of explicit mutation endp const endpoint = partyRegistryCommandRecoveryApi.groups.partyCommandRecovery.endpoints.resolve; expect(endpoint.path).toBe('/party-registry/action-commits/resolve'); for (const state of ['OPEN', 'COMMITTED']) { - expect( - yield* Schema.decodeUnknownEffect(ResolvePartyCommandCommitResultSchema)({ - _tag: 'PartyCommandCommitResolution', - invocationId, - retryCommand: false, - state, - }), - ).toEqual({ _tag: 'PartyCommandCommitResolution', invocationId, retryCommand: false, state }); + const resolution = yield* Schema.decodeUnknownEffect(ResolvePartyCommandCommitResultSchema)({ + _tag: 'PartyCommandCommitResolution', + invocationId, + retryCommand: false, + state, + }); + expect(Schema.is(ResolvePartyCommandCommitResultSchema)(resolution)).toBe(true); + expect(Struct.omit(resolution, ['_tag'])).toEqual({ + invocationId, + retryCommand: false, + state, + }); } }), ); @@ -130,7 +138,8 @@ it.effect('the command client decodes indeterminate commits without losing recov if (!Result.isFailure(result)) { throw new Error('Expected truthy value'); } - expect(result.failure).toEqual(problem); + expect(Schema.is(PartyCommandCommitIndeterminateProblemSchema)(result.failure)).toBe(true); + expect(Struct.omit(result.failure, ['_tag'])).toEqual(Struct.omit(problem, ['_tag'])); }), ); @@ -163,7 +172,8 @@ it.effect('the command client preserves committed invocation metadata across HTT if (!Result.isFailure(result)) { throw new Error('Expected truthy value'); } - expect(result.failure).toEqual(problem); + expect(Schema.is(PartyCommandAlreadyCommittedProblemSchema)(result.failure)).toBe(true); + expect(Struct.omit(result.failure, ['_tag'])).toEqual(Struct.omit(problem, ['_tag'])); }), ); 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 2af4a05e7..85fa4039d 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,7 +1,7 @@ import { assert, expect, it } from '@app/effect-rstest'; import { randomUUID } from 'node:crypto'; -import { ConfigProvider, Context, Effect, Layer, Logger, Schema, Predicate } from 'effect'; +import { ConfigProvider, Context, Effect, Layer, Logger, Schema, Predicate, Struct } from 'effect'; import { ActionHandlerExecutionError, @@ -55,7 +55,10 @@ 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, @@ -1683,8 +1686,9 @@ it.live('an open invocation resolves explicitly without authorizing automatic co } const resolution = yield* handle(app, recoveryRequest(invocationId, assertion.token)); expect(resolution.status).toBe(200); - expect(yield* Effect.promise(() => resolution.json())).toEqual({ - _tag: 'PartyCommandCommitResolution', + 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', @@ -1824,8 +1828,9 @@ it.live( expect(deniedRecovery.status).toBe(404); const resolution = yield* handle(app, recoveryRequest(invocationId, assertion.token)); expect(resolution.status).toBe(200); - expect(yield* Effect.promise(() => resolution.json())).toEqual({ - _tag: 'PartyCommandCommitResolution', + 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', 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 53598b4d7..ac757cf9f 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 @@ -2,7 +2,7 @@ import { assert, expect, it } from '@app/effect-rstest'; import { readFile, readdir } from 'node:fs/promises'; -import { Effect, Schema, SchemaAST, Predicate } from 'effect'; +import { Effect, Schema, SchemaAST, Predicate, Struct } from 'effect'; import { getReadHandler } from '../../../../packages/core-runtime/src/reads/definition.ts'; import { AresLookupApi, @@ -204,8 +204,12 @@ it.effect('publishes safe status-matched Problem Details and no provider payload type: 'https://ontos.dev/problems/test', }; expect((yield* Schema.decodeUnknownEffect(schema)(fixture)).status).toBe(status); - expect(schema.ast.annotations?.['~httpApiEncoding']).toEqual({ - _tag: 'Json', + 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', }); } 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 764fce4d3..d6638c58f 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,6 +1,6 @@ import { TestClock } from 'effect/testing'; import { expect, it } from '@app/effect-rstest'; -import { DateTime, Effect, Predicate } from 'effect'; +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'; @@ -223,8 +223,8 @@ it.effect('rejects inactivity evidence before persisting a CUSTOMER end', () => endInput('2027-01-01T00:00:00.000Z', 'ENGAGEMENT_INACTIVITY'), ); - expect(result).toEqual({ - _tag: 'evidence_insufficient', + expect(Predicate.isTagged(result, 'evidence_insufficient')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ method: 'ENGAGEMENT_INACTIVITY', roleType: 'CUSTOMER', }); 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 82281b376..9450dc2e9 100644 --- a/app/verticals/party-registry/tests/unit/identity-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/identity-contract.test.ts @@ -1,5 +1,5 @@ import { expect, it } from '@app/effect-rstest'; -import { Effect, DateTime, Option, Schema } from 'effect'; +import { Effect, DateTime, Option, Schema, Struct } from 'effect'; import { PartyCandidateSchema, IsoTimestampSchema, @@ -118,8 +118,9 @@ it.effect('Party identity failures retain branded identifiers in encoded JSON', reason: 'The Party does not exist', }); - expect(yield* Schema.encodeEffect(PartyNotFound)(failure)).toEqual({ - _tag: 'PartyNotFound', + 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', 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 6e0587a38..8329360a0 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 @@ -3,7 +3,7 @@ import { expect, it } from '@app/effect-rstest'; import type { SQL } from 'drizzle-orm'; import { PgDialect } from 'drizzle-orm/pg-core'; -import { DateTime, Effect, Layer, Match, Option, Result, Schema, Predicate } from 'effect'; +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'; @@ -258,21 +258,21 @@ it.layer( ); it('unarchive owner classification distinguishes conflict from ambiguity deterministically', () => { - expect(classifyUnarchiveClaimOwners(partyId, [{}, { partyId }])).toEqual({ - _tag: 'available', - }); - expect(classifyUnarchiveClaimOwners(partyId, [{ partyId: firstOwnerId }])).toEqual({ - _tag: 'identity_conflict', + 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, }); - expect( - classifyUnarchiveClaimOwners(partyId, [ - { partyId: secondOwnerId }, - { partyId: firstOwnerId }, - { partyId: secondOwnerId }, - ]), - ).toEqual({ - _tag: 'identity_ambiguous', + const ambiguousOwners = classifyUnarchiveClaimOwners(partyId, [ + { partyId: secondOwnerId }, + { partyId: firstOwnerId }, + { partyId: secondOwnerId }, + ]); + expect(Predicate.isTagged(ambiguousOwners, 'identity_ambiguous')).toBe(true); + expect(Struct.omit(ambiguousOwners, ['_tag'])).toEqual({ candidatePartyIds: [firstOwnerId, secondOwnerId], }); }); @@ -312,8 +312,8 @@ it.layer( const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); - expect(result).toEqual({ - _tag: 'identity_conflict', + expect(Predicate.isTagged(result, 'identity_conflict')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ conflictingPartyId: firstOwnerId, }); assertTenantLockIsFirst(harness); @@ -566,8 +566,8 @@ it.layer( ]); const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); - expect(result).toEqual({ - _tag: 'identity_ambiguous', + expect(Predicate.isTagged(result, 'identity_ambiguous')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ candidatePartyIds: [firstOwnerId, secondOwnerId], }); expect(harness.insertedValues).toEqual([]); @@ -602,8 +602,8 @@ it.layer( [{ candidateCaseId: caseId }], ]); const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); - expect(result).toEqual({ - _tag: 'review_required', + expect(Predicate.isTagged(result, 'review_required')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ caseIds: [caseId], reasonCode: 'OPEN_DUPLICATE_CASE', }); @@ -624,8 +624,8 @@ it.layer( [], ]); const result = yield* unarchivePartyRecord(harness.transaction, tenantId, partyId, 4); - expect(result).toEqual({ - _tag: 'review_required', + expect(Predicate.isTagged(result, 'review_required')).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ caseIds: [], reasonCode: 'UNRESOLVED_IDENTITY', }); @@ -763,7 +763,8 @@ it.layer( partyId, 'PERSON', ); - expect(result).toEqual({ _tag: 'available', eligibleClaimCount: 0 }); + 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); 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 45a53e831..492fc0b9e 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,8 +1,9 @@ import { expect, it } from '@app/effect-rstest'; -import { DateTime, Predicate } from 'effect'; +import { DateTime, Predicate, Struct, Schema } from 'effect'; import type { PartyRef } from '../../shared/resources/party.ts'; import { + CanonicalPartyResolutionSchema, assertCanonicalWriteTarget, resolveCanonicalPartyRef, } from '../../src/merge/party-alias-resolution.ts'; @@ -32,8 +33,8 @@ it('resolves an historical alias chain to one final canonical Party', () => { alias('party-a', 'party-c'), ]); - expect(result).toEqual({ - _tag: 'CanonicalPartyResolved', + expect(Schema.is(CanonicalPartyResolutionSchema.members[1])(result)).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ canonicalPartyRef: party('party-c'), requestedAlias: party('party-b'), traversedAliasPartyRefs: [party('party-b'), party('party-a')], @@ -70,14 +71,20 @@ it('rejects alias cycles, self aliases, and cross-tenant targets', () => { }); it('rejects new writes addressed to an absorbed alias instead of forwarding them', () => { - expect(assertCanonicalWriteTarget(party('party-b'), [alias('party-b', 'party-a')])).toEqual({ - _tag: 'AliasWriteRejected', + 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', }); - expect(assertCanonicalWriteTarget(party('party-a'), [alias('party-b', 'party-a')])).toEqual({ - _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 f52553c15..85a229214 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 { expect, it } from '@app/effect-rstest'; -import { DateTime, Match } from 'effect'; +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'; @@ -295,8 +295,8 @@ it('blocks readiness for unsupported references and incomplete retry contracts', ], }); - expect(result).toEqual({ - _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' }, @@ -310,8 +310,8 @@ it('blocks every external reference owner without reconciliation evidence', () = references: [{ class: 'COMMERCE_PROFILE', ownerKey: 'commerce', partyRef: party('party-b') }], }); - expect(result).toEqual({ - _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 8a596b2c0..bb588bb9b 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,7 +1,7 @@ // @effect-diagnostics nodeBuiltinImport:off -- Source-only contract checks require reading TypeScript files; remove-when: manifests are importable without TSX loaders. import { expect, it } from '@app/effect-rstest'; import { readFile } from 'node:fs/promises'; -import { Effect, Match, Schema } from 'effect'; +import { Effect, Match, Schema, Struct, Predicate } from 'effect'; import { PartyMergeReadinessRequestSchema, PartyMergeReadinessResponseSchema, @@ -75,8 +75,8 @@ it.effect( }); const rejection = rejectProductionMergeExecution(); - expect(rejection).toEqual({ - _tag: 'ProductionMergeExecutionRejected', + 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.', 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 e8ae735e2..fafc94c28 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,5 +1,5 @@ import { expect, it } from '@app/effect-rstest'; -import { Match, Predicate } from 'effect'; +import { Match, Predicate, Struct, Schema } from 'effect'; import type { PartyRef } from '../../shared/resources/party.ts'; import type { MergeSurvivorCandidate, @@ -9,7 +9,10 @@ import { ConfirmedDuplicateDecisionIdSchema, DecisionActorPrincipalIdSchema, } from '../../shared/domain/merge-selection.ts'; -import { selectCanonicalSurvivor } from '../../src/merge/canonical-survivor-selection.ts'; +import { + CanonicalSurvivorSelectionSchema, + selectCanonicalSurvivor, +} from '../../src/merge/canonical-survivor-selection.ts'; import type { CanonicalSurvivorSelection } from '../../src/merge/canonical-survivor-selection.ts'; const tenantId = '11111111-1111-4111-8111-111111111111'; @@ -60,8 +63,8 @@ it('blocks survivor selection when authoritative identity truth is unresolved', ]), ); - expect(result).toEqual({ - _tag: 'SurvivorSelectionBlocked', + expect(Schema.is(CanonicalSurvivorSelectionSchema.members[1])(result)).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ blocker: 'AUTHORITATIVE_IDENTITY_CONFLICT', conflictingPartyRefs: [party('party-b')], }); @@ -125,8 +128,8 @@ it('rejects a cross-tenant merge set before selection', () => { ]), ); - expect(result).toEqual({ - _tag: 'SurvivorSelectionBlocked', + expect(Schema.is(CanonicalSurvivorSelectionSchema.members[1])(result)).toBe(true); + expect(Struct.omit(result, ['_tag'])).toEqual({ blocker: 'CROSS_TENANT_MERGE_SET', conflictingPartyRefs: [ party('party-a'), @@ -137,8 +140,9 @@ it('rejects a cross-tenant merge set before selection', () => { it('rejects selection without an explicit confirmed duplicate decision and matching evidence set', () => { const candidates = [candidate('party-a'), candidate('party-b')]; - expect(selectCanonicalSurvivor({ candidates, confirmation: null })).toEqual({ - _tag: 'SurvivorSelectionBlocked', + const unconfirmedSelection = selectCanonicalSurvivor({ candidates, confirmation: null }); + expect(Schema.is(CanonicalSurvivorSelectionSchema.members[1])(unconfirmedSelection)).toBe(true); + expect(Struct.omit(unconfirmedSelection, ['_tag'])).toEqual({ blocker: 'DUPLICATE_SET_NOT_CONFIRMED', conflictingPartyRefs: [party('party-a'), party('party-b')], }); 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 d3e1ae06a..ebd1405a8 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,5 +1,5 @@ import { expect, it } from '@app/effect-rstest'; -import { Effect, DateTime, Option, Schema } from 'effect'; +import { Effect, DateTime, Option, Schema, Predicate, Struct } from 'effect'; import { ContactPersonOfRelationshipType, CreatePartyRelationshipPayloadSchema, @@ -223,123 +223,126 @@ it('create reuses an exact period and conflicts on a distinct overlap', () => { validFrom: presentInstant('2026-09-01T10:00:00.000Z'), validTo: presentInstant('2026-10-01T10:00:00.000Z'), } as const; - expect(decideRelationshipCreate([existing], { ...existing })).toEqual({ - _tag: 'reuse', + const exactPeriodDecision = decideRelationshipCreate([existing], { ...existing }); + expect(Predicate.isTagged(exactPeriodDecision, 'reuse')).toBe(true); + expect(Struct.omit(exactPeriodDecision, ['_tag'])).toEqual({ relationshipId: relationshipRef.resourceId, }); - expect( - decideRelationshipCreate([existing], { - relationshipId: 'ignored', - validFrom: presentInstant('2026-09-15T10:00:00.000Z'), - validTo: absentInstant, - }), - ).toEqual({ _tag: 'overlap', relationshipId: relationshipRef.resourceId }); - expect( - decideRelationshipCreate([existing], { - relationshipId: 'ignored', - validFrom: presentInstant('2026-10-01T10:00:00.000Z'), - validTo: absentInstant, - }), - ).toEqual({ _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({}); }); it('only a still-future validity plan is ordinarily updateable', () => { - expect( - 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'), - ), - ).toEqual({ _tag: 'update' }); - expect( - 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'), - ), - ).toEqual({ _tag: 'correction_required', fact: 'validTo' }); - expect( - 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'), - ), - ).toEqual({ _tag: 'end_required' }); - expect( - 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'), - ), - ).toEqual({ _tag: 'revision_conflict', actualRevision: 2 }); - expect( - 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'), - ), - ).toEqual({ _tag: 'update' }); - expect( - 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'), - ), - ).toEqual({ _tag: 'update' }); - expect( - 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'), - ), - ).toEqual({ _tag: 'correction_required', fact: 'validFrom' }); + 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'), + ); + 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'), + ); + 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'), + ); + 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'), + ); + 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'), + ); + 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'), + ); + 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' }); }); it('end retry is exact and changed historical evidence requires correction', () => { @@ -357,21 +360,21 @@ it('end retry is exact and changed historical evidence requires correction', () provenance: { method: 'MANUAL_CONFIRMATION', source: 'ENGAGEMENT_REVIEW' }, reason: 'No longer a contact', } as const; - expect(decideRelationshipEnd(current, exact, instant('2026-09-03T00:00:00.000Z'))).toEqual({ - _tag: 'unchanged', - }); - expect( - decideRelationshipEnd( - current, - { ...exact, reason: 'A different historical explanation' }, - instant('2026-09-03T00:00:00.000Z'), - ), - ).toEqual({ _tag: 'correction_required', fact: 'validTo' }); - expect( - decideRelationshipEnd( - { ...current, endProvenanceMethod: null, endProvenanceSource: null, endReason: null }, - exact, - instant('2026-09-03T00:00:00.000Z'), - ), - ).toEqual({ _tag: 'attach_end_evidence' }); + 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'), + ); + 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/search-semantics.test.ts b/app/verticals/party-registry/tests/unit/search-semantics.test.ts index e650cd4e6..3bfaa8e31 100644 --- a/app/verticals/party-registry/tests/unit/search-semantics.test.ts +++ b/app/verticals/party-registry/tests/unit/search-semantics.test.ts @@ -1,5 +1,5 @@ import { expect, it } from '@app/effect-rstest'; -import { Match, Predicate } from 'effect'; +import { Match, Predicate, Struct } from 'effect'; import { normalizeCounterpartySearchHits, normalizePartySearchHits, @@ -47,8 +47,9 @@ it('Party Search hides archived hits by default and explicitly labels included a { archived: true, canonicalPartyRef: partyRef('archived'), title: 'Archived' }, ]; - expect(normalizePartySearchHits({ includeArchived: false, tenantId }, hits)).toEqual({ - _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); From 50c3d45c6aed5e1a075235a2e95e5d0abdb9cfe0 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 12:32:26 +0200 Subject: [PATCH 31/38] test(integration): scope Party fixture mutations to owned records Prevent parallel governed tests from having their customer roles ended by the database-boundary fixture. Also scope delivery-purpose updates/readback and the billing constraint probe. Preserve independent tenant rows with full-row regression snapshots; both defects reproduced within owned tenants before correction and all five Party integrations pass five parallel repeats. Co-Authored-By: Claude Fable 5.1 --- .../integration/database-boundary.test.ts | 99 ++++++++++++++++++- 1 file changed, 95 insertions(+), 4 deletions(-) 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 7317165ca..0e9cd44ed 100644 --- a/app/verticals/party-registry/tests/integration/database-boundary.test.ts +++ b/app/verticals/party-registry/tests/integration/database-boundary.test.ts @@ -33,6 +33,7 @@ import type { PartyTransaction } from '../../src/db/types.ts'; 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'; @@ -46,7 +47,9 @@ 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'; @@ -344,6 +347,17 @@ it.live('enforces Party owner invariants, tenant isolation, and independent fact 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, @@ -371,6 +385,16 @@ it.live('enforces Party owner invariants, tenant isolation, and independent fact ), ), ).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 @@ -399,7 +423,19 @@ it.live('enforces Party owner invariants, tenant isolation, and independent fact endedRecordedAt: contactEndRecordedAt, validTo: futureContactEnd, }) - .where(eq(partyContactPointPurposes.purposeKey, 'DELIVERY')); + .where( + and( + eq(partyContactPointPurposes.tenantId, tenantA), + eq(partyContactPointPurposes.contactPointId, addressA), + eq(partyContactPointPurposes.purposeKey, 'DELIVERY'), + ), + ); + expect( + yield* admin + .select() + .from(partyContactPointPurposes) + .where(eq(partyContactPointPurposes.contactPointId, addressB)), + ).toEqual(otherTenantPurposes); const [scheduledContactEnd] = yield* admin .select() .from(partyContactPoints) @@ -412,7 +448,13 @@ it.live('enforces Party owner invariants, tenant isolation, and independent fact const [scheduledPurposeEnd] = yield* admin .select() .from(partyContactPointPurposes) - .where(eq(partyContactPointPurposes.purposeKey, 'DELIVERY')); + .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']); @@ -422,7 +464,13 @@ it.live('enforces Party owner invariants, tenant isolation, and independent fact admin .update(partyContactPointPurposes) .set({ validTo: futureContactEnd }) - .where(eq(partyContactPointPurposes.purposeKey, 'BILLING')), + .where( + and( + eq(partyContactPointPurposes.tenantId, tenantA), + eq(partyContactPointPurposes.contactPointId, addressA), + eq(partyContactPointPurposes.purposeKey, 'BILLING'), + ), + ), ), ), ).toBe(true); @@ -569,6 +617,37 @@ it.live('enforces Party owner invariants, tenant isolation, and independent fact ), ), ).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, @@ -613,7 +692,19 @@ it.live('enforces Party owner invariants, tenant isolation, and independent fact state: 'ENDED', validTo: DateTime.toDateUtc(DateTime.makeUnsafe('2026-06-30T00:00:00.000Z')), }) - .where(eq(counterpartyRolePeriods.roleType, 'CUSTOMER')); + .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, From bdeaf513109aa41ec5a1eb6a045a2196dddeef0c Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 12:49:43 +0200 Subject: [PATCH 32/38] fix(lint): recognize native test hooks and property tag assertions Co-Authored-By: Claude Fable 5.1 --- .../rules/no-manual-tag-comparison.ts | 47 +++++++++++++++++++ .../rules/no-promise-shaped-port.ts | 2 +- .../invalid/apps/property-assertions.ts | 28 +++++++++++ .../property-options/configured-assertions.ts | 5 ++ .../property-assertions.ts | 8 ++++ .../valid/apps/property-assertions.ts | 38 +++++++++++++++ .../property-options/configured-assertions.ts | 5 ++ .../effect-rstest-hook-provenance.test.ts | 33 +++++++++++++ .../tests/unit/effect-rstest-hooks.test.ts | 24 ++++++++++ 9 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/apps/property-assertions.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/apps/property-options/configured-assertions.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/apps/error-combinators-disabled/property-assertions.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/apps/property-assertions.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/apps/property-options/configured-assertions.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/apps/shell-super-app/tests/unit/effect-rstest-hook-provenance.test.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/apps/shell-super-app/tests/unit/effect-rstest-hooks.test.ts 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 9987112ff..e2e4671c5 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 @@ -427,6 +427,28 @@ function staticString(context: Context, node: ESTree.Node): string | null { return initialiser === null ? null : asStringLiteral(initialiser); } +/** 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; + let hasTag = false; + for (const element of expression.elements) { + if (element === null || element.type === 'SpreadElement') return false; + const key = staticString(context, element); + if (key === null) { + const value = unwrap(element); + if (value.type !== 'Literal' || typeof value.value !== 'number') return false; + } + hasTag = key === TAG_PROPERTY; + } + return hasTag; +} + /** 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 { const expression = unwrap(node); @@ -1041,6 +1063,31 @@ export const rule = defineRule({ const receiver = callee.type === 'MemberExpression' ? callee.object : null; if (method === null || method === undefined) return; + // Property assertions are shape/equality probes only on a proven `expect(subject)` chain. + if (assertion?.method === 'toHaveProperty' && assertion.subject !== null) { + const path = node.arguments[0]; + const subject = assertion.subject.arguments[0]; + if ( + path === undefined || + path.type === 'SpreadElement' || + subject === undefined || + subject.type === 'SpreadElement' || + !tagPropertyPath(context, path) || + suppressed(node) + ) + return; + const expected = node.arguments[1]; + if (expected?.type === 'SpreadElement') return; + const literal = expected === undefined ? null : staticString(context, expected); + if (literal !== null && exempt.has(literal)) return; + context.report({ + node, + messageId: expected === undefined ? 'tagPresenceCheck' : 'tagEqualityCall', + data: { callee: describe(context, node.callee), text: describe(context, subject) }, + }); + return; + } + // Assertions are comparisons too, including tag projections in arrays and aliased values. const assertionMethods = new Set([ 'equal', 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 733977b81..9c9568f36 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 @@ -542,7 +542,7 @@ export const rule = defineRule({ 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)(?:\.|$))/u.test( + /^(?:node:test:(?:test|it|before|after|beforeEach|afterEach|\*)(?:\.|$)|(?:@playwright\/test|@rstest\/core|vitest):(?:test|it|beforeAll|afterAll|beforeEach|afterEach|rstest\.mock)(?:\.|$)|@app\/effect-rstest:(?:\*\.)?(?:beforeAll|afterAll|beforeEach|afterEach)$)/u.test( imported(call.callee) ?? '', ) ) 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..4a970c716 --- /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 '@app/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..5c2c5961c --- /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 '@app/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/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..e1ba612f2 --- /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 '@app/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..064f44b53 --- /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 '@app/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..6a0c2ed04 --- /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 '@app/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-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..358dba79b --- /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 '@app/effect-rstest'; +import * as runner from '@app/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/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..0bdb5660f --- /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 '@app/effect-rstest'; +import * as runner from '@app/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(); }); From bc46ea05b7926a7ad4422af74dc4f171ec8e1ccc Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 13:01:56 +0200 Subject: [PATCH 33/38] test(effect-rstest): pin upstream table row semantics Co-Authored-By: Claude Fable 5.1 --- .../effect-rstest/tests/index.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/app/packages/effect-rstest/tests/index.test.ts b/app/packages/effect-rstest/tests/index.test.ts index 60ab36083..51553691c 100644 --- a/app/packages/effect-rstest/tests/index.test.ts +++ b/app/packages/effect-rstest/tests/index.test.ts @@ -56,6 +56,41 @@ it.live.each([1, 2, 3])('live each %s', (n) => ), ); +// Match @effect/vitest: each passes one intact row, including readonly tuples. +const tupleCases = [[1, 2]] as const; +const objectCases = [{ left: 1, right: 2 }] as const; + +it.effect.each(tupleCases)('effect each preserves a readonly tuple row', (row, ...extra) => + Effect.sync(() => { + const tuple: readonly [1, 2] = row; + expect(tuple).toBe(tupleCases[0]); + expect(tuple).toEqual([1, 2]); + expect(extra).toEqual([]); + }), +); +it.live.each(tupleCases)('live each preserves a readonly tuple row', (row, ...extra) => + Effect.sync(() => { + const tuple: readonly [1, 2] = row; + expect(tuple).toBe(tupleCases[0]); + expect(tuple).toEqual([1, 2]); + expect(extra).toEqual([]); + }), +); +it.effect.each(objectCases)('effect each preserves an object row', (row, ...extra) => + Effect.sync(() => { + expect(row).toBe(objectCases[0]); + expect(row).toEqual({ left: 1, right: 2 }); + expect(extra).toEqual([]); + }), +); +it.live.each(objectCases)('live each preserves an object row', (row, ...extra) => + Effect.sync(() => { + expect(row).toBe(objectCases[0]); + expect(row).toEqual({ left: 1, right: 2 }); + expect(extra).toEqual([]); + }), +); + // skip it.live.skip('live skipped', () => Effect.die('skipped anyway')); From a4a6eb5e4209a937bdbbe90662477dd10759214e Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 13:01:56 +0200 Subject: [PATCH 34/38] fix(lint): inspect inherited and applied generic Promise ports Co-Authored-By: Claude Fable 5.1 --- .../rules/no-promise-shaped-port.ts | 52 +++++++++++++++---- .../src/generic-interface-heritage.ts | 32 ++++++++++++ .../src/generic-interface-heritage.ts | 43 +++++++++++++++ 3 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/invalid/packages/core-runtime/src/generic-interface-heritage.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-promise-shaped-port/valid/packages/core-runtime/src/generic-interface-heritage.ts 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 9c9568f36..253cbb798 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 @@ -640,7 +640,12 @@ export const rule = defineRule({ /** The `Promise` / `PromiseLike` reference of a return/value annotation, if any. */ const promiseReference = ( - annotation: ESTree.TSTypeAnnotation | ESTree.TSTypeReference | null | undefined, + annotation: + | ESTree.TSTypeAnnotation + | ESTree.TSTypeReference + | ESTree.TSInterfaceHeritage + | null + | undefined, functionAliasOnly = false, ): string | null => { if (annotation === null || annotation === undefined) return null; @@ -676,10 +681,10 @@ export const rule = defineRule({ } return null; } - // Object members have their own visitors too; only an applied generic alias - // (`type Service = { run: () => T }` used as `Service>`) needs this. + // Object members have their own visitors too; applied generic aliases/heritage + // need substitutions here. Function-returned records remain non-port continuations. if (raw.type === 'TSTypeLiteral' || raw.type === 'TSInterfaceBody') { - if (substitutions.size === 0) return null; + if (substitutions.size === 0 || insideFunction) return null; for (const member of raw.members ?? raw.body) { const isValue = member.type === 'TSPropertySignature' || member.type === 'TSIndexSignature'; @@ -693,8 +698,9 @@ export const rule = defineRule({ } return null; } - if (raw.type !== 'TSTypeReference') return null; - const names = typeNameSegments(raw.typeName); + if (raw.type !== 'TSTypeReference' && raw.type !== 'TSInterfaceHeritage') return null; + const typeName = raw.typeName ?? raw.expression; + const names = typeNameSegments(typeName); if (!names) return null; const name = names.at(-1)!; if ( @@ -705,7 +711,7 @@ export const rule = defineRule({ ) return functionAliasOnly && !insideFunction ? null : `${name}<…>`; if (names.length !== 1) return null; - const variable = variableFor(raw.typeName, name); + const variable = variableFor(typeName, name); const bound = substitutions.get(variable); if (bound) return resolve(bound.node, seen, bound.substitutions, insideFunction); const alias = variable?.defs.find((d: any) => @@ -724,7 +730,20 @@ export const rule = defineRule({ substitutions: argument ? substitutions : applied, }); } - return resolve(alias.typeAnnotation ?? alias.body, seen, applied, insideFunction); + if (seen.has(alias)) return null; + seen.add(alias); + const own = resolve( + alias.typeAnnotation ?? alias.body, + new Set(seen), + applied, + insideFunction, + ); + if (own) return own; + for (const heritage of alias.extends ?? []) { + const inherited = resolve(heritage, new Set(seen), applied, insideFunction); + if (inherited) return inherited; + } + return null; } const parameter = variable?.defs.find((d: any) => d.node.type === 'TSTypeParameter')?.node; if (parameter) return resolve(parameter, seen, substitutions, insideFunction); @@ -1083,11 +1102,22 @@ export const rule = defineRule({ 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) => { const annotation = parentOf(node as unknown as AnyNode); - if (annotation?.type !== 'TSTypeAnnotation') return; - const owner = parentOf(annotation); - if (owner?.type !== 'Identifier' && owner?.type !== 'RestElement') return; + if (annotation?.type !== 'TSTypeAliasDeclaration') { + if (annotation?.type !== 'TSTypeAnnotation') return; + const owner = parentOf(annotation); + if (owner?.type !== 'Identifier' && owner?.type !== 'RestElement') return; + } if (!isPortFunctionTypePosition(node as unknown as AnyNode)) return; const wrapper = promiseReference(node, true); if (wrapper === null) return; 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/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>; From 7aa2ab5f780aac37f5ea9cdbd94bfb0e5af342f1 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 15:42:58 +0200 Subject: [PATCH 35/38] refactor(test): replace vendored runner with patched effect-rstest Adopt the immutable upstream package and remove the owned adapter. Carry the generic conformance fixes from ScriptedAlchemy/effect-rstest#4 in one temporary pnpm patch, tracked for removal by TechsioCZ/ontos#507. Update actual imports and lint provenance without compatibility aliases or runner wrappers. Co-Authored-By: Claude Fable 5.1 --- app/apps/shell-super-app/package.json | 4 +- .../tests/integration/auth-runtime.test.ts | 2 +- .../generated-owner-isolation.test.ts | 2 +- .../identity-modes-runtime.test.ts | 2 +- .../module-catalog-runtime.test.ts | 2 +- .../module-federation-i18n-runtime.test.ts | 2 +- .../integration/stage-demo-bootstrap.test.ts | 2 +- .../tests/unit/api-index.test.ts | 2 +- .../tests/unit/auth-boundary.test.ts | 2 +- .../tests/unit/auth-config.test.ts | 2 +- .../tests/unit/auth-contract.test.ts | 2 +- .../tests/unit/auth-db-client.test.ts | 2 +- .../tests/unit/auth-schema.test.ts | 2 +- .../tests/unit/browser-effect-runtime.test.ts | 2 +- .../tests/unit/deployment-allowlist.test.ts | 2 +- .../tests/unit/gateway-issuer.test.ts | 2 +- .../tests/unit/identity-lifecycle.test.ts | 2 +- .../tests/unit/impersonation-service.test.ts | 2 +- .../unit/installed-module-catalog.test.ts | 2 +- .../unit/installed-outbox-matcher.test.ts | 2 +- .../tests/unit/installed-verticals.test.ts | 2 +- .../tests/unit/layout.test.tsx | 2 +- .../tests/unit/legal-entity-selection.test.ts | 2 +- .../unit/module-entrypoint-loader.test.ts | 2 +- .../tests/unit/routes/home/loader.test.ts | 2 +- .../tests/unit/routes/home/page.test.tsx | 2 +- .../tests/unit/routes/login/locales.test.ts | 2 +- .../tests/unit/routes/login/page.test.tsx | 2 +- .../tests/unit/routes/modules/loader.test.ts | 2 +- .../tests/unit/routes/modules/page.test.tsx | 2 +- .../tests/unit/shell-composition.test.ts | 2 +- .../unit/shell-governed-read-schemas.test.ts | 2 +- .../tests/unit/shell-resources.test.ts | 2 +- .../tests/unit/stage-demo-bootstrap.test.ts | 2 +- .../EFFECT_V4_LINT_ENFORCEMENT.md | 2 +- app/oxlint.config.ts | 2 - app/package.json | 4 +- app/packages/core-runtime/package.json | 4 +- .../integration/action-permission.test.ts | 2 +- .../tests/integration/action-runtime.test.ts | 2 +- .../contacts-identity-migration.test.ts | 2 +- .../tests/integration/context-access.test.ts | 2 +- .../integration/identity-runtime.test.ts | 2 +- .../integration/legal-entity-context.test.ts | 2 +- .../integration/module-state-gate.test.ts | 2 +- .../tests/integration/outbox-runtime.test.ts | 2 +- .../tests/integration/pool-deadlines.test.ts | 2 +- .../integration/principal-management.test.ts | 2 +- .../integration/principal-resolver.test.ts | 2 +- .../tests/integration/read-runtime.test.ts | 2 +- .../integration/search-persistence.test.ts | 2 +- .../search-worker-snapshot.test.ts | 2 +- .../integration/tenant-isolation.test.ts | 2 +- .../integration/tenant-module-state.test.ts | 2 +- .../unit/action-authorization-rollout.test.ts | 2 +- .../tests/unit/action-collector.test.ts | 2 +- .../tests/unit/action-definition.test.ts | 2 +- .../tests/unit/action-errors.test.ts | 2 +- .../tests/unit/action-http-runner.test.ts | 2 +- .../tests/unit/action-identity.test.ts | 2 +- .../tests/unit/action-permission.test.ts | 2 +- .../tests/unit/action-policy.test.ts | 2 +- .../tests/unit/action-public-surface.test.ts | 2 +- .../tests/unit/action-runtime.test.ts | 2 +- .../tests/unit/action-testing-harness.test.ts | 2 +- .../unit/application-composition.test.ts | 2 +- .../tests/unit/catalog-contract.test.ts | 2 +- .../unit/commit-recovery-metadata.test.ts | 2 +- .../core-runtime/tests/unit/config.test.ts | 2 +- .../tests/unit/context-access.test.ts | 2 +- .../unit/database-driver-failure.test.ts | 2 +- .../unit/entrypoint-classification.test.ts | 2 +- .../tests/unit/governed-read-http.test.ts | 2 +- .../http-principal-authentication.test.ts | 2 +- .../tests/unit/legal-entity-context.test.ts | 2 +- .../tests/unit/module-catalog.test.ts | 2 +- .../tests/unit/module-manifest.test.ts | 2 +- .../tests/unit/module-state-gate.test.ts | 2 +- .../unit/native-transaction-context.test.ts | 2 +- .../tests/unit/native-transaction.test.ts | 2 +- .../tests/unit/operation-context.test.ts | 2 +- .../tests/unit/outbox-definition.test.ts | 2 +- .../tests/unit/outbox-health.test.ts | 2 +- .../tests/unit/outbox-poller.test.ts | 2 +- .../tests/unit/outbox-process.test.ts | 2 +- .../tests/unit/outbox-runtime.test.ts | 2 +- .../tests/unit/permission-client.test.ts | 2 +- .../tests/unit/pool-configuration.test.ts | 2 +- .../tests/unit/principal-management.test.ts | 2 +- .../tests/unit/principal-resolver.test.ts | 2 +- .../tests/unit/read-definition.test.ts | 2 +- .../tests/unit/read-runtime.test.ts | 2 +- .../tests/unit/schema-contract.test.ts | 2 +- .../tests/unit/scoped-transaction.test.ts | 2 +- .../tests/unit/search-ingestion.test.ts | 2 +- .../tests/unit/search-projection.test.ts | 2 +- .../tests/unit/search-schema.test.ts | 2 +- .../tests/unit/search-worker-snapshot.test.ts | 2 +- .../tests/unit/service-public-surface.test.ts | 2 +- .../tests/unit/shell-contribution.test.ts | 2 +- .../tests/unit/spicedb-client.test.ts | 2 +- .../unit/spicedb-database-bootstrap.test.ts | 2 +- .../unit/stage-context-bootstrap.test.ts | 2 +- .../unit/system-principal-context.test.ts | 2 +- .../tests/unit/tenant-module-state.test.ts | 2 +- app/packages/effect-rstest/LICENSE | 22 - app/packages/effect-rstest/README.md | 95 ----- app/packages/effect-rstest/package.json | 24 -- app/packages/effect-rstest/rstest.config.ts | 6 - app/packages/effect-rstest/src/index.ts | 282 ------------- .../effect-rstest/src/internal/internal.ts | 377 ------------------ app/packages/effect-rstest/src/utils.ts | 335 ---------------- .../effect-rstest/tests/equality.test.ts | 48 --- .../tests/fixtures/layer-lifetime.fixture.ts | 63 --- .../tests/fixtures/test-lifetime.fixture.ts | 105 ----- .../effect-rstest/tests/index.test.ts | 299 -------------- .../effect-rstest/tests/isolation.test.ts | 117 ------ .../tests/layer-lifetime.test.ts | 97 ----- .../tests/nested-isolation.test.ts | 203 ---------- .../tests/prop-schema-record.test.ts | 23 -- .../tests/prop-schema-tuple.test.ts | 23 -- .../effect-rstest/tests/support/bar.ts | 3 - .../effect-rstest/tests/support/child.ts | 5 - .../effect-rstest/tests/support/foo.ts | 3 - .../effect-rstest/tests/support/parent.ts | 5 - .../effect-rstest/tests/support/scoped.ts | 5 - .../tests/support/shared-child.ts | 5 - .../effect-rstest/tests/support/sleeper.ts | 9 - .../effect-rstest/tests/support/state.ts | 7 - .../tests/support/todo-service.ts | 12 - .../effect-rstest/tests/test-lifetime.test.ts | 111 ------ app/packages/effect-rstest/tsconfig.json | 21 - .../gateway-principal-verifier/package.json | 4 +- .../unit/gateway-principal-verifier.test.ts | 2 +- app/packages/shared-contracts/package.json | 4 +- .../tests/unit/client-runtime.test.ts | 2 +- .../tests/unit/effect-bff-runtime.test.ts | 2 +- .../tests/unit/gateway-context.test.ts | 2 +- .../unit/microvertical-api-baseline.test.ts | 2 +- .../tests/unit/operation-gateway.test.ts | 2 +- .../tests/unit/problem-details.test.ts | 2 +- app/patches/effect-rstest@0.1.0.patch | 214 ++++++++++ app/pnpm-lock.yaml | 128 +++--- app/pnpm-workspace.yaml | 1 + app/scripts/local-environment-values.test.mts | 2 +- .../tests/module-contract-generator.test.mts | 2 +- .../tests/resource-generator.test.mts | 2 +- .../tests/retire-contribution.test.mts | 2 +- .../tests/scaffold-generators.test.mts | 2 +- app/scripts/tests/api-only-tooling.test.mts | 2 +- .../audit-database-trust-boundaries.test.mts | 2 +- .../authorization-rollout-contract.test.mts | 2 +- .../check-authorization-readiness.test.mts | 2 +- .../tests/database-access-boundaries.test.mts | 2 +- .../tests/effect-rstest-package.test.mts | 57 +++ .../initialize-local-development.test.mts | 2 +- app/scripts/tests/locki-feature.test.mts | 2 +- .../migrate-contacts-authorization.test.mts | 2 +- .../module-entrypoint-boundaries.test.mts | 2 +- .../tests/outbox-worker-delivery.test.mts | 2 +- .../tests/plan-deployment-impact.test.mts | 2 +- .../protected-entrypoint-inventory.test.mts | 2 +- ...sion-current-action-authorization.test.mts | 2 +- .../tests/quality-audit-model.test.mts | 2 +- .../quality-audit-runtime-model.test.mts | 2 +- app/scripts/tests/quality-audit.test.mts | 2 +- ...-fail-closed-authorization-impact.test.mts | 2 +- app/scripts/tests/root-environment.test.mts | 2 +- .../typecheck-project-references.test.mts | 2 +- .../validate-ultramodern-workspace.mts | 1 - app/tools/oxlint/effect-native/README.md | 10 +- .../rules/no-effect-run-in-tests.ts | 16 +- .../rules/no-manual-tag-comparison.ts | 4 +- .../rules/no-promise-shaped-port.ts | 10 +- .../shared/test-restricted-imports.ts | 8 +- .../tests/discover-rules.test.mts | 2 +- .../effect-native/tests/fixtures.test.mts | 2 +- .../valid/harness-usage.test.tsx | 2 +- .../valid/tests/harness/it-layer.ts | 2 +- .../valid/tests/support/effect-harness.ts | 2 +- .../invalid/apps/property-assertions.ts | 2 +- .../property-options/configured-assertions.ts | 2 +- .../src/destructured-assertions.ts | 2 +- .../src/object-equality-assertions.ts | 2 +- .../src/partial-object-assertions.ts | 2 +- .../tests/unit/assertions.test.ts | 2 +- .../property-assertions.ts | 2 +- .../valid/apps/property-assertions.ts | 2 +- .../property-options/configured-assertions.ts | 2 +- .../src/destructured-assertions.ts | 6 +- .../playwright-extend-owned-service.spec.ts | 2 +- .../tests/unit/async-test-callback.test.ts | 4 +- .../describe-wrapped-test-callback.test.ts | 4 +- .../effect-rstest-hook-provenance.test.ts | 4 +- .../unit/inferred-promise-callback.test.ts | 2 +- .../tests/unit/layer-test-callback.test.ts | 2 +- .../example/tests/promise-round-trip.test.ts | 2 +- .../tools/example/tests/async-tooling.test.ts | 2 +- .../tests/unit/async-test-boundaries.test.ts | 2 +- .../describe-wrapped-test-callback.test.ts | 4 +- .../tests/unit/effect-rstest-hooks.test.ts | 4 +- .../example/tests/promise-round-trip.test.ts | 2 +- .../example/tests/synchronous-tooling.test.ts | 2 +- .../effect-native/tests/launcher.test.mts | 2 +- .../effect-native/tests/oxlint.test.mts | 2 +- .../oxlint/effect-native/tests/paths.test.mts | 2 +- .../tests/production-options.test.mts | 2 +- .../effect-native/tests/registration.test.mts | 2 +- .../tests/repository-policy.test.mts | 2 +- .../effect-native/tests/script-scope.test.mts | 2 +- .../tests/temporary-workspace.test.mts | 2 +- app/tsconfig.json | 3 - app/verticals/party-registry/package.json | 4 +- .../tests/components/contacts-page.test.tsx | 2 +- .../tests/integration/ares-governed.test.ts | 2 +- .../integration/database-boundary.test.ts | 2 +- .../engagement-database-boundary.test.ts | 2 +- .../integration/governed-identity.test.ts | 2 +- .../integration/identity-concurrency.test.ts | 2 +- .../api-integration-ares-application.test.ts | 2 +- .../unit/api-integration-client-url.test.ts | 2 +- .../api-integration-command-client.test.ts | 2 +- .../api-integration-command-contract.test.ts | 2 +- .../api-integration-command-recovery.test.ts | 2 +- .../api-integration-command-runtime.test.ts | 2 +- .../unit/api-integration-contract.test.ts | 2 +- .../api-integration-correction-client.test.ts | 2 +- .../unit/api-integration-runtime.test.ts | 2 +- .../unit/ares-application-policy.test.ts | 2 +- .../tests/unit/ares-evidence-contract.test.ts | 2 +- .../tests/unit/ares-lookup-read.test.ts | 2 +- .../tests/unit/ares-subject.service.test.ts | 2 +- .../unit/audit-evidence-contract.test.ts | 2 +- .../tests/unit/catalog-contract.test.ts | 2 +- .../tests/unit/contact-point-contract.test.ts | 2 +- .../contact-point-correction-action.test.ts | 2 +- .../contact-point-persistence.service.test.ts | 2 +- .../tests/unit/correction-contract.test.ts | 2 +- .../tests/unit/cors-origin.test.ts | 2 +- .../tests/unit/counterparty-contract.test.ts | 2 +- .../counterparty-persistence.service.test.ts | 2 +- .../unit/counterparty-role-lifecycle.test.ts | 2 +- .../tests/unit/database-client.test.ts | 2 +- .../unit/engagement-catalog-contract.test.ts | 2 +- .../engagement-profile-api-contract.test.ts | 2 +- ...gement-profile-persistence-service.test.ts | 2 +- .../engagement-reference-validation.test.ts | 2 +- .../unit/engagement-schema-contract.test.ts | 2 +- ...teway-assertion-redemption-runtime.test.ts | 2 +- .../tests/unit/identifier-contract.test.ts | 2 +- .../identifier-persistence.service.test.ts | 2 +- .../unit/identifier-update-outbox.test.ts | 2 +- .../unit/identity-action-evidence.test.ts | 2 +- .../tests/unit/identity-contract.test.ts | 2 +- ...y-create-without-strong-identifier.test.ts | 2 +- .../unit/identity-party-detail-alias.test.ts | 2 +- .../identity-party-detail-history.test.ts | 2 +- .../unit/identity-persistence.service.test.ts | 2 +- .../tests/unit/matching-contract.test.ts | 2 +- .../tests/unit/matching-persistence.test.ts | 2 +- .../merge-alias-resolution-service.test.ts | 2 +- .../tests/unit/merge-alias-resolution.test.ts | 2 +- .../unit/merge-collision-reference.test.ts | 2 +- .../unit/merge-readiness-contract.test.ts | 2 +- .../unit/merge-survivor-selection.test.ts | 2 +- .../unit/prepare-contacts-migration.test.ts | 2 +- .../unit/relationship-domain-contract.test.ts | 2 +- .../relationship-operation-contract.test.ts | 2 +- .../relationship-persistence.service.test.ts | 2 +- .../tests/unit/runtime-locales.test.ts | 2 +- .../tests/unit/schema-contract.test.ts | 2 +- .../tests/unit/search-contract.test.ts | 2 +- .../tests/unit/search-core-adapter.test.ts | 2 +- .../tests/unit/search-identifier-sync.test.ts | 2 +- .../tests/unit/search-projector.test.ts | 2 +- .../tests/unit/search-provider.test.ts | 2 +- .../tests/unit/search-rebuild-request.test.ts | 2 +- .../tests/unit/search-semantics.test.ts | 2 +- .../tests/unit/search-source.test.ts | 2 +- .../unit/search-worker-registration.test.ts | 2 +- 280 files changed, 604 insertions(+), 2663 deletions(-) delete mode 100644 app/packages/effect-rstest/LICENSE delete mode 100644 app/packages/effect-rstest/README.md delete mode 100644 app/packages/effect-rstest/package.json delete mode 100644 app/packages/effect-rstest/rstest.config.ts delete mode 100644 app/packages/effect-rstest/src/index.ts delete mode 100644 app/packages/effect-rstest/src/internal/internal.ts delete mode 100644 app/packages/effect-rstest/src/utils.ts delete mode 100644 app/packages/effect-rstest/tests/equality.test.ts delete mode 100644 app/packages/effect-rstest/tests/fixtures/layer-lifetime.fixture.ts delete mode 100644 app/packages/effect-rstest/tests/fixtures/test-lifetime.fixture.ts delete mode 100644 app/packages/effect-rstest/tests/index.test.ts delete mode 100644 app/packages/effect-rstest/tests/isolation.test.ts delete mode 100644 app/packages/effect-rstest/tests/layer-lifetime.test.ts delete mode 100644 app/packages/effect-rstest/tests/nested-isolation.test.ts delete mode 100644 app/packages/effect-rstest/tests/prop-schema-record.test.ts delete mode 100644 app/packages/effect-rstest/tests/prop-schema-tuple.test.ts delete mode 100644 app/packages/effect-rstest/tests/support/bar.ts delete mode 100644 app/packages/effect-rstest/tests/support/child.ts delete mode 100644 app/packages/effect-rstest/tests/support/foo.ts delete mode 100644 app/packages/effect-rstest/tests/support/parent.ts delete mode 100644 app/packages/effect-rstest/tests/support/scoped.ts delete mode 100644 app/packages/effect-rstest/tests/support/shared-child.ts delete mode 100644 app/packages/effect-rstest/tests/support/sleeper.ts delete mode 100644 app/packages/effect-rstest/tests/support/state.ts delete mode 100644 app/packages/effect-rstest/tests/support/todo-service.ts delete mode 100644 app/packages/effect-rstest/tests/test-lifetime.test.ts delete mode 100644 app/packages/effect-rstest/tsconfig.json create mode 100644 app/patches/effect-rstest@0.1.0.patch create mode 100644 app/scripts/tests/effect-rstest-package.test.mts diff --git a/app/apps/shell-super-app/package.json b/app/apps/shell-super-app/package.json index c75fbde34..77ea2fd35 100644 --- a/app/apps/shell-super-app/package.json +++ b/app/apps/shell-super-app/package.json @@ -62,7 +62,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", @@ -78,7 +78,7 @@ "typescript": "7.0.2", "wrangler": "4.110.0", "zephyr-rspack-plugin": "1.2.4", - "@app/effect-rstest": "workspace:*" + "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc" }, "modernjs": { "preset": "presetUltramodern", 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 f0fe51059..227650a3c 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 181233930..e4232f4dc 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,5 +1,5 @@ import { TestClock } from 'effect/testing'; -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { NodeServices } from '@effect/platform-node'; import { makeFaultInjectableCoreDatabase, 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 39835c0f6..88cb96100 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 f6bbf3dc0..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { OntosModuleDeploymentContractSchema, defineAction, 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 23df4ea02..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,6 +1,6 @@ import { readFileSync } from 'node:fs'; import { createRequire, registerHooks } from 'node:module'; -import { describe, expect, it } from '@app/effect-rstest'; +import { describe, expect, it } from 'effect-rstest'; import { Effect } from 'effect'; import * as Schema from 'effect/Schema'; 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 41cc6b700..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,4 +1,4 @@ -import { describe, expect, it, rstest } from '@app/effect-rstest'; +import { describe, expect, it, rstest } from 'effect-rstest'; import { randomUUID } from 'node:crypto'; import { memoryAdapter } from 'better-auth/adapters/memory'; import { Cause, Deferred, Effect, Exit, Fiber } from 'effect'; 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 32e048bb6..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 '@app/effect-rstest'; +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 347321c60..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 '@app/effect-rstest'; +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 a576ee0ef..a6e36b182 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema } from 'effect'; import { AuthConfigError, parseAuthConfig } from '../../api/auth/config.ts'; import { 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 382d5abed..3f1997762 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { DateTime, Effect, Schema, SchemaAST, Predicate, Struct } from 'effect'; import { AuthenticationUnavailableProblemSchema, 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 01b470509..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 bc8c30f57..3d29d444f 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 '@app/effect-rstest'; +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 41930f124..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Deferred, Effect, Exit, Fiber, Schema } from 'effect'; import { browserRuntime } from '../../src/runtime/browser-effect-runtime.ts'; 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 c7d143ff7..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,6 +1,6 @@ import { Effect } from 'effect'; -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { deriveDeploymentAllowlist } from '../../api/modules/deployment-allowlist.ts'; import { createModuleDeploymentAllowlistBuildInput } from '../../module-deployment-allowlist.config.ts'; 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 3f52d1a4d..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,4 +1,4 @@ -import { expect, rs, it } from '@app/effect-rstest'; +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'; 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 aa0488436..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { ActionTransactionError, IdentityTargetInvalidError, 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 b6f708572..d71af51d1 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,5 +1,5 @@ import { TestClock } from 'effect/testing'; -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { ActionRuntime, ActionAlreadyCommitted, 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 4ac3dab4e..2bb65e617 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Predicate } from 'effect'; import type { DeploymentAllowlist } from '../../api/modules/deployment-allowlist.ts'; import { 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 ada046976..4c505c41d 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 2d0005646..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,5 +1,5 @@ import fs from 'node:fs'; -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema } from 'effect'; import { deriveInstalledVerticalIds, 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 b2e2ded88..6a19e6de2 100644 --- a/app/apps/shell-super-app/tests/unit/layout.test.tsx +++ b/app/apps/shell-super-app/tests/unit/layout.test.tsx @@ -1,4 +1,4 @@ -import { afterEach, expect, it, rstest, test } from '@app/effect-rstest'; +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' }; 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 6c358773a..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { ContextAccess, LegalEntityContext } from '@app/core-runtime'; import type { ContextAccessService, LegalEntityContextService } from '@app/core-runtime'; import { Effect, Layer, Predicate } from 'effect'; 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 c78d6c32a..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Clock, Effect, Fiber, Function as Fn, Match, Predicate, Schema } from 'effect'; import { TestClock } from 'effect/testing'; import { 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 ddb1527e9..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,4 +1,4 @@ -import { beforeEach, expect, rstest, it } from '@app/effect-rstest'; +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', 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 3afcde321..6000a58b7 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,7 +1,7 @@ import { browserRuntime } from '../../../../src/runtime/browser-effect-runtime.ts' with { rstest: 'importActual', }; -import { afterEach, beforeEach, expect, rstest, it } from '@app/effect-rstest'; +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'; 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 816f06c20..af6d53bc8 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 '@app/effect-rstest'; +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 bf1420939..fb79685a8 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,7 +1,7 @@ import { browserRuntime } from '../../../../src/runtime/browser-effect-runtime.ts' with { rstest: 'importActual', }; -import { afterEach, beforeEach, expect, rstest, it } from '@app/effect-rstest'; +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'; 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 6f383fcd3..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,4 +1,4 @@ -import { beforeEach, expect, rstest, it } from '@app/effect-rstest'; +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 { 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 1ee6369e4..f4a3f9455 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,4 +1,4 @@ -import { afterEach, beforeEach, expect, rstest, it } from '@app/effect-rstest'; +import { afterEach, beforeEach, expect, rstest, it } from 'effect-rstest'; import { cleanup, render, screen, waitFor } from '@testing-library/react'; import { Effect, Schema } from 'effect'; import type { ReactNode } from 'react'; 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 ec7bcab6a..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { buildInstalledModuleCatalog, resolveInstalledModuleCatalog } from '@app/core-runtime'; import type { ContextAccessDecision, 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 131fca654..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 '@app/effect-rstest'; +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 3f3ef85cf..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { buildInstalledModuleCatalog } from '@app/core-runtime'; import type { ContextAccessDecision, 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 ad15f882a..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { readFile } from 'node:fs/promises'; import { Effect } from 'effect'; import { diff --git a/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md b/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md index e114796eb..f8dd185bc 100644 --- a/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md +++ b/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md @@ -153,7 +153,7 @@ 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 uses the certified `@app/effect-rstest` harness (`it.effect`/`it.live`/`it.layer`), +- 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 517a1213b..80c82d46e 100644 --- a/app/oxlint.config.ts +++ b/app/oxlint.config.ts @@ -169,8 +169,6 @@ export default defineConfig({ 'dist', 'node_modules', 'repos/**', - // vendored port of @effect/vitest; kept diffable against upstream - 'packages/effect-rstest/src/**', '.modern', '.modernjs', '**/modern-tanstack/**', diff --git a/app/package.json b/app/package.json index 4580daa65..9a4c4aa9f 100644 --- a/app/package.json +++ b/app/package.json @@ -97,8 +97,8 @@ "@effect/sql-pg": "4.0.0-beta.107" }, "devDependencies": { - "@app/effect-rstest": "workspace:*", - "@rstest/core": "0.11.10", + "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", diff --git a/app/packages/core-runtime/package.json b/app/packages/core-runtime/package.json index a7efd3507..028009fe3 100644 --- a/app/packages/core-runtime/package.json +++ b/app/packages/core-runtime/package.json @@ -43,7 +43,7 @@ "@types/node": "^20.19.43", "@types/pg": "8.20.0", "drizzle-kit": "1.0.0-rc.5-ab785fc", - "@app/effect-rstest": "workspace:*", - "@rstest/core": "0.11.10" + "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc", + "@rstest/core": "0.11.11" } } 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 816d3ab24..ddc6734b3 100644 --- a/app/packages/core-runtime/tests/integration/action-permission.test.ts +++ b/app/packages/core-runtime/tests/integration/action-permission.test.ts @@ -1,5 +1,5 @@ // 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 '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { v1 } from '@authzed/authzed-node'; import { and, eq } from 'drizzle-orm'; import { Context, Effect, Layer, Exit, Schema, Predicate } from 'effect'; 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 665ca79cd..add4ac2a0 100644 --- a/app/packages/core-runtime/tests/integration/action-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/action-runtime.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { ConnectionError, SqlError, UnknownError } from 'effect/unstable/sql/SqlError'; import { and, eq } from 'drizzle-orm'; import { Cause, Deferred, Effect, Layer, Exit, Fiber, Option, Schema, Predicate } from 'effect'; 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 d14e5f7cf..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { NodeServices } from '@effect/platform-node'; import { Crypto, Effect, FileSystem, Schema } from 'effect'; import { Pool } from 'pg'; 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 0c8af3649..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { NodeServices } from '@effect/platform-node'; import { v1 } from '@authzed/authzed-node'; import { Crypto, Effect, FileSystem } from 'effect'; 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 4fd34a319..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { v1 } from '@authzed/authzed-node'; import { and, eq, inArray } from 'drizzle-orm'; 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 48d975903..2dc23435b 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { eq } from 'drizzle-orm'; import { Effect, Predicate } from 'effect'; 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 f0ced9c3d..56b9af4e0 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { and, eq } from 'drizzle-orm'; import { Effect, Exit, Option, Predicate } from 'effect'; import { randomUUID } from 'node:crypto'; 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 8bec0821c..10b8bdd7e 100644 --- a/app/packages/core-runtime/tests/integration/outbox-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/outbox-runtime.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { randomUUID } from 'node:crypto'; import { and, asc, eq, inArray } from 'drizzle-orm'; import { DateTime, Effect, Option, Schema, pipe } from 'effect'; 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 0e151d21e..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Redacted } from 'effect'; import { Pool } from 'pg'; import type { PoolClient } from 'pg'; 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 5d83f2f31..3e4ff1376 100644 --- a/app/packages/core-runtime/tests/integration/principal-management.test.ts +++ b/app/packages/core-runtime/tests/integration/principal-management.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { eq } from 'drizzle-orm'; import { Effect, Predicate, Schema } from 'effect'; 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 a907a4f05..30c45fae3 100644 --- a/app/packages/core-runtime/tests/integration/principal-resolver.test.ts +++ b/app/packages/core-runtime/tests/integration/principal-resolver.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { and, eq } from 'drizzle-orm'; import { DateTime, Effect, Predicate } from 'effect'; 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 1615fe69c..c821b4535 100644 --- a/app/packages/core-runtime/tests/integration/read-runtime.test.ts +++ b/app/packages/core-runtime/tests/integration/read-runtime.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { getTableConfig } from 'drizzle-orm/pg-core'; import { Effect, Schema } from 'effect'; 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 8bef0da0f..4e11e939a 100644 --- a/app/packages/core-runtime/tests/integration/search-persistence.test.ts +++ b/app/packages/core-runtime/tests/integration/search-persistence.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Function as Fn, Schema, Predicate } from 'effect'; import { randomUUID } from 'node:crypto'; 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 acdcaf4fd..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { NodeServices } from '@effect/platform-node'; import { eq, sql } from 'drizzle-orm'; 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 d4ec5e576..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { getTableConfig, pgSchema, text, uuid } from 'drizzle-orm/pg-core'; import { Effect, Option, Schema } from 'effect'; 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 c21696130..695ce8429 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { SqlError, UnknownError } from 'effect/unstable/sql/SqlError'; import { and, asc, eq, inArray } from 'drizzle-orm'; 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 32af05c70..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 { Effect, Schema } from 'effect'; -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { decideAuthorizationRollout } from '../../src/authorization/rollout-decision.ts'; import type { AuthorizationWouldDenyEvent } from '../../src/authorization/rollout-decision.ts'; 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 eadc43450..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema, Predicate } from 'effect'; import { createActionCollector } from '../../src/actions/collector.ts'; 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 1bf5ba775..895ac03ba 100644 --- a/app/packages/core-runtime/tests/unit/action-definition.test.ts +++ b/app/packages/core-runtime/tests/unit/action-definition.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { DateTime, Effect, Option, Schema, Predicate } from 'effect'; import { decodeActionPayload, 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 dcc160b61..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Schema, Predicate } from 'effect'; import { ACTION_CORE_ERROR_TAGS, 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 66bb8dffd..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 a06ade87c..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema } from 'effect'; import { bindManagedApiKeyAction, 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 de79fffb6..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { v1 } from '@authzed/authzed-node'; import { Effect, Schema } from 'effect'; import { 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 c9f72809e..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Predicate } from 'effect'; import { defineGlobalPolicy, 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 9c8828a9d..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { computeActionRequestHash, computeCanonicalValueHash, 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 df2756d42..70713d23b 100644 --- a/app/packages/core-runtime/tests/unit/action-runtime.test.ts +++ b/app/packages/core-runtime/tests/unit/action-runtime.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Cause, DateTime, 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 a540a4bd5..bc0113e65 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema, Predicate } from 'effect'; import { defineAction } from '../../src/actions/definition.ts'; import { ACTION_RUNTIME_STAGES } from '../../src/actions/runtime.ts'; 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 7f06d1d5c..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema, Struct } from 'effect'; import { canonicalizeApplicationComposition, 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 879a27096..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { compareApplicationCatalog, expectedCoreTableCatalog } from '../../src/db/catalog.ts'; import type { CatalogEntry } from '../../src/db/catalog.ts'; 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 a63622011..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; /* oxlint-disable sonarjs/no-undefined-assignment -- Existing compatibility boundary; expires: 2026-12-31. */ diff --git a/app/packages/core-runtime/tests/unit/config.test.ts b/app/packages/core-runtime/tests/unit/config.test.ts index 298d856c6..0465cd1ef 100644 --- a/app/packages/core-runtime/tests/unit/config.test.ts +++ b/app/packages/core-runtime/tests/unit/config.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Predicate } from 'effect'; import { acquirePoolResource } from '../../src/db/client.ts'; 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 40fe9b2ba..9487b0b4f 100644 --- a/app/packages/core-runtime/tests/unit/context-access.test.ts +++ b/app/packages/core-runtime/tests/unit/context-access.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { v1 } from '@authzed/authzed-node'; import { Effect } from 'effect'; 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 cd2fdaa9c..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { EffectDrizzleQueryError } from 'drizzle-orm/effect-core'; import { Cause, Option, Schema, Predicate } from 'effect'; import { SqlError, UniqueViolation } from 'effect/unstable/sql/SqlError'; 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 9eff1c3cd..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Schema } from 'effect'; import { 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 9fff01a88..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,5 +1,5 @@ // @effect-diagnostics strictEffectProvide:off -- Test-owned logger capture entrypoint; expires: 2026-12-31. -import { expect, it } from '@app/effect-rstest'; +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'; 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 34f2d3566..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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 { Effect, Match, Redacted, Schema, Predicate } from 'effect'; 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 84b481216..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Predicate } from 'effect'; import { ConnectionError, SqlError } from 'effect/unstable/sql/SqlError'; 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 a6621e05b..c444d6c81 100644 --- a/app/packages/core-runtime/tests/unit/module-catalog.test.ts +++ b/app/packages/core-runtime/tests/unit/module-catalog.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { buildInstalledModuleCatalog, resolveInstalledModuleCatalog, 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 1a263df83..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 926a83892..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Tracer, Predicate } from 'effect'; import { MODULE_ENTRYPOINT_ACCESSES, 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 e898c9918..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Clock, Config, 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 46e070c1c..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 c9ea91a72..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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 { 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 c99959d0e..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema, Predicate } from 'effect'; import { defineOutboxWorker, 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 cb6ec058c..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 5a04682ac..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Fiber, Schema } from 'effect'; import { TestClock } from 'effect/testing'; import { defineOutboxWorker } from '../../src/outbox/definition.ts'; 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 e96cfc122..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,5 +1,5 @@ import { NodeServices } from '@effect/platform-node'; -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Deferred, Effect, Fiber, Layer, Stream } from 'effect'; import { ChildProcess } from 'effect/unstable/process'; 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 3eb7b041a..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 052aad0d2..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,4 +1,4 @@ -import { expect, it, rstest } from '@app/effect-rstest'; +import { expect, it, rstest } from 'effect-rstest'; import { v1 } from '@authzed/authzed-node'; import { Cause, Effect, Fiber, Predicate, Schema } from 'effect'; import { TestClock } from 'effect/testing'; 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 26dfb63dc..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Redacted, Predicate } from 'effect'; import { DEFAULT_DATABASE_POOL_DEADLINES, 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 1a406a4c5..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Option, Predicate } from 'effect'; import type { 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 f4df12e85..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { DateTime, Effect, Predicate } from 'effect'; import { ConnectionError, SqlError } from 'effect/unstable/sql/SqlError'; 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 d42522157..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,5 +1,5 @@ // @effect-diagnostics nodeBuiltinImport:off -- Verifies package source files via the Node filesystem boundary; expires: 2026-12-31. -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { readFile } from 'node:fs/promises'; 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 52a126268..5d0551971 100644 --- a/app/packages/core-runtime/tests/unit/read-runtime.test.ts +++ b/app/packages/core-runtime/tests/unit/read-runtime.test.ts @@ -1,5 +1,5 @@ /* oxlint-disable sonarjs/use-type-alias, typescript/no-unsafe-type-assertion -- Existing compatibility boundary; expires: 2026-12-31. */ -import { expect, it } from '@app/effect-rstest'; +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'; 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 52e16bab9..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { getTableName, isTable } from 'drizzle-orm'; import { getTableConfig, PgDialect } from 'drizzle-orm/pg-core'; 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 65073ddc4..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Option, Predicate } from 'effect'; import { 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 c60f35f58..917f7eb62 100644 --- a/app/packages/core-runtime/tests/unit/search-ingestion.test.ts +++ b/app/packages/core-runtime/tests/unit/search-ingestion.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Predicate } from 'effect'; import { CORE_SEARCH_INGESTION_REGISTRATIONS, 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 df560ca15..4eb5e9869 100644 --- a/app/packages/core-runtime/tests/unit/search-projection.test.ts +++ b/app/packages/core-runtime/tests/unit/search-projection.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema, Predicate } from 'effect'; import { TestClock } from 'effect/testing'; import { 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 3cccb558e..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { getTableConfig, PgDialect } from 'drizzle-orm/pg-core'; import { searchIndexEntries, 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 9b09e71c6..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 f27c09267..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import type { ContextAccessService, InstalledModuleCatalogServiceContract, 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 2b62df33e..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,5 +1,5 @@ import { Schema } from 'effect'; -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { validateShellContributions } from '../../src/modules/shell-contribution.ts'; const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Any)); 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 57489e822..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 41b563aa4..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,6 +1,6 @@ // @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 '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { readFile } from 'node:fs/promises'; import { parseSpiceDbDatabaseBootstrapConfig } from '../../src/install/spicedb-database-config.ts'; import { toModuleAccessObjectId } from '../../src/permissions/context-access.ts'; 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 9aca37712..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { STAGE_CONTEXTS } from '../../src/install/stage-context-bootstrap.ts'; it('defines the exact Techsio and Siampark stage contexts', () => { 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 ade723dde..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 6820dbea1..c2d9d0c4c 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,5 +1,5 @@ // @effect-diagnostics preferSchemaOverJson:off -- Verifies native JSON serialization of errors and schema AST metadata; expires: 2026-12-31. -import { expect, it } from '@app/effect-rstest'; +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 { InstalledModuleCatalog, OntosModuleDeploymentContract } from '../../src/index.ts'; diff --git a/app/packages/effect-rstest/LICENSE b/app/packages/effect-rstest/LICENSE deleted file mode 100644 index d5aad8163..000000000 --- a/app/packages/effect-rstest/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -MIT License - -Copyright (c) 2023 Effectful Technologies Inc -Copyright (c) 2026 ScriptedAlchemy (effect-rstest port) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/app/packages/effect-rstest/README.md b/app/packages/effect-rstest/README.md deleted file mode 100644 index c9acd57bc..000000000 --- a/app/packages/effect-rstest/README.md +++ /dev/null @@ -1,95 +0,0 @@ -# @app/effect-rstest - -Vendored community port of `@effect/vitest` to Rstest by ScriptedAlchemy, commit `79abbf6`. Source: https://github.com/ScriptedAlchemy/effect-rstest. MIT licensed; the original copyright and permission notice are preserved in [LICENSE](./LICENSE). - -Workspace exports use TypeScript source. Local corrections cover Rstest lifecycle integration, schema-backed property tests, and Effect semantic equality, alongside import and diagnostic adaptations. - -## Usage - -Import `it` and `expect` from `@app/effect-rstest`. Choose the smallest runner that fits: - -- **Plain `it`**: synchronous assertions with no Effect program. -- **`it.effect`**: return an Effect directly. It supplies a test clock and test console; use `TestClock.adjust` from `effect/testing` to advance Effect sleeps deterministically. -- **`it.live`**: return an Effect using live services instead of the test clock/console. Use it when real elapsed time is necessary, such as integration tests coordinating Effect deadlines with external database or network I/O. Ordinary deterministic Effect tests should use `it.effect`. - -```ts -import { expect, it } from '@app/effect-rstest'; -import { Effect } from 'effect'; - -it('adds numbers', () => { - expect(1 + 1).toBe(2); -}); - -it.effect('reads an Effect value', () => - Effect.gen(function* readsValue() { - const value = yield* Effect.succeed(42); - expect(value).toBe(42); - }), -); -``` - -### Properties and equality - -`it.prop` accepts FastCheck arbitraries and Effect schemas in either tuples or records. Use -`it.effect.prop` when the property itself returns an Effect. - -```ts -import { addEqualityTesters, expect, it } from '@app/effect-rstest'; -import { Schema } from 'effect'; - -addEqualityTesters(); - -it.prop('generates schema values', [Schema.String], ([value]) => { - expect(typeof value).toBe('string'); -}); -``` - -`addEqualityTesters` enables Effect's `Equal.equals` for values implementing its equality protocol. -Ordinary objects and asymmetric matchers retain Rstest's native equality behavior. - -### Shared layers - -Use `it.layer` to share a layer across a suite; its resources are released when the suite ends. Set `excludeTestServices: true` when the suite needs live services instead of the test clock/console (the default is `false`). Replace `Layer.empty` below with your fixture layer: - -```ts -import { expect, it } from '@app/effect-rstest'; -import { Effect, Layer } from 'effect'; - -it.layer(Layer.empty, { excludeTestServices: true })('live fixture suite', (it) => { - it.effect('reads an Effect value', () => - Effect.gen(function* readsValue() { - const value = yield* Effect.succeed(42); - expect(value).toBe(42); - }), - ); -}); -``` - -### Scoped cleanup - -Both `it.effect` and `it.live` automatically own and close a per-test scope. Register cleanup with `Effect.acquireRelease` (or `Effect.addFinalizer` for an already-acquired resource); no extra `Effect.scoped` or Promise bridge is needed. Finalizers run on success, failure, and interruption. - -```ts -it.effect('cleans up its fixture', () => - Effect.gen(function* scopedFixture() { - const cache = yield* Effect.acquireRelease( - Effect.sync(() => new Map()), - (resource) => Effect.sync(() => resource.clear()), - ); - cache.set('answer', 42); - expect(cache.get('answer')).toBe(42); - }), -); -``` - -For an existing resource, register its Effect cleanup before using it: `yield* Effect.addFinalizer(() => release(resource))`. Use `acquireRelease` when acquisition and registration must be interruption-safe together. - -**Do not use JavaScript `try/finally` for Effect cleanup.** A failed yielded Effect short-circuits the generator; JavaScript `finally` does not finalize failed yielded Effects. Register an Effect finalizer instead, so cleanup also runs when a yield fails or the test is interrupted. - -On a runner timeout, the adapter interrupts the Effect and waits for its finalizers before the next -sequential test and enclosing layer teardown. Cleanup is not cut short by the runner's hook timeout, -so a finalizer that never settles can hold completion indefinitely. - -Rstest runs native `afterEach` hooks before its test-finished callbacks. Those hooks can therefore -run while timed-out Effect cleanup is still pending. Keep resource release in Effect finalizers, -not native hooks. This ordering guarantee does not serialize concurrent tests. diff --git a/app/packages/effect-rstest/package.json b/app/packages/effect-rstest/package.json deleted file mode 100644 index faac4cc64..000000000 --- a/app/packages/effect-rstest/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@app/effect-rstest", - "version": "0.1.0", - "private": true, - "type": "module", - "license": "MIT", - "exports": { - ".": "./src/index.ts", - "./utils": "./src/utils.ts" - }, - "scripts": { - "test:unit": "rstest", - "typecheck": "node ../../scripts/ultramodern-typecheck.mts --project tsconfig.json" - }, - "dependencies": { - "effect": "4.0.0-beta.107", - "@rstest/core": "0.11.10" - }, - "devDependencies": { - "@effect/platform-node": "4.0.0-beta.107", - "@effect/tsgo": "0.19.0", - "@types/node": "20.19.43" - } -} diff --git a/app/packages/effect-rstest/rstest.config.ts b/app/packages/effect-rstest/rstest.config.ts deleted file mode 100644 index b3d0280f9..000000000 --- a/app/packages/effect-rstest/rstest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from '@rstest/core'; - -export default defineConfig({ - include: ['tests/**/*.test.ts'], - testEnvironment: 'node', -}); diff --git a/app/packages/effect-rstest/src/index.ts b/app/packages/effect-rstest/src/index.ts deleted file mode 100644 index 30f51988f..000000000 --- a/app/packages/effect-rstest/src/index.ts +++ /dev/null @@ -1,282 +0,0 @@ -/** - * Helpers for testing Effect (v4) code with [Rstest](https://rstest.rs). - * - * This module is a port of `@effect/vitest` to the Rstest runner. The public - * API mirrors `@effect/vitest`: an enhanced `it` with `effect`, `live`, - * `layer`, `prop` and `flakyTest`, plus `addEqualityTesters` and - * `describeWrapped`. - * - * @since 0.1.0 - */ -import type * as Duration from 'effect/Duration'; -import type * as Effect from 'effect/Effect'; -import type * as Layer from 'effect/Layer'; -import type * as Schema from 'effect/Schema'; -import type * as Scope from 'effect/Scope'; -import type * as FC from 'effect/testing/FastCheck'; -import * as Rs from '@rstest/core'; -import * as internal from './internal/internal.ts'; - -/** - * Re-exports everything from `@rstest/core` (`describe`, `expect`, `assert`, - * hooks, `rs`, ...). - * - * @since 0.1.0 - */ -export * from '@rstest/core'; - -/** - * @since 0.1.0 - */ -export type API = Rs.TestAPIs; - -/** - * Type namespace retained from `@effect/vitest` for source compatibility. - * - * @since 0.1.0 - */ -export namespace Vitest { - /** - * @since 0.1.0 - */ - export interface TestFunction> { - (...args: TestArgs): Effect.Effect; - } - - /** - * @since 0.1.0 - */ - export interface Test { - ( - name: string, - self: TestFunction, - timeout?: number | Rs.TestOptions, - ): void; - } - - /** - * @since 0.1.0 - */ - export type Arbitraries = - | Array | FC.Arbitrary> - | { [K in string]: Schema.Schema | FC.Arbitrary }; - - /** - * @since 0.1.0 - */ - export interface Tester extends Vitest.Test { - skip: Vitest.Test; - skipIf: (condition: unknown) => Vitest.Test; - runIf: (condition: unknown) => Vitest.Test; - only: Vitest.Test; - each: ( - cases: ReadonlyArray, - ) => ( - name: string, - self: TestFunction>, - timeout?: number | Rs.TestOptions, - ) => void; - fails: Vitest.Test; - - /** - * @since 0.1.0 - */ - prop: ( - name: string, - arbitraries: Arbs, - self: TestFunction< - A, - E, - R, - [ - { - [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary - ? T - : Arbs[K] extends Schema.Schema - ? T - : never; - }, - Rs.TestContext, - ] - >, - timeout?: - | number - | (Rs.TestOptions & { - fastCheck?: FC.Parameters<{ - [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary - ? T - : Arbs[K] extends Schema.Schema - ? T - : never; - }>; - }), - ) => void; - } - - /** - * @since 0.1.0 - */ - export interface MethodsNonLive extends API { - readonly effect: Vitest.Tester; - readonly describe: Rs.Describe; - readonly flakyTest: ( - self: Effect.Effect, - timeout?: Duration.Input, - ) => Effect.Effect; - readonly layer: ( - layer: Layer.Layer, - options?: { - readonly timeout?: Duration.Input; - }, - ) => { - (f: (it: Vitest.MethodsNonLive) => void): void; - (name: string, f: (it: Vitest.MethodsNonLive) => void): void; - }; - - /** - * @since 0.1.0 - */ - readonly prop: ( - name: string, - arbitraries: Arbs, - self: ( - properties: { - [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary - ? T - : Arbs[K] extends Schema.Schema - ? T - : never; - }, - ctx: Rs.TestContext, - ) => void, - timeout?: - | number - | (Rs.TestOptions & { - fastCheck?: FC.Parameters<{ - [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary - ? T - : Arbs[K] extends Schema.Schema - ? T - : never; - }>; - }), - ) => void; - } - - /** - * @since 0.1.0 - */ - export interface Methods extends MethodsNonLive { - readonly live: Vitest.Tester; - readonly layer: ( - layer: Layer.Layer, - options?: { - readonly memoMap?: Layer.MemoMap; - readonly timeout?: Duration.Input; - readonly excludeTestServices?: boolean; - }, - ) => { - (f: (it: Vitest.MethodsNonLive) => void): void; - (name: string, f: (it: Vitest.MethodsNonLive) => void): void; - }; - } -} - -/** - * @since 0.1.0 - */ -export const addEqualityTesters: () => void = internal.addEqualityTesters; - -/** - * @since 0.1.0 - */ -export const effect: Vitest.Tester = internal.effect; - -/** - * @since 0.1.0 - */ -export const live: Vitest.Tester = internal.live; - -/** - * Share a `Layer` between multiple tests, optionally wrapping - * the tests in a `describe` block if a name is provided. - * - * @since 0.1.0 - * - * ```ts - * import { assert, layer } from "effect-rstest" - * import { Effect, Layer, Context } from "effect" - * - * class Foo extends Context.Service()("Foo") { - * static layer = Layer.succeed(Foo, "foo") - * } - * - * class Bar extends Context.Service()("Bar") { - * static layer = Layer.effect( - * Bar, - * Effect.map(Foo, () => "bar" as const) - * ) - * } - * - * layer(Foo.layer)("layer", (it) => { - * it.effect("adds context", () => - * Effect.gen(function*() { - * const foo = yield* Foo - * assert.strictEqual(foo, "foo") - * })) - * - * it.layer(Bar.layer)("nested", (it) => { - * it.effect("adds context", () => - * Effect.gen(function*() { - * const foo = yield* Foo - * const bar = yield* Bar - * assert.strictEqual(foo, "foo") - * assert.strictEqual(bar, "bar") - * })) - * }) - * }) - * ``` - */ -export const layer: ( - layer_: Layer.Layer, - options?: { - readonly memoMap?: Layer.MemoMap; - readonly timeout?: Duration.Input; - readonly excludeTestServices?: boolean; - }, -) => { - (f: (it: Vitest.MethodsNonLive) => void): void; - (name: string, f: (it: Vitest.MethodsNonLive) => void): void; -} = internal.layer; - -/** - * @since 0.1.0 - */ -export const flakyTest: ( - self: Effect.Effect, - timeout?: Duration.Input, -) => Effect.Effect = internal.flakyTest; - -/** - * @since 0.1.0 - */ -export const prop: Vitest.Methods['prop'] = internal.prop; - -/** - * @since 0.1.0 - */ -export const it: Vitest.Methods = internal.makeMethods(Rs.it); - -/** - * @since 0.1.0 - */ -export const makeMethods: (it: Rs.TestAPIs) => Vitest.Methods = internal.makeMethods; - -/** - * Unlike `@effect/vitest`, this returns `void` because Rstest's `describe` - * does not return a `SuiteCollector`. - * - * @since 0.1.0 - */ -export const describeWrapped: (name: string, f: (it: Vitest.Methods) => void) => void = - internal.describeWrapped; diff --git a/app/packages/effect-rstest/src/internal/internal.ts b/app/packages/effect-rstest/src/internal/internal.ts deleted file mode 100644 index ebbab9cab..000000000 --- a/app/packages/effect-rstest/src/internal/internal.ts +++ /dev/null @@ -1,377 +0,0 @@ -// @effect-diagnostics missedPipeableOpportunity:off strictEffectProvide:off -- vendored port of @effect/vitest; remove-when: upstream removes this runtime boundary -/** - * @since 0.1.0 - */ - -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'; -import * as Rec from 'effect/Record'; -import * as Schedule from 'effect/Schedule'; -import * as Schema from 'effect/Schema'; -import * as Scope from 'effect/Scope'; -import * as fc from 'effect/testing/FastCheck'; -import * as TestClock from 'effect/testing/TestClock'; -import * as TestConsole from 'effect/testing/TestConsole'; -import * as Rs from '@rstest/core'; -import type * as EffectRstest from '../index.ts'; - -const runPromise: ( - _: Effect.Effect, - ctx?: Rs.TestContext | undefined, -) => Promise = Effect.fnUntraced( - function* (effect: Effect.Effect, _ctx?: Rs.TestContext) { - const exit = yield* Effect.exit(effect); - if (Exit.isFailure(exit)) { - const errors = Cause.prettyErrors(exit.cause); - for (let i = 0; i < errors.length; i++) { - yield* Effect.logError(errors[i]); - } - } - return yield* exit; - }, - (effect, _, ctx) => Effect.runPromise(effect, { signal: ctx?.signal }), -); - -/** @internal */ -const runTest = - (ctx?: Rs.TestContext) => - (effect: Effect.Effect) => { - let settlement: Promise | undefined; - // Rstest aborts on timeout without awaiting the callback. Keep its outcome, - // but await Effect finalizers before the next test or suite teardown. Native - // afterEach hooks run earlier than onTestFinished and are not covered. - // Do not race cleanup against another hook timeout and reintroduce the leak. - ctx?.onTestFinished(() => settlement, 0); - const result = runPromise(effect, ctx); - // Cleanup must not rethrow a failure that Rstest already handled (e.g. fails). - settlement = result.then( - () => {}, - () => {}, - ); - return result; - }; - -/** @internal */ -export type TestContext = TestConsole.TestConsole | TestClock.TestClock; - -const TestEnv = Layer.mergeAll(TestConsole.layer, TestClock.layer()); - -/** @internal */ -export const addEqualityTesters = () => { - Rs.expect.addEqualityTesters([ - (a, b) => (Equal.isEqual(a) && Equal.isEqual(b) ? Equal.equals(a, b) : undefined), - ]); -}; - -/** @internal */ -const testOptions = (timeout?: number | Rs.TestOptions): Rs.TestOptions => - typeof timeout === 'number' ? { timeout } : (timeout ?? {}); - -const hookTimeout = (timeout?: Duration.Input) => - timeout === undefined ? undefined : Duration.toMillis(Duration.fromInputUnsafe(timeout)); - -const makeItProxy = ( - it: Rs.TestAPIs, - overrides: Methods, -): Methods & Rs.TestAPIs => - new Proxy(it as Methods & Rs.TestAPIs, { - apply(target, thisArg, argArray) { - return Reflect.apply(target, thisArg, argArray); - }, - get(target, property, receiver) { - if (Object.hasOwn(overrides, property)) { - return Reflect.get(overrides, property); - } - // do not bind: binding would strip rstest's static helpers (e.g. `it.each`) - return Reflect.get(target, property, receiver); - }, - }); - -/** @internal */ -const makeTester = ( - mapEffect: (self: Effect.Effect) => Effect.Effect, - it: Rs.TestAPIs = Rs.it, -): EffectRstest.Vitest.Tester => { - // rstest's test callbacks must return `MaybePromise`, so the test - // value is intentionally discarded here (vitest accepts any return value) - const run = >( - ctx: Rs.TestContext & object, - args: TestArgs, - self: EffectRstest.Vitest.TestFunction, - ): Promise => - pipe( - Effect.suspend(() => self(...args)), - mapEffect, - runTest(ctx), - ); - - const f: EffectRstest.Vitest.Test = (name, self, timeout) => - it(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)); - - const skip: EffectRstest.Vitest.Tester['only'] = (name, self, timeout) => - it.skip(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)); - - const skipIf: EffectRstest.Vitest.Tester['skipIf'] = (condition) => (name, self, timeout) => - it.skipIf(Boolean(condition))(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)); - - const runIf: EffectRstest.Vitest.Tester['runIf'] = (condition) => (name, self, timeout) => - it.runIf(Boolean(condition))(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)); - - const only: EffectRstest.Vitest.Tester['only'] = (name, self, timeout) => - it.only(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)); - - const each: EffectRstest.Vitest.Tester['each'] = (cases) => (name, self, timeout) => - it.for(cases)(name, testOptions(timeout), (args, ctx) => run(ctx, [args], self) as any); - - const fails: EffectRstest.Vitest.Tester['fails'] = (name, self, timeout) => - it.fails(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self)); - - const prop: EffectRstest.Vitest.Tester['prop'] = (name, arbitraries, self, timeout) => { - if (Array.isArray(arbitraries)) { - const arbs = arbitraries.map((arbitrary) => { - if (Schema.isSchema(arbitrary)) { - return Schema.toArbitrary(arbitrary)(fc); - } - return arbitrary as fc.Arbitrary; - }); - return it(name, testOptions(timeout), (ctx) => - // @ts-ignore - fc.assert( - // @ts-ignore - fc.asyncProperty(...arbs, (...as) => run(ctx, [as as any, ctx], self)), - // @ts-ignore - // @ts-ignore -- upstream variadic FastCheck options - isObject(timeout) ? timeout?.['fastCheck'] : {}, - ), - ); - } - - const arbs = fc.record( - Object.keys(arbitraries).reduce( - function (result, key) { - const arb: any = arbitraries[key]; - Rec.assignProperty(result, key, Schema.isSchema(arb) ? Schema.toArbitrary(arb)(fc) : arb); - return result; - }, - {} as Record>, - ), - ); - - return it(name, testOptions(timeout), (ctx) => - // @ts-ignore - fc.assert( - fc.asyncProperty(arbs, (...as) => - // @ts-ignore - run(ctx, [as[0] as any, ctx], self), - ), - // @ts-ignore - // @ts-ignore -- upstream variadic FastCheck options - isObject(timeout) ? timeout?.['fastCheck'] : {}, - ), - ); - }; - - return Object.assign(f, { skip, skipIf, runIf, only, each, fails, prop }); -}; - -/** @internal */ -export const prop: EffectRstest.Vitest.Methods['prop'] = (name, arbitraries, self, timeout) => { - if (Array.isArray(arbitraries)) { - const arbs = arbitraries.map((arbitrary) => { - if (Schema.isSchema(arbitrary)) { - return Schema.toArbitrary(arbitrary)(fc); - } - return arbitrary; - }); - return Rs.it( - name, - testOptions(timeout), - // @ts-ignore - (ctx) => - fc.assert( - // @ts-ignore -- upstream variadic FastCheck typing - fc.property(...arbs, (...as) => self(as, ctx)), - // @ts-ignore -- upstream variadic FastCheck options - isObject(timeout) ? timeout?.['fastCheck'] : {}, - ), - ); - } - - const arbs = fc.record( - Object.keys(arbitraries).reduce( - function (result, key) { - const arb: any = arbitraries[key]; - Rec.assignProperty(result, key, Schema.isSchema(arb) ? Schema.toArbitrary(arb)(fc) : arb); - return result; - }, - {} as Record>, - ), - ); - - return Rs.it( - name, - testOptions(timeout), - // @ts-ignore - (ctx) => - fc.assert( - // @ts-ignore -- upstream variadic FastCheck typing - fc.property(arbs, (as) => self(as, ctx)), - // @ts-ignore -- upstream variadic FastCheck options - isObject(timeout) ? timeout?.['fastCheck'] : {}, - ), - ); -}; - -/** @internal */ -export const layer = - ( - layer_: Layer.Layer, - options?: { - readonly memoMap?: Layer.MemoMap; - readonly timeout?: Duration.Input; - readonly excludeTestServices?: boolean; - }, - ): { - (f: (it: EffectRstest.Vitest.MethodsNonLive) => void): void; - (name: string, f: (it: EffectRstest.Vitest.MethodsNonLive) => void): void; - } => - ( - ...args: - | [name: string, f: (it: EffectRstest.Vitest.MethodsNonLive) => void] - | [f: (it: EffectRstest.Vitest.MethodsNonLive) => void] - ) => { - const excludeTestServices = options?.excludeTestServices ?? false; - const withTestEnv = excludeTestServices - ? (layer_ as Layer.Layer) - : Layer.provideMerge(layer_, TestEnv); - 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: 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; - // SuiteContext has no AbortSignal: a timed-out beforeAll keeps running. - // Stop and await setup 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 => - makeItProxy(it, { - effect: makeTester( - (effect) => - Effect.flatMap(contextEffect, (context) => - effect.pipe(Effect.scoped, Effect.provide(context)), - ), - it, - ), - describe: Rs.describe, - prop, - flakyTest, - layer( - nestedLayer: Layer.Layer, - options?: { - readonly timeout?: Duration.Input; - }, - ) { - return layer(Layer.provideMerge(nestedLayer, withTestEnv), { - ...options, - memoMap: Layer.forkMemoMapUnsafe(memoMap), - excludeTestServices, - }); - }, - }); - - if (args.length === 1) { - // Rstest has no `getCurrentSuite`, so use an empty nested suite as the - // lifecycle boundary for an unnamed layer block. Rstest omits empty suite - // names from test paths, while its beforeAll/afterAll hooks ensure the - // scope closes before later tests in the enclosing suite run. - return Rs.describe('', () => { - 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(buildContext, hookTimeout(options?.timeout)); - Rs.afterAll(() => closeScope(), hookTimeout(options?.timeout)); - return args[1](makeIt(Rs.it)); - }); - }; - -/** @internal */ -export const flakyTest = ( - self: Effect.Effect, - timeout: Duration.Input = Duration.seconds(30), -) => - pipe( - self, - Effect.scoped, - Effect.sandbox, - Effect.retry( - pipe( - Schedule.recurs(10), - Schedule.while((_) => - Effect.succeed( - Duration.isLessThanOrEqualTo( - Duration.fromInputUnsafe(_.elapsed), - Duration.fromInputUnsafe(timeout), - ), - ), - ), - ), - ), - Effect.orDie, - ); - -/** @internal */ -export const makeMethods = (it: Rs.TestAPIs): EffectRstest.Vitest.Methods => - makeItProxy(it, { - effect: makeTester(flow(Effect.scoped, Effect.provide(TestEnv)), it), - live: makeTester(Effect.scoped, it), - describe: Rs.describe, - flakyTest, - layer, - prop, - }); - -/** @internal */ -export const { - /** @internal */ - effect, - /** @internal */ - live, -} = makeMethods(Rs.it); - -/** @internal */ -export const describeWrapped = (name: string, f: (it: EffectRstest.Vitest.Methods) => void): void => - Rs.describe(name, () => f(makeMethods(Rs.it))); diff --git a/app/packages/effect-rstest/src/utils.ts b/app/packages/effect-rstest/src/utils.ts deleted file mode 100644 index 487aa7390..000000000 --- a/app/packages/effect-rstest/src/utils.ts +++ /dev/null @@ -1,335 +0,0 @@ -// @effect-diagnostics asyncFunction:off -- vendored port of @effect/vitest; remove-when: upstream removes this runtime boundary -/** - * Provides assertion helpers used by `effect-rstest` tests. - * - * This module defines small assertion functions built on Node's `assert`, - * Rstest's instance checks, and Effect's equality support. The helpers cover - * basic equality, thrown errors, defined and undefined values, strings, regular - * expressions, class instances, `Option`, `Result`, and `Exit`. Most helpers are - * synchronous; `throwsAsync` handles rejected promises. - * - * @since 0.1.0 - */ -import type * as Cause from 'effect/Cause'; -import * as Equal from 'effect/Equal'; -import * as Exit from 'effect/Exit'; -import * as Option from 'effect/Option'; -import * as Predicate from 'effect/Predicate'; -import * as Result from 'effect/Result'; -import * as assert from 'node:assert'; -import { assert as rassert } from '@rstest/core'; - -// ---------------------------- -// Primitives -// ---------------------------- - -/** - * Fails the current test with the provided error message. - * - * @category testing - * @since 0.1.0 - */ -export function fail(message: string) { - assert.fail(message); -} - -/** - * Asserts that `actual` is deeply strictly equal to `expected` using Node's `assert.deepStrictEqual`. - * - * @category testing - * @since 0.1.0 - */ -export function deepStrictEqual(actual: A, expected: A, message?: string, ..._: Array) { - assert.deepStrictEqual(actual, expected, message as string); -} - -/** - * Asserts that `actual` is not deeply strictly equal to `expected` using Node's `assert.notDeepStrictEqual`. - * - * @category testing - * @since 0.1.0 - */ -export function notDeepStrictEqual( - actual: A, - expected: A, - message?: string, - ..._: Array -) { - assert.notDeepStrictEqual(actual, expected, message as string); -} - -/** - * Asserts that `actual` is strictly equal to `expected` using Node's `assert.strictEqual`. - * - * @category testing - * @since 0.1.0 - */ -export function strictEqual(actual: A, expected: A, message?: string, ..._: Array) { - if (message !== undefined) { - assert.strictEqual(actual, expected, message); - } else { - assert.strictEqual(actual, expected); - } -} - -/** - * Asserts that `actual` is equal to `expected` using the `Equal.equals` trait. - * - * @category testing - * @since 0.1.0 - */ -export function assertEquals(actual: A, expected: A, message?: string, ..._: Array) { - if (!Equal.equals(actual, expected)) { - deepStrictEqual(actual, expected, message); // show diff - fail(message ?? 'Expected values to be Equal.equals'); - } -} - -/** - * Asserts that `thunk` does not throw an error. - * - * @category testing - * @since 0.1.0 - */ -export function doesNotThrow(thunk: () => void, message?: string, ..._: Array) { - assert.doesNotThrow(thunk, message); -} - -// ---------------------------- -// Derived -// ---------------------------- - -/** - * Asserts that `value` is an instance of `constructor`. - * - * @category testing - * @since 0.1.0 - */ -export function assertInstanceOf any>( - value: unknown, - constructor: C, - message?: string, - ..._: Array -): asserts value is InstanceType { - rassert.instanceOf(value, constructor as any, message); -} - -/** - * Asserts that `self` is `true`. - * - * @category testing - * @since 0.1.0 - */ -export function assertTrue(self: unknown, message?: string, ..._: Array): asserts self { - strictEqual(self, true, message); -} - -/** - * Asserts that `self` is `false`. - * - * @category testing - * @since 0.1.0 - */ -export function assertFalse(self: boolean, message?: string, ..._: Array) { - strictEqual(self, false, message); -} - -/** - * Asserts that `actual` includes `expected`. - * - * @category testing - * @since 0.1.0 - */ -export function assertInclude(actual: string | undefined, expected: string, ..._: Array) { - if (typeof expected === 'string') { - if (actual?.includes(expected) !== true) { - fail(`Expected\n\n${actual}\n\nto include\n\n${expected}`); - } - } -} - -/** - * Asserts that `actual` matches `regExp`. - * - * @category testing - * @since 0.1.0 - */ -export function assertMatch(actual: string, regExp: RegExp, ..._: Array) { - if (!regExp.test(actual)) { - fail(`Expected\n\n${actual}\n\nto match\n\n${regExp}`); - } -} - -/** - * Asserts that `thunk` throws, optionally checking the thrown value against an expected `Error` or validation function. - * - * @category testing - * @since 0.1.0 - */ -export function throws( - thunk: () => void, - error?: Error | ((u: unknown) => undefined), - ..._: Array -) { - try { - thunk(); - } catch (e) { - if (error !== undefined) { - if (Predicate.isFunction(error)) { - error(e); - } else { - deepStrictEqual(e, error); - } - } - return; - } - fail('Expected to throw an error'); -} - -/** - * Asserts that `thunk` throws or returns a rejected promise, optionally checking the failure value against an expected `Error` or validation function. - * - * @category testing - * @since 0.1.0 - */ -export async function throwsAsync( - thunk: () => Promise, - error?: Error | ((u: unknown) => undefined), - ..._: Array -) { - try { - await thunk(); - } catch (e) { - if (error !== undefined) { - if (Predicate.isFunction(error)) { - error(e); - } else { - deepStrictEqual(e, error); - } - } - return; - } - fail('Expected to throw an error'); -} - -// ---------------------------- -// Option -// ---------------------------- - -/** - * Asserts that `option` is `None`. - * - * @category testing - * @since 0.1.0 - */ -export function assertNone( - option: Option.Option, - ..._: Array -): asserts option is Option.None { - deepStrictEqual(option, Option.none()); -} - -/** - * Asserts that `a` is not `undefined`. - * - * @category testing - * @since 0.1.0 - */ -export function assertDefined( - a: A | undefined, - ..._: Array -): asserts a is Exclude { - if (a === undefined) { - fail('Expected value to be defined'); - } -} - -/** - * Asserts that `a` is `undefined`. - * - * @category testing - * @since 0.1.0 - */ -export function assertUndefined(a: A | undefined, ..._: Array): asserts a is undefined { - if (a !== undefined) { - fail('Expected value to be undefined'); - } -} - -/** - * Asserts that `option` is `Some` and contains a value equal to `expected`. - * - * @category testing - * @since 0.1.0 - */ -export function assertSome( - option: Option.Option, - expected: A, - ..._: Array -): asserts option is Option.Some { - deepStrictEqual(option, Option.some(expected)); -} - -// ---------------------------- -// Result -// ---------------------------- - -/** - * Asserts that `result` is `Success` and contains a value equal to `expected`. - * - * @category testing - * @since 0.1.0 - */ -export function assertSuccess( - result: Result.Result, - expected: A, - ..._: Array -): asserts result is Result.Success { - deepStrictEqual(result, Result.succeed(expected)); -} - -/** - * Asserts that `result` is `Failure` and contains an error equal to `expected`. - * - * @category testing - * @since 0.1.0 - */ -export function assertFailure( - result: Result.Result, - expected: E, - ..._: Array -): asserts result is Result.Failure { - deepStrictEqual(result, Result.fail(expected)); -} - -// ---------------------------- -// Exit -// ---------------------------- - -/** - * Asserts that `exit` is a failure with a cause equal to `expected`. - * - * @category testing - * @since 0.1.0 - */ -export function assertExitFailure( - exit: Exit.Exit, - expected: Cause.Cause, - ..._: Array -): asserts exit is Exit.Failure { - deepStrictEqual(exit, Exit.failCause(expected)); -} - -/** - * Asserts that `exit` is a success with a value equal to `expected`. - * - * @category testing - * @since 0.1.0 - */ -export function assertExitSuccess( - exit: Exit.Exit, - expected: A, - ..._: Array -): asserts exit is Exit.Success { - deepStrictEqual(exit, Exit.succeed(expected)); -} diff --git a/app/packages/effect-rstest/tests/equality.test.ts b/app/packages/effect-rstest/tests/equality.test.ts deleted file mode 100644 index 25001db9c..000000000 --- a/app/packages/effect-rstest/tests/equality.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { addEqualityTesters, expect, it } from '@app/effect-rstest'; -import { Equal, Hash } from 'effect'; - -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 { - // Deliberate collision: unequal values must reach the equality method. - return this.#key.length; - } -} - -addEqualityTesters(); - -it('uses semantic equality despite different enumerable representations', () => { - const left = new SemanticValue('same', 'left'); - const right = new SemanticValue('same', 'right'); - expect(left.representation).not.toBe(right.representation); - expect(Equal.equals(left, right)).toBe(true); - expect(left).toEqual(right); - expect({ value: left }).toEqual({ value: right }); -}); - -it('respects semantic inequality despite identical enumerable representations', () => { - const left = new SemanticValue('left', 'same'); - const right = new SemanticValue('next', 'same'); - expect(left.representation).toBe(right.representation); - expect(Equal.equals(left, right)).toBe(false); - expect(left).not.toEqual(right); - expect({ value: left }).not.toEqual({ value: right }); -}); - -it('preserves native plain-object deep equality and asymmetric matchers', () => { - expect({ nested: { value: 1 } }).toEqual({ nested: { value: 1 } }); - expect({ nested: { value: 1 } }).not.toEqual({ nested: { value: 2 } }); - expect({ nested: { value: 1 } }).toEqual({ nested: { value: expect.any(Number) } }); -}); diff --git a/app/packages/effect-rstest/tests/fixtures/layer-lifetime.fixture.ts b/app/packages/effect-rstest/tests/fixtures/layer-lifetime.fixture.ts deleted file mode 100644 index ed0aeed07..000000000 --- a/app/packages/effect-rstest/tests/fixtures/layer-lifetime.fixture.ts +++ /dev/null @@ -1,63 +0,0 @@ -// oxlint-disable effect-native/no-native-timers -- real runner hook deadlines cannot be driven by TestClock; remove-when: Rstest exposes controllable hook timers -import { describe, expect, it, layer } from '@app/effect-rstest'; -import { Effect, Layer } from 'effect'; - -// Run only in the child runner: setup failures here are intentional. -for (const named of [true, false]) { - for (const mode of ['delayed', 'never', 'failure'] as const) { - describe(`${named ? 'named' : 'unnamed'} ${mode}`, () => { - const events: string[] = []; - const setup = Layer.effectDiscard( - Effect.gen(function* setupEffect() { - yield* Effect.acquireRelease( - Effect.sync(() => events.push('acquired')), - () => Effect.sync(() => events.push('released')), - ); - if (mode === 'failure') { - return yield* Effect.die('early-setup-failure'); - } - yield* (mode === 'never' ? Effect.never : Effect.sleep(400)).pipe( - Effect.onInterrupt(() => - Effect.gen(function* interruptSetup() { - yield* Effect.sleep(10); - events.push('interrupted'); - }), - ), - ); - events.push('late-effect'); - return yield* Effect.acquireRelease( - Effect.sync(() => events.push('late-acquired')), - () => Effect.sync(() => events.push('late-released')), - ); - }), - ); - // Named suites exercise an explicit timeout; unnamed suites inherit the - // runner's hookTimeout, which is also 100ms in the child configuration. - const withLayer = layer( - setup, - named ? { excludeTestServices: true, timeout: 100 } : { excludeTestServices: true }, - ); - if (named) { - withLayer('setup', (suiteIt) => { - suiteIt.effect('unreachable', () => Effect.sync(() => events.push('test-ran'))); - }); - } else { - withLayer((suiteIt) => { - suiteIt.effect('unreachable', () => Effect.sync(() => events.push('test-ran'))); - }); - } - - it.live('setup stops before resource release and later tests', () => - Effect.gen(function* observeSetupLifetime() { - const expected = - mode === 'failure' ? ['acquired', 'released'] : ['acquired', 'interrupted', 'released']; - expect(events).toEqual(expected); - if (mode === 'delayed') { - yield* Effect.sleep(600); - expect(events).toEqual(expected); - } - }), - ); - }); - } -} diff --git a/app/packages/effect-rstest/tests/fixtures/test-lifetime.fixture.ts b/app/packages/effect-rstest/tests/fixtures/test-lifetime.fixture.ts deleted file mode 100644 index fe75233b0..000000000 --- a/app/packages/effect-rstest/tests/fixtures/test-lifetime.fixture.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { afterEach, describe, expect, it, layer } from '@app/effect-rstest'; -import { Effect, Fiber, Layer } from 'effect'; -import { TestClock } from 'effect/testing'; - -const suiteAcquired = 'suite acquired'; -const testAcquired = 'test acquired'; -const testReleased = 'test released'; - -const expectedTimeout = 'expected-timeout'; -const expectedFailure = 'expected-failure'; -// Run only in the child runner: ordinary failures/timeouts are intentional. -for (const mode of [ - 'timeout', - expectedTimeout, - 'success', - 'failure', - expectedFailure, - 'unexpected-success', - 'skipped', - 'runtime-skip', -] as const) { - describe(mode, () => { - const events: string[] = []; - const timedOut = mode === 'timeout' || mode === expectedTimeout; - const skipped = mode === 'skipped'; - const afterTest = skipped - ? [suiteAcquired] - : [ - suiteAcquired, - testAcquired, - ...(timedOut ? ['afterEach', testReleased] : [testReleased, 'afterEach']), - 'finished', - ]; - const resource = Layer.effectDiscard( - Effect.acquireRelease( - Effect.sync(() => events.push(suiteAcquired)), - () => Effect.sync(() => events.push('suite released')), - ), - ); - layer(resource, { excludeTestServices: true })('resource', (suiteIt) => { - afterEach((ctx) => { - if (ctx.task.name !== mode) { - return; - } - // Rstest runs native afterEach BEFORE onTestFinished. The barrier cannot - // order native hooks after timeout cleanup; record that boundary explicitly. - expect(events).toEqual([suiteAcquired, testAcquired, ...(timedOut ? [] : [testReleased])]); - events.push('afterEach'); - }); - const expectedToFail = - mode === expectedTimeout || mode === expectedFailure || mode === 'unexpected-success'; - const activeTest = expectedToFail ? suiteIt.effect.fails : suiteIt.effect; - const test = skipped ? suiteIt.effect.skip : activeTest; - test( - mode, - (ctx) => - Effect.gen(function* testLifetime() { - ctx.onTestFinished(() => { - expect(events).toEqual(afterTest.slice(0, -1)); - events.push('finished'); - }); - yield* Effect.acquireRelease( - Effect.sync(() => events.push(testAcquired)), - () => - // This layer intentionally uses live time: cleanup must outlast - // the runner's real deadline, not an Effect/TestClock deadline. - Effect.sleep(150).pipe( - Effect.andThen(Effect.sync(() => events.push(testReleased))), - ), - ); - if (timedOut) { - return yield* Effect.never; - } - if (mode === 'runtime-skip') { - ctx.skip(); - } - if (mode === 'failure' || mode === expectedFailure) { - return yield* Effect.die('intentional-test-failure'); - } - return false; - }), - timedOut ? 30 : 2000, - ); - suiteIt.effect('next test waits for cleanup', () => - Effect.sync(() => { - expect(events).toEqual(afterTest); - events.push('next test'); - }), - ); - }); - it.effect('parent releases after test cleanup', () => - Effect.sync(() => { - expect(events).toEqual([...afterTest, 'next test', 'suite released']); - }), - ); - }); -} - -it.effect('virtual-clock success still completes normally', () => - Effect.gen(function* virtualClockSuccess() { - const fiber = yield* Effect.forkChild(Effect.sleep('1 hour').pipe(Effect.as(42))); - yield* TestClock.adjust('1 hour'); - expect(yield* Fiber.join(fiber)).toBe(42); - }), -); diff --git a/app/packages/effect-rstest/tests/index.test.ts b/app/packages/effect-rstest/tests/index.test.ts deleted file mode 100644 index 51553691c..000000000 --- a/app/packages/effect-rstest/tests/index.test.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { Sleeper } from './support/sleeper.ts'; -import { Scoped } from './support/scoped.ts'; -import { Foo } from './support/foo.ts'; -import { Bar } from './support/bar.ts'; -import { afterAll, assert, describe, describeWrapped, expect, it, layer } from '@app/effect-rstest'; -import { throws, throwsAsync } from '@app/effect-rstest/utils'; -import { Clock, Duration, Effect, Exit, Fiber, Layer, Schema } from 'effect'; -import { FastCheck, TestClock } from 'effect/testing'; - -const realNumber = FastCheck.float({ noDefaultInfinity: true, noNaN: true }); - -it.effect('effect', () => - Effect.acquireRelease( - Effect.sync(() => expect(1).toEqual(1)), - () => Effect.void, - ), -); -it.live('live', () => - Effect.acquireRelease( - Effect.sync(() => expect(1).toEqual(1)), - () => Effect.void, - ), -); - -describeWrapped('describeWrapped', (suiteIt0) => { - suiteIt0.effect('provides the enhanced test API', () => - Effect.sync(() => expect(suiteIt0.layer).toBeTypeOf('function')), - ); -}); - -it('throws fails when the thunk does not throw', () => { - expect(() => throws(() => {})).toThrow(); -}); - -const resolvedPromiseThrows = throwsAsync.bind(undefined, Promise.resolve.bind(Promise), undefined); - -it.effect('throwsAsync fails when the promise resolves', () => - Effect.gen(function* throwsResolved() { - const result = yield* Effect.exit(Effect.tryPromise(() => resolvedPromiseThrows())); - expect(Exit.isFailure(result)).toBe(true); - }), -); - -// each - -it.effect.each([1, 2, 3])('effect each %s', (n) => - Effect.acquireRelease( - Effect.sync(() => expect(n).toEqual(n)), - () => Effect.void, - ), -); -it.live.each([1, 2, 3])('live each %s', (n) => - Effect.acquireRelease( - Effect.sync(() => expect(n).toEqual(n)), - () => Effect.void, - ), -); - -// Match @effect/vitest: each passes one intact row, including readonly tuples. -const tupleCases = [[1, 2]] as const; -const objectCases = [{ left: 1, right: 2 }] as const; - -it.effect.each(tupleCases)('effect each preserves a readonly tuple row', (row, ...extra) => - Effect.sync(() => { - const tuple: readonly [1, 2] = row; - expect(tuple).toBe(tupleCases[0]); - expect(tuple).toEqual([1, 2]); - expect(extra).toEqual([]); - }), -); -it.live.each(tupleCases)('live each preserves a readonly tuple row', (row, ...extra) => - Effect.sync(() => { - const tuple: readonly [1, 2] = row; - expect(tuple).toBe(tupleCases[0]); - expect(tuple).toEqual([1, 2]); - expect(extra).toEqual([]); - }), -); -it.effect.each(objectCases)('effect each preserves an object row', (row, ...extra) => - Effect.sync(() => { - expect(row).toBe(objectCases[0]); - expect(row).toEqual({ left: 1, right: 2 }); - expect(extra).toEqual([]); - }), -); -it.live.each(objectCases)('live each preserves an object row', (row, ...extra) => - Effect.sync(() => { - expect(row).toBe(objectCases[0]); - expect(row).toEqual({ left: 1, right: 2 }); - expect(extra).toEqual([]); - }), -); - -// skip - -it.live.skip('live skipped', () => Effect.die('skipped anyway')); -it.effect.skip('effect skipped', () => Effect.die('skipped anyway')); - -// skipIf - -it.effect.skipIf(true)('effect skipIf (true)', () => Effect.die('skipped anyway')); -it.effect.skipIf(false)('effect skipIf (false)', () => Effect.sync(() => expect(1).toEqual(1))); - -// runIf - -it.effect.runIf(true)('effect runIf (true)', () => Effect.sync(() => expect(1).toEqual(1))); -it.effect.runIf(false)('effect runIf (false)', () => Effect.die('not run anyway')); - -// chained helpers - -it.describe.each(['foo', 'bar'] as const)('describe.each %s', (text) => { - it.effect('runs an Effect test', () => - Effect.sync(() => { - assert.include(['foo', 'bar'], text); - }), - ); -}); - -it.skip.each([1])('skip.each %s', () => assert.fail('skipped anyway')); - -// The following test is expected to fail because it simulates a test timeout. -// Be aware that eventual 'failure' of the test is only logged out. -it.live.fails( - 'interrupts on timeout', - (ctx) => - Effect.gen(function* testEffect() { - let acquired = false; - - ctx.onTestFailed(() => { - expect(acquired).toBe(false); - }); - - yield* Effect.acquireRelease( - Effect.sync(() => (acquired = true)), - () => Effect.sync(() => (acquired = false)), - ); - yield* Effect.sleep(1000); - }), - 1, -); - -const fooLayer = Layer.succeed(Foo)('foo'); - -const barLayer = Layer.effect(Bar)(Foo.pipe(Effect.as('bar' as const))); - -const sleeperLayer = Layer.effect(Sleeper)( - Effect.gen(function* testEffect() { - const clock = yield* Clock.Clock; - - return { - sleep: (ms: number) => clock.sleep(Duration.millis(ms)), - }; - }), -); - -describe('layer', () => { - layer(fooLayer)((suiteIt1) => { - suiteIt1.effect('adds context', () => - Effect.gen(function* testEffect() { - const foo = yield* Foo; - expect(foo).toEqual('foo'); - }), - ); - - suiteIt1.layer(barLayer)('nested', (suiteIt2) => { - suiteIt2.effect('adds context', () => - Effect.gen(function* testEffect() { - const foo = yield* Foo; - const bar = yield* Bar; - expect(foo).toEqual('foo'); - expect(bar).toEqual('bar'); - }), - ); - }); - - suiteIt1.layer(barLayer)((suiteIt3) => { - suiteIt3.effect('without name', () => - Effect.gen(function* testEffect() { - const foo = yield* Foo; - const bar = yield* Bar; - expect(foo).toEqual('foo'); - expect(bar).toEqual('bar'); - }), - ); - }); - - describe('release', () => { - let released = false; - afterAll(() => { - expect(released).toEqual(true); - }); - - const scopedLayer = Layer.effect(Scoped)( - Effect.acquireRelease(Effect.succeed('scoped' as const), () => - Effect.sync(() => (released = true)), - ), - ); - - suiteIt1.layer(scopedLayer)((suiteIt4) => { - suiteIt4.effect('adds context', () => - Effect.gen(function* testEffect() { - const foo = yield* Foo; - const scoped = yield* Scoped; - expect(foo).toEqual('foo'); - expect(scoped).toEqual('scoped'); - }), - ); - }); - - suiteIt1.effect.prop( - 'adds context', - [realNumber], - ([num]) => - Effect.gen(function* testEffect() { - const foo = yield* Foo; - expect(foo).toEqual('foo'); - return !Number.isNaN(num); - }), - { fastCheck: { numRuns: 200 } }, - ); - }); - }); - - layer(sleeperLayer)('test services', (suiteIt5) => { - suiteIt5.effect('TestClock', () => - Effect.gen(function* testEffect() { - const sleeper = yield* Sleeper; - const fiber = yield* Effect.forkChild(sleeper.sleep(100_000)); - yield* Effect.yieldNow; - yield* TestClock.adjust(100_000); - yield* Fiber.join(fiber); - }), - ); - }); - - layer(fooLayer)('with a name', (suiteIt6) => { - describe('with a nested describe', () => { - suiteIt6.effect('adds context', () => - Effect.gen(function* testEffect() { - const foo = yield* Foo; - expect(foo).toEqual('foo'); - }), - ); - }); - suiteIt6.effect('adds context', () => - Effect.gen(function* testEffect() { - const foo = yield* Foo; - expect(foo).toEqual('foo'); - }), - ); - }); - - layer(sleeperLayer, { excludeTestServices: true })('live services', (suiteIt7) => { - suiteIt7.effect('Clock', () => - Effect.gen(function* testEffect() { - const sleeper = yield* Sleeper; - yield* sleeper.sleep(1); - }), - ); - }); -}); - -// property testing - -it.prop('symmetry', [realNumber, FastCheck.integer()], ([a, b]) => { - expect(a + b).toBe(b + a); -}); - -it.prop('symmetry with object', { a: realNumber, b: FastCheck.integer() }, ({ a, b }) => { - expect(a + b).toBe(b + a); -}); - -it.live.prop('schema with object', { value: Schema.Int }, ({ value }) => - Effect.sync(() => assert.isTrue(Number.isInteger(value))), -); - -it.effect.prop('symmetry', [realNumber, FastCheck.integer()], ([a, b]) => - Effect.gen(function* testEffect() { - yield* Effect.void; - assert.isTrue(a + b === b + a); - }), -); - -it.effect.prop('symmetry with object', { a: realNumber, b: FastCheck.integer() }, ({ a, b }) => - Effect.gen(function* testEffect() { - yield* Effect.void; - assert.strictEqual(a + b, b + a); - }), -); - -it.effect.prop( - 'should detect the substring', - { a: FastCheck.string(), b: FastCheck.string(), c: FastCheck.string() }, - ({ a, b, c }) => - Effect.gen(function* testEffect() { - yield* Effect.scope; - assert.include(a + b + c, b); - }), -); diff --git a/app/packages/effect-rstest/tests/isolation.test.ts b/app/packages/effect-rstest/tests/isolation.test.ts deleted file mode 100644 index b077d662a..000000000 --- a/app/packages/effect-rstest/tests/isolation.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { TodoService } from './support/todo-service.ts'; -import { State } from './support/state.ts'; -import { Scoped } from './support/scoped.ts'; -import { afterAll, assert, describe, it } from '@app/effect-rstest'; -import { Effect, Layer, Ref } from 'effect'; - -describe('top-level it.layer isolation', () => { - let nextId = 0; - const observedStateIds: number[] = []; - - const baseLayer = Layer.effect(State)( - Effect.gen(function* testEffect() { - nextId += 1; - const id = nextId; - const todos = yield* Ref.make([]); - const migrated = yield* Ref.make(false); - return { id, migrated, todos }; - }), - ); - - const migrationLayer = Layer.effectDiscard( - Effect.gen(function* testEffect() { - const state = yield* State; - yield* Ref.set(state.migrated, true); - }), - ); - - const migratedLayer = Layer.merge(baseLayer, migrationLayer.pipe(Layer.provide(baseLayer))); - - const inMemoryLayer = Layer.effect(TodoService)( - Effect.gen(function* testEffect() { - const state = yield* State; - return { - add: (title: string) => Ref.update(state.todos, (todos) => [...todos, title]), - list: Ref.get(state.todos), - migrated: Ref.get(state.migrated), - stateId: Effect.succeed(state.id), - } as const; - }), - ).pipe(Layer.provide(migratedLayer)); - - it.layer(inMemoryLayer)((suiteIt0) => { - suiteIt0.effect('first block mutates isolated state', () => - Effect.gen(function* testEffect() { - const service = yield* TodoService; - const stateId = yield* service.stateId; - const migrated = yield* service.migrated; - - observedStateIds.push(stateId); - yield* service.add('write tests'); - - assert.isTrue(migrated); - assert.deepStrictEqual(yield* service.list, ['write tests']); - }), - ); - }); - - it.layer(inMemoryLayer)((suiteIt1) => { - suiteIt1.effect('second block starts fresh', () => - Effect.gen(function* testEffect() { - const service = yield* TodoService; - const stateId = yield* service.stateId; - const migrated = yield* service.migrated; - - observedStateIds.push(stateId); - - assert.isTrue(migrated); - assert.deepStrictEqual(yield* service.list, []); - - yield* service.add('ship feature'); - assert.deepStrictEqual(yield* service.list, ['ship feature']); - }), - ); - }); - - it.layer(inMemoryLayer)((suiteIt2) => { - suiteIt2.effect('third block also starts fresh', () => - Effect.gen(function* testEffect() { - const service = yield* TodoService; - const stateId = yield* service.stateId; - const migrated = yield* service.migrated; - - observedStateIds.push(stateId); - - assert.isTrue(migrated); - assert.deepStrictEqual(yield* service.list, []); - }), - ); - }); - - afterAll(() => { - assert.deepStrictEqual(observedStateIds, [1, 2, 3]); - }); -}); - -describe('unnamed layer release boundary', () => { - let released = false; - - const scopedLayer = Layer.effect(Scoped)( - Effect.acquireRelease(Effect.succeed('scoped' as const), () => - Effect.sync(() => (released = true)), - ), - ); - - it.layer(scopedLayer)((suiteIt3) => { - suiteIt3.effect('uses resource', () => - Effect.gen(function* usesResource() { - const value = yield* Scoped; - assert.strictEqual(value, 'scoped'); - }), - ); - }); - - it('later test sees released resource', () => { - assert.isTrue(released); - }); -}); diff --git a/app/packages/effect-rstest/tests/layer-lifetime.test.ts b/app/packages/effect-rstest/tests/layer-lifetime.test.ts deleted file mode 100644 index 1dad62b37..000000000 --- a/app/packages/effect-rstest/tests/layer-lifetime.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { NodeServices } from '@effect/platform-node'; -import { expect, it } from '@app/effect-rstest'; -import { Effect, FileSystem, Schema, Stream } from 'effect'; -import { ChildProcess } from 'effect/unstable/process'; - -const runnerReport = Schema.fromJsonString( - Schema.Struct({ - files: Schema.Array( - Schema.Struct({ - errors: Schema.Array(Schema.Struct({ message: Schema.String })), - }), - ), - summary: Schema.Struct({ - failedTests: Schema.Finite, - passedTests: Schema.Finite, - skippedTests: Schema.Finite, - tests: Schema.Finite, - }), - unhandledErrors: Schema.Array(Schema.Unknown), - }), -); - -it.layer(NodeServices.layer, { excludeTestServices: true })((suiteIt) => { - suiteIt.effect( - 'layer setup fibers stop on hook timeout and release resources on early failure', - () => - Effect.gen(function* runLayerLifetimeFixture() { - const fs = yield* FileSystem.FileSystem; - const directory = yield* fs.makeTempDirectoryScoped({ prefix: 'effect-rstest-layer-' }); - const reportPath = `${directory}/report.json`; - const configPath = `${directory}/rstest.config.mjs`; - const config = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ - reporters: [['json', { outputPath: reportPath }]], - root: new URL('..', import.meta.url).pathname, - }); - yield* fs.writeFileString(configPath, `export default ${config};`); - const child = yield* ChildProcess.make( - process.execPath, - [ - 'node_modules/@rstest/core/bin/rstest.js', - 'run', - '--include', - 'tests/fixtures/layer-lifetime.fixture.ts', - '--config', - configPath, - '--hookTimeout', - '100', - '--pool.maxWorkers', - '1', - ], - { - cwd: new URL('..', import.meta.url).pathname, - // Exercise normal CLI mode: its banner makes stdout unsuitable for JSON. - env: { RSTEST_NO_AGENT: '1' }, - forceKillAfter: '1 second', - stderr: 'pipe', - stdin: 'ignore', - stdout: 'pipe', - }, - ); - const [status, stdout, stderr] = yield* Effect.all( - [ - child.exitCode, - child.stdout.pipe(Stream.decodeText(), Stream.mkString), - child.stderr.pipe(Stream.decodeText(), Stream.mkString), - ], - { concurrency: 'unbounded' }, - ).pipe( - // oxlint-disable-next-line effect-native/no-native-timers -- subprocess deadline uses real time; remove-when: Rstest can control child-process time - Effect.timeout('20 seconds'), - ); - // Failing hooks must still fail the runner, not become swallowed failures. - expect(Number(status), `${stdout}\n${stderr}`).toBe(1); - const report = yield* Schema.decodeEffect(runnerReport)( - yield* fs.readFileString(reportPath), - ); - expect(report.summary).toEqual({ - failedTests: 0, - passedTests: 6, - skippedTests: 6, - tests: 12, - }); - expect(report.unhandledErrors).toEqual([]); - expect(report.files).toHaveLength(1); - const errors = report.files.flatMap((file) => file.errors); - expect(errors.map((error) => error.message)).toEqual([ - 'beforeAll hook timed out in 100ms', - 'beforeAll hook timed out in 100ms', - 'early-setup-failure', - 'beforeAll hook timed out in 100ms', - 'beforeAll hook timed out in 100ms', - 'early-setup-failure', - ]); - }), - 30_000, - ); -}); diff --git a/app/packages/effect-rstest/tests/nested-isolation.test.ts b/app/packages/effect-rstest/tests/nested-isolation.test.ts deleted file mode 100644 index 5fb3c5217..000000000 --- a/app/packages/effect-rstest/tests/nested-isolation.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { TodoService } from './support/todo-service.ts'; -import { State } from './support/state.ts'; -import { SharedChild } from './support/shared-child.ts'; -import { Parent } from './support/parent.ts'; -import { Child } from './support/child.ts'; -import { afterAll, assert, beforeAll, describe, expect, it, layer } from '@app/effect-rstest'; -import { Effect, Layer, Ref } from 'effect'; - -describe('nested sibling layers', () => { - let nextChildId = 0; - let firstChildId = -1; - let secondChildId = -1; - const releasedChildIds: number[] = []; - - const parentLayer = Layer.succeed(Parent)('parent'); - - const childLayer = Layer.effect(Child)( - Parent.pipe( - Effect.flatMap(() => { - nextChildId += 1; - const id = nextChildId; - return Effect.acquireRelease(Effect.succeed({ id }), () => - Effect.sync(() => { - releasedChildIds.push(id); - }), - ); - }), - ), - ); - - layer(parentLayer)('parent', (suiteIt0) => { - suiteIt0.layer(childLayer)('first sibling', (suiteIt1) => { - suiteIt1.effect('allocates child', () => - Effect.gen(function* testEffect() { - const child = yield* Child; - firstChildId = child.id; - - assert.strictEqual(child.id, 1); - assert.deepStrictEqual(releasedChildIds, []); - }), - ); - }); - - suiteIt0.layer(childLayer)('second sibling', (suiteIt2) => { - beforeAll(() => { - expect(releasedChildIds).toEqual([firstChildId]); - }); - - suiteIt2.effect('allocates a fresh child', () => - Effect.gen(function* testEffect() { - const child = yield* Child; - secondChildId = child.id; - - assert.strictEqual(child.id, 2); - assert.isTrue(child.id !== firstChildId); - assert.deepStrictEqual(releasedChildIds, [firstChildId]); - }), - ); - }); - - afterAll(() => { - expect(firstChildId).toEqual(1); - expect(secondChildId).toEqual(2); - expect(releasedChildIds).toEqual([1, 2]); - }); - }); -}); - -describe.concurrent('nested sibling layers in concurrent suites', () => { - let nextSharedId = 0; - let firstSharedId: number | undefined; - let secondSharedId: number | undefined; - const releasedSharedIds: number[] = []; - - const parentLayer = Layer.succeed(Parent)('parent'); - - const sharedChildLayer = Layer.effect(SharedChild)( - Parent.pipe( - Effect.flatMap(() => - Effect.gen(function* testEffect() { - yield* Effect.yieldNow; - - nextSharedId += 1; - const id = nextSharedId; - return yield* Effect.acquireRelease(Effect.succeed({ id }), () => - Effect.sync(() => { - releasedSharedIds.push(id); - }), - ); - }), - ), - ), - ); - - layer(parentLayer)('parent', (suiteIt3) => { - describe.concurrent('concurrent siblings', () => { - suiteIt3.layer(sharedChildLayer)('first sibling', (suiteIt4) => { - suiteIt4.effect('captures shared child', () => - Effect.gen(function* testEffect() { - const child = yield* SharedChild; - firstSharedId = child.id; - assert.isTrue(child.id === 1 || child.id === 2); - }), - ); - }); - - suiteIt3.layer(sharedChildLayer)('second sibling', (suiteIt5) => { - suiteIt5.effect('allocates an isolated child', () => - Effect.gen(function* testEffect() { - const child = yield* SharedChild; - secondSharedId = child.id; - assert.isTrue(child.id === 1 || child.id === 2); - }), - ); - }); - }); - - afterAll(() => { - expect(firstSharedId).not.toEqual(secondSharedId); - expect(nextSharedId).toEqual(2); - expect(releasedSharedIds.toSorted((a, b) => a - b)).toEqual([1, 2]); - }); - }); -}); - -describe('nested sibling isolation with provided state graph', () => { - const parentLayer = Layer.succeed(Parent)('parent'); - - let nextId = 0; - let firstStateId = -1; - let secondStateId = -1; - - const baseLayer = Layer.effect(State)( - Effect.gen(function* testEffect() { - nextId += 1; - const id = nextId; - const todos = yield* Ref.make([]); - const migrated = yield* Ref.make(false); - return { id, migrated, todos }; - }), - ); - - const migrationLayer = Layer.effectDiscard( - Effect.gen(function* testEffect() { - const state = yield* State; - yield* Ref.set(state.migrated, true); - }), - ); - - const migratedLayer = Layer.merge(baseLayer, migrationLayer.pipe(Layer.provide(baseLayer))); - - const inMemoryLayer = Layer.effect(TodoService)( - Effect.gen(function* testEffect() { - const state = yield* State; - return { - add: (title: string) => Ref.update(state.todos, (todos) => [...todos, title]), - list: Ref.get(state.todos), - migrated: Ref.get(state.migrated), - stateId: Effect.succeed(state.id), - } as const; - }), - ).pipe(Layer.provide(migratedLayer)); - - layer(parentLayer)('parent', (suiteIt6) => { - suiteIt6.layer(inMemoryLayer)('first sibling', (suiteIt7) => { - suiteIt7.effect('mutates isolated provided state', () => - Effect.gen(function* testEffect() { - const service = yield* TodoService; - firstStateId = yield* service.stateId; - - assert.isTrue(yield* service.migrated); - yield* service.add('write tests'); - assert.deepStrictEqual(yield* service.list, ['write tests']); - }), - ); - }); - - suiteIt6.layer(inMemoryLayer)('second sibling', (suiteIt8) => { - suiteIt8.effect('starts fresh with a new provided state', () => - Effect.gen(function* testEffect() { - const service = yield* TodoService; - secondStateId = yield* service.stateId; - - assert.isTrue(yield* service.migrated); - assert.deepStrictEqual(yield* service.list, []); - - yield* service.add('ship feature'); - assert.deepStrictEqual(yield* service.list, ['ship feature']); - }), - ); - }); - - afterAll(() => { - expect(firstStateId).toEqual(1); - expect(secondStateId).toEqual(2); - expect(nextId).toEqual(2); - }); - }); -}); - -it('exposes nested layer API', () => { - expect(it.layer).toBeTypeOf('function'); -}); diff --git a/app/packages/effect-rstest/tests/prop-schema-record.test.ts b/app/packages/effect-rstest/tests/prop-schema-record.test.ts deleted file mode 100644 index 803c95c9b..000000000 --- a/app/packages/effect-rstest/tests/prop-schema-record.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { expect, it } from '@app/effect-rstest'; -import { Schema } from 'effect'; -import { FastCheck } from 'effect/testing'; - -it.prop( - 'plain record properties generate schemas alongside arbitraries', - { count: FastCheck.integer({ max: 10, min: 1 }), label: Schema.Literal('schema') }, - ({ count, label }) => { - expect(label).toBe('schema'); - expect(Number.isInteger(count)).toBe(true); - expect(count).toBeGreaterThanOrEqual(1); - expect(count).toBeLessThanOrEqual(10); - }, - { fastCheck: { numRuns: 20 } }, -); - -it.prop( - 'plain record properties retain FastCheck-only support', - { value: FastCheck.constant(7) }, - ({ value }) => { - expect(value).toBe(7); - }, -); diff --git a/app/packages/effect-rstest/tests/prop-schema-tuple.test.ts b/app/packages/effect-rstest/tests/prop-schema-tuple.test.ts deleted file mode 100644 index 944e0a387..000000000 --- a/app/packages/effect-rstest/tests/prop-schema-tuple.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { expect, it } from '@app/effect-rstest'; -import { Schema } from 'effect'; -import { FastCheck } from 'effect/testing'; - -it.prop( - 'plain tuple properties generate schemas alongside arbitraries', - [Schema.Literal('schema'), FastCheck.integer({ max: 10, min: 1 })], - ([label, count]) => { - expect(label).toBe('schema'); - expect(Number.isInteger(count)).toBe(true); - expect(count).toBeGreaterThanOrEqual(1); - expect(count).toBeLessThanOrEqual(10); - }, - { fastCheck: { numRuns: 20 } }, -); - -it.prop( - 'plain tuple properties retain FastCheck-only support', - [FastCheck.constant(7)], - ([value]) => { - expect(value).toBe(7); - }, -); diff --git a/app/packages/effect-rstest/tests/support/bar.ts b/app/packages/effect-rstest/tests/support/bar.ts deleted file mode 100644 index 823005bf2..000000000 --- a/app/packages/effect-rstest/tests/support/bar.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Context } from 'effect'; - -export class Bar extends Context.Service()('@app/effect-rstest/tests/support/bar') {} diff --git a/app/packages/effect-rstest/tests/support/child.ts b/app/packages/effect-rstest/tests/support/child.ts deleted file mode 100644 index af4bee949..000000000 --- a/app/packages/effect-rstest/tests/support/child.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Context } from 'effect'; - -export class Child extends Context.Service()( - '@app/effect-rstest/tests/support/child', -) {} diff --git a/app/packages/effect-rstest/tests/support/foo.ts b/app/packages/effect-rstest/tests/support/foo.ts deleted file mode 100644 index fbfda9fb9..000000000 --- a/app/packages/effect-rstest/tests/support/foo.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Context } from 'effect'; - -export class Foo extends Context.Service()('@app/effect-rstest/tests/support/foo') {} diff --git a/app/packages/effect-rstest/tests/support/parent.ts b/app/packages/effect-rstest/tests/support/parent.ts deleted file mode 100644 index 6eb27db44..000000000 --- a/app/packages/effect-rstest/tests/support/parent.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Context } from 'effect'; - -export class Parent extends Context.Service()( - '@app/effect-rstest/tests/support/parent', -) {} diff --git a/app/packages/effect-rstest/tests/support/scoped.ts b/app/packages/effect-rstest/tests/support/scoped.ts deleted file mode 100644 index 3040e3a1e..000000000 --- a/app/packages/effect-rstest/tests/support/scoped.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Context } from 'effect'; - -export class Scoped extends Context.Service()( - '@app/effect-rstest/tests/support/scoped', -) {} diff --git a/app/packages/effect-rstest/tests/support/shared-child.ts b/app/packages/effect-rstest/tests/support/shared-child.ts deleted file mode 100644 index 58bd2add0..000000000 --- a/app/packages/effect-rstest/tests/support/shared-child.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Context } from 'effect'; - -export class SharedChild extends Context.Service()( - '@app/effect-rstest/tests/support/shared-child/SharedChild', -) {} diff --git a/app/packages/effect-rstest/tests/support/sleeper.ts b/app/packages/effect-rstest/tests/support/sleeper.ts deleted file mode 100644 index 809afaf9c..000000000 --- a/app/packages/effect-rstest/tests/support/sleeper.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Context } from 'effect'; -import type { Effect } from 'effect'; - -export class Sleeper extends Context.Service< - Sleeper, - { - readonly sleep: (ms: number) => Effect.Effect; - } ->()('@app/effect-rstest/tests/support/sleeper') {} diff --git a/app/packages/effect-rstest/tests/support/state.ts b/app/packages/effect-rstest/tests/support/state.ts deleted file mode 100644 index 0db192f47..000000000 --- a/app/packages/effect-rstest/tests/support/state.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Context } from 'effect'; -import type { Ref } from 'effect'; - -export class State extends Context.Service< - State, - { readonly id: number; readonly migrated: Ref.Ref; readonly todos: Ref.Ref } ->()('@app/effect-rstest/tests/support/state') {} diff --git a/app/packages/effect-rstest/tests/support/todo-service.ts b/app/packages/effect-rstest/tests/support/todo-service.ts deleted file mode 100644 index 1c62973b7..000000000 --- a/app/packages/effect-rstest/tests/support/todo-service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Context } from 'effect'; -import type { Effect } from 'effect'; - -export class TodoService extends Context.Service< - TodoService, - { - readonly add: (title: string) => Effect.Effect; - readonly list: Effect.Effect; - readonly migrated: Effect.Effect; - readonly stateId: Effect.Effect; - } ->()('@app/effect-rstest/tests/support/todo-service/TodoService') {} diff --git a/app/packages/effect-rstest/tests/test-lifetime.test.ts b/app/packages/effect-rstest/tests/test-lifetime.test.ts deleted file mode 100644 index 23039a675..000000000 --- a/app/packages/effect-rstest/tests/test-lifetime.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { NodeServices } from '@effect/platform-node'; -import { expect, it } from '@app/effect-rstest'; -import { Effect, FileSystem, Schema, Stream } from 'effect'; -import { ChildProcess } from 'effect/unstable/process'; - -const runnerReport = Schema.fromJsonString( - Schema.Struct({ - files: Schema.Array( - Schema.Struct({ errors: Schema.Array(Schema.Struct({ message: Schema.String })) }), - ), - summary: Schema.Struct({ - failedTests: Schema.Finite, - passedTests: Schema.Finite, - skippedTests: Schema.Finite, - tests: Schema.Finite, - }), - tests: Schema.Array( - Schema.Struct({ - errors: Schema.optional(Schema.Array(Schema.Struct({ message: Schema.String }))), - name: Schema.String, - status: Schema.String, - }), - ), - unhandledErrors: Schema.Array(Schema.Unknown), - }), -); - -it.layer(NodeServices.layer, { excludeTestServices: true })((suiteIt) => { - suiteIt.effect( - 'test timeout cleanup settles before later tests and suite release without changing outcomes', - () => - Effect.gen(function* runTestLifetimeFixture() { - const fs = yield* FileSystem.FileSystem; - const directory = yield* fs.makeTempDirectoryScoped({ - prefix: 'effect-rstest-test-lifetime-', - }); - const config = `${directory}/rstest.config.mjs`; - yield* fs.writeFileString( - config, - `export default { - testEnvironment: 'node', - reporters: [['json', { outputPath: new URL('./report.json', import.meta.url).pathname }]], - };`, - ); - const child = yield* ChildProcess.make( - process.execPath, - [ - 'node_modules/@rstest/core/bin/rstest.js', - 'run', - '--include', - 'tests/fixtures/test-lifetime.fixture.ts', - '--config', - config, - '--pool.maxWorkers', - '1', - '--hookTimeout', - '50', - ], - { - cwd: new URL('..', import.meta.url).pathname, - env: { RSTEST_NO_AGENT: '1' }, - forceKillAfter: '1 second', - stderr: 'pipe', - stdin: 'ignore', - stdout: 'pipe', - }, - ); - const [status, stdout, stderr] = yield* Effect.all( - [ - child.exitCode, - child.stdout.pipe(Stream.decodeText(), Stream.mkString), - child.stderr.pipe(Stream.decodeText(), Stream.mkString), - ], - { concurrency: 'unbounded' }, - ); - expect(Number(status), `${stdout}\n${stderr}`).toBe(1); - const report = yield* fs - .readFileString(`${directory}/report.json`) - .pipe(Effect.flatMap(Schema.decodeEffect(runnerReport))); - expect(report.summary).toEqual({ - failedTests: 3, - passedTests: 20, - skippedTests: 2, - tests: 25, - }); - expect(report.unhandledErrors).toEqual([]); - expect(report.files).toHaveLength(1); - expect(report.files.flatMap((file) => file.errors)).toEqual([]); - const failures = report.tests.filter((test) => test.status === 'fail'); - expect(failures.map((test) => test.name)).toEqual([ - 'timeout', - 'failure', - 'unexpected-success', - ]); - expect(failures.flatMap((test) => test.errors ?? []).map((error) => error.message)).toEqual( - [ - 'test timed out in 30ms (no expect assertions completed)', - 'intentional-test-failure', - 'Expect test to fail', - ], - ); - for (const name of ['expected-timeout', 'expected-failure', 'success']) { - expect(report.tests.find((test) => test.name === name)?.status).toBe('pass'); - } - for (const name of ['skipped', 'runtime-skip']) { - expect(report.tests.find((test) => test.name === name)?.status).toBe('skip'); - } - }), - 30_000, - ); -}); diff --git a/app/packages/effect-rstest/tsconfig.json b/app/packages/effect-rstest/tsconfig.json deleted file mode 100644 index f6e6427b1..000000000 --- a/app/packages/effect-rstest/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "declaration": true, - "declarationMap": false, - "emitDeclarationOnly": true, - "incremental": true, - "noEmit": false, - "outDir": "../../node_modules/.cache/tsgo/declarations/packages__effect-rstest", - "tsBuildInfoFile": "../../node_modules/.cache/tsgo/packages__effect-rstest.tsbuildinfo", - "types": [ - "node" - ] - }, - "include": [ - "src", - "tests", - "rstest.config.ts" - ] -} diff --git a/app/packages/gateway-principal-verifier/package.json b/app/packages/gateway-principal-verifier/package.json index f670e2454..422764635 100644 --- a/app/packages/gateway-principal-verifier/package.json +++ b/app/packages/gateway-principal-verifier/package.json @@ -20,7 +20,7 @@ "devDependencies": { "@effect/tsgo": "0.19.0", "@types/node": "20.19.43", - "@app/effect-rstest": "workspace:*", - "@rstest/core": "0.11.10" + "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc", + "@rstest/core": "0.11.11" } } 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 8ed28a36a..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { GatewayAssertionRedemptionUnavailableError, GatewayAssertionReplayError, diff --git a/app/packages/shared-contracts/package.json b/app/packages/shared-contracts/package.json index bcda3ec9e..23655e68a 100644 --- a/app/packages/shared-contracts/package.json +++ b/app/packages/shared-contracts/package.json @@ -23,7 +23,7 @@ "devDependencies": { "@effect/tsgo": "0.19.0", "@types/node": "20.19.43", - "@app/effect-rstest": "workspace:*", - "@rstest/core": "0.11.10" + "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc", + "@rstest/core": "0.11.11" } } 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 01f687994..2b4a2a842 100644 --- a/app/packages/shared-contracts/tests/unit/client-runtime.test.ts +++ b/app/packages/shared-contracts/tests/unit/client-runtime.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { makeEffectBffClient } from '@app/shared-contracts/client-runtime'; import { 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 60b957e9f..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, 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 4631d8da3..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { TrustedPrincipalContextSchema } from '@app/core-runtime/actions/principal-context'; import { Effect, Schema, SchemaAST, Struct } from 'effect'; import { 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 6520d3793..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema } from 'effect'; import { 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 d2961ee74..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect } from 'effect'; import { makeOperationGateway } from '../../src/operation-gateway.ts'; 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 38598812c..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { makeEffectHttpApiClient } from '@modern-js/plugin-bff/effect-client'; import { HttpApi, 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 acfffc925..aa461288f 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -24,6 +24,7 @@ patchedDependencies: '@module-federation/modern-js-v3@2.8.0': 56ff0f8c26c40b18be1de105abd019422ebaa648941607d1d30b94cd620b57f3 '@tanstack/router-core@1.171.21': 413c2453d06aa521ed65ab7fcfb16bac8700e58e97693c2ba4d40727d7c9790d '@vercel/nft@0.29.2': c0ed4897b98e9055716031187bb8ea16739f6ae0843d35e4873f1a177472cac7 + effect-rstest@0.1.0: aa7e505a2b575757ce3951715189b3cd30d90b7a57c17669f9f89340e751f138 effect@4.0.0-beta.107: ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f importers: @@ -52,9 +53,6 @@ importers: specifier: 8.22.0 version: 8.22.0 devDependencies: - '@app/effect-rstest': - specifier: workspace:* - version: link:packages/effect-rstest '@effect/platform-node': specifier: 4.0.0-beta.107 version: 4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f))(ioredis@5.11.1(supports-color@10.2.2)) @@ -83,8 +81,8 @@ importers: specifier: 1.79.0 version: 1.79.0 '@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) '@types/node': specifier: 20.19.43 version: 20.19.43 @@ -100,6 +98,9 @@ importers: effect: specifier: 4.0.0-beta.107 version: 4.0.0-beta.107(patch_hash=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f) + 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=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f)) esbuild: specifier: 0.28.1 version: 0.28.1 @@ -248,9 +249,6 @@ importers: specifier: 7.18.1 version: 7.18.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) devDependencies: - '@app/effect-rstest': - specifier: workspace:* - version: link:../../packages/effect-rstest '@cloudflare/workers-types': specifier: 5.20260810.1 version: 5.20260810.1 @@ -259,7 +257,7 @@ 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)' @@ -270,8 +268,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 @@ -302,6 +300,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=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f)) happy-dom: specifier: 20.8.3 version: 20.8.3 @@ -342,12 +343,9 @@ importers: specifier: 8.22.0 version: 8.22.0 devDependencies: - '@app/effect-rstest': - specifier: workspace:* - version: link:../effect-rstest '@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) '@types/node': specifier: ^20.19.43 version: 20.19.43 @@ -357,25 +355,9 @@ importers: drizzle-kit: specifier: 1.0.0-rc.5-ab785fc version: 1.0.0-rc.5-ab785fc - - packages/effect-rstest: - dependencies: - '@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) - effect: - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(patch_hash=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f) - devDependencies: - '@effect/platform-node': - specifier: 4.0.0-beta.107 - version: 4.0.0-beta.107(effect@4.0.0-beta.107(patch_hash=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f))(ioredis@5.11.1(supports-color@10.2.2)) - '@effect/tsgo': - specifier: 0.19.0 - version: 0.19.0 - '@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=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f)) packages/gateway-principal-verifier: dependencies: @@ -392,18 +374,18 @@ importers: specifier: 6.2.5 version: 6.2.5 devDependencies: - '@app/effect-rstest': - specifier: workspace:* - version: link:../effect-rstest '@effect/tsgo': specifier: 0.19.0 version: 0.19.0 '@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) '@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=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f)) packages/shared-contracts: dependencies: @@ -417,18 +399,18 @@ importers: specifier: 4.0.0-beta.107 version: 4.0.0-beta.107(patch_hash=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f) devDependencies: - '@app/effect-rstest': - specifier: workspace:* - version: link:../effect-rstest '@effect/tsgo': specifier: 0.19.0 version: 0.19.0 '@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) '@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=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f)) packages/shared-design-tokens: devDependencies: @@ -517,15 +499,12 @@ importers: specifier: 7.18.1 version: 7.18.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) devDependencies: - '@app/effect-rstest': - specifier: workspace:* - version: link:../../packages/effect-rstest '@effect/tsgo': specifier: 0.19.0 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)' @@ -533,8 +512,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 @@ -562,6 +541,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=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f)) fast-check: specifier: 4.9.0 version: 4.9.0 @@ -3830,8 +3812,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: @@ -5722,6 +5704,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==} @@ -9267,18 +9256,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'} @@ -9732,10 +9709,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)) + '@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' @@ -12493,12 +12470,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 @@ -14462,6 +14439,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=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f)): + 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=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f) + effect@4.0.0-beta.107(patch_hash=ba67c866590cfab0c3cbbab16475ec80811dce9a0add857294c0053d9149640f): dependencies: '@standard-schema/spec': 1.1.0 @@ -15306,12 +15288,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 @@ -18329,8 +18311,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 af842116e..49ff524e8 100644 --- a/app/pnpm-workspace.yaml +++ b/app/pnpm-workspace.yaml @@ -133,3 +133,4 @@ patchedDependencies: '@bleedingdev/modern-js-create@3.8.2-ultramodern.12': patches/@bleedingdev__modern-js-create@3.8.2-ultramodern.12.patch '@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 + effect-rstest@0.1.0: patches/effect-rstest@0.1.0.patch diff --git a/app/scripts/local-environment-values.test.mts b/app/scripts/local-environment-values.test.mts index 1e7d4b5f8..e9fd8c7c0 100644 --- a/app/scripts/local-environment-values.test.mts +++ b/app/scripts/local-environment-values.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { localPublicClientValues, localSpiceDbValues } from './local-environment-values.mts'; const spiceDbGrpcPort = '50052'; diff --git a/app/scripts/scaffolding/tests/module-contract-generator.test.mts b/app/scripts/scaffolding/tests/module-contract-generator.test.mts index a0a4de57b..4aefb2439 100644 --- a/app/scripts/scaffolding/tests/module-contract-generator.test.mts +++ b/app/scripts/scaffolding/tests/module-contract-generator.test.mts @@ -1,5 +1,5 @@ import { Cause, Effect, Schema } from 'effect'; -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { NodeServices } from '@effect/platform-node'; import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; diff --git a/app/scripts/scaffolding/tests/resource-generator.test.mts b/app/scripts/scaffolding/tests/resource-generator.test.mts index f99e4fd00..2a50667ce 100644 --- a/app/scripts/scaffolding/tests/resource-generator.test.mts +++ b/app/scripts/scaffolding/tests/resource-generator.test.mts @@ -1,5 +1,5 @@ import { Cause, Effect, Fiber, FileSystem, Schema } from 'effect'; -import { afterEach, expect, it, rs } from '@app/effect-rstest'; +import { afterEach, expect, it, rs } from 'effect-rstest'; import { CodeSmith, GeneratorCore } from '@modern-js/codesmith'; import { applyMutationPlanEffect } from '../shared.mts'; diff --git a/app/scripts/scaffolding/tests/retire-contribution.test.mts b/app/scripts/scaffolding/tests/retire-contribution.test.mts index 4d290ab37..8e45c69aa 100644 --- a/app/scripts/scaffolding/tests/retire-contribution.test.mts +++ b/app/scripts/scaffolding/tests/retire-contribution.test.mts @@ -1,5 +1,5 @@ import { Cause, Effect } from 'effect'; -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { NodeServices } from '@effect/platform-node'; diff --git a/app/scripts/scaffolding/tests/scaffold-generators.test.mts b/app/scripts/scaffolding/tests/scaffold-generators.test.mts index 4a584a901..818bc6aa8 100644 --- a/app/scripts/scaffolding/tests/scaffold-generators.test.mts +++ b/app/scripts/scaffolding/tests/scaffold-generators.test.mts @@ -1,6 +1,6 @@ import { Cause, Clock, ConfigProvider, Predicate, Redacted } from 'effect'; import type { Scope } from 'effect'; -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { NodeServices } from '@effect/platform-node'; import { spawnSync } from 'node:child_process'; diff --git a/app/scripts/tests/api-only-tooling.test.mts b/app/scripts/tests/api-only-tooling.test.mts index db2ec1a90..baeba060a 100644 --- a/app/scripts/tests/api-only-tooling.test.mts +++ b/app/scripts/tests/api-only-tooling.test.mts @@ -1,6 +1,6 @@ import type { defineEffectBff } from '@modern-js/plugin-bff/effect-edge'; import { Cause, Effect, Predicate, Schema } from 'effect'; -import { afterEach, expect, it, rs } from '@app/effect-rstest'; +import { afterEach, expect, it, rs } from 'effect-rstest'; import { execFileSync, spawnSync } from 'node:child_process'; import type { ExecFileSyncOptionsWithStringEncoding } from 'node:child_process'; import { mkdtemp, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises'; diff --git a/app/scripts/tests/audit-database-trust-boundaries.test.mts b/app/scripts/tests/audit-database-trust-boundaries.test.mts index 5c08ec822..5b75d8240 100644 --- a/app/scripts/tests/audit-database-trust-boundaries.test.mts +++ b/app/scripts/tests/audit-database-trust-boundaries.test.mts @@ -1,5 +1,5 @@ import { Effect, Cause } from 'effect'; -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { readFile } from 'node:fs/promises'; diff --git a/app/scripts/tests/authorization-rollout-contract.test.mts b/app/scripts/tests/authorization-rollout-contract.test.mts index aecc28b9d..5b38836f8 100644 --- a/app/scripts/tests/authorization-rollout-contract.test.mts +++ b/app/scripts/tests/authorization-rollout-contract.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { validateAuthorizationRolloutContract } from '../authorization/rollout-contract.mts'; diff --git a/app/scripts/tests/check-authorization-readiness.test.mts b/app/scripts/tests/check-authorization-readiness.test.mts index f534e48ba..799575d41 100644 --- a/app/scripts/tests/check-authorization-readiness.test.mts +++ b/app/scripts/tests/check-authorization-readiness.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import type { ProtectedEntrypointInventory } from '../authorization/protected-entrypoint-inventory.mts'; import { diff --git a/app/scripts/tests/database-access-boundaries.test.mts b/app/scripts/tests/database-access-boundaries.test.mts index 861a763a3..a3f9a304e 100644 --- a/app/scripts/tests/database-access-boundaries.test.mts +++ b/app/scripts/tests/database-access-boundaries.test.mts @@ -1,6 +1,6 @@ import { Effect } from 'effect'; import { NodeServices } from '@effect/platform-node'; -import { expect, it } from '@app/effect-rstest'; +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'; 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/initialize-local-development.test.mts b/app/scripts/tests/initialize-local-development.test.mts index 14b0a4728..04b0c629b 100644 --- a/app/scripts/tests/initialize-local-development.test.mts +++ b/app/scripts/tests/initialize-local-development.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; diff --git a/app/scripts/tests/locki-feature.test.mts b/app/scripts/tests/locki-feature.test.mts index 76735f0cb..ed5f9fc35 100644 --- a/app/scripts/tests/locki-feature.test.mts +++ b/app/scripts/tests/locki-feature.test.mts @@ -1,5 +1,5 @@ import { Effect } from 'effect'; -import { expect, it } from '@app/effect-rstest'; +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'; diff --git a/app/scripts/tests/migrate-contacts-authorization.test.mts b/app/scripts/tests/migrate-contacts-authorization.test.mts index 1dcfbd963..5101447f6 100644 --- a/app/scripts/tests/migrate-contacts-authorization.test.mts +++ b/app/scripts/tests/migrate-contacts-authorization.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { planContactsAuthorizationContext } from '../migrate-contacts-authorization.mts'; import type { ContactsAuthorizationRelationship } from '../migrate-contacts-authorization.mts'; diff --git a/app/scripts/tests/module-entrypoint-boundaries.test.mts b/app/scripts/tests/module-entrypoint-boundaries.test.mts index 6a8d332a6..cd810541d 100644 --- a/app/scripts/tests/module-entrypoint-boundaries.test.mts +++ b/app/scripts/tests/module-entrypoint-boundaries.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; diff --git a/app/scripts/tests/outbox-worker-delivery.test.mts b/app/scripts/tests/outbox-worker-delivery.test.mts index 55f22e607..bbda7a8d1 100644 --- a/app/scripts/tests/outbox-worker-delivery.test.mts +++ b/app/scripts/tests/outbox-worker-delivery.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { spawn } from 'node:child_process'; import type { ChildProcess } from 'node:child_process'; import { existsSync } from 'node:fs'; diff --git a/app/scripts/tests/plan-deployment-impact.test.mts b/app/scripts/tests/plan-deployment-impact.test.mts index b549c4c6a..42cdff8a5 100644 --- a/app/scripts/tests/plan-deployment-impact.test.mts +++ b/app/scripts/tests/plan-deployment-impact.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { execFileSync } from 'node:child_process'; import { access, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; diff --git a/app/scripts/tests/protected-entrypoint-inventory.test.mts b/app/scripts/tests/protected-entrypoint-inventory.test.mts index 855d935b7..9fadec15e 100644 --- a/app/scripts/tests/protected-entrypoint-inventory.test.mts +++ b/app/scripts/tests/protected-entrypoint-inventory.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { makeProtectedEntrypointInventory, serializeProtectedEntrypointInventory, diff --git a/app/scripts/tests/provision-current-action-authorization.test.mts b/app/scripts/tests/provision-current-action-authorization.test.mts index e49005186..373c1b26a 100644 --- a/app/scripts/tests/provision-current-action-authorization.test.mts +++ b/app/scripts/tests/provision-current-action-authorization.test.mts @@ -1,6 +1,6 @@ import type { deriveOntosModuleDeploymentContract as DeriveModuleContract } from '../generate-ontos-module-contract.mts'; -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { NodeServices } from '@effect/platform-node'; import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; diff --git a/app/scripts/tests/quality-audit-model.test.mts b/app/scripts/tests/quality-audit-model.test.mts index 047ce0bb7..b9e15cf03 100644 --- a/app/scripts/tests/quality-audit-model.test.mts +++ b/app/scripts/tests/quality-audit-model.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { runPinnedKnip } from './quality-audit-test-support.mts'; import { diff --git a/app/scripts/tests/quality-audit-runtime-model.test.mts b/app/scripts/tests/quality-audit-runtime-model.test.mts index 719ff2540..dca5a8317 100644 --- a/app/scripts/tests/quality-audit-runtime-model.test.mts +++ b/app/scripts/tests/quality-audit-runtime-model.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { runPinnedKnip } from './quality-audit-test-support.mts'; import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; diff --git a/app/scripts/tests/quality-audit.test.mts b/app/scripts/tests/quality-audit.test.mts index afb5ad198..4a8547d16 100644 --- a/app/scripts/tests/quality-audit.test.mts +++ b/app/scripts/tests/quality-audit.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { copyFileSync, 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 6092429b2..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { reduceAuthorizationImpact } from '../report-fail-closed-authorization-impact.mts'; diff --git a/app/scripts/tests/root-environment.test.mts b/app/scripts/tests/root-environment.test.mts index e17ee16f5..4042ecbed 100644 --- a/app/scripts/tests/root-environment.test.mts +++ b/app/scripts/tests/root-environment.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect } from 'effect'; import { spawnSync } from 'node:child_process'; diff --git a/app/scripts/tests/typecheck-project-references.test.mts b/app/scripts/tests/typecheck-project-references.test.mts index 5edc4e9e7..370d238cf 100644 --- a/app/scripts/tests/typecheck-project-references.test.mts +++ b/app/scripts/tests/typecheck-project-references.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; import os from 'node:os'; diff --git a/app/scripts/validate-ultramodern-workspace.mts b/app/scripts/validate-ultramodern-workspace.mts index de672da36..63a8579d5 100644 --- a/app/scripts/validate-ultramodern-workspace.mts +++ b/app/scripts/validate-ultramodern-workspace.mts @@ -5069,7 +5069,6 @@ const assertTsConfigReferenceGraph = () => { SHARED_VALIDATOR_STRING_047, ...fullStackVerticals.map((vertical) => vertical.path), ...additionalShellPaths, - 'packages/effect-rstest', ].map((referencePath) => ({ path: referencePath })); const expectedShellReferences = [ SHARED_VALIDATOR_STRING_092, diff --git a/app/tools/oxlint/effect-native/README.md b/app/tools/oxlint/effect-native/README.md index a36bedf0e..712334632 100644 --- a/app/tools/oxlint/effect-native/README.md +++ b/app/tools/oxlint/effect-native/README.md @@ -142,10 +142,12 @@ values; full serialized-object assertions and diagnostic tag output remain valid `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 `@app/effect-rstest` so the runner owns services, scopes, -test time, and configuration. There is no harness-path allowlist: the runner implementation in -`packages/effect-rstest/src/**` is already outside test-file scope. That vendored upstream port is -ignored by workspace lint; `packages/effect-rstest/tests/**` remains linted. +`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 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 2ebe5f1a2..8894329f7 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 @@ -37,8 +37,8 @@ * * ## What is deliberately allowed * - * - The runner implementation in `packages/effect-rstest/src/**` is outside test-file scope and - * owns the Effect.run* boundary. Test support and harness directories have no exemption. + * - 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. @@ -208,28 +208,28 @@ export const rule = defineRule({ docs: { description: 'Audit B2 + A1: tests must not call Effect.run* directly. Route every test program through the ' + - 'repository-owned @app/effect-rstest it.effect/it.layer 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 @app/effect-rstest it.effect/it.layer 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 @app/effect-rstest it.effect/it.layer 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 @app/effect-rstest it.effect/it.layer ' + + '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 @app/effect-rstest it.effect/it.layer 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 @app/effect-rstest it.effect/it.layer harness so services, time and configuration stay substitutable.', + 'shared effect-rstest it.effect/it.layer harness so services, time and configuration stay substitutable.', }, schema: [ { 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 e2e4671c5..380064e87 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 @@ -388,9 +388,7 @@ function assertionCall( const method = members[0]; return members.length === 1 && method !== undefined ? { method, subject: null } : null; } - if ( - !['@rstest/core', '@app/effect-rstest', 'vitest', '@jest/globals', 'expect'].includes(source) - ) + if (!['@rstest/core', 'effect-rstest', 'vitest', '@jest/globals', 'expect'].includes(source)) return null; if (specifier.type === 'ImportDefaultSpecifier' && source === 'expect') members.unshift('expect'); 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 253cbb798..d477e1a39 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 @@ -361,11 +361,11 @@ export const rule = defineRule({ let factory = unwrap(registration.callee); while (factory?.type === 'CallExpression') factory = unwrap(factory.callee); if ( - /^@app\/effect-rstest:(?:\*\.)?(?:describeWrapped|(?:(?:it|test)\.)?layer)$/u.test( + /^effect-rstest:(?:\*\.)?(?:describeWrapped|(?:(?:it|test)\.)?layer)$/u.test( imported(factory, seen) ?? '', ) ) - return '@app/effect-rstest:it'; + return 'effect-rstest:it'; } } if ( @@ -478,7 +478,7 @@ export const rule = defineRule({ const assertion = unwrap(node.object); if ( assertion?.type === 'CallExpression' && - /^(?:@app\/effect-rstest|@rstest\/core):(?:\*\.)?expect$/u.test( + /^(?:effect-rstest|@rstest\/core):(?:\*\.)?expect$/u.test( imported(assertion.callee) ?? '', ) ) @@ -528,7 +528,7 @@ export const rule = defineRule({ let callee = call.callee; // Parameterized registrations, e.g. test.each(rows)(name, callback). while (callee.type === 'CallExpression') callee = callee.callee; - return /^(?:@app\/effect-rstest|@rstest\/core|vitest|node:test):(?:\*\.)?(?:test|it)(?:\.|$)/u.test( + return /^(?:effect-rstest|@rstest\/core|vitest|node:test):(?:\*\.)?(?:test|it)(?:\.|$)/u.test( imported(callee) ?? '', ); }; @@ -542,7 +542,7 @@ export const rule = defineRule({ 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)(?:\.|$)|@app\/effect-rstest:(?:\*\.)?(?:beforeAll|afterAll|beforeEach|afterEach)$)/u.test( + /^(?: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) ?? '', ) ) diff --git a/app/tools/oxlint/effect-native/shared/test-restricted-imports.ts b/app/tools/oxlint/effect-native/shared/test-restricted-imports.ts index 6fd158089..710bf9771 100644 --- a/app/tools/oxlint/effect-native/shared/test-restricted-imports.ts +++ b/app/tools/oxlint/effect-native/shared/test-restricted-imports.ts @@ -1,16 +1,16 @@ /** Test APIs must use the Effect-native runner in both lint entrypoints. */ export const testRestrictedImports = [ - { message: 'Import test APIs from @app/effect-rstest instead.', name: 'node:test' }, + { message: 'Import test APIs from effect-rstest instead.', name: 'node:test' }, { - message: 'Import assertions from @app/effect-rstest instead.', + message: 'Import assertions from effect-rstest instead.', name: 'node:assert', }, { - message: 'Import assertions from @app/effect-rstest instead.', + message: 'Import assertions from effect-rstest instead.', name: 'node:assert/strict', }, { - message: 'Import test APIs from @app/effect-rstest instead.', + 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 b0019b91c..38189ba16 100644 --- a/app/tools/oxlint/effect-native/tests/discover-rules.test.mts +++ b/app/tools/oxlint/effect-native/tests/discover-rules.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Predicate } from 'effect'; import { spawnSync } from 'node:child_process'; diff --git a/app/tools/oxlint/effect-native/tests/fixtures.test.mts b/app/tools/oxlint/effect-native/tests/fixtures.test.mts index 98ca1944d..8cc99fe1b 100644 --- a/app/tools/oxlint/effect-native/tests/fixtures.test.mts +++ b/app/tools/oxlint/effect-native/tests/fixtures.test.mts @@ -1,6 +1,6 @@ import { readFileSync } from 'node:fs'; import path from 'node:path'; -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { fixtureConfigPath, 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 d3df3a7ef..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,7 +1,7 @@ import { Effect, Layer, ManagedRuntime, Schema } from "effect"; import { TestClock } from "effect/testing"; -import { it } from "@app/effect-rstest"; +import { it } from "effect-rstest"; declare const ContactsLayer: Layer.Layer; declare const resolve: (id: string) => Effect.Effect; 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 7a589e265..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,5 +1,5 @@ // A harness directory is ordinary test code, not a runtime exemption. -import { it } from "@app/effect-rstest"; +import { it } from "effect-rstest"; import { Effect, Layer } from "effect"; it.layer(Layer.empty)("shared layer", (it) => { 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 da4c2cb4a..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,5 +1,5 @@ // Test support delegates runtime ownership to the shared runner. -import { it } from "@app/effect-rstest"; +import { it } from "effect-rstest"; import { Effect } from "effect"; it.effect("uses the shared runner from test support", () => Effect.void); 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 index 4a970c716..91c68bff9 100644 --- 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 @@ -1,5 +1,5 @@ // expect-count: 16 -import { expect } from '@app/effect-rstest'; +import { expect } from 'effect-rstest'; import * as rstest from '@rstest/core'; import { expect as check } from 'vitest'; import jestExpect from 'expect'; 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 index 5c2c5961c..00c74ea41 100644 --- 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 @@ -1,5 +1,5 @@ // expect-count: 2 -import { expect } from '@app/effect-rstest'; +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/destructured-assertions.ts b/app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/destructured-assertions.ts index 62b2245a2..142c55912 100644 --- 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 @@ -1,7 +1,7 @@ // expect-count: 5 import assert from 'node:assert/strict'; import * as assertions from 'node:assert'; -import { expect } from '@app/effect-rstest'; +import { expect } from 'effect-rstest'; declare const error: { _tag: string }; const { strictEqual } = assert; strictEqual(error._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 index b4c7b354e..74d80c810 100644 --- 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 @@ -1,6 +1,6 @@ // expect-count: 10 import assert from 'node:assert/strict'; -import { expect } from '@app/effect-rstest'; +import { expect } from 'effect-rstest'; declare const error: unknown; declare const tag: string; expect(error).toEqual({ _tag: 'Missing', reason: 'denied' }); 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 index 9db5ea9e6..678c5e61a 100644 --- 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 @@ -1,5 +1,5 @@ // expect-count: 4 -import { expect } from '@app/effect-rstest'; +import { expect } from 'effect-rstest'; declare const error: unknown; declare const tag: string; expect(error).toMatchObject({ _tag: 'ModuleStateDeniedError' }); 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 index 0c534e283..ea3c1a5b7 100644 --- 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 @@ -1,7 +1,7 @@ // 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 '@app/effect-rstest'; +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'; 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 index e1ba612f2..9b83f4e9a 100644 --- 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 @@ -1,4 +1,4 @@ -import { expect } from '@app/effect-rstest'; +import { expect } from 'effect-rstest'; import { Effect } from 'effect'; declare const program: Effect.Effect; program.pipe(Effect.catch((error) => { 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 index 064f44b53..ac936df0c 100644 --- 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 @@ -1,4 +1,4 @@ -import { expect } from '@app/effect-rstest'; +import { expect } from 'effect-rstest'; import { expect as foreignExpect } from 'foreign-assertions'; import * as assert from 'node:assert'; declare const error: unknown; 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 index 6a0c2ed04..93d7c1bd9 100644 --- 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 @@ -1,4 +1,4 @@ -import { expect } from '@app/effect-rstest'; +import { expect } from 'effect-rstest'; declare const error: unknown; expect(error).toHaveProperty('_tag', 'Legacy'); expect(error).toHaveProperty('cause._tag', 'Legacy'); 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 index 57f67f5de..79b2328c9 100644 --- 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 @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { expect } from '@app/effect-rstest'; +import { expect } from 'effect-rstest'; import foreign from 'foreign-assertions'; declare const error: { _tag: string }; const { strictEqual: foreignEqual } = foreign; @@ -21,7 +21,7 @@ const fixture = { _tag: 'Missing' }; expect(error).toMatchObject({ message: 'Missing' }); expect(error).toMatchObject({ _tag: 'Failure' }); foreign(error).toMatchObject(fixture); -function shadowExpect(expect: typeof import('@app/effect-rstest').expect) { +function shadowExpect(expect: typeof import('effect-rstest').expect) { expect(error).toMatchObject({ _tag: 'Missing' }); } export { shadow, shadowExpect }; @@ -38,7 +38,7 @@ assert.deepStrictEqual(error, { _tag: 'Failure' }); assert.notDeepEqual(error, { field: 'Missing' }); assert.notDeepStrictEqual(error, { _tag: 'None' }); foreign(error).toEqual(fixture); -function shadowObjectExpect(expect: typeof import('@app/effect-rstest').expect) { +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-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 index a329bdd78..d05e3f6cb 100644 --- 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 @@ -1,6 +1,6 @@ // expect-count: 2 import { test as base } from '@playwright/test'; -import { test as unitTest } from '@app/effect-rstest'; +import { test as unitTest } from 'effect-rstest'; const test = base.extend({}); async function 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 index 53f951918..9e6549448 100644 --- 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 @@ -1,6 +1,6 @@ // expect-count: 6 -import { it as check, test } from '@app/effect-rstest'; -import * as suite from '@app/effect-rstest'; +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'; 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 index 2e07d687a..ca3cc6077 100644 --- 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 @@ -1,6 +1,6 @@ // expect-count: 7 -import { describeWrapped, describeWrapped as describeSuite } from '@app/effect-rstest'; -import * as suite from '@app/effect-rstest'; +import { describeWrapped, describeWrapped as describeSuite } from 'effect-rstest'; +import * as suite from 'effect-rstest'; import { Layer } from 'effect'; describeWrapped('suite', (it) => { 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 index 358dba79b..12432291d 100644 --- 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 @@ -1,6 +1,6 @@ // expect-count: 10 -import { beforeAll, it, test } from '@app/effect-rstest'; -import * as runner from '@app/effect-rstest'; +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(); }; 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 index 778373a0d..fe69f6b91 100644 --- 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 @@ -1,5 +1,5 @@ // expect-count: 7 -import { it } from '@app/effect-rstest'; +import { it } from 'effect-rstest'; import { Effect } from 'effect'; const complete = () => Promise.resolve(); 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 index a6f550855..529f2d7f6 100644 --- 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 @@ -1,5 +1,5 @@ // expect-count: 3 -import { it, layer } from '@app/effect-rstest'; +import { it, layer } from 'effect-rstest'; import { Layer } from 'effect'; it.layer(Layer.empty)('suite', (suiteIt) => { 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 index 0ba1e7dd6..1afcb1fc0 100644 --- 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 @@ -1,5 +1,5 @@ // expect-count: 6 -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import * as rstest from '@rstest/core'; import { Cause, Effect } from 'effect'; import * as E from 'effect'; 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 index 073ad05b6..e76f4170e 100644 --- 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 @@ -1,4 +1,4 @@ // expect-count: 1 -import { it } from '@app/effect-rstest'; +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/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 index 1e9151462..280003734 100644 --- 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 @@ -1,4 +1,4 @@ -import { it, test, rstest } from '@app/effect-rstest'; +import { it, test, rstest } from 'effect-rstest'; import { test as browserTest } from '@playwright/test'; import { Effect } from 'effect'; 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 index 22751cefc..13ed82e35 100644 --- 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 @@ -1,5 +1,5 @@ -import { describeWrapped, describeWrapped as describeSuite } from '@app/effect-rstest'; -import * as suite from '@app/effect-rstest'; +import { describeWrapped, describeWrapped as describeSuite } from 'effect-rstest'; +import * as suite from 'effect-rstest'; import { Effect, Layer } from 'effect'; describeWrapped('suite', (it) => { 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 index 0bdb5660f..64869aaea 100644 --- 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 @@ -1,5 +1,5 @@ -import { beforeAll, afterAll as teardown, beforeEach, afterEach } from '@app/effect-rstest'; -import * as runner from '@app/effect-rstest'; +import { beforeAll, afterAll as teardown, beforeEach, afterEach } from 'effect-rstest'; +import * as runner from 'effect-rstest'; const setup = async () => { await initialize(); }; beforeAll(setup); 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 index c744f4754..5132ac79a 100644 --- 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 @@ -1,4 +1,4 @@ -import { expect, it, rstest } from '@app/effect-rstest'; +import { expect, it, rstest } from 'effect-rstest'; import { Effect, Fiber } from 'effect'; import { client } from 'external-sdk'; import { expect as foreignExpect } from 'external-assertions'; 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 index 5b9e1eaac..3bca399f4 100644 --- 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 @@ -1,4 +1,4 @@ -import { it, layer } from '@app/effect-rstest'; +import { it, layer } from 'effect-rstest'; import { Effect, Layer } from 'effect'; import { test } from '@playwright/test'; diff --git a/app/tools/oxlint/effect-native/tests/launcher.test.mts b/app/tools/oxlint/effect-native/tests/launcher.test.mts index 1ca14e251..db1da5ef5 100644 --- a/app/tools/oxlint/effect-native/tests/launcher.test.mts +++ b/app/tools/oxlint/effect-native/tests/launcher.test.mts @@ -1,4 +1,4 @@ -import { expect, it, rstest } from '@app/effect-rstest'; +import { expect, it, rstest } from 'effect-rstest'; import { Schema } from 'effect'; import { spawnSync } from 'node:child_process'; import { readFileSync, writeFileSync } from 'node:fs'; diff --git a/app/tools/oxlint/effect-native/tests/oxlint.test.mts b/app/tools/oxlint/effect-native/tests/oxlint.test.mts index c07991021..b8aa8acbf 100644 --- a/app/tools/oxlint/effect-native/tests/oxlint.test.mts +++ b/app/tools/oxlint/effect-native/tests/oxlint.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { parseOxlintOutput } from './oxlint.mts'; diff --git a/app/tools/oxlint/effect-native/tests/paths.test.mts b/app/tools/oxlint/effect-native/tests/paths.test.mts index 4108e8607..1d0c128ce 100644 --- a/app/tools/oxlint/effect-native/tests/paths.test.mts +++ b/app/tools/oxlint/effect-native/tests/paths.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { isScriptFile, normalisePath } from '../shared/paths.ts'; 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 62a3bcd59..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Schema } from 'effect'; import { cpSync, readFileSync } from 'node:fs'; import nodePath from 'node:path'; diff --git a/app/tools/oxlint/effect-native/tests/registration.test.mts b/app/tools/oxlint/effect-native/tests/registration.test.mts index 8c40d075a..fda91e4f0 100644 --- a/app/tools/oxlint/effect-native/tests/registration.test.mts +++ b/app/tools/oxlint/effect-native/tests/registration.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Schema } from 'effect'; import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import nodePath from 'node:path'; diff --git a/app/tools/oxlint/effect-native/tests/repository-policy.test.mts b/app/tools/oxlint/effect-native/tests/repository-policy.test.mts index 47e620312..8f3023fc9 100644 --- a/app/tools/oxlint/effect-native/tests/repository-policy.test.mts +++ b/app/tools/oxlint/effect-native/tests/repository-policy.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import nodePath from 'node:path'; import { appRoot, pluginDirectory, runOxlint } from './oxlint.mts'; 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 1974344b7..46fdccc4c 100644 --- a/app/tools/oxlint/effect-native/tests/script-scope.test.mts +++ b/app/tools/oxlint/effect-native/tests/script-scope.test.mts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { mkdirSync, writeFileSync } from 'node:fs'; import nodePath from 'node:path'; 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 ec089cf63..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { spawnSync } from 'node:child_process'; import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'; import nodePath from 'node:path'; diff --git a/app/tsconfig.json b/app/tsconfig.json index 5611403cb..e500a6889 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -18,9 +18,6 @@ }, { "path": "verticals/party-registry" - }, - { - "path": "packages/effect-rstest" } ] } diff --git a/app/verticals/party-registry/package.json b/app/verticals/party-registry/package.json index 8b4b56309..65c8ebf53 100644 --- a/app/verticals/party-registry/package.json +++ b/app/verticals/party-registry/package.json @@ -93,7 +93,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", "@testing-library/user-event": "14.6.1", @@ -110,7 +110,7 @@ "typescript": "7.0.2", "wrangler": "4.110.0", "zephyr-rspack-plugin": "1.2.4", - "@app/effect-rstest": "workspace:*" + "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc" }, "modernjs": { "preset": "presetUltramodern", 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 f7aef5817..f38227726 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 '@app/effect-rstest'; +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 6faeae31e..2b09118c3 100644 --- a/app/verticals/party-registry/tests/integration/ares-governed.test.ts +++ b/app/verticals/party-registry/tests/integration/ares-governed.test.ts @@ -1,4 +1,4 @@ -import { assert, expect, it } from '@app/effect-rstest'; +import { assert, expect, it } from 'effect-rstest'; import { DatabaseConfig, loadDatabaseConnectionPair } from '@app/core-runtime'; import { makeLiveOperationFixture } from '@app/core-runtime/testing/actions'; 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 0e9cd44ed..8b454f524 100644 --- a/app/verticals/party-registry/tests/integration/database-boundary.test.ts +++ b/app/verticals/party-registry/tests/integration/database-boundary.test.ts @@ -1,4 +1,4 @@ -import { assert, expect, it } from '@app/effect-rstest'; +import { assert, expect, it } from 'effect-rstest'; import { findPostgresFailure, loadDatabaseConnectionPair } from '@app/core-runtime'; 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 8956f3401..b55b7cd90 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { findPostgresFailure, loadDatabaseConnectionPair } from '@app/core-runtime'; import { eq, inArray, sql } from 'drizzle-orm'; 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 1e60e05c7..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,4 +1,4 @@ -import { assert, expect, it } from '@app/effect-rstest'; +import { assert, expect, it } from 'effect-rstest'; import type { TrustedPrincipalContext } from '@app/core-runtime'; import { 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 cf46315b6..58bcddc2a 100644 --- a/app/verticals/party-registry/tests/integration/identity-concurrency.test.ts +++ b/app/verticals/party-registry/tests/integration/identity-concurrency.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { loadDatabaseConnectionPair } from '@app/core-runtime'; import { eq, sql } from 'drizzle-orm'; import { DateTime, Effect, Option } from 'effect'; 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 0e16e90ed..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { DateTime, Effect, Layer, Match, Option, Result, Schema } from 'effect'; import { TestClock } from 'effect/testing'; import { 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 24da8f856..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema } from 'effect'; import { FetchHttpClient } from 'effect/unstable/http'; 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 b41bf25a1..045e02f31 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Result, Schema, Struct } from 'effect'; import { PartyCommandConflictProblemSchema } from '../../shared/command-api.ts'; import { FetchHttpClient } from 'effect/unstable/http'; 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 2b032df2b..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,5 +1,5 @@ // @effect-diagnostics nodeBuiltinImport:off -- Inspect source files through the Node filesystem boundary; expires: 2026-12-31. -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { readFile, readdir } from 'node:fs/promises'; import { Effect, Schema, Struct } from 'effect'; import { 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 013788ec4..3e8e0ac24 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Match, Result, Schema, Struct } from 'effect'; import { FetchHttpClient } from 'effect/unstable/http'; import { 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 85fa4039d..3f08526f1 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,4 +1,4 @@ -import { assert, expect, it } from '@app/effect-rstest'; +import { assert, expect, it } from 'effect-rstest'; import { randomUUID } from 'node:crypto'; import { ConfigProvider, Context, Effect, Layer, Logger, Schema, Predicate, Struct } from 'effect'; 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 6dfac36f5..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,5 +1,5 @@ // @effect-diagnostics nodeBuiltinImport:off -- Inspect source files through the Node filesystem boundary; expires: 2026-12-31. -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { readFile } from 'node:fs/promises'; import { Effect, Schema } from 'effect'; 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 46d89b75a..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { DateTime, Effect, Option, Schema } from 'effect'; import { 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 fe15fb6eb..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,4 +1,4 @@ -import { assert, expect, it } from '@app/effect-rstest'; +import { assert, expect, it } from 'effect-rstest'; import { randomUUID } from 'node:crypto'; import { ActionRuntime, GatewayAssertionRedemptionService, ReadRuntime } from '@app/core-runtime'; 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 f444aed9f..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { DateTime, Option, Result, Schema } from 'effect'; import { 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 53a65e6b5..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema } from 'effect'; import { 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 ac757cf9f..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,5 +1,5 @@ // @effect-diagnostics nodeBuiltinImport:off -- Read-only architecture assertions use native filesystem promises. expires: 2026-12-31. -import { assert, expect, it } from '@app/effect-rstest'; +import { assert, expect, it } from 'effect-rstest'; import { readFile, readdir } from 'node:fs/promises'; import { Effect, Schema, SchemaAST, Predicate, Struct } from 'effect'; 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 80a4b1de3..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,5 +1,5 @@ // @effect-diagnostics strictEffectProvide:off -- Tests intentionally provide isolated adapter and logger layers. expires: 2026-12-31. -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { DateTime, Effect, Fiber, Logger, Option, Predicate, Schema } from 'effect'; import { TestClock } from 'effect/testing'; 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 ea29775e9..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Schema } from 'effect'; import { PartySubjectEvidenceSchema, 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 753f6ee0b..4f10da1c5 100644 --- a/app/verticals/party-registry/tests/unit/catalog-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/catalog-contract.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { comparePartyCatalog, expectedPartyTableCatalog } from '../../src/db/catalog.ts'; it('reports exact Party Registry catalog differences', () => { 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 32280ed3f..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Schema } from 'effect'; import { AddressContactPointValueSchema, 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 122925cd2..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 cde2c523c..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,5 +1,5 @@ import { TestClock } from 'effect/testing'; -import { expect, it } from '@app/effect-rstest'; +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 { is, SQL } from 'drizzle-orm'; import { PgDialect } from 'drizzle-orm/pg-core'; 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 af2a62dc0..4bea57dce 100644 --- a/app/verticals/party-registry/tests/unit/correction-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/correction-contract.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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 { DateTime, Effect, Match, Option, Schema, Predicate } from 'effect'; import { 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 9afaa6864..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { partyRegistryCorsAllowedOrigins, resolvePartyRegistryShellOrigin, 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 045ac5665..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema } from 'effect'; import { CounterpartyCreatePayloadSchema, 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 d6638c58f..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,5 +1,5 @@ import { TestClock } from 'effect/testing'; -import { expect, it } from '@app/effect-rstest'; +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'; 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 03339cb25..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { counterpartyContextEvidenceIsSufficient, roleEvidenceIsSufficient, 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 266753156..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Predicate } from 'effect'; import { acquirePoolResource, makePartyDatabase } from '../../src/db/client.ts'; 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 2cd912df6..46b7963f4 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { compareContactsCatalog, expectedContactsTableCatalog, 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 9fd330609..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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 { Effect, Option, Schema } from 'effect'; 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 f37a5225c..b4fa9a272 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { 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, Predicate } from 'effect'; import type { OrganizationEngagementProfileRecord } from '../../src/db/engagement-schema.ts'; 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 3e852626d..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 2c1dcd7f9..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { getTableConfig } from 'drizzle-orm/pg-core'; import { CONTACTS_SCHEMA_NAME, 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 78113ee0e..033724f2b 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,4 @@ -import { assert, expect, it } from '@app/effect-rstest'; +import { assert, expect, it } from 'effect-rstest'; import { GatewayAssertionRedemptionUnavailableError, GatewayAssertionReplayError, 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 1e784414a..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Schema } from 'effect'; import { OfficialIdentifierInputSchema, 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 94974a8b0..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,5 +1,5 @@ import { TestClock } from 'effect/testing'; -import { expect, it } from '@app/effect-rstest'; +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'; 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 c3c2d13ee..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 4bb348b25..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 9450dc2e9..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, DateTime, Option, Schema, Struct } from 'effect'; import { PartyCandidateSchema, 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 9c6c33b0f..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { DateTime } from 'effect'; import { partySubjectKeyFromString } from '../../shared/domain/identity-contracts.ts'; import type { 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 321e5b57f..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 f20fd19dd..11297913c 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Option, Schema } from 'effect'; import { makeTestDatabase } from '../../../../packages/core-runtime/tests/support/database.ts'; import { PartyFactAssertionSchema } from '../../shared/apis/party-detail.ts'; 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 8329360a0..ee99e659f 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,5 +1,5 @@ import { TestClock } from 'effect/testing'; -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import type { SQL } from 'drizzle-orm'; import { PgDialect } from 'drizzle-orm/pg-core'; 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 5b5fe4a71..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect } from 'effect'; import { createActionCollector } from '../../../../packages/core-runtime/src/actions/collector.ts'; 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 c09982d58..9a80aa720 100644 --- a/app/verticals/party-registry/tests/unit/matching-persistence.test.ts +++ b/app/verticals/party-registry/tests/unit/matching-persistence.test.ts @@ -1,5 +1,5 @@ import { TestClock } from 'effect/testing'; -import { expect, it } from '@app/effect-rstest'; +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'; 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 31e3b9b19..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Match, Option, Predicate } from 'effect'; import { makePartyAliasResolutionService } from '../../src/merge/party-alias-resolution.service.ts'; 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 492fc0b9e..4e4f360f7 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { DateTime, Predicate, Struct, Schema } from 'effect'; import type { PartyRef } from '../../shared/resources/party.ts'; 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 85a229214..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { DateTime, Match, Struct, Predicate } from 'effect'; import type { PartyRef } from '../../shared/resources/party.ts'; 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 bb588bb9b..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,5 +1,5 @@ // @effect-diagnostics nodeBuiltinImport:off -- Source-only contract checks require reading TypeScript files; remove-when: manifests are importable without TSX loaders. -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { readFile } from 'node:fs/promises'; import { Effect, Match, Schema, Struct, Predicate } from 'effect'; import { 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 fafc94c28..81d9e8b8c 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Match, Predicate, Struct, Schema } from 'effect'; import type { PartyRef } from '../../shared/resources/party.ts'; import type { 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 ca6f7c9a3..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect } from 'effect'; import { Client } from 'pg'; import { 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 ebd1405a8..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, DateTime, Option, Schema, Predicate, Struct } from 'effect'; import { ContactPersonOfRelationshipType, 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 de494b8ec..b7049efc0 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 8620bafbf..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,5 +1,5 @@ import { TestClock } from 'effect/testing'; -import { expect, it } from '@app/effect-rstest'; +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, Layer, Option, Schema, Predicate } from 'effect'; 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 3af4f6055..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Predicate } from 'effect'; import runtime from '../../src/modern.runtime.ts'; 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 e60222856..30b65215a 100644 --- a/app/verticals/party-registry/tests/unit/schema-contract.test.ts +++ b/app/verticals/party-registry/tests/unit/schema-contract.test.ts @@ -1,5 +1,5 @@ // @effect-diagnostics nodeBuiltinImport:off -- Filesystem migration contract verifies actual checked-in SQL files; expires: 2026-12-31. -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect } from 'effect'; import { readdir, readFile } from 'node:fs/promises'; 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 946f5bf70..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema } from 'effect'; import { 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 c0774b73f..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect, Schema, Predicate } from 'effect'; import { CoreSearchProjectionHitSchema } from '@app/core-runtime'; 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 f5e52b58a..01290588d 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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { DateTime, Effect, Option, Schema } from 'effect'; import { 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 d4317c448..d26440e0b 100644 --- a/app/verticals/party-registry/tests/unit/search-projector.test.ts +++ b/app/verticals/party-registry/tests/unit/search-projector.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { TestClock } from 'effect/testing'; import { Effect, Exit, Match, Predicate } from 'effect'; 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 9e45f13e8..530a629ac 100644 --- a/app/verticals/party-registry/tests/unit/search-provider.test.ts +++ b/app/verticals/party-registry/tests/unit/search-provider.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Effect } from 'effect'; import type { PartySearchProjectionGatewayService } from '../../shared/domain/search-projection-gateway.ts'; import { 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 40bcfae19..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +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'; 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 3bfaa8e31..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { Match, Predicate, Struct } from 'effect'; import { normalizeCounterpartySearchHits, 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 da408b112..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import type { CoreSearchSnapshotReadExecutor, CoreSearchWorkerSnapshotService, 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 1a0d55fea..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,4 +1,4 @@ -import { expect, it } from '@app/effect-rstest'; +import { expect, it } from 'effect-rstest'; import { CORE_SEARCH_INGESTION_REGISTRATIONS } from '@app/core-runtime'; import { outboxWorkers } from '../../src/workers/index.ts'; From ddaa97c0995bd408ceb38ac0dc23a176df724c2b Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 18:06:40 +0200 Subject: [PATCH 36/38] fix(quality): model native Rstest project environments Follow exported static project configurations with source evidence and pinned-Knip positive and negative controls. Co-Authored-By: Claude Fable 5.1 --- app/quality-audit/knip-model.mts | 45 +++++-- .../tests/quality-audit-model.test.mts | 120 +++++++++++++++++- 2 files changed, 150 insertions(+), 15 deletions(-) diff --git a/app/quality-audit/knip-model.mts b/app/quality-audit/knip-model.mts index 3e7b8dfa3..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]; @@ -737,17 +770,7 @@ const sourceEvidence = ( NewExpression: recordUrl, ObjectExpression: recordLintPlugin, }).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'), - ); - } - } + 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/scripts/tests/quality-audit-model.test.mts b/app/scripts/tests/quality-audit-model.test.mts index f14a20277..4b965d409 100644 --- a/app/scripts/tests/quality-audit-model.test.mts +++ b/app/scripts/tests/quality-audit-model.test.mts @@ -33,6 +33,9 @@ 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({ @@ -467,7 +470,7 @@ it.live( `require.resolve('target', { paths: [${JSON.stringify(path.join(root, owner, 'index.js'))}] });`, ].join('\n'), ); - const consumerPath = path.join(root, '.audit/consumers.mts'); + const consumerPath = path.join(root, auditConsumersFile); const build = () => Effect.gen(function* testEffect9() { return yield* buildKnipModel( @@ -540,7 +543,7 @@ it.live( const policyTest = `${lintDirectory}/tests/repository-policy.test.mts`; const helperTest = `${lintDirectory}/tests/shared-helpers.test.mts`; const loadedFiles = [ - 'rstest.config.ts', + rstestConfigFile, `${lintDirectory}/repository-policy.config.ts`, `${lintDirectory}/tests/shared-helpers-probe.ts`, ]; @@ -565,13 +568,13 @@ it.live( 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, '.audit/consumers.mts'); + const consumerPath = path.join(root, auditConsumersFile); const base = { workspaces: { '.': { entry: [policyTest, helperTest], node: false, - project: ['rstest.config.ts', 'src/*.ts', toolsPattern], + project: [rstestConfigFile, 'src/*.ts', toolsPattern], rstest: { config: [] }, }, }, @@ -654,3 +657,112 @@ it.live( ).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'); + } + }), +); From 06d58ee478271542daad8fe707b9ee5e00033b61 Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 18:06:40 +0200 Subject: [PATCH 37/38] fix(lint): inspect proven asymmetric tag assertions Resolve framework objectContaining and arrayContaining expected values while preserving lexical identity, mutation guards, ADT exemptions and payload semantics. Co-Authored-By: Claude Fable 5.1 --- .../rules/no-manual-tag-comparison.ts | 186 +++++++++++++++--- .../core-runtime/src/configured-assertions.ts | 4 +- .../core-runtime/src/expected-wrappers.ts | 37 ++++ .../src/stable-expected-wrappers.ts | 14 ++ .../core-runtime/src/configured-assertions.ts | 3 + .../core-runtime/src/expected-wrappers.ts | 41 ++++ .../src/mutated-expected-wrappers.ts | 36 ++++ 7 files changed, 298 insertions(+), 23 deletions(-) create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/expected-wrappers.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/stable-expected-wrappers.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/expected-wrappers.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/mutated-expected-wrappers.ts 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 a72830960..db72ff802 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 @@ -288,6 +288,7 @@ function destructuredMethod( 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. */ @@ -301,6 +302,16 @@ function nodeAssertion( 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 method = members[1]; + if (members.length !== 2 || 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, @@ -310,12 +321,7 @@ function importedAssertion( 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) { - const method = members[1]; - return members.length === 2 && members[0] === 'assert' && method !== undefined - ? { method, subject: null } - : null; - } + if (subject === null) return staticAssertion(members); if (members.shift() !== 'expect') return null; const method = members.pop(); if ( @@ -343,13 +349,14 @@ function assertionImport( 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 importedAssertion(source, members, subject); + return stableAssertion(context, expression, path, importedAssertion(source, members, subject)); } function assertionAlias( @@ -363,6 +370,109 @@ function assertionAlias( 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); @@ -823,22 +933,55 @@ const OBJECT_ASSERTION_METHODS = new Set([ 'notDeepStrictEqual', ]); -/** Preserve the bounded immutable shape walk, including cycles and depth-limit behavior. */ -function expectedShapeTag(context: Context, expected: ESTree.Node): ESTree.Node | null { - let shape = unwrap(expected); - const seen = new Set(); - for (let depth = 0; depth < MAX_DEPTH && !seen.has(shape); depth += 1) { - seen.add(shape); - const initialiser = constInitialiser(context, shape); - if (initialiser === null) break; - shape = unwrap(initialiser); - } - if (shape.type !== 'ObjectExpression') return null; +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 : null; + 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. */ @@ -1205,9 +1348,8 @@ export const rule = defineRule({ 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 tag = expectedShapeTag(context, expected); - if (tag === null) return false; - if (exemptStaticTag(tag)) return false; + const tags = expectedShapeTags(context, expected); + if (!tags.some((tag) => !exemptStaticTag(tag))) return false; if (suppressed(node)) return false; reportAssertion( node, 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 index fd2d74014..7b835bd87 100644 --- 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 @@ -1,4 +1,4 @@ -// expect-count: 4 +// expect-count: 5 import assert from 'node:assert/strict'; import { expect } from '@rstest/core'; assert.equal(value._tag, 'Missing', 'Legacy'); @@ -9,3 +9,5 @@ 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/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/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/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 index 1f2e0cf30..3a80d6389 100644 --- 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 @@ -9,3 +9,6 @@ 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/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' })); From dfe49e6d7c32d39aaa03d62902c77c4aa6037bca Mon Sep 17 00:00:00 2001 From: Petr Glaser Date: Tue, 8 Sep 2026 18:20:13 +0200 Subject: [PATCH 38/38] fix(lint): recognize negated asymmetric tag assertions Accept one static expect.not modifier without weakening helper provenance or mutation guards. Add ten failing-before positive cases and foreign, shadowed, mutated, payload and unsupported-modifier controls. Co-Authored-By: Claude Fable 5.1 --- .../rules/no-manual-tag-comparison.ts | 5 +++-- .../src/negated-expected-wrappers.ts | 21 +++++++++++++++++++ .../src/mutated-negated-wrappers.ts | 16 ++++++++++++++ .../src/negated-expected-wrappers.ts | 17 +++++++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/invalid/packages/core-runtime/src/negated-expected-wrappers.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/mutated-negated-wrappers.ts create mode 100644 app/tools/oxlint/effect-native/tests/fixtures/no-manual-tag-comparison/valid/packages/core-runtime/src/negated-expected-wrappers.ts 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 db72ff802..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 @@ -304,8 +304,9 @@ function nodeAssertion( /** Static expect helpers are expected values, never subject-bearing assertions. */ function staticAssertion(members: string[]): AssertionCall | null { - const method = members[1]; - if (members.length !== 2 || method === undefined) return 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 } 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/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/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 };