@bongos/core 1.19.1017 → 1.19.1019

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.
@@ -0,0 +1,115 @@
1
+ // tests/offboarding_secret_scrub.mjs — deactivateBuilder scrubs stored secrets
2
+ // (task 1003338 / ADR 0332).
3
+ //
4
+ // DB-free, mirroring the withTx-stub pattern in tests/government_rank_sync.mjs:
5
+ // kernel.withTx is stubbed to hand deactivateBuilder/reactivateBuilder a single
6
+ // recording fake client BEFORE src/bongos/db.js is required (it destructures
7
+ // withTx at load time). No government seam is registered, so
8
+ // syncGovernanceRankRole's optional port resolves to nothing and no-ops —
9
+ // out of scope for this file.
10
+ //
11
+ // Two secrets, two different guarantees under test:
12
+ // 1. github_oauth_tokens (core-owned) — deleteGithubToken must be called
13
+ // INSIDE deactivateBuilder's transaction (a real DELETE, not an event).
14
+ // 2. the module-owned Gemini BYOK key — core never touches those columns;
15
+ // it emits builder.secrets-scrub-needed and a listening module (not
16
+ // vendored in this checkout) clears its own store. Asserted here by
17
+ // registering a stub listener and checking the payload.
18
+ // reactivateBuilder must do neither — restoring a stored secret on return is
19
+ // exactly the behaviour ADR 0332 rules out.
20
+ //
21
+ // Run: node --test tests/offboarding_secret_scrub.mjs
22
+
23
+ import assert from 'node:assert/strict';
24
+ import { test } from 'node:test';
25
+ import { createRequire } from 'node:module';
26
+ import path from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
28
+
29
+ const require = createRequire(import.meta.url);
30
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
31
+
32
+ const kernel = require(path.join(ROOT, 'src/bongos/db-kernel.js'));
33
+ const seams = require(path.join(ROOT, 'src/module-seams.js'));
34
+
35
+ // A minimal recording client: answers just enough of deactivateBuilder's /
36
+ // reactivateBuilder's queries to run to completion, and records every call so
37
+ // tests can assert on what was (or wasn't) issued.
38
+ function exitTxClient({ builderId, rank, status, newRank, newStatus }) {
39
+ const calls = [];
40
+ return {
41
+ calls,
42
+ find: (re) => calls.find((c) => re.test(c.sql)),
43
+ async query(sql, params) {
44
+ calls.push({ sql, params });
45
+ if (/FROM builders WHERE id = \$1 FOR UPDATE/.test(sql)) {
46
+ return { rows: [{ id: builderId, rank, status }] };
47
+ }
48
+ if (/FROM claims/.test(sql)) return { rows: [] };
49
+ if (/UPDATE builders/.test(sql)) {
50
+ return { rows: [{ id: builderId, rank: newRank, status: newStatus }] };
51
+ }
52
+ return { rows: [] };
53
+ },
54
+ };
55
+ }
56
+
57
+ let txClient = null;
58
+ kernel.withTx = async (fn) => fn(txClient);
59
+ const coreDb = require(path.join(ROOT, 'src/bongos/db.js'));
60
+
61
+ test('deactivateBuilder deletes the github_oauth_tokens row inside its transaction', async () => {
62
+ txClient = exitTxClient({
63
+ builderId: 9001, rank: 'metic', status: 'active', newRank: 'xenos', newStatus: 'inactive',
64
+ });
65
+ await coreDb.deactivateBuilder({ builderId: 9001, reason: 'left the team', actorBuilderId: 3 });
66
+
67
+ const del = txClient.find(/DELETE FROM github_oauth_tokens/);
68
+ assert.ok(del, 'deactivateBuilder must delete the stored GitHub token in-transaction');
69
+ assert.deepEqual(del.params, [9001]);
70
+
71
+ const sessionRevokeIdx = txClient.calls.findIndex((c) => /UPDATE builder_sessions SET revoked_at/.test(c.sql));
72
+ const tokenDeleteIdx = txClient.calls.findIndex((c) => /DELETE FROM github_oauth_tokens/.test(c.sql));
73
+ assert.ok(tokenDeleteIdx > sessionRevokeIdx, 'the token delete must be inside the same transaction, after the session revoke');
74
+ });
75
+
76
+ test('deactivateBuilder emits builder.secrets-scrub-needed with { builderId, actor }', async () => {
77
+ txClient = exitTxClient({
78
+ builderId: 9002, rank: 'metic', status: 'active', newRank: 'xenos', newStatus: 'inactive',
79
+ });
80
+ let received = null;
81
+ const off = seams.on('builder.secrets-scrub-needed', async (payload) => { received = payload; });
82
+ try {
83
+ await coreDb.deactivateBuilder({ builderId: 9002, reason: null, actorBuilderId: 5 });
84
+ } finally {
85
+ off();
86
+ }
87
+ assert.ok(received, 'the module teardown event must fire post-commit');
88
+ assert.equal(received.builderId, 9002);
89
+ assert.equal(received.actor, 'builder-exit:builder#5');
90
+ });
91
+
92
+ test('a listener-free instance (no module installed) still completes deactivation cleanly', async () => {
93
+ // No seams.on registered here — mirrors a vanilla instance with art-pipeline off.
94
+ txClient = exitTxClient({
95
+ builderId: 9003, rank: 'xenos', status: 'active', newRank: 'xenos', newStatus: 'inactive',
96
+ });
97
+ const result = await coreDb.deactivateBuilder({ builderId: 9003, reason: null, actorBuilderId: 3 });
98
+ assert.equal(result.builder.status, 'inactive');
99
+ assert.ok(txClient.find(/DELETE FROM github_oauth_tokens/), 'the core-owned delete still runs with no module listening');
100
+ });
101
+
102
+ test('reactivateBuilder does not delete the github token row or emit the scrub event', async () => {
103
+ txClient = exitTxClient({
104
+ builderId: 9004, rank: 'xenos', status: 'inactive', newRank: 'xenos', newStatus: 'active',
105
+ });
106
+ let scrubFired = false;
107
+ const off = seams.on('builder.secrets-scrub-needed', async () => { scrubFired = true; });
108
+ try {
109
+ await coreDb.reactivateBuilder({ builderId: 9004, actorBuilderId: 3 });
110
+ } finally {
111
+ off();
112
+ }
113
+ assert.ok(!txClient.find(/DELETE FROM github_oauth_tokens/), 'reactivation must not touch the token table');
114
+ assert.ok(!scrubFired, 'reactivation must not re-trigger the secrets scrub');
115
+ });