@jskit-ai/workspaces-core 0.2.4 → 0.2.6

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 (75) hide show
  1. package/locales/en.json +2 -2
  2. package/package.json +38 -11
  3. package/patterns/workspace-server/PATTERN.md +42 -17
  4. package/patterns/workspace-server/example/packages/main/src/server/WorkspaceInvitationsFeature.js +27 -0
  5. package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteEmail.js +11 -10
  6. package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteMailer.js +27 -0
  7. package/src/server/WorkspacesFeature.js +74 -183
  8. package/src/server/WorkspacesIntegrationsProvider.js +26 -10
  9. package/src/server/common/contributors/workspaceActionContextContributor.js +60 -70
  10. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +12 -27
  11. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +48 -63
  12. package/src/server/common/formatters/workspaceFormatter.js +12 -10
  13. package/src/server/common/repositories/workspaceInvitesRepository.js +74 -75
  14. package/src/server/common/repositories/workspaceMembershipsRepository.js +23 -45
  15. package/src/server/common/repositories/workspacesRepository.js +17 -25
  16. package/src/server/common/services/{workspaceContextService.js → workspaceDirectoryService.js} +52 -72
  17. package/src/server/common/support/realtimeServiceEvents.js +20 -12
  18. package/src/server/common/support/workspaceRecordId.js +21 -0
  19. package/src/server/common/validators/routeParamsValidator.js +3 -2
  20. package/src/server/i18n.js +5 -2
  21. package/src/server/support/resolveWorkspace.js +10 -7
  22. package/src/server/support/workspaceActionSurfaces.js +5 -41
  23. package/src/server/support/workspaceCrud.js +25 -0
  24. package/src/server/support/workspaceInvitationsPolicy.js +16 -27
  25. package/src/server/support/workspaceRouteInput.js +19 -2
  26. package/src/server/{common/support → support}/workspaceRoutePaths.js +8 -2
  27. package/src/server/types.js +47 -0
  28. package/src/server/workspaceBootstrapContributor.js +105 -142
  29. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +105 -137
  30. package/src/server/workspaceDirectory/{bootWorkspaceDirectoryRoutes.js → workspaceDirectoryRoutes.js} +7 -19
  31. package/src/server/workspaceMembers/defaultWorkspaceInviteEmail.js +15 -29
  32. package/src/server/workspaceMembers/unconfiguredWorkspaceInviteMailer.js +25 -0
  33. package/src/server/workspaceMembers/workspaceInviteUrls.js +24 -23
  34. package/src/server/workspaceMembers/workspaceMembersActions.js +189 -219
  35. package/src/server/workspaceMembers/{bootWorkspaceMembers.js → workspaceMembersRoutes.js} +14 -26
  36. package/src/server/workspaceMembers/workspaceMembersService.js +110 -97
  37. package/src/server/workspacePendingInvitations/noInviteAcceptanceParticipant.js +8 -0
  38. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +79 -112
  39. package/src/server/workspacePendingInvitations/{bootWorkspacePendingInvitations.js → workspacePendingInvitationsRoutes.js} +11 -8
  40. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +54 -57
  41. package/src/server/workspaceSettings/workspaceSettingsActions.js +53 -73
  42. package/src/server/workspaceSettings/workspaceSettingsRepository.js +20 -20
  43. package/src/server/workspaceSettings/{bootWorkspaceSettings.js → workspaceSettingsRoutes.js} +9 -21
  44. package/src/server/workspaceSettings/workspaceSettingsService.js +24 -26
  45. package/src/server/workspacesRuntime.js +185 -0
  46. package/src/shared/operationMessages.js +6 -15
  47. package/src/shared/resources/workspaceMembersResource.js +2 -2
  48. package/src/shared/resources/workspacePendingInvitationsResource.js +2 -2
  49. package/src/shared/resources/workspaceResource.js +4 -1
  50. package/src/shared/roles.js +15 -12
  51. package/src/shared/tenancyProfile.js +2 -2
  52. package/test/exportsContract.test.js +5 -0
  53. package/test/featureRuntime.test.js +5 -6
  54. package/test/packageMetadata.test.js +12 -0
  55. package/test/repositories.integration.test.js +1 -1
  56. package/test/requestedWorkspaceScope.test.js +24 -0
  57. package/test/roleLabels.test.js +29 -7
  58. package/test/support/invitationTransactions.js +35 -19
  59. package/test/workspaceActionSurfaces.test.js +0 -55
  60. package/test/workspaceAuthPolicyContextResolver.test.js +1 -1
  61. package/test/workspaceBootstrapContributor.test.js +7 -6
  62. package/test/workspaceCrud.test.js +23 -0
  63. package/test/workspaceInvitesRepository.test.js +9 -8
  64. package/test/workspaceMembersService.test.js +131 -28
  65. package/test/workspacePendingInvitationsResource.test.js +2 -2
  66. package/test/workspacePendingInvitationsService.test.js +69 -22
  67. package/test/workspaceService.test.js +58 -47
  68. package/test/workspaceSettingsActions.test.js +14 -7
  69. package/test/workspaceSettingsRepository.test.js +11 -0
  70. package/test/workspaceSettingsService.test.js +11 -9
  71. package/test/workspacesRepository.test.js +16 -16
  72. package/test/workspacesRouteRequestInputValidator.test.js +4 -4
  73. package/src/server/common/repositories/repositoryUtils.js +0 -43
  74. package/src/server/common/support/deepFreeze.js +0 -1
  75. package/src/server/common/support/resolveActionUser.js +0 -9
