@bongos/core 1.19.909 → 1.19.911
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.
- package/.bongos-core.json +52 -32
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +62 -2
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +25 -25
- package/docs/copy-registry.json +29 -29
- package/docs/file-map.md +3 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/ideas.css +25 -0
- package/modules/hall-ui/public/ideas.html +12 -0
- package/modules/hall-ui/public/ideas.js +115 -2
- package/modules/ideas/landing-preview.js +180 -0
- package/modules/ideas/routes/inbox.js +47 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/api-alias-caller-scan.js +213 -0
- package/scripts/gds/fitness-ratchets.js +31 -0
- package/scripts/gds/fitness.js +1 -1
- package/scripts/gds/gen-file-map.js +96 -2
- package/scripts/gds/ship-check.js +1 -1
- package/scripts/hall-preview/server.js +14 -1
- package/src/module-api.js +1 -1
- package/tests/api_alias_caller_ratchet.mjs +130 -0
- package/tests/file_map.mjs +59 -0
- package/tests/idea_landing_preview.mjs +337 -0
- package/tests/idea_spark_hall.mjs +183 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// tests/api_alias_caller_ratchet.mjs — the lowercase `/api/gds` CALLER ratchet
|
|
2
|
+
// (task 1004064, from idea 1001030). Mirrors tests/fitness_gds_ratchet.mjs, its
|
|
3
|
+
// case-sensitive sibling.
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS FILE IS DEFENDING AGAINST. The brief names the trap exactly: "this is a static scan;
|
|
6
|
+
// it breaks by finding nothing, and a green run looks identical to a working one." Every case
|
|
7
|
+
// below is written so that a scanner which enumerates nothing, excludes everything, or counts
|
|
8
|
+
// nothing FAILS here rather than passing quietly. The aggregation cases inject their own file
|
|
9
|
+
// list and reader, so the real exclusion + counting path runs without writing into the repo —
|
|
10
|
+
// the unit lane runs test files in parallel, and a test that edited a tracked file would break
|
|
11
|
+
// whichever suite happened to be scanning the tree at that moment.
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { test } from 'node:test';
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const scan = require(path.join(ROOT, 'scripts', 'gds', 'api-alias-caller-scan.js'));
|
|
22
|
+
|
|
23
|
+
// A reader over an in-memory tree — no repo writes.
|
|
24
|
+
const fake = (tree) => ({ files: Object.keys(tree), readFile: (rel) => tree[rel] });
|
|
25
|
+
|
|
26
|
+
test('the counter COUNTS — a file that should count, does (the trap case)', () => {
|
|
27
|
+
const r = scan.scanAliasCallers(fake({
|
|
28
|
+
'src/bongos/routes/thing.js': "await fetch(`${base}/api/gds/tasks/${id}`);\n",
|
|
29
|
+
}));
|
|
30
|
+
assert.equal(r.total, 1, 'a plain in-repo caller must be counted');
|
|
31
|
+
assert.deepEqual(Object.keys(r.byFile), ['src/bongos/routes/thing.js']);
|
|
32
|
+
assert.equal(r.scanned, 1, 'and the file must actually have been scanned');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('a collapsed enumeration is visible as scanned=0, not as a flattering total of 0', () => {
|
|
36
|
+
const empty = scan.scanAliasCallers(fake({}));
|
|
37
|
+
assert.equal(empty.total, 0);
|
|
38
|
+
assert.equal(empty.scanned, 0, 'scanned is what distinguishes "nothing to find" from "nothing looked at"');
|
|
39
|
+
|
|
40
|
+
const allExcluded = scan.scanAliasCallers(fake({
|
|
41
|
+
'tests/x.mjs': '/api/gds\n',
|
|
42
|
+
'docs/x.md': '/api/gds\n',
|
|
43
|
+
}));
|
|
44
|
+
assert.equal(allExcluded.total, 0);
|
|
45
|
+
assert.equal(allExcluded.scanned, 0, 'an every-path-excluded run is also scanned=0 — the same signal');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('the REAL scan enumerates the repo and lands on the committed baseline', () => {
|
|
49
|
+
const r = scan.scanAliasCallers();
|
|
50
|
+
assert.ok(r.scanned > 500, `the real tree must enumerate substantially (got ${r.scanned})`);
|
|
51
|
+
const baselines = JSON.parse(readFileSync(path.join(ROOT, 'config', 'fitness-baselines.json'), 'utf8'));
|
|
52
|
+
const floor = baselines.metrics.legacy_api_gds_callers;
|
|
53
|
+
assert.equal(typeof floor, 'number', 'the metric must have a baseline, or the ratchet only warns');
|
|
54
|
+
assert.ok(r.total <= floor, `caller count ${r.total} is ABOVE baseline ${floor} — a new /api/gds caller entered the tree`);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('the exclusions are real and each one is reachable', () => {
|
|
58
|
+
// Rule numbers refer to the header of api-alias-caller-scan.js.
|
|
59
|
+
for (const p of [
|
|
60
|
+
'src/bongos/api-prefix.js', // 1 the alias contract
|
|
61
|
+
'src/bongos/routes/auth.js', // 2 the pinned OAuth callbacks
|
|
62
|
+
'modules/provisioning/provisioning.js',
|
|
63
|
+
'src/bongos/uptime-poller.js', // 3 the live proof
|
|
64
|
+
'scripts/gds/smoke-gds.js',
|
|
65
|
+
'scripts/gds/gds-literal-scan.js', // 4 the rename guardrails
|
|
66
|
+
'scripts/gds/fitness-ratchets.js',
|
|
67
|
+
'scripts/gds/api-alias-caller-scan.js',
|
|
68
|
+
'tests/api_alias_exceptions.mjs', // 5 tests
|
|
69
|
+
'modules/dev-box/app/test/thing.mjs',
|
|
70
|
+
'docs/architecture.md', // 6 docs
|
|
71
|
+
'migrations/033_audit_log.sql', // 7 migrations
|
|
72
|
+
'scripts/hall-preview/fixtures/tasks.json', // 9 captured fixtures
|
|
73
|
+
'node_modules/pkg/index.js', // vendored
|
|
74
|
+
]) {
|
|
75
|
+
assert.equal(scan.isExcludedPath(p), true, `${p} must be excluded`);
|
|
76
|
+
}
|
|
77
|
+
// …and the exclusion is not a blanket that swallows the tree.
|
|
78
|
+
for (const p of [
|
|
79
|
+
'src/bongos/serve-internal.js',
|
|
80
|
+
'modules/lifecycle/abandon-leftovers.js',
|
|
81
|
+
'clients/bongos-client/index.mjs', // a .mjs that is NOT a test — a blanket \.mjs$ would blind the ratchet
|
|
82
|
+
'bin/bongos.js',
|
|
83
|
+
'CLAUDE.md',
|
|
84
|
+
]) {
|
|
85
|
+
assert.equal(scan.isExcludedPath(p), false, `${p} must NOT be excluded`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('a line ABOUT the alias is not a caller of it — and the rule is not so loose it excuses everything', () => {
|
|
90
|
+
// Excluded: names the canonical prefix too, or says what it is.
|
|
91
|
+
assert.equal(scan.isCountedLine('mount /api/bongos and /api/gds'), false);
|
|
92
|
+
assert.equal(scan.isCountedLine('// /api/gds is a permanent alias'), false);
|
|
93
|
+
assert.equal(scan.isCountedLine('// the legacy /api/gds mount'), false);
|
|
94
|
+
assert.equal(scan.isCountedLine('// /api/gds stays FOREVER'), false);
|
|
95
|
+
// Counted: a bare caller, with none of those tells.
|
|
96
|
+
assert.equal(scan.isCountedLine('const r = await fetch(`${base}/api/gds/me`);'), true);
|
|
97
|
+
assert.equal(scan.isCountedLine(' node scripts/gds/api.js GET /api/gds/tasks/1'), true);
|
|
98
|
+
// And a line with no alias at all is never counted, whatever else it says.
|
|
99
|
+
assert.equal(scan.isCountedLine('fetch(`${base}/api/bongos/me`)'), false);
|
|
100
|
+
assert.equal(scan.isCountedLine('a line about a legacy alias, forever'), false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('occurrences are counted, not lines — moving ONE of two callers on a line lowers the number', () => {
|
|
104
|
+
assert.equal(scan.countInText('f("/api/gds/a"); f("/api/gds/b");\n'), 2);
|
|
105
|
+
assert.equal(scan.countInText('f("/api/bongos/a"); f("/api/gds/b");\n'), 0, 'the canonical name on the line is the alias-commentary rule, by design');
|
|
106
|
+
assert.equal(scan.countInText('nothing here\n'), 0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('the ratchet is WIRED — computeMetrics reports the metric and compareToBaselines fails a rise', () => {
|
|
110
|
+
const ratchets = require(path.join(ROOT, 'scripts', 'gds', 'fitness-ratchets.js'));
|
|
111
|
+
const baselines = JSON.parse(readFileSync(path.join(ROOT, 'config', 'fitness-baselines.json'), 'utf8')).metrics;
|
|
112
|
+
|
|
113
|
+
// A rise is a violation…
|
|
114
|
+
const worse = ratchets.compareToBaselines(
|
|
115
|
+
{ legacy_api_gds_callers: baselines.legacy_api_gds_callers + 1 },
|
|
116
|
+
baselines,
|
|
117
|
+
);
|
|
118
|
+
assert.equal(worse.violations.length, 1, 'one more caller must RED the build');
|
|
119
|
+
assert.match(worse.violations[0], /legacy_api_gds_callers regressed/);
|
|
120
|
+
assert.match(worse.violations[0], /api-alias-caller-scan/, 'the hint must name the tool that lists the offenders');
|
|
121
|
+
|
|
122
|
+
// …a fall is a ratchet-down warning, not a failure.
|
|
123
|
+
const better = ratchets.compareToBaselines(
|
|
124
|
+
{ legacy_api_gds_callers: baselines.legacy_api_gds_callers - 1 },
|
|
125
|
+
baselines,
|
|
126
|
+
);
|
|
127
|
+
assert.equal(better.violations.length, 0);
|
|
128
|
+
assert.equal(better.warnings.length, 1);
|
|
129
|
+
assert.match(better.warnings[0], /ratchet-down available/);
|
|
130
|
+
});
|
package/tests/file_map.mjs
CHANGED
|
@@ -93,6 +93,65 @@ test('generation is deterministic + idempotent (a second run is a no-op)', () =>
|
|
|
93
93
|
assert.match(r2.stdout, /already current/);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
// ── module coverage (task 1004066) ────────────────────────────────────────────
|
|
97
|
+
// The check is a set comparison over a directory listing, and a set comparison breaks by
|
|
98
|
+
// comparing two empty sets and passing. Every case below is written so that a matcher which
|
|
99
|
+
// vouches for everything, or a scan which enumerates nothing, FAILS here.
|
|
100
|
+
|
|
101
|
+
test('every dir under modules/ is documented in file-map.md — and the set being compared is not empty', () => {
|
|
102
|
+
const cov = gen.moduleCoverage();
|
|
103
|
+
assert.ok(cov.onDisk.length > 0, 'modules/ enumerated to nothing — the coverage check would be vacuous');
|
|
104
|
+
assert.deepEqual(cov.defects, [], `module-coverage scan defects: ${cov.defects.join('; ')}`);
|
|
105
|
+
assert.deepEqual(cov.undocumented, [], `modules with no file-map entry: ${cov.undocumented.join(', ')}`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('--check FAILS (exit 2) on a module with no file-map entry — the trap case', () => {
|
|
109
|
+
const probe = path.join(ROOT, 'modules', '__filemap_probe__');
|
|
110
|
+
try {
|
|
111
|
+
mkdirSync(probe, { recursive: true });
|
|
112
|
+
writeFileSync(path.join(probe, 'module.json'), '{"key":"__filemap_probe__"}\n');
|
|
113
|
+
const cov = gen.moduleCoverage();
|
|
114
|
+
assert.ok(cov.onDisk.includes('__filemap_probe__'), 'the probe module must be enumerated');
|
|
115
|
+
assert.deepEqual(cov.undocumented, ['__filemap_probe__'], 'an undocumented module must be the ONLY finding');
|
|
116
|
+
const r = run(['--check']);
|
|
117
|
+
assert.equal(r.code, 2, 'an undocumented module must fail the gate');
|
|
118
|
+
assert.match(r.stderr, /modules with NO docs\/file-map\.md entry/);
|
|
119
|
+
assert.match(r.stderr, /__filemap_probe__/);
|
|
120
|
+
} finally {
|
|
121
|
+
rmSync(probe, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
// Asserted on the module finding rather than the exit code: --check reports every problem class
|
|
124
|
+
// at once, and a checkout carrying unrelated local noise (materialised module skills with no
|
|
125
|
+
// note) would otherwise make this read as "the probe is still there".
|
|
126
|
+
const after = run(['--check']);
|
|
127
|
+
assert.equal(gen.moduleCoverage().undocumented.length, 0, 'coverage is clean once the probe module is removed');
|
|
128
|
+
assert.doesNotMatch(after.stderr || '', /modules with NO docs\/file-map\.md entry/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('the matcher discriminates: a real module passes, an absent one does not, and a prefix cannot vouch', () => {
|
|
132
|
+
const md = readFileSync(path.join(ROOT, 'docs', 'file-map.md'), 'utf8');
|
|
133
|
+
const block = gen.modulesBlock(md);
|
|
134
|
+
assert.ok(block.length > 0, 'the modules/ tree block must be findable — an empty block would make entries unreadable');
|
|
135
|
+
assert.ok(gen.isModuleDocumented(md, block, 'government'), 'a documented module reads as documented');
|
|
136
|
+
assert.equal(gen.isModuleDocumented(md, block, '__no_such_module__'), false, 'an absent module must NOT read as documented');
|
|
137
|
+
// A bare substring test would let `modules/agents-extra` vouch for `agents`.
|
|
138
|
+
assert.equal(
|
|
139
|
+
gen.isModuleDocumented('see modules/agents-extra/thing.js', '', 'agents'),
|
|
140
|
+
false,
|
|
141
|
+
'a longer path sharing the prefix must not count as an entry',
|
|
142
|
+
);
|
|
143
|
+
// A skill dir listed in the same `├── name/` shape elsewhere in the map must not vouch either:
|
|
144
|
+
// the entry search is scoped to the modules/ block.
|
|
145
|
+
assert.equal(gen.modulesBlock('├── .claude/skills/\n├── recall/ ← a skill\n'), '', 'no modules/ block → no block to read entries from');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('module coverage reports a SCAN DEFECT rather than passing when the map cannot be read', () => {
|
|
149
|
+
const cov = gen.moduleCoverage('');
|
|
150
|
+
assert.ok(cov.onDisk.length > 0);
|
|
151
|
+
assert.equal(cov.defects.length, 1, 'an unreadable map is a defect, not a pass');
|
|
152
|
+
assert.match(cov.defects[0], /cannot run/);
|
|
153
|
+
});
|
|
154
|
+
|
|
96
155
|
test('every note maps to a real skill/routine (sidecar has no dangling keys)', () => {
|
|
97
156
|
const notes = JSON.parse(readFileSync(path.join(ROOT, 'docs', 'file-map.notes.json'), 'utf8'));
|
|
98
157
|
for (const section of gen.SECTIONS) {
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// tests/idea_landing_preview.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1004083 (BV2.R02, goal 1000095) — GRAVITY: the landing verdict read
|
|
4
|
+
// before the write. This drives the REAL `previewLanding` over a fake pool and a
|
|
5
|
+
// fake lifecycle port, so `decideLanding`, `authorizeRoutedLanding` and every
|
|
6
|
+
// branch inside the preview actually run.
|
|
7
|
+
//
|
|
8
|
+
// WHY THIS FILE EXISTS, stated plainly because it was a grader finding: the
|
|
9
|
+
// first cut of this task tested only the hall composer, against a mock of
|
|
10
|
+
// `postInboxPreviewLanding`. That proves the LINE renders what the server says
|
|
11
|
+
// and proves nothing at all about what the server says — the verdict every one
|
|
12
|
+
// of those cases asserted on was written by the test itself. The two halves need
|
|
13
|
+
// two harnesses, and this is the server one.
|
|
14
|
+
//
|
|
15
|
+
// DB-FREE, the tests/capture_goal_routing.mjs way: the collaborators the preview
|
|
16
|
+
// reaches through (`pool`, the `lifecycle` port, `moduleScopeMap`) are all
|
|
17
|
+
// injectable or patchable, and none of them is the thing under test — the matrix
|
|
18
|
+
// is, and the matrix is pure.
|
|
19
|
+
//
|
|
20
|
+
// THE PROPERTY THAT MATTERS MOST IS THE NEGATIVE ONE: previewing must never
|
|
21
|
+
// WRITE. A preview runs on every keystroke of a debounced composer, and the
|
|
22
|
+
// homeless path in the real capture resolves through `ensureMaintenanceGoal`,
|
|
23
|
+
// which is find-or-CREATE. The last case in this file fails if the preview ever
|
|
24
|
+
// reaches for it.
|
|
25
|
+
//
|
|
26
|
+
// Run: node tests/idea_landing_preview.mjs
|
|
27
|
+
|
|
28
|
+
import { strict as assert } from 'node:assert';
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
30
|
+
import { readFileSync } from 'node:fs';
|
|
31
|
+
import { makeRunner } from './helpers.mjs';
|
|
32
|
+
|
|
33
|
+
const require = createRequire(import.meta.url);
|
|
34
|
+
const { test, summary } = makeRunner();
|
|
35
|
+
|
|
36
|
+
process.env.NODE_ENV = 'test';
|
|
37
|
+
|
|
38
|
+
const api = require('../src/module-api');
|
|
39
|
+
const { previewLanding, OUTCOMES } = require('../modules/ideas/landing-preview');
|
|
40
|
+
|
|
41
|
+
// The goal row the preview reads, in the shape its SELECT returns. Defaults are
|
|
42
|
+
// the happy one: a real, open goal on a building version, unprotected scope.
|
|
43
|
+
function goalRow(over = {}) {
|
|
44
|
+
return {
|
|
45
|
+
id: '77', title: 'Idea routing', status: 'open',
|
|
46
|
+
version_id: 'BV2', scope_modules: ['ideas'], version_status: 'building',
|
|
47
|
+
...over,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A pool that answers one query and COUNTS them, so a case can assert that a
|
|
52
|
+
// branch never reached the database at all.
|
|
53
|
+
function fakePool(rows = []) {
|
|
54
|
+
const calls = [];
|
|
55
|
+
return { calls, query: async (sql, params) => { calls.push({ sql, params }); return { rows }; } };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Patch the two singletons the preview reaches through the doorway, and restore
|
|
59
|
+
// afterwards so cases cannot leak into each other.
|
|
60
|
+
// `version` defaults to a real building version, because "there IS one" is the
|
|
61
|
+
// ordinary world and a case that means the opposite must say so. The first cut
|
|
62
|
+
// of this helper defaulted the lifecycle port to NULL, which made two cases pass
|
|
63
|
+
// for the wrong reason — including the one whose whole subject is the missing
|
|
64
|
+
// version, so it asserted the right answer via the wrong cause.
|
|
65
|
+
async function withApi({ version = 'BV2', protectedScope = false } = {}, fn) {
|
|
66
|
+
const realResolve = api.resolveOptional;
|
|
67
|
+
const realMap = Object.getOwnPropertyDescriptor(api, 'moduleScopeMap');
|
|
68
|
+
const reached = { ensureMaintenanceGoal: 0, currentBuildingVersionId: 0 };
|
|
69
|
+
const lifecycle = { currentBuildingVersionId: async () => { reached.currentBuildingVersionId++; return version; } };
|
|
70
|
+
api.resolveOptional = (name) => (name === 'lifecycle' ? lifecycle : realResolve(name));
|
|
71
|
+
Object.defineProperty(api, 'moduleScopeMap', {
|
|
72
|
+
configurable: true,
|
|
73
|
+
value: { scopeIncludesProtected: () => protectedScope },
|
|
74
|
+
});
|
|
75
|
+
try { return await fn(reached); } finally {
|
|
76
|
+
api.resolveOptional = realResolve;
|
|
77
|
+
if (realMap) Object.defineProperty(api, 'moduleScopeMap', realMap);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A lifecycle port that records which methods were touched. `ensureMaintenanceGoal`
|
|
82
|
+
// THROWS rather than answering: the preview must never call it, and a method that
|
|
83
|
+
// quietly succeeds would let that regression pass as a green test.
|
|
84
|
+
function fakeLifecycle(versionId, reached) {
|
|
85
|
+
return {
|
|
86
|
+
currentBuildingVersionId: async () => { reached.currentBuildingVersionId++; return versionId; },
|
|
87
|
+
ensureMaintenanceGoal: async () => {
|
|
88
|
+
reached.ensureMaintenanceGoal++;
|
|
89
|
+
throw new Error('previewLanding must never create a goal');
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// the two lies the composer's picker tells — the reason this task exists
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
await test('a homeless BUG routes, and the preview names whose maintenance goal catches it', async () => {
|
|
99
|
+
await withApi({}, async (reached) => {
|
|
100
|
+
const v = await previewLanding(
|
|
101
|
+
{ goalId: null, kind: 'bug', grade: null, filerRank: 'archon', vector: 'api' },
|
|
102
|
+
{ pool: fakePool() }
|
|
103
|
+
);
|
|
104
|
+
assert.equal(v.outcome, OUTCOMES.ROUTED_HOME, 'the picker says "homeless"; ADR 0262 says otherwise');
|
|
105
|
+
assert.equal(v.status, 'ready', 'a trusted filer\'s bug is claimable at once');
|
|
106
|
+
assert.equal(v.version_id, 'BV2');
|
|
107
|
+
assert.equal(v.goal, null, 'the goal row does not exist yet, so it is not named');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await test('the same bug from an UNTRUSTED filer still lands, but at backlog', async () => {
|
|
112
|
+
await withApi({}, async () => {
|
|
113
|
+
const v = await previewLanding(
|
|
114
|
+
{ goalId: null, kind: 'bug', filerRank: 'thetes', vector: 'api' },
|
|
115
|
+
{ pool: fakePool() }
|
|
116
|
+
);
|
|
117
|
+
assert.equal(v.outcome, OUTCOMES.ROUTED_HOME);
|
|
118
|
+
assert.equal(v.status, 'backlog', 'the rank split is the matrix\'s, not re-derived here');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await test('a homeless NON-bug floats — the homeless door is bug-only', async () => {
|
|
123
|
+
await withApi({}, async () => {
|
|
124
|
+
const v = await previewLanding({ goalId: null, kind: 'feature', filerRank: 'archon', vector: 'api' }, { pool: fakePool() });
|
|
125
|
+
assert.equal(v.outcome, OUTCOMES.FLOATS);
|
|
126
|
+
assert.equal(v.reason, 'no_goal');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await test('a homeless bug from an UNRECOGNISED vector floats — the allowlist holds', async () => {
|
|
131
|
+
await withApi({}, async () => {
|
|
132
|
+
const v = await previewLanding({ goalId: null, kind: 'bug', filerRank: 'archon', vector: 'discord-ideas' }, { pool: fakePool() });
|
|
133
|
+
assert.equal(v.outcome, OUTCOMES.FLOATS, '#ideas files with hints only (ADR 0234)');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await test('a NAMED goal on a judgment kind floats, and the goal is not claimed to catch it', async () => {
|
|
138
|
+
await withApi({}, async () => {
|
|
139
|
+
const v = await previewLanding(
|
|
140
|
+
{ goalId: 77, kind: 'decision', filerRank: 'archon', vector: 'api' },
|
|
141
|
+
{ pool: fakePool([goalRow()]) }
|
|
142
|
+
);
|
|
143
|
+
assert.equal(v.outcome, OUTCOMES.FLOATS);
|
|
144
|
+
assert.equal(v.reason, 'judgment_kind', 'the second lie the picker tells');
|
|
145
|
+
assert.equal(v.goal, null);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await test('a Full Idea never routes, however confidently a goal is picked', async () => {
|
|
150
|
+
await withApi({}, async () => {
|
|
151
|
+
const v = await previewLanding(
|
|
152
|
+
{ goalId: 77, kind: 'feature', grade: 'full', filerRank: 'archon', vector: 'api' },
|
|
153
|
+
{ pool: fakePool([goalRow()]) }
|
|
154
|
+
);
|
|
155
|
+
assert.equal(v.outcome, OUTCOMES.FLOATS);
|
|
156
|
+
assert.equal(v.reason, 'full_idea_never_routes');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// a named goal: the happy paths and the wall
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
await test('a named goal is named back, with the status the matrix chose', async () => {
|
|
165
|
+
await withApi({}, async () => {
|
|
166
|
+
const pool = fakePool([goalRow()]);
|
|
167
|
+
const v = await previewLanding({ goalId: 77, kind: 'bug', filerRank: 'archon', vector: 'api' }, { pool });
|
|
168
|
+
assert.equal(v.outcome, OUTCOMES.ROUTED);
|
|
169
|
+
assert.equal(v.status, 'ready');
|
|
170
|
+
assert.deepEqual(v.goal, { id: '77', title: 'Idea routing' });
|
|
171
|
+
assert.equal(pool.calls.length, 1, 'one read, and it is a SELECT');
|
|
172
|
+
assert.match(pool.calls[0].sql, /^\s*SELECT/i);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await test('a non-bug with a home soft-lands at backlog', async () => {
|
|
177
|
+
await withApi({}, async () => {
|
|
178
|
+
const v = await previewLanding({ goalId: 77, kind: 'refactor', filerRank: 'archon', vector: 'api' }, { pool: fakePool([goalRow()]) });
|
|
179
|
+
assert.equal(v.outcome, OUTCOMES.ROUTED);
|
|
180
|
+
assert.equal(v.status, 'backlog');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await test('PROTECTED scope + a sub-Metic filer is refused, in words, before the filing is spent', async () => {
|
|
185
|
+
await withApi({ protectedScope: true }, async () => {
|
|
186
|
+
const v = await previewLanding({ goalId: 77, kind: 'bug', filerRank: 'thetes', vector: 'api' }, { pool: fakePool([goalRow()]) });
|
|
187
|
+
assert.equal(v.outcome, OUTCOMES.REFUSED);
|
|
188
|
+
assert.equal(v.refusal.code, 'protected_scope');
|
|
189
|
+
assert.match(v.refusal.reason, /protected territory/);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await test('PROTECTED scope + a Metic+ filer is NOT refused — the two are layers, not alternatives', async () => {
|
|
194
|
+
await withApi({ protectedScope: true }, async () => {
|
|
195
|
+
const v = await previewLanding({ goalId: 77, kind: 'bug', filerRank: 'archon', vector: 'api' }, { pool: fakePool([goalRow()]) });
|
|
196
|
+
assert.equal(v.outcome, OUTCOMES.ROUTED);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// the degraded states — silence, never a guess
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
for (const [label, row] of [
|
|
205
|
+
['a goal that is gone', undefined],
|
|
206
|
+
['a goal that is archived', goalRow({ status: 'archived' })],
|
|
207
|
+
['a goal on a version that is not building', goalRow({ version_status: 'shipped' })],
|
|
208
|
+
]) {
|
|
209
|
+
await test(`${label} states NOTHING rather than guessing a sentence`, async () => {
|
|
210
|
+
await withApi({}, async () => {
|
|
211
|
+
const v = await previewLanding({ goalId: 77, kind: 'bug', filerRank: 'archon', vector: 'api' }, { pool: fakePool(row ? [row] : []) });
|
|
212
|
+
assert.equal(v.outcome, null, 'the real capture will name the refusal; the preview does not invent one');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
await test('no building version means no promise — the homeless path says nothing', async () => {
|
|
218
|
+
await withApi({ version: null }, async (reached) => {
|
|
219
|
+
const v = await previewLanding({ goalId: null, kind: 'bug', filerRank: 'archon', vector: 'api' }, { pool: fakePool() });
|
|
220
|
+
assert.equal(v.outcome, null, 'the real capture fails closed here (NO_BUILDING_VERSION)');
|
|
221
|
+
assert.equal(reached.currentBuildingVersionId, 1, 'and it got there by asking, not by the port being absent');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await test('a pool that throws degrades to silence — a composer must never red on a keystroke', async () => {
|
|
226
|
+
await withApi({}, async () => {
|
|
227
|
+
const angry = { query: async () => { throw new Error('connection reset'); } };
|
|
228
|
+
const v = await previewLanding({ goalId: 77, kind: 'bug', filerRank: 'archon', vector: 'api' }, { pool: angry });
|
|
229
|
+
assert.equal(v.outcome, null);
|
|
230
|
+
assert.equal(v.refusal, null);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await test('a null input does not throw — the destructuring-defaults trap decideLanding records', async () => {
|
|
235
|
+
await withApi({}, async () => {
|
|
236
|
+
const v = await previewLanding(null, { pool: fakePool() });
|
|
237
|
+
assert.equal(v.outcome, OUTCOMES.FLOATS, 'nothing named, so nothing catches it');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// THE NEGATIVE PROPERTY — the one that makes this safe to run per keystroke
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
await test('previewing a homeless bug NEVER creates the maintenance goal', async () => {
|
|
246
|
+
const reached = { ensureMaintenanceGoal: 0, currentBuildingVersionId: 0 };
|
|
247
|
+
const realResolve = api.resolveOptional;
|
|
248
|
+
api.resolveOptional = (n) => (n === 'lifecycle' ? fakeLifecycle('BV2', reached) : realResolve(n));
|
|
249
|
+
try {
|
|
250
|
+
const v = await previewLanding({ goalId: null, kind: 'bug', filerRank: 'archon', vector: 'api' }, { pool: fakePool() });
|
|
251
|
+
assert.equal(v.outcome, OUTCOMES.ROUTED_HOME, 'it still answers');
|
|
252
|
+
assert.equal(reached.currentBuildingVersionId, 1, 'by READING the version');
|
|
253
|
+
assert.equal(reached.ensureMaintenanceGoal, 0,
|
|
254
|
+
'ensureMaintenanceGoal is find-or-CREATE; a preview that called it would mint a goal per keystroke');
|
|
255
|
+
} finally { api.resolveOptional = realResolve; }
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await test('no branch writes — every pool call in this file was a SELECT', async () => {
|
|
259
|
+
await withApi({}, async () => {
|
|
260
|
+
const pool = fakePool([goalRow()]);
|
|
261
|
+
for (const kind of ['bug', 'feature', 'decision', 'criterion-proposal', 'refactor']) {
|
|
262
|
+
await previewLanding({ goalId: 77, kind, filerRank: 'archon', vector: 'api' }, { pool });
|
|
263
|
+
}
|
|
264
|
+
assert.ok(pool.calls.length > 0, 'the goal read did happen');
|
|
265
|
+
for (const c of pool.calls) {
|
|
266
|
+
assert.match(c.sql, /^\s*SELECT/i, `a preview issued a non-SELECT: ${c.sql.slice(0, 40)}`);
|
|
267
|
+
assert.doesNotMatch(c.sql, /FOR SHARE|FOR UPDATE/i, 'a preview holds no lock — it races nothing');
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// THE ROUTE — asserted in SOURCE, the tests/filing_abuse_matrix.mjs idiom
|
|
274
|
+
//
|
|
275
|
+
// Those properties are guarantees about the CALL SITE, not about a return value:
|
|
276
|
+
// "the rank is the authenticated one" cannot be observed from the outside,
|
|
277
|
+
// because a route that read it from the body would answer identically to one
|
|
278
|
+
// that did not — for a caller who sent the true rank. So they are asserted where
|
|
279
|
+
// they live, exactly as F3 and F18 of the filing abuse matrix already do for
|
|
280
|
+
// POST /inbox. This file is the ideas module's second reader of that source.
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
const routeSrc = readFileSync(new URL('../modules/ideas/routes/inbox.js', import.meta.url), 'utf8');
|
|
284
|
+
const previewBlock = routeSrc.slice(routeSrc.indexOf("router.post('/inbox/preview-landing'"));
|
|
285
|
+
|
|
286
|
+
await test('[route] the preview exists, is builder-gated, and declares its validator', () => {
|
|
287
|
+
assert.ok(previewBlock.length > 0, 'POST /inbox/preview-landing is declared');
|
|
288
|
+
assert.match(previewBlock.slice(0, 400), /api\.requireBuilder/, 'no anonymous caller');
|
|
289
|
+
assert.match(previewBlock.slice(0, 900), /validateOrRespond\(req, res, \{/, 'the input SHAPE is validated');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
await test('[route] rank and vector come from AUTH and a literal — never from the body', () => {
|
|
293
|
+
const head = previewBlock.slice(0, 1600);
|
|
294
|
+
assert.match(head, /filerRank:\s*req\.builder\?\.rank/,
|
|
295
|
+
'a filer must not be able to preview a landing their rank does not grant');
|
|
296
|
+
assert.match(head, /vector:\s*'api'/, 'the vector is the door, hardcoded');
|
|
297
|
+
assert.ok(!/filerRank:\s*body\.|vector:\s*body\.|vector:\s*req\.body/.test(head),
|
|
298
|
+
'neither may be read from the request body');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
await test('[route] the verdict is passed through, not re-shaped into a second vocabulary', () => {
|
|
302
|
+
assert.match(previewBlock.slice(0, 1600), /const verdict = await previewLanding\(/);
|
|
303
|
+
assert.match(previewBlock.slice(0, 1600), /res\.json\(verdict\)/,
|
|
304
|
+
'a route that rebuilt the object by hand would be a second place the shape can drift');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await test('[route] it is declared BEFORE the :id routes that would otherwise shadow it', () => {
|
|
308
|
+
// The file header calls this ordering load-bearing. A literal path declared
|
|
309
|
+
// after `/inbox/:id` binds `:id = 'preview-landing'`, parseId 400s, and the
|
|
310
|
+
// whole feature disappears with no error naming it.
|
|
311
|
+
const mine = routeSrc.indexOf("'/inbox/preview-landing'");
|
|
312
|
+
const byId = routeSrc.indexOf("'/inbox/:id(\\\\d+)'");
|
|
313
|
+
assert.ok(mine > 0, 'the route is there');
|
|
314
|
+
assert.ok(byId === -1 || mine < byId, 'and it is declared above the id-bearing routes');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
await test('[route] the preview is not wired into the CAPTURE path — it is a read beside it', () => {
|
|
318
|
+
// The whole claim of "no new routing" is that the matrix has ONE caller that
|
|
319
|
+
// can write. previewLanding must never appear inside POST /inbox.
|
|
320
|
+
const capture = routeSrc.slice(routeSrc.indexOf("router.post('/inbox',"), routeSrc.indexOf("router.get('/inbox',"));
|
|
321
|
+
assert.ok(!/previewLanding/.test(capture), 'the capture does not consult the preview');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Probes run, and these are the MEASURED counts:
|
|
325
|
+
// make the homeless branch call ensureMaintenanceGoal -> 4 red (incl. the negative property)
|
|
326
|
+
// let authorizeRoutedLanding's refusal fall through -> 1 red (protected-scope)
|
|
327
|
+
// drop the grade check from decideLanding's early exit -> 1 red (full-idea)
|
|
328
|
+
// fabricate the goal instead of reading it -> 1 red (goal-is-gone)
|
|
329
|
+
//
|
|
330
|
+
// Writing this file is what found two defects the composer suite could not:
|
|
331
|
+
// `previewLanding(null)` THREW (a default in a parameter list fires on undefined
|
|
332
|
+
// and not on null — the trap routing.js's own comment records, reproduced one
|
|
333
|
+
// file away from it), and this harness's first `withApi` defaulted the lifecycle
|
|
334
|
+
// port to null, which made two cases pass for the wrong reason — including the
|
|
335
|
+
// one whose entire subject is the missing version.
|
|
336
|
+
|
|
337
|
+
summary('idea_landing_preview');
|