@bongos/core 1.19.576 → 1.19.578
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 +37 -17
- package/docs/adr/0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md +140 -0
- package/docs/adr/README.md +1 -0
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/public-landing/public/projects.html +94 -59
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/box-connect-lib.js +1 -1
- package/scripts/gds/build-cli-package.js +517 -0
- package/src/module-api.js +1 -1
- package/tests/cli_package.mjs +217 -0
- package/tests/manage_manifest_shared_read.mjs +205 -0
- package/tests/projects_hub.mjs +31 -4
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// tests/cli_package.mjs — the public @cloudbongos/cli package is client-only AND actually runs.
|
|
2
|
+
//
|
|
3
|
+
// Guards task 1003679. Two halves, and the second is the one that matters:
|
|
4
|
+
//
|
|
5
|
+
// 1. Shape: the generated tree carries no server surface, and the redaction gate really fails
|
|
6
|
+
// a build when instance identity or a credential shape appears in it.
|
|
7
|
+
//
|
|
8
|
+
// 2. BEHAVIOUR: pack the tarball, install it into an empty directory with NO repo present, and
|
|
9
|
+
// run every advertised verb. This is the only instrument that can prove the file list is
|
|
10
|
+
// complete. A static require-closure cannot: src/module-api.js is the module doorway
|
|
11
|
+
// (ADR 0083) and by design *names* every kernel capability, so a static walk sees the whole
|
|
12
|
+
// server even though the getters are lazy and never resolve. If a real code path needs a
|
|
13
|
+
// file the manifest omits, the verb exits with MODULE_NOT_FOUND and this test says so.
|
|
14
|
+
|
|
15
|
+
import test from 'node:test';
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const REPO_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
|
+
const BUILDER = path.join(REPO_ROOT, 'scripts', 'gds', 'build-cli-package.js');
|
|
25
|
+
|
|
26
|
+
const mod = await import(path.join(REPO_ROOT, 'scripts', 'gds', 'build-cli-package.js'));
|
|
27
|
+
const { build, FILES, VERBS, CHECKOUT_ONLY, ALLOWED_SRC_BONGOS, PACKAGE_NAME } = mod.default ?? mod;
|
|
28
|
+
|
|
29
|
+
function tmpdir(tag) {
|
|
30
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), `cli-pkg-${tag}-`));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── 1. Shape ────────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
test('the generated package contains no server surface', () => {
|
|
36
|
+
const out = tmpdir('shape');
|
|
37
|
+
const { files } = build({ out, quiet: true });
|
|
38
|
+
|
|
39
|
+
const kernel = files.filter((f) => f.startsWith('src/bongos/'));
|
|
40
|
+
assert.deepEqual(kernel, [...ALLOWED_SRC_BONGOS],
|
|
41
|
+
`only ${[...ALLOWED_SRC_BONGOS].join(', ')} may ship from the kernel, got ${kernel.join(', ')}`);
|
|
42
|
+
|
|
43
|
+
for (const f of files) {
|
|
44
|
+
assert.ok(!f.startsWith('src/bongos/routes/'), `${f}: routes must never ship`);
|
|
45
|
+
assert.ok(!/(^|\/)(pool|db|server|serve-internal)\.js$/.test(f), `${f}: server surface must never ship`);
|
|
46
|
+
assert.ok(!f.startsWith('migrations/'), `${f}: migrations must never ship`);
|
|
47
|
+
assert.ok(!f.startsWith('infra/'), `${f}: infra must never ship`);
|
|
48
|
+
}
|
|
49
|
+
fs.rmSync(out, { recursive: true, force: true });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('the one kernel file that ships imports nothing', () => {
|
|
53
|
+
const src = fs.readFileSync(path.join(REPO_ROOT, 'src', 'bongos', 'api-prefix.js'), 'utf8');
|
|
54
|
+
const rel = [...src.matchAll(/require\(\s*['"](\.[^'"]+)['"]\s*\)/g)].map((m) => m[1]);
|
|
55
|
+
assert.deepEqual(rel, [], `api-prefix.js must stay dependency-free, found: ${rel.join(', ')}`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('every file in the manifest exists', () => {
|
|
59
|
+
for (const rel of FILES) {
|
|
60
|
+
assert.ok(fs.existsSync(path.join(REPO_ROOT, rel)), `manifest names a missing file: ${rel}`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('every advertised verb maps to a script the manifest ships', () => {
|
|
65
|
+
for (const [verb, def] of Object.entries(VERBS)) {
|
|
66
|
+
assert.ok(FILES.includes(`scripts/gds/${def.script}`),
|
|
67
|
+
`verb "${verb}" runs scripts/gds/${def.script}, which the manifest does not ship`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('the redaction gate fails the build on an injected needle and on a credential shape', () => {
|
|
72
|
+
const out = tmpdir('gate');
|
|
73
|
+
|
|
74
|
+
// A domain that really is in the tree, supplied as forbidden → must refuse to emit.
|
|
75
|
+
const withNeedle = spawnSync(process.execPath, [BUILDER, '--out', out, '--quiet'], {
|
|
76
|
+
cwd: REPO_ROOT, encoding: 'utf8',
|
|
77
|
+
env: { ...process.env, CLI_PACKAGE_FORBIDDEN: 'cli-lib' },
|
|
78
|
+
});
|
|
79
|
+
assert.notEqual(withNeedle.status, 0, 'an instance needle present in the tree must fail the build');
|
|
80
|
+
assert.match(withNeedle.stderr, /redaction gate FAILED/);
|
|
81
|
+
|
|
82
|
+
// A planted credential shape must fail too, with no needle configured at all.
|
|
83
|
+
const planted = path.join(REPO_ROOT, 'scripts', 'gds', '.cli-pkg-gate-probe.js');
|
|
84
|
+
const victim = path.join(REPO_ROOT, 'scripts', 'gds', 'preflight.js');
|
|
85
|
+
const original = fs.readFileSync(victim, 'utf8');
|
|
86
|
+
try {
|
|
87
|
+
fs.writeFileSync(victim, `${original}\n// ghp_0123456789abcdefghijABCDEFGHIJ0123\n`);
|
|
88
|
+
const withToken = spawnSync(process.execPath, [BUILDER, '--out', out, '--quiet'], {
|
|
89
|
+
cwd: REPO_ROOT, encoding: 'utf8',
|
|
90
|
+
});
|
|
91
|
+
assert.notEqual(withToken.status, 0, 'a github token shape must fail the build');
|
|
92
|
+
assert.match(withToken.stderr, /github token/);
|
|
93
|
+
} finally {
|
|
94
|
+
fs.writeFileSync(victim, original);
|
|
95
|
+
fs.rmSync(planted, { force: true });
|
|
96
|
+
}
|
|
97
|
+
fs.rmSync(out, { recursive: true, force: true });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('a clean build emits and reports itself', () => {
|
|
101
|
+
const out = tmpdir('clean');
|
|
102
|
+
const res = spawnSync(process.execPath, [BUILDER, '--out', out], { cwd: REPO_ROOT, encoding: 'utf8' });
|
|
103
|
+
assert.equal(res.status, 0, `clean build failed:\n${res.stderr}`);
|
|
104
|
+
assert.match(res.stdout, /redaction gate: clean/);
|
|
105
|
+
assert.ok(fs.existsSync(path.join(out, 'package.json')));
|
|
106
|
+
assert.ok(fs.existsSync(path.join(out, 'bin', 'bongos.js')));
|
|
107
|
+
|
|
108
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(out, 'package.json'), 'utf8'));
|
|
109
|
+
assert.equal(pkg.name, PACKAGE_NAME);
|
|
110
|
+
assert.deepEqual(Object.keys(pkg.dependencies), ['undici'], 'exactly one runtime dependency');
|
|
111
|
+
assert.equal(pkg.bin.bongos, 'bin/bongos.js');
|
|
112
|
+
assert.equal(pkg.publishConfig.access, 'public');
|
|
113
|
+
assert.ok(!('repository' in pkg), 'no repository URL while the core repo is private');
|
|
114
|
+
fs.rmSync(out, { recursive: true, force: true });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ── 2. Behaviour: install the tarball where there is no repo, and run it ─────────────────────
|
|
118
|
+
|
|
119
|
+
test('the packed tarball installs and every verb runs with no repo present', (t) => {
|
|
120
|
+
const buildOut = tmpdir('pack');
|
|
121
|
+
const home = tmpdir('home');
|
|
122
|
+
const site = tmpdir('site');
|
|
123
|
+
|
|
124
|
+
const built = spawnSync(process.execPath, [BUILDER, '--out', buildOut, '--quiet'], {
|
|
125
|
+
cwd: REPO_ROOT, encoding: 'utf8',
|
|
126
|
+
});
|
|
127
|
+
assert.equal(built.status, 0, `build failed:\n${built.stderr}`);
|
|
128
|
+
|
|
129
|
+
const packed = spawnSync('npm', ['pack', '--silent', '--pack-destination', site], {
|
|
130
|
+
cwd: buildOut, encoding: 'utf8',
|
|
131
|
+
});
|
|
132
|
+
assert.equal(packed.status, 0, `npm pack failed:\n${packed.stderr}`);
|
|
133
|
+
const tarball = fs.readdirSync(site).find((f) => f.endsWith('.tgz'));
|
|
134
|
+
assert.ok(tarball, `no tarball produced in ${site}`);
|
|
135
|
+
|
|
136
|
+
// An empty directory: no repo, no core, no config, and a HOME of its own so a real session
|
|
137
|
+
// file on this machine cannot make a verb look healthier than it is.
|
|
138
|
+
fs.writeFileSync(path.join(site, 'package.json'), JSON.stringify({ name: 'probe', private: true }));
|
|
139
|
+
const installed = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--silent', path.join(site, tarball)], {
|
|
140
|
+
cwd: site, encoding: 'utf8', env: { ...process.env, HOME: home },
|
|
141
|
+
});
|
|
142
|
+
assert.equal(installed.status, 0, `npm install failed:\n${installed.stderr}`);
|
|
143
|
+
|
|
144
|
+
const bongos = path.join(site, 'node_modules', '.bin', 'bongos');
|
|
145
|
+
assert.ok(fs.existsSync(bongos), 'the package did not install a `bongos` binary');
|
|
146
|
+
|
|
147
|
+
const runVerb = (args) => spawnSync(bongos, args, {
|
|
148
|
+
cwd: site, encoding: 'utf8', input: '',
|
|
149
|
+
env: { ...process.env, HOME: home, CLOUDBONGOS_API_BASE: 'http://127.0.0.1:45999', NO_COLOR: '1' },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// help and version must work with nothing configured at all.
|
|
153
|
+
const help = runVerb(['help']);
|
|
154
|
+
assert.equal(help.status, 0, `bongos help failed:\n${help.stderr}`);
|
|
155
|
+
assert.match(help.stdout, /bongos login https:\/\//, 'help must lead with how to sign in');
|
|
156
|
+
|
|
157
|
+
const version = runVerb(['version']);
|
|
158
|
+
assert.equal(version.status, 0);
|
|
159
|
+
assert.match(version.stdout, /^bongos \d+\.\d+\.\d+/m);
|
|
160
|
+
|
|
161
|
+
// Every advertised verb must actually LOAD its script. Args are chosen so the dispatcher
|
|
162
|
+
// cannot answer on the script's behalf — `--help` on a non-selfHelp verb is intercepted by
|
|
163
|
+
// the dispatcher and would leave the script untouched, which is how this test could pass
|
|
164
|
+
// while proving nothing. Each verb below reaches its own code, then fails on the dead API
|
|
165
|
+
// base or on missing arguments. Failing that way is fine; failing to find a file is not.
|
|
166
|
+
const LOAD_ARGS = {
|
|
167
|
+
login: ['login'],
|
|
168
|
+
reauth: ['reauth'],
|
|
169
|
+
setup: ['setup'],
|
|
170
|
+
box: ['box'],
|
|
171
|
+
shell: ['shell'],
|
|
172
|
+
code: ['code'],
|
|
173
|
+
start: ['start'],
|
|
174
|
+
status: ['status'],
|
|
175
|
+
task: ['task', 'show', '1'],
|
|
176
|
+
recall: ['recall', 'probe'],
|
|
177
|
+
cost: ['cost'],
|
|
178
|
+
release: ['release'],
|
|
179
|
+
api: ['api', 'GET', '/api/gds/me'],
|
|
180
|
+
};
|
|
181
|
+
assert.deepEqual(
|
|
182
|
+
Object.keys(LOAD_ARGS).sort(), Object.keys(VERBS).sort(),
|
|
183
|
+
'every advertised verb needs an entry here, or it goes unexercised',
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
for (const [verb, args] of Object.entries(LOAD_ARGS)) {
|
|
187
|
+
const res = runVerb(args);
|
|
188
|
+
const all = `${res.stdout}\n${res.stderr}`;
|
|
189
|
+
assert.ok(!/Cannot find module|MODULE_NOT_FOUND|ERR_MODULE_NOT_FOUND/.test(all),
|
|
190
|
+
`bongos ${verb} hit a missing file — the manifest is incomplete:\n${all}`);
|
|
191
|
+
assert.ok(!/ERR_REQUIRE_ESM|SyntaxError|ReferenceError/.test(all),
|
|
192
|
+
`bongos ${verb} failed to load:\n${all}`);
|
|
193
|
+
// `TypeError: fetch failed` is what an unreachable API base looks like and is expected
|
|
194
|
+
// here; any OTHER TypeError means the package itself is broken.
|
|
195
|
+
assert.ok(!/TypeError(?!: fetch failed)/.test(all),
|
|
196
|
+
`bongos ${verb} threw a real TypeError:\n${all}`);
|
|
197
|
+
assert.notEqual(res.status, null, `bongos ${verb} was killed by a signal:\n${all}`);
|
|
198
|
+
// The dispatcher's own help card is proof the script was never reached.
|
|
199
|
+
assert.ok(!/^bongos \w+ — .*\nForwards to scripts/m.test(all),
|
|
200
|
+
`bongos ${verb} was answered by the dispatcher instead of loading:\n${all}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The excluded verbs must teach the next step, not print a bare "unknown command".
|
|
204
|
+
for (const verb of Object.keys(CHECKOUT_ONLY)) {
|
|
205
|
+
const res = runVerb([verb]);
|
|
206
|
+
const all = `${res.stdout}\n${res.stderr}`;
|
|
207
|
+
assert.match(all, /is not in the public CLI/, `bongos ${verb} must say why it is absent`);
|
|
208
|
+
assert.match(all, /bongos shell/, `bongos ${verb} must point at a way to get a checkout`);
|
|
209
|
+
assert.ok(!/unknown command/.test(all), `bongos ${verb} must not read as a typo`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// An unknown verb still behaves like one.
|
|
213
|
+
const bogus = runVerb(['definitely-not-a-verb']);
|
|
214
|
+
assert.match(bogus.stderr, /unknown command/);
|
|
215
|
+
|
|
216
|
+
for (const d of [buildOut, home, site]) fs.rmSync(d, { recursive: true, force: true });
|
|
217
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// tests/manage_manifest_shared_read.mjs — one read of the project's OWN
|
|
2
|
+
// manifest per manage entry, EXECUTED (task 1003554).
|
|
3
|
+
//
|
|
4
|
+
// The defect this pins shut: entering /projects?view=manage fetched
|
|
5
|
+
// https://<project>/api/gds/instance twice, identically and cross-origin,
|
|
6
|
+
// because the two cards that render a "what it runs right now" line — how the
|
|
7
|
+
// project is reached (brand.project.platformVisibility) and how people join
|
|
8
|
+
// (brand.project.joinability) — each asked the project for themselves. Two
|
|
9
|
+
// round trips to somebody else's server, each with its own 8s budget, for one
|
|
10
|
+
// answer.
|
|
11
|
+
//
|
|
12
|
+
// It is the sibling of tests/manage_settings_shared_read.mjs, and the reason it
|
|
13
|
+
// is a separate reader rather than the same memo: the settings row is read once
|
|
14
|
+
// per entry and never again, so a memo that lives for the entry is right. This
|
|
15
|
+
// answer goes stale the moment a settings push is queued, and the re-read once
|
|
16
|
+
// that push settles is the ONLY thing that can say the new value took. So the
|
|
17
|
+
// memo is dropped for as long as the push is open. A lifetime memo here would
|
|
18
|
+
// freeze both lines at their pre-restart value — an efficiency cleanup turned
|
|
19
|
+
// into a wrong answer on screen.
|
|
20
|
+
//
|
|
21
|
+
// tests/projects_hub.mjs pins that both cards go through the shared reader;
|
|
22
|
+
// this file lifts the reader out of modules/public-landing/public/projects.html,
|
|
23
|
+
// runs it in a vm under a scripted fetch, and counts the round trips. The
|
|
24
|
+
// vm-harness precedent is tests/project_door_ui.mjs.
|
|
25
|
+
//
|
|
26
|
+
// What it proves, and why each matters:
|
|
27
|
+
// • both cards of one entry cost ONE round trip — the defect itself
|
|
28
|
+
// • forgetManifest() makes the next read real, so entry and tab return each
|
|
29
|
+
// ask the project as it is now, never as it was last visit
|
|
30
|
+
// • the push-open drop is what makes the settle re-read real, and the two
|
|
31
|
+
// cards re-reading after one settle still cost one round trip
|
|
32
|
+
// • switching projects is never served the previous project's manifest
|
|
33
|
+
// • a refusal RESOLVES carrying its status and an unreachable project
|
|
34
|
+
// resolves with status 0 — so ONE failure reaches BOTH lines and neither
|
|
35
|
+
// card goes silent waiting on a rejection nobody handed it
|
|
36
|
+
// • the 8s abort budget is the shared read's, and it is cleared once
|
|
37
|
+
//
|
|
38
|
+
// Run: node --test --test-reporter=tap tests/manage_manifest_shared_read.mjs
|
|
39
|
+
|
|
40
|
+
import assert from 'node:assert/strict';
|
|
41
|
+
import { test } from 'node:test';
|
|
42
|
+
import fs from 'node:fs';
|
|
43
|
+
import path from 'node:path';
|
|
44
|
+
import vm from 'node:vm';
|
|
45
|
+
import { fileURLToPath } from 'node:url';
|
|
46
|
+
|
|
47
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
48
|
+
const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8');
|
|
49
|
+
const SCRIPT = (HUB.match(/<script>([\s\S]*?)<\/script>\s*<\/body>/) || [, ''])[1];
|
|
50
|
+
|
|
51
|
+
// The reader, sliced from the page verbatim. Both boundaries are asserted so a
|
|
52
|
+
// rename cannot leave this file executing an empty string and passing.
|
|
53
|
+
const START = SCRIPT.indexOf(' var manifestPromise = null;');
|
|
54
|
+
const END = SCRIPT.indexOf(' function loadVisRuns(');
|
|
55
|
+
assert.ok(START > 0, 'the shared reader must start at its memo declaration');
|
|
56
|
+
assert.ok(END > START, 'the shared reader must sit above its first consumer');
|
|
57
|
+
const REGION = SCRIPT.slice(START, END);
|
|
58
|
+
for (const fn of ['function ensureManifest(', 'function forgetManifest(']) {
|
|
59
|
+
assert.ok(REGION.includes(fn), `the region must carry ${fn}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── the harness ──────────────────────────────────────────────────────────────
|
|
63
|
+
// Narrow on purpose: the region touches fetch, manageId and the abort timer,
|
|
64
|
+
// and nothing else — so the stub stays honest about what it can prove.
|
|
65
|
+
|
|
66
|
+
function boot({ ok = true, status = 200, body = { brand: { project: { platformVisibility: 'public', joinability: 'open' } } }, reject = false } = {}) {
|
|
67
|
+
const requests = [];
|
|
68
|
+
const timers = { set: 0, cleared: 0 };
|
|
69
|
+
const sandbox = {
|
|
70
|
+
Promise, JSON, String, Object, Error, AbortController,
|
|
71
|
+
manageId: '18',
|
|
72
|
+
setTimeout: (fn, ms) => { timers.set++; timers.ms = ms; return { fn }; },
|
|
73
|
+
clearTimeout: () => { timers.cleared++; },
|
|
74
|
+
fetch: (url, init = {}) => {
|
|
75
|
+
requests.push({ url, mode: init.mode });
|
|
76
|
+
return reject
|
|
77
|
+
? Promise.reject(new Error('network'))
|
|
78
|
+
: Promise.resolve({ ok, status, json: () => Promise.resolve(body) });
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
sandbox.globalThis = sandbox;
|
|
82
|
+
vm.createContext(sandbox);
|
|
83
|
+
vm.runInContext(REGION, sandbox, { filename: 'projects.html#shared-manifest-read' });
|
|
84
|
+
|
|
85
|
+
return { requests, timers, sandbox };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── the tests ────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
test('the two cards of one manage entry cost ONE round trip, not two', async () => {
|
|
91
|
+
// Arrange — the page's own entry pass: the reach card, then the join card
|
|
92
|
+
const ui = boot();
|
|
93
|
+
|
|
94
|
+
// Act
|
|
95
|
+
const answers = await Promise.all([
|
|
96
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
97
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
// Assert
|
|
101
|
+
assert.equal(ui.requests.length, 1, 'one cross-origin GET served both lines');
|
|
102
|
+
assert.equal(ui.requests[0].url, 'https://demo.example.com/api/gds/instance');
|
|
103
|
+
assert.equal(ui.requests[0].mode, 'cors', 'the project is somebody else’s origin');
|
|
104
|
+
for (const a of answers) {
|
|
105
|
+
assert.equal(a.ok, true);
|
|
106
|
+
assert.equal(a.data.brand.project.joinability, 'open', 'both callers got the same manifest');
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('forgetManifest makes the next read real — entry and tab return never reuse the last visit’s answer', async () => {
|
|
111
|
+
// Arrange
|
|
112
|
+
const ui = boot();
|
|
113
|
+
await ui.sandbox.ensureManifest('demo.example.com');
|
|
114
|
+
|
|
115
|
+
// Act — what loadManage does at the top of every entry + tab return pass
|
|
116
|
+
ui.sandbox.forgetManifest();
|
|
117
|
+
await ui.sandbox.ensureManifest('demo.example.com');
|
|
118
|
+
|
|
119
|
+
// Assert
|
|
120
|
+
assert.equal(ui.requests.length, 2, 'the second entry asked the project again');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('a settled push re-reads for real, and the two cards still share ONE round trip', async () => {
|
|
124
|
+
// Arrange — the reason this memo is not ensureSettings()'s: an answer read
|
|
125
|
+
// before a push cannot say whether the restart took
|
|
126
|
+
const ui = boot();
|
|
127
|
+
await ui.sandbox.ensureManifest('demo.example.com');
|
|
128
|
+
|
|
129
|
+
// Act — the push opens (renderVisState/renderJoinState each drop the memo on
|
|
130
|
+
// every repaint while it is open), then it settles and both cards re-read
|
|
131
|
+
ui.sandbox.forgetManifest();
|
|
132
|
+
ui.sandbox.forgetManifest();
|
|
133
|
+
const after = await Promise.all([
|
|
134
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
135
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
// Assert
|
|
139
|
+
assert.equal(ui.requests.length, 2, 'one re-read after the settle — not one per card, and not zero');
|
|
140
|
+
assert.equal(after[0].ok, true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('switching projects is never served the previous project’s manifest', async () => {
|
|
144
|
+
// Arrange — the memo is warm for project 18
|
|
145
|
+
const ui = boot();
|
|
146
|
+
await ui.sandbox.ensureManifest('one.example.com');
|
|
147
|
+
|
|
148
|
+
// Act — the owner opens a different project; loadManage has not forgotten yet
|
|
149
|
+
ui.sandbox.manageId = '19';
|
|
150
|
+
await ui.sandbox.ensureManifest('two.example.com');
|
|
151
|
+
|
|
152
|
+
// Assert
|
|
153
|
+
assert.equal(ui.requests.length, 2, 'the memo is keyed on the project, not merely present');
|
|
154
|
+
assert.equal(ui.requests[1].url, 'https://two.example.com/api/gds/instance');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('a project that answered-and-refused RESOLVES carrying its status — both lines can say so', async () => {
|
|
158
|
+
// Arrange — a pre-launch gate or a proxy error page, which is NOT an older core
|
|
159
|
+
const ui = boot({ ok: false, status: 403 });
|
|
160
|
+
|
|
161
|
+
// Act
|
|
162
|
+
const [a, b] = await Promise.all([
|
|
163
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
164
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
// Assert
|
|
168
|
+
assert.equal(a.ok, false);
|
|
169
|
+
assert.equal(a.status, 403, 'the status reaches the card, which prints it verbatim');
|
|
170
|
+
assert.equal(a.data, null, 'no body is claimed for a refusal');
|
|
171
|
+
assert.deepEqual(b, a, 'the second card is handed the same refusal, not left waiting');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('an unreachable project RESOLVES with status 0, so one failure fans out to both lines', async () => {
|
|
175
|
+
// Arrange — the failure that used to reject into two separate catches
|
|
176
|
+
const ui = boot({ reject: true });
|
|
177
|
+
|
|
178
|
+
// Act
|
|
179
|
+
const [a, b] = await Promise.all([
|
|
180
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
181
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
// Assert
|
|
185
|
+
assert.equal(a.ok, false);
|
|
186
|
+
assert.equal(a.status, 0, 'status 0 is "could not reach it at all" — a different sentence from a refusal');
|
|
187
|
+
assert.deepEqual(b, a, 'neither line goes silent on a shared rejection');
|
|
188
|
+
assert.equal(ui.requests.length, 1, 'one failed attempt, not one per card');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('the 8s abort budget belongs to the shared read, and is cleared once', async () => {
|
|
192
|
+
// Arrange
|
|
193
|
+
const ui = boot();
|
|
194
|
+
|
|
195
|
+
// Act
|
|
196
|
+
await Promise.all([
|
|
197
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
198
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
// Assert
|
|
202
|
+
assert.equal(ui.timers.set, 1, 'one timer for the pair, not one each');
|
|
203
|
+
assert.equal(ui.timers.ms, 8000, 'the budget the two cards used to keep separately');
|
|
204
|
+
assert.equal(ui.timers.cleared, 1, 'and it is cleared when the answer lands');
|
|
205
|
+
});
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -1418,14 +1418,17 @@ test('the save sends the choice and shows the platform’s receipt verbatim —
|
|
|
1418
1418
|
|
|
1419
1419
|
test('what the project RUNS comes only from its own manifest, and every answer is said', () => {
|
|
1420
1420
|
const runs = fnHub('loadVisRuns');
|
|
1421
|
+
const manifest = fnHub('ensureManifest');
|
|
1421
1422
|
assert.match(runs, /inst\.status !== 'active' \|\| !inst\.domain/, 'only a live project with an address can be asked');
|
|
1422
|
-
assert.match(
|
|
1423
|
+
assert.match(manifest, /'https:\/\/' \+ domain \+ '\/api\/gds\/instance'/,
|
|
1423
1424
|
'the same public GET /instance the platform’s runner reads after a restart');
|
|
1425
|
+
assert.doesNotMatch(runs, /fetch\(/, 'the card fires no GET of its own — one manifest read serves the pair (task 1003554)');
|
|
1424
1426
|
assert.match(runs, /m\.brand && m\.brand\.project/);
|
|
1425
1427
|
assert.match(runs, /doesn’t report this setting yet/, 'an older core is named, not shown as applied');
|
|
1426
1428
|
assert.match(runs, /hasn’t taken effect there yet/, 'a saved-but-not-applied value is said');
|
|
1427
1429
|
assert.match(runs, /Couldn’t reach the project/, 'an unreachable project is said');
|
|
1428
|
-
assert.match(
|
|
1430
|
+
assert.match(manifest, /if \(!res\.ok\) return \{ ok: false, status: res\.status, data: null \};/,
|
|
1431
|
+
'a refusal is not an older core — its status is carried out to both cards, not thrown away');
|
|
1429
1432
|
assert.match(runs, /answered but didn’t share what it’s running/, 'a project that answered-and-refused is said as such');
|
|
1430
1433
|
assert.doesNotMatch(runs, /mVisSettings\.value\)\s*\+\s*'<\/b>'|runs as <b>' \+ esc\(VIS_OPTIONS\[want\]/,
|
|
1431
1434
|
'the platform’s stored choice is never painted as what the project runs');
|
|
@@ -1598,15 +1601,39 @@ test('the joinability save sends the one key and shows the platform’s receipt
|
|
|
1598
1601
|
test('what the project RUNS for joinability comes only from its own manifest, and every answer is said', () => {
|
|
1599
1602
|
const runs = fnHub('loadJoinRuns');
|
|
1600
1603
|
assert.match(runs, /inst\.status !== 'active' \|\| !inst\.domain/, 'only a live project with an address can be asked');
|
|
1601
|
-
assert.match(runs, /
|
|
1604
|
+
assert.match(runs, /ensureManifest\(inst\.domain\)/, 'the same public GET /instance the runner reads, through the shared reader');
|
|
1605
|
+
assert.doesNotMatch(runs, /fetch\(/, 'the second card of the pair adds no round trip of its own (task 1003554)');
|
|
1602
1606
|
assert.match(runs, /p && p\.joinability/, 'the manifest key is brand.project.joinability');
|
|
1603
1607
|
assert.match(runs, /doesn’t report this setting yet/, 'an older core is named, not shown as applied');
|
|
1604
1608
|
assert.match(runs, /hasn’t taken effect there yet/, 'a saved-but-not-applied value is said');
|
|
1605
1609
|
assert.match(runs, /Couldn’t reach the project/, 'an unreachable project is said');
|
|
1606
|
-
assert.match(runs, /if \(!res\.ok\) throw new Error\('http:' \+ res\.status\)/, 'a refusal is not an older core');
|
|
1607
1610
|
assert.match(runs, /answered but didn’t share what it’s running/, 'a project that answered-and-refused is said as such');
|
|
1608
1611
|
});
|
|
1609
1612
|
|
|
1613
|
+
/* One read of the project's own manifest per manage entry (task 1003554), the
|
|
1614
|
+
sibling of the settings-row share above. Two cards read two keys out of the
|
|
1615
|
+
SAME cross-origin GET and each used to fetch it for itself. The memo differs
|
|
1616
|
+
from ensureSettings() in one load-bearing way — it is dropped for as long as
|
|
1617
|
+
a push is open, because the re-read after it settles is the only thing that
|
|
1618
|
+
can say the new value took. tests/manage_manifest_shared_read.mjs runs the
|
|
1619
|
+
reader; this pins that both cards go through it and that the drop is there. */
|
|
1620
|
+
test('the two “what it runs” lines share ONE read of the project’s manifest', () => {
|
|
1621
|
+
const ensure = fnHub('ensureManifest');
|
|
1622
|
+
assert.match(ensure, /if \(manifestPromise && manifestFor === manageId\) return manifestPromise;/,
|
|
1623
|
+
'memoised AND keyed on the project — a switch must never be served the last one’s manifest');
|
|
1624
|
+
assert.match(ensure, /setTimeout\(function \(\) \{ ctl\.abort\(\); \}, 8000\)/, 'one 8s budget for the pair, not one each');
|
|
1625
|
+
for (const fn of ['loadVisRuns', 'loadJoinRuns']) {
|
|
1626
|
+
assert.match(fnHub(fn), /ensureManifest\(inst\.domain\)/, `${fn} reads the manifest through the shared reader`);
|
|
1627
|
+
assert.doesNotMatch(fnHub(fn), /fetch\(/, `${fn} fires no GET of its own`);
|
|
1628
|
+
}
|
|
1629
|
+
assert.match(SCRIPT, /if \(!keepRepo\) forgetManifest\(\);/,
|
|
1630
|
+
'the memo is dropped on entry + tab return, so neither is ever served the last visit’s answer');
|
|
1631
|
+
for (const fn of ['renderVisState', 'renderJoinState']) {
|
|
1632
|
+
assert.match(fnHub(fn), /forgetManifest\(\);/,
|
|
1633
|
+
`${fn} drops the memo while a push is open — a lifetime memo would freeze the line at its pre-restart value`);
|
|
1634
|
+
}
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1610
1637
|
test('the joinability in-flight line shares the visibility section’s push predicate and re-asks the manifest when it settles', () => {
|
|
1611
1638
|
const st = fnHub('renderJoinState');
|
|
1612
1639
|
assert.match(st, /if \(visPushOpen\(inst\)\)/, 'one settings push carries every key — one predicate, shared');
|