@jskit-ai/workspaces-web 0.1.139-oldline.2 → 0.1.140

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 (40) hide show
  1. package/package.json +247 -9
  2. package/patterns/workspace-surfaces/PATTERN.md +63 -0
  3. package/src/client/account-settings/accountSettingsInvitesRuntime.js +2 -2
  4. package/src/client/account-settings/useAccountSettingsInvitesSectionRuntime.js +3 -3
  5. package/src/client/components/MembersAdminClientElement.vue +1 -1
  6. package/src/client/components/WorkspaceInviteLanding.vue +4 -4
  7. package/src/client/components/WorkspaceMembersClientElement.vue +6 -6
  8. package/src/client/components/{UsersWorkspaceMembersMenuItem.vue → WorkspaceMembersMenuItem.vue} +2 -2
  9. package/src/client/components/{UsersWorkspacePermissionMenuItem.vue → WorkspacePermissionMenuItem.vue} +3 -3
  10. package/src/client/components/{UsersProfileSurfaceSwitchMenuItem.vue → WorkspaceProfileSurfaceSwitchMenuItem.vue} +1 -1
  11. package/src/client/components/{UsersWorkspaceSelector.vue → WorkspaceSelector.vue} +1 -1
  12. package/src/client/components/{UsersWorkspaceSettingsMenuItem.vue → WorkspaceSettingsMenuItem.vue} +2 -2
  13. package/src/client/components/WorkspacesClientElement.vue +13 -13
  14. package/src/client/composables/useWorkspaceRouteContext.js +1 -1
  15. package/src/client/index.js +0 -5
  16. package/src/client/lib/surfaceAccessPolicy.js +4 -1
  17. package/src/client/providers/WorkspacesWebClientProvider.js +54 -50
  18. package/src/client/runtime/bootstrapPlacementRuntime.js +18 -32
  19. package/src/client/runtime/bootstrapPlacementRuntimeHelpers.js +4 -4
  20. package/test/bootstrapPlacementRuntime.test.js +40 -125
  21. package/test/packageMetadataOwnership.test.js +19 -0
  22. package/test/settingsPlacementContract.test.js +87 -389
  23. package/test/{usersWorkspaceSelectorContract.test.js → workspaceSelectorContract.test.js} +3 -3
  24. package/package.descriptor.mjs +0 -492
  25. package/src/client/lib/httpClient.js +0 -10
  26. package/src/client/lib/permissions.js +0 -27
  27. package/test/packageDescriptorOwnership.test.js +0 -16
  28. /package/{templates → patterns/workspace-surfaces/example}/packages/main/src/client/components/AccountPendingInvitesCue.vue +0 -0
  29. /package/{templates → patterns/workspace-surfaces/example}/packages/main/src/client/components/AccountSettingsInvitesSection.vue +0 -0
  30. /package/{templates → patterns/workspace-surfaces/example}/src/components/WorkspaceNotFoundCard.vue +0 -0
  31. /package/{templates → patterns/workspace-surfaces/example}/src/composables/useWorkspaceNotFoundState.js +0 -0
  32. /package/{templates → patterns/workspace-surfaces/example}/src/pages/admin/members/index.vue +0 -0
  33. /package/{templates → patterns/workspace-surfaces/example}/src/pages/admin/workspace/settings/index.vue +0 -0
  34. /package/{templates → patterns/workspace-surfaces/example}/src/pages/admin/workspace/settings.vue +0 -0
  35. /package/{templates → patterns/workspace-surfaces/example}/src/pages/invite/[token].vue +0 -0
  36. /package/{templates → patterns/workspace-surfaces/example}/src/surfaces/admin/index.vue +0 -0
  37. /package/{templates → patterns/workspace-surfaces/example}/src/surfaces/admin/root.vue +0 -0
  38. /package/{templates → patterns/workspace-surfaces/example}/src/surfaces/app/index.vue +0 -0
  39. /package/{templates → patterns/workspace-surfaces/example}/src/surfaces/app/root.vue +0 -0
  40. /package/src/client/components/{UsersWorkspaceToolsWidget.vue → WorkspaceToolsWidget.vue} +0 -0