@@ -11,8 +11,13 @@ import { createRepository as invitesRepository } from "../../src/server/common/r
11
11
  import { createRepository as membershipsRepository } from "../../src/server/common/repositories/workspaceMembershipsRepository.js";
12
12
  import { createRepository as workspacesRepository } from "../../src/server/common/repositories/workspacesRepository.js";
13
13
  import { createService as membersService } from "../../src/server/workspaceMembers/workspaceMembersService.js";
14
+ import { createWorkspacesServerI18n } from "../../src/server/i18n.js";
14
15
  import { createService as pendingService } from "../../src/server/workspacePendingInvitations/workspacePendingInvitationsService.js";
15
- import { buildWorkspacePendingInvitationsActions } from "../../src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js";
16
+ import { noInviteAcceptanceParticipant } from "../../src/server/workspacePendingInvitations/noInviteAcceptanceParticipant.js";
17
+ import { createWorkspacePendingInvitationsActions } from "../../src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js";
18
+ import { createWorkspaceInviteUrlBuilder } from "../../src/server/workspaceMembers/workspaceInviteUrls.js";
19
+ import { renderDefaultWorkspaceInviteEmail } from "../../src/server/workspaceMembers/defaultWorkspaceInviteEmail.js";
20
+ import { unconfiguredWorkspaceInviteMailer } from "../../src/server/workspaceMembers/unconfiguredWorkspaceInviteMailer.js";
16
21
 
17
22
  // The caller verifies an empty disposable database and runs package migrations.
18
23
  // These application fixtures demonstrate composition, not production policy.
@@ -77,6 +82,8 @@ export async function verifyInvitationTransactions(t, db) {
77
82
  const events = [];
78
83
  let failDelivery = false;
79
84
  const members = membersService({
85
+ workspaceInvitationsEnabled: true,
86
+ i18n: createWorkspacesServerI18n(),
80
87
  workspaceInvitesRepository: inviteRepository, workspaceMembershipsRepository: memberships,
81
88
  inviteExpiresInMs: 3600000, roleCatalog,
82
89
  workspaceInviteMailer: mailer ? { async sendWorkspaceInvite(message, options) {
@@ -89,13 +96,20 @@ export async function verifyInvitationTransactions(t, db) {
89
96
  if (failDelivery) throw new Error(`Controlled mail failure containing secret ${message.inviteUrl}`);
90
97
  delivered.push(String(saved.id));
91
98
  return { status: "sent", providerMessageId: "controlled-message" };
92
- } } : null
99
+ } } : unconfiguredWorkspaceInviteMailer,
100
+ inviteUrlBuilder: createWorkspaceInviteUrlBuilder({ appConfig: {}, env: {} }),
101
+ workspaceInviteEmailTemplate: renderDefaultWorkspaceInviteEmail
102
+ });
103
+ // Each test chooses the application's acceptance participant.
104
+ let participant = noInviteAcceptanceParticipant;
105
+ const pending = pendingService({
106
+ workspaceInvitesRepository: inviteRepository, workspaceMembershipsRepository: memberships, roleCatalog: createWorkspaceRoleCatalog(),
107
+ acceptanceParticipant: (...args) => participant(...args)
93
108
  });
94
- const pending = pendingService({ workspaceInvitesRepository: inviteRepository, workspaceMembershipsRepository: memberships });
95
109
  const options = { context: { actor: user, channel: "api", permissions: ["workspace.members.invite"] } };
