@starci/jest-preset 2.3.6 → 2.4.0

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +1 -1
  3. package/README.md +6 -6
  4. package/{builders.cjs → dist/builders.d.ts} +2 -7
  5. package/dist/builders.js +15 -0
  6. package/dist/cache.d.ts +48 -0
  7. package/dist/cache.js +68 -0
  8. package/dist/clock.d.ts +20 -0
  9. package/dist/clock.js +32 -0
  10. package/dist/entity-manager.d.ts +114 -0
  11. package/dist/entity-manager.js +138 -0
  12. package/dist/event-bus.d.ts +52 -0
  13. package/dist/event-bus.js +106 -0
  14. package/dist/ids.d.ts +21 -0
  15. package/dist/ids.js +42 -0
  16. package/dist/inbox.d.ts +65 -0
  17. package/dist/inbox.js +173 -0
  18. package/dist/index.d.ts +114 -0
  19. package/dist/index.js +289 -0
  20. package/dist/lcov.d.ts +16 -0
  21. package/dist/lcov.js +25 -0
  22. package/dist/lock.d.ts +37 -0
  23. package/dist/lock.js +57 -0
  24. package/dist/matchers.d.ts +27 -0
  25. package/dist/matchers.js +36 -0
  26. package/dist/merge.d.ts +14 -0
  27. package/dist/merge.js +55 -0
  28. package/dist/mock.d.ts +21 -0
  29. package/dist/mock.js +52 -0
  30. package/dist/queue.d.ts +30 -0
  31. package/dist/queue.js +50 -0
  32. package/dist/sonar-exclusions.d.ts +7 -0
  33. package/dist/sonar-exclusions.js +8 -0
  34. package/dist/unit-metadata.d.ts +23 -0
  35. package/dist/unit-metadata.js +71 -0
  36. package/dist/world-runner.d.ts +42 -0
  37. package/dist/world-runner.js +142 -0
  38. package/package.json +52 -32
  39. package/builders.d.ts +0 -2
  40. package/cache.cjs +0 -67
  41. package/cache.d.ts +0 -31
  42. package/clock.cjs +0 -34
  43. package/clock.d.ts +0 -13
  44. package/entity-manager.cjs +0 -146
  45. package/entity-manager.d.ts +0 -103
  46. package/event-bus.cjs +0 -100
  47. package/event-bus.d.ts +0 -54
  48. package/ids.cjs +0 -41
  49. package/ids.d.ts +0 -13
  50. package/inbox.cjs +0 -162
  51. package/inbox.d.ts +0 -58
  52. package/index.cjs +0 -295
  53. package/index.d.ts +0 -38
  54. package/lcov.cjs +0 -25
  55. package/lock.cjs +0 -56
  56. package/lock.d.ts +0 -26
  57. package/matchers.cjs +0 -45
  58. package/matchers.d.ts +0 -17
  59. package/merge.cjs +0 -45
  60. package/mock.cjs +0 -53
  61. package/mock.d.ts +0 -12
  62. package/queue.cjs +0 -51
  63. package/queue.d.ts +0 -26
  64. package/sonar-exclusions.cjs +0 -8
  65. package/unit-metadata.cjs +0 -63
  66. package/world-runner.cjs +0 -142
  67. /package/{unit-metadata.generated.cjs → dist/unit-metadata.generated.js} +0 -0
