@bongos/core 1.19.579 → 1.19.581
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 +75 -40
- package/bin/bongos.js +10 -1
- 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 +4 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0260-the-application-is-the-consent-and-the-echo-is-the-gate.md +146 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +73 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +21 -18
- package/docs/copy-registry.json +50 -23
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/hall-ui/public/oversight.css +7 -0
- package/modules/hall-ui/public/watch.js +75 -0
- package/modules/onboarding/routes/access-requests.js +56 -1
- package/modules/platform-identity/applicant-profile.js +158 -0
- package/modules/platform-identity/application-echo.js +50 -0
- package/modules/platform-identity/recruiter-sliver.js +19 -0
- package/modules/platform-identity/routes/sso.js +126 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/build-cli-package.js +8 -2
- package/scripts/gds/cli-lib.js +11 -3
- package/scripts/gds/recall.js +1 -1
- package/scripts/gds/start.js +39 -26
- package/scripts/gds/surface.js +89 -0
- package/src/bongos/auth-admission.js +61 -0
- package/src/bongos/auth.js +2 -1
- package/src/module-api.js +11 -1
- package/tests/adr_renumber_integrity.mjs +84 -0
- package/tests/applicant_profile_boundary.mjs +641 -0
- package/tests/applicant_profile_read_bounds.mjs +95 -0
- package/tests/cli_surface.mjs +146 -0
- package/tests/module_api.mjs +1 -0
- package/tests/recruiter_sliver_boundary.mjs +35 -2
- package/tests/watch_applications_queue.mjs +94 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// tests/applicant_profile_read_bounds.mjs — the one hub call that FANS OUT is
|
|
2
|
+
// bounded in time (task 1003681, from a grader finding).
|
|
3
|
+
//
|
|
4
|
+
// THE DEFECT. `readApplicantProfileFromHub()` had no timeout on its fetch to the
|
|
5
|
+
// hub. Its own doc-comment already promised the outcome — "a queue that fails to
|
|
6
|
+
// load because the hub is slow is a broken one" — but nothing enforced it.
|
|
7
|
+
//
|
|
8
|
+
// WHY IT MATTERS MORE HERE THAN FOR ITS TWO SIBLINGS. reportMembershipCheckIn
|
|
9
|
+
// and reportActivityRollup fire at most once per sign-in or per ship. This one
|
|
10
|
+
// runs up to MAX_LIVE_PROFILE_READS (25) times CONCURRENTLY inside the
|
|
11
|
+
// Promise.all that `GET /access-requests` awaits before responding (D7 resolves
|
|
12
|
+
// the profile live on every render, deliberately — a snapshot would keep
|
|
13
|
+
// publishing a consent since withdrawn). So one unresponsive hub held the whole
|
|
14
|
+
// reviewer queue open for as long as the sockets lived, 25 at a time.
|
|
15
|
+
//
|
|
16
|
+
// The fix is a bound, not a behaviour change: every other failure mode here
|
|
17
|
+
// already answers `null`, so a timeout simply joins them.
|
|
18
|
+
//
|
|
19
|
+
// DB-free and network-free: auth-config is pinned in require.cache so the
|
|
20
|
+
// federation config resolves without env, and global fetch is a spy.
|
|
21
|
+
//
|
|
22
|
+
// Run: node --test --test-reporter=tap tests/applicant_profile_read_bounds.mjs
|
|
23
|
+
|
|
24
|
+
import assert from 'node:assert/strict';
|
|
25
|
+
import { test } from 'node:test';
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
|
|
34
|
+
// Pin the federation config BEFORE auth-admission loads, so loadIdpConfig()
|
|
35
|
+
// answers without HUB_* env (the tests/profile_route.mjs require.cache idiom).
|
|
36
|
+
const cfgPath = require.resolve('../src/bongos/auth-config.js');
|
|
37
|
+
const realCfg = require('../src/bongos/auth-config.js');
|
|
38
|
+
require.cache[cfgPath] = {
|
|
39
|
+
id: cfgPath,
|
|
40
|
+
filename: cfgPath,
|
|
41
|
+
loaded: true,
|
|
42
|
+
exports: {
|
|
43
|
+
...realCfg,
|
|
44
|
+
loadIdpConfig: () => ({ origin: 'https://hub.example', clientId: 'cid', clientSecret: 'sec' }),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const admission = require('../src/bongos/auth-admission.js');
|
|
49
|
+
|
|
50
|
+
function withFetch(impl, run) {
|
|
51
|
+
const real = globalThis.fetch;
|
|
52
|
+
globalThis.fetch = impl;
|
|
53
|
+
return Promise.resolve(run()).finally(() => { globalThis.fetch = real; });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
test('the hub read carries an abort signal — the queue can never wait on it forever', async () => {
|
|
57
|
+
// Arrange
|
|
58
|
+
let seen = null;
|
|
59
|
+
const ok = { ok: true, json: async () => ({ profile: { bio: 'hi' } }) };
|
|
60
|
+
|
|
61
|
+
// Act
|
|
62
|
+
const profile = await withFetch(async (_url, init) => { seen = init; return ok; },
|
|
63
|
+
() => admission.readApplicantProfileFromHub(42));
|
|
64
|
+
|
|
65
|
+
// Assert
|
|
66
|
+
assert.deepEqual(profile, { bio: 'hi' }, 'the happy path still returns the hub’s view');
|
|
67
|
+
assert.ok(seen, 'fetch was called');
|
|
68
|
+
assert.ok(seen.signal, 'an AbortSignal must be passed — without one the call is unbounded');
|
|
69
|
+
assert.equal(typeof seen.signal.aborted, 'boolean', 'and it must be a real AbortSignal');
|
|
70
|
+
assert.equal(seen.signal.aborted, false, 'not already aborted when the request goes out');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('a timed-out hub answers null, exactly like every other failure here', async () => {
|
|
74
|
+
// Arrange — what AbortSignal.timeout produces when the budget expires
|
|
75
|
+
const abortErr = Object.assign(new Error('This operation was aborted'), { name: 'TimeoutError' });
|
|
76
|
+
|
|
77
|
+
// Act
|
|
78
|
+
const profile = await withFetch(async () => { throw abortErr; },
|
|
79
|
+
() => admission.readApplicantProfileFromHub(42));
|
|
80
|
+
|
|
81
|
+
// Assert — a degraded queue, never a broken one
|
|
82
|
+
assert.equal(profile, null, 'an abort must fail soft to null, not reject into the route');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('the timeout is a real, small budget — a 25-wide fan-out cannot outlive it', () => {
|
|
86
|
+
// Structural: the constant is the bound the route inherits, so it is pinned
|
|
87
|
+
// rather than measured (a real 5s wait would make this suite sleep).
|
|
88
|
+
const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'auth-admission.js'), 'utf8');
|
|
89
|
+
const m = /const APPLICANT_PROFILE_TIMEOUT_MS = (\d+);/.exec(src);
|
|
90
|
+
assert.ok(m, 'the budget must be a named constant, not an inline number');
|
|
91
|
+
const ms = Number(m[1]);
|
|
92
|
+
assert.ok(ms > 0 && ms <= 10000, `the budget must be small and real (got ${ms}ms)`);
|
|
93
|
+
assert.match(src, /signal: AbortSignal\.timeout\(APPLICANT_PROFILE_TIMEOUT_MS\)/,
|
|
94
|
+
'and the fetch must actually use it — the constant alone bounds nothing');
|
|
95
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// tests/cli_surface.mjs — the CLI addresses the reader it actually has (task 1003680).
|
|
2
|
+
//
|
|
3
|
+
// Two obligations, and the second is the one that could silently break the whole methodology:
|
|
4
|
+
//
|
|
5
|
+
// 1. A terminal reader never sees output written to a model — no `[otb-card]` directive, no
|
|
6
|
+
// `/builder-*` slash command (those exist only inside Claude Code), no raw markdown link.
|
|
7
|
+
//
|
|
8
|
+
// 2. The AGENT shape is unchanged. The skills and the card-delivery hook relay `start.js`'s
|
|
9
|
+
// stdout verbatim and branch on its first character; a stray difference there breaks the card
|
|
10
|
+
// contract for every session. So the agent cases below assert the exact old strings.
|
|
11
|
+
|
|
12
|
+
import test from 'node:test';
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
|
|
18
|
+
const REPO_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const surface = require(path.join(REPO_ROOT, 'scripts', 'gds', 'surface.js'));
|
|
21
|
+
|
|
22
|
+
const AGENT_ENV = {};
|
|
23
|
+
const TERM_ENV = { BONGOS_SURFACE: 'terminal' };
|
|
24
|
+
|
|
25
|
+
// ── 1. The signal itself ────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
test('agent is the default — nothing changes unless something opts out', () => {
|
|
28
|
+
assert.equal(surface.resolveSurface({}), 'agent');
|
|
29
|
+
assert.equal(surface.resolveSurface({ BONGOS_SURFACE: '' }), 'agent');
|
|
30
|
+
assert.equal(surface.resolveSurface({ BONGOS_SURFACE: 'nonsense' }), 'agent');
|
|
31
|
+
assert.equal(surface.isTerminal({}), false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('terminal is opt-in, case- and space-insensitive', () => {
|
|
35
|
+
assert.equal(surface.resolveSurface({ BONGOS_SURFACE: 'terminal' }), 'terminal');
|
|
36
|
+
assert.equal(surface.resolveSurface({ BONGOS_SURFACE: ' TERMINAL ' }), 'terminal');
|
|
37
|
+
assert.equal(surface.isTerminal(TERM_ENV), true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('the surface is never inferred from isTTY', () => {
|
|
41
|
+
// A Claude Code session's stdout is not a TTY either, so TTY-sniffing would classify every
|
|
42
|
+
// agent run as a terminal and break the card contract. The module must not consult it.
|
|
43
|
+
const src = require('node:fs').readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'surface.js'), 'utf8');
|
|
44
|
+
const code = src.split('\n').filter((l) => !l.trim().startsWith('//')).join('\n');
|
|
45
|
+
assert.ok(!/isTTY/.test(code), 'surface.js must not branch on isTTY');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ── 2. What each reader gets ────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
test('actions name a command the reader can actually run', () => {
|
|
51
|
+
assert.equal(surface.action('ship', 123, AGENT_ENV), '/builder-ship 123');
|
|
52
|
+
assert.equal(surface.action('ship', 123, TERM_ENV), 'bongos ship 123');
|
|
53
|
+
assert.equal(surface.action('start', null, AGENT_ENV), '/builder-start');
|
|
54
|
+
assert.equal(surface.action('start', null, TERM_ENV), 'bongos start');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('task refs stay hall links for agents and go plain for terminals', () => {
|
|
58
|
+
const url = 'https://example.com/builders#/task/';
|
|
59
|
+
assert.equal(surface.taskRef(7, url, AGENT_ENV), '[#7](https://example.com/builders#/task/7)');
|
|
60
|
+
assert.equal(surface.taskRef(7, url, TERM_ENV), '#7');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('emphasis is markdown for agents and bare text for terminals', () => {
|
|
64
|
+
assert.equal(surface.strong('hi', AGENT_ENV), '**hi**');
|
|
65
|
+
assert.equal(surface.strong('hi', TERM_ENV), 'hi');
|
|
66
|
+
assert.equal(surface.em('hi', AGENT_ENV), '_hi_');
|
|
67
|
+
assert.equal(surface.em('hi', TERM_ENV), 'hi');
|
|
68
|
+
assert.equal(surface.code('x', AGENT_ENV), '`x`');
|
|
69
|
+
assert.equal(surface.code('x', TERM_ENV), 'x');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('the agent table is still GitHub-flavoured markdown, byte for byte', () => {
|
|
73
|
+
const out = surface.table(['#', 'What'], [['a', 'b']], AGENT_ENV);
|
|
74
|
+
assert.equal(out, '| # | What |\n|---|---|\n| a | b |');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('the terminal table aligns into columns with no pipe soup', () => {
|
|
78
|
+
const out = surface.table(['#', 'What'], [['1', 'short'], ['22', 'a longer one']], TERM_ENV);
|
|
79
|
+
const lines = out.split('\n');
|
|
80
|
+
assert.ok(!out.includes('|'), `no pipes in a terminal table:\n${out}`);
|
|
81
|
+
assert.ok(!/\|---/.test(out), 'no markdown separator row');
|
|
82
|
+
assert.match(lines[1], /^─+ {2}─+$/, 'a rule under the header');
|
|
83
|
+
// Column two must start at the same offset on every row.
|
|
84
|
+
const col2 = lines.filter((_, i) => i !== 1).map((l) => l.indexOf(l.trim().split(/\s{2,}/)[1]));
|
|
85
|
+
assert.equal(new Set(col2).size, 1, `column 2 misaligned:\n${out}`);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('no trailing whitespace on a terminal row', () => {
|
|
89
|
+
const out = surface.table(['a', 'bbbb'], [['x', 'y']], TERM_ENV);
|
|
90
|
+
for (const line of out.split('\n')) assert.ok(!/\s$/.test(line), `trailing space: ${JSON.stringify(line)}`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── 3. The call sites ───────────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
test('start.js routes every reader-facing idiom through the surface', () => {
|
|
96
|
+
const src = require('node:fs').readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'start.js'), 'utf8');
|
|
97
|
+
const code = src.split('\n').filter((l) => !l.trim().startsWith('//')).join('\n');
|
|
98
|
+
|
|
99
|
+
// Two exemptions, both agent-only BY CONSTRUCTION:
|
|
100
|
+
// • CARD_DIRECTIVE describes the card TO the model, so naming the slash command is correct.
|
|
101
|
+
// • the `--widget` HTML renderer only ever runs for a session with show_widget; its markup
|
|
102
|
+
// is identifiable by hesc()/class="bs-" and never reaches a terminal.
|
|
103
|
+
const scanned = code
|
|
104
|
+
.replace(/const CARD_DIRECTIVE = \{[\s\S]*?\};/, '')
|
|
105
|
+
.split('\n')
|
|
106
|
+
.filter((l) => !l.includes('hesc(') && !l.includes('class="bs-'))
|
|
107
|
+
.join('\n');
|
|
108
|
+
const slashes = [...scanned.matchAll(/\/builder-[a-z-]+/g)].map((m) => m[0]);
|
|
109
|
+
assert.deepEqual(slashes, [], `hard-coded slash commands left in start.js: ${slashes.join(', ')}`);
|
|
110
|
+
|
|
111
|
+
// A hall link built by hand would bypass the switch.
|
|
112
|
+
const rawRefs = [...code.matchAll(/\[#\$\{[^}]+\}\]\(/g)].map((m) => m[0]);
|
|
113
|
+
assert.deepEqual(rawRefs, [], 'hand-built markdown task links left in start.js');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('the card directive is suppressed for a terminal reader', () => {
|
|
117
|
+
const src = require('node:fs').readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'start.js'), 'utf8');
|
|
118
|
+
assert.match(
|
|
119
|
+
src,
|
|
120
|
+
/function emitCardDirective\(form\) \{[\s\S]{0,400}?surface\.isTerminal\(\)\) return;/,
|
|
121
|
+
'emitCardDirective must return early on a terminal surface',
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('only the dispatchers declare the terminal surface', () => {
|
|
126
|
+
const fs = require('node:fs');
|
|
127
|
+
const dispatcher = fs.readFileSync(path.join(REPO_ROOT, 'bin', 'bongos.js'), 'utf8');
|
|
128
|
+
assert.match(dispatcher, /BONGOS_SURFACE: 'terminal'/, 'bin/bongos.js must mark verb spawns as terminal');
|
|
129
|
+
|
|
130
|
+
// `exec` is the agent's escape hatch into a core script and must NOT be marked.
|
|
131
|
+
const execLine = dispatcher.split('\n').find((l) => l.includes('args.slice(2)') && l.includes('spawnSync'));
|
|
132
|
+
assert.ok(execLine, 'could not find the exec spawn');
|
|
133
|
+
assert.ok(!execLine.includes('BONGOS_SURFACE'), '`bongos exec` must stay on the agent surface');
|
|
134
|
+
|
|
135
|
+
const builder = fs.readFileSync(path.join(REPO_ROOT, 'scripts', 'gds', 'build-cli-package.js'), 'utf8');
|
|
136
|
+
assert.match(builder, /BONGOS_SURFACE: 'terminal'/, 'the generated dispatcher must mark spawns as terminal');
|
|
137
|
+
assert.match(builder, /'scripts\/gds\/surface\.js'/, 'surface.js must ship in the package');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('the skills still invoke the scripts directly, which is what keeps agents on the agent surface', () => {
|
|
141
|
+
// If a skill ever routed through `bongos <verb>`, it would silently inherit the terminal
|
|
142
|
+
// surface and the card contract would break with no test failing anywhere else.
|
|
143
|
+
const fs = require('node:fs');
|
|
144
|
+
const skill = fs.readFileSync(path.join(REPO_ROOT, '.claude', 'skills', 'builder-start', 'SKILL.md'), 'utf8');
|
|
145
|
+
assert.match(skill, /node scripts\/gds\/start\.js/, 'builder-start must call the script directly');
|
|
146
|
+
});
|
package/tests/module_api.mjs
CHANGED
|
@@ -38,6 +38,7 @@ const PUBLISHED_SURFACE = [
|
|
|
38
38
|
'resolveRotDays', 'projectSettings', // added by task 1003279 (ADR 0232; goal 1000072): the ROT timer + the live knob store behind it. A DIFFERENT question from the stale-claim timer above — days not hours, DB not env, work nobody picked up rather than a builder who went quiet, and it only ever ASKS a human where that one ACTS. Both ride the doorway so no module hardcodes either.
|
|
39
39
|
'joinabilityMode', // added by task 1003044 (ADR 0194; the carrier after 1.19.374): the project's membership door, one reader for the sign-in gate AND the onboarding module's public POST
|
|
40
40
|
'projectJoinDoor', // added by task 1002331 (ADR 0182 D6 / ADR 0247): the COMPOSED join/apply door — visibility × join grant × joinability, one answer, so a module reading a single knob cannot re-open a door the other two closed
|
|
41
|
+
'readApplicantProfileFromHub', // added by task 1002972 (privacy spec D7): the reviewer queue's LIVE per-render read of one applicant's hub profile view. NARROW by design — the underlying call is authenticated with this instance's hub client secret, and loadIdpConfig stays OFF the doorway so no module ever holds the credential that speaks for the whole project
|
|
41
42
|
'enabledDisciplines', // BV1.R81: instance offered-disciplines (1.9.0; onboarding restock backstop)
|
|
42
43
|
'registerProvider', 'hasProvider', 'resolve', 'resolveOptional', 'listPorts', 'verifyPortsSatisfied',
|
|
43
44
|
'on', 'emit', 'emitAsync', 'listEvents', 'seamSnapshot',
|
|
@@ -803,6 +803,25 @@ test('each route spells its OWN gate in its OWN middleware list', () => {
|
|
|
803
803
|
// 7. IT GRANTS NOTHING (ADR 0016)
|
|
804
804
|
// ===========================================================================
|
|
805
805
|
|
|
806
|
+
// The first-party files allowed to read the sliver module, each with the reason
|
|
807
|
+
// it is not a DECIDER. The list is the assertion: a new name here is a claim
|
|
808
|
+
// that must be argued, not a line added to make a red test green.
|
|
809
|
+
//
|
|
810
|
+
// routes/scouting.js serializes the sliver onto its one archon-gated
|
|
811
|
+
// route. The original sole reader.
|
|
812
|
+
// applicant-profile.js reuses the PROJECTION for the D7 applicant view
|
|
813
|
+
// (task 1002972) and supplies its own consent floor —
|
|
814
|
+
// see sliverShapeFor's contract. Sharing the
|
|
815
|
+
// projection is what keeps D7's "no new disclosure
|
|
816
|
+
// class" literally true: the alternative was a second
|
|
817
|
+
// five-key object free to drift from this one field
|
|
818
|
+
// by field, which this whitelist proof would not
|
|
819
|
+
// cover. It reads the shape; it decides nothing.
|
|
820
|
+
const SLIVER_READERS = [
|
|
821
|
+
'modules/platform-identity/applicant-profile.js',
|
|
822
|
+
'modules/platform-identity/routes/scouting.js',
|
|
823
|
+
];
|
|
824
|
+
|
|
806
825
|
test('the sliver module is read by exactly one first-party server file — the one that serializes it', () => {
|
|
807
826
|
const files = [];
|
|
808
827
|
const walk = (dir) => {
|
|
@@ -817,8 +836,22 @@ test('the sliver module is read by exactly one first-party server file — the o
|
|
|
817
836
|
const readers = files
|
|
818
837
|
.filter((f) => /require\(\s*['"][^'"]*recruiter-sliver['"]\s*\)/.test(fs.readFileSync(f, 'utf8')))
|
|
819
838
|
.map((f) => path.relative(ROOT, f).split(path.sep).join('/'));
|
|
820
|
-
assert.deepEqual(readers,
|
|
821
|
-
'nothing may consult the sliver to DECIDE anything: cross-project numbers are descriptive analytics, never authority (ADR 0016)'
|
|
839
|
+
assert.deepEqual(readers, SLIVER_READERS,
|
|
840
|
+
'nothing may consult the sliver to DECIDE anything: cross-project numbers are descriptive analytics, never authority (ADR 0016). '
|
|
841
|
+
+ 'A new reader needs a stated reason in SLIVER_READERS — and it must serialize the shape, never branch on it.');
|
|
842
|
+
|
|
843
|
+
// The reason each allowance rests on, enforced rather than trusted: a reader
|
|
844
|
+
// may PROJECT the sliver, never READ A VALUE OUT OF IT to decide something. A
|
|
845
|
+
// comparison against a sliver field is what "authority" would look like here,
|
|
846
|
+
// so the shape of that mistake is what this refuses.
|
|
847
|
+
for (const rel of SLIVER_READERS) {
|
|
848
|
+
const code = fs.readFileSync(path.join(ROOT, rel), 'utf8')
|
|
849
|
+
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
|
850
|
+
for (const key of SLIVER_KEYS) {
|
|
851
|
+
assert.doesNotMatch(code, new RegExp(`\\.${key}\\s*(?:===|!==|==|!=|>|<)`),
|
|
852
|
+
`${rel} compares a sliver's \`${key}\` — a reader that BRANCHES on this data is consulting descriptive analytics as authority (ADR 0016)`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
822
855
|
});
|
|
823
856
|
|
|
824
857
|
test('no rank, role, permission or membership travels with the sliver, under any name', async () => {
|
|
@@ -378,4 +378,98 @@ await test('the section lede tells the owner the queue is more than one decision
|
|
|
378
378
|
assert.match(h.lede(), /decided/i);
|
|
379
379
|
});
|
|
380
380
|
|
|
381
|
+
// ===========================================================================
|
|
382
|
+
// The applicant's own profile beside their ask (task 1002972, privacy spec D7)
|
|
383
|
+
// ===========================================================================
|
|
384
|
+
//
|
|
385
|
+
// The SERVER decides what may be disclosed — tests/applicant_profile_boundary.mjs
|
|
386
|
+
// is that proof. What is asserted HERE is the half a reviewer actually sees: that
|
|
387
|
+
// the row draws whichever shape it was handed, draws NOTHING when handed null,
|
|
388
|
+
// and escapes the applicant's own words. A source-text assertion cannot see any
|
|
389
|
+
// of that; this runs the real watch.js over the real markup.
|
|
390
|
+
|
|
391
|
+
const PUBLIC_PROFILE = {
|
|
392
|
+
view: 'public',
|
|
393
|
+
account: {
|
|
394
|
+
handle: 'mona', github_login: 'octocat', display_name: 'Mona Lisa',
|
|
395
|
+
avatar_url: 'https://avatars.githubusercontent.com/u/583231?v=4',
|
|
396
|
+
member_since: '2025-01-01T00:00:00Z', bio: 'I draw maps.', links: null,
|
|
397
|
+
},
|
|
398
|
+
cross_project: {
|
|
399
|
+
totals: { projects: 3, credits: 4200, works_shipped: 41, karma: 7 },
|
|
400
|
+
disciplines: ['engineer', 'artist'], last_active: '2026-08-30T00:00:00Z',
|
|
401
|
+
projects: [], more_projects: 0,
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
const SLIVER_PROFILE = {
|
|
405
|
+
view: 'sliver',
|
|
406
|
+
sliver: { handle: 'quiet', display_name: 'Quiet Person', avatar_url: null, disciplines: ['engineer'], active_recently: true },
|
|
407
|
+
};
|
|
408
|
+
const withProfile = (profile, over = {}) => [{ ...WAITING[0], profile, ...over }];
|
|
409
|
+
|
|
410
|
+
await test('a PUBLIC applicant renders their record beside the ask', async () => {
|
|
411
|
+
const h = await boot({ rows: { pending: withProfile(PUBLIC_PROFILE), invited: [], dismissed: [] } });
|
|
412
|
+
const block_ = h.rows()[0].querySelector('.ov-row__profile');
|
|
413
|
+
assert.ok(block_, 'the public applicant view rendered no profile block at all');
|
|
414
|
+
const text = block_.textContent;
|
|
415
|
+
assert.match(text, /I draw maps\./, 'the bio is part of what the public page already shows');
|
|
416
|
+
assert.match(text, /3 projects/);
|
|
417
|
+
assert.match(text, /41 shipped/);
|
|
418
|
+
assert.match(text, /engineer/);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
await test('a PRIVATE applicant renders the sliver AND says why it is thin', async () => {
|
|
422
|
+
const h = await boot({ rows: { pending: withProfile(SLIVER_PROFILE), invited: [], dismissed: [] } });
|
|
423
|
+
const block_ = h.rows()[0].querySelector('.ov-row__profile');
|
|
424
|
+
assert.ok(block_, 'the sliver rendered no profile block');
|
|
425
|
+
const text = block_.textContent;
|
|
426
|
+
assert.match(text, /Private account/,
|
|
427
|
+
'a reviewer must not read the thinness as a poor record — it is a boundary, and applying is what opened even this much');
|
|
428
|
+
assert.match(text, /engineer/);
|
|
429
|
+
assert.match(text, /Active recently/);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
await test('hidden stats: a public applicant keeps their name and says the record is hidden', async () => {
|
|
433
|
+
const hidden = { ...PUBLIC_PROFILE, cross_project: null };
|
|
434
|
+
const h = await boot({ rows: { pending: withProfile(hidden), invited: [], dismissed: [] } });
|
|
435
|
+
const text = h.rows()[0].querySelector('.ov-row__profile').textContent;
|
|
436
|
+
assert.match(text, /hides their cross-project record/,
|
|
437
|
+
'a blank would read as "this applicant has done nothing", which is a different claim entirely');
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
await test('NO profile is not an error — the row renders exactly as it always did', async () => {
|
|
441
|
+
const h = await boot({ rows: { pending: withProfile(null), invited: [], dismissed: [] } });
|
|
442
|
+
const row = h.rows()[0];
|
|
443
|
+
assert.equal(row.querySelector('.ov-row__profile'), null,
|
|
444
|
+
'null covers unvouched, no hub, hub down and hidden alike — none of them may draw a box');
|
|
445
|
+
assert.ok(row.querySelector('.fact-pill'), 'and the vouch pill still says whether anyone confirmed the name');
|
|
446
|
+
assert.match(row.textContent, /octocat/);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
await test('an unrecognised view shape draws nothing rather than guessing', async () => {
|
|
450
|
+
const h = await boot({ rows: { pending: withProfile({ view: 'everything', account: { bio: 'x' } }), invited: [], dismissed: [] } });
|
|
451
|
+
assert.equal(h.rows()[0].querySelector('.ov-row__profile'), null,
|
|
452
|
+
'a shape this page does not know is a shape it must not render — fail closed');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
await test('an applicant own words are ESCAPED, never linkified (the render contract)', async () => {
|
|
456
|
+
const nasty = {
|
|
457
|
+
...PUBLIC_PROFILE,
|
|
458
|
+
account: { ...PUBLIC_PROFILE.account, bio: '<img src=x onerror=alert(1)>ping http://evil.example' },
|
|
459
|
+
};
|
|
460
|
+
const h = await boot({ rows: { pending: withProfile(nasty), invited: [], dismissed: [] } });
|
|
461
|
+
const block_ = h.rows()[0].querySelector('.ov-row__profile');
|
|
462
|
+
assert.equal(block_.querySelectorAll('img').length, 0, 'a bio is user content on a page an Archon reads');
|
|
463
|
+
assert.equal(block_.querySelectorAll('a').length, 0, 'the contract says never linkify — an Archon must not be handed a one-click link a stranger wrote');
|
|
464
|
+
assert.match(block_.textContent, /evil\.example/, 'the text itself is still shown, just inert');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
await test('the profile changes what the row SAYS, never what it offers (ADR 0016)', async () => {
|
|
468
|
+
const rich = await boot({ rows: { pending: withProfile(PUBLIC_PROFILE), invited: [], dismissed: [] } });
|
|
469
|
+
const bare = await boot({ rows: { pending: withProfile(null), invited: [], dismissed: [] } });
|
|
470
|
+
const actionsOf = (h) => [...h.rows()[0].querySelectorAll('button')].map((b) => b.textContent.trim()).sort();
|
|
471
|
+
assert.deepEqual(actionsOf(rich), actionsOf(bare),
|
|
472
|
+
'a rich record is not a reason to admit and an empty one is not a reason to refuse — the decisions must be identical');
|
|
473
|
+
});
|
|
474
|
+
|
|
381
475
|
summary();
|