@bongos/core 1.20.37 → 1.20.39

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.
Files changed (68) hide show
  1. package/.bongos-core.json +119 -64
  2. package/.claude/skills/backlog-review/SKILL.md +1 -1
  3. package/.claude/skills/bongos-feedback/SKILL.md +66 -0
  4. package/.claude/skills/collab-review/SKILL.md +1 -1
  5. package/.claude/skills/priority-session/SKILL.md +1 -1
  6. package/.claude/skills/session-handoff/SKILL.md +1 -1
  7. package/.claude/skills/status/SKILL.md +1 -1
  8. package/bin/bongos.js +1 -0
  9. package/clients/bongos-client/README.md +1 -1
  10. package/clients/bongos-client/bongos-client.global.js +8 -0
  11. package/clients/bongos-client/index.cjs +8 -0
  12. package/clients/bongos-client/index.d.ts +12 -1
  13. package/clients/bongos-client/index.mjs +8 -0
  14. package/docs/adr/0136-update-channel-subscription-policy.md +1 -1
  15. package/docs/adr/0357-the-update-rule-set-on-deploy-is-the-one-the-sweep-follows.md +41 -0
  16. package/docs/adr/README.md +1 -0
  17. package/docs/api/openapi.json +188 -2
  18. package/docs/api-reference.md +12 -4
  19. package/docs/copy-inventory.md +10 -10
  20. package/docs/copy-registry.json +18 -18
  21. package/docs/file-map.md +1 -0
  22. package/docs/module-api-changelog.md +4 -0
  23. package/docs/page-readings.json +253 -250
  24. package/docs/recipes/upgrading-the-core.md +2 -0
  25. package/modules/autonomy/db.js +39 -8
  26. package/modules/autonomy/fence.js +26 -4
  27. package/modules/autonomy/migrations/autonomy_003_priority_goal.sql +32 -0
  28. package/modules/autonomy/routes/autonomy.js +28 -1
  29. package/modules/hall-ui/public/gate.js +33 -4
  30. package/modules/ideas/routing.js +8 -1
  31. package/modules/npm-release/preview/childlog.js +95 -0
  32. package/modules/npm-release/preview/commands.js +46 -5
  33. package/modules/npm-release/preview/env.js +18 -7
  34. package/modules/npm-release/preview/runtime.js +43 -9
  35. package/modules/npm-release/preview/supervisor.js +170 -31
  36. package/modules/npm-release/public/work.css +3 -0
  37. package/modules/npm-release/public/work.js +6 -1
  38. package/modules/npm-release/routes/preview.js +1 -0
  39. package/modules/platform-identity/instance-feedback.js +142 -0
  40. package/modules/platform-identity/module.json +1 -1
  41. package/modules/platform-identity/routes/sso.js +62 -0
  42. package/modules/provisioning/render-standup.js +13 -1
  43. package/modules/public-landing/public/projects.html +22 -8
  44. package/package-lock.json +2 -2
  45. package/package.json +1 -1
  46. package/release-notes.json +30 -0
  47. package/scripts/gds/autobongos-run.js +14 -4
  48. package/scripts/gds/feedback-send.js +131 -0
  49. package/scripts/gds/provision-render.js +13 -1
  50. package/scripts/gds/render-payload.js +6 -5
  51. package/scripts/gds/update-channel.js +82 -0
  52. package/src/bongos/routes/feedback.js +110 -0
  53. package/src/bongos/routes.js +2 -0
  54. package/src/module-api.js +1 -1
  55. package/tests/autobongos_fence.mjs +62 -0
  56. package/tests/autobongos_loop.mjs +76 -0
  57. package/tests/autonomy_fence_priority.mjs +211 -0
  58. package/tests/bongos_cli.mjs +7 -1
  59. package/tests/filing_abuse_matrix.mjs +7 -2
  60. package/tests/idea_routing.mjs +1 -1
  61. package/tests/instance_feedback.mjs +327 -0
  62. package/tests/npm_release_preview_commands.mjs +1 -1
  63. package/tests/npm_release_preview_diagnosis.mjs +376 -0
  64. package/tests/npm_release_preview_supervisor.mjs +4 -4
  65. package/tests/projects_hub_app_step.mjs +49 -0
  66. package/tests/projects_hub_render_connect.mjs +2 -1
  67. package/tests/provision_render.mjs +23 -2
  68. package/tests/update_channel_db.mjs +124 -0