96
110
  const payload = { email: user.email, roleSid: "member" };
97
111
  const actions = createActionCatalogue({ events: { async publish(event) { events.push(event); } } });
98
- actions.register({ contributorId: suffix, domain: "workspace", actions: buildWorkspacePendingInvitationsActions({ workspacePendingInvitationsService: pending }) });
112
+ actions.register({ contributorId: suffix, domain: "workspace", actions: createWorkspacePendingInvitationsActions({ workspacePendingInvitationsService: pending }) });
99
113
  const redeem = (prepared, decision = "accept", actor = user, extraInput = {}) => actions.execute({
100
114
  actionId: "workspace.invite.redeem",
101
115
  input: { token: prepared.inviteTokenPreview, decision, ...extraInput },
@@ -146,12 +160,13 @@ export async function verifyInvitationTransactions(t, db) {
146
160
  assert.equal(events.length, 0);
147
161
  }
148
162
  return { user, owner, workspace, draftId, draft, invitation, assertUnlinked, members, pending, redeem, events,
149
- delivered, options, payload, prepareOnce, applyAssociation, failDelivery() { failDelivery = true; } };
163
+ delivered, options, payload, prepareOnce, applyAssociation, failDelivery() { failDelivery = true; },
164
+ participate(fn) { participant = fn; } };
150
165
  }
151
166
 
