@bongos/core 1.20.35 → 1.20.37

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.
@@ -517,7 +517,7 @@ test('the runner’s shell output is folded behind a disclosure, never the headl
517
517
  }) });
518
518
  await b.settle();
519
519
  const html = b.panel();
520
- assert.match(html, /<details class="dp-settled__detail"><summary>What the upgrade said<\/summary>/, 'folded');
520
+ assert.match(html, /<details class="dp-settled__detail"><summary>Technical detail — what the upgrade said<\/summary>/, 'folded');
521
521
  assert.match(html, /working tree not clean/, 'and the words are still all there');
522
522
  assert.ok(html.indexOf('did not land') < html.indexOf('<details'), 'the plain sentence comes first');
523
523
  });
@@ -645,3 +645,167 @@ test('the reconnect link returns to the page\u2019s full https address when ther
645
645
  await b.settle();
646
646
  assert.ok(b.panel().includes(`return=${encodeURIComponent(here)}`), b.panel());
647
647
  });
648
+
649
+ // ── why a move stopped, by kind; the update rule; waiting is not moving (task 1004450) ──
650
+
651
+ // A body built inside the vm is another realm's object; compare its data, not its prototype.
652
+ const plain = (o) => JSON.parse(JSON.stringify(o));
653
+ const soon = (mins) => new Date(Date.now() + mins * 60000).toISOString();
654
+ const failedAs = (cls, reason, over = {}) => ({
655
+ target_version: '1.19.871', state: 'error', at: new Date().toISOString(), error: 'raw runner words', superseded: false,
656
+ failure: { class: cls, reason }, ...over,
657
+ });
658
+ const waitingIntent = (cls, reason, mins = 60) => ({
659
+ action: 'core-upgrade', state: 'pending', target_version: '1.19.871', not_before: soon(mins),
660
+ last_error: 'x', failure: cls ? { class: cls, reason } : null,
661
+ });
662
+
663
+ test('a move waiting out a temporary failure says WHEN it tries again, and offers to try now', async () => {
664
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { open_intent: waitingIntent('transient', 'not_published') }) });
665
+ await b.settle();
666
+ const html = b.panel();
667
+ assert.match(html, /dp-settled--waiting/);
668
+ assert.match(html, /Waiting to try core 1\.19\.871 again at .*in about 60 min/);
669
+ assert.match(html, /not published yet/, 'the reason, in plain words');
670
+ assert.match(html, /usually temporary/);
671
+ assert.doesNotMatch(html, /is pending for this box/, 'not the generic busy line');
672
+ b.els.get('dp-retry-1')._on.click();
673
+ await b.settle();
674
+ const post = b.posts.find((p) => /core-upgrade$/.test(p.path));
675
+ assert.deepEqual(post && plain(post.body), { to: '1.19.871' }, 'Try now asks for the SAME move');
676
+ });
677
+
678
+ test('a known snag the runner just fixed says what it did, and that it tries once more', async () => {
679
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { open_intent: waitingIntent('known_wedge', 'dirty_tree', 2) }) });
680
+ await b.settle();
681
+ const html = b.panel();
682
+ assert.match(html, /A known snag stopped the move/);
683
+ assert.match(html, /unsaved file changes/);
684
+ assert.match(html, /saved those changes on its own/);
685
+ assert.match(html, /tries the move once more/);
686
+ });
687
+
688
+ test('a pending move with a PAST not_before is still a move under way, not a wait', async () => {
689
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { open_intent: waitingIntent('transient', 'not_published', -5) }) });
690
+ await b.settle();
691
+ assert.doesNotMatch(b.panel(), /dp-settled--waiting/);
692
+ assert.match(b.panel(), /is pending for this box/);
693
+ });
694
+
695
+ test('a move that falls into a WAIT stops the poll at once, with no "still working" toast', async () => {
696
+ const b = boot({
697
+ scope: 'hoodify-finance', moveTries: 50,
698
+ instanceOver: (inst, n) => (n >= 2 ? { open_intent: waitingIntent('transient', 'registry_unreadable') } : {}),
699
+ });
700
+ await b.settle();
701
+ b.els.get('dp-preview-32')._on.click();
702
+ b.els.get('dp-confirm-32')._on.click();
703
+ for (let i = 0; i < 4; i++) await b.settle();
704
+ assert.match(b.panel(), /dp-settled--waiting/, 'the row shows the wait');
705
+ assert.doesNotMatch(b.panel(), /dp-moving/, 'and is no longer painted as moving');
706
+ assert.ok(!b.toasts.some((t) => /Still working/.test(t)), `no false "still working": ${b.toasts}`);
707
+ assert.ok(b.reads.get('32') < 10, `the poll stopped early (read ${b.reads.get('32')} times, bound 50)`);
708
+ });
709
+
710
+ test('a refusal by the update rule offers ONE click: allow new releases, then move', async () => {
711
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { update_channel: 'patch', last_move: failedAs('needs_decision', 'outside_channel', { target_version: '1.20.1' }) }) });
712
+ await b.settle();
713
+ const html = b.panel();
714
+ assert.match(html, /dp-settled--decision/);
715
+ assert.match(html, /Your call/);
716
+ assert.match(html, /outside this project’s update rule/);
717
+ assert.match(html, /Allow new releases and move to 1\.20\.1/);
718
+ b.els.get('dp-allow-1')._on.click();
719
+ await b.settle();
720
+ const paths = b.posts.map((p) => p.path);
721
+ const rule = paths.indexOf('/provisioning/instances/1/update-channel');
722
+ const mv = paths.indexOf('/provisioning/instances/1/core-upgrade');
723
+ assert.ok(rule >= 0 && mv > rule, `the rule changes FIRST, then the move is queued: ${paths}`);
724
+ assert.deepEqual(plain(b.posts[rule].body), { channel: 'minor' });
725
+ assert.deepEqual(plain(b.posts[mv].body), { to: '1.20.1' });
726
+ });
727
+
728
+ test('under the widest rule already, the refusal offers no allow button', async () => {
729
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { update_channel: 'minor', last_move: failedAs('needs_decision', 'outside_channel') }) });
730
+ await b.settle();
731
+ assert.doesNotMatch(b.panel(), /dp-allow-1/);
732
+ });
733
+
734
+ test('the other decisions say what the owner can do, with no button that cannot help', async () => {
735
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { last_move: failedAs('needs_decision', 'artist_gate') }) });
736
+ await b.settle();
737
+ assert.match(b.panel(), /Approve the artist review/);
738
+ assert.doesNotMatch(b.panel(), /dp-allow-1/);
739
+ });
740
+
741
+ test('a failure nothing automatic can fix points at the blocker filed for it', async () => {
742
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { last_move: failedAs('unknown', 'health') }) });
743
+ await b.settle();
744
+ const html = b.panel();
745
+ assert.match(html, /nothing automatic can fix it/);
746
+ assert.match(html, /did not answer after restarting/);
747
+ assert.match(html, /href="\/blockers"/);
748
+ assert.match(html, /<details class="dp-settled__detail">/, 'the runner’s words are still one click away');
749
+ });
750
+
751
+ test('a failed ROLLBACK says the project may be down', async () => {
752
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { last_move: failedAs('unknown', 'rollback_failed') }) });
753
+ await b.settle();
754
+ assert.match(b.panel(), /may be down/);
755
+ assert.match(b.panel(), /href="\/blockers"/);
756
+ });
757
+
758
+ test('a temporary failure that ran out of retries offers to try again — only for the move on offer', async () => {
759
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { last_move: failedAs('transient', 'npm_install') }) });
760
+ await b.settle();
761
+ assert.match(b.panel(), /kept stopping/);
762
+ assert.match(b.panel(), /dp-again-1/);
763
+ const other = boot({ controlPlane: preview('1', 'cloudbongos', { last_move: failedAs('transient', 'npm_install', { target_version: '1.19.850' }) }) });
764
+ await other.settle();
765
+ assert.doesNotMatch(other.panel(), /dp-again-1/, 'a try-again for a version no longer on offer would be a different move');
766
+ });
767
+
768
+ test('the update rule is shown in words and can be changed from the page', async () => {
769
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { update_channel: 'patch' }) });
770
+ await b.settle();
771
+ const html = b.panel();
772
+ assert.match(html, /Update rule: Fixes only/);
773
+ assert.match(html, /<option value="patch" selected>/);
774
+ b.els.set('dp-rule-sel-1', { value: 'minor' });
775
+ b.els.get('dp-rule-save-1')._on.click();
776
+ await b.settle();
777
+ const post = b.posts.find((p) => /update-channel$/.test(p.path));
778
+ assert.deepEqual(post && plain(post.body), { channel: 'minor' });
779
+ assert.ok(b.toasts.length, 'the server’s sentence is shown');
780
+ });
781
+
782
+ test('a preview with no rule reading shows no rule control, rather than a guessed one', async () => {
783
+ const b = boot();
784
+ await b.settle();
785
+ assert.doesNotMatch(b.panel(), /Update rule/);
786
+ });
787
+
788
+ test('every reason the runner can give has owner words on this page — none falls to the fallback', async () => {
789
+ // upgrade-outcome.js's REASON_CLASS is the vocabulary; a reason added there with no
790
+ // sentence here would reach the owner as "no words for this yet".
791
+ const { createRequire } = await import('node:module');
792
+ const req = createRequire(import.meta.url);
793
+ const vocab = Object.keys(req(path.join(ROOT, 'scripts/gds/upgrade-outcome.js')).REASON_CLASS || {});
794
+ assert.ok(vocab.length > 20, 'the vocabulary was read');
795
+ const block = SRC.slice(SRC.indexOf('const REASON_WORDS = {'), SRC.indexOf('const reasonWords'));
796
+ const missing = vocab.filter((k) => !new RegExp(`\\b${k}:`).test(block));
797
+ assert.deepEqual(missing, [], `reasons with no owner words: ${missing}`);
798
+ });
799
+
800
+ test('a module that is not ready says what the owner can do about it', async () => {
801
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { last_move: failedAs('needs_decision', 'module_incompatible') }) });
802
+ await b.settle();
803
+ assert.match(b.panel(), /Turn that module off/);
804
+ });
805
+
806
+ test('a known snag it could not fix names the next step, and does NOT point at a blocker that was never filed', async () => {
807
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { last_move: failedAs('known_wedge', 'disk_full') }) });
808
+ await b.settle();
809
+ assert.match(b.panel(), /whoever runs that machine clears the snag/);
810
+ assert.doesNotMatch(b.panel(), /href="\/blockers"/, 'move-escalation files no blocker for this class');
811
+ });
@@ -0,0 +1,232 @@
1
+ // tests/module_assess_tests.mjs — the Tests assessment signal (task 1003791, ADR 0343).
2
+ // No database: the module's tests really run (real node child processes, from a REAL
3
+ // packed tarball verified by module-artifact.js), and the signal row goes to a fake
4
+ // pg client, so what is pinned is the outcome vocabulary core_265 stores:
5
+ // no tests -> no_data, never a fail
6
+ // tests ran -> scored, round(100 x passing files / files), sample_size = files
7
+ // unreadable -> not_scored
8
+ // plus the two safety properties: the sandbox is always removed, and a module's test
9
+ // process never inherits the parent's credentials. And it runs regardless of whether
10
+ // the module is enabled — nothing here consults isModuleEnabled.
11
+ //
12
+ // Run: node tests/module_assess_tests.mjs
13
+
14
+ import { test } from 'node:test';
15
+ import { strict as assert } from 'node:assert';
16
+ import { createRequire } from 'node:module';
17
+ import fs from 'node:fs';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+
21
+ const require = createRequire(import.meta.url);
22
+ const { discoverTestFiles, assessTests, recordTestsSignal, insertSignal, runRefusal, testEnv, SANDBOX_PREFIX } = require('../scripts/gds/module-assess-tests.js');
23
+ const { packModule, verifyModuleArtifact } = require('../scripts/gds/module-artifact.js');
24
+ const { relativeArtifactPath } = require('../src/bongos/module-store.js');
25
+
26
+ const PASS = "import assert from 'node:assert'; assert.equal(1 + 1, 2); console.log('1 passed, 0 failed');\n";
27
+ const FAIL = "import assert from 'node:assert'; assert.equal(1 + 1, 3);\n";
28
+ const file = (p, text) => ({ path: p, buf: Buffer.from(text) });
29
+ const tmpRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), 'mod-assess-root-'));
30
+ const sandboxesIn = (root) => fs.readdirSync(root).filter((n) => n.startsWith(SANDBOX_PREFIX));
31
+
32
+ test('only tests/*.mjs at the top of tests/ count, as in the unit gate', () => {
33
+ assert.deepEqual(
34
+ discoverTestFiles(['module.json', 'tests/b.mjs', 'tests/a.mjs', 'tests/fixtures/x.mjs', 'tests/helper.js', 'lib/tests/c.mjs']),
35
+ ['tests/a.mjs', 'tests/b.mjs']);
36
+ });
37
+
38
+ test('a module that declares no tests is no_data, not a fail', async () => {
39
+ const root = tmpRoot();
40
+ const s = await assessTests('weather', [file('module.json', '{}'), file('index.js', '')], { trusted: true, root });
41
+ assert.equal(s.outcome, 'no_data');
42
+ assert.equal(s.score, null, 'no_data carries no number (core_265: score exactly when scored)');
43
+ assert.equal(s.sample_size, 0);
44
+ assert.match(s.reason, /declares no tests/);
45
+ assert.deepEqual(sandboxesIn(root), [], 'nothing is unpacked when there is nothing to run');
46
+ });
47
+
48
+ test('all passing scores 100; one of two failing scores 50', async () => {
49
+ const root = tmpRoot();
50
+ const all = await assessTests('weather', [file('tests/a.mjs', PASS), file('tests/b.mjs', PASS)], { trusted: true, root });
51
+ assert.equal(all.outcome, 'scored');
52
+ assert.equal(all.score, 100);
53
+ assert.equal(all.sample_size, 2);
54
+
55
+ const half = await assessTests('weather', [file('tests/a.mjs', PASS), file('tests/b.mjs', FAIL)], { trusted: true, root });
56
+ assert.equal(half.outcome, 'scored');
57
+ assert.equal(half.score, 50);
58
+ assert.equal(half.sample_size, 2);
59
+ assert.match(half.reason, /1 of 2/);
60
+ const failed = half.detail.files.find((f) => f.file === 'tests/b.mjs');
61
+ assert.equal(failed.passed, false);
62
+ assert.match(failed.tail, /AssertionError|assert/i, 'a failing file keeps a bounded tail of why');
63
+ assert.deepEqual(sandboxesIn(root), [], 'the sandbox is removed after every run');
64
+ });
65
+
66
+ test('every failing still scores 0 — distinct from no_data', async () => {
67
+ const s = await assessTests('weather', [file('tests/a.mjs', FAIL)], { trusted: true, root: tmpRoot() });
68
+ assert.equal(s.outcome, 'scored');
69
+ assert.equal(s.score, 0);
70
+ });
71
+
72
+ test('a module test reaches its own files by relative path, at the modules/<key>/ depth', async () => {
73
+ const s = await assessTests('weather', [
74
+ file('lib/add.js', 'module.exports = (a, b) => a + b;\n'),
75
+ file('tests/a.mjs', "import { createRequire } from 'node:module'; import assert from 'node:assert';\nconst add = createRequire(import.meta.url)('../lib/add.js'); assert.equal(add(2, 2), 4);\n"),
76
+ ], { trusted: true, root: tmpRoot() });
77
+ assert.equal(s.score, 100, JSON.stringify(s.detail));
78
+ });
79
+
80
+ test('a module test process never inherits the parent\'s credentials', async () => {
81
+ process.env.DATABASE_URL = 'postgres://secret@host/db';
82
+ process.env.BONGOS_TOKEN = 'secret-token';
83
+ try {
84
+ const s = await assessTests('weather', [file('tests/env.mjs',
85
+ "import assert from 'node:assert'; assert.equal(process.env.DATABASE_URL, undefined); assert.equal(process.env.BONGOS_TOKEN, undefined); assert.equal(process.env.NODE_ENV, 'test');\n")],
86
+ { trusted: true, root: tmpRoot() });
87
+ assert.equal(s.score, 100, JSON.stringify(s.detail));
88
+ } finally {
89
+ delete process.env.DATABASE_URL;
90
+ delete process.env.BONGOS_TOKEN;
91
+ }
92
+ });
93
+
94
+ test('a hung test file is killed at the timeout and counted as failed', async () => {
95
+ const s = await assessTests('weather', [file('tests/hang.mjs', 'setInterval(() => {}, 1000);\n'), file('tests/ok.mjs', PASS)],
96
+ { trusted: true, root: tmpRoot(), timeoutMs: 1500 });
97
+ assert.equal(s.score, 50);
98
+ assert.equal(s.detail.files.find((f) => f.file === 'tests/hang.mjs').timed_out, true);
99
+ });
100
+
101
+ // ---- recordTestsSignal: tarball in, one appended row out ----
102
+
103
+ const SANDBOX_ENV = { MODULE_TEST_SANDBOX: '1', PATH: process.env.PATH, SystemRoot: process.env.SystemRoot };
104
+
105
+ test('outside the separate testing environment, a published module is never run', async () => {
106
+ let ran = false;
107
+ const db = { async query() { ran = true; return { rows: [] }; } };
108
+ for (const env of [{}, { MODULE_TEST_SANDBOX: 'true' }, { MODULE_TEST_SANDBOX: '0' }]) {
109
+ const res = await recordTestsSignal(42, { db, env, root: tmpRoot() });
110
+ assert.equal(res.ok, false);
111
+ assert.equal(res.code, 'not_in_test_sandbox');
112
+ }
113
+ assert.equal(ran, false, 'refused before reading the store or the tarball');
114
+ });
115
+
116
+ test('the test environment keeps no credential and pins HOME to the sandbox', () => {
117
+ process.env.SOME_API_KEY = 'x';
118
+ try {
119
+ const env = testEnv('/sb');
120
+ assert.equal(env.SOME_API_KEY, undefined);
121
+ assert.equal(env.HOME, '/sb');
122
+ assert.equal(env.NODE_ENV, 'test');
123
+ } finally { delete process.env.SOME_API_KEY; }
124
+ });
125
+
126
+ function fakeDb(versionRow) {
127
+ const inserts = [];
128
+ return {
129
+ inserts,
130
+ async query(sql, params) {
131
+ if (/FROM store_module_versions/.test(sql)) return { rows: versionRow ? [versionRow] : [] };
132
+ if (/INSERT INTO module_assessment_signals/.test(sql)) {
133
+ assert.doesNotMatch(sql, /UPDATE/i, 'append-only');
134
+ inserts.push(params);
135
+ const [version_id, outcome, score, sample_size, reason] = params;
136
+ return { rows: [{ id: 7, version_id, part: 'tests', outcome, score, sample_size, reason, measured_at: new Date() }] };
137
+ }
138
+ throw new Error(`unexpected query: ${sql}`);
139
+ },
140
+ };
141
+ }
142
+
143
+ function publishedFixture(extraFiles) {
144
+ const src = fs.mkdtempSync(path.join(os.tmpdir(), 'mod-assess-src-'));
145
+ const dir = path.join(src, 'weather');
146
+ fs.mkdirSync(path.join(dir, 'tests'), { recursive: true });
147
+ fs.writeFileSync(path.join(dir, 'module.json'), JSON.stringify({ key: 'weather', title: 'Weather', description: 'x', version: '1.2.0', coreVersion: '^1.0.0', contributes: {} }));
148
+ for (const [p, text] of Object.entries(extraFiles)) fs.writeFileSync(path.join(dir, ...p.split('/')), text);
149
+ const { tgz } = packModule('weather', { modulesDir: src, now: () => new Date('2026-09-30T00:00:00Z'), modeOf: () => '644' });
150
+ const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mod-assess-store-'));
151
+ const final = path.join(storeDir, 'weather', 'weather-1.2.0.tgz');
152
+ fs.mkdirSync(path.dirname(final), { recursive: true });
153
+ fs.writeFileSync(final, tgz);
154
+ return { storeDir, final, row: { id: 42, module_key: 'weather', version: '1.2.0', artifact_path: relativeArtifactPath(final, { dir: storeDir }) } };
155
+ }
156
+
157
+ test('a published version is tested from its tarball and one tests row is appended', async () => {
158
+ const { storeDir, row } = publishedFixture({ 'tests/a.mjs': PASS, 'tests/b.mjs': FAIL });
159
+ const db = fakeDb(row);
160
+ const res = await recordTestsSignal(42, { db, storeDir, root: tmpRoot(), env: SANDBOX_ENV });
161
+ assert.equal(res.ok, true);
162
+ assert.equal(db.inserts.length, 1);
163
+ const [versionId, outcome, score, sample, , detail] = db.inserts[0];
164
+ assert.equal(versionId, 42);
165
+ assert.equal(outcome, 'scored');
166
+ assert.equal(score, 50);
167
+ assert.equal(sample, 2);
168
+ assert.equal(JSON.parse(detail).files.length, 2);
169
+ });
170
+
171
+ test('a version whose tarball is missing or tampered is not_scored, not failed', async () => {
172
+ const { storeDir, final, row } = publishedFixture({ 'tests/a.mjs': PASS });
173
+ fs.writeFileSync(final, Buffer.from('not a tarball'));
174
+ const db = fakeDb(row);
175
+ const res = await recordTestsSignal(42, { db, storeDir, root: tmpRoot(), env: SANDBOX_ENV });
176
+ assert.equal(res.ok, true);
177
+ const [, outcome, score] = db.inserts[0];
178
+ assert.equal(outcome, 'not_scored');
179
+ assert.equal(score, null);
180
+
181
+ fs.rmSync(final);
182
+ const db2 = fakeDb(row);
183
+ await recordTestsSignal(42, { db: db2, storeDir, root: tmpRoot(), env: SANDBOX_ENV });
184
+ assert.equal(db2.inserts[0][1], 'not_scored');
185
+ assert.doesNotMatch(db2.inserts[0][4], /[\/]/, 'the stored reason carries an error code, never a local path');
186
+ });
187
+
188
+ test('an unknown version writes nothing', async () => {
189
+ const db = fakeDb(null);
190
+ const res = await recordTestsSignal(99, { db, root: tmpRoot(), env: SANDBOX_ENV });
191
+ assert.equal(res.ok, false);
192
+ assert.equal(res.code, 'version_not_found');
193
+ assert.equal(db.inserts.length, 0);
194
+ });
195
+
196
+ test('the runner never consults whether the module is enabled', () => {
197
+ const src = fs.readFileSync(new URL('../scripts/gds/module-assess-tests.js', import.meta.url), 'utf8').replace(/\/\/.*$/gm, '');
198
+ assert.doesNotMatch(src, /isModuleEnabled/);
199
+ });
200
+
201
+ test('the sandbox dir is gitignored in case a run is killed mid-way', () => {
202
+ const gi = fs.readFileSync(new URL('../.gitignore', import.meta.url), 'utf8');
203
+ assert.match(gi, /^\/\.module-assess-\*\/$/m);
204
+ });
205
+
206
+ test('the gate lives in the runner itself: assessTests refuses untrusted code outside the testing environment', async () => {
207
+ const root = tmpRoot();
208
+ await assert.rejects(assessTests('weather', [file('tests/a.mjs', PASS)], { root, env: {} }), { code: 'not_in_test_sandbox' });
209
+ assert.deepEqual(sandboxesIn(root), [], 'refused before anything is unpacked');
210
+ });
211
+
212
+ test('a mis-set flag on a machine that holds secrets is still refused', async () => {
213
+ const root = tmpRoot();
214
+ for (const k of ['DATABASE_URL', 'BONGOS_TOKEN', 'STRIPE_SECRET', 'OPENAI_API_KEY']) {
215
+ assert.equal(runRefusal({ env: { ...SANDBOX_ENV, [k]: 'x' }, root }).code, 'secrets_present', k);
216
+ }
217
+ assert.equal(runRefusal({ env: SANDBOX_ENV, root }), null, 'a clean testing environment may run');
218
+ fs.writeFileSync(path.join(root, '.env'), 'DATABASE_URL=x\n');
219
+ assert.equal(runRefusal({ env: SANDBOX_ENV, root }).code, 'secrets_present', 'a .env at the core root counts');
220
+ await assert.rejects(assessTests('weather', [file('tests/a.mjs', PASS)], { root, env: SANDBOX_ENV }), { code: 'secrets_present' });
221
+ assert.equal(runRefusal({ trusted: true, env: {}, root }), null, 'your own checkout (--dir) is trusted');
222
+ });
223
+
224
+ test('insertSignal appends exactly one tests row with the signal fields', async () => {
225
+ const seen = [];
226
+ const db = { async query(sql, params) { seen.push({ sql, params }); return { rows: [{ id: 1 }] }; } };
227
+ await insertSignal(5, { outcome: 'scored', score: 80, sample_size: 5, reason: 'r', detail: { files: [] } }, { db });
228
+ assert.equal(seen.length, 1);
229
+ assert.match(seen[0].sql, /INSERT INTO module_assessment_signals[\s\S]*'tests'/);
230
+ assert.doesNotMatch(seen[0].sql, /UPDATE/i);
231
+ assert.deepEqual(seen[0].params, [5, 'scored', 80, 5, 'r', '{"files":[]}']);
232
+ });