@bongos/core 1.20.64 → 1.20.66

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.
@@ -0,0 +1,285 @@
1
+ // tests/goal_move.mjs — POST /goals/:id/move re-homes a goal, whole, on another
2
+ // version (task 1004485).
3
+ //
4
+ // Three layers, the goal_archive_disposition / goal_archive_route_e2e split in one
5
+ // file: the pure gate's truth table, the writer's transaction shape against a fake
6
+ // pool (every write inside ONE transaction, tasks moved by version_id AND goal,
7
+ // the cross-goal criterion refusal BEFORE any write), and the real router over a
8
+ // real socket with the db layer stubbed at the module boundary.
9
+ //
10
+ // Run: node --test tests/goal_move.mjs
11
+
12
+ import assert from 'node:assert/strict';
13
+ import { test, after } from 'node:test';
14
+ import { readFileSync } from 'node:fs';
15
+ import { createRequire } from 'node:module';
16
+ import { makeSqlAwareClient } from './helpers.mjs';
17
+
18
+ process.env.NODE_ENV = 'test';
19
+ const require = createRequire(import.meta.url);
20
+ const { authorizeGoalMove, moveGoalToVersion } = require('../modules/lifecycle/goal-move.js');
21
+
22
+ const OPEN_GOAL = { id: '7', version_id: 'V2', status: 'open', is_maintenance: false };
23
+ const V2 = { id: 'V2', status: 'building' };
24
+ const V3 = { id: 'V3', status: 'planning' };
25
+
26
+ // ---- the pure gate ----------------------------------------------------------
27
+
28
+ test('an open goal moves to the planning version', () => {
29
+ const d = authorizeGoalMove({ goal: OPEN_GOAL, source: V2, target: V3 });
30
+ assert.deepEqual(d, { ok: true, overrideApplied: false });
31
+ });
32
+
33
+ test('refusals, each by name', () => {
34
+ const cases = [
35
+ [{ goal: null, source: null, target: V3 }, 404, 'goal_not_found'],
36
+ [{ goal: { ...OPEN_GOAL, is_maintenance: true }, source: V2, target: V3 }, 409, 'maintenance_goal'],
37
+ [{ goal: { ...OPEN_GOAL, status: 'achieved' }, source: V2, target: V3 }, 409, 'goal_not_open'],
38
+ [{ goal: { ...OPEN_GOAL, status: 'archived' }, source: V2, target: V3 }, 409, 'goal_not_open'],
39
+ [{ goal: OPEN_GOAL, source: { id: 'V2', status: 'shipped' }, target: V3 }, 409, 'source_version_shipped'],
40
+ [{ goal: OPEN_GOAL, source: V2, target: V2 }, 409, 'same_version'],
41
+ [{ goal: OPEN_GOAL, source: V2, target: null }, 404, 'version_not_found'],
42
+ [{ goal: OPEN_GOAL, source: V2, target: { id: 'V1', status: 'shipped' } }, 409, 'version_closed'],
43
+ [{ goal: { ...OPEN_GOAL, version_id: 'V3' }, source: V3, target: V2 }, 409, 'version_not_planning'],
44
+ ];
45
+ for (const [input, status, error] of cases) {
46
+ const d = authorizeGoalMove(input);
47
+ assert.equal(d.ok, false, error);
48
+ assert.equal(d.status, status, error);
49
+ assert.equal(d.body.error, error);
50
+ }
51
+ });
52
+
53
+ test('a building target opens only with the admit_reason override, and says so', () => {
54
+ const goal = { ...OPEN_GOAL, version_id: 'V3' };
55
+ assert.deepEqual(authorizeGoalMove({ goal, source: V3, target: V2, admitReason: 'owner pulled it back' }),
56
+ { ok: true, overrideApplied: true });
57
+ // On a planning target the reason changes nothing — so nothing is recorded.
58
+ assert.deepEqual(authorizeGoalMove({ goal: OPEN_GOAL, source: V2, target: V3, admitReason: 'x' }),
59
+ { ok: true, overrideApplied: false });
60
+ });
61
+
62
+ // ---- the writer ---------------------------------------------------------------
63
+
64
+ const flat = (q) => String(q.sql).replace(/\s+/g, ' ').trim();
65
+ function txPool(handler) {
66
+ const c = makeSqlAwareClient(handler);
67
+ return { connect: c.connect, query: (sql, params) => c._client.query(sql, params), queries: c._client.queries };
68
+ }
69
+ function happyPool(over = {}) {
70
+ return txPool((q) => {
71
+ for (const [re, rows] of Object.entries(over)) if (new RegExp(re).test(q)) return { rows };
72
+ if (/FROM goals WHERE id = \$1 FOR UPDATE/.test(q)) return { rows: [{ id: '7', version_id: 'V2', status: 'open' }] };
73
+ if (/FROM versions WHERE id = \$1 FOR SHARE/.test(q)) return { rows: [{ status: 'planning' }] };
74
+ if (/FROM task_criteria/.test(q)) return { rows: [] };
75
+ if (/UPDATE done_when_criteria/.test(q)) return { rows: [{ id: '51' }] };
76
+ if (/UPDATE tasks/.test(q)) return { rows: [{ id: '12' }, { id: '3' }] };
77
+ return { rows: [] };
78
+ });
79
+ }
80
+ const ARGS = { goalId: 7, fromVersionId: 'V2', toVersionId: 'V3', targetStatus: 'planning' };
81
+
82
+ test('goal, criteria and every task move inside ONE transaction', async () => {
83
+ const pool = happyPool();
84
+ const out = await moveGoalToVersion(ARGS, { pool });
85
+ assert.deepEqual(out, { fromVersionId: 'V2', toVersionId: 'V3', taskIds: ['3', '12'], criterionIds: ['51'] });
86
+ const sqls = pool.queries.map(flat);
87
+ const at = (re) => sqls.findIndex((s) => re.test(s));
88
+ assert.equal(sqls.filter((s) => s === 'BEGIN').length, 1);
89
+ assert.equal(sqls.filter((s) => s === 'COMMIT').length, 1);
90
+ for (const re of [/^UPDATE goals SET version_id = \$2/, /^UPDATE done_when_criteria SET version_id = \$2 WHERE goal_id = \$1/, /^UPDATE tasks SET version_id = \$2/]) {
91
+ const i = at(re);
92
+ assert.ok(i > at(/^BEGIN$/) && i < at(/^COMMIT$/), `${re} is inside the transaction`);
93
+ }
94
+ // Every task in the goal — not only the open ones — or a shipped task would be
95
+ // left in a goal of another version.
96
+ const taskWrite = sqls[at(/^UPDATE tasks/)];
97
+ assert.match(taskWrite, /WHERE goal_id = \$1 RETURNING/);
98
+ assert.doesNotMatch(taskWrite, /status/);
99
+ // The goal is locked before anything is read or written.
100
+ assert.ok(at(/FROM goals WHERE id = \$1 FOR UPDATE/) < at(/^UPDATE/));
101
+ });
102
+
103
+ test('a criterion link crossing the goal edge refuses BEFORE any write, and names the pair', async () => {
104
+ const pool = happyPool({ 'FROM task_criteria': [{ task_id: '12', criterion_id: '900' }] });
105
+ await assert.rejects(moveGoalToVersion(ARGS, { pool }), (e) => {
106
+ assert.equal(e.code, 'CRITERION_LINKS_CROSS_GOAL');
107
+ assert.deepEqual(e.links, [{ task_id: '12', criterion_id: '900' }]);
108
+ return true;
109
+ });
110
+ const sqls = pool.queries.map(flat);
111
+ assert.equal(sqls.filter((s) => /^UPDATE|^INSERT/.test(s)).length, 0, 'nothing written');
112
+ assert.ok(sqls.includes('ROLLBACK'));
113
+ });
114
+
115
+ test('the crossing test asks "exactly one side inside the goal"', async () => {
116
+ const pool = happyPool();
117
+ await moveGoalToVersion(ARGS, { pool });
118
+ const q = pool.queries.map(flat).find((s) => /FROM task_criteria/.test(s));
119
+ assert.match(q, /\(t\.goal_id IS NOT DISTINCT FROM \$1\) <> \(c\.goal_id IS NOT DISTINCT FROM \$1\)/);
120
+ });
121
+
122
+ test('a goal or target that changed since the route checked rolls back', async () => {
123
+ for (const [over, code] of [
124
+ [{ 'FROM goals WHERE id = \\$1 FOR UPDATE': [{ id: '7', version_id: 'V9', status: 'open' }] }, 'GOAL_CHANGED'],
125
+ [{ 'FROM goals WHERE id = \\$1 FOR UPDATE': [{ id: '7', version_id: 'V2', status: 'archived' }] }, 'GOAL_CHANGED'],
126
+ [{ 'FROM versions WHERE id = \\$1 FOR SHARE': [{ status: 'building' }] }, 'TARGET_CHANGED'],
127
+ [{ 'FROM goals WHERE id = \\$1 FOR UPDATE': [] }, 'GOAL_NOT_FOUND'],
128
+ ]) {
129
+ const pool = happyPool(over);
130
+ await assert.rejects(moveGoalToVersion(ARGS, { pool }), (e) => e.code === code);
131
+ assert.equal(pool.queries.map(flat).filter((s) => /^UPDATE/.test(s)).length, 0, code);
132
+ }
133
+ });
134
+
135
+ test('a criterion slug the target already uses becomes CRITERION_ID_COLLISION', async () => {
136
+ const pool = txPool((q) => {
137
+ if (/FROM goals WHERE id = \$1 FOR UPDATE/.test(q)) return { rows: [{ id: '7', version_id: 'V2', status: 'open' }] };
138
+ if (/FROM versions WHERE id = \$1 FOR SHARE/.test(q)) return { rows: [{ status: 'planning' }] };
139
+ if (/UPDATE done_when_criteria/.test(q)) throw Object.assign(new Error('dup'), { code: '23505' });
140
+ return { rows: [] };
141
+ });
142
+ await assert.rejects(moveGoalToVersion(ARGS, { pool }), (e) => e.code === 'CRITERION_ID_COLLISION');
143
+ });
144
+
145
+ test('the admission is written only when the override opened a building target', async () => {
146
+ const plain = happyPool();
147
+ await moveGoalToVersion(ARGS, { pool: plain });
148
+ assert.equal(plain.queries.map(flat).filter((s) => /lifecycle_goal_version_admissions/.test(s)).length, 0);
149
+
150
+ const admitted = happyPool({ 'FROM versions WHERE id = \\$1 FOR SHARE': [{ status: 'building' }] });
151
+ await moveGoalToVersion({ ...ARGS, targetStatus: 'building', admission: { admittedBy: '3', reason: 'r' } }, { pool: admitted });
152
+ const ins = admitted.queries.find((q) => /lifecycle_goal_version_admissions/.test(flat(q)));
153
+ assert.ok(ins, 'admission recorded');
154
+ assert.deepEqual(ins.params, [7, 'V3', '3', 'r']);
155
+ });
156
+
157
+ // ---- the route, over a real socket ---------------------------------------------
158
+
159
+ const express = require('express');
160
+ const api = require('../src/module-api.js');
161
+ const db = require('../modules/lifecycle/db.js');
162
+ const goalMove = require('../modules/lifecycle/goal-move.js');
163
+
164
+ let permSeen = null;
165
+ api.requireBuilder = (req, _res, next) => { req.builder = { id: '3', rank: 'archon' }; next(); };
166
+ api.requirePermission = (...p) => { permSeen = p; return (_req, _res, next) => next(); };
167
+
168
+ const GOALS = { 7: { ...OPEN_GOAL }, 8: { id: '8', version_id: 'V2', status: 'open', is_maintenance: true } };
169
+ const VERSIONS = { V2, V3 };
170
+ let writes = [];
171
+ db.getGoal = async (id) => GOALS[Number(id)] ?? null;
172
+ db.getVersion = async (id) => VERSIONS[id] ?? null;
173
+ goalMove.moveGoalToVersion = async (args) => {
174
+ writes.push(args);
175
+ GOALS[args.goalId] = { ...GOALS[args.goalId], version_id: args.toVersionId };
176
+ return { fromVersionId: args.fromVersionId, toVersionId: args.toVersionId, taskIds: ['3', '12'], criterionIds: [] };
177
+ };
178
+
179
+ const app = express();
180
+ app.use(express.json());
181
+ app.use((req, res, next) => {
182
+ res.fail = (code, statusOrOpts, details) => {
183
+ const o = typeof statusOrOpts === 'number' ? { status: statusOrOpts } : (statusOrOpts || {});
184
+ return res.status(o.status || 400).json({ error: code, message: o.message, details: o.details ?? details });
185
+ };
186
+ next();
187
+ });
188
+ app.use('/api/gds', require('../modules/lifecycle/routes/goal-move.js')());
189
+ const server = app.listen(0);
190
+ await new Promise((r) => server.once('listening', r));
191
+ after(() => server.close());
192
+ const base = `http://127.0.0.1:${server.address().port}/api/gds`;
193
+ const post = async (p, body) => {
194
+ const r = await fetch(base + p, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body ?? {}) });
195
+ return { status: r.status, body: await r.json().catch(() => null) };
196
+ };
197
+
198
+ test('the route is gated on the version-authoring atom', () => {
199
+ assert.deepEqual(permSeen, ['version.create']);
200
+ const pins = readFileSync(new URL('../src/bongos/route-rank-check.js', import.meta.url), 'utf8');
201
+ assert.match(pins, /'POST \/goals\/:id\/move': 'perm:version\.create'/);
202
+ });
203
+
204
+ test('a move returns the goal on its new version and the tasks that went with it', async () => {
205
+ writes = [];
206
+ const r = await post('/goals/7/move', { version_id: 'V3' });
207
+ assert.equal(r.status, 200, JSON.stringify(r.body));
208
+ assert.equal(r.body.goal.version_id, 'V3');
209
+ assert.equal(r.body.from_version_id, 'V2');
210
+ assert.deepEqual(r.body.moved_task_ids, ['3', '12']);
211
+ assert.equal(r.body.admitted, false);
212
+ assert.deepEqual(writes, [{ goalId: 7, fromVersionId: 'V2', toVersionId: 'V3', targetStatus: 'planning', admission: null }]);
213
+ });
214
+
215
+ test('a refused move writes nothing and renders the gate verbatim', async () => {
216
+ writes = [];
217
+ let r = await post('/goals/8/move', { version_id: 'V3' });
218
+ assert.equal(r.status, 409); assert.equal(r.body.error, 'maintenance_goal');
219
+ r = await post('/goals/404/move', { version_id: 'V3' });
220
+ assert.equal(r.status, 404); assert.equal(r.body.error, 'goal_not_found');
221
+ r = await post('/goals/8/move', {});
222
+ assert.equal(r.status, 400);
223
+ r = await post('/goals/8/move', { version_id: 'V3', versionid: 'typo' });
224
+ assert.equal(r.status, 400, 'an unknown field is rejected, not ignored');
225
+ assert.equal(writes.length, 0);
226
+ });
227
+
228
+ test('a crossing criterion link surfaces as 409 with the pairs', async () => {
229
+ GOALS[9] = { id: '9', version_id: 'V2', status: 'open', is_maintenance: false };
230
+ const real = goalMove.moveGoalToVersion;
231
+ goalMove.moveGoalToVersion = async () => { throw Object.assign(new Error('x'), { code: 'CRITERION_LINKS_CROSS_GOAL', links: [{ task_id: '1', criterion_id: '2' }] }); };
232
+ const r = await post('/goals/9/move', { version_id: 'V3' });
233
+ goalMove.moveGoalToVersion = real;
234
+ assert.equal(r.status, 409);
235
+ assert.equal(r.body.error, 'criterion_links_cross_goal');
236
+ assert.deepEqual(r.body.details.links, [{ task_id: '1', criterion_id: '2' }]);
237
+ });
238
+
239
+ // ---- the CLI wrapper ----------------------------------------------------------
240
+
241
+ test('goal.js move parses a real goal id and version id, and refuses junk', () => {
242
+ const { parseMoveArgs } = require('../scripts/gds/goal.js');
243
+ assert.deepEqual(parseMoveArgs(['1000122', '--to', 'BONGOS-V3']), { id: 1000122, body: { version_id: 'BONGOS-V3' } });
244
+ assert.deepEqual(parseMoveArgs(['7', '--to=V3.1', '--admit-reason', 'owner call']),
245
+ { id: 7, body: { version_id: 'V3.1', admit_reason: 'owner call' } });
246
+ for (const bad of [[], ['abc', '--to', 'V3'], ['7'], ['7', '--to'], ['7', '--to', 'V 3'], ['7', '--to', 'V3;rm']]) {
247
+ assert.equal(parseMoveArgs(bad), null, JSON.stringify(bad));
248
+ }
249
+ });
250
+
251
+ // ---- version progress follows the move --------------------------------------
252
+ //
253
+ // The live view counts a version's tasks by tasks.version_id — so the counts move
254
+ // with the goal exactly when the writer re-points every task's version_id, which
255
+ // the transaction test above pins. Both halves are asserted here together: if the
256
+ // view ever counted through goals instead, or the writer stopped touching
257
+ // tasks.version_id, V2's progress would keep counting moved work.
258
+
259
+ test('version_progress counts by tasks.version_id, and the move rewrites it for every task in the goal', async () => {
260
+ const { readdirSync } = await import('node:fs');
261
+ const files = readdirSync(new URL('../migrations/', import.meta.url)).filter((f) => f.endsWith('.sql')).sort();
262
+ const latest = files.filter((f) => /VIEW version_progress/.test(readFileSync(new URL('../migrations/' + f, import.meta.url), 'utf8'))).pop();
263
+ assert.ok(latest, 'version_progress view found');
264
+ const view = readFileSync(new URL('../migrations/' + latest, import.meta.url), 'utf8');
265
+ assert.match(view, /LEFT JOIN tasks t ON t\.version_id = v\.id/);
266
+
267
+ // Before/after against the writer's own statements: a goal holding tasks 3 and
268
+ // 12 on V2 — after the move both carry V3, so V2 counts 0 of them and V3 counts 2.
269
+ const tasks = [{ id: '3', goal_id: 7, version_id: 'V2' }, { id: '12', goal_id: 7, version_id: 'V2' }, { id: '99', goal_id: 8, version_id: 'V2' }];
270
+ const count = (v) => tasks.filter((t) => t.version_id === v).length;
271
+ assert.equal(count('V2'), 3); assert.equal(count('V3'), 0);
272
+ const pool = txPool((q, params) => {
273
+ if (/FROM goals WHERE id = \$1 FOR UPDATE/.test(q)) return { rows: [{ id: '7', version_id: 'V2', status: 'open' }] };
274
+ if (/FROM versions WHERE id = \$1 FOR SHARE/.test(q)) return { rows: [{ status: 'planning' }] };
275
+ if (/^UPDATE tasks SET version_id = \$2/.test(q)) {
276
+ const hit = tasks.filter((t) => t.goal_id === params[0]);
277
+ for (const t of hit) t.version_id = params[1];
278
+ return { rows: hit.map((t) => ({ id: t.id })) };
279
+ }
280
+ return { rows: [] };
281
+ });
282
+ await moveGoalToVersion(ARGS, { pool });
283
+ assert.equal(count('V2'), 1, 'only the other goal\'s task still counts on V2');
284
+ assert.equal(count('V3'), 2);
285
+ });
@@ -149,7 +149,7 @@ test('a probe that sees a database failure fails the preview, says why, and clea
149
149
  assert.match(s.error, /schemaPending null/);
150
150
  assert.ok(events.includes('kill:SIGTERM'), 'the child was stopped');
151
151
  assert.deepEqual(calls[calls.length - 1], cmds.dropDbCommand(), 'the copy was dropped');
152
- assert.equal(fake.rm.length, 1, 'the scratch directory was removed');
152
+ assert.equal(fake.rm.filter((p) => !String(p).replace(/\\/g, '/').endsWith('/live.dump')).length, 1, 'the scratch directory was removed');
153
153
  });