@@ -1,35 +1,48 @@
1
- import UsersProfileSurfaceSwitchMenuItem from "../components/UsersProfileSurfaceSwitchMenuItem.vue";
2
- import UsersWorkspaceSelector from "../components/UsersWorkspaceSelector.vue";
3
- import UsersWorkspaceToolsWidget from "../components/UsersWorkspaceToolsWidget.vue";
4
- import UsersWorkspaceSettingsMenuItem from "../components/UsersWorkspaceSettingsMenuItem.vue";
5
- import UsersWorkspaceMembersMenuItem from "../components/UsersWorkspaceMembersMenuItem.vue";
1
+ import WorkspaceProfileSurfaceSwitchMenuItem from "../components/WorkspaceProfileSurfaceSwitchMenuItem.vue";
2
+ import WorkspaceSelector from "../components/WorkspaceSelector.vue";
3
+ import WorkspaceToolsWidget from "../components/WorkspaceToolsWidget.vue";
4
+ import WorkspaceSettingsMenuItem from "../components/WorkspaceSettingsMenuItem.vue";
5
+ import WorkspaceMembersMenuItem from "../components/WorkspaceMembersMenuItem.vue";
6
6
  import MembersAdminClientElement from "../components/MembersAdminClientElement.vue";
7
- import { registerBootstrapPayloadHandler } from "@jskit-ai/shell-web/client/bootstrap";
8
7
  import { createBootstrapPlacementRuntime } from "../runtime/bootstrapPlacementRuntime.js";
9
8
  import {
10
9
  WORKSPACES_WEB_SCOPE_SUPPORT_INJECTION_KEY,
11
10
  createWorkspaceScopeSupport
12
11
  } from "../support/workspaceScopeSupport.js";
13
12
 
