@bongos/core 1.21.172 → 1.21.174
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 +53 -38
- package/docs/adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md +10 -0
- package/docs/api/openapi.json +1 -1
- package/docs/copy-inventory.md +75 -72
- package/docs/copy-registry.json +117 -90
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +2 -1
- package/docs/page-readings.json +122 -120
- package/modules/copy-desk/routes/copy-desk.js +4 -4
- package/modules/hall-ui/public/city-decide.js +36 -2
- package/modules/hall-ui/public/city.css +8 -2
- package/modules/hall-ui/public/city.html +5 -5
- package/modules/lifecycle/artist-review-goal.js +103 -0
- package/modules/lifecycle/db-tasks.js +17 -1
- package/modules/lifecycle/db-versions.js +5 -1
- package/modules/lifecycle/page-tweak-claim.js +3 -3
- package/modules/lifecycle/page-tweak-write.js +12 -4
- package/modules/public-landing/public/index.html +9 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +24 -0
- package/src/bongos/auth-github.js +6 -1
- package/src/bongos/module-listing.js +76 -11
- package/src/bongos/routes/auth.js +6 -2
- package/src/bongos/routes/modules.js +15 -1
- package/src/module-api.js +1 -1
- package/tests/artist_review_goal.mjs +119 -0
- package/tests/auth_switch_account.mjs +38 -0
- package/tests/copy_desk_page_claim.mjs +6 -1
- package/tests/copy_desk_page_submit.mjs +1 -1
- package/tests/fixtures/page-ledger.mjs +13 -0
- package/tests/hall_city_rewording.mjs +11 -0
- package/tests/module_store_listing.mjs +105 -5
- package/tests/speciality_injection.mjs +20 -0
- package/tests/task_goal_required.mjs +12 -2
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// tests/auth_switch_account.mjs — the home page's "New account" door (task 1004789).
|
|
2
|
+
//
|
|
3
|
+
// WHY. GitHub silently re-uses whoever is logged in to github.com in the browser,
|
|
4
|
+
// so a second Cloud Bongos account was unreachable. ?switch=1 on /auth/web/start
|
|
5
|
+
// must add prompt=select_account (GitHub's account picker); a plain sign-in must not.
|
|
6
|
+
import { strict as assert } from 'node:assert';
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
|
+
process.env.BONGOS_GITHUB_CLIENT_ID = process.env.BONGOS_GITHUB_CLIENT_ID || 'test-client';
|
|
16
|
+
process.env.BONGOS_GITHUB_CLIENT_SECRET = process.env.BONGOS_GITHUB_CLIENT_SECRET || 'test-secret';
|
|
17
|
+
const auth = require(path.join(ROOT, 'src/bongos/auth.js'));
|
|
18
|
+
|
|
19
|
+
test('switchAccount adds prompt=select_account; a plain sign-in does not', () => {
|
|
20
|
+
const plain = new URL(auth.buildAuthorizeUrl('s', 'https://x/cb'));
|
|
21
|
+
assert.equal(plain.searchParams.get('prompt'), null);
|
|
22
|
+
const sw = new URL(auth.buildAuthorizeUrl('s', 'https://x/cb', { switchAccount: true }));
|
|
23
|
+
assert.equal(sw.searchParams.get('prompt'), 'select_account');
|
|
24
|
+
assert.equal(sw.searchParams.get('scope'), plain.searchParams.get('scope'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('the start route reads ?switch=1 and passes it to the GitHub leg', () => {
|
|
28
|
+
const src = fs.readFileSync(path.join(ROOT, 'src/bongos/routes/auth.js'), 'utf8');
|
|
29
|
+
assert.match(src, /const wantsSwitch = req\.query\.switch === '1'/);
|
|
30
|
+
assert.match(src, /switchAccount: wantsSwitch/);
|
|
31
|
+
// a switch must reach GitHub, never short-circuit through the live-session handoff
|
|
32
|
+
assert.match(src, /if \(!wantsRepos && !wantsSwitch\) \{/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('the landing carries the New account door beside Sign in', () => {
|
|
36
|
+
const html = fs.readFileSync(path.join(ROOT, 'modules/public-landing/public/index.html'), 'utf8');
|
|
37
|
+
assert.match(html, /<a class="pill newacct" href="\/api\/bongos\/auth\/web\/start\?return=\/&repos=1&switch=1"[^>]*>New account<\/a>/);
|
|
38
|
+
});
|
|
@@ -86,8 +86,13 @@ try {
|
|
|
86
86
|
assert.equal(db.tasks.length, 1);
|
|
87
87
|
assert.deepEqual(
|
|
88
88
|
[t.source, t.source_ref, t.title, t.kind, t.discipline, t.credits_reward, t.version_id, t.goal_id, t.status],
|
|
89
|
-
['page-tweak', 'page-tweak/builders:studio/r1', 'Tweak: Studio (builders:studio)', 'feature', 'artist', 30, 'V9', '
|
|
89
|
+
['page-tweak', 'page-tweak/builders:studio/r1', 'Tweak: Studio (builders:studio)', 'feature', 'artist', 30, 'V9', '88', 'active']
|
|
90
90
|
);
|
|
91
|
+
// task 1004819: the round lands in the version's artist review goal, made on
|
|
92
|
+
// first use with its two standing criteria — never the catch-all.
|
|
93
|
+
assert.equal(db.artistGoal.title, 'V9 — artist review');
|
|
94
|
+
assert.deepEqual(db.criteria.map((x) => [x.criterion_id, x.goal_id]),
|
|
95
|
+
[['artist-every-page-reviewed', '88'], ['artist-pages-simple-to-use', '88']]);
|
|
91
96
|
assert.deepEqual(t.touches, F.STUDIO_FILES, 'touches are the inventory page files');
|
|
92
97
|
const c = db.claims[0];
|
|
93
98
|
assert.deepEqual([c.builder_id, c.worktree_name, c.creator_session_id], [ALICE, null, null], 'a web claim: no worktree, no session');
|
|
@@ -208,7 +208,7 @@ try {
|
|
|
208
208
|
`page-tweak-unplaced/${studioTask().id}/L0005`,
|
|
209
209
|
]);
|
|
210
210
|
for (const t of eng) {
|
|
211
|
-
assert.deepEqual([t.status, t.discipline, t.kind, t.version_id, t.goal_id, t.created_by], ['backlog', 'engineer', 'feature', 'V9', '
|
|
211
|
+
assert.deepEqual([t.status, t.discipline, t.kind, t.version_id, t.goal_id, t.created_by], ['backlog', 'engineer', 'feature', 'V9', '88', ALICE], 'an unplaced line lands in the artist review goal (task 1004819)');
|
|
212
212
|
assert.match(t.description, /builders:studio/);
|
|
213
213
|
}
|
|
214
214
|
const quiet = eng.find((t) => t.source_ref.endsWith('/L0005'));
|
|
@@ -36,6 +36,7 @@ export function installPageLedger(api, { builders }) {
|
|
|
36
36
|
// first commits or rolls back.
|
|
37
37
|
const locks = new Map();
|
|
38
38
|
async function lock(key, owner) {
|
|
39
|
+
if (locks.has(key) && locks.get(key).owner === owner) return; // re-entrant, as Postgres is for one session
|
|
39
40
|
while (locks.has(key)) await locks.get(key).done;
|
|
40
41
|
let release;
|
|
41
42
|
const done = new Promise((r) => { release = r; });
|
|
@@ -109,6 +110,18 @@ export function installPageLedger(api, { builders }) {
|
|
|
109
110
|
if (/FROM goals g JOIN versions v ON v\.id = g\.version_id WHERE g\.version_id = \$1 AND g\.title = \$2/.test(q)) {
|
|
110
111
|
return { rows: [{ id: '77', version_status: 'building' }] };
|
|
111
112
|
}
|
|
113
|
+
// task 1004819: studio work lands in the version's artist review goal, found
|
|
114
|
+
// (or made, with its two criteria) on the same transaction client.
|
|
115
|
+
if (/^SELECT status FROM versions WHERE id = \$1/.test(q)) return { rows: [{ status: 'building' }] };
|
|
116
|
+
if (/^SELECT id, version_id, title FROM goals WHERE version_id = \$1 AND title = \$2/.test(q)) {
|
|
117
|
+
assert.ok(owner, 'the artist review goal must be found on the transaction client');
|
|
118
|
+
return { rows: db.artistGoal ? [{ ...db.artistGoal }] : [] };
|
|
119
|
+
}
|
|
120
|
+
if (/^INSERT INTO goals \(version_id, title, description, scope_modules, status, created_by, sort_order\)/.test(q)) {
|
|
121
|
+
db.artistGoal = { id: '88', version_id: p[0], title: p[1] };
|
|
122
|
+
return { rows: [{ ...db.artistGoal }] };
|
|
123
|
+
}
|
|
124
|
+
if (/^INSERT INTO done_when_criteria/.test(q)) { (db.criteria = db.criteria || []).push({ version_id: p[0], criterion_id: p[1], goal_id: p[3] }); return { rows: [] }; }
|
|
112
125
|
if (/^SELECT scope_modules FROM goals WHERE id = \$1/.test(q)) return { rows: [{ scope_modules: [] }] };
|
|
113
126
|
if (/^INSERT INTO tasks \(version_id, title, description, status, touches,/.test(q)) {
|
|
114
127
|
assert.ok(owner, 'the round must be created inside the claim transaction');
|
|
@@ -49,3 +49,14 @@ test('the "?" asks before it sends: the send lives behind a confirm button', ()
|
|
|
49
49
|
assert.match(fn, /Send this to Claude to rewrite\?/);
|
|
50
50
|
assert.match(fn, /Nothing is decided or changed/);
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
test('a card never cuts what it says: the whole text is one tap away, and only when there is more', () => {
|
|
54
|
+
const G = boot();
|
|
55
|
+
const m = { id: 'override_requests:1', title: 'Override: a long title', detail: 'The whole story, word for word.' };
|
|
56
|
+
assert.equal(G.fullText(m, { question: 'Release it?' }), 'Override: a long title\n\nThe whole story, word for word.');
|
|
57
|
+
assert.equal(G.fullText(m, { question: 'Override: a long title' }), 'The whole story, word for word.', 'the question is not repeated');
|
|
58
|
+
assert.equal(G.fullText({ id: 'x:1', title: 'Same' }, { question: 'Same' }), '', 'nothing more to show, no opener');
|
|
59
|
+
assert.equal(G.fullText(null, { question: 'Q?' }), '');
|
|
60
|
+
const src = fs.readFileSync(path.join(PUB, 'city-decide.js'), 'utf8');
|
|
61
|
+
assert.match(src, /str\(o\.input && o\.input\.label\) \|\| str\(o\.label\)/, 'an answer uses its full words, not the desk chip cut to fit');
|
|
62
|
+
});
|
|
@@ -15,9 +15,18 @@ import { createRequire } from 'node:module';
|
|
|
15
15
|
const require = createRequire(import.meta.url);
|
|
16
16
|
|
|
17
17
|
// ---- the fake registry the route reads ----
|
|
18
|
-
const state = { modules: {}, versions: [], scores: [], signals: [] };
|
|
18
|
+
const state = { modules: {}, versions: [], scores: [], signals: [], prices: [] };
|
|
19
19
|
const pool = {
|
|
20
20
|
async query(sql, params = []) {
|
|
21
|
+
if (/FROM store_module_prices/.test(sql)) {
|
|
22
|
+
const newest = new Map();
|
|
23
|
+
for (const p of state.prices) {
|
|
24
|
+
if (!params[0].includes(p.module_key)) continue;
|
|
25
|
+
const have = newest.get(p.module_key);
|
|
26
|
+
if (!have || p.set_at > have.set_at) newest.set(p.module_key, p);
|
|
27
|
+
}
|
|
28
|
+
return { rows: [...newest.values()] };
|
|
29
|
+
}
|
|
21
30
|
if (/FROM store_modules m JOIN store_module_versions v/.test(sql)) {
|
|
22
31
|
return {
|
|
23
32
|
rows: state.versions
|
|
@@ -53,7 +62,7 @@ const poolPath = require.resolve('../src/bongos/pool.js');
|
|
|
53
62
|
require.cache[poolPath] = { id: poolPath, filename: poolPath, loaded: true, exports: { pool: { connect: async () => ({ ...pool, release() {} }), query: pool.query } } };
|
|
54
63
|
|
|
55
64
|
const buildModulesRouter = require('../src/bongos/routes/modules.js');
|
|
56
|
-
const { QUALITY_FLOOR, SORT_PARTS, placeVersion, sortByPart } = require('../src/bongos/module-listing.js');
|
|
65
|
+
const { QUALITY_FLOOR, SORT_PARTS, RANKING, placeVersion, sortByPart, priceOf, withinMaxPrice, rankListed } = require('../src/bongos/module-listing.js');
|
|
57
66
|
|
|
58
67
|
function handlerFor(router, method, p) {
|
|
59
68
|
const layer = router.stack.find((l) => l.route && l.route.path === p && l.route.methods[method]);
|
|
@@ -153,7 +162,7 @@ test('GET /store/modules: lists by the newest version\'s current score, drops wh
|
|
|
153
162
|
assert.equal(res.statusCode, 200);
|
|
154
163
|
assert.equal(res.headers['Cache-Control'], 'no-store');
|
|
155
164
|
assert.equal(res.body.floor, 50);
|
|
156
|
-
assert.deepEqual(res.body.listed.map((m) => [m.key, m.version, m.overall]), [['
|
|
165
|
+
assert.deepEqual(res.body.listed.map((m) => [m.key, m.version, m.overall]), [['good', '1.10.0', 80], ['fixed', '2.0.0', 60]], 'highest score first');
|
|
157
166
|
assert.deepEqual(res.body.new.map((m) => [m.key, m.is_new, m.overall]), [['fresh', true, null]]);
|
|
158
167
|
const all = [...res.body.listed, ...res.body.new].map((m) => m.key);
|
|
159
168
|
for (const k of ['low', 'unsafe', 'unscored', 'gone']) assert.ok(!all.includes(k), `${k} must not be listed`);
|
|
@@ -163,7 +172,7 @@ test('GET /store/modules: an empty store answers empty lists and the floor', asy
|
|
|
163
172
|
state.modules = {}; state.versions = []; state.scores = [];
|
|
164
173
|
const { handle } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
|
|
165
174
|
const res = await invoke(handle, {});
|
|
166
|
-
assert.deepEqual(res.body, { floor: 50, sort: null, sort_parts: SORT_PARTS, listed: [], new: [] });
|
|
175
|
+
assert.deepEqual(res.body, { floor: 50, ranking: RANKING, sort: null, sort_parts: SORT_PARTS, max_price: null, listed: [], new: [] });
|
|
167
176
|
});
|
|
168
177
|
|
|
169
178
|
// ---------------------------------------------------------------------------
|
|
@@ -229,7 +238,7 @@ test('GET /store/modules?sort=: orders the listed modules by the part, leaves Ne
|
|
|
229
238
|
assert.deepEqual(byTests.body.listed.map((m) => m.key), ['tested', 'steady']);
|
|
230
239
|
const plain = await invoke(handle, { query: {} });
|
|
231
240
|
assert.equal(plain.body.sort, null);
|
|
232
|
-
assert.deepEqual(plain.body.listed.map((m) => m.key), ['steady', 'tested'], 'no sort is
|
|
241
|
+
assert.deepEqual(plain.body.listed.map((m) => m.key), ['steady', 'tested'], 'no sort is the ranking: equal scores, both free, so key order');
|
|
233
242
|
state.signals = [];
|
|
234
243
|
});
|
|
235
244
|
|
|
@@ -307,3 +316,94 @@ test('the criterion: two modules with the same overall score read differently',
|
|
|
307
316
|
assert.equal(by.steady.parts.reliability.score, 90);
|
|
308
317
|
state.signals = [];
|
|
309
318
|
});
|
|
319
|
+
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// the ranking: quality first, price breaks ties, optional max price (task 1003808)
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
test('priceOf: no price row is free; 0 in either currency is free; null is not offered', () => {
|
|
325
|
+
assert.deepEqual(priceOf(null), { cents: null, credits: null, free: true });
|
|
326
|
+
assert.deepEqual(priceOf({ price_cents: 0, price_credits: null }), { cents: 0, credits: null, free: true });
|
|
327
|
+
assert.deepEqual(priceOf({ price_cents: 500, price_credits: 0 }), { cents: 500, credits: 0, free: true });
|
|
328
|
+
assert.deepEqual(priceOf({ price_cents: 500, price_credits: null }), { cents: 500, credits: null, free: false });
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('rankListed: a higher score always wins, whatever it costs — price never buys a place', () => {
|
|
332
|
+
const ranked = rankListed([
|
|
333
|
+
{ key: 'cheap', overall: 70, price: priceOf(null) },
|
|
334
|
+
{ key: 'dear', overall: 90, price: priceOf({ price_cents: 99900 }) },
|
|
335
|
+
]);
|
|
336
|
+
assert.deepEqual(ranked.map((e) => e.key), ['dear', 'cheap']);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('rankListed: between equal scores, free first, then lowest cash, then lowest credits, then key', () => {
|
|
340
|
+
const input = [
|
|
341
|
+
{ key: 'z-credits', overall: 80, price: priceOf({ price_credits: 5 }) },
|
|
342
|
+
{ key: 'cents-900', overall: 80, price: priceOf({ price_cents: 900 }) },
|
|
343
|
+
{ key: 'cents-100', overall: 80, price: priceOf({ price_cents: 100 }) },
|
|
344
|
+
{ key: 'b-free', overall: 80, price: priceOf(null) },
|
|
345
|
+
{ key: 'a-free', overall: 80, price: priceOf({ price_cents: 0 }) },
|
|
346
|
+
];
|
|
347
|
+
assert.deepEqual(rankListed(input).map((e) => e.key), ['a-free', 'b-free', 'cents-100', 'cents-900', 'z-credits']);
|
|
348
|
+
assert.equal(input[0].key, 'z-credits', 'the input is untouched');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('withinMaxPrice: free always passes; a priced module must be offered at or under a cap the buyer set', () => {
|
|
352
|
+
const p = priceOf({ price_cents: 500, price_credits: 7 });
|
|
353
|
+
assert.equal(withinMaxPrice(p, null), true, 'no cap, no filter');
|
|
354
|
+
assert.equal(withinMaxPrice(p, { cents: 500, credits: null }), true, 'at the cap is in');
|
|
355
|
+
assert.equal(withinMaxPrice(p, { cents: 499, credits: null }), false);
|
|
356
|
+
assert.equal(withinMaxPrice(p, { cents: 1, credits: 7 }), true, 'either currency under its cap is enough');
|
|
357
|
+
assert.equal(withinMaxPrice(priceOf({ price_cents: 500 }), { cents: null, credits: 100 }), false, 'not offered in credits, so a credit cap cannot let it in');
|
|
358
|
+
assert.equal(withinMaxPrice(priceOf(null), { cents: 0, credits: null }), true);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('GET /store/modules: ranks by score then price, and ?max_cents= leaves out what costs more', async () => {
|
|
362
|
+
state.modules = {
|
|
363
|
+
top: { status: 'listed', title: 'Top' }, 'tie-paid': { status: 'listed', title: 'Tie paid' },
|
|
364
|
+
'tie-free': { status: 'listed', title: 'Tie free' }, fresh: { status: 'listed', title: 'Fresh' },
|
|
365
|
+
};
|
|
366
|
+
state.versions = [
|
|
367
|
+
{ id: 31, module_key: 'top', version: '1.0.0', published_at: 't' },
|
|
368
|
+
{ id: 32, module_key: 'tie-paid', version: '1.0.0', published_at: 't' },
|
|
369
|
+
{ id: 33, module_key: 'tie-free', version: '1.0.0', published_at: 't' },
|
|
370
|
+
{ id: 34, module_key: 'fresh', version: '1.0.0', published_at: 't' },
|
|
371
|
+
];
|
|
372
|
+
state.scores = [
|
|
373
|
+
{ id: 31, version_id: 31, computed_at: 1, ...score({ overall: 92 }) },
|
|
374
|
+
{ id: 32, version_id: 32, computed_at: 1, ...score({ overall: 75 }) },
|
|
375
|
+
{ id: 33, version_id: 33, computed_at: 1, ...score({ overall: 75 }) },
|
|
376
|
+
{ id: 34, version_id: 34, computed_at: 1, ...score({ is_new: true }) },
|
|
377
|
+
];
|
|
378
|
+
state.signals = [];
|
|
379
|
+
state.prices = [
|
|
380
|
+
{ module_key: 'top', price_cents: 2000, price_credits: null, set_at: 1 },
|
|
381
|
+
{ module_key: 'tie-paid', price_cents: 900, price_credits: null, set_at: 1 },
|
|
382
|
+
{ module_key: 'tie-paid', price_cents: 500, price_credits: null, set_at: 2 },
|
|
383
|
+
{ module_key: 'fresh', price_cents: 3000, price_credits: null, set_at: 1 },
|
|
384
|
+
];
|
|
385
|
+
const { handle } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
|
|
386
|
+
const all = await invoke(handle, { query: {} });
|
|
387
|
+
assert.equal(all.body.ranking, RANKING);
|
|
388
|
+
assert.deepEqual(all.body.listed.map((m) => m.key), ['top', 'tie-free', 'tie-paid']);
|
|
389
|
+
assert.deepEqual(all.body.listed.map((m) => m.price.cents), [2000, null, 500], 'the CURRENT price is shown');
|
|
390
|
+
assert.equal(all.body.max_price, null);
|
|
391
|
+
|
|
392
|
+
const capped = await invoke(handle, { query: { max_cents: '1000' } });
|
|
393
|
+
assert.deepEqual(capped.body.max_price, { cents: 1000, credits: null });
|
|
394
|
+
assert.deepEqual(capped.body.listed.map((m) => m.key), ['tie-free', 'tie-paid']);
|
|
395
|
+
assert.deepEqual(capped.body.new, [], 'the cap applies to New modules too');
|
|
396
|
+
|
|
397
|
+
const free = await invoke(handle, { query: { max_cents: '0' } });
|
|
398
|
+
assert.deepEqual(free.body.listed.map((m) => m.key), ['tie-free'], 'a cap of 0 is free only');
|
|
399
|
+
state.prices = [];
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test('GET /store/modules: a maximum price that is not a whole number of 0 or more is refused', async () => {
|
|
403
|
+
const { handle } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
|
|
404
|
+
for (const query of [{ max_cents: '-1' }, { max_cents: '5.5' }, { max_credits: 'ten' }, { max_cents: ['1', '2'] }]) {
|
|
405
|
+
const res = await invoke(handle, { query });
|
|
406
|
+
assert.equal(res.statusCode, 400, JSON.stringify(query));
|
|
407
|
+
assert.match(res.body.message, /maximum price/);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
@@ -105,3 +105,23 @@ test('the adoption read selects the contract itself', () => {
|
|
|
105
105
|
// while still looking well-formed.
|
|
106
106
|
assert.match(src('modules/specialities/db.js'), /SELECT s\.id, s\.slug, s\.name, s\.discipline, s\.summary, s\.contract_md/);
|
|
107
107
|
});
|
|
108
|
+
|
|
109
|
+
// task 1004817: two sessions, two worktrees, two claims in two crafts. Each
|
|
110
|
+
// session gets ONLY the contract for the claim bound to its own worktree; the
|
|
111
|
+
// account-wide pool is only the fallback when no claim is bound here.
|
|
112
|
+
test('two claims in two worktrees: each session gets only its own contract', () => {
|
|
113
|
+
const claims = [
|
|
114
|
+
{ ...claim('artist'), task_id: '1', worktree_name: 'task-1' },
|
|
115
|
+
{ ...claim('engineer'), task_id: '2', worktree_name: 'task-2' },
|
|
116
|
+
];
|
|
117
|
+
const specs = [
|
|
118
|
+
sp({ discipline: 'artist', contract: 'Speciality: Pixel art (under artist).' }),
|
|
119
|
+
sp({ discipline: 'engineer', contract: 'Speciality: Compilers (under engineer).' }),
|
|
120
|
+
];
|
|
121
|
+
const got = (worktree) => ids(conductor.evaluateTriggers({ activeClaims: claims, specialities: specs, worktree }))
|
|
122
|
+
.filter((i) => String(i).startsWith('speciality:'));
|
|
123
|
+
assert.deepEqual(got('task-1'), ['speciality:artist']);
|
|
124
|
+
assert.deepEqual(got('task-2'), ['speciality:engineer']);
|
|
125
|
+
// No claim bound to this tree (main checkout): today's behaviour, every craft in play.
|
|
126
|
+
assert.deepEqual(got('main-checkout').sort(), ['speciality:artist', 'speciality:engineer']);
|
|
127
|
+
});
|
|
@@ -113,12 +113,11 @@ test('POST /tasks does NOT pass the escape hatch — this door always has a goal
|
|
|
113
113
|
|
|
114
114
|
const OPT_INS = [
|
|
115
115
|
['modules/onboarding/newcomer-restock.js', 'the beginner queue, stamped from templates'],
|
|
116
|
-
['modules/copy-desk/routes/copy-desk.js', 'copy-change proposals'],
|
|
117
116
|
['modules/lifecycle/kickoff-seed.js', "the founder's first board, on an instance with no goals at all"],
|
|
118
117
|
['modules/lifecycle/cascade.js', 'a follow-up off a parent task that is itself goal-less'],
|
|
119
118
|
];
|
|
120
119
|
|
|
121
|
-
test('exactly
|
|
120
|
+
test('exactly three vectors opt in, and each says why', () => {
|
|
122
121
|
for (const [file, why] of OPT_INS) {
|
|
123
122
|
const body = src(file);
|
|
124
123
|
assert.match(body, /allowCatchAll/, `${file} should opt in (${why})`);
|
|
@@ -127,6 +126,17 @@ test('exactly four vectors opt in, and each says why', () => {
|
|
|
127
126
|
}
|
|
128
127
|
});
|
|
129
128
|
|
|
129
|
+
// task 1004819: studio work names its home instead of the catch-all. Each of the
|
|
130
|
+
// three studio create vectors passes artistReview, which lands it in the
|
|
131
|
+
// version's artist review goal; none of them opts into the catch-all any more.
|
|
132
|
+
test('the three studio vectors land in the artist review goal, not the catch-all', () => {
|
|
133
|
+
for (const file of ['modules/copy-desk/routes/copy-desk.js', 'modules/lifecycle/page-tweak-claim.js', 'modules/lifecycle/page-tweak-write.js']) {
|
|
134
|
+
const body = src(file);
|
|
135
|
+
assert.match(body, /artistReview: true/, `${file} should name the artist review goal`);
|
|
136
|
+
assert.equal(/allowCatchAll/.test(body), false, `${file} must not opt into the catch-all`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
130
140
|
test('cascade opts in ONLY when inheritance actually failed', () => {
|
|
131
141
|
// The others opt in unconditionally because they have no goal to name at all.
|
|
132
142
|
// A cascade normally inherits its parent's goal and must keep doing so — the
|