152
167
  await t.test("ordinary creation commits before controlled delivery and built-in acceptance", async () => {
153
168
  const f = await fixture();
154
- const created = await f.members.createInvite(f.workspace, f.owner, f.payload, f.options);
169
+ const created = (await f.members.createInvite(f.workspace, f.owner, f.payload, f.options)).value;
155
170
  assert.equal(created.inviteDelivery.status, "sent");
156
171
  assert.deepEqual(f.delivered, [created.createdInviteId]);
157
172
  await f.redeem(created);
@@ -162,9 +177,9 @@ export async function verifyInvitationTransactions(t, db) {
162
177
 
163
178
  await t.test("ordinary invitation works with an application participant and no association", async () => {
164
179
  const f = await fixture({ mailer: false });
165
- const created = await f.members.createInvite(f.workspace, f.owner, f.payload);
180
+ const created = (await f.members.createInvite(f.workspace, f.owner, f.payload)).value;
166
181
  assert.equal(created.inviteDelivery.status, "mailer_unconfigured");
167
- f.pending.registerAcceptanceParticipant(f.applyAssociation);
182
+ f.participate(f.applyAssociation);
168
183
  await f.redeem(created);
169
184
  assert.equal((await f.invitation(created)).status, "accepted");
170
185
  assert.equal((await f.draft()).user_id, null);
@@ -172,7 +187,7 @@ export async function verifyInvitationTransactions(t, db) {
172
187
 
173
188
  await t.test("association rollback restores earlier invitation state and sends nothing", async () => {
174
189
  const f = await fixture();
175
- const previous = await f.members.createInvite(f.workspace, f.owner, f.payload);
190
+ const previous = (await f.members.createInvite(f.workspace, f.owner, f.payload)).value;
176
191
  await assert.rejects(f.prepareOnce({ fail: true }), (error) => error.transactionOutcome === "rolledBack");
177
192
  const rows = await db("workspace_invites").where({ workspace_id: f.workspace.id });
178
193
  assert.equal(rows.length, 1);
@@ -192,7 +207,7 @@ export async function verifyInvitationTransactions(t, db) {
192
207
  ...receipt, email: "other@example.test", roleSid: "owner", inviteUrl: "https://untrusted.example"
193
208
  });
194
209
  assert.equal(sent.createdInviteId, prepared.createdInviteId);
195
- f.pending.registerAcceptanceParticipant(f.applyAssociation);
210
+ f.participate(f.applyAssociation);
196
211
  await f.redeem(sent);
197
212
  const linked = await f.draft();
198
213
  assert.equal(String(linked.user_id), f.user.id);
@@ -204,7 +219,7 @@ export async function verifyInvitationTransactions(t, db) {
204
219
  await t.test("participant failure rolls back membership, identity, training, scopes, audit and status", async () => {
205
220
  const f = await fixture();
206
221
  const prepared = await f.prepareOnce();
207
- f.pending.registerAcceptanceParticipant(async (...args) => {
222
+ f.participate(async (...args) => {
208
223
  await f.applyAssociation(...args);
209
224
  throw new Error("Controlled audit failure");
210
225
  });
@@ -223,7 +238,7 @@ export async function verifyInvitationTransactions(t, db) {
223
238
  } } });
224
239
  const prepared = await f.prepareOnce();
225
240
  await memberships.upsertMembership(f.workspace.id, f.user.id, { roleSid: "existing-role", status: "revoked" });
226
- f.pending.registerAcceptanceParticipant(f.applyAssociation);
241
+ f.participate(f.applyAssociation);
227
242
  await assert.rejects(f.redeem(prepared), /final status failure/);
228
243
  const previous = await memberships.findByWorkspaceIdAndUserId(f.workspace.id, f.user.id);
229
244
  assert.equal(previous.roleSid, "existing-role");
@@ -238,7 +253,7 @@ export async function verifyInvitationTransactions(t, db) {
238
253
  await t.test("acceptance completion failure reports committed and retains all accepted writes", async () => {
239
254
  const f = await fixture();
240
255
  const prepared = await f.prepareOnce();
241
- f.pending.registerAcceptanceParticipant(f.applyAssociation);
256
+ f.participate(f.applyAssociation);
242
257
  failCompletionForMethod = "patch";
243
258
  try {
244
259
  await assert.rejects(f.redeem(prepared), (error) => error.transactionOutcome === "committed");
@@ -287,7 +302,7 @@ export async function verifyInvitationTransactions(t, db) {
287
302
  : { review_state: state };
288
303
  await db("test_invitation_drafts").where({ id: f.draftId }).update(patch);
289
304
  const before = await f.draft();
290
- f.pending.registerAcceptanceParticipant(f.applyAssociation);
305
+ f.participate(f.applyAssociation);
291
306
  await assert.rejects(f.redeem(prepared), (error) => error.transactionOutcome === "rolledBack");
292
307
  assert.deepEqual(await f.draft(), before);
293
308
  assert.equal(await memberships.findByWorkspaceIdAndUserId(f.workspace.id, f.user.id), null);
@@ -304,9 +319,10 @@ export async function verifyInvitationTransactions(t, db) {
304
319
  if (state === "revoked") await f.members.revokeInvite(f.workspace, prepared.createdInviteId);
305
320
  if (state === "accepted") await f.redeem(prepared);
306
321
  let called = false;
307
- // The earlier acceptance closed registration on the original service.
308
- const pending = pendingService({ workspaceInvitesRepository: invites, workspaceMembershipsRepository: memberships });
309
- pending.registerAcceptanceParticipant(async () => { called = true; });
322
+ const pending = pendingService({
323
+ workspaceInvitesRepository: invites, workspaceMembershipsRepository: memberships, roleCatalog: createWorkspaceRoleCatalog(),
324
+ acceptanceParticipant: async () => { called = true; }
325
+ });
310
326
  const membershipBefore = await memberships.findByWorkspaceIdAndUserId(f.workspace.id, f.user.id);
311
327
  await assert.rejects(pending.acceptInviteByToken({
312
328
  user: state === "unauthenticated" ? null : state === "wrong-email" ? f.owner : f.user,
@@ -322,7 +338,7 @@ export async function verifyInvitationTransactions(t, db) {
322
338
  const f = await fixture();
323
339
  const prepared = await f.prepareOnce();
324
340
  let called = false;
325
- f.pending.registerAcceptanceParticipant(async () => { called = true; });
341
+ f.participate(async () => { called = true; });
326
342
  await assert.rejects(f.redeem(prepared, "accept", f.owner, { user: f.user, personId: f.draftId, roleSid: "owner" }));
327
343
  assert.equal(called, false);
328
344
  await f.assertUnlinked(prepared);
@@ -428,7 +444,7 @@ export async function verifyInvitationTransactions(t, db) {
428
444
  const f = await fixture({ inviteRepository: repository });
429
445
  const prepared = await f.prepareOnce();
430
446
  let participation = 0;
431
- f.pending.registerAcceptanceParticipant(async (...args) => { participation++; await f.applyAssociation(...args); });
447
+ f.participate(async (...args) => { participation++; await f.applyAssociation(...args); });
432
448
  const compete = () => competitor === "accept" || competitor === "refuse" ? f.redeem(prepared, competitor)
433
449
  : competitor === "revoke" ? f.members.revokeInvite(f.workspace, prepared.createdInviteId)
434
450
  : f.members.createInvite(f.workspace, f.owner, f.payload);
@@ -1,7 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import {
4
- materializeWorkspaceActionSurfacesFromAppConfig,
5
4
  resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig,
6
5
  resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig
7
6
  } from "../src/server/support/workspaceActionSurfaces.js";
@@ -40,60 +39,6 @@ test("resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig honors explicit
40
39
  assert.deepEqual(surfaceIds, ["app"]);
41
40
  });
42
41
 
43
- test("materializeWorkspaceActionSurfacesFromAppConfig resolves workspace surfaces from appConfig", () => {
44
- const actionDefinitions = [
45
- {
46
- id: "workspace.settings.read",
47
- surfacesFrom: "workspace"
48
- },
49
- {
50
- id: "auth.session.read",
51
- surfacesFrom: "enabled"
52
- }
53
- ];
54
-
55
- const materialized = materializeWorkspaceActionSurfacesFromAppConfig(actionDefinitions, {
56
- appConfig: {
57
- surfaceDefinitions: {
58
- app: { id: "app", enabled: true, requiresWorkspace: true },
59
- admin: { id: "admin", enabled: true, requiresWorkspace: true },
60
- ops: { id: "ops", enabled: true, requiresWorkspace: false }
61
- }
62
- }
63
- });
64
-
65
- assert.deepEqual(materialized, [
66
- {
67
- id: "workspace.settings.read",
68
- surfaces: ["app", "admin"]
69
- },
70
- {
71
- id: "auth.session.read",
72
- surfacesFrom: "enabled"
73
- }
74
- ]);
75
- });
76
-
77
- test("materializeWorkspaceActionSurfacesFromAppConfig drops workspace actions when no workspace surfaces are enabled", () => {
78
- const actionDefinitions = [
79
- {
80
- id: "workspace.settings.read",
81
- surfacesFrom: "workspace"
82
- }
83
- ];
84
-
85
- const materialized = materializeWorkspaceActionSurfacesFromAppConfig(actionDefinitions, {
86
- appConfig: {
87
- surfaceDefinitions: {
88
- app: { id: "app", enabled: true, requiresWorkspace: false },
89
- ops: { id: "ops", enabled: true, requiresWorkspace: false }
90
- }
91
- }
92
- });
93
-
94
- assert.deepEqual(materialized, []);
95
- });
96
-
97
42
  test("resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig picks a workspace surface when app default is non-workspace", () => {
98
43
  const surfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig({
99
44
  surfaceDefaultId: "home",
@@ -75,7 +75,7 @@ test("workspace auth policy context resolver resolves workspace context from use
75
75
  actor,
76
76
  workspaceSlug: "acme",
77
77
  options: {
78
- request
78
+ requireMembership: true
79
79
  }
80
80
  }
81
81
  ]);
@@ -1,6 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { createWorkspaceBootstrapContributor } from "../src/server/workspaceBootstrapContributor.js";
4
+ import { resolveTenancyProfile } from "../src/shared/tenancyProfile.js";
4
5
 
5
6
  function createAuthenticatedProfile(overrides = {}) {
6
7
  return {
@@ -41,9 +42,9 @@ test("workspace bootstrap contributor passes actor context to pending invites se
41
42
  }
42
43
  },
43
44
  workspaceInvitationsEnabled: true,
44
- appConfig: {
45
+ tenancyProfile: resolveTenancyProfile({
45
46
  tenancyMode: "workspaces"
46
- }
47
+ })
47
48
  });
48
49
 
49
50
  await contributor.contribute({
@@ -84,9 +85,9 @@ test("workspace bootstrap contributor resolves workspace slug from bootstrap que
84
85
  }
85
86
  },
86
87
  workspaceInvitationsEnabled: true,
87
- appConfig: {
88
+ tenancyProfile: resolveTenancyProfile({
88
89
  tenancyMode: "workspaces"
89
- }
90
+ })
90
91
  });
91
92
 
92
93
  const payload = await contributor.contribute({
@@ -129,9 +130,9 @@ test("workspace bootstrap contributor reports unauthenticated requested workspac
129
130
  }
130
131
  },
131
132
  workspaceInvitationsEnabled: true,
132
- appConfig: {
133
+ tenancyProfile: resolveTenancyProfile({
133
134
  tenancyMode: "workspaces"
134
- }
135
+ })
135
136
  });
136
137
 
137
138
  const payload = await contributor.contribute({
@@ -0,0 +1,23 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { WORKSPACE_CRUD_SCOPE } from "../src/server/support/workspaceCrud.js";
4
+ import { WORKSPACE_ROUTE_BASE_PATH, resolveWorkspaceRoutePath } from "../src/server/support/workspaceRoutePaths.js";
5
+ import { routeParamsValidator, workspaceSlugParamsValidator } from "../src/server/common/validators/routeParamsValidator.js";
6
+
7
+ test("the workspace CRUD scope routes under the workspace slug and reads it from the route", () => {
8
+ assert.ok(Object.isFrozen(WORKSPACE_CRUD_SCOPE));
9
+ assert.ok(Object.isFrozen(WORKSPACE_CRUD_SCOPE.inputKeys));
10
+ assert.equal(WORKSPACE_CRUD_SCOPE.routeBase, "/w/:workspaceSlug");
11
+ assert.equal(WORKSPACE_CRUD_SCOPE.actionInputValidator, workspaceSlugParamsValidator);
12
+ assert.equal(WORKSPACE_CRUD_SCOPE.routeParamsValidator, routeParamsValidator);
13
+ assert.deepEqual(WORKSPACE_CRUD_SCOPE.inputKeys, ["workspaceSlug"]);
14
+ assert.deepEqual(WORKSPACE_CRUD_SCOPE.input({ input: { params: { workspaceSlug: " Acme " } } }), { workspaceSlug: "acme" });
15
+ assert.deepEqual(WORKSPACE_CRUD_SCOPE.input({ input: { params: {} } }), {});
16
+ });
17
+
18
+ test("workspace route paths live under /api/w/:workspaceSlug", () => {
19
+ assert.equal(WORKSPACE_ROUTE_BASE_PATH, "/api/w/:workspaceSlug");
20
+ assert.equal(resolveWorkspaceRoutePath("rewarded"), "/api/w/:workspaceSlug/rewarded");
21
+ assert.equal(resolveWorkspaceRoutePath("/members/"), "/api/w/:workspaceSlug/members");
22
+ assert.equal(resolveWorkspaceRoutePath(), "/api/w/:workspaceSlug");
23
+ });
@@ -50,6 +50,7 @@ function createWorkspaceInvitesApiStub({
50
50
  forUpdate() { return this; },
51
51
  then(resolve) { return Promise.resolve([...rowById.values()].filter((row) => row.status === "pending")).then(resolve); }
52
52
  });
53
+ trx.isCompleted = () => false;
53
54
  const api = {
54
55
  async transaction(work) { return work(trx); },
55
56
  resources: {
@@ -185,8 +186,8 @@ test("workspaceInvitesRepository.findByTokenHashWithWorkspace preserves workspac
185
186
  id: "45",
186
187
  workspace: {
187
188
  id: "9",
188
- slug: "tonymobily3",
189
- name: "TonyMobily3",
189
+ slug: "tony3",
190
+ name: "Tony3",
190
191
  avatarUrl: "https://example.com/avatar.png"
191
192
  },
192
193
  email: "invitee@example.com",
@@ -208,8 +209,8 @@ test("workspaceInvitesRepository.findByTokenHashWithWorkspace preserves workspac
208
209
 
209
210
  assert.equal(invite?.id, "45");
210
211
  assert.equal(invite?.status, "accepted");
211
- assert.equal(invite?.workspaceSlug, "tonymobily3");
212
- assert.equal(invite?.workspaceName, "TonyMobily3");
212
+ assert.equal(invite?.workspaceSlug, "tony3");
213
+ assert.equal(invite?.workspaceName, "Tony3");
213
214
  assert.equal(invite?.workspaceAvatarUrl, "https://example.com/avatar.png");
214
215
  });
215
216
 
@@ -249,8 +250,8 @@ test("workspaceInvitesRepository.listPendingByWorkspaceIdWithWorkspace keeps wor
249
250
  id: "1",
250
251
  workspace: {
251
252
  id: "9",
252
- slug: "tonymobily3",
253
- name: "TonyMobily3",
253
+ slug: "tony3",
254
+ name: "Tony3",
254
255
  avatarUrl: "https://example.com/avatar.png"
255
256
  },
256
257
  email: "invitee@example.com",
@@ -284,8 +285,8 @@ test("workspaceInvitesRepository.listPendingByWorkspaceIdWithWorkspace keeps wor
284
285
  revokedAt: null,
285
286
  createdAt: toIsoString("2026-03-09 00:26:35.710"),
286
287
  updatedAt: toIsoString("2026-03-09 00:26:35.710"),
287
- workspaceSlug: "tonymobily3",
288
- workspaceName: "TonyMobily3",
288
+ workspaceSlug: "tony3",
289
+ workspaceName: "Tony3",
289
290
  workspaceAvatarUrl: "https://example.com/avatar.png"
290
291
  }
291
292
  ]);