@bongos/core 1.21.117 → 1.21.118
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 +48 -28
- package/docs/adr/0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md +2 -1
- package/docs/adr/0381-the-hub-moves-each-hosted-project-by-its-update-rule.md +34 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +49 -49
- package/docs/copy-registry.json +56 -56
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +1 -1
- package/docs/recipes/upgrading-the-core.md +3 -1
- package/modules/hall-ui/public/deploy.js +3 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +10 -0
- package/scripts/gds/provision-auto-upgrade.js +139 -0
- package/scripts/gds/provision-repo.js +60 -0
- package/scripts/gds/provision.js +7 -1
- package/scripts/gds/run-unit-tests.js +4 -0
- package/scripts/gds/upgrade-outcome.js +4 -0
- package/scripts/gds/wedge-remedy.js +13 -2
- package/src/module-api.js +1 -1
- package/tests/core_auto_upgrade.mjs +189 -0
- package/tests/db_ownership_handover.mjs +156 -0
- package/tests/provision.mjs +34 -0
- package/tests/upgrade_migrate.mjs +10 -0
- package/tests/wedge_remedy.mjs +23 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// tests/db_ownership_handover.mjs — the ownership handover against a REAL Postgres
|
|
2
|
+
// (task 1004735).
|
|
3
|
+
//
|
|
4
|
+
// A project with its own database account had its tables made by postgres at provisioning
|
|
5
|
+
// and only GRANTed to the account. Later core moves migrate AS that account, and GRANT ALL
|
|
6
|
+
// does not carry the right to ALTER TABLE or CREATE OR REPLACE FUNCTION — only ownership
|
|
7
|
+
// does — so toast's first ALTERing move failed "must be owner of table tasks" and rolled
|
|
8
|
+
// back. dbOwnershipSql (scripts/gds/provision-repo.js) hands everything over. It is one long
|
|
9
|
+
// DO block of catalog queries, which a string test cannot prove: whether it actually runs,
|
|
10
|
+
// which objects it reaches, and that it leaves extension-owned objects alone are questions
|
|
11
|
+
// only a real engine answers.
|
|
12
|
+
//
|
|
13
|
+
// Hermetic within the database: a throwaway schema and role, both dropped at the end, so the
|
|
14
|
+
// shared fixture database's public schema is never touched. Needs a superuser (CREATE ROLE,
|
|
15
|
+
// giving away postgres-owned objects) — the integration lane's postgres is one.
|
|
16
|
+
//
|
|
17
|
+
// Real-DB integration test: self-skips without a reachable Postgres; in the INTEGRATION set
|
|
18
|
+
// so the default DB-free lane never runs it.
|
|
19
|
+
//
|
|
20
|
+
// Run: DATABASE_URL=... node tests/db_ownership_handover.mjs
|
|
21
|
+
|
|
22
|
+
import { strict as assert } from 'node:assert';
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
const { Pool } = require('pg');
|
|
27
|
+
const { dbOwnershipSql } = require('../scripts/gds/provision-repo.js');
|
|
28
|
+
|
|
29
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
30
|
+
let client;
|
|
31
|
+
try {
|
|
32
|
+
client = await pool.connect();
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.log(`db_ownership_handover.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
35
|
+
await pool.end().catch(() => {});
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
const { rows: [me] } = await client.query('SELECT rolsuper FROM pg_roles WHERE rolname = current_user');
|
|
39
|
+
if (!me || !me.rolsuper) {
|
|
40
|
+
console.log('db_ownership_handover.mjs SKIPPED — the connecting role is not a superuser, so it cannot stand in for postgres here.');
|
|
41
|
+
client.release();
|
|
42
|
+
await pool.end();
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SCHEMA = `own_t_${process.pid}`;
|
|
47
|
+
const ROLE = `own-t-role-${process.pid}`; // a hyphen, like the real bongos-<slug> roles
|
|
48
|
+
const q = (sql) => client.query(sql);
|
|
49
|
+
let passed = 0;
|
|
50
|
+
let failed = 0;
|
|
51
|
+
const t = async (name, fn) => {
|
|
52
|
+
try { await fn(); passed += 1; console.log(` PASS ${name}`); }
|
|
53
|
+
catch (e) { failed += 1; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Every object in the schema that the handover is meant to reach, with its owner. Excludes
|
|
57
|
+
// what follows something else: table row types, auto-made array types, column-owned sequences.
|
|
58
|
+
const OWNERS = `
|
|
59
|
+
SELECT c.relname AS name, pg_get_userbyid(c.relowner) AS owner FROM pg_class c
|
|
60
|
+
WHERE c.relnamespace = '${SCHEMA}'::regnamespace AND c.relkind IN ('r','p','v','m','f','S')
|
|
61
|
+
UNION ALL
|
|
62
|
+
SELECT p.proname, pg_get_userbyid(p.proowner) FROM pg_proc p WHERE p.pronamespace = '${SCHEMA}'::regnamespace
|
|
63
|
+
UNION ALL
|
|
64
|
+
SELECT y.typname, pg_get_userbyid(y.typowner) FROM pg_type y
|
|
65
|
+
WHERE y.typnamespace = '${SCHEMA}'::regnamespace AND y.typtype IN ('e','d','r')`;
|
|
66
|
+
|
|
67
|
+
let extension = null;
|
|
68
|
+
try {
|
|
69
|
+
await q(`CREATE ROLE "${ROLE}" LOGIN`);
|
|
70
|
+
// The schema is the role's, as public is the database owner's on a real project (PG15+:
|
|
71
|
+
// pg_database_owner) — the objects INSIDE it are still postgres's, which is the bug.
|
|
72
|
+
await q(`CREATE SCHEMA ${SCHEMA} AUTHORIZATION "${ROLE}"`);
|
|
73
|
+
await q(`SET search_path = ${SCHEMA}`);
|
|
74
|
+
await q(`CREATE TABLE tasks (id serial PRIMARY KEY, n int GENERATED BY DEFAULT AS IDENTITY, title text)`);
|
|
75
|
+
await q(`CREATE TABLE events (id bigint, at date) PARTITION BY RANGE (at)`);
|
|
76
|
+
await q(`CREATE TABLE events_2026 PARTITION OF events FOR VALUES FROM ('2026-01-01') TO ('2027-01-01')`);
|
|
77
|
+
await q(`CREATE VIEW open_tasks AS SELECT * FROM tasks`);
|
|
78
|
+
await q(`CREATE MATERIALIZED VIEW task_count AS SELECT count(*) FROM tasks`);
|
|
79
|
+
await q(`CREATE SEQUENCE free_seq`);
|
|
80
|
+
await q(`CREATE FUNCTION promote_dependents_on_ship() RETURNS int LANGUAGE sql AS 'SELECT 1'`);
|
|
81
|
+
await q(`CREATE PROCEDURE tidy() LANGUAGE sql AS 'SELECT 1'`);
|
|
82
|
+
await q(`CREATE AGGREGATE total(int) (SFUNC = int4pl, STYPE = int)`);
|
|
83
|
+
await q(`CREATE TYPE mood AS ENUM ('ok', 'not')`);
|
|
84
|
+
await q(`CREATE DOMAIN positive AS int CHECK (VALUE > 0)`);
|
|
85
|
+
await q(`CREATE TYPE pair AS (a int, b int)`);
|
|
86
|
+
// An extension's objects in the same schema must be left alone (and must not make the
|
|
87
|
+
// handover error). Use whichever stock extension this server has and has not installed.
|
|
88
|
+
const { rows: ext } = await q(`SELECT name FROM pg_available_extensions
|
|
89
|
+
WHERE name IN ('pg_trgm','citext','hstore','tablefunc','fuzzystrmatch') AND installed_version IS NULL ORDER BY name LIMIT 1`);
|
|
90
|
+
if (ext.length) {
|
|
91
|
+
extension = ext[0].name;
|
|
92
|
+
await q(`CREATE EXTENSION ${extension} SCHEMA ${SCHEMA}`);
|
|
93
|
+
}
|
|
94
|
+
await q(`RESET search_path`);
|
|
95
|
+
|
|
96
|
+
console.log('\nthe ownership handover against a real Postgres:');
|
|
97
|
+
|
|
98
|
+
await t('before: the account can USE the tables but cannot ALTER one — the bug, reproduced', async () => {
|
|
99
|
+
await q(`GRANT ALL ON ALL TABLES IN SCHEMA ${SCHEMA} TO "${ROLE}"`);
|
|
100
|
+
await q(`SET ROLE "${ROLE}"`);
|
|
101
|
+
try {
|
|
102
|
+
await assert.rejects(q(`ALTER TABLE ${SCHEMA}.tasks ADD COLUMN parked boolean`), /must be owner of table tasks/);
|
|
103
|
+
} finally { await q('RESET ROLE'); }
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await t('the handover runs, and every table, view, sequence, function and type is the account\'s', async () => {
|
|
107
|
+
await q(dbOwnershipSql(ROLE, SCHEMA));
|
|
108
|
+
const { rows } = await q(OWNERS);
|
|
109
|
+
const extOwned = extension
|
|
110
|
+
? new Set((await q(`SELECT p.proname AS name FROM pg_proc p JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e'
|
|
111
|
+
WHERE p.pronamespace = '${SCHEMA}'::regnamespace
|
|
112
|
+
UNION SELECT y.typname FROM pg_type y JOIN pg_depend d ON d.objid = y.oid AND d.deptype = 'e'
|
|
113
|
+
WHERE y.typnamespace = '${SCHEMA}'::regnamespace`)).rows.map((r) => r.name))
|
|
114
|
+
: new Set();
|
|
115
|
+
const mine = rows.filter((r) => !extOwned.has(r.name));
|
|
116
|
+
for (const name of ['tasks', 'tasks_id_seq', 'tasks_n_seq', 'events', 'events_2026', 'open_tasks', 'task_count', 'free_seq',
|
|
117
|
+
'promote_dependents_on_ship', 'tidy', 'total', 'mood', 'positive']) {
|
|
118
|
+
const row = mine.find((r) => r.name === name);
|
|
119
|
+
assert.ok(row, `${name} exists`);
|
|
120
|
+
assert.equal(row.owner, ROLE, `${name} is owned by the account`);
|
|
121
|
+
}
|
|
122
|
+
const { rows: [pair] } = await q(`SELECT pg_get_userbyid(typowner) AS owner FROM pg_type WHERE typname = 'pair' AND typnamespace = '${SCHEMA}'::regnamespace`);
|
|
123
|
+
assert.equal(pair.owner, ROLE, 'a CREATE TYPE composite too');
|
|
124
|
+
const left = mine.filter((r) => r.owner !== ROLE);
|
|
125
|
+
assert.deepEqual(left, [], 'nothing of the project\'s is left with postgres');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await t('extension objects stay with the extension\'s owner — never handed over', async () => {
|
|
129
|
+
if (!extension) { console.log(' (no spare stock extension on this server — sub-case not exercised)'); return; }
|
|
130
|
+
const { rows } = await q(`SELECT pg_get_userbyid(p.proowner) AS owner FROM pg_proc p JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e'
|
|
131
|
+
WHERE p.pronamespace = '${SCHEMA}'::regnamespace`);
|
|
132
|
+
assert.ok(rows.length > 0, `precondition: ${extension} put functions in the schema`);
|
|
133
|
+
assert.ok(rows.every((r) => r.owner !== ROLE), `${extension}'s functions are untouched`);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await t('after: the account can ALTER a table and CREATE OR REPLACE a function — what core_274 needed', async () => {
|
|
137
|
+
await q(`SET ROLE "${ROLE}"`);
|
|
138
|
+
try {
|
|
139
|
+
await q(`ALTER TABLE ${SCHEMA}.tasks ADD COLUMN parked boolean`);
|
|
140
|
+
await q(`CREATE OR REPLACE FUNCTION ${SCHEMA}.promote_dependents_on_ship() RETURNS int LANGUAGE sql AS 'SELECT 2'`);
|
|
141
|
+
} finally { await q('RESET ROLE'); }
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await t('a second run is a clean no-op (provisioning re-runs it; the remedy may meet a healed project)', async () => {
|
|
145
|
+
await q(dbOwnershipSql(ROLE, SCHEMA));
|
|
146
|
+
});
|
|
147
|
+
} finally {
|
|
148
|
+
await q('RESET ROLE').catch(() => {});
|
|
149
|
+
await q(`DROP SCHEMA IF EXISTS ${SCHEMA} CASCADE`).catch((e) => console.log(` cleanup: ${e.message}`));
|
|
150
|
+
await q(`DROP ROLE IF EXISTS "${ROLE}"`).catch((e) => console.log(` cleanup: ${e.message}`));
|
|
151
|
+
client.release();
|
|
152
|
+
await pool.end();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(`\ndb_ownership_handover: ${passed} passed, ${failed} failed`);
|
|
156
|
+
process.exit(failed ? 1 : 0);
|
package/tests/provision.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { join } from 'node:path';
|
|
|
18
18
|
|
|
19
19
|
const require = createRequire(import.meta.url);
|
|
20
20
|
const P = require('../scripts/gds/provision.js');
|
|
21
|
+
const PR = require('../scripts/gds/provision-repo.js'); // task 1004735: dbOwnershipSql/Cmd are not re-exported through provision.js
|
|
21
22
|
const AP = require('../scripts/gds/instance-apex.js');
|
|
22
23
|
const RP = require('../scripts/gds/provision-repo.js');
|
|
23
24
|
|
|
@@ -3370,6 +3371,39 @@ t('dbRoleGrantsCmd: runs on the INSTANCE db and grants forward too — the migra
|
|
|
3370
3371
|
assert.match(c, /GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO %I/);
|
|
3371
3372
|
});
|
|
3372
3373
|
|
|
3374
|
+
t('dbOwnershipCmd: hands OWNERSHIP of every object to the instance role, as postgres, in its own db (task 1004735)', () => {
|
|
3375
|
+
// GRANT ALL is not ownership: a later migrate run AS the instance role (--migrate-as,
|
|
3376
|
+
// task 1004512) cannot ALTER TABLE or CREATE OR REPLACE FUNCTION on an object postgres
|
|
3377
|
+
// owns ("must be owner of"), so toast's first ALTERing core move failed and rolled back.
|
|
3378
|
+
const c = PR.dbOwnershipCmd(coTenant, { privileged: true });
|
|
3379
|
+
assert.ok(c.startsWith('sudo -u postgres psql -v ON_ERROR_STOP=1 -d mercury -c '), c.slice(0, 80));
|
|
3380
|
+
for (const kind of ['TABLE', 'SEQUENCE', 'FUNCTION', 'PROCEDURE', 'AGGREGATE', 'TYPE', 'DOMAIN', 'VIEW']) assert.ok(c.includes(`$t$${kind}$t$`) || c.includes(`ALTER ${kind} `), `hands over ${kind}s`);
|
|
3381
|
+
assert.match(c, /OWNER TO %I/);
|
|
3382
|
+
assert.match(c, /\$r\$bongos-mercury\$r\$/, 'to the instance\'s own role');
|
|
3383
|
+
assert.match(c, /deptype IN \(\$e\$e\$e\$\)/, 'skips objects an EXTENSION owns — altering those errors, and they are not the project\'s');
|
|
3384
|
+
assert.match(c, /\$s\$public\$s\$::regnamespace/, 'only the public schema');
|
|
3385
|
+
assert.doesNotMatch(c, /REASSIGN OWNED/, 'REASSIGN OWNED BY postgres would also hand over every SIBLING database postgres owns');
|
|
3386
|
+
});
|
|
3387
|
+
|
|
3388
|
+
await ta('provisionInstance: hands ownership over right after the grants, and a failure there does not fail the provision (task 1004735)', async () => {
|
|
3389
|
+
const { deps } = recorder();
|
|
3390
|
+
const seen = [];
|
|
3391
|
+
deps.exec = (cmd, opts = {}) => { seen.push({ cmd, opts }); return { ok: true, stdout: '' }; };
|
|
3392
|
+
await P.provisionInstance(coTenant, deps);
|
|
3393
|
+
const grants = seen.findIndex((c) => c.cmd.includes('GRANT ALL ON SCHEMA public'));
|
|
3394
|
+
const own = seen.findIndex((c) => c.cmd.includes('OWNER TO %I') && c.cmd.includes('regnamespace'));
|
|
3395
|
+
assert.ok(grants >= 0 && own === grants + 1, `the handover follows the grants (grants at ${grants}, handover at ${own})`);
|
|
3396
|
+
assert.equal(seen[own].opts.allowFail, true, 'non-fatal: the migrate_not_owner remedy is the backstop');
|
|
3397
|
+
});
|
|
3398
|
+
|
|
3399
|
+
t('dbOwnershipSql: the SQL body carries no quote of either kind, so it survives psql -c and bash -s', () => {
|
|
3400
|
+
const sql = PR.dbOwnershipSql('bongos-mercury');
|
|
3401
|
+
assert.ok(!sql.includes("'"), sql);
|
|
3402
|
+
assert.ok(!sql.includes(String.fromCharCode(34)), sql);
|
|
3403
|
+
assert.ok(!sql.includes('$p$'), 'never the password marker the runner log redaction keys on');
|
|
3404
|
+
assert.throws(() => PR.dbOwnershipSql('x; drop'), /role/, 'a role name that is not a plain identifier is refused, never quoted');
|
|
3405
|
+
});
|
|
3406
|
+
|
|
3373
3407
|
t('the role + seed commands render with NO double quote — they must survive `bash -s` over SSH unchanged', () => {
|
|
3374
3408
|
const dq = String.fromCharCode(34);
|
|
3375
3409
|
for (const c of [P.dbRoleCmd(coTenant, { password: 'pw_ABC-123' }), P.dbRoleGrantsCmd(coTenant), P.seedFirstVersionCmd(coTenant)]) {
|
|
@@ -254,6 +254,16 @@ test('a full upgrade whose migrate fails reports the migration, the error, and a
|
|
|
254
254
|
assert.equal(outcome.classifyFailure('migrate_check_violation').class, 'unknown', 'existing rows break the new rule: a person decides, never an unattended retry');
|
|
255
255
|
});
|
|
256
256
|
|
|
257
|
+
test('"must be owner of" in a failed migrate is the known not-owner snag, not an unknown failure (task 1004735)', () => {
|
|
258
|
+
const outcome = require('../scripts/gds/upgrade-outcome.js');
|
|
259
|
+
const res = { ok: false, rolledBack: true, rollback: {}, error: 'migrate failed (exit 3) at core_274_parked_backlog_stays_parked: ERROR: must be owner of table tasks' };
|
|
260
|
+
assert.equal(outcome.upgradeReason(res), 'migrate_not_owner');
|
|
261
|
+
assert.deepEqual(outcome.classifyFailure('migrate_not_owner'), { class: 'known_wedge', reason: 'migrate_not_owner' });
|
|
262
|
+
const fn = { ...res, error: 'migrate failed (exit 3) at core_274_x: ERROR: must be owner of function promote_dependents_on_ship' };
|
|
263
|
+
assert.equal(outcome.upgradeReason(fn), 'migrate_not_owner', 'functions too — CREATE OR REPLACE on a postgres-owned function');
|
|
264
|
+
assert.equal(outcome.upgradeReason({ ...res, error: 'migrate failed (exit 3) at core_5: ERROR: boom' }), 'migrate', 'any other migrate failure is unchanged');
|
|
265
|
+
});
|
|
266
|
+
|
|
257
267
|
test('teeSpawn streams the child\'s output live AND keeps it for the failure report', async () => {
|
|
258
268
|
const pipe = () => Object.assign(new EventEmitter(), { setEncoding() {} });
|
|
259
269
|
let child;
|
package/tests/wedge_remedy.mjs
CHANGED
|
@@ -52,7 +52,7 @@ function harness({ execFails = false, privileged = false } = {}) {
|
|
|
52
52
|
console.log('\nthe fix for each known snag (planRemedy):');
|
|
53
53
|
|
|
54
54
|
await t('every reason with a fix is a known_wedge in the classifier — and back', () => {
|
|
55
|
-
assert.deepEqual([...W.REMEDY_REASONS].sort(), ['dirty_tree', 'disk_full', 'schema_pending']);
|
|
55
|
+
assert.deepEqual([...W.REMEDY_REASONS].sort(), ['dirty_tree', 'disk_full', 'migrate_not_owner', 'schema_pending']);
|
|
56
56
|
for (const r of W.REMEDY_REASONS) assert.equal(REASON_CLASS[r], 'known_wedge', r);
|
|
57
57
|
for (const [r, c] of Object.entries(REASON_CLASS)) if (c === 'known_wedge') assert.ok(W.REMEDY_REASONS.includes(r), `${r} is known_wedge but has no fix — it would be retired with nothing done`);
|
|
58
58
|
assert.equal(REASON_CLASS.remedy_did_not_hold, 'unknown', 'a fix that did not hold reaches a person');
|
|
@@ -92,6 +92,28 @@ await t('schema pending: migrates the project\'s OWN database, PGDATABASE always
|
|
|
92
92
|
assert.ok(W.planRemedy(inst(), 'schema_pending', { selfRoot, privileged: true, runAs: null }).refused);
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
await t('migrate not owner: hands the project\'s tables to ITS OWN account, as postgres (task 1004735)', () => {
|
|
96
|
+
const p = W.planRemedy(inst(), 'migrate_not_owner', { selfRoot, privileged: true });
|
|
97
|
+
assert.match(p.cmd, /^sudo -u postgres psql -v ON_ERROR_STOP=1 -d charter -c '/, 'only postgres can give away what postgres owns, and only in THIS project\'s database');
|
|
98
|
+
assert.match(p.cmd, /OWNER TO %I/);
|
|
99
|
+
assert.match(p.cmd, /\$r\$bongos-charter\$r\$/, 'to the account the move migrates as');
|
|
100
|
+
assert.match(p.describe, /bongos-charter/);
|
|
101
|
+
const plain = W.planRemedy(inst(), 'migrate_not_owner', { selfRoot });
|
|
102
|
+
assert.match(plain.cmd, /^psql -v ON_ERROR_STOP=1 -d charter -c '/);
|
|
103
|
+
assert.ok(W.planRemedy(inst({ hosting_shape: 'control-plane' }), 'migrate_not_owner', { selfRoot }).refused, 'the control plane migrates as its own app user — there is no project account to hand anything to');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await t('migrate not owner: remedied once, then the move is tried again', async () => {
|
|
107
|
+
const h = harness({ privileged: true });
|
|
108
|
+
const r = await W.remedyAfterFailure({ intent: intent(), inst: inst(), result: wedge('migrate_not_owner'), deps: h.deps });
|
|
109
|
+
assert.deepEqual(r, { repended: true });
|
|
110
|
+
assert.equal(h.ran.length, 1);
|
|
111
|
+
assert.match(h.ran[0].cmd, /^sudo -u postgres psql /);
|
|
112
|
+
const again = await W.remedyAfterFailure({ intent: intent({ remedied_reason: 'migrate_not_owner' }), inst: inst(), result: wedge('migrate_not_owner'), deps: harness({ privileged: true }).deps });
|
|
113
|
+
assert.equal(again.repended, false, 'a handover that did not clear it reaches a person');
|
|
114
|
+
assert.equal(again.failure.reason, 'remedy_did_not_hold');
|
|
115
|
+
});
|
|
116
|
+
|
|
95
117
|
await t('no command is built from a name that is not shell-safe', () => {
|
|
96
118
|
assert.ok(W.planRemedy(inst({ slug: 'a;rm -rf' }), 'dirty_tree', { selfRoot }).refused);
|
|
97
119
|
assert.ok(W.planRemedy(inst({ db_name: 'x; drop' }), 'disk_full', { selfRoot }).refused);
|