@bongos/core 1.24.14 → 1.24.16
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 +308 -138
- package/.claude/skills/blocker-review/SKILL.md +2 -0
- package/.claude/skills/session-board/SKILL.md +29 -0
- package/.claude/skills/strand-fix/SKILL.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +14 -0
- package/clients/bongos-client/index.cjs +14 -0
- package/clients/bongos-client/index.d.ts +23 -2
- package/clients/bongos-client/index.mjs +14 -0
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +4 -0
- package/docs/api/openapi.json +369 -6
- package/docs/api-reference.md +19 -4
- package/docs/architecture.md +6 -0
- package/docs/copy-inventory.md +231 -204
- package/docs/copy-registry.json +488 -255
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -2
- package/docs/page-inventory.json +7 -1
- package/docs/page-readings.json +1132 -1103
- package/migrations/core_289_task_short_name.sql +34 -0
- package/modules/ai-studio/db.js +10 -0
- package/modules/ai-studio/gemini.js +73 -5
- package/modules/ai-studio/openrouter.js +17 -2
- package/modules/ai-studio/routes/ai-studio.js +7 -1
- package/modules/ai-studio/studio.js +8 -0
- package/modules/builder-settings/checkout-freshness.js +71 -0
- package/modules/builder-settings/routes/settings.js +26 -0
- package/modules/builder-settings/settings.js +2 -0
- package/modules/economy/migrations/economy_002_project_account.sql +83 -0
- package/modules/economy/module.json +1 -1
- package/modules/economy/project-account.js +166 -0
- package/modules/economy/routes/project-account.js +40 -0
- package/modules/hall-ui/public/ai-studio.css +3 -0
- package/modules/hall-ui/public/ai-studio.html +2 -1
- package/modules/hall-ui/public/ai-studio.js +72 -1
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/github-chip.js +85 -0
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/market-watch-item.html +2 -2
- package/modules/hall-ui/public/market-watch-item.js +10 -3
- package/modules/hall-ui/public/market-watch-item.states.json +41 -0
- package/modules/hall-ui/public/market-watch-lib.js +36 -5
- package/modules/hall-ui/public/market-watch-rules.html +1 -1
- package/modules/hall-ui/public/market-watch-settings.html +1 -1
- package/modules/hall-ui/public/market-watch.css +5 -0
- package/modules/hall-ui/public/market-watch.html +2 -2
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/my-agents.js +3 -0
- package/modules/hall-ui/public/panel.css +5 -0
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings-age.js +65 -0
- package/modules/hall-ui/public/settings.html +27 -1
- package/modules/hall-ui/public/settings.js +1 -0
- package/modules/hall-ui/public/shell.js +22 -22
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +28 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/ideas/blockers.js +13 -7
- package/modules/lifecycle/db-claim-reads.js +1 -1
- package/modules/lifecycle/db-ship.js +2 -2
- package/modules/lifecycle/db-tasks.js +4 -4
- package/modules/lifecycle/migrations/lifecycle_019_pay_type_gates.sql +50 -0
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/pay-type-gates.js +180 -0
- package/modules/lifecycle/publish-reconciler.js +31 -1
- package/modules/lifecycle/rebase-gate.js +3 -2
- package/modules/lifecycle/routes/claims.js +3 -2
- package/modules/lifecycle/routes/pay-type-gates.js +59 -0
- package/modules/lifecycle/routes/project-settings.js +4 -0
- package/modules/lifecycle/routes/task-write-routes.js +43 -0
- package/modules/lifecycle/ship-card.js +4 -1
- package/modules/lifecycle/task-name-db.js +55 -0
- package/modules/lifecycle/task-name.js +140 -0
- package/modules/market-watch/db.js +10 -1
- package/modules/market-watch/migrations/market_watch_004_entry_corrections.sql +20 -0
- package/modules/market-watch/pages.js +20 -1
- package/modules/market-watch/proposals.js +57 -7
- package/modules/market-watch/routes/pages.js +2 -0
- package/modules/market-watch/routes/rules.js +4 -0
- package/modules/platform-identity/terms-acceptance.js +9 -9
- package/modules/public-landing/public/account-google.states.json +14 -0
- package/modules/public-landing/public/assets/cosmos.css +12 -0
- package/modules/public-landing/public/assets/hub-bar.js +44 -1
- package/modules/public-landing/public/privacy.html +5 -1
- package/modules/public-landing/public/projects-google.states.json +14 -0
- package/modules/public-landing/public/projects.html +39 -0
- package/modules/public-landing/public/terms.html +12 -2
- package/modules/ui-design/kit/fixtures/me-google.json +10 -0
- package/modules/ui-design/kit/fixtures/me-sign-in-methods-google.json +4 -0
- package/modules/ui-design/kit/serve.js +7 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +55 -1
- package/scripts/bongos/claim.js +4 -2
- package/scripts/bongos/land-watch.js +1 -0
- package/scripts/bongos/report-checkout.js +30 -0
- package/scripts/bongos/run-unit-tests.js +11 -0
- package/scripts/bongos/sessions-board.js +264 -0
- package/scripts/bongos/ship-finish.js +1 -0
- package/scripts/bongos/ship-flow.js +5 -2
- package/scripts/bongos/strand-lease.js +5 -3
- package/scripts/bongos/strand-watch.js +38 -7
- package/scripts/gds/report-checkout.js +7 -0
- package/scripts/gds/sessions-board.js +7 -0
- package/src/bongos/agent-standing-rules.js +13 -3
- package/src/bongos/routes/me.js +7 -1
- package/src/module-api.js +1 -1
- package/tests/agent_standing_rules.mjs +18 -1
- package/tests/ai_studio_billing.mjs +13 -0
- package/tests/ai_studio_gemini_models.mjs +81 -0
- package/tests/ai_studio_requests.mjs +24 -0
- package/tests/blocker_detail_hydration.mjs +5 -3
- package/tests/blocker_tasks_named_db.mjs +74 -0
- package/tests/claim_gate_ci_unblock.mjs +1 -1
- package/tests/github_chip.mjs +217 -0
- package/tests/hall_ai_studio.mjs +129 -5
- package/tests/host_topology_skips.mjs +1 -0
- package/tests/hub_account_page.mjs +3 -2
- package/tests/landing_legal_pages.mjs +3 -2
- package/tests/market_watch_data_db.mjs +6 -0
- package/tests/market_watch_pages.mjs +87 -2
- package/tests/market_watch_rules.mjs +101 -5
- package/tests/market_watch_rules_db.mjs +42 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/pay_terms.mjs +8 -0
- package/tests/pay_type_gates.mjs +234 -0
- package/tests/pay_type_gates_db.mjs +84 -0
- package/tests/project_account.mjs +125 -0
- package/tests/project_account_db.mjs +97 -0
- package/tests/self_healing_start.mjs +162 -0
- package/tests/sessions_board.mjs +153 -0
- package/tests/settings_age.mjs +110 -0
- package/tests/stop_refcheck.mjs +40 -0
- package/tests/task_name.mjs +237 -0
- package/tests/task_name_db.mjs +79 -0
- package/tests/terms_acceptance.mjs +25 -3
- package/tests/terms_grandfather_db.mjs +11 -4
|
@@ -85,9 +85,10 @@ test('the door returns here, and the page calls only its own-scoped routes', ()
|
|
|
85
85
|
assert.match(PAGE, /href="\/api\/bongos\/auth\/web\/start\?return=\/account&repos=1"/, 'sign-in lands back on /account, asking for repo access once (task 1004549)');
|
|
86
86
|
const calls = [...SCRIPT.matchAll(/request\('(GET|PATCH|POST|DELETE)', '([^']+)'/g)].map((m) => `${m[1]} ${m[2]}`);
|
|
87
87
|
assert.deepEqual([...new Set(calls)].sort(), ['GET /me/cross-project', 'PATCH /me/cross-project', 'PATCH /me/handle', 'PATCH /me/profile', 'GET /my-projects', 'POST /my-projects/', 'GET /me/guilds', 'GET /me/sign-in-methods', 'DELETE /me/sign-in-methods/'].sort());
|
|
88
|
-
// the bar's
|
|
88
|
+
// the bar's calls live in the shared asset: the identity read, sign out, and the
|
|
89
|
+
// ways in the Link GitHub chip asks (task 1004996)
|
|
89
90
|
const barCalls = [...BAR.matchAll(/fetch\(API \+ '([^']+)', \{ method: '(GET|POST)'/g)].map((m) => `${m[2]} ${m[1]}`);
|
|
90
|
-
assert.deepEqual(barCalls.sort(), ['GET /me', 'POST /auth/logout']);
|
|
91
|
+
assert.deepEqual(barCalls.sort(), ['GET /me', 'GET /me/sign-in-methods', 'POST /auth/logout']);
|
|
91
92
|
assert.match(BAR, /var API = '\/api\/bongos';/);
|
|
92
93
|
});
|
|
93
94
|
|
|
@@ -239,10 +239,11 @@ test('cosmos.css still carries the bar and the bands the doors lean on', () => {
|
|
|
239
239
|
for (const rule of ['.bar{', '.barMark{', '.trig{', '.signinDoor{', '.menuBand{']) assert.ok(css.includes(rule), `cosmos.css still carries ${rule}`);
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
-
test('assets/hub-bar.js builds no markup from strings and makes only the bar’s
|
|
242
|
+
test('assets/hub-bar.js builds no markup from strings and makes only the bar’s three calls', () => {
|
|
243
243
|
assert.doesNotMatch(BAR, /innerHTML|insertAdjacentHTML|outerHTML/);
|
|
244
244
|
const calls = [...BAR.matchAll(/fetch\(API \+ '([^']+)', \{ method: '(GET|POST)'/g)].map((m) => `${m[2]} ${m[1]}`);
|
|
245
|
-
|
|
245
|
+
// the identity read, sign out, and the ways in the Link GitHub chip asks (task 1004996)
|
|
246
|
+
assert.deepEqual(calls.sort(), ['GET /me', 'GET /me/sign-in-methods', 'POST /auth/logout']);
|
|
246
247
|
assert.doesNotMatch(BAR, /gtag|plausible|googletagmanager/, 'the privacy page promises no trackers');
|
|
247
248
|
});
|
|
248
249
|
|
|
@@ -29,6 +29,7 @@ const db = require('../modules/market-watch/db.js');
|
|
|
29
29
|
const MIGRATION = readFileSync(new URL('../modules/market-watch/migrations/market_watch_001_tables.sql', import.meta.url), 'utf8');
|
|
30
30
|
const MIGRATION_002 = readFileSync(new URL('../modules/market-watch/migrations/market_watch_002_run.sql', import.meta.url), 'utf8');
|
|
31
31
|
const MIGRATION_003 = readFileSync(new URL('../modules/market-watch/migrations/market_watch_003_ask_pool.sql', import.meta.url), 'utf8');
|
|
32
|
+
const MIGRATION_004 = readFileSync(new URL('../modules/market-watch/migrations/market_watch_004_entry_corrections.sql', import.meta.url), 'utf8');
|
|
32
33
|
const run = require('../modules/market-watch/run.js');
|
|
33
34
|
const store = require('../modules/market-watch/store.js');
|
|
34
35
|
const settings = require('../modules/market-watch/settings.js');
|
|
@@ -86,6 +87,11 @@ try {
|
|
|
86
87
|
// market_watch_003 (task 1004940): the weekly ask's pool-lookup index.
|
|
87
88
|
await q(MIGRATION_003);
|
|
88
89
|
await q(MIGRATION_003);
|
|
90
|
+
// market_watch_004 (task 1005016): the applied-correction lookup index.
|
|
91
|
+
await q(MIGRATION_004);
|
|
92
|
+
await q(MIGRATION_004);
|
|
93
|
+
const { rows: [ix] } = await q(`SELECT count(*)::int AS n FROM pg_indexes WHERE indexname = 'market_watch_proposals_entry_corrections'`);
|
|
94
|
+
assert.equal(ix.n, 1, 'the correction lookup index exists');
|
|
89
95
|
const { rows } = await q(`SELECT count(*)::int AS n FROM information_schema.tables WHERE table_name LIKE 'market\\_watch\\_%'`);
|
|
90
96
|
assert.ok(rows[0].n >= 10, `tables: ${rows[0].n}`);
|
|
91
97
|
});
|
|
@@ -134,7 +134,10 @@ test('each of the four changes, with a one-line reason, becomes a proposal the s
|
|
|
134
134
|
'the direction comes from the item, never from the client');
|
|
135
135
|
assert.deepEqual(norm({ kind: 'move_kind', reason: 'r' }, { id: 9, kind: 'partner' }).value.payload, { from: 'partner', to: 'competitor' });
|
|
136
136
|
assert.deepEqual(norm({ kind: 'add_source', url: 'https://example.org/changelog', reason: 'r' }).value.payload, { source_url: 'https://example.org/changelog' });
|
|
137
|
-
|
|
137
|
+
// task 1005016: the proposal carries the correction itself, or a wrong flag
|
|
138
|
+
assert.deepEqual(norm({ kind: 'correct_entry', log_entry_id: '15', takeaway: ' Raised a seed,\n not a Series B. ', reason: 'r' }).value,
|
|
139
|
+
{ kind: 'correct_entry', item_id: 4, log_entry_id: 15, reason: 'r', payload: { takeaway: 'Raised a seed, not a Series B.', summary: 'Correct an entry to read "Raised a seed, not a Series B."' } });
|
|
140
|
+
assert.deepEqual(norm({ kind: 'correct_entry', log_entry_id: 15, wrong: true, reason: 'r' }).value.payload, { wrong: true, summary: 'Mark an entry as wrong' });
|
|
138
141
|
assert.deepEqual(norm({ kind: 'remove_item', reason: 'shut down' }).value.payload, {}, 'stop tracking');
|
|
139
142
|
assert.deepEqual(pages.normalizeProposal({ kind: 'add_item', scout: 'topic', name: ' agent pay models ', reason: 'r' }).value.payload,
|
|
140
143
|
{ kind: 'topic', name: 'agent pay models', url: null });
|
|
@@ -151,7 +154,11 @@ test('a proposal is refused without a reason, with a bad value, or about somethi
|
|
|
151
154
|
assert.equal(err({ kind: 'rescore', dimension: 'human_gates', value: '', reason: 'r' }), 'invalid_value');
|
|
152
155
|
assert.equal(err({ kind: 'move_kind', reason: 'r' }, { id: 3, kind: 'topic' }), 'cannot_move_topic');
|
|
153
156
|
assert.equal(err({ kind: 'add_source', url: 'javascript:alert(1)', reason: 'r' }), 'invalid_url');
|
|
154
|
-
assert.equal(err({ kind: 'correct_entry', log_entry_id: '0', reason: 'r' }), 'invalid_entry');
|
|
157
|
+
assert.equal(err({ kind: 'correct_entry', log_entry_id: '0', takeaway: 'x', reason: 'r' }), 'invalid_entry');
|
|
158
|
+
assert.equal(err({ kind: 'correct_entry', log_entry_id: '15', reason: 'r' }), 'correction_required', 'the reason alone is no correction');
|
|
159
|
+
assert.equal(err({ kind: 'correct_entry', log_entry_id: '15', takeaway: ' ', reason: 'r' }), 'correction_required');
|
|
160
|
+
assert.equal(err({ kind: 'correct_entry', log_entry_id: '15', takeaway: 'x', wrong: true, reason: 'r' }), 'invalid_correction', 'not both');
|
|
161
|
+
assert.equal(err({ kind: 'correct_entry', log_entry_id: '15', takeaway: 'x'.repeat(501), reason: 'r' }), 'correction_too_long');
|
|
155
162
|
assert.equal(err({ kind: 'rule_change', reason: 'r' }), 'invalid_kind', 'a rules change is the rules page\'s, not this one\'s');
|
|
156
163
|
assert.equal(pages.normalizeProposal({ kind: 'rescore', reason: 'r' }).error, 'item_not_found');
|
|
157
164
|
assert.equal(pages.normalizeProposal({ kind: 'add_item', scout: 'friend', name: 'x', reason: 'r' }).error, 'invalid_scout');
|
|
@@ -166,10 +173,79 @@ test('the propose dialog offers exactly the four changes, and posts what the ser
|
|
|
166
173
|
assert.deepEqual(MW.bodyFor('change', form({ kind: 'rescore', dimension: 'human_gates', value: '8', reason: ' r ' }), { id: 4 }),
|
|
167
174
|
{ kind: 'rescore', item_id: 4, reason: 'r', dimension: 'human_gates', value: '8' });
|
|
168
175
|
assert.deepEqual(MW.bodyFor('stop', form({ reason: 'gone' }), { id: 4 }), { kind: 'remove_item', item_id: 4, reason: 'gone' });
|
|
176
|
+
// correct_entry (task 1005016): the corrected words, or the wrong flag instead
|
|
177
|
+
assert.match(html, /name="takeaway"/);
|
|
178
|
+
assert.match(html, /name="wrong" type="checkbox"/);
|
|
179
|
+
const fix = form({ kind: 'correct_entry', log_entry_id: '15', takeaway: ' seed ', reason: 'r' });
|
|
180
|
+
fix.elements.wrong = { value: 'true', checked: false };
|
|
181
|
+
assert.deepEqual(MW.bodyFor('change', fix, { id: 4 }), { kind: 'correct_entry', item_id: 4, reason: 'r', log_entry_id: '15', takeaway: 'seed' });
|
|
182
|
+
fix.elements.wrong.checked = true;
|
|
183
|
+
assert.deepEqual(MW.bodyFor('change', fix, { id: 4 }), { kind: 'correct_entry', item_id: 4, reason: 'r', log_entry_id: '15', wrong: true });
|
|
184
|
+
for (const b of [MW.bodyFor('change', fix, { id: 4 }), { ...MW.bodyFor('change', fix, { id: 4 }), wrong: undefined, takeaway: 'seed' }]) {
|
|
185
|
+
assert.equal(pages.normalizeProposal(b, { item: COP }).ok, true, 'the dialog posts what the server reads');
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('a corrected entry reads corrected on the item page; one marked wrong is no report on the home', () => {
|
|
190
|
+
const it = byName('GitHub Copilot', 'competitor');
|
|
191
|
+
const cf = rows.caseFileFrom(R, RUNS, it.id);
|
|
192
|
+
cf[0] = { ...cf[0], corrected_at: new Date('2026-10-30T10:00:00Z'), correction_reason: '<b>their post</b>', marked_wrong: false };
|
|
193
|
+
cf[1] = { ...cf[1], corrected_at: new Date('2026-10-30T11:00:00Z'), correction_reason: 'still running', marked_wrong: true };
|
|
194
|
+
const v = pages.shapeItem({ item: it, entries: cf, history: { series: {}, runs: [] }, rules: RULES });
|
|
195
|
+
assert.deepEqual(v.entries[0].corrected, { on: '2026-10-30', reason: '<b>their post</b>', wrong: false });
|
|
196
|
+
assert.deepEqual(v.entries[1].corrected, { on: '2026-10-30', reason: 'still running', wrong: true });
|
|
197
|
+
assert.equal(v.entries[2].corrected, null, 'an entry nobody corrected carries no note');
|
|
198
|
+
// the read behind it: the latest APPLIED correct_entry of this project, per entry
|
|
199
|
+
const caseSql = readFileSync(new URL('../modules/market-watch/db.js', import.meta.url), 'utf8');
|
|
200
|
+
assert.match(caseSql, /p\.project = i\.project AND p\.log_entry_id = e\.id AND p\.kind = 'correct_entry' AND p\.status = 'applied'/);
|
|
201
|
+
const pagesSrc = readFileSync(new URL('../modules/market-watch/pages.js', import.meta.url), 'utf8');
|
|
202
|
+
assert.match(pagesSrc, /AND NOT COALESCE\(\(SELECT p\.payload->>'wrong' = 'true' FROM market_watch_proposals p/, 'the home skips an entry marked wrong');
|
|
203
|
+
// both lookups are served by market_watch_004's partial index, in the order they read
|
|
204
|
+
const m4 = readFileSync(new URL('../modules/market-watch/migrations/market_watch_004_entry_corrections.sql', import.meta.url), 'utf8');
|
|
205
|
+
assert.match(m4, /CREATE INDEX IF NOT EXISTS market_watch_proposals_entry_corrections\s+ON market_watch_proposals \(log_entry_id, decided_at DESC, id DESC\)\s+WHERE kind = 'correct_entry' AND status = 'applied';/);
|
|
206
|
+
assert.match(m4, /^[\s\S]*BEGIN;[\s\S]*COMMIT;\s*$/, 'transactional');
|
|
207
|
+
assert.ok(/^[\x00-\x7F]*$/.test(m4), 'ASCII only');
|
|
208
|
+
for (const src of [caseSql, pagesSrc]) assert.match(src, /ORDER BY p\.decided_at DESC, p\.id DESC/);
|
|
169
209
|
});
|
|
170
210
|
|
|
171
211
|
// ---- untrusted text becomes markup only through esc --------------------------------
|
|
172
212
|
|
|
213
|
+
// task 1005018: the dialog's screen-reader name follows the action that opened it.
|
|
214
|
+
test('the propose dialog is named by its own title: propose a change, stop tracking or send a new scout', () => {
|
|
215
|
+
const titleOf = (html) => {
|
|
216
|
+
const all = [...html.matchAll(/<span[^>]*id="mw-propose-title"[^>]*>([^<]*)<\/span>/g)];
|
|
217
|
+
assert.equal(all.length, 1, 'exactly one title to point at');
|
|
218
|
+
return all[0][1];
|
|
219
|
+
};
|
|
220
|
+
const item = { id: 4, kind: 'competitor', name: 'Copilot' };
|
|
221
|
+
const expected = { change: 'propose a change', stop: 'stop tracking', scout: 'send a new scout' };
|
|
222
|
+
for (const [mode, words] of Object.entries(expected)) {
|
|
223
|
+
// a fake <dialog> that starts with the old fixed label, as a page served before this fix did
|
|
224
|
+
const attrs = new Map([['aria-label', 'Propose a change']]);
|
|
225
|
+
const stub = () => ({ value: '', checked: false, hidden: false, addEventListener() {}, focus() {} });
|
|
226
|
+
const fields = new Proxy({}, { get: (o, k) => (o[k] = o[k] || stub()) });
|
|
227
|
+
const form = { elements: fields, addEventListener() {}, querySelector: () => stub() };
|
|
228
|
+
let html = '';
|
|
229
|
+
const dlg = {
|
|
230
|
+
set innerHTML(v) { html = v; }, get innerHTML() { return html; },
|
|
231
|
+
setAttribute: (k, v) => attrs.set(k, String(v)), removeAttribute: (k) => attrs.delete(k), getAttribute: (k) => attrs.get(k) ?? null,
|
|
232
|
+
querySelector: (s) => (s === 'form' ? form : stub()), querySelectorAll: () => [], showModal() {}, close() {},
|
|
233
|
+
};
|
|
234
|
+
const saved = globalThis.document;
|
|
235
|
+
globalThis.document = { getElementById: (id) => (id === 'mw-propose' ? dlg : null) };
|
|
236
|
+
try { MW.openPropose(mode, { item, dimensions: [], entries: [] }, () => {}); } finally { globalThis.document = saved; }
|
|
237
|
+
assert.equal(attrs.has('aria-label'), false, `${mode}: no fixed label left to override the title`);
|
|
238
|
+
assert.equal(attrs.get('aria-labelledby'), 'mw-propose-title', mode);
|
|
239
|
+
assert.equal(titleOf(html), words, `${mode}: the name a screen reader hears`);
|
|
240
|
+
}
|
|
241
|
+
for (const page of ['market-watch.html', 'market-watch-item.html']) {
|
|
242
|
+
const src = readFileSync(new URL(`../modules/hall-ui/public/${page}`, import.meta.url), 'utf8');
|
|
243
|
+
const tag = src.match(/<dialog[^>]*id="mw-propose"[^>]*>/)[0];
|
|
244
|
+
assert.doesNotMatch(tag, /aria-label=/, `${page}: no fixed name on the dialog`);
|
|
245
|
+
assert.match(tag, /aria-labelledby="mw-propose-title"/, page);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
173
249
|
test('an item name or takeaway with markup in it is escaped on the landscape and in the dialog', () => {
|
|
174
250
|
const evil = '<img src=x onerror=alert(1)>"\'';
|
|
175
251
|
const h = home();
|
|
@@ -307,6 +383,15 @@ test('a member files a proposal: one row, origin member, their id, this project;
|
|
|
307
383
|
assert.equal((await call('POST', '/market-watch/proposals', { kind: 'remove_item', item_id: 4, reason: 'r', status: 'approved' })).status, 400,
|
|
308
384
|
'an unknown field (a member trying to approve their own proposal) is refused');
|
|
309
385
|
assert.equal(inserts.length, 2, 'refused proposals write nothing');
|
|
386
|
+
// task 1005016: the route takes the correction (or the wrong flag) and stores it on the row
|
|
387
|
+
const fix = await call('POST', '/market-watch/proposals', { kind: 'correct_entry', item_id: 4, log_entry_id: 15, takeaway: 'Raised a seed.', reason: 'their post' });
|
|
388
|
+
assert.equal(fix.status, 201);
|
|
389
|
+
assert.deepEqual([inserts[2].params[3], JSON.parse(inserts[2].params[4]).takeaway], [15, 'Raised a seed.']);
|
|
390
|
+
const wrong = await call('POST', '/market-watch/proposals', { kind: 'correct_entry', item_id: 4, log_entry_id: 15, wrong: true, reason: 'still running' });
|
|
391
|
+
assert.equal(wrong.status, 201);
|
|
392
|
+
assert.equal(JSON.parse(inserts[3].params[4]).wrong, true);
|
|
393
|
+
assert.equal((await call('POST', '/market-watch/proposals', { kind: 'correct_entry', item_id: 4, log_entry_id: 15, reason: 'r' })).status, 400, 'no correction, no proposal');
|
|
394
|
+
assert.equal(inserts.length, 4);
|
|
310
395
|
});
|
|
311
396
|
|
|
312
397
|
// ---- the quiet off state -----------------------------------------------------------
|
|
@@ -45,7 +45,7 @@ const V1 = {
|
|
|
45
45
|
|
|
46
46
|
// ── the in-memory database ─────────────────────────────────────────────────────
|
|
47
47
|
function makeDb() {
|
|
48
|
-
const t = { versions: [], proposals: [], items: [], scores: [], runs: [{ id: 50, project: P, status: 'done' }], seq: 100 };
|
|
48
|
+
const t = { versions: [], proposals: [], items: [], scores: [], entries: [], runs: [{ id: 50, project: P, status: 'done' }], seq: 100 };
|
|
49
49
|
const nextId = () => ++t.seq;
|
|
50
50
|
t.versions.push({ id: nextId(), project: P, ...JSON.parse(JSON.stringify(V1)), approved_at: new Date('2026-10-28'), created_at: new Date('2026-10-28') });
|
|
51
51
|
t.items.push({ id: nextId(), project: P, kind: 'competitor', name: 'OpenHands', url: 'https://openhands.dev', status: 'active', why_tracked: '' });
|
|
@@ -76,7 +76,11 @@ function makeDb() {
|
|
|
76
76
|
case 'mw:proposal-lock':
|
|
77
77
|
case 'mw:proposal-get-open': {
|
|
78
78
|
const p = prop(a, b);
|
|
79
|
-
|
|
79
|
+
if (!p || !(tag === 'mw:proposal-lock' || p.status === 'open')) return one(null);
|
|
80
|
+
// Hand back only the columns the real SELECT names (task 1005026): returning the
|
|
81
|
+
// whole row hid a lock that never read log_entry_id, so every correction failed.
|
|
82
|
+
const cols = String(sql).match(/SELECT\s+([\s\S]+?)\s+FROM/)[1].split(',').map((x) => x.trim());
|
|
83
|
+
return one(Object.fromEntries(cols.map((k) => [k, p[k]])));
|
|
80
84
|
}
|
|
81
85
|
case 'mw:rules-max-version':
|
|
82
86
|
return one({ v: Math.max(0, ...t.versions.filter((v) => v.project === a).map((v) => v.version)) });
|
|
@@ -131,6 +135,18 @@ function makeDb() {
|
|
|
131
135
|
Object.assign(p, { status: c, decided_by: d, decided_at: new Date(), rule_version_id: e ?? p.rule_version_id, item_id: f ?? p.item_id, payload: { ...p.payload, ...JSON.parse(g) } });
|
|
132
136
|
return one(p);
|
|
133
137
|
}
|
|
138
|
+
case 'mw:entry-lock': {
|
|
139
|
+
// the entry, only through an item of this project, and only the proposal's item
|
|
140
|
+
const e = t.entries.find((x) => String(x.id) === String(a) && String(x.item_id) === String(c));
|
|
141
|
+
const it = e && t.items.find((x) => x.id === e.item_id && x.project === b);
|
|
142
|
+
return one(e && it ? { ...e } : null);
|
|
143
|
+
}
|
|
144
|
+
case 'mw:entry-correct': {
|
|
145
|
+
const e = t.entries.find((x) => String(x.id) === String(a));
|
|
146
|
+
if (!e || t.entries.some((o) => o.id !== e.id && o.item_id === e.item_id && o.run_id === e.run_id && o.takeaway === b)) return one(null);
|
|
147
|
+
e.takeaway = b;
|
|
148
|
+
return one({ id: e.id });
|
|
149
|
+
}
|
|
134
150
|
case 'mw:proposal-at-vote': {
|
|
135
151
|
const p = prop(a, b);
|
|
136
152
|
if (p && p.status === 'open') p.payload = { ...p.payload, matter_id: c };
|
|
@@ -282,15 +298,71 @@ test('an approved add-item proposal creates the tracked item; the run\'s and the
|
|
|
282
298
|
assert.equal(proposals.itemFromPayload({}), null);
|
|
283
299
|
});
|
|
284
300
|
|
|
285
|
-
test('remove_item takes the item off the watch
|
|
301
|
+
test('remove_item takes the item off the watch', async () => {
|
|
286
302
|
const { t, pool, withTx } = makeDb();
|
|
287
303
|
const itemId = t.items[0].id;
|
|
288
304
|
const filed = await proposals.createProposal(pool, { project: P, step: 'names', change: { op: 'remove', item_id: itemId }, reason: 'not a rival', proposedBy: ADA });
|
|
289
305
|
await proposals.decideProposal({ project: P, id: filed.proposal.id, decision: 'approve', decidedBy: OWNER, withTx });
|
|
290
306
|
assert.equal(t.items[0].status, 'removed');
|
|
291
|
-
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// task 1005016: an approved correct_entry corrects the entry it names.
|
|
310
|
+
function withEntries() {
|
|
311
|
+
const d = makeDb();
|
|
312
|
+
const itemId = d.t.items[0].id;
|
|
313
|
+
d.t.items.push({ id: 300, project: P, kind: 'competitor', name: 'Other', url: 'https://other.dev', status: 'active' });
|
|
314
|
+
d.t.items.push({ id: 400, project: 'https://other.example', kind: 'competitor', name: 'Theirs', url: 'https://theirs.dev', status: 'active' });
|
|
315
|
+
d.t.entries.push(
|
|
316
|
+
{ id: 7, item_id: itemId, run_id: 50, takeaway: 'OpenHands raised a Series B.' },
|
|
317
|
+
{ id: 8, item_id: itemId, run_id: 50, takeaway: 'OpenHands shut down.' },
|
|
318
|
+
{ id: 9, item_id: 300, run_id: 50, takeaway: 'Another item\'s entry.' },
|
|
319
|
+
{ id: 10, item_id: 400, run_id: 50, takeaway: 'Another project\'s entry.' },
|
|
320
|
+
);
|
|
321
|
+
const file = (id, payload, { item = itemId, entry = 7 } = {}) => d.t.proposals.push({
|
|
322
|
+
id, project: P, kind: 'correct_entry', origin: 'member', item_id: item, log_entry_id: entry, payload, reason: 'their post says seed', status: 'open', proposed_by: ADA,
|
|
323
|
+
});
|
|
324
|
+
return { ...d, itemId, file };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
test('CORRECT AN ENTRY: approving rewrites the entry, and keeps who, when, why and the old text', async () => {
|
|
328
|
+
const { t, withTx, file } = withEntries();
|
|
329
|
+
file(900, { takeaway: 'OpenHands raised a seed round, not a Series B.' });
|
|
292
330
|
const out = await proposals.decideProposal({ project: P, id: 900, decision: 'approve', decidedBy: OWNER, withTx });
|
|
293
|
-
assert.deepEqual([out.status, out.applied], ['
|
|
331
|
+
assert.deepEqual([out.ok, out.status, out.applied, out.log_entry_id], [true, 'applied', true, 7]);
|
|
332
|
+
assert.equal(t.entries.find((e) => e.id === 7).takeaway, 'OpenHands raised a seed round, not a Series B.', 'the entry itself now reads corrected');
|
|
333
|
+
const p = t.proposals.find((x) => x.id === 900);
|
|
334
|
+
assert.equal(p.status, 'applied');
|
|
335
|
+
assert.equal(p.proposed_by, ADA, 'who proposed it');
|
|
336
|
+
assert.equal(p.decided_by, OWNER, 'who approved it');
|
|
337
|
+
assert.ok(p.decided_at instanceof Date, 'when');
|
|
338
|
+
assert.equal(p.reason, 'their post says seed', 'why');
|
|
339
|
+
assert.equal(p.payload.was, 'OpenHands raised a Series B.', 'the text it replaced is never lost');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('CORRECT AN ENTRY: a wrong flag leaves the text as found and applies the flag', async () => {
|
|
343
|
+
const { t, withTx, file } = withEntries();
|
|
344
|
+
file(901, { wrong: true }, { entry: 8 });
|
|
345
|
+
const out = await proposals.decideProposal({ project: P, id: 901, decision: 'approve', decidedBy: OWNER, withTx });
|
|
346
|
+
assert.deepEqual([out.status, out.applied], ['applied', true]);
|
|
347
|
+
assert.equal(t.entries.find((e) => e.id === 8).takeaway, 'OpenHands shut down.');
|
|
348
|
+
assert.deepEqual([t.proposals.find((x) => x.id === 901).payload.wrong, t.proposals.find((x) => x.id === 901).payload.was], [true, 'OpenHands shut down.']);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('CORRECT AN ENTRY: refused, and left open, when it names nothing it may change', async () => {
|
|
352
|
+
const { t, withTx, file } = withEntries();
|
|
353
|
+
const decide = (id) => proposals.decideProposal({ project: P, id, decision: 'approve', decidedBy: OWNER, withTx });
|
|
354
|
+
file(902, {}); // filed before task 1005016: the reason was the correction
|
|
355
|
+
assert.equal((await decide(902)).reason, 'no_correction');
|
|
356
|
+
file(903, { takeaway: 'x' }, { entry: 9 }); // another item's entry
|
|
357
|
+
assert.equal((await decide(903)).reason, 'entry_gone');
|
|
358
|
+
file(904, { takeaway: 'x' }, { item: 400, entry: 10 }); // another project's item and entry
|
|
359
|
+
assert.equal((await decide(904)).reason, 'entry_gone');
|
|
360
|
+
file(905, { takeaway: 'OpenHands shut down.' }); // would duplicate entry 8 in the same run
|
|
361
|
+
assert.equal((await decide(905)).reason, 'same_as_another_entry');
|
|
362
|
+
for (const id of [902, 903, 904, 905]) assert.equal(t.proposals.find((x) => x.id === id).status, 'open', `proposal ${id} stays open`);
|
|
363
|
+
assert.deepEqual(t.entries.map((e) => e.takeaway), ['OpenHands raised a Series B.', 'OpenHands shut down.', 'Another item\'s entry.', 'Another project\'s entry.'], 'no entry changed');
|
|
364
|
+
// a decline of the old kind still works
|
|
365
|
+
assert.equal((await proposals.decideProposal({ project: P, id: 902, decision: 'decline', decidedBy: OWNER, withTx })).status, 'rejected');
|
|
294
366
|
});
|
|
295
367
|
|
|
296
368
|
test('a project cannot decide another project\'s proposal', async () => {
|
|
@@ -422,6 +494,24 @@ test('OWNER ONLY: a member who is not the owner gets 403 and nothing changes; th
|
|
|
422
494
|
assert.equal((await call(OWNER, 'POST', `/market-watch/proposals/${id}/decide`, { decision: 'maybe' })).status, 400);
|
|
423
495
|
});
|
|
424
496
|
|
|
497
|
+
test('OWNER ONLY, correcting an entry: a member gets 403 and the entry is untouched; the owner\'s approval corrects it', async () => {
|
|
498
|
+
const itemId = RT.t.items[0].id;
|
|
499
|
+
RT.t.entries.push({ id: 70, item_id: itemId, run_id: 50, takeaway: 'Old words.' });
|
|
500
|
+
RT.t.proposals.push({ id: 970, project: P, kind: 'correct_entry', origin: 'member', item_id: itemId, log_entry_id: 70, payload: { takeaway: 'New words.' }, reason: 'r', status: 'open', proposed_by: ADA });
|
|
501
|
+
const denied = await call(ADA, 'POST', '/market-watch/proposals/970/decide', { decision: 'approve' });
|
|
502
|
+
assert.deepEqual([denied.status, denied.code], [403, 'owner_only']);
|
|
503
|
+
assert.equal(RT.t.entries.find((e) => e.id === 70).takeaway, 'Old words.');
|
|
504
|
+
assert.equal(RT.t.proposals.find((p) => p.id === 970).status, 'open');
|
|
505
|
+
assert.equal((await call(null, 'POST', '/market-watch/proposals/970/decide', { decision: 'approve' })).status, 401);
|
|
506
|
+
const ok = await call(OWNER, 'POST', '/market-watch/proposals/970/decide', { decision: 'approve' });
|
|
507
|
+
assert.equal(ok.status, 200, JSON.stringify(ok.json));
|
|
508
|
+
assert.equal(RT.t.entries.find((e) => e.id === 70).takeaway, 'New words.');
|
|
509
|
+
RT.t.proposals.push({ id: 971, project: P, kind: 'correct_entry', origin: 'member', item_id: itemId, log_entry_id: 70, payload: {}, reason: 'r', status: 'open', proposed_by: ADA });
|
|
510
|
+
const old = await call(OWNER, 'POST', '/market-watch/proposals/971/decide', { decision: 'approve' });
|
|
511
|
+
assert.deepEqual([old.status, old.code], [409, 'no_correction'], 'an old proposal with no correction says so in words');
|
|
512
|
+
assert.match(old.json.error.message, /does not say how the entry should read/);
|
|
513
|
+
});
|
|
514
|
+
|
|
425
515
|
test('a government that hands Rules to a sitting: the owner cannot decide alone (409), the page offers the vote', async () => {
|
|
426
516
|
const filed = await proposals.createProposal(RT.pool, { project: P, step: 'brief', change: { op: 'set', text: 'Five lines.' }, reason: 'r', proposedBy: ADA });
|
|
427
517
|
const sitting = {
|
|
@@ -553,4 +643,10 @@ test('the payloads the item page files are the ones an approval applies', () =>
|
|
|
553
643
|
assert.ok(n.ok, body.kind);
|
|
554
644
|
assert.ok(proposals.itemChange(n.value.kind, n.value.payload), `${body.kind}: pages.js writes a payload proposals.js applies`);
|
|
555
645
|
}
|
|
646
|
+
// correct_entry (task 1005016): both shapes the item page files are ones an approval applies
|
|
647
|
+
const fix = pages.normalizeProposal({ kind: 'correct_entry', log_entry_id: '7', takeaway: ' seed,\nnot B ', reason: 'r' }, { item });
|
|
648
|
+
assert.deepEqual(proposals.entryCorrection(fix.value.payload), { takeaway: 'seed, not B' });
|
|
649
|
+
const wrong = pages.normalizeProposal({ kind: 'correct_entry', log_entry_id: '7', wrong: true, reason: 'r' }, { item });
|
|
650
|
+
assert.deepEqual(proposals.entryCorrection(wrong.value.payload), { wrong: true, takeaway: null });
|
|
651
|
+
assert.equal(proposals.entryCorrection({}), null);
|
|
556
652
|
});
|
|
@@ -27,11 +27,14 @@ const seed = require('../modules/market-watch/seed.js');
|
|
|
27
27
|
const store = require('../modules/market-watch/store.js');
|
|
28
28
|
const proposals = require('../modules/market-watch/proposals.js');
|
|
29
29
|
const view = require('../modules/market-watch/rules-view.js');
|
|
30
|
+
const pages = require('../modules/market-watch/pages.js');
|
|
31
|
+
const db = require('../modules/market-watch/db.js');
|
|
30
32
|
const research = require('../modules/market-watch/research.js');
|
|
31
33
|
const pricing = require('../src/bongos/llm-pricing.js');
|
|
32
34
|
|
|
33
35
|
const MIGRATION = readFileSync(new URL('../modules/market-watch/migrations/market_watch_001_tables.sql', import.meta.url), 'utf8');
|
|
34
36
|
const MIGRATION_002 = readFileSync(new URL('../modules/market-watch/migrations/market_watch_002_run.sql', import.meta.url), 'utf8');
|
|
37
|
+
const MIGRATION_004 = readFileSync(new URL('../modules/market-watch/migrations/market_watch_004_entry_corrections.sql', import.meta.url), 'utf8');
|
|
35
38
|
|
|
36
39
|
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
37
40
|
try {
|
|
@@ -60,6 +63,8 @@ const withTx = async (fn) => {
|
|
|
60
63
|
|
|
61
64
|
async function cleanup() {
|
|
62
65
|
await q(`DELETE FROM market_watch_proposals WHERE project = $1`, [P]).catch(() => {});
|
|
66
|
+
await q(`DELETE FROM market_watch_log_entries WHERE item_id IN (SELECT id FROM market_watch_items WHERE project = $1)`, [P]).catch(() => {});
|
|
67
|
+
await q(`DELETE FROM market_watch_runs WHERE project = $1`, [P]).catch(() => {});
|
|
63
68
|
await q(`DELETE FROM market_watch_items WHERE project = $1`, [P]).catch(() => {});
|
|
64
69
|
await q(`DELETE FROM market_watch_rule_versions WHERE project = $1`, [P]).catch(() => {});
|
|
65
70
|
await q(`DELETE FROM builders WHERE id = ANY($1::bigint[])`, [[OWNER, ADA]]).catch(() => {});
|
|
@@ -73,6 +78,7 @@ async function check(name, fn) {
|
|
|
73
78
|
try {
|
|
74
79
|
await q(MIGRATION);
|
|
75
80
|
await q(MIGRATION_002);
|
|
81
|
+
await q(MIGRATION_004);
|
|
76
82
|
await cleanup();
|
|
77
83
|
await insertTestBuilders(pool, [{ id: OWNER, login: 't1004941-owner' }, { id: ADA, login: 't1004941-ada' }]);
|
|
78
84
|
const r = seed.RULES_V1;
|
|
@@ -112,6 +118,42 @@ try {
|
|
|
112
118
|
assert.deepEqual([it.kind, it.url, it.added_via, String(it.added_by), it.status], ['competitor', 'https://algora.io', 'proposal', String(ADA), 'active']);
|
|
113
119
|
});
|
|
114
120
|
|
|
121
|
+
// task 1005016: an approved correct_entry rewrites the entry, keeps who/when/why and
|
|
122
|
+
// the old text on the proposal, and the case file reads the correction; a wrong flag
|
|
123
|
+
// leaves the text and reads as marked wrong.
|
|
124
|
+
await check('an approved correct_entry corrects the log entry, and the case file shows it', async () => {
|
|
125
|
+
const { rows: [it] } = await q(`SELECT id FROM market_watch_items WHERE project = $1 AND name = 'Algora'`, [P]);
|
|
126
|
+
const { rows: [run] } = await q(`INSERT INTO market_watch_runs (project, status, trigger) VALUES ($1, 'done', 'manual') RETURNING id`, [P]);
|
|
127
|
+
const { rows: [e1] } = await q(`INSERT INTO market_watch_log_entries (item_id, run_id, type, takeaway, source_url, confidence)
|
|
128
|
+
VALUES ($1, $2, 'news', 'Algora raised a Series B.', 'https://algora.io/blog', 2) RETURNING id`, [it.id, run.id]);
|
|
129
|
+
const { rows: [e2] } = await q(`INSERT INTO market_watch_log_entries (item_id, run_id, type, takeaway, source_url, confidence)
|
|
130
|
+
VALUES ($1, $2, 'news', 'Algora shut down.', 'https://algora.io/x', 1) RETURNING id`, [it.id, run.id]);
|
|
131
|
+
const file = async (body) => {
|
|
132
|
+
const n = pages.normalizeProposal({ kind: 'correct_entry', item_id: it.id, ...body }, { item: { id: Number(it.id), kind: 'competitor' } });
|
|
133
|
+
assert.equal(n.ok, true, JSON.stringify(n));
|
|
134
|
+
return pages.insertProposal(pool, { project: P, proposal: n.value, builderId: ADA });
|
|
135
|
+
};
|
|
136
|
+
const fix = await file({ log_entry_id: String(e1.id), takeaway: 'Algora raised a seed round, not a Series B.', reason: 'their post says seed' });
|
|
137
|
+
const out = await proposals.decideProposal({ project: P, id: fix.id, decision: 'approve', decidedBy: OWNER, withTx });
|
|
138
|
+
assert.deepEqual([out.ok, out.status], [true, 'applied'], JSON.stringify(out));
|
|
139
|
+
const { rows: [e] } = await q(`SELECT takeaway FROM market_watch_log_entries WHERE id = $1`, [e1.id]);
|
|
140
|
+
assert.equal(e.takeaway, 'Algora raised a seed round, not a Series B.');
|
|
141
|
+
const { rows: [p] } = await q(`SELECT status, proposed_by, decided_by, decided_at, reason, payload FROM market_watch_proposals WHERE id = $1`, [fix.id]);
|
|
142
|
+
assert.deepEqual([p.status, String(p.proposed_by), String(p.decided_by), p.reason, p.payload.was],
|
|
143
|
+
['applied', String(ADA), String(OWNER), 'their post says seed', 'Algora raised a Series B.']);
|
|
144
|
+
assert.ok(p.decided_at instanceof Date);
|
|
145
|
+
const wrong = await file({ log_entry_id: String(e2.id), wrong: true, reason: 'they are still running' });
|
|
146
|
+
assert.equal((await proposals.decideProposal({ project: P, id: wrong.id, decision: 'approve', decidedBy: OWNER, withTx })).status, 'applied');
|
|
147
|
+
const cf = await db.caseFile(pool, { project: P, itemId: it.id });
|
|
148
|
+
const byId = new Map(cf.map((x) => [String(x.id), x]));
|
|
149
|
+
assert.equal(byId.get(String(e1.id)).correction_reason, 'their post says seed');
|
|
150
|
+
assert.equal(byId.get(String(e1.id)).marked_wrong, false);
|
|
151
|
+
assert.equal(byId.get(String(e2.id)).marked_wrong, true);
|
|
152
|
+
assert.equal(byId.get(String(e2.id)).takeaway, 'Algora shut down.', 'a wrong entry keeps its text');
|
|
153
|
+
const shaped = pages.shapeItem({ item: { id: it.id }, entries: cf, history: {}, rules: null });
|
|
154
|
+
assert.equal(shaped.entries.find((x) => x.id === Number(e2.id)).corrected.wrong, true);
|
|
155
|
+
});
|
|
156
|
+
|
|
115
157
|
await check('the page read runs on the real schema', async () => {
|
|
116
158
|
await proposals.createProposal(pool, { project: P, step: 'brief', change: { op: 'set', text: 'Write five lines.' }, reason: 'shorter', proposedBy: ADA });
|
|
117
159
|
const page = await view.readView(pool, { project: P, decide: { mode: 'owner' }, pricing, model: research.MODEL });
|
|
@@ -94,7 +94,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
94
94
|
// "Upgraded to core X" notice after a move; task 1004826: Standing is Accomplishments;
|
|
95
95
|
// task 1004846: the new logo (its bump landed late, in task 1004852); task 1004938: the
|
|
96
96
|
// Market Watch settings nav item and its fuel glyph.
|
|
97
|
-
const SHELL_SHA = '
|
|
97
|
+
const SHELL_SHA = '1a23b1cf13567eff';
|
|
98
98
|
|
|
99
99
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
100
100
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|
package/tests/pay_terms.mjs
CHANGED
|
@@ -23,6 +23,14 @@ let statements = [];
|
|
|
23
23
|
const query = async (sql, params = []) => {
|
|
24
24
|
statements.push(sql);
|
|
25
25
|
if (/^\s*(BEGIN|COMMIT|ROLLBACK)/i.test(sql)) return { rows: [] };
|
|
26
|
+
if (/FROM lifecycle_pay_type_gates/.test(sql)) {
|
|
27
|
+
// task 1004991: these cases are about the terms, so every gate reads as cleared;
|
|
28
|
+
// tests/pay_type_gates.mjs is where an open gate refuses.
|
|
29
|
+
const gates = require('../modules/lifecycle/pay-type-gates.js');
|
|
30
|
+
const at = new Date(Date.UTC(2026, 9, 30));
|
|
31
|
+
return { rows: params[0].flatMap((pay_type) => gates.requirementIds(pay_type).map((requirement) => (
|
|
32
|
+
{ pay_type, requirement, state: 'cleared', evidence_ref: 'test', decided_by: 1, decided_at: at }))) };
|
|
33
|
+
}
|
|
26
34
|
if (/INSERT INTO lifecycle_pay_term_sets/.test(sql)) {
|
|
27
35
|
const row = { id: sets.length + 1, task_id: params[0], set_by: params[1], set_at: new Date(Date.UTC(2026, 9, 30, 0, sets.length)) };
|
|
28
36
|
sets.push(row);
|