@orkestrel/scaffold 0.0.72 → 0.0.74

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.
@@ -5,6 +5,7 @@ import { spawnSync } from 'node:child_process'
5
5
  import {
6
6
  existsSync,
7
7
  globSync,
8
+ lstatSync,
8
9
  mkdtempSync,
9
10
  mkdirSync,
10
11
  readdirSync,
@@ -63,6 +64,33 @@ import { describe, expect, it } from 'vitest'
63
64
 
64
65
  const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
65
66
 
67
+ // The `src` axis a declaration roll-up reads, derived from a recognized source environment that is
68
+ // a physical directory rather than from a `src` entry of any shape. A regular file, an empty
69
+ // directory, a directory holding no recognized environment, a symbolic link, and an entry this host
70
+ // refuses to inspect each leave a workspace publishing nothing, and reading the entry alone would
71
+ // put every one of them under the face proof. The inspection is caught rather than excused by
72
+ // option: `throwIfNoEntry` excuses an absent path alone and rethrows a permission refusal, and a
73
+ // throw on this line is a collection error that takes this whole file down in that target rather
74
+ // than failing one case. `isPhysicalDirectory` reads the same two facts off an `lstat` and returns
75
+ // false for every inspection error, and this reads them the same way. This is the derivation
76
+ // `targetToEnvironments` uses, which filters the environment names by a contained physical
77
+ // directory, and it answers the same question `blueprintToScripts`
78
+ // answers from a blueprint's own `src` list. A workspace declaring app environments alone publishes
79
+ // no library, so it vendors no `configs/src/` face wrapper and has no roll-up to measure. A face
80
+ // proof is conditioned on this axis rather than on a face wrapper: a workspace that declares the
81
+ // axis and vendors no wrapper carries the defect such a proof reports, so that workspace must still
82
+ // fail the proof. The manifest cases read the same fact from their own `private` flag and name it
83
+ // `manifestPublishes` for the source that decides it there; this reads the axis on disk, so a
84
+ // workspace whose manifest and shape disagree fails one of the two rather than neither.
85
+ const publishes = ['core', 'browser', 'server'].some((environment) => {
86
+ try {
87
+ const entry = lstatSync(resolve(root, 'src', environment))
88
+ return entry.isDirectory() && !entry.isSymbolicLink()
89
+ } catch {
90
+ return false
91
+ }
92
+ })
93
+
66
94
  // A declaration roll-up loads the extractor package, which only a workspace publishing source from
67
95
  // `src` installs. The resolution below is the mechanism its proof is conditioned on.
68
96
  let extractorPath: string | undefined
@@ -199,8 +227,8 @@ describe('root configuration', () => {
199
227
  setup: ['./tests/setup.ts'],
200
228
  })
201
229
  // A row that is a configuration rather than a factory. Every generated
202
- // workspace registers the factory itself, so this shape is required here
203
- // rather than observed: the proof exercises that resolution wherever it runs
230
+ // workspace registers the factory itself, so this shape is required by the
231
+ // expectation rather than observed: the proof exercises that resolution wherever it runs
204
232
  // instead of only where a hand-written configuration happens to produce it.
205
233
  expected.set('concrete', { include: 'tests/concrete.test.ts', setup: ['./tests/setup.ts'] })
206
234
 
@@ -418,6 +446,9 @@ describe('root configuration', () => {
418
446
  if (existsSync(resolve(root, 'configs/app/vite.showcase.config.ts'))) {
419
447
  required.push('configs/app/vite.showcase.config.ts')
420
448
  }
449
+ if (existsSync(resolve(root, 'configs/app/vite.journey.config.ts'))) {
450
+ required.push('configs/app/vite.journey.config.ts')
451
+ }
421
452
  if (required[0] === undefined) {
422
453
  throw new Error('The workspace selects no configuration target')
423
454
  }
@@ -432,80 +463,172 @@ describe('root configuration', () => {
432
463
  ).map((path) => path.replaceAll('\\', '/'))
433
464
  const extra = 'configs/app/vite.core.config.ts'
434
465
  const controlled = found.concat(extra)
466
+ const planted = createPolicyScratch({ prefix: 'config-journey-' })
467
+ const journey = 'configs/app/vite.journey.config.ts'
468
+ planted.write(
469
+ journey,
470
+ `export default {
471
+ test: { projects: [() => ({
472
+ test: {
473
+ name: { label: 'journey:desktop' },
474
+ include: ['tests/app/browser/integration.test.ts'],
475
+ exclude: [],
476
+ setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
477
+ provide: { variant: 'desktop', variants: [{ name: 'desktop', width: 1280, height: 800 }], capture: false },
478
+ browser: { enabled: true, instances: [{ browser: 'chromium', headless: true }] },
479
+ },
480
+ })] },
481
+ }\n`,
482
+ )
483
+ const journeys: object[][] = []
484
+ const targets = required.map((wrapper) => ({ wrapper, directory: root }))
485
+ targets.push({ wrapper: journey, directory: planted.path })
486
+ controlled.push(journey)
435
487
 
436
488
  // Required wrappers come from selected src/app targets. Only that set is loaded and validated.
437
489
  // Extra wrappers remain in the found population but are ignored before their content is read.
438
490
  expect(controlled).toContain(extra)
439
491
  expect(required).not.toContain(extra)
440
- for (const wrapper of required) {
441
- expect(controlled).toContain(wrapper)
442
- const viteMatch =
443
- /^configs\/(src|app)\/vite\.(core|browser|server|bin|showcase)\.config\.ts$/u.exec(wrapper)
444
- if (viteMatch !== null) {
445
- const [, axis, environment] = viteMatch
492
+ try {
493
+ for (const { wrapper, directory } of targets) {
494
+ expect(controlled).toContain(wrapper)
495
+ const viteMatch =
496
+ /^configs\/(src|app)\/vite\.(core|browser|server|bin|showcase|journey)\.config\.ts$/u.exec(
497
+ wrapper,
498
+ )
499
+ if (viteMatch !== null) {
500
+ const [, axis, environment] = viteMatch
501
+ if (axis === undefined || environment === undefined) {
502
+ throw new Error(`${wrapper} carries no target`)
503
+ }
504
+ const loaded = await loadConfigFromFile(
505
+ { command: 'build', mode: 'test', isSsrBuild: false, isPreview: false },
506
+ resolve(directory, wrapper),
507
+ directory,
508
+ 'silent',
509
+ )
510
+ if (loaded === null) throw new Error(`${wrapper} did not load`)
511
+ if (environment === 'journey') {
512
+ const projects = loaded.config.test?.projects
513
+ if (!Array.isArray(projects) || projects.length === 0) {
514
+ throw new Error(`${wrapper} carries no journey projects`)
515
+ }
516
+ const tests: object[] = []
517
+ for (const factory of projects) {
518
+ if (typeof factory !== 'function')
519
+ throw new Error(`${wrapper} carries no project factory`)
520
+ const project: unknown = await Reflect.apply(factory, undefined, [
521
+ { command: 'serve', mode: 'test' },
522
+ ])
523
+ if (typeof project !== 'object' || project === null)
524
+ throw new Error('A journey project is not a configuration')
525
+ const test: unknown = Object.getOwnPropertyDescriptor(project, 'test')?.value
526
+ if (typeof test !== 'object' || test === null)
527
+ throw new Error('A journey project carries no test block')
528
+ tests.push(test)
529
+ }
530
+ journeys.push(tests)
531
+ continue
532
+ }
533
+ const output = loaded.config.build?.outDir
534
+ if (output === undefined) throw new Error(`${wrapper} carries no output`)
535
+ const expected =
536
+ environment === 'bin'
537
+ ? 'dist/bin'
538
+ : environment === 'showcase'
539
+ ? 'dist/showcase'
540
+ : `dist/${axis}/${environment}`
541
+ if (resolve(root, output) !== resolve(root, expected)) {
542
+ throw new Error(`${wrapper} resolves to the wrong output`)
543
+ }
544
+ continue
545
+ }
546
+
547
+ const tsconfigMatch =
548
+ /^configs\/(src|app)\/tsconfig\.(core|browser|server|bin)\.json$/u.exec(wrapper)
549
+ if (tsconfigMatch === null) {
550
+ throw new Error(`${wrapper} is not a required target wrapper`)
551
+ }
552
+ const [, axis, environment] = tsconfigMatch
446
553
  if (axis === undefined || environment === undefined) {
447
- throw new Error(`${wrapper} carries no target`)
554
+ throw new Error(`${wrapper} carries no TypeScript scope`)
448
555
  }
449
- const loaded = await loadConfigFromFile(
450
- { command: 'build', mode: 'test', isSsrBuild: false, isPreview: false },
451
- resolve(root, wrapper),
452
- root,
453
- 'silent',
454
- )
455
- if (loaded === null) throw new Error(`${wrapper} did not load`)
456
- const output = loaded.config.build?.outDir
457
- if (output === undefined) throw new Error(`${wrapper} carries no output`)
458
- const expected =
459
- environment === 'bin'
460
- ? 'dist/bin'
461
- : environment === 'showcase'
462
- ? 'dist/showcase'
463
- : `dist/${axis}/${environment}`
464
- if (resolve(root, output) !== resolve(root, expected)) {
465
- throw new Error(`${wrapper} resolves to the wrong output`)
556
+ const parsed: unknown = JSON.parse(readFileSync(resolve(root, wrapper), 'utf8'))
557
+ if (typeof parsed !== 'object' || parsed === null) {
558
+ throw new Error(`${wrapper} is not a TypeScript configuration record`)
466
559
  }
467
- continue
468
- }
469
-
470
- const tsconfigMatch = /^configs\/(src|app)\/tsconfig\.(core|browser|server|bin)\.json$/u.exec(
471
- wrapper,
472
- )
473
- if (tsconfigMatch === null) {
474
- throw new Error(`${wrapper} is not a required target wrapper`)
475
- }
476
- const [, axis, environment] = tsconfigMatch
477
- if (axis === undefined || environment === undefined) {
478
- throw new Error(`${wrapper} carries no TypeScript scope`)
479
- }
480
- const parsed: unknown = JSON.parse(readFileSync(resolve(root, wrapper), 'utf8'))
481
- if (typeof parsed !== 'object' || parsed === null) {
482
- throw new Error(`${wrapper} is not a TypeScript configuration record`)
560
+ const compilerOptions: unknown = Object.getOwnPropertyDescriptor(
561
+ parsed,
562
+ 'compilerOptions',
563
+ )?.value
564
+ if (typeof compilerOptions !== 'object' || compilerOptions === null) {
565
+ throw new Error(`${wrapper} carries no compiler options`)
566
+ }
567
+ const lib: unknown = Object.getOwnPropertyDescriptor(compilerOptions, 'lib')?.value
568
+ const types: unknown = Object.getOwnPropertyDescriptor(compilerOptions, 'types')?.value
569
+ const expectedLib =
570
+ environment === 'core'
571
+ ? ['ESNext', 'WebWorker']
572
+ : environment === 'browser'
573
+ ? ['ESNext', 'DOM', 'DOM.Iterable']
574
+ : ['ESNext']
575
+ const expectedTypes =
576
+ environment === 'core'
577
+ ? []
578
+ : environment === 'browser'
579
+ ? axis === 'app'
580
+ ? ['vite/client', 'vue']
581
+ : ['vite/client']
582
+ : ['node']
583
+ expect(lib).toStrictEqual(expectedLib)
584
+ expect(types).toStrictEqual(expectedTypes)
483
585
  }
484
- const compilerOptions: unknown = Object.getOwnPropertyDescriptor(
485
- parsed,
486
- 'compilerOptions',
487
- )?.value
488
- if (typeof compilerOptions !== 'object' || compilerOptions === null) {
489
- throw new Error(`${wrapper} carries no compiler options`)
586
+ for (const tests of journeys) {
587
+ const names: string[] = []
588
+ for (const test of tests) {
589
+ expect(Object.getOwnPropertyDescriptor(test, 'include')?.value).toStrictEqual([
590
+ 'tests/app/browser/integration.test.ts',
591
+ ])
592
+ expect(Object.getOwnPropertyDescriptor(test, 'exclude')?.value).toStrictEqual([])
593
+ expect(Object.getOwnPropertyDescriptor(test, 'setupFiles')?.value).toStrictEqual([
594
+ './tests/setup.ts',
595
+ './tests/setupBrowser.ts',
596
+ ])
597
+ const name: unknown = Object.getOwnPropertyDescriptor(test, 'name')?.value
598
+ const provide: unknown = Object.getOwnPropertyDescriptor(test, 'provide')?.value
599
+ const browser: unknown = Object.getOwnPropertyDescriptor(test, 'browser')?.value
600
+ if (
601
+ typeof name !== 'object' ||
602
+ name === null ||
603
+ typeof provide !== 'object' ||
604
+ provide === null ||
605
+ typeof browser !== 'object' ||
606
+ browser === null
607
+ )
608
+ throw new Error('A journey project carries no name, provide, or browser block')
609
+ const variant: unknown = Object.getOwnPropertyDescriptor(provide, 'variant')?.value
610
+ if (typeof variant !== 'string' || variant.length === 0)
611
+ throw new Error('A journey project carries no variant name')
612
+ expect(Object.getOwnPropertyDescriptor(name, 'label')?.value).toBe(`journey:${variant}`)
613
+ expect(typeof Object.getOwnPropertyDescriptor(provide, 'capture')?.value).toBe('boolean')
614
+ expect(Object.getOwnPropertyDescriptor(browser, 'enabled')?.value).toBe(true)
615
+ const variants: unknown = Object.getOwnPropertyDescriptor(provide, 'variants')?.value
616
+ if (
617
+ !Array.isArray(variants) ||
618
+ !variants.some(
619
+ (candidate: unknown) =>
620
+ typeof candidate === 'object' &&
621
+ candidate !== null &&
622
+ Object.getOwnPropertyDescriptor(candidate, 'name')?.value === variant,
623
+ )
624
+ )
625
+ throw new Error('A journey variant is absent from its declared set')
626
+ expect(names).not.toContain(variant)
627
+ names.push(variant)
628
+ }
490
629
  }
491
- const lib: unknown = Object.getOwnPropertyDescriptor(compilerOptions, 'lib')?.value
492
- const types: unknown = Object.getOwnPropertyDescriptor(compilerOptions, 'types')?.value
493
- const expectedLib =
494
- environment === 'core'
495
- ? ['ESNext', 'WebWorker']
496
- : environment === 'browser'
497
- ? ['ESNext', 'DOM', 'DOM.Iterable']
498
- : ['ESNext']
499
- const expectedTypes =
500
- environment === 'core'
501
- ? []
502
- : environment === 'browser'
503
- ? axis === 'app'
504
- ? ['vite/client', 'vue']
505
- : ['vite/client']
506
- : ['node']
507
- expect(lib).toStrictEqual(expectedLib)
508
- expect(types).toStrictEqual(expectedTypes)
630
+ } finally {
631
+ planted.destroy()
509
632
  }
510
633
 
511
634
  const controlRequired = [
@@ -530,7 +653,7 @@ describe('root configuration', () => {
530
653
  if (typeof scripts !== 'object' || scripts === null) {
531
654
  throw new Error('The package manifest carries no scripts')
532
655
  }
533
- const publishes = Object.getOwnPropertyDescriptor(manifest, 'private')?.value !== true
656
+ const manifestPublishes = Object.getOwnPropertyDescriptor(manifest, 'private')?.value !== true
534
657
  const test = Object.getOwnPropertyDescriptor(scripts, 'test')?.value
535
658
  const config = Object.getOwnPropertyDescriptor(scripts, 'test:config')?.value
536
659
  const distribution = Object.getOwnPropertyDescriptor(scripts, 'test:distribution')?.value
@@ -576,9 +699,9 @@ describe('root configuration', () => {
576
699
  )
577
700
  expect(typeof test === 'string' && test.includes('test:distribution')).toBe(false)
578
701
  expect(typeof publish === 'string' && publish.includes('npm run test:distribution')).toBe(
579
- hasDistribution && publishes,
702
+ hasDistribution && manifestPublishes,
580
703
  )
581
- expect(typeof publish === 'string').toBe(publishes)
704
+ expect(typeof publish === 'string').toBe(manifestPublishes)
582
705
  expect(integration).toBe(
583
706
  hasIntegration
584
707
  ? 'vitest run --config vite.config.ts --no-cache --reporter=dot --project integration'
@@ -610,10 +733,10 @@ describe('root configuration', () => {
610
733
  : undefined,
611
734
  )
612
735
  expect(typeof test === 'string' && test.includes('npm run test:service')).toBe(
613
- hasService && !publishes,
736
+ hasService && !manifestPublishes,
614
737
  )
615
738
  expect(typeof publish === 'string' && publish.includes('npm run test:service')).toBe(
616
- hasService && publishes,
739
+ hasService && manifestPublishes,
617
740
  )
618
741
  })
619
742
 
@@ -626,14 +749,14 @@ describe('root configuration', () => {
626
749
  if (typeof scripts !== 'object' || scripts === null) {
627
750
  throw new Error('The package manifest carries no scripts')
628
751
  }
629
- const publishes = Object.getOwnPropertyDescriptor(manifest, 'private')?.value !== true
752
+ const manifestPublishes = Object.getOwnPropertyDescriptor(manifest, 'private')?.value !== true
630
753
  const prepack = Object.getOwnPropertyDescriptor(scripts, 'prepack')?.value
631
- expect(prepack).toBe(publishes ? 'npm run build' : undefined)
754
+ expect(prepack).toBe(manifestPublishes ? 'npm run build' : undefined)
632
755
 
633
756
  const controlled = { ...scripts, prepack: 'npm run control' }
634
757
  expect(() => {
635
758
  const control = Object.getOwnPropertyDescriptor(controlled, 'prepack')?.value
636
- expect(control).toBe(publishes ? 'npm run build' : undefined)
759
+ expect(control).toBe(manifestPublishes ? 'npm run build' : undefined)
637
760
  }).toThrow(/expected/u)
638
761
  })
639
762
 
@@ -2150,54 +2273,81 @@ describe('configuration helpers', () => {
2150
2273
  }
2151
2274
  })
2152
2275
 
2153
- it('reads the compiler scope and fixed extractor override a declaration roll-up requires', () => {
2154
- const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
2155
- // The order mirrors ENVIRONMENTS in src/core/constants.ts; a server-only workspace vendors
2156
- // no core project, so this walks to the first face the workspace actually carries.
2157
- const faces = ['core', 'browser', 'server']
2158
- const face = faces.find((candidate) =>
2159
- existsSync(resolve(root, `configs/src/tsconfig.${candidate}.json`)),
2160
- )
2161
- if (face === undefined) throw new Error('The workspace declares no face project')
2162
- const project = resolve(root, `configs/src/tsconfig.${face}.json`)
2163
- const declared: unknown = JSON.parse(readFileSync(project, 'utf8'))
2164
- if (typeof declared !== 'object' || declared === null) {
2165
- throw new Error(`The ${face} project is not a TypeScript configuration record`)
2166
- }
2167
- const declaredOptions: unknown = Object.getOwnPropertyDescriptor(
2168
- declared,
2169
- 'compilerOptions',
2170
- )?.value
2171
- if (typeof declaredOptions !== 'object' || declaredOptions === null) {
2172
- throw new Error(`The ${face} project carries no compiler options`)
2173
- }
2174
- const declaredLib: unknown = Object.getOwnPropertyDescriptor(declaredOptions, 'lib')?.value
2175
- const declaredTypes: unknown = Object.getOwnPropertyDescriptor(declaredOptions, 'types')?.value
2176
- if (!configHelpers.isStringList(declaredLib) || !configHelpers.isStringList(declaredTypes)) {
2177
- throw new Error(`The ${face} project declares no lib or types`)
2178
- }
2179
- const declaredRootDir: unknown = Object.getOwnPropertyDescriptor(
2180
- declaredOptions,
2181
- 'rootDir',
2182
- )?.value
2183
- if (typeof declaredRootDir !== 'string') {
2184
- throw new Error(`The ${face} project declares no rootDir`)
2185
- }
2186
- const expectedRoot = resolve(dirname(project), declaredRootDir)
2276
+ // The scope reading applies only where the workspace publishes source from `src`, which is what
2277
+ // `publishes` reads, and the name ends at that mechanism. An absent face project is the defect
2278
+ // this case exists to report rather than a second reason to excuse it, so a workspace holding
2279
+ // the axis and vendoring no `configs/src/` wrapper reaches the throw inside the body.
2280
+ // A skip raises the run's skipped count where a guard inside the case would
2281
+ // raise its passed count with nothing measured. Every generated workspace runs its projects
2282
+ // under `--reporter=dot`, which prints a skip as an unnamed `-`, so the case name and the
2283
+ // condition in it are read by re-running the project with a reporter that names skipped cases.
2284
+ it.skipIf(!publishes)(
2285
+ 'reads the compiler scope a declaration roll-up requires [inapplicable where src holds no recognized environment directory]',
2286
+ () => {
2287
+ const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
2288
+ // The order mirrors ENVIRONMENTS in src/core/constants.ts; a server-only workspace vendors
2289
+ // no core project, so this walks to the first face the workspace actually carries.
2290
+ const faces = ['core', 'browser', 'server']
2291
+ const face = faces.find((candidate) =>
2292
+ existsSync(resolve(root, `configs/src/tsconfig.${candidate}.json`)),
2293
+ )
2294
+ if (face === undefined) throw new Error('The workspace declares no face project')
2295
+ const project = resolve(root, `configs/src/tsconfig.${face}.json`)
2296
+ const declared: unknown = JSON.parse(readFileSync(project, 'utf8'))
2297
+ if (typeof declared !== 'object' || declared === null) {
2298
+ throw new Error(`The ${face} project is not a TypeScript configuration record`)
2299
+ }
2300
+ const declaredOptions: unknown = Object.getOwnPropertyDescriptor(
2301
+ declared,
2302
+ 'compilerOptions',
2303
+ )?.value
2304
+ if (typeof declaredOptions !== 'object' || declaredOptions === null) {
2305
+ throw new Error(`The ${face} project carries no compiler options`)
2306
+ }
2307
+ const declaredLib: unknown = Object.getOwnPropertyDescriptor(declaredOptions, 'lib')?.value
2308
+ const declaredTypes: unknown = Object.getOwnPropertyDescriptor(
2309
+ declaredOptions,
2310
+ 'types',
2311
+ )?.value
2312
+ if (!configHelpers.isStringList(declaredLib) || !configHelpers.isStringList(declaredTypes)) {
2313
+ throw new Error(`The ${face} project declares no lib or types`)
2314
+ }
2315
+ const declaredRootDir: unknown = Object.getOwnPropertyDescriptor(
2316
+ declaredOptions,
2317
+ 'rootDir',
2318
+ )?.value
2319
+ if (typeof declaredRootDir !== 'string') {
2320
+ throw new Error(`The ${face} project declares no rootDir`)
2321
+ }
2322
+ const expectedRoot = resolve(dirname(project), declaredRootDir)
2187
2323
 
2188
- const scope = configHelpers.parseProjectScope(
2189
- configHelpers.readCompilerOutput(compiler, ['--showConfig', '-p', project]),
2190
- project,
2191
- )
2192
- if (scope === undefined) throw new Error(`The ${face} project resolved no compiler scope`)
2193
- // The compiler lowercases every resolved library name, so the committed project is the
2194
- // second mechanism this reading is compared against rather than the reading itself.
2195
- expect(scope.lib.map((entry) => entry.toLowerCase())).toStrictEqual(
2196
- declaredLib.map((entry) => entry.toLowerCase()),
2197
- )
2198
- expect(scope.types).toStrictEqual(declaredTypes)
2199
- expect(scope.root).toBe(expectedRoot)
2324
+ const scope = configHelpers.parseProjectScope(
2325
+ configHelpers.readCompilerOutput(compiler, ['--showConfig', '-p', project]),
2326
+ project,
2327
+ )
2328
+ if (scope === undefined) throw new Error(`The ${face} project resolved no compiler scope`)
2329
+ // The compiler lowercases every resolved library name, so the committed project is the
2330
+ // second mechanism this reading is compared against rather than the reading itself.
2331
+ expect(scope.lib.map((entry) => entry.toLowerCase())).toStrictEqual(
2332
+ declaredLib.map((entry) => entry.toLowerCase()),
2333
+ )
2334
+ expect(scope.types).toStrictEqual(declaredTypes)
2335
+ expect(scope.root).toBe(expectedRoot)
2336
+ },
2337
+ )
2200
2338
 
2339
+ // No reading in this case reads anything under `configs/src/`, so each one applies to a workspace
2340
+ // on either axis. The refusals are decided by the text handed to them; the compiler refusal by a
2341
+ // path no workspace shape carries; the manifest name, the module resolution, and the extractor
2342
+ // guard by files and packages a workspace carries on either axis. They sit apart from the
2343
+ // preceding scope reading for that reason: the skip that excuses an app-only workspace from
2344
+ // reading a face project must not excuse it from the helpers behind the roll-up, which it
2345
+ // vendors whether or not it publishes.
2346
+ it('reads the refusals, guards, overrides, and rewrites a declaration roll-up requires from every workspace', () => {
2347
+ const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
2348
+ // Any project path resolves these, because each reading is refused by the text before the
2349
+ // path is read. The root project is the one file every workspace carries.
2350
+ const project = resolve(root, 'tsconfig.json')
2201
2351
  expect(configHelpers.parseProjectScope('not a configuration', project)).toBeUndefined()
2202
2352
  expect(
2203
2353
  configHelpers.parseProjectScope('{"compilerOptions":{"lib":[],"types":[]}}', project),
@@ -2205,11 +2355,12 @@ describe('configuration helpers', () => {
2205
2355
  expect(
2206
2356
  configHelpers.parseProjectScope('{"compilerOptions":{"lib":[1],"rootDir":"."}}', project),
2207
2357
  ).toBeUndefined()
2358
+ // The compiler's own refusal, read from a path no workspace shape carries.
2208
2359
  expect(() =>
2209
2360
  configHelpers.readCompilerOutput(compiler, [
2210
2361
  '--showConfig',
2211
2362
  '-p',
2212
- resolve(root, 'configs/src/tsconfig.absent.json'),
2363
+ resolve(root, 'configs/tsconfig.absent.json'),
2213
2364
  ]),
2214
2365
  ).toThrow('The declaration compiler failed')
2215
2366
 
@@ -524,7 +524,17 @@ describe('skill family policy', () => {
524
524
  expect(frontmatter?.description).toBe('Use this skill when a continuation contains: a colon.')
525
525
  })
526
526
 
527
- for (const control of SKILL_POLICY_CONTROLS) {
527
+ for (const control of SKILL_POLICY_CONTROLS.filter(
528
+ (candidate) => candidate.violations !== undefined,
529
+ )) {
530
+ it(`${control.label} [membership: ${control.membership}]`, () => {
531
+ expect(inspectPolicyControl(control)).toEqual(control.violations)
532
+ })
533
+ }
534
+
535
+ for (const control of SKILL_POLICY_CONTROLS.filter(
536
+ (candidate) => candidate.violations === undefined,
537
+ )) {
528
538
  it(`${control.label} [membership: ${control.membership}]`, () => {
529
539
  const violations = inspectPolicyControl(control)
530
540
  expect(violations).toHaveLength(1)