@bongos/core 1.21.117 → 1.21.119

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,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);
@@ -68,7 +68,8 @@ const SECRET_HASH = 'HASH-DO-NOT-LEAK-p05';
68
68
  const SECRET_PROJECT = 'P05-PRIVATE-DO-NOT-LEAK';
69
69
  const SECRET_PREV = 'p05-prev-sentinel';
70
70
  const PUB_ORIGIN = 'https://p05-pub.example', PRIV_ORIGIN = 'https://p05-priv.example';
71
- const CP_KEYS = ['totals', 'disciplines', 'last_active', 'projects', 'more_projects'];
71
+ // crafts: each craft's own summed numbers (task 1004440), named or not like the totals.
72
+ const CP_KEYS = ['totals', 'disciplines', 'crafts', 'last_active', 'projects', 'more_projects'];
72
73
  const ACCOUNT_KEYS = ['handle', 'github_login', 'display_name', 'avatar_url', 'member_since', 'bio', 'links'];
73
74
 
74
75
  const q = (sql, params) => pool.query(sql, params);
@@ -128,6 +129,16 @@ try {
128
129
  // 42703 "column does not exist" instead of a 404-vs-200 assertion.
129
130
  await q(migrationSql('platform_identity_012_account_privacy.sql'));
130
131
  await q(migrationSql('platform_identity_019_terms_acceptance.sql'));
132
+ // getRollupForBuilder reads four more (task 1004664 — this file failed 3 of 7 on a FRESH
133
+ // database: every check that reaches the rollup answered 42703, while an old shared
134
+ // database that already had the columns hid it). It names a project only when it is ON
135
+ // THE MAP (projects-feed.js onMapSql: status, the publish gate's description, a known
136
+ // door) AND its door is public — 011 + 020 — and serializes a.recent_ships (012) and
137
+ // a.crafts (029).
138
+ await q(migrationSql('platform_identity_011_project_visibility.sql'));
139
+ await q(migrationSql('platform_identity_012_recent_ships.sql'));
140
+ await q(migrationSql('platform_identity_020_visibility_pull.sql'));
141
+ await q(migrationSql('platform_identity_029_activity_crafts.sql'));
131
142
  await cleanup();
132
143
 
133
144
  // The active account carries the P02 fields + a user-set name source, so the
@@ -150,9 +161,13 @@ try {
150
161
  VALUES ($1, $3, 'P05 Pub Proj', $4), ($2, $3, $5, $6)`,
151
162
  [SECRET_CLIENT_PUB, SECRET_CLIENT_PRIV, SECRET_HASH, PUB_ORIGIN, SECRET_PROJECT, PRIV_ORIGIN],
152
163
  );
164
+ // Both projects are on the map — active, described, door known — so the ONLY thing that
165
+ // keeps the second one unnamed is its private door. A fixture that left out the
166
+ // description would pass the "never named" check for the wrong reason (unpublishable).
153
167
  await q(
154
- `INSERT INTO platform_identity_projects (origin, name, featured, status)
155
- VALUES ($1, 'P05 Pub Proj', true, 'active'), ($2, $3, false, 'active')`,
168
+ `INSERT INTO platform_identity_projects (origin, name, featured, status, description, visibility, visibility_source)
169
+ VALUES ($1, 'P05 Pub Proj', true, 'active', 'A public P05 project.', 'public', 'control-plane'),
170
+ ($2, $3, false, 'active', 'A private P05 project.', 'private', 'control-plane')`,
156
171
  [PUB_ORIGIN, PRIV_ORIGIN, SECRET_PROJECT],
157
172
  );
158
173
  await q(
@@ -180,7 +195,7 @@ try {
180
195
  assert.equal(r.json.cross_project.totals.projects, 2, 'both projects count');
181
196
  assert.equal(r.json.cross_project.projects.length, 1, 'only the publicly listed project is named');
182
197
  assert.equal(r.json.cross_project.projects[0].name, 'P05 Pub Proj');
183
- assert.deepEqual(Object.keys(r.json.cross_project.projects[0]).sort(), ['name', 'origin', 'rank_held'].sort());
198
+ assert.deepEqual(Object.keys(r.json.cross_project.projects[0]).sort(), ['name', 'origin', 'rank_held', 'recent_ships'].sort());
184
199
  assert.equal(r.json.cross_project.more_projects, 1, 'the non-public project is counted, never named');
185
200
  assert.equal(r.json.account.bio, 'Builds things.', 'bio serializes (P02)');
186
201
  assert.deepEqual(r.json.account.links, { website: 'https://p05.example/' }, 'links serialize (P02)');
@@ -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;
@@ -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);