@celilo/e2e 0.20.1 → 0.20.2
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/bin/e2e-bake-management +17 -1
- package/package.json +4 -3
- package/src/block-timing.ts +24 -3
- package/src/cli/build.test.ts +60 -2
- package/src/cli/build.ts +16 -1
- package/src/container-manager.ts +47 -1
- package/src/docker-compose-generator.ts +33 -3
- package/src/extract-failure.ts +36 -14
- package/src/live-stack.test.ts +52 -1
- package/src/live-stack.ts +59 -1
- package/src/module-host.test.ts +55 -1
- package/src/module-host.ts +31 -0
- package/src/netapp-staleness.test.ts +139 -0
- package/src/netapp-staleness.ts +62 -0
- package/src/network-builder.ts +19 -3
- package/src/registry-bundle.ts +14 -2
- package/src/runner-keep.test.ts +72 -0
- package/src/runner.ts +19 -2
- package/src/types.ts +21 -7
- package/registry-server/src/auth.test.ts +0 -76
- package/registry-server/src/bootstrap-packaging.test.ts +0 -71
- package/registry-server/src/introspection.test.ts +0 -243
- package/registry-server/src/module-owner-store.test.ts +0 -85
- package/registry-server/src/rate-limit.test.ts +0 -62
- package/registry-server/src/scoped-token-store.test.ts +0 -93
- package/registry-server/src/server.test.ts +0 -991
- package/registry-server/src/storage.test.ts +0 -152
- package/registry-server/src/sweep.test.ts +0 -326
- package/registry-server/src/validation.test.ts +0 -86
package/src/module-host.ts
CHANGED
|
@@ -70,3 +70,34 @@ export function parseModuleHost(statusOutput: string): ModuleHost | null {
|
|
|
70
70
|
|
|
71
71
|
return null;
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The address(es) a module's deploy recorded, from `celilo module where --json`.
|
|
76
|
+
*
|
|
77
|
+
* The old source was `grep target_ip` over the module's `generated/` tree
|
|
78
|
+
* (celilo#1334). D4 of control-plane-stops-building-modules makes that tree
|
|
79
|
+
* ephemeral — a successful deploy deletes it — so the inventory is where the
|
|
80
|
+
* address lives now: `module_systems` is written by the deploy itself and
|
|
81
|
+
* survives it. Every current module declares exactly one system, so callers
|
|
82
|
+
* take the first address; a module with none is an API-only module or an
|
|
83
|
+
* undeployed one, and that distinction belongs to the caller.
|
|
84
|
+
*/
|
|
85
|
+
export function parseModuleWhere(whereOutput: string): string[] {
|
|
86
|
+
// The success path prints the payload verbatim (celilo#698), but defensive
|
|
87
|
+
// ANSI stripping costs one line and dev-mode shims have surprised us before.
|
|
88
|
+
const stripped = whereOutput.replace(/\x1b\[[0-9;]*m/g, '').trim();
|
|
89
|
+
|
|
90
|
+
let parsed: { systems?: { ipv4_address?: string }[] };
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(stripped) as { systems?: { ipv4_address?: string }[] };
|
|
93
|
+
} catch {
|
|
94
|
+
// A non-JSON answer is a CLI failure (version drift, crash text) arriving
|
|
95
|
+
// on stdout. An empty list reads as "no systems", which is a real state a
|
|
96
|
+
// caller may legitimately handle — these are not the same problem.
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (parsed.systems ?? [])
|
|
101
|
+
.map((system) => system.ipv4_address ?? '')
|
|
102
|
+
.filter((address) => address !== '');
|
|
103
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { afterAll, beforeAll, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
symlinkSync,
|
|
8
|
+
utimesSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { isNetappCurrent } from './netapp-staleness';
|
|
14
|
+
|
|
15
|
+
let dir: string;
|
|
16
|
+
let src: string;
|
|
17
|
+
let netapp: string;
|
|
18
|
+
const scratch: string[] = [];
|
|
19
|
+
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
dir = mkdtempSync(join(tmpdir(), 'netapp-stale-'));
|
|
22
|
+
scratch.push(dir);
|
|
23
|
+
src = join(dir, 'src');
|
|
24
|
+
netapp = join(dir, 'out.netapp');
|
|
25
|
+
mkdirSync(src);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterAll(() => {
|
|
29
|
+
for (const path of scratch) rmSync(path, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function writeSource(name: string, contents: string): string {
|
|
33
|
+
const target = join(src, name);
|
|
34
|
+
writeFileSync(target, contents);
|
|
35
|
+
return target;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test('no netapp yet: not current, even over an empty tree', () => {
|
|
39
|
+
expect(existsSync(netapp)).toBe(false);
|
|
40
|
+
expect(isNetappCurrent(netapp, dir)).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('netapp newer than every source file: current (the skip case)', () => {
|
|
44
|
+
writeSource('main.sh', 'echo hello');
|
|
45
|
+
writeFileSync(netapp, 'netapp-bytes');
|
|
46
|
+
// The netapp was written after the source, so repackaging would produce
|
|
47
|
+
// the same bytes. build-infra must skip this module.
|
|
48
|
+
expect(isNetappCurrent(netapp, dir)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('recurrence gate (celilo#1258): touching a source file makes the netapp stale', () => {
|
|
52
|
+
// Touch, don't rewrite: proves the check reads mtimes, not contents.
|
|
53
|
+
const touched = writeSource('touched.txt', 'x');
|
|
54
|
+
const later = new Date(Date.now() + 60_000);
|
|
55
|
+
utimesSync(touched, later, later);
|
|
56
|
+
expect(isNetappCurrent(netapp, dir)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('netapp older than a source edit: stale', () => {
|
|
60
|
+
const older = new Date(Date.now() - 120_000);
|
|
61
|
+
utimesSync(netapp, older, older);
|
|
62
|
+
expect(isNetappCurrent(netapp, dir)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('a rebuilt binlink dir never marks the netapp stale (the bun install shape)', () => {
|
|
66
|
+
// Fresh tree: shared state above would leak src/'s own dir mtime into this
|
|
67
|
+
// assertion, and src/ is a shipped path whose mtime must count.
|
|
68
|
+
const tree = mkdtempSync(join(tmpdir(), 'netapp-binlinks-'));
|
|
69
|
+
scratch.push(tree);
|
|
70
|
+
const moduleDir = join(tree, 'mod');
|
|
71
|
+
const binDir = join(moduleDir, 'e2e', 'node_modules', '.bin');
|
|
72
|
+
mkdirSync(binDir, { recursive: true });
|
|
73
|
+
writeFileSync(join(moduleDir, 'main.sh'), 'echo hello');
|
|
74
|
+
symlinkSync('../real-tool', join(binDir, 'tool'));
|
|
75
|
+
const staged = join(tree, 'staged.netapp');
|
|
76
|
+
writeFileSync(staged, 'netapp-bytes');
|
|
77
|
+
expect(isNetappCurrent(staged, moduleDir)).toBe(true);
|
|
78
|
+
|
|
79
|
+
// bun install recreates the binlink: the symlink file AND the .bin
|
|
80
|
+
// directory move past the netapp. Neither ships (see classifyModulePath
|
|
81
|
+
// and includeNodeModulesPath), so the netapp stays current. This is the
|
|
82
|
+
// dir-level case the file-level exclusion alone misses, probed 2026-09-08:
|
|
83
|
+
// `-not -path '*/node_modules/.bin/*'` does not match `.bin` itself, and
|
|
84
|
+
// creating an entry moves the dir's mtime.
|
|
85
|
+
rmSync(join(binDir, 'tool'));
|
|
86
|
+
symlinkSync('../real-tool', join(binDir, 'tool'));
|
|
87
|
+
expect(isNetappCurrent(staged, moduleDir)).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('a change confined to the module e2e/ tree never marks the netapp stale', () => {
|
|
91
|
+
const tree = mkdtempSync(join(tmpdir(), 'netapp-e2e-'));
|
|
92
|
+
scratch.push(tree);
|
|
93
|
+
const moduleDir = join(tree, 'mod');
|
|
94
|
+
mkdirSync(join(moduleDir, 'e2e'), { recursive: true });
|
|
95
|
+
writeFileSync(join(moduleDir, 'main.sh'), 'echo hello');
|
|
96
|
+
writeFileSync(join(moduleDir, 'e2e', 'suite.test.ts'), 'test("x", () => {});');
|
|
97
|
+
const staged = join(tree, 'staged.netapp');
|
|
98
|
+
writeFileSync(staged, 'netapp-bytes');
|
|
99
|
+
expect(isNetappCurrent(staged, moduleDir)).toBe(true);
|
|
100
|
+
|
|
101
|
+
// An e2e-only edit: the file and its parent dir are both unshipped, so
|
|
102
|
+
// the netapp stays current. A change to main.sh would NOT be excluded —
|
|
103
|
+
// covered by the recurrence gate above.
|
|
104
|
+
const later = new Date(Date.now() + 60_000);
|
|
105
|
+
const e2eFile = join(moduleDir, 'e2e', 'new.test.ts');
|
|
106
|
+
writeFileSync(e2eFile, 'test("y", () => {});');
|
|
107
|
+
utimesSync(e2eFile, later, later);
|
|
108
|
+
expect(isNetappCurrent(staged, moduleDir)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('a change to a SHIPPED bin-closure path marks the netapp stale', () => {
|
|
112
|
+
// scripts/node_modules minus .bin is the hook runtime closure the packager
|
|
113
|
+
// ships in full (ISS-0046). A change there must repackage, so the
|
|
114
|
+
// exclusion cannot be as broad as "anything under node_modules".
|
|
115
|
+
const tree = mkdtempSync(join(tmpdir(), 'netapp-hookrt-'));
|
|
116
|
+
scratch.push(tree);
|
|
117
|
+
const moduleDir = join(tree, 'mod');
|
|
118
|
+
const hookDep = join(moduleDir, 'scripts', 'node_modules', 'tldts');
|
|
119
|
+
mkdirSync(hookDep, { recursive: true });
|
|
120
|
+
writeFileSync(join(moduleDir, 'main.sh'), 'echo hello');
|
|
121
|
+
writeFileSync(join(hookDep, 'index.js'), 'module.exports = {};');
|
|
122
|
+
const staged = join(tree, 'staged.netapp');
|
|
123
|
+
writeFileSync(staged, 'netapp-bytes');
|
|
124
|
+
expect(isNetappCurrent(staged, moduleDir)).toBe(true);
|
|
125
|
+
|
|
126
|
+
const later = new Date(Date.now() + 60_000);
|
|
127
|
+
const depFile = join(hookDep, 'index.js');
|
|
128
|
+
writeFileSync(depFile, 'module.exports = { changed: true };');
|
|
129
|
+
utimesSync(depFile, later, later);
|
|
130
|
+
expect(isNetappCurrent(staged, moduleDir)).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('a nonexistent source tree repackages (a check that cannot answer never reuses)', () => {
|
|
134
|
+
const tree = mkdtempSync(join(tmpdir(), 'netapp-missing-'));
|
|
135
|
+
scratch.push(tree);
|
|
136
|
+
const staged = join(tree, 'staged.netapp');
|
|
137
|
+
writeFileSync(staged, 'netapp-bytes');
|
|
138
|
+
expect(isNetappCurrent(staged, join(tree, 'does-not-exist'))).toBe(false);
|
|
139
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staleness helper for build-infra's `packageNetapp` (cli/build.ts).
|
|
3
|
+
*
|
|
4
|
+
* Before celilo#1258 build-infra repackaged all 37 modules on every run
|
|
5
|
+
* (about 5 minutes to reach a 38 second test) because nothing reused a
|
|
6
|
+
* current .netapp. publishModule's reuse of the staged netapp is a separate
|
|
7
|
+
* fix that landed from ce-nvd8 (bf05c946) and keeps its own probe.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* True when `netappPath` exists and nothing that ships in the package has
|
|
15
|
+
* changed under `sourceDir` since it was written.
|
|
16
|
+
*
|
|
17
|
+
* `find -newer -quit` asks the filesystem instead of walking in JS and stops
|
|
18
|
+
* at the first newer file. A staleness check that cannot answer repackages:
|
|
19
|
+
* reusing on an inconclusive result is how a test silently runs against a
|
|
20
|
+
* stale module.
|
|
21
|
+
*
|
|
22
|
+
* Only paths the package actually contains can make it stale. `e2e/` is
|
|
23
|
+
* excluded from a module package wholesale and `node_modules/.bin` is excluded
|
|
24
|
+
* from the hook runtime closure — see `classifyModulePath` and
|
|
25
|
+
* `includeNodeModulesPath` in apps/celilo/src/module/packaging/, which are the
|
|
26
|
+
* authority. They are restated rather than imported because packages/e2e has
|
|
27
|
+
* no import path into apps/celilo, and registry-server's bootstrap.ts already
|
|
28
|
+
* carries the same duplication for the same reason.
|
|
29
|
+
*
|
|
30
|
+
* Both subtrees are excluded at the DIRECTORY level as well as the file
|
|
31
|
+
* level (path suffixes `e2e` and `node_modules/.bin`, with and without a
|
|
32
|
+
* trailing component): a directory's mtime moves whenever an entry inside
|
|
33
|
+
* it is created or replaced, so `bun install` recreating the binlinks in a
|
|
34
|
+
* module's `e2e/node_modules/.bin` would otherwise mark every module
|
|
35
|
+
* permanently dirty through the dir entry alone.
|
|
36
|
+
*
|
|
37
|
+
* Getting these wrong is safe in one direction only, and it is this one:
|
|
38
|
+
* counting a non-packaged path makes us repackage needlessly (slow), while
|
|
39
|
+
* MISSING a packaged path would reuse a stale netapp (wrong). Every exclusion
|
|
40
|
+
* here is a path the packager does not ship, so it cannot cause the latter.
|
|
41
|
+
*/
|
|
42
|
+
export function isNetappCurrent(netappPath: string, sourceDir: string): boolean {
|
|
43
|
+
if (!existsSync(netappPath)) return false;
|
|
44
|
+
// Each exclude needs both the bare dir and its contents: `find -newer`
|
|
45
|
+
// matches the directory's own mtime too, and creating or replacing an
|
|
46
|
+
// entry inside moves it.
|
|
47
|
+
const excludes = [
|
|
48
|
+
"-not -path '*/e2e'",
|
|
49
|
+
"-not -path '*/e2e/*'",
|
|
50
|
+
"-not -path '*/node_modules/.bin'",
|
|
51
|
+
"-not -path '*/node_modules/.bin/*'",
|
|
52
|
+
];
|
|
53
|
+
try {
|
|
54
|
+
const newer = execSync(
|
|
55
|
+
`find ${JSON.stringify(sourceDir)} -newer ${JSON.stringify(netappPath)} ${excludes.join(' ')} -print -quit`,
|
|
56
|
+
{ encoding: 'utf-8', timeout: 30_000 },
|
|
57
|
+
).trim();
|
|
58
|
+
return newer === '';
|
|
59
|
+
} catch {
|
|
60
|
+
return false; // a check that cannot answer repackages
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/network-builder.ts
CHANGED
|
@@ -38,6 +38,10 @@ async function ensureModuleDeps(hostPath: string): Promise<void> {
|
|
|
38
38
|
export class NetworkBuilder {
|
|
39
39
|
private config: NetworkConfig = {
|
|
40
40
|
topology: 'default',
|
|
41
|
+
// The production topology: celilo-mgr sits on its own control-plane
|
|
42
|
+
// network. A suite that genuinely needs the single-network legacy
|
|
43
|
+
// topology opts out with `.managementZone('internal')` and says why.
|
|
44
|
+
managementZone: 'secure-mgmt',
|
|
41
45
|
dmzMachines: [],
|
|
42
46
|
appMachines: [],
|
|
43
47
|
secureMachines: [],
|
|
@@ -72,9 +76,11 @@ export class NetworkBuilder {
|
|
|
72
76
|
|
|
73
77
|
/**
|
|
74
78
|
* Place the celilo management container in a given zone. Defaults to
|
|
75
|
-
* `
|
|
76
|
-
*
|
|
77
|
-
*
|
|
79
|
+
* `secure-mgmt` (the production topology, celilo-mgr on its own
|
|
80
|
+
* control-plane network). `internal` opts back into the legacy
|
|
81
|
+
* single-network topology, where "trusted subnet == network.internal.subnet"
|
|
82
|
+
* comes out true by construction — only for a suite that genuinely tests a
|
|
83
|
+
* behaviour on that topology, and says so.
|
|
78
84
|
*/
|
|
79
85
|
managementZone(zone: 'internal' | 'secure-mgmt'): this {
|
|
80
86
|
this.config.managementZone = zone;
|
|
@@ -278,6 +284,16 @@ export class NetworkBuilder {
|
|
|
278
284
|
return this;
|
|
279
285
|
}
|
|
280
286
|
|
|
287
|
+
/**
|
|
288
|
+
* The config this builder will hand to `startNetwork`, read back for tests
|
|
289
|
+
* that assert on the DEFAULTS rather than construct a literal. Without this
|
|
290
|
+
* a default is invisible to every gate (the literal-shaped configs in the
|
|
291
|
+
* unit tests never exercise it).
|
|
292
|
+
*/
|
|
293
|
+
snapshotConfig(): Readonly<NetworkConfig> {
|
|
294
|
+
return { ...this.config };
|
|
295
|
+
}
|
|
296
|
+
|
|
281
297
|
async start(): Promise<NetworkHandle> {
|
|
282
298
|
// Pre-flight: ensure mounted module directories have deps installed.
|
|
283
299
|
// Without this, `module import` tries to `bun install` inside Docker
|
package/src/registry-bundle.ts
CHANGED
|
@@ -28,7 +28,16 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { cpSync, existsSync, rmSync } from 'node:fs';
|
|
31
|
-
import { join } from 'node:path';
|
|
31
|
+
import { basename, join } from 'node:path';
|
|
32
|
+
|
|
33
|
+
// @psbanka - 2026-09: Test files are excluded from the bundle (ce-62u1). A
|
|
34
|
+
// bundled test copy is one directory deeper than its source, so
|
|
35
|
+
// workspace-relative imports like '../../../apps/...' resolve one level short
|
|
36
|
+
// and fail. The bundle exists to serve the sim image, which runs no tests, so
|
|
37
|
+
// the tests belong to the source package only.
|
|
38
|
+
function isTestFile(path: string): boolean {
|
|
39
|
+
return /\.test\.tsx?$/.test(basename(path));
|
|
40
|
+
}
|
|
32
41
|
|
|
33
42
|
/**
|
|
34
43
|
* Ensure <pkgDir>/registry-server/ contains a fresh copy of the
|
|
@@ -76,6 +85,9 @@ function ensureSiblingBundle(pkgDir: string, name: string, entries: string[]): v
|
|
|
76
85
|
|
|
77
86
|
rmSync(bundle, { recursive: true, force: true });
|
|
78
87
|
for (const entry of entries) {
|
|
79
|
-
cpSync(join(upstream, entry), join(bundle, entry), {
|
|
88
|
+
cpSync(join(upstream, entry), join(bundle, entry), {
|
|
89
|
+
recursive: true,
|
|
90
|
+
filter: (src) => !isTestFile(src),
|
|
91
|
+
});
|
|
80
92
|
}
|
|
81
93
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate on the wiring of the --keep exit path (celilo#1313).
|
|
3
|
+
*
|
|
4
|
+
* `cele2e run <suite> --keep` kept the per-test project but tore the SHARED
|
|
5
|
+
* infrastructure down anyway, so the kept stack had no DNS, no CA and no
|
|
6
|
+
* registry — half a system, in the one mode the debugging method prescribes.
|
|
7
|
+
* The mechanism was a wiring bug: the end-of-run `stopSharedInfra()` call
|
|
8
|
+
* carried no flagKeep guard, while the per-test path right above it was
|
|
9
|
+
* guarded. Like the sibling exit-cleanup gate, the unit gate is on the wiring;
|
|
10
|
+
* the behavioral proof (one cheap suite run with --keep, shared containers
|
|
11
|
+
* inspected afterwards) is an e2e run and needs the rig.
|
|
12
|
+
*
|
|
13
|
+
* Read the source instead of importing the runner: main() runs at import time
|
|
14
|
+
* (module top-level), so importing it would execute a docker run.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { expect, test } from 'bun:test';
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const SRC = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'runner.ts'), 'utf-8');
|
|
23
|
+
|
|
24
|
+
/** The end-of-run block, from the results summary to the last-run write. */
|
|
25
|
+
function endOfRunBlock(): string {
|
|
26
|
+
return SRC.slice(SRC.indexOf('saveTiming(history);\n saveBlockTiming(blockTiming);'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test('end-of-run stopSharedInfra is guarded by flagKeep (celilo#1313)', () => {
|
|
30
|
+
// Today the call ran unconditionally, so a --keep run destroyed exactly the
|
|
31
|
+
// shared infra the kept stack depends on.
|
|
32
|
+
const block = endOfRunBlock();
|
|
33
|
+
const guardAt = block.indexOf('if (flagKeep)');
|
|
34
|
+
const stopAt = block.indexOf('stopSharedInfra()');
|
|
35
|
+
expect(guardAt).toBeGreaterThan(-1);
|
|
36
|
+
expect(stopAt).toBeGreaterThan(-1);
|
|
37
|
+
// The unconditional call sat BEFORE any guard; after the fix the guard
|
|
38
|
+
// precedes it, so the call is inside the flagKeep branch.
|
|
39
|
+
expect(guardAt).toBeLessThan(stopAt);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('a --keep exit names what was kept, not just what stopped (celilo#1313)', () => {
|
|
43
|
+
// "Stopping shared infrastructure..." printed while the user had asked to
|
|
44
|
+
// keep things, and nothing said DNS and certs would be missing. The kept
|
|
45
|
+
// path must name the shared pieces (DNS, CA, registry) and the one teardown
|
|
46
|
+
// command that reaps them.
|
|
47
|
+
const block = endOfRunBlock();
|
|
48
|
+
const keepPath = block.slice(block.indexOf('if (flagKeep)'), block.indexOf('stopSharedInfra()'));
|
|
49
|
+
expect(keepPath).toContain('DNS');
|
|
50
|
+
expect(keepPath).toContain('cele2e down');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('the kept lock marks under --keep even when no project was captured', () => {
|
|
54
|
+
// With shared infra left up, a --keep run that failed to capture a project
|
|
55
|
+
// (startNetwork failure) still leaves a live stack. The kept lock is the
|
|
56
|
+
// instrument that says so: the next run's live-stack guard reads the same
|
|
57
|
+
// state, but the refusal reads as contention rather than a deliberate kept
|
|
58
|
+
// stack unless the lock marks. Idempotent with the per-test markKept().
|
|
59
|
+
const block = endOfRunBlock();
|
|
60
|
+
const keepPath = block.slice(block.indexOf('if (flagKeep)'), block.indexOf('} else'));
|
|
61
|
+
expect(keepPath).toContain('markKept()');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('SIGINT still stops shared infra unconditionally (abort is not a kept stack)', () => {
|
|
65
|
+
// Pinned as intended behavior: Ctrl-C is an abort, the persistent project
|
|
66
|
+
// was never written and no kept lock was marked, so tearing the shared
|
|
67
|
+
// pieces down is right. The next run's live-stack guard names the remedy
|
|
68
|
+
// for anything the abort left live.
|
|
69
|
+
const sigint = SRC.slice(SRC.indexOf("process.on('SIGINT'"), SRC.indexOf('for (let i = 0;'));
|
|
70
|
+
expect(sigint).toContain('stopSharedInfra()');
|
|
71
|
+
expect(sigint).not.toContain('flagKeep');
|
|
72
|
+
});
|
package/src/runner.ts
CHANGED
|
@@ -1080,8 +1080,25 @@ async function main() {
|
|
|
1080
1080
|
saveTiming(history);
|
|
1081
1081
|
saveBlockTiming(blockTiming);
|
|
1082
1082
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1083
|
+
// A --keep run keeps the WHOLE stack: the per-test project (persistent file
|
|
1084
|
+
// written in the loop) and the shared infra here. Tearing the shared pieces
|
|
1085
|
+
// down under --keep gave a kept stack with no name resolution and no certs
|
|
1086
|
+
// (celilo#1313) — half a system, in the mode the debugging method
|
|
1087
|
+
// prescribes. The exit stays one command: `cele2e down` sweeps every
|
|
1088
|
+
// celilo-e2e-* container, network and volume by name filter (bin/e2e-down)
|
|
1089
|
+
// and releases the kept lock, so the kept shared infra costs nothing extra
|
|
1090
|
+
// to reap. markKept() here too: with shared infra left up, a run that
|
|
1091
|
+
// captured no project still leaves a live stack, and an unmarked lock makes
|
|
1092
|
+
// the next run's live-stack refusal read as contention instead of a
|
|
1093
|
+
// deliberate kept stack. Idempotent with the per-test markKept().
|
|
1094
|
+
if (flagKeep) {
|
|
1095
|
+
markKept();
|
|
1096
|
+
console.log(`${dim}Keeping shared infrastructure (DNS, CA, registry) for debugging.${reset}`);
|
|
1097
|
+
console.log(`${dim} Tear down everything (project + shared infra): cele2e down${reset}`);
|
|
1098
|
+
} else {
|
|
1099
|
+
console.log(`${dim}Stopping shared infrastructure...${reset}`);
|
|
1100
|
+
stopSharedInfra();
|
|
1101
|
+
}
|
|
1085
1102
|
|
|
1086
1103
|
const suiteDuration = Math.floor((Date.now() - suiteStart) / 1000);
|
|
1087
1104
|
|
package/src/types.ts
CHANGED
|
@@ -120,14 +120,16 @@ export interface NetworkConfig {
|
|
|
120
120
|
*/
|
|
121
121
|
signalRelease: boolean;
|
|
122
122
|
/**
|
|
123
|
-
* Which zone the celilo management container sits in. Defaults to
|
|
124
|
-
* — the
|
|
123
|
+
* Which zone the celilo management container sits in. Defaults to
|
|
124
|
+
* `secure-mgmt` — the production topology, celilo-mgr on its own
|
|
125
|
+
* control-plane network. (It used to default to `internal`, the
|
|
126
|
+
* single-network topology, and that default is why the control-plane-network
|
|
127
|
+
* bug reached production: with management always on `internal`, the hardcoded
|
|
128
|
+
* "trusted subnet == network.internal.subnet" assumption was accidentally
|
|
129
|
+
* true in every test.)
|
|
125
130
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* gap is why the control-plane-network bug reached production: with management
|
|
129
|
-
* always on `internal`, the hardcoded "trusted subnet == network.internal.subnet"
|
|
130
|
-
* assumption was accidentally true in every test.
|
|
131
|
+
* A suite that genuinely needs the legacy single-network topology sets
|
|
132
|
+
* `internal` EXPLICITLY and says why in a comment.
|
|
131
133
|
*/
|
|
132
134
|
managementZone?: 'internal' | 'secure-mgmt';
|
|
133
135
|
/**
|
|
@@ -290,6 +292,18 @@ export interface NetworkHandle {
|
|
|
290
292
|
*/
|
|
291
293
|
moduleHost(moduleId: string): Promise<ModuleHost>;
|
|
292
294
|
|
|
295
|
+
/**
|
|
296
|
+
* The IPv4 address a module's deploy recorded, from `celilo module where
|
|
297
|
+
* --json` (the `module_systems` inventory).
|
|
298
|
+
*
|
|
299
|
+
* Replaces `grep target_ip` over the module's `generated/` tree, which D4
|
|
300
|
+
* of control-plane-stops-building-modules deletes after every successful
|
|
301
|
+
* deploy (celilo#1334). The inventory is the deploy's own durable record of
|
|
302
|
+
* the address, so this works on the machine pool and the container service
|
|
303
|
+
* alike. Throws when the deploy recorded nothing.
|
|
304
|
+
*/
|
|
305
|
+
targetIp(moduleId: string): Promise<string>;
|
|
306
|
+
|
|
293
307
|
/**
|
|
294
308
|
* Tell celilo where its own control plane lives, and wait until the box is
|
|
295
309
|
* usable again.
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { ADMIN_SCOPE, TokenAuth, hashToken } from './auth';
|
|
3
|
-
|
|
4
|
-
describe('TokenAuth (package-scoped — ISS-0140)', () => {
|
|
5
|
-
test('empty specs → no tokens; nothing authorizes', () => {
|
|
6
|
-
const auth = new TokenAuth([]);
|
|
7
|
-
expect(auth.hasTokens()).toBe(false);
|
|
8
|
-
expect(auth.authorize('anything', 'caddy')).toBe(false);
|
|
9
|
-
expect(auth.isAdmin('anything')).toBe(false);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
test('admin-scoped token publishes ANY package', () => {
|
|
13
|
-
const auth = new TokenAuth([{ token: 'admin-tok', scope: ADMIN_SCOPE }]);
|
|
14
|
-
expect(auth.authorize('admin-tok', 'caddy')).toBe(true);
|
|
15
|
-
expect(auth.authorize('admin-tok', 'lunacycle')).toBe(true);
|
|
16
|
-
expect(auth.isAdmin('admin-tok')).toBe(true);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('package-scoped token publishes ONLY its package', () => {
|
|
20
|
-
const auth = new TokenAuth([{ token: 'luna-tok', scope: 'lunacycle' }]);
|
|
21
|
-
expect(auth.authorize('luna-tok', 'lunacycle')).toBe(true);
|
|
22
|
-
expect(auth.authorize('luna-tok', 'caddy')).toBe(false);
|
|
23
|
-
// A scoped token is NOT an admin token — it cannot mint.
|
|
24
|
-
expect(auth.isAdmin('luna-tok')).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('unknown token never authorizes', () => {
|
|
28
|
-
const auth = new TokenAuth([{ token: 'known', scope: 'lunacycle' }]);
|
|
29
|
-
expect(auth.authorize('unknown', 'lunacycle')).toBe(false);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('tokens are stored hashed, not in cleartext', () => {
|
|
33
|
-
const auth = new TokenAuth([{ token: 'secret-token', scope: ADMIN_SCOPE }]);
|
|
34
|
-
expect(JSON.stringify(auth)).not.toContain('secret-token');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('tolerates a Bearer prefix and surrounding whitespace', () => {
|
|
38
|
-
const auth = new TokenAuth([{ token: 'clean', scope: ADMIN_SCOPE }]);
|
|
39
|
-
expect(auth.authorize(' clean ', 'caddy')).toBe(true);
|
|
40
|
-
expect(auth.authorize('Bearer clean', 'caddy')).toBe(true);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('empty header rejected', () => {
|
|
44
|
-
const auth = new TokenAuth([{ token: 'secret', scope: ADMIN_SCOPE }]);
|
|
45
|
-
expect(auth.authorize('', 'caddy')).toBe(false);
|
|
46
|
-
expect(auth.scopeOf('')).toBe(null);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('addHashed / removeHashed manage minted tokens at runtime', () => {
|
|
50
|
-
const auth = new TokenAuth([]);
|
|
51
|
-
const hash = hashToken('minted-tok');
|
|
52
|
-
auth.addHashed(hash, 'lunacycle');
|
|
53
|
-
expect(auth.authorize('minted-tok', 'lunacycle')).toBe(true);
|
|
54
|
-
auth.removeHashed(hash);
|
|
55
|
-
expect(auth.authorize('minted-tok', 'lunacycle')).toBe(false);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('TokenAuth.fromEnv (PUBLISH_TOKENS format)', () => {
|
|
60
|
-
test('bare line → admin scope; "token pkg" line → scoped', () => {
|
|
61
|
-
process.env.PUBLISH_TOKENS = 'admin-tok\nluna-tok lunacycle\n';
|
|
62
|
-
const auth = TokenAuth.fromEnv();
|
|
63
|
-
expect(auth.isAdmin('admin-tok')).toBe(true);
|
|
64
|
-
expect(auth.authorize('luna-tok', 'lunacycle')).toBe(true);
|
|
65
|
-
expect(auth.authorize('luna-tok', 'caddy')).toBe(false);
|
|
66
|
-
process.env.PUBLISH_TOKENS = '';
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('blank lines ignored', () => {
|
|
70
|
-
process.env.PUBLISH_TOKENS = '\n \nt1\n';
|
|
71
|
-
const auth = TokenAuth.fromEnv();
|
|
72
|
-
expect(auth.hasTokens()).toBe(true);
|
|
73
|
-
expect(auth.isAdmin('t1')).toBe(true);
|
|
74
|
-
process.env.PUBLISH_TOKENS = '';
|
|
75
|
-
});
|
|
76
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lockstep gate (ISS-0046): the registry-server's on-demand `walkDir` packager
|
|
3
|
-
* can't import the canonical node_modules-bundling rule at runtime (this server
|
|
4
|
-
* ships standalone, with no @celilo deps), so it carries a structural copy.
|
|
5
|
-
* This test imports the canonical rule directly (zero-dependency module, safe to
|
|
6
|
-
* pull in at test time from the workspace) and asserts walkDir's actual output
|
|
7
|
-
* conforms to it — so a drift in either copy fails here instead of shipping a
|
|
8
|
-
* broken `.netapp`.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
12
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
13
|
-
import { tmpdir } from 'node:os';
|
|
14
|
-
import { join } from 'node:path';
|
|
15
|
-
// Canonical rule lives in apps/celilo; importing it cross-package is fine in a
|
|
16
|
-
// workspace test (it's not bundled into the standalone registry-sim image).
|
|
17
|
-
import { includeNodeModulesPath } from '../../../apps/celilo/src/module/packaging/package-rules';
|
|
18
|
-
import { walkDir } from './bootstrap';
|
|
19
|
-
|
|
20
|
-
describe('bootstrap walkDir stays in lockstep with the canonical packaging rule', () => {
|
|
21
|
-
let dir: string;
|
|
22
|
-
|
|
23
|
-
// Representative node_modules paths spanning both branches of the rule.
|
|
24
|
-
const nmFiles = [
|
|
25
|
-
'scripts/node_modules/@celilo/capabilities/src/dns-internal.ts',
|
|
26
|
-
'scripts/node_modules/tldts/index.js',
|
|
27
|
-
'scripts/node_modules/drizzle-orm/index.js',
|
|
28
|
-
'scripts/node_modules/.bin/tldts',
|
|
29
|
-
'node_modules/@celilo/capabilities/index.js',
|
|
30
|
-
'node_modules/lodash/index.js',
|
|
31
|
-
'node_modules/@celilo/e2e/index.js',
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
function write(rel: string): void {
|
|
35
|
-
const full = join(dir, rel);
|
|
36
|
-
mkdirSync(join(full, '..'), { recursive: true });
|
|
37
|
-
writeFileSync(full, 'x');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
beforeEach(() => {
|
|
41
|
-
dir = mkdtempSync(join(tmpdir(), 'celilo-bootstrap-rule-'));
|
|
42
|
-
write('manifest.yml');
|
|
43
|
-
write('scripts/on-install.ts');
|
|
44
|
-
for (const f of nmFiles) write(f);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
afterEach(() => {
|
|
48
|
-
try {
|
|
49
|
-
rmSync(dir, { recursive: true, force: true });
|
|
50
|
-
} catch {
|
|
51
|
-
/* ignore */
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("walkDir's node_modules decisions match includeNodeModulesPath", () => {
|
|
56
|
-
const bundled = new Set(walkDir(dir));
|
|
57
|
-
for (const rel of nmFiles) {
|
|
58
|
-
expect(bundled.has(rel)).toBe(includeNodeModulesPath(rel));
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('bundles scripts deps (tldts), drops .bin shims and non-scripts cruft', () => {
|
|
63
|
-
const bundled = new Set(walkDir(dir));
|
|
64
|
-
expect(bundled.has('scripts/node_modules/tldts/index.js')).toBe(true);
|
|
65
|
-
expect(bundled.has('scripts/node_modules/drizzle-orm/index.js')).toBe(true);
|
|
66
|
-
expect(bundled.has('scripts/node_modules/.bin/tldts')).toBe(false);
|
|
67
|
-
expect(bundled.has('node_modules/lodash/index.js')).toBe(false);
|
|
68
|
-
expect(bundled.has('node_modules/@celilo/e2e/index.js')).toBe(false);
|
|
69
|
-
expect(bundled.has('node_modules/@celilo/capabilities/index.js')).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
});
|