@bongos/core 1.19.789 → 1.19.791
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 +143 -53
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +6 -0
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md +124 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +129 -1
- package/docs/api-reference.md +5 -2
- package/docs/copy-inventory.md +89 -72
- package/docs/copy-registry.json +272 -98
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/core-release-pipeline.md +12 -0
- package/migrations/core_241_core_upgrades_actor.sql +65 -0
- package/migrations/core_242_core_version_snapshot.sql +54 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +6 -0
- package/modules/government/catalog.js +8 -0
- package/modules/government/migrations/government_014_core_pin_move.sql +46 -0
- package/modules/hall-ui/public/deploy.html +64 -0
- package/modules/hall-ui/public/deploy.js +188 -0
- package/modules/hall-ui/public/hall-kit.js +7 -65
- package/modules/hall-ui/public/hall-render.js +1 -1
- package/modules/hall-ui/public/hall-visual.js +85 -0
- package/modules/hall-ui/public/index.html +1 -0
- package/modules/hall-ui/public/profile.html +1 -0
- package/modules/hall-ui/public/profile.js +2 -2
- package/modules/hall-ui/public/shell.js +4 -0
- package/modules/hall-ui/public/studio.css +1 -1
- package/modules/hall-ui/public/studio.html +1 -0
- package/modules/hall-ui/public/studio.js +4 -3
- package/modules/hall-ui/public/style.css +0 -16
- package/modules/hall-ui/public/task.html +1 -0
- package/modules/hall-ui/public/task.js +10 -13
- package/modules/provisioning/control-plane-guard.js +102 -0
- package/modules/provisioning/core-upgrade.js +289 -0
- package/modules/provisioning/migrations/provisioning_018_core_upgrade_intent.sql +98 -0
- package/modules/provisioning/module.json +1 -1
- package/modules/provisioning/provisioning.js +8 -8
- package/modules/provisioning/routes/core-upgrade.js +95 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/accessibility-replay.js +195 -0
- package/scripts/gds/enroll-control-plane.js +101 -0
- package/scripts/gds/provision-core-upgrade.js +311 -0
- package/scripts/gds/provision.js +4 -2
- package/scripts/gds/upgrade.js +53 -14
- package/src/bongos/auth.js +19 -0
- package/src/bongos/serve-internal.js +21 -0
- package/src/module-api.js +1 -1
- package/tests/accessibility_replay.mjs +201 -0
- package/tests/auth_resolve_failure.mjs +4 -2
- package/tests/core_upgrade_door.mjs +408 -0
- package/tests/core_upgrade_runner.mjs +377 -0
- package/tests/fixtures/accessibility-replay.json +93 -0
- package/tests/hall_nav.mjs +4 -1
- package/tests/hall_page_gate_map.mjs +7 -0
- package/tests/hall_studio_world.mjs +19 -11
- package/tests/provisioning_disconnect.mjs +30 -5
- package/tests/provisioning_paid_shape_gate.mjs +14 -4
- package/tests/provisioning_restart.mjs +25 -9
- package/tests/provisioning_settings_apply.mjs +11 -2
- package/tests/provisioning_teardown_intent.mjs +7 -0
- package/tests/task_visuals.mjs +91 -27
|
@@ -161,10 +161,20 @@ await ta('a case-variant shape does not slip past the gate INTO a dedicated row'
|
|
|
161
161
|
}
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
await ta('HOSTING_SHAPES
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
164
|
+
await ta('HOSTING_SHAPES carries the shapes the gate reasons about, and control-plane is NOT metered', () => {
|
|
165
|
+
// This tripwire fired for the first time at task 1003159, which added a fourth
|
|
166
|
+
// shape — 'control-plane', the platform's own box, enrolled so the owner's deploy
|
|
167
|
+
// door has a row to point at (ADR 0293 D2). Its question was "is the new shape
|
|
168
|
+
// also metered?", and the answer is NO, stated here rather than implied by
|
|
169
|
+
// absence: the control plane is infrastructure the project already runs and pays
|
|
170
|
+
// for, not a droplet allocated per request, so nothing about standing it up is
|
|
171
|
+
// billed through this gate. It is also not creatable through the create route at
|
|
172
|
+
// all — the self row is enrolled by the platform, not asked for by a builder.
|
|
173
|
+
assert.deepEqual([...provisioning.HOSTING_SHAPES].sort(),
|
|
174
|
+
['co-tenant', 'control-plane', 'dedicated', 'standalone']);
|
|
175
|
+
// The assumption the tripwire actually guards is that exactly ONE shape is
|
|
176
|
+
// metered. A fifth shape that IS billed must widen the gate itself, in
|
|
177
|
+
// paid-shape-gate.js, and not merely this list.
|
|
168
178
|
});
|
|
169
179
|
|
|
170
180
|
server.close();
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
import { strict as assert } from 'node:assert';
|
|
31
31
|
import { createRequire } from 'node:module';
|
|
32
|
-
import { readFileSync } from 'node:fs';
|
|
32
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
33
33
|
import http from 'node:http';
|
|
34
34
|
|
|
35
35
|
const require = createRequire(import.meta.url);
|
|
@@ -51,17 +51,33 @@ const PROMISE = /shortly|briefly|minute|second|soon\b/i;
|
|
|
51
51
|
|
|
52
52
|
console.log('the module slice — constants, the migration, the enqueue classifier:');
|
|
53
53
|
|
|
54
|
-
t("INTENT_ACTIONS includes 'restart', in lockstep with the
|
|
54
|
+
t("INTENT_ACTIONS includes 'restart', in lockstep with the LATEST action CHECK", () => {
|
|
55
|
+
// Pinned to provisioning_011 by name until task 1003159, which added
|
|
56
|
+
// 'core-upgrade' in provisioning_016 — and a lockstep assertion aimed at a
|
|
57
|
+
// superseded migration compares the live vocabulary against a frozen one, so it
|
|
58
|
+
// reds on the next widening rather than on a real drift. It now reads whichever
|
|
59
|
+
// migration most recently defines the CHECK, the shape
|
|
60
|
+
// tests/claimable_blocker_status.mjs uses for the claimable view.
|
|
55
61
|
assert.ok(provisioning.INTENT_ACTIONS.includes('restart'));
|
|
56
|
-
const
|
|
62
|
+
const migDir = new URL('../modules/provisioning/migrations/', import.meta.url);
|
|
63
|
+
const defining = readdirSync(migDir)
|
|
64
|
+
.filter((f) => f.endsWith('.sql'))
|
|
65
|
+
.filter((f) => /CHECK \(action IN \(/.test(readFileSync(new URL(f, migDir), 'utf8')))
|
|
66
|
+
.sort();
|
|
67
|
+
assert.ok(defining.length, 'some migration defines the action CHECK');
|
|
68
|
+
const latest = defining[defining.length - 1];
|
|
69
|
+
const sql = readFileSync(new URL(latest, migDir), 'utf8');
|
|
57
70
|
const m = /CHECK \(action IN \(([^)]*)\)\)/.exec(sql);
|
|
58
71
|
assert.ok(m, 'the migration widens the action CHECK');
|
|
59
|
-
const inSql = m[1].split(',').map((s) => s.trim().replace(/^'|'$/g, ''));
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
const inSql = m[1].split(',').map((s) => s.trim().replace(/^'|'$/g, '')).sort();
|
|
73
|
+
// The NEWEST migration owns the strict set equality — the CHECK a live DB ends up
|
|
74
|
+
// with is the last one applied, so this is the assertion that would catch an action
|
|
75
|
+
// added in code but never admitted by the schema. That is provisioning_018 today:
|
|
76
|
+
// 016_disconnect and 018_core_upgrade_intent each rewrite the CHECK, so the later
|
|
77
|
+
// one must carry BOTH actions or the earlier one's is silently dropped.
|
|
78
|
+
assert.deepEqual(inSql, [...provisioning.INTENT_ACTIONS].sort(), 'the CHECK and INTENT_ACTIONS carry the same set');
|
|
79
|
+
const sql011 = readFileSync(new URL('provisioning_011_restart.sql', migDir), 'utf8');
|
|
80
|
+
assert.match(sql011, /INSERT INTO schema_migrations \(version\) VALUES \('provisioning_011_restart'\)/, 'self-registers');
|
|
65
81
|
});
|
|
66
82
|
|
|
67
83
|
t('RESTART_FROM is exactly {active} — every other state is already served by the provision path', () => {
|
|
@@ -84,8 +84,17 @@ await ta('enqueueSettingsApply: a fresh intent → created:true; the INSERT carr
|
|
|
84
84
|
const r = await provisioning.enqueueSettingsApply(db, 18, 'api:self');
|
|
85
85
|
assert.equal(r.created, true);
|
|
86
86
|
assert.equal(r.conflict, undefined);
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
// calls[0] is the self-row guard's shape lookup (ADR 0293 D2) — every enqueue now
|
|
88
|
+
// asks "is this the control plane?" before it writes. Assert on the INSERT itself
|
|
89
|
+
// rather than on a position, so this test says what it means and does not move
|
|
90
|
+
// again the next time a check joins the front of this function.
|
|
91
|
+
const insert = calls.find((c) => /INSERT INTO provisioning_intents/.test(c.sql));
|
|
92
|
+
assert.ok(insert, 'the enqueue reaches its INSERT');
|
|
93
|
+
assert.match(calls[0].sql, /SELECT hosting_shape FROM provisioning_instances/, 'and the guard ran FIRST');
|
|
94
|
+
// The trailing null is the target_version enqueueIntent gained in task 1003159:
|
|
95
|
+
// only a 'core-upgrade' names one, and the column's CHECK is an EQUIVALENCE, so a
|
|
96
|
+
// settings-apply carrying a target would be refused by the database.
|
|
97
|
+
assert.deepEqual(insert.params, [18, 'settings-apply', 'api:self', null]);
|
|
89
98
|
});
|
|
90
99
|
|
|
91
100
|
await ta('enqueueSettingsApply: an identical PENDING push already open → created:false, no conflict (it carries this change)', async () => {
|
|
@@ -55,6 +55,13 @@ function fakeIntentDb(seed = []) {
|
|
|
55
55
|
return {
|
|
56
56
|
rows,
|
|
57
57
|
query: async (sql, params) => {
|
|
58
|
+
// The self-row guard's shape lookup (task 1003159, ADR 0293 D2): every enqueue
|
|
59
|
+
// now asks "is this the control plane?" before it writes. These cases are all
|
|
60
|
+
// ORDINARY instances, so the honest answer is a tenant shape — answering
|
|
61
|
+
// 'control-plane' here would refuse every teardown in this file.
|
|
62
|
+
if (/SELECT hosting_shape FROM provisioning_instances/.test(sql)) {
|
|
63
|
+
return { rows: [{ hosting_shape: 'co-tenant' }] };
|
|
64
|
+
}
|
|
58
65
|
if (/INSERT INTO provisioning_intents/.test(sql)) {
|
|
59
66
|
const [instance_id, action, requested_by] = params;
|
|
60
67
|
if (open(instance_id).length) {
|
package/tests/task_visuals.mjs
CHANGED
|
@@ -13,13 +13,16 @@ import { createRequire } from 'node:module';
|
|
|
13
13
|
import os from 'node:os';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import fs from 'node:fs/promises';
|
|
16
|
+
import fsSync from 'node:fs';
|
|
16
17
|
const require = createRequire(import.meta.url);
|
|
17
18
|
|
|
18
19
|
// Point storage at a throwaway dir BEFORE requiring the module.
|
|
19
20
|
const TMP = path.join(os.tmpdir(), `otb-taskvisual-${process.pid}`);
|
|
20
21
|
process.env.TASK_VISUALS_DIR = TMP;
|
|
21
22
|
const tv = require('../modules/lifecycle/task-visuals.js');
|
|
22
|
-
|
|
23
|
+
// The renderers left hall-kit.js for hall-visual.js (task 1003835) — the task
|
|
24
|
+
// record page draws a visual and needs nothing else the kit holds.
|
|
25
|
+
const vis = require('../modules/hall-ui/public/hall-visual.js');
|
|
23
26
|
|
|
24
27
|
let passed = 0, failed = 0;
|
|
25
28
|
function t(name, fn) {
|
|
@@ -140,7 +143,7 @@ await t('persistImageBuffer: writes a name the serve route accepts, under the ri
|
|
|
140
143
|
assert.equal(tv.taskIdFromName(r.file), 1003109);
|
|
141
144
|
assert.equal(r.url, `/api/bongos/task-visuals/${r.file}`);
|
|
142
145
|
// Both prefixes are permanent aliases, so a URL minted under either resolves.
|
|
143
|
-
assert.equal(
|
|
146
|
+
assert.equal(vis.isTaskVisualUrl(r.url), true);
|
|
144
147
|
assert.equal(r.bytes, PNG.length);
|
|
145
148
|
const onDisk = await fs.readFile(path.join(TMP, r.file));
|
|
146
149
|
assert.deepEqual(onDisk, PNG);
|
|
@@ -170,23 +173,23 @@ await t('deleteStored: removes by name or URL, is idempotent, refuses a traversa
|
|
|
170
173
|
});
|
|
171
174
|
|
|
172
175
|
// ---- the client-side URL guard --------------------------------------------
|
|
173
|
-
await t('hall-
|
|
174
|
-
assert.equal(
|
|
175
|
-
assert.equal(
|
|
176
|
+
await t('hall-visual: only a server-minted visual URL is ever rendered', () => {
|
|
177
|
+
assert.equal(vis.isTaskVisualUrl('/api/gds/task-visuals/task-1003109-0011223344556677.png'), true);
|
|
178
|
+
assert.equal(vis.isTaskVisualUrl('/api/bongos/task-visuals/task-1-aabbccddeeff0011.webp'), true);
|
|
176
179
|
// Anything that is not that exact shape renders nothing — including an
|
|
177
180
|
// absolute URL to another origin, and a javascript: payload.
|
|
178
|
-
assert.equal(
|
|
179
|
-
assert.equal(
|
|
180
|
-
assert.equal(
|
|
181
|
-
assert.equal(
|
|
182
|
-
assert.equal(
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
await t('hall-
|
|
186
|
-
assert.equal(
|
|
187
|
-
assert.equal(
|
|
188
|
-
assert.equal(
|
|
189
|
-
const html =
|
|
181
|
+
assert.equal(vis.isTaskVisualUrl('https://evil.example/x.png'), false);
|
|
182
|
+
assert.equal(vis.isTaskVisualUrl('javascript:alert(1)'), false);
|
|
183
|
+
assert.equal(vis.isTaskVisualUrl('/api/gds/task-visuals/../../secrets'), false);
|
|
184
|
+
assert.equal(vis.isTaskVisualUrl(''), false);
|
|
185
|
+
assert.equal(vis.isTaskVisualUrl(null), false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await t('hall-visual: no visual renders nothing at all — never a placeholder', () => {
|
|
189
|
+
assert.equal(vis.shipVisualThumbHtml({ id: 1 }), '');
|
|
190
|
+
assert.equal(vis.shipVisualThumbHtml({ id: 1, visual_url: null }), '');
|
|
191
|
+
assert.equal(vis.shipVisualThumbHtml(null), '');
|
|
192
|
+
const html = vis.shipVisualThumbHtml({ id: 1, visual_url: '/api/gds/task-visuals/task-1-0011223344556677.png', visual_alt: 'a "before" shot' });
|
|
190
193
|
assert.ok(html.includes('ship-visual-thumb'));
|
|
191
194
|
// The caption is attacker-adjacent text (a builder wrote it) — it must be escaped.
|
|
192
195
|
assert.ok(html.includes('"before"'));
|
|
@@ -194,18 +197,18 @@ await t('hall-kit: no visual renders nothing at all — never a placeholder', ()
|
|
|
194
197
|
});
|
|
195
198
|
|
|
196
199
|
// ---- the full-size figure (task 1003829) -----------------------------------
|
|
197
|
-
await t('hall-
|
|
200
|
+
await t('hall-visual: the full-size figure is the same guard, at reading size', () => {
|
|
198
201
|
// Same no-placeholder rule as the thumbnail: nothing at all without a visual.
|
|
199
|
-
assert.equal(
|
|
200
|
-
assert.equal(
|
|
201
|
-
assert.equal(
|
|
202
|
+
assert.equal(vis.taskVisualFigureHtml({ id: 1 }), '');
|
|
203
|
+
assert.equal(vis.taskVisualFigureHtml({ id: 1, visual_url: null }), '');
|
|
204
|
+
assert.equal(vis.taskVisualFigureHtml(null), '');
|
|
202
205
|
// And the same URL wall — an off-origin src or a javascript: payload is not
|
|
203
206
|
// "rendered without a caption", it is not rendered.
|
|
204
|
-
assert.equal(
|
|
205
|
-
assert.equal(
|
|
207
|
+
assert.equal(vis.taskVisualFigureHtml({ id: 1, visual_url: 'https://evil.example/x.png' }), '');
|
|
208
|
+
assert.equal(vis.taskVisualFigureHtml({ id: 1, visual_url: 'javascript:alert(1)' }), '');
|
|
206
209
|
|
|
207
210
|
const url = '/api/gds/task-visuals/task-1-0011223344556677.png';
|
|
208
|
-
const html =
|
|
211
|
+
const html = vis.taskVisualFigureHtml({ id: 1, visual_url: url, visual_alt: 'a "before" shot' });
|
|
209
212
|
assert.ok(html.includes('<figure class="task-visual">'));
|
|
210
213
|
assert.ok(html.includes('class="task-visual__img"'));
|
|
211
214
|
// The caption is the SHIPPER's text — attacker-adjacent, so escaped in both
|
|
@@ -215,19 +218,80 @@ await t('hall-kit: the full-size figure is the same guard, at reading size', ()
|
|
|
215
218
|
|
|
216
219
|
// The caller owns the size, so it owns the class base; anything but
|
|
217
220
|
// [A-Za-z0-9_-] is stripped rather than interpolated into the attribute.
|
|
218
|
-
const studio =
|
|
221
|
+
const studio = vis.taskVisualFigureHtml({ id: 1, visual_url: url }, { className: 'studio-plate' });
|
|
219
222
|
assert.ok(studio.includes('<figure class="studio-plate">'));
|
|
220
223
|
assert.ok(studio.includes('class="studio-plate__img"'));
|
|
221
|
-
const hostile =
|
|
224
|
+
const hostile = vis.taskVisualFigureHtml({ id: 1, visual_url: url }, { className: 'x" onload="a' });
|
|
222
225
|
assert.ok(hostile.includes('<figure class="xonloada">'), hostile);
|
|
223
226
|
|
|
224
227
|
// No written alt → no caption at all. The fallback alt is a machine sentence
|
|
225
228
|
// for a screen reader, never a caption shown to a reader.
|
|
226
|
-
const bare =
|
|
229
|
+
const bare = vis.taskVisualFigureHtml({ id: 7, visual_url: url });
|
|
227
230
|
assert.ok(!bare.includes('figcaption'));
|
|
228
231
|
assert.ok(bare.includes('alt="Visual for task 7"'));
|
|
229
232
|
});
|
|
230
233
|
|
|
234
|
+
// ---- the convergence is the deliverable (task 1003835) ---------------------
|
|
235
|
+
//
|
|
236
|
+
// Reading the source is the right altitude here: the defect this task closes is
|
|
237
|
+
// a SECOND definition existing, which no behavioural assertion against one of
|
|
238
|
+
// them can see. A duplicate renderer passes every test above.
|
|
239
|
+
|
|
240
|
+
const HALL = (f) => fsSync.readFileSync(new URL(`../modules/hall-ui/public/${f}`, import.meta.url), 'utf8');
|
|
241
|
+
|
|
242
|
+
await t('hall-kit no longer defines a visual renderer', () => {
|
|
243
|
+
const kitSrc = HALL('hall-kit.js');
|
|
244
|
+
// The function DEFINITIONS are what must be gone — the pointer comment naming
|
|
245
|
+
// them is the thing that tells the next reader where they went, so match on
|
|
246
|
+
// the definition syntax rather than on the bare name.
|
|
247
|
+
assert.ok(!/function\s+taskVisualFigureHtml\s*\(/.test(kitSrc), 'hall-kit.js still defines taskVisualFigureHtml');
|
|
248
|
+
assert.ok(!/function\s+shipVisualThumbHtml\s*\(/.test(kitSrc), 'hall-kit.js still defines shipVisualThumbHtml');
|
|
249
|
+
assert.ok(!/function\s+isTaskVisualUrl\s*\(/.test(kitSrc), 'hall-kit.js still defines isTaskVisualUrl');
|
|
250
|
+
// …and it must not re-export them either, or callers keep two ways in.
|
|
251
|
+
const kitApi = require('../modules/hall-ui/public/hall-kit.js');
|
|
252
|
+
assert.equal(kitApi.taskVisualFigureHtml, undefined);
|
|
253
|
+
assert.equal(kitApi.shipVisualThumbHtml, undefined);
|
|
254
|
+
assert.equal(kitApi.isTaskVisualUrl, undefined);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await t('task.js renders through the shared module, not its own <figure>', () => {
|
|
258
|
+
const src = HALL('task.js');
|
|
259
|
+
assert.ok(src.includes('vis.taskVisualFigureHtml(t, { className: \'tk-visual\' })'),
|
|
260
|
+
'task.js should delegate to OTBVisual with its own class base');
|
|
261
|
+
// The hand-rolled markup is the thing that came back three times. If any of it
|
|
262
|
+
// reappears in this file, the convergence has been undone.
|
|
263
|
+
assert.ok(!src.includes('<figure class="tk-visual">'), 'task.js hand-rolls the figure again');
|
|
264
|
+
assert.ok(!src.includes('tk-visual__img'), 'task.js hand-rolls the img class again');
|
|
265
|
+
assert.ok(!src.includes('task-visuals/task-'), 'task.js carries its own copy of the URL guard again');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await t('every page whose script draws a visual actually loads hall-visual.js', () => {
|
|
269
|
+
// A missing <script> is the one failure this split introduces, and it is
|
|
270
|
+
// silent: the guard in each caller degrades to rendering nothing, so a visual
|
|
271
|
+
// would simply stop appearing with no error anywhere.
|
|
272
|
+
for (const page of ['index.html', 'profile.html', 'studio.html', 'task.html']) {
|
|
273
|
+
assert.ok(HALL(page).includes('/builders/hall-visual.js'), `${page} does not load hall-visual.js`);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await t('the callers reach for OTBVisual, and no longer for the kit', () => {
|
|
278
|
+
for (const f of ['hall-render.js', 'profile.js', 'studio.js', 'task.js']) {
|
|
279
|
+
const src = HALL(f);
|
|
280
|
+
assert.ok(!/OTBKit(\.|\[')?\s*(shipVisualThumbHtml|taskVisualFigureHtml|isTaskVisualUrl)/.test(src),
|
|
281
|
+
`${f} still reads a visual renderer off OTBKit`);
|
|
282
|
+
}
|
|
283
|
+
assert.ok(HALL('hall-render.js').includes('window.OTBVisual.shipVisualThumbHtml'));
|
|
284
|
+
assert.ok(HALL('studio.js').includes('vis.taskVisualFigureHtml('));
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await t('the dead .td-visual rules are gone, and nothing emits the class', () => {
|
|
288
|
+
// task 1003829's comment and this task's own brief both asserted a second
|
|
289
|
+
// renderer in task-detail.js. `git log -S` shows that file never carried one:
|
|
290
|
+
// the CSS was the fossil of a renderer that was planned and not built.
|
|
291
|
+
assert.ok(!HALL('style.css').includes('.td-visual'), 'style.css still carries the dead rules');
|
|
292
|
+
assert.ok(!HALL('task-detail.js').includes('td-visual'));
|
|
293
|
+
});
|
|
294
|
+
|
|
231
295
|
// ---- cleanup ---------------------------------------------------------------
|
|
232
296
|
await fs.rm(TMP, { recursive: true, force: true });
|
|
233
297
|
|