@theaiplatform/miniapp-sdk 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1293 @@
1
+ #!/usr/bin/env node
2
+ import * as __rspack_external_node_fs_promises_3b710708 from "node:fs/promises";
3
+ import * as __rspack_external_ajv_dist_2020_js_e85899db from "ajv/dist/2020.js";
4
+ import * as __rspack_external_node_crypto_2e7c4b46 from "node:crypto";
5
+ import * as __rspack_external_node_path_806ed179 from "node:path";
6
+ import * as __rspack_external_zod from "zod";
7
+ import { registerTapManifestAjvFormats } from "../internal/json-schema.js";
8
+ const SDK_PACKAGE = '@theaiplatform/miniapp-sdk';
9
+ const TEST_DESCRIPTOR_SCHEMA_ID = 'https://the-ai-platform.dev/schemas/tap.test.schema.json';
10
+ const TARGET_ORDER = [
11
+ 'desktop',
12
+ 'mobile',
13
+ 'quickjs',
14
+ 'worker',
15
+ 'node',
16
+ 'workflow-host'
17
+ ];
18
+ const MAX_JSON_BYTES = 8388608;
19
+ const MAX_SCANNED_TEST_FILES = 10000;
20
+ const V1_DENIED_ACTIONS = Object.freeze({
21
+ default: [],
22
+ 'all-denied': [
23
+ '*'
24
+ ],
25
+ 'read-only': [
26
+ 'do:*'
27
+ ],
28
+ 'http-denied': [
29
+ 'network.request'
30
+ ]
31
+ });
32
+ const jsonValueSchema = __rspack_external_zod.z.lazy(()=>__rspack_external_zod.z.union([
33
+ __rspack_external_zod.z["null"](),
34
+ __rspack_external_zod.z.boolean(),
35
+ __rspack_external_zod.z.number().finite(),
36
+ __rspack_external_zod.z.string(),
37
+ __rspack_external_zod.z.array(jsonValueSchema),
38
+ __rspack_external_zod.z.record(__rspack_external_zod.z.string(), jsonValueSchema)
39
+ ]));
40
+ const v1DescriptorSchema = __rspack_external_zod.z.object({
41
+ schemaVersion: __rspack_external_zod.z.literal(1),
42
+ packageId: __rspack_external_zod.z.string().trim().min(1).max(512),
43
+ runner: __rspack_external_zod.z.literal('rstest'),
44
+ configPath: __rspack_external_zod.z.string().trim().min(1).max(512),
45
+ testRoots: __rspack_external_zod.z.array(__rspack_external_zod.z.string().trim().min(1).max(512)).min(1).max(64),
46
+ defaultMode: __rspack_external_zod.z["enum"]([
47
+ 'surface',
48
+ 'live-tap'
49
+ ]),
50
+ surfaces: __rspack_external_zod.z.array(__rspack_external_zod.z.string().trim().min(1).max(256)).min(1).max(64),
51
+ permissionScenarios: __rspack_external_zod.z.array(__rspack_external_zod.z.string().trim().min(1).max(256)).min(1).max(64)
52
+ }).strict();
53
+ function compareCodePoints(left, right) {
54
+ return left < right ? -1 : left > right ? 1 : 0;
55
+ }
56
+ function isJsonObject(value) {
57
+ return null !== value && 'object' == typeof value && !Array.isArray(value);
58
+ }
59
+ function requireObject(value, label) {
60
+ if (void 0 === value || !isJsonObject(value)) throw new Error(`${label} must be an object.`);
61
+ return value;
62
+ }
63
+ function requireString(value, label) {
64
+ if ('string' != typeof value || 0 === value.length) throw new Error(`${label} must be a non-empty string.`);
65
+ return value;
66
+ }
67
+ function optionalStringArray(value, label) {
68
+ if (void 0 === value) return [];
69
+ if (!Array.isArray(value) || value.some((entry)=>'string' != typeof entry)) throw new Error(`${label} must be an array of strings.`);
70
+ return value.map((entry)=>requireString(entry, label));
71
+ }
72
+ function requireArray(value, label) {
73
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
74
+ return value;
75
+ }
76
+ function normalizeRelativePath(value, label) {
77
+ const normalized = value.trim();
78
+ if (0 === normalized.length || normalized.length > 512 || normalized.includes('\0') || normalized.includes('\\') || normalized.startsWith('/') || /^[a-zA-Z]:/u.test(normalized) || normalized.split('/').some((segment)=>'..' === segment)) throw new Error(`${label} must be a confined slash-relative path.`);
79
+ return normalized.replace(/^\.\//u, '');
80
+ }
81
+ function projectPath(root, relativePath) {
82
+ const relative = normalizeRelativePath(relativePath, 'Project path');
83
+ const absolute = __rspack_external_node_path_806ed179["default"].resolve(root, relative);
84
+ if (absolute !== root && !absolute.startsWith(`${__rspack_external_node_path_806ed179["default"].resolve(root)}${__rspack_external_node_path_806ed179["default"].sep}`)) throw new Error(`Project path escapes the source root: ${relative}`);
85
+ return absolute;
86
+ }
87
+ function parseJson(source, label) {
88
+ if (Buffer.byteLength(source, 'utf8') > MAX_JSON_BYTES) throw new Error(`${label} exceeds ${MAX_JSON_BYTES} bytes.`);
89
+ let parsed;
90
+ try {
91
+ parsed = JSON.parse(source);
92
+ } catch (error) {
93
+ throw new Error(`${label} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
94
+ }
95
+ const result = jsonValueSchema.safeParse(parsed);
96
+ if (!result.success) throw new Error(`${label} must contain strict finite JSON.`);
97
+ return result.data;
98
+ }
99
+ function schemaValidator(schema, label) {
100
+ if (!isJsonObject(schema) && 'boolean' != typeof schema) throw new Error(`${label} must be a JSON Schema object or boolean.`);
101
+ const ajv = registerTapManifestAjvFormats(new __rspack_external_ajv_dist_2020_js_e85899db["default"]({
102
+ allErrors: true,
103
+ strict: true
104
+ }));
105
+ return ajv.compile(schema);
106
+ }
107
+ function formatSchemaErrors(errors) {
108
+ return (errors ?? []).slice(0, 16).map((error)=>{
109
+ const location = error.instancePath || '/';
110
+ if ('additionalProperties' === error.keyword) return `${location} has unsupported property "${String(error.params.additionalProperty)}"`;
111
+ if ('required' === error.keyword) return `${location} is missing "${String(error.params.missingProperty)}"`;
112
+ return `${location} ${error.message ?? 'is invalid'}`;
113
+ }).join('; ');
114
+ }
115
+ function assertSchema(value, schema, label) {
116
+ const validate = schemaValidator(schema, `${label} schema`);
117
+ if (!validate(value)) throw new Error(`${label} is invalid: ${formatSchemaErrors(validate.errors)}`);
118
+ }
119
+ async function canonicalRoot(root) {
120
+ const resolved = await (0, __rspack_external_node_fs_promises_3b710708.realpath)(__rspack_external_node_path_806ed179["default"].resolve(root));
121
+ const stats = await (0, __rspack_external_node_fs_promises_3b710708.lstat)(resolved);
122
+ if (!stats.isDirectory()) throw new Error(`Project root is not a directory: ${root}`);
123
+ return resolved;
124
+ }
125
+ async function readProjectFile(root, relativePath) {
126
+ const normalized = normalizeRelativePath(relativePath, 'Project file');
127
+ const absolutePath = projectPath(root, normalized);
128
+ const resolved = await (0, __rspack_external_node_fs_promises_3b710708.realpath)(absolutePath).catch(()=>void 0);
129
+ if (!resolved) throw new Error(`Required project file is missing: ${normalized}`);
130
+ if (resolved !== root && !resolved.startsWith(`${root}${__rspack_external_node_path_806ed179["default"].sep}`)) throw new Error(`Project file resolves outside the source root: ${normalized}`);
131
+ const stats = await (0, __rspack_external_node_fs_promises_3b710708.lstat)(absolutePath);
132
+ if (!stats.isFile() || stats.isSymbolicLink()) throw new Error(`Project file must be a regular non-symlink: ${normalized}`);
133
+ return {
134
+ absolutePath,
135
+ source: await (0, __rspack_external_node_fs_promises_3b710708.readFile)(absolutePath, 'utf8')
136
+ };
137
+ }
138
+ async function readJsonProjectFile(root, relativePath) {
139
+ const file = await readProjectFile(root, relativePath);
140
+ return {
141
+ path: normalizeRelativePath(relativePath, 'Project JSON file'),
142
+ source: file.source,
143
+ value: parseJson(file.source, relativePath)
144
+ };
145
+ }
146
+ function manifestInventory(value) {
147
+ const manifest = requireObject(value, 'Manifest');
148
+ const packageRecord = requireObject(manifest.package, 'manifest.package');
149
+ const compatibility = requireObject(manifest.compatibility, 'manifest.compatibility');
150
+ const targetRecord = requireObject(manifest.targets, 'manifest.targets');
151
+ const targets = TARGET_ORDER.filter((target)=>Object.hasOwn(targetRecord, target));
152
+ const targetSet = new Set(targets);
153
+ const contributions = requireArray(manifest.contributions, 'manifest.contributions');
154
+ const permissionActions = new Map();
155
+ for (const entry of contributions){
156
+ const contribution = requireObject(entry, 'manifest contribution');
157
+ if ('permission.catalog' !== contribution.kind) continue;
158
+ const options = requireObject(contribution.options, 'permission.catalog options');
159
+ for (const actionValue of requireArray(options.actions, 'permission.catalog options.actions')){
160
+ const action = requireObject(actionValue, 'permission action');
161
+ const id = requireString(action.id, 'permission action id');
162
+ if (permissionActions.has(id)) throw new Error(`Manifest declares duplicate permission action ${id}.`);
163
+ permissionActions.set(id, {
164
+ autonomyCeiling: requireString(action.autonomyCeiling, `permission action ${id} autonomyCeiling`),
165
+ consent: requireString(action.consent, `permission action ${id} consent`),
166
+ risk: requireString(action.risk, `permission action ${id} risk`)
167
+ });
168
+ }
169
+ }
170
+ const surfaces = contributions.filter((entry)=>{
171
+ const contribution = requireObject(entry, 'manifest contribution');
172
+ return 'ui.surface' === contribution.kind;
173
+ }).map((entry)=>{
174
+ const contribution = requireObject(entry, 'UI surface contribution');
175
+ const id = requireString(contribution.id, 'UI surface id');
176
+ const contributionTargetRecord = requireObject(contribution.targets, `UI surface ${id} targets`);
177
+ const contributionTargets = TARGET_ORDER.filter((target)=>Object.hasOwn(contributionTargetRecord, target));
178
+ if (0 === contributionTargets.length) throw new Error(`UI surface ${id} has no target bindings.`);
179
+ for (const target of contributionTargets)if (!targetSet.has(target)) throw new Error(`UI surface ${id} references undeclared manifest target ${target}.`);
180
+ const authorization = void 0 === contribution.authorization ? {} : requireObject(contribution.authorization, `UI surface ${id} authorization`);
181
+ const requiredOrigins = new Set();
182
+ const effects = [
183
+ ...new Set(requireArray(authorization.effects ?? [], `UI surface ${id} authorization.effects`).flatMap((effectValue)=>{
184
+ const effect = requireObject(effectValue, `UI surface ${id} effect`);
185
+ const kind = requireString(effect.kind, `UI surface ${id} effect kind`);
186
+ const resources = [
187
+ ...new Set(optionalStringArray(effect.resources, `UI surface ${id} effect resources`))
188
+ ];
189
+ if ('external-network' === kind || 'host-http' === kind || 'network' === kind || 'embedded-frame' === kind) for (const resource of resources)try {
190
+ const url = new URL(resource);
191
+ if ('http:' === url.protocol || 'https:' === url.protocol) requiredOrigins.add(url.origin);
192
+ } catch {}
193
+ return 0 === resources.length ? [
194
+ `effect:${kind}:*`
195
+ ] : resources.map((resource)=>`effect:${kind}:${resource}`);
196
+ }))
197
+ ];
198
+ const authorizationActions = [
199
+ ...new Set(optionalStringArray(authorization.allOf, `UI surface ${id} authorization.allOf`))
200
+ ];
201
+ const highRiskActions = authorizationActions.filter((actionId)=>{
202
+ const action = permissionActions.get(actionId);
203
+ if (!action) throw new Error(`UI surface ${id} references undeclared permission action ${actionId}.`);
204
+ return 'write' === action.risk || 'consequential' === action.risk || 'do' === action.autonomyCeiling && 'none' !== action.consent;
205
+ });
206
+ return {
207
+ id,
208
+ targets: contributionTargets,
209
+ authorizationActions,
210
+ authorizationEffects: effects,
211
+ highRiskCapabilities: [
212
+ ...new Set([
213
+ ...highRiskActions.map((actionId)=>`action:${actionId}`),
214
+ ...effects
215
+ ])
216
+ ].sort(compareCodePoints),
217
+ requiredOrigins: [
218
+ ...requiredOrigins
219
+ ].sort(compareCodePoints)
220
+ };
221
+ }).sort((left, right)=>compareCodePoints(left.id, right.id));
222
+ if (0 === surfaces.length) throw new Error('Manifest declares no ui.surface contributions.');
223
+ const surfaceIds = new Set();
224
+ for (const surface of surfaces){
225
+ if (surfaceIds.has(surface.id)) throw new Error(`Manifest declares duplicate UI surface ${surface.id}.`);
226
+ surfaceIds.add(surface.id);
227
+ }
228
+ return {
229
+ packageId: requireString(packageRecord.packageId, 'manifest.package.packageId'),
230
+ sdkVersion: requireString(compatibility.tapSdk, 'manifest.compatibility.tapSdk'),
231
+ targets,
232
+ surfaces
233
+ };
234
+ }
235
+ async function loadTapToolingSchemas(manifestSchemaPath, testDescriptorSchemaPath) {
236
+ const [manifestSource, descriptorSource] = await Promise.all([
237
+ (0, __rspack_external_node_fs_promises_3b710708.readFile)(manifestSchemaPath, 'utf8'),
238
+ (0, __rspack_external_node_fs_promises_3b710708.readFile)(testDescriptorSchemaPath, 'utf8')
239
+ ]);
240
+ return Object.freeze({
241
+ manifest: parseJson(manifestSource, 'TAP package manifest schema'),
242
+ testDescriptor: parseJson(descriptorSource, "TAP test descriptor schema")
243
+ });
244
+ }
245
+ async function inspectTapManifest(options, schemas) {
246
+ const root = await canonicalRoot(options.root);
247
+ const document = await readJsonProjectFile(root, options.manifestPath ?? 'manifest.tap.json');
248
+ assertSchema(document.value, schemas.manifest, document.path);
249
+ return manifestInventory(document.value);
250
+ }
251
+ function parseTapSdkPackageVersion(source) {
252
+ const value = requireObject(parseJson(source, 'SDK package.json'), 'SDK package.json');
253
+ if (value.name !== SDK_PACKAGE) throw new Error(`Executing package must be ${SDK_PACKAGE}.`);
254
+ return requireString(value.version, 'SDK package.json version');
255
+ }
256
+ function defaultEnvironment() {
257
+ return {
258
+ viewport: {
259
+ width: 1280,
260
+ height: 720
261
+ },
262
+ locale: 'en-US',
263
+ timezone: 'UTC',
264
+ theme: 'light',
265
+ reducedMotion: true,
266
+ seed: 1,
267
+ fixedNow: '2026-01-01T00:00:00Z'
268
+ };
269
+ }
270
+ function defaultArtifacts() {
271
+ return {
272
+ trace: 'failure-only',
273
+ screenshots: 'failure-only'
274
+ };
275
+ }
276
+ function profileId(surface, target) {
277
+ const value = `${surface}-${target}`.toLowerCase().replace(/[^a-z0-9._-]+/gu, '-').replace(/^-+|-+$/gu, '');
278
+ const digest = __rspack_external_node_crypto_2e7c4b46["default"].createHash('sha256').update(`${surface}\0${target}`).digest('hex').slice(0, 24);
279
+ if (0 === value.length) return digest;
280
+ return `${value.slice(0, 103)}-${digest}`;
281
+ }
282
+ function scaffoldDescriptor(inventory) {
283
+ const profiles = {};
284
+ const matrix = [];
285
+ for (const surface of inventory.surfaces)for (const target of surface.targets){
286
+ const id = profileId(surface.id, target);
287
+ if (Object.hasOwn(profiles, id)) throw new Error(`Derived duplicate profile id ${id}.`);
288
+ profiles[id] = {
289
+ surface: surface.id,
290
+ target,
291
+ mode: 'surface',
292
+ permissionScenario: 'default',
293
+ deniedActions: [],
294
+ fixtureFiles: [
295
+ 'tests/fixtures/default.seed.json'
296
+ ],
297
+ httpRouteFiles: [
298
+ 'tests/fixtures/default.http-routes.json'
299
+ ],
300
+ requiredOrigins: [
301
+ ...surface.requiredOrigins
302
+ ],
303
+ requiredEffects: [
304
+ ...surface.authorizationEffects
305
+ ],
306
+ credentialSlots: [],
307
+ environment: defaultEnvironment(),
308
+ artifacts: defaultArtifacts()
309
+ };
310
+ matrix.push({
311
+ id,
312
+ profile: id,
313
+ capabilities: [
314
+ ...surface.authorizationActions.map((actionId)=>`action:${actionId}`),
315
+ ...surface.authorizationEffects
316
+ ].sort(compareCodePoints)
317
+ });
318
+ }
319
+ return {
320
+ $schema: TEST_DESCRIPTOR_SCHEMA_ID,
321
+ schemaVersion: 2,
322
+ packageId: inventory.packageId,
323
+ configPath: 'rstest.tap.config.ts',
324
+ testMatch: [
325
+ 'tests/e2e/**'
326
+ ],
327
+ profiles,
328
+ matrix,
329
+ waivers: []
330
+ };
331
+ }
332
+ function deniedActionsForV1Scenario(scenario) {
333
+ if ('default' === scenario) return [
334
+ ...V1_DENIED_ACTIONS.default
335
+ ];
336
+ if ('all-denied' === scenario) return [
337
+ ...V1_DENIED_ACTIONS['all-denied']
338
+ ];
339
+ if ('read-only' === scenario) return [
340
+ ...V1_DENIED_ACTIONS['read-only']
341
+ ];
342
+ if ('http-denied' === scenario) return [
343
+ ...V1_DENIED_ACTIONS['http-denied']
344
+ ];
345
+ if (scenario.startsWith('deny:') && scenario.length > 5) return [
346
+ scenario.slice(5)
347
+ ];
348
+ return [];
349
+ }
350
+ function expandMigratedDescriptor(v1Value, inventory) {
351
+ const parsed = v1DescriptorSchema.parse(v1Value);
352
+ if (parsed.packageId !== inventory.packageId) throw new Error('tap.test.json packageId does not match manifest.package.packageId.');
353
+ const surfaces = new Map(inventory.surfaces.map((surface)=>[
354
+ surface.id,
355
+ surface
356
+ ]));
357
+ const profiles = {};
358
+ const matrix = [];
359
+ let sequence = 1;
360
+ for (const surfaceId of parsed.surfaces){
361
+ const surface = surfaces.get(surfaceId);
362
+ if (!surface) throw new Error(`v1 tap.test.json surface ${surfaceId} is not declared by the manifest.`);
363
+ for (const target of surface.targets)for (const permissionScenario of parsed.permissionScenarios){
364
+ const suffix = String(sequence).padStart(3, '0');
365
+ const profile = `v1-profile-${suffix}`;
366
+ profiles[profile] = {
367
+ surface: surfaceId,
368
+ target,
369
+ mode: 'surface' === parsed.defaultMode ? 'surface' : 'live',
370
+ permissionScenario,
371
+ deniedActions: deniedActionsForV1Scenario(permissionScenario),
372
+ fixtureFiles: [],
373
+ httpRouteFiles: [],
374
+ requiredOrigins: [
375
+ ...surface.requiredOrigins
376
+ ],
377
+ requiredEffects: [
378
+ ...surface.authorizationEffects
379
+ ],
380
+ credentialSlots: [],
381
+ environment: defaultEnvironment(),
382
+ artifacts: defaultArtifacts()
383
+ };
384
+ matrix.push({
385
+ id: `v1-matrix-${suffix}`,
386
+ profile,
387
+ capabilities: [
388
+ ...surface.authorizationActions.map((actionId)=>`action:${actionId}`),
389
+ ...surface.authorizationEffects
390
+ ].sort(compareCodePoints)
391
+ });
392
+ sequence += 1;
393
+ }
394
+ }
395
+ return {
396
+ $schema: TEST_DESCRIPTOR_SCHEMA_ID,
397
+ schemaVersion: 2,
398
+ packageId: parsed.packageId,
399
+ configPath: parsed.configPath,
400
+ testMatch: parsed.testRoots.map((root)=>{
401
+ const confined = normalizeRelativePath(root, 'v1 testRoot');
402
+ return confined.endsWith('/**') ? confined : `${confined}/**`;
403
+ }),
404
+ profiles,
405
+ matrix,
406
+ waivers: []
407
+ };
408
+ }
409
+ function descriptorProfiles(descriptor) {
410
+ return requireObject(descriptor.profiles, 'tap.test.json profiles');
411
+ }
412
+ function descriptorTestMatch(descriptor, label = 'tap.test.json testMatch') {
413
+ return optionalStringArray(descriptor.testMatch, label);
414
+ }
415
+ function matrixRows(value) {
416
+ const descriptor = requireObject(value, 'tap.test.json');
417
+ const profiles = descriptorProfiles(descriptor);
418
+ const rootTestMatch = descriptorTestMatch(descriptor);
419
+ return requireArray(descriptor.matrix, 'tap.test.json matrix').map((entryValue)=>{
420
+ const entry = requireObject(entryValue, 'tap.test.json matrix entry');
421
+ const id = requireString(entry.id, 'matrix id');
422
+ const profileName = requireString(entry.profile, `matrix ${id} profile`);
423
+ const profile = requireObject(profiles[profileName], `matrix ${id} profile ${profileName}`);
424
+ const testMatch = optionalStringArray(entry.testMatch, `matrix ${id} testMatch`);
425
+ return {
426
+ id,
427
+ profile: profileName,
428
+ surface: requireString(profile.surface, `profile ${profileName} surface`),
429
+ target: requireString(profile.target, `profile ${profileName} target`),
430
+ mode: requireString(profile.mode, `profile ${profileName} mode`),
431
+ permissionScenario: requireString(profile.permissionScenario, `profile ${profileName} permissionScenario`),
432
+ deniedActions: optionalStringArray(profile.deniedActions, `profile ${profileName} deniedActions`),
433
+ capabilities: optionalStringArray(entry.capabilities, `matrix ${id} capabilities`),
434
+ testMatch: testMatch.length > 0 ? testMatch : rootTestMatch
435
+ };
436
+ });
437
+ }
438
+ async function inspectTapMatrix(options, schemas) {
439
+ const root = await canonicalRoot(options.root);
440
+ const descriptorPath = options.descriptorPath ?? 'tap.test.json';
441
+ const document = await readJsonProjectFile(root, descriptorPath);
442
+ const object = requireObject(document.value, descriptorPath);
443
+ const descriptor = 1 === object.schemaVersion ? expandMigratedDescriptor(document.value, await inspectTapManifest({
444
+ ...options,
445
+ root
446
+ }, schemas)) : object;
447
+ assertSchema(descriptor, schemas.testDescriptor, descriptorPath);
448
+ return Object.freeze({
449
+ descriptor,
450
+ rows: matrixRows(descriptor)
451
+ });
452
+ }
453
+ function jsonFile(value) {
454
+ return `${JSON.stringify(value, null, 2)}\n`;
455
+ }
456
+ function configFile() {
457
+ return `import { defineTapRstestConfig } from '${SDK_PACKAGE}/testing/rstest-config';\n\nexport default defineTapRstestConfig();\n`;
458
+ }
459
+ function testRootFromPattern(pattern) {
460
+ const confined = normalizeRelativePath(pattern, 'testMatch');
461
+ const globIndex = confined.search(/[*?[{]/u);
462
+ const prefix = (-1 === globIndex ? confined : confined.slice(0, globIndex)).replace(/\/+$/u, '');
463
+ if (0 === prefix.length) throw new Error(`testMatch must have a confined directory prefix: ${pattern}`);
464
+ const extension = __rspack_external_node_path_806ed179["default"].posix.extname(prefix);
465
+ return extension.length > 0 ? __rspack_external_node_path_806ed179["default"].posix.dirname(prefix) : prefix;
466
+ }
467
+ function tapTsconfig(configPath, testRoots) {
468
+ return jsonFile({
469
+ extends: './tsconfig.json',
470
+ compilerOptions: {
471
+ incremental: true,
472
+ noEmit: true,
473
+ tsBuildInfoFile: '.turbo/typecheck.tap.tsbuildinfo'
474
+ },
475
+ include: [
476
+ ...testRoots.flatMap((root)=>[
477
+ `${root}/**/*.ts`,
478
+ `${root}/**/*.tsx`
479
+ ]),
480
+ configPath
481
+ ],
482
+ exclude: [
483
+ 'node_modules',
484
+ 'dist',
485
+ 'build',
486
+ '.tap-build',
487
+ '.tap-legacy'
488
+ ]
489
+ });
490
+ }
491
+ function smokeTest(packageId, sdkVersion, rows) {
492
+ const cells = [
493
+ ...new Set(rows.map((row)=>`${row.surface}\0${row.target}`))
494
+ ].sort(compareCodePoints);
495
+ const profileIds = [
496
+ ...new Set(rows.map((row)=>row.profile))
497
+ ].sort(compareCodePoints);
498
+ const matrixEntryIds = [
499
+ ...new Set(rows.map((row)=>row.id))
500
+ ].sort(compareCodePoints);
501
+ return `import { expect, test } from '${SDK_PACKAGE}/testing/rstest';\n\nconst PACKAGE_ID = ${JSON.stringify(packageId)};\nconst SDK_VERSION = ${JSON.stringify(sdkVersion)};\nconst SURFACE_TARGET_CELLS = new Set(${JSON.stringify(cells)});\nconst PROFILE_IDS = new Set(${JSON.stringify(profileIds)});\nconst MATRIX_ENTRY_IDS = new Set(${JSON.stringify(matrixEntryIds)});\nconst SHA256 = /^[a-f0-9]{64}$/u;\nconst SEMVER = /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/u;\n\ntest('mounts the exact declared TAP cell with reproducible provenance', async ({ surface, tap }) => {\n expect(tap.packageId).toBe(PACKAGE_ID);\n expect(SURFACE_TARGET_CELLS.has(\`\${tap.surfaceId}\\0\${tap.target}\`)).toBe(true);\n expect(PROFILE_IDS.has(tap.profileId)).toBe(true);\n expect(MATRIX_ENTRY_IDS.has(tap.matrixEntryId)).toBe(true);\n expect(tap.seed).toBe(tap.environment.seed);\n expect(tap.adapterVersion).toBe(SDK_VERSION);\n expect(tap.hostVersion).toMatch(SEMVER);\n expect(tap.hostContractVersion).toMatch(/^\\d+$/u);\n expect(tap.runnerName).toBe('rstest');\n expect(tap.runnerVersion).toMatch(SEMVER);\n expect(tap.sourceDigest).toMatch(SHA256);\n expect(tap.testBundleDigest).toMatch(SHA256);\n expect(tap.descriptorDigest).toMatch(SHA256);\n expect(tap.policyDigest).toMatch(SHA256);\n if (tap.mode === 'surface') expect(tap.fixtureDigest).toMatch(SHA256);\n else expect(tap.fixtureDigest).toBeUndefined();\n\n await tap.control.remountSurface();\n await expect(surface.locator('body')).toBeVisible();\n});\n`;
502
+ }
503
+ function collectProfileFiles(descriptor, key) {
504
+ const values = new Set();
505
+ for (const [profileName, profileValue] of Object.entries(descriptorProfiles(descriptor))){
506
+ const profile = requireObject(profileValue, `profile ${profileName}`);
507
+ for (const file of optionalStringArray(profile[key], `profile ${profileName} ${key}`))values.add(normalizeRelativePath(file, `profile ${profileName} ${key}`));
508
+ }
509
+ return [
510
+ ...values
511
+ ].sort(compareCodePoints);
512
+ }
513
+ function scaffoldFiles(descriptor, descriptorPath, sdkVersion) {
514
+ const configPath = normalizeRelativePath(requireString(descriptor.configPath, 'tap.test.json configPath'), 'tap.test.json configPath');
515
+ const rows = matrixRows(descriptor);
516
+ const rootTestMatch = descriptorTestMatch(descriptor);
517
+ const testMatch = rootTestMatch.length > 0 ? rootTestMatch : [
518
+ ...new Set(rows.flatMap((row)=>row.testMatch))
519
+ ].sort(compareCodePoints);
520
+ if (0 === testMatch.length) throw new Error('tap.test.json must declare an effective root or matrix testMatch for scaffolding.');
521
+ const testRoots = [
522
+ ...new Set(testMatch.map(testRootFromPattern))
523
+ ].sort(compareCodePoints);
524
+ const files = [
525
+ {
526
+ relativePath: descriptorPath,
527
+ content: jsonFile(descriptor)
528
+ },
529
+ {
530
+ relativePath: configPath,
531
+ content: configFile()
532
+ },
533
+ {
534
+ relativePath: 'tsconfig.tap-test.json',
535
+ content: tapTsconfig(configPath, testRoots)
536
+ },
537
+ {
538
+ relativePath: `${testRoots[0]}/tap-surface.smoke.test.ts`,
539
+ content: smokeTest(requireString(descriptor.packageId, 'tap.test.json packageId'), sdkVersion, rows)
540
+ },
541
+ ...collectProfileFiles(descriptor, 'fixtureFiles').map((relativePath)=>({
542
+ relativePath,
543
+ content: '{}\n'
544
+ })),
545
+ ...collectProfileFiles(descriptor, 'httpRouteFiles').map((relativePath)=>({
546
+ relativePath,
547
+ content: '[]\n'
548
+ }))
549
+ ];
550
+ const unique = new Map();
551
+ for (const file of files){
552
+ const relativePath = normalizeRelativePath(file.relativePath, 'Scaffold file');
553
+ const existing = unique.get(relativePath);
554
+ if (existing && existing.content !== file.content) throw new Error(`Scaffold planned conflicting content for ${relativePath}.`);
555
+ unique.set(relativePath, {
556
+ ...file,
557
+ relativePath
558
+ });
559
+ }
560
+ return [
561
+ ...unique.values()
562
+ ].sort((left, right)=>compareCodePoints(left.relativePath, right.relativePath));
563
+ }
564
+ async function pathKind(root, relativePath) {
565
+ const absolute = projectPath(root, relativePath);
566
+ const stats = await (0, __rspack_external_node_fs_promises_3b710708.lstat)(absolute).catch((error)=>{
567
+ if (null !== error && 'object' == typeof error && 'code' in error && 'ENOENT' === error.code) return;
568
+ throw error;
569
+ });
570
+ if (!stats) return 'missing';
571
+ if (!stats.isFile() || stats.isSymbolicLink()) throw new Error(`Scaffold destination must be a regular file or absent: ${relativePath}`);
572
+ const resolved = await (0, __rspack_external_node_fs_promises_3b710708.realpath)(absolute);
573
+ if (resolved !== root && !resolved.startsWith(`${root}${__rspack_external_node_path_806ed179["default"].sep}`)) throw new Error(`Scaffold destination escapes the source root: ${relativePath}`);
574
+ return 'regular';
575
+ }
576
+ async function ensureSafeDirectory(root, directory) {
577
+ const relative = __rspack_external_node_path_806ed179["default"].relative(root, directory);
578
+ if ('' === relative) return;
579
+ let current = root;
580
+ for (const segment of relative.split(__rspack_external_node_path_806ed179["default"].sep)){
581
+ current = __rspack_external_node_path_806ed179["default"].join(current, segment);
582
+ const stats = await (0, __rspack_external_node_fs_promises_3b710708.lstat)(current).catch((error)=>{
583
+ if (null !== error && 'object' == typeof error && 'code' in error && 'ENOENT' === error.code) return;
584
+ throw error;
585
+ });
586
+ if (!stats) {
587
+ await (0, __rspack_external_node_fs_promises_3b710708.mkdir)(current);
588
+ continue;
589
+ }
590
+ if (!stats.isDirectory() || stats.isSymbolicLink()) throw new Error(`Scaffold directory is not a regular directory: ${current}`);
591
+ }
592
+ }
593
+ async function atomicCreateFiles(root, files) {
594
+ const staged = [];
595
+ const created = [];
596
+ try {
597
+ for (const file of files){
598
+ const destination = projectPath(root, file.relativePath);
599
+ await ensureSafeDirectory(root, __rspack_external_node_path_806ed179["default"].dirname(destination));
600
+ const temporary = `${destination}.tap-scaffold-${__rspack_external_node_crypto_2e7c4b46["default"].randomUUID()}.tmp`;
601
+ const handle = await (0, __rspack_external_node_fs_promises_3b710708.open)(temporary, 'wx', 384);
602
+ try {
603
+ await handle.writeFile(file.content, 'utf8');
604
+ await handle.sync();
605
+ } finally{
606
+ await handle.close();
607
+ }
608
+ staged.push({
609
+ destination,
610
+ temporary
611
+ });
612
+ }
613
+ for (const file of staged){
614
+ await (0, __rspack_external_node_fs_promises_3b710708.link)(file.temporary, file.destination);
615
+ created.push(file.destination);
616
+ await (0, __rspack_external_node_fs_promises_3b710708.chmod)(file.destination, 420);
617
+ }
618
+ } catch (error) {
619
+ await Promise.all(created.map((destination)=>(0, __rspack_external_node_fs_promises_3b710708.unlink)(destination)));
620
+ throw error;
621
+ } finally{
622
+ await Promise.all(staged.map((file)=>(0, __rspack_external_node_fs_promises_3b710708.rm)(file.temporary, {
623
+ force: true
624
+ })));
625
+ }
626
+ }
627
+ async function optionalDescriptor(root, relativePath) {
628
+ if (await pathKind(root, relativePath) === 'missing') return;
629
+ return readJsonProjectFile(root, relativePath);
630
+ }
631
+ async function scaffoldTapTests(options, schemas) {
632
+ const root = await canonicalRoot(options.root);
633
+ const inventory = await inspectTapManifest({
634
+ ...options,
635
+ root
636
+ }, schemas);
637
+ const descriptorPath = normalizeRelativePath(options.descriptorPath ?? 'tap.test.json', "Descriptor path");
638
+ const existing = await optionalDescriptor(root, descriptorPath);
639
+ let descriptor;
640
+ if (existing) {
641
+ const object = requireObject(existing.value, existing.path);
642
+ if (1 === object.schemaVersion) throw new Error(`${descriptorPath} uses schemaVersion 1; run "tap-miniapp-test migrate" first.`);
643
+ assertSchema(existing.value, schemas.testDescriptor, existing.path);
644
+ descriptor = object;
645
+ } else {
646
+ descriptor = scaffoldDescriptor(inventory);
647
+ assertSchema(descriptor, schemas.testDescriptor, descriptorPath);
648
+ }
649
+ if (descriptor.packageId !== inventory.packageId) throw new Error(`${descriptorPath} packageId does not match manifest.package.packageId.`);
650
+ const plan = scaffoldFiles(descriptor, descriptorPath, inventory.sdkVersion);
651
+ const missing = [];
652
+ const statuses = [];
653
+ for (const file of plan){
654
+ const kind = await pathKind(root, file.relativePath);
655
+ if ('missing' === kind) {
656
+ missing.push(file);
657
+ statuses.push({
658
+ path: file.relativePath,
659
+ status: options.check ? 'missing' : 'created'
660
+ });
661
+ } else statuses.push({
662
+ path: file.relativePath,
663
+ status: 'preserved'
664
+ });
665
+ }
666
+ if (!options.check && missing.length > 0) await atomicCreateFiles(root, missing);
667
+ return {
668
+ schemaVersion: 1,
669
+ command: 'scaffold',
670
+ ok: !options.check || 0 === missing.length,
671
+ check: options.check ?? false,
672
+ packageId: inventory.packageId,
673
+ root,
674
+ files: statuses
675
+ };
676
+ }
677
+ function valuesOutsideAllowlist(values, allowlist) {
678
+ const allowed = new Set(allowlist);
679
+ return [
680
+ ...new Set(values)
681
+ ].filter((entry)=>!allowed.has(entry)).sort(compareCodePoints);
682
+ }
683
+ function authorizationAllowsCapability(allowed, capability) {
684
+ if (allowed.has(capability)) return true;
685
+ const effect = /^effect:([a-z0-9][a-z0-9._-]*):\S+$/u.exec(capability);
686
+ return null !== effect && allowed.has(`effect:${effect[1]}:*`);
687
+ }
688
+ function valuesOutsideAuthorizationAllowlist(values, allowlist) {
689
+ const allowed = new Set(allowlist);
690
+ return [
691
+ ...new Set(values)
692
+ ].filter((entry)=>!authorizationAllowsCapability(allowed, entry)).sort(compareCodePoints);
693
+ }
694
+ function cellKey(surface, target) {
695
+ return `${surface}\0${target}`;
696
+ }
697
+ function capabilityIsWaived(descriptor, capability, now) {
698
+ return requireArray(descriptor.waivers ?? [], 'tap.test.json waivers').some((waiverValue)=>{
699
+ const waiver = requireObject(waiverValue, 'tap.test.json waiver');
700
+ if (waiver.capability !== capability) return false;
701
+ if (void 0 === waiver.expiresAt) return false;
702
+ const expiresAt = requireString(waiver.expiresAt, 'waiver expiresAt');
703
+ const expiration = Date.parse(expiresAt);
704
+ return Number.isFinite(expiration) && expiration > now.getTime();
705
+ });
706
+ }
707
+ function rowDeniesCapability(row, capability) {
708
+ if (row.deniedActions.includes('*')) return true;
709
+ if (capability.startsWith('action:')) {
710
+ const action = capability.slice(7);
711
+ return row.deniedActions.includes(action);
712
+ }
713
+ if (!capability.startsWith('effect:')) return false;
714
+ const effectKind = capability.split(':', 3)[1] ?? '';
715
+ if ('credentials' === effectKind) return row.deniedActions.includes('credentials.use');
716
+ return [
717
+ 'external-network',
718
+ 'host-http',
719
+ 'network'
720
+ ].includes(effectKind) && ('http-denied' === row.permissionScenario || row.deniedActions.includes('network.request'));
721
+ }
722
+ function globToRegExp(pattern) {
723
+ const confined = normalizeRelativePath(pattern, 'testMatch');
724
+ let source = '^';
725
+ for(let index = 0; index < confined.length; index += 1){
726
+ const character = confined[index];
727
+ if ('*' === character) {
728
+ if ('*' === confined[index + 1]) {
729
+ index += 1;
730
+ if ('/' === confined[index + 1]) {
731
+ index += 1;
732
+ source += '(?:.*/)?';
733
+ } else source += '.*';
734
+ } else source += '[^/]*';
735
+ continue;
736
+ }
737
+ if ('?' === character) {
738
+ source += '[^/]';
739
+ continue;
740
+ }
741
+ if ('[' === character) {
742
+ const closing = confined.indexOf(']', index + 1);
743
+ if (-1 === closing) throw new Error(`testMatch has an unterminated character class: ${pattern}`);
744
+ const body = confined.slice(index + 1, closing);
745
+ if (0 === body.length || body.includes('/') || body.includes('\\') || body.includes('[')) throw new Error(`testMatch has an unsafe character class: ${pattern}`);
746
+ const negated = body.startsWith('!') ? `^${body.slice(1)}` : body;
747
+ source += `[${negated.replaceAll('^', '\\^')}]`;
748
+ index = closing;
749
+ continue;
750
+ }
751
+ if ('{' === character) {
752
+ const closing = confined.indexOf('}', index + 1);
753
+ if (-1 === closing) throw new Error(`testMatch has an unterminated alternation: ${pattern}`);
754
+ const alternatives = confined.slice(index + 1, closing).split(',');
755
+ if (alternatives.length < 2 || alternatives.some((entry)=>0 === entry.length || !/^[a-zA-Z0-9._-]+$/u.test(entry))) throw new Error(`testMatch has an unsafe alternation: ${pattern}`);
756
+ source += `(?:${alternatives.join('|')})`;
757
+ index = closing;
758
+ continue;
759
+ }
760
+ source += /[\\^$+.()|{}]/u.test(character) ? `\\${character}` : character;
761
+ }
762
+ source += '$';
763
+ return new RegExp(source, 'u');
764
+ }
765
+ async function collectTestFiles(root) {
766
+ const files = [];
767
+ const excludedDirectories = new Set([
768
+ '.git',
769
+ '.tap-build',
770
+ '.tap-legacy',
771
+ '.turbo',
772
+ 'build',
773
+ 'dist',
774
+ 'node_modules'
775
+ ]);
776
+ const visit = async (directory)=>{
777
+ const entries = await (0, __rspack_external_node_fs_promises_3b710708.readdir)(directory, {
778
+ withFileTypes: true
779
+ });
780
+ entries.sort((left, right)=>compareCodePoints(left.name, right.name));
781
+ for (const entry of entries){
782
+ if (entry.isSymbolicLink()) continue;
783
+ const absolute = __rspack_external_node_path_806ed179["default"].join(directory, entry.name);
784
+ if (entry.isDirectory()) {
785
+ if (!excludedDirectories.has(entry.name)) await visit(absolute);
786
+ continue;
787
+ }
788
+ if (entry.isFile()) {
789
+ if (/\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(entry.name)) {
790
+ files.push(__rspack_external_node_path_806ed179["default"].relative(root, absolute).split(__rspack_external_node_path_806ed179["default"].sep).join('/'));
791
+ if (files.length > MAX_SCANNED_TEST_FILES) throw new Error(`Test discovery exceeds ${MAX_SCANNED_TEST_FILES} files.`);
792
+ }
793
+ }
794
+ }
795
+ };
796
+ await visit(root);
797
+ return files;
798
+ }
799
+ function hasSurfaceApplicabilityBranch(source) {
800
+ return /if\s*\([^)]*\btap\.surfaceId\b[^)]*\)/su.test(source) || /switch\s*\(\s*tap\.surfaceId\s*\)/su.test(source) || /\btap\.surfaceId\b[^\n?]{0,160}\?/u.test(source) || /\breturn\b[^\n;]{0,160}\btap\.surfaceId\b/u.test(source);
801
+ }
802
+ function dependencyVersions(packageJson, dependency) {
803
+ const versions = [];
804
+ for (const field of [
805
+ 'dependencies',
806
+ 'devDependencies',
807
+ 'optionalDependencies',
808
+ 'peerDependencies'
809
+ ]){
810
+ const dependencies = packageJson[field];
811
+ if (void 0 === dependencies) continue;
812
+ const record = requireObject(dependencies, `package.json ${field}`);
813
+ const version = record[dependency];
814
+ if (void 0 !== version) versions.push({
815
+ field,
816
+ version: requireString(version, `package.json ${field}.${dependency}`)
817
+ });
818
+ }
819
+ return versions;
820
+ }
821
+ function diagnostic(diagnostics, code, message, relativePath, severity = 'error') {
822
+ diagnostics.push({
823
+ code,
824
+ severity,
825
+ message,
826
+ ...relativePath ? {
827
+ path: relativePath
828
+ } : {}
829
+ });
830
+ }
831
+ async function validateDescriptorFiles(root, descriptor, diagnostics) {
832
+ const configPath = normalizeRelativePath(requireString(descriptor.configPath, 'tap.test.json configPath'), 'tap.test.json configPath');
833
+ for (const required of [
834
+ configPath,
835
+ 'tsconfig.tap-test.json'
836
+ ])try {
837
+ const file = await readProjectFile(root, required);
838
+ if (required === configPath && (!/\bdefineTapRstestConfig\b/u.test(file.source) || !/['"]@theaiplatform\/miniapp-sdk\/testing\/rstest-config['"]/u.test(file.source))) diagnostic(diagnostics, 'config.deterministic-helper-required', `Rstest TAP config must import and use defineTapRstestConfig from ${SDK_PACKAGE}/testing/rstest-config so Test Lab owns isolation, concurrency, retries, environment, and the default timeout.`, required);
839
+ } catch (error) {
840
+ diagnostic(diagnostics, 'file.missing-or-unsafe', error instanceof Error ? error.message : String(error), required);
841
+ }
842
+ for (const file of collectProfileFiles(descriptor, 'fixtureFiles'))try {
843
+ const document = await readJsonProjectFile(root, file);
844
+ if (!isJsonObject(document.value)) throw new Error(`${file} must contain a fixture seed JSON object.`);
845
+ } catch (error) {
846
+ diagnostic(diagnostics, 'fixture.invalid', error instanceof Error ? error.message : String(error), file);
847
+ }
848
+ for (const file of collectProfileFiles(descriptor, 'httpRouteFiles'))try {
849
+ const document = await readJsonProjectFile(root, file);
850
+ if (!Array.isArray(document.value)) throw new Error(`${file} must contain an HTTP route JSON array.`);
851
+ } catch (error) {
852
+ diagnostic(diagnostics, 'http-routes.invalid', error instanceof Error ? error.message : String(error), file);
853
+ }
854
+ }
855
+ async function validateMatrixTestApplicability(root, rows, diagnostics) {
856
+ const testFiles = await collectTestFiles(root);
857
+ const matchers = new Map();
858
+ const matches = (pattern, file)=>{
859
+ let matcher = matchers.get(pattern);
860
+ if (!matcher) {
861
+ matcher = globToRegExp(pattern);
862
+ matchers.set(pattern, matcher);
863
+ }
864
+ return matcher.test(file);
865
+ };
866
+ for (const row of rows){
867
+ if (0 === row.testMatch.length) {
868
+ diagnostic(diagnostics, 'matrix.test-match-missing', `Matrix ${row.id} has no effective testMatch.`, 'tap.test.json');
869
+ continue;
870
+ }
871
+ if (!testFiles.some((file)=>row.testMatch.some((glob)=>matches(glob, file)))) diagnostic(diagnostics, 'matrix.test-match-empty', `Matrix ${row.id} testMatch selects no test files.`, 'tap.test.json');
872
+ }
873
+ for (const file of testFiles){
874
+ const applicableRows = rows.filter((row)=>row.testMatch.some((glob)=>matches(glob, file)));
875
+ const surfaces = new Set(applicableRows.map((row)=>row.surface));
876
+ if (surfaces.size < 2) continue;
877
+ const source = await (0, __rspack_external_node_fs_promises_3b710708.readFile)(projectPath(root, file), 'utf8');
878
+ if (hasSurfaceApplicabilityBranch(source)) diagnostic(diagnostics, 'test.surface-branch', `${file} branches or returns on tap.surfaceId while matching ${surfaces.size} surfaces. Split the cases into surface-specific files and express applicability with matrix.testMatch.`, file);
879
+ }
880
+ }
881
+ function validateMatrixCoverage(inventory, descriptor, rows, diagnostics, now) {
882
+ for (const waiverValue of requireArray(descriptor.waivers ?? [], 'tap.test.json waivers')){
883
+ const waiver = requireObject(waiverValue, 'tap.test.json waiver');
884
+ if (void 0 === waiver.expiresAt) diagnostic(diagnostics, 'waiver.expiry-required', `Waiver for ${requireString(waiver.capability, 'waiver capability')} must declare a future expiresAt. Open-ended waivers never satisfy denied/error coverage.`, 'tap.test.json');
885
+ }
886
+ const surfaces = new Map(inventory.surfaces.map((surface)=>[
887
+ surface.id,
888
+ surface
889
+ ]));
890
+ const cells = new Map();
891
+ for (const surface of inventory.surfaces)for (const target of surface.targets)cells.set(cellKey(surface.id, target), surface);
892
+ for (const row of rows){
893
+ const surface = surfaces.get(row.surface);
894
+ if (!surface || !surface.targets.includes(row.target)) {
895
+ diagnostic(diagnostics, 'matrix.unknown-cell', `Matrix ${row.id} selects undeclared surface×target ${row.surface}×${row.target}.`, 'tap.test.json');
896
+ continue;
897
+ }
898
+ const declaredCapabilities = new Set([
899
+ ...surface.authorizationActions.map((action)=>`action:${action}`),
900
+ ...surface.authorizationEffects
901
+ ]);
902
+ const undeclaredCapabilities = row.capabilities.filter((capability)=>!authorizationAllowsCapability(declaredCapabilities, capability));
903
+ if (undeclaredCapabilities.length > 0) diagnostic(diagnostics, 'matrix.capability-not-authorized', `Matrix ${row.id} names capabilities absent from ${row.surface} authorization: ${JSON.stringify(undeclaredCapabilities)}.`, 'tap.test.json');
904
+ }
905
+ for (const [key, surface] of cells){
906
+ const separator = key.indexOf('\0');
907
+ const surfaceId = key.slice(0, separator);
908
+ const target = key.slice(separator + 1);
909
+ const applicable = rows.filter((row)=>row.surface === surfaceId && row.target === target);
910
+ const positive = applicable.filter((row)=>0 === row.deniedActions.length);
911
+ if (1 !== positive.length) diagnostic(diagnostics, 'matrix.positive-cell-count', `Surface×target ${surfaceId}×${target} must have exactly one positive matrix entry; found ${positive.length}.`, 'tap.test.json');
912
+ const declaredCapabilities = [
913
+ ...surface.authorizationActions.map((action)=>`action:${action}`),
914
+ ...surface.authorizationEffects
915
+ ];
916
+ for (const capability of declaredCapabilities){
917
+ const named = positive.some((row)=>row.capabilities.includes(capability));
918
+ if (!named) diagnostic(diagnostics, 'coverage.capability-unnamed', `The positive matrix entry for ${surfaceId}×${target} must name declared capability ${capability}. Matrix capabilities are descriptor requirements and cannot rely on ambient manifest authority.`, 'tap.test.json');
919
+ }
920
+ for (const capability of surface.highRiskCapabilities){
921
+ const denied = applicable.some((row)=>row.capabilities.includes(capability) && rowDeniesCapability(row, capability));
922
+ if (!denied && !capabilityIsWaived(descriptor, capability, now)) {
923
+ const guidance = capability.startsWith('action:') ? `Add a denied/error row with capabilities ["${capability}"] and deniedActions ["${capability.slice(7)}"].` : /^effect:(?:external-network|host-http|network):/u.test(capability) ? `Add an HTTP-denied error row with capabilities ["${capability}"], permissionScenario "http-denied", and deniedActions ["network.request"].` : /^effect:credentials:/u.test(capability) ? `Add a credential-denied error row with capabilities ["${capability}"], permissionScenario "deny:credentials.use", and deniedActions ["credentials.use"].` : `Add an all-denied error row with capabilities ["${capability}"] and deniedActions ["*"], or use an explicit waiver when no action can express this host-effect denial.`;
924
+ diagnostic(diagnostics, 'coverage.denied-case-missing', `Missing real denied/error coverage for ${capability} on ${surfaceId}×${target}. ${guidance}`, 'tap.test.json');
925
+ }
926
+ }
927
+ }
928
+ const profiles = descriptorProfiles(descriptor);
929
+ for (const [profileName, profileValue] of Object.entries(profiles)){
930
+ const profile = requireObject(profileValue, `profile ${profileName}`);
931
+ const surfaceId = requireString(profile.surface, `profile ${profileName} surface`);
932
+ const surface = surfaces.get(surfaceId);
933
+ if (!surface) continue;
934
+ const requiredEffects = optionalStringArray(profile.requiredEffects, `profile ${profileName} requiredEffects`);
935
+ const unknownEffects = valuesOutsideAuthorizationAllowlist(requiredEffects, surface.authorizationEffects);
936
+ if (unknownEffects.length > 0) diagnostic(diagnostics, 'profile.required-effects-drift', `Profile ${profileName} requires effects absent from ${surfaceId} authorization.effects: ${JSON.stringify(unknownEffects)}.`, 'tap.test.json');
937
+ const requiredOrigins = optionalStringArray(profile.requiredOrigins, `profile ${profileName} requiredOrigins`);
938
+ const unknownOrigins = valuesOutsideAllowlist(requiredOrigins, surface.requiredOrigins);
939
+ if (unknownOrigins.length > 0) diagnostic(diagnostics, 'profile.required-origins-drift', `Profile ${profileName} requires origins absent from ${surfaceId} authorization.effects: ${JSON.stringify(unknownOrigins)}.`, 'tap.test.json');
940
+ }
941
+ }
942
+ async function doctorTapTests(options, schemas, sdkVersion, now = new Date()) {
943
+ const root = await canonicalRoot(options.root);
944
+ const diagnostics = [];
945
+ let inventory;
946
+ let descriptor;
947
+ try {
948
+ inventory = await inspectTapManifest({
949
+ ...options,
950
+ root
951
+ }, schemas);
952
+ } catch (error) {
953
+ diagnostic(diagnostics, 'manifest.invalid', error instanceof Error ? error.message : String(error), options.manifestPath ?? 'manifest.tap.json');
954
+ }
955
+ try {
956
+ const descriptorPath = options.descriptorPath ?? 'tap.test.json';
957
+ const document = await readJsonProjectFile(root, descriptorPath);
958
+ const object = requireObject(document.value, descriptorPath);
959
+ if (1 === object.schemaVersion) throw new Error(`${descriptorPath} uses schemaVersion 1; run "tap-miniapp-test migrate".`);
960
+ assertSchema(document.value, schemas.testDescriptor, descriptorPath);
961
+ descriptor = object;
962
+ } catch (error) {
963
+ diagnostic(diagnostics, "descriptor.invalid", error instanceof Error ? error.message : String(error), options.descriptorPath ?? 'tap.test.json');
964
+ }
965
+ try {
966
+ const packageDocument = await readJsonProjectFile(root, 'package.json');
967
+ const packageJson = requireObject(packageDocument.value, 'package.json');
968
+ const versions = dependencyVersions(packageJson, SDK_PACKAGE);
969
+ if (1 !== versions.length || versions[0]?.version !== sdkVersion) diagnostic(diagnostics, 'sdk.version-not-exact', `package.json must declare exactly one ${SDK_PACKAGE} dependency pinned to ${sdkVersion}; found ${0 === versions.length ? 'none' : versions.map((entry)=>`${entry.field}:${entry.version}`).join(', ')}.`, 'package.json');
970
+ } catch (error) {
971
+ diagnostic(diagnostics, 'package-json.invalid', error instanceof Error ? error.message : String(error), 'package.json');
972
+ }
973
+ if (inventory && inventory.sdkVersion !== sdkVersion) diagnostic(diagnostics, 'manifest.sdk-version-not-exact', `manifest.compatibility.tapSdk must equal the executing SDK version ${sdkVersion}; found ${inventory.sdkVersion}. Ranges and tags are not accepted.`, options.manifestPath ?? 'manifest.tap.json');
974
+ if (inventory && descriptor) {
975
+ if (descriptor.packageId !== inventory.packageId) diagnostic(diagnostics, "descriptor.package-id-mismatch", `tap.test.json packageId must equal ${inventory.packageId}.`, options.descriptorPath ?? 'tap.test.json');
976
+ let rows = [];
977
+ try {
978
+ rows = matrixRows(descriptor);
979
+ validateMatrixCoverage(inventory, descriptor, rows, diagnostics, now);
980
+ await validateDescriptorFiles(root, descriptor, diagnostics);
981
+ await validateMatrixTestApplicability(root, rows, diagnostics);
982
+ } catch (error) {
983
+ diagnostic(diagnostics, "descriptor.semantic-error", error instanceof Error ? error.message : String(error), options.descriptorPath ?? 'tap.test.json');
984
+ }
985
+ }
986
+ diagnostics.sort((left, right)=>{
987
+ const byCode = compareCodePoints(left.code, right.code);
988
+ return 0 !== byCode ? byCode : compareCodePoints(left.path ?? '', right.path ?? '');
989
+ });
990
+ return {
991
+ schemaVersion: 1,
992
+ command: 'doctor',
993
+ ok: diagnostics.every((entry)=>'error' !== entry.severity),
994
+ ...inventory ? {
995
+ packageId: inventory.packageId
996
+ } : {},
997
+ sdkVersion,
998
+ root,
999
+ diagnostics
1000
+ };
1001
+ }
1002
+ async function atomicReplaceFile(destination, content) {
1003
+ const temporary = `${destination}.tap-migrate-${__rspack_external_node_crypto_2e7c4b46["default"].randomUUID()}.tmp`;
1004
+ const handle = await (0, __rspack_external_node_fs_promises_3b710708.open)(temporary, 'wx', 384);
1005
+ try {
1006
+ await handle.writeFile(content, 'utf8');
1007
+ await handle.sync();
1008
+ } finally{
1009
+ await handle.close();
1010
+ }
1011
+ try {
1012
+ await (0, __rspack_external_node_fs_promises_3b710708.chmod)(temporary, 420);
1013
+ await (0, __rspack_external_node_fs_promises_3b710708.rename)(temporary, destination);
1014
+ } finally{
1015
+ await (0, __rspack_external_node_fs_promises_3b710708.rm)(temporary, {
1016
+ force: true
1017
+ });
1018
+ }
1019
+ }
1020
+ async function migrateTapTests(options, schemas) {
1021
+ const root = await canonicalRoot(options.root);
1022
+ const descriptorPath = normalizeRelativePath(options.descriptorPath ?? 'tap.test.json', "Descriptor path");
1023
+ const source = await readJsonProjectFile(root, descriptorPath);
1024
+ const object = requireObject(source.value, descriptorPath);
1025
+ if (2 === object.schemaVersion) {
1026
+ assertSchema(source.value, schemas.testDescriptor, descriptorPath);
1027
+ return {
1028
+ schemaVersion: 1,
1029
+ command: 'migrate',
1030
+ ok: true,
1031
+ changed: false,
1032
+ dryRun: options.dryRun ?? false,
1033
+ descriptorPath,
1034
+ descriptor: object
1035
+ };
1036
+ }
1037
+ const inventory = await inspectTapManifest({
1038
+ ...options,
1039
+ root
1040
+ }, schemas);
1041
+ const descriptor = expandMigratedDescriptor(source.value, inventory);
1042
+ assertSchema(descriptor, schemas.testDescriptor, descriptorPath);
1043
+ const backupPath = descriptorPath.replace(/\.json$/u, '.v1.json');
1044
+ if (backupPath === descriptorPath) throw new Error("Descriptor path must end in .json for migration backup.");
1045
+ const backupKind = await pathKind(root, backupPath);
1046
+ if ('regular' === backupKind) {
1047
+ const backup = await readProjectFile(root, backupPath);
1048
+ if (backup.source !== source.source) throw new Error(`Migration backup ${backupPath} already exists with different content.`);
1049
+ }
1050
+ if (!options.dryRun) {
1051
+ let createdBackup = false;
1052
+ try {
1053
+ if ('missing' === backupKind) {
1054
+ await atomicCreateFiles(root, [
1055
+ {
1056
+ relativePath: backupPath,
1057
+ content: source.source
1058
+ }
1059
+ ]);
1060
+ createdBackup = true;
1061
+ }
1062
+ await atomicReplaceFile(projectPath(root, descriptorPath), jsonFile(descriptor));
1063
+ } catch (error) {
1064
+ if (createdBackup) await (0, __rspack_external_node_fs_promises_3b710708.unlink)(projectPath(root, backupPath));
1065
+ throw error;
1066
+ }
1067
+ }
1068
+ return {
1069
+ schemaVersion: 1,
1070
+ command: 'migrate',
1071
+ ok: true,
1072
+ changed: true,
1073
+ dryRun: options.dryRun ?? false,
1074
+ descriptorPath,
1075
+ backupPath,
1076
+ descriptor
1077
+ };
1078
+ }
1079
+ const COMMANDS = [
1080
+ 'doctor',
1081
+ 'list',
1082
+ 'matrix',
1083
+ 'scaffold',
1084
+ 'migrate'
1085
+ ];
1086
+ const HELP = `The AI Platform miniapp test tooling
1087
+
1088
+ Usage:
1089
+ tap-miniapp-test doctor [options]
1090
+ tap-miniapp-test list [options]
1091
+ tap-miniapp-test matrix [options]
1092
+ tap-miniapp-test scaffold [--check] [options]
1093
+ tap-miniapp-test migrate [--dry-run] [options]
1094
+
1095
+ Options:
1096
+ --root <path> Miniapp project root (default: current directory)
1097
+ --manifest <path> Package manifest (default: manifest.tap.json)
1098
+ --descriptor <path> Test descriptor (default: tap.test.json)
1099
+ --json Emit one machine-readable JSON document
1100
+ --check Check scaffold completeness without writing
1101
+ --dry-run Print migration output without writing
1102
+ --help Show this help
1103
+ `;
1104
+ function isCommand(value) {
1105
+ return COMMANDS.some((command)=>command === value);
1106
+ }
1107
+ function optionValue(args, index, option) {
1108
+ const value = args[index + 1];
1109
+ if (!value || value.startsWith('--')) throw new Error(`${option} requires a value.`);
1110
+ return value;
1111
+ }
1112
+ function parseArgs(args) {
1113
+ if (0 === args.length || args.includes('--help') || args.includes('-h')) return 'help';
1114
+ const command = args[0];
1115
+ if (!command || !isCommand(command)) throw new Error(`Unknown command ${JSON.stringify(command ?? '')}. Expected ${COMMANDS.join(', ')}.`);
1116
+ let root = process.cwd();
1117
+ let manifestPath;
1118
+ let descriptorPath;
1119
+ let check = false;
1120
+ let dryRun = false;
1121
+ let json = false;
1122
+ for(let index = 1; index < args.length; index += 1){
1123
+ const argument = args[index];
1124
+ if ('--json' === argument) {
1125
+ json = true;
1126
+ continue;
1127
+ }
1128
+ if ('--check' === argument) {
1129
+ check = true;
1130
+ continue;
1131
+ }
1132
+ if ('--dry-run' === argument) {
1133
+ dryRun = true;
1134
+ continue;
1135
+ }
1136
+ if ('--root' === argument) {
1137
+ root = optionValue(args, index, argument);
1138
+ index += 1;
1139
+ continue;
1140
+ }
1141
+ if ('--manifest' === argument) {
1142
+ manifestPath = optionValue(args, index, argument);
1143
+ index += 1;
1144
+ continue;
1145
+ }
1146
+ if ("--descriptor" === argument) {
1147
+ descriptorPath = optionValue(args, index, argument);
1148
+ index += 1;
1149
+ continue;
1150
+ }
1151
+ throw new Error(`Unknown option ${JSON.stringify(argument)}.`);
1152
+ }
1153
+ if (check && 'scaffold' !== command) throw new Error('--check is supported only by scaffold.');
1154
+ if (dryRun && 'migrate' !== command) throw new Error('--dry-run is supported only by migrate.');
1155
+ return {
1156
+ command,
1157
+ root,
1158
+ ...manifestPath ? {
1159
+ manifestPath
1160
+ } : {},
1161
+ ...descriptorPath ? {
1162
+ descriptorPath
1163
+ } : {},
1164
+ check,
1165
+ dryRun,
1166
+ json
1167
+ };
1168
+ }
1169
+ function projectOptions(options) {
1170
+ return {
1171
+ root: options.root,
1172
+ ...options.manifestPath ? {
1173
+ manifestPath: options.manifestPath
1174
+ } : {},
1175
+ ...options.descriptorPath ? {
1176
+ descriptorPath: options.descriptorPath
1177
+ } : {}
1178
+ };
1179
+ }
1180
+ function printJson(value) {
1181
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
1182
+ }
1183
+ function printInventory(inventory) {
1184
+ process.stdout.write([
1185
+ `${inventory.packageId} (SDK ${inventory.sdkVersion})`,
1186
+ `Targets: ${inventory.targets.join(', ')}`,
1187
+ ...inventory.surfaces.map((surface)=>`- ${surface.id}: ${surface.targets.join(', ')}`),
1188
+ ''
1189
+ ].join('\n'));
1190
+ }
1191
+ function printRows(rows) {
1192
+ process.stdout.write([
1193
+ 'ID\tPROFILE\tSURFACE\tTARGET\tMODE\tPERMISSION',
1194
+ ...rows.map((row)=>[
1195
+ row.id,
1196
+ row.profile,
1197
+ row.surface,
1198
+ row.target,
1199
+ row.mode,
1200
+ row.permissionScenario
1201
+ ].join('\t')),
1202
+ ''
1203
+ ].join('\n'));
1204
+ }
1205
+ function printDoctor(result) {
1206
+ process.stdout.write(result.ok ? `Miniapp test project is ready for ${result.sdkVersion}.\n` : [
1207
+ `Miniapp test project has ${result.diagnostics.length} issue(s):`,
1208
+ ...result.diagnostics.map((entry)=>`- [${entry.code}]${entry.path ? ` ${entry.path}:` : ''} ${entry.message}`),
1209
+ ''
1210
+ ].join('\n'));
1211
+ }
1212
+ function printScaffold(result) {
1213
+ process.stdout.write([
1214
+ `${result.check ? 'Checked' : 'Scaffolded'} ${result.packageId}:`,
1215
+ ...result.files.map((file)=>`- ${file.status}: ${file.path}`),
1216
+ ''
1217
+ ].join('\n'));
1218
+ }
1219
+ function printMigration(result) {
1220
+ if (!result.changed) return void process.stdout.write(`${result.descriptorPath} already uses schemaVersion 2.\n`);
1221
+ process.stdout.write(result.dryRun ? `${JSON.stringify(result.descriptor, null, 2)}\n` : `Migrated ${result.descriptorPath} to schemaVersion 2; preserved v1 at ${result.backupPath}.\n`);
1222
+ }
1223
+ async function run(options) {
1224
+ const [schemas, sdkVersion] = await Promise.all([
1225
+ loadTapToolingSchemas(new URL('../../config-schema.json', import.meta.url), new URL('../testing/tap.test.schema.json', import.meta.url)),
1226
+ (0, __rspack_external_node_fs_promises_3b710708.readFile)(new URL('../../package.json', import.meta.url), 'utf8').then(parseTapSdkPackageVersion)
1227
+ ]);
1228
+ const project = projectOptions(options);
1229
+ if ('doctor' === options.command) {
1230
+ const result = await doctorTapTests(project, schemas, sdkVersion);
1231
+ options.json ? printJson(result) : printDoctor(result);
1232
+ return result.ok;
1233
+ }
1234
+ if ('list' === options.command) {
1235
+ const inventory = await inspectTapManifest(project, schemas);
1236
+ const result = {
1237
+ schemaVersion: 1,
1238
+ command: 'list',
1239
+ ok: true,
1240
+ ...inventory
1241
+ };
1242
+ options.json ? printJson(result) : printInventory(inventory);
1243
+ return true;
1244
+ }
1245
+ if ('matrix' === options.command) {
1246
+ const result = await inspectTapMatrix(project, schemas);
1247
+ const output = {
1248
+ schemaVersion: 1,
1249
+ command: 'matrix',
1250
+ ok: true,
1251
+ rows: result.rows
1252
+ };
1253
+ options.json ? printJson(output) : printRows(result.rows);
1254
+ return true;
1255
+ }
1256
+ if ('scaffold' === options.command) {
1257
+ const result = await scaffoldTapTests({
1258
+ ...project,
1259
+ check: options.check
1260
+ }, schemas);
1261
+ options.json ? printJson(result) : printScaffold(result);
1262
+ return result.ok;
1263
+ }
1264
+ const result = await migrateTapTests({
1265
+ ...project,
1266
+ dryRun: options.dryRun
1267
+ }, schemas);
1268
+ options.json ? printJson(result) : printMigration(result);
1269
+ return result.ok;
1270
+ }
1271
+ async function main() {
1272
+ let parsed;
1273
+ try {
1274
+ parsed = parseArgs(process.argv.slice(2));
1275
+ if ('help' === parsed) return void process.stdout.write(HELP);
1276
+ if (!await run(parsed)) process.exitCode = 1;
1277
+ } catch (error) {
1278
+ const message = error instanceof Error ? error.message : String(error);
1279
+ const json = process.argv.includes('--json');
1280
+ if (json) printJson({
1281
+ schemaVersion: 1,
1282
+ command: process.argv[2] ?? null,
1283
+ ok: false,
1284
+ error: {
1285
+ code: 'TAP_MINIAPP_TEST_TOOLING_ERROR',
1286
+ message
1287
+ }
1288
+ });
1289
+ else process.stderr.write(`tap-miniapp-test: ${message}\n`);
1290
+ process.exitCode = 1;
1291
+ }
1292
+ }
1293
+ main();