@satorio/machinery 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/dictionary/all.json +4656 -0
- package/dictionary/all.toon +639 -0
- package/dictionary/core.json +221 -0
- package/dictionary/core.toon +33 -0
- package/dictionary/graphql.json +212 -0
- package/dictionary/graphql.toon +32 -0
- package/dictionary/http.json +113 -0
- package/dictionary/http.toon +21 -0
- package/dictionary/identifiers.json +266 -0
- package/dictionary/identifiers.toon +38 -0
- package/dictionary/index.json +96 -0
- package/dictionary/index.toon +18 -0
- package/dictionary/integration.json +131 -0
- package/dictionary/integration.toon +23 -0
- package/dictionary/money.json +383 -0
- package/dictionary/money.toon +51 -0
- package/dictionary/ports.json +392 -0
- package/dictionary/ports.toon +52 -0
- package/dictionary/postgres.json +473 -0
- package/dictionary/postgres.toon +61 -0
- package/dictionary/publishing.json +131 -0
- package/dictionary/publishing.toon +23 -0
- package/dictionary/queue.json +365 -0
- package/dictionary/queue.toon +49 -0
- package/dictionary/reference/api.mdx +843 -0
- package/dictionary/reference/uk/api.mdx +844 -0
- package/dictionary/storage.json +194 -0
- package/dictionary/storage.toon +30 -0
- package/dictionary/testing.json +707 -0
- package/dictionary/testing.toon +87 -0
- package/dictionary/time.json +311 -0
- package/dictionary/time.toon +43 -0
- package/dictionary/vitest.json +356 -0
- package/dictionary/vitest.toon +48 -0
- package/dictionary/workflow.json +185 -0
- package/dictionary/workflow.toon +29 -0
- package/dist/bin/command.d.ts +45 -0
- package/dist/bin/command.js +113 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +11 -0
- package/dist/core/bigint-json.d.ts +6 -0
- package/dist/core/bigint-json.js +6 -0
- package/dist/core/closed-set.d.ts +14 -0
- package/dist/core/closed-set.js +8 -0
- package/dist/core/domain-error.d.ts +8 -0
- package/dist/core/domain-error.js +11 -0
- package/dist/core/environment.d.ts +124 -0
- package/dist/core/environment.js +115 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +13 -0
- package/dist/core/ulid.d.ts +27 -0
- package/dist/core/ulid.js +27 -0
- package/dist/graphql/builder.d.ts +36 -0
- package/dist/graphql/builder.js +35 -0
- package/dist/graphql/context.d.ts +36 -0
- package/dist/graphql/context.js +69 -0
- package/dist/graphql/failure-classification.d.ts +11 -0
- package/dist/graphql/failure-classification.js +1 -0
- package/dist/graphql/failures.d.ts +62 -0
- package/dist/graphql/failures.js +30 -0
- package/dist/graphql/graphiql.d.ts +45 -0
- package/dist/graphql/graphiql.js +56 -0
- package/dist/graphql/index.d.ts +15 -0
- package/dist/graphql/index.js +14 -0
- package/dist/graphql/layer.d.ts +15 -0
- package/dist/graphql/layer.js +23 -0
- package/dist/graphql/request-handler.d.ts +26 -0
- package/dist/graphql/request-handler.js +33 -0
- package/dist/graphql/server.d.ts +19 -0
- package/dist/graphql/server.js +16 -0
- package/dist/graphql/settings.d.ts +55 -0
- package/dist/graphql/settings.js +82 -0
- package/dist/http/health.d.ts +70 -0
- package/dist/http/health.js +60 -0
- package/dist/http/index.d.ts +5 -0
- package/dist/http/index.js +5 -0
- package/dist/identifiers/client-code.d.ts +37 -0
- package/dist/identifiers/client-code.js +41 -0
- package/dist/identifiers/creditor-reference.d.ts +38 -0
- package/dist/identifiers/creditor-reference.js +32 -0
- package/dist/identifiers/damm.d.ts +22 -0
- package/dist/identifiers/damm.js +50 -0
- package/dist/identifiers/document-number.d.ts +68 -0
- package/dist/identifiers/document-number.js +73 -0
- package/dist/identifiers/document-numbers.d.ts +27 -0
- package/dist/identifiers/document-numbers.js +16 -0
- package/dist/identifiers/failures.d.ts +46 -0
- package/dist/identifiers/failures.js +48 -0
- package/dist/identifiers/index.d.ts +14 -0
- package/dist/identifiers/index.js +13 -0
- package/dist/identifiers/next-document-number.d.ts +26 -0
- package/dist/identifiers/next-document-number.js +31 -0
- package/dist/identifiers/ulid.d.ts +28 -0
- package/dist/identifiers/ulid.js +71 -0
- package/dist/integration/external-http-client.d.ts +22 -0
- package/dist/integration/external-http-client.js +74 -0
- package/dist/integration/index.d.ts +12 -0
- package/dist/integration/index.js +12 -0
- package/dist/integration/layer.d.ts +19 -0
- package/dist/integration/layer.js +30 -0
- package/dist/integration/retry.d.ts +65 -0
- package/dist/integration/retry.js +54 -0
- package/dist/integration/settings.d.ts +73 -0
- package/dist/integration/settings.js +92 -0
- package/dist/money/amount.d.ts +60 -0
- package/dist/money/amount.js +67 -0
- package/dist/money/arithmetic.d.ts +60 -0
- package/dist/money/arithmetic.js +73 -0
- package/dist/money/currency.d.ts +25 -0
- package/dist/money/currency.js +24 -0
- package/dist/money/decimal.d.ts +29 -0
- package/dist/money/decimal.js +39 -0
- package/dist/money/exchange-rate.d.ts +32 -0
- package/dist/money/exchange-rate.js +28 -0
- package/dist/money/failures.d.ts +86 -0
- package/dist/money/failures.js +97 -0
- package/dist/money/index.d.ts +15 -0
- package/dist/money/index.js +15 -0
- package/dist/money/percent.d.ts +35 -0
- package/dist/money/percent.js +50 -0
- package/dist/money/schema.d.ts +25 -0
- package/dist/money/schema.js +41 -0
- package/dist/ports/blob-store.d.ts +42 -0
- package/dist/ports/blob-store.js +13 -0
- package/dist/ports/document-number-counter.d.ts +36 -0
- package/dist/ports/document-number-counter.js +12 -0
- package/dist/ports/event-publisher.d.ts +29 -0
- package/dist/ports/event-publisher.js +13 -0
- package/dist/ports/external-http-client.d.ts +60 -0
- package/dist/ports/external-http-client.js +25 -0
- package/dist/ports/index.d.ts +17 -0
- package/dist/ports/index.js +17 -0
- package/dist/ports/integration-event-outbox.d.ts +44 -0
- package/dist/ports/integration-event-outbox.js +13 -0
- package/dist/ports/port-unavailable.d.ts +21 -0
- package/dist/ports/port-unavailable.js +30 -0
- package/dist/ports/processed-event-registry.d.ts +57 -0
- package/dist/ports/processed-event-registry.js +24 -0
- package/dist/ports/queue-message-producer.d.ts +26 -0
- package/dist/ports/queue-message-producer.js +12 -0
- package/dist/ports/queue-message-source.d.ts +39 -0
- package/dist/ports/queue-message-source.js +13 -0
- package/dist/ports/readiness-probe.d.ts +27 -0
- package/dist/ports/readiness-probe.js +13 -0
- package/dist/ports/transaction-boundary.d.ts +28 -0
- package/dist/ports/transaction-boundary.js +13 -0
- package/dist/postgres/client.d.ts +51 -0
- package/dist/postgres/client.js +81 -0
- package/dist/postgres/document-number-counter.d.ts +14 -0
- package/dist/postgres/document-number-counter.js +27 -0
- package/dist/postgres/failures.d.ts +28 -0
- package/dist/postgres/failures.js +18 -0
- package/dist/postgres/index.d.ts +18 -0
- package/dist/postgres/index.js +18 -0
- package/dist/postgres/integration-event-outbox.d.ts +15 -0
- package/dist/postgres/integration-event-outbox.js +60 -0
- package/dist/postgres/jsonb-value.d.ts +10 -0
- package/dist/postgres/jsonb-value.js +9 -0
- package/dist/postgres/layer.d.ts +57 -0
- package/dist/postgres/layer.js +72 -0
- package/dist/postgres/migrations/advisory-lock.d.ts +47 -0
- package/dist/postgres/migrations/advisory-lock.js +59 -0
- package/dist/postgres/migrations/runner.d.ts +56 -0
- package/dist/postgres/migrations/runner.js +69 -0
- package/dist/postgres/migrations/sql-file-loader.d.ts +59 -0
- package/dist/postgres/migrations/sql-file-loader.js +99 -0
- package/dist/postgres/processed-event-registry.d.ts +13 -0
- package/dist/postgres/processed-event-registry.js +44 -0
- package/dist/postgres/readiness-probe.d.ts +27 -0
- package/dist/postgres/readiness-probe.js +33 -0
- package/dist/postgres/settings.d.ts +134 -0
- package/dist/postgres/settings.js +210 -0
- package/dist/postgres/sql/0001_processed_event.sql +4 -0
- package/dist/postgres/sql/0002_integration_event_outbox.sql +13 -0
- package/dist/postgres/sql/0003_document_number_counter.sql +6 -0
- package/dist/postgres/sql-failure.d.ts +19 -0
- package/dist/postgres/sql-failure.js +22 -0
- package/dist/postgres/store-failure.d.ts +17 -0
- package/dist/postgres/store-failure.js +16 -0
- package/dist/postgres/transaction-boundary.d.ts +13 -0
- package/dist/postgres/transaction-boundary.js +21 -0
- package/dist/publishing/event-publisher.sns.d.ts +66 -0
- package/dist/publishing/event-publisher.sns.js +75 -0
- package/dist/publishing/event-publisher.stdout.d.ts +15 -0
- package/dist/publishing/event-publisher.stdout.js +24 -0
- package/dist/publishing/index.d.ts +10 -0
- package/dist/publishing/index.js +10 -0
- package/dist/publishing/layer.d.ts +25 -0
- package/dist/publishing/layer.js +39 -0
- package/dist/publishing/settings.d.ts +60 -0
- package/dist/publishing/settings.js +85 -0
- package/dist/queue/consumer.d.ts +91 -0
- package/dist/queue/consumer.js +92 -0
- package/dist/queue/envelope.d.ts +39 -0
- package/dist/queue/envelope.js +39 -0
- package/dist/queue/failures.d.ts +33 -0
- package/dist/queue/failures.js +15 -0
- package/dist/queue/index.d.ts +14 -0
- package/dist/queue/index.js +14 -0
- package/dist/queue/layer.d.ts +33 -0
- package/dist/queue/layer.js +58 -0
- package/dist/queue/message-codec.d.ts +43 -0
- package/dist/queue/message-codec.js +33 -0
- package/dist/queue/queue-message-producer.d.ts +61 -0
- package/dist/queue/queue-message-producer.js +52 -0
- package/dist/queue/queue-message-source.d.ts +93 -0
- package/dist/queue/queue-message-source.js +86 -0
- package/dist/queue/readiness.d.ts +78 -0
- package/dist/queue/readiness.js +86 -0
- package/dist/queue/settings.d.ts +106 -0
- package/dist/queue/settings.js +160 -0
- package/dist/storage/blob-store.d.ts +84 -0
- package/dist/storage/blob-store.js +109 -0
- package/dist/storage/index.d.ts +9 -0
- package/dist/storage/index.js +9 -0
- package/dist/storage/layer.d.ts +17 -0
- package/dist/storage/layer.js +30 -0
- package/dist/storage/settings.d.ts +48 -0
- package/dist/storage/settings.js +65 -0
- package/dist/testing/blob-store.d.ts +27 -0
- package/dist/testing/blob-store.js +45 -0
- package/dist/testing/clock.d.ts +29 -0
- package/dist/testing/clock.js +41 -0
- package/dist/testing/dna-perturbator/attention.d.ts +55 -0
- package/dist/testing/dna-perturbator/attention.js +49 -0
- package/dist/testing/dna-perturbator/chains.d.ts +31 -0
- package/dist/testing/dna-perturbator/chains.js +53 -0
- package/dist/testing/dna-perturbator/report.d.ts +12 -0
- package/dist/testing/dna-perturbator/report.js +22 -0
- package/dist/testing/dna-perturbator/verdict.d.ts +26 -0
- package/dist/testing/dna-perturbator/verdict.js +28 -0
- package/dist/testing/dna-perturbator.d.ts +48 -0
- package/dist/testing/dna-perturbator.js +145 -0
- package/dist/testing/document-number-counter.d.ts +29 -0
- package/dist/testing/document-number-counter.js +39 -0
- package/dist/testing/event-publisher.d.ts +28 -0
- package/dist/testing/event-publisher.js +33 -0
- package/dist/testing/external-http-client.d.ts +29 -0
- package/dist/testing/external-http-client.js +49 -0
- package/dist/testing/index.d.ts +24 -0
- package/dist/testing/index.js +24 -0
- package/dist/testing/integration-event-outbox.d.ts +37 -0
- package/dist/testing/integration-event-outbox.js +67 -0
- package/dist/testing/mutation-settings.d.ts +71 -0
- package/dist/testing/mutation-settings.js +57 -0
- package/dist/testing/postgres-rows.d.ts +12 -0
- package/dist/testing/postgres-rows.js +19 -0
- package/dist/testing/processed-event-registry.d.ts +25 -0
- package/dist/testing/processed-event-registry.js +39 -0
- package/dist/testing/queue-message-producer.d.ts +27 -0
- package/dist/testing/queue-message-producer.js +41 -0
- package/dist/testing/queue-message-source.d.ts +54 -0
- package/dist/testing/queue-message-source.js +71 -0
- package/dist/testing/quiet-reporter.d.ts +86 -0
- package/dist/testing/quiet-reporter.js +81 -0
- package/dist/testing/readiness-probe.d.ts +27 -0
- package/dist/testing/readiness-probe.js +33 -0
- package/dist/testing/scenario.d.ts +94 -0
- package/dist/testing/scenario.js +81 -0
- package/dist/testing/transaction-boundary.d.ts +30 -0
- package/dist/testing/transaction-boundary.js +36 -0
- package/dist/testing/typed-failure.d.ts +8 -0
- package/dist/testing/typed-failure.js +15 -0
- package/dist/time/calendar-day.d.ts +83 -0
- package/dist/time/calendar-day.js +112 -0
- package/dist/time/failures.d.ts +51 -0
- package/dist/time/failures.js +56 -0
- package/dist/time/index.d.ts +11 -0
- package/dist/time/index.js +11 -0
- package/dist/time/instant.d.ts +64 -0
- package/dist/time/instant.js +67 -0
- package/dist/time/period.d.ts +45 -0
- package/dist/time/period.js +55 -0
- package/dist/vitest/container-reuse.d.ts +14 -0
- package/dist/vitest/container-reuse.js +15 -0
- package/dist/vitest/dotenv.d.ts +16 -0
- package/dist/vitest/dotenv.js +48 -0
- package/dist/vitest/effect-single-instance.d.ts +36 -0
- package/dist/vitest/effect-single-instance.js +125 -0
- package/dist/vitest/fast-check-single-instance.d.ts +11 -0
- package/dist/vitest/fast-check-single-instance.js +26 -0
- package/dist/vitest/index.d.ts +31 -0
- package/dist/vitest/index.js +30 -0
- package/dist/vitest/localstack-container.d.ts +32 -0
- package/dist/vitest/localstack-container.js +55 -0
- package/dist/vitest/port-contract-tester.d.ts +8 -0
- package/dist/vitest/port-contract-tester.js +1 -0
- package/dist/vitest/port-contracts/blob-store.contract.d.ts +22 -0
- package/dist/vitest/port-contracts/blob-store.contract.js +108 -0
- package/dist/vitest/port-contracts/document-number-counter.contract.d.ts +18 -0
- package/dist/vitest/port-contracts/document-number-counter.contract.js +90 -0
- package/dist/vitest/port-contracts/event-publisher.contract.d.ts +35 -0
- package/dist/vitest/port-contracts/event-publisher.contract.js +67 -0
- package/dist/vitest/port-contracts/external-http-client.contract.d.ts +35 -0
- package/dist/vitest/port-contracts/external-http-client.contract.js +52 -0
- package/dist/vitest/port-contracts/integration-event-outbox.contract.d.ts +23 -0
- package/dist/vitest/port-contracts/integration-event-outbox.contract.js +75 -0
- package/dist/vitest/port-contracts/processed-event-registry.contract.d.ts +18 -0
- package/dist/vitest/port-contracts/processed-event-registry.contract.js +84 -0
- package/dist/vitest/port-contracts/queue-message-producer.contract.d.ts +26 -0
- package/dist/vitest/port-contracts/queue-message-producer.contract.js +48 -0
- package/dist/vitest/port-contracts/queue-message-source.contract.d.ts +29 -0
- package/dist/vitest/port-contracts/queue-message-source.contract.js +71 -0
- package/dist/vitest/port-contracts/readiness-probe.contract.d.ts +18 -0
- package/dist/vitest/port-contracts/readiness-probe.contract.js +41 -0
- package/dist/vitest/port-contracts/transaction-boundary.contract.d.ts +21 -0
- package/dist/vitest/port-contracts/transaction-boundary.contract.js +64 -0
- package/dist/vitest/postgres-container.d.ts +57 -0
- package/dist/vitest/postgres-container.js +118 -0
- package/dist/vitest/reader-port-contract.d.ts +41 -0
- package/dist/vitest/reader-port-contract.js +43 -0
- package/dist/vitest/repo-source-files.d.ts +37 -0
- package/dist/vitest/repo-source-files.js +61 -0
- package/dist/vitest/reset-tables.d.ts +18 -0
- package/dist/vitest/reset-tables.js +30 -0
- package/dist/vitest/settings-contract.d.ts +38 -0
- package/dist/vitest/settings-contract.js +77 -0
- package/dist/vitest/stub-server.d.ts +43 -0
- package/dist/vitest/stub-server.js +60 -0
- package/dist/vitest/term-registry.json +730 -0
- package/dist/vitest/testclock-fork-isolated-files.d.ts +13 -0
- package/dist/vitest/testclock-fork-isolated-files.js +18 -0
- package/dist/vitest/testclock-fork-isolation.d.ts +77 -0
- package/dist/vitest/testclock-fork-isolation.js +188 -0
- package/dist/workflow/claim-once.d.ts +16 -0
- package/dist/workflow/claim-once.js +9 -0
- package/dist/workflow/failures.d.ts +47 -0
- package/dist/workflow/failures.js +22 -0
- package/dist/workflow/index.d.ts +12 -0
- package/dist/workflow/index.js +12 -0
- package/dist/workflow/on-claim.d.ts +17 -0
- package/dist/workflow/on-claim.js +8 -0
- package/dist/workflow/publish-pending-events.d.ts +31 -0
- package/dist/workflow/publish-pending-events.js +31 -0
- package/dist/workflow/transactionally.d.ts +8 -0
- package/dist/workflow/transactionally.js +8 -0
- package/dist/workflow/versioned-payload.d.ts +35 -0
- package/dist/workflow/versioned-payload.js +43 -0
- package/package.json +309 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync, readdirSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { repoRoot } from './repo-source-files.js';
|
|
7
|
+
const isSkippedEntry = (name) => name.startsWith('.');
|
|
8
|
+
/**
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export const walkForEffectCopies = (nodeModulesDirectory, visited, found) => {
|
|
12
|
+
let entries;
|
|
13
|
+
try {
|
|
14
|
+
entries = readdirSync(nodeModulesDirectory);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
for (const name of entries) {
|
|
20
|
+
if (isSkippedEntry(name))
|
|
21
|
+
continue;
|
|
22
|
+
const entryPath = join(nodeModulesDirectory, name);
|
|
23
|
+
let realEntryPath;
|
|
24
|
+
try {
|
|
25
|
+
realEntryPath = realpathSync(entryPath);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (visited.has(realEntryPath))
|
|
31
|
+
continue;
|
|
32
|
+
visited.add(realEntryPath);
|
|
33
|
+
if (name.startsWith('@')) {
|
|
34
|
+
walkForEffectCopies(entryPath, visited, found);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (name === 'effect' && existsSync(join(entryPath, 'package.json'))) {
|
|
38
|
+
found.push({ packageDirectory: realEntryPath });
|
|
39
|
+
}
|
|
40
|
+
const nested = join(entryPath, 'node_modules');
|
|
41
|
+
if (existsSync(nested))
|
|
42
|
+
walkForEffectCopies(nested, visited, found);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const resolveEffectEntryFrom = (directory) => {
|
|
46
|
+
const anchor = join(directory, 'package.json');
|
|
47
|
+
try {
|
|
48
|
+
return realpathSync(createRequire(anchor).resolve('effect'));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const WHY_ONE_COPY_MATTERS = "why one copy matters: effect's Context.Tag identity and TestClock " +
|
|
55
|
+
'depend on true module-singleton state. Two physical copies break both ' +
|
|
56
|
+
'silently — a service registered against one copy is invisible to code ' +
|
|
57
|
+
'running against the other, and a TestClock the test believes it is ' +
|
|
58
|
+
'driving never touches the runtime under test.';
|
|
59
|
+
const WHAT_TO_DO = 'what to do: dedupe node_modules (reinstall, or run your package ' +
|
|
60
|
+
"manager's dedupe command) so only one effect copy remains, or align " +
|
|
61
|
+
"the offending path's effect version with this package's peer range " +
|
|
62
|
+
'(^3.22.1) so the package manager collapses it back to one copy.';
|
|
63
|
+
const tooManyCopiesMessage = (directories) => [
|
|
64
|
+
`what happened: found ${directories.length} physical copies of the ` +
|
|
65
|
+
`effect package under node_modules:\n${directories.map((d) => ` - ${d}`).join('\n')}`,
|
|
66
|
+
WHY_ONE_COPY_MATTERS,
|
|
67
|
+
WHAT_TO_DO,
|
|
68
|
+
].join('\n');
|
|
69
|
+
const noCopyAtAllMessage = (root) => [
|
|
70
|
+
'what happened: the walk over ' +
|
|
71
|
+
`${root}/node_modules found no copy of the effect package at all.`,
|
|
72
|
+
'why that is a finding, not a pass: this suite proves there is exactly ' +
|
|
73
|
+
'one copy. Zero copies means it walked the wrong tree — an unbuilt ' +
|
|
74
|
+
'install, a moved root, a workspace whose packages live elsewhere — ' +
|
|
75
|
+
'and a comparison over nothing passes every time.',
|
|
76
|
+
'what to do: install dependencies at that root, or pass the root that ' +
|
|
77
|
+
"really holds node_modules through the guard's `root` option.",
|
|
78
|
+
].join('\n');
|
|
79
|
+
const resolveMismatchMessage = (fromConsumerRoot, fromPackageDirectory) => [
|
|
80
|
+
"what happened: require.resolve('effect') resolves to a different " +
|
|
81
|
+
`file depending on where it is called from — from the consumer ` +
|
|
82
|
+
`root: ${fromConsumerRoot ?? '(could not resolve)'}; from the ` +
|
|
83
|
+
`package's own directory: ${fromPackageDirectory ?? '(could not resolve)'}.`,
|
|
84
|
+
WHY_ONE_COPY_MATTERS,
|
|
85
|
+
WHAT_TO_DO,
|
|
86
|
+
].join('\n');
|
|
87
|
+
/**
|
|
88
|
+
* Registers a vitest suite that fails when a tree holds more than one copy of
|
|
89
|
+
* Effect.
|
|
90
|
+
*
|
|
91
|
+
* Registers a vitest suite that fails if more than one physical copy of the
|
|
92
|
+
* `effect` package is reachable from a consumer's `node_modules` tree, or if
|
|
93
|
+
* `require.resolve('effect')` disagrees between the consumer root and this
|
|
94
|
+
* package's own installed directory. Call it from a consumer's `*.test.ts`
|
|
95
|
+
* file — effect is a peer dependency precisely so the consumer keeps a single
|
|
96
|
+
* instance, and this is the guard that makes that invariant loud instead of
|
|
97
|
+
* silent (see `dec-20260902-8898ccfd`). It walks `node_modules` rather than
|
|
98
|
+
* the repository, so it takes no floor option; instead it refuses on zero
|
|
99
|
+
* copies found and on a resolution that answers nothing, because a comparison
|
|
100
|
+
* over an empty walk passes every time.
|
|
101
|
+
*/
|
|
102
|
+
export const oneEffectInstanceGuard = (options = {}) => {
|
|
103
|
+
const root = options.root ?? repoRoot();
|
|
104
|
+
const packageOwnDirectory = fileURLToPath(new URL('.', import.meta.url));
|
|
105
|
+
describe('machinery: effect lives in a single instance', () => {
|
|
106
|
+
it('node_modules holds exactly one physical copy of effect', () => {
|
|
107
|
+
const found = [];
|
|
108
|
+
walkForEffectCopies(join(root, 'node_modules'), new Set(), found);
|
|
109
|
+
const distinctDirectories = [
|
|
110
|
+
...new Set(found.map((copy) => copy.packageDirectory)),
|
|
111
|
+
].sort();
|
|
112
|
+
const violation = distinctDirectories.length > 1 ? distinctDirectories : [];
|
|
113
|
+
console.log(`effect single instance: found ${distinctDirectories.length} copy ` +
|
|
114
|
+
`or copies under ${root}/node_modules`);
|
|
115
|
+
expect(distinctDirectories.length, noCopyAtAllMessage(root)).toBeGreaterThanOrEqual(1);
|
|
116
|
+
expect(violation, tooManyCopiesMessage(distinctDirectories)).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
it("require.resolve('effect') agrees from the consumer root and the package's own directory", () => {
|
|
119
|
+
const fromConsumerRoot = resolveEffectEntryFrom(root);
|
|
120
|
+
const fromPackageDirectory = resolveEffectEntryFrom(packageOwnDirectory);
|
|
121
|
+
expect(fromConsumerRoot, resolveMismatchMessage(fromConsumerRoot, fromPackageDirectory)).toBeDefined();
|
|
122
|
+
expect(fromConsumerRoot, resolveMismatchMessage(fromConsumerRoot, fromPackageDirectory)).toBe(fromPackageDirectory);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers a vitest suite that fails when the property library resolves to
|
|
3
|
+
* two module instances.
|
|
4
|
+
*
|
|
5
|
+
* Registers a vitest suite that fails if the direct `fast-check` import and
|
|
6
|
+
* `effect/FastCheck` resolve to two separate module instances — a split that
|
|
7
|
+
* silently breaks arbitraries built from one being usable in a property built
|
|
8
|
+
* from the other. Call it from a consumer's `*.test.ts` file that depends on
|
|
9
|
+
* both `effect` and `fast-check`.
|
|
10
|
+
*/
|
|
11
|
+
export declare const oneFastCheckInstanceGuard: () => void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as viaEffect from 'effect/FastCheck';
|
|
2
|
+
import * as direct from 'fast-check';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
/**
|
|
5
|
+
* Registers a vitest suite that fails when the property library resolves to
|
|
6
|
+
* two module instances.
|
|
7
|
+
*
|
|
8
|
+
* Registers a vitest suite that fails if the direct `fast-check` import and
|
|
9
|
+
* `effect/FastCheck` resolve to two separate module instances — a split that
|
|
10
|
+
* silently breaks arbitraries built from one being usable in a property built
|
|
11
|
+
* from the other. Call it from a consumer's `*.test.ts` file that depends on
|
|
12
|
+
* both `effect` and `fast-check`.
|
|
13
|
+
*/
|
|
14
|
+
export const oneFastCheckInstanceGuard = () => {
|
|
15
|
+
describe('machinery: fast-check lives in a single instance', () => {
|
|
16
|
+
it('the direct import and effect/FastCheck are the same module', () => {
|
|
17
|
+
expect(direct.integer).toBe(viaEffect.integer);
|
|
18
|
+
expect(direct.bigInt).toBe(viaEffect.bigInt);
|
|
19
|
+
expect(direct.property).toBe(viaEffect.property);
|
|
20
|
+
});
|
|
21
|
+
it('an arbitrary from the direct import works in an effect property', () => {
|
|
22
|
+
const outcome = viaEffect.check(viaEffect.property(direct.bigInt({ min: -8n, max: 8n }), (value) => typeof value === 'bigint'), { numRuns: 25 });
|
|
23
|
+
expect(outcome.failed).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry to the vitest mechanism: live stands, port contracts, and
|
|
3
|
+
* the guards that read the test runtime. It gives you fixtures tied to the
|
|
4
|
+
* test runner — suites on live stands (Postgres and the localstack AWS
|
|
5
|
+
* stand, both in containers), port contracts, the settings contract, a stub
|
|
6
|
+
* HTTP server, and the three guards that hold the runtime this package
|
|
7
|
+
* loads. The guards that hold a tree to its written canon ship separately,
|
|
8
|
+
* as `@satorio/canon`.
|
|
9
|
+
*/
|
|
10
|
+
export { oneEffectInstanceGuard } from './effect-single-instance.js';
|
|
11
|
+
export { oneFastCheckInstanceGuard } from './fast-check-single-instance.js';
|
|
12
|
+
export { type AwsStand, LOCALSTACK_REGION, onLocalstack, } from './localstack-container.js';
|
|
13
|
+
export type { PortContractTester } from './port-contract-tester.js';
|
|
14
|
+
export { blobStoreContract, storedBlob, } from './port-contracts/blob-store.contract.js';
|
|
15
|
+
export { documentNumberCounterContract } from './port-contracts/document-number-counter.contract.js';
|
|
16
|
+
export { eventPublisherContract, outboxEvent, } from './port-contracts/event-publisher.contract.js';
|
|
17
|
+
export { externalHttpClientContract } from './port-contracts/external-http-client.contract.js';
|
|
18
|
+
export { integrationEventOutboxContract, outboxMessage, } from './port-contracts/integration-event-outbox.contract.js';
|
|
19
|
+
export { processedEventRegistryContract } from './port-contracts/processed-event-registry.contract.js';
|
|
20
|
+
export { queueMessageProducerContract } from './port-contracts/queue-message-producer.contract.js';
|
|
21
|
+
export { queueMessageSourceContract } from './port-contracts/queue-message-source.contract.js';
|
|
22
|
+
export { readinessProbeContract } from './port-contracts/readiness-probe.contract.js';
|
|
23
|
+
export { transactionBoundaryContract } from './port-contracts/transaction-boundary.contract.js';
|
|
24
|
+
export { onPostgres, onPostgresContainer, POSTGRES_IMAGE, PostgresPortsUnderTestLive, postgresInContainerLive, postgresUnderTestLive, } from './postgres-container.js';
|
|
25
|
+
export { type ReaderPortUnderTest, readerPortContract, } from './reader-port-contract.js';
|
|
26
|
+
export { repoCommittableFiles, repoFilesUnder, repoRoot, } from './repo-source-files.js';
|
|
27
|
+
export { truncateTables } from './reset-tables.js';
|
|
28
|
+
export { settingsContract } from './settings-contract.js';
|
|
29
|
+
export { type RecordedStubRequest, type StubResponse, type StubRoute, scopedStubServer, } from './stub-server.js';
|
|
30
|
+
export { FILES_THAT_FORK_A_FIBER_AND_ADJUST_TEST_CLOCK } from './testclock-fork-isolated-files.js';
|
|
31
|
+
export { testclockForkIsolationGuard } from './testclock-fork-isolation.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry to the vitest mechanism: live stands, port contracts, and
|
|
3
|
+
* the guards that read the test runtime. It gives you fixtures tied to the
|
|
4
|
+
* test runner — suites on live stands (Postgres and the localstack AWS
|
|
5
|
+
* stand, both in containers), port contracts, the settings contract, a stub
|
|
6
|
+
* HTTP server, and the three guards that hold the runtime this package
|
|
7
|
+
* loads. The guards that hold a tree to its written canon ship separately,
|
|
8
|
+
* as `@satorio/canon`.
|
|
9
|
+
*/
|
|
10
|
+
export { oneEffectInstanceGuard } from './effect-single-instance.js';
|
|
11
|
+
export { oneFastCheckInstanceGuard } from './fast-check-single-instance.js';
|
|
12
|
+
export { LOCALSTACK_REGION, onLocalstack, } from './localstack-container.js';
|
|
13
|
+
export { blobStoreContract, storedBlob, } from './port-contracts/blob-store.contract.js';
|
|
14
|
+
export { documentNumberCounterContract } from './port-contracts/document-number-counter.contract.js';
|
|
15
|
+
export { eventPublisherContract, outboxEvent, } from './port-contracts/event-publisher.contract.js';
|
|
16
|
+
export { externalHttpClientContract } from './port-contracts/external-http-client.contract.js';
|
|
17
|
+
export { integrationEventOutboxContract, outboxMessage, } from './port-contracts/integration-event-outbox.contract.js';
|
|
18
|
+
export { processedEventRegistryContract } from './port-contracts/processed-event-registry.contract.js';
|
|
19
|
+
export { queueMessageProducerContract } from './port-contracts/queue-message-producer.contract.js';
|
|
20
|
+
export { queueMessageSourceContract } from './port-contracts/queue-message-source.contract.js';
|
|
21
|
+
export { readinessProbeContract } from './port-contracts/readiness-probe.contract.js';
|
|
22
|
+
export { transactionBoundaryContract } from './port-contracts/transaction-boundary.contract.js';
|
|
23
|
+
export { onPostgres, onPostgresContainer, POSTGRES_IMAGE, PostgresPortsUnderTestLive, postgresInContainerLive, postgresUnderTestLive, } from './postgres-container.js';
|
|
24
|
+
export { readerPortContract, } from './reader-port-contract.js';
|
|
25
|
+
export { repoCommittableFiles, repoFilesUnder, repoRoot, } from './repo-source-files.js';
|
|
26
|
+
export { truncateTables } from './reset-tables.js';
|
|
27
|
+
export { settingsContract } from './settings-contract.js';
|
|
28
|
+
export { scopedStubServer, } from './stub-server.js';
|
|
29
|
+
export { FILES_THAT_FORK_A_FIBER_AND_ADJUST_TEST_CLOCK } from './testclock-fork-isolated-files.js';
|
|
30
|
+
export { testclockForkIsolationGuard } from './testclock-fork-isolation.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Vitest } from '@effect/vitest';
|
|
2
|
+
/**
|
|
3
|
+
* The one region every client and resource in a suite agrees on. The
|
|
4
|
+
* value itself is arbitrary; what matters is that nothing in the suite
|
|
5
|
+
* reaches for a second one.
|
|
6
|
+
*/
|
|
7
|
+
export declare const LOCALSTACK_REGION = "eu-central-1";
|
|
8
|
+
type SuiteChecks = (it: Vitest.MethodsNonLive<never, true>) => void;
|
|
9
|
+
/**
|
|
10
|
+
* The address and the region of the live AWS stand a suite talks to.
|
|
11
|
+
*/
|
|
12
|
+
export interface AwsStand {
|
|
13
|
+
readonly endpoint: string;
|
|
14
|
+
readonly region: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `prepare` sets up the suite's own AWS resources (bucket, topic, queue) and
|
|
18
|
+
* returns the environment variables the machinery needs to find them. If
|
|
19
|
+
* setup fails, the suite fails too.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export interface LocalstackSuiteOptions {
|
|
24
|
+
readonly prepare: (stand: AwsStand) => Promise<ReadonlyMap<string, string>>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Runs a suite against a real AWS stand (localstack) for `subject`. Pass
|
|
28
|
+
* `checks` to declare the tests, and `options.prepare` to seed AWS resources
|
|
29
|
+
* before they run.
|
|
30
|
+
*/
|
|
31
|
+
export declare const onLocalstack: (options: LocalstackSuiteOptions, subject: string, checks: SuiteChecks) => void;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { layer } from '@effect/vitest';
|
|
2
|
+
import { LocalstackContainer } from '@testcontainers/localstack';
|
|
3
|
+
import { Config, ConfigProvider, Duration, Effect, Layer, Option, } from 'effect';
|
|
4
|
+
import { standsMayBeReused } from './container-reuse.js';
|
|
5
|
+
const LOCALSTACK_IMAGE = 'localstack/localstack:4.9';
|
|
6
|
+
/**
|
|
7
|
+
* The one region every client and resource in a suite agrees on. The
|
|
8
|
+
* value itself is arbitrary; what matters is that nothing in the suite
|
|
9
|
+
* reaches for a second one.
|
|
10
|
+
*/
|
|
11
|
+
export const LOCALSTACK_REGION = 'eu-central-1';
|
|
12
|
+
const SUITE_STARTUP_BUDGET = Duration.minutes(5);
|
|
13
|
+
// @effect-diagnostics effect/processEnvInEffect:off
|
|
14
|
+
const ensureSdkCredentials = Effect.sync(() => {
|
|
15
|
+
delete process.env.AWS_PROFILE;
|
|
16
|
+
process.env.AWS_ACCESS_KEY_ID ??= 'test';
|
|
17
|
+
process.env.AWS_SECRET_ACCESS_KEY ??= 'test';
|
|
18
|
+
});
|
|
19
|
+
// @effect-diagnostics effect/processEnvInEffect:error
|
|
20
|
+
const REUSING_STANDS = standsMayBeReused();
|
|
21
|
+
const startedContainer = Effect.acquireRelease(Effect.promise(() => {
|
|
22
|
+
const container = new LocalstackContainer(LOCALSTACK_IMAGE);
|
|
23
|
+
return (REUSING_STANDS ? container.withReuse() : container).start();
|
|
24
|
+
}), (container) => REUSING_STANDS ? Effect.void : Effect.promise(() => container.stop()));
|
|
25
|
+
const containerStand = Effect.map(startedContainer, (container) => ({
|
|
26
|
+
endpoint: container.getConnectionUri(),
|
|
27
|
+
region: LOCALSTACK_REGION,
|
|
28
|
+
}));
|
|
29
|
+
const externallyConfiguredStand = Config.all({
|
|
30
|
+
endpoint: Config.string('AWS_ENDPOINT_URL'),
|
|
31
|
+
region: Config.string('AWS_REGION').pipe(Config.withDefault(LOCALSTACK_REGION)),
|
|
32
|
+
});
|
|
33
|
+
const standUnderTest = Effect.flatMap(Config.option(externallyConfiguredStand), Option.match({
|
|
34
|
+
onSome: (Effect.succeed),
|
|
35
|
+
onNone: () => containerStand,
|
|
36
|
+
}));
|
|
37
|
+
const configuredStand = (options) => Layer.unwrapScoped(Effect.gen(function* () {
|
|
38
|
+
yield* ensureSdkCredentials;
|
|
39
|
+
const stand = yield* standUnderTest;
|
|
40
|
+
const prepared = yield* Effect.promise(() => options.prepare(stand));
|
|
41
|
+
return Layer.setConfigProvider(ConfigProvider.fromMap(new Map([
|
|
42
|
+
['AWS_ENDPOINT_URL', stand.endpoint],
|
|
43
|
+
['AWS_REGION', stand.region],
|
|
44
|
+
...prepared,
|
|
45
|
+
])));
|
|
46
|
+
})).pipe(Layer.orDie);
|
|
47
|
+
/**
|
|
48
|
+
* Runs a suite against a real AWS stand (localstack) for `subject`. Pass
|
|
49
|
+
* `checks` to declare the tests, and `options.prepare` to seed AWS resources
|
|
50
|
+
* before they run.
|
|
51
|
+
*/
|
|
52
|
+
export const onLocalstack = (options, subject, checks) => layer(configuredStand(options), {
|
|
53
|
+
excludeTestServices: true,
|
|
54
|
+
timeout: SUITE_STARTUP_BUDGET,
|
|
55
|
+
})(subject, checks);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Vitest } from '@effect/vitest';
|
|
2
|
+
/**
|
|
3
|
+
* The subset of vitest test methods a port contract needs from its tester.
|
|
4
|
+
* Pass the tester that matches the runtime under test — plain `it` for
|
|
5
|
+
* in-memory implementations, or an `it.layer` suite's `it` for a live
|
|
6
|
+
* Postgres.
|
|
7
|
+
*/
|
|
8
|
+
export type PortContractTester = Pick<Vitest.MethodsNonLive<never, true>, 'effect'>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { BlobStore, type StoredBlob } from '@satorio/machinery/ports';
|
|
2
|
+
import { type Layer } from 'effect';
|
|
3
|
+
import type { PortContractTester } from '../port-contract-tester.js';
|
|
4
|
+
/**
|
|
5
|
+
* One blob a contract stores and reads back, so every implementation is asked
|
|
6
|
+
* the same question.
|
|
7
|
+
*/
|
|
8
|
+
export declare const storedBlob: (text: string) => StoredBlob;
|
|
9
|
+
/**
|
|
10
|
+
* One contract, every blob store. The same body runs against an
|
|
11
|
+
* in-memory store and a live S3 store, so a difference between them shows
|
|
12
|
+
* up as a red test, not a surprise in production. Hand it the port's
|
|
13
|
+
* unavailable stand-in as a fourth argument and it walks the refusal too:
|
|
14
|
+
* every operation fails, and the failure reaches the caller.
|
|
15
|
+
*
|
|
16
|
+
* What this contract cannot prove: that a real S3 answers an absent key
|
|
17
|
+
* the way the in-memory store does. Both sides of that promise are held
|
|
18
|
+
* here, and only the live run in
|
|
19
|
+
* `src/storage/ports.localstack.test.ts` proves the S3 half of
|
|
20
|
+
* it — the fake decides its own answer, the bucket does not.
|
|
21
|
+
*/
|
|
22
|
+
export declare const blobStoreContract: (it: PortContractTester, implementationName: string, layer: Layer.Layer<BlobStore>, unavailable?: Layer.Layer<BlobStore>) => void;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect } from '@effect/vitest';
|
|
2
|
+
import { BlobStore } from '@satorio/machinery/ports';
|
|
3
|
+
import { Effect, Option } from 'effect';
|
|
4
|
+
const encoder = new TextEncoder();
|
|
5
|
+
/**
|
|
6
|
+
* One blob a contract stores and reads back, so every implementation is asked
|
|
7
|
+
* the same question.
|
|
8
|
+
*/
|
|
9
|
+
export const storedBlob = (text) => ({
|
|
10
|
+
body: encoder.encode(text),
|
|
11
|
+
contentType: 'text/plain',
|
|
12
|
+
metadata: { origin: 'contract' },
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* One contract, every blob store. The same body runs against an
|
|
16
|
+
* in-memory store and a live S3 store, so a difference between them shows
|
|
17
|
+
* up as a red test, not a surprise in production. Hand it the port's
|
|
18
|
+
* unavailable stand-in as a fourth argument and it walks the refusal too:
|
|
19
|
+
* every operation fails, and the failure reaches the caller.
|
|
20
|
+
*
|
|
21
|
+
* What this contract cannot prove: that a real S3 answers an absent key
|
|
22
|
+
* the way the in-memory store does. Both sides of that promise are held
|
|
23
|
+
* here, and only the live run in
|
|
24
|
+
* `src/storage/ports.localstack.test.ts` proves the S3 half of
|
|
25
|
+
* it — the fake decides its own answer, the bucket does not.
|
|
26
|
+
*/
|
|
27
|
+
export const blobStoreContract = (it, implementationName, layer, unavailable) => {
|
|
28
|
+
const run = (effect) => Effect.provide(effect, layer);
|
|
29
|
+
describe(`BlobStore contract (${implementationName})`, () => {
|
|
30
|
+
it.effect('what is put is read back the same: bytes, content type, metadata', () => run(Effect.gen(function* () {
|
|
31
|
+
const store = yield* BlobStore;
|
|
32
|
+
const blob = {
|
|
33
|
+
body: encoder.encode('hello, store'),
|
|
34
|
+
contentType: 'text/plain',
|
|
35
|
+
metadata: { origin: 'contract', kind: 'greeting' },
|
|
36
|
+
};
|
|
37
|
+
yield* store.put('blob-1', blob);
|
|
38
|
+
const fetched = yield* store.get('blob-1');
|
|
39
|
+
expect(Option.isSome(fetched)).toBe(true);
|
|
40
|
+
if (Option.isSome(fetched)) {
|
|
41
|
+
expect(fetched.value.body).toEqual(blob.body);
|
|
42
|
+
expect(fetched.value.contentType).toBe('text/plain');
|
|
43
|
+
expect(fetched.value.metadata).toEqual(blob.metadata);
|
|
44
|
+
}
|
|
45
|
+
})));
|
|
46
|
+
it.effect('an unknown key is Option.none, not a failure', () => run(Effect.gen(function* () {
|
|
47
|
+
const store = yield* BlobStore;
|
|
48
|
+
const fetched = yield* store.get('blob-never-put');
|
|
49
|
+
expect(Option.isNone(fetched)).toBe(true);
|
|
50
|
+
})));
|
|
51
|
+
it.effect('exists reports false before put and true after', () => run(Effect.gen(function* () {
|
|
52
|
+
const store = yield* BlobStore;
|
|
53
|
+
const before = yield* store.exists('blob-2');
|
|
54
|
+
yield* store.put('blob-2', storedBlob('appeared'));
|
|
55
|
+
const after = yield* store.exists('blob-2');
|
|
56
|
+
expect(before).toBe(false);
|
|
57
|
+
expect(after).toBe(true);
|
|
58
|
+
})));
|
|
59
|
+
it.effect('putting the same key again replaces the object', () => run(Effect.gen(function* () {
|
|
60
|
+
const store = yield* BlobStore;
|
|
61
|
+
yield* store.put('blob-3', storedBlob('first version'));
|
|
62
|
+
yield* store.put('blob-3', storedBlob('second version'));
|
|
63
|
+
const fetched = yield* store.get('blob-3');
|
|
64
|
+
expect(Option.isSome(fetched)).toBe(true);
|
|
65
|
+
if (Option.isSome(fetched)) {
|
|
66
|
+
expect(new TextDecoder().decode(fetched.value.body)).toBe('second version');
|
|
67
|
+
}
|
|
68
|
+
})));
|
|
69
|
+
it.effect('after delete, the key is gone', () => run(Effect.gen(function* () {
|
|
70
|
+
const store = yield* BlobStore;
|
|
71
|
+
yield* store.put('blob-4', storedBlob('temporary'));
|
|
72
|
+
yield* store.delete('blob-4');
|
|
73
|
+
const fetched = yield* store.get('blob-4');
|
|
74
|
+
const present = yield* store.exists('blob-4');
|
|
75
|
+
expect(Option.isNone(fetched)).toBe(true);
|
|
76
|
+
expect(present).toBe(false);
|
|
77
|
+
})));
|
|
78
|
+
it.effect('deleting an absent key does not fail', () => run(Effect.gen(function* () {
|
|
79
|
+
const store = yield* BlobStore;
|
|
80
|
+
yield* store.delete('blob-never-put');
|
|
81
|
+
})));
|
|
82
|
+
});
|
|
83
|
+
if (unavailable === undefined)
|
|
84
|
+
return;
|
|
85
|
+
describe(`BlobStore contract (${implementationName}, not mounted)`, () => {
|
|
86
|
+
it.effect('every operation fails, and the caller reads the refusal', () => Effect.gen(function* () {
|
|
87
|
+
const store = yield* BlobStore;
|
|
88
|
+
const refusals = yield* Effect.all([
|
|
89
|
+
Effect.flip(store.put('blob-5', storedBlob('never stored'))),
|
|
90
|
+
Effect.flip(store.get('blob-5')),
|
|
91
|
+
Effect.flip(store.exists('blob-5')),
|
|
92
|
+
Effect.flip(store.delete('blob-5')),
|
|
93
|
+
]);
|
|
94
|
+
expect(refusals.map((refusal) => refusal._tag)).toEqual([
|
|
95
|
+
'BlobStoreUnavailableError',
|
|
96
|
+
'BlobStoreUnavailableError',
|
|
97
|
+
'BlobStoreUnavailableError',
|
|
98
|
+
'BlobStoreUnavailableError',
|
|
99
|
+
]);
|
|
100
|
+
expect(refusals.map((refusal) => refusal.operation)).toEqual([
|
|
101
|
+
'put',
|
|
102
|
+
'get',
|
|
103
|
+
'exists',
|
|
104
|
+
'delete',
|
|
105
|
+
]);
|
|
106
|
+
}).pipe(Effect.provide(unavailable)));
|
|
107
|
+
});
|
|
108
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DocumentNumberCounter } from '@satorio/machinery/ports';
|
|
2
|
+
import { type Layer } from 'effect';
|
|
3
|
+
import type { PortContractTester } from '../port-contract-tester.js';
|
|
4
|
+
/**
|
|
5
|
+
* One contract, every counter implementation. The same body runs against
|
|
6
|
+
* an in-memory counter and a live Postgres counter, so a difference
|
|
7
|
+
* between them shows up as a red test rather than as two documents
|
|
8
|
+
* carrying one number. Hand it the port's unavailable stand-in as a
|
|
9
|
+
* fourth argument and it walks the refusal too.
|
|
10
|
+
*
|
|
11
|
+
* What this contract cannot prove: that two callers asking for the same
|
|
12
|
+
* client and month at the same instant get two different counts. The
|
|
13
|
+
* in-memory counter runs one fiber at a time over a `Ref`, so it cannot
|
|
14
|
+
* lose that race; only the live run in
|
|
15
|
+
* `src/postgres/ports.postgres.test.ts` puts a real row lock behind the
|
|
16
|
+
* count.
|
|
17
|
+
*/
|
|
18
|
+
export declare const documentNumberCounterContract: (it: PortContractTester, implementationName: string, layer: Layer.Layer<DocumentNumberCounter>, unavailable?: Layer.Layer<DocumentNumberCounter>) => void;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect } from '@effect/vitest';
|
|
2
|
+
import { DocumentNumberCounter } from '@satorio/machinery/ports';
|
|
3
|
+
import { Effect } from 'effect';
|
|
4
|
+
const A_CLIENT = '41738297';
|
|
5
|
+
const ANOTHER_CLIENT = '55511122';
|
|
6
|
+
const SEPTEMBER = '2609';
|
|
7
|
+
const OCTOBER = '2610';
|
|
8
|
+
/**
|
|
9
|
+
* One contract, every counter implementation. The same body runs against
|
|
10
|
+
* an in-memory counter and a live Postgres counter, so a difference
|
|
11
|
+
* between them shows up as a red test rather than as two documents
|
|
12
|
+
* carrying one number. Hand it the port's unavailable stand-in as a
|
|
13
|
+
* fourth argument and it walks the refusal too.
|
|
14
|
+
*
|
|
15
|
+
* What this contract cannot prove: that two callers asking for the same
|
|
16
|
+
* client and month at the same instant get two different counts. The
|
|
17
|
+
* in-memory counter runs one fiber at a time over a `Ref`, so it cannot
|
|
18
|
+
* lose that race; only the live run in
|
|
19
|
+
* `src/postgres/ports.postgres.test.ts` puts a real row lock behind the
|
|
20
|
+
* count.
|
|
21
|
+
*/
|
|
22
|
+
export const documentNumberCounterContract = (it, implementationName, layer, unavailable) => {
|
|
23
|
+
const run = (effect) => Effect.provide(effect, layer);
|
|
24
|
+
describe(`DocumentNumberCounter contract (${implementationName})`, () => {
|
|
25
|
+
it.effect('the first document of a month is the first one', () => run(Effect.gen(function* () {
|
|
26
|
+
const counter = yield* DocumentNumberCounter;
|
|
27
|
+
const count = yield* counter.nextCount({
|
|
28
|
+
clientCode: A_CLIENT,
|
|
29
|
+
reportingMonth: SEPTEMBER,
|
|
30
|
+
});
|
|
31
|
+
expect(count).toBe(1);
|
|
32
|
+
})));
|
|
33
|
+
it.effect('the next document of that month steps past it', () => run(Effect.gen(function* () {
|
|
34
|
+
const counter = yield* DocumentNumberCounter;
|
|
35
|
+
const input = {
|
|
36
|
+
clientCode: ANOTHER_CLIENT,
|
|
37
|
+
reportingMonth: SEPTEMBER,
|
|
38
|
+
};
|
|
39
|
+
const first = yield* counter.nextCount(input);
|
|
40
|
+
const second = yield* counter.nextCount(input);
|
|
41
|
+
const third = yield* counter.nextCount(input);
|
|
42
|
+
expect([first, second, third]).toEqual([1, 2, 3]);
|
|
43
|
+
})));
|
|
44
|
+
it.effect('a new month starts its own count', () => run(Effect.gen(function* () {
|
|
45
|
+
const counter = yield* DocumentNumberCounter;
|
|
46
|
+
yield* counter.nextCount({
|
|
47
|
+
clientCode: A_CLIENT,
|
|
48
|
+
reportingMonth: OCTOBER,
|
|
49
|
+
});
|
|
50
|
+
yield* counter.nextCount({
|
|
51
|
+
clientCode: A_CLIENT,
|
|
52
|
+
reportingMonth: OCTOBER,
|
|
53
|
+
});
|
|
54
|
+
const november = yield* counter.nextCount({
|
|
55
|
+
clientCode: A_CLIENT,
|
|
56
|
+
reportingMonth: '2611',
|
|
57
|
+
});
|
|
58
|
+
expect(november).toBe(1);
|
|
59
|
+
})));
|
|
60
|
+
it.effect('one client never moves another client count', () => run(Effect.gen(function* () {
|
|
61
|
+
const counter = yield* DocumentNumberCounter;
|
|
62
|
+
yield* counter.nextCount({
|
|
63
|
+
clientCode: A_CLIENT,
|
|
64
|
+
reportingMonth: '2612',
|
|
65
|
+
});
|
|
66
|
+
yield* counter.nextCount({
|
|
67
|
+
clientCode: A_CLIENT,
|
|
68
|
+
reportingMonth: '2612',
|
|
69
|
+
});
|
|
70
|
+
const other = yield* counter.nextCount({
|
|
71
|
+
clientCode: ANOTHER_CLIENT,
|
|
72
|
+
reportingMonth: '2612',
|
|
73
|
+
});
|
|
74
|
+
expect(other).toBe(1);
|
|
75
|
+
})));
|
|
76
|
+
});
|
|
77
|
+
if (unavailable === undefined)
|
|
78
|
+
return;
|
|
79
|
+
describe(`DocumentNumberCounter contract (${implementationName}, not mounted)`, () => {
|
|
80
|
+
it.effect('the count fails, and the caller reads the refusal', () => Effect.gen(function* () {
|
|
81
|
+
const counter = yield* DocumentNumberCounter;
|
|
82
|
+
const refusal = yield* Effect.flip(counter.nextCount({
|
|
83
|
+
clientCode: A_CLIENT,
|
|
84
|
+
reportingMonth: SEPTEMBER,
|
|
85
|
+
}));
|
|
86
|
+
expect(refusal._tag).toBe('DocumentNumberCounterUnavailableError');
|
|
87
|
+
expect(refusal.operation).toBe('nextCount');
|
|
88
|
+
}).pipe(Effect.provide(unavailable)));
|
|
89
|
+
});
|
|
90
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { EventPublisher, type OutboxMessage } from '@satorio/machinery/ports';
|
|
2
|
+
import { Effect, type Layer } from 'effect';
|
|
3
|
+
import type { PortContractTester } from '../port-contract-tester.js';
|
|
4
|
+
/**
|
|
5
|
+
* One message, built the way an outbox hands it over.
|
|
6
|
+
*/
|
|
7
|
+
export declare const outboxEvent: (messageId: string, payload: {
|
|
8
|
+
readonly [key: string]: unknown;
|
|
9
|
+
}) => OutboxMessage;
|
|
10
|
+
/**
|
|
11
|
+
* One publisher, described the way a contract can hold it: the layer
|
|
12
|
+
* that mounts the port, and the one call that reads back the message ids
|
|
13
|
+
* that reached whatever stands behind it.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export interface PublisherUnderTest {
|
|
18
|
+
readonly layer: Layer.Layer<EventPublisher>;
|
|
19
|
+
readonly delivered: Effect.Effect<readonly string[]>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One contract, every event publisher. The same body runs against an
|
|
23
|
+
* in-memory publisher and a live SNS topic, so a difference between them
|
|
24
|
+
* shows up as a red test rather than as a surprise in production. Hand
|
|
25
|
+
* it the port's unavailable stand-in as a fourth argument and it walks
|
|
26
|
+
* the refusal too.
|
|
27
|
+
*
|
|
28
|
+
* What this contract cannot prove: that a subscriber reads the envelope
|
|
29
|
+
* the publisher wrote. The shape on the wire is each implementation's
|
|
30
|
+
* own — `stdoutLineOf` carries a channel, `snsMessageBodyOf` does not —
|
|
31
|
+
* so the live test in `src/publishing/ports.localstack.test.ts` owns
|
|
32
|
+
* that branch, reading the envelope back off a queue subscribed to the
|
|
33
|
+
* topic.
|
|
34
|
+
*/
|
|
35
|
+
export declare const eventPublisherContract: (it: PortContractTester, implementationName: string, publisher: PublisherUnderTest, unavailable?: Layer.Layer<EventPublisher>) => void;
|