154
154
 
155
155
  test('a preview is running only once the probe says it can serve data', async () => {
@@ -98,3 +98,26 @@ test('a missing port or scratch home is refused rather than guessed', () => {
98
98
  assert.throws(() => buildPreviewEnv({ env: LIVE, scratchHome: '/h' }), /port/);
99
99
  assert.throws(() => buildPreviewEnv({ env: LIVE, port: 1 }), /scratchHome/);
100
100
  });
101
+
102
+ // THE FIRST REAL RUN ON THE DROPLET (task 1004388). The live hall connects by Unix-socket peer
103
+ // auth with no PGUSER set, so the database driver takes the login name from USER — and an
104
+ // environment built from nothing has no USER. Every query in the preview then failed with
105
+ // "no PostgreSQL user name specified", while the migration (psql, which asks the OS) passed.
106
+ // The login name is therefore stated: the carried PGUSER when there is one, else the OS user
107
+ // this process runs as — the same identity peer auth would have checked.
108
+ test('with no PGUSER to carry, the preview logs in as the OS user it runs as', () => {
109
+ const { PGUSER, ...noUser } = LIVE;
110
+ const out = buildPreviewEnv({ env: noUser, port: 3190, scratchHome: '/h', osUser: () => 'lars' });
111
+ assert.equal(out.PGUSER, 'lars');
112
+ });
113
+
114
+ test('a carried PGUSER wins over the OS user', () => {
115
+ const out = buildPreviewEnv({ env: LIVE, port: 3190, scratchHome: '/h', osUser: () => 'lars' });
116
+ assert.equal(out.PGUSER, 'hall');
117
+ });
118
+
119
+ test('an OS user that cannot be read leaves PGUSER unset rather than failing the start', () => {
120
+ const { PGUSER, ...noUser } = LIVE;
121
+ const out = buildPreviewEnv({ env: noUser, port: 3190, scratchHome: '/h', osUser: () => { throw new Error('no passwd entry'); } });
122
+ assert.equal('PGUSER' in out, false);
123
+ });
@@ -46,6 +46,8 @@ function rig({ failOn, probeResults, env } = {}) {
46
46
  }
47
47
 
48
48
  const fwd = (x) => String(x).split(String.fromCharCode(92)).join('/');
49
+ // The scratch directory's own removals; the dump file is removed separately, right after the restore.
50
+ const scratchRms = (r) => r.filter((p) => !fwd(p).endsWith('/live.dump'));
49
51
  const DROP = { cmd: 'dropdb', args: ['--if-exists', '--force', PREVIEW_DB] };
50
52
 
51
53
  test('a start runs install, copy, restore and migrate, in that order, then launches and waits for health', async () => {
@@ -92,7 +94,7 @@ for (const failOn of ['npm', 'dropdb', 'createdb', 'dump', 'pg_restore', 'bash']
92
94
  assert.match(s.error, failOn === 'dropdb' ? /could not be dropped/ : /failed [(]exit 3[)]/);
93
95
  assert.equal(fake.spawned.length, 0, 'no process was started');
94
96
  if (failOn !== 'dropdb') assert.deepEqual(calls[calls.length - 1], DROP, 'the copy is dropped last');
95
- assert.equal(rmCalls.length, 1, 'the scratch directory is removed');
97
+ assert.equal(scratchRms(rmCalls).length, 1, 'the scratch directory is removed');
96
98
  });
97
99
  }
98
100
 
@@ -102,7 +104,7 @@ test('a preview that never answers is stopped and cleaned up', async () => {
102
104
  assert.equal(sup.status().state, 'failed');
103
105
  assert.match(sup.status().error, /did not answer/);
104
106
  assert.equal(fake.killed, 1);
105
- assert.equal(rmCalls.length, 1);
107
+ assert.equal(scratchRms(rmCalls).length, 1);
106
108
  });
107
109
 
108
110
  test('only one preview at a time: a second version is refused, the same version is not restarted', async () => {
@@ -139,7 +141,7 @@ test('stop kills the child, drops the copy, removes scratch and returns to idle'
139
141
  await sup.stop();
140
142
  assert.equal(fake.killed, 1);
141
143
  assert.deepEqual(calls[calls.length - 1], DROP);
142
- assert.equal(rmCalls.length, 1);
144
+ assert.equal(scratchRms(rmCalls).length, 1);
143
145
  assert.equal(sup.status().state, 'idle');
144
146
  assert.equal(sup.status().version, null);
145
147
  assert.equal(sup.start('1.19.1001').ok, true, 'and a new one may start');
@@ -201,3 +203,16 @@ test('when the running version is not in the list the window counts from the new
201
203
  assert.equal(previewable('1.19.1029', { published, running: '0.0.1' }).ok, true);
202
204
  assert.equal(previewable('1.19.1028', { published, running: '0.0.1' }).ok, false);
203
205
  });
206
+
207
+ // THE DUMP IS A FULL COPY OF THE LIVE DATABASE, sessions included (task 1004388). It is needed
208
+ // only until the restore has read it; leaving it to the stop's cleanup meant a restart of the
209
+ // live service (which cannot finish an async stop) left it in /tmp — found there 11 hours on.
210
+ test('the dump file is deleted as soon as the restore has read it, before the child launches', async () => {
211
+ const { sup, rmCalls, fake } = rig();
212
+ const r = sup.start('1.19.1000');
213
+ await r.done;
214
+ const dumpRm = rmCalls.findIndex((p) => fwd(p).endsWith('/live.dump'));
215
+ assert.ok(dumpRm >= 0, 'the dump was never removed: ' + JSON.stringify(rmCalls));
216
+ assert.equal(sup.status().state, 'running');
217
+ assert.equal(fake.spawned.length, 1);
218
+ });