@bongos/core 1.19.665 → 1.19.667
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 +40 -35
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/session-store.js +33 -9
- package/modules/hall-ui/public/shell.js +1 -1
- package/modules/public-landing/public/index.html +1 -1
- package/modules/public-landing/public/projects.html +1 -1
- package/modules/security/artifact-scan/detectors/credentials.js +8 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/box-infra.js +1 -1
- package/scripts/gds/cli-lib.js +55 -16
- package/scripts/gds/conductor-dispatch.js +102 -4
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/init.js +1 -1
- package/scripts/gds/paste-token.js +10 -5
- package/scripts/gds/portability-smoke-memory.sh +1 -1
- package/scripts/gds/setup.js +4 -4
- package/scripts/gds/smoke-write.sh +2 -1
- package/scripts/hall-preview/refresh-fixtures.js +1 -1
- package/src/bongos/auth.js +27 -5
- package/src/bongos/middleware/rate-limit.js +17 -2
- package/src/bongos/routes/auth.js +46 -15
- package/src/instance-config.js +28 -2
- package/src/module-api.js +1 -1
- package/tests/cli_sessions.mjs +6 -5
- package/tests/conductor.mjs +106 -0
- package/tests/landing_page.mjs +5 -1
- package/tests/lib_sh_resolution.mjs +84 -17
- package/tests/session_rename_fallback.mjs +383 -0
- package/tests/setup_device_flow_fallback.mjs +4 -1
package/tests/conductor.mjs
CHANGED
|
@@ -372,6 +372,112 @@ test('bestMatchingClaim: single claim returned; empty/null → null', () => {
|
|
|
372
372
|
assert.equal(dispatch.bestMatchingClaim(null, ['x']), null);
|
|
373
373
|
});
|
|
374
374
|
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// Attribution: which claim does this diff belong to? (task 1003834)
|
|
377
|
+
//
|
|
378
|
+
// The regression these pin is not hypothetical — it happened, and the fixture
|
|
379
|
+
// below is the real data from 2026-09-10.
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
// The observed failure, verbatim. Reviewing task 1003829's diff from its own
|
|
383
|
+
// worktree, the dispatch announced task 1003704 — a concurrent rename claim
|
|
384
|
+
// whose touches[] are four coarse directory prefixes. The reviewer was briefed
|
|
385
|
+
// on unrelated work and had to reason its way out.
|
|
386
|
+
const CLAIM_1003704 = {
|
|
387
|
+
task_id: '1003704',
|
|
388
|
+
title: 'C3b: rename gds-session.json and the gds_session cookie',
|
|
389
|
+
worktree_name: 'task-1003704',
|
|
390
|
+
touches: ['scripts/gds/', 'src/bongos/', 'modules/', 'clients/'],
|
|
391
|
+
};
|
|
392
|
+
const CLAIM_1003829 = {
|
|
393
|
+
task_id: '1003829',
|
|
394
|
+
title: 'The Studio shows an artist no visuals',
|
|
395
|
+
worktree_name: 'task-1003829',
|
|
396
|
+
touches: ['modules/hall-ui/public/studio.js', 'modules/hall-ui/public/studio.html', 'modules/hall-ui/public/studio.css'],
|
|
397
|
+
};
|
|
398
|
+
// The diff as it actually was: three studio files plus the wider blast radius a
|
|
399
|
+
// real change carries. Under a RAW COUNT, 1003704 matches 8 of these and
|
|
400
|
+
// 1003829 only 3 — which is how the wrong task won.
|
|
401
|
+
const DIFF_1003829 = [
|
|
402
|
+
'modules/hall-ui/public/studio.js',
|
|
403
|
+
'modules/hall-ui/public/studio.html',
|
|
404
|
+
'modules/hall-ui/public/studio.css',
|
|
405
|
+
'modules/hall-ui/public/hall-kit.js',
|
|
406
|
+
'modules/hall-ui/public/studio.states.json',
|
|
407
|
+
'scripts/hall-preview/server.js',
|
|
408
|
+
'scripts/hall-preview/task-visual.js',
|
|
409
|
+
'scripts/hall-preview/fixtures/tasks__detail.json',
|
|
410
|
+
'docs/copy-inventory.md',
|
|
411
|
+
'docs/copy-registry.json',
|
|
412
|
+
'tests/hall_studio_world.mjs',
|
|
413
|
+
'tests/task_visuals.mjs',
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
test('attribution: the worktree decides, even when another claim out-matches the diff', () => {
|
|
417
|
+
const claims = [CLAIM_1003704, CLAIM_1003829];
|
|
418
|
+
const got = dispatch.attributeClaim(claims, DIFF_1003829, 'task-1003829');
|
|
419
|
+
assert.equal(got.claim.task_id, '1003829', 'the claim BOUND to this worktree wins');
|
|
420
|
+
assert.equal(got.signal, 'worktree');
|
|
421
|
+
// And it wins regardless of claim order, so this cannot pass by accident.
|
|
422
|
+
assert.equal(dispatch.attributeClaim([CLAIM_1003829, CLAIM_1003704], DIFF_1003829, 'task-1003704').claim.task_id, '1003704');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('attribution: specificity beats breadth, so the honest touches[] no longer loses', () => {
|
|
426
|
+
// The same fixture with NO worktree signal — running from the main checkout,
|
|
427
|
+
// or a pre-worktree_name claim. The heuristic alone must now get it right.
|
|
428
|
+
const got = dispatch.attributeClaim([CLAIM_1003704, CLAIM_1003829], DIFF_1003829, null);
|
|
429
|
+
assert.equal(got.claim.task_id, '1003829', 'three exact paths outweigh four directory prefixes');
|
|
430
|
+
assert.equal(got.signal, 'overlap');
|
|
431
|
+
// Proof the OLD scoring is what was wrong: by raw count, 1003704 matches more.
|
|
432
|
+
const rawCount = (c) => DIFF_1003829.filter((f) => (c.touches || []).some((t) => f === t || f.startsWith(t.endsWith('/') ? t : t + '/'))).length;
|
|
433
|
+
assert.ok(rawCount(CLAIM_1003704) > rawCount(CLAIM_1003829), 'the broad claim really does match more files');
|
|
434
|
+
assert.ok(dispatch.claimOverlapScore(CLAIM_1003829, DIFF_1003829) > dispatch.claimOverlapScore(CLAIM_1003704, DIFF_1003829), 'and still scores lower');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test('attribution: volume still counts — a genuinely broad claim keeps its diff', () => {
|
|
438
|
+
// Specificity must not invert the heuristic: a rename touching fifty files
|
|
439
|
+
// under scripts/gds/ should not lose to a claim sharing one exact file.
|
|
440
|
+
const broad = { task_id: 'broad', touches: ['scripts/gds/'] };
|
|
441
|
+
const narrow = { task_id: 'narrow', touches: ['scripts/gds/one.js'] };
|
|
442
|
+
const files = Array.from({ length: 50 }, (_, i) => `scripts/gds/f${i}.js`).concat('scripts/gds/one.js');
|
|
443
|
+
assert.equal(dispatch.attributeClaim([narrow, broad], files, null).claim.task_id, 'broad');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('attribution: an empty touches[] is unreachable by overlap — the worktree saves it', () => {
|
|
447
|
+
// Most tasks in this repo declare no touches at all, so they score zero
|
|
448
|
+
// against every diff. That is the other half of why the worktree leads.
|
|
449
|
+
const bare = { task_id: 'bare', worktree_name: 'task-bare', touches: [] };
|
|
450
|
+
assert.equal(dispatch.claimOverlapScore(bare, ['a.js', 'b.js']), 0);
|
|
451
|
+
assert.equal(dispatch.attributeClaim([CLAIM_1003704, bare], ['a.js'], 'task-bare').claim.task_id, 'bare');
|
|
452
|
+
// With no worktree match it cannot win, and the signal says the answer is weak.
|
|
453
|
+
const guess = dispatch.attributeClaim([bare, CLAIM_1003704], ['docs/x.md'], 'somewhere-else');
|
|
454
|
+
assert.equal(guess.signal, 'fallback', 'zero overlap everywhere is reported as no signal, not as a match');
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test('attribution: a worktree bound to no claim does not borrow one silently', () => {
|
|
458
|
+
const claims = [CLAIM_1003704, CLAIM_1003829];
|
|
459
|
+
const got = dispatch.attributeClaim(claims, DIFF_1003829, 'baseline-main-2');
|
|
460
|
+
assert.equal(got.signal, 'overlap', 'it falls back rather than refusing');
|
|
461
|
+
// …and the header SAYS so, which is the part that would have saved the reader.
|
|
462
|
+
const note = dispatch.attributionNote({ id: got.claim.task_id, attribution: got.signal, claim_count: claims.length });
|
|
463
|
+
assert.match(note, /attribution: guessed from touches\[\] overlap, across 2 open claims/);
|
|
464
|
+
assert.match(note, /discount findings about intent/);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test('attribution: the note stays quiet when it would only add noise', () => {
|
|
468
|
+
// Certain attribution, and the single-claim case, have nothing to warn about.
|
|
469
|
+
assert.equal(dispatch.attributionNote({ id: '1', attribution: 'worktree', claim_count: 4 }), '');
|
|
470
|
+
assert.equal(dispatch.attributionNote({ id: '1', attribution: 'overlap', claim_count: 1 }), '');
|
|
471
|
+
assert.equal(dispatch.attributionNote({ id: null, attribution: 'overlap', claim_count: 9 }), '');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test('attribution: claimForWorktree ignores claims with no recorded worktree', () => {
|
|
475
|
+
// Pre-column claims carry null; they must not match an absent cwd name.
|
|
476
|
+
assert.equal(dispatch.claimForWorktree([{ task_id: 'x', worktree_name: null }], 'task-x'), null);
|
|
477
|
+
assert.equal(dispatch.claimForWorktree([{ task_id: 'x', worktree_name: 'task-x' }], null), null);
|
|
478
|
+
assert.equal(dispatch.claimForWorktree(null, 'task-x'), null);
|
|
479
|
+
});
|
|
480
|
+
|
|
375
481
|
test('isCovered now delegates to the canonical matcher (still correct)', () => {
|
|
376
482
|
// Regression guard for the dedup: isCovered is a thin wrapper over
|
|
377
483
|
// path-match.matchOne (relocated from the deleted touches-scan in task 879),
|
package/tests/landing_page.mjs
CHANGED
|
@@ -45,6 +45,7 @@ const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
|
|
|
45
45
|
const HTML = fs.readFileSync(path.join(LANDING, 'index.html'), 'utf8');
|
|
46
46
|
const WORLD = fs.readFileSync(path.join(LANDING, 'assets', 'world.css'), 'utf8');
|
|
47
47
|
|
|
48
|
+
const { SESSION_COOKIE_NAMES } = require('../src/bongos/auth.js');
|
|
48
49
|
const { stripApexOnly } = require('../src/platform-server.js');
|
|
49
50
|
const { applyBrandTokens } = require('../src/bongos/serve-internal.js');
|
|
50
51
|
|
|
@@ -484,7 +485,10 @@ test('sign out POSTs, and expires both cookies in both Domain forms', () => {
|
|
|
484
485
|
// only matches the cookie whose Domain attribute it names, so the host-only
|
|
485
486
|
// and apex-scoped forms must BOTH be expired (#726).
|
|
486
487
|
assert.match(CODE, /fetch\(API \+ '\/auth\/logout', \{ method: 'POST', credentials: 'same-origin' \}\)/);
|
|
487
|
-
|
|
488
|
+
// Every accepted cookie name must be expired, not just the newest: a browser arriving with an
|
|
489
|
+
// older name would otherwise keep a live cookie through "sign out". Derived from the canonical
|
|
490
|
+
// list so this follows the rename chain (task 1003704, and again at 1003706).
|
|
491
|
+
assert.match(CODE, new RegExp('\\[' + SESSION_COOKIE_NAMES.map((n) => `'${n}'`).join(', ') + '\\]'));
|
|
488
492
|
assert.match(CODE, /name \+ '=; Max-Age=0; path=\/';/);
|
|
489
493
|
assert.match(CODE, /name \+ '=; Max-Age=0; path=\/; Domain=\.' \+ apex/);
|
|
490
494
|
// it runs whether the POST lands or not
|
|
@@ -35,6 +35,12 @@ const require = createRequire(import.meta.url);
|
|
|
35
35
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
36
36
|
const GDS = path.join(ROOT, 'scripts', 'gds');
|
|
37
37
|
const fitness = require(path.join(GDS, 'fitness.js'));
|
|
38
|
+
// The CURRENT session filename, so the expectations below follow the rename chain
|
|
39
|
+
// (task 1003704 renamed the file; 1003706 retires the old names) instead of being
|
|
40
|
+
// re-edited at each step. SESSION_FILENAMES[0] is what the CLI WRITES; the tail is
|
|
41
|
+
// what it still accepts on read.
|
|
42
|
+
const ic = require(path.join(ROOT, 'src', 'instance-config.js'));
|
|
43
|
+
const SESSION_FILE = ic.SESSION_FILENAMES[0];
|
|
38
44
|
|
|
39
45
|
// Every bash script that resolves a GDS token or API base.
|
|
40
46
|
const SMOKES = [
|
|
@@ -184,9 +190,13 @@ const boxCode = (f) => fitness.stripCommentsForIdentity(fs.readFileSync(path.joi
|
|
|
184
190
|
test('every baked box script resolves the session across config dirs, not just the legacy one', () => {
|
|
185
191
|
for (const f of BOX_SCRIPTS) {
|
|
186
192
|
const src = boxCode(f);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
193
|
+
for (const name of ic.SESSION_FILENAMES.slice(0, 2)) {
|
|
194
|
+
assert.ok(src.includes(`.config/*/${name}`),
|
|
195
|
+
`${f} must scan ~/.config/*/${name} — without the glob a /builder-reauth into a `
|
|
196
|
+
+ 'non-"otb" configDir strands the box on a dead token, and without BOTH names '
|
|
197
|
+
+ 'half the fleet is unreadable: a box provisioned before the rename has only the '
|
|
198
|
+
+ 'old name, one provisioned after has only the new (task 1003704)');
|
|
199
|
+
}
|
|
190
200
|
// The legacy literal may survive ONLY as the last-resort default, never as the
|
|
191
201
|
// sole source — i.e. it must be preceded by the glob scan.
|
|
192
202
|
// The scan widens what these scripts will read, so each candidate must be owned
|
|
@@ -194,8 +204,8 @@ test('every baked box script resolves the session across config dirs, not just t
|
|
|
194
204
|
// simply by being newest.
|
|
195
205
|
assert.match(src, /\[ -O "\$_sess" \]/,
|
|
196
206
|
`${f} must require candidate session files to be owned by the current user`);
|
|
197
|
-
const globAt = src.indexOf('.config/*/
|
|
198
|
-
const legacyAt = src.indexOf(
|
|
207
|
+
const globAt = src.indexOf('.config/*/');
|
|
208
|
+
const legacyAt = src.indexOf(`.config/otb/${SESSION_FILE}`);
|
|
199
209
|
if (legacyAt !== -1) {
|
|
200
210
|
assert.ok(globAt < legacyAt,
|
|
201
211
|
`${f} reaches the legacy path before scanning ~/.config/*/ — the fallback must come first`);
|
|
@@ -217,10 +227,17 @@ test('every baked box script passes bash -n', (t) => {
|
|
|
217
227
|
function resolverBlock(f) {
|
|
218
228
|
const src = fs.readFileSync(path.join(INFRA, f), 'utf8');
|
|
219
229
|
const start = src.indexOf('if [ -z "${GDS_SESSION:-}" ]; then');
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
230
|
+
// Matched by PREFIX, up to the closing quote — never by the full line including the
|
|
231
|
+
// filename. The filename is the one thing a rename changes, so spelling it here made
|
|
232
|
+
// this test fail with "could not locate the resolver block" on a perfectly good
|
|
233
|
+
// rename, and on Windows (where it already failed for its own reason) that looked
|
|
234
|
+
// like no new failure at all (task 1003704).
|
|
235
|
+
const endPrefix = 'GDS_SESSION="${GDS_SESSION:-$HOME/.config/otb/';
|
|
236
|
+
const at = src.indexOf(endPrefix, start);
|
|
237
|
+
const close = at === -1 ? -1 : src.indexOf('"', at + endPrefix.length);
|
|
238
|
+
assert.ok(start !== -1 && at !== -1 && close !== -1,
|
|
239
|
+
`could not locate the resolver block in ${f}`);
|
|
240
|
+
return src.slice(start, close + 1);
|
|
224
241
|
}
|
|
225
242
|
|
|
226
243
|
test('the resolver prefers the most recently written session across config dirs', (t) => {
|
|
@@ -253,7 +270,7 @@ test('the resolver prefers the most recently written session across config dirs'
|
|
|
253
270
|
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'boxsess-empty-'));
|
|
254
271
|
const r3 = spawnSync('bash', ['-c', `set -uo pipefail\n${block}\nprintf '%s' "$GDS_SESSION"`],
|
|
255
272
|
{ encoding: 'utf8', env: { PATH: process.env.PATH, HOME: empty } });
|
|
256
|
-
assert.equal(r3.stdout, path.join(empty, '.config', 'otb',
|
|
273
|
+
assert.equal(r3.stdout, path.join(empty, '.config', 'otb', SESSION_FILE));
|
|
257
274
|
|
|
258
275
|
fs.rmSync(home, { recursive: true, force: true });
|
|
259
276
|
fs.rmSync(empty, { recursive: true, force: true });
|
|
@@ -264,16 +281,25 @@ test('the resolver prefers the most recently written session across config dirs'
|
|
|
264
281
|
// override but assigned to SF, so a caller that SET the override had it silently
|
|
265
282
|
// dropped and fell through to the legacy default — the exact bug being fixed,
|
|
266
283
|
// reintroduced through the back door. Pin all three branches.
|
|
284
|
+
// box-heartbeat.sh's resolver, pulled straight out of the committed file. Shared by the
|
|
285
|
+
// two tests below so there is a single extraction to fix when the names next move.
|
|
286
|
+
function heartbeatSfBlock() {
|
|
287
|
+
const src = fs.readFileSync(path.join(INFRA, 'box-heartbeat.sh'), 'utf8');
|
|
288
|
+
const start = src.indexOf('SF="${OTB_GDS_SESSION:-}"');
|
|
289
|
+
// Prefix, not the whole line — see resolverBlock above.
|
|
290
|
+
const endPrefix = 'SF="${SF:-$HOME/.config/otb/';
|
|
291
|
+
const at = src.indexOf(endPrefix, start);
|
|
292
|
+
const close = at === -1 ? -1 : src.indexOf('"', at + endPrefix.length);
|
|
293
|
+
assert.ok(start !== -1 && at !== -1 && close !== -1,
|
|
294
|
+
'could not locate the SF resolver in box-heartbeat.sh');
|
|
295
|
+
return src.slice(start, close + 1);
|
|
296
|
+
}
|
|
297
|
+
|
|
267
298
|
test('box-heartbeat.sh honors OTB_GDS_SESSION, scans, then falls back', (t) => {
|
|
268
299
|
const probe = bash(['-n', 'infra/box-heartbeat.sh']);
|
|
269
300
|
if (!probe) return t.skip('bash not available on this machine');
|
|
270
301
|
|
|
271
|
-
const
|
|
272
|
-
const start = src.indexOf('SF="${OTB_GDS_SESSION:-}"');
|
|
273
|
-
const endMark = 'SF="${SF:-$HOME/.config/otb/gds-session.json}"';
|
|
274
|
-
const end = src.indexOf(endMark, start);
|
|
275
|
-
assert.ok(start !== -1 && end !== -1, 'could not locate the SF resolver in box-heartbeat.sh');
|
|
276
|
-
const block = src.slice(start, end + endMark.length);
|
|
302
|
+
const block = heartbeatSfBlock();
|
|
277
303
|
|
|
278
304
|
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'hbsess-'));
|
|
279
305
|
const configured = path.join(home, '.config', 'cloudbongos');
|
|
@@ -289,8 +315,49 @@ test('box-heartbeat.sh honors OTB_GDS_SESSION, scans, then falls back', (t) => {
|
|
|
289
315
|
assert.equal(run({}), path.join(configured, 'gds-session.json'));
|
|
290
316
|
|
|
291
317
|
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'hbsess-empty-'));
|
|
292
|
-
assert.equal(run({}, empty), path.join(empty, '.config', 'otb',
|
|
318
|
+
assert.equal(run({}, empty), path.join(empty, '.config', 'otb', SESSION_FILE));
|
|
293
319
|
|
|
294
320
|
fs.rmSync(home, { recursive: true, force: true });
|
|
295
321
|
fs.rmSync(empty, { recursive: true, force: true });
|
|
296
322
|
});
|
|
323
|
+
|
|
324
|
+
// The rename leaves the fleet in FOUR states at once, and the two tests above only cover
|
|
325
|
+
// the pre-rename half (their fixtures write the old name). These are the other two, and
|
|
326
|
+
// they are the ones a rename actually breaks (task 1003704). Same extracted resolver — one
|
|
327
|
+
// harness for this block, deliberately, so part 4 (task 1003706) has one place to change.
|
|
328
|
+
test('the box resolver handles a post-rename box and a migrated one', (t) => {
|
|
329
|
+
const probe = bash(['-n', 'infra/box-heartbeat.sh']);
|
|
330
|
+
if (!probe) return t.skip('bash not available on this machine');
|
|
331
|
+
|
|
332
|
+
const block = heartbeatSfBlock();
|
|
333
|
+
const run = (h) => spawnSync('bash', ['-c', `set -euo pipefail\n${block}\nprintf '%s' "$SF"`],
|
|
334
|
+
{ encoding: 'utf8', env: { PATH: process.env.PATH, HOME: h } }).stdout;
|
|
335
|
+
|
|
336
|
+
// A box provisioned AFTER the rename has ONLY the new name. Before the glob was widened
|
|
337
|
+
// this fell through to the historical default, which does not exist, so every heartbeat
|
|
338
|
+
// tick failed with a misleading "no session".
|
|
339
|
+
const fresh = fs.mkdtempSync(path.join(os.tmpdir(), 'hbsess-new-'));
|
|
340
|
+
const freshCfg = path.join(fresh, '.config', 'cloudbongos');
|
|
341
|
+
fs.mkdirSync(freshCfg, { recursive: true });
|
|
342
|
+
fs.writeFileSync(path.join(freshCfg, SESSION_FILE), '{"token":"fresh"}');
|
|
343
|
+
const tail = (p) => p.replace(/\\/g, '/').split('/').slice(-2).join('/');
|
|
344
|
+
assert.equal(tail(run(fresh)), `cloudbongos/${SESSION_FILE}`,
|
|
345
|
+
'a box provisioned after the rename must resolve the new name');
|
|
346
|
+
|
|
347
|
+
// A MIGRATED box carries both, the new one written later by the CLI's migrate-on-read.
|
|
348
|
+
// Newest-wins must take the new one; taking the older would read a stale token and report
|
|
349
|
+
// the misleading "session expired" of task 1003252.
|
|
350
|
+
const both = fs.mkdtempSync(path.join(os.tmpdir(), 'hbsess-both-'));
|
|
351
|
+
const bothCfg = path.join(both, '.config', 'cloudbongos');
|
|
352
|
+
fs.mkdirSync(bothCfg, { recursive: true });
|
|
353
|
+
const legacyName = ic.SESSION_FILENAMES[1];
|
|
354
|
+
fs.writeFileSync(path.join(bothCfg, legacyName), '{"token":"stale"}');
|
|
355
|
+
fs.writeFileSync(path.join(bothCfg, SESSION_FILE), '{"token":"fresh"}');
|
|
356
|
+
const older = new Date(Date.now() - 60_000);
|
|
357
|
+
fs.utimesSync(path.join(bothCfg, legacyName), older, older);
|
|
358
|
+
assert.equal(tail(run(both)), `cloudbongos/${SESSION_FILE}`,
|
|
359
|
+
'a migrated box must prefer the newly written file, not the superseded one');
|
|
360
|
+
|
|
361
|
+
fs.rmSync(fresh, { recursive: true, force: true });
|
|
362
|
+
fs.rmSync(both, { recursive: true, force: true });
|
|
363
|
+
});
|