@bongos/core 1.21.14 → 1.21.16
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 +51 -41
- package/docs/adr/0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md +3 -0
- package/docs/adr/0364-repo-access-is-asked-at-sign-in-and-held-for-the-session.md +30 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +255 -255
- package/docs/copy-registry.json +288 -288
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +363 -362
- package/docs/recipes/upgrading-the-core.md +1 -0
- package/modules/hall-ui/public/deploy.js +3 -0
- package/modules/public-landing/public/account.html +2 -2
- package/modules/public-landing/public/account.probes.json +2 -2
- package/modules/public-landing/public/index.html +1 -1
- package/modules/public-landing/public/projects.html +22 -13
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/provision-core-upgrade.js +52 -20
- package/scripts/gds/upgrade-migrate.js +101 -4
- package/scripts/gds/upgrade-outcome.js +6 -0
- package/scripts/gds/upgrade.js +39 -44
- package/scripts/gds/wedge-remedy.js +10 -4
- package/src/bongos/db.js +7 -6
- package/src/bongos/routes/auth.js +17 -4
- package/src/module-api.js +1 -1
- package/tests/core_upgrade_runner.mjs +34 -10
- package/tests/github_token_session_life.mjs +110 -0
- package/tests/hub_account_page.mjs +2 -2
- package/tests/instance_db_migrated_on_code_move.mjs +20 -2
- package/tests/landing_page.mjs +2 -2
- package/tests/projects_hub_pre_uat.mjs +2 -1
- package/tests/upgrade.mjs +147 -0
- package/tests/upgrade_outcome.mjs +3 -0
- package/tests/wedge_remedy.mjs +36 -2
- package/tests/wizard_draft_resume.mjs +49 -0
- package/tests/wizard_intent_resume.mjs +4 -2
package/tests/upgrade.mjs
CHANGED
|
@@ -1926,6 +1926,153 @@ t('parseArgs: --allow-downgrade is read, and is OFF unless asked for', () => {
|
|
|
1926
1926
|
assert.equal(u.parseArgs(['--to', '1.0.0', '--allow-downgrade']).allowDowngrade, true);
|
|
1927
1927
|
});
|
|
1928
1928
|
|
|
1929
|
+
// ---- 11. WHICH ACCOUNT DOES WHAT (task 1004512) -----------------------------
|
|
1930
|
+
//
|
|
1931
|
+
// Found live on cloudbongos.com: every core move on a project provisioned since task
|
|
1932
|
+
// 1003369 failed, and reported a dirty tree it had invented. The move ran wholly as the
|
|
1933
|
+
// project's own `bongos-<slug>` account, which can only READ the checkout, so `git status`
|
|
1934
|
+
// refused with `detected dubious ownership`, the clean-tree pre-flight read that as dirt,
|
|
1935
|
+
// the self-heal chain committed a tree nobody had touched, and the retry failed identically.
|
|
1936
|
+
// Both halves are pinned here: git's refusal is its own failure, and the database steps run
|
|
1937
|
+
// as the account that actually owns the database.
|
|
1938
|
+
|
|
1939
|
+
t('a `git status` that could NOT ANSWER is not reported as uncommitted work', async () => {
|
|
1940
|
+
const dir = scratchConsumer();
|
|
1941
|
+
const res = await u.runUpgrade({ to: '1.15.0', instance: dir }, {
|
|
1942
|
+
log: () => {}, err: () => {},
|
|
1943
|
+
run: (cmd, args) => (cmd === 'git' && args[0] === 'status'
|
|
1944
|
+
? { status: 128, stdout: '', stderr: `fatal: detected dubious ownership in repository at '${dir}'\n` }
|
|
1945
|
+
: { status: 0 }),
|
|
1946
|
+
});
|
|
1947
|
+
assert.equal(res.ok, false, 'it still refuses — nothing is known about the tree');
|
|
1948
|
+
assert.match(res.error, /git could not read the working tree/, 'and says which question went unanswered');
|
|
1949
|
+
assert.match(res.error, /dubious ownership/, 'carrying git\'s OWN words, not "not a git repo?"');
|
|
1950
|
+
assert.doesNotMatch(res.error, /^working tree not clean/m, 'it must never claim there is uncommitted work');
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
t('…and that refusal is classified as unknown, so no remedy commits a guess', async () => {
|
|
1954
|
+
const o = require('../scripts/gds/upgrade-outcome.js');
|
|
1955
|
+
const dir = scratchConsumer();
|
|
1956
|
+
const res = await u.runUpgrade({ to: '1.15.0', instance: dir }, {
|
|
1957
|
+
log: () => {}, err: () => {},
|
|
1958
|
+
run: (cmd, args) => (cmd === 'git' && args[0] === 'status'
|
|
1959
|
+
? { status: 128, stdout: '', stderr: 'fatal: detected dubious ownership in repository\n' }
|
|
1960
|
+
: { status: 0 }),
|
|
1961
|
+
});
|
|
1962
|
+
// The whole point: dirty_tree is known_wedge, and wedge-remedy.js answers it by running
|
|
1963
|
+
// `git add -u && git commit`. Doing that to a tree git would not describe commits whatever
|
|
1964
|
+
// happens to be lying there, under a runner identity, in someone else's repository.
|
|
1965
|
+
const reason = o.upgradeReason(res);
|
|
1966
|
+
assert.equal(reason, 'git_unreadable', `got ${reason} — a failed "git status" is not a dirty tree`);
|
|
1967
|
+
assert.equal(o.classifyFailure(reason).class, 'unknown', 'unknown reaches a person; known_wedge would "fix" it');
|
|
1968
|
+
assert.notEqual(o.classifyFailure(reason).class, 'known_wedge');
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
t('--migrate-as hands the DATABASE steps to another account, and nothing else', async () => {
|
|
1972
|
+
const dir = scratchConsumer();
|
|
1973
|
+
const calls = [];
|
|
1974
|
+
const res = await u.runUpgrade(
|
|
1975
|
+
{ to: '1.15.0', registry: true, instance: dir, migrateAs: 'bongos-p', healthCheck: false },
|
|
1976
|
+
{
|
|
1977
|
+
log: () => {}, err: () => {},
|
|
1978
|
+
run: (cmd, args) => {
|
|
1979
|
+
calls.push(`${cmd} ${(args || []).join(' ')}`.trim());
|
|
1980
|
+
if (cmd === 'git' && args[0] === 'status') return { status: 0, stdout: '' };
|
|
1981
|
+
if (cmd === 'npm' && args[0] === 'install') {
|
|
1982
|
+
writeFileSync(join(dir, 'node_modules', '@bongos', 'core', 'package.json'),
|
|
1983
|
+
JSON.stringify({ name: '@bongos/core', version: '1.15.0' }) + '\n');
|
|
1984
|
+
}
|
|
1985
|
+
return { status: 0, stdout: '' };
|
|
1986
|
+
},
|
|
1987
|
+
materialize: () => ({ skills: 0, writtenPaths: [] }),
|
|
1988
|
+
regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
|
|
1989
|
+
resolveInstanceDb: () => ({ db: 'p', ran: true }),
|
|
1990
|
+
databaseUrl: '', pgDatabase: '',
|
|
1991
|
+
}
|
|
1992
|
+
);
|
|
1993
|
+
assert.equal(res.ok, true, `the bump ran: ${res.error || ''}`);
|
|
1994
|
+
const migrate = calls.find((c) => /\bnpm run migrate\b/.test(c));
|
|
1995
|
+
assert.ok(migrate, `migrate ran: ${JSON.stringify(calls)}`);
|
|
1996
|
+
assert.match(migrate, /^sudo -n -u bongos-p env /, 'migrate connects as the account that owns the database');
|
|
1997
|
+
const identity = calls.find((c) => /psql -tAc/.test(c));
|
|
1998
|
+
assert.match(identity, /^sudo -n -u bongos-p env /, 'and so does the pre-check that decides whether it could');
|
|
1999
|
+
// The other half of the split: the file work stays with whoever is running, because the
|
|
2000
|
+
// project's own account cannot write this checkout.
|
|
2001
|
+
for (const bare of ['git status', 'npm install']) {
|
|
2002
|
+
assert.ok(calls.some((c) => c.startsWith(bare)), `${bare} runs as the move itself, unescalated`);
|
|
2003
|
+
assert.ok(!calls.some((c) => c.includes(`bongos-p env ${bare}`)), `${bare} must NOT be handed to the project's account`);
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
t('with no --migrate-as, every step is spawned exactly as it was before', async () => {
|
|
2008
|
+
// The shape half the fleet is on (a control plane, a project older than the per-instance
|
|
2009
|
+
// account model). A flag that quietly started escalating there would be a new failure.
|
|
2010
|
+
const dir = scratchConsumer();
|
|
2011
|
+
const calls = [];
|
|
2012
|
+
await u.runUpgrade(
|
|
2013
|
+
{ to: '1.15.0', registry: true, instance: dir, healthCheck: false },
|
|
2014
|
+
{
|
|
2015
|
+
log: () => {}, err: () => {},
|
|
2016
|
+
run: (cmd, args) => {
|
|
2017
|
+
calls.push(`${cmd} ${(args || []).join(' ')}`.trim());
|
|
2018
|
+
if (cmd === 'npm' && args[0] === 'install') {
|
|
2019
|
+
writeFileSync(join(dir, 'node_modules', '@bongos', 'core', 'package.json'),
|
|
2020
|
+
JSON.stringify({ name: '@bongos/core', version: '1.15.0' }) + '\n');
|
|
2021
|
+
}
|
|
2022
|
+
return { status: 0, stdout: '' };
|
|
2023
|
+
},
|
|
2024
|
+
materialize: () => ({ skills: 0, writtenPaths: [] }),
|
|
2025
|
+
regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
|
|
2026
|
+
resolveInstanceDb: () => ({ db: 'p', ran: true }),
|
|
2027
|
+
databaseUrl: '', pgDatabase: '',
|
|
2028
|
+
}
|
|
2029
|
+
);
|
|
2030
|
+
assert.ok(!calls.some((c) => c.startsWith('sudo')), `nothing escalated: ${JSON.stringify(calls.filter((c) => c.startsWith('sudo')))}`);
|
|
2031
|
+
assert.ok(calls.some((c) => /^npm run migrate$/.test(c)), 'migrate is still the bare command');
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
t('sudo refusing the hand-over is caught BEFORE the pin moves, not at migrate', async () => {
|
|
2035
|
+
// The one failure this split can add. Soft-skipping it (as "psql failed for another
|
|
2036
|
+
// reason") would move the pin, install the new core, and then fail at migrate for a
|
|
2037
|
+
// reason that was knowable up front — a full rollback, restarting a live service twice,
|
|
2038
|
+
// to learn something one `select 1` already said.
|
|
2039
|
+
const dir = scratchConsumer();
|
|
2040
|
+
const calls = [];
|
|
2041
|
+
const res = await u.runUpgrade(
|
|
2042
|
+
{ to: '1.15.0', registry: true, instance: dir, migrateAs: 'bongos-p', healthCheck: false },
|
|
2043
|
+
{
|
|
2044
|
+
log: () => {}, err: () => {},
|
|
2045
|
+
run: (cmd, args) => {
|
|
2046
|
+
calls.push(`${cmd} ${(args || []).join(' ')}`.trim());
|
|
2047
|
+
if (cmd === 'sudo') return { status: 1, stdout: '', stderr: 'sudo: a password is required\n' };
|
|
2048
|
+
return { status: 0, stdout: '' };
|
|
2049
|
+
},
|
|
2050
|
+
resolveInstanceDb: () => ({ db: 'p', ran: true }),
|
|
2051
|
+
materialize: () => ({ skills: 0, writtenPaths: [] }),
|
|
2052
|
+
databaseUrl: '', pgDatabase: '',
|
|
2053
|
+
}
|
|
2054
|
+
);
|
|
2055
|
+
assert.equal(res.ok, false);
|
|
2056
|
+
assert.match(res.error, /cannot run as "bongos-p"/);
|
|
2057
|
+
assert.match(res.error, /a password is required/, 'carrying what sudo actually said');
|
|
2058
|
+
assert.ok(!calls.some((c) => c.startsWith('npm install')), 'and nothing was installed — the pin never moved');
|
|
2059
|
+
assert.equal(u.readPinnedCoreVersion(dir), '1.14.1', 'the pin is untouched');
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
t('an account name that is not one is refused before anything moves', () => {
|
|
2063
|
+
// It reaches `sudo` as an argv entry, so it is refused rather than quoted into shape.
|
|
2064
|
+
return u.runUpgrade({ to: '1.15.0', instance: scratchConsumer(), migrateAs: 'root; rm -rf /' }, { log: () => {}, err: () => {} })
|
|
2065
|
+
.then((res) => {
|
|
2066
|
+
assert.equal(res.ok, false);
|
|
2067
|
+
assert.match(res.error, /not a unix account name/);
|
|
2068
|
+
});
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
t('parseArgs: --migrate-as is read, and is absent unless passed', () => {
|
|
2072
|
+
assert.equal(u.parseArgs(['--to', '1.0.0']).migrateAs, null);
|
|
2073
|
+
assert.equal(u.parseArgs(['--to', '1.0.0', '--migrate-as', 'bongos-p']).migrateAs, 'bongos-p');
|
|
2074
|
+
});
|
|
2075
|
+
|
|
1929
2076
|
await Promise.all(pending);
|
|
1930
2077
|
console.log(`\nupgrade: ${passed} passed, ${failed} failed`);
|
|
1931
2078
|
process.exit(failed ? 1 : 0);
|
|
@@ -33,6 +33,9 @@ const TABLE = [
|
|
|
33
33
|
['downgrade refusal', { ok: false, downgrade: true, error: 'refusing to DOWNGRADE: --to 1.19.1 is older' }, 'refused', 'downgrade', 'needs_decision'],
|
|
34
34
|
['already on the target', { ok: false, error: 'already on 1.20.1 (pass --force to re-run the steps anyway)' }, 'refused', 'already_on', 'unknown'],
|
|
35
35
|
['dirty working tree', { ok: false, error: 'working tree not clean — commit/stash first, or pass --force.\n M package.json' }, 'refused', 'dirty_tree', 'known_wedge'],
|
|
36
|
+
// task 1004512: git REFUSING to read the checkout is not a report that the checkout is
|
|
37
|
+
// dirty, and must not be classified as one — see the dedicated section at the end.
|
|
38
|
+
['git would not read the tree', { ok: false, error: 'git could not read the working tree in /srv/cloudbongos/p — the clean-tree pre-flight never ran, so this is NOT a report of uncommitted work.\nfatal: detected dubious ownership in repository at \'/srv/cloudbongos/p\'' }, 'refused', 'git_unreadable', 'unknown'],
|
|
36
39
|
['module pre-check', { ok: false, error: 'module pre-check failed: game incompatible with core 1.20.1' }, 'refused', 'module_incompatible', 'needs_decision'],
|
|
37
40
|
['database unresolvable', { ok: false, error: 'cannot determine which database to migrate.\n Set PGDATABASE' }, 'refused', 'db_unresolvable', 'unknown'],
|
|
38
41
|
['database identity', { ok: false, error: 'database "cb" is not reachable as the current user: role "root" does not exist' }, 'refused', 'db_identity', 'unknown'],
|
package/tests/wedge_remedy.mjs
CHANGED
|
@@ -37,10 +37,10 @@ const intent = (over = {}) => ({ id: 99, action: 'core-upgrade', target_version:
|
|
|
37
37
|
const wedge = (reason) => ({ ok: false, error: `core upgrade to 1.20.31 was refused (${reason})`, failure: { class: 'known_wedge', reason } });
|
|
38
38
|
const selfRoot = () => '/srv/charter';
|
|
39
39
|
|
|
40
|
-
function harness({ execFails = false } = {}) {
|
|
40
|
+
function harness({ execFails = false, privileged = false } = {}) {
|
|
41
41
|
const ran = []; const events = []; const queries = [];
|
|
42
42
|
const deps = {
|
|
43
|
-
apply: true, privileged
|
|
43
|
+
apply: true, privileged, log: () => {}, selfInstanceRoot: selfRoot,
|
|
44
44
|
exec: (cmd, opts) => { ran.push({ cmd, ...opts }); return execFails ? { ok: false, softFailed: true, stderr: 'fatal: nope' } : { ok: true, stdout: '' }; },
|
|
45
45
|
db: { query: async (sql, params) => { queries.push({ sql, params }); return { rows: [] }; } },
|
|
46
46
|
provisioning: { recordEvent: async (_db, e) => { events.push(e); } },
|
|
@@ -143,6 +143,40 @@ for (const reason of W.REMEDY_REASONS) {
|
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
await t('schema_pending on a PRIVILEGED runner migrates as the account that owns the DATABASE (task 1004512)', async () => {
|
|
147
|
+
// THE ONE PATH THAT CALLS runAsFor, and until this test nothing did: every other case
|
|
148
|
+
// above runs unprivileged, where planRemedy takes its no-sudo branch and the account is
|
|
149
|
+
// never resolved at all. So the account-selection change this task is about — runAsFor
|
|
150
|
+
// reading `migrateAs` instead of `runAs` — shipped with no coverage.
|
|
151
|
+
//
|
|
152
|
+
// The two answers are DIFFERENT accounts, and only one of them can connect: the project's
|
|
153
|
+
// database is owned by its own Postgres login role with CONNECT revoked from PUBLIC, so a
|
|
154
|
+
// migration run as the app user (which owns the checkout) fails outright. Asserting the
|
|
155
|
+
// assembled command, because that is where the wrong account would actually show up.
|
|
156
|
+
const h = harness({ privileged: true });
|
|
157
|
+
const r = await W.remedyAfterFailure({ intent: intent(), inst: inst(), result: wedge('schema_pending'), deps: h.deps });
|
|
158
|
+
assert.deepEqual(r, { repended: true });
|
|
159
|
+
// Two commands now: the `id -u` probe resolveRunAs spends to learn whether the project
|
|
160
|
+
// HAS an account of its own, then the migration itself.
|
|
161
|
+
assert.ok(h.ran.some((c) => /^id -u bongos-charter$/.test(c.cmd)), `the account is probed, not assumed: ${JSON.stringify(h.ran.map((c) => c.cmd))}`);
|
|
162
|
+
const migrate = (h.ran.find((c) => /migrate/.test(c.cmd)) || {}).cmd || '';
|
|
163
|
+
assert.match(migrate, /^sudo -u bongos-charter env PGDATABASE=charter /,
|
|
164
|
+
`must migrate as the project's own account — got: ${migrate}`);
|
|
165
|
+
assert.doesNotMatch(migrate, /sudo -u lars\b/,
|
|
166
|
+
'the app user owns the checkout, not the database — it has no role to connect with');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await t('…and the OTHER two fixes never escalate, privileged or not', async () => {
|
|
170
|
+
// The split is per-step, not per-run: dirty_tree touches the checkout and disk_full
|
|
171
|
+
// touches the backup dir, both of which belong to the runner. A sudo appearing here
|
|
172
|
+
// would mean the account change had leaked beyond the database.
|
|
173
|
+
for (const reason of ['dirty_tree', 'disk_full']) {
|
|
174
|
+
const h = harness({ privileged: true });
|
|
175
|
+
await W.remedyAfterFailure({ intent: intent(), inst: inst(), result: wedge(reason), deps: h.deps });
|
|
176
|
+
assert.doesNotMatch(h.ran[0].cmd, /sudo/, `${reason} runs as the runner, which owns what it touches`);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
146
180
|
await t('the same reason again after a fix: no second fix — retired as unknown for a person', async () => {
|
|
147
181
|
const h = harness();
|
|
148
182
|
const r = await W.remedyAfterFailure({ intent: intent({ remedied_reason: 'dirty_tree' }), inst: inst(), result: wedge('dirty_tree'), deps: h.deps });
|
|
@@ -204,3 +204,52 @@ test('the team pill is a plain pick on the Team size row — a second click keep
|
|
|
204
204
|
assert.deepEqual([...HUB.matchAll(/data-team="([a-z-]+)"/g)].map((m) => m[1]), ['small-team', 'community'],
|
|
205
205
|
'the row offers who can join a team; one person is the "One person" pill, not a team shape');
|
|
206
206
|
});
|
|
207
|
+
|
|
208
|
+
// ── slice 4 · a second draft beside a stored create keeps its place (task 1004549) ──
|
|
209
|
+
// Owner report 2026-10-02: "Allow repo access" seemed to throw the founder out of the
|
|
210
|
+
// flow. GitHub already held the approval, so the round-trip was a blink — and the page
|
|
211
|
+
// came back on the EARLIER project's done panel, because enter() sent every arrival with
|
|
212
|
+
// a stored create (cb-created-instance) to enterCreated(), which treats a non-deliberate
|
|
213
|
+
// arrival as "the same visit, show the watched project". With a draft part-way through
|
|
214
|
+
// the wizard, the same visit IS the draft.
|
|
215
|
+
const ENTER_RE = /function enter\(focus\) \{[\s\S]*?\n {4}\}/;
|
|
216
|
+
const enterSlice = (HUB.match(ENTER_RE) || [null])[0];
|
|
217
|
+
|
|
218
|
+
test('the enter() slice is still findable', () => {
|
|
219
|
+
assert.ok(enterSlice, 'enter() was not found in projects.html');
|
|
220
|
+
assert.match(enterSlice, /enterCreated\(/, 'enter() is where the stored create is routed');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
function runEnter({ draftStep, focus = false, resumeIntent = false, created = { id: 101, name: 'Old one' } }) {
|
|
224
|
+
const log = { shown: [], created: [], offer: undefined };
|
|
225
|
+
const sandbox = {
|
|
226
|
+
authKnown: true, me: { github_login: 'probe' }, resumeIntent,
|
|
227
|
+
state: { step: draftStep },
|
|
228
|
+
createdRecord: () => created,
|
|
229
|
+
enterCreated: (rec, f) => log.created.push({ id: rec.id, focus: f }),
|
|
230
|
+
paintResumeOffer: (rec) => { log.offer = rec; },
|
|
231
|
+
show: (step) => { sandbox.state.step = step; log.shown.push(step); },
|
|
232
|
+
saveDraft() {},
|
|
233
|
+
$: () => ({ focus() {} }),
|
|
234
|
+
};
|
|
235
|
+
sandbox.globalThis = sandbox;
|
|
236
|
+
vm.createContext(sandbox);
|
|
237
|
+
vm.runInContext(enterSlice + '\nenter(' + JSON.stringify(focus) + ');', sandbox);
|
|
238
|
+
return log;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
test('back from GitHub mid-draft, with an earlier project stored: the draft step, not the old done panel', () => {
|
|
242
|
+
for (const step of [2, 3, 4, 5]) {
|
|
243
|
+
const log = runEnter({ draftStep: step });
|
|
244
|
+
assert.deepEqual(log.created, [], `step ${step}: the earlier project's panel is not entered`);
|
|
245
|
+
assert.deepEqual(log.shown, [step], `step ${step}: the draft keeps its place`);
|
|
246
|
+
assert.equal(log.offer && log.offer.id, 101, 'the earlier project stays one click away on the resume line');
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('the done panel still comes back when no second draft is in flight, or on a deliberate nav', () => {
|
|
251
|
+
assert.equal(runEnter({ draftStep: 6 }).created.length, 1, 'a reload on the done panel stays on it');
|
|
252
|
+
assert.equal(runEnter({ draftStep: 0 }).created.length, 1, 'no draft position: the watched project');
|
|
253
|
+
assert.deepEqual(runEnter({ draftStep: 4, focus: true }).created, [{ id: 101, focus: true }], 'a deliberate New project nav is enterCreated\'s to decide');
|
|
254
|
+
assert.equal(runEnter({ draftStep: 4, resumeIntent: true }).created.length, 1, 'the resume link still resumes');
|
|
255
|
+
});
|
|
@@ -61,8 +61,10 @@ test('both slices are still findable (a rename must fail loudly, not silently pa
|
|
|
61
61
|
assert.match(enterSlice, /startProgress\(created\.id\);/, 'so is the resume branch it guards');
|
|
62
62
|
assert.match(enterSlice, /draft\.step >= 1 && draft\.step <= 5 \?/,
|
|
63
63
|
'the STORED draft\'s step decides, not state.step — state.step is 6 for the whole life of a finished standup');
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
// since task 1004549 a second draft in flight keeps its place on a non-deliberate
|
|
65
|
+
// arrival (tests/wizard_draft_resume.mjs slice 4); a deliberate nav still comes here
|
|
66
|
+
assert.match(SCRIPT, /if \(created && created\.id && \(focus \|\| !draftInFlight \|\| resumeIntent\)\) \{ enterCreated\(created, !!focus\); return; \}/,
|
|
67
|
+
'enter() still hands the created record to it on a deliberate nav');
|
|
66
68
|
});
|
|
67
69
|
|
|
68
70
|
// A faithful miniature: the stubs record what enterCreated ASKS FOR, which is the whole
|