@bongos/core 1.19.1033 → 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 +186 -86
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/grade-recover/SKILL.md +2 -1
- package/.claude/skills/grader-health/SKILL.md +4 -0
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
- package/docs/adr/README.md +1 -1
- package/docs/api/openapi.json +1 -1
- package/docs/architecture.md +3 -2
- package/docs/canonical-permissions.md +2 -2
- package/docs/copy-inventory.md +34 -30
- package/docs/copy-registry.json +69 -33
- package/docs/module-api-changelog.md +3 -1
- package/docs/recipes/local-dev.md +45 -3
- package/docs/recipes/packaging-the-core.md +1 -1
- package/modules/autonomy/cadence.js +52 -0
- 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/lifecycle/routes/tasks.js +19 -4
- 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 +70 -0
- package/scripts/gds/autobongos-run.js +10 -2
- package/scripts/gds/backfill-cost.js +20 -9
- package/scripts/gds/claim.js +2 -2
- package/scripts/gds/credential-presence.js +281 -0
- 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/plain-cards.js +4 -0
- package/scripts/gds/preflight.js +12 -4
- package/scripts/gds/provision-core-upgrade.js +155 -4
- 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/run-routine.js +18 -5
- package/scripts/gds/run-unit-tests.js +14 -3
- package/scripts/gds/session-cost.js +12 -3
- package/scripts/gds/ship-deploy-target.js +11 -6
- package/scripts/gds/ship-grade-steps.js +33 -0
- package/scripts/gds/ship-land.js +157 -11
- package/scripts/gds/ship-merge.js +3 -2
- package/scripts/gds/ship-preflight-steps.js +30 -8
- package/scripts/gds/ship.js +1 -1
- package/scripts/gds/upgrade.js +3 -3
- 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/autobongos_cadence.mjs +74 -0
- package/tests/consumer_layout_boot.mjs +363 -31
- package/tests/core_upgrade_door.mjs +40 -6
- package/tests/core_upgrade_runner.mjs +240 -4
- package/tests/credential_presence.mjs +153 -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/integration_parked_ratchet.mjs +24 -1
- package/tests/provisioning_public_refusal.mjs +195 -0
- package/tests/publish_status_branch.mjs +128 -0
- package/tests/ship_land_red_pr.mjs +236 -0
- package/tests/ship_preflight.mjs +67 -1
- package/tests/start_env_guard.mjs +16 -0
- package/tests/upgrade.mjs +20 -1
|
@@ -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);
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// tests/env_knobs_neutral_prefix.mjs — the operational env knobs answer to the
|
|
2
|
+
// instance's own prefix, not only to OTB_ (task 1001441, the R61 follow-up).
|
|
3
|
+
//
|
|
4
|
+
// R61 (task 1200) put the config dir and the auth/API-base env vars behind
|
|
5
|
+
// src/instance-config.js resolveEnv(), and task 1003703 made BONGOS the canonical
|
|
6
|
+
// prefix. The operational knobs below still read `process.env.OTB_*` directly, so a
|
|
7
|
+
// fresh instance setting BONGOS_DEPLOY_MODE (or any knob here) was silently ignored.
|
|
8
|
+
// Each test sets ONLY the canonical `<PREFIX>_<KNOB>` spelling — the prefix is
|
|
9
|
+
// read from the branding contract, never assumed — and asserts the reader obeys
|
|
10
|
+
// it. Every one of them fails against the direct OTB_ reads this task replaced.
|
|
11
|
+
// The legacy OTB_ spellings keep working through resolveEnv's warned fallback;
|
|
12
|
+
// the existing tests that set them (pre_push_hook, preflight_checks,
|
|
13
|
+
// push_path_brief, ship_broadcast_server, autobongos_loop) are that proof.
|
|
14
|
+
//
|
|
15
|
+
// SCOPE, knob by knob (the task named ~17):
|
|
16
|
+
// converted here — DEPLOY_MODE, PUSH_VIA_SERVER, ALLOW_MAIN_PUSH (reader AND
|
|
17
|
+
// push-main.js's writer), CLAUDE_BIN (grader, preflight/doctor, the Autobongos
|
|
18
|
+
// worker and its env allowlist), COST_CRON_ROUTINE (run-routine.js's writer AND
|
|
19
|
+
// session-cost.js's reader), COST_LEDGER, GIT_SHA, MEMORY_DIR,
|
|
20
|
+
// ONBOARDING_WINDOW_DAYS, ROUTINE_ALERT_CHANNEL, and AUDIT_RANGE. AUDIT_RANGE's
|
|
21
|
+
// two writers are a bash deploy gate and a CI workflow, which cannot compute the
|
|
22
|
+
// branded prefix — so they now hand the range to main-audit.js as --range, and
|
|
23
|
+
// only an operator's own override goes through the env knob. Left as an env
|
|
24
|
+
// handoff, every deploy and CI audit would print a deprecation warning about a
|
|
25
|
+
// name no operator sets (pinned in tests/deploy_audit_gate.mjs).
|
|
26
|
+
// already done before this task — the ACTOR, PUBLIC_ORIGIN, SECURITY_ADR_PATH,
|
|
27
|
+
// STALE_CLAIM_HOURS and STATUS_DEFAULT_VERSION knobs, whose readers went through
|
|
28
|
+
// resolveEnv in task 1003703 and its predecessors (only their operator-facing
|
|
29
|
+
// text still named the old spelling, fixed here).
|
|
30
|
+
// gone — OTB_PREVIEW_STAMP has no reader or writer left anywhere.
|
|
31
|
+
//
|
|
32
|
+
// Run: node --test tests/env_knobs_neutral_prefix.mjs
|
|
33
|
+
|
|
34
|
+
import { test } from 'node:test';
|
|
35
|
+
import assert from 'node:assert/strict';
|
|
36
|
+
import { spawnSync } from 'node:child_process';
|
|
37
|
+
import { createRequire } from 'node:module';
|
|
38
|
+
import fs from 'node:fs';
|
|
39
|
+
import os from 'node:os';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
import { fileURLToPath } from 'node:url';
|
|
42
|
+
|
|
43
|
+
const require = createRequire(import.meta.url);
|
|
44
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
45
|
+
const { envPrefix, LEGACY_ENV_PREFIXES } = require('../src/instance-config.js');
|
|
46
|
+
|
|
47
|
+
const PREFIX = envPrefix();
|
|
48
|
+
const canonical = (knob) => `${PREFIX}_${knob}`;
|
|
49
|
+
|
|
50
|
+
// Every spelling of a knob, so a test can clear the ones it is not about — a
|
|
51
|
+
// developer's shell exporting OTB_DEPLOY_MODE must not decide these results.
|
|
52
|
+
const allSpellings = (knob) => [...new Set([PREFIX, ...LEGACY_ENV_PREFIXES])].map((p) => `${p}_${knob}`);
|
|
53
|
+
function envWith(knob, value, base = process.env) {
|
|
54
|
+
const env = { ...base };
|
|
55
|
+
for (const name of allSpellings(knob)) delete env[name];
|
|
56
|
+
env[canonical(knob)] = value;
|
|
57
|
+
return env;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Run a snippet in a fresh node, so module-level reads (and the per-process
|
|
61
|
+
// deprecation-warning ledger) start clean.
|
|
62
|
+
function runNode(code, env) {
|
|
63
|
+
const r = spawnSync(process.execPath, ['-e', code], { cwd: ROOT, env, encoding: 'utf8' });
|
|
64
|
+
assert.equal(r.status, 0, `child failed: ${r.stderr}`);
|
|
65
|
+
return r.stdout.trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A PATH with nothing on it: any `gh` / `claude` probe fails instantly and
|
|
69
|
+
// deterministically, so an answer can only have come from the env override.
|
|
70
|
+
const EMPTY_BIN = fs.mkdtempSync(path.join(os.tmpdir(), 'knobs-nobin-'));
|
|
71
|
+
test.after(() => fs.rmSync(EMPTY_BIN, { recursive: true, force: true }));
|
|
72
|
+
|
|
73
|
+
test('DEPLOY_MODE: <PREFIX>_DEPLOY_MODE pins the mode, with no gh probe', () => {
|
|
74
|
+
// 'laptop' from the env is honoured as-is. Ignored, the same run would probe gh
|
|
75
|
+
// (absent here), find no credential and fall back to 'ci'.
|
|
76
|
+
const out = runNode(
|
|
77
|
+
"console.log(require('./scripts/gds/ship-deploy-target.js').resolveDeployMode())",
|
|
78
|
+
{ ...envWith('DEPLOY_MODE', 'laptop'), PATH: EMPTY_BIN },
|
|
79
|
+
);
|
|
80
|
+
assert.equal(out.split('\n').pop(), 'laptop');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('PUSH_VIA_SERVER: <PREFIX>_PUSH_VIA_SERVER=0 forces the local push path', () => {
|
|
84
|
+
const out = runNode(
|
|
85
|
+
"console.log(require('./scripts/gds/ship-deploy-target.js').pushVia())",
|
|
86
|
+
{ ...envWith('PUSH_VIA_SERVER', '0'), PATH: EMPTY_BIN },
|
|
87
|
+
);
|
|
88
|
+
assert.equal(out, 'local', 'with no gh on PATH, only the override can answer local');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('ALLOW_MAIN_PUSH: push-main.js writes the canonical spelling, and the hook obeys it', () => {
|
|
92
|
+
const { pushEnv } = require('../scripts/gds/push-main.js');
|
|
93
|
+
const env = pushEnv({ PATH: process.env.PATH, HOME: process.env.HOME });
|
|
94
|
+
assert.equal(env[canonical('ALLOW_MAIN_PUSH')], '1', 'the writer uses this instance\'s own prefix');
|
|
95
|
+
|
|
96
|
+
const hook = path.join(ROOT, 'scripts', 'gds', 'hooks', 'pre-push.js');
|
|
97
|
+
const sha = 'a'.repeat(40);
|
|
98
|
+
const push = (e) => spawnSync(process.execPath, [hook, 'origin', 'https://example.invalid/repo.git'], {
|
|
99
|
+
cwd: ROOT, input: `refs/heads/main ${sha} refs/heads/main ${sha}\n`, encoding: 'utf8', env: e,
|
|
100
|
+
});
|
|
101
|
+
const cleared = { ...process.env };
|
|
102
|
+
for (const name of allSpellings('ALLOW_MAIN_PUSH')) delete cleared[name];
|
|
103
|
+
assert.equal(push(cleared).status, 13, 'without the override a main push is blocked (exit 13)');
|
|
104
|
+
assert.equal(push({ ...cleared, ...pushEnv({}) }).status, 0, 'the canonical override lets the pipeline push');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('CLAUDE_BIN: the preflight probe, the Autobongos worker and its env allowlist', () => {
|
|
108
|
+
const { preflightChecks } = require('../scripts/gds/preflight.js');
|
|
109
|
+
let seen = null;
|
|
110
|
+
const spy = (bin) => { seen = bin; return { status: 0, stdout: 'x\n' }; };
|
|
111
|
+
const fsOk = { existsSync: () => true };
|
|
112
|
+
const r = preflightChecks({ nodeVersion: 'v22.0.0', fs: fsOk, spawn: spy, env: { [canonical('CLAUDE_BIN')]: '/opt/claude' } });
|
|
113
|
+
assert.equal(seen, '/opt/claude');
|
|
114
|
+
const row = r.find((x) => x.title === 'claude CLI');
|
|
115
|
+
assert.match(row.detail, new RegExp(`via ${canonical('CLAUDE_BIN')}`), 'the label names the spelling that supplied it');
|
|
116
|
+
|
|
117
|
+
const runner = require('../scripts/gds/autobongos-run.js');
|
|
118
|
+
assert.equal(runner.resolveWorkerBin({ [canonical('CLAUDE_BIN')]: '/x/claude' }, 'linux').bin, '/x/claude');
|
|
119
|
+
const wenv = runner.workerEnv({ [canonical('CLAUDE_BIN')]: '/x/claude', DATABASE_URL: 'postgres://secret' });
|
|
120
|
+
assert.equal(wenv[canonical('CLAUDE_BIN')], '/x/claude', 'the worker receives the canonical spelling');
|
|
121
|
+
assert.equal(wenv.DATABASE_URL, undefined, 'the allowlist still refuses everything else');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('CLAUDE_BIN: the grader spawns the binary <PREFIX>_CLAUDE_BIN names', async () => {
|
|
125
|
+
const { runSubagent } = require('../modules/grading/grader-subagent.js');
|
|
126
|
+
const probe = path.join(EMPTY_BIN, 'claude-probe-does-not-exist');
|
|
127
|
+
const saved = { ...process.env };
|
|
128
|
+
try {
|
|
129
|
+
for (const name of [...allSpellings('CLAUDE_BIN'), 'CLAUDE_BIN']) delete process.env[name];
|
|
130
|
+
process.env[canonical('CLAUDE_BIN')] = probe;
|
|
131
|
+
// An empty PATH as well: if the override were ignored, a bare `claude` must fail
|
|
132
|
+
// to resolve rather than launch a real, paid session.
|
|
133
|
+
process.env.PATH = EMPTY_BIN;
|
|
134
|
+
await assert.rejects(runSubagent({ prompt: 'x', opts: { timeoutMs: 5000 } }), (err) => {
|
|
135
|
+
assert.equal(err.code, 'SUBAGENT_SPAWN_FAILED');
|
|
136
|
+
assert.ok(err.message.includes(probe), `spawned the override, not a PATH lookup: ${err.message}`);
|
|
137
|
+
return true;
|
|
138
|
+
});
|
|
139
|
+
} finally {
|
|
140
|
+
for (const k of Object.keys(process.env)) if (!(k in saved)) delete process.env[k];
|
|
141
|
+
Object.assign(process.env, saved);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('COST_CRON_ROUTINE: run-routine.js writes the canonical name and session-cost.js reads it', () => {
|
|
146
|
+
const { stepEnv } = require('../scripts/gds/run-routine.js');
|
|
147
|
+
const { cronRoutineFromEnv } = require('../scripts/gds/session-cost.js');
|
|
148
|
+
const env = stepEnv('bfg', {});
|
|
149
|
+
assert.equal(env[canonical('COST_CRON_ROUTINE')], 'bfg', 'the writer uses this instance\'s own prefix');
|
|
150
|
+
assert.equal(cronRoutineFromEnv(env), 'bfg', 'the reader finds what the writer wrote');
|
|
151
|
+
assert.equal(cronRoutineFromEnv({ [canonical('COST_CRON_ROUTINE')]: ' nightly ' }), 'nightly');
|
|
152
|
+
assert.equal(cronRoutineFromEnv({}), '');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('ROUTINE_ALERT_CHANNEL: <PREFIX>_ROUTINE_ALERT_CHANNEL picks the alert channel', () => {
|
|
156
|
+
const { alertChannel } = require('../scripts/gds/run-routine.js');
|
|
157
|
+
assert.equal(alertChannel({ [canonical('ROUTINE_ALERT_CHANNEL')]: 'ops' }), 'ops');
|
|
158
|
+
assert.equal(alertChannel({}), 'ship_feed');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('COST_LEDGER: <PREFIX>_COST_LEDGER is the first place a ledger is looked for', () => {
|
|
162
|
+
const { knownLedgerPaths } = require('../scripts/gds/backfill-cost.js');
|
|
163
|
+
assert.equal(knownLedgerPaths({ [canonical('COST_LEDGER')]: '/data/cost_ledger.jsonl' })[0], '/data/cost_ledger.jsonl');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('GIT_SHA: <PREFIX>_GIT_SHA is the deployed commit /version reports', () => {
|
|
167
|
+
const sha = 'abcdef0123456789abcdef0123456789abcdef01';
|
|
168
|
+
const out = JSON.parse(runNode(
|
|
169
|
+
"console.log(JSON.stringify(require('./src/build-info.js').buildInfo()))",
|
|
170
|
+
envWith('GIT_SHA', sha),
|
|
171
|
+
));
|
|
172
|
+
assert.equal(out.commit, sha);
|
|
173
|
+
assert.equal(out.resolvedVia, 'env');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('MEMORY_DIR: <PREFIX>_MEMORY_DIR is where memory-map.js reads MEMORY.md', () => {
|
|
177
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'knobs-memory-'));
|
|
178
|
+
try {
|
|
179
|
+
fs.writeFileSync(path.join(dir, 'MEMORY.md'), '# Memory index\n\n- [a note](a.md) — one line\n');
|
|
180
|
+
const r = spawnSync(process.execPath, ['scripts/gds/memory-map.js'], { cwd: ROOT, env: envWith('MEMORY_DIR', dir), encoding: 'utf8' });
|
|
181
|
+
assert.equal(r.status, 0, r.stderr);
|
|
182
|
+
assert.match(r.stdout, /^map: \d+ bytes/m, 'it measured the map in that directory (ignored, it prints usage and exits 2)');
|
|
183
|
+
} finally {
|
|
184
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('AUDIT_RANGE: <PREFIX>_AUDIT_RANGE is the range main-audit.js audits, and --range still wins', () => {
|
|
189
|
+
const { requestedRange } = require('../scripts/gds/main-audit.js');
|
|
190
|
+
assert.equal(requestedRange(undefined, { [canonical('AUDIT_RANGE')]: 'abc123..HEAD' }), 'abc123..HEAD');
|
|
191
|
+
assert.equal(requestedRange('def456..HEAD', { [canonical('AUDIT_RANGE')]: 'abc123..HEAD' }), 'def456..HEAD',
|
|
192
|
+
'the flag the deploy gate and CI pass beats any env value');
|
|
193
|
+
assert.equal(requestedRange(true, {}), null, 'a bare --range with no value falls back to the default window');
|
|
194
|
+
assert.equal(requestedRange(undefined, {}), null);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('ONBOARDING_WINDOW_DAYS: <PREFIX>_ONBOARDING_WINDOW_DAYS sets the window', () => {
|
|
198
|
+
const out = runNode(
|
|
199
|
+
"console.log(require('./scripts/gds/onboarding-config.js').windowDays())",
|
|
200
|
+
envWith('ONBOARDING_WINDOW_DAYS', '3'),
|
|
201
|
+
);
|
|
202
|
+
assert.equal(out, '3');
|
|
203
|
+
});
|