14
- class WorkspacesWebClientProvider {
15
- static id = "workspaces.web.client";
16
- static dependsOn = ["users.web.client"];
13
+ const WorkspacesWebClientProvider = defineProvider({
14
+ id: "workspaces.web.client",
15
+ requires: {
16
+ components: "client.components",
17
+ logger: "client.logger",
18
+ router: "client.router",
19
+ shell: "client.shell",
20
+ vueApp: "client.vue"
21
+ },
22
+ optional: {
23
+ realtime: "client.realtime"
24
+ },
25
+ provides: {
26
+ workspaces: "client.workspaces"
27
+ },
28
+ setup({ components, logger, realtime, router, shell, vueApp }) {
29
+ components.register("workspaces.web.profile.menu.surface-switch-item", WorkspaceProfileSurfaceSwitchMenuItem);
30
+ components.register("workspaces.web.workspace.selector", WorkspaceSelector);
31
+ components.register("workspaces.web.workspace.tools.widget", WorkspaceToolsWidget);
32
+ components.register("workspaces.web.workspace-settings.menu-item", WorkspaceSettingsMenuItem);
33
+ components.register("workspaces.web.workspace-members.menu-item", WorkspaceMembersMenuItem);
34
+ components.register("workspaces.web.members-admin.element", MembersAdminClientElement);
17
35
 
18
- register(app) {
19
- if (!app || typeof app.singleton !== "function" || typeof app.tag !== "function") {
20
- throw new Error("WorkspacesWebClientProvider requires application singleton()/tag().");
21
- }
22
-
23
- app.singleton("workspaces.web.profile.menu.surface-switch-item", () => UsersProfileSurfaceSwitchMenuItem);
24
- app.singleton("workspaces.web.workspace.selector", () => UsersWorkspaceSelector);
25
- app.singleton("workspaces.web.workspace.tools.widget", () => UsersWorkspaceToolsWidget);
26
- app.singleton("workspaces.web.workspace-settings.menu-item", () => UsersWorkspaceSettingsMenuItem);
27
- app.singleton("workspaces.web.workspace-members.menu-item", () => UsersWorkspaceMembersMenuItem);
28
- app.singleton("workspaces.web.members-admin.element", () => MembersAdminClientElement);
29
- app.singleton("workspaces.web.bootstrap-placement.runtime", (scope) => createBootstrapPlacementRuntime({ app: scope }));
30
- registerBootstrapPayloadHandler(app, "workspaces.web.bootstrap.handler", (scope) => {
31
- const runtime = scope.make("workspaces.web.bootstrap-placement.runtime");
32
- return Object.freeze({
36
+ const runtime = createBootstrapPlacementRuntime({
37
+ bootstrapRuntime: shell.bootstrap,
38
+ placementRuntime: shell.placement,
39
+ realtime,
40
+ router,
41
+ vueApp,
42
+ logger
43
+ });
44
+ shell.bootstrapHandlers.register(
45
+ Object.freeze({
33
46
  handlerId: "workspaces.web.bootstrap",
34
47
  order: 100,
35
48
  resolveBootstrapRequest(input = {}) {
@@ -41,46 +54,37 @@ class WorkspacesWebClientProvider {
41
54
  handleBootstrapError(input = {}) {
42
55
  return runtime.handleBootstrapError(input);
43
56
  }
44
- });
45
- });
46
- app.singleton("workspaces.web.scope-support", () => createWorkspaceScopeSupport());
47
- }
48
-
49
- async boot(app) {
50
- if (!app || typeof app.make !== "function") {
51
- throw new Error("WorkspacesWebClientProvider boot requires application make().");
52
- }
53
-
54
- const runtime = app.make("workspaces.web.bootstrap-placement.runtime");
57
+ })
58
+ );
59
+ return {
60
+ workspaces: Object.freeze({
61
+ bootstrap: runtime,
62
+ scopeSupport: createWorkspaceScopeSupport()
63
+ })
64
+ };
65
+ },
66
+ async boot({ vueApp }, { outputs }) {
67
+ const runtime = outputs.workspaces.bootstrap;
55
68
  if (runtime && typeof runtime.initialize === "function") {
56
69
  await runtime.initialize();
57
70
  }
58
71
 
59
- if (!app.has("jskit.client.vue.app")) {
60
- return;
61
- }
62
-
63
- const vueApp = app.make("jskit.client.vue.app");
64
72
  if (!vueApp || typeof vueApp.provide !== "function") {
65
73
  return;
66
74
  }
67
75
 
68
76
  vueApp.provide(
69
77
  WORKSPACES_WEB_SCOPE_SUPPORT_INJECTION_KEY,
70
- app.make("workspaces.web.scope-support")
78
+ outputs.workspaces.scopeSupport
71
79
  );
72
- }
73
-
74
- shutdown(app) {
75
- if (!app || typeof app.make !== "function") {
76
- return;
77
- }
78
-
79
- const runtime = app.make("workspaces.web.bootstrap-placement.runtime");
80
+ },
81
+ shutdown(_dependencies, { outputs }) {
82
+ const runtime = outputs.workspaces.bootstrap;
80
83
  if (runtime && typeof runtime.shutdown === "function") {
81
84
  runtime.shutdown();
82
85
  }
83
86
  }
84
- }
87
+ });
85
88
 
86
89
  export { WorkspacesWebClientProvider };
90
+ import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
@@ -2,7 +2,7 @@ import {
2
2
  findWorkspaceBySlug,
3
3
  normalizeWorkspaceList
4
4
  } from "../lib/bootstrap.js";
5
- import { normalizePermissionList } from "../lib/permissions.js";
5
+ import { normalizePermissionList } from "@jskit-ai/http-web/client/lib/permissions";
6
6
  import {
7
7
  persistBootstrapThemePreference,
8
8
  resolveBootstrapThemeName,
@@ -28,21 +28,24 @@ import {
28
28
  } from "./bootstrapPlacementRuntimeHelpers.js";
29
29
  import { resolveErrorStatusCode } from "../support/runtimeNormalization.js";
30
30
 
31
- function createBootstrapPlacementRuntime({ app, logger = null } = {}) {
32
- if (!app || typeof app.has !== "function" || typeof app.make !== "function") {
33
- throw new Error("createBootstrapPlacementRuntime requires application has()/make().");
34
- }
35
- if (!app.has("runtime.web-placement.client")) {
31
+ function createBootstrapPlacementRuntime({
32
+ bootstrapRuntime,
33
+ placementRuntime,
34
+ realtime = null,
35
+ router = null,
36
+ vueApp = null,
37
+ logger = null
38
+ } = {}) {
39
+ if (!placementRuntime || typeof placementRuntime.getContext !== "function") {
36
40
  throw new Error("createBootstrapPlacementRuntime requires shell-web placement runtime.");
37
41
  }
42
+ if (!bootstrapRuntime || typeof bootstrapRuntime.refresh !== "function") {
43
+ throw new Error("createBootstrapPlacementRuntime requires shell-web bootstrap runtime.");
44
+ }
38
45
 
39
- const runtimeLogger = logger || createProviderLogger(app);
40
- const placementRuntime = app.make("runtime.web-placement.client");
41
- const router = app.has("jskit.client.router") ? app.make("jskit.client.router") : null;
42
- let vuetifyThemeController = resolveVuetifyThemeController(
43
- app.has("jskit.client.vue.app") ? app.make("jskit.client.vue.app") : null
44
- );
45
- const socket = app.has("runtime.realtime.client.socket") ? app.make("runtime.realtime.client.socket") : null;
46
+ const runtimeLogger = createProviderLogger(logger);
47
+ let vuetifyThemeController = resolveVuetifyThemeController(vueApp);
48
+ const socket = realtime?.socket || null;
46
49
  const cleanup = [];
47
50
  let shutdownRequested = false;
48
51
  let lastRouteWorkspaceSlug = resolveRouteState(placementRuntime, router).workspaceSlug;
@@ -181,11 +184,7 @@ function createBootstrapPlacementRuntime({ app, logger = null } = {}) {
181
184
  if (vuetifyThemeController) {
182
185
  return vuetifyThemeController;
183
186
  }
184
- if (!app.has("jskit.client.vue.app")) {
185
- return null;
186
- }
187
-
188
- vuetifyThemeController = resolveVuetifyThemeController(app.make("jskit.client.vue.app"));
187
+ vuetifyThemeController = resolveVuetifyThemeController(vueApp);
189
188
  return vuetifyThemeController;
190
189
  }
191
190
 
@@ -386,25 +385,12 @@ function createBootstrapPlacementRuntime({ app, logger = null } = {}) {
386
385
  return null;
387
386
  }
388
387
 
389
- function resolveBootstrapRuntime() {
390
- if (!app.has("runtime.web-bootstrap.client")) {
391
- throw new Error("createBootstrapPlacementRuntime requires shell-web bootstrap runtime.");
392
- }
393
-
394
- const bootstrapRuntime = app.make("runtime.web-bootstrap.client");
395
- if (!bootstrapRuntime || typeof bootstrapRuntime.refresh !== "function") {
396
- throw new Error("createBootstrapPlacementRuntime requires runtime.web-bootstrap.client.refresh().");
397
- }
398
-
399
- return bootstrapRuntime;
400
- }
401
-
402
388
  function refresh(reason = "manual") {
403
389
  if (shutdownRequested) {
404
390
  return Promise.resolve(null);
405
391
  }
406
392
 
407
- return resolveBootstrapRuntime().refresh(reason);
393
+ return bootstrapRuntime.refresh(reason);
408
394
  }
409
395
 
410
396
  function enforceCurrentWorkspaceStatus() {
@@ -7,15 +7,15 @@ import {
7
7
  } from "@jskit-ai/shell-web/client/placement";
8
8
  import { parseWorkspacePathname } from "@jskit-ai/workspaces-core/shared/support/workspacePathModel";
9
9
  import { extractWorkspaceSlugFromSurfacePathname } from "../lib/workspaceSurfacePaths.js";
10
- import { workspacesWebHttpClient } from "../lib/httpClient.js";
10
+ import { httpWebClient } from "@jskit-ai/http-web/client/lib/httpClient";
11
11
  import { buildBootstrapApiPath } from "../lib/bootstrap.js";
12
12
  import {
13
13
  normalizeWorkspaceBootstrapStatusValue
14
14
  } from "../support/runtimeNormalization.js";
15
15
  import { WORKSPACE_BOOTSTRAP_STATUSES } from "./bootstrapPlacementRuntimeConstants.js";
16
16
 
17
- function createProviderLogger(app) {
18
- return createSharedProviderLogger(app);
17
+ function createProviderLogger(logger) {
18
+ return createSharedProviderLogger(logger);
19
19
  }
20
20
 
21
21
  function resolveRouteState(placementRuntime, router) {
@@ -127,7 +127,7 @@ function countPendingInvites(entries = []) {
127
127
  }
128
128
 
129
129
  async function fetchBootstrapPayload(workspaceSlug = "") {
130
- return workspacesWebHttpClient.request(buildBootstrapApiPath(workspaceSlug), {
130
+ return httpWebClient.request(buildBootstrapApiPath(workspaceSlug), {
131
131
  method: "GET"
132
132
  });
133
133
  }
@@ -183,20 +183,20 @@ function createBootstrapRuntimeStub() {
183
183
  };
184
184
  }
185
185
 
186
- function createAppStub(records = {}) {
187
- const registry = new Map();
188
- for (const key of Reflect.ownKeys(records)) {
189
- registry.set(key, records[key]);
190
- }
191
- return {
192
- has(token) {
193
- return registry.has(token);
194
- },
195
- make(token) {
196
- return registry.get(token);
197
- },
198
- warn() {}
199
- };
186
+ function createTestRuntime({
187
+ placementRuntime,
188
+ router,
189
+ bootstrapRuntime = createBootstrapRuntimeStub(),
190
+ socket = null,
191
+ vueApp = null
192
+ } = {}) {
193
+ return createBootstrapPlacementRuntime({
194
+ placementRuntime,
195
+ bootstrapRuntime,
196
+ realtime: socket ? { socket } : null,
197
+ router,
198
+ vueApp
199
+ });
200
200
  }
201
201
 
202
202
  function createVuetifyThemeController(initial = "light") {
@@ -265,13 +265,7 @@ function createErrorWithStatus(status, message = "") {
265
265
  test("bootstrap placement runtime contributes workspace slug to shared bootstrap request and writes payload into placement context", async () => {
266
266
  const placementRuntime = createPlacementRuntimeStub();
267
267
  const router = createRouterStub("/w/acme/dashboard");
268
- const runtime = createBootstrapPlacementRuntime({
269
- app: createAppStub({
270
- ["runtime.web-placement.client"]: placementRuntime,
271
- ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
272
- ["jskit.client.router"]: router
273
- })
274
- });
268
+ const runtime = createTestRuntime({ placementRuntime, router });
275
269
 
276
270
  await runtime.initialize();
277
271
  const request = runtime.resolveBootstrapRequest();
@@ -329,13 +323,7 @@ test("bootstrap placement runtime resolves workspace slug from pathname when sur
329
323
  const placementRuntime = createPlacementRuntimeStub();
330
324
  placementRuntime.setContext({}, { replace: true, source: "test.clear" });
331
325
  const router = createRouterStub("/w/acme/admin");
332
- const runtime = createBootstrapPlacementRuntime({
333
- app: createAppStub({
334
- ["runtime.web-placement.client"]: placementRuntime,
335
- ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
336
- ["jskit.client.router"]: router
337
- })
338
- });
326
+ const runtime = createTestRuntime({ placementRuntime, router });
339
327
 
340
328
  await runtime.initialize();
341
329
  const request = runtime.resolveBootstrapRequest();
@@ -378,13 +366,7 @@ test("bootstrap placement runtime does not mutate placement auth context", async
378
366
  { source: "test.seed" }
379
367
  );
380
368
  const router = createRouterStub("/w/acme/dashboard");
381
- const runtime = createBootstrapPlacementRuntime({
382
- app: createAppStub({
383
- ["runtime.web-placement.client"]: placementRuntime,
384
- ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
385
- ["jskit.client.router"]: router
386
- })
387
- });
369
+ const runtime = createTestRuntime({ placementRuntime, router });
388
370
 
389
371
  await runtime.initialize();
390
372
  await runtime.applyBootstrapPayload({
@@ -418,14 +400,7 @@ test("bootstrap placement runtime delegates route and realtime refreshes to the
418
400
  const router = createRouterStub("/w/acme/dashboard");
419
401
  const socket = createSocketStub();
420
402
  const bootstrapRuntime = createBootstrapRuntimeStub();
421
- const runtime = createBootstrapPlacementRuntime({
422
- app: createAppStub({
423
- ["runtime.web-placement.client"]: placementRuntime,
424
- ["runtime.web-bootstrap.client"]: bootstrapRuntime,
425
- ["jskit.client.router"]: router,
426
- ["runtime.realtime.client.socket"]: socket
427
- })
428
- });
403
+ const runtime = createTestRuntime({ placementRuntime, bootstrapRuntime, router, socket });
429
404
 
430
405
  await runtime.initialize();
431
406
  assert.deepEqual(bootstrapRuntime.calls, []);
@@ -451,13 +426,10 @@ test("bootstrap placement runtime applies theme changes from bootstrap payloads"
451
426
  const placementRuntime = createPlacementRuntimeStub();
452
427
  const router = createRouterStub("/w/acme/dashboard");
453
428
  const themeController = createVuetifyThemeController("light");
454
- const runtime = createBootstrapPlacementRuntime({
455
- app: createAppStub({
456
- ["runtime.web-placement.client"]: placementRuntime,
457
- ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
458
- ["jskit.client.router"]: router,
459
- ["jskit.client.vue.app"]: createVueAppWithThemeController(themeController)
460
- })
429
+ const runtime = createTestRuntime({
430
+ placementRuntime,
431
+ router,
432
+ vueApp: createVueAppWithThemeController(themeController)
461
433
  });
462
434
 
463
435
  await runtime.initialize();
@@ -499,13 +471,10 @@ test("bootstrap placement runtime applies workspace palette and clears it when l
499
471
  const placementRuntime = createPlacementRuntimeStub();
500
472
  const router = createRouterStub("/w/acme/dashboard");
501
473
  const themeController = createVuetifyThemeController("light");
502
- const runtime = createBootstrapPlacementRuntime({
503
- app: createAppStub({
504
- ["runtime.web-placement.client"]: placementRuntime,
505
- ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
506
- ["jskit.client.router"]: router,
507
- ["jskit.client.vue.app"]: createVueAppWithThemeController(themeController)
508
- })
474
+ const runtime = createTestRuntime({
475
+ placementRuntime,
476
+ router,
477
+ vueApp: createVueAppWithThemeController(themeController)
509
478
  });
510
479
 
511
480
  await runtime.initialize();
@@ -575,12 +544,9 @@ test("bootstrap placement runtime marks workspace slug as not_found and clears w
575
544
  },
576
545
  { source: "test.seed" }
577
546
  );
578
- const runtime = createBootstrapPlacementRuntime({
579
- app: createAppStub({
580
- ["runtime.web-placement.client"]: placementRuntime,
581
- ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
582
- ["jskit.client.router"]: createRouterStub("/w/acme/dashboard")
583
- })
547
+ const runtime = createTestRuntime({
548
+ placementRuntime,
549
+ router: createRouterStub("/w/acme/dashboard")
584
550
  });
585
551
 
586
552
  await runtime.initialize();
@@ -606,13 +572,7 @@ test("bootstrap placement runtime tracks status per workspace slug across route
606
572
  const placementRuntime = createPlacementRuntimeStub();
607
573
  const router = createRouterStub("/w/acme/dashboard");
608
574
  const bootstrapRuntime = createBootstrapRuntimeStub();
609
- const runtime = createBootstrapPlacementRuntime({
610
- app: createAppStub({
611
- ["runtime.web-placement.client"]: placementRuntime,
612
- ["runtime.web-bootstrap.client"]: bootstrapRuntime,
613
- ["jskit.client.router"]: router
614
- })
615
- });
575
+ const runtime = createTestRuntime({ placementRuntime, bootstrapRuntime, router });
616
576
 
617
577
  await runtime.initialize();
618
578
  await runtime.applyBootstrapPayload({
@@ -651,13 +611,7 @@ test("bootstrap placement runtime tracks status per workspace slug across route
651
611
  test("bootstrap placement runtime uses requestedWorkspace status and keeps global workspace list on inaccessible slug", async () => {
652
612
  const placementRuntime = createPlacementRuntimeStub();
653
613
  const router = createRouterStub("/w/tonymobily");
654
- const runtime = createBootstrapPlacementRuntime({
655
- app: createAppStub({
656
- ["runtime.web-placement.client"]: placementRuntime,
657
- ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
658
- ["jskit.client.router"]: router
659
- })
660
- });
614
+ const runtime = createTestRuntime({ placementRuntime, router });
661
615
 
662
616
  await runtime.initialize();
663
617
  await runtime.applyBootstrapPayload({
@@ -695,13 +649,7 @@ test("bootstrap placement runtime uses requestedWorkspace status and keeps globa
695
649
  test("bootstrap placement runtime uses requestedWorkspace=not_found without forcing forbidden fallback", async () => {
696
650
  const placementRuntime = createPlacementRuntimeStub();
697
651
  const router = createRouterStub("/w/missing");
698
- const runtime = createBootstrapPlacementRuntime({
699
- app: createAppStub({
700
- ["runtime.web-placement.client"]: placementRuntime,
701
- ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
702
- ["jskit.client.router"]: router
703
- })
704
- });
652
+ const runtime = createTestRuntime({ placementRuntime, router });
705
653
 
706
654
  await runtime.initialize();
707
655
  await runtime.applyBootstrapPayload({
@@ -745,13 +693,7 @@ test("bootstrap placement runtime guard wrapper preserves delegated deny outcome
745
693
  });
746
694
  globalThis[SHELL_GUARD_EVALUATOR_KEY] = () => delegatedOutcome;
747
695
 
748
- const runtime = createBootstrapPlacementRuntime({
749
- app: createAppStub({
750
- ["runtime.web-placement.client"]: placementRuntime,
751
- ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
752
- ["jskit.client.router"]: router
753
- })
754
- });
696
+ const runtime = createTestRuntime({ placementRuntime, router });
755
697
 
756
698
  await runtime.initialize();
757
699
  await runtime.applyBootstrapPayload({
@@ -789,13 +731,7 @@ test("bootstrap placement runtime guard wrapper preserves delegated deny outcome
789
731
  test("bootstrap placement runtime guard wrapper blocks forbidden workspace routes and redirects nested workspace paths", async () => {
790
732
  const placementRuntime = createPlacementRuntimeStub();
791
733
  const router = createRouterStub("/w/acme/admin/workspace/settings?tab=general");
792
- const runtime = createBootstrapPlacementRuntime({
793
- app: createAppStub({
794
- ["runtime.web-placement.client"]: placementRuntime,
795
- ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
796
- ["jskit.client.router"]: router
797
- })
798
- });
734
+ const runtime = createTestRuntime({ placementRuntime, router });
799
735
 
800
736
  await runtime.initialize();
801
737
  await runtime.handleBootstrapError({
@@ -831,13 +767,7 @@ test("bootstrap placement runtime guard wrapper blocks forbidden workspace route
831
767
  test("bootstrap placement runtime guard wrapper redirects nested not_found routes to workspace surface root", async () => {
832
768
  const placementRuntime = createPlacementRuntimeStub();
833
769
  const router = createRouterStub("/w/acme/projects");
834
- const runtime = createBootstrapPlacementRuntime({
835
- app: createAppStub({
836
- ["runtime.web-placement.client"]: placementRuntime,
837
- ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
838
- ["jskit.client.router"]: router
839
- })
840
- });
770
+ const runtime = createTestRuntime({ placementRuntime, router });
841
771
 
842
772
  await runtime.initialize();
843
773
  await runtime.handleBootstrapError({
@@ -891,13 +821,7 @@ test("bootstrap placement runtime guard wrapper redirects nested not_found route
891
821
  test("bootstrap placement runtime redirects admin nested route to admin root when workspace is not_found", async () => {
892
822
  const placementRuntime = createPlacementRuntimeStub();
893
823
  const router = createRouterStub("/w/acme/admin/workspace/settings");
894
- const runtime = createBootstrapPlacementRuntime({
895
- app: createAppStub({
896
- ["runtime.web-placement.client"]: placementRuntime,
897
- ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
898
- ["jskit.client.router"]: router
899
- })
900
- });
824
+ const runtime = createTestRuntime({ placementRuntime, router });
901
825
 
902
826
  await runtime.initialize();
903
827
  await runtime.handleBootstrapError({
@@ -950,13 +874,7 @@ test("bootstrap placement runtime enforces surface access policies after bootstr
950
874
  }
951
875
  });
952
876
  const router = createRouterStub("/ops");
953
- const runtime = createBootstrapPlacementRuntime({
954
- app: createAppStub({
955
- ["runtime.web-placement.client"]: placementRuntime,
956
- ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
957
- ["jskit.client.router"]: router
958
- })
959
- });
877
+ const runtime = createTestRuntime({ placementRuntime, router });
960
878
 
961
879
  await runtime.initialize();
962
880
  await runtime.applyBootstrapPayload({
@@ -976,12 +894,9 @@ test("bootstrap placement runtime enforces surface access policies after bootstr
976
894
 
977
895
  test("bootstrap placement runtime handles unauthenticated errors and marks workspace status", async () => {
978
896
  const placementRuntime = createPlacementRuntimeStub();
979
- const runtime = createBootstrapPlacementRuntime({
980
- app: createAppStub({
981
- ["runtime.web-placement.client"]: placementRuntime,
982
- ["runtime.web-bootstrap.client"]: createBootstrapRuntimeStub(),
983
- ["jskit.client.router"]: createRouterStub("/w/acme/dashboard")
984
- })
897
+ const runtime = createTestRuntime({
898
+ placementRuntime,
899
+ router: createRouterStub("/w/acme/dashboard")
985
900
  });
986
901
 
987
902
  await runtime.initialize();
@@ -0,0 +1,19 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import test from "node:test";
4
+ import { readFile } from "node:fs/promises";
5
+ import { fileURLToPath } from "node:url";
6
+ import packageJson from "../package.json" with { type: "json" };
7
+
8
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+
10
+ test("workspaces-web publishes editable workspace surfaces as an agent-readable pattern", async () => {
11
+ assert.equal(packageJson.jskit?.mutations, undefined);
12
+ const pattern = await readFile(path.join(packageRoot, "patterns", "workspace-surfaces", "PATTERN.md"), "utf8");
13
+
14
+ assert.match(pattern, /## Product decisions/);
15
+ assert.match(pattern, /## Invariants/);
16
+ assert.match(pattern, /Loading uses skeletons/);
17
+ assert.match(pattern, /mutation failures use toasts/);
18
+ assert.match(pattern, /## Example files/);
19
+ });