@bongos/core 1.19.1032 → 1.19.1034
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 +322 -117
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/grade-audit/SKILL.md +1 -1
- package/.claude/skills/grade-recover/SKILL.md +2 -1
- package/.claude/skills/grader-health/SKILL.md +5 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
- package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
- package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
- package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
- package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
- package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
- package/docs/adr/README.md +4 -4
- package/docs/api/openapi.json +1 -1
- package/docs/architecture.md +4 -3
- package/docs/canonical-permissions.md +2 -2
- package/docs/copy-inventory.md +34 -30
- package/docs/copy-registry.json +69 -33
- package/docs/file-map.md +4 -3
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/autobongos-windows-host.md +3 -1
- package/docs/recipes/local-dev.md +45 -3
- package/docs/recipes/packaging-the-core.md +3 -1
- package/docs/recipes/redteam-patrol.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/cadence.js +52 -0
- package/modules/autonomy/gauge.js +57 -9
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/grading/grade-engine.js +267 -0
- package/modules/grading/grader-subagent.js +4 -3
- package/modules/grading/grader-workers/index.js +67 -11
- package/modules/grading/grader-workers/worker-verdict.js +32 -2
- package/modules/grading/grader.js +109 -88
- package/modules/hall-ui/public/deploy.js +35 -0
- package/modules/hall-ui/public/project-settings.css +7 -0
- package/modules/hall-ui/records/builders-roster.md +10 -0
- package/modules/hall-ui/records/catalog.md +10 -0
- package/modules/hall-ui/records/collaboration.md +14 -0
- package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
- package/modules/hall-ui/records/oversight-family.md +19 -0
- package/modules/hall-ui/records/panel-family.md +19 -0
- package/modules/hall-ui/records/reading-rooms.md +12 -0
- package/modules/hall-ui/records/record-family.md +13 -0
- package/modules/hall-ui/records/reference-room.md +9 -0
- package/modules/hall-ui/records/roster-and-ledger.md +9 -0
- package/modules/hall-ui/records/sky.md +23 -0
- package/modules/lifecycle/dependency-advisory.js +2 -1
- package/modules/lifecycle/routes/tasks.js +19 -4
- package/modules/lifecycle/ship-preflight.js +91 -0
- package/modules/provisioning/control-plane-guard.js +14 -13
- package/modules/provisioning/core-upgrade.js +30 -17
- package/modules/provisioning/public-refusal.js +44 -0
- package/modules/provisioning/routes/disconnect.js +2 -1
- package/modules/provisioning/routes/env-manifest.js +5 -4
- package/modules/provisioning/routes/provisioning.js +16 -15
- package/modules/provisioning/routes/repo-private.js +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +7481 -0
- package/scripts/gds/address-park-sweep.sh +3 -1
- package/scripts/gds/autobongos-run.js +91 -3
- package/scripts/gds/backfill-cost.js +20 -9
- package/scripts/gds/baseline-staleness.js +1 -1
- package/scripts/gds/claim.js +2 -2
- package/scripts/gds/credential-presence.js +281 -0
- package/scripts/gds/db-backup-nightly.sh +9 -4
- package/scripts/gds/db-backup-offsite.sh +4 -2
- package/scripts/gds/deploy-main-audit-gate.sh +11 -4
- package/scripts/gds/dev-lib.js +212 -5
- package/scripts/gds/dev.js +102 -50
- package/scripts/gds/doctor.js +3 -2
- package/scripts/gds/grade-correlation-audit.js +280 -0
- package/scripts/gds/grade-replay.js +237 -0
- package/scripts/gds/hooks/pre-push.js +7 -3
- package/scripts/gds/main-audit.js +19 -10
- package/scripts/gds/memory-map.js +3 -1
- package/scripts/gds/onboarding-config.js +3 -2
- package/scripts/gds/package-core.js +72 -1
- package/scripts/gds/plain-cards.js +4 -0
- package/scripts/gds/preflight.js +12 -4
- package/scripts/gds/provision-core-upgrade.js +282 -80
- package/scripts/gds/provision-repo-auth.js +15 -0
- package/scripts/gds/provision.js +1 -1
- package/scripts/gds/publish-credential-check.js +3 -0
- package/scripts/gds/push-main.js +14 -3
- package/scripts/gds/push-path-brief.js +3 -2
- package/scripts/gds/redteam-patrol.js +4 -4
- package/scripts/gds/release-notes.js +271 -0
- package/scripts/gds/run-routine.js +18 -5
- package/scripts/gds/run-unit-tests.js +14 -3
- package/scripts/gds/seed-board-room-tasks.js +1 -1
- package/scripts/gds/session-cost.js +12 -3
- package/scripts/gds/ship-deploy-target.js +11 -6
- package/scripts/gds/ship-flow.js +5 -3
- package/scripts/gds/ship-grade-steps.js +33 -0
- package/scripts/gds/ship-land.js +241 -16
- package/scripts/gds/ship-merge.js +3 -2
- package/scripts/gds/ship-preflight-steps.js +86 -11
- package/scripts/gds/ship.js +1 -1
- package/scripts/gds/stray-unit-sweep.sh +3 -1
- package/scripts/gds/upgrade-migrate.js +67 -0
- package/scripts/gds/upgrade.js +9 -7
- package/scripts/hall-preview/README.md +20 -3
- package/scripts/hall-preview/live-identity.js +1 -1
- package/scripts/hall-preview/refresh-fixtures.js +71 -11
- package/scripts/hall-preview/scrub-identity.js +296 -0
- package/src/bongos/routes/security.js +3 -3
- package/src/build-info.js +4 -2
- package/src/module-api.js +1 -1
- package/tests/adr_0089_scope.mjs +70 -0
- package/tests/adr_0162_citation.mjs +184 -0
- package/tests/autobongos_cadence.mjs +74 -0
- package/tests/autobongos_loop.mjs +114 -0
- package/tests/autonomy_gauge.mjs +56 -0
- package/tests/consumer_layout_boot.mjs +363 -31
- package/tests/core_upgrade_door.mjs +40 -6
- package/tests/core_upgrade_runner.mjs +329 -5
- package/tests/credential_presence.mjs +153 -0
- package/tests/db_backup_nightly.mjs +21 -3
- package/tests/db_backup_offsite.mjs +81 -0
- package/tests/deploy_audit_gate.mjs +5 -2
- package/tests/deploy_page_projects.mjs +63 -2
- package/tests/dev_lib.mjs +2 -1
- package/tests/dev_windows.mjs +324 -0
- package/tests/env_knobs_neutral_prefix.mjs +203 -0
- package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
- package/tests/government_protected_surfaces.mjs +10 -0
- package/tests/grade_correlation_audit.mjs +159 -0
- package/tests/grade_engine.mjs +249 -0
- package/tests/grade_engine_replay.mjs +99 -0
- package/tests/grader_prompt_window_bound.mjs +132 -0
- package/tests/grader_unbacked_score.mjs +194 -0
- package/tests/hall_preview_scrub.mjs +214 -0
- package/tests/hall_records.mjs +85 -0
- package/tests/helpers.mjs +25 -0
- package/tests/instance_db_migrated_on_code_move.mjs +8 -1
- package/tests/integration_parked_ratchet.mjs +24 -1
- package/tests/plain_cards.mjs +1 -1
- package/tests/provisioning_public_refusal.mjs +195 -0
- package/tests/publish_status_branch.mjs +128 -0
- package/tests/redteam_patrol.mjs +1 -1
- package/tests/release_notes.mjs +327 -0
- package/tests/shell_stat_portability.mjs +49 -0
- package/tests/ship_land_conflict.mjs +181 -0
- package/tests/ship_land_red_pr.mjs +236 -0
- package/tests/ship_preflight.mjs +226 -3
- package/tests/start_env_guard.mjs +16 -0
- package/tests/sweep_app_account.mjs +15 -2
- package/tests/upgrade.mjs +20 -1
- package/tests/upgrade_migrate.mjs +195 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// tests/db_backup_offsite.mjs — the weekly offsite copy sizes what it ships the same way
|
|
2
|
+
// on every platform (task 1004170).
|
|
3
|
+
//
|
|
4
|
+
// THE BUG. db-backup-offsite.sh read both of its sizes — the local dump it picks and the
|
|
5
|
+
// ciphertext it uploads — with GNU-only `stat -c '%s'`, which BSD/macOS stat refuses
|
|
6
|
+
// outright. Every run on a Mac died before it encrypted anything, and CI's Linux never
|
|
7
|
+
// saw it, because nothing ran the script at all. The empty-ciphertext guard rides on the
|
|
8
|
+
// second of those sizes, so it is pinned here too.
|
|
9
|
+
//
|
|
10
|
+
// The script runs for real under macOS-shaped `stat` and `wc` (tests/helpers.mjs), with
|
|
11
|
+
// stub `age` and `python3` first on PATH: nothing is encrypted, no bucket is touched and
|
|
12
|
+
// no network is used.
|
|
13
|
+
//
|
|
14
|
+
// Run: node --test tests/db_backup_offsite.mjs
|
|
15
|
+
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { test } from 'node:test';
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { macShapedTools } from './helpers.mjs';
|
|
24
|
+
|
|
25
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const SCRIPT = path.join(ROOT, 'scripts', 'gds', 'db-backup-offsite.sh');
|
|
27
|
+
const skip = process.platform === 'win32' ? 'needs a POSIX bash + coreutils (runs in CI)' : false;
|
|
28
|
+
// The script's env-var prefix, built rather than written — the same reason as in
|
|
29
|
+
// tests/db_backup_nightly.mjs: the legacy-literal ratchet counts the spelled-out form.
|
|
30
|
+
const ENV = 'gds_'.toUpperCase();
|
|
31
|
+
|
|
32
|
+
const DUMP_NAME = 'smallproj-2026-09-20T000000Z.sql.gz';
|
|
33
|
+
const DUMP = '--\n-- PostgreSQL database dump complete\n--\n';
|
|
34
|
+
const CIPHER = 'age-encryption.org/v1\n-> X25519 stub\n';
|
|
35
|
+
|
|
36
|
+
// `cipher` is what the stub age writes to its -o file. The stub python3 answers the
|
|
37
|
+
// boto3 probe and, for the upload program, records the object key it was handed.
|
|
38
|
+
function runOffsite({ cipher = CIPHER } = {}) {
|
|
39
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'db-offsite-'));
|
|
40
|
+
const bin = path.join(tmp, 'bin');
|
|
41
|
+
const dir = path.join(tmp, 'backups');
|
|
42
|
+
const uploads = path.join(tmp, 'uploads.log');
|
|
43
|
+
fs.mkdirSync(bin);
|
|
44
|
+
fs.mkdirSync(dir);
|
|
45
|
+
macShapedTools(bin);
|
|
46
|
+
fs.writeFileSync(path.join(dir, DUMP_NAME), DUMP);
|
|
47
|
+
fs.writeFileSync(path.join(tmp, 'cipher'), cipher);
|
|
48
|
+
fs.writeFileSync(path.join(bin, 'age'),
|
|
49
|
+
`#!/bin/bash\nwhile [ $# -gt 0 ]; do [ "$1" = -o ] && out="$2"; shift; done\ncp "${path.join(tmp, 'cipher')}" "$out"\n`, { mode: 0o755 });
|
|
50
|
+
fs.writeFileSync(path.join(bin, 'python3'),
|
|
51
|
+
`#!/bin/bash\n[ "$1" = -c ] && exit 0\ncat > /dev/null\necho "$${ENV}OFFSITE_KEY" >> "${uploads}"\n`, { mode: 0o755 });
|
|
52
|
+
const r = spawnSync('bash', [SCRIPT], {
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
env: {
|
|
55
|
+
...process.env,
|
|
56
|
+
PATH: `${bin}:${process.env.PATH}`,
|
|
57
|
+
[`${ENV}BACKUP_DB`]: 'smallproj',
|
|
58
|
+
[`${ENV}BACKUP_DIR`]: dir,
|
|
59
|
+
[`${ENV}SPACES_ENV`]: path.join(tmp, 'no-such-spaces.env'),
|
|
60
|
+
[`${ENV}OFFSITE_AGE_RECIPIENT`]: 'age1stubrecipient',
|
|
61
|
+
SPACES_KEY: 'k', SPACES_SECRET: 's', SPACES_BUCKET: 'b', SPACES_REGION: 'r', SPACES_ENDPOINT: 'https://example.invalid',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const sent = fs.existsSync(uploads) ? fs.readFileSync(uploads, 'utf8').split('\n').filter(Boolean) : [];
|
|
65
|
+
return { status: r.status, stdout: r.stdout, stderr: r.stderr, sent };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test('it sizes the dump and its ciphertext on macOS as on Linux, then ships it (task 1004170)', { skip }, () => {
|
|
69
|
+
const r = runOffsite();
|
|
70
|
+
assert.equal(r.status, 0, r.stderr);
|
|
71
|
+
assert.match(r.stdout, new RegExp(`\\(${Buffer.byteLength(DUMP)} bytes → ${Buffer.byteLength(CIPHER)} encrypted\\)`),
|
|
72
|
+
'both sizes are the real byte counts, unpadded');
|
|
73
|
+
assert.deepEqual(r.sent, [`gds-offsite/${DUMP_NAME}.age`]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('an empty ciphertext is refused, never uploaded as a backup', { skip }, () => {
|
|
77
|
+
const r = runOffsite({ cipher: '' });
|
|
78
|
+
assert.equal(r.status, 2, r.stderr);
|
|
79
|
+
assert.match(r.stderr, /encryption produced an empty file/);
|
|
80
|
+
assert.deepEqual(r.sent, [], 'nothing reached the bucket');
|
|
81
|
+
});
|
|
@@ -89,13 +89,16 @@ test('the range handed to the auditor is the exact deploy delta, not a window',
|
|
|
89
89
|
try {
|
|
90
90
|
const gdsDir = path.join(repo.dir, 'node_modules', '@bongos', 'core', 'scripts', 'gds');
|
|
91
91
|
fs.mkdirSync(gdsDir, { recursive: true });
|
|
92
|
+
// The range rides --range, not an env var (task 1001441): a shell cannot compute
|
|
93
|
+
// the instance's branded env prefix, and argv needs none.
|
|
92
94
|
fs.writeFileSync(path.join(gdsDir, 'main-audit.js'),
|
|
93
|
-
'
|
|
95
|
+
'const a = process.argv; const i = a.indexOf("--range");\n'
|
|
96
|
+
+ 'require("fs").writeFileSync("range.txt", i >= 0 ? a[i + 1] : "UNSET");\n');
|
|
94
97
|
const r = runGate(repo, { OTB_DEPLOY_PREV_SHA: repo.prev });
|
|
95
98
|
assert.equal(r.code, 0, r.out);
|
|
96
99
|
assert.equal(
|
|
97
100
|
fs.readFileSync(path.join(repo.dir, 'range.txt'), 'utf8'), `${repo.prev}..HEAD`,
|
|
98
|
-
'the auditor must receive <previously-deployed>..HEAD',
|
|
101
|
+
'the auditor must receive --range <previously-deployed>..HEAD',
|
|
99
102
|
);
|
|
100
103
|
} finally { fs.rmSync(repo.dir, { recursive: true, force: true }); }
|
|
101
104
|
});
|
|
@@ -37,7 +37,7 @@ const FLEET = [
|
|
|
37
37
|
{ id: '35', slug: 'unreadable-one', status: 'active', hosting_shape: 'cloud-host' },
|
|
38
38
|
];
|
|
39
39
|
|
|
40
|
-
function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus = 200, fleet = FLEET, fleetStatus = 200, instanceOver = null, moveTries = 4, postStatus = 200, scope = null } = {}) {
|
|
40
|
+
function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus = 200, fleet = FLEET, fleetStatus = 200, instanceOver = null, moveTries = 4, postStatus = 200, scope = null, href = null } = {}) {
|
|
41
41
|
const els = new Map();
|
|
42
42
|
const posts = [];
|
|
43
43
|
const gets = [];
|
|
@@ -94,7 +94,7 @@ function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus =
|
|
|
94
94
|
OTB: { escapeHtml: (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])), toast(m) { toasts.push(String(m)); } },
|
|
95
95
|
// `search` is what decides which project the page is about. A stub without it is the
|
|
96
96
|
// shape the page has to survive, and the default here keeps that path exercised.
|
|
97
|
-
location: { assign() {}, search: scope ? `?project=${encodeURIComponent(scope)}` : '' },
|
|
97
|
+
location: { assign() {}, search: scope ? `?project=${encodeURIComponent(scope)}` : '', ...(href ? { href } : {}) },
|
|
98
98
|
};
|
|
99
99
|
// The page's poll interval, driven rather than slept through. Injecting the REAL
|
|
100
100
|
// setTimeout made every case whose preview never converges sit out the page's full
|
|
@@ -583,3 +583,64 @@ test('a queue that is REFUSED leaves no in-flight state to wait on', async () =>
|
|
|
583
583
|
assert.ok(b.toasts.some((t) => /currently running/i.test(t)), 'and the refusal is reported');
|
|
584
584
|
assert.match(b.panel(), /id="dp-confirm-32"/, 'the button comes back so it can be retried');
|
|
585
585
|
});
|
|
586
|
+
|
|
587
|
+
// ---- landed, but GitHub was not updated (task 1004065) ----------------------------
|
|
588
|
+
//
|
|
589
|
+
// The hub pushes a hosted project's pin to its GitHub repo after the upgrade. When it
|
|
590
|
+
// cannot, the move is still DONE (the site runs the new core) but the page must not
|
|
591
|
+
// read "and the box confirmed it" as though everything was saved — the box and the
|
|
592
|
+
// repo have split, and changes pushed to GitHub can stop reaching the live site.
|
|
593
|
+
|
|
594
|
+
const landedWith = (warning) => () => ({
|
|
595
|
+
served_version: '1.19.871', recommended_version: '1.19.871', behind: false, open_intent: null,
|
|
596
|
+
last_move: { target_version: '1.19.871', state: 'done', at: new Date(Date.now() - 60 * 1000).toISOString(), error: null, warning },
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test('a move that landed but did NOT reach GitHub says so, instead of a clean confirmation', async () => {
|
|
600
|
+
const b = boot({ scope: 'mercury', instanceOver: landedWith({ code: 'no_access', text: 'The hub had no permission to write to the project’s GitHub repository.' }) });
|
|
601
|
+
await b.settle();
|
|
602
|
+
const html = b.panel();
|
|
603
|
+
assert.match(html, /Now running core 1\.19\.871/, 'the upgrade itself is still stated — it worked');
|
|
604
|
+
assert.match(html, /GitHub was not updated/i, 'and the half that did not happen is the headline beside it');
|
|
605
|
+
assert.match(html, /no permission to write/, 'with the runner’s reason in plain words');
|
|
606
|
+
assert.match(html, /dp-settled--warn/, 'a warning mark — not the success mark, not the failure one');
|
|
607
|
+
assert.doesNotMatch(html, /dp-settled--ok/);
|
|
608
|
+
assert.doesNotMatch(html, /dp-settled--failed/);
|
|
609
|
+
assert.doesNotMatch(html, /did not land/i, 'it DID land');
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test('when the fix is reconnecting GitHub, the page links straight to it and comes back here', async () => {
|
|
613
|
+
for (const code of ['no_access', 'refused']) {
|
|
614
|
+
const b = boot({ scope: 'mercury', instanceOver: landedWith({ code, text: 'x' }) });
|
|
615
|
+
await b.settle();
|
|
616
|
+
const html = b.panel();
|
|
617
|
+
assert.match(html, /href="\/api\/bongos\/auth\/web\/start\?repos=1&return=%2Fdeploy%3Fproject%3Dmercury"/,
|
|
618
|
+
`${code}: the repo-access sign-in, returning to this project's deploy page — got ${html}`);
|
|
619
|
+
assert.match(html, /within the hour/i, `${code}: and it says the access is short-lived, rather than promising a fix`);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test('when reconnecting cannot help, no reconnect link is offered', async () => {
|
|
624
|
+
for (const code of ['behind', 'no_repo', 'failed', 'no_branch']) {
|
|
625
|
+
const b = boot({ scope: 'mercury', instanceOver: landedWith({ code, text: 'x' }) });
|
|
626
|
+
await b.settle();
|
|
627
|
+
assert.doesNotMatch(b.panel(), /auth\/web\/start/, `${code}: a sign-in does not fix this`);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test('a clean move still reads as the plain confirmation', async () => {
|
|
632
|
+
const b = boot({ scope: 'mercury', instanceOver: landedWith(null) });
|
|
633
|
+
await b.settle();
|
|
634
|
+
const html = b.panel();
|
|
635
|
+
assert.match(html, /dp-settled--ok/);
|
|
636
|
+
assert.doesNotMatch(html, /GitHub was not updated/i);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test('the reconnect link returns to the page\u2019s full https address when there is one', async () => {
|
|
640
|
+
// A relative return resolves against the sign-in callback's host, which is not the
|
|
641
|
+
// hall's. The absolute address is admitted by the same allowlist and lands back here.
|
|
642
|
+
const here = 'https://builders.example.com/deploy?project=mercury';
|
|
643
|
+
const b = boot({ scope: 'mercury', href: here, instanceOver: landedWith({ code: 'no_access', text: 'x' }) });
|
|
644
|
+
await b.settle();
|
|
645
|
+
assert.ok(b.panel().includes(`return=${encodeURIComponent(here)}`), b.panel());
|
|
646
|
+
});
|
package/tests/dev_lib.mjs
CHANGED
|
@@ -86,7 +86,8 @@ t('resolveRunParams: port from branding, db from repo dir, TCP localhost, gdsOnl
|
|
|
86
86
|
'config/branding.neutral.json': NEUTRAL,
|
|
87
87
|
'config/modules.json': JSON.stringify({ modules: { game: false } }),
|
|
88
88
|
});
|
|
89
|
-
|
|
89
|
+
// platform pinned: on Windows the default role is `postgres` (tests/dev_windows.mjs).
|
|
90
|
+
const p = lib.resolveRunParams({ instanceRoot: '/Users/x/test4', coreRoot: '/core', f, env: {}, userInfo: () => ({ username: 'lars' }), argv: [], platform: 'darwin' });
|
|
90
91
|
assert.equal(p.port, 3006);
|
|
91
92
|
assert.equal(p.dbName, 'test4');
|
|
92
93
|
assert.equal(p.pgHost, 'localhost'); // NOT the /var/run/postgresql linux socket
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// tests/dev_windows.mjs — `bongos dev` on Windows (task 1002707), proven on any OS.
|
|
2
|
+
//
|
|
3
|
+
// Two layers:
|
|
4
|
+
// 1. dev-lib's WINDOWS decisions, called with platform:'win32' and injected
|
|
5
|
+
// env / fs / where, so every Windows path is exercised on a Mac or Linux CI box.
|
|
6
|
+
// 2. dev.js's wiring of those decisions (npm install, preflight, migrate, createdb),
|
|
7
|
+
// driven with process.platform STUBBED to 'win32' and child_process.spawnSync +
|
|
8
|
+
// fs.existsSync/readdirSync STUBBED. Nothing real is spawned, installed or read.
|
|
9
|
+
//
|
|
10
|
+
// Each case fails on the pre-fix code: a plain `npm` spawn with no shell, a WSL
|
|
11
|
+
// bash.exe accepted for migrations, a backslash migrate.sh path, the Windows login
|
|
12
|
+
// name as the Postgres role, and Homebrew/apt-get help printed to a Windows owner.
|
|
13
|
+
//
|
|
14
|
+
// Run: node tests/dev_windows.mjs
|
|
15
|
+
|
|
16
|
+
import { strict as assert } from 'node:assert';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const cp = require('node:child_process');
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const lib = require('../scripts/gds/dev-lib.js');
|
|
23
|
+
const devCli = require('../scripts/gds/dev.js');
|
|
24
|
+
|
|
25
|
+
let passed = 0, failed = 0;
|
|
26
|
+
function t(name, fn) {
|
|
27
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
28
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.stack || e.message}`); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const PF = 'C:\\Program Files';
|
|
32
|
+
const GIT_BASH = 'C:\\Program Files\\Git\\bin\\bash.exe';
|
|
33
|
+
const WSL_BASH = 'C:\\Windows\\System32\\bash.exe';
|
|
34
|
+
const PG16_BIN = 'C:\\Program Files\\PostgreSQL\\16\\bin';
|
|
35
|
+
const lower = (s) => String(s).toLowerCase();
|
|
36
|
+
// A fake existsSync over an exact (case-insensitive, Windows-style) path set.
|
|
37
|
+
const existsIn = (...paths) => (p) => paths.map(lower).includes(lower(p));
|
|
38
|
+
|
|
39
|
+
// ======================= 1. dev-lib, platform:'win32' =======================
|
|
40
|
+
|
|
41
|
+
t('npmInvocation: Windows runs npm-cli.js through node, no shell', () => {
|
|
42
|
+
const inv = lib.npmInvocation(['ci'], {
|
|
43
|
+
platform: 'win32', execPath: 'C:\\Program Files\\nodejs\\node.exe',
|
|
44
|
+
exists: existsIn('C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js'),
|
|
45
|
+
});
|
|
46
|
+
assert.equal(inv.cmd, 'C:\\Program Files\\nodejs\\node.exe');
|
|
47
|
+
assert.deepEqual(inv.args, ['C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js', 'ci']);
|
|
48
|
+
assert.equal(inv.shell, false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
t('npmInvocation: Windows without npm-cli.js falls back to the .cmd shim through a shell, as one string', () => {
|
|
52
|
+
const inv = lib.npmInvocation(['install'], { platform: 'win32', execPath: 'C:\\n\\node.exe', exists: () => false });
|
|
53
|
+
assert.equal(inv.shell, true);
|
|
54
|
+
assert.equal(inv.cmd, 'npm install');
|
|
55
|
+
assert.deepEqual(inv.args, []); // no args array beside shell:true (DEP0190)
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
t('npmInvocation: Mac/Linux keep the plain spawn', () => {
|
|
59
|
+
for (const platform of ['darwin', 'linux']) {
|
|
60
|
+
assert.deepEqual(lib.npmInvocation(['ci'], { platform, exists: () => true }), { cmd: 'npm', args: ['ci'], shell: false });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
t('isWslBashLauncher: System32 / Sysnative / WindowsApps bash are WSL, Git Bash is not', () => {
|
|
65
|
+
assert.equal(lib.isWslBashLauncher(WSL_BASH), true);
|
|
66
|
+
assert.equal(lib.isWslBashLauncher('C:\\WINDOWS\\Sysnative\\bash.exe'), true);
|
|
67
|
+
assert.equal(lib.isWslBashLauncher('C:\\Users\\a\\AppData\\Local\\Microsoft\\WindowsApps\\bash.exe'), true);
|
|
68
|
+
assert.equal(lib.isWslBashLauncher(GIT_BASH), false);
|
|
69
|
+
assert.equal(lib.isWslBashLauncher('C:\\Program Files\\Git\\usr\\bin\\bash.exe'), false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
t('resolveBash: Windows finds Git Bash in Program Files and never picks WSL’s bash', () => {
|
|
73
|
+
const r = lib.resolveBash({
|
|
74
|
+
platform: 'win32', env: { ProgramFiles: PF },
|
|
75
|
+
whereAll: (b) => (b === 'bash' ? [WSL_BASH] : []),
|
|
76
|
+
exists: existsIn(GIT_BASH, WSL_BASH),
|
|
77
|
+
});
|
|
78
|
+
assert.equal(r.ok, true);
|
|
79
|
+
assert.equal(r.bin, GIT_BASH);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
t('resolveBash: Windows prefers the Git Bash this terminal runs in (EXEPATH)', () => {
|
|
83
|
+
const own = 'D:\\tools\\Git\\bin\\bash.exe';
|
|
84
|
+
const r = lib.resolveBash({ platform: 'win32', env: { EXEPATH: 'D:\\tools\\Git\\bin', ProgramFiles: PF }, exists: existsIn(own, GIT_BASH) });
|
|
85
|
+
assert.equal(r.bin, own);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
t('resolveBash: Windows finds Git Bash beside the git on PATH (custom install folder)', () => {
|
|
89
|
+
const custom = 'E:\\Apps\\Git\\bin\\bash.exe';
|
|
90
|
+
const r = lib.resolveBash({
|
|
91
|
+
platform: 'win32', env: {},
|
|
92
|
+
whereAll: (b) => (b === 'git' ? ['E:\\Apps\\Git\\cmd\\git.exe'] : []),
|
|
93
|
+
exists: existsIn(custom),
|
|
94
|
+
});
|
|
95
|
+
assert.equal(r.ok, true);
|
|
96
|
+
assert.equal(r.bin, custom);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
t('resolveBash: Windows env names are read case-insensitively (PROGRAMFILES)', () => {
|
|
100
|
+
const r = lib.resolveBash({ platform: 'win32', env: { PROGRAMFILES: PF }, exists: existsIn(GIT_BASH) });
|
|
101
|
+
assert.equal(r.bin, GIT_BASH);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
t('resolveBash: a WSL-only machine is refused and says so', () => {
|
|
105
|
+
const r = lib.resolveBash({ platform: 'win32', env: { ProgramFiles: PF }, whereAll: (b) => (b === 'bash' ? [WSL_BASH] : []), exists: existsIn(WSL_BASH) });
|
|
106
|
+
assert.equal(r.ok, false);
|
|
107
|
+
assert.equal(r.wslOnly, true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
t('resolveBash: Mac/Linux still use `bash` from PATH', () => {
|
|
111
|
+
assert.deepEqual(lib.resolveBash({ platform: 'linux', has: (b) => b === 'bash' }), { ok: true, bin: 'bash', source: 'PATH' });
|
|
112
|
+
assert.equal(lib.resolveBash({ platform: 'darwin', has: () => false }).ok, false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
t('bashScriptPath: Windows gets forward slashes (bash dirname splits on / only)', () => {
|
|
116
|
+
assert.equal(lib.bashScriptPath('C:\\x\\core\\scripts\\migrate.sh', 'win32'), 'C:/x/core/scripts/migrate.sh');
|
|
117
|
+
assert.equal(lib.bashScriptPath('/x/core/scripts/migrate.sh', 'linux'), '/x/core/scripts/migrate.sh');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
t('windowsPgInstalls: finds installer folders newest-first, deduped, psql.exe required', () => {
|
|
121
|
+
const dirs = { [lower(`${PF}\\PostgreSQL`)]: ['15', '16', 'data-backup', '17'] };
|
|
122
|
+
const installs = lib.windowsPgInstalls({
|
|
123
|
+
env: { ProgramFiles: PF, ProgramW6432: PF }, // the same folder twice on 64-bit Windows
|
|
124
|
+
readdir: (d) => { const v = dirs[lower(d)]; if (!v) throw new Error('ENOENT'); return v; },
|
|
125
|
+
exists: existsIn(`${PF}\\PostgreSQL\\15\\bin\\psql.exe`, `${PF}\\PostgreSQL\\16\\bin\\psql.exe`), // 17 has no psql.exe
|
|
126
|
+
});
|
|
127
|
+
assert.deepEqual(installs.map((i) => i.bin), [PG16_BIN, `${PF}\\PostgreSQL\\15\\bin`]);
|
|
128
|
+
assert.equal(installs[0].service, 'postgresql-x64-16');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
t('pgStartPlan: Windows tries each installed version’s Windows service', () => {
|
|
132
|
+
const plan = lib.pgStartPlan({ platform: 'win32', env: {}, pgInstalls: [{ service: 'postgresql-x64-16' }, { service: 'postgresql-x64-15' }] });
|
|
133
|
+
assert.deepEqual(plan.map((s) => [s.cmd, ...s.args]), [['net', 'start', 'postgresql-x64-16'], ['net', 'start', 'postgresql-x64-15']]);
|
|
134
|
+
assert.deepEqual(lib.pgStartPlan({ platform: 'win32', env: {} }), []);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
t('pgClientArgs: Windows adds -w (never prompt) except for pg_isready', () => {
|
|
138
|
+
const params = { pgHost: 'localhost', pgUser: 'postgres' };
|
|
139
|
+
assert.deepEqual(lib.pgClientArgs(params, { platform: 'win32' }), ['-h', 'localhost', '-U', 'postgres', '-w']);
|
|
140
|
+
assert.deepEqual(lib.pgClientArgs(params, { platform: 'win32', forReady: true }), ['-h', 'localhost', '-U', 'postgres']);
|
|
141
|
+
assert.deepEqual(lib.pgClientArgs(params, { platform: 'darwin' }), ['-h', 'localhost', '-U', 'postgres']);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
t('resolveRunParams: Windows defaults the Postgres role to `postgres`; PGUSER still wins', () => {
|
|
145
|
+
const f = { readFileSync: () => { throw new Error('ENOENT'); } };
|
|
146
|
+
const base = { instanceRoot: 'C:\\Users\\Lars\\proj', coreRoot: 'C:\\core', f, userInfo: () => ({ username: 'Lars' }), argv: [] };
|
|
147
|
+
assert.equal(lib.resolveRunParams({ ...base, env: {}, platform: 'win32' }).pgUser, 'postgres');
|
|
148
|
+
assert.equal(lib.resolveRunParams({ ...base, env: { PGUSER: 'me' }, platform: 'win32' }).pgUser, 'me');
|
|
149
|
+
assert.equal(lib.resolveRunParams({ ...base, env: {}, platform: 'darwin' }).pgUser, 'Lars');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
t('pgInstallHint / pgStartHint: Windows steps for Windows, unchanged elsewhere', () => {
|
|
153
|
+
const win = lib.pgInstallHint('win32').join('\n');
|
|
154
|
+
assert.match(win, /postgresql\.org\/download\/windows/);
|
|
155
|
+
assert.match(win, /PGPASSWORD/);
|
|
156
|
+
assert.match(win, /self-host\.md/); // Docker named as the fallback
|
|
157
|
+
assert.doesNotMatch(win, /brew|apt-get/);
|
|
158
|
+
assert.match(lib.pgInstallHint('darwin').join('\n'), /brew install postgresql@16/);
|
|
159
|
+
assert.match(lib.pgInstallHint('linux').join('\n'), /apt-get/);
|
|
160
|
+
const start = lib.pgStartHint('win32', { pgInstalls: [{ service: 'postgresql-x64-17' }] }).join('\n');
|
|
161
|
+
assert.match(start, /services\.msc/);
|
|
162
|
+
assert.match(start, /Start-Service postgresql-x64-17/);
|
|
163
|
+
assert.match(lib.pgStartHint('linux').join('\n'), /sudo service postgresql start/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
t('pgAuthHint: only on Windows, only for an authentication refusal', () => {
|
|
167
|
+
assert.match(lib.pgAuthHint('win32', 'fe_sendauth: no password supplied').join('\n'), /PGPASSWORD/);
|
|
168
|
+
assert.match(lib.pgAuthHint('win32', 'password authentication failed for user "postgres"').join('\n'), /"postgres"/);
|
|
169
|
+
assert.deepEqual(lib.pgAuthHint('win32', 'disk full'), []);
|
|
170
|
+
assert.deepEqual(lib.pgAuthHint('linux', 'no password supplied'), []);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
t('bashMissingHint: Git for Windows first, Docker as the fallback, WSL explained', () => {
|
|
174
|
+
const h = lib.bashMissingHint('win32', { wslOnly: true }).join('\n');
|
|
175
|
+
assert.match(h, /git-scm\.com\/download\/win/);
|
|
176
|
+
assert.match(h, /self-host\.md/);
|
|
177
|
+
assert.match(h, /WSL/);
|
|
178
|
+
assert.doesNotMatch(lib.bashMissingHint('win32').join('\n'), /WSL/);
|
|
179
|
+
assert.deepEqual(lib.bashMissingHint('darwin'), []);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ============ 2. dev.js wiring, process.platform + spawnSync stubbed ============
|
|
183
|
+
|
|
184
|
+
// Run fn() as if on Windows: process.platform, spawnSync, existsSync, readdirSync
|
|
185
|
+
// and the env keys the lookups read are all swapped, then restored however fn ends.
|
|
186
|
+
// `spawn(cmd, args, opts)` answers each spawnSync; every call is recorded.
|
|
187
|
+
function onWindows({ spawn, exists = () => false, readdir = null, env = {} }, fn) {
|
|
188
|
+
const realPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
189
|
+
const real = { spawnSync: cp.spawnSync, existsSync: fs.existsSync, readdirSync: fs.readdirSync, log: console.log, error: console.error };
|
|
190
|
+
const envKeys = ['PATH', 'EXEPATH', 'ProgramFiles', 'ProgramW6432', 'ProgramFiles(x86)', 'LOCALAPPDATA', ...Object.keys(env)];
|
|
191
|
+
const savedEnv = Object.fromEntries(envKeys.map((k) => [k, process.env[k]]));
|
|
192
|
+
const calls = [];
|
|
193
|
+
const out = [];
|
|
194
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
195
|
+
for (const k of envKeys) if (k !== 'PATH') delete process.env[k];
|
|
196
|
+
Object.assign(process.env, env);
|
|
197
|
+
cp.spawnSync = (cmd, args = [], opts = {}) => {
|
|
198
|
+
calls.push({ cmd, args, opts });
|
|
199
|
+
const r = spawn(cmd, args, opts) || {};
|
|
200
|
+
return { status: 0, stdout: '', stderr: '', ...r };
|
|
201
|
+
};
|
|
202
|
+
fs.existsSync = (p) => exists(String(p));
|
|
203
|
+
if (readdir) fs.readdirSync = (d) => readdir(String(d));
|
|
204
|
+
console.log = (...a) => out.push(a.join(' '));
|
|
205
|
+
console.error = (...a) => out.push(a.join(' '));
|
|
206
|
+
try {
|
|
207
|
+
return { value: fn(), calls, out: out.join('\n') };
|
|
208
|
+
} finally {
|
|
209
|
+
Object.defineProperty(process, 'platform', realPlatform);
|
|
210
|
+
cp.spawnSync = real.spawnSync; fs.existsSync = real.existsSync; fs.readdirSync = real.readdirSync;
|
|
211
|
+
console.log = real.log; console.error = real.error;
|
|
212
|
+
for (const [k, v] of Object.entries(savedEnv)) { if (v === undefined) delete process.env[k]; else process.env[k] = v; }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// `where <bin>` answers from a { bin: [paths] } map; everything else exits 0.
|
|
217
|
+
const whereFrom = (onPath) => (cmd, args) => {
|
|
218
|
+
if (cmd !== 'where') return {};
|
|
219
|
+
const hits = typeof onPath === 'function' ? onPath(args[0]) : onPath[args[0]];
|
|
220
|
+
return hits && hits.length ? { status: 0, stdout: hits.join('\r\n') + '\r\n' } : { status: 1 };
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const INST = { instanceRoot: '/x/inst' };
|
|
224
|
+
|
|
225
|
+
t('dev.js installDeps on Windows: npm runs through node + npm-cli.js, not a bare `npm` spawn', () => {
|
|
226
|
+
const { value, calls } = onWindows({
|
|
227
|
+
spawn: whereFrom({ npm: ['C:\\Program Files\\nodejs\\npm.cmd'] }),
|
|
228
|
+
exists: (p) => p.endsWith('package-lock.json') || p.endsWith('npm-cli.js'),
|
|
229
|
+
}, () => devCli.installDeps(INST));
|
|
230
|
+
assert.equal(value, true);
|
|
231
|
+
const npmRun = calls.find((c) => c.cmd !== 'where');
|
|
232
|
+
assert.equal(npmRun.cmd, process.execPath);
|
|
233
|
+
assert.match(npmRun.args[0], /npm-cli\.js$/);
|
|
234
|
+
assert.equal(npmRun.args[1], 'ci');
|
|
235
|
+
assert.ok(!npmRun.opts.shell, 'no shell needed when npm-cli.js is used');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
t('dev.js installDeps on Windows without npm-cli.js: the npm.cmd shim goes through a shell', () => {
|
|
239
|
+
const { calls } = onWindows({
|
|
240
|
+
spawn: whereFrom({ npm: ['C:\\Program Files\\nodejs\\npm.cmd'] }),
|
|
241
|
+
exists: (p) => p.endsWith('package-lock.json'),
|
|
242
|
+
}, () => devCli.installDeps(INST));
|
|
243
|
+
const npmRun = calls.find((c) => c.cmd !== 'where');
|
|
244
|
+
assert.equal(npmRun.cmd, 'npm ci');
|
|
245
|
+
assert.equal(npmRun.opts.shell, true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
t('dev.js preflight on Windows: finds installed Postgres off PATH and Git Bash over WSL bash', () => {
|
|
249
|
+
const psqlVisible = () => lower(process.env.PATH || '').includes(lower(PG16_BIN));
|
|
250
|
+
const { value, out } = onWindows({
|
|
251
|
+
env: { ProgramFiles: PF },
|
|
252
|
+
spawn: whereFrom((bin) => {
|
|
253
|
+
if (['psql', 'createdb', 'pg_isready'].includes(bin)) return psqlVisible() ? [`${PG16_BIN}\\${bin}.exe`] : [];
|
|
254
|
+
if (bin === 'bash') return [WSL_BASH];
|
|
255
|
+
return [];
|
|
256
|
+
}),
|
|
257
|
+
readdir: (d) => { if (lower(d) === lower(`${PF}\\PostgreSQL`)) return ['16']; throw new Error('ENOENT'); },
|
|
258
|
+
exists: (p) => [lower(`${PG16_BIN}\\psql.exe`), lower(GIT_BASH), lower(WSL_BASH)].includes(lower(p)),
|
|
259
|
+
}, () => {
|
|
260
|
+
const r = devCli.preflight({});
|
|
261
|
+
return { ...r, pathHadPg: psqlVisible() };
|
|
262
|
+
});
|
|
263
|
+
assert.equal(value.ok, true, out);
|
|
264
|
+
assert.equal(value.pathHadPg, true, 'the installer’s bin folder was put on PATH');
|
|
265
|
+
assert.equal(value.bashBin, GIT_BASH);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
t('dev.js preflight on Windows with no Postgres: Windows install steps, not Homebrew/apt-get', () => {
|
|
269
|
+
const { value, out } = onWindows({ env: { ProgramFiles: PF }, spawn: whereFrom({}), readdir: () => { throw new Error('ENOENT'); } },
|
|
270
|
+
() => devCli.preflight({}));
|
|
271
|
+
assert.equal(value.ok, false);
|
|
272
|
+
assert.match(out, /postgresql\.org\/download\/windows/);
|
|
273
|
+
assert.doesNotMatch(out, /brew install|apt-get/);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
t('dev.js preflight on Windows with only WSL bash: refuses, names Git for Windows and Docker', () => {
|
|
277
|
+
const { value, out } = onWindows({
|
|
278
|
+
env: { ProgramFiles: PF },
|
|
279
|
+
spawn: whereFrom({ psql: ['p'], createdb: ['c'], pg_isready: ['r'], bash: [WSL_BASH] }),
|
|
280
|
+
exists: (p) => lower(p) === lower(WSL_BASH),
|
|
281
|
+
}, () => devCli.preflight({}));
|
|
282
|
+
assert.equal(value.ok, false);
|
|
283
|
+
assert.match(out, /git-scm\.com\/download\/win/);
|
|
284
|
+
assert.match(out, /self-host\.md/);
|
|
285
|
+
assert.match(out, /WSL/);
|
|
286
|
+
assert.doesNotMatch(out, /use WSL/); // WSL is no longer the recommended path
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
t('dev.js migrate on Windows: Git Bash gets a forward-slash migrate.sh path', () => {
|
|
290
|
+
const params = { coreRoot: 'C:\\core', instanceRoot: 'C:\\inst', pgHost: 'localhost', pgUser: 'postgres', dbName: 'inst', envPrefix: 'CLOUDBONGOS', gdsOnly: true };
|
|
291
|
+
const { value, calls } = onWindows({ spawn: () => ({ status: 0, stdout: 'apply core_001_x.sql\n' }) },
|
|
292
|
+
() => devCli.migrate(params, GIT_BASH));
|
|
293
|
+
assert.equal(value, true);
|
|
294
|
+
assert.equal(calls[0].cmd, GIT_BASH);
|
|
295
|
+
assert.equal(calls[0].args[0], 'C:/core/scripts/migrate.sh');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
t('dev.js createDb on Windows: psql/createdb never prompt (-w) and a refusal explains PGPASSWORD', () => {
|
|
299
|
+
const params = { pgHost: 'localhost', pgUser: 'postgres', dbName: 'inst' };
|
|
300
|
+
const { value, calls, out } = onWindows({
|
|
301
|
+
spawn: (cmd) => (cmd === 'createdb'
|
|
302
|
+
? { status: 1, stderr: 'createdb: error: connection to server failed: fe_sendauth: no password supplied' }
|
|
303
|
+
: { status: 2, stderr: 'fe_sendauth: no password supplied' }),
|
|
304
|
+
}, () => devCli.createDb(params));
|
|
305
|
+
assert.equal(value, false);
|
|
306
|
+
for (const c of calls.filter((x) => x.cmd === 'psql' || x.cmd === 'createdb')) assert.ok(c.args.includes('-w'), `${c.cmd} got -w`);
|
|
307
|
+
assert.match(out, /PGPASSWORD/);
|
|
308
|
+
assert.match(out, /"postgres"/);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
t('dev.js usage: Windows is supported (Git Bash), Docker is the fallback, WSL is not the answer', () => {
|
|
312
|
+
const lines = [];
|
|
313
|
+
const realLog = console.log;
|
|
314
|
+
console.log = (...a) => lines.push(a.join(' '));
|
|
315
|
+
try { devCli.usage(); } finally { console.log = realLog; }
|
|
316
|
+
const u = lines.join('\n');
|
|
317
|
+
assert.match(u, /Windows/);
|
|
318
|
+
assert.match(u, /Git Bash/);
|
|
319
|
+
assert.match(u, /self-host\.md/);
|
|
320
|
+
assert.doesNotMatch(u, /use WSL/);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
324
|
+
process.exit(failed > 0 ? 1 : 0);
|