@bongos/core 1.19.238 → 1.19.239

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.
@@ -42,10 +42,10 @@ const POST_GOALS_SCHEMA = {
42
42
  scope_modules: { type: 'array', maxItems: 64, itemsType: 'string' },
43
43
  succeeds_goal_id: { type: 'integer', min: 1 }, // BV1.R64 rescope carry-forward
44
44
  };
45
- // /members validates only `role` declaratively; builder_id is coerced in-handler
45
+ // /members validates only `membership_kind` declaratively; builder_id is coerced in-handler
46
46
  // (accepts a number OR a numeric string — ids round-trip through JSON as strings).
47
47
  const POST_MEMBERS_SCHEMA = {
48
- role: { type: 'string' },
48
+ membership_kind: { type: 'string' },
49
49
  };
50
50
  // POST /goals/:id/scope (BV1.R64 ask-to-widen) — `add` is the module list; the
51
51
  // empty-array, unknown-key, and protected-module checks are in-handler.
@@ -122,15 +122,15 @@ await test('POST /goals/:id/scope: a non-string add item is rejected', () => {
122
122
  // ---- POST /goals/:id/members schema ----
123
123
  // builder_id is coerced via Number() + Number.isInteger in-handler (a number or a
124
124
  // numeric string passes; a non-numeric string → 400 bad_builder_id), verified
125
- // live against Postgres. Here we pin the declarative half (role).
126
- await test('POST /members: role optional, valid when present', () => {
125
+ // live against Postgres. Here we pin the declarative half (membership_kind).
126
+ await test('POST /members: membership_kind optional, valid when present', () => {
127
127
  assert.equal(validate({}, POST_MEMBERS_SCHEMA), null);
128
- assert.equal(validate({ role: 'lead' }, POST_MEMBERS_SCHEMA), null);
128
+ assert.equal(validate({ membership_kind: 'lead' }, POST_MEMBERS_SCHEMA), null);
129
129
  });
130
130
 
131
- await test('POST /members: non-string role → wrong_type', () => {
132
- const r = validate({ role: 42 }, POST_MEMBERS_SCHEMA);
133
- assert.ok(r && r.details.some((d) => d.field === 'role' && d.reason === 'wrong_type'));
131
+ await test('POST /members: non-string membership_kind → wrong_type', () => {
132
+ const r = validate({ membership_kind: 42 }, POST_MEMBERS_SCHEMA);
133
+ assert.ok(r && r.details.some((d) => d.field === 'membership_kind' && d.reason === 'wrong_type'));
134
134
  });
135
135
 
136
136
  await test('builder_id coercion contract: Number() accepts int + numeric string, rejects junk', () => {
@@ -49,7 +49,7 @@ const GOAL_ROW = {
49
49
  scope_modules: ['gds-core'], status: 'open', created_by: 42,
50
50
  succeeded_by_goal_id: null, sort_order: 0, created_at: 'T0', updated_at: 'T0',
51
51
  };
52
- const MEMBER_ROW = { goal_id: 7, builder_id: 42, role: 'member', joined_at: 'T0' };
52
+ const MEMBER_ROW = { goal_id: 7, builder_id: 42, membership_kind: 'member', joined_at: 'T0' };
53
53
 
54
54
  // =========================================================================
55
55
  // createGoal
@@ -76,7 +76,7 @@ await test('createGoal: the creator becomes the first lead in the same statement
76
76
  const pool = fakePool((s) => (/INSERT INTO goals/.test(s) ? { rows: [GOAL_ROW] } : null));
77
77
  await createGoal({ versionId: 'V1', title: 'g', createdBy: 42 }, { pool });
78
78
  const ins = find(pool.queries, /INSERT INTO goals/);
79
- assert.match(ins.sql, /INSERT INTO goal_members \(goal_id, builder_id, role\)/,
79
+ assert.match(ins.sql, /INSERT INTO goal_members \(goal_id, builder_id, membership_kind\)/,
80
80
  'the member insert is part of the createGoal statement');
81
81
  assert.match(ins.sql, /'lead'/, 'the creator is inserted as lead, not member');
82
82
  assert.match(ins.sql, /WHERE \$6::bigint IS NOT NULL/,
@@ -157,19 +157,19 @@ await test('setGoalStatus: unknown goal returns null', async () => {
157
157
  // task 1002888 wrapped the bare upsert in a CTE that also cancels any pending
158
158
  // membership request, so the statement now STARTS with `WITH added AS (` — the
159
159
  // INSERT is the first arm. The pending-ask cancellation itself is pinned in
160
- // tests/goal_membership_requests.mjs; here we only re-anchor the role contract.
161
- await test('addGoalMember: upsert PRESERVES the existing role (re-join cannot demote a lead)', async () => {
162
- const pool = fakePool((s) => (/^WITH added AS/.test(s) ? { rows: [{ ...MEMBER_ROW, role: 'lead' }] } : null));
163
- const out = await addGoalMember({ goalId: 7, builderId: 42, role: 'member' }, { pool });
164
- assert.equal(out.role, 'lead', 'an existing lead row survives a role:member re-add');
160
+ // tests/goal_membership_requests.mjs; here we only re-anchor the membership contract.
161
+ await test('addGoalMember: upsert PRESERVES the existing membership_kind (re-join cannot demote a lead)', async () => {
162
+ const pool = fakePool((s) => (/^WITH added AS/.test(s) ? { rows: [{ ...MEMBER_ROW, membership_kind: 'lead' }] } : null));
163
+ const out = await addGoalMember({ goalId: 7, builderId: 42, membershipKind: 'member' }, { pool });
164
+ assert.equal(out.membership_kind, 'lead', 'an existing lead row survives a membership_kind:member re-add');
165
165
  const ins = find(pool.queries, /^WITH added AS/);
166
166
  assert.deepEqual(ins.params, [7, 42, 'member', null], 'resolvedBy defaults to null when no actor is supplied');
167
167
  assert.match(ins.sql, /INSERT INTO goal_members/, 'the membership write is the first CTE arm');
168
- assert.match(ins.sql, /ON CONFLICT \(goal_id, builder_id\) DO UPDATE SET role = goal_members\.role/,
169
- 'conflict keeps the stored role — role changes go through PATCH /goals/:id/members/:builderId only (task 1002689)');
168
+ assert.match(ins.sql, /ON CONFLICT \(goal_id, builder_id\) DO UPDATE SET membership_kind = goal_members\.membership_kind/,
169
+ 'conflict keeps the stored membership_kind — changes go through PATCH /goals/:id/members/:builderId only (task 1002689)');
170
170
  });
171
171
 
172
- await test('addGoalMember: role defaults to member', async () => {
172
+ await test('addGoalMember: membershipKind defaults to member', async () => {
173
173
  const pool = fakePool((s) => (/^WITH added AS/.test(s) ? { rows: [MEMBER_ROW] } : null));
174
174
  await addGoalMember({ goalId: 7, builderId: 42 }, { pool });
175
175
  assert.equal(find(pool.queries, /^WITH added AS/).params[2], 'member');