@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 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" } }))
@@ -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";
@@ -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" || WRITE_SQL.test(String(args[0])))
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
- /** The coverage scope `starci app sync` renders from the slot manifest: the measured roots (a directory pattern per measured slot, a placeholder folder written as a star), the logic roles measured in them and the directories inside a root that are never measured (each ending in a double star). */
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, then the none directories inside a
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, then the none directories inside a
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 [...coverage.roots.map((root) => rootGlob(root, coverage.roles)), ...[...coverage.excludes, ...COVERAGE_EXCLUDES].map((glob) => `!${glob}`)];
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
- /** The scope `{ roots, roles, excludes }` of an options object, or a refusal: the scope is rendered by `starci app sync`, never defaulted. */
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.0",
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",