@jskit-ai/kernel 0.1.245 → 0.1.246

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 (101) 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 +14 -58
  15. package/package.json +2 -4
  16. package/server/actions/actionCatalogue.js +13 -34
  17. package/{shared → server}/actions/actionContributorHelpers.js +0 -5
  18. package/{shared → server}/actions/actionDefinitions.js +24 -131
  19. package/{shared → server}/actions/actionDefinitions.test.js +75 -21
  20. package/server/actions/actionEvents.js +97 -2
  21. package/server/actions/actionEvents.test.js +79 -0
  22. package/{shared → server}/actions/executionContext.js +3 -13
  23. package/server/actions/idempotency.js +24 -0
  24. package/server/actions/pipeline.js +87 -0
  25. package/{shared → server}/actions/policies.js +20 -33
  26. package/{shared → server}/actions/policies.test.js +6 -6
  27. package/{shared → server}/actions/registry.js +3 -22
  28. package/{shared → server}/actions/registry.test.js +70 -32
  29. package/server/actions/requestMeta.js +17 -0
  30. package/server/http/capabilityHttpRuntime.js +1 -1
  31. package/server/http/capabilityHttpRuntime.test.js +56 -0
  32. package/server/http/errorHandling.js +291 -0
  33. package/server/{runtime/fastifyBootstrap.test.js → http/errorHandling.test.js} +72 -120
  34. package/server/http/lib/routeRegistration.js +36 -150
  35. package/server/http/lib/routeTransport.js +2 -0
  36. package/server/http/lib/routeValidator.js +5 -47
  37. package/server/http/lib/routeValidator.test.js +15 -17
  38. package/server/http/lib/router.js +10 -94
  39. package/server/platform/providerRuntime/capabilityProviderLoader.js +2 -1
  40. package/server/platform/providerRuntime/helpers.js +1 -34
  41. package/server/runtime/integers.js +1 -5
  42. package/server/runtime/serviceAuthorization.js +4 -9
  43. package/server/support/appConfig.js +2 -1
  44. package/server/support/appConfigFiles.js +1 -38
  45. package/server/support/appConfigFiles.test.js +3 -39
  46. package/server/support/index.js +1 -20
  47. package/shared/capabilities/defineProvider.js +1 -1
  48. package/shared/capabilities/index.js +1 -1
  49. package/shared/index.js +1 -1
  50. package/shared/support/index.js +1 -4
  51. package/shared/support/mobileConfig.js +99 -0
  52. package/shared/support/mobileConfig.test.js +113 -0
  53. package/shared/support/normalize.js +0 -108
  54. package/shared/support/normalize.test.js +0 -134
  55. package/shared/{actions/actionContributorHelpers.test.js → support/permissions.test.js} +1 -1
  56. package/shared/support/returnToPath.js +1 -20
  57. package/shared/support/returnToPath.test.js +1 -36
  58. package/shared/support/shellLayoutTargets.js +2 -140
  59. package/shared/support/shellLayoutTargets.test.js +1 -110
  60. package/shared/support/sorting.js +1 -9
  61. package/shared/surface/apiPaths.js +1 -82
  62. package/shared/surface/paths.js +38 -171
  63. package/shared/surface/registry.js +2 -1
  64. package/shared/surface/runtime.js +18 -109
  65. package/shared/surface/runtime.test.js +57 -0
  66. package/shared/validators/index.js +1 -4
  67. package/shared/validators/jsonRestSchemaSupport.js +0 -9
  68. package/shared/validators/recordIdParamsValidator.js +0 -6
  69. package/shared/validators/schemaDefinitions.js +4 -30
  70. package/shared/validators/schemaPayloadValidation.js +2 -6
  71. package/test/barrelExposure.test.js +3 -7
  72. package/test/exportsContract.test.js +1 -3
  73. package/client/mobileLaunchRouting.js +0 -231
  74. package/client/mobileLaunchRouting.test.js +0 -230
  75. package/server/http/lib/controller.js +0 -183
  76. package/server/http/lib/controller.test.js +0 -143
  77. package/server/http/lib/index.js +0 -5
  78. package/server/runtime/canonicalJson.js +0 -74
  79. package/server/runtime/entityChangeEvents.js +0 -307
  80. package/server/runtime/entityChangeEvents.test.js +0 -274
  81. package/server/runtime/fastifyBootstrap.js +0 -553
  82. package/server/runtime/moduleConfig.js +0 -223
  83. package/server/runtime/moduleConfig.test.js +0 -165
  84. package/server/runtime/pagination.js +0 -13
  85. package/server/runtime/realtimeNormalization.js +0 -21
  86. package/server/runtime/routeUtils.js +0 -20
  87. package/server/runtime/securityAudit.js +0 -269
  88. package/server/runtime/securityAudit.test.js +0 -41
  89. package/server/support/pageTargets.js +0 -772
  90. package/server/support/pageTargets.test.js +0 -659
  91. package/server/support/shellOutlets.js +0 -576
  92. package/server/support/shellOutlets.test.js +0 -336
  93. package/server/surface/index.js +0 -18
  94. package/shared/actions/audit.js +0 -7
  95. package/shared/actions/idempotency.js +0 -62
  96. package/shared/actions/observability.js +0 -10
  97. package/shared/actions/pipeline.js +0 -309
  98. package/shared/actions/requestMeta.js +0 -36
  99. package/shared/actions/textNormalization.js +0 -3
  100. package/shared/actions/withActionDefaults.js +0 -34
  101. /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
- });