@jskit-ai/kernel 0.1.245 → 0.1.247

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/client/appConfig.js +2 -1
  2. package/client/asyncModuleRecovery.js +2 -4
  3. package/client/index.d.ts +13 -75
  4. package/client/index.js +1 -1
  5. package/client/logging.js +8 -2
  6. package/client/moduleBootstrap.js +41 -33
  7. package/client/moduleBootstrap.test.js +35 -0
  8. package/client/shellBootstrap.js +1 -8
  9. package/client/shellBootstrap.test.js +32 -5
  10. package/client/shellRouting.js +16 -61
  11. package/client/shellRouting.test.js +30 -1
  12. package/client/vite/clientBootstrapPlugin.js +0 -6
  13. package/client/vite/clientBootstrapPlugin.test.js +2 -3
  14. package/internal/node/installedPackages.js +19 -62
  15. package/internal/node/installedPackages.test.js +33 -0
  16. package/package.json +2 -4
  17. package/server/actions/actionCatalogue.js +13 -34
  18. package/{shared → server}/actions/actionContributorHelpers.js +0 -5
  19. package/{shared → server}/actions/actionDefinitions.js +24 -131
  20. package/{shared → server}/actions/actionDefinitions.test.js +75 -21
  21. package/server/actions/actionEvents.js +97 -2
  22. package/server/actions/actionEvents.test.js +79 -0
  23. package/{shared → server}/actions/executionContext.js +3 -13
  24. package/server/actions/idempotency.js +24 -0
  25. package/server/actions/pipeline.js +87 -0
  26. package/{shared → server}/actions/policies.js +20 -33
  27. package/{shared → server}/actions/policies.test.js +6 -6
  28. package/{shared → server}/actions/registry.js +3 -22
  29. package/{shared → server}/actions/registry.test.js +70 -32
  30. package/server/actions/requestMeta.js +17 -0
  31. package/server/http/capabilityHttpRuntime.js +1 -1
  32. package/server/http/capabilityHttpRuntime.test.js +56 -0
  33. package/server/http/errorHandling.js +291 -0
  34. package/server/{runtime/fastifyBootstrap.test.js → http/errorHandling.test.js} +72 -120
  35. package/server/http/lib/routeRegistration.js +36 -150
  36. package/server/http/lib/routeTransport.js +2 -0
  37. package/server/http/lib/routeValidator.js +5 -47
  38. package/server/http/lib/routeValidator.test.js +15 -17
  39. package/server/http/lib/router.js +10 -94
  40. package/server/platform/providerRuntime/capabilityProviderLoader.js +2 -1
  41. package/server/platform/providerRuntime/helpers.js +1 -34
  42. package/server/runtime/integers.js +1 -5
  43. package/server/runtime/serviceAuthorization.js +4 -9
  44. package/server/support/appConfig.js +2 -1
  45. package/server/support/appConfigFiles.js +1 -38
  46. package/server/support/appConfigFiles.test.js +3 -39
  47. package/server/support/index.js +1 -20
  48. package/shared/capabilities/defineProvider.js +1 -1
  49. package/shared/capabilities/index.js +1 -1
  50. package/shared/index.js +1 -1
  51. package/shared/support/index.js +1 -4
  52. package/shared/support/mobileConfig.js +99 -0
  53. package/shared/support/mobileConfig.test.js +113 -0
  54. package/shared/support/normalize.js +0 -108
  55. package/shared/support/normalize.test.js +0 -134
  56. package/shared/{actions/actionContributorHelpers.test.js → support/permissions.test.js} +1 -1
  57. package/shared/support/returnToPath.js +1 -20
  58. package/shared/support/returnToPath.test.js +1 -36
  59. package/shared/support/shellLayoutTargets.js +2 -140
  60. package/shared/support/shellLayoutTargets.test.js +1 -110
  61. package/shared/support/sorting.js +1 -9
  62. package/shared/surface/apiPaths.js +1 -82
  63. package/shared/surface/paths.js +38 -171
  64. package/shared/surface/registry.js +2 -1
  65. package/shared/surface/runtime.js +18 -109
  66. package/shared/surface/runtime.test.js +57 -0
  67. package/shared/validators/index.js +1 -4
  68. package/shared/validators/jsonRestSchemaSupport.js +0 -9
  69. package/shared/validators/recordIdParamsValidator.js +0 -6
  70. package/shared/validators/schemaDefinitions.js +4 -30
  71. package/shared/validators/schemaPayloadValidation.js +2 -6
  72. package/test/barrelExposure.test.js +3 -7
  73. package/test/exportsContract.test.js +1 -3
  74. package/client/mobileLaunchRouting.js +0 -231
  75. package/client/mobileLaunchRouting.test.js +0 -230
  76. package/server/http/lib/controller.js +0 -183
  77. package/server/http/lib/controller.test.js +0 -143
  78. package/server/http/lib/index.js +0 -5
  79. package/server/runtime/canonicalJson.js +0 -74
  80. package/server/runtime/entityChangeEvents.js +0 -307
  81. package/server/runtime/entityChangeEvents.test.js +0 -274
  82. package/server/runtime/fastifyBootstrap.js +0 -553
  83. package/server/runtime/moduleConfig.js +0 -223
  84. package/server/runtime/moduleConfig.test.js +0 -165
  85. package/server/runtime/pagination.js +0 -13
  86. package/server/runtime/realtimeNormalization.js +0 -21
  87. package/server/runtime/routeUtils.js +0 -20
  88. package/server/runtime/securityAudit.js +0 -269
  89. package/server/runtime/securityAudit.test.js +0 -41
  90. package/server/support/pageTargets.js +0 -772
  91. package/server/support/pageTargets.test.js +0 -659
  92. package/server/support/shellOutlets.js +0 -576
  93. package/server/support/shellOutlets.test.js +0 -336
  94. package/server/surface/index.js +0 -18
  95. package/shared/actions/audit.js +0 -7
  96. package/shared/actions/idempotency.js +0 -62
  97. package/shared/actions/observability.js +0 -10
  98. package/shared/actions/pipeline.js +0 -309
  99. package/shared/actions/requestMeta.js +0 -36
  100. package/shared/actions/textNormalization.js +0 -3
  101. package/shared/actions/withActionDefaults.js +0 -34
  102. /package/{shared → server}/actions/executionContext.test.js +0 -0
