cursedbelt-core 1.0.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.
- package/LICENSE +15 -0
- package/dist/client/wsWire.d.ts +49 -0
- package/dist/client/wsWire.js +57 -0
- package/dist/config/appConfig.d.ts +361 -0
- package/dist/config/appConfig.js +308 -0
- package/dist/core/activity/index.d.ts +8 -0
- package/dist/core/activity/index.js +7 -0
- package/dist/core/activity/model.d.ts +115 -0
- package/dist/core/activity/model.js +63 -0
- package/dist/core/analytics/index.d.ts +18 -0
- package/dist/core/analytics/index.js +18 -0
- package/dist/core/analytics/range.d.ts +186 -0
- package/dist/core/analytics/range.js +271 -0
- package/dist/core/analytics/schedule.d.ts +301 -0
- package/dist/core/analytics/schedule.js +372 -0
- package/dist/core/category/index.d.ts +3 -0
- package/dist/core/category/index.js +6 -0
- package/dist/core/category/memoryStore.d.ts +21 -0
- package/dist/core/category/memoryStore.js +97 -0
- package/dist/core/category/tree.d.ts +26 -0
- package/dist/core/category/tree.js +113 -0
- package/dist/core/category/types.d.ts +48 -0
- package/dist/core/category/types.js +8 -0
- package/dist/core/color.d.ts +14 -0
- package/dist/core/color.js +32 -0
- package/dist/core/colorScheme/colorSchemeStore.d.ts +70 -0
- package/dist/core/colorScheme/colorSchemeStore.js +123 -0
- package/dist/core/colorScheme/index.d.ts +7 -0
- package/dist/core/colorScheme/index.js +7 -0
- package/dist/core/domainOwners.d.ts +19 -0
- package/dist/core/domainOwners.js +15 -0
- package/dist/core/download.d.ts +14 -0
- package/dist/core/download.js +37 -0
- package/dist/core/events/ccEventBus.d.ts +44 -0
- package/dist/core/events/ccEventBus.js +70 -0
- package/dist/core/file-tree/fileTreeModel.d.ts +276 -0
- package/dist/core/file-tree/fileTreeModel.js +312 -0
- package/dist/core/folder-tree/actions.d.ts +84 -0
- package/dist/core/folder-tree/actions.js +119 -0
- package/dist/core/folder-tree/index.d.ts +3 -0
- package/dist/core/folder-tree/index.js +20 -0
- package/dist/core/folder-tree/path.d.ts +76 -0
- package/dist/core/folder-tree/path.js +146 -0
- package/dist/core/folder-tree/tree.d.ts +86 -0
- package/dist/core/folder-tree/tree.js +184 -0
- package/dist/core/layout/index.d.ts +1 -0
- package/dist/core/layout/index.js +7 -0
- package/dist/core/master-lock/index.d.ts +14 -0
- package/dist/core/master-lock/index.js +14 -0
- package/dist/core/master-lock/kdf.d.ts +63 -0
- package/dist/core/master-lock/kdf.js +117 -0
- package/dist/core/master-lock/policy.d.ts +32 -0
- package/dist/core/master-lock/policy.js +52 -0
- package/dist/core/master-lock/presence.d.ts +52 -0
- package/dist/core/master-lock/presence.js +61 -0
- package/dist/core/master-lock/wire.d.ts +139 -0
- package/dist/core/master-lock/wire.js +81 -0
- package/dist/core/media/bgRemoval.d.ts +27 -0
- package/dist/core/media/bgRemoval.js +101 -0
- package/dist/core/media/ffmpeg.d.ts +57 -0
- package/dist/core/media/ffmpeg.js +126 -0
- package/dist/core/media/format.d.ts +20 -0
- package/dist/core/media/format.js +54 -0
- package/dist/core/media/index.d.ts +12 -0
- package/dist/core/media/index.js +18 -0
- package/dist/core/media/mediaItem.d.ts +15 -0
- package/dist/core/media/mediaItem.js +34 -0
- package/dist/core/media/renderPlan.d.ts +63 -0
- package/dist/core/media/renderPlan.js +232 -0
- package/dist/core/media/segment.d.ts +79 -0
- package/dist/core/media/segment.js +247 -0
- package/dist/core/media/size.d.ts +1 -0
- package/dist/core/media/size.js +3 -0
- package/dist/core/media/trim.d.ts +21 -0
- package/dist/core/media/trim.js +40 -0
- package/dist/core/media/types.d.ts +148 -0
- package/dist/core/media/types.js +11 -0
- package/dist/core/media/uploadLimits.d.ts +7 -0
- package/dist/core/media/uploadLimits.js +16 -0
- package/dist/core/media/validate.d.ts +9 -0
- package/dist/core/media/validate.js +121 -0
- package/dist/core/media/videoCuts.d.ts +152 -0
- package/dist/core/media/videoCuts.js +284 -0
- package/dist/core/navigation/breadcrumbCompat.d.ts +24 -0
- package/dist/core/navigation/breadcrumbCompat.js +21 -0
- package/dist/core/navigation/coverageTest.d.ts +24 -0
- package/dist/core/navigation/coverageTest.js +48 -0
- package/dist/core/navigation/index.d.ts +6 -0
- package/dist/core/navigation/index.js +10 -0
- package/dist/core/navigation/migrateNavPreferences.d.ts +55 -0
- package/dist/core/navigation/migrateNavPreferences.js +63 -0
- package/dist/core/navigation/navigationModel.d.ts +165 -0
- package/dist/core/navigation/navigationModel.js +14 -0
- package/dist/core/navigation/navigationSelectors.d.ts +63 -0
- package/dist/core/navigation/navigationSelectors.js +167 -0
- package/dist/core/navigation/searchCatalogue.d.ts +23 -0
- package/dist/core/navigation/searchCatalogue.js +76 -0
- package/dist/core/net/reconnectSchedule.d.ts +46 -0
- package/dist/core/net/reconnectSchedule.js +38 -0
- package/dist/core/palette/ShortcutRegistry.d.ts +33 -0
- package/dist/core/palette/ShortcutRegistry.js +52 -0
- package/dist/core/palette/fuzzyRank.d.ts +18 -0
- package/dist/core/palette/fuzzyRank.js +73 -0
- package/dist/core/palette/hotkeys.d.ts +28 -0
- package/dist/core/palette/hotkeys.js +118 -0
- package/dist/core/palette/index.d.ts +3 -0
- package/dist/core/palette/index.js +8 -0
- package/dist/core/schema/projection.d.ts +84 -0
- package/dist/core/schema/projection.js +184 -0
- package/dist/core/session/claims.d.ts +145 -0
- package/dist/core/session/claims.js +157 -0
- package/dist/core/sharing/index.d.ts +5 -0
- package/dist/core/sharing/index.js +4 -0
- package/dist/core/sharing/model.d.ts +195 -0
- package/dist/core/sharing/model.js +150 -0
- package/dist/core/site/siteCopy.d.ts +92 -0
- package/dist/core/site/siteCopy.js +91 -0
- package/dist/core/slots.d.ts +22 -0
- package/dist/core/slots.js +31 -0
- package/dist/core/styles/constructableSheet.d.ts +51 -0
- package/dist/core/styles/constructableSheet.js +80 -0
- package/dist/core/tokens/registry.d.ts +119 -0
- package/dist/core/tokens/registry.js +447 -0
- package/dist/core/uiScale/index.d.ts +8 -0
- package/dist/core/uiScale/index.js +8 -0
- package/dist/core/uiScale/scale.d.ts +36 -0
- package/dist/core/uiScale/scale.js +47 -0
- package/dist/core/uiScale/uiScaleStore.d.ts +47 -0
- package/dist/core/uiScale/uiScaleStore.js +63 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.d.ts +1 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +23 -0
- package/dist/core/wire/index.d.ts +17 -0
- package/dist/core/wire/index.js +17 -0
- package/dist/core/wire/ingress.d.ts +44 -0
- package/dist/core/wire/ingress.js +134 -0
- package/dist/core/wire/marshalWorker.d.ts +70 -0
- package/dist/core/wire/marshalWorker.js +273 -0
- package/dist/core/wire/marshalling.d.ts +122 -0
- package/dist/core/wire/marshalling.js +126 -0
- package/dist/core/wire/wireHub.d.ts +80 -0
- package/dist/core/wire/wireHub.js +116 -0
- package/dist/ctgr/base64url.d.ts +8 -0
- package/dist/ctgr/base64url.js +39 -0
- package/dist/ctgr/chunkAccumulator.d.ts +214 -0
- package/dist/ctgr/chunkAccumulator.js +307 -0
- package/dist/ctgr/chunkStore.d.ts +24 -0
- package/dist/ctgr/chunkStore.js +125 -0
- package/dist/ctgr/codec.d.ts +44 -0
- package/dist/ctgr/codec.js +153 -0
- package/dist/ctgr/compress.d.ts +2 -0
- package/dist/ctgr/compress.js +20 -0
- package/dist/ctgr/crc32.d.ts +2 -0
- package/dist/ctgr/crc32.js +23 -0
- package/dist/ctgr/digest.d.ts +1 -0
- package/dist/ctgr/digest.js +15 -0
- package/dist/ctgr/index.d.ts +9 -0
- package/dist/ctgr/index.js +17 -0
- package/dist/ctgr/types.d.ts +64 -0
- package/dist/ctgr/types.js +21 -0
- package/dist/ctgr/v0compat.d.ts +27 -0
- package/dist/ctgr/v0compat.js +128 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/shared/errors.d.ts +101 -0
- package/dist/shared/errors.js +99 -0
- package/dist/testing/bytes.d.ts +32 -0
- package/dist/testing/bytes.js +33 -0
- package/dist/testing/domInspect.d.ts +59 -0
- package/dist/testing/domInspect.js +115 -0
- package/dist/testing/fakeFetch.d.ts +23 -0
- package/dist/testing/fakeFetch.js +21 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +11 -0
- package/package.json +373 -0
- package/src/client/wsWire.ts +83 -0
- package/src/config/appConfig.spec.ts +158 -0
- package/src/config/appConfig.ts +372 -0
- package/src/core/activity/index.ts +21 -0
- package/src/core/activity/model.ts +160 -0
- package/src/core/analytics/index.ts +18 -0
- package/src/core/analytics/range.spec.ts +280 -0
- package/src/core/analytics/range.ts +383 -0
- package/src/core/analytics/schedule.spec.ts +311 -0
- package/src/core/analytics/schedule.ts +624 -0
- package/src/core/category/category.spec.ts +144 -0
- package/src/core/category/index.ts +19 -0
- package/src/core/category/memoryStore.ts +104 -0
- package/src/core/category/tree.ts +118 -0
- package/src/core/category/types.ts +59 -0
- package/src/core/color.spec.ts +35 -0
- package/src/core/color.ts +35 -0
- package/src/core/colorScheme/colorScheme.spec.ts +127 -0
- package/src/core/colorScheme/colorSchemeStore.ts +184 -0
- package/src/core/colorScheme/index.ts +23 -0
- package/src/core/domainOwners.ts +32 -0
- package/src/core/download.spec.ts +38 -0
- package/src/core/download.ts +35 -0
- package/src/core/events/ccEventBus.spec.ts +90 -0
- package/src/core/events/ccEventBus.ts +120 -0
- package/src/core/file-tree/fileTreeModel.spec.ts +462 -0
- package/src/core/file-tree/fileTreeModel.ts +511 -0
- package/src/core/folder-tree/actions.spec.ts +103 -0
- package/src/core/folder-tree/actions.ts +153 -0
- package/src/core/folder-tree/index.ts +46 -0
- package/src/core/folder-tree/path.spec.ts +113 -0
- package/src/core/folder-tree/path.ts +148 -0
- package/src/core/folder-tree/tree.spec.ts +157 -0
- package/src/core/folder-tree/tree.ts +244 -0
- package/src/core/layout/index.ts +7 -0
- package/src/core/master-lock/index.ts +50 -0
- package/src/core/master-lock/kdf.spec.ts +95 -0
- package/src/core/master-lock/kdf.ts +145 -0
- package/src/core/master-lock/policy.ts +55 -0
- package/src/core/master-lock/presence.spec.ts +73 -0
- package/src/core/master-lock/presence.ts +71 -0
- package/src/core/master-lock/wire.ts +155 -0
- package/src/core/media/bgRemoval.spec.ts +120 -0
- package/src/core/media/bgRemoval.ts +123 -0
- package/src/core/media/ffmpeg.spec.ts +135 -0
- package/src/core/media/ffmpeg.ts +162 -0
- package/src/core/media/format.spec.ts +41 -0
- package/src/core/media/format.ts +58 -0
- package/src/core/media/index.ts +105 -0
- package/src/core/media/mediaItem.spec.ts +46 -0
- package/src/core/media/mediaItem.ts +40 -0
- package/src/core/media/renderPlan.spec.ts +140 -0
- package/src/core/media/renderPlan.ts +318 -0
- package/src/core/media/segment.spec.ts +159 -0
- package/src/core/media/segment.ts +352 -0
- package/src/core/media/size.spec.ts +19 -0
- package/src/core/media/size.ts +3 -0
- package/src/core/media/trim.spec.ts +75 -0
- package/src/core/media/trim.ts +60 -0
- package/src/core/media/types.ts +147 -0
- package/src/core/media/uploadLimits.ts +18 -0
- package/src/core/media/validate.spec.ts +106 -0
- package/src/core/media/validate.ts +131 -0
- package/src/core/media/videoCuts.spec.ts +327 -0
- package/src/core/media/videoCuts.ts +355 -0
- package/src/core/navigation/breadcrumbCompat.ts +44 -0
- package/src/core/navigation/coverageTest.ts +54 -0
- package/src/core/navigation/index.ts +50 -0
- package/src/core/navigation/migrateNavPreferences.ts +119 -0
- package/src/core/navigation/navigation.spec.ts +388 -0
- package/src/core/navigation/navigationModel.ts +185 -0
- package/src/core/navigation/navigationSelectors.ts +206 -0
- package/src/core/navigation/searchCatalogue.ts +89 -0
- package/src/core/net/reconnectSchedule.spec.ts +49 -0
- package/src/core/net/reconnectSchedule.ts +78 -0
- package/src/core/palette/ShortcutRegistry.ts +69 -0
- package/src/core/palette/fuzzyRank.ts +78 -0
- package/src/core/palette/hotkeys.ts +132 -0
- package/src/core/palette/index.ts +22 -0
- package/src/core/palette/palette.spec.ts +160 -0
- package/src/core/schema/projection.spec.ts +122 -0
- package/src/core/schema/projection.ts +225 -0
- package/src/core/session/claims.ts +181 -0
- package/src/core/sharing/index.ts +33 -0
- package/src/core/sharing/model.spec.ts +104 -0
- package/src/core/sharing/model.ts +296 -0
- package/src/core/site/siteCopy.ts +115 -0
- package/src/core/slots.ts +38 -0
- package/src/core/styles/constructableSheet.ts +86 -0
- package/src/core/tokens/registry.spec.ts +160 -0
- package/src/core/tokens/registry.ts +536 -0
- package/src/core/uiScale/index.ts +32 -0
- package/src/core/uiScale/scale.ts +63 -0
- package/src/core/uiScale/uiScale.spec.ts +77 -0
- package/src/core/uiScale/uiScaleStore.ts +100 -0
- package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +24 -0
- package/src/core/wire/index.ts +56 -0
- package/src/core/wire/ingress.spec.ts +276 -0
- package/src/core/wire/ingress.ts +182 -0
- package/src/core/wire/marshalWorker.spec.ts +196 -0
- package/src/core/wire/marshalWorker.ts +361 -0
- package/src/core/wire/marshalling.spec.ts +139 -0
- package/src/core/wire/marshalling.ts +238 -0
- package/src/core/wire/wireHub.spec.ts +137 -0
- package/src/core/wire/wireHub.ts +213 -0
- package/src/ctgr/base64url.ts +44 -0
- package/src/ctgr/chunkAccumulator.ts +403 -0
- package/src/ctgr/chunkStore.spec.ts +101 -0
- package/src/ctgr/chunkStore.ts +168 -0
- package/src/ctgr/codec.spec.ts +208 -0
- package/src/ctgr/codec.ts +202 -0
- package/src/ctgr/compress.ts +21 -0
- package/src/ctgr/crc32.ts +23 -0
- package/src/ctgr/digest.ts +14 -0
- package/src/ctgr/index.ts +39 -0
- package/src/ctgr/types.ts +80 -0
- package/src/ctgr/v0compat.spec.ts +73 -0
- package/src/ctgr/v0compat.ts +156 -0
- package/src/declaredDepsAreImported.spec.ts +87 -0
- package/src/index.ts +25 -0
- package/src/leafSubpathsImportNothing.spec.ts +287 -0
- package/src/lockfileIsTracked.spec.ts +50 -0
- package/src/namedSubpathsResolve.spec.ts +122 -0
- package/src/noPathDeps.spec.ts +24 -0
- package/src/publishShape.spec.ts +89 -0
- package/src/shared/errors.ts +153 -0
- package/src/skippedTestsAreAnswered.spec.ts +221 -0
- package/src/testTmpRoot.spec.ts +102 -0
- package/src/testing/bytes.ts +38 -0
- package/src/testing/domInspect.spec.ts +79 -0
- package/src/testing/domInspect.ts +127 -0
- package/src/testing/fakeFetch.ts +26 -0
- package/src/testing/index.ts +11 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import pkg from '../package.json';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every `cursedbelt/<subpath>` this repo NAMES must be one it actually exports
|
|
6
|
+
* (2026-09-13).
|
|
7
|
+
*
|
|
8
|
+
* Removing the 23 subpaths nothing referenced left 19 sentences in `src` and
|
|
9
|
+
* `docs` still advertising them — `cursedbelt/ctgr`, `cursedbelt/server/media`,
|
|
10
|
+
* `cursedbelt/tokens` and friends, each one a copy-pasteable import that now
|
|
11
|
+
* throws ERR_PACKAGE_PATH_NOT_EXPORTED. Nothing could see them, because a
|
|
12
|
+
* subpath named in prose is invisible to `tsc` (it is a comment), invisible to
|
|
13
|
+
* Biome (it is a string), and invisible to `check-paths.ts` (it is not a
|
|
14
|
+
* filesystem path). The only reason those 19 were found is that one agent
|
|
15
|
+
* grepped for them by hand while deleting the exports.
|
|
16
|
+
*
|
|
17
|
+
* That is the drift this file exists to make impossible: the manifest and the
|
|
18
|
+
* prose describing it can no longer disagree without something going red.
|
|
19
|
+
*
|
|
20
|
+
* 🔴 Bun.Glob + Bun.file, NOT node:fs — this package's test preload virtualizes
|
|
21
|
+
* node:fs, so a readdirSync/readFileSync scan reads MOCK data and passes against
|
|
22
|
+
* files it never opened. Same trap as publishShape.spec.ts; see its header.
|
|
23
|
+
*
|
|
24
|
+
* Escape hatch: a line that must name a removed subpath (a changelog entry, a
|
|
25
|
+
* migration note) carries `subpath-check:ignore` and is skipped.
|
|
26
|
+
*/
|
|
27
|
+
const ROOTS = ['src', 'scripts', 'docs'] as const;
|
|
28
|
+
|
|
29
|
+
const BINARY = /\.(png|jpe?g|gif|webp|avif|ico|mp4|webm|mp3|wav|pdf|zip|gz|woff2?|ttf|otf|eot|wasm)$/i;
|
|
30
|
+
|
|
31
|
+
const IGNORE = 'subpath-check:ignore';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `cursedbelt/<subpath>` wherever it appears — code, comment or markdown. The
|
|
35
|
+
* trailing class stops `cursedbelt/react` from matching inside
|
|
36
|
+
* `cursedbelt/react/timeline`, so a removed deep subpath cannot hide behind a
|
|
37
|
+
* live shallow one.
|
|
38
|
+
*
|
|
39
|
+
* The leading `(?<![\w/.-])` is what separates an IMPORT SPECIFIER from a REPO
|
|
40
|
+
* PATH: `packages/cursedbelt/src/...` and `../cursedbelt/scripts/...` name files
|
|
41
|
+
* on disk (check-paths.ts' job), not subpaths, and flagging them would make this
|
|
42
|
+
* gate unpassable and therefore useless.
|
|
43
|
+
*/
|
|
44
|
+
// Parameterized on pkg.name since the 2026-09-15 split: in THIS repo a bare
|
|
45
|
+
// `cursedbelt/...` mention names the react package's surface, not ours, and
|
|
46
|
+
// only `cursedbelt-core/...` mentions are checkable against our own manifest.
|
|
47
|
+
const NAMED = new RegExp(String.raw`(?<![\w/.-])${pkg.name}(\/[a-z0-9][a-z0-9._/-]*[a-z0-9])`, 'gi');
|
|
48
|
+
|
|
49
|
+
/** First segment is a directory in THIS repo ⇒ the mention is a file path. */
|
|
50
|
+
const REPO_DIRS = /^\/(src|scripts|dist|docs|e2e|demo|node_modules|releases)(\/|$)/;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Ends in a file extension ⇒ a file path, not a subpath — except the three CSS
|
|
54
|
+
* entries, which genuinely ARE exports (`cursedbelt/styles.css`).
|
|
55
|
+
*/
|
|
56
|
+
const FILE_EXT = /\.(?!css$)[a-z0-9]+$/i;
|
|
57
|
+
|
|
58
|
+
const exported = new Set(Object.keys(pkg.exports).map((k) => k.replace(/^\./, '')));
|
|
59
|
+
|
|
60
|
+
// This file and the task/report prose quoting the removal are allowed to name them.
|
|
61
|
+
const SELF = 'namedSubpathsResolve.spec.ts';
|
|
62
|
+
|
|
63
|
+
type Offence = { file: string; line: number; subpath: string; text: string };
|
|
64
|
+
|
|
65
|
+
let cache: Promise<Offence[]> | undefined;
|
|
66
|
+
const offences = (): Promise<Offence[]> => {
|
|
67
|
+
cache ??= (async () => {
|
|
68
|
+
const repo = `${import.meta.dir}/..`;
|
|
69
|
+
const found: Offence[] = [];
|
|
70
|
+
for (const root of ROOTS) {
|
|
71
|
+
const dir = `${repo}/${root}`;
|
|
72
|
+
for (const rel of new Bun.Glob('**/*').scanSync({ cwd: dir, onlyFiles: true })) {
|
|
73
|
+
if (BINARY.test(rel) || rel.endsWith(SELF)) continue;
|
|
74
|
+
const text = await Bun.file(`${dir}/${rel}`).text();
|
|
75
|
+
const lines = text.split('\n');
|
|
76
|
+
for (let i = 0; i < lines.length; i++) {
|
|
77
|
+
const line = lines[i] ?? '';
|
|
78
|
+
if (line.includes(IGNORE)) continue;
|
|
79
|
+
for (const m of line.matchAll(NAMED)) {
|
|
80
|
+
const subpath = m[1] ?? '';
|
|
81
|
+
if (exported.has(subpath)) continue;
|
|
82
|
+
if (REPO_DIRS.test(subpath) || FILE_EXT.test(subpath)) continue;
|
|
83
|
+
found.push({ file: `${root}/${rel}`, line: i + 1, subpath, text: line.trim() });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return found;
|
|
89
|
+
})();
|
|
90
|
+
return cache;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
describe('subpaths named in this repo resolve', () => {
|
|
94
|
+
it('has scanned a real source tree', async () => {
|
|
95
|
+
// Guards the whole file: if the glob or the fs mock ever returns nothing,
|
|
96
|
+
// the assertion below passes vacuously and the gate quietly dies.
|
|
97
|
+
const repo = `${import.meta.dir}/..`;
|
|
98
|
+
let seen = 0;
|
|
99
|
+
for (const root of ROOTS) {
|
|
100
|
+
for (const _ of new Bun.Glob('**/*').scanSync({ cwd: `${repo}/${root}`, onlyFiles: true })) seen++;
|
|
101
|
+
}
|
|
102
|
+
expect(seen).toBeGreaterThan(100);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('knows what the manifest exports', () => {
|
|
106
|
+
// If `exports` is ever read wrong, every offence below silently becomes one.
|
|
107
|
+
expect(exported.has('')).toBe(true); // the "." root entry
|
|
108
|
+
expect(exported.has('/master-lock')).toBe(true);
|
|
109
|
+
expect(exported.has('/react')).toBe(false); // the react tier stayed in `cursedbelt` at the 2026-09-15 split
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('names no subpath package.json does not export', async () => {
|
|
113
|
+
const dead = await offences();
|
|
114
|
+
expect(
|
|
115
|
+
dead,
|
|
116
|
+
`these name a cursedbelt subpath that is not in package.json exports — a consumer who\n` +
|
|
117
|
+
`copies one gets ERR_PACKAGE_PATH_NOT_EXPORTED. Point at the surviving subpath, or at\n` +
|
|
118
|
+
`the repo path, or mark the line \`${IGNORE}\`:\n` +
|
|
119
|
+
dead.map((d) => ` ${d.file}:${d.line} cursedbelt${d.subpath}\n ${d.text}`).join('\n'),
|
|
120
|
+
).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import pkg from '../package.json';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rule 1 of `~/code` (2026-09-12): share by published version, never by path.
|
|
6
|
+
*
|
|
7
|
+
* `file:`, `link:`, `workspace:*` and a directory-valued `overrides` entry each
|
|
8
|
+
* defeat `bun install --frozen-lockfile`, and a `file:` install is served from a
|
|
9
|
+
* cache keyed on the VERSION STRING — so re-packing the same version silently
|
|
10
|
+
* ships stale bytes straight through `bun install --force`. That is the defect
|
|
11
|
+
* that made a `cursed-satellites` change cost twenty minutes; this is the line
|
|
12
|
+
* that stops it coming back one convenient shortcut at a time.
|
|
13
|
+
*/
|
|
14
|
+
const PATHY = /^(file:|link:|workspace:|portal:)/;
|
|
15
|
+
|
|
16
|
+
describe('no path dependencies', () => {
|
|
17
|
+
for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'overrides'] as const) {
|
|
18
|
+
it(`${field} names no path-valued spec`, () => {
|
|
19
|
+
const block = (pkg as Record<string, unknown>)[field] as Record<string, string> | undefined;
|
|
20
|
+
const bad = Object.entries(block ?? {}).filter(([, v]) => typeof v === 'string' && PATHY.test(v));
|
|
21
|
+
expect(bad, `share by published version, never by path:\n ${bad.map(([k, v]) => `${k} = ${v}`).join('\n ')}`).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import pkg from '../package.json';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What this exists for (2026-09-12).
|
|
6
|
+
*
|
|
7
|
+
* Two ways this package can publish something that installs clean and does not
|
|
8
|
+
* work, both found the hard way while extracting it from the monorepo:
|
|
9
|
+
*
|
|
10
|
+
* 1. `exports` resolves the `bun` and `source` conditions to `./src/*.ts`, but
|
|
11
|
+
* `files` shipped only `dist`. Every consumer runs bun, so every subpath
|
|
12
|
+
* resolved to a path that was not in the tarball. Invisible under a `file:`
|
|
13
|
+
* link, because the whole directory was there.
|
|
14
|
+
*
|
|
15
|
+
* 2. A `tsc` run with no tsconfig falls back to defaults and emits `.js`/`.d.ts`
|
|
16
|
+
* NEXT TO the sources. Those get packed, and a consumer's bun condition then
|
|
17
|
+
* loads the stale `.js` instead of the `.ts` — a second module instance, so
|
|
18
|
+
* every module-level singleton silently doubles.
|
|
19
|
+
*
|
|
20
|
+
* Both are one assertion each, and both fail loudly at `bun test` rather than at
|
|
21
|
+
* a consumer's runtime three repos away.
|
|
22
|
+
*/
|
|
23
|
+
// 🔴 Bun.Glob, NOT node:fs: this package's own test preload virtualizes node:fs
|
|
24
|
+
// (`[cwip/testing] System mocks enabled`), so readdirSync here returns MOCK data
|
|
25
|
+
// and the assertion below passes against a directory it never read. Cost an hour
|
|
26
|
+
// to find; the scan must not go through a mocked module.
|
|
27
|
+
const emittedInSrc = (): string[] => {
|
|
28
|
+
const all = new Set([...new Bun.Glob('**/*').scanSync({ cwd: import.meta.dir })]);
|
|
29
|
+
// A hand-written ambient declaration (css.d.ts) has no .ts sibling and is
|
|
30
|
+
// legitimate; an EMITTED one always sits next to the source it came from.
|
|
31
|
+
return [...all].filter((f) => {
|
|
32
|
+
if (f.endsWith('.js') || f.endsWith('.js.map')) return true;
|
|
33
|
+
if (!f.endsWith('.d.ts')) return false;
|
|
34
|
+
return all.has(`${f.slice(0, -5)}.ts`) || all.has(`${f.slice(0, -5)}.tsx`);
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe('publish shape', () => {
|
|
39
|
+
it('ships every directory its exports resolve into', () => {
|
|
40
|
+
const files = (pkg as { files?: string[] }).files ?? [];
|
|
41
|
+
const roots = new Set<string>();
|
|
42
|
+
for (const entry of Object.values((pkg as { exports: Record<string, unknown> }).exports)) {
|
|
43
|
+
if (typeof entry !== 'object' || entry === null) continue;
|
|
44
|
+
for (const target of Object.values(entry as Record<string, string>)) {
|
|
45
|
+
if (typeof target === 'string' && target.startsWith('./')) {
|
|
46
|
+
const top = target.slice(2).split('/')[0];
|
|
47
|
+
if (top !== undefined) roots.add(top);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const root of roots) {
|
|
52
|
+
expect(
|
|
53
|
+
files.some((f) => f === root || f.startsWith(`${root}/`)),
|
|
54
|
+
`exports resolve into "${root}/" but package.json files does not ship it`,
|
|
55
|
+
).toBe(true);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('has no compiled output sitting in src', () => {
|
|
60
|
+
const emitted = emittedInSrc();
|
|
61
|
+
expect(emitted, `tsc emitted into src instead of dist:\n ${emitted.slice(0, 10).join('\n ')}`).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('declares bin paths npm will actually keep', () => {
|
|
65
|
+
// npm rejects a "./"-prefixed bin value, calls the NAME invalid, and drops
|
|
66
|
+
// the entry — with a warning, not an error. The package publishes fine and
|
|
67
|
+
// installs with no CLI at all. Invisible under a file: link, where the bin
|
|
68
|
+
// is linked from the source manifest before npm ever normalises it.
|
|
69
|
+
const bin = (pkg as { bin?: Record<string, string> }).bin ?? {};
|
|
70
|
+
const prefixed = Object.entries(bin).filter(([, v]) => v.startsWith('./'));
|
|
71
|
+
expect(
|
|
72
|
+
prefixed,
|
|
73
|
+
`npm drops "./"-prefixed bin entries on publish:\n ${prefixed.map(([k, v]) => `${k} = ${v}`).join('\n ')}`,
|
|
74
|
+
).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('lists "types" first in every conditional export', () => {
|
|
78
|
+
// Condition order is significant and "types" must win, or tsc resolves the
|
|
79
|
+
// `bun`/`source` condition and typechecks our shipped .ts instead of the
|
|
80
|
+
// .d.ts. A consumer then inherits type errors from OUR optional peers —
|
|
81
|
+
// cursedbelt could not typecheck against cwip because `unpdf`, an optional
|
|
82
|
+
// peer it does not install, was reachable through cwip/src.
|
|
83
|
+
const wrong = Object.entries((pkg as { exports: Record<string, unknown> }).exports)
|
|
84
|
+
.filter(([, e]) => typeof e === 'object' && e !== null && 'types' in e)
|
|
85
|
+
.filter(([, e]) => Object.keys(e as object)[0] !== 'types')
|
|
86
|
+
.map(([k]) => k);
|
|
87
|
+
expect(wrong, `"types" must be the first condition:\n ${wrong.join('\n ')}`).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical server error class + error/validation envelope for the whole
|
|
3
|
+
* monorepo — cursedbelt apps AND cursedshell plugins.
|
|
4
|
+
*
|
|
5
|
+
* Every API failure — thrown deep in a handler, surfaced by middleware, or raised
|
|
6
|
+
* by a plugin service — is shaped into ONE wire contract, the **rich** envelope
|
|
7
|
+
* defined once in cwip (`cwip/error`):
|
|
8
|
+
*
|
|
9
|
+
* { "error": { "name", "message", "code?", "status?", "category?", "context?",
|
|
10
|
+
* "isOperational", "timestamp" }, "correlationId?": "…" }
|
|
11
|
+
*
|
|
12
|
+
* Clients parse exactly this shape, so there is no per-route error drift — and no
|
|
13
|
+
* drift between a cwip-based server (which already emits it via `cwip/server`'s
|
|
14
|
+
* `errorHandler`) and a cursedshell one. The extra fields are additive:
|
|
15
|
+
* a client that only reads `message`/`code`/`context` keeps working unchanged.
|
|
16
|
+
*
|
|
17
|
+
* `ApiError` is the ergonomic throwable: `throw new ApiError(404, 'not found')`.
|
|
18
|
+
* It is a thin subclass of cwip's `AppError` with the status-first constructor
|
|
19
|
+
* shape every plugin already uses, so a single `getSummary()`/`toErrorEnvelope`
|
|
20
|
+
* produces the rich body. The `errorEnvelope` Hono `onError` handler
|
|
21
|
+
* (cursedbelt-server) turns it (and any other thrown value) into the envelope +
|
|
22
|
+
* the right HTTP status; {@link ApiError.fromEnvelope} rebuilds one on the CLIENT
|
|
23
|
+
* from a parsed error body.
|
|
24
|
+
*
|
|
25
|
+
* This module is **framework-free** (no Hono, no DOM, no server deps — it imports
|
|
26
|
+
* only cwip's framework-free `cwip/error` and a type-only `zod` for the validation
|
|
27
|
+
* builder), so it is safe to import from browser bundles and from cursedshell's
|
|
28
|
+
* plugin-pure layer via the `cursedbelt-core/errors` subpath. Server code reaches the
|
|
29
|
+
* same symbols through `cursedbelt-server` (which re-exports this module).
|
|
30
|
+
*/
|
|
31
|
+
import {
|
|
32
|
+
AppError,
|
|
33
|
+
type AppErrorSummary,
|
|
34
|
+
type ErrorEnvelope,
|
|
35
|
+
isAppError,
|
|
36
|
+
type ToErrorEnvelopeOptions,
|
|
37
|
+
toErrorEnvelope,
|
|
38
|
+
} from 'cwip/error';
|
|
39
|
+
import type { ZodError } from 'zod';
|
|
40
|
+
|
|
41
|
+
// ── Re-export the ONE canonical envelope + formatter (defined in cwip) ────────
|
|
42
|
+
// So `cursedbelt-core/errors` and `cwip/error` speak the exact same wire contract.
|
|
43
|
+
export { AppError, type AppErrorSummary, type ErrorEnvelope, isAppError, type ToErrorEnvelopeOptions, toErrorEnvelope };
|
|
44
|
+
|
|
45
|
+
// ── Throwable ────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export interface ApiErrorOptions {
|
|
48
|
+
/** Stable machine code carried into the envelope. */
|
|
49
|
+
code?: string;
|
|
50
|
+
/** Structured detail carried into the envelope (e.g. `{ issues }`). */
|
|
51
|
+
context?: Record<string, unknown>;
|
|
52
|
+
/** Underlying cause, preserved for server-side logging (never serialized). */
|
|
53
|
+
cause?: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A throwable HTTP error carrying its status + canonical-envelope fields. A thin,
|
|
58
|
+
* status-first subclass of cwip's {@link AppError} — `new ApiError(404, 'nope',
|
|
59
|
+
* { code, context })` is the ergonomic form every handler/middleware/plugin
|
|
60
|
+
* service uses; the inherited `getSummary()` yields the rich `AppErrorSummary` so
|
|
61
|
+
* `toErrorEnvelope` maps it to the response with no extra work. On the client,
|
|
62
|
+
* {@link ApiError.fromEnvelope} rebuilds it from a parsed error body so callers
|
|
63
|
+
* can branch on `.status`/`.code`/`.context`.
|
|
64
|
+
*/
|
|
65
|
+
export class ApiError extends AppError {
|
|
66
|
+
/**
|
|
67
|
+
* Always set — narrows the base's optional `status` (an `ApiError` is defined
|
|
68
|
+
* by its HTTP status). The value is assigned by `AppError`'s constructor from
|
|
69
|
+
* the `status` we pass through; `declare` just re-types it without re-emitting
|
|
70
|
+
* the field (so the base's assignment isn't shadowed under class-field semantics).
|
|
71
|
+
*/
|
|
72
|
+
declare readonly status: number;
|
|
73
|
+
|
|
74
|
+
constructor(status: number, message: string, options: ApiErrorOptions = {}) {
|
|
75
|
+
super(message, {
|
|
76
|
+
status,
|
|
77
|
+
code: options.code,
|
|
78
|
+
context: options.context,
|
|
79
|
+
cause: options.cause,
|
|
80
|
+
});
|
|
81
|
+
// `AppError` sets `this.name = new.target.name` → 'ApiError' already.
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Rebuild an `ApiError` from a parsed error envelope's `error` object — the
|
|
86
|
+
* client half of the contract (e.g. `createServiceClient`'s response parser).
|
|
87
|
+
* Reads only `message`/`code`/`context`, so it works against both the rich
|
|
88
|
+
* envelope and any legacy minimal body. Missing/absent envelope falls back to a
|
|
89
|
+
* status-derived message.
|
|
90
|
+
*/
|
|
91
|
+
static fromEnvelope(status: number, envelope?: Partial<AppErrorSummary>): ApiError {
|
|
92
|
+
return new ApiError(status, envelope?.message ?? `Request failed with status ${status}`, {
|
|
93
|
+
code: envelope?.code,
|
|
94
|
+
context: envelope?.context,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Type guard for {@link ApiError} (the status-first server throwable). */
|
|
100
|
+
export const isApiError = (value: unknown): value is ApiError => value instanceof ApiError;
|
|
101
|
+
|
|
102
|
+
// ── Validation envelope (the ONE shape both zValidator and the cursedshell ────
|
|
103
|
+
// service-router emit; mirrors Zod's own `issue.path`) ─────────────────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* One field-level validation error on the wire. `path` is the dotted Zod path to
|
|
107
|
+
* the offending field (`''` for a top-level/whole-body issue) — it mirrors
|
|
108
|
+
* `z.ZodIssue.path`, so server code maps 1:1 and clients read a stable name.
|
|
109
|
+
*/
|
|
110
|
+
export interface ValidationIssue {
|
|
111
|
+
/** Dotted path to the offending field (`''` for a top-level issue). */
|
|
112
|
+
path: string;
|
|
113
|
+
/** Human-readable, zod-authored message. */
|
|
114
|
+
message: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The canonical machine code carried in a validation envelope's `error.code`, so a
|
|
119
|
+
* client can branch on it without string-matching the (localizable) message.
|
|
120
|
+
*/
|
|
121
|
+
export const VALIDATION_ERROR_CODE = 'validation';
|
|
122
|
+
|
|
123
|
+
/** The HTTP status every validation failure maps to (both emitters respond 400). */
|
|
124
|
+
export const VALIDATION_STATUS = 400;
|
|
125
|
+
|
|
126
|
+
/** The default client-facing message when validation fails. */
|
|
127
|
+
const DEFAULT_VALIDATION_MESSAGE = 'Validation failed';
|
|
128
|
+
|
|
129
|
+
/** Map a {@link https://zod.dev | zod} error's issues to the wire {@link ValidationIssue}s. */
|
|
130
|
+
export function toValidationIssues(error: ZodError): ValidationIssue[] {
|
|
131
|
+
return error.issues.map((issue) => ({
|
|
132
|
+
path: issue.path.map((segment) => String(segment)).join('.'),
|
|
133
|
+
message: issue.message,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Build the canonical validation envelope from a zod error (or pre-mapped issues).
|
|
139
|
+
* It constructs an {@link ApiError} (status {@link VALIDATION_STATUS}, code
|
|
140
|
+
* {@link VALIDATION_ERROR_CODE}, `context.issues`) and runs it through the shared
|
|
141
|
+
* {@link toErrorEnvelope}, so a validation failure is shaped identically to every
|
|
142
|
+
* other error on the wire — `{ error: { …, code: 'validation', status: 400,
|
|
143
|
+
* context: { issues } } }`. This is the single builder — both cursedbelt's
|
|
144
|
+
* `zValidator` middleware and cursedshell's `buildServiceRouter` call it, so a
|
|
145
|
+
* client parses validation failures identically no matter which layer emitted them.
|
|
146
|
+
*/
|
|
147
|
+
export function validationEnvelope(
|
|
148
|
+
errorOrIssues: ZodError | ValidationIssue[],
|
|
149
|
+
message: string = DEFAULT_VALIDATION_MESSAGE,
|
|
150
|
+
): ErrorEnvelope {
|
|
151
|
+
const issues = Array.isArray(errorOrIssues) ? errorOrIssues : toValidationIssues(errorOrIssues);
|
|
152
|
+
return toErrorEnvelope(new ApiError(VALIDATION_STATUS, message, { code: VALIDATION_ERROR_CODE, context: { issues } }));
|
|
153
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔴 NO COMMENT, NO SKIP — and the doc that answers "what were they for" must stay true.
|
|
3
|
+
*
|
|
4
|
+
* ## What this cost, 2026-09-13
|
|
5
|
+
*
|
|
6
|
+
* `bun run verify` reported **5,132 pass / 82 skip / 0 fail**. Nobody could say what the 82
|
|
7
|
+
* were. Each had been written because something mattered and each had stopped running for a
|
|
8
|
+
* reason that was, by then, invisible: the gate condition was in the code
|
|
9
|
+
* (`describe.skipIf(!URL_)`), but *why that variable is unset on every host we own* was in
|
|
10
|
+
* nobody's head. A suite that carries a number like that quietly is a suite whose green means
|
|
11
|
+
* less than it says.
|
|
12
|
+
*
|
|
13
|
+
* Answering it took 16m 5s of agent time — the figure the runner's ledger recorded for the task,
|
|
14
|
+
* not an estimate — spent booting a throwaway Postgres, locating a binary-server checkout, and
|
|
15
|
+
* running each gated suite by hand to learn whether it still passes. All 82 turned out to be
|
|
16
|
+
* healthy, environment-gated proofs. That is the good outcome, and it is exactly why the next
|
|
17
|
+
* person must not have to re-derive it: the cost was never the fixing, it was the *finding
|
|
18
|
+
* out*.
|
|
19
|
+
*
|
|
20
|
+
* ## So this is a check, not a paragraph
|
|
21
|
+
*
|
|
22
|
+
* Two assertions, both of which fail the moment the record stops matching the code:
|
|
23
|
+
*
|
|
24
|
+
* 1. **Every skip site in `src` carries its reason in the comment directly above it.** A new
|
|
25
|
+
* `describe.skipIf` with no comment is a red gate, at the moment it is written, by the
|
|
26
|
+
* person who knows the answer — which is the only moment the answer is cheap.
|
|
27
|
+
* 2. **`docs/skipped-tests.md` names exactly the files that have one.** A skip that is
|
|
28
|
+
* deleted or added without updating the record reds too, so the doc cannot rot into the
|
|
29
|
+
* confident-but-wrong state that makes a doc worse than nothing.
|
|
30
|
+
*
|
|
31
|
+
* It deliberately does NOT assert a skip COUNT. The counts move with the host — ffmpeg
|
|
32
|
+
* installed or not, `CC_PG_URL` set or not — and a check that reds because someone installed
|
|
33
|
+
* `sharp` teaches people to delete the check.
|
|
34
|
+
*/
|
|
35
|
+
import { describe, expect, test } from 'bun:test';
|
|
36
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
37
|
+
import path from 'node:path';
|
|
38
|
+
import { fileURLToPath } from 'node:url';
|
|
39
|
+
|
|
40
|
+
const SRC = fileURLToPath(new URL('.', import.meta.url));
|
|
41
|
+
const REPO = path.resolve(SRC, '..');
|
|
42
|
+
const SELF = 'src/skippedTestsAreAnswered.spec.ts';
|
|
43
|
+
const DOC = 'docs/skipped-tests.md';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A skip site: `describe.skip`, `.todo`, `.failing`, `.skipIf`, `.todoIf` on any of bun's three
|
|
47
|
+
* entry points, plus the jasmine-era `xit(` / `xdescribe(`.
|
|
48
|
+
*
|
|
49
|
+
* Anchored on the entry point so that ordinary identifiers containing "skip" — `skipDirs`,
|
|
50
|
+
* `skipToken`, `skipKdfReadinessGate`, all of which live in this package — are not sites.
|
|
51
|
+
*/
|
|
52
|
+
const SKIP_SITE = /\b(?:describe|it|test)\s*\.\s*(?:skip|todo|failing|skipIf|todoIf)\b|\bx(?:it|describe)\s*\(/;
|
|
53
|
+
|
|
54
|
+
/** A comment line, in either style, as it appears immediately above a skip site. */
|
|
55
|
+
const COMMENT_LINE = /^\s*(?:\/\/|\/\*|\*)/;
|
|
56
|
+
|
|
57
|
+
export interface SkipSite {
|
|
58
|
+
/** 1-indexed, so a failure message is clickable. */
|
|
59
|
+
readonly line: number;
|
|
60
|
+
readonly text: string;
|
|
61
|
+
/** The contiguous comment block directly above, with no blank line between it and the site. */
|
|
62
|
+
readonly reason: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Every skip site in one file's source, each carrying whatever comment sits directly above it.
|
|
67
|
+
*
|
|
68
|
+
* "Directly above" is strict on purpose: a blank line between the comment and the site ends the
|
|
69
|
+
* block. The failure this guards against is a skip drifting away from the explanation that was
|
|
70
|
+
* once next to it, and a blank line is exactly how that drift starts.
|
|
71
|
+
*/
|
|
72
|
+
export const findSkipSites = (source: string): SkipSite[] => {
|
|
73
|
+
const lines = source.split('\n');
|
|
74
|
+
const sites: SkipSite[] = [];
|
|
75
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
76
|
+
const text = lines[i] as string;
|
|
77
|
+
if (!SKIP_SITE.test(text)) continue;
|
|
78
|
+
const above: string[] = [];
|
|
79
|
+
for (let j = i - 1; j >= 0; j -= 1) {
|
|
80
|
+
const prev = lines[j] as string;
|
|
81
|
+
if (!COMMENT_LINE.test(prev)) break;
|
|
82
|
+
above.unshift(prev);
|
|
83
|
+
}
|
|
84
|
+
sites.push({ line: i + 1, text: text.trim(), reason: above.join('\n') });
|
|
85
|
+
}
|
|
86
|
+
return sites;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** A reason has to be ABOUT the skip. Any comment at all would pass otherwise. */
|
|
90
|
+
export const isAnswered = (site: SkipSite): boolean => /skip/i.test(site.reason);
|
|
91
|
+
|
|
92
|
+
/** Every `src` file that contains at least one skip site, repo-relative and sorted. */
|
|
93
|
+
const scanSrc = (): Map<string, SkipSite[]> => {
|
|
94
|
+
const found = new Map<string, SkipSite[]>();
|
|
95
|
+
for (const entry of readdirSync(SRC, { recursive: true })) {
|
|
96
|
+
const name = String(entry);
|
|
97
|
+
if (!/\.tsx?$/.test(name)) continue;
|
|
98
|
+
const rel = `src/${name.split(path.sep).join('/')}`;
|
|
99
|
+
if (rel === SELF) continue; // the checker names the patterns it bans, so it cannot scan itself
|
|
100
|
+
const sites = findSkipSites(readFileSync(path.join(SRC, name), 'utf8'));
|
|
101
|
+
if (sites.length > 0) found.set(rel, sites);
|
|
102
|
+
}
|
|
103
|
+
return new Map([...found].sort(([a], [b]) => a.localeCompare(b)));
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The suites `docs/skipped-tests.md` claims to cover.
|
|
108
|
+
*
|
|
109
|
+
* Only backticked paths in a TABLE ROW count, so the prose above and below the table stays free
|
|
110
|
+
* to mention any file it likes — including this one.
|
|
111
|
+
*/
|
|
112
|
+
const documentedSuites = async (): Promise<Set<string>> => {
|
|
113
|
+
const doc = await Bun.file(path.join(REPO, DOC)).text();
|
|
114
|
+
const rows = doc.split('\n').filter((l) => l.trimStart().startsWith('|'));
|
|
115
|
+
const named = new Set<string>();
|
|
116
|
+
for (const row of rows) for (const m of row.matchAll(/`(src\/[^`]+\.tsx?)`/g)) named.add(m[1] as string);
|
|
117
|
+
return named;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
describe('every skip in src answers for itself', () => {
|
|
121
|
+
test('each skip site carries its reason in the comment directly above it', () => {
|
|
122
|
+
const unanswered: string[] = [];
|
|
123
|
+
for (const [file, sites] of scanSrc())
|
|
124
|
+
for (const site of sites) if (!isAnswered(site)) unanswered.push(`${file}:${site.line} ${site.text}`);
|
|
125
|
+
|
|
126
|
+
expect(
|
|
127
|
+
unanswered,
|
|
128
|
+
`A skipped test with no stated reason is a question nobody can answer later — answering the 82 already here\n` +
|
|
129
|
+
`cost a task of its own on 2026-09-13 (16m 5s, per the runner's ledger). Put one line directly above the\n` +
|
|
130
|
+
`skip saying what it is WAITING FOR (a service, a fixture, a\n` +
|
|
131
|
+
`browser, a decision), mentioning the word "skip", and add a row to ${DOC}. If it passes today, unskip it\n` +
|
|
132
|
+
`instead. Never delete a test to make the number fall.\n\nUnanswered:\n ${unanswered.join('\n ')}`,
|
|
133
|
+
).toEqual([]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe(`${DOC} matches the suite`, () => {
|
|
138
|
+
test('it names every file that carries a skip', async () => {
|
|
139
|
+
const documented = await documentedSuites();
|
|
140
|
+
const missing = [...scanSrc().keys()].filter((f) => !documented.has(f));
|
|
141
|
+
expect(
|
|
142
|
+
missing,
|
|
143
|
+
`These files gained a skip that ${DOC} does not mention. Add a table row: what it waits for, how to run it,\n` +
|
|
144
|
+
`and when it was last seen green.\n\n ${missing.join('\n ')}`,
|
|
145
|
+
).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('it names no file that has stopped carrying one', async () => {
|
|
149
|
+
const live = scanSrc();
|
|
150
|
+
const stale = [...(await documentedSuites())].filter((f) => !live.has(f));
|
|
151
|
+
expect(
|
|
152
|
+
stale,
|
|
153
|
+
`${DOC} still lists these as skipped, but they no longer contain a skip site. Good news — delete the rows\n` +
|
|
154
|
+
`(and move the count) so the record stops claiming something that is no longer true.\n\n ${stale.join('\n ')}`,
|
|
155
|
+
).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The detector's own failure path. A gate nobody has seen RED is a gate nobody knows works, and
|
|
161
|
+
* this one's whole value is that it fires on the day a skip is introduced.
|
|
162
|
+
*/
|
|
163
|
+
describe('the detector itself', () => {
|
|
164
|
+
test('an uncommented skip is a finding', () => {
|
|
165
|
+
const sites = findSkipSites(["describe.skipIf(!URL_)('x', () => {", '});'].join('\n'));
|
|
166
|
+
expect(sites).toHaveLength(1);
|
|
167
|
+
expect(isAnswered(sites[0] as SkipSite)).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('a skip whose comment explains the skip is not', () => {
|
|
171
|
+
const sites = findSkipSites(
|
|
172
|
+
['// SKIPPED unless CC_PG_URL names a real Postgres.', "describe.skipIf(!URL_)('x', () => {", '});'].join('\n'),
|
|
173
|
+
);
|
|
174
|
+
expect(isAnswered(sites[0] as SkipSite)).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('a comment about something else does NOT answer for the skip', () => {
|
|
178
|
+
const sites = findSkipSites(
|
|
179
|
+
['// posterOnly is the memory-constrained-host path.', "describe.skipIf(!ffmpeg)('x', () => {", '});'].join('\n'),
|
|
180
|
+
);
|
|
181
|
+
expect(isAnswered(sites[0] as SkipSite)).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('a blank line between the comment and the skip breaks the block', () => {
|
|
185
|
+
const sites = findSkipSites(
|
|
186
|
+
['// SKIPPED unless CC_PG_URL is set.', '', "describe.skipIf(!URL_)('x', () => {", '});'].join('\n'),
|
|
187
|
+
);
|
|
188
|
+
expect(sites[0]?.reason).toBe('');
|
|
189
|
+
expect(isAnswered(sites[0] as SkipSite)).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('a JSDoc block above the skip counts as the reason', () => {
|
|
193
|
+
const sites = findSkipSites(
|
|
194
|
+
['/**', ' * SKIPPED until the browser harness exists.', ' */', "it.skip('x', () => {});"].join('\n'),
|
|
195
|
+
);
|
|
196
|
+
expect(isAnswered(sites[0] as SkipSite)).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('it finds the other spellings too', () => {
|
|
200
|
+
expect(findSkipSites("it.todo('later');")).toHaveLength(1);
|
|
201
|
+
expect(findSkipSites("test.failing('known bad');")).toHaveLength(1);
|
|
202
|
+
expect(findSkipSites("xit('old style', () => {});")).toHaveLength(1);
|
|
203
|
+
expect(findSkipSites("xdescribe('old style', () => {});")).toHaveLength(1);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('an identifier that merely contains "skip" is not a skip site', () => {
|
|
207
|
+
expect(findSkipSites('const skip = new Set(opts.skipDirs ?? []);')).toEqual([]);
|
|
208
|
+
expect(findSkipSites('if (options.skipKdfReadinessGate !== true) assertArgon2idReady();')).toEqual([]);
|
|
209
|
+
expect(findSkipSites('const q = useQuery({ queryKey: k, initialPageParam: skipToken });')).toEqual([]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('the walker reads a real tree — a scan that visits nothing proves nothing', () => {
|
|
213
|
+
// This package split out of cursedbelt with ZERO skip sites (2026-09-15), so
|
|
214
|
+
// detector liveness cannot come from finding one in the tree. The fixture
|
|
215
|
+
// assertions above prove the DETECTOR; this proves the WALK: it must visit
|
|
216
|
+
// the same order of spec files the suite runs, or the scan is reading a
|
|
217
|
+
// wrong directory and every green above is about nothing.
|
|
218
|
+
const specs = [...readdirSync(SRC, { recursive: true })].filter((e) => /\.spec\.tsx?$/.test(String(e)));
|
|
219
|
+
expect(specs.length).toBeGreaterThan(20);
|
|
220
|
+
});
|
|
221
|
+
});
|