|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * [#2909 T2] Lock the seed semantics of bootstrapDeclaredPositions. |
| 5 | + * |
| 6 | + * sys_position is RECORD-AUTHORITATIVE (ADR-0094 addendum): the declared |
| 7 | + * `positions: []` metadata seeds row IDENTITY + display fields only. The |
| 8 | + * record side — permission-set bindings, `active`, `is_default`, |
| 9 | + * `delegatable`, `managed_by` provenance — belongs to the runtime/admin and |
| 10 | + * must never be touched by a re-seed. These tests exist to keep that |
| 11 | + * contract from regressing silently (the behavior predates them but was |
| 12 | + * never locked). |
| 13 | + */ |
| 14 | + |
| 15 | +import { describe, it, expect } from 'vitest'; |
| 16 | +import { bootstrapDeclaredPositions } from './bootstrap-declared-positions.js'; |
| 17 | + |
| 18 | +/** Minimal in-memory ql for sys_position seeding. */ |
| 19 | +function makeQl(declared: any[] = []) { |
| 20 | + const rows: any[] = []; |
| 21 | + return { |
| 22 | + rows, |
| 23 | + _registry: { listItems: (type: string) => (type === 'position' ? declared.map((c) => ({ content: c })) : []) }, |
| 24 | + async find(object: string, q: any) { |
| 25 | + if (object !== 'sys_position') return []; |
| 26 | + const where = q?.where ?? {}; |
| 27 | + return rows.filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)); |
| 28 | + }, |
| 29 | + async insert(object: string, data: any) { |
| 30 | + if (object !== 'sys_position') return null; |
| 31 | + rows.push({ ...data }); |
| 32 | + return { id: data.id }; |
| 33 | + }, |
| 34 | + async update(object: string, data: any) { |
| 35 | + if (object !== 'sys_position') return; |
| 36 | + const r = rows.find((x) => x.id === data.id); |
| 37 | + if (r) Object.assign(r, data); |
| 38 | + }, |
| 39 | + }; |
| 40 | +} |
| 41 | + |
| 42 | +describe('bootstrapDeclaredPositions (#2909 T2 — seed-only semantics locked)', () => { |
| 43 | + it('inserts new declared positions with identity + display fields only', async () => { |
| 44 | + const ql = makeQl([{ name: 'contributor', label: 'Contributor', description: 'Does work' }]); |
| 45 | + const r = await bootstrapDeclaredPositions(ql, null); |
| 46 | + expect(r.seeded).toBe(1); |
| 47 | + const row = ql.rows[0]; |
| 48 | + expect(row).toMatchObject({ name: 'contributor', label: 'Contributor', active: true, is_default: false }); |
| 49 | + // Provenance is NOT stamped by the declared seeder (bootstrapBuiltinRoles |
| 50 | + // owns the built-in anchors; declared positions carry the object default). |
| 51 | + expect(row.managed_by).toBeUndefined(); |
| 52 | + }); |
| 53 | + |
| 54 | + it('refreshes ONLY label/description on existing rows', async () => { |
| 55 | + const ql = makeQl([{ name: 'contributor', label: 'Contributor v2', description: 'new text' }]); |
| 56 | + ql.rows.push({ |
| 57 | + id: 'pos_1', name: 'contributor', label: 'Contributor', description: 'old', |
| 58 | + active: true, is_default: false, |
| 59 | + }); |
| 60 | + const r = await bootstrapDeclaredPositions(ql, null); |
| 61 | + expect(r.updated).toBe(1); |
| 62 | + expect(ql.rows[0].label).toBe('Contributor v2'); |
| 63 | + expect(ql.rows[0].description).toBe('new text'); |
| 64 | + }); |
| 65 | + |
| 66 | + it('NEVER touches authoritative record fields (active/is_default/delegatable/managed_by)', async () => { |
| 67 | + const ql = makeQl([{ name: 'contributor', label: 'Contributor v2' }]); |
| 68 | + // Admin turned the position off, made it default, marked it delegatable, |
| 69 | + // and the row carries provenance — a re-seed must not reset any of it. |
| 70 | + ql.rows.push({ |
| 71 | + id: 'pos_1', name: 'contributor', label: 'Contributor', description: 'old', |
| 72 | + active: false, is_default: true, delegatable: true, managed_by: 'package', |
| 73 | + permissions: ['something_admin_set'], |
| 74 | + }); |
| 75 | + await bootstrapDeclaredPositions(ql, null); |
| 76 | + const row = ql.rows[0]; |
| 77 | + expect(row.active).toBe(false); |
| 78 | + expect(row.is_default).toBe(true); |
| 79 | + expect(row.delegatable).toBe(true); |
| 80 | + expect(row.managed_by).toBe('package'); |
| 81 | + expect(row.permissions).toEqual(['something_admin_set']); |
| 82 | + // …while the display fields did refresh. |
| 83 | + expect(row.label).toBe('Contributor v2'); |
| 84 | + }); |
| 85 | + |
| 86 | + it('is idempotent — a re-run inserts nothing new', async () => { |
| 87 | + const ql = makeQl([{ name: 'a', label: 'A' }, { name: 'b', label: 'B' }]); |
| 88 | + const r1 = await bootstrapDeclaredPositions(ql, null); |
| 89 | + expect(r1.seeded).toBe(2); |
| 90 | + const r2 = await bootstrapDeclaredPositions(ql, null); |
| 91 | + expect(r2.seeded).toBe(0); |
| 92 | + expect(ql.rows).toHaveLength(2); |
| 93 | + }); |
| 94 | +}); |
0 commit comments