@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,45 @@
|
|
|
1
|
+
import { BlobStore, BlobStoreUnavailableError, } from '@satorio/machinery/ports';
|
|
2
|
+
import { Effect, Layer, Option, Ref } from 'effect';
|
|
3
|
+
const NOT_MOUNTED = 'object store unavailable (test)';
|
|
4
|
+
/**
|
|
5
|
+
* An in-memory blob store over a `Ref` you keep, so a test can seed it
|
|
6
|
+
* before the run and read it after.
|
|
7
|
+
*/
|
|
8
|
+
export const makeInMemoryBlobStoreWith = (blobs) => ({
|
|
9
|
+
put: (key, blob) => Ref.update(blobs, (current) => new Map(current).set(key, blob)),
|
|
10
|
+
get: (key) => Ref.get(blobs).pipe(Effect.map((current) => Option.fromNullable(current.get(key)))),
|
|
11
|
+
exists: (key) => Ref.get(blobs).pipe(Effect.map((current) => current.has(key))),
|
|
12
|
+
delete: (key) => Ref.update(blobs, (current) => {
|
|
13
|
+
const next = new Map(current);
|
|
14
|
+
next.delete(key);
|
|
15
|
+
return next;
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* An in-memory blob store over a `Ref` of its own, empty at the start.
|
|
20
|
+
*/
|
|
21
|
+
export const makeInMemoryBlobStore = Effect.gen(function* () {
|
|
22
|
+
const blobs = yield* Ref.make(new Map());
|
|
23
|
+
return makeInMemoryBlobStoreWith(blobs);
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* The in-memory blob store, mounted.
|
|
27
|
+
*/
|
|
28
|
+
export const InMemoryBlobStoreLive = Layer.effect(BlobStore, makeInMemoryBlobStore);
|
|
29
|
+
/**
|
|
30
|
+
* A blob store that answers nothing: every operation fails with
|
|
31
|
+
* `BlobStoreUnavailableError`. Mount it to walk the branch a working
|
|
32
|
+
* stand-in never reaches.
|
|
33
|
+
*/
|
|
34
|
+
export const BlobStoreUnavailableLive = Layer.succeed(BlobStore, {
|
|
35
|
+
put: () => new BlobStoreUnavailableError({ operation: 'put', cause: NOT_MOUNTED }),
|
|
36
|
+
get: () => new BlobStoreUnavailableError({ operation: 'get', cause: NOT_MOUNTED }),
|
|
37
|
+
exists: () => new BlobStoreUnavailableError({
|
|
38
|
+
operation: 'exists',
|
|
39
|
+
cause: NOT_MOUNTED,
|
|
40
|
+
}),
|
|
41
|
+
delete: () => new BlobStoreUnavailableError({
|
|
42
|
+
operation: 'delete',
|
|
43
|
+
cause: NOT_MOUNTED,
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Clock, DateTime, type Duration, Effect } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Sets the controlled test clock to the given ISO moment. Tests read
|
|
4
|
+
* time through Effect, so this is the only clock code ever sees.
|
|
5
|
+
*/
|
|
6
|
+
export declare const setNowTo: (isoTimestamp: string) => Effect.Effect<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Moves the controlled test clock on by the span handed in, so a test reaches
|
|
9
|
+
* a later moment without waiting for it.
|
|
10
|
+
*/
|
|
11
|
+
export declare const advanceBy: (duration: Duration.DurationInput) => Effect.Effect<void>;
|
|
12
|
+
/**
|
|
13
|
+
* The moment the clock reads now, as a moment on the world's clock.
|
|
14
|
+
*/
|
|
15
|
+
export declare const nowAsDateTime: Effect.Effect<DateTime.Utc>;
|
|
16
|
+
/**
|
|
17
|
+
* The moment the clock reads now, written the way it travels the wire.
|
|
18
|
+
*/
|
|
19
|
+
export declare const nowAsIsoString: Effect.Effect<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Builds a Clock pinned to one moment: every time read inside returns
|
|
22
|
+
* that moment, while `sleep` still waits on the real clock.
|
|
23
|
+
*/
|
|
24
|
+
export declare const clockPinnedAt: (at: DateTime.Utc) => Clock.Clock;
|
|
25
|
+
/**
|
|
26
|
+
* Runs the given work "at" the given moment: every clock read inside
|
|
27
|
+
* it sees `isoTimestamp`.
|
|
28
|
+
*/
|
|
29
|
+
export declare const atTheMoment: (isoTimestamp: string) => <A, E, R>(work: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Clock, DateTime, Effect, TestClock } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Sets the controlled test clock to the given ISO moment. Tests read
|
|
4
|
+
* time through Effect, so this is the only clock code ever sees.
|
|
5
|
+
*/
|
|
6
|
+
export const setNowTo = (isoTimestamp) => TestClock.setTime(DateTime.toEpochMillis(DateTime.unsafeMake(isoTimestamp)));
|
|
7
|
+
/**
|
|
8
|
+
* Moves the controlled test clock on by the span handed in, so a test reaches
|
|
9
|
+
* a later moment without waiting for it.
|
|
10
|
+
*/
|
|
11
|
+
export const advanceBy = (duration) => TestClock.adjust(duration);
|
|
12
|
+
/**
|
|
13
|
+
* The moment the clock reads now, as a moment on the world's clock.
|
|
14
|
+
*/
|
|
15
|
+
export const nowAsDateTime = DateTime.now;
|
|
16
|
+
/**
|
|
17
|
+
* The moment the clock reads now, written the way it travels the wire.
|
|
18
|
+
*/
|
|
19
|
+
export const nowAsIsoString = DateTime.now.pipe(Effect.map(DateTime.formatIso));
|
|
20
|
+
/**
|
|
21
|
+
* Builds a Clock pinned to one moment: every time read inside returns
|
|
22
|
+
* that moment, while `sleep` still waits on the real clock.
|
|
23
|
+
*/
|
|
24
|
+
export const clockPinnedAt = (at) => {
|
|
25
|
+
const millis = DateTime.toEpochMillis(at);
|
|
26
|
+
const nanos = BigInt(millis) * 1000000n;
|
|
27
|
+
const live = Clock.make();
|
|
28
|
+
return {
|
|
29
|
+
[Clock.ClockTypeId]: Clock.ClockTypeId,
|
|
30
|
+
unsafeCurrentTimeMillis: () => millis,
|
|
31
|
+
currentTimeMillis: Effect.succeed(millis),
|
|
32
|
+
unsafeCurrentTimeNanos: () => nanos,
|
|
33
|
+
currentTimeNanos: Effect.succeed(nanos),
|
|
34
|
+
sleep: (duration) => live.sleep(duration),
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Runs the given work "at" the given moment: every clock read inside
|
|
39
|
+
* it sees `isoTimestamp`.
|
|
40
|
+
*/
|
|
41
|
+
export const atTheMoment = (isoTimestamp) => (work) => Effect.withClock(work, clockPinnedAt(DateTime.unsafeMake(isoTimestamp)));
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ChainRange } from './chains.js';
|
|
2
|
+
/**
|
|
3
|
+
* How much attention one chain receives, counted in mutants.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export interface ChainAttention {
|
|
8
|
+
readonly chain: string;
|
|
9
|
+
readonly mutants: number;
|
|
10
|
+
readonly caughtByStaticAnalysis: number;
|
|
11
|
+
readonly killedByTests: number;
|
|
12
|
+
readonly killingTestRuns: number;
|
|
13
|
+
readonly survived: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export interface ReportMutant {
|
|
19
|
+
readonly status: string;
|
|
20
|
+
readonly location: {
|
|
21
|
+
readonly start: {
|
|
22
|
+
readonly line: number;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
readonly killedBy?: readonly string[];
|
|
26
|
+
readonly testsCompleted?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One mutation-testing report, in the shape every mutation runner
|
|
30
|
+
* writes: files by path, each with the mutants it produced.
|
|
31
|
+
*
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export interface MutationReport {
|
|
35
|
+
readonly files: Readonly<Record<string, {
|
|
36
|
+
readonly mutants: readonly ReportMutant[];
|
|
37
|
+
}>>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The attention map: for every chain of one dna file, how many mutants
|
|
41
|
+
* the mutation run produced, how many static analysis refused before a
|
|
42
|
+
* single test ran, how many the tests killed and with how many test
|
|
43
|
+
* runs, and how many survived. A chain with survivors is a chain nobody
|
|
44
|
+
* is really watching, whatever the coverage percentage says.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export declare const attentionMapOf: (report: MutationReport, dnaPath: string, ranges: readonly ChainRange[]) => readonly ChainAttention[];
|
|
49
|
+
/**
|
|
50
|
+
* The attention map as the lines a person reads, one chain per line,
|
|
51
|
+
* with the chains nobody watches named last.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
export declare const attentionLinesOf: (implant: string, map: readonly ChainAttention[]) => readonly string[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const isKilled = (status) => status === 'Killed' || status === 'Timeout';
|
|
2
|
+
const isCompileError = (status) => status === 'CompileError';
|
|
3
|
+
/**
|
|
4
|
+
* The attention map: for every chain of one dna file, how many mutants
|
|
5
|
+
* the mutation run produced, how many static analysis refused before a
|
|
6
|
+
* single test ran, how many the tests killed and with how many test
|
|
7
|
+
* runs, and how many survived. A chain with survivors is a chain nobody
|
|
8
|
+
* is really watching, whatever the coverage percentage says.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export const attentionMapOf = (report, dnaPath, ranges) => {
|
|
13
|
+
const mutants = report.files[dnaPath]?.mutants ?? [];
|
|
14
|
+
return ranges.map((range) => {
|
|
15
|
+
const own = mutants.filter((mutant) => mutant.location.start.line >= range.startLine &&
|
|
16
|
+
mutant.location.start.line <= range.endLine);
|
|
17
|
+
const killed = own.filter((mutant) => isKilled(mutant.status));
|
|
18
|
+
return {
|
|
19
|
+
chain: range.chain,
|
|
20
|
+
mutants: own.length,
|
|
21
|
+
caughtByStaticAnalysis: own.filter((mutant) => isCompileError(mutant.status)).length,
|
|
22
|
+
killedByTests: killed.length,
|
|
23
|
+
killingTestRuns: killed.reduce((total, mutant) => total + (mutant.killedBy?.length ?? mutant.testsCompleted ?? 0), 0),
|
|
24
|
+
survived: own.filter((mutant) => !isKilled(mutant.status) && !isCompileError(mutant.status)).length,
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The attention map as the lines a person reads, one chain per line,
|
|
30
|
+
* with the chains nobody watches named last.
|
|
31
|
+
*
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export const attentionLinesOf = (implant, map) => {
|
|
35
|
+
const rows = map.map((entry) => `${implant}/${entry.chain} — ${entry.mutants} mutants: ` +
|
|
36
|
+
`${entry.caughtByStaticAnalysis} caught by static analysis before a ` +
|
|
37
|
+
`test ran, ${entry.killedByTests} killed by tests ` +
|
|
38
|
+
`(${entry.killingTestRuns} test runs did the killing), ` +
|
|
39
|
+
`${entry.survived} survived`);
|
|
40
|
+
const unwatched = map.filter((entry) => entry.survived > 0);
|
|
41
|
+
return [
|
|
42
|
+
...rows,
|
|
43
|
+
unwatched.length === 0
|
|
44
|
+
? 'every chain of this implant has a test that notices when it changes.'
|
|
45
|
+
: `chains with survivors, watch these first: ${unwatched
|
|
46
|
+
.map((entry) => entry.chain)
|
|
47
|
+
.join(', ')}`,
|
|
48
|
+
];
|
|
49
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One chain of a dna file and the lines it occupies, counted from one.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export interface ChainRange {
|
|
7
|
+
readonly chain: string;
|
|
8
|
+
readonly startLine: number;
|
|
9
|
+
readonly endLine: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Every chain of a dna file with the lines it owns: a chain runs from
|
|
13
|
+
* its own `export` line to the line before the next one.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export declare const chainRangesOf: (text: string) => readonly ChainRange[];
|
|
18
|
+
/**
|
|
19
|
+
* The dna file with one chain removed outright. A removal is the break
|
|
20
|
+
* the demonstration wants: nothing subtle, nothing that only a linter
|
|
21
|
+
* would notice — the business chain is simply gone.
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export declare const withChainRemoved: (text: string, chain: string) => string | null;
|
|
26
|
+
/**
|
|
27
|
+
* The dna file with one line commented out, counted from one.
|
|
28
|
+
*
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
export declare const withLineCommented: (text: string, line: number) => string | null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const EXPORTED_VALUE = /^export\s+(?:const|function)\s+([A-Za-z_$][\w$]*)/;
|
|
2
|
+
/**
|
|
3
|
+
* Every chain of a dna file with the lines it owns: a chain runs from
|
|
4
|
+
* its own `export` line to the line before the next one.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export const chainRangesOf = (text) => {
|
|
9
|
+
const lines = text.split('\n');
|
|
10
|
+
const starts = [];
|
|
11
|
+
lines.forEach((line, index) => {
|
|
12
|
+
const match = EXPORTED_VALUE.exec(line);
|
|
13
|
+
if (match?.[1] !== undefined) {
|
|
14
|
+
starts.push({ chain: match[1], startLine: index + 1 });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
return starts.map((start, index) => ({
|
|
18
|
+
chain: start.chain,
|
|
19
|
+
startLine: start.startLine,
|
|
20
|
+
endLine: (starts[index + 1]?.startLine ?? lines.length + 1) - 1,
|
|
21
|
+
}));
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The dna file with one chain removed outright. A removal is the break
|
|
25
|
+
* the demonstration wants: nothing subtle, nothing that only a linter
|
|
26
|
+
* would notice — the business chain is simply gone.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export const withChainRemoved = (text, chain) => {
|
|
31
|
+
const range = chainRangesOf(text).find((entry) => entry.chain === chain);
|
|
32
|
+
if (range === undefined)
|
|
33
|
+
return null;
|
|
34
|
+
const lines = text.split('\n');
|
|
35
|
+
return [
|
|
36
|
+
...lines.slice(0, range.startLine - 1),
|
|
37
|
+
...lines.slice(range.endLine),
|
|
38
|
+
].join('\n');
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The dna file with one line commented out, counted from one.
|
|
42
|
+
*
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
export const withLineCommented = (text, line) => {
|
|
46
|
+
const lines = text.split('\n');
|
|
47
|
+
const target = lines[line - 1];
|
|
48
|
+
if (target === undefined)
|
|
49
|
+
return null;
|
|
50
|
+
return lines
|
|
51
|
+
.map((entry, index) => (index === line - 1 ? `// ${target}` : entry))
|
|
52
|
+
.join('\n');
|
|
53
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The path of the json report a mutation run has just written, read out
|
|
3
|
+
* of that run's own output. Stryker names the file itself, once per
|
|
4
|
+
* reporter, so the tool never has to guess a location or trust a report
|
|
5
|
+
* an earlier run left behind: no line, no attention map. The html
|
|
6
|
+
* reporter prints the same sentence about its own file, so the line is
|
|
7
|
+
* matched by the reporter that wrote the json one, and the colour codes
|
|
8
|
+
* a terminal run wraps the line in are cut off the path.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare const jsonReportPathIn: (output: string) => string | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const REPORTED_AT = /JsonReporter[^\n]*?Your report can be found at:\s*(\S+)/;
|
|
2
|
+
const ESCAPE = String.fromCharCode(27);
|
|
3
|
+
const FILE_URL = 'file://';
|
|
4
|
+
/**
|
|
5
|
+
* The path of the json report a mutation run has just written, read out
|
|
6
|
+
* of that run's own output. Stryker names the file itself, once per
|
|
7
|
+
* reporter, so the tool never has to guess a location or trust a report
|
|
8
|
+
* an earlier run left behind: no line, no attention map. The html
|
|
9
|
+
* reporter prints the same sentence about its own file, so the line is
|
|
10
|
+
* matched by the reporter that wrote the json one, and the colour codes
|
|
11
|
+
* a terminal run wraps the line in are cut off the path.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export const jsonReportPathIn = (output) => {
|
|
16
|
+
const named = REPORTED_AT.exec(output)?.[1]?.split(ESCAPE)[0];
|
|
17
|
+
if (named === undefined || named === '')
|
|
18
|
+
return null;
|
|
19
|
+
return named.startsWith(FILE_URL)
|
|
20
|
+
? decodeURIComponent(named.slice(FILE_URL.length))
|
|
21
|
+
: named;
|
|
22
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one deliberate break did: how many tests turned red, and whether
|
|
3
|
+
* static analysis refused the file before a single test ran.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export interface BreakOutcome {
|
|
8
|
+
readonly what: string;
|
|
9
|
+
readonly caughtByStaticAnalysis: boolean;
|
|
10
|
+
readonly testsRed: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The one honest line a deliberate break earns. A break that leaves the
|
|
14
|
+
* suite green is printed as a finding, not as a pass: it means the
|
|
15
|
+
* chain that was removed is a chain nothing was holding.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export declare const verdictLineOf: (outcome: BreakOutcome) => string;
|
|
20
|
+
/**
|
|
21
|
+
* How many tests a test-runner output says went red. Zero when the
|
|
22
|
+
* output never says.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export declare const redTestCountIn: (output: string) => number;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one honest line a deliberate break earns. A break that leaves the
|
|
3
|
+
* suite green is printed as a finding, not as a pass: it means the
|
|
4
|
+
* chain that was removed is a chain nothing was holding.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export const verdictLineOf = (outcome) => {
|
|
9
|
+
if (outcome.caughtByStaticAnalysis) {
|
|
10
|
+
return (`${outcome.what}: static analysis refused it before a single test ` +
|
|
11
|
+
`ran, and ${outcome.testsRed} test(s) went red after that.`);
|
|
12
|
+
}
|
|
13
|
+
if (outcome.testsRed === 0) {
|
|
14
|
+
return (`${outcome.what}: FINDING — nothing went red. Static analysis let it ` +
|
|
15
|
+
'through and every test still passed, so nothing was holding this ' +
|
|
16
|
+
'chain.');
|
|
17
|
+
}
|
|
18
|
+
return (`${outcome.what}: static analysis let it through, and ${outcome.testsRed} ` +
|
|
19
|
+
'test(s) went red.');
|
|
20
|
+
};
|
|
21
|
+
const FAILED_TESTS = /Tests\s+(\d+)\s+failed/;
|
|
22
|
+
/**
|
|
23
|
+
* How many tests a test-runner output says went red. Zero when the
|
|
24
|
+
* output never says.
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export const redTestCountIn = (output) => Number(FAILED_TESTS.exec(output)?.[1] ?? 0);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type MutationReport } from './dna-perturbator/attention.js';
|
|
2
|
+
/**
|
|
3
|
+
* What one command did.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export interface CommandRun {
|
|
8
|
+
readonly code: number;
|
|
9
|
+
readonly output: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Everything the perturbator touches outside itself, handed in so a test
|
|
13
|
+
* can drive the whole tool without starting a mutation run.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export interface PerturbatorTools {
|
|
18
|
+
readonly run: (command: string, args: readonly string[]) => CommandRun;
|
|
19
|
+
readonly print: (line: string) => void;
|
|
20
|
+
readonly readFile: (path: string) => string;
|
|
21
|
+
readonly writeFile: (path: string, text: string) => void;
|
|
22
|
+
readonly readReport: (path: string) => MutationReport | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The dna file of one implant.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export declare const dnaPathOf: (implant: string) => string;
|
|
30
|
+
/**
|
|
31
|
+
* The mutation run this tool asks for: every mutant of one implant's dna
|
|
32
|
+
* file, reported as json. Stryker takes the file name of that report from
|
|
33
|
+
* its own configuration and never from the command line, so the tool asks
|
|
34
|
+
* for the reporter and then reads back the path the run itself names.
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export declare const mutationCommandOf: (implant: string) => readonly string[];
|
|
39
|
+
/**
|
|
40
|
+
* The whole tool. With one argument it runs mutation testing over that
|
|
41
|
+
* implant's dna file and prints the attention map. With a second one — a
|
|
42
|
+
* chain name or a line number — it removes that chain or comments that
|
|
43
|
+
* line, runs static analysis and then the implant's tests, prints one
|
|
44
|
+
* honest line, and puts the file back whatever happens.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export declare const runDnaPerturbator: (argv: readonly string[], tools: PerturbatorTools) => number;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { attentionLinesOf, attentionMapOf, } from './dna-perturbator/attention.js';
|
|
4
|
+
import { chainRangesOf, withChainRemoved, withLineCommented, } from './dna-perturbator/chains.js';
|
|
5
|
+
import { jsonReportPathIn } from './dna-perturbator/report.js';
|
|
6
|
+
import { redTestCountIn, verdictLineOf } from './dna-perturbator/verdict.js';
|
|
7
|
+
/**
|
|
8
|
+
* The dna file of one implant.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export const dnaPathOf = (implant) => `business/${implant}/domain/dna.ts`;
|
|
13
|
+
/**
|
|
14
|
+
* The mutation run this tool asks for: every mutant of one implant's dna
|
|
15
|
+
* file, reported as json. Stryker takes the file name of that report from
|
|
16
|
+
* its own configuration and never from the command line, so the tool asks
|
|
17
|
+
* for the reporter and then reads back the path the run itself names.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
export const mutationCommandOf = (implant) => [
|
|
22
|
+
'stryker',
|
|
23
|
+
'run',
|
|
24
|
+
'--mutate',
|
|
25
|
+
dnaPathOf(implant),
|
|
26
|
+
'--reporters',
|
|
27
|
+
'json',
|
|
28
|
+
];
|
|
29
|
+
const mutationRun = (implant, tools) => {
|
|
30
|
+
const dnaPath = dnaPathOf(implant);
|
|
31
|
+
const started = tools.run('bunx', [...mutationCommandOf(implant)]);
|
|
32
|
+
const reportPath = jsonReportPathIn(started.output);
|
|
33
|
+
if (reportPath === null) {
|
|
34
|
+
tools.print('dna:perturbator: the mutation run named no json report ' +
|
|
35
|
+
`(it exited ${started.code}), so there is nothing honest to say ` +
|
|
36
|
+
'about attention here. Switch the json reporter on in this ' +
|
|
37
|
+
"repository's stryker configuration and run it again.");
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
const report = tools.readReport(reportPath);
|
|
41
|
+
if (report === null) {
|
|
42
|
+
tools.print(`dna:perturbator: the mutation run named ${reportPath} and nothing ` +
|
|
43
|
+
`readable is there (it exited ${started.code}), so there is ` +
|
|
44
|
+
'nothing honest to say about attention here.');
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
const ranges = chainRangesOf(tools.readFile(dnaPath));
|
|
48
|
+
for (const line of attentionLinesOf(implant, attentionMapOf(report, dnaPath, ranges))) {
|
|
49
|
+
tools.print(line);
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
};
|
|
53
|
+
const perturbedTextOf = (text, target) => {
|
|
54
|
+
const asLine = Number(target);
|
|
55
|
+
if (Number.isInteger(asLine) && asLine > 0) {
|
|
56
|
+
const commented = withLineCommented(text, asLine);
|
|
57
|
+
return commented === null
|
|
58
|
+
? null
|
|
59
|
+
: { text: commented, what: `commenting out line ${asLine}` };
|
|
60
|
+
}
|
|
61
|
+
const removed = withChainRemoved(text, target);
|
|
62
|
+
return removed === null
|
|
63
|
+
? null
|
|
64
|
+
: { text: removed, what: `removing the chain ${target}` };
|
|
65
|
+
};
|
|
66
|
+
const breakRun = (implant, target, tools) => {
|
|
67
|
+
const dnaPath = dnaPathOf(implant);
|
|
68
|
+
const before = tools.readFile(dnaPath);
|
|
69
|
+
const perturbed = perturbedTextOf(before, target);
|
|
70
|
+
if (perturbed === null) {
|
|
71
|
+
tools.print(`dna:perturbator: ${dnaPath} holds no chain or line named ${target}.`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
tools.writeFile(dnaPath, perturbed.text);
|
|
76
|
+
const typecheck = tools.run('bun', ['run', 'typecheck']);
|
|
77
|
+
const tests = tools.run('bunx', ['vitest', 'run', `business/${implant}`]);
|
|
78
|
+
const testsRed = redTestCountIn(tests.output);
|
|
79
|
+
if (testsRed === 0 && tests.code !== 0) {
|
|
80
|
+
tools.print(`${perturbed.what}: the test run itself never reported a count ` +
|
|
81
|
+
`(it exited ${tests.code}), so this run proves nothing. Read its ` +
|
|
82
|
+
'output before trusting either colour.');
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
const line = verdictLineOf({
|
|
86
|
+
what: perturbed.what,
|
|
87
|
+
caughtByStaticAnalysis: typecheck.code !== 0,
|
|
88
|
+
testsRed,
|
|
89
|
+
});
|
|
90
|
+
tools.print(line);
|
|
91
|
+
return line.includes('FINDING') ? 1 : 0;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
tools.writeFile(dnaPath, before);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* The whole tool. With one argument it runs mutation testing over that
|
|
99
|
+
* implant's dna file and prints the attention map. With a second one — a
|
|
100
|
+
* chain name or a line number — it removes that chain or comments that
|
|
101
|
+
* line, runs static analysis and then the implant's tests, prints one
|
|
102
|
+
* honest line, and puts the file back whatever happens.
|
|
103
|
+
*
|
|
104
|
+
* @internal
|
|
105
|
+
*/
|
|
106
|
+
export const runDnaPerturbator = (argv, tools) => {
|
|
107
|
+
const implant = argv[0];
|
|
108
|
+
if (implant === undefined || implant === '') {
|
|
109
|
+
tools.print('dna:perturbator: name the implant. With one argument it measures ' +
|
|
110
|
+
'attention across every chain; with a second one — a chain name or ' +
|
|
111
|
+
'a line number — it breaks exactly that and tells you who noticed.');
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
const target = argv[1];
|
|
115
|
+
return target === undefined || target === ''
|
|
116
|
+
? mutationRun(implant, tools)
|
|
117
|
+
: breakRun(implant, target, tools);
|
|
118
|
+
};
|
|
119
|
+
const spawnTools = {
|
|
120
|
+
run: (command, args) => {
|
|
121
|
+
const finished = spawnSync(command, [...args], { encoding: 'utf8' });
|
|
122
|
+
return {
|
|
123
|
+
code: finished.status ?? 1,
|
|
124
|
+
output: `${finished.stdout ?? ''}${finished.stderr ?? ''}`,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
print: (line) => {
|
|
128
|
+
console.log(line);
|
|
129
|
+
},
|
|
130
|
+
readFile: (path) => readFileSync(path, 'utf8'),
|
|
131
|
+
writeFile: (path, text) => {
|
|
132
|
+
writeFileSync(path, text);
|
|
133
|
+
},
|
|
134
|
+
readReport: (path) => {
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
if (import.meta.main) {
|
|
144
|
+
process.exitCode = runDnaPerturbator(process.argv.slice(2), spawnTools);
|
|
145
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DocumentNumberCounter, type DocumentNumberCounterShape } from '@satorio/machinery/ports';
|
|
2
|
+
import { Effect, Layer, Ref } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* What an in-memory counter holds: how many documents each client has
|
|
5
|
+
* been sent in each reporting month, keyed by the two together. A
|
|
6
|
+
* scenario seeds this map to start a month part way through.
|
|
7
|
+
*/
|
|
8
|
+
export type DocumentCountsByClientAndMonth = ReadonlyMap<string, number>;
|
|
9
|
+
/**
|
|
10
|
+
* An in-memory counter over a reference you keep, so a test can seed it and
|
|
11
|
+
* read it back.
|
|
12
|
+
*
|
|
13
|
+
* An in-memory counter over a `Ref` you keep, so a test can seed how far a
|
|
14
|
+
* month has already run and read back where it ended.
|
|
15
|
+
*/
|
|
16
|
+
export declare const makeInMemoryDocumentNumberCounterWith: (counts: Ref.Ref<DocumentCountsByClientAndMonth>) => DocumentNumberCounterShape;
|
|
17
|
+
/**
|
|
18
|
+
* An in-memory counter over a `Ref` of its own, with every month at zero.
|
|
19
|
+
*/
|
|
20
|
+
export declare const makeInMemoryDocumentNumberCounter: Effect.Effect<DocumentNumberCounterShape, never, never>;
|
|
21
|
+
/**
|
|
22
|
+
* The in-memory counter, mounted.
|
|
23
|
+
*/
|
|
24
|
+
export declare const InMemoryDocumentNumberCounterLive: Layer.Layer<DocumentNumberCounter, never, never>;
|
|
25
|
+
/**
|
|
26
|
+
* A counter that counts nothing: every request comes back as a refusal.
|
|
27
|
+
* Mount it to walk the branch a working stand-in never reaches.
|
|
28
|
+
*/
|
|
29
|
+
export declare const DocumentNumberCounterUnavailableLive: Layer.Layer<DocumentNumberCounter>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DocumentNumberCounter, DocumentNumberCounterUnavailableError, } from '@satorio/machinery/ports';
|
|
2
|
+
import { Effect, Layer, Ref } from 'effect';
|
|
3
|
+
const NOT_MOUNTED = 'document number counter unavailable (test)';
|
|
4
|
+
const keyOf = (clientCode, reportingMonth) => `${clientCode}/${reportingMonth}`;
|
|
5
|
+
/**
|
|
6
|
+
* An in-memory counter over a reference you keep, so a test can seed it and
|
|
7
|
+
* read it back.
|
|
8
|
+
*
|
|
9
|
+
* An in-memory counter over a `Ref` you keep, so a test can seed how far a
|
|
10
|
+
* month has already run and read back where it ended.
|
|
11
|
+
*/
|
|
12
|
+
export const makeInMemoryDocumentNumberCounterWith = (counts) => ({
|
|
13
|
+
nextCount: (input) => Ref.modify(counts, (current) => {
|
|
14
|
+
const key = keyOf(input.clientCode, input.reportingMonth);
|
|
15
|
+
const next = (current.get(key) ?? 0) + 1;
|
|
16
|
+
return [next, new Map(current).set(key, next)];
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* An in-memory counter over a `Ref` of its own, with every month at zero.
|
|
21
|
+
*/
|
|
22
|
+
export const makeInMemoryDocumentNumberCounter = Effect.gen(function* () {
|
|
23
|
+
const counts = yield* Ref.make(new Map());
|
|
24
|
+
return makeInMemoryDocumentNumberCounterWith(counts);
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* The in-memory counter, mounted.
|
|
28
|
+
*/
|
|
29
|
+
export const InMemoryDocumentNumberCounterLive = Layer.effect(DocumentNumberCounter, makeInMemoryDocumentNumberCounter);
|
|
30
|
+
/**
|
|
31
|
+
* A counter that counts nothing: every request comes back as a refusal.
|
|
32
|
+
* Mount it to walk the branch a working stand-in never reaches.
|
|
33
|
+
*/
|
|
34
|
+
export const DocumentNumberCounterUnavailableLive = Layer.succeed(DocumentNumberCounter, {
|
|
35
|
+
nextCount: () => new DocumentNumberCounterUnavailableError({
|
|
36
|
+
operation: 'nextCount',
|
|
37
|
+
cause: NOT_MOUNTED,
|
|
38
|
+
}),
|
|
39
|
+
});
|