@starci/jest-preset 2.5.0 → 2.5.2
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/CHANGELOG.md +9 -0
- package/README.md +13 -7
- package/dist/entity-manager.d.ts +3 -1
- package/dist/entity-manager.js +9 -1
- package/dist/inbox.d.ts +26 -2
- package/dist/inbox.js +32 -1
- package/dist/index.d.ts +11 -5
- package/dist/index.js +20 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.5.2 - 2026-10-30
|
|
4
|
+
|
|
5
|
+
- Added: `coverage.files` of the rendered be/jest.config.js: the files a slot declares as logic (hfs 4.11.20 `logic`) are collected and held to 100 per file like the logic roles; a config rendered without `files` loads as before. Specs: `test/coverage-scoped.spec.ts`, `test/preset.spec.ts`.
|
|
6
|
+
|
|
7
|
+
## 2.5.1 - 2026-10-29
|
|
8
|
+
|
|
9
|
+
- Fixed: `fakeTransaction` records a `query` that calls a migration's database function the way `callDbFunction` writes it (`SELECT * FROM "<schema>"."<fn>"(...)`, any schema but `pg_catalog`) as a write, so a rolled-back inbox claim or event send appears in `rolledBackWrites`; any other `SELECT` stays a read.
|
|
10
|
+
- Added: `fakeInbox().claimRow(source, eventId, tx, { entity, row })`, the double of the lite `EntityInbox` claim: it refuses a missing or blank id (`DATABASE_WEBHOOK_DELIVERY_ID_REQUIRED`), claims the trimmed id and, for the winning claim only, records the row `row(id)` builds in `receipts` (with the manager and `inTransaction`) and answers `receipt-<n>`; a repeat, or a pair marked by `receiptExists(source, eventId)`, answers undefined. `failNext("claimRow", error)` scripts a failure.
|
|
11
|
+
|
|
3
12
|
## 2.5.0 - 2026-10-29
|
|
4
13
|
|
|
5
14
|
- Added: `fakeQueryBuilder(answers)` and `queueQueryBuilders(em, answers)` (also at `@starci/jest-preset/query-builder`), the double of `createQueryBuilder(Entity, "alias")` beside `mockEntityManager`: every chained call is recorded and returned to the chain, the terminals resolve the spec's answers. Lite persistence reads through entities and that builder (ruling `lite-persistence-typeorm-entities`), so the double moves here from the apps.
|
package/README.md
CHANGED
|
@@ -5,13 +5,13 @@ The jest config of a Nest repository and the one shared unit-test double kit (`m
|
|
|
5
5
|
```js
|
|
6
6
|
// jest.config.js (a managed file: `starci app sync` renders it, `starci app check` compares it; do not edit)
|
|
7
7
|
module.exports = require("@starci/jest-preset").starciJestConfig({
|
|
8
|
-
coverage: { roots, roles, excludes },
|
|
8
|
+
coverage: { roots, roles, files, excludes },
|
|
9
9
|
})
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
The only option is `coverage`: the measured scope `starci app sync` derives once from the slot manifest
|
|
13
|
-
(`scripts/hfs/coverage-scope.mjs`, R204 `HFS_COVERAGE_SCOPE_DRIFT`) — the measured roots, the roles a measured file carries
|
|
14
|
-
and the exclusions. A repository has no other jest setting to tune.
|
|
13
|
+
(`scripts/hfs/coverage-scope.mjs`, R204 `HFS_COVERAGE_SCOPE_DRIFT`) — the measured roots, the roles a measured file carries,
|
|
14
|
+
the logic files a slot declares (`logic`) and the exclusions. A repository has no other jest setting to tune.
|
|
15
15
|
|
|
16
16
|
## What it sets
|
|
17
17
|
|
|
@@ -32,9 +32,9 @@ and the exclusions. A repository has no other jest setting to tune.
|
|
|
32
32
|
(`src/tests/tsconfig.json`), and `test:integration`/`test:e2e`/`test:contract` run `typecheck:tests` first, so no world spec
|
|
33
33
|
runs on code that does not type-check.
|
|
34
34
|
- **Coverage of the measured logic, per-file 100.** `collectCoverageFrom` is one glob per `coverage.roots` entry covering `<root>/**/*.{<roles>}.ts`
|
|
35
|
-
(the `ruleParams.be.logicRoles` of the rendered scope), minus its `excludes`, `dist` and `coverage`, and `coverageThreshold` holds
|
|
36
|
-
`{ lines: 100, branches: 100, functions: 100, statements: 100 }` on one union glob of those roots. Jest applies a glob threshold to each file on its own, so every file of a
|
|
37
|
-
`ruleParams.be.logicRoles` role inside a measured slot must reach 100 on every metric (an average never passes). The
|
|
35
|
+
(the `ruleParams.be.logicRoles` of the rendered scope) plus each `coverage.files` entry (a logic file its slot declares), minus its `excludes`, `dist` and `coverage`, and `coverageThreshold` holds
|
|
36
|
+
`{ lines: 100, branches: 100, functions: 100, statements: 100 }` on one union glob of those roots and files. Jest applies a glob threshold to each file on its own, so every file of a
|
|
37
|
+
`ruleParams.be.logicRoles` role inside a measured slot, and every declared logic file, must reach 100 on every metric (an average never passes). The
|
|
38
38
|
managed `test` script runs the unit project with `--coverage` and fails below it. Coverage is measured by v8, which counts the
|
|
39
39
|
real source. The unit project overlays `isolatedModules: true`, `importHelpers: true` and `sourceMap: true` on the repository tsconfig, so ts-jest
|
|
40
40
|
transpiles each file without building a type program. `unit-metadata.js`, a ts-jest wrapper and after-transformer, maps
|
|
@@ -103,7 +103,9 @@ expect(tx.committedWrites).toHaveLength(1) // after a throwing body: tx.r
|
|
|
103
103
|
The stubbable methods are `findOne`, `findOneBy`, `findOneOrFail`, `findOneByOrFail`, `find`, `findBy`, `findAndCount`,
|
|
104
104
|
`findAndCountBy`, `count`, `countBy`, `exists`, `existsBy`, `save`, `remove`, `softRemove`, `recover`, `insert`, `update`, `upsert`,
|
|
105
105
|
`delete`, `softDelete`, `restore`, `increment`, `decrement` and `query`. `fakeTransaction(em)` returns `{ em, outcomes, commits,
|
|
106
|
-
rollbacks, committedWrites, rolledBackWrites }`.
|
|
106
|
+
rollbacks, committedWrites, rolledBackWrites }`. A write is a write method, a `query` of a DML statement, or a `query` that calls a
|
|
107
|
+
migration's database function the way `callDbFunction` does (`SELECT * FROM "public"."inbox_claim_event"($1, $2)`, any schema but
|
|
108
|
+
`pg_catalog`); any other `SELECT` is a read.
|
|
107
109
|
|
|
108
110
|
## `fakeQueryBuilder` and `queueQueryBuilders`
|
|
109
111
|
|
|
@@ -156,6 +158,10 @@ await inbox.claim("sepay", "e-1") // true the first time, false for the s
|
|
|
156
158
|
await inbox.release("sepay", "e-1") // gives the claim back: the next claim answers true
|
|
157
159
|
inbox.seen("sepay", "e-2") // a redelivery: the next claim of the pair answers false
|
|
158
160
|
inbox.claims; inbox.claimed; inbox.released; inbox.failNext("claim", new Error("db down")); inbox.clear()
|
|
161
|
+
// claimRow: the EntityInbox claim that inserts the receipt row only for the winning claim and answers its id
|
|
162
|
+
await inbox.claimRow("webhook.sepay", "e-3", tx, { entity: SepayInboxEntity, row: (deliveryId) => ({ deliveryId }) }) // "receipt-1"
|
|
163
|
+
inbox.receipts // every inserted receipt: id, source, eventId, entity, row, tx, inTransaction
|
|
164
|
+
inbox.receiptExists("webhook.sepay", "e-4") // the receipt row is already there: the winning claim inserts nothing, answers undefined
|
|
159
165
|
|
|
160
166
|
const clientInbox = fakeInbox(clock) // the driven clock stamps client claims for retention tests
|
|
161
167
|
await clientInbox.runClient({ principalId: "p-1", operation: "checkout.place", idempotencyKey: "key-1", request: { quantity: 2 } }, tx, async () => ({ kind: "ok", value: { orderId: "o-1" } }))
|
package/dist/entity-manager.d.ts
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
*
|
|
15
15
|
* `fakeTransaction(em)` makes `em.transaction(work)` (and `em.transaction(isolation, work)`) run `work` with a scoped view
|
|
16
16
|
* of the same double and record how it ended (`commit` when `work` resolves, `rollback` when it throws and the error is
|
|
17
|
-
* rethrown) and which writes it made, so a spec can assert that a rolled-back transaction committed nothing.
|
|
17
|
+
* rethrown) and which writes it made, so a spec can assert that a rolled-back transaction committed nothing. A write is a
|
|
18
|
+
* write method (`save`, `insert`, ...), a `query` of a DML statement, or a `query` calling a migration's database function
|
|
19
|
+
* the way `callDbFunction` does (`SELECT * FROM "public"."inbox_claim_event"($1, $2)`); any other `SELECT` is a read.
|
|
18
20
|
*/
|
|
19
21
|
import type { DeleteResult, EntityManager, InsertResult, UpdateResult } from "typeorm";
|
|
20
22
|
import type { MockOf } from "./mock.js";
|
package/dist/entity-manager.js
CHANGED
|
@@ -12,6 +12,14 @@ const WRITE_METHODS = new Set([
|
|
|
12
12
|
"increment", "decrement", "clear",
|
|
13
13
|
]);
|
|
14
14
|
const WRITE_SQL = /^\s*(insert|update|delete|upsert|merge|truncate)\b/i;
|
|
15
|
+
/**
|
|
16
|
+
* A call of a migration's database function in the shape `callDbFunction` writes (`SELECT * FROM "<schema>"."<fn>"(...)`), in
|
|
17
|
+
* any schema but the pg_catalog built-ins: such a function holds a write TypeORM cannot express (an inbox claim, an event send),
|
|
18
|
+
* so the transaction that makes the call records it as a write.
|
|
19
|
+
*/
|
|
20
|
+
const DB_FUNCTION_SQL = /^\s*select\s+\*\s+from\s+"(?!pg_catalog")[^"]+"\."[^"]+"\s*\(/i;
|
|
21
|
+
/** Whether a `query` writes: a DML statement, or a call of a migration's database function. */
|
|
22
|
+
const isWriteSql = (sql) => WRITE_SQL.test(String(sql)) || DB_FUNCTION_SQL.test(String(sql));
|
|
15
23
|
const describeTarget = (target) => typeof target === "function" ? target.name || "<anonymous class>" : JSON.stringify(String(target)).slice(0, 80);
|
|
16
24
|
const isPair = (value) => Array.isArray(value) && value.length === 2 && (typeof value[0] === "function" || typeof value[0] === "string");
|
|
17
25
|
/** `{ method: [target, result] | [[target, result], ...] }` as `Map<method, Array<{ target, result, used }>>`. */
|
|
@@ -74,7 +82,7 @@ function createEntityManagerKit(makeFn) {
|
|
|
74
82
|
if (!WRITE_METHODS.has(property) && property !== "query")
|
|
75
83
|
return value;
|
|
76
84
|
return (...args) => {
|
|
77
|
-
if (property !== "query" ||
|
|
85
|
+
if (property !== "query" || isWriteSql(args[0]))
|
|
78
86
|
run.writes.push({ method: property, args });
|
|
79
87
|
return value(...args);
|
|
80
88
|
};
|
package/dist/inbox.d.ts
CHANGED
|
@@ -4,7 +4,20 @@ export interface InboxEntry {
|
|
|
4
4
|
readonly eventId: string;
|
|
5
5
|
}
|
|
6
6
|
/** The inbox operations `failNext` can make reject. */
|
|
7
|
-
export type InboxOperation = "claim" | "release" | "runClient" | "sweepClients";
|
|
7
|
+
export type InboxOperation = "claim" | "claimRow" | "release" | "runClient" | "sweepClients";
|
|
8
|
+
/** The receipt `claimRow` inserts for a winning claim: its entity, and its row built from the normalized delivery id. */
|
|
9
|
+
export interface InboxReceiptRequest<Row> {
|
|
10
|
+
readonly entity: unknown;
|
|
11
|
+
readonly row: (eventId: string) => Row;
|
|
12
|
+
}
|
|
13
|
+
/** One receipt row `claimRow` inserted, with the id it answered and the manager passed by its caller. */
|
|
14
|
+
export interface InboxReceipt extends InboxEntry {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly entity: unknown;
|
|
17
|
+
readonly row: unknown;
|
|
18
|
+
readonly tx: unknown;
|
|
19
|
+
readonly inTransaction: boolean;
|
|
20
|
+
}
|
|
8
21
|
/** The client operation namespace, exact key and JSON request whose outcome the inbox stores. */
|
|
9
22
|
export interface InboxClientOperation {
|
|
10
23
|
readonly principalId: string;
|
|
@@ -27,6 +40,13 @@ export interface InboxSweep {
|
|
|
27
40
|
export interface FakeInbox {
|
|
28
41
|
/** True for the first claim of a (source, eventId) pair, false for every later one until it is released. */
|
|
29
42
|
claim(source: string, eventId: string): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* The `EntityInbox` claim: refuses a missing or blank delivery id, claims the trimmed id, and only for the winning claim
|
|
45
|
+
* inserts the receipt row and answers its id (`receipt-<n>`); a repeat, or a receipt that already exists, answers undefined.
|
|
46
|
+
*/
|
|
47
|
+
claimRow<Row>(source: string, eventId: string | undefined, tx: unknown, receipt: InboxReceiptRequest<Row>): Promise<string | undefined>;
|
|
48
|
+
/** Marks the receipt row of a pair as already inserted: its next winning `claimRow` inserts nothing and answers undefined. */
|
|
49
|
+
receiptExists(source: string, eventId: string): void;
|
|
30
50
|
/** Gives an ordinary delivery claim back while preserving client receipts and their outcomes. */
|
|
31
51
|
release(source: string, eventId: string): Promise<void>;
|
|
32
52
|
/** Runs once for the client namespace and key, replays the JSON outcome and refuses a different body. */
|
|
@@ -47,6 +67,8 @@ export interface FakeInbox {
|
|
|
47
67
|
readonly clientEntries: ReadonlyArray<InboxClientEntry>;
|
|
48
68
|
/** Every sweep attempt with its cutoff and transaction, including a scripted failure. */
|
|
49
69
|
readonly sweeps: ReadonlyArray<InboxSweep>;
|
|
70
|
+
/** Every receipt row `claimRow` inserted, in order, with its id, row and manager. */
|
|
71
|
+
readonly receipts: ReadonlyArray<InboxReceipt>;
|
|
50
72
|
clear(): void;
|
|
51
73
|
}
|
|
52
74
|
/**
|
|
@@ -57,7 +79,9 @@ export interface FakeInbox {
|
|
|
57
79
|
* real unique key; `release(source, eventId)` gives an ordinary delivery claim back while preserving client outcomes.
|
|
58
80
|
* `claims` is every claim in order, `claimed` the pairs that are held now, `released` every release, `seen(source, eventId)` marks a
|
|
59
81
|
* pair as already delivered (a redelivery), and `failNext(operation, error)` makes the next call of that operation reject once, so
|
|
60
|
-
* a spec drives the failure path.
|
|
82
|
+
* a spec drives the failure path. `claimRow(source, eventId, tx, { entity, row })` is the `EntityInbox` claim: it refuses a
|
|
83
|
+
* missing or blank id with `DATABASE_WEBHOOK_DELIVERY_ID_REQUIRED`, claims the trimmed id and, for the winning claim only, records
|
|
84
|
+
* the receipt row `row(id)` builds in `receipts` and answers its id; `receiptExists(source, eventId)` makes that insert a no-op.
|
|
61
85
|
*/
|
|
62
86
|
/** The inbox double. Pass a driven clock to test retention; omitted clocks stamp client claims at the Unix epoch. */
|
|
63
87
|
export declare function fakeInbox(clock?: {
|
package/dist/inbox.js
CHANGED
|
@@ -35,7 +35,9 @@ const snapshot = (value, ancestors = new Set()) => {
|
|
|
35
35
|
* real unique key; `release(source, eventId)` gives an ordinary delivery claim back while preserving client outcomes.
|
|
36
36
|
* `claims` is every claim in order, `claimed` the pairs that are held now, `released` every release, `seen(source, eventId)` marks a
|
|
37
37
|
* pair as already delivered (a redelivery), and `failNext(operation, error)` makes the next call of that operation reject once, so
|
|
38
|
-
* a spec drives the failure path.
|
|
38
|
+
* a spec drives the failure path. `claimRow(source, eventId, tx, { entity, row })` is the `EntityInbox` claim: it refuses a
|
|
39
|
+
* missing or blank id with `DATABASE_WEBHOOK_DELIVERY_ID_REQUIRED`, claims the trimmed id and, for the winning claim only, records
|
|
40
|
+
* the receipt row `row(id)` builds in `receipts` and answers its id; `receiptExists(source, eventId)` makes that insert a no-op.
|
|
39
41
|
*/
|
|
40
42
|
/** The inbox double. Pass a driven clock to test retention; omitted clocks stamp client claims at the Unix epoch. */
|
|
41
43
|
function fakeInbox(clock) {
|
|
@@ -45,6 +47,8 @@ function fakeInbox(clock) {
|
|
|
45
47
|
const clients = new Map();
|
|
46
48
|
const clientEntries = [];
|
|
47
49
|
const sweeps = [];
|
|
50
|
+
const receipts = [];
|
|
51
|
+
const existingReceipts = new Set();
|
|
48
52
|
const failures = new Map();
|
|
49
53
|
const keyOf = (source, eventId) => `${source}\u0000${eventId}`;
|
|
50
54
|
const failIfScripted = (operation) => {
|
|
@@ -106,6 +110,27 @@ function fakeInbox(clock) {
|
|
|
106
110
|
held.add(key);
|
|
107
111
|
return true;
|
|
108
112
|
},
|
|
113
|
+
async claimRow(source, eventId, tx, receipt) {
|
|
114
|
+
failIfScripted("claimRow");
|
|
115
|
+
const normalized = eventId?.trim();
|
|
116
|
+
if (!normalized)
|
|
117
|
+
throw inboxError("DATABASE_WEBHOOK_DELIVERY_ID_REQUIRED");
|
|
118
|
+
claims.push({ source, eventId: normalized });
|
|
119
|
+
const key = keyOf(source, normalized);
|
|
120
|
+
if (held.has(key))
|
|
121
|
+
return undefined;
|
|
122
|
+
held.add(key);
|
|
123
|
+
if (existingReceipts.has(key))
|
|
124
|
+
return undefined;
|
|
125
|
+
existingReceipts.add(key);
|
|
126
|
+
const id = `receipt-${String(receipts.length + 1)}`;
|
|
127
|
+
receipts.push({ id, source, eventId: normalized, entity: receipt.entity, row: receipt.row(normalized), tx, inTransaction: (0, entity_manager_ts_1.isTransactionManager)(tx) });
|
|
128
|
+
return id;
|
|
129
|
+
},
|
|
130
|
+
/** Marks the receipt row of a pair as already inserted, so its next winning `claimRow` answers undefined. */
|
|
131
|
+
receiptExists(source, eventId) {
|
|
132
|
+
existingReceipts.add(keyOf(source, eventId));
|
|
133
|
+
},
|
|
109
134
|
async release(source, eventId) {
|
|
110
135
|
failIfScripted("release");
|
|
111
136
|
released.push({ source, eventId });
|
|
@@ -160,7 +185,13 @@ function fakeInbox(clock) {
|
|
|
160
185
|
get sweeps() {
|
|
161
186
|
return [...sweeps];
|
|
162
187
|
},
|
|
188
|
+
/** Every receipt row `claimRow` inserted, in order. */
|
|
189
|
+
get receipts() {
|
|
190
|
+
return [...receipts];
|
|
191
|
+
},
|
|
163
192
|
clear() {
|
|
193
|
+
receipts.length = 0;
|
|
194
|
+
existingReceipts.clear();
|
|
164
195
|
held.clear();
|
|
165
196
|
claims.length = 0;
|
|
166
197
|
released.length = 0;
|
package/dist/index.d.ts
CHANGED
|
@@ -19,10 +19,15 @@ export type { RecordingEventBus } from "./event-bus.js";
|
|
|
19
19
|
export type { RecordingQueueOutbox } from "./queue.js";
|
|
20
20
|
export type { FakeInbox } from "./inbox.js";
|
|
21
21
|
export type { RefusalReason } from "./matchers.js";
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* The coverage scope `starci app sync` renders from the slot manifest: the measured roots (a directory pattern per measured slot, a placeholder
|
|
24
|
+
* folder written as a star), the logic roles measured in them, the logic files a slot declares (`logic`: a file that carries logic without a
|
|
25
|
+
* logic-role suffix, measured like one) and the directories inside a root that are never measured (each ending in a double star).
|
|
26
|
+
*/
|
|
23
27
|
export interface StarciCoverageScope {
|
|
24
28
|
readonly roots: readonly string[];
|
|
25
29
|
readonly roles: readonly string[];
|
|
30
|
+
readonly files?: readonly string[];
|
|
26
31
|
readonly excludes: readonly string[];
|
|
27
32
|
}
|
|
28
33
|
export interface StarciJestOptions {
|
|
@@ -46,10 +51,11 @@ export type StarciJestConfig = Omit<Config, "projects" | "coverageReporters" | "
|
|
|
46
51
|
};
|
|
47
52
|
/**
|
|
48
53
|
* Coverage is measured where logic runs: every file `<name>.<role>.ts` of a LOGIC role (service, policy, projection, guard, mapper, client, ...)
|
|
49
|
-
* inside the roots the slot manifest marks `coverage: required` (the modules of the back end: domain, integrations, platform, projections)
|
|
54
|
+
* inside the roots the slot manifest marks `coverage: required` (the modules of the back end: domain, integrations, platform, projections), and
|
|
55
|
+
* every logic file such a slot declares (`logic`: a file that carries logic without a logic-role suffix).
|
|
50
56
|
* The features (handlers, resolvers, controllers, consumers, processors, steps, sagas, cli commands) are thin by rule (BE_FEATURE_THIN) and are
|
|
51
57
|
* never measured. The scope is NOT written here: `starci app sync` renders it from the manifest (scripts/hfs/coverage-scope.mjs) into the repository's
|
|
52
|
-
* jest.config.js as `starciJestConfig({ coverage: { roots, roles, excludes } })`, and the same derivation renders Sonar's
|
|
58
|
+
* jest.config.js as `starciJestConfig({ coverage: { roots, roles, files, excludes } })`, and the same derivation renders Sonar's
|
|
53
59
|
* `sonar.coverage.exclusions` and the Codecov paths and components, so the three can never disagree. Every measured file must reach 100 on every metric.
|
|
54
60
|
* The unit run also writes `coverage/lcov.info`: Sonar and Codecov import it.
|
|
55
61
|
*/
|
|
@@ -64,8 +70,8 @@ declare const COVERAGE_THRESHOLD: Readonly<{
|
|
|
64
70
|
/** The glob one measured root stands for: every file of a logic role below it. */
|
|
65
71
|
declare function rootGlob(root: string, roles: readonly string[]): string;
|
|
66
72
|
/**
|
|
67
|
-
* jest's `collectCoverageFrom` of a coverage scope `{ roots, roles, excludes }`: the glob of every root,
|
|
68
|
-
* root (`excludes`, each a glob that ends in `**`) and the generated folders as negations.
|
|
73
|
+
* jest's `collectCoverageFrom` of a coverage scope `{ roots, roles, files, excludes }`: the glob of every root, every declared logic file, then
|
|
74
|
+
* the none directories inside a root (`excludes`, each a glob that ends in `**`) and the generated folders as negations.
|
|
69
75
|
*/
|
|
70
76
|
declare function collectCoverageFrom(coverage: StarciCoverageScope): string[];
|
|
71
77
|
/**
|
package/dist/index.js
CHANGED
|
@@ -37,10 +37,11 @@ Object.defineProperty(exports, "FakeIds", { enumerable: true, get: function () {
|
|
|
37
37
|
const SONAR_EXCLUSIONS = require("./sonar-exclusions.js");
|
|
38
38
|
/**
|
|
39
39
|
* Coverage is measured where logic runs: every file `<name>.<role>.ts` of a LOGIC role (service, policy, projection, guard, mapper, client, ...)
|
|
40
|
-
* inside the roots the slot manifest marks `coverage: required` (the modules of the back end: domain, integrations, platform, projections)
|
|
40
|
+
* inside the roots the slot manifest marks `coverage: required` (the modules of the back end: domain, integrations, platform, projections), and
|
|
41
|
+
* every logic file such a slot declares (`logic`: a file that carries logic without a logic-role suffix).
|
|
41
42
|
* The features (handlers, resolvers, controllers, consumers, processors, steps, sagas, cli commands) are thin by rule (BE_FEATURE_THIN) and are
|
|
42
43
|
* never measured. The scope is NOT written here: `starci app sync` renders it from the manifest (scripts/hfs/coverage-scope.mjs) into the repository's
|
|
43
|
-
* jest.config.js as `starciJestConfig({ coverage: { roots, roles, excludes } })`, and the same derivation renders Sonar's
|
|
44
|
+
* jest.config.js as `starciJestConfig({ coverage: { roots, roles, files, excludes } })`, and the same derivation renders Sonar's
|
|
44
45
|
* `sonar.coverage.exclusions` and the Codecov paths and components, so the three can never disagree. Every measured file must reach 100 on every metric.
|
|
45
46
|
* The unit run also writes `coverage/lcov.info`: Sonar and Codecov import it.
|
|
46
47
|
*/
|
|
@@ -54,15 +55,19 @@ function rootGlob(root, roles) {
|
|
|
54
55
|
return `${root}**/*.${roles.length === 1 ? roles[0] : `{${roles.join(",")}}`}.ts`;
|
|
55
56
|
}
|
|
56
57
|
/**
|
|
57
|
-
* jest's `collectCoverageFrom` of a coverage scope `{ roots, roles, excludes }`: the glob of every root,
|
|
58
|
-
* root (`excludes`, each a glob that ends in `**`) and the generated folders as negations.
|
|
58
|
+
* jest's `collectCoverageFrom` of a coverage scope `{ roots, roles, files, excludes }`: the glob of every root, every declared logic file, then
|
|
59
|
+
* the none directories inside a root (`excludes`, each a glob that ends in `**`) and the generated folders as negations.
|
|
59
60
|
*/
|
|
60
61
|
function collectCoverageFrom(coverage) {
|
|
61
|
-
return [
|
|
62
|
+
return [
|
|
63
|
+
...coverage.roots.map((root) => rootGlob(root, coverage.roles)),
|
|
64
|
+
...(coverage.files ?? []),
|
|
65
|
+
...[...coverage.excludes, ...COVERAGE_EXCLUDES].map((glob) => `!${glob}`),
|
|
66
|
+
];
|
|
62
67
|
}
|
|
63
|
-
/** One union glob keeps Jest's per-file checks while a scoped map may contain files from only one measured root. */
|
|
68
|
+
/** One union glob keeps Jest's per-file checks while a scoped map may contain files from only one measured root or declared logic file. */
|
|
64
69
|
function coverageThresholdOf(coverage, root) {
|
|
65
|
-
const rootGlobs = coverage.roots.map((entry) => rootGlob(entry, coverage.roles)).filter((glob) => hasCoverageSubjects(root, glob));
|
|
70
|
+
const rootGlobs = [...coverage.roots.map((entry) => rootGlob(entry, coverage.roles)), ...(coverage.files ?? [])].filter((glob) => hasCoverageSubjects(root, glob));
|
|
66
71
|
if (rootGlobs.length === 0)
|
|
67
72
|
return {};
|
|
68
73
|
const glob = rootGlobs.length === 1 ? rootGlobs[0] : `{${rootGlobs.join(",")}}`;
|
|
@@ -215,16 +220,20 @@ function hasCoverageSubjects(root, glob) {
|
|
|
215
220
|
const isList = (value) => Array.isArray(value) && value.every((item) => typeof item === "string" && item.length > 0);
|
|
216
221
|
/** A property of an unknown value, `undefined` when the value is not an object that has it. */
|
|
217
222
|
const fieldOf = (value, name) => (typeof value === "object" && value !== null && name in value ? Reflect.get(value, name) : undefined);
|
|
218
|
-
/**
|
|
223
|
+
/**
|
|
224
|
+
* The scope `{ roots, roles, files, excludes }` of an options object, or a refusal: the scope is rendered by `starci app sync`, never defaulted.
|
|
225
|
+
* `files` (the declared logic files) is absent from a config rendered before slots could declare them, and is then empty.
|
|
226
|
+
*/
|
|
219
227
|
function coverageOf(options) {
|
|
220
228
|
const coverage = fieldOf(options, "coverage");
|
|
221
229
|
const roots = fieldOf(coverage, "roots");
|
|
222
230
|
const roles = fieldOf(coverage, "roles");
|
|
231
|
+
const files = fieldOf(coverage, "files") ?? [];
|
|
223
232
|
const excludes = fieldOf(coverage, "excludes");
|
|
224
|
-
if (!coverage || !isList(roots) || !isList(roles) || !roles.length || !isList(excludes)) {
|
|
225
|
-
throw new Error("starciJestConfig({ coverage: { roots, roles, excludes } }) needs the coverage scope starci app sync renders from the slot manifest; run \"npx starci app sync --write\"");
|
|
233
|
+
if (!coverage || !isList(roots) || !isList(roles) || !roles.length || !isList(files) || !isList(excludes)) {
|
|
234
|
+
throw new Error("starciJestConfig({ coverage: { roots, roles, files, excludes } }) needs the coverage scope starci app sync renders from the slot manifest; run \"npx starci app sync --write\"");
|
|
226
235
|
}
|
|
227
|
-
return { roots, roles, excludes };
|
|
236
|
+
return { roots, roles, files, excludes };
|
|
228
237
|
}
|
|
229
238
|
/** The whole jest config: projects `unit`, `integration`, `e2e` and `contract`. It takes the coverage scope the managed repository config renders; without it it refuses. */
|
|
230
239
|
function starciJestConfig(options) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/jest-preset",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.2",
|
|
4
4
|
"description": "The StarCi jest config for Nest repositories (unit, integration, e2e and contract projects, ts-jest with diagnostics off, per-file 100 coverage of *.service.ts only) and the one shared unit-test double kit: typed mock<T>(), mockEntityManager(), fakeTransaction(), fakeQueryBuilder(), queueQueryBuilders(), fakeCache(), fakeLock(), recordingEventBus(), recordingQueueOutbox(), fakeInbox(), FakeClock, fakeIds(), builder() and the Outcome matchers toBeRefused/toSucceedWith.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "starci183",
|