package/inbox.d.ts DELETED
@@ -1,58 +0,0 @@
1
- /** One claim or release of a delivery. */
2
- export interface InboxEntry {
3
- readonly source: string
4
- readonly eventId: string
5
- }
6
- /** The inbox operations `failNext` can make reject. */
7
- export type InboxOperation = "claim" | "release" | "runClient" | "sweepClients"
8
-
9
- /** The client operation namespace, exact key and JSON request whose outcome the inbox stores. */
10
- export interface InboxClientOperation {
11
- readonly principalId: string
12
- readonly operation: string
13
- readonly idempotencyKey: string
14
- readonly request: unknown
15
- }
16
-
17
- /** A client claim with the manager passed by its caller. */
18
- export interface InboxClientEntry extends InboxEntry {
19
- readonly tx: unknown
20
- readonly inTransaction: boolean
21
- }
22
-
23
- /** One retention sweep with the cutoff instant and manager passed by its caller. */
24
- export interface InboxSweep {
25
- readonly cutoff: Date
26
- readonly tx: unknown
27
- readonly inTransaction: boolean
28
- }
29
-
30
- /** An `Inbox` double with the behaviour of the real claim. */
31
- export interface FakeInbox {
32
- /** True for the first claim of a (source, eventId) pair, false for every later one until it is released. */
33
- claim(source: string, eventId: string): Promise<boolean>
34
- /** Gives an ordinary delivery claim back while preserving client receipts and their outcomes. */
35
- release(source: string, eventId: string): Promise<void>
36
- /** Runs once for the client namespace and key, replays the JSON outcome and refuses a different body. */
37
- runClient<T>(request: InboxClientOperation, tx: unknown, effect: () => Promise<T>): Promise<T>
38
- /** Deletes at most one batch of completed client claims older than the cutoff. */
39
- sweepClients(cutoff: Date, tx: unknown): Promise<number>
40
- /** Marks a pair as already claimed (a redelivery). */
41
- seen(source: string, eventId: string): void
42
- /** Makes the next call of `operation` reject with `error`, once. */
43
- failNext(operation: InboxOperation, error: Error): void
44
- /** Every claim, in order, duplicates included. */
45
- readonly claims: ReadonlyArray<InboxEntry>
46
- /** The pairs held now: claimed and not released. */
47
- readonly claimed: ReadonlyArray<InboxEntry>
48
- /** Every release, in order. */
49
- readonly released: ReadonlyArray<InboxEntry>
50
- /** Every client claim with its manager; this double records transaction ownership, not database commit semantics. */
51
- readonly clientEntries: ReadonlyArray<InboxClientEntry>
52
- /** Every sweep attempt with its cutoff and transaction, including a scripted failure. */
53
- readonly sweeps: ReadonlyArray<InboxSweep>
54
- clear(): void
55
- }
56
-
57
- /** The inbox double. Pass a driven clock to test retention; omitted clocks stamp client claims at the Unix epoch. */
58
- export declare function fakeInbox(clock?: { now(): Date }): FakeInbox
package/index.cjs DELETED
@@ -1,295 +0,0 @@
1
- "use strict"
2
-
3
- const { mock, createMock } = require("./mock.cjs")
4
- const { mockEntityManager, fakeTransaction } = require("./entity-manager.cjs")
5
- const { FakeClock } = require("./clock.cjs")
6
- const { fakeCache } = require("./cache.cjs")
7
- const { fakeLock } = require("./lock.cjs")
8
- const { recordingEventBus } = require("./event-bus.cjs")
9
- const { recordingQueueOutbox } = require("./queue.cjs")
10
- const { fakeInbox } = require("./inbox.cjs")
11
- const { builder } = require("./builders.cjs")
12
- const { fakeIds, FakeIds } = require("./ids.cjs")
13
-
14
- /**
15
- * Coverage is measured where logic runs: every file `<name>.<role>.ts` of a LOGIC role (service, policy, projection, guard, mapper, client, ...)
16
- * inside the roots the slot manifest marks `coverage: required` (the modules of the back end: domain, integrations, platform, projections).
17
- * The features (handlers, resolvers, controllers, consumers, processors, steps, sagas, cli commands) are thin by rule (BE_FEATURE_THIN) and are
18
- * 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
19
- * jest.config.js as `starciJestConfig({ coverage: { roots, roles, excludes } })`, and the same derivation renders Sonar's
20
- * `sonar.coverage.exclusions` and the Codecov paths and components, so the three can never disagree. Every measured file must reach 100 on every metric.
21
- * The unit run also writes `coverage/lcov.info`: Sonar and Codecov import it.
22
- */
23
- const COVERAGE_EXCLUDES = ["**/dist/**", "**/coverage/**"]
24
- /** The four metrics, each held at 100 per file. */
25
- const COVERAGE_THRESHOLD = Object.freeze({ lines: 100, branches: 100, functions: 100, statements: 100 })
26
-
27
- /** The glob one measured root stands for: every file of a logic role below it. */
28
- function rootGlob(root, roles) {
29
- return `${root}**/*.${roles.length === 1 ? roles[0] : `{${roles.join(",")}}`}.ts`
30
- }
31
-
32
- /**
33
- * jest's `collectCoverageFrom` of a coverage scope `{ roots, roles, excludes }`: the glob of every root, then the none directories inside a
34
- * root (`excludes`, each a glob that ends in `**`) and the generated folders as negations.
35
- */
36
- function collectCoverageFrom(coverage) {
37
- return [...coverage.roots.map((root) => rootGlob(root, coverage.roles)), ...[...coverage.excludes, ...COVERAGE_EXCLUDES].map((glob) => `!${glob}`)]
38
- }
39
-
40
- /** One union glob keeps Jest's per-file checks while a scoped map may contain files from only one measured root. */
41
- function coverageThresholdOf(coverage, root) {
42
- const rootGlobs = coverage.roots.map((entry) => rootGlob(entry, coverage.roles)).filter((glob) => hasCoverageSubjects(root, glob))
43
- if (rootGlobs.length === 0) return {}
44
- const glob = rootGlobs.length === 1 ? rootGlobs[0] : `{${rootGlobs.join(",")}}`
45
- return { [`./${glob}`]: { ...COVERAGE_THRESHOLD } }
46
- }
47
-
48
- /**
49
- * Sonar `sonar.exclusions`: specs, e2e specs, dist and coverage output are not analysed at all. The list lives in sonar-exclusions.cjs, a
50
- * require-free module @starci/hfs also ships byte for byte (packages/hfs/runtime, scripts/hfs/sync-runtime.mjs), so `starci app scaffold`
51
- * renders the same exclusions before any app dependency is installed.
52
- */
53
- const SONAR_EXCLUSIONS = require("./sonar-exclusions.cjs")
54
-
55
- function sonarExclusions() {
56
- return SONAR_EXCLUSIONS.join(",")
57
- }
58
-
59
- const TS_TRANSFORM = String.raw`^.+\.ts$`
60
-
61
- /**
62
- * ts-jest without a type check (`diagnostics: false`; `isolatedModules: true` comes from @starci/tsconfig, which is where
63
- * ts-jest 29.4 reads it — the ts-jest option of the same name is deprecated): types are checked once, by `typecheck` / `typecheck:tests`, never per test file.
64
- * `npm run test:e2e` (and `test:integration`, `test:contract`) runs `npm run typecheck:tests` first so nobody runs a world spec on code that
65
- * does not type-check.
66
- */
67
- function transform(tsconfig) {
68
- return { [TS_TRANSFORM]: ["ts-jest", { tsconfig, diagnostics: false }], ...esmDependencyTransform() }
69
- }
70
-
71
- /**
72
- * The runtime dependencies of a back end that ship ECMAScript modules only: jest's CommonJS runtime cannot load them, so their
73
- * files are compiled to CommonJS like the repository's own (ts-jest with `allowJs`) instead of being skipped as node_modules.
74
- * `jose` is the Supabase access-token verifier of every lite back end (be.integrations.supabase); a lite app keeps full's unit
75
- * run (ruling:R212/case-1, amended by ruling:R212/case-4), so a spec that imports its guard must load it.
76
- */
77
- const ESM_ONLY_DEPENDENCIES = Object.freeze(["jose"])
78
- const ESM_DEPENDENCY_TRANSFORM = String.raw`^.+[\\/]node_modules[\\/](?:${ESM_ONLY_DEPENDENCIES.join("|")})[\\/].+\.js$`
79
-
80
- function esmDependencyTransform() {
81
- return { [ESM_DEPENDENCY_TRANSFORM]: ["ts-jest", { tsconfig: { allowJs: true, module: "commonjs", target: "es2022", esModuleInterop: true }, diagnostics: false }] }
82
- }
83
-
84
- /** node_modules stay untransformed except the ESM-only dependencies above. */
85
- const TRANSFORM_IGNORE_PATTERNS = Object.freeze([String.raw`[\\/]node_modules[\\/](?!(?:${ESM_ONLY_DEPENDENCIES.join("|")})[\\/])`])
86
-
87
- /** The unit project transpiles each file independently; helpers stay in tslib, source maps distinguish generated metadata, and type checks run separately. */
88
- const UNIT_COMPILER_OPTIONS = Object.freeze({ isolatedModules: true, importHelpers: true, sourceMap: true })
89
-
90
- /** Generated constructor metadata keeps its runtime guards, mapped to a generated-metadata coverage anchor instead of source parameters. */
91
- function unitTransform() {
92
- const transforms = transform({ ...UNIT_COMPILER_OPTIONS })
93
- transforms[TS_TRANSFORM][0] = require.resolve("./unit-metadata.cjs")
94
- transforms[TS_TRANSFORM][1].astTransformers = { after: [require.resolve("./unit-metadata.cjs")] }
95
- return transforms
96
- }
97
-
98
- /**
99
- * The path aliases of a StarCi back end: the same three `paths` the managed `tsconfig.json` declares, so jest resolves what
100
- * `tsc` resolves. They are part of the preset, not an option: a repository has no other alias.
101
- */
102
- const MODULE_NAME_MAPPER = Object.freeze({
103
- "^@features/(.*)$": "<rootDir>/src/features/$1",
104
- "^@modules/(.*)$": "<rootDir>/src/modules/$1",
105
- "^@tests/(.*)$": "<rootDir>/src/tests/$1",
106
- })
107
-
108
- /** The test folders under `src/tests/` that a project other than `unit` owns (owner test layout 2026-09-30). */
109
- const TEST_KIND_FOLDERS = Object.freeze(["world", "integration", "e2e", "contract"])
110
- const underTests = (folder) => String.raw`[\\/]src[\\/]tests[\\/]` + folder + String.raw`[\\/]`
111
-
112
- /**
113
- * The whole jest config of a Nest repository: projects `unit` (colocated `<name>.spec.ts`), `integration`
114
- * (`src/tests/integration/<capability>/<name>.integration-spec.ts`), `e2e` (`src/tests/e2e/<area>/<name>.e2e-spec.ts`) and
115
- * `contract` (`src/tests/contract/<provider>/<name>.contract-spec.ts`). It takes ONE option, the coverage scope: the repository's `jest.config.js` is a managed file
116
- * (R05), rendered by `starci app sync` as `module.exports = require("@starci/jest-preset").starciJestConfig({ coverage: { roots, roles, excludes } })`, the scope derived from the slot manifest.
117
- *
118
- * The managed scripts select one project each (`test` = unit, `test:integration`, `test:e2e`, `test:contract`); the
119
- * contract project is never part of `test` or `test:e2e`, and a contract spec skips itself without sandbox config. The
120
- * test tree compiles against `src/tests/tsconfig.json`, the nearest config of every file under `src/tests/`. The
121
- * integration, e2e and contract projects share the world's `global-setup.ts`/`global-teardown.ts` (`src/tests/world/`) and
122
- * run their spec files up to `min(--maxWorkers, slots)` at once, each in a worker process of its own bound to one data slot
123
- * of the test world (`world-runner.cjs`; a slot's data is reset when a file boots, so no two files ever share a slot). A declared lite app instead runs its existing integration and contract specs without a world, on jest's stock runner. The unit project alone has a `setupFilesAfterEnv` (the Outcome matchers).
124
- * Coverage is collected from the logic roles of the measured roots only (`coverage` option), with a per-file threshold of 100 on lines, branches, functions and
125
- * statements: the `test` script runs the unit project with `--coverage`, fails below it and writes `coverage/lcov.info` for Sonar. It uses v8: istanbul instruments the helpers TypeScript emits (`__decorate`, `__param`, `__awaiter`, interop wrappers)
126
- * as thousands of branches no spec can cover, while v8 measures the real source.
127
- */
128
- /** The runner of the integration, e2e and contract projects: one fresh worker process per spec file. */
129
- const WORLD_RUNNER = require.resolve("./world-runner.cjs")
130
-
131
- /** True when the repository at `root` has the test world's global setup (`src/tests/world/global-setup.ts`). */
132
- function hasTestWorld(root) {
133
- return require("node:fs").existsSync(require("node:path").join(root, "src", "tests", "world", "global-setup.ts"))
134
- }
135
-
136
- /** The managed config runs in `be/`, one level below the app's edition declaration. */
137
- function isLiteApp(root) {
138
- try {
139
- const file = require("node:path").join(root, "..", "hfs.json")
140
- return JSON.parse(require("node:fs").readFileSync(file, "utf8")).edition === "lite"
141
- } catch {
142
- return false
143
- }
144
- }
145
-
146
- /** Every alternative a `{a,b}` pattern spells, braces expanded left to right. */
147
- function braceVariants(value) {
148
- const match = /{([^{}]+)}/.exec(value)
149
- return match ? match[1].split(",").flatMap((part) => braceVariants(`${value.slice(0, match.index)}${part}${value.slice(match.index + match[0].length)}`)) : [value]
150
- }
151
-
152
- /** The anchored RegExp of one brace-free glob over a posix path: `**` spans directories, `*` and `?` stay inside a segment. */
153
- function globRegExp(glob) {
154
- let source = ""
155
- for (let i = 0; i < glob.length; i += 1) {
156
- const c = glob[i]
157
- if (c === "*" && glob[i + 1] === "*") {
158
- i += 1
159
- if (glob[i + 1] === "/") {
160
- i += 1
161
- source += "(?:.*/)?"
162
- } else source += ".*"
163
- } else if (c === "*") source += "[^/]*"
164
- else if (c === "?") source += "[^/]"
165
- else source += /[.+^${}()|[\]\\]/.test(c) ? `\\${c}` : c
166
- }
167
- return new RegExp(`^${source}$`)
168
- }
169
-
170
- /**
171
- * True when the repository at `root` holds a file the coverage glob matches: jest refuses a threshold key that matches no covered file,
172
- * and a back end may have no file of a measured root yet (no projection, no integration), so a root without files gets no key (its files,
173
- * once added, are measured and held at 100 like every other).
174
- */
175
- function hasCoverageSubjects(root, glob) {
176
- const fsx = require("node:fs")
177
- const pathx = require("node:path")
178
- const base = glob.slice(0, Math.max(0, glob.search(/[*?{]/))).replace(/[^/]*$/, "")
179
- const patterns = braceVariants(glob).map(globRegExp)
180
- const walk = (dir) => {
181
- let entries
182
- try {
183
- entries = fsx.readdirSync(pathx.join(root, dir), { withFileTypes: true })
184
- } catch {
185
- return false
186
- }
187
- return entries.some((entry) => {
188
- const rel = `${dir}${entry.name}`
189
- if (entry.isDirectory()) return !["node_modules", "dist", "coverage"].includes(entry.name) && walk(`${rel}/`)
190
- return patterns.some((pattern) => pattern.test(rel))
191
- })
192
- }
193
- return walk(base)
194
- }
195
-
196
- /** The scope `{ roots, roles, excludes }` of an options object, or a refusal: the scope is rendered by `starci app sync`, never defaulted. */
197
- function coverageOf(options) {
198
- const coverage = options?.coverage
199
- const list = (value) => Array.isArray(value) && value.every((item) => typeof item === "string" && item.length > 0)
200
- if (!coverage || !list(coverage.roots) || !list(coverage.roles) || !coverage.roles.length || !list(coverage.excludes)) {
201
- 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\"")
202
- }
203
- return coverage
204
- }
205
-
206
- function starciJestConfig(options) {
207
- const coverage = coverageOf(options)
208
- const root = process.cwd()
209
- const world = hasTestWorld(root)
210
- const shared = {
211
- preset: "ts-jest",
212
- testEnvironment: "node",
213
- rootDir: ".",
214
- roots: ["<rootDir>/src", "<rootDir>/apps"],
215
- moduleNameMapper: { ...MODULE_NAME_MAPPER },
216
- transformIgnorePatterns: [...TRANSFORM_IGNORE_PATTERNS],
217
- }
218
- const suite = (displayName, suffix) => ({
219
- ...shared,
220
- transform: transform("src/tests/tsconfig.json"),
221
- displayName,
222
- // A glob without the root's absolute path: jest's roots (src, apps) already bound the search, and a root path that holds a dot-named folder (a checkout under `.claude`) turns its backslash separator into a glob escape on Windows, so a path-prefixed pattern would match no file.
223
- testMatch: [`**/src/tests/${displayName}/**/*.${suffix}.ts`],
224
- // Full's world gives every spec file its own process and data slot; lite needs neither its runner nor its hooks.
225
- ...(world ? {
226
- runner: WORLD_RUNNER,
227
- globalSetup: "<rootDir>/src/tests/world/global-setup.ts",
228
- globalTeardown: "<rootDir>/src/tests/world/global-teardown.ts",
229
- } : {}),
230
- })
231
- const testProjects = world ? [suite("integration", "integration-spec"), suite("e2e", "e2e-spec"), suite("contract", "contract-spec")] : []
232
- if (!world && isLiteApp(root)) {
233
- for (const name of ["integration", "contract"]) {
234
- const suffix = `${name}-spec`
235
- if (hasCoverageSubjects(root, `src/tests/${name}/**/*.${suffix}.ts`)) testProjects.push(suite(name, suffix))
236
- }
237
- }
238
- // A shard of the managed CI (STARCI_COVERAGE_SHARD=1, one of npm test -- --shard=i/N) measures only the files its tests load, without the per-file threshold, and
239
- // writes istanbul JSON: the coverage job merges the shards by source location (merge.cjs) and the coverage proof enforces 100 per file on the merged report. The
240
- // all-files instrumentation of collectCoverageFrom is off there: it instruments an unloaded file with another transform (other functions and branches).
241
- const shard = process.env.STARCI_COVERAGE_SHARD === "1"
242
- return {
243
- coverageProvider: "v8",
244
- ...(shard ? {} : {
245
- collectCoverageFrom: collectCoverageFrom(coverage),
246
- // A glob key is applied to every matching file on its own: each measured file must reach 100, not the average.
247
- coverageThreshold: coverageThresholdOf(coverage, root),
248
- }),
249
- coverageDirectory: "coverage",
250
- // lcov is what Sonar imports (coverage/lcov.info); text-summary and text are what the developer reads; a shard writes the json the merge reads.
251
- coverageReporters: shard ? ["text-summary", "json"] : ["text-summary", "text", [require.resolve("./lcov.cjs"), {}]],
252
- projects: [
253
- {
254
- ...shared,
255
- transform: unitTransform(),
256
- displayName: "unit",
257
- clearMocks: true,
258
- // The Outcome matchers (`toBeRefused`, `toSucceedWith`) exist in the unit project only.
259
- setupFilesAfterEnv: [require.resolve("./matchers.cjs")],
260
- testMatch: ["**/*.spec.ts"],
261
- testPathIgnorePatterns: ["/node_modules/", ...TEST_KIND_FOLDERS.map(underTests)],
262
- },
263
- // Jest validates every project's world hooks even under --selectProjects unit. An unfinished full scaffold
264
- // keeps only unit; a lite app's present integration/contract lanes need no test world.
265
- ...testProjects,
266
- ],
267
- }
268
- }
269
-
270
- module.exports = {
271
- starciJestConfig,
272
- TEST_KIND_FOLDERS,
273
- MODULE_NAME_MAPPER,
274
- mock,
275
- createMock,
276
- mockEntityManager,
277
- fakeTransaction,
278
- FakeClock,
279
- fakeCache,
280
- fakeLock,
281
- recordingEventBus,
282
- recordingQueueOutbox,
283
- fakeInbox,
284
- builder,
285
- fakeIds,
286
- FakeIds,
287
- collectCoverageFrom,
288
- rootGlob,
289
- sonarExclusions,
290
- hasCoverageSubjects,
291
- COVERAGE_EXCLUDES,
292
- COVERAGE_THRESHOLD,
293
- UNIT_COMPILER_OPTIONS,
294
- WORLD_RUNNER,
295
- }
package/index.d.ts DELETED
@@ -1,38 +0,0 @@
1
- import type { Config } from "jest"
2
-
3
- export { mock, createMock, MockOf } from "./mock"
4
- export { mockEntityManager, fakeTransaction, MockEntityManager, FakeTransaction, EntityClass } from "./entity-manager"
5
- export { fakeCache, FakeCache } from "./cache"
6
- export { fakeLock, FakeLock } from "./lock"
7
- export { recordingEventBus, RecordingEventBus } from "./event-bus"
8
- export { recordingQueueOutbox, RecordingQueueOutbox } from "./queue"
9
- export { fakeInbox, FakeInbox } from "./inbox"
10
- export { builder } from "./builders"
11
- export { FakeClock } from "./clock"
12
- export { fakeIds, FakeIds } from "./ids"
13
- export { RefusalReason } from "./matchers"
14
-
15
- /** 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). */
16
- export interface StarciCoverageScope {
17
- readonly roots: readonly string[]
18
- readonly roles: readonly string[]
19
- readonly excludes: readonly string[]
20
- }
21
- export interface StarciJestOptions {
22
- readonly coverage: StarciCoverageScope
23
- }
24
- /** The whole jest config: projects `unit`, `integration`, `e2e` and `contract`. It takes the coverage scope the managed repository config renders; without it it refuses. */
25
- export declare function starciJestConfig(options: StarciJestOptions): Config
26
- export declare const MODULE_NAME_MAPPER: Readonly<Record<string, string>>
27
- export declare function collectCoverageFrom(coverage: StarciCoverageScope): string[]
28
- /** The glob one measured root stands for: every file of a logic role below it. */
29
- export declare function rootGlob(root: string, roles: readonly string[]): string
30
- /** True when `root` holds a file the coverage `glob` matches (a threshold key that matches nothing is refused by jest). */
31
- export declare function hasCoverageSubjects(root: string, glob: string): boolean
32
- export declare function sonarExclusions(): string
33
- export declare const COVERAGE_EXCLUDES: string[]
34
- /** The compiler options the unit project overlays on the repository tsconfig so per-file 100 coverage of a decorated service is reachable. */
35
- export declare const UNIT_COMPILER_OPTIONS: Readonly<{ isolatedModules: true; importHelpers: true; sourceMap: true }>
36
- /** The runner of the integration, e2e and contract projects (`world-runner.cjs`): every spec file in a worker process of its own. */
37
- export declare const WORLD_RUNNER: string
38
- export declare const COVERAGE_THRESHOLD: Readonly<{ lines: 100; branches: 100; functions: 100; statements: 100 }>
package/lcov.cjs DELETED
@@ -1,25 +0,0 @@
1
- "use strict"
2
-
3
- const { createRequire } = require("node:module")
4
- const fromJest = createRequire(require.resolve("jest"))
5
- const fromCore = createRequire(fromJest.resolve("@jest/core"))
6
- const fromReporters = createRequire(fromCore.resolve("@jest/reporters"))
7
- const LcovReport = fromReporters("istanbul-reports").create("lcov").constructor
8
- const METADATA_SOURCE = require.resolve("./unit-metadata.generated.cjs")
9
-
10
- /** LCOV identifies functions by name; append their stable coverage id so same-named callbacks retain separate counts. */
11
- class StarciLcovReport extends LcovReport {
12
- onDetail(node, context) {
13
- if (node.getFileCoverage().path === METADATA_SOURCE) return
14
- const entries = Object.entries(node.getFileCoverage().fnMap)
15
- const names = entries.map(([, meta]) => meta.name)
16
- try {
17
- entries.forEach(([id, meta]) => { meta.name = `${meta.name}#${id}` })
18
- super.onDetail(node, context)
19
- } finally {
20
- entries.forEach(([, meta], index) => { meta.name = names[index] })
21
- }
22
- }
23
- }
24
-
25
- module.exports = StarciLcovReport
package/lock.cjs DELETED
@@ -1,56 +0,0 @@
1
- "use strict"
2
-
3
- /**
4
- * `fakeLock(clock)` -- a behavioural in-memory double of a lease (cross-replica lock with fencing).
5
- *
6
- * `acquire({ name, holder, ttlMs, at? })` grants `{ name, holder, fence }` when nobody holds the name (or the previous
7
- * ttl ran out on the clock) and answers `null` on contention; the fence grows with every grant of one name.
8
- * `release({ grant })` lets go only when the grant is still current. A holder acquiring its own live lease renews it.
9
- * Inspection: `isHeld(name)`, `holderOf(name)`, `fenceOf(name)`. Expiry is driven by the `FakeClock`; a request may carry
10
- * its own `at`, which wins.
11
- */
12
-
13
- /** @param {{ now(): Date }} [clock] */
14
- function fakeLock(clock) {
15
- const state = new Map()
16
- const fences = new Map()
17
- const nowOf = (params) => {
18
- const at = params.at ?? clock?.now()
19
- if (at === undefined) throw new Error("fakeLock: pass the FakeClock, fakeLock(clock), or an `at` on each acquire")
20
- return at.getTime()
21
- }
22
- const liveHold = (name) => {
23
- const hold = state.get(name)
24
- if (hold === undefined) return undefined
25
- if (clock !== undefined && clock.now().getTime() >= hold.expiresAt) {
26
- state.delete(name)
27
- return undefined
28
- }
29
- return hold
30
- }
31
-
32
- return {
33
- async acquire(params) {
34
- const at = nowOf(params)
35
- const held = state.get(params.name)
36
- const alive = held !== undefined && at < held.expiresAt
37
- if (alive && held.holder !== params.holder) return null
38
- const fence = alive ? held.fence : (fences.get(params.name) ?? 0) + 1
39
- fences.set(params.name, fence)
40
- state.set(params.name, { holder: params.holder, fence, expiresAt: at + params.ttlMs })
41
- return { name: params.name, holder: params.holder, fence }
42
- },
43
- async release({ grant }) {
44
- const held = state.get(grant.name)
45
- if (held !== undefined && held.fence === grant.fence && held.holder === grant.holder) state.delete(grant.name)
46
- },
47
- /** Whether a live lease exists for `name`. */
48
- isHeld: (name) => liveHold(name) !== undefined,
49
- /** The holder of the live lease, or null. */
50
- holderOf: (name) => liveHold(name)?.holder ?? null,
51
- /** The fence of the latest grant of `name`, or null when it was never granted. */
52
- fenceOf: (name) => fences.get(name) ?? null,
53
- }
54
- }
55
-
56
- module.exports = { fakeLock }
package/lock.d.ts DELETED
@@ -1,26 +0,0 @@
1
- /** What a caller asks for when it wants a named lease. */
2
- export interface FakeLockAcquire {
3
- readonly name: string
4
- readonly holder: string
5
- readonly ttlMs: number
6
- /** The instant of the request; defaults to the clock of the double. */
7
- readonly at?: Date
8
- }
9
- /** A granted lease. */
10
- export interface FakeLockGrant {
11
- readonly name: string
12
- readonly holder: string
13
- readonly fence: number
14
- }
15
-
16
- /** A behavioural in-memory lease: acquire, contention, release, ttl expiry on the clock, growing fence. */
17
- export interface FakeLock {
18
- acquire(params: FakeLockAcquire): Promise<FakeLockGrant | null>
19
- release(params: { readonly grant: FakeLockGrant }): Promise<void>
20
- isHeld(name: string): boolean
21
- holderOf(name: string): string | null
22
- fenceOf(name: string): number | null
23
- }
24
-
25
- /** The lock double; expiry follows `clock`, so `clock.advance(ms)` frees a lease whose ttl ran out. */
26
- export declare function fakeLock(clock?: { now(): Date }): FakeLock
package/matchers.cjs DELETED
@@ -1,45 +0,0 @@
1
- "use strict"
2
-
3
- /**
4
- * Outcome matchers, registered by the `unit` project's `setupFilesAfterEnv` (nowhere else).
5
- *
6
- * They match the platform `Outcome<V, C>` structurally: `{ kind: "ok", value }` or `{ kind: "refused", code, params? }`.
7
- * `expect(outcome).toBeRefused("STOCK_EXHAUSTED")` (or `{ code, params }`) and `expect(outcome).toSucceedWith(value)`.
8
- */
9
-
10
- const isOutcome = (received) => typeof received === "object" && received !== null && (received.kind === "ok" || received.kind === "refused")
11
-
12
- const notOutcome = (utils, received) => `expected an Outcome ({ kind: "ok" | "refused" }), received ${utils.printReceived(received)}`
13
-
14
- const matchers = {
15
- toBeRefused(received, reason) {
16
- if (!isOutcome(received)) return { pass: false, message: () => notOutcome(this.utils, received) }
17
- const expected = typeof reason === "string" ? { code: reason } : reason
18
- const pass =
19
- received.kind === "refused" &&
20
- received.code === expected.code &&
21
- (!("params" in expected) || this.equals(received.params, expected.params))
22
- return {
23
- pass,
24
- message: () =>
25
- pass
26
- ? `expected the outcome not to be refused with ${this.utils.printExpected(expected)}`
27
- : `expected the outcome to be refused with ${this.utils.printExpected(expected)}, received ${this.utils.printReceived(received)}`,
28
- }
29
- },
30
- toSucceedWith(received, value) {
31
- if (!isOutcome(received)) return { pass: false, message: () => notOutcome(this.utils, received) }
32
- const pass = received.kind === "ok" && this.equals(received.value, value)
33
- return {
34
- pass,
35
- message: () =>
36
- pass
37
- ? `expected the outcome not to succeed with ${this.utils.printExpected(value)}`
38
- : `expected the outcome to succeed with ${this.utils.printExpected(value)}, received ${this.utils.printReceived(received)}`,
39
- }
40
- },
41
- }
42
-
43
- if (typeof expect !== "undefined" && typeof expect.extend === "function") expect.extend(matchers)
44
-
45
- module.exports = { matchers }
package/matchers.d.ts DELETED
@@ -1,17 +0,0 @@
1
- /// <reference types="jest" />
2
-
3
- /** The refusal a matcher expects: a capability code, or a code with the parameters of its display text. */
4
- export type RefusalReason = string | { readonly code: string; readonly params?: Record<string, unknown> }
5
-
6
- declare global {
7
- namespace jest {
8
- interface Matchers<R> {
9
- /** The received `Outcome` is `{ kind: "refused" }` with this code (and these params, when given). */
10
- toBeRefused(reason: RefusalReason): R
11
- /** The received `Outcome` is `{ kind: "ok" }` whose value equals `value` (recursive equality). */
12
- toSucceedWith(value: unknown): R
13
- }
14
- }
15
- }
16
-
17
- export declare const matchers: Record<"toBeRefused" | "toSucceedWith", (this: jest.MatcherContext, received: unknown, expected: unknown) => jest.CustomMatcherResult>
package/merge.cjs DELETED
@@ -1,45 +0,0 @@
1
- "use strict"
2
-
3
- // merge.cjs - the coverage of a sharded unit run merged into the one report the coverage proof, Codecov and Sonar read (`starci app coverage merge`).
4
- // Each shard runs with STARCI_COVERAGE_SHARD=1 (starciJestConfig): it measures ONLY the files its tests load (no collectCoverageFrom all-files
5
- // instrumentation, whose fallback transform gives an unloaded file another function and branch set) and writes istanbul JSON (coverage-final.json).
6
- // The maps of one file come from the same v8 transform in every shard; istanbul-lib-coverage merges them by source location (FileCoverage.merge), and the
7
- // merged map is written as lcov through this preset's own lcov reporter (lcov.cjs: stable function ids, the generated metadata source left out), exactly
8
- // as an unsharded run writes it. A measured file no shard loaded is absent from the merged map: the coverage proof refuses it (no lcov record, 0%).
9
- const fs = require("node:fs")
10
- const path = require("node:path")
11
- const { createRequire } = require("node:module")
12
-
13
- const fromJest = createRequire(require.resolve("jest"))
14
- const fromCore = createRequire(fromJest.resolve("@jest/core"))
15
- const fromReporters = createRequire(fromCore.resolve("@jest/reporters"))
16
- const libCoverage = fromReporters("istanbul-lib-coverage")
17
- const libReport = fromReporters("istanbul-lib-report")
18
- const StarciLcovReport = require("./lcov.cjs")
19
-
20
- /** Every coverage-final.json at or below `target` (a file or a directory), sorted. */
21
- function coverageFilesUnder(target) {
22
- if (!fs.statSync(target).isDirectory()) return [target]
23
- return fs.readdirSync(target, { recursive: true, withFileTypes: true })
24
- .filter((entry) => entry.isFile() && entry.name === "coverage-final.json")
25
- .map((entry) => path.join(entry.parentPath ?? entry.path, entry.name))
26
- .sort()
27
- }
28
-
29
- /**
30
- * Merge the shard reports `inputs` (coverage-final.json files or directories holding them) and write `<outDir>/lcov.info` and `<outDir>/coverage-final.json`.
31
- * Returns { reports, files }; throws when no report is found (an empty merge would read as "nothing to measure").
32
- */
33
- function mergeCoverage({ inputs, outDir }) {
34
- const reports = inputs.flatMap((input) => coverageFilesUnder(input))
35
- if (!reports.length) throw new Error(`no coverage-final.json under ${inputs.join(", ")}: the unit shards wrote none (run them with STARCI_COVERAGE_SHARD=1)`)
36
- const map = libCoverage.createCoverageMap({})
37
- for (const report of reports) map.merge(JSON.parse(fs.readFileSync(report, "utf8")))
38
- fs.mkdirSync(outDir, { recursive: true })
39
- fs.writeFileSync(path.join(outDir, "coverage-final.json"), JSON.stringify(map.toJSON()))
40
- const context = libReport.createContext({ dir: outDir, coverageMap: map })
41
- new StarciLcovReport({ file: "lcov.info", projectRoot: process.cwd() }).execute(context)
42
- return { reports: reports.length, files: map.files().length }
43
- }
44
-
45
- module.exports = { mergeCoverage, coverageFilesUnder }