@@ -0,0 +1,327 @@
1
+ // tests/instance_feedback.mjs — `bongos feedback`: a builder on a managed project
2
+ // reports something about Bongos itself, their project relays it to the hub, and
3
+ // the hub files it (task 1004462). DB-free: the hub half is driven through its
4
+ // pure module with a fake `ideas.capture`, the project half through its real
5
+ // router with a fake hub.
6
+
7
+ import assert from 'node:assert/strict';
8
+ import { test } from 'node:test';
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { createRequire } from 'node:module';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
+ const require_ = createRequire(import.meta.url);
16
+ const IF = require_(path.join(ROOT, 'modules/platform-identity/instance-feedback.js'));
17
+ const { decideLanding } = require_(path.join(ROOT, 'modules/ideas/routing.js'));
18
+
19
+ const CLIENT = { client_id: 'acme', name: 'Acme', origin: 'https://acme.example.com' };
20
+
21
+ // ── the hub: who gets to file, and where it lands ─────────────────────────────
22
+
23
+ test('a relayed bug lands as a homeless bug AWAITING A NOD, never claimable', () => {
24
+ // The owner's rule (2026-09-30): every report waits. The relay files with no
25
+ // rank, so no sender — the hub's own Archon included — can skip the nod.
26
+ const d = decideLanding({ goalId: null, kind: 'bug', filerRank: null, vector: 'instance-feedback' });
27
+ assert.deepEqual(d, { route: true, status: 'backlog', homeless: true, reason: 'bug_homeless_awaiting_nod' });
28
+ });
29
+
30
+ test('a relayed idea is not a defect — it stays in the inbox', () => {
31
+ const d = decideLanding({ goalId: null, kind: IF.FEEDBACK_KINDS.idea, filerRank: null, vector: 'instance-feedback' });
32
+ assert.equal(d.route, false);
33
+ });
34
+
35
+ test('fileInstanceFeedback files with its own vector, no rank, and the hub\'s own login', async () => {
36
+ let captured = null;
37
+ const landing = await IF.fileInstanceFeedback({
38
+ client: CLIENT, githubId: '42', message: 'claim hung\nfor two minutes', kind: 'bug',
39
+ context: { core_version: '1.19.900', cli_version: '0.2.0', os: 'darwin 24.6.0' },
40
+ }, {
41
+ capture: async (c) => { captured = c; return { id: 7, routing: { routed: true, task_id: 1234, goal_id: 1000118, status: 'backlog' } }; },
42
+ getAccount: async () => ({ github_login: 'hub-login' }),
43
+ hubBuilderId: async () => 99,
44
+ });
45
+ assert.equal(captured.vector, 'instance-feedback');
46
+ assert.equal(captured.filerRank, null);
47
+ assert.equal(captured.kind, 'bug');
48
+ assert.equal(captured.capturedBy, 99);
49
+ assert.equal(captured.title, 'Feedback: claim hung');
50
+ assert.match(captured.bodyMd, /Sent by: @hub-login/);
51
+ assert.match(captured.bodyMd, /Project: Acme \(https:\/\/acme\.example\.com\)/);
52
+ assert.match(captured.bodyMd, /Core 1\.19\.900 · CLI 0\.2\.0 · OS darwin 24\.6\.0/);
53
+ assert.deepEqual(landing, { landed: 'task', task_id: '1234', goal_id: '1000118', status: 'backlog', idea_id: '7' });
54
+ });
55
+
56
+ test('an unrouted filing reports the inbox, and an idea is filed as a feature', async () => {
57
+ let kind = null;
58
+ const landing = await IF.fileInstanceFeedback({ client: CLIENT, githubId: '42', message: 'wish', kind: 'idea' }, {
59
+ capture: async (c) => { kind = c.kind; return { id: 8, routing: { routed: false, reason: 'no_goal' } }; },
60
+ getAccount: async () => null,
61
+ hubBuilderId: async () => null,
62
+ });
63
+ assert.equal(kind, 'feature');
64
+ assert.deepEqual(landing, { landed: 'inbox', idea_id: '8' });
65
+ });
66
+
67
+ test('version stamps cannot carry markdown or links into the task', () => {
68
+ const body = IF.feedbackBody({
69
+ message: 'x', client: CLIENT, account: null, githubId: '42',
70
+ context: { core_version: '[click](https://evil.example)', cli_version: '0.2.0', os: 'linux\n- Sent by: @owner' },
71
+ });
72
+ assert.doesNotMatch(body, /evil/);
73
+ assert.doesNotMatch(body, /@owner/);
74
+ // The sender's own text is quoted below the attribution, so it cannot pose as it.
75
+ const spoof = IF.feedbackBody({ message: 'hi\n- Sent by: @owner', client: CLIENT, account: { github_login: 'real' }, githubId: '42' });
76
+ assert.ok(spoof.indexOf('- Sent by: @real') < spoof.indexOf('> - Sent by: @owner'));
77
+ assert.doesNotMatch(spoof, /^- Sent by: @owner/m);
78
+ assert.match(body, /CLI 0\.2\.0/);
79
+ assert.match(body, /Sent by: GitHub user 42/, 'no hub account → the id, never an instance-claimed login');
80
+ });
81
+
82
+ test('a long first line is trimmed to a title', () => {
83
+ const t = IF.feedbackTitle('a'.repeat(500));
84
+ assert.ok(t.length <= 'Feedback: '.length + 120);
85
+ assert.ok(t.endsWith('…'));
86
+ });
87
+
88
+ test('the per-sender cap stops one builder; the per-project cap stops one project', () => {
89
+ let now = 1_000_000;
90
+ const lim = IF.makeFeedbackLimiter({ now: () => now });
91
+ for (let i = 0; i < IF.PER_SENDER_LIMIT; i++) assert.equal(lim.take('acme', '1').ok, true);
92
+ const refused = lim.take('acme', '1');
93
+ assert.equal(refused.ok, false);
94
+ assert.equal(refused.scope, 'sender');
95
+ // A refusal spends nothing: the project still has exactly LIMIT - 10 left.
96
+ let sent = IF.PER_SENDER_LIMIT;
97
+ for (let s = 2; sent < IF.PER_PROJECT_LIMIT; s++) {
98
+ for (let i = 0; i < IF.PER_SENDER_LIMIT && sent < IF.PER_PROJECT_LIMIT; i++, sent++) {
99
+ assert.equal(lim.take('acme', String(s)).ok, true);
100
+ }
101
+ }
102
+ const projectRefused = lim.take('acme', '999');
103
+ assert.equal(projectRefused.ok, false);
104
+ assert.equal(projectRefused.scope, 'project');
105
+ assert.equal(lim.take('other', '1').ok, true, 'another project is untouched');
106
+ now += IF.FEEDBACK_WINDOW_MS;
107
+ assert.equal(lim.take('acme', '1').ok, true, 'the window slides');
108
+ });
109
+
110
+ test('the hub route authenticates the project and the sender BEFORE spending or filing', () => {
111
+ const src = fs.readFileSync(path.join(ROOT, 'modules/platform-identity/routes/sso.js'), 'utf8');
112
+ const route = src.slice(src.indexOf("router.post('/sso/feedback'"));
113
+ const at = (s) => { const i = route.indexOf(s); assert.ok(i > 0, `route has ${s}`); return i; };
114
+ assert.ok(at('clientSecretValid') < at('membershipBacksRollup'));
115
+ assert.ok(at('membershipBacksRollup') < at('feedbackLimiter.take'));
116
+ assert.ok(at('feedbackLimiter.take') < at('fileInstanceFeedback'));
117
+ assert.doesNotMatch(route.slice(0, route.indexOf('return router')), /filerRank|vector:/,
118
+ 'the route passes neither a rank nor a vector — both are fixed in instance-feedback.js');
119
+ });
120
+
121
+ // ── the project: the relay ────────────────────────────────────────────────────
122
+
123
+ async function withRoute({ idp, hub }, fn) {
124
+ const r2 = createRequire(import.meta.url);
125
+ const authPath = r2.resolve(path.join(ROOT, 'src/bongos/auth.js'));
126
+ const routePath = r2.resolve(path.join(ROOT, 'src/bongos/routes/feedback.js'));
127
+ const envPath = r2.resolve(path.join(ROOT, 'src/bongos/middleware/error-envelope.js'));
128
+ const realAuth = r2(authPath);
129
+ const saved = realAuth.requireBuilder;
130
+ realAuth.requireBuilder = (req, _res, next) => { req.builder = { id: 5, github_id: '42', github_login: 'someone' }; next(); };
131
+ delete r2.cache[routePath];
132
+ const sent = [];
133
+ const fetchImpl = async (url, opts) => {
134
+ sent.push({ url, body: JSON.parse(opts.body) });
135
+ const { status, body } = hub;
136
+ return { status, json: async () => body };
137
+ };
138
+ const express = r2('express');
139
+ const app = express();
140
+ app.use(express.json());
141
+ app.use(r2(envPath).attachFail);
142
+ app.use(r2(routePath)({ idpConfig: () => idp, fetchImpl }));
143
+ const server = app.listen(0);
144
+ await new Promise((res) => server.once('listening', res));
145
+ const post = (body) => fetch(`http://127.0.0.1:${server.address().port}/feedback`, {
146
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
147
+ }).then(async (r) => ({ status: r.status, body: await r.json() }));
148
+ try {
149
+ await fn({ post, sent });
150
+ } finally {
151
+ server.close();
152
+ realAuth.requireBuilder = saved;
153
+ delete r2.cache[routePath];
154
+ }
155
+ }
156
+
157
+ const IDP = { origin: 'https://hub.example.com', clientId: 'acme', clientSecret: 's3cret' };
158
+
159
+ test('the relay sends the SESSION\'s identity and the running core to the hub, and returns the landing', async () => {
160
+ await withRoute({ idp: IDP, hub: { status: 201, body: { ok: true, landed: 'task', task_id: '1234', status: 'backlog' } } }, async ({ post, sent }) => {
161
+ assert.equal((await post({ message: 'x', github_id: '1' })).status, 400, 'a body naming a sender is refused outright');
162
+ assert.equal(sent.length, 0);
163
+ const r = await post({ message: ' it broke ', context: { cli_version: '0.2.0', os: 'darwin', core_version: 'forged' } });
164
+ assert.equal(r.status, 201);
165
+ assert.equal(r.body.task_id, '1234');
166
+ assert.equal(r.body.hub_origin, IDP.origin);
167
+ assert.equal(sent.length, 1);
168
+ assert.equal(sent[0].url, 'https://hub.example.com/api/bongos/sso/feedback');
169
+ assert.equal(sent[0].body.github_id, '42', 'from the session, never the body');
170
+ assert.equal(sent[0].body.message, 'it broke');
171
+ assert.equal(sent[0].body.kind, 'bug');
172
+ assert.notEqual(sent[0].body.context.core_version, 'forged', 'the core version is this server\'s own');
173
+ assert.equal(sent[0].body.client_secret, 's3cret');
174
+ });
175
+ });
176
+
177
+ test('a project with no hub link says so, and sends nothing', async () => {
178
+ await withRoute({ idp: null, hub: { status: 500, body: {} } }, async ({ post, sent }) => {
179
+ const r = await post({ message: 'hello' });
180
+ assert.equal(r.status, 409);
181
+ assert.equal(r.body.error.code, 'not_hub_connected');
182
+ assert.equal(sent.length, 0);
183
+ });
184
+ });
185
+
186
+ test('an empty message or an unknown kind is refused locally', async () => {
187
+ await withRoute({ idp: IDP, hub: { status: 201, body: { ok: true } } }, async ({ post, sent }) => {
188
+ assert.equal((await post({ message: ' ' })).body.error.code, 'message_required');
189
+ const badKind = await post({ message: 'x', kind: 'task' });
190
+ assert.equal(badKind.status, 400);
191
+ assert.equal((await post({ message: 'x'.repeat(4001) })).status, 400, 'over the cap');
192
+ assert.equal(sent.length, 0);
193
+ });
194
+ });
195
+
196
+ test('a hub refusal the builder can act on is passed through; anything else is generic', async () => {
197
+ await withRoute({ idp: IDP, hub: { status: 429, body: { error: { code: 'rate_limited', details: { retry_after_seconds: 600 } } } } }, async ({ post }) => {
198
+ const r = await post({ message: 'x' });
199
+ assert.equal(r.status, 429);
200
+ assert.equal(r.body.error.code, 'rate_limited');
201
+ assert.equal(r.body.error.details.retry_after_seconds, 600);
202
+ });
203
+ await withRoute({ idp: IDP, hub: { status: 401, body: { error: { code: 'invalid_client', message: 'secret mismatch' } } } }, async ({ post }) => {
204
+ const r = await post({ message: 'x' });
205
+ assert.equal(r.status, 502);
206
+ assert.equal(r.body.error.code, 'hub_refused');
207
+ assert.doesNotMatch(JSON.stringify(r.body), /secret/);
208
+ });
209
+ });
210
+
211
+ // ── the CLI ───────────────────────────────────────────────────────────────────
212
+
213
+ test('`bongos feedback` is a registered verb, and every refusal code has a plain line', () => {
214
+ const { VERBS } = require_(path.join(ROOT, 'bin/bongos.js'));
215
+ assert.equal(VERBS.feedback.script, 'feedback-send.js');
216
+ const { REFUSALS, readMessage } = require_(path.join(ROOT, 'scripts/gds/feedback-send.js'));
217
+ for (const code of ['message_required', 'not_hub_connected', 'membership_required', 'rate_limited', 'feedback_unavailable', 'hub_unreachable']) {
218
+ assert.ok(REFUSALS[code], code);
219
+ }
220
+ for (const code of ['validation_failed']) {
221
+ assert.ok(REFUSALS[code], code);
222
+ }
223
+ assert.equal(readMessage(['--idea', 'filter', 'by', 'goal']), 'filter by goal');
224
+ });
225
+
226
+ // ── the hub route, driven for real ────────────────────────────────────────────
227
+ // The assembled /sso router with its DB edges stubbed: the client lookup, the
228
+ // membership read and the account read on the platform-identity domain, the
229
+ // builders lookup on the doorway's pool, and the `ideas.capture` port.
230
+
231
+ async function withHub(overrides, fn) {
232
+ const r2 = createRequire(import.meta.url);
233
+ const api = r2(path.join(ROOT, 'src/module-api.js'));
234
+ const seams = r2(path.join(ROOT, 'src/module-seams.js'));
235
+ const pi = r2(path.join(ROOT, 'modules/platform-identity/platform-identity.js'));
236
+ const saved = { ...pi };
237
+ const savedQuery = api.pool.query;
238
+ const captured = [];
239
+ Object.assign(pi, {
240
+ getClient: async (id) => (id === 'acme' ? { ...CLIENT, status: 'active' } : null),
241
+ clientSecretValid: (_c, s) => s === 'right',
242
+ getMembership: async () => ({ membership_kind: 'member', signin_verified_at: new Date() }),
243
+ getAccount: async () => ({ github_login: 'hub-login' }),
244
+ }, overrides);
245
+ api.pool.query = async () => ({ rows: [] });
246
+ if (!seams.hasProvider('ideas.capture')) {
247
+ seams.registerProvider('ideas.capture', async (c) => { captured.push(c); return { id: 1, routing: { routed: true, task_id: 55, goal_id: 9, status: 'backlog' } }; });
248
+ }
249
+ const routerPath = r2.resolve(path.join(ROOT, 'modules/platform-identity/routes/sso.js'));
250
+ delete r2.cache[routerPath];
251
+ const express = r2('express');
252
+ const app = express();
253
+ app.use(express.json());
254
+ app.use(r2(path.join(ROOT, 'src/bongos/middleware/error-envelope.js')).attachFail);
255
+ app.use(r2(routerPath)());
256
+ const server = app.listen(0);
257
+ await new Promise((res) => server.once('listening', res));
258
+ const post = (body) => fetch(`http://127.0.0.1:${server.address().port}/sso/feedback`, {
259
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
260
+ }).then(async (res) => ({ status: res.status, body: await res.json() }));
261
+ try { await fn({ post, captured }); } finally {
262
+ server.close();
263
+ Object.assign(pi, saved);
264
+ api.pool.query = savedQuery;
265
+ delete r2.cache[routerPath];
266
+ }
267
+ }
268
+
269
+ const GOOD = { client_id: 'acme', client_secret: 'right', github_id: '42', message: 'claim hung' };
270
+
271
+ test('hub: a witnessed member\'s bug is filed at backlog and the landing comes back 201', async () => {
272
+ await withHub({}, async ({ post, captured }) => {
273
+ const r = await post(GOOD);
274
+ assert.equal(r.status, 201, JSON.stringify(r.body));
275
+ assert.deepEqual(r.body, { ok: true, landed: 'task', task_id: '55', goal_id: '9', status: 'backlog', idea_id: '1' });
276
+ assert.equal(captured.at(-1).vector, 'instance-feedback');
277
+ assert.equal(captured.at(-1).filerRank, null);
278
+ });
279
+ });
280
+
281
+ test('hub: a wrong secret, an unwitnessed sender and a forged field are all refused before filing', async () => {
282
+ await withHub({}, async ({ post, captured }) => {
283
+ const before = captured.length;
284
+ assert.equal((await post({ ...GOOD, client_secret: 'wrong' })).status, 401);
285
+ assert.equal((await post({ ...GOOD, github_id: 'abc' })).status, 400);
286
+ assert.equal((await post({ ...GOOD, filer_rank: 'archon' })).status, 400, 'no rank can ride the body');
287
+ assert.equal(captured.length, before);
288
+ });
289
+ await withHub({ getMembership: async () => ({ membership_kind: 'member', signin_verified_at: null }) }, async ({ post, captured }) => {
290
+ const before = captured.length;
291
+ const r = await post(GOOD);
292
+ assert.equal(r.status, 409);
293
+ assert.equal(r.body.error.code, 'membership_required');
294
+ assert.equal(captured.length, before);
295
+ });
296
+ });
297
+
298
+ // ── proactive filing (task 1004467) ───────────────────────────────────────────
299
+ // The owner's rule: a builder venting that Bongos is broken gets it filed without
300
+ // asking. That lives in two places, and both must keep saying it.
301
+
302
+ test('the skill files unasked on a Bongos rant, and the kernel tells every session to', () => {
303
+ const skill = fs.readFileSync(path.join(ROOT, '.claude/skills/bongos-feedback/SKILL.md'), 'utf8');
304
+ const description = skill.split('\n---')[0];
305
+ assert.match(description, /unasked/, 'the listing entry itself says it acts unprompted');
306
+ assert.match(description, /Bongos is broken/, 'a venting phrase is a trigger');
307
+ assert.match(skill, /File it without being asked/);
308
+ assert.match(skill, /Never paste the rant/, 'a report, not the raw complaint');
309
+ assert.match(skill, /own project/, 'their own code is out of scope');
310
+ assert.match(skill, /One filing per distinct problem per session/);
311
+ const kernel = fs.readFileSync(path.join(ROOT, 'CLAUDE.md'), 'utf8');
312
+ assert.match(kernel, /vents that Bongos itself is broken.*\/bongos-feedback/);
313
+ });
314
+
315
+ test('every report is secret-scrubbed before it leaves the machine, and never sent unscrubbed', async () => {
316
+ const { scrubReport } = require_(path.join(ROOT, 'scripts/gds/feedback-send.js'));
317
+ const token = 'ghp_' + 'a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8';
318
+ const envValue = 'zq9-private-value-7781';
319
+ const r = scrubReport(`claim failed, my token is ${token} and the key is ${envValue}`, { MY_API_KEY: envValue });
320
+ assert.doesNotMatch(r.text, /ghp_/);
321
+ assert.ok(!r.text.includes(envValue), 'an env secret is stripped by value');
322
+ assert.ok(r.removed >= 2);
323
+ assert.equal(scrubReport('bongos claim hung for two minutes', {}).text, 'bongos claim hung for two minutes', 'plain prose is untouched');
324
+ const src = fs.readFileSync(path.join(ROOT, 'scripts/gds/feedback-send.js'), 'utf8');
325
+ assert.ok(src.indexOf('scrubReport(message)') < src.indexOf("api.request('POST', '/feedback'"), 'scrubbed before the request');
326
+ assert.match(src, /message: scrubbed\.text/, 'only the scrubbed text is sent');
327
+ });
@@ -52,7 +52,7 @@ test('the copy is a custom-format dump of the live database, restored with no ow
52
52
 
53
53
  test('the copy is created empty and dropped if it is already there', () => {
54
54
  assert.deepEqual(c.createDbCommand(), { cmd: 'createdb', args: [PREVIEW_DB] });
55
- assert.deepEqual(c.dropDbCommand(), { cmd: 'dropdb', args: ['--if-exists', PREVIEW_DB] });
55
+ assert.deepEqual(c.dropDbCommand(), { cmd: 'dropdb', args: ['--if-exists', '--force', PREVIEW_DB] });
56
56
  });
57
57
 
58
58
  test('the install is one version of the core into its own directory, production dependencies only', () => {