@@ -1,659 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import path from "node:path";
4
- import { tmpdir } from "node:os";
5
- import test from "node:test";
6
- import {
7
- deriveDefaultSubpagesHost,
8
- normalizePagesRelativeTargetRoot,
9
- resolvePageLinkTargetDetails,
10
- resolvePageTargetDetails
11
- } from "./pageTargets.js";
12
-
13
- async function withTempApp(run) {
14
- const appRoot = await mkdtemp(path.join(tmpdir(), "kernel-page-targets-"));
15
- try {
16
- return await run(appRoot);
17
- } finally {
18
- await rm(appRoot, { recursive: true, force: true });
19
- }
20
- }
21
-
22
- async function writeFileInApp(appRoot, relativePath, source) {
23
- const absoluteFile = path.join(appRoot, relativePath);
24
- await mkdir(path.dirname(absoluteFile), { recursive: true });
25
- await writeFile(absoluteFile, source, "utf8");
26
- }
27
-
28
- async function writeConfig(appRoot, source) {
29
- await writeFileInApp(appRoot, "config/public.js", source);
30
- }
31
-
32
- async function writeShellLayout(appRoot, source = "") {
33
- await writeFileInApp(
34
- appRoot,
35
- "src/components/ShellLayout.vue",
36
- source ||
37
- `<template>
38
- <div>
39
- <ShellOutlet target="shell-layout:top-right" />
40
- <ShellOutlet
41
- target="shell-layout:primary-menu"
42
- default
43
- />
44
- </div>
45
- </template>
46
- `
47
- );
48
- await writePlacementTopology(appRoot);
49
- }
50
-
51
- function renderTopologyVariant(outlet, { linkRenderer = "" } = {}) {
52
- const rendererLines = linkRenderer
53
- ? `,
54
- renderers: {
55
- link: "${linkRenderer}"
56
- }`
57
- : "";
58
- return `{
59
- outlet: "${outlet}"${rendererLines}
60
- }`;
61
- }
62
-
63
- function renderTopologyEntry({
64
- id = "",
65
- owner = "",
66
- surfaces = ["*"],
67
- defaultPlacement = false,
68
- outlet = "",
69
- linkRenderer = ""
70
- } = {}) {
71
- const ownerLine = owner ? ` owner: "${owner}",\n` : "";
72
- const defaultLine = defaultPlacement ? " default: true,\n" : "";
73
- return ` {
74
- id: "${id}",
75
- ${ownerLine} surfaces: ${JSON.stringify(surfaces)},
76
- ${defaultLine} variants: {
77
- compact: ${renderTopologyVariant(outlet, { linkRenderer })},
78
- medium: ${renderTopologyVariant(outlet, { linkRenderer })},
79
- expanded: ${renderTopologyVariant(outlet, { linkRenderer })}
80
- }
81
- }`;
82
- }
83
-
84
- async function writePlacementTopology(appRoot, entries = []) {
85
- const defaultEntries = [
86
- renderTopologyEntry({
87
- id: "shell.primary-nav",
88
- surfaces: ["*"],
89
- defaultPlacement: true,
90
- outlet: "shell-layout:primary-menu",
91
- linkRenderer: "local.main.ui.surface-aware-menu-link-item"
92
- }),
93
- renderTopologyEntry({
94
- id: "shell.status",
95
- surfaces: ["*"],
96
- outlet: "shell-layout:top-right",
97
- linkRenderer: "local.main.ui.surface-aware-menu-link-item"
98
- })
99
- ];
100
- await writeFileInApp(
101
- appRoot,
102
- "src/placementTopology.js",
103
- `export default {
104
- placements: [
105
- ${[...defaultEntries, ...entries].join(",\n")}
106
- ]
107
- };
108
- `
109
- );
110
- }
111
-
112
- test("resolvePageTargetDetails derives the surface and route data from an explicit page file", async () => {
113
- await withTempApp(async (appRoot) => {
114
- await writeConfig(
115
- appRoot,
116
- `export const config = {
117
- surfaceDefinitions: {
118
- admin: { id: "admin", pagesRoot: "w/[workspaceSlug]/admin", enabled: true }
119
- }
120
- };
121
- `
122
- );
123
-
124
- const pageTarget = await resolvePageTargetDetails({
125
- appRoot,
126
- targetFile: "w/[workspaceSlug]/admin/catalog/index/products/index.vue",
127
- context: "page target"
128
- });
129
-
130
- assert.equal(pageTarget.surfaceId, "admin");
131
- assert.equal(pageTarget.surfacePagesRoot, "w/[workspaceSlug]/admin");
132
- assert.equal(pageTarget.routeUrlSuffix, "/catalog/products");
133
- assert.equal(pageTarget.placementId, "ui-generator.page.admin.catalog.products.link");
134
- assert.deepEqual(pageTarget.visibleRouteSegments, ["catalog", "products"]);
135
- assert.equal(deriveDefaultSubpagesHost(pageTarget), "catalog-products");
136
- });
137
- });
138
-
139
- test("resolvePageTargetDetails includes surface in placement ids for identical routes on different surfaces", async () => {
140
- await withTempApp(async (appRoot) => {
141
- await writeConfig(
142
- appRoot,
143
- `export const config = {
144
- surfaceDefinitions: {
145
- app: { id: "app", pagesRoot: "app", enabled: true },
146
- admin: { id: "admin", pagesRoot: "admin", enabled: true }
147
- }
148
- };
149
- `
150
- );
151
-
152
- const appPageTarget = await resolvePageTargetDetails({
153
- appRoot,
154
- targetFile: "app/reports/index.vue",
155
- context: "page target"
156
- });
157
- const adminPageTarget = await resolvePageTargetDetails({
158
- appRoot,
159
- targetFile: "admin/reports/index.vue",
160
- context: "page target"
161
- });
162
-
163
- assert.equal(appPageTarget.placementId, "ui-generator.page.app.reports.link");
164
- assert.equal(adminPageTarget.placementId, "ui-generator.page.admin.reports.link");
165
- assert.notEqual(appPageTarget.placementId, adminPageTarget.placementId);
166
- });
167
- });
168
-
169
- test("resolvePageTargetDetails chooses the most specific matching surface pagesRoot", async () => {
170
- await withTempApp(async (appRoot) => {
171
- await writeConfig(
172
- appRoot,
173
- `export const config = {
174
- surfaceDefinitions: {
175
- app: { id: "app", pagesRoot: "w/[workspaceSlug]", enabled: true },
176
- admin: { id: "admin", pagesRoot: "w/[workspaceSlug]/admin", enabled: true }
177
- }
178
- };
179
- `
180
- );
181
-
182
- const pageTarget = await resolvePageTargetDetails({
183
- appRoot,
184
- targetFile: "w/[workspaceSlug]/admin/catalog/index.vue",
185
- context: "page target"
186
- });
187
-
188
- assert.equal(pageTarget.surfaceId, "admin");
189
- assert.equal(pageTarget.surfacePagesRoot, "w/[workspaceSlug]/admin");
190
- assert.equal(pageTarget.surfaceRelativeFilePath, "catalog/index.vue");
191
- assert.equal(pageTarget.routeUrlSuffix, "/catalog");
192
- assert.equal(pageTarget.placementId, "ui-generator.page.admin.catalog.link");
193
- });
194
- });
195
-
196
- test("resolvePageTargetDetails derives surface auth requirement from surface access policy", async () => {
197
- await withTempApp(async (appRoot) => {
198
- await writeConfig(
199
- appRoot,
200
- `export const config = {
201
- surfaceAccessPolicies: {
202
- public: {},
203
- authenticated: {
204
- requireAuth: true
205
- }
206
- },
207
- surfaceDefinitions: {
208
- home: { id: "home", pagesRoot: "home", enabled: true, accessPolicyId: "public" },
209
- app: { id: "app", pagesRoot: "app", enabled: true, accessPolicyId: "authenticated" }
210
- }
211
- };
212
- `
213
- );
214
-
215
- const publicPageTarget = await resolvePageTargetDetails({
216
- appRoot,
217
- targetFile: "home/index.vue",
218
- context: "page target"
219
- });
220
- const authenticatedPageTarget = await resolvePageTargetDetails({
221
- appRoot,
222
- targetFile: "app/index.vue",
223
- context: "page target"
224
- });
225
-
226
- assert.equal(publicPageTarget.surfaceRequiresAuth, false);
227
- assert.equal(authenticatedPageTarget.surfaceRequiresAuth, true);
228
- });
229
- });
230
-
231
- test("resolvePageTargetDetails rejects duplicate matching surface pagesRoot definitions", async () => {
232
- await withTempApp(async (appRoot) => {
233
- await writeConfig(
234
- appRoot,
235
- `export const config = {
236
- surfaceDefinitions: {
237
- adminA: { id: "admin-a", pagesRoot: "w/[workspaceSlug]/admin", enabled: true },
238
- adminB: { id: "admin-b", pagesRoot: "w/[workspaceSlug]/admin", enabled: true }
239
- }
240
- };
241
- `
242
- );
243
-
244
- await assert.rejects(
245
- () =>
246
- resolvePageTargetDetails({
247
- appRoot,
248
- targetFile: "w/[workspaceSlug]/admin/catalog/index.vue",
249
- context: "page target"
250
- }),
251
- /multiple surfaces share pagesRoot "w\/\[workspaceSlug\]\/admin" \(admin-a, admin-b\)/
252
- );
253
- });
254
- });
255
-
256
- test("resolvePageTargetDetails accepts target files with a src/pages prefix", async () => {
257
- await withTempApp(async (appRoot) => {
258
- await writeConfig(
259
- appRoot,
260
- `export const config = {
261
- surfaceDefinitions: {
262
- admin: { id: "admin", pagesRoot: "admin", enabled: true }
263
- }
264
- };
265
- `
266
- );
267
-
268
- const details = await resolvePageTargetDetails({
269
- appRoot,
270
- targetFile: "src/pages/admin/reports/index.vue",
271
- context: "page target"
272
- });
273
-
274
- assert.equal(details.targetFilePath.relativePath, "src/pages/admin/reports/index.vue");
275
- assert.equal(details.surfaceId, "admin");
276
- assert.equal(details.routeUrlSuffix, "/reports");
277
- });
278
- });
279
-
280
- test("normalizePagesRelativeTargetRoot accepts route roots with a src/pages prefix", () => {
281
- assert.equal(
282
- normalizePagesRelativeTargetRoot("src/pages/admin/customers", {
283
- context: "crud-ui-generator",
284
- label: 'option "target-root"'
285
- }),
286
- "src/pages/admin/customers"
287
- );
288
- });
289
-
290
- test("resolvePageLinkTargetDetails falls back to the app default placement target", async () => {
291
- await withTempApp(async (appRoot) => {
292
- await writeConfig(
293
- appRoot,
294
- `export const config = {
295
- surfaceDefinitions: {
296
- admin: { id: "admin", pagesRoot: "admin", enabled: true }
297
- }
298
- };
299
- `
300
- );
301
- await writeShellLayout(appRoot);
302
-
303
- const details = await resolvePageLinkTargetDetails({
304
- appRoot,
305
- targetFile: "admin/reports/index.vue",
306
- context: "page target"
307
- });
308
-
309
- assert.equal(details.pageTarget.surfaceId, "admin");
310
- assert.equal(details.placementTarget.id, "shell.primary-nav");
311
- assert.equal(details.componentToken, "");
312
- assert.equal(details.linkTo, "");
313
- assert.equal(details.whenLine, "");
314
- });
315
- });
316
-
317
- test("resolvePageLinkTargetDetails emits an auth guard when the surface policy requires auth", async () => {
318
- await withTempApp(async (appRoot) => {
319
- await writeConfig(
320
- appRoot,
321
- `export const config = {
322
- surfaceAccessPolicies: {
323
- authenticated: {
324
- requireAuth: true
325
- }
326
- },
327
- surfaceDefinitions: {
328
- app: { id: "app", pagesRoot: "app", enabled: true, accessPolicyId: "authenticated" }
329
- }
330
- };
331
- `
332
- );
333
- await writeShellLayout(appRoot);
334
-
335
- const details = await resolvePageLinkTargetDetails({
336
- appRoot,
337
- targetFile: "app/reports/index.vue",
338
- context: "page target"
339
- });
340
-
341
- assert.equal(details.whenLine, " when: ({ auth }) => auth?.authenticated === true\n");
342
- });
343
- });
344
-
345
- test("resolvePageLinkTargetDetails prefers an outlet-declared default link token over subpage heuristics", async () => {
346
- await withTempApp(async (appRoot) => {
347
- await writeConfig(
348
- appRoot,
349
- `export const config = {
350
- surfaceDefinitions: {
351
- home: { id: "home", pagesRoot: "home", enabled: true }
352
- }
353
- };
354
- `
355
- );
356
- await writePlacementTopology(appRoot, [
357
- renderTopologyEntry({
358
- id: "page.section-nav",
359
- owner: "home-settings",
360
- surfaces: ["home"],
361
- outlet: "home-settings:primary-menu",
362
- linkRenderer: "local.main.ui.surface-aware-menu-link-item"
363
- })
364
- ]);
365
- await writeFileInApp(
366
- appRoot,
367
- "src/pages/home/settings.vue",
368
- `<template>
369
- <section>
370
- <ShellOutlet target="home-settings:primary-menu" />
371
- <RouterView />
372
- </section>
373
- </template>
374
- `
375
- );
376
-
377
- const details = await resolvePageLinkTargetDetails({
378
- appRoot,
379
- targetFile: "home/settings/pollen-types/index.vue",
380
- context: "page target"
381
- });
382
-
383
- assert.equal(details.parentHost?.id, "home-settings:primary-menu");
384
- assert.equal(details.placementTarget.id, "page.section-nav");
385
- assert.equal(details.placementTarget.owner, "home-settings");
386
- assert.equal(details.componentToken, "");
387
- assert.equal(details.linkTo, "");
388
- });
389
- });
390
-
391
- test("resolvePageLinkTargetDetails infers owner-scoped placement for sibling file-route children", async () => {
392
- await withTempApp(async (appRoot) => {
393
- await writeConfig(
394
- appRoot,
395
- `export const config = {
396
- surfaceDefinitions: {
397
- home: { id: "home", pagesRoot: "home", enabled: true }
398
- }
399
- };
400
- `
401
- );
402
- await writePlacementTopology(appRoot, [
403
- renderTopologyEntry({
404
- id: "page.section-nav",
405
- owner: "home-settings",
406
- surfaces: ["home"],
407
- outlet: "home-settings:primary-menu",
408
- linkRenderer: "local.main.ui.surface-aware-menu-link-item"
409
- })
410
- ]);
411
- await writeFileInApp(
412
- appRoot,
413
- "src/pages/home/settings.vue",
414
- `<template>
415
- <section>
416
- <ShellOutlet target="home-settings:primary-menu" />
417
- <RouterView />
418
- </section>
419
- </template>
420
- `
421
- );
422
-
423
- const details = await resolvePageLinkTargetDetails({
424
- appRoot,
425
- targetFile: "home/settings/profile.vue",
426
- context: "page target"
427
- });
428
-
429
- assert.equal(details.parentHost?.id, "home-settings:primary-menu");
430
- assert.equal(details.placementTarget.id, "page.section-nav");
431
- assert.equal(details.placementTarget.owner, "home-settings");
432
- });
433
- });
434
-
435
- test("resolvePageLinkTargetDetails prefers owner-scoped topology over a global mapping for the same concrete outlet", async () => {
436
- await withTempApp(async (appRoot) => {
437
- await writeConfig(
438
- appRoot,
439
- `export const config = {
440
- surfaceDefinitions: {
441
- home: { id: "home", pagesRoot: "home", enabled: true }
442
- }
443
- };
444
- `
445
- );
446
- await writePlacementTopology(appRoot, [
447
- renderTopologyEntry({
448
- id: "page.section-nav",
449
- surfaces: ["home"],
450
- outlet: "home-settings:primary-menu",
451
- linkRenderer: "local.main.ui.surface-aware-menu-link-item"
452
- }),
453
- renderTopologyEntry({
454
- id: "page.section-nav",
455
- owner: "home-settings",
456
- surfaces: ["home"],
457
- outlet: "home-settings:primary-menu",
458
- linkRenderer: "local.main.ui.surface-aware-menu-link-item"
459
- })
460
- ]);
461
- await writeFileInApp(
462
- appRoot,
463
- "src/pages/home/settings.vue",
464
- `<template>
465
- <section>
466
- <ShellOutlet target="home-settings:primary-menu" />
467
- <RouterView />
468
- </section>
469
- </template>
470
- `
471
- );
472
-
473
- const details = await resolvePageLinkTargetDetails({
474
- appRoot,
475
- targetFile: "home/settings/profile.vue",
476
- context: "page target"
477
- });
478
-
479
- assert.equal(details.placementTarget.id, "page.section-nav");
480
- assert.equal(details.placementTarget.owner, "home-settings");
481
- });
482
- });
483
-
484
- test("resolvePageLinkTargetDetails rejects ambiguous semantic mappings for the same owner outlet", async () => {
485
- await withTempApp(async (appRoot) => {
486
- await writeConfig(
487
- appRoot,
488
- `export const config = {
489
- surfaceDefinitions: {
490
- home: { id: "home", pagesRoot: "home", enabled: true }
491
- }
492
- };
493
- `
494
- );
495
- await writePlacementTopology(appRoot, [
496
- renderTopologyEntry({
497
- id: "page.section-nav",
498
- owner: "home-settings",
499
- surfaces: ["home"],
500
- outlet: "home-settings:primary-menu",
501
- linkRenderer: "local.main.ui.surface-aware-menu-link-item"
502
- }),
503
- renderTopologyEntry({
504
- id: "page.actions",
505
- owner: "home-settings",
506
- surfaces: ["home"],
507
- outlet: "home-settings:primary-menu",
508
- linkRenderer: "local.main.ui.surface-aware-menu-link-item"
509
- })
510
- ]);
511
- await writeFileInApp(
512
- appRoot,
513
- "src/pages/home/settings.vue",
514
- `<template>
515
- <section>
516
- <ShellOutlet target="home-settings:primary-menu" />
517
- <RouterView />
518
- </section>
519
- </template>
520
- `
521
- );
522
-
523
- await assert.rejects(
524
- resolvePageLinkTargetDetails({
525
- appRoot,
526
- targetFile: "home/settings/profile.vue",
527
- context: "page target"
528
- }),
529
- /found multiple semantic placements mapped to concrete outlet "home-settings:primary-menu": page\.actions \[owner:home-settings\], page\.section-nav \[owner:home-settings\]/
530
- );
531
- });
532
- });
533
-
534
- test("resolvePageLinkTargetDetails inherits a file-route parent subpages host", async () => {
535
- await withTempApp(async (appRoot) => {
536
- await writeConfig(
537
- appRoot,
538
- `export const config = {
539
- surfaceDefinitions: {
540
- admin: { id: "admin", pagesRoot: "admin", enabled: true }
541
- }
542
- };
543
- `
544
- );
545
- await writeShellLayout(appRoot);
546
- await writePlacementTopology(appRoot, [
547
- renderTopologyEntry({
548
- id: "page.section-nav",
549
- owner: "contact-view",
550
- surfaces: ["admin"],
551
- outlet: "contact-view:sub-pages",
552
- linkRenderer: "local.main.ui.surface-aware-menu-link-item"
553
- })
554
- ]);
555
- await writeFileInApp(
556
- appRoot,
557
- "src/pages/admin/contacts/[contactId].vue",
558
- `<template>
559
- <SectionContainerShell>
560
- <template #tabs>
561
- <ShellOutlet target="contact-view:sub-pages" />
562
- </template>
563
- <RouterView />
564
- </SectionContainerShell>
565
- </template>
566
- `
567
- );
568
-
569
- const details = await resolvePageLinkTargetDetails({
570
- appRoot,
571
- targetFile: "admin/contacts/[contactId]/notes/index.vue",
572
- context: "page target"
573
- });
574
-
575
- assert.equal(details.parentHost?.id, "contact-view:sub-pages");
576
- assert.equal(details.placementTarget.id, "page.section-nav");
577
- assert.equal(details.placementTarget.owner, "contact-view");
578
- assert.equal(details.componentToken, "");
579
- assert.equal(details.linkTo, "./notes");
580
- });
581
- });
582
-
583
- test("resolvePageLinkTargetDetails honors explicit placement and link overrides", async () => {
584
- await withTempApp(async (appRoot) => {
585
- await writeConfig(
586
- appRoot,
587
- `export const config = {
588
- surfaceDefinitions: {
589
- admin: { id: "admin", pagesRoot: "admin", enabled: true }
590
- }
591
- };
592
- `
593
- );
594
- await writeShellLayout(appRoot);
595
-
596
- const details = await resolvePageLinkTargetDetails({
597
- appRoot,
598
- targetFile: "admin/contacts/[contactId]/index/notes/index.vue",
599
- placement: "shell.status",
600
- componentToken: "custom.link-item",
601
- linkTo: "./assistant-notes",
602
- context: "page target"
603
- });
604
-
605
- assert.equal(details.placementTarget.id, "shell.status");
606
- assert.equal(details.componentToken, "custom.link-item");
607
- assert.equal(details.linkTo, "./assistant-notes");
608
- });
609
- });
610
-
611
- test("resolvePageLinkTargetDetails inherits an index-route parent subpages host for index children", async () => {
612
- await withTempApp(async (appRoot) => {
613
- await writeConfig(
614
- appRoot,
615
- `export const config = {
616
- surfaceDefinitions: {
617
- admin: { id: "admin", pagesRoot: "admin", enabled: true }
618
- }
619
- };
620
- `
621
- );
622
- await writeShellLayout(appRoot);
623
- await writePlacementTopology(appRoot, [
624
- renderTopologyEntry({
625
- id: "page.section-nav",
626
- owner: "customer-view",
627
- surfaces: ["admin"],
628
- outlet: "customer-view:sub-pages",
629
- linkRenderer: "local.main.ui.surface-aware-menu-link-item"
630
- })
631
- ]);
632
- await writeFileInApp(
633
- appRoot,
634
- "src/pages/admin/customers/[customerId]/index.vue",
635
- `<template>
636
- <SectionContainerShell>
637
- <template #tabs>
638
- <ShellOutlet target="customer-view:sub-pages" />
639
- </template>
640
- <RouterView />
641
- </SectionContainerShell>
642
- </template>
643
- `
644
- );
645
-
646
- const details = await resolvePageLinkTargetDetails({
647
- appRoot,
648
- targetFile: "admin/customers/[customerId]/index/pets/index.vue",
649
- context: "page target"
650
- });
651
-
652
- assert.equal(details.parentHost?.id, "customer-view:sub-pages");
653
- assert.equal(details.parentHost?.pageFile, "src/pages/admin/customers/[customerId]/index.vue");
654
- assert.equal(details.placementTarget.id, "page.section-nav");
655
- assert.equal(details.placementTarget.owner, "customer-view");
656
- assert.equal(details.componentToken, "");
657
- assert.equal(details.linkTo, "./pets");
658
- });
659
- });