@bongos/core 1.19.870 → 1.19.872
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 +164 -94
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/CONTRIBUTING.md +1 -1
- package/README.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +10 -0
- package/docs/api/openapi.json +61 -3
- package/docs/api-reference.md +7 -1
- package/docs/architecture.md +7 -7
- package/docs/branding-contract.md +1 -1
- package/docs/copy-inventory.md +190 -189
- package/docs/copy-registry.json +264 -255
- package/docs/design/hall-direction-v2.md +2 -2
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/diagrams/README.md +4 -4
- package/docs/onboarding/drachmae-and-karma.md +2 -2
- package/docs/onboarding/primer.md +1 -1
- package/docs/recipes/README.md +1 -1
- package/docs/recipes/bongos-cli-release.md +4 -4
- package/docs/recipes/core-release-pipeline.md +3 -3
- package/docs/recipes/instance-deploy.md +2 -2
- package/docs/recipes/ops-gotchas.md +2 -2
- package/docs/recipes/search-index-ingestion.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/gauge.js +218 -0
- package/modules/dev-box/app/README.md +1 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
- package/modules/hall-ui/public/blockers.js +138 -34
- package/modules/hall-ui/public/builders.js +2 -1
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/collab.js +6 -3
- package/modules/hall-ui/public/dom-utils.js +46 -5
- package/modules/hall-ui/public/goals-page.js +7 -3
- package/modules/hall-ui/public/goals-render.js +4 -3
- package/modules/hall-ui/public/hall-render.js +1 -1
- package/modules/hall-ui/public/people.js +3 -3
- package/modules/hall-ui/public/profile-charts.js +5 -2
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/profile.js +6 -6
- package/modules/hall-ui/public/settings-specialities.js +2 -2
- package/modules/hall-ui/public/settings.html +4 -4
- package/modules/hall-ui/public/settings.js +2 -2
- package/modules/hall-ui/public/shell.js +70 -0
- package/modules/hall-ui/public/style.css +31 -0
- package/modules/hall-ui/public/work.js +12 -14
- package/modules/provisioning/module.json +3 -3
- package/modules/public-landing/public/terms.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/autobongos-loop.js +170 -0
- package/scripts/gds/autobongos-run.js +317 -0
- package/scripts/gds/autobongos-verify.js +377 -0
- package/scripts/gds/autonomy-gauge-statusline.js +124 -0
- package/scripts/gds/autonomy-gauge.js +84 -0
- package/scripts/gds/run-unit-tests.js +120 -2
- package/scripts/gds/sequence.js +3 -0
- package/scripts/gds/start.js +16 -10
- package/src/bongos/core-update.js +147 -0
- package/src/bongos/routes/core-update.js +49 -0
- package/src/bongos/routes.js +2 -0
- package/src/module-api.js +1 -1
- package/tests/account_privacy_flags.mjs +3 -1
- package/tests/autobongos_loop.mjs +446 -0
- package/tests/autobongos_verify.mjs +325 -0
- package/tests/autonomy_gauge.mjs +344 -0
- package/tests/blockers_hall.mjs +20 -6
- package/tests/blockers_kit_pager.mjs +260 -0
- package/tests/blockers_page_ui.mjs +5 -4
- package/tests/box_ship_permissions.mjs +30 -0
- package/tests/core_update_banner.mjs +308 -0
- package/tests/eviction_policy.mjs +22 -1
- package/tests/git_guard.mjs +68 -0
- package/tests/goal_criterion_confirm_ui.mjs +7 -0
- package/tests/goal_invite_consent.mjs +7 -0
- package/tests/goal_sort_staleness.mjs +6 -2
- package/tests/goal_subtitle.mjs +48 -4
- package/tests/hall_board_world.mjs +4 -1
- package/tests/hall_builders_page_pass.mjs +7 -0
- package/tests/hall_landing_boot.mjs +7 -0
- package/tests/hall_profile_world.mjs +20 -2
- package/tests/hall_record_world.mjs +7 -0
- package/tests/hall_roster_world.mjs +2 -1
- package/tests/hall_studio_world.mjs +7 -0
- package/tests/hook_shell_tools.mjs +23 -1
- package/tests/idea_detail_page.mjs +7 -0
- package/tests/idea_spark_hall.mjs +7 -0
- package/tests/integration_parked_ratchet.mjs +79 -0
- package/tests/module_contributions.mjs +134 -8
- package/tests/people_directory_clip.mjs +7 -0
- package/tests/profile_activity.mjs +7 -0
- package/tests/profile_ui_cross_project.mjs +9 -1
- package/tests/provision_disconnect.mjs +7 -0
- package/tests/provision_restart.mjs +7 -0
- package/tests/provision_settings_apply.mjs +7 -0
- package/tests/sessions_kit_pager.mjs +7 -0
- package/tests/watch_applications_queue.mjs +7 -0
- package/tests/watch_roster_self_row.mjs +7 -0
- package/tests/watch_sealed_floor.mjs +7 -0
package/tests/blockers_hall.mjs
CHANGED
|
@@ -72,15 +72,29 @@ test('blockers.js renders rows through the SAME shared lib the attention slice u
|
|
|
72
72
|
assert.match(BLOCKERS_JS_SRC, /lib\.blockerFacts\(/);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
test('blockers.js pages through OTBKit
|
|
75
|
+
test('blockers.js pages through OTBKit\'s SERVER-OFFSET pager, not a client-held fetch-then-slice', () => {
|
|
76
|
+
// task 1003983: `?limit=200` fetched once and sliced client-side with
|
|
77
|
+
// kit.paginate() silently dropped everything past the 200th open blocker
|
|
78
|
+
// (200 is _helpers.js parsePagination's MAX_LIMIT) with no notice, and the
|
|
79
|
+
// headline pill counted the fetched (possibly-truncated) array's length
|
|
80
|
+
// rather than the route's own `page.total`. The fix asks for one page at a
|
|
81
|
+
// time — the profile activity-log / session-ledger serverPager precedent
|
|
82
|
+
// (hall-ui/CLAUDE.md) — so the browser never holds more than one page's
|
|
83
|
+
// worth of rows.
|
|
76
84
|
assert.match(BLOCKERS_JS_SRC, /kit\.makePager\(/);
|
|
77
|
-
assert.match(BLOCKERS_JS_SRC, /kit\.paginate\(/);
|
|
78
85
|
assert.match(BLOCKERS_JS_SRC, /kit\.wirePager\(/);
|
|
79
86
|
assert.match(BLOCKERS_JS_SRC, /kit\.pagerBarHtml\(/);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
87
|
+
assert.match(BLOCKERS_JS_SRC, /kit\.pageOffset\(/);
|
|
88
|
+
assert.match(BLOCKERS_JS_SRC, /kit\.renderServerPage\(/);
|
|
89
|
+
// The negative half: no client-side pager.paginate() slicing, and no fixed
|
|
90
|
+
// `?limit=200` fetch — both are the truncation shape this task removes.
|
|
91
|
+
assert.doesNotMatch(BLOCKERS_JS_SRC, /kit\.paginate\(/);
|
|
92
|
+
assert.doesNotMatch(BLOCKERS_JS_SRC, /\?limit=200/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('blockers.js reads the server\'s own page.total for the headline count, never a held array\'s length', () => {
|
|
96
|
+
assert.match(BLOCKERS_JS_SRC, /data\.page\.total/);
|
|
97
|
+
assert.doesNotMatch(BLOCKERS_JS_SRC, /all\.length/);
|
|
84
98
|
});
|
|
85
99
|
|
|
86
100
|
test('blockers.html loads blockers-lib.js BEFORE blockers.js', () => {
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// tests/blockers_kit_pager.mjs — task 1003983: blockers.js's server-offset
|
|
2
|
+
// paging, EXECUTED against the real kit and the real page, not just pinned by
|
|
3
|
+
// call-shape regex (tests/blockers_hall.mjs does that half).
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS FILE EXISTS. blockers.js used to fetch the whole open-blocker
|
|
6
|
+
// queue at `?limit=200` once and slice it client-side with `kit.paginate` —
|
|
7
|
+
// past 200 rows it silently dropped everything beyond the 200th and
|
|
8
|
+
// undercounted its own headline pill from that truncated array. The fix
|
|
9
|
+
// moved it onto the kit's server-offset pager primitives, hand-wired the way
|
|
10
|
+
// sessions.js wires them (tests/sessions_kit_pager.mjs is this file's direct
|
|
11
|
+
// model). The first version of that migration shipped WITHOUT sessions.js's
|
|
12
|
+
// rollback behaviour — a failed page-turn wiped the whole list and pager and
|
|
13
|
+
// never restored `pager.page`/`perPage`, so one failed click left the page
|
|
14
|
+
// dead until reload. This file runs the REAL hall-kit.js + the REAL
|
|
15
|
+
// blockers.js in one vm against a stub DOM and a stub `fetch`, so what is
|
|
16
|
+
// asserted is the request the page actually issues and the bar a reader
|
|
17
|
+
// actually sees.
|
|
18
|
+
//
|
|
19
|
+
// Run: node tests/blockers_kit_pager.mjs
|
|
20
|
+
import { strict as assert } from 'node:assert';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import vm from 'node:vm';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { makeRunner } from './helpers.mjs';
|
|
26
|
+
|
|
27
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
29
|
+
const KIT_JS = read('modules/hall-ui/public/hall-kit.js');
|
|
30
|
+
const BLOCKERS_JS = read('modules/hall-ui/public/blockers.js');
|
|
31
|
+
|
|
32
|
+
const { test, summary } = makeRunner();
|
|
33
|
+
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
34
|
+
|
|
35
|
+
function makeEl(id) {
|
|
36
|
+
const listeners = new Map();
|
|
37
|
+
return {
|
|
38
|
+
id,
|
|
39
|
+
innerHTML: '', textContent: '', className: '', value: '',
|
|
40
|
+
hidden: false, disabled: false,
|
|
41
|
+
options: [], style: {}, dataset: {},
|
|
42
|
+
classList: { add() {}, remove() {}, toggle() {} },
|
|
43
|
+
addEventListener(ev, fn) {
|
|
44
|
+
if (!listeners.has(ev)) listeners.set(ev, []);
|
|
45
|
+
listeners.get(ev).push(fn);
|
|
46
|
+
},
|
|
47
|
+
fire(ev, arg) { for (const fn of listeners.get(ev) || []) fn(arg); },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// `b(id)` is a minimal blocker row — enough for blockerFacts() (loaded as
|
|
52
|
+
// window.OTBBlockersLib) to render without throwing.
|
|
53
|
+
const b = (id) => ({ id, title: `Blocker ${id}`, body_md: '', created_at: '2026-08-01T00:00:00Z', blocking_task_ids: [] });
|
|
54
|
+
|
|
55
|
+
function harness({ total = 57, failAfter = null, perPagePref = null } = {}) {
|
|
56
|
+
const els = new Map();
|
|
57
|
+
const getEl = (id) => {
|
|
58
|
+
if (!els.has(id)) els.set(id, makeEl(id));
|
|
59
|
+
return els.get(id);
|
|
60
|
+
};
|
|
61
|
+
const documentObj = {
|
|
62
|
+
getElementById: getEl,
|
|
63
|
+
querySelector(sel) { return getEl(String(sel).replace(/^#/, '')); },
|
|
64
|
+
addEventListener() {},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const requests = [];
|
|
68
|
+
let calls = 0;
|
|
69
|
+
const rowsFor = (limit, offset) => Array.from(
|
|
70
|
+
{ length: Math.max(0, Math.min(limit, total - offset)) },
|
|
71
|
+
(_, i) => b(offset + i + 1)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const fetchImpl = async (url) => {
|
|
75
|
+
requests.push(url);
|
|
76
|
+
calls += 1;
|
|
77
|
+
if (failAfter != null && calls > failAfter) {
|
|
78
|
+
return { ok: false, status: 500, json: async () => ({}) };
|
|
79
|
+
}
|
|
80
|
+
const q = new URLSearchParams(url.split('?')[1] || '');
|
|
81
|
+
const limit = Number(q.get('limit'));
|
|
82
|
+
const offset = Number(q.get('offset'));
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
status: 200,
|
|
86
|
+
json: async () => ({ blockers: rowsFor(limit, offset), page: { limit, offset, total } }),
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const store = new Map();
|
|
91
|
+
if (perPagePref != null) store.set('hall-blockers-per-page', String(perPagePref));
|
|
92
|
+
|
|
93
|
+
const toasts = [];
|
|
94
|
+
const windowObj = {
|
|
95
|
+
fetch: fetchImpl,
|
|
96
|
+
localStorage: {
|
|
97
|
+
getItem: (k) => (store.has(k) ? store.get(k) : null),
|
|
98
|
+
setItem: (k, v) => store.set(k, String(v)),
|
|
99
|
+
},
|
|
100
|
+
OTB: { toast: (msg) => toasts.push(msg) },
|
|
101
|
+
addEventListener() {},
|
|
102
|
+
// loadViewer() calls this for GET /me; signed-out (401) keeps isReviewer()
|
|
103
|
+
// false, which is fine for these paging tests — they don't inspect the
|
|
104
|
+
// Resolve/Link action column.
|
|
105
|
+
BongosClient: { createClient: () => ({ request: async () => ({ status: 401, ok: false, data: null }) }) },
|
|
106
|
+
OTBBlockersLib: {
|
|
107
|
+
esc: (s) => String(s ?? ''),
|
|
108
|
+
blockerFacts: (row) => ({
|
|
109
|
+
titleHtml: `<a>${row.title}</a>`, noteHtml: '', factsHtml: '<span></span>',
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const sandbox = {
|
|
115
|
+
window: windowObj, document: documentObj, fetch: fetchImpl,
|
|
116
|
+
console: { error() {}, warn() {}, log() {} },
|
|
117
|
+
setTimeout, clearTimeout, URLSearchParams,
|
|
118
|
+
};
|
|
119
|
+
sandbox.globalThis = sandbox;
|
|
120
|
+
vm.createContext(sandbox);
|
|
121
|
+
vm.runInContext(KIT_JS, sandbox, { filename: 'hall-kit.js' });
|
|
122
|
+
vm.runInContext(BLOCKERS_JS, sandbox, { filename: 'blockers.js' });
|
|
123
|
+
|
|
124
|
+
const q = (n) => {
|
|
125
|
+
const url = requests[n < 0 ? requests.length + n : n];
|
|
126
|
+
return Object.fromEntries(new URLSearchParams(url.split('?')[1]));
|
|
127
|
+
};
|
|
128
|
+
return { getEl, requests, q, store, toasts };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await test('the first load asks the SERVER for one page — the corpus is never held', async () => {
|
|
132
|
+
const h = harness({ total: 57 });
|
|
133
|
+
await tick(); await tick();
|
|
134
|
+
assert.equal(h.requests.length, 1);
|
|
135
|
+
const p = h.q(0);
|
|
136
|
+
assert.equal(p.offset, '0');
|
|
137
|
+
assert.equal(p.limit, '25', 'the page keeps its default per-page');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await test('the headline pill reads page.total, not the fetched rows\' own count', async () => {
|
|
141
|
+
const h = harness({ total: 57 });
|
|
142
|
+
await tick(); await tick();
|
|
143
|
+
assert.equal(h.getEl('blockers-count').textContent, '57');
|
|
144
|
+
assert.equal(h.getEl('blockers-count').hidden, false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await test('Next moves the offset by the reader\'s per-page, and the bar says where they are', async () => {
|
|
148
|
+
const h = harness({ total: 57 });
|
|
149
|
+
await tick(); await tick();
|
|
150
|
+
h.getEl('blockers-next').fire('click');
|
|
151
|
+
await tick(); await tick();
|
|
152
|
+
assert.equal(h.requests.length, 2);
|
|
153
|
+
assert.equal(h.q(-1).offset, '25');
|
|
154
|
+
assert.equal(h.getEl('blockers-page').textContent, 'Page 2 of 3');
|
|
155
|
+
assert.equal(h.getEl('blockers-range').textContent, 'Showing 26–50 of 57');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await test('per-page is a real control: it re-asks, resets to page 1, and persists', async () => {
|
|
159
|
+
const h = harness({ total: 57 });
|
|
160
|
+
await tick(); await tick();
|
|
161
|
+
h.getEl('blockers-next').fire('click');
|
|
162
|
+
await tick(); await tick();
|
|
163
|
+
assert.equal(h.q(-1).offset, '25', 'sanity: on page 2');
|
|
164
|
+
|
|
165
|
+
h.getEl('blockers-per-page').fire('change', { target: { value: '50' } });
|
|
166
|
+
await tick(); await tick();
|
|
167
|
+
const p = h.q(-1);
|
|
168
|
+
assert.equal(p.limit, '50');
|
|
169
|
+
assert.equal(p.offset, '0', 'a per-page change returns to page 1');
|
|
170
|
+
assert.equal(h.store.get('hall-blockers-per-page'), '50');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await test('a remembered per-page is used on the very first request', async () => {
|
|
174
|
+
const h = harness({ total: 57, perPagePref: 10 });
|
|
175
|
+
await tick(); await tick();
|
|
176
|
+
assert.equal(h.requests.length, 1);
|
|
177
|
+
assert.equal(h.q(0).limit, '10');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// the bug this task fixes: a failed PAGE-TURN used to wipe the whole list and
|
|
182
|
+
// pager and never restore pager.page/perPage — one failed click left the page
|
|
183
|
+
// permanently dead. sessions.js's contract (hall-ui/CLAUDE.md) is the fix.
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
await test('a failed page-turn ROLLS BACK — the bar never claims a page whose rows never came', async () => {
|
|
187
|
+
const h = harness({ total: 57, failAfter: 1 });
|
|
188
|
+
await tick(); await tick();
|
|
189
|
+
assert.equal(h.getEl('blockers-page').textContent, 'Page 1 of 3');
|
|
190
|
+
|
|
191
|
+
h.getEl('blockers-next').fire('click');
|
|
192
|
+
await tick(); await tick();
|
|
193
|
+
assert.equal(h.requests.length, 2, 'it did try');
|
|
194
|
+
assert.equal(h.getEl('blockers-page').textContent, 'Page 1 of 3',
|
|
195
|
+
'the failed page 2 must not be left showing in the bar');
|
|
196
|
+
assert.equal(h.getEl('blockers-range').textContent, 'Showing 1–25 of 57',
|
|
197
|
+
'and the range must still describe the rows actually on screen');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await test('a failed page-turn does NOT wipe the list — the reader can retry from the pager', async () => {
|
|
201
|
+
const h = harness({ total: 57, failAfter: 1 });
|
|
202
|
+
await tick(); await tick();
|
|
203
|
+
const before = h.getEl('blockers-list').innerHTML;
|
|
204
|
+
assert.ok(before.length > 0, 'sanity: rows painted');
|
|
205
|
+
|
|
206
|
+
h.getEl('blockers-next').fire('click');
|
|
207
|
+
await tick(); await tick();
|
|
208
|
+
assert.equal(h.getEl('blockers-list').innerHTML, before,
|
|
209
|
+
'the last successfully painted rows must still be on screen');
|
|
210
|
+
assert.equal(h.getEl('blockers-body').className, '',
|
|
211
|
+
'notice()\'s full-body wipe must not fire once something has painted');
|
|
212
|
+
assert.equal(h.toasts.length, 1, 'the reader is told, even though the list stays put');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await test('a failed FIRST load still shows the honest notice — nothing has painted to keep', async () => {
|
|
216
|
+
const h = harness({ total: 57, failAfter: 0 });
|
|
217
|
+
await tick(); await tick();
|
|
218
|
+
assert.equal(h.getEl('blockers-body').className, 'note');
|
|
219
|
+
assert.match(h.getEl('blockers-body').textContent, /couldn.t load/i);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await test('a failed per-page change rolls the SIZE back too', async () => {
|
|
223
|
+
const h = harness({ total: 57, failAfter: 1 });
|
|
224
|
+
await tick(); await tick();
|
|
225
|
+
h.getEl('blockers-per-page').fire('change', { target: { value: '10' } });
|
|
226
|
+
await tick(); await tick();
|
|
227
|
+
h.getEl('blockers-next').fire('click');
|
|
228
|
+
await tick(); await tick();
|
|
229
|
+
// Back on the 25-row page 1, the only page whose rows ever arrived, so Next
|
|
230
|
+
// asks for offset 25, not offset 10.
|
|
231
|
+
assert.equal(h.q(-1).limit, '25');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await test('nothing is pressable while a fetch is in flight', async () => {
|
|
235
|
+
const h = harness({ total: 57 });
|
|
236
|
+
await tick(); await tick();
|
|
237
|
+
assert.equal(h.getEl('blockers-per-page').disabled, false, 'idle: the control is live');
|
|
238
|
+
h.getEl('blockers-next').fire('click');
|
|
239
|
+
// deliberately not awaited — this is mid-flight
|
|
240
|
+
assert.equal(h.getEl('blockers-next').disabled, true, 'in flight: Next is dead');
|
|
241
|
+
assert.equal(h.getEl('blockers-prev').disabled, true, 'in flight: Prev is dead');
|
|
242
|
+
assert.equal(h.getEl('blockers-per-page').disabled, true, 'in flight: per-page is dead');
|
|
243
|
+
await tick(); await tick();
|
|
244
|
+
assert.equal(h.getEl('blockers-per-page').disabled, false, 'landed: live again');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await test('Prev stays dead on page 1 — the kit decides that, not the page', async () => {
|
|
248
|
+
const h = harness({ total: 57 });
|
|
249
|
+
await tick(); await tick();
|
|
250
|
+
assert.equal(h.getEl('blockers-prev').disabled, true);
|
|
251
|
+
assert.equal(h.getEl('blockers-next').disabled, false);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await test('a queue that fits one page shows no bar at all', async () => {
|
|
255
|
+
const h = harness({ total: 4 });
|
|
256
|
+
await tick(); await tick();
|
|
257
|
+
assert.equal(h.getEl('blockers-pager').hidden, true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
summary();
|
|
@@ -285,8 +285,9 @@ await test('a reviewer resolving a row posts to the real route, toasts, and relo
|
|
|
285
285
|
assert.equal(posts.length, 1);
|
|
286
286
|
assert.equal(posts[0].body.resolution_note, 'raised the budget');
|
|
287
287
|
assert.ok(h.toasts.some((t) => t === 'Blocker #12 resolved.'));
|
|
288
|
-
// reload() re-fetches
|
|
289
|
-
|
|
288
|
+
// reload() re-fetches the current server page (task 1003983's server-offset
|
|
289
|
+
// paging) — a second GET beyond the initial boot fetch, same page/offset.
|
|
290
|
+
assert.ok(h.requestsTo('GET', /\/blockers\?limit=25&offset=0$/).length >= 2, 'the queue reloads after a mutation');
|
|
290
291
|
});
|
|
291
292
|
|
|
292
293
|
await test('a reviewer linking a row posts task_id as a NUMBER', async () => {
|
|
@@ -312,13 +313,13 @@ await test('a 403 on resolve renders HONEST copy and leaves the form open — ne
|
|
|
312
313
|
|
|
313
314
|
await test('resolving through the shared #/blocker/N overlay (otb:blocker-resolved) reloads this page\'s queue too', async () => {
|
|
314
315
|
const h = await boot({ rank: 'metic' });
|
|
315
|
-
const before = h.requestsTo('GET', /\/blockers\?limit=
|
|
316
|
+
const before = h.requestsTo('GET', /\/blockers\?limit=25&offset=0$/).length;
|
|
316
317
|
// Simulate task-detail.js's blockerPanel firing the event on THIS window —
|
|
317
318
|
// the SAME event the overlay dispatches when a builder resolves a blocker
|
|
318
319
|
// from the deep-link card opened on top of this page.
|
|
319
320
|
h.dispatchWindowEvent('otb:blocker-resolved');
|
|
320
321
|
await h.settle();
|
|
321
|
-
const after = h.requestsTo('GET', /\/blockers\?limit=
|
|
322
|
+
const after = h.requestsTo('GET', /\/blockers\?limit=25&offset=0$/).length;
|
|
322
323
|
assert.equal(after, before + 1, 'the queue re-fetched exactly once in response to the event');
|
|
323
324
|
});
|
|
324
325
|
|
|
@@ -92,6 +92,7 @@ function allowCovers(allow, command) {
|
|
|
92
92
|
return parts.every((part) => allow.some((rule) => ruleCovers(rule, part)));
|
|
93
93
|
}
|
|
94
94
|
const isBlocked = (command) => guard.decide({ tool_name: 'Bash', tool_input: { command } }, {}).block;
|
|
95
|
+
const isBlockedPwsh = (command) => guard.decide({ tool_name: 'PowerShell', tool_input: { command } }, {}).block;
|
|
95
96
|
|
|
96
97
|
// At least one sample per git-guard rule, keyed by the rule's own name.
|
|
97
98
|
const BLOCKED_SAMPLES = {
|
|
@@ -101,13 +102,42 @@ const BLOCKED_SAMPLES = {
|
|
|
101
102
|
'git branch -D': ['git branch -D task-1', 'git branch --list -D task-1', 'git branch -a -D task-1'],
|
|
102
103
|
};
|
|
103
104
|
|
|
105
|
+
// The samples above are all Bash-shaped, and replaying them against PowerShell would
|
|
106
|
+
// prove nothing: they are the one spelling that already matched (task 1004028). A
|
|
107
|
+
// Windows session writes the executable in whatever case it likes, appends `.exe`,
|
|
108
|
+
// and may carry a backtick that vanishes — so each rule owns a sample in a spelling
|
|
109
|
+
// only PowerShell resolves, and a new rule cannot be added without one.
|
|
110
|
+
const BLOCKED_SAMPLES_PWSH = {
|
|
111
|
+
'git stash': ['Git stash', 'GIT.EXE stash pop'],
|
|
112
|
+
'git worktree remove': ['git.exe worktree remove ../wt', 'Git worktree remove --force ../wt'],
|
|
113
|
+
'git push origin main': ['g`it push origin main', 'GIT push -u origin HEAD:main'],
|
|
114
|
+
'git branch -D': ['GIT branch -D task-1', 'Git.exe branch -a -D task-1'],
|
|
115
|
+
};
|
|
116
|
+
|
|
104
117
|
test('every git-guard rule has blocked samples here (a new rule forces a new sample)', () => {
|
|
105
118
|
for (const [name] of guard.RULES) {
|
|
106
119
|
assert.ok((BLOCKED_SAMPLES[name] || []).length, `no blocked sample for git-guard rule "${name}"`);
|
|
120
|
+
assert.ok((BLOCKED_SAMPLES_PWSH[name] || []).length, `no PowerShell-spelled sample for git-guard rule "${name}"`);
|
|
107
121
|
}
|
|
108
122
|
for (const samples of Object.values(BLOCKED_SAMPLES)) {
|
|
109
123
|
for (const cmd of samples) assert.ok(isBlocked(cmd), `sample is not actually blocked by git-guard: ${cmd}`);
|
|
110
124
|
}
|
|
125
|
+
for (const samples of Object.values(BLOCKED_SAMPLES_PWSH)) {
|
|
126
|
+
for (const cmd of samples) {
|
|
127
|
+
assert.ok(isBlockedPwsh(cmd), `PowerShell sample is not actually blocked by git-guard: ${cmd}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Each PowerShell sample must be spelled in a way the Bash-shaped list does not
|
|
133
|
+
// already cover — otherwise the new list is a copy and proves nothing. Every one
|
|
134
|
+
// differs from a plain `git …` in the executable token alone.
|
|
135
|
+
test('the PowerShell samples are genuinely different spellings, not a copy', () => {
|
|
136
|
+
const bashSamples = new Set(Object.values(BLOCKED_SAMPLES).flat());
|
|
137
|
+
for (const cmd of Object.values(BLOCKED_SAMPLES_PWSH).flat()) {
|
|
138
|
+
assert.ok(!bashSamples.has(cmd), `PowerShell sample repeats a Bash sample verbatim: ${cmd}`);
|
|
139
|
+
assert.ok(!/^git /.test(cmd), `PowerShell sample uses the one spelling that always matched: ${cmd}`);
|
|
140
|
+
}
|
|
111
141
|
});
|
|
112
142
|
|
|
113
143
|
test('no allow-rule covers a command git-guard blocks', () => {
|