@orkestrel/scaffold 0.0.101 → 0.0.102
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/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +6 -14
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +41 -28
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +5 -12
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +25 -13
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +18 -25
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +11 -9
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +233 -232
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +95 -89
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +5 -7
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +1273 -0
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +99 -89
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +54 -52
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +160 -161
- package/dist/bin/main.js +483 -444
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +1 -0
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +8 -11
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +58 -28
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +5 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +31 -12
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +28 -23
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +12 -10
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +318 -237
- package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +2 -5
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +7 -6
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +12 -10
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -5
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +124 -82
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +5 -7
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1655 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +128 -97
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +70 -55
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +215 -155
- package/dist/host/claude/rules/architecture.md +2 -2
- package/dist/host/claude/rules/tests.md +8 -2
- package/dist/host/claude/rules/typescript.md +130 -0
- package/dist/host/claude/rules/workspace.md +30 -12
- package/dist/host/configs/helpers.ts +206 -133
- package/dist/host/configs/policy.ts +110 -105
- package/dist/host/guides/scaffold.md +244 -170
- package/dist/host/manifest.json +35 -29
- package/dist/host/tests/config.test.ts +1220 -633
- package/dist/host/tests/policy.test.ts +19 -0
- package/dist/host/tests/setupPolicy.ts +1669 -985
- package/dist/src/core/index.cjs +1595 -1686
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +202 -18
- package/dist/src/core/index.d.ts +202 -18
- package/dist/src/core/index.js +1590 -1687
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +448 -392
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1182 -1052
- package/dist/src/server/index.d.ts +1182 -1052
- package/dist/src/server/index.js +447 -395
- package/dist/src/server/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
// P1: Every checked population must exist and be non-empty; absence fails instead of passing vacuously.
|
|
2
2
|
// P2: Required items are checked strictly; extra items are ignored before their shape is read.
|
|
3
3
|
|
|
4
|
-
import type { UserConfig } from 'vite'
|
|
4
|
+
import type { InlineConfig, UserConfig } from 'vite'
|
|
5
|
+
import type {
|
|
6
|
+
PolicyComment,
|
|
7
|
+
PolicyContext,
|
|
8
|
+
PolicyDiagnostic,
|
|
9
|
+
PolicyExpression,
|
|
10
|
+
PolicySourceCode,
|
|
11
|
+
} from '../configs/policy.js'
|
|
12
|
+
import type { PolicyProject } from './setupPolicy.js'
|
|
5
13
|
import { spawnSync } from 'node:child_process'
|
|
6
14
|
import {
|
|
15
|
+
closeSync,
|
|
7
16
|
existsSync,
|
|
8
17
|
globSync,
|
|
9
|
-
lstatSync,
|
|
10
18
|
mkdirSync,
|
|
19
|
+
openSync,
|
|
11
20
|
readdirSync,
|
|
12
21
|
readFileSync,
|
|
13
22
|
realpathSync,
|
|
@@ -16,9 +25,9 @@ import {
|
|
|
16
25
|
} from 'node:fs'
|
|
17
26
|
import { createRequire } from 'node:module'
|
|
18
27
|
import { tmpdir } from 'node:os'
|
|
19
|
-
import { dirname, join, relative, resolve } from 'node:path'
|
|
20
|
-
import {
|
|
21
|
-
import { build, createServer, loadConfigFromFile } from 'vite'
|
|
28
|
+
import { dirname, join, parse, relative, resolve } from 'node:path'
|
|
29
|
+
import { pathToFileURL } from 'node:url'
|
|
30
|
+
import { build, createServer, loadConfigFromFile, parseSync, resolveConfig } from 'vite'
|
|
22
31
|
import { RuleTester } from 'oxlint/plugins-dev'
|
|
23
32
|
import * as configHelpers from '../configs/helpers.js'
|
|
24
33
|
import policyPlugin, {
|
|
@@ -49,7 +58,11 @@ import policyPlugin, {
|
|
|
49
58
|
blankPolicyText,
|
|
50
59
|
commentToPolicyParagraph,
|
|
51
60
|
isPolicyVoiced,
|
|
61
|
+
nodeToPolicyOuter,
|
|
52
62
|
paragraphToPolicyOpener,
|
|
63
|
+
programToPolicyDocs,
|
|
64
|
+
reportDomain,
|
|
65
|
+
statementToPolicyBindings,
|
|
53
66
|
stripPolicyCode,
|
|
54
67
|
textToPolicyHits,
|
|
55
68
|
} from '../configs/policy.js'
|
|
@@ -59,6 +72,7 @@ import tsconfig from '../tsconfig.json' with { type: 'json' }
|
|
|
59
72
|
import {
|
|
60
73
|
ARTIFACT_IMPORT_CASES,
|
|
61
74
|
collectSheets,
|
|
75
|
+
collectStudies,
|
|
62
76
|
collectFrameworks,
|
|
63
77
|
readConfigRecord,
|
|
64
78
|
readConfigScript,
|
|
@@ -68,25 +82,29 @@ import {
|
|
|
68
82
|
SHEET_POLICY_BARREL_PATTERN,
|
|
69
83
|
SHEET_POLICY_ORDER_PATTERN,
|
|
70
84
|
readImportDiagnostics,
|
|
85
|
+
createPolicyLazyCases,
|
|
71
86
|
createPolicyScratch,
|
|
87
|
+
createPolicySparse,
|
|
72
88
|
inspectPolicyConfiguration,
|
|
73
89
|
inspectPolicyWiring,
|
|
74
90
|
normalizePolicyFilename,
|
|
91
|
+
POLICY_EXTRACTOR,
|
|
92
|
+
POLICY_LAZY_SHAPES,
|
|
93
|
+
POLICY_PUBLISHES,
|
|
94
|
+
POLICY_ROOT,
|
|
75
95
|
} from './setupPolicy.js'
|
|
76
96
|
import { describe, expect, it } from 'vitest'
|
|
77
97
|
|
|
78
|
-
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
79
|
-
|
|
80
98
|
describe('selected faces', () => {
|
|
81
99
|
it('collects each selected sheet proof from the effective project root and rejects a wrong root', async () => {
|
|
82
|
-
for (const face of collectSheets(
|
|
83
|
-
const proofs = resolve(
|
|
100
|
+
for (const face of collectSheets(POLICY_ROOT)) {
|
|
101
|
+
const proofs = resolve(POLICY_ROOT, 'tests/src', face)
|
|
84
102
|
if (!existsSync(proofs)) continue
|
|
85
|
-
const wrapper = resolve(
|
|
103
|
+
const wrapper = resolve(POLICY_ROOT, `configs/src/vite.${face}.config.ts`)
|
|
86
104
|
const loaded = await loadConfigFromFile(
|
|
87
105
|
{ command: 'serve', mode: 'test' },
|
|
88
106
|
wrapper,
|
|
89
|
-
|
|
107
|
+
POLICY_ROOT,
|
|
90
108
|
'silent',
|
|
91
109
|
)
|
|
92
110
|
if (loaded === null) throw new Error(`Unloaded wrapper ${wrapper}`)
|
|
@@ -95,16 +113,19 @@ describe('selected faces', () => {
|
|
|
95
113
|
if (include === undefined) throw new Error(`Missing sheet include ${face}`)
|
|
96
114
|
// Vitest roots a file project at its wrapper directory unless test.root overrides it.
|
|
97
115
|
const directory = resolve(
|
|
98
|
-
|
|
116
|
+
POLICY_ROOT,
|
|
99
117
|
project.test?.root ?? dirname(wrapper),
|
|
100
118
|
project.test?.dir || '.',
|
|
101
119
|
)
|
|
102
120
|
const collected = globSync(include, { cwd: directory }).map((path) =>
|
|
103
121
|
resolve(directory, path),
|
|
104
122
|
)
|
|
105
|
-
const expected = [
|
|
106
|
-
|
|
107
|
-
|
|
123
|
+
const expected = [
|
|
124
|
+
resolve(proofs, 'index.test.ts'),
|
|
125
|
+
...(face === 'styles' && existsSync(resolve(proofs, 'themes/index.test.ts'))
|
|
126
|
+
? [resolve(proofs, 'themes/index.test.ts')]
|
|
127
|
+
: []),
|
|
128
|
+
]
|
|
108
129
|
expect(collected).toEqual(expect.arrayContaining(expected))
|
|
109
130
|
const control = globSync(include, { cwd: dirname(wrapper) }).map((path) =>
|
|
110
131
|
resolve(dirname(wrapper), path),
|
|
@@ -151,14 +172,59 @@ describe('selected faces', () => {
|
|
|
151
172
|
}
|
|
152
173
|
})
|
|
153
174
|
|
|
175
|
+
it('collects study tests by exact case, nested or direct, and nothing a lookalike supplies', () => {
|
|
176
|
+
// Each lookalike gets its own workspace, because a host that folds case writes
|
|
177
|
+
// `tests/Studies/adapter.test.ts` into an existing `tests/studies` directory.
|
|
178
|
+
const lookalikes = [
|
|
179
|
+
['no tests directory', []],
|
|
180
|
+
['a types module alone', ['tests/studies/types.ts']],
|
|
181
|
+
['the harness module alone', ['tests/setupStudy.ts']],
|
|
182
|
+
['a root studies directory alone', ['studies/adapter.test.ts']],
|
|
183
|
+
['a case-folded directory', ['tests/Studies/adapter.test.ts']],
|
|
184
|
+
['a test of another suffix', ['tests/studies/adapter.test.js']],
|
|
185
|
+
['a studies entry that is a file', ['tests/studies']],
|
|
186
|
+
[
|
|
187
|
+
'a test inside a nested node_modules',
|
|
188
|
+
['tests/studies/fixture/node_modules/kit/index.test.ts'],
|
|
189
|
+
],
|
|
190
|
+
['a test inside a .git directory', ['tests/studies/fixture/.git/hooks/index.test.ts']],
|
|
191
|
+
] as const
|
|
192
|
+
for (const [label, paths] of lookalikes) {
|
|
193
|
+
const fixture = createPolicyScratch({ prefix: 'config-studies-lookalike-' })
|
|
194
|
+
try {
|
|
195
|
+
for (const path of paths) fixture.write(path, '')
|
|
196
|
+
expect({ label, studies: collectStudies(fixture.path) }).toEqual({ label, studies: [] })
|
|
197
|
+
} finally {
|
|
198
|
+
fixture.destroy()
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const fixture = createPolicyScratch({ prefix: 'config-studies-' })
|
|
202
|
+
try {
|
|
203
|
+
for (const path of [
|
|
204
|
+
'tests/studies/types.ts',
|
|
205
|
+
'tests/studies/memory/deep/adapter.test.ts',
|
|
206
|
+
'tests/studies/session.test.ts',
|
|
207
|
+
'tests/studies/memory/adapter.test.js',
|
|
208
|
+
'src/core/index.ts',
|
|
209
|
+
])
|
|
210
|
+
fixture.write(path, '')
|
|
211
|
+
expect(collectStudies(fixture.path)).toEqual([
|
|
212
|
+
'tests/studies/memory/deep/adapter.test.ts',
|
|
213
|
+
'tests/studies/session.test.ts',
|
|
214
|
+
])
|
|
215
|
+
} finally {
|
|
216
|
+
fixture.destroy()
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
|
|
154
220
|
it('loads each selected sheet and framework wrapper and checks its packaging and browser project', async () => {
|
|
155
221
|
const manifest = readConfigRecord(
|
|
156
|
-
JSON.parse(readFileSync(resolve(
|
|
222
|
+
JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8')),
|
|
157
223
|
)
|
|
158
224
|
const scripts = readConfigRecord(manifest.scripts)
|
|
159
|
-
const sheets = collectSheets(
|
|
160
|
-
const frameworks = collectFrameworks(
|
|
161
|
-
const wrappers = collectFaceWrappers(
|
|
225
|
+
const sheets = collectSheets(POLICY_ROOT)
|
|
226
|
+
const frameworks = collectFrameworks(POLICY_ROOT)
|
|
227
|
+
const wrappers = collectFaceWrappers(POLICY_ROOT)
|
|
162
228
|
const readings: Array<{
|
|
163
229
|
readonly axis: string
|
|
164
230
|
readonly face: string
|
|
@@ -171,8 +237,8 @@ describe('selected faces', () => {
|
|
|
171
237
|
for (const wrapper of wrappers) {
|
|
172
238
|
const loaded = await loadConfigFromFile(
|
|
173
239
|
{ command: 'build', mode: 'test' },
|
|
174
|
-
resolve(
|
|
175
|
-
|
|
240
|
+
resolve(POLICY_ROOT, wrapper),
|
|
241
|
+
POLICY_ROOT,
|
|
176
242
|
'silent',
|
|
177
243
|
)
|
|
178
244
|
if (loaded === null) throw new Error(`Unloaded wrapper ${wrapper}`)
|
|
@@ -199,7 +265,7 @@ describe('selected faces', () => {
|
|
|
199
265
|
for (const { face, wrapper, project } of readings.filter((reading) =>
|
|
200
266
|
sheets.includes(reading.face),
|
|
201
267
|
)) {
|
|
202
|
-
expect(readFileSync(resolve(
|
|
268
|
+
expect(readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8')).toContain('sheetProject(')
|
|
203
269
|
expect(inspectSheetConfiguration(project)).toEqual([])
|
|
204
270
|
const exports = readConfigRecord(manifest.exports)
|
|
205
271
|
expect(exports[`./${face}`]).toBe(`./dist/src/${face}/index.css`)
|
|
@@ -222,23 +288,23 @@ describe('selected faces', () => {
|
|
|
222
288
|
for (const { wrapper } of readings.filter(
|
|
223
289
|
(reading) => reading.axis === 'src' && frameworks.includes(`${reading.axis}/${reading.face}`),
|
|
224
290
|
)) {
|
|
225
|
-
const text = readFileSync(resolve(
|
|
291
|
+
const text = readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8')
|
|
226
292
|
expect(text).toContain('rewriteCoreSpecifier')
|
|
227
293
|
expect(text).toContain('rewriteBrowserSpecifier')
|
|
228
294
|
}
|
|
229
|
-
if (!existsSync(resolve(
|
|
295
|
+
if (!existsSync(resolve(POLICY_ROOT, 'src/styles/themes/index.scss'))) return
|
|
230
296
|
const loaded = await loadConfigFromFile(
|
|
231
297
|
{ command: 'build', mode: 'test' },
|
|
232
|
-
resolve(
|
|
233
|
-
|
|
298
|
+
resolve(POLICY_ROOT, 'configs/src/vite.themes.config.ts'),
|
|
299
|
+
POLICY_ROOT,
|
|
234
300
|
'silent',
|
|
235
301
|
)
|
|
236
302
|
if (loaded === null) throw new Error('Unloaded themes wrapper')
|
|
237
303
|
expect(loaded.config.build?.outDir).toBe('dist/src/styles/themes')
|
|
238
|
-
const barrel = readFileSync(resolve(
|
|
304
|
+
const barrel = readFileSync(resolve(POLICY_ROOT, 'src/styles/themes/index.scss'), 'utf8')
|
|
239
305
|
expect(readSheetPrelude(barrel)).toMatch(SHEET_POLICY_BARREL_PATTERN)
|
|
240
306
|
expect(
|
|
241
|
-
readSheetPrelude(readFileSync(resolve(
|
|
307
|
+
readSheetPrelude(readFileSync(resolve(POLICY_ROOT, 'src/styles/_tokens.scss'), 'utf8')),
|
|
242
308
|
).toMatch(SHEET_POLICY_ORDER_PATTERN)
|
|
243
309
|
const target = sheets.includes('styles') ? 'styles' : 'themes'
|
|
244
310
|
const command = sheets.includes('styles')
|
|
@@ -272,25 +338,29 @@ describe('selected faces', () => {
|
|
|
272
338
|
it('resolves showcase and journey modes for every occupied application', async () => {
|
|
273
339
|
const applications = [
|
|
274
340
|
'browser',
|
|
275
|
-
...collectFrameworks(
|
|
341
|
+
...collectFrameworks(POLICY_ROOT)
|
|
276
342
|
.filter((face) => face.startsWith('app/'))
|
|
277
343
|
.map((face) => face.slice(4)),
|
|
278
344
|
]
|
|
279
345
|
const manifest = readConfigRecord(
|
|
280
|
-
JSON.parse(readFileSync(resolve(
|
|
346
|
+
JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8')),
|
|
281
347
|
)
|
|
282
348
|
const scripts = readConfigRecord(manifest.scripts)
|
|
283
349
|
const factories = readConfigRecord(rootConfiguration)
|
|
284
350
|
const showcases = ['configs/app/vite.showcase.config.ts'].filter(
|
|
285
|
-
(path) =>
|
|
351
|
+
(path) =>
|
|
352
|
+
existsSync(resolve(POLICY_ROOT, path)) || existsSync(resolve(POLICY_ROOT, 'showcase')),
|
|
286
353
|
)
|
|
287
354
|
for (const showcase of showcases) {
|
|
288
|
-
const wrapper = readFileSync(
|
|
355
|
+
const wrapper = readFileSync(
|
|
356
|
+
resolve(POLICY_ROOT, 'configs/app/vite.showcase.config.ts'),
|
|
357
|
+
'utf8',
|
|
358
|
+
)
|
|
289
359
|
expect(wrapper).toContain('defineConfig(({ mode }) => appShowcase(mode))')
|
|
290
360
|
expect(wrapper).not.toContain('dist/showcase')
|
|
291
|
-
expect(
|
|
292
|
-
'
|
|
293
|
-
)
|
|
361
|
+
expect(
|
|
362
|
+
readFileSync(resolve(POLICY_ROOT, '.prettierignore'), 'utf8').split(/\r\n|\n/u),
|
|
363
|
+
).toContain('showcase/')
|
|
294
364
|
for (const application of applications) {
|
|
295
365
|
const script = 'build:showcase' + (application === 'browser' ? '' : ':' + application)
|
|
296
366
|
expect(readConfigScript(scripts, script)).toContain('configs/app/vite.showcase.config.ts')
|
|
@@ -302,27 +372,27 @@ describe('selected faces', () => {
|
|
|
302
372
|
}
|
|
303
373
|
const loaded = await loadConfigFromFile(
|
|
304
374
|
{ command: 'build', mode: application },
|
|
305
|
-
resolve(
|
|
306
|
-
|
|
375
|
+
resolve(POLICY_ROOT, showcase),
|
|
376
|
+
POLICY_ROOT,
|
|
307
377
|
'silent',
|
|
308
378
|
)
|
|
309
379
|
if (loaded === null) throw new Error('Unloaded showcase wrapper')
|
|
310
380
|
const output = loaded.config.build?.outDir
|
|
311
381
|
if (output === undefined) throw new Error('Missing showcase output')
|
|
312
|
-
expect(relative(
|
|
382
|
+
expect(relative(POLICY_ROOT, output)).toBe('showcase')
|
|
313
383
|
expect(() =>
|
|
314
|
-
expect(relative(
|
|
384
|
+
expect(relative(POLICY_ROOT, resolve(POLICY_ROOT, 'dist/showcase'))).toBe('showcase'),
|
|
315
385
|
).toThrow('expected')
|
|
316
386
|
expect(loaded.config.build?.emptyOutDir).toBe(false)
|
|
317
387
|
}
|
|
318
388
|
expect(scripts.show).toBeUndefined()
|
|
319
389
|
}
|
|
320
|
-
if (!existsSync(resolve(
|
|
390
|
+
if (!existsSync(resolve(POLICY_ROOT, 'configs/app/vite.journey.config.ts'))) return
|
|
321
391
|
for (const application of applications) {
|
|
322
392
|
const loaded = await loadConfigFromFile(
|
|
323
393
|
{ command: 'serve', mode: application },
|
|
324
|
-
resolve(
|
|
325
|
-
|
|
394
|
+
resolve(POLICY_ROOT, 'configs/app/vite.journey.config.ts'),
|
|
395
|
+
POLICY_ROOT,
|
|
326
396
|
'silent',
|
|
327
397
|
)
|
|
328
398
|
if (
|
|
@@ -331,8 +401,8 @@ describe('selected faces', () => {
|
|
|
331
401
|
loaded.config.test.projects.length === 0
|
|
332
402
|
)
|
|
333
403
|
throw new Error('Missing journey projects')
|
|
334
|
-
|
|
335
|
-
const
|
|
404
|
+
// Each awaited factory appends the variant it provides and the variants it declares.
|
|
405
|
+
const provided: Array<{ readonly variant: string; readonly names: readonly string[] }> = []
|
|
336
406
|
for (const factory of loaded.config.test.projects) {
|
|
337
407
|
if (typeof factory !== 'function') throw new Error('Missing journey factory')
|
|
338
408
|
const project = readConfigRecord(
|
|
@@ -344,12 +414,14 @@ describe('selected faces', () => {
|
|
|
344
414
|
const provide = readConfigRecord(test.provide)
|
|
345
415
|
if (typeof provide.variant !== 'string' || !Array.isArray(provide.variants))
|
|
346
416
|
throw new Error('Missing journey variant values')
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
417
|
+
provided.push({
|
|
418
|
+
variant: provide.variant,
|
|
419
|
+
names: provide.variants.map((value: unknown) => {
|
|
420
|
+
const variant = readConfigRecord(value)
|
|
421
|
+
if (typeof variant.name !== 'string') throw new Error('Unnamed journey variant')
|
|
422
|
+
return variant.name
|
|
423
|
+
}),
|
|
424
|
+
})
|
|
353
425
|
const selected = provide.variants
|
|
354
426
|
.map(readConfigRecord)
|
|
355
427
|
.find((variant) => variant.name === provide.variant)
|
|
@@ -371,7 +443,51 @@ describe('selected faces', () => {
|
|
|
371
443
|
expect.arrayContaining(['@orkestrel/test', '@orkestrel/test/browser']),
|
|
372
444
|
)
|
|
373
445
|
}
|
|
374
|
-
expect(
|
|
446
|
+
expect(new Set(provided.map(({ variant }) => variant))).toEqual(
|
|
447
|
+
new Set(provided.flatMap(({ names }) => names)),
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
// The runner loads the wrapper beside this file's own root configuration, so every journey
|
|
451
|
+
// project's record and the commands module's export are one object graph. A wrapper can
|
|
452
|
+
// add commands of its own; it keeps every command the module registers.
|
|
453
|
+
const registered = (
|
|
454
|
+
await Promise.all(
|
|
455
|
+
Object.values(import.meta.glob<unknown>('../configs/commands.ts')).map((load) => load()),
|
|
456
|
+
)
|
|
457
|
+
).map((module) => readConfigRecord(readConfigRecord(module).commands))
|
|
458
|
+
const wrappers = await Promise.all(
|
|
459
|
+
Object.values(
|
|
460
|
+
import.meta.glob<unknown>('../configs/app/vite.journey.config.ts', { import: 'default' }),
|
|
461
|
+
).map((load) => load()),
|
|
462
|
+
)
|
|
463
|
+
expect(wrappers).toHaveLength(1)
|
|
464
|
+
for (const wrapper of wrappers) {
|
|
465
|
+
if (typeof wrapper !== 'function') throw new Error('Missing journey wrapper factory')
|
|
466
|
+
for (const application of applications) {
|
|
467
|
+
const composed = readConfigRecord(
|
|
468
|
+
Reflect.apply(wrapper, undefined, [{ command: 'serve', mode: application }]),
|
|
469
|
+
)
|
|
470
|
+
const projects = readConfigRecord(composed.test).projects
|
|
471
|
+
if (!Array.isArray(projects) || projects.length === 0)
|
|
472
|
+
throw new Error('Missing journey projects')
|
|
473
|
+
for (const project of projects) {
|
|
474
|
+
if (typeof project !== 'function') throw new Error('Missing journey factory')
|
|
475
|
+
const held = readConfigRecord(
|
|
476
|
+
readConfigRecord(
|
|
477
|
+
readConfigRecord(
|
|
478
|
+
readConfigRecord(
|
|
479
|
+
await Reflect.apply(project, undefined, [
|
|
480
|
+
{ command: 'serve', mode: application },
|
|
481
|
+
]),
|
|
482
|
+
).test,
|
|
483
|
+
).browser,
|
|
484
|
+
).commands,
|
|
485
|
+
)
|
|
486
|
+
for (const commands of registered) {
|
|
487
|
+
for (const [key, command] of Object.entries(commands)) expect(held[key]).toBe(command)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
375
491
|
}
|
|
376
492
|
const factory = factories.appJourney
|
|
377
493
|
if (typeof factory !== 'function') throw new Error('Missing application journey')
|
|
@@ -384,15 +500,56 @@ describe('selected faces', () => {
|
|
|
384
500
|
).toThrow('not declared')
|
|
385
501
|
})
|
|
386
502
|
|
|
387
|
-
it('collects both browser setup proofs and optimizes every selected browser factory', () => {
|
|
503
|
+
it('collects both browser setup proofs and optimizes every selected browser factory', async () => {
|
|
388
504
|
const factories = readConfigRecord(rootConfiguration)
|
|
389
505
|
const manifest = readConfigRecord(
|
|
390
|
-
JSON.parse(readFileSync(resolve(
|
|
506
|
+
JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8')),
|
|
391
507
|
)
|
|
392
508
|
const dependencies = {
|
|
393
509
|
...readConfigRecord(manifest.dependencies ?? {}),
|
|
394
510
|
...readConfigRecord(manifest.devDependencies ?? {}),
|
|
395
511
|
}
|
|
512
|
+
// The runner loads the module this file's root configuration imports, so each factory's
|
|
513
|
+
// record and the module's export are one object graph.
|
|
514
|
+
const registered = (
|
|
515
|
+
await Promise.all(
|
|
516
|
+
Object.values(import.meta.glob<unknown>('../configs/commands.ts')).map((load) => load()),
|
|
517
|
+
)
|
|
518
|
+
).map((module) => readConfigRecord(readConfigRecord(module).commands))
|
|
519
|
+
// The configuration bundle carries `configs/commands.ts` into every load, builds included,
|
|
520
|
+
// and leaves an alias it cannot resolve as a bare package import. The module therefore
|
|
521
|
+
// imports only a `node:` module or a declared package, and loads workspace source inside a
|
|
522
|
+
// command. The control carries both refused forms.
|
|
523
|
+
const control =
|
|
524
|
+
"import { GREETING } from '@src/core'\nexport * from '../src/server/index.js'\nimport { join } from 'node:path'\nimport type { BrowserConfigOptions } from 'vitest/node'\n"
|
|
525
|
+
for (const [path, text, refused] of [
|
|
526
|
+
['control', control, ['@src/core', '../src/server/index.js']],
|
|
527
|
+
...['configs/commands.ts']
|
|
528
|
+
.filter((module) => existsSync(resolve(POLICY_ROOT, module)))
|
|
529
|
+
.map((module) => [module, readFileSync(resolve(POLICY_ROOT, module), 'utf8'), []] as const),
|
|
530
|
+
] as const) {
|
|
531
|
+
const specifiers = parseSync('commands.ts', text).program.body.flatMap((statement) =>
|
|
532
|
+
statement.type === 'ImportDeclaration' ||
|
|
533
|
+
statement.type === 'ExportAllDeclaration' ||
|
|
534
|
+
(statement.type === 'ExportNamedDeclaration' && statement.source !== null)
|
|
535
|
+
? [statement.source?.value ?? '']
|
|
536
|
+
: [],
|
|
537
|
+
)
|
|
538
|
+
expect({
|
|
539
|
+
path,
|
|
540
|
+
refused: specifiers.filter(
|
|
541
|
+
(specifier) =>
|
|
542
|
+
!specifier.startsWith('node:') &&
|
|
543
|
+
!Object.hasOwn(
|
|
544
|
+
dependencies,
|
|
545
|
+
specifier
|
|
546
|
+
.split('/')
|
|
547
|
+
.slice(0, specifier.startsWith('@') ? 2 : 1)
|
|
548
|
+
.join('/'),
|
|
549
|
+
),
|
|
550
|
+
),
|
|
551
|
+
}).toStrictEqual({ path, refused })
|
|
552
|
+
}
|
|
396
553
|
for (const name of [
|
|
397
554
|
'srcBrowser',
|
|
398
555
|
'appBrowser',
|
|
@@ -423,9 +580,13 @@ describe('selected faces', () => {
|
|
|
423
580
|
...(dependencies['@orkestrel/contract'] === undefined ? [] : ['@orkestrel/contract']),
|
|
424
581
|
]),
|
|
425
582
|
)
|
|
583
|
+
for (const commands of registered) {
|
|
584
|
+
const held = readConfigRecord(readConfigRecord(test.browser).commands)
|
|
585
|
+
for (const [key, command] of Object.entries(commands)) expect(held[key]).toBe(command)
|
|
586
|
+
}
|
|
426
587
|
}
|
|
427
588
|
for (const proof of ['tests/setupBrowser.test.ts', 'tests/setupStyles.test.ts'].filter((path) =>
|
|
428
|
-
existsSync(resolve(
|
|
589
|
+
existsSync(resolve(POLICY_ROOT, path)),
|
|
429
590
|
)) {
|
|
430
591
|
const factory = factories.setupBrowser
|
|
431
592
|
if (typeof factory !== 'function') throw new Error('Missing browser setup project')
|
|
@@ -444,59 +605,32 @@ describe('selected faces', () => {
|
|
|
444
605
|
})
|
|
445
606
|
})
|
|
446
607
|
|
|
447
|
-
// The `src` axis a declaration roll-up reads, derived from a recognized source environment that is
|
|
448
|
-
// a physical directory rather than from a `src` entry of any shape. A regular file, an empty
|
|
449
|
-
// directory, a directory holding no recognized environment, a symbolic link, and an entry this host
|
|
450
|
-
// refuses to inspect each leave a workspace publishing nothing, and reading the entry alone would
|
|
451
|
-
// put every one of them under the face proof. The inspection is caught rather than excused by
|
|
452
|
-
// option: `throwIfNoEntry` excuses an absent path alone and rethrows a permission refusal, and a
|
|
453
|
-
// throw on this line is a collection error that takes this whole file down in that target rather
|
|
454
|
-
// than failing one case. `isPhysicalDirectory` reads the same two facts off an `lstat` and returns
|
|
455
|
-
// false for every inspection error, and this reads them the same way. This is the derivation
|
|
456
|
-
// `targetToEnvironments` uses, which filters the environment names by a contained physical
|
|
457
|
-
// directory, and it answers the same question `blueprintToScripts`
|
|
458
|
-
// answers from a blueprint's own `src` list. A workspace declaring app environments alone publishes
|
|
459
|
-
// no library, so it vendors no `configs/src/` face wrapper and has no roll-up to measure. A face
|
|
460
|
-
// proof is conditioned on this axis rather than on a face wrapper: a workspace that declares the
|
|
461
|
-
// axis and vendors no wrapper carries the defect such a proof reports, so that workspace must still
|
|
462
|
-
// fail the proof. The manifest cases read the same fact from their own `private` flag and name it
|
|
463
|
-
// `manifestPublishes` for the source that decides it there; this reads the axis on disk, so a
|
|
464
|
-
// workspace whose manifest and shape disagree fails one of the two rather than neither.
|
|
465
|
-
const publishes = ['core', 'browser', 'server'].some((environment) => {
|
|
466
|
-
try {
|
|
467
|
-
const entry = lstatSync(resolve(root, 'src', environment))
|
|
468
|
-
return entry.isDirectory() && !entry.isSymbolicLink()
|
|
469
|
-
} catch {
|
|
470
|
-
return false
|
|
471
|
-
}
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
// A declaration roll-up loads the extractor package, which only a workspace publishing source from
|
|
475
|
-
// `src` installs. The resolution below is the mechanism its proof is conditioned on.
|
|
476
|
-
let extractorPath: string | undefined
|
|
477
|
-
try {
|
|
478
|
-
extractorPath = createRequire(import.meta.url).resolve('@microsoft/api-extractor')
|
|
479
|
-
} catch {
|
|
480
|
-
extractorPath = undefined
|
|
481
|
-
}
|
|
482
|
-
|
|
483
608
|
describe('root configuration', () => {
|
|
484
609
|
it('resolves every declared alias to its real entry', () => {
|
|
485
610
|
const aliases = configuration.resolve?.alias
|
|
486
611
|
if (typeof aliases !== 'object' || aliases === null || Array.isArray(aliases)) {
|
|
487
612
|
throw new Error('The root configuration carries no alias record')
|
|
488
613
|
}
|
|
489
|
-
const required = new Map<string, string>(
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
614
|
+
const required = new Map<string, string>([
|
|
615
|
+
...['src', 'app'].flatMap((axis) =>
|
|
616
|
+
['core', 'browser', 'server'].flatMap((environment): Array<[string, string]> => {
|
|
617
|
+
const path = `${axis}/${environment}/index.ts`
|
|
618
|
+
return existsSync(resolve(POLICY_ROOT, path)) ? [[`@${axis}/${environment}`, path]] : []
|
|
619
|
+
}),
|
|
620
|
+
),
|
|
621
|
+
...collectSheets(POLICY_ROOT).map((face): [string, string] => [
|
|
622
|
+
`@src/${face}`,
|
|
623
|
+
`src/${face}/index.ts`,
|
|
624
|
+
]),
|
|
625
|
+
...collectFrameworks(POLICY_ROOT).map((face): [string, string] => [
|
|
626
|
+
`@${face}`,
|
|
627
|
+
`${face}/index.ts`,
|
|
628
|
+
]),
|
|
629
|
+
])
|
|
498
630
|
const declared = new Map(Object.entries(tsconfig.compilerOptions.paths))
|
|
499
|
-
expect(
|
|
631
|
+
expect(
|
|
632
|
+
required.size > 0 || existsSync(resolve(POLICY_ROOT, 'src/styles/themes/sheet.ts')),
|
|
633
|
+
).toBe(true)
|
|
500
634
|
if (required.size === 0) {
|
|
501
635
|
if (declared.size !== 0 || Object.keys(aliases).length !== 0)
|
|
502
636
|
throw new Error('The themes-only workspace declares an unexpected alias')
|
|
@@ -515,138 +649,146 @@ describe('root configuration', () => {
|
|
|
515
649
|
if (path === undefined) throw new Error(`${key} carries no target`)
|
|
516
650
|
const target = resolveWorkspacePath(path)
|
|
517
651
|
expect(existsSync(target)).toBe(true)
|
|
518
|
-
expect(target).toBe(resolve(
|
|
652
|
+
expect(target).toBe(resolve(POLICY_ROOT, expected))
|
|
519
653
|
expect(Object.getOwnPropertyDescriptor(aliases, key)?.value).toBe(target)
|
|
520
654
|
}
|
|
521
655
|
})
|
|
522
656
|
|
|
523
657
|
it('registers every workspace project with its fixed include and setup files', () => {
|
|
524
|
-
const expected = new Map<
|
|
525
|
-
string,
|
|
526
|
-
{
|
|
527
|
-
readonly benchmark?: readonly string[]
|
|
528
|
-
readonly include: string | readonly string[]
|
|
529
|
-
readonly parallel?: boolean
|
|
530
|
-
readonly pool?: string
|
|
531
|
-
readonly setup: readonly string[]
|
|
532
|
-
}
|
|
533
|
-
>()
|
|
534
|
-
if (existsSync(resolve(root, 'src/core'))) {
|
|
535
|
-
expected.set('src:core', {
|
|
536
|
-
include: 'tests/src/core/**/*.test.ts',
|
|
537
|
-
setup: ['./tests/setup.ts'],
|
|
538
|
-
})
|
|
539
|
-
}
|
|
540
|
-
if (existsSync(resolve(root, 'src/browser'))) {
|
|
541
|
-
expected.set('src:browser', {
|
|
542
|
-
include: 'tests/src/browser/**/*.test.ts',
|
|
543
|
-
setup: ['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
544
|
-
})
|
|
545
|
-
}
|
|
546
|
-
if (existsSync(resolve(root, 'src/server'))) {
|
|
547
|
-
expected.set('src:server', {
|
|
548
|
-
include: 'tests/src/server/**/*.test.ts',
|
|
549
|
-
setup: ['./tests/setup.ts', './tests/setupServer.ts'],
|
|
550
|
-
})
|
|
551
|
-
}
|
|
552
|
-
if (existsSync(resolve(root, 'src/bin'))) {
|
|
553
|
-
expected.set('src:bin', {
|
|
554
|
-
include: 'tests/src/bin/**/*.test.ts',
|
|
555
|
-
setup: ['./tests/setup.ts', './tests/setupServer.ts'],
|
|
556
|
-
})
|
|
557
|
-
}
|
|
558
|
-
if (existsSync(resolve(root, 'app/core'))) {
|
|
559
|
-
expected.set('app:core', {
|
|
560
|
-
include: 'tests/app/core/**/*.test.ts',
|
|
561
|
-
setup: ['./tests/setup.ts'],
|
|
562
|
-
})
|
|
563
|
-
}
|
|
564
|
-
if (existsSync(resolve(root, 'app/browser'))) {
|
|
565
|
-
expected.set('app:browser', {
|
|
566
|
-
include: 'tests/app/browser/**/*.test.ts',
|
|
567
|
-
setup: ['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
568
|
-
})
|
|
569
|
-
}
|
|
570
|
-
if (existsSync(resolve(root, 'app/server'))) {
|
|
571
|
-
expected.set('app:server', {
|
|
572
|
-
include: 'tests/app/server/**/*.test.ts',
|
|
573
|
-
setup: ['./tests/setup.ts', './tests/setupServer.ts'],
|
|
574
|
-
})
|
|
575
|
-
}
|
|
576
|
-
// The `configs/agents/tsconfig.skills.json` wrapper selects the `skills` blueprint fact, and the
|
|
577
|
-
// project it registers runs the mirrored proofs under `tests/agents/`.
|
|
578
|
-
if (existsSync(resolve(root, 'configs/agents/tsconfig.skills.json'))) {
|
|
579
|
-
expected.set('skills', {
|
|
580
|
-
include: 'tests/agents/**/*.test.ts',
|
|
581
|
-
setup: ['./tests/setup.ts'],
|
|
582
|
-
})
|
|
583
|
-
}
|
|
584
|
-
for (const label of [
|
|
585
|
-
'policy',
|
|
586
|
-
'config',
|
|
587
|
-
'guides',
|
|
588
|
-
'conformance',
|
|
589
|
-
'distribution',
|
|
590
|
-
'integration',
|
|
591
|
-
]) {
|
|
592
|
-
if (!existsSync(resolve(root, `tests/${label}.test.ts`))) continue
|
|
593
|
-
const setup = ['./tests/setup.ts']
|
|
594
|
-
if (label === 'conformance') setup.push('./tests/setupServer.ts')
|
|
595
|
-
if (
|
|
596
|
-
label === 'integration' &&
|
|
597
|
-
(collectSheets(root).length > 0 ||
|
|
598
|
-
existsSync(resolve(root, 'src/styles/themes/index.scss')))
|
|
599
|
-
)
|
|
600
|
-
setup.push('./tests/setupBrowser.ts', './tests/setupStyles.ts')
|
|
601
|
-
expected.set(label, {
|
|
602
|
-
include: `tests/${label}.test.ts`,
|
|
603
|
-
setup,
|
|
604
|
-
})
|
|
605
|
-
}
|
|
606
658
|
const browserProofs = ['tests/setupBrowser.test.ts', 'tests/setupStyles.test.ts']
|
|
607
|
-
const setupProofs = globSync('tests/setup*.test.ts', { cwd:
|
|
659
|
+
const setupProofs = globSync('tests/setup*.test.ts', { cwd: POLICY_ROOT })
|
|
608
660
|
.map((path) => path.replaceAll('\\', '/'))
|
|
609
661
|
.filter((path) => /^tests\/setup[^/]*\.test\.ts$/u.test(path))
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
662
|
+
const expected = new Map<string, PolicyProject>([
|
|
663
|
+
// Each row is a project label, the path whose presence selects it, its include, and its setup.
|
|
664
|
+
...(
|
|
665
|
+
[
|
|
666
|
+
['src:core', 'src/core', 'tests/src/core/**/*.test.ts', ['./tests/setup.ts']],
|
|
667
|
+
[
|
|
668
|
+
'src:browser',
|
|
669
|
+
'src/browser',
|
|
670
|
+
'tests/src/browser/**/*.test.ts',
|
|
671
|
+
['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
672
|
+
],
|
|
673
|
+
[
|
|
674
|
+
'src:server',
|
|
675
|
+
'src/server',
|
|
676
|
+
'tests/src/server/**/*.test.ts',
|
|
677
|
+
['./tests/setup.ts', './tests/setupServer.ts'],
|
|
678
|
+
],
|
|
679
|
+
[
|
|
680
|
+
'src:bin',
|
|
681
|
+
'src/bin',
|
|
682
|
+
'tests/src/bin/**/*.test.ts',
|
|
683
|
+
['./tests/setup.ts', './tests/setupServer.ts'],
|
|
684
|
+
],
|
|
685
|
+
['app:core', 'app/core', 'tests/app/core/**/*.test.ts', ['./tests/setup.ts']],
|
|
686
|
+
[
|
|
687
|
+
'app:browser',
|
|
688
|
+
'app/browser',
|
|
689
|
+
'tests/app/browser/**/*.test.ts',
|
|
690
|
+
['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
691
|
+
],
|
|
692
|
+
[
|
|
693
|
+
'app:server',
|
|
694
|
+
'app/server',
|
|
695
|
+
'tests/app/server/**/*.test.ts',
|
|
696
|
+
['./tests/setup.ts', './tests/setupServer.ts'],
|
|
697
|
+
],
|
|
698
|
+
// The `configs/agents/tsconfig.skills.json` wrapper selects the `skills` blueprint fact, and the
|
|
699
|
+
// project it registers runs the mirrored proofs under `tests/agents/`.
|
|
700
|
+
[
|
|
701
|
+
'skills',
|
|
702
|
+
'configs/agents/tsconfig.skills.json',
|
|
703
|
+
'tests/agents/**/*.test.ts',
|
|
704
|
+
['./tests/setup.ts'],
|
|
705
|
+
],
|
|
706
|
+
] as const
|
|
707
|
+
)
|
|
708
|
+
.filter(([, path]) => existsSync(resolve(POLICY_ROOT, path)))
|
|
709
|
+
.map(([label, , include, setup]): [string, PolicyProject] => [label, { include, setup }]),
|
|
710
|
+
...['policy', 'config', 'guides', 'conformance', 'distribution', 'integration']
|
|
711
|
+
.filter((label) => existsSync(resolve(POLICY_ROOT, `tests/${label}.test.ts`)))
|
|
712
|
+
.map((label): [string, PolicyProject] => [
|
|
713
|
+
label,
|
|
714
|
+
{
|
|
715
|
+
include: `tests/${label}.test.ts`,
|
|
716
|
+
setup: [
|
|
717
|
+
'./tests/setup.ts',
|
|
718
|
+
...(label === 'conformance' ? ['./tests/setupServer.ts'] : []),
|
|
719
|
+
...(label === 'integration' &&
|
|
720
|
+
(collectSheets(POLICY_ROOT).length > 0 ||
|
|
721
|
+
existsSync(resolve(POLICY_ROOT, 'src/styles/themes/index.scss')))
|
|
722
|
+
? ['./tests/setupBrowser.ts', './tests/setupStyles.ts']
|
|
723
|
+
: []),
|
|
724
|
+
],
|
|
725
|
+
},
|
|
726
|
+
]),
|
|
727
|
+
...(setupProofs.some((path) => !browserProofs.includes(path))
|
|
728
|
+
? [
|
|
729
|
+
['setup', { include: 'tests/setup*.test.ts', setup: ['./tests/setup.ts'] }] satisfies [
|
|
730
|
+
string,
|
|
731
|
+
PolicyProject,
|
|
732
|
+
],
|
|
733
|
+
]
|
|
734
|
+
: []),
|
|
735
|
+
...(setupProofs.some((path) => browserProofs.includes(path))
|
|
736
|
+
? [
|
|
737
|
+
[
|
|
738
|
+
'setup:browser',
|
|
739
|
+
{ include: browserProofs, setup: ['./tests/setup.ts', './tests/setupBrowser.ts'] },
|
|
740
|
+
] satisfies [string, PolicyProject],
|
|
741
|
+
]
|
|
742
|
+
: []),
|
|
743
|
+
// The live-service project covers a directory rather than one proof, so its
|
|
744
|
+
// readiness module is the fact that selects it. A suite beneath
|
|
745
|
+
// `tests/service` with no setup module is a project nothing configures.
|
|
746
|
+
...(existsSync(resolve(POLICY_ROOT, 'tests/setupService.ts'))
|
|
747
|
+
? [
|
|
748
|
+
[
|
|
749
|
+
'service',
|
|
750
|
+
{
|
|
751
|
+
include: 'tests/service/**/*.test.ts',
|
|
752
|
+
setup: ['./tests/setup.ts', './tests/setupService.ts'],
|
|
753
|
+
},
|
|
754
|
+
] satisfies [string, PolicyProject],
|
|
755
|
+
]
|
|
756
|
+
: []),
|
|
757
|
+
// The study project covers a directory too, and the exact-case test files beneath
|
|
758
|
+
// `tests/studies` select it, outside any `node_modules` or `.git` directory, which Vitest's
|
|
759
|
+
// default exclude never collects. `tests/setupStudy.ts` is a harness module those tests import
|
|
760
|
+
// and no project loads it as a setup file, so the setup list names `tests/setup.ts` alone.
|
|
761
|
+
...(collectStudies(POLICY_ROOT).length > 0
|
|
762
|
+
? [
|
|
763
|
+
[
|
|
764
|
+
'studies',
|
|
765
|
+
{ include: 'tests/studies/**/*.test.ts', setup: ['./tests/setup.ts'] },
|
|
766
|
+
] satisfies [string, PolicyProject],
|
|
767
|
+
]
|
|
768
|
+
: []),
|
|
769
|
+
[
|
|
770
|
+
'probe',
|
|
771
|
+
{
|
|
772
|
+
benchmark: ['tmp/probes/**/*.test.ts', 'tests/**/*.test.ts'],
|
|
773
|
+
include: 'tmp/probes/**/*.test.ts',
|
|
774
|
+
parallel: false,
|
|
775
|
+
pool: 'threads',
|
|
776
|
+
setup: ['./tests/setup.ts'],
|
|
777
|
+
},
|
|
778
|
+
],
|
|
779
|
+
// A row that is a configuration rather than a factory. Every generated
|
|
780
|
+
// workspace registers the factory itself, so this shape is required by the
|
|
781
|
+
// expectation rather than observed: the proof exercises that resolution wherever it runs
|
|
782
|
+
// instead of only where a hand-written configuration happens to produce it.
|
|
783
|
+
['concrete', { include: 'tests/concrete.test.ts', setup: ['./tests/setup.ts'] }],
|
|
784
|
+
])
|
|
643
785
|
|
|
644
786
|
const projects = configuration.test?.projects
|
|
645
787
|
if (!Array.isArray(projects)) throw new Error('The root configuration carries no projects')
|
|
646
|
-
|
|
788
|
+
const extraLoads: string[] = []
|
|
647
789
|
const control = Object.defineProperty(
|
|
648
790
|
() => {
|
|
649
|
-
|
|
791
|
+
extraLoads.push('control')
|
|
650
792
|
return {
|
|
651
793
|
test: {
|
|
652
794
|
name: { label: 'control' },
|
|
@@ -666,109 +808,107 @@ describe('root configuration', () => {
|
|
|
666
808
|
},
|
|
667
809
|
}
|
|
668
810
|
const controlled = projects.concat(control, concrete)
|
|
669
|
-
const configured = new Map
|
|
670
|
-
string,
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (typeof candidate !== 'object' || candidate === null) return false
|
|
690
|
-
const block: unknown = Object.getOwnPropertyDescriptor(candidate, 'test')?.value
|
|
691
|
-
if (typeof block !== 'object' || block === null) return false
|
|
692
|
-
const named: unknown = Object.getOwnPropertyDescriptor(block, 'name')?.value
|
|
693
|
-
if (typeof named !== 'object' || named === null) return false
|
|
694
|
-
return Object.getOwnPropertyDescriptor(named, 'label')?.value === requiredLabel
|
|
695
|
-
})
|
|
696
|
-
if (row === undefined) {
|
|
697
|
-
throw new Error(`${requiredLabel} has no project factory or configuration`)
|
|
698
|
-
}
|
|
699
|
-
const project: unknown = typeof row === 'function' ? Reflect.apply(row, undefined, []) : row
|
|
700
|
-
if (typeof project !== 'object' || project === null) {
|
|
701
|
-
throw new Error('A project factory returned no configuration')
|
|
702
|
-
}
|
|
703
|
-
const test: unknown = Object.getOwnPropertyDescriptor(project, 'test')?.value
|
|
704
|
-
if (typeof test !== 'object' || test === null) {
|
|
705
|
-
throw new Error('A project configuration carries no test block')
|
|
706
|
-
}
|
|
707
|
-
const name: unknown = Object.getOwnPropertyDescriptor(test, 'name')?.value
|
|
708
|
-
const include: unknown = Object.getOwnPropertyDescriptor(test, 'include')?.value
|
|
709
|
-
const exclude: unknown = Object.getOwnPropertyDescriptor(test, 'exclude')?.value
|
|
710
|
-
const setup: unknown = Object.getOwnPropertyDescriptor(test, 'setupFiles')?.value
|
|
711
|
-
const label =
|
|
712
|
-
typeof name === 'object' && name !== null
|
|
713
|
-
? Object.getOwnPropertyDescriptor(name, 'label')?.value
|
|
714
|
-
: undefined
|
|
715
|
-
if (
|
|
716
|
-
typeof label !== 'string' ||
|
|
717
|
-
!Array.isArray(include) ||
|
|
718
|
-
typeof include[0] !== 'string' ||
|
|
719
|
-
!Array.isArray(setup) ||
|
|
720
|
-
!setup.every((path) => typeof path === 'string')
|
|
721
|
-
) {
|
|
722
|
-
throw new Error('A project does not expose one include and its setup files')
|
|
723
|
-
}
|
|
724
|
-
const effective = include.filter(
|
|
725
|
-
(path) => typeof path === 'string' && (!Array.isArray(exclude) || !exclude.includes(path)),
|
|
726
|
-
)
|
|
727
|
-
if (
|
|
728
|
-
effective.length !== (label === 'setup:browser' ? 2 : 1) ||
|
|
729
|
-
typeof effective[0] !== 'string'
|
|
730
|
-
) {
|
|
731
|
-
throw new Error(`${label} does not resolve to its required includes`)
|
|
732
|
-
}
|
|
733
|
-
for (const required of label === 'setup' ? browserProofs : [])
|
|
734
|
-
expect(exclude).toContain(required)
|
|
735
|
-
if (label === 'probe') {
|
|
736
|
-
const benchmark: unknown = Object.getOwnPropertyDescriptor(test, 'benchmark')?.value
|
|
737
|
-
const parallel: unknown = Object.getOwnPropertyDescriptor(test, 'fileParallelism')?.value
|
|
738
|
-
const pool: unknown = Object.getOwnPropertyDescriptor(test, 'pool')?.value
|
|
739
|
-
if (typeof benchmark !== 'object' || benchmark === null) {
|
|
740
|
-
throw new Error('The probe project carries no benchmark block')
|
|
811
|
+
const configured = new Map(
|
|
812
|
+
[...expected].map(([requiredLabel]): [string, PolicyProject] => {
|
|
813
|
+
const factoryName = requiredLabel.replace(/:([a-z])/gu, (_match, letter: string) =>
|
|
814
|
+
letter.toUpperCase(),
|
|
815
|
+
)
|
|
816
|
+
// A row is either the factory named for the project or the configuration
|
|
817
|
+
// that project resolves to, and a required project is found as whichever
|
|
818
|
+
// it is. Only the required row is read, so an extra factory is still
|
|
819
|
+
// selected by name and never called.
|
|
820
|
+
const row = controlled.find((candidate) => {
|
|
821
|
+
if (typeof candidate === 'function') return candidate.name === factoryName
|
|
822
|
+
if (typeof candidate !== 'object' || candidate === null) return false
|
|
823
|
+
const block: unknown = Object.getOwnPropertyDescriptor(candidate, 'test')?.value
|
|
824
|
+
if (typeof block !== 'object' || block === null) return false
|
|
825
|
+
const named: unknown = Object.getOwnPropertyDescriptor(block, 'name')?.value
|
|
826
|
+
if (typeof named !== 'object' || named === null) return false
|
|
827
|
+
return Object.getOwnPropertyDescriptor(named, 'label')?.value === requiredLabel
|
|
828
|
+
})
|
|
829
|
+
if (row === undefined) {
|
|
830
|
+
throw new Error(`${requiredLabel} has no project factory or configuration`)
|
|
741
831
|
}
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
'
|
|
745
|
-
|
|
832
|
+
const project: unknown = typeof row === 'function' ? Reflect.apply(row, undefined, []) : row
|
|
833
|
+
if (typeof project !== 'object' || project === null) {
|
|
834
|
+
throw new Error('A project factory returned no configuration')
|
|
835
|
+
}
|
|
836
|
+
const test: unknown = Object.getOwnPropertyDescriptor(project, 'test')?.value
|
|
837
|
+
if (typeof test !== 'object' || test === null) {
|
|
838
|
+
throw new Error('A project configuration carries no test block')
|
|
839
|
+
}
|
|
840
|
+
const name: unknown = Object.getOwnPropertyDescriptor(test, 'name')?.value
|
|
841
|
+
const include: unknown = Object.getOwnPropertyDescriptor(test, 'include')?.value
|
|
842
|
+
const exclude: unknown = Object.getOwnPropertyDescriptor(test, 'exclude')?.value
|
|
843
|
+
const setup: unknown = Object.getOwnPropertyDescriptor(test, 'setupFiles')?.value
|
|
844
|
+
const label =
|
|
845
|
+
typeof name === 'object' && name !== null
|
|
846
|
+
? Object.getOwnPropertyDescriptor(name, 'label')?.value
|
|
847
|
+
: undefined
|
|
746
848
|
if (
|
|
747
|
-
|
|
748
|
-
!
|
|
749
|
-
typeof
|
|
750
|
-
|
|
849
|
+
typeof label !== 'string' ||
|
|
850
|
+
!Array.isArray(include) ||
|
|
851
|
+
typeof include[0] !== 'string' ||
|
|
852
|
+
!Array.isArray(setup) ||
|
|
853
|
+
!setup.every((path) => typeof path === 'string')
|
|
751
854
|
) {
|
|
752
|
-
throw new Error('
|
|
855
|
+
throw new Error('A project does not expose one include and its setup files')
|
|
753
856
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
setup:
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
857
|
+
const effective = include.filter(
|
|
858
|
+
(path) =>
|
|
859
|
+
typeof path === 'string' && (!Array.isArray(exclude) || !exclude.includes(path)),
|
|
860
|
+
)
|
|
861
|
+
if (
|
|
862
|
+
effective.length !== (label === 'setup:browser' ? 2 : 1) ||
|
|
863
|
+
typeof effective[0] !== 'string'
|
|
864
|
+
) {
|
|
865
|
+
throw new Error(`${label} does not resolve to its required includes`)
|
|
866
|
+
}
|
|
867
|
+
for (const required of label === 'setup' ? browserProofs : [])
|
|
868
|
+
expect(exclude).toContain(required)
|
|
869
|
+
if (label === 'probe') {
|
|
870
|
+
const benchmark: unknown = Object.getOwnPropertyDescriptor(test, 'benchmark')?.value
|
|
871
|
+
const parallel: unknown = Object.getOwnPropertyDescriptor(test, 'fileParallelism')?.value
|
|
872
|
+
const pool: unknown = Object.getOwnPropertyDescriptor(test, 'pool')?.value
|
|
873
|
+
if (typeof benchmark !== 'object' || benchmark === null) {
|
|
874
|
+
throw new Error('The probe project carries no benchmark block')
|
|
875
|
+
}
|
|
876
|
+
const benchmarkInclude: unknown = Object.getOwnPropertyDescriptor(
|
|
877
|
+
benchmark,
|
|
878
|
+
'include',
|
|
879
|
+
)?.value
|
|
880
|
+
if (
|
|
881
|
+
!Array.isArray(benchmarkInclude) ||
|
|
882
|
+
!benchmarkInclude.every((path) => typeof path === 'string') ||
|
|
883
|
+
typeof parallel !== 'boolean' ||
|
|
884
|
+
typeof pool !== 'string'
|
|
885
|
+
) {
|
|
886
|
+
throw new Error('The probe project carries an invalid benchmark configuration')
|
|
887
|
+
}
|
|
888
|
+
return [
|
|
889
|
+
label,
|
|
890
|
+
{
|
|
891
|
+
benchmark: benchmarkInclude,
|
|
892
|
+
include: effective[0],
|
|
893
|
+
parallel,
|
|
894
|
+
pool,
|
|
895
|
+
setup: [...new Set(setup)],
|
|
896
|
+
},
|
|
897
|
+
]
|
|
898
|
+
}
|
|
899
|
+
return [
|
|
900
|
+
label,
|
|
901
|
+
{
|
|
902
|
+
include: label === 'setup:browser' ? effective : effective[0],
|
|
903
|
+
setup: [...new Set(setup)],
|
|
904
|
+
},
|
|
905
|
+
]
|
|
906
|
+
}),
|
|
907
|
+
)
|
|
768
908
|
|
|
769
909
|
// Required projects come from present source and test paths. Each factory is selected by name
|
|
770
910
|
// before its result is read. Extra factories are ignored without validating their result shape.
|
|
771
|
-
expect(
|
|
911
|
+
expect(extraLoads).toStrictEqual([])
|
|
772
912
|
for (const [label, project] of expected) expect(configured.get(label)).toStrictEqual(project)
|
|
773
913
|
for (const label of ['setup', 'setup:browser']) {
|
|
774
914
|
const name = label === 'setup' ? 'setup' : 'setupBrowser'
|
|
@@ -824,16 +964,20 @@ describe('root configuration', () => {
|
|
|
824
964
|
value: 'spreading',
|
|
825
965
|
})
|
|
826
966
|
const wrappers = projects.filter((entry) => typeof entry === 'string')
|
|
827
|
-
const sheets = collectSheets(
|
|
828
|
-
const expectedWrappers =
|
|
829
|
-
|
|
830
|
-
|
|
967
|
+
const sheets = collectSheets(POLICY_ROOT)
|
|
968
|
+
const expectedWrappers = [
|
|
969
|
+
...sheets.map((face) => `./configs/src/vite.${face}.config.ts`),
|
|
970
|
+
...(!sheets.includes('styles') &&
|
|
971
|
+
existsSync(resolve(POLICY_ROOT, 'src/styles/themes/index.scss'))
|
|
972
|
+
? ['./configs/src/vite.themes.config.ts']
|
|
973
|
+
: []),
|
|
974
|
+
]
|
|
831
975
|
expect([...wrappers].sort()).toEqual(expectedWrappers.sort())
|
|
832
976
|
for (const wrapper of wrappers) {
|
|
833
977
|
const loaded = await loadConfigFromFile(
|
|
834
978
|
{ command: 'serve', mode: invocation.mode },
|
|
835
|
-
resolve(
|
|
836
|
-
|
|
979
|
+
resolve(POLICY_ROOT, wrapper),
|
|
980
|
+
POLICY_ROOT,
|
|
837
981
|
'silent',
|
|
838
982
|
)
|
|
839
983
|
if (loaded === null) throw new Error(`Unloaded file project ${wrapper}`)
|
|
@@ -898,28 +1042,32 @@ describe('root configuration', () => {
|
|
|
898
1042
|
})
|
|
899
1043
|
|
|
900
1044
|
it('requires and validates every selected target wrapper', async () => {
|
|
901
|
-
const required
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1045
|
+
const required = [
|
|
1046
|
+
...['src', 'app'].flatMap((axis) =>
|
|
1047
|
+
['core', 'browser', 'server'].flatMap((environment) =>
|
|
1048
|
+
existsSync(resolve(POLICY_ROOT, axis, environment))
|
|
1049
|
+
? [
|
|
1050
|
+
`configs/${axis}/tsconfig.${environment}.json`,
|
|
1051
|
+
...(axis === 'src' || environment !== 'core'
|
|
1052
|
+
? [`configs/${axis}/vite.${environment}.config.ts`]
|
|
1053
|
+
: []),
|
|
1054
|
+
]
|
|
1055
|
+
: [],
|
|
1056
|
+
),
|
|
1057
|
+
),
|
|
1058
|
+
...collectFrameworks(POLICY_ROOT).map(
|
|
1059
|
+
(face) => `configs/${face.replace('/', '/tsconfig.')}.json`,
|
|
1060
|
+
),
|
|
1061
|
+
...(existsSync(resolve(POLICY_ROOT, 'src/bin'))
|
|
1062
|
+
? ['configs/src/tsconfig.bin.json', 'configs/src/vite.bin.config.ts']
|
|
1063
|
+
: []),
|
|
1064
|
+
...(existsSync(resolve(POLICY_ROOT, 'configs/app/vite.showcase.config.ts'))
|
|
1065
|
+
? ['configs/app/vite.showcase.config.ts']
|
|
1066
|
+
: []),
|
|
1067
|
+
...(existsSync(resolve(POLICY_ROOT, 'configs/app/vite.journey.config.ts'))
|
|
1068
|
+
? ['configs/app/vite.journey.config.ts']
|
|
1069
|
+
: []),
|
|
1070
|
+
]
|
|
923
1071
|
if (required[0] === undefined) {
|
|
924
1072
|
throw new Error('The workspace selects no configuration target')
|
|
925
1073
|
}
|
|
@@ -930,10 +1078,9 @@ describe('root configuration', () => {
|
|
|
930
1078
|
'configs/src/tsconfig.*.json',
|
|
931
1079
|
'configs/app/tsconfig.*.json',
|
|
932
1080
|
],
|
|
933
|
-
{ cwd:
|
|
1081
|
+
{ cwd: POLICY_ROOT },
|
|
934
1082
|
).map((path) => path.replaceAll('\\', '/'))
|
|
935
1083
|
const extra = 'configs/app/vite.core.config.ts'
|
|
936
|
-
const controlled = found.concat(extra)
|
|
937
1084
|
const planted = createPolicyScratch({ prefix: 'config-journey-' })
|
|
938
1085
|
const journey = 'configs/app/vite.journey.config.ts'
|
|
939
1086
|
planted.write(
|
|
@@ -952,9 +1099,11 @@ describe('root configuration', () => {
|
|
|
952
1099
|
}\n`,
|
|
953
1100
|
)
|
|
954
1101
|
const journeys: object[][] = []
|
|
955
|
-
const targets =
|
|
956
|
-
|
|
957
|
-
|
|
1102
|
+
const targets = [
|
|
1103
|
+
...required.map((wrapper) => ({ wrapper, directory: POLICY_ROOT })),
|
|
1104
|
+
{ wrapper: journey, directory: planted.path },
|
|
1105
|
+
]
|
|
1106
|
+
const controlled = [...found, extra, journey]
|
|
958
1107
|
|
|
959
1108
|
// Required wrappers come from selected src/app targets. Only that set is loaded and validated.
|
|
960
1109
|
// Extra wrappers remain in the found population but are ignored before their content is read.
|
|
@@ -1009,7 +1158,7 @@ describe('root configuration', () => {
|
|
|
1009
1158
|
: environment === 'showcase'
|
|
1010
1159
|
? 'showcase'
|
|
1011
1160
|
: `dist/${axis}/${environment}`
|
|
1012
|
-
if (resolve(
|
|
1161
|
+
if (resolve(POLICY_ROOT, output) !== resolve(POLICY_ROOT, expected)) {
|
|
1013
1162
|
throw new Error(`${wrapper} resolves to the wrong output`)
|
|
1014
1163
|
}
|
|
1015
1164
|
continue
|
|
@@ -1024,7 +1173,7 @@ describe('root configuration', () => {
|
|
|
1024
1173
|
if (axis === undefined || environment === undefined) {
|
|
1025
1174
|
throw new Error(`${wrapper} carries no TypeScript scope`)
|
|
1026
1175
|
}
|
|
1027
|
-
const parsed: unknown = JSON.parse(readFileSync(resolve(
|
|
1176
|
+
const parsed: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8'))
|
|
1028
1177
|
if (typeof parsed !== 'object' || parsed === null) {
|
|
1029
1178
|
throw new Error(`${wrapper} is not a TypeScript configuration record`)
|
|
1030
1179
|
}
|
|
@@ -1058,8 +1207,7 @@ describe('root configuration', () => {
|
|
|
1058
1207
|
}
|
|
1059
1208
|
}
|
|
1060
1209
|
for (const tests of journeys) {
|
|
1061
|
-
const names
|
|
1062
|
-
for (const test of tests) {
|
|
1210
|
+
const names = tests.map((test) => {
|
|
1063
1211
|
expect(Object.getOwnPropertyDescriptor(test, 'include')?.value).toStrictEqual([
|
|
1064
1212
|
'tests/app/browser/integration.test.ts',
|
|
1065
1213
|
])
|
|
@@ -1097,9 +1245,9 @@ describe('root configuration', () => {
|
|
|
1097
1245
|
)
|
|
1098
1246
|
)
|
|
1099
1247
|
throw new Error('A journey variant is absent from its declared set')
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1248
|
+
return variant
|
|
1249
|
+
})
|
|
1250
|
+
expect(new Set(names).size).toBe(names.length)
|
|
1103
1251
|
}
|
|
1104
1252
|
} finally {
|
|
1105
1253
|
planted.destroy()
|
|
@@ -1113,13 +1261,13 @@ describe('root configuration', () => {
|
|
|
1113
1261
|
expect(() => {
|
|
1114
1262
|
for (const wrapper of controlRequired) expect(controlFound).toContain(wrapper)
|
|
1115
1263
|
}).toThrow(/vite\.browser/u)
|
|
1116
|
-
expect(() =>
|
|
1117
|
-
/
|
|
1118
|
-
)
|
|
1264
|
+
expect(() =>
|
|
1265
|
+
expect(resolve(POLICY_ROOT, 'dist/actual')).toBe(resolve(POLICY_ROOT, 'dist/control')),
|
|
1266
|
+
).toThrow(/expected/u)
|
|
1119
1267
|
})
|
|
1120
1268
|
|
|
1121
1269
|
it('registers proof scripts in the correct gate', () => {
|
|
1122
|
-
const manifest: unknown = JSON.parse(readFileSync(resolve(
|
|
1270
|
+
const manifest: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8'))
|
|
1123
1271
|
if (typeof manifest !== 'object' || manifest === null) {
|
|
1124
1272
|
throw new Error('The package manifest is not a record')
|
|
1125
1273
|
}
|
|
@@ -1134,34 +1282,34 @@ describe('root configuration', () => {
|
|
|
1134
1282
|
const integration = Object.getOwnPropertyDescriptor(scripts, 'test:integration')?.value
|
|
1135
1283
|
const conformance = Object.getOwnPropertyDescriptor(scripts, 'test:conformance')?.value
|
|
1136
1284
|
const service = Object.getOwnPropertyDescriptor(scripts, 'test:service')?.value
|
|
1285
|
+
const studies = Object.getOwnPropertyDescriptor(scripts, 'test:studies')?.value
|
|
1137
1286
|
const publish = Object.getOwnPropertyDescriptor(scripts, 'prepublishOnly')?.value
|
|
1138
|
-
const hasIntegration = existsSync(resolve(
|
|
1287
|
+
const hasIntegration = existsSync(resolve(POLICY_ROOT, 'tests/integration.test.ts'))
|
|
1139
1288
|
// The optional proofs are read off the registered project set rather than off
|
|
1140
1289
|
// their files, because the defect this measures is a registered project no
|
|
1141
1290
|
// gate runs. A project selected by a path that is not yet there is still
|
|
1142
1291
|
// registered, and it is exactly the one whose script goes missing.
|
|
1143
1292
|
const rows = configuration.test?.projects
|
|
1144
1293
|
if (!Array.isArray(rows)) throw new Error('The root configuration carries no projects')
|
|
1145
|
-
const registered = new Set
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
if (typeof label === 'string') registered.add(label)
|
|
1158
|
-
}
|
|
1294
|
+
const registered = new Set(
|
|
1295
|
+
rows.flatMap((row) => {
|
|
1296
|
+
if (typeof row === 'function') return [row.name]
|
|
1297
|
+
if (typeof row !== 'object' || row === null) return []
|
|
1298
|
+
const block: unknown = Object.getOwnPropertyDescriptor(row, 'test')?.value
|
|
1299
|
+
if (typeof block !== 'object' || block === null) return []
|
|
1300
|
+
const named: unknown = Object.getOwnPropertyDescriptor(block, 'name')?.value
|
|
1301
|
+
if (typeof named !== 'object' || named === null) return []
|
|
1302
|
+
const label: unknown = Object.getOwnPropertyDescriptor(named, 'label')?.value
|
|
1303
|
+
return typeof label === 'string' ? [label] : []
|
|
1304
|
+
}),
|
|
1305
|
+
)
|
|
1159
1306
|
// The population must be able to answer both ways before either answer counts.
|
|
1160
1307
|
expect(registered.has('config')).toBe(true)
|
|
1161
1308
|
expect(registered.has('control')).toBe(false)
|
|
1162
1309
|
const hasConformance = registered.has('conformance')
|
|
1163
1310
|
const hasDistribution = registered.has('distribution')
|
|
1164
1311
|
const hasService = registered.has('service')
|
|
1312
|
+
const hasStudies = registered.has('studies')
|
|
1165
1313
|
expect(config).toBe(
|
|
1166
1314
|
'vitest run --config vite.config.ts --no-cache --reporter=dot --project config',
|
|
1167
1315
|
)
|
|
@@ -1212,10 +1360,28 @@ describe('root configuration', () => {
|
|
|
1212
1360
|
expect(typeof publish === 'string' && publish.includes('npm run test:service')).toBe(
|
|
1213
1361
|
hasService && manifestPublishes,
|
|
1214
1362
|
)
|
|
1363
|
+
// The study proof spawns processes, so it follows the same split as the live-service proof:
|
|
1364
|
+
// the publish gate of a publishing workspace runs it after `npm test` and before the release
|
|
1365
|
+
// proof, and a private workspace reaches it from `test`.
|
|
1366
|
+
expect(studies).toBe(
|
|
1367
|
+
hasStudies
|
|
1368
|
+
? 'vitest run --config vite.config.ts --no-cache --reporter=dot --project studies'
|
|
1369
|
+
: undefined,
|
|
1370
|
+
)
|
|
1371
|
+
expect(typeof test === 'string' && test.includes('npm run test:studies')).toBe(
|
|
1372
|
+
hasStudies && !manifestPublishes,
|
|
1373
|
+
)
|
|
1374
|
+
expect(typeof publish === 'string' && publish.includes('npm run test:studies')).toBe(
|
|
1375
|
+
hasStudies && manifestPublishes,
|
|
1376
|
+
)
|
|
1377
|
+
const steps = ['npm test &&', 'npm run test:studies', 'npm run test:distribution']
|
|
1378
|
+
const positions = typeof publish === 'string' ? steps.map((step) => publish.indexOf(step)) : []
|
|
1379
|
+
const sequenced = positions.every((position, index) => position > (positions[index - 1] ?? -1))
|
|
1380
|
+
expect(sequenced || !(hasStudies && hasDistribution && manifestPublishes)).toBe(true)
|
|
1215
1381
|
})
|
|
1216
1382
|
|
|
1217
1383
|
it('rebuilds publishing workspaces before packing', () => {
|
|
1218
|
-
const manifest: unknown = JSON.parse(readFileSync(resolve(
|
|
1384
|
+
const manifest: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8'))
|
|
1219
1385
|
if (typeof manifest !== 'object' || manifest === null) {
|
|
1220
1386
|
throw new Error('The package manifest is not a record')
|
|
1221
1387
|
}
|
|
@@ -1237,7 +1403,9 @@ describe('root configuration', () => {
|
|
|
1237
1403
|
it('keeps the committed host inventory aligned with the vendored checkout bytes', async () => {
|
|
1238
1404
|
// Run this gate against a quiescent checkout. It compares two reads and cannot
|
|
1239
1405
|
// distinguish stale committed data from a source edit made while it runs.
|
|
1240
|
-
const packageValue: unknown = JSON.parse(
|
|
1406
|
+
const packageValue: unknown = JSON.parse(
|
|
1407
|
+
readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8'),
|
|
1408
|
+
)
|
|
1241
1409
|
if (typeof packageValue !== 'object' || packageValue === null) {
|
|
1242
1410
|
throw new Error('The package manifest is not a record')
|
|
1243
1411
|
}
|
|
@@ -1248,7 +1416,7 @@ describe('root configuration', () => {
|
|
|
1248
1416
|
const generator: unknown = Object.getOwnPropertyDescriptor(scripts, 'build:inventory')?.value
|
|
1249
1417
|
expect(generator === undefined || typeof generator === 'string').toBe(true)
|
|
1250
1418
|
if (generator === undefined) {
|
|
1251
|
-
if (existsSync(resolve(
|
|
1419
|
+
if (existsSync(resolve(POLICY_ROOT, 'host.json'))) {
|
|
1252
1420
|
throw new Error('A committed host inventory exists without a generator')
|
|
1253
1421
|
}
|
|
1254
1422
|
return
|
|
@@ -1256,17 +1424,17 @@ describe('root configuration', () => {
|
|
|
1256
1424
|
if (typeof generator !== 'string') {
|
|
1257
1425
|
throw new Error('The host inventory generator is not a script')
|
|
1258
1426
|
}
|
|
1259
|
-
const committed = resolve(
|
|
1427
|
+
const committed = resolve(POLICY_ROOT, 'host.json')
|
|
1260
1428
|
if (!existsSync(committed)) {
|
|
1261
1429
|
throw new Error('The committed host inventory is absent at host.json')
|
|
1262
1430
|
}
|
|
1263
|
-
const helper = resolve(
|
|
1431
|
+
const helper = resolve(POLICY_ROOT, 'src/server/helpers.ts')
|
|
1264
1432
|
if (!existsSync(helper)) {
|
|
1265
1433
|
throw new Error('The committed host inventory has no server stager')
|
|
1266
1434
|
}
|
|
1267
1435
|
const server = await createServer({
|
|
1268
1436
|
configFile: false,
|
|
1269
|
-
root,
|
|
1437
|
+
root: POLICY_ROOT,
|
|
1270
1438
|
...(configuration.resolve === undefined ? {} : { resolve: configuration.resolve }),
|
|
1271
1439
|
server: { middlewareMode: true },
|
|
1272
1440
|
})
|
|
@@ -1283,49 +1451,47 @@ describe('root configuration', () => {
|
|
|
1283
1451
|
const workspace = scratch.path
|
|
1284
1452
|
try {
|
|
1285
1453
|
const generated = join(workspace, 'host.json')
|
|
1286
|
-
Reflect.apply(stage, undefined, [
|
|
1454
|
+
Reflect.apply(stage, undefined, [POLICY_ROOT, generated])
|
|
1287
1455
|
const generatedText = readFileSync(generated, 'utf8')
|
|
1288
1456
|
const committedText = readFileSync(committed, 'utf8')
|
|
1289
1457
|
const values: readonly unknown[] = [JSON.parse(generatedText), JSON.parse(committedText)]
|
|
1290
|
-
const indexes
|
|
1291
|
-
for (const value of values) {
|
|
1458
|
+
const indexes = values.map((value) => {
|
|
1292
1459
|
if (typeof value !== 'object' || value === null) {
|
|
1293
1460
|
throw new Error('A host inventory is not a record')
|
|
1294
1461
|
}
|
|
1295
1462
|
const entries: unknown = Object.getOwnPropertyDescriptor(value, 'entries')?.value
|
|
1296
1463
|
if (!Array.isArray(entries)) throw new Error('A host inventory carries no entry list')
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
}
|
|
1464
|
+
return new Map(
|
|
1465
|
+
entries.map((entry: unknown): [string, string] => {
|
|
1466
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
1467
|
+
throw new Error('A host inventory carries a malformed entry')
|
|
1468
|
+
}
|
|
1469
|
+
const destination: unknown = Object.getOwnPropertyDescriptor(
|
|
1470
|
+
entry,
|
|
1471
|
+
'destination',
|
|
1472
|
+
)?.value
|
|
1473
|
+
const digest: unknown = Object.getOwnPropertyDescriptor(entry, 'digest')?.value
|
|
1474
|
+
if (typeof destination !== 'string' || typeof digest !== 'string') {
|
|
1475
|
+
throw new Error('A host inventory entry carries no destination digest')
|
|
1476
|
+
}
|
|
1477
|
+
return [destination, digest]
|
|
1478
|
+
}),
|
|
1479
|
+
)
|
|
1480
|
+
})
|
|
1314
1481
|
const generatedIndex = indexes[0]
|
|
1315
1482
|
const committedIndex = indexes[1]
|
|
1316
1483
|
if (generatedIndex === undefined || committedIndex === undefined) {
|
|
1317
1484
|
throw new Error('The host inventories were not indexed')
|
|
1318
1485
|
}
|
|
1319
1486
|
if (generatedText !== committedText) {
|
|
1320
|
-
const stale
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
}
|
|
1487
|
+
const stale = [
|
|
1488
|
+
...[...generatedIndex].flatMap(([destination, digest]) =>
|
|
1489
|
+
committedIndex.get(destination) === digest ? [] : [destination],
|
|
1490
|
+
),
|
|
1491
|
+
...[...committedIndex.keys()].filter((destination) => !generatedIndex.has(destination)),
|
|
1492
|
+
]
|
|
1327
1493
|
throw new Error(
|
|
1328
|
-
`The committed host inventory is stale at ${stale.length > 0 ? stale.
|
|
1494
|
+
`The committed host inventory is stale at ${stale.length > 0 ? stale.toSorted().join(', ') : 'host.json'}`,
|
|
1329
1495
|
)
|
|
1330
1496
|
}
|
|
1331
1497
|
console.info(`host-inventory: entries=${generatedIndex.size}`)
|
|
@@ -1338,7 +1504,7 @@ describe('root configuration', () => {
|
|
|
1338
1504
|
})
|
|
1339
1505
|
|
|
1340
1506
|
it('keeps policy rules active across every linted workspace path', () => {
|
|
1341
|
-
const parsed: unknown = JSON.parse(readFileSync(resolve(
|
|
1507
|
+
const parsed: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
|
|
1342
1508
|
expect(inspectPolicyConfiguration(parsed)).toEqual([])
|
|
1343
1509
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
1344
1510
|
throw new Error('The Oxlint configuration is not a record')
|
|
@@ -1361,7 +1527,7 @@ describe('root configuration', () => {
|
|
|
1361
1527
|
})
|
|
1362
1528
|
|
|
1363
1529
|
it('omits the audit-confirmed dead policy type exports', () => {
|
|
1364
|
-
const source = readFileSync(resolve(
|
|
1530
|
+
const source = readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8')
|
|
1365
1531
|
expect(source).not.toMatch(/\bPolicy(?:Call|ClassMember)\b/u)
|
|
1366
1532
|
})
|
|
1367
1533
|
})
|
|
@@ -2533,6 +2699,85 @@ createSomething({
|
|
|
2533
2699
|
expect(commentToPolicyParagraph({ type: 'Block', value: '* ', range: [0, 0] })).toBe('')
|
|
2534
2700
|
})
|
|
2535
2701
|
|
|
2702
|
+
describe('readers over a caller-supplied list with a hole', () => {
|
|
2703
|
+
// A `for…of` loop and a spread read a hole as `undefined`, where `map`, `filter`, `flatMap`, and
|
|
2704
|
+
// `findLast` skip it or stop before it, so each case puts the hole where the reader would not
|
|
2705
|
+
// reach it and pins the refusal the loop gave.
|
|
2706
|
+
const name: PolicyExpression = { type: 'Identifier', range: [0, 0], name: 'run' }
|
|
2707
|
+
const exported: PolicyExpression = {
|
|
2708
|
+
type: 'ExportNamedDeclaration',
|
|
2709
|
+
range: [47, 63],
|
|
2710
|
+
declaration: { type: 'FunctionDeclaration', range: [0, 0], id: name },
|
|
2711
|
+
}
|
|
2712
|
+
const source: PolicySourceCode = { text: '', getAllComments: () => [] }
|
|
2713
|
+
|
|
2714
|
+
it('refuses a hole in the declarators of a statement as the declarator it reads as', () => {
|
|
2715
|
+
const declarator: PolicyExpression = {
|
|
2716
|
+
type: 'VariableDeclarator',
|
|
2717
|
+
range: [0, 0],
|
|
2718
|
+
id: name,
|
|
2719
|
+
init: { type: 'ArrowFunctionExpression', range: [0, 0] },
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
expect(
|
|
2723
|
+
statementToPolicyBindings({
|
|
2724
|
+
type: 'VariableDeclaration',
|
|
2725
|
+
range: [0, 0],
|
|
2726
|
+
declarations: [declarator],
|
|
2727
|
+
}),
|
|
2728
|
+
).toHaveLength(1)
|
|
2729
|
+
expect(() =>
|
|
2730
|
+
statementToPolicyBindings({
|
|
2731
|
+
type: 'VariableDeclaration',
|
|
2732
|
+
range: [0, 0],
|
|
2733
|
+
declarations: createPolicySparse(2, new Map([[0, declarator]])),
|
|
2734
|
+
}),
|
|
2735
|
+
).toThrow(TypeError)
|
|
2736
|
+
})
|
|
2737
|
+
|
|
2738
|
+
it('refuses a hole in the statements of a program as the statement it reads as', () => {
|
|
2739
|
+
const program: PolicyExpression = {
|
|
2740
|
+
type: 'Program',
|
|
2741
|
+
range: [0, 0],
|
|
2742
|
+
body: createPolicySparse(2, new Map([[0, exported]])),
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
expect(() => programToPolicyDocs(program, source)).toThrow(TypeError)
|
|
2746
|
+
})
|
|
2747
|
+
|
|
2748
|
+
it('refuses a hole in the comments of a source as the comment it reads as', () => {
|
|
2749
|
+
const comment: PolicyComment = { type: 'Block', value: '* One.', range: [0, 11] }
|
|
2750
|
+
const comments = createPolicySparse(2, new Map([[1, comment]]))
|
|
2751
|
+
const program: PolicyExpression = { type: 'Program', range: [0, 0], body: [exported] }
|
|
2752
|
+
|
|
2753
|
+
expect(() =>
|
|
2754
|
+
programToPolicyDocs(program, { text: '/** One. */\n', getAllComments: () => comments }),
|
|
2755
|
+
).toThrow(TypeError)
|
|
2756
|
+
})
|
|
2757
|
+
|
|
2758
|
+
it('refuses a hole in the body of a function-domain module as the statement it reads as', () => {
|
|
2759
|
+
const reports: PolicyDiagnostic[] = []
|
|
2760
|
+
const context: PolicyContext = {
|
|
2761
|
+
filename: '/ws/src/server/execution/run.ts',
|
|
2762
|
+
cwd: '/ws',
|
|
2763
|
+
sourceCode: source,
|
|
2764
|
+
report: (diagnostic) => {
|
|
2765
|
+
reports.push(diagnostic)
|
|
2766
|
+
},
|
|
2767
|
+
}
|
|
2768
|
+
const dense: PolicyExpression = { type: 'Program', range: [0, 0], body: [exported] }
|
|
2769
|
+
const sparse: PolicyExpression = {
|
|
2770
|
+
type: 'Program',
|
|
2771
|
+
range: [0, 0],
|
|
2772
|
+
body: createPolicySparse(2, new Map([[0, exported]])),
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
reportDomain(context, dense)
|
|
2776
|
+
expect(reports).toStrictEqual([])
|
|
2777
|
+
expect(() => reportDomain(context, sparse)).toThrow(TypeError)
|
|
2778
|
+
})
|
|
2779
|
+
})
|
|
2780
|
+
|
|
2536
2781
|
it('reads the opening word of a paragraph as its letters alone', () => {
|
|
2537
2782
|
expect(paragraphToPolicyOpener('Creates a control.')).toBe('Creates')
|
|
2538
2783
|
expect(paragraphToPolicyOpener('"Creates" a control.')).toBe('Creates')
|
|
@@ -2579,26 +2824,28 @@ createSomething({
|
|
|
2579
2824
|
it('admits computed ESM imports only in artifact loaders while refusing computed require', () => {
|
|
2580
2825
|
const fixture = createPolicyScratch({ prefix: 'artifact-imports-' })
|
|
2581
2826
|
try {
|
|
2582
|
-
fixture.write(
|
|
2827
|
+
fixture.write(
|
|
2828
|
+
'configs/policy.ts',
|
|
2829
|
+
readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
|
|
2830
|
+
)
|
|
2583
2831
|
const configured = readConfigRecord(
|
|
2584
|
-
JSON.parse(readFileSync(resolve(
|
|
2832
|
+
JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')),
|
|
2585
2833
|
)
|
|
2586
2834
|
// Include campaign fixtures even when the workspace's default lint walk ignores them.
|
|
2587
2835
|
fixture.write('.oxlintrc.json', JSON.stringify({ ...configured, ignorePatterns: [] }))
|
|
2588
|
-
const
|
|
2589
|
-
const requires: string[] = []
|
|
2590
|
-
for (const [path, admitted] of ARTIFACT_IMPORT_CASES) {
|
|
2836
|
+
const written = ARTIFACT_IMPORT_CASES.map(([path, admitted]) => {
|
|
2591
2837
|
fixture.write(path, 'const selected = process.argv[2]\nvoid import(selected)\ndebugger\n')
|
|
2592
|
-
|
|
2593
|
-
if (
|
|
2594
|
-
const required = path.replace(/\.(\w+)$/u, '.require.$1')
|
|
2838
|
+
const required = admitted ? path.replace(/\.(\w+)$/u, '.require.$1') : undefined
|
|
2839
|
+
if (required !== undefined) {
|
|
2595
2840
|
fixture.write(
|
|
2596
2841
|
required,
|
|
2597
2842
|
'const selected = process.argv[2]\nvoid require(selected)\ndebugger\n',
|
|
2598
2843
|
)
|
|
2599
|
-
requires.push(required)
|
|
2600
2844
|
}
|
|
2601
|
-
|
|
2845
|
+
return { path, required }
|
|
2846
|
+
})
|
|
2847
|
+
const paths = written.map(({ path }) => path)
|
|
2848
|
+
const requires = written.flatMap(({ required }) => (required === undefined ? [] : [required]))
|
|
2602
2849
|
const codes = readImportDiagnostics(fixture.path, [...paths, ...requires])
|
|
2603
2850
|
for (const [path, admitted] of ARTIFACT_IMPORT_CASES) {
|
|
2604
2851
|
expect({ path, refused: codes.has('import(no-dynamic-require) ' + path) }).toEqual({
|
|
@@ -2653,22 +2900,20 @@ createSomething({
|
|
|
2653
2900
|
const scratch = fixture.path
|
|
2654
2901
|
try {
|
|
2655
2902
|
const configured = readConfigRecord(
|
|
2656
|
-
JSON.parse(readFileSync(resolve(
|
|
2903
|
+
JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')),
|
|
2657
2904
|
)
|
|
2658
2905
|
if (!Array.isArray(configured.overrides)) throw new Error('Missing overrides')
|
|
2659
|
-
const overrides:
|
|
2660
|
-
const expected = new Map<string, boolean>()
|
|
2661
|
-
for (const [blockIndex, value] of configured.overrides.entries()) {
|
|
2906
|
+
const built = configured.overrides.flatMap((value: unknown, blockIndex: number) => {
|
|
2662
2907
|
const block = readConfigRecord(value)
|
|
2663
2908
|
const rule = readConfigRecord(block.rules)['no-restricted-imports']
|
|
2664
|
-
if (!Array.isArray(rule))
|
|
2909
|
+
if (!Array.isArray(rule)) return []
|
|
2665
2910
|
const patterns = readConfigRecord(rule[1]).patterns
|
|
2666
2911
|
if (!Array.isArray(patterns)) throw new Error('Missing restriction patterns')
|
|
2667
|
-
|
|
2912
|
+
return patterns.map((patternValue: unknown, patternIndex: number) => {
|
|
2668
2913
|
const pattern = readConfigRecord(patternValue)
|
|
2669
2914
|
const message = pattern.message
|
|
2670
2915
|
if (typeof message !== 'string') throw new Error('Missing pattern message')
|
|
2671
|
-
|
|
2916
|
+
const listed: ReadonlyArray<readonly [string, boolean]> = message.includes('URL schemes')
|
|
2672
2917
|
? [
|
|
2673
2918
|
['https://host/x', true],
|
|
2674
2919
|
['data:text/plain,x', true],
|
|
@@ -2734,49 +2979,59 @@ createSomething({
|
|
|
2734
2979
|
true,
|
|
2735
2980
|
],
|
|
2736
2981
|
]
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
[
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2982
|
+
const face =
|
|
2983
|
+
message.includes('another styles face') && typeof pattern.regex === 'string'
|
|
2984
|
+
? /@src\/(bootstrap|tailwindcss|styles)\(/u.exec(pattern.regex)?.[1]
|
|
2985
|
+
: undefined
|
|
2986
|
+
if (message.includes('another styles face') && face === undefined)
|
|
2987
|
+
throw new Error('Missing styles face in restriction pattern')
|
|
2988
|
+
const sources: ReadonlyArray<readonly [string, boolean]> =
|
|
2989
|
+
face === undefined
|
|
2990
|
+
? listed
|
|
2991
|
+
: [
|
|
2992
|
+
[`@src/${face}`, true],
|
|
2993
|
+
[`@src/${face}?raw`, true],
|
|
2994
|
+
[`@orkestrel/x/${face}`, true],
|
|
2995
|
+
[`../${face}/index.js`, true],
|
|
2996
|
+
[`../src/${face}/index.js`, true],
|
|
2997
|
+
[`../../src/${face}/index.js`, true],
|
|
2998
|
+
['@src/browser', false],
|
|
2999
|
+
[`@src/${face}x`, false],
|
|
3000
|
+
[`../${face}.ext/index.js`, false],
|
|
3001
|
+
]
|
|
2755
3002
|
const owner =
|
|
2756
3003
|
Array.isArray(block.files) && typeof block.files[0] === 'string'
|
|
2757
3004
|
? block.files[0].split('/**')[0]
|
|
2758
3005
|
: undefined
|
|
2759
3006
|
if (owner === undefined) throw new Error('Missing pattern owner')
|
|
2760
|
-
const
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
3007
|
+
const cases = [...sources, ['./index.js', false] as const].map(
|
|
3008
|
+
([source, refusal], index) => {
|
|
3009
|
+
const path = `${owner}/pattern${blockIndex}_${patternIndex}_${index}.ts`
|
|
3010
|
+
mkdirSync(dirname(resolve(scratch, path)), { recursive: true })
|
|
3011
|
+
writeFileSync(
|
|
3012
|
+
resolve(scratch, path),
|
|
3013
|
+
'import * as boundary from ' +
|
|
3014
|
+
JSON.stringify(source) +
|
|
3015
|
+
'\nvoid boundary\ndebugger\n',
|
|
3016
|
+
)
|
|
3017
|
+
return { path, refusal }
|
|
3018
|
+
},
|
|
3019
|
+
)
|
|
3020
|
+
return {
|
|
3021
|
+
cases,
|
|
3022
|
+
override: {
|
|
3023
|
+
files: cases.map(({ path }) => path),
|
|
3024
|
+
rules: { 'no-restricted-imports': ['error', { patterns: [pattern] }] },
|
|
3025
|
+
},
|
|
2773
3026
|
}
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
}
|
|
2779
|
-
|
|
3027
|
+
})
|
|
3028
|
+
})
|
|
3029
|
+
const overrides = built.map(({ override }) => override)
|
|
3030
|
+
const expected = new Map(
|
|
3031
|
+
built.flatMap(({ cases }) =>
|
|
3032
|
+
cases.map(({ path, refusal }): [string, boolean] => [path, refusal]),
|
|
3033
|
+
),
|
|
3034
|
+
)
|
|
2780
3035
|
writeFileSync(
|
|
2781
3036
|
resolve(scratch, '.oxlintrc.json'),
|
|
2782
3037
|
JSON.stringify({ rules: { 'no-debugger': 'error' }, overrides }),
|
|
@@ -2812,132 +3067,154 @@ createSomething({
|
|
|
2812
3067
|
mkdirSync(resolve(scratch, 'configs'))
|
|
2813
3068
|
writeFileSync(
|
|
2814
3069
|
resolve(scratch, 'configs/policy.ts'),
|
|
2815
|
-
readFileSync(resolve(
|
|
3070
|
+
readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts')),
|
|
2816
3071
|
)
|
|
2817
|
-
const text = readFileSync(resolve(
|
|
3072
|
+
const text = readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')
|
|
2818
3073
|
writeFileSync(resolve(scratch, '.oxlintrc.json'), text)
|
|
2819
|
-
const
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
3074
|
+
const faces = ['bootstrap', 'tailwindcss', 'styles']
|
|
3075
|
+
const expected = new Map([
|
|
3076
|
+
...[
|
|
3077
|
+
'src/vue',
|
|
3078
|
+
'app/vue',
|
|
3079
|
+
'src/browser',
|
|
3080
|
+
'src/bootstrap',
|
|
3081
|
+
'src/tailwindcss',
|
|
3082
|
+
'src/styles',
|
|
3083
|
+
'app/browser',
|
|
3084
|
+
'src/core',
|
|
3085
|
+
'app/core',
|
|
3086
|
+
'src/server',
|
|
3087
|
+
'app/server',
|
|
3088
|
+
'src/bin',
|
|
3089
|
+
].flatMap((owner) => {
|
|
3090
|
+
const sheet = ['src/bootstrap', 'src/tailwindcss', 'src/styles'].includes(owner)
|
|
3091
|
+
const refused: ReadonlyArray<string | readonly [string, string]> = [
|
|
3092
|
+
...(owner.endsWith('/vue')
|
|
3093
|
+
? [
|
|
3094
|
+
'node:fs',
|
|
3095
|
+
'@src/server',
|
|
3096
|
+
'../server/index.js',
|
|
3097
|
+
'@src/print',
|
|
3098
|
+
'../print/index.js',
|
|
3099
|
+
...(owner.startsWith('src/')
|
|
3100
|
+
? ['@app/core', '../../app/core/index.js']
|
|
3101
|
+
: ['@app/server', '@app/print']),
|
|
3102
|
+
]
|
|
3103
|
+
: [
|
|
3104
|
+
'vue',
|
|
3105
|
+
'vue/runtime-dom',
|
|
3106
|
+
'@vue/runtime-core',
|
|
3107
|
+
'@vitejs/plugin-vue',
|
|
3108
|
+
'@src/vue',
|
|
3109
|
+
'@orkestrel/sample/vue',
|
|
3110
|
+
'@app/vue',
|
|
3111
|
+
'../vue/index.js',
|
|
3112
|
+
'../../src/vue/index.js',
|
|
3113
|
+
'../../app/vue/index.js',
|
|
3114
|
+
]),
|
|
3115
|
+
...(owner === 'src/core' ? ['@src/browser', './index.css'] : []),
|
|
3116
|
+
...(sheet
|
|
3117
|
+
? [
|
|
3118
|
+
'node:fs',
|
|
3119
|
+
'@src/server',
|
|
3120
|
+
'../server/index.js',
|
|
3121
|
+
'@app/core',
|
|
3122
|
+
...faces
|
|
3123
|
+
.filter((face) => owner !== `src/${face}`)
|
|
3124
|
+
.flatMap((face): ReadonlyArray<string | readonly [string, string]> => [
|
|
3125
|
+
`@src/${face}`,
|
|
3126
|
+
`@orkestrel/x/${face}`,
|
|
3127
|
+
`../${face}/index.js`,
|
|
3128
|
+
`../src/${face}/index.js`,
|
|
3129
|
+
`../../src/${face}/index.js`,
|
|
3130
|
+
['export * from', `@src/${face}`],
|
|
3131
|
+
['import type * as boundary from', `@src/${face}`],
|
|
3132
|
+
]),
|
|
3133
|
+
]
|
|
3134
|
+
: []),
|
|
3135
|
+
]
|
|
3136
|
+
const admitted = [
|
|
3137
|
+
...(owner.endsWith('/vue')
|
|
3138
|
+
? [
|
|
3139
|
+
'@src/core',
|
|
3140
|
+
'@src/browser',
|
|
3141
|
+
'../core/index.js',
|
|
3142
|
+
'../browser/index.js',
|
|
3143
|
+
'vue',
|
|
3144
|
+
'@vue/runtime-core',
|
|
3145
|
+
...(owner.startsWith('app/')
|
|
3146
|
+
? ['@app/core', '@app/browser', '@src/vue', '../../src/vue/index.js']
|
|
3147
|
+
: []),
|
|
3148
|
+
]
|
|
3149
|
+
: ['@src/core']),
|
|
3150
|
+
...(sheet
|
|
3151
|
+
? [
|
|
3152
|
+
...faces
|
|
3153
|
+
.filter((face) => owner !== `src/${face}`)
|
|
3154
|
+
.flatMap((face) => [`@src/${face}x`, `../${face}.ext/index.js`]),
|
|
3155
|
+
'@src/browser',
|
|
3156
|
+
'./sheet.js',
|
|
3157
|
+
'@orkestrel/contract',
|
|
3158
|
+
]
|
|
3159
|
+
: []),
|
|
3160
|
+
]
|
|
3161
|
+
return (
|
|
2890
3162
|
[
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
3163
|
+
[
|
|
3164
|
+
[
|
|
3165
|
+
...refused,
|
|
3166
|
+
'https://host/x',
|
|
3167
|
+
'data:text/plain,x',
|
|
3168
|
+
'../core/../server/index.js',
|
|
3169
|
+
'../core/./index.js',
|
|
3170
|
+
],
|
|
3171
|
+
true,
|
|
3172
|
+
],
|
|
3173
|
+
[
|
|
3174
|
+
[
|
|
3175
|
+
...admitted,
|
|
3176
|
+
'./index.js',
|
|
3177
|
+
'../core/index.js',
|
|
3178
|
+
...(owner.endsWith('/server') || owner === 'src/bin' ? ['node:fs'] : []),
|
|
3179
|
+
],
|
|
3180
|
+
false,
|
|
3181
|
+
],
|
|
3182
|
+
] as const
|
|
3183
|
+
).flatMap(([sources, refusal]) =>
|
|
3184
|
+
sources.map((source, index): [string, boolean] => {
|
|
3185
|
+
const file = owner + '/' + (refusal ? 'refused' : 'admitted') + index + '.ts'
|
|
3186
|
+
mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
|
|
3187
|
+
writeFileSync(
|
|
3188
|
+
resolve(scratch, file),
|
|
3189
|
+
typeof source === 'string'
|
|
3190
|
+
? 'import * as boundary from ' +
|
|
3191
|
+
JSON.stringify(source) +
|
|
3192
|
+
'\nvoid boundary\ndebugger\n'
|
|
3193
|
+
: source[0] + ' ' + JSON.stringify(source[1]) + '\ndebugger\n',
|
|
3194
|
+
)
|
|
3195
|
+
return [file, refusal]
|
|
3196
|
+
}),
|
|
3197
|
+
)
|
|
3198
|
+
}),
|
|
3199
|
+
...(
|
|
2899
3200
|
[
|
|
2900
|
-
[
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
],
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
'\nvoid boundary\ndebugger\n'
|
|
2918
|
-
: source[0] + ' ' + JSON.stringify(source[1]) + '\ndebugger\n',
|
|
2919
|
-
)
|
|
2920
|
-
expected.set(file, refusal)
|
|
2921
|
-
}
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
for (const [file, source, refusal] of [
|
|
2925
|
-
['probe.config.ts', 'typescript', true],
|
|
2926
|
-
['allowed.config.ts', 'node:fs', false],
|
|
2927
|
-
['src/core/compiler.ts', 'typescript', true],
|
|
2928
|
-
['tests/compiler.ts', 'typescript', true],
|
|
2929
|
-
['configs/compiler.ts', 'typescript', true],
|
|
2930
|
-
['scripts/compiler.ts', 'typescript', true],
|
|
2931
|
-
] as const) {
|
|
2932
|
-
mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
|
|
2933
|
-
writeFileSync(
|
|
2934
|
-
resolve(scratch, file),
|
|
2935
|
-
'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
|
|
2936
|
-
)
|
|
2937
|
-
expected.set(file, refusal)
|
|
2938
|
-
}
|
|
2939
|
-
const reports: Array<ReadonlySet<string>> = []
|
|
2940
|
-
for (const disabled of [false, true]) {
|
|
3201
|
+
['probe.config.ts', 'typescript', true],
|
|
3202
|
+
['allowed.config.ts', 'node:fs', false],
|
|
3203
|
+
['src/core/compiler.ts', 'typescript', true],
|
|
3204
|
+
['tests/compiler.ts', 'typescript', true],
|
|
3205
|
+
['configs/compiler.ts', 'typescript', true],
|
|
3206
|
+
['scripts/compiler.ts', 'typescript', true],
|
|
3207
|
+
] as const
|
|
3208
|
+
).map(([file, source, refusal]): [string, boolean] => {
|
|
3209
|
+
mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
|
|
3210
|
+
writeFileSync(
|
|
3211
|
+
resolve(scratch, file),
|
|
3212
|
+
'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
|
|
3213
|
+
)
|
|
3214
|
+
return [file, refusal]
|
|
3215
|
+
}),
|
|
3216
|
+
])
|
|
3217
|
+
const reports = [false, true].map((disabled) => {
|
|
2941
3218
|
if (disabled) {
|
|
2942
3219
|
const parsed: unknown = JSON.parse(text)
|
|
2943
3220
|
if (typeof parsed !== 'object' || parsed === null)
|
|
@@ -2960,9 +3237,8 @@ createSomething({
|
|
|
2960
3237
|
}
|
|
2961
3238
|
writeFileSync(resolve(scratch, '.oxlintrc.json'), JSON.stringify(parsed))
|
|
2962
3239
|
}
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
}
|
|
3240
|
+
return readImportDiagnostics(scratch, [...expected.keys()])
|
|
3241
|
+
})
|
|
2966
3242
|
const [enabled, disabled] = reports
|
|
2967
3243
|
if (enabled === undefined || disabled === undefined)
|
|
2968
3244
|
throw new Error('Missing boundary readings')
|
|
@@ -2988,7 +3264,7 @@ createSomething({
|
|
|
2988
3264
|
})
|
|
2989
3265
|
|
|
2990
3266
|
it('enables every plugin rule over the population its law names', () => {
|
|
2991
|
-
const parsed: unknown = JSON.parse(readFileSync(resolve(
|
|
3267
|
+
const parsed: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
|
|
2992
3268
|
const rules: unknown = Object.getOwnPropertyDescriptor(policyPlugin, 'rules')?.value
|
|
2993
3269
|
if (typeof rules !== 'object' || rules === null) {
|
|
2994
3270
|
throw new Error('The policy plugin declares no rules')
|
|
@@ -3010,8 +3286,11 @@ createSomething({
|
|
|
3010
3286
|
try {
|
|
3011
3287
|
// Compare diagnostics from the real directory while the child keeps the scratch cwd.
|
|
3012
3288
|
const comparisonRoot = realpathSync.native(scratch.path)
|
|
3013
|
-
scratch.write('.oxlintrc.json', readFileSync(resolve(
|
|
3014
|
-
scratch.write(
|
|
3289
|
+
scratch.write('.oxlintrc.json', readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
|
|
3290
|
+
scratch.write(
|
|
3291
|
+
'configs/policy.ts',
|
|
3292
|
+
readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
|
|
3293
|
+
)
|
|
3015
3294
|
scratch.write(
|
|
3016
3295
|
'src/violations/fixture.ts',
|
|
3017
3296
|
[
|
|
@@ -3075,7 +3354,9 @@ createSomething({
|
|
|
3075
3354
|
// extensionless form; spawning the `.cmd` would need `shell: true`, which breaks on paths
|
|
3076
3355
|
// containing spaces. Resolving through `createRequire` reads oxlint's own `bin` field, so
|
|
3077
3356
|
// the entry survives hoisting, a nested `node_modules` layout, and a future rename.
|
|
3078
|
-
const manifestPath = createRequire(join(
|
|
3357
|
+
const manifestPath = createRequire(join(POLICY_ROOT, 'package.json')).resolve(
|
|
3358
|
+
'oxlint/package.json',
|
|
3359
|
+
)
|
|
3079
3360
|
const manifest: unknown = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
3080
3361
|
if (typeof manifest !== 'object' || manifest === null) {
|
|
3081
3362
|
throw new Error('The oxlint package manifest is not an object')
|
|
@@ -3102,8 +3383,7 @@ createSomething({
|
|
|
3102
3383
|
[binary, '--config', config, '--format', 'json', 'src/clean'],
|
|
3103
3384
|
{ cwd: scratch.path, encoding: 'utf8', timeout: 15_000 },
|
|
3104
3385
|
)
|
|
3105
|
-
const reports: string[]
|
|
3106
|
-
for (const result of [violations, clean]) {
|
|
3386
|
+
const reports = [violations, clean].map((result): string[] => {
|
|
3107
3387
|
if (result.error !== undefined) throw result.error
|
|
3108
3388
|
const report: unknown = JSON.parse(result.stdout)
|
|
3109
3389
|
if (typeof report !== 'object' || report === null) {
|
|
@@ -3111,8 +3391,7 @@ createSomething({
|
|
|
3111
3391
|
}
|
|
3112
3392
|
const diagnostics: unknown = Object.getOwnPropertyDescriptor(report, 'diagnostics')?.value
|
|
3113
3393
|
if (!Array.isArray(diagnostics)) throw new Error('Oxlint returned no diagnostic list')
|
|
3114
|
-
|
|
3115
|
-
for (const diagnostic of diagnostics) {
|
|
3394
|
+
return diagnostics.map((diagnostic: unknown) => {
|
|
3116
3395
|
if (typeof diagnostic !== 'object' || diagnostic === null) {
|
|
3117
3396
|
throw new Error('Oxlint returned a malformed diagnostic')
|
|
3118
3397
|
}
|
|
@@ -3121,10 +3400,9 @@ createSomething({
|
|
|
3121
3400
|
if (typeof code !== 'string' || typeof filename !== 'string') {
|
|
3122
3401
|
throw new Error('Oxlint returned a diagnostic without a rule id and a file')
|
|
3123
3402
|
}
|
|
3124
|
-
|
|
3125
|
-
}
|
|
3126
|
-
|
|
3127
|
-
}
|
|
3403
|
+
return `${code} ${normalizePolicyFilename(comparisonRoot, filename)}`
|
|
3404
|
+
})
|
|
3405
|
+
})
|
|
3128
3406
|
|
|
3129
3407
|
const violationCodes = reports[0]
|
|
3130
3408
|
const cleanCodes = reports[1]
|
|
@@ -3303,9 +3581,11 @@ describe('configuration helpers', () => {
|
|
|
3303
3581
|
|
|
3304
3582
|
it('exposes every helper this proof requires', () => {
|
|
3305
3583
|
const required = [
|
|
3584
|
+
'BuildState',
|
|
3306
3585
|
'ENVIRONMENT_MODULE_BYTES',
|
|
3307
3586
|
'PACKAGE_MANIFEST_BYTES',
|
|
3308
3587
|
'WORKSPACE_ROOT',
|
|
3588
|
+
'attemptValue',
|
|
3309
3589
|
'buildExtractorOverride',
|
|
3310
3590
|
'containedPath',
|
|
3311
3591
|
'decodeAssetSource',
|
|
@@ -3317,6 +3597,7 @@ describe('configuration helpers', () => {
|
|
|
3317
3597
|
'environmentPathError',
|
|
3318
3598
|
'environmentSourceError',
|
|
3319
3599
|
'fileSystemPath',
|
|
3600
|
+
'findPackageDirectory',
|
|
3320
3601
|
'hasAsciiUrlControl',
|
|
3321
3602
|
'isBoundaryExemptModule',
|
|
3322
3603
|
'isExtractorModule',
|
|
@@ -3333,10 +3614,10 @@ describe('configuration helpers', () => {
|
|
|
3333
3614
|
'parseProjectScope',
|
|
3334
3615
|
'physicalPath',
|
|
3335
3616
|
'readBoundedFile',
|
|
3617
|
+
'readHandleBytes',
|
|
3336
3618
|
'readCompilerOutput',
|
|
3337
3619
|
'rewriteCoreSpecifier',
|
|
3338
3620
|
'sourceFallback',
|
|
3339
|
-
'trustedPackageRootFor',
|
|
3340
3621
|
'workspacePath',
|
|
3341
3622
|
]
|
|
3342
3623
|
const found = Object.keys(configHelpers)
|
|
@@ -3375,17 +3656,19 @@ describe('configuration helpers', () => {
|
|
|
3375
3656
|
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-outside-' })
|
|
3376
3657
|
try {
|
|
3377
3658
|
const outside = scratch.path
|
|
3378
|
-
const importer = resolve(
|
|
3379
|
-
expect(configHelpers.WORKSPACE_ROOT).toBe(realpathSync.native(
|
|
3380
|
-
expect(configHelpers.fileSystemPath(`/@fs/${
|
|
3381
|
-
expect(configHelpers.physicalPath(`${
|
|
3659
|
+
const importer = resolve(POLICY_ROOT, 'tests/config.test.ts')
|
|
3660
|
+
expect(configHelpers.WORKSPACE_ROOT).toBe(realpathSync.native(POLICY_ROOT))
|
|
3661
|
+
expect(configHelpers.fileSystemPath(`/@fs/${POLICY_ROOT}`)).toBe(POLICY_ROOT)
|
|
3662
|
+
expect(configHelpers.physicalPath(`${POLICY_ROOT}?control=true`)).toBe(
|
|
3663
|
+
realpathSync.native(POLICY_ROOT),
|
|
3664
|
+
)
|
|
3382
3665
|
expect(configHelpers.sourceFallback(importer, './setup.ts')).toBe(
|
|
3383
|
-
resolve(
|
|
3666
|
+
resolve(POLICY_ROOT, 'tests/setup.ts'),
|
|
3384
3667
|
)
|
|
3385
3668
|
expect(configHelpers.sourceFallback(importer, pathToFileURL(importer).href)).toBe(importer)
|
|
3386
3669
|
expect(configHelpers.workspacePath(importer)).toBe('tests/config.test.ts')
|
|
3387
|
-
expect(configHelpers.containedPath(
|
|
3388
|
-
expect(configHelpers.containedPath(
|
|
3670
|
+
expect(configHelpers.containedPath(POLICY_ROOT, importer)).toBe(true)
|
|
3671
|
+
expect(configHelpers.containedPath(POLICY_ROOT, outside)).toBe(false)
|
|
3389
3672
|
expect(configHelpers.workspacePath(outside)).toBeUndefined()
|
|
3390
3673
|
expect(configHelpers.isOutsideWorkspacePath(outside)).toBe(true)
|
|
3391
3674
|
expect(configHelpers.isOutsideWorkspacePath('src/core/index.ts')).toBe(false)
|
|
@@ -3424,14 +3707,175 @@ describe('configuration helpers', () => {
|
|
|
3424
3707
|
realpathSync.native(packageRoot),
|
|
3425
3708
|
)
|
|
3426
3709
|
expect(configHelpers.packageRootForResolved(source)).toBe(realpathSync.native(packageRoot))
|
|
3710
|
+
} finally {
|
|
3711
|
+
scratch.destroy()
|
|
3712
|
+
}
|
|
3713
|
+
})
|
|
3714
|
+
|
|
3715
|
+
it('returns a producer value, or undefined when the producer throws', () => {
|
|
3716
|
+
expect(configHelpers.attemptValue(() => decodeURI('%20'))).toBe(' ')
|
|
3717
|
+
expect(configHelpers.attemptValue(() => decodeURI('%E0%A4%A'))).toBeUndefined()
|
|
3718
|
+
expect(configHelpers.attemptValue(() => undefined)).toBeUndefined()
|
|
3719
|
+
expect(configHelpers.attemptValue(() => 0)).toBe(0)
|
|
3720
|
+
})
|
|
3721
|
+
|
|
3722
|
+
it('fills a buffer from an open handle until it ends, and refuses a file over the limit', () => {
|
|
3723
|
+
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-handle-' })
|
|
3724
|
+
try {
|
|
3725
|
+
// Each case is a file's text, the offset already read into the buffer, and the largest total allowed.
|
|
3726
|
+
const readings = [
|
|
3727
|
+
{ text: 'abc', offset: 0, limit: 4 },
|
|
3728
|
+
{ text: 'abcd', offset: 0, limit: 4 },
|
|
3729
|
+
{ text: 'abcde', offset: 0, limit: 4 },
|
|
3730
|
+
{ text: '', offset: 0, limit: 4 },
|
|
3731
|
+
{ text: 'abc', offset: 2, limit: 10 },
|
|
3732
|
+
{ text: 'abc', offset: 2, limit: 4 },
|
|
3733
|
+
].map(({ text, offset, limit }) => {
|
|
3734
|
+
scratch.write('subject.txt', text)
|
|
3735
|
+
const handle = openSync(resolve(scratch.path, 'subject.txt'), 'r')
|
|
3736
|
+
try {
|
|
3737
|
+
const bytes = Buffer.alloc(16)
|
|
3738
|
+
const count = configHelpers.readHandleBytes(handle, bytes, offset, limit)
|
|
3739
|
+
return { count, filled: bytes.toString('utf8', offset, count ?? offset) }
|
|
3740
|
+
} finally {
|
|
3741
|
+
closeSync(handle)
|
|
3742
|
+
}
|
|
3743
|
+
})
|
|
3744
|
+
// A file at or under the limit answers its length; the shifted read adds the offset to the
|
|
3745
|
+
// length and fills the buffer past it; a file over the limit answers nothing.
|
|
3746
|
+
expect(readings.map(({ count }) => count)).toStrictEqual([3, 4, undefined, 0, 5, undefined])
|
|
3747
|
+
expect(readings.map(({ filled }) => filled)).toStrictEqual(['abc', 'abcd', '', '', 'abc', ''])
|
|
3748
|
+
} finally {
|
|
3749
|
+
scratch.destroy()
|
|
3750
|
+
}
|
|
3751
|
+
})
|
|
3752
|
+
|
|
3753
|
+
it('finds the nearest package directory from a manifest, a package boundary, or nothing', () => {
|
|
3754
|
+
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-find-' })
|
|
3755
|
+
try {
|
|
3756
|
+
scratch.write('held/package.json', '{"name":"held"}')
|
|
3757
|
+
scratch.write('held/src/deep/file.txt', '')
|
|
3758
|
+
scratch.write('modules/node_modules/plain/lib/file.txt', '')
|
|
3759
|
+
scratch.write('modules/node_modules/@scope/named/lib/file.txt', '')
|
|
3760
|
+
scratch.write('modules/node_modules/@scope/file.txt', '')
|
|
3761
|
+
const held = resolve(scratch.path, 'held')
|
|
3762
|
+
expect(configHelpers.findPackageDirectory(held)).toBe(held)
|
|
3763
|
+
expect(configHelpers.findPackageDirectory(resolve(held, 'src/deep'))).toBe(held)
|
|
3764
|
+
const plain = resolve(scratch.path, 'modules/node_modules/plain')
|
|
3765
|
+
expect(configHelpers.findPackageDirectory(resolve(plain, 'lib'))).toBe(plain)
|
|
3766
|
+
const named = resolve(scratch.path, 'modules/node_modules/@scope/named')
|
|
3767
|
+
expect(configHelpers.findPackageDirectory(resolve(named, 'lib'))).toBe(named)
|
|
3768
|
+
expect(configHelpers.findPackageDirectory(named)).toBe(named)
|
|
3769
|
+
// A scope directory alone is not a package boundary, so the walk passes it by.
|
|
3427
3770
|
expect(
|
|
3428
|
-
configHelpers.
|
|
3429
|
-
).toBe(
|
|
3771
|
+
configHelpers.isPackageBoundary(resolve(scratch.path, 'modules/node_modules/@scope')),
|
|
3772
|
+
).toBe(false)
|
|
3773
|
+
// The walk ends at the filesystem root; the answer there is the root only if it holds a manifest.
|
|
3774
|
+
const root = parse(scratch.path).root
|
|
3775
|
+
expect(configHelpers.findPackageDirectory(root)).toBe(
|
|
3776
|
+
existsSync(resolve(root, 'package.json')) ? root : undefined,
|
|
3777
|
+
)
|
|
3430
3778
|
} finally {
|
|
3431
3779
|
scratch.destroy()
|
|
3432
3780
|
}
|
|
3433
3781
|
})
|
|
3434
3782
|
|
|
3783
|
+
it('holds the workspace defaults until a resolved build config replaces them', async () => {
|
|
3784
|
+
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-state-' })
|
|
3785
|
+
try {
|
|
3786
|
+
const fresh = new configHelpers.BuildState()
|
|
3787
|
+
expect(fresh.building).toBe(false)
|
|
3788
|
+
expect(fresh.output).toBe(configHelpers.WORKSPACE_ROOT)
|
|
3789
|
+
expect(fresh.root).toBe(configHelpers.WORKSPACE_ROOT)
|
|
3790
|
+
expect(fresh.source).toBe('')
|
|
3791
|
+
expect(new configHelpers.BuildState('C:/output').output).toBe('C:/output')
|
|
3792
|
+
|
|
3793
|
+
const entry = resolve(scratch.path, 'index.ts')
|
|
3794
|
+
const inline: InlineConfig = {
|
|
3795
|
+
root: scratch.path,
|
|
3796
|
+
configFile: false,
|
|
3797
|
+
logLevel: 'silent',
|
|
3798
|
+
build: { outDir: 'out', lib: { entry, formats: ['es'] } },
|
|
3799
|
+
}
|
|
3800
|
+
const building = new configHelpers.BuildState()
|
|
3801
|
+
building.resolve(await resolveConfig(inline, 'build'))
|
|
3802
|
+
expect(building.building).toBe(true)
|
|
3803
|
+
expect(building.output).toBe(resolve(scratch.path, 'out'))
|
|
3804
|
+
expect(building.root).toBe(realpathSync.native(scratch.path))
|
|
3805
|
+
expect(building.source).toBe(entry)
|
|
3806
|
+
|
|
3807
|
+
// The control: a serve command is not a build, and a config with no library entry has no source.
|
|
3808
|
+
const serving = new configHelpers.BuildState()
|
|
3809
|
+
serving.resolve(await resolveConfig({ ...inline, build: { outDir: 'out' } }, 'serve'))
|
|
3810
|
+
expect(serving.building).toBe(false)
|
|
3811
|
+
expect(serving.source).toBe('')
|
|
3812
|
+
} finally {
|
|
3813
|
+
scratch.destroy()
|
|
3814
|
+
}
|
|
3815
|
+
})
|
|
3816
|
+
|
|
3817
|
+
it('admits each physical package root once and locates the first admitted root containing a path', () => {
|
|
3818
|
+
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-locate-' })
|
|
3819
|
+
try {
|
|
3820
|
+
scratch.write('modules/outer/inner/index.ts', '')
|
|
3821
|
+
scratch.write('modules/other/index.ts', '')
|
|
3822
|
+
const outer = realpathSync.native(resolve(scratch.path, 'modules/outer'))
|
|
3823
|
+
const inner = realpathSync.native(resolve(scratch.path, 'modules/outer/inner'))
|
|
3824
|
+
const inside = resolve(scratch.path, 'modules/outer/inner/index.ts')
|
|
3825
|
+
const beside = resolve(scratch.path, 'modules/other/index.ts')
|
|
3826
|
+
|
|
3827
|
+
const state = new configHelpers.BuildState()
|
|
3828
|
+
// A fresh state admits nothing, so it locates nothing.
|
|
3829
|
+
expect(state.locate(inside)).toBeUndefined()
|
|
3830
|
+
state.trust(outer)
|
|
3831
|
+
state.trust(outer)
|
|
3832
|
+
state.trust(inner)
|
|
3833
|
+
expect(state.locate(inside)).toBe(outer)
|
|
3834
|
+
expect(state.locate(beside)).toBeUndefined()
|
|
3835
|
+
// Admission order decides which of two containing roots answers.
|
|
3836
|
+
const reversed = new configHelpers.BuildState()
|
|
3837
|
+
reversed.trust(inner)
|
|
3838
|
+
reversed.trust(outer)
|
|
3839
|
+
expect(reversed.locate(inside)).toBe(inner)
|
|
3840
|
+
expect(new configHelpers.BuildState().locate(inside)).toBeUndefined()
|
|
3841
|
+
// The admitted roots are never returned, so the class publishes no member that lists them.
|
|
3842
|
+
expect(Object.getOwnPropertyNames(configHelpers.BuildState.prototype).sort()).toEqual([
|
|
3843
|
+
'building',
|
|
3844
|
+
'constructor',
|
|
3845
|
+
'locate',
|
|
3846
|
+
'output',
|
|
3847
|
+
'resolve',
|
|
3848
|
+
'root',
|
|
3849
|
+
'source',
|
|
3850
|
+
'trust',
|
|
3851
|
+
])
|
|
3852
|
+
} finally {
|
|
3853
|
+
scratch.destroy()
|
|
3854
|
+
}
|
|
3855
|
+
})
|
|
3856
|
+
|
|
3857
|
+
it('climbs from a policy node through its enclosing parentheses alone', () => {
|
|
3858
|
+
const call = { type: 'CallExpression', range: [0, 9] } satisfies PolicyExpression
|
|
3859
|
+
const outer = {
|
|
3860
|
+
type: 'ParenthesizedExpression',
|
|
3861
|
+
range: [1, 8],
|
|
3862
|
+
parent: call,
|
|
3863
|
+
} satisfies PolicyExpression
|
|
3864
|
+
const inner = {
|
|
3865
|
+
type: 'ParenthesizedExpression',
|
|
3866
|
+
range: [2, 7],
|
|
3867
|
+
parent: outer,
|
|
3868
|
+
} satisfies PolicyExpression
|
|
3869
|
+
const leaf = { type: 'Identifier', range: [3, 4], parent: inner } satisfies PolicyExpression
|
|
3870
|
+
expect(nodeToPolicyOuter(leaf)).toBe(outer)
|
|
3871
|
+
expect(nodeToPolicyOuter(inner)).toBe(outer)
|
|
3872
|
+
expect(nodeToPolicyOuter(outer)).toBe(outer)
|
|
3873
|
+
// The control: a node under a non-parenthesized parent, or under none, stays where it is.
|
|
3874
|
+
const argument = { type: 'Identifier', range: [1, 2], parent: call } satisfies PolicyExpression
|
|
3875
|
+
expect(nodeToPolicyOuter(argument)).toBe(argument)
|
|
3876
|
+
expect(nodeToPolicyOuter(call)).toBe(call)
|
|
3877
|
+
})
|
|
3878
|
+
|
|
3435
3879
|
it('classifies module boundaries and extracts static asset sources', async () => {
|
|
3436
3880
|
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-assets-' })
|
|
3437
3881
|
try {
|
|
@@ -3444,8 +3888,12 @@ describe('configuration helpers', () => {
|
|
|
3444
3888
|
expect(configHelpers.hasAsciiUrlControl('clean')).toBe(false)
|
|
3445
3889
|
expect(configHelpers.hasAsciiUrlControl('line\u0000break')).toBe(true)
|
|
3446
3890
|
expect(configHelpers.isBoundaryExemptModule('virtual:control')).toBe(true)
|
|
3447
|
-
expect(configHelpers.isBoundaryExemptModule(resolve(
|
|
3448
|
-
|
|
3891
|
+
expect(configHelpers.isBoundaryExemptModule(resolve(POLICY_ROOT, 'src/core/index.ts'))).toBe(
|
|
3892
|
+
false,
|
|
3893
|
+
)
|
|
3894
|
+
expect(
|
|
3895
|
+
configHelpers.isWorkspaceBoundaryModule(resolve(POLICY_ROOT, 'src/core/index.ts')),
|
|
3896
|
+
).toBe(true)
|
|
3449
3897
|
expect(configHelpers.isWorkspaceBoundaryModule(source)).toBe(false)
|
|
3450
3898
|
expect(configHelpers.isStylesheetPath('src/styles/index.scss?direct')).toBe(true)
|
|
3451
3899
|
expect(configHelpers.isStylesheetPath('src/core/index.ts')).toBe(false)
|
|
@@ -3471,12 +3919,12 @@ describe('configuration helpers', () => {
|
|
|
3471
3919
|
'Browser modules cannot depend on Node or server-only modules',
|
|
3472
3920
|
)
|
|
3473
3921
|
expect(configHelpers.environmentSourceError('src/browser', '@src/core')).toBeUndefined()
|
|
3474
|
-
const expected = resolve(
|
|
3922
|
+
const expected = resolve(POLICY_ROOT, 'dist/config-control')
|
|
3475
3923
|
expect(() => configHelpers.enforceOutputPath(expected, expected)).not.toThrow()
|
|
3476
3924
|
expect(() =>
|
|
3477
|
-
configHelpers.enforceOutputPath(resolve(
|
|
3925
|
+
configHelpers.enforceOutputPath(resolve(POLICY_ROOT, 'dist/outside-control'), expected),
|
|
3478
3926
|
).toThrow('Build output must use its exact configured workspace directory')
|
|
3479
|
-
const outside = resolve(dirname(
|
|
3927
|
+
const outside = resolve(dirname(POLICY_ROOT), 'outside-config-control')
|
|
3480
3928
|
expect(() => configHelpers.enforceOutputPath(outside, outside)).toThrow(
|
|
3481
3929
|
'Build output must remain inside the workspace',
|
|
3482
3930
|
)
|
|
@@ -3485,25 +3933,31 @@ describe('configuration helpers', () => {
|
|
|
3485
3933
|
it('admits computed artifact imports through Vite while retaining literal boundary inspection', async () => {
|
|
3486
3934
|
for (const [path] of ARTIFACT_IMPORT_CASES.filter(([, admitted]) => admitted)) {
|
|
3487
3935
|
await expect(
|
|
3488
|
-
configHelpers.environmentAssetSources(
|
|
3936
|
+
configHelpers.environmentAssetSources(
|
|
3937
|
+
'void import(process.argv[2])',
|
|
3938
|
+
resolve(POLICY_ROOT, path),
|
|
3939
|
+
),
|
|
3489
3940
|
).resolves.toEqual([])
|
|
3490
3941
|
}
|
|
3491
3942
|
for (const [path] of ARTIFACT_IMPORT_CASES.filter(([, admitted]) => !admitted)) {
|
|
3492
3943
|
await expect(
|
|
3493
|
-
configHelpers.environmentAssetSources(
|
|
3944
|
+
configHelpers.environmentAssetSources(
|
|
3945
|
+
'void import(process.argv[2])',
|
|
3946
|
+
resolve(POLICY_ROOT, path),
|
|
3947
|
+
),
|
|
3494
3948
|
).rejects.toThrow('Dynamic imports must use static string values')
|
|
3495
3949
|
}
|
|
3496
3950
|
await expect(
|
|
3497
3951
|
configHelpers.environmentAssetSources(
|
|
3498
3952
|
"void import('@src/browser')",
|
|
3499
|
-
resolve(
|
|
3953
|
+
resolve(POLICY_ROOT, 'src/server/artifacts/Artifacts.ts'),
|
|
3500
3954
|
),
|
|
3501
3955
|
).resolves.toEqual(['@src/browser'])
|
|
3502
3956
|
expect(configHelpers.environmentSourceError('src/server', '@src/browser')).toBeDefined()
|
|
3503
3957
|
await expect(
|
|
3504
3958
|
configHelpers.environmentAssetSources(
|
|
3505
3959
|
'void import(process.argv[2])',
|
|
3506
|
-
resolve(
|
|
3960
|
+
resolve(POLICY_ROOT, '../outside/src/server/artifacts/Artifacts.ts'),
|
|
3507
3961
|
),
|
|
3508
3962
|
).rejects.toThrow('Dynamic imports must use static string values')
|
|
3509
3963
|
})
|
|
@@ -3512,16 +3966,19 @@ describe('configuration helpers', () => {
|
|
|
3512
3966
|
const environments: ReadonlyArray<
|
|
3513
3967
|
'src/core' | 'src/browser' | 'src/server' | 'app/core' | 'app/browser' | 'app/server'
|
|
3514
3968
|
> = ['src/core', 'src/browser', 'src/server', 'app/core', 'app/browser', 'app/server']
|
|
3515
|
-
const owner = environments.find((environment) => existsSync(resolve(
|
|
3969
|
+
const owner = environments.find((environment) => existsSync(resolve(POLICY_ROOT, environment)))
|
|
3516
3970
|
if (owner === undefined) throw new Error('The workspace carries no environment plugin target')
|
|
3517
|
-
const scratch = createPolicyScratch({
|
|
3971
|
+
const scratch = createPolicyScratch({
|
|
3972
|
+
parent: resolve(POLICY_ROOT, owner),
|
|
3973
|
+
prefix: 'config-build-',
|
|
3974
|
+
})
|
|
3518
3975
|
const workspace = scratch.path
|
|
3519
3976
|
try {
|
|
3520
3977
|
const source = resolve(workspace, 'index.ts')
|
|
3521
3978
|
writeFileSync(source, 'export const control = true\n', 'utf8')
|
|
3522
3979
|
await expect(
|
|
3523
3980
|
build({
|
|
3524
|
-
root,
|
|
3981
|
+
root: POLICY_ROOT,
|
|
3525
3982
|
configFile: false,
|
|
3526
3983
|
logLevel: 'silent',
|
|
3527
3984
|
publicDir: false,
|
|
@@ -3568,7 +4025,7 @@ describe('configuration helpers', () => {
|
|
|
3568
4025
|
// raise its passed count with nothing measured. Every generated workspace runs its projects
|
|
3569
4026
|
// under `--reporter=dot`, which prints a skip as an unnamed `-`, so the case name and the
|
|
3570
4027
|
// condition in it are read by re-running the project with a reporter that names skipped cases.
|
|
3571
|
-
it.skipIf(!
|
|
4028
|
+
it.skipIf(!POLICY_PUBLISHES)(
|
|
3572
4029
|
'reads the compiler scope a declaration roll-up requires [inapplicable where src holds no recognized environment directory]',
|
|
3573
4030
|
() => {
|
|
3574
4031
|
const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
|
|
@@ -3576,10 +4033,10 @@ describe('configuration helpers', () => {
|
|
|
3576
4033
|
// no core project, so this walks to the first face the workspace actually carries.
|
|
3577
4034
|
const faces = ['core', 'browser', 'server']
|
|
3578
4035
|
const face = faces.find((candidate) =>
|
|
3579
|
-
existsSync(resolve(
|
|
4036
|
+
existsSync(resolve(POLICY_ROOT, `configs/src/tsconfig.${candidate}.json`)),
|
|
3580
4037
|
)
|
|
3581
4038
|
if (face === undefined) throw new Error('The workspace declares no face project')
|
|
3582
|
-
const project = resolve(
|
|
4039
|
+
const project = resolve(POLICY_ROOT, `configs/src/tsconfig.${face}.json`)
|
|
3583
4040
|
const declared: unknown = JSON.parse(readFileSync(project, 'utf8'))
|
|
3584
4041
|
if (typeof declared !== 'object' || declared === null) {
|
|
3585
4042
|
throw new Error(`The ${face} project is not a TypeScript configuration record`)
|
|
@@ -3634,7 +4091,7 @@ describe('configuration helpers', () => {
|
|
|
3634
4091
|
const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
|
|
3635
4092
|
// Any project path resolves these, because each reading is refused by the text before the
|
|
3636
4093
|
// path is read. The root project is the one file every workspace carries.
|
|
3637
|
-
const project = resolve(
|
|
4094
|
+
const project = resolve(POLICY_ROOT, 'tsconfig.json')
|
|
3638
4095
|
expect(configHelpers.parseProjectScope('not a configuration', project)).toBeUndefined()
|
|
3639
4096
|
expect(
|
|
3640
4097
|
configHelpers.parseProjectScope('{"compilerOptions":{"lib":[],"types":[]}}', project),
|
|
@@ -3647,7 +4104,7 @@ describe('configuration helpers', () => {
|
|
|
3647
4104
|
configHelpers.readCompilerOutput(compiler, [
|
|
3648
4105
|
'--showConfig',
|
|
3649
4106
|
'-p',
|
|
3650
|
-
resolve(
|
|
4107
|
+
resolve(POLICY_ROOT, 'configs/tsconfig.absent.json'),
|
|
3651
4108
|
]),
|
|
3652
4109
|
).toThrow('The declaration compiler failed')
|
|
3653
4110
|
|
|
@@ -3720,12 +4177,12 @@ describe('configuration helpers', () => {
|
|
|
3720
4177
|
|
|
3721
4178
|
// A hoisted install can place the extractor anywhere `require.resolve` reaches, so the skip
|
|
3722
4179
|
// control compares against the path resolution actually returned rather than a fixed layout.
|
|
3723
|
-
it.skipIf(
|
|
3724
|
-
if (
|
|
3725
|
-
expect(existsSync(
|
|
4180
|
+
it.skipIf(POLICY_EXTRACTOR === undefined)('finds the resolved extractor on disk', () => {
|
|
4181
|
+
if (POLICY_EXTRACTOR === undefined) throw new Error('The extractor resolution left no path')
|
|
4182
|
+
expect(existsSync(POLICY_EXTRACTOR)).toBe(true)
|
|
3726
4183
|
})
|
|
3727
4184
|
|
|
3728
|
-
it.skipIf(
|
|
4185
|
+
it.skipIf(POLICY_EXTRACTOR !== undefined)('rejects resolving the unavailable extractor', () => {
|
|
3729
4186
|
expect(() => createRequire(import.meta.url).resolve('@microsoft/api-extractor')).toThrow(
|
|
3730
4187
|
'Cannot find module',
|
|
3731
4188
|
)
|
|
@@ -3733,7 +4190,7 @@ describe('configuration helpers', () => {
|
|
|
3733
4190
|
|
|
3734
4191
|
// The roll-up loads the declaration extractor, which only a workspace publishing source from
|
|
3735
4192
|
// `src` installs. Where `require.resolve` rejects that package, this proof does not apply.
|
|
3736
|
-
it.skipIf(
|
|
4193
|
+
it.skipIf(POLICY_EXTRACTOR === undefined)(
|
|
3737
4194
|
'rolls one face into a single declaration and rewrites its core and browser specifiers',
|
|
3738
4195
|
async () => {
|
|
3739
4196
|
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-rollup-' })
|
|
@@ -3878,3 +4335,133 @@ describe('configuration helpers', () => {
|
|
|
3878
4335
|
},
|
|
3879
4336
|
)
|
|
3880
4337
|
})
|
|
4338
|
+
|
|
4339
|
+
// A `for…of` loop reads one entry, works on it, and stops at the first failure, where a copy of the
|
|
4340
|
+
// list reads every entry first. Each case runs a policy reader over a recorded list and pins the
|
|
4341
|
+
// outcome and the entries it read, as the loop the reader replaced gave them.
|
|
4342
|
+
describe('policy readers consume a caller-supplied list one entry at a time', () => {
|
|
4343
|
+
const name: PolicyExpression = { type: 'Identifier', range: [0, 0], name: 'run' }
|
|
4344
|
+
const declarator = (): PolicyExpression => ({
|
|
4345
|
+
type: 'VariableDeclarator',
|
|
4346
|
+
range: [0, 0],
|
|
4347
|
+
id: name,
|
|
4348
|
+
init: { type: 'ArrowFunctionExpression', range: [0, 0] },
|
|
4349
|
+
})
|
|
4350
|
+
const exported = (): PolicyExpression => ({
|
|
4351
|
+
type: 'ExportNamedDeclaration',
|
|
4352
|
+
range: [47, 63],
|
|
4353
|
+
declaration: { type: 'FunctionDeclaration', range: [0, 0], id: name },
|
|
4354
|
+
})
|
|
4355
|
+
const comment = (): PolicyComment => ({ type: 'Block', value: '* One.', range: [0, 11] })
|
|
4356
|
+
// A revoked proxy refuses every read, which fails the reader once it holds the entry.
|
|
4357
|
+
const refused =
|
|
4358
|
+
<T extends object>(entry: () => T) =>
|
|
4359
|
+
(): T => {
|
|
4360
|
+
const { proxy, revoke } = Proxy.revocable(entry(), {})
|
|
4361
|
+
revoke()
|
|
4362
|
+
return proxy
|
|
4363
|
+
}
|
|
4364
|
+
const throwing = [
|
|
4365
|
+
[POLICY_LAZY_SHAPES.hole, 'throw TypeError', []],
|
|
4366
|
+
[POLICY_LAZY_SHAPES.holeFailure, 'throw TypeError', []],
|
|
4367
|
+
[POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
|
|
4368
|
+
[POLICY_LAZY_SHAPES.refusalFirst, 'throw TypeError', [0]],
|
|
4369
|
+
[POLICY_LAZY_SHAPES.holeBetween, 'throw TypeError', [0]],
|
|
4370
|
+
[POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
|
|
4371
|
+
] as const
|
|
4372
|
+
|
|
4373
|
+
it.each([
|
|
4374
|
+
...createPolicyLazyCases(
|
|
4375
|
+
'statementToPolicyBindings',
|
|
4376
|
+
(declarations: PolicyExpression[]) =>
|
|
4377
|
+
statementToPolicyBindings({ type: 'VariableDeclaration', range: [0, 0], declarations }).map(
|
|
4378
|
+
(binding) => binding.name,
|
|
4379
|
+
),
|
|
4380
|
+
{ valid: declarator, refusal: refused(declarator) },
|
|
4381
|
+
[
|
|
4382
|
+
[POLICY_LAZY_SHAPES.empty, 'return []', []],
|
|
4383
|
+
[POLICY_LAZY_SHAPES.dense, 'return ["run"]', [0]],
|
|
4384
|
+
...throwing,
|
|
4385
|
+
],
|
|
4386
|
+
),
|
|
4387
|
+
...createPolicyLazyCases(
|
|
4388
|
+
'reportDomain',
|
|
4389
|
+
(body: PolicyExpression[]) => {
|
|
4390
|
+
const reports: Array<string | undefined> = []
|
|
4391
|
+
reportDomain(
|
|
4392
|
+
{
|
|
4393
|
+
filename: '/ws/src/server/execution/run.ts',
|
|
4394
|
+
cwd: '/ws',
|
|
4395
|
+
sourceCode: { text: '', getAllComments: () => [] },
|
|
4396
|
+
report: (diagnostic) => {
|
|
4397
|
+
reports.push(diagnostic.messageId)
|
|
4398
|
+
},
|
|
4399
|
+
},
|
|
4400
|
+
{ type: 'Program', range: [0, 0], body },
|
|
4401
|
+
)
|
|
4402
|
+
return reports
|
|
4403
|
+
},
|
|
4404
|
+
{ valid: exported, refusal: refused(exported) },
|
|
4405
|
+
[
|
|
4406
|
+
[POLICY_LAZY_SHAPES.empty, 'return ["module"]', []],
|
|
4407
|
+
[POLICY_LAZY_SHAPES.dense, 'return []', [0]],
|
|
4408
|
+
...throwing,
|
|
4409
|
+
],
|
|
4410
|
+
),
|
|
4411
|
+
...createPolicyLazyCases(
|
|
4412
|
+
'programToPolicyDocs statements',
|
|
4413
|
+
(body: PolicyExpression[]) =>
|
|
4414
|
+
programToPolicyDocs(
|
|
4415
|
+
{ type: 'Program', range: [0, 0], body },
|
|
4416
|
+
{ text: '/** One. */\nexport function run() {}', getAllComments: () => [comment()] },
|
|
4417
|
+
).map((doc) => doc.name),
|
|
4418
|
+
{ valid: exported, refusal: refused(exported) },
|
|
4419
|
+
[
|
|
4420
|
+
[POLICY_LAZY_SHAPES.empty, 'return []', []],
|
|
4421
|
+
[POLICY_LAZY_SHAPES.dense, 'return []', [0]],
|
|
4422
|
+
...throwing,
|
|
4423
|
+
],
|
|
4424
|
+
),
|
|
4425
|
+
...createPolicyLazyCases(
|
|
4426
|
+
'programToPolicyDocs comments',
|
|
4427
|
+
(comments: PolicyComment[]) =>
|
|
4428
|
+
programToPolicyDocs(
|
|
4429
|
+
{ type: 'Program', range: [0, 0], body: [exported()] },
|
|
4430
|
+
{ text: '/** One. */\n', getAllComments: () => comments },
|
|
4431
|
+
).map((doc) => doc.name),
|
|
4432
|
+
{ valid: comment, refusal: refused(comment) },
|
|
4433
|
+
[
|
|
4434
|
+
[POLICY_LAZY_SHAPES.empty, 'return []', []],
|
|
4435
|
+
[POLICY_LAZY_SHAPES.dense, 'return ["run"]', [0]],
|
|
4436
|
+
...throwing,
|
|
4437
|
+
],
|
|
4438
|
+
),
|
|
4439
|
+
...createPolicyLazyCases(
|
|
4440
|
+
'programToPolicyDocs comments of a program with no export',
|
|
4441
|
+
(comments: PolicyComment[]) =>
|
|
4442
|
+
programToPolicyDocs(
|
|
4443
|
+
{ type: 'Program', range: [0, 0], body: [{ type: 'ImportDeclaration', range: [0, 0] }] },
|
|
4444
|
+
{ text: 'import x', getAllComments: () => comments },
|
|
4445
|
+
).map((doc) => doc.name),
|
|
4446
|
+
{ valid: comment, refusal: refused(comment) },
|
|
4447
|
+
[
|
|
4448
|
+
[POLICY_LAZY_SHAPES.empty, 'return []', []],
|
|
4449
|
+
[POLICY_LAZY_SHAPES.dense, 'return []', []],
|
|
4450
|
+
[POLICY_LAZY_SHAPES.hole, 'return []', []],
|
|
4451
|
+
[POLICY_LAZY_SHAPES.holeFailure, 'return []', []],
|
|
4452
|
+
[POLICY_LAZY_SHAPES.failureFirst, 'return []', []],
|
|
4453
|
+
[POLICY_LAZY_SHAPES.refusalFirst, 'return []', []],
|
|
4454
|
+
[POLICY_LAZY_SHAPES.holeBetween, 'return []', []],
|
|
4455
|
+
[POLICY_LAZY_SHAPES.unlisted, 'return []', []],
|
|
4456
|
+
],
|
|
4457
|
+
),
|
|
4458
|
+
])('$label', ({ observe, outcome, reads }) => {
|
|
4459
|
+
const read: number[] = []
|
|
4460
|
+
expect(
|
|
4461
|
+
observe((index) => {
|
|
4462
|
+
read.push(index)
|
|
4463
|
+
}),
|
|
4464
|
+
).toBe(outcome)
|
|
4465
|
+
expect(read).toStrictEqual(reads)
|
|
4466
|
+
})
|
|
4467
|
+
})
|