@celilo/e2e 0.11.0 → 0.11.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/npm-registry-server/src/packument.test.ts +2 -5
- package/npm-registry-server/src/packument.ts +3 -3
- package/npm-registry-server/src/server.ts +1 -1
- package/package.json +3 -3
- package/registry-server/src/bootstrap.ts +1 -1
- package/registry-server/src/introspection.test.ts +8 -12
- package/registry-server/src/server.test.ts +2 -2
- package/registry-server/src/server.ts +1 -1
- package/registry-server/src/storage.test.ts +8 -4
- package/registry-server/src/storage.ts +1 -1
- package/registry-server/src/validation.ts +2 -4
- package/scripts/consumer-paths-changed.ts +83 -0
- package/scripts/pack-celilo-packages.ts +1 -4
- package/scripts/stage-apt-repo.ts +1 -1
- package/scripts/stage-libsignal.ts +1 -3
- package/simulators/greenwave/server.ts +3 -1
- package/simulators/greenwave/state.ts +17 -10
- package/simulators/isitup/server.ts +1 -5
- package/simulators/namecheap-ddns/server.ts +2 -4
- package/simulators/namecheap-ddns/zone-updater.test.ts +3 -1
- package/simulators/namecheap-ddns/zone-updater.ts +6 -10
- package/simulators/signal-cli/server.ts +3 -1
- package/src/browser.ts +3 -3
- package/src/bus-events.ts +2 -2
- package/src/cli/command-tree-parser.ts +8 -2
- package/src/cli/completion.ts +5 -8
- package/src/cli/index.ts +9 -2
- package/src/cli/module-discovery.ts +3 -1
- package/src/cli-version-contract.ts +5 -6
- package/src/extract-failure.ts +92 -0
- package/src/fixtures.ts +4 -11
- package/src/network-builder.ts +12 -7
- package/src/observer.test.ts +42 -11
- package/src/observer.ts +12 -5
- package/src/registry-bundle.ts +1 -3
- package/src/run-lock.test.ts +2 -2
- package/src/run-lock.ts +19 -5
- package/src/runner.ts +9 -67
- package/src/shared-infra.ts +50 -22
- package/src/socks-proxy.ts +14 -17
- package/src/vantage-helpers.ts +5 -1
- package/src/vantage.test.ts +78 -24
- package/src/vantage.ts +24 -17
- package/src/wait-for-run.ts +1 -1
- package/src/zone-classifier.test.ts +7 -1
- package/src/zone-classifier.ts +2 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
1
2
|
import { execSync } from 'node:child_process';
|
|
2
3
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
5
|
import { join } from 'node:path';
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
buildPackument,
|
|
@@ -28,10 +28,7 @@ function packFixture(name: string, version: string): string {
|
|
|
28
28
|
const slug = `${name.replace(/[@/]/g, '_')}-${version}`;
|
|
29
29
|
const pkgDir = join(tmp, 'pkg', slug);
|
|
30
30
|
mkdirSync(pkgDir, { recursive: true });
|
|
31
|
-
writeFileSync(
|
|
32
|
-
join(pkgDir, 'package.json'),
|
|
33
|
-
JSON.stringify({ name, version, main: 'index.js' }),
|
|
34
|
-
);
|
|
31
|
+
writeFileSync(join(pkgDir, 'package.json'), JSON.stringify({ name, version, main: 'index.js' }));
|
|
35
32
|
writeFileSync(join(pkgDir, 'index.js'), `module.exports = '${name}@${version}';\n`);
|
|
36
33
|
|
|
37
34
|
const stagingDir = join(tmp, 'staging', slug);
|
|
@@ -52,7 +52,7 @@ function* iterTarEntries(tar: Buffer): Generator<{ name: string; data: Buffer }>
|
|
|
52
52
|
|
|
53
53
|
const rawName = header.subarray(0, 100).toString('utf-8').replace(/\0.*$/, '');
|
|
54
54
|
const sizeStr = header.subarray(124, 136).toString('utf-8').replace(/\0.*$/, '').trim();
|
|
55
|
-
const size = parseInt(sizeStr, 8) || 0;
|
|
55
|
+
const size = Number.parseInt(sizeStr, 8) || 0;
|
|
56
56
|
const dataStart = offset + 512;
|
|
57
57
|
const dataEnd = dataStart + size;
|
|
58
58
|
|
|
@@ -109,8 +109,8 @@ export function loadTarballsFromDir(dir: string): TarballMeta[] {
|
|
|
109
109
|
*/
|
|
110
110
|
export function highestVersion(versions: string[]): string {
|
|
111
111
|
return [...versions].sort((a, b) => {
|
|
112
|
-
const pa = a.split('.').map((s) => parseInt(s, 10) || 0);
|
|
113
|
-
const pb = b.split('.').map((s) => parseInt(s, 10) || 0);
|
|
112
|
+
const pa = a.split('.').map((s) => Number.parseInt(s, 10) || 0);
|
|
113
|
+
const pb = b.split('.').map((s) => Number.parseInt(s, 10) || 0);
|
|
114
114
|
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
115
115
|
const da = pa[i] ?? 0;
|
|
116
116
|
const db = pb[i] ?? 0;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* different staging directory in tests.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { indexTarballs, loadTarballsFromDir
|
|
19
|
+
import { type TarballMeta, indexTarballs, loadTarballsFromDir } from './packument';
|
|
20
20
|
import { buildPackument } from './packument';
|
|
21
21
|
|
|
22
22
|
interface ServerOptions {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/e2e",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "E2E test infrastructure for Celilo-deployed applications. Provides a simulated internet with DNS hierarchy, ACME server, firewalls, and target machines in Docker.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"README.md"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@celilo/cli-display": "^0.1.
|
|
41
|
-
"@celilo/event-bus": "^0.
|
|
40
|
+
"@celilo/cli-display": "^0.1.10",
|
|
41
|
+
"@celilo/event-bus": "^0.2.0",
|
|
42
42
|
"yaml": "^2.8.0",
|
|
43
43
|
"zod": "^3.24.1"
|
|
44
44
|
},
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
* inert — storage.ts is the only code that touches disk.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
18
19
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
19
20
|
import { basename, dirname, join, relative } from 'node:path';
|
|
20
|
-
import { createHash } from 'node:crypto';
|
|
21
21
|
import type { IndexEntry } from './storage';
|
|
22
22
|
|
|
23
23
|
export interface BootstrapEntry {
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from 'bun:test';
|
|
2
2
|
import { ADMIN_SCOPE } from './auth';
|
|
3
|
-
import {
|
|
4
|
-
IntrospectionVerifier,
|
|
5
|
-
claimToScope,
|
|
6
|
-
introspectionConfigFromEnv,
|
|
7
|
-
} from './introspection';
|
|
3
|
+
import { IntrospectionVerifier, claimToScope, introspectionConfigFromEnv } from './introspection';
|
|
8
4
|
|
|
9
5
|
const CONFIG = {
|
|
10
6
|
endpoint: 'https://idp.test/application/o/introspect/',
|
|
@@ -88,9 +84,7 @@ describe('IntrospectionVerifier.scopeOf', () => {
|
|
|
88
84
|
const verifier = new IntrospectionVerifier(CONFIG);
|
|
89
85
|
|
|
90
86
|
test('active admin-group token → ADMIN_SCOPE', async () => {
|
|
91
|
-
stubFetch(() =>
|
|
92
|
-
introspectResponse({ active: true, sub: 'alice', groups: ['celilo-admins'] }),
|
|
93
|
-
);
|
|
87
|
+
stubFetch(() => introspectResponse({ active: true, sub: 'alice', groups: ['celilo-admins'] }));
|
|
94
88
|
expect(await verifier.scopeOf('Bearer idp-token')).toBe(ADMIN_SCOPE);
|
|
95
89
|
});
|
|
96
90
|
|
|
@@ -106,9 +100,7 @@ describe('IntrospectionVerifier.scopeOf', () => {
|
|
|
106
100
|
|
|
107
101
|
test('expired exp → null even if active', async () => {
|
|
108
102
|
const pastSec = Math.floor(Date.now() / 1000) - 60;
|
|
109
|
-
stubFetch(() =>
|
|
110
|
-
introspectResponse({ active: true, groups: ['celilo-admins'], exp: pastSec }),
|
|
111
|
-
);
|
|
103
|
+
stubFetch(() => introspectResponse({ active: true, groups: ['celilo-admins'], exp: pastSec }));
|
|
112
104
|
expect(await verifier.scopeOf('idp-token')).toBeNull();
|
|
113
105
|
});
|
|
114
106
|
|
|
@@ -238,7 +230,11 @@ describe('IntrospectionVerifier.identify (ce-1ch)', () => {
|
|
|
238
230
|
});
|
|
239
231
|
|
|
240
232
|
test('admin group is configurable, not a literal', async () => {
|
|
241
|
-
const custom = new IntrospectionVerifier({
|
|
233
|
+
const custom = new IntrospectionVerifier({
|
|
234
|
+
...CONFIG,
|
|
235
|
+
adminGroup: 'ops',
|
|
236
|
+
publisherGroup: 'devs',
|
|
237
|
+
});
|
|
242
238
|
stubFetch(() => introspectResponse({ active: true, sub: 'dan', groups: ['ops'] }));
|
|
243
239
|
const id = await custom.identify('Bearer t');
|
|
244
240
|
expect(id?.isAdmin).toBe(true);
|
|
@@ -714,7 +714,7 @@ describe('module-owner authorization (ce-1ch)', () => {
|
|
|
714
714
|
const res = await fetch(`${url}/api/v1/modules/owners/caddy`, {
|
|
715
715
|
headers: { Authorization: 'Bearer opaque-admin' },
|
|
716
716
|
});
|
|
717
|
-
expect((await res.json() as { owner: { ownerSub: string } }).owner.ownerSub).toBe('ops');
|
|
717
|
+
expect(((await res.json()) as { owner: { ownerSub: string } }).owner.ownerSub).toBe('ops');
|
|
718
718
|
});
|
|
719
719
|
|
|
720
720
|
test('admin reassign lets a new owner publish the name', async () => {
|
|
@@ -741,7 +741,7 @@ describe('module-owner authorization (ce-1ch)', () => {
|
|
|
741
741
|
headers: { Authorization: 'Bearer opaque-admin' },
|
|
742
742
|
});
|
|
743
743
|
expect(ok.status).toBe(200);
|
|
744
|
-
expect((await ok.json() as { owners: unknown[] }).owners).toEqual([]);
|
|
744
|
+
expect(((await ok.json()) as { owners: unknown[] }).owners).toEqual([]);
|
|
745
745
|
});
|
|
746
746
|
|
|
747
747
|
test('owner reassign requires admin (a plain publisher cannot)', async () => {
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
|
|
22
22
|
import { join } from 'node:path';
|
|
23
23
|
import { ADMIN_SCOPE, TokenAuth } from './auth';
|
|
24
|
-
import { IntrospectionVerifier } from './introspection';
|
|
25
24
|
import {
|
|
26
25
|
type BootstrapEntry,
|
|
27
26
|
bootstrapIndexEntry,
|
|
@@ -29,6 +28,7 @@ import {
|
|
|
29
28
|
scanBootstrapDir,
|
|
30
29
|
scanUploadsDir,
|
|
31
30
|
} from './bootstrap';
|
|
31
|
+
import { IntrospectionVerifier } from './introspection';
|
|
32
32
|
import { landingResponse } from './landing';
|
|
33
33
|
import { ModuleOwnerStore, fileModuleOwnerPersistence } from './module-owner-store';
|
|
34
34
|
import { type RateLimiter, clientIp, createRateLimiter } from './rate-limit';
|
|
@@ -37,9 +37,7 @@ describe('RegistryStorage.indexPath', () => {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
test('long name uses first 4 chars for directories', () => {
|
|
40
|
-
expect(storage.indexPath('homebridge')).toBe(
|
|
41
|
-
join(dataDir, 'index', 'ho', 'me', 'homebridge'),
|
|
42
|
-
);
|
|
40
|
+
expect(storage.indexPath('homebridge')).toBe(join(dataDir, 'index', 'ho', 'me', 'homebridge'));
|
|
43
41
|
});
|
|
44
42
|
|
|
45
43
|
test('real module names produce expected paths', () => {
|
|
@@ -128,7 +126,13 @@ describe('RegistryStorage listModules / getModule', () => {
|
|
|
128
126
|
|
|
129
127
|
test('listModules returns stored modules with their index entries', () => {
|
|
130
128
|
storage.storePackage('homebridge', '1.0.0+1', Buffer.from('a'));
|
|
131
|
-
storage.appendIndex({
|
|
129
|
+
storage.appendIndex({
|
|
130
|
+
name: 'homebridge',
|
|
131
|
+
vers: '1.0.0+1',
|
|
132
|
+
deps: [],
|
|
133
|
+
cksum: 'x',
|
|
134
|
+
yanked: false,
|
|
135
|
+
});
|
|
132
136
|
const list = storage.listModules();
|
|
133
137
|
expect(list).toHaveLength(1);
|
|
134
138
|
expect(list[0].name).toBe('homebridge');
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* downloads/{name} — per-module download counter (single integer)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
10
|
import { createHash } from 'node:crypto';
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { dirname, join } from 'node:path';
|
|
13
13
|
|
|
14
14
|
export interface IndexEntry {
|
|
@@ -44,16 +44,14 @@ export function validateNameAndVersion(
|
|
|
44
44
|
return {
|
|
45
45
|
ok: false,
|
|
46
46
|
field: 'name',
|
|
47
|
-
message:
|
|
48
|
-
'Invalid module name — must be kebab-case lowercase, 1-128 chars (e.g. "my-module")',
|
|
47
|
+
message: 'Invalid module name — must be kebab-case lowercase, 1-128 chars (e.g. "my-module")',
|
|
49
48
|
};
|
|
50
49
|
}
|
|
51
50
|
if (!isValidVersion(vers)) {
|
|
52
51
|
return {
|
|
53
52
|
ok: false,
|
|
54
53
|
field: 'vers',
|
|
55
|
-
message:
|
|
56
|
-
'Invalid version — must match "<major>.<minor>.<patch>+<pkgrev>" (e.g. "1.4.2+1")',
|
|
54
|
+
message: 'Invalid version — must match "<major>.<minor>.<patch>+<pkgrev>" (e.g. "1.4.2+1")',
|
|
57
55
|
};
|
|
58
56
|
}
|
|
59
57
|
return { ok: true };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Does this push touch the npm-consumer-smoke consumer path?
|
|
4
|
+
*
|
|
5
|
+
* This list used to live in `npm-consumer-smoke.yml`'s `on.push.paths`, where
|
|
6
|
+
* it decided whether the WORKFLOW TRIGGERED at all. That made a required status
|
|
7
|
+
* check that often posted no status: a push touching none of these paths
|
|
8
|
+
* created no run, so the `npm-consumer-smoke*` context branch protection
|
|
9
|
+
* requires never reported, and the PR was blocked on a check that was never
|
|
10
|
+
* going to run (celilo#603).
|
|
11
|
+
*
|
|
12
|
+
* The workflow now always triggers and always reports; this decides whether it
|
|
13
|
+
* does the expensive Docker work. Same paths, same intent — the filter moved
|
|
14
|
+
* from "should CI speak" to "should CI do the work".
|
|
15
|
+
*
|
|
16
|
+
* DEFAULT TO RUNNING. Every uncertainty here resolves to `true`. Skipping when
|
|
17
|
+
* we should have run is a silent false green on a release-path gate; running
|
|
18
|
+
* when we needn't costs a few minutes of builder time.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Kept verbatim from the trigger's former `paths:` list. */
|
|
22
|
+
export const CONSUMER_PATHS = [
|
|
23
|
+
'packages/e2e/**',
|
|
24
|
+
'packages/registry-server/**',
|
|
25
|
+
// The publish driver (moved from the dead apps/celilo/scripts/publish.ts).
|
|
26
|
+
'apps/celilo/src/cli/commands/publish/**',
|
|
27
|
+
'scripts/publish.ts',
|
|
28
|
+
// (The derived release graph lives at packages/e2e/scripts/workspace-graph.ts,
|
|
29
|
+
// already covered by packages/e2e/** above.)
|
|
30
|
+
// Every workspace package.json — the actual input to the package graph.
|
|
31
|
+
// Narrowed to workspace members (not **/package.json) so a devDependency bump
|
|
32
|
+
// in an unrelated dir doesn't fire this heavy Docker job; the cheap sync gate
|
|
33
|
+
// in ci.yml catches graph drift on every PR regardless.
|
|
34
|
+
'apps/*/package.json',
|
|
35
|
+
'packages/*/package.json',
|
|
36
|
+
'package.json',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
interface PushCommit {
|
|
40
|
+
added?: string[];
|
|
41
|
+
modified?: string[];
|
|
42
|
+
removed?: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PushPayload {
|
|
46
|
+
commits?: PushCommit[];
|
|
47
|
+
total_commits?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function touchesConsumerPath(files: string[]): boolean {
|
|
51
|
+
return files.some((file) => CONSUMER_PATHS.some((glob) => new Bun.Glob(glob).match(file)));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function pushIsRelevant(payload: PushPayload): boolean {
|
|
55
|
+
const commits = payload.commits ?? [];
|
|
56
|
+
|
|
57
|
+
// No commit list to reason from — a payload shape we don't understand is not
|
|
58
|
+
// evidence that nothing changed.
|
|
59
|
+
if (commits.length === 0) return true;
|
|
60
|
+
|
|
61
|
+
// Forgejo caps `commits[]` on a large push while `total_commits` reports the
|
|
62
|
+
// real number. The unlisted commits are exactly the ones we cannot inspect.
|
|
63
|
+
if ((payload.total_commits ?? commits.length) > commits.length) return true;
|
|
64
|
+
|
|
65
|
+
// A commit carrying none of the three arrays told us nothing about its
|
|
66
|
+
// files. That is not the same as a commit that changed nothing — no such
|
|
67
|
+
// commit exists — so it is an unknown, and unknowns run.
|
|
68
|
+
if (commits.some((c) => !c.added && !c.modified && !c.removed)) return true;
|
|
69
|
+
|
|
70
|
+
return touchesConsumerPath(
|
|
71
|
+
commits.flatMap((c) => [...(c.added ?? []), ...(c.modified ?? []), ...(c.removed ?? [])]),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (import.meta.main) {
|
|
76
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
77
|
+
// A manual dispatch (or any non-push event) means someone asked for it.
|
|
78
|
+
const relevant =
|
|
79
|
+
process.env.GITHUB_EVENT_NAME !== 'push' || !eventPath
|
|
80
|
+
? true
|
|
81
|
+
: pushIsRelevant(await Bun.file(eventPath).json());
|
|
82
|
+
console.log(relevant ? 'true' : 'false');
|
|
83
|
+
}
|
|
@@ -127,10 +127,7 @@ export function rewriteWorkspacePins(
|
|
|
127
127
|
const version = versions.get(name);
|
|
128
128
|
if (!version) {
|
|
129
129
|
throw new Error(
|
|
130
|
-
`Cannot rewrite ${bucket} "${name}": "${spec}" — ${name} is not a known
|
|
131
|
-
`@celilo/* workspace package. Add it to the workspace, or fix the dep name. ` +
|
|
132
|
-
`(This is the loud failure the old hardcoded pack list lacked — ` +
|
|
133
|
-
`see apps/celilo/designs/WORKSPACE_PACKAGE_GRAPH.md.)`,
|
|
130
|
+
`Cannot rewrite ${bucket} "${name}": "${spec}" — ${name} is not a known @celilo/* workspace package. Add it to the workspace, or fix the dep name. (This is the loud failure the old hardcoded pack list lacked — see apps/celilo/designs/WORKSPACE_PACKAGE_GRAPH.md.)`,
|
|
134
131
|
);
|
|
135
132
|
}
|
|
136
133
|
const range = spec.slice('workspace:'.length);
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { spawnSync } from 'node:child_process';
|
|
27
|
-
import { cpSync, existsSync, mkdirSync,
|
|
27
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
28
28
|
import { join } from 'node:path';
|
|
29
29
|
|
|
30
30
|
const green = '\x1b[32m';
|
|
@@ -102,9 +102,7 @@ export function stageLibsignal(repoRoot: string, pkgDir: string): boolean {
|
|
|
102
102
|
console.log(`${yellow}skip${reset} ${dim}(build failed)${reset}`);
|
|
103
103
|
return false;
|
|
104
104
|
}
|
|
105
|
-
console.log(
|
|
106
|
-
`${green}✔${reset} ${dim}${Math.round((Date.now() - t0) / 1000)}s${reset}`,
|
|
107
|
-
);
|
|
105
|
+
console.log(`${green}✔${reset} ${dim}${Math.round((Date.now() - t0) / 1000)}s${reset}`);
|
|
108
106
|
}
|
|
109
107
|
|
|
110
108
|
process.stdout.write(` ${'libsignal-jni (stage)'.padEnd(28)} `);
|
|
@@ -161,4 +161,6 @@ const server = Bun.serve({
|
|
|
161
161
|
},
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
console.log(
|
|
164
|
+
console.log(
|
|
165
|
+
`[greenwave] C4000XG simulator listening on ${BIND_HOST}:${server.port} (${tlsOptions ? 'HTTPS' : 'HTTP'})`,
|
|
166
|
+
);
|
|
@@ -117,7 +117,12 @@ export function setDhcpPool(params: Record<string, string>): CgiObject[] {
|
|
|
117
117
|
rewriteDnsmasqConfig();
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
return [
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
ObjName: 'Device.DHCPv4.Server.Pool.1.',
|
|
123
|
+
Param: [{ ParamName: 'Success', ParamValue: 'Success' }],
|
|
124
|
+
},
|
|
125
|
+
];
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
function rewriteDnsmasqConfig(): void {
|
|
@@ -144,10 +149,13 @@ function rewriteDnsmasqConfig(): void {
|
|
|
144
149
|
writeFileSync('/etc/dnsmasq.d/e2e.conf', `${lines.join('\n')}\n`);
|
|
145
150
|
// dnsmasq requires full restart to re-read config files (SIGHUP only reloads /etc/hosts)
|
|
146
151
|
try {
|
|
147
|
-
execSync(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
execSync(
|
|
153
|
+
'pkill -9 dnsmasq; sleep 0.5; dnsmasq --conf-dir=/etc/dnsmasq.d --log-facility=/dev/stderr',
|
|
154
|
+
{
|
|
155
|
+
timeout: 5000,
|
|
156
|
+
shell: '/bin/bash',
|
|
157
|
+
},
|
|
158
|
+
);
|
|
151
159
|
} catch {}
|
|
152
160
|
console.log(`[greenwave] dnsmasq restarted: DNS=${dnsServers}, domain=${domainName}`);
|
|
153
161
|
} catch (err) {
|
|
@@ -168,10 +176,9 @@ function applyIptablesRule(mapping: PortMapping): void {
|
|
|
168
176
|
{ timeout: 5000 },
|
|
169
177
|
);
|
|
170
178
|
// Allow forwarded traffic
|
|
171
|
-
execSync(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
);
|
|
179
|
+
execSync(`iptables -A FORWARD -p ${p} -d ${intIp} --dport ${intPort} -j ACCEPT`, {
|
|
180
|
+
timeout: 5000,
|
|
181
|
+
});
|
|
175
182
|
}
|
|
176
183
|
|
|
177
184
|
try {
|
|
@@ -180,7 +187,7 @@ function applyIptablesRule(mapping: PortMapping): void {
|
|
|
180
187
|
addRules('udp');
|
|
181
188
|
}
|
|
182
189
|
} catch (err) {
|
|
183
|
-
console.error(
|
|
190
|
+
console.error('[greenwave] Failed to apply iptables rule:', err);
|
|
184
191
|
}
|
|
185
192
|
}
|
|
186
193
|
|
|
@@ -41,11 +41,7 @@ async function probe(targetUrl: string): Promise<ProbeResponse> {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const port = parsed.port
|
|
45
|
-
? Number(parsed.port)
|
|
46
|
-
: parsed.protocol === 'https:'
|
|
47
|
-
? 443
|
|
48
|
-
: 80;
|
|
44
|
+
const port = parsed.port ? Number(parsed.port) : parsed.protocol === 'https:' ? 443 : 80;
|
|
49
45
|
|
|
50
46
|
const start = Date.now();
|
|
51
47
|
try {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* Returns XML matching the real Namecheap response format.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { updateZone } from './zone-updater';
|
|
12
11
|
import type { DdnsRecord } from './types';
|
|
12
|
+
import { updateZone } from './zone-updater';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Minimal structural type for the bits of Bun's server we use — avoids
|
|
@@ -56,9 +56,7 @@ function expectedPasswordFor(domain: string): string {
|
|
|
56
56
|
const records: Map<string, DdnsRecord> = new Map();
|
|
57
57
|
|
|
58
58
|
function xmlResponse(errCount: number, ip: string, errors: string[]): Response {
|
|
59
|
-
const errorElements = errors
|
|
60
|
-
.map((e, i) => `<Err${i + 1}>${e}</Err${i + 1}>`)
|
|
61
|
-
.join('\n ');
|
|
59
|
+
const errorElements = errors.map((e, i) => `<Err${i + 1}>${e}</Err${i + 1}>`).join('\n ');
|
|
62
60
|
|
|
63
61
|
const xml = `<?xml version="1.0" encoding="utf-16"?>
|
|
64
62
|
<interface-response>
|
|
@@ -23,7 +23,9 @@ describe('namecheap-ddns protected records (e2e-confidence #257)', () => {
|
|
|
23
23
|
// The guard runs before any readFileSync, so this throws the protection
|
|
24
24
|
// error (not an ENOENT) even with no /config present — proving a DDNS apex
|
|
25
25
|
// clobber of the website-sim can never reach the zone file.
|
|
26
|
-
expect(() => updateZone('celilo.computer', '@', '100.64.0.1')).toThrow(
|
|
26
|
+
expect(() => updateZone('celilo.computer', '@', '100.64.0.1')).toThrow(
|
|
27
|
+
/reserved e2e infrastructure/,
|
|
28
|
+
);
|
|
27
29
|
expect(() => updateZone('celilo.computer', 'www', '100.64.0.1')).toThrow(
|
|
28
30
|
/reserved e2e infrastructure/,
|
|
29
31
|
);
|
|
@@ -33,21 +33,17 @@ export function isProtectedRecord(domain: string, host: string): boolean {
|
|
|
33
33
|
export function updateZone(domain: string, host: string, ip: string): void {
|
|
34
34
|
if (isProtectedRecord(domain, host)) {
|
|
35
35
|
throw new Error(
|
|
36
|
-
`Refusing DDNS update to "${host}.${domain}": it is reserved e2e infrastructure
|
|
37
|
-
`(the website-sim), not a DDNS-managed host (e2e-confidence #257).`,
|
|
36
|
+
`Refusing DDNS update to "${host}.${domain}": it is reserved e2e infrastructure (the website-sim), not a DDNS-managed host (e2e-confidence #257).`,
|
|
38
37
|
);
|
|
39
38
|
}
|
|
40
39
|
const zoneFile = `${ZONE_DIR}/${domain}.zone`;
|
|
41
40
|
let content = readFileSync(zoneFile, 'utf-8');
|
|
42
41
|
|
|
43
42
|
// Increment SOA serial (YYYYMMDDnn format — just increment the number)
|
|
44
|
-
content = content.replace(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return `${newSerial}${rest}`;
|
|
49
|
-
},
|
|
50
|
-
);
|
|
43
|
+
content = content.replace(/(\d{10})(\s+\d+\s+\d+\s+\d+\s+\d+\s*\))/, (_match, serial, rest) => {
|
|
44
|
+
const newSerial = String(Number(serial) + 1);
|
|
45
|
+
return `${newSerial}${rest}`;
|
|
46
|
+
});
|
|
51
47
|
|
|
52
48
|
// Check if host already has an A record. Match ANY existing value (\S+),
|
|
53
49
|
// not just dotted-decimal: a prior update may have written a malformed
|
|
@@ -61,7 +57,7 @@ export function updateZone(domain: string, host: string, ip: string): void {
|
|
|
61
57
|
content = content.replace(recordRegex, `${host} IN A ${ip}`);
|
|
62
58
|
} else {
|
|
63
59
|
// Append new record
|
|
64
|
-
content = content.trimEnd()
|
|
60
|
+
content = `${content.trimEnd()}\n${host} IN A ${ip}\n`;
|
|
65
61
|
}
|
|
66
62
|
|
|
67
63
|
writeFileSync(zoneFile, content);
|
|
@@ -280,7 +280,9 @@ Bun.serve({
|
|
|
280
280
|
|
|
281
281
|
// Verified endpoints on the real daemon.
|
|
282
282
|
if (url.pathname === '/api/v1/check') {
|
|
283
|
-
return req.method === 'GET'
|
|
283
|
+
return req.method === 'GET'
|
|
284
|
+
? new Response(null, { status: 200 })
|
|
285
|
+
: new Response(null, { status: 405 });
|
|
284
286
|
}
|
|
285
287
|
if (url.pathname === '/api/v1/events') {
|
|
286
288
|
return new Response('', { status: 200, headers: { 'content-type': 'text/event-stream' } });
|
package/src/browser.ts
CHANGED
|
@@ -14,10 +14,10 @@ export async function startBrowser(
|
|
|
14
14
|
): Promise<BrowserHandle> {
|
|
15
15
|
if (!options.chromium) {
|
|
16
16
|
throw new Error(
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
'net.browser() requires a `chromium` launcher in options.\n\n' +
|
|
18
|
+
'Pass it from your own playwright install:\n' +
|
|
19
19
|
" import { chromium } from 'playwright-core';\n" +
|
|
20
|
-
|
|
20
|
+
' const browser = await net.browser({ vantage, chromium });',
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
23
|
const proxy = await startSocksProxy(projectName, options);
|
package/src/bus-events.ts
CHANGED
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
* without the bus, just without observability hooks.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
22
22
|
import { homedir } from 'node:os';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
25
24
|
import { dirname } from 'node:path';
|
|
25
|
+
import { type Bus, defineEvents, openBus } from '@celilo/event-bus';
|
|
26
26
|
import { z } from 'zod';
|
|
27
27
|
|
|
28
28
|
const NO_SCHEMAS = defineEvents({});
|
|
@@ -21,8 +21,14 @@ function parseCommandsFromHelp(text: string): string[] {
|
|
|
21
21
|
let inCommandsSection = false;
|
|
22
22
|
|
|
23
23
|
for (const line of text.split('\n')) {
|
|
24
|
-
if (/^\s*Commands?:\s*$/i.test(line)) {
|
|
25
|
-
|
|
24
|
+
if (/^\s*Commands?:\s*$/i.test(line)) {
|
|
25
|
+
inCommandsSection = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (inCommandsSection && /^\s*(Options|Examples|Usage):/i.test(line)) {
|
|
29
|
+
inCommandsSection = false;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
26
32
|
if (!inCommandsSection) continue;
|
|
27
33
|
|
|
28
34
|
// Match " command-name" at 2-3 spaces indent, followed by space/bracket/end
|
package/src/cli/completion.ts
CHANGED
|
@@ -3,13 +3,7 @@
|
|
|
3
3
|
* All completions are static (no DB lookups needed).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
BUILD_INFRA_FLAGS,
|
|
8
|
-
COMMANDS,
|
|
9
|
-
DOWN_FLAGS,
|
|
10
|
-
RUN_FLAGS,
|
|
11
|
-
UP_PRESETS,
|
|
12
|
-
} from './command-registry';
|
|
6
|
+
import { BUILD_INFRA_FLAGS, COMMANDS, DOWN_FLAGS, RUN_FLAGS, UP_PRESETS } from './command-registry';
|
|
13
7
|
import { findAllTestNames } from './module-discovery';
|
|
14
8
|
|
|
15
9
|
export function getCompletions(words: string[], current: number): string[] {
|
|
@@ -58,7 +52,10 @@ export function getCompletions(words: string[], current: number): string[] {
|
|
|
58
52
|
|
|
59
53
|
if (command === 'list' && currentIndex >= 1) {
|
|
60
54
|
const used = new Set(args.slice(1));
|
|
61
|
-
return filterSuggestions(
|
|
55
|
+
return filterSuggestions(
|
|
56
|
+
['--paths'].filter((f) => !used.has(f)),
|
|
57
|
+
args[currentIndex] || '',
|
|
58
|
+
);
|
|
62
59
|
}
|
|
63
60
|
|
|
64
61
|
if (command === 'completion' && currentIndex === 1) {
|
package/src/cli/index.ts
CHANGED
|
@@ -333,6 +333,7 @@ switch (command) {
|
|
|
333
333
|
}
|
|
334
334
|
console.log(`\n${tests.length} tests. Run all: cele2e run --all`);
|
|
335
335
|
process.exit(0);
|
|
336
|
+
break;
|
|
336
337
|
}
|
|
337
338
|
|
|
338
339
|
case 'up': {
|
|
@@ -350,15 +351,18 @@ switch (command) {
|
|
|
350
351
|
throw err;
|
|
351
352
|
}
|
|
352
353
|
runScript('e2e-up', args);
|
|
354
|
+
break;
|
|
353
355
|
}
|
|
354
356
|
|
|
355
357
|
case 'down':
|
|
356
358
|
// Tearing the network down frees the (possibly kept) lock.
|
|
357
359
|
clearLock();
|
|
358
360
|
runScript('e2e-down', args, { E2E_TEST_DIR: stateDir });
|
|
361
|
+
break;
|
|
359
362
|
|
|
360
363
|
case 'shell':
|
|
361
364
|
runScript('e2e-shell', args);
|
|
365
|
+
break;
|
|
362
366
|
|
|
363
367
|
case 'release': {
|
|
364
368
|
// Free a lock left by `--keep`/`up` (a `kept` stack) so the next run can
|
|
@@ -366,6 +370,7 @@ switch (command) {
|
|
|
366
370
|
const had = clearLock();
|
|
367
371
|
console.log(had ? 'Released the e2e run-lock.' : 'No e2e run-lock held.');
|
|
368
372
|
process.exit(0);
|
|
373
|
+
break;
|
|
369
374
|
}
|
|
370
375
|
|
|
371
376
|
case 'status': {
|
|
@@ -394,18 +399,20 @@ switch (command) {
|
|
|
394
399
|
console.log(` Project: ${data.projectName}`);
|
|
395
400
|
console.log(` Test: ${data.testFile}`);
|
|
396
401
|
console.log(` Created: ${data.createdAt}`);
|
|
397
|
-
console.log(
|
|
398
|
-
console.log(
|
|
402
|
+
console.log(' Reuse: cele2e run --reuse');
|
|
403
|
+
console.log(' Tear down: cele2e down');
|
|
399
404
|
console.log('');
|
|
400
405
|
} catch {
|
|
401
406
|
/* ignore parse errors */
|
|
402
407
|
}
|
|
403
408
|
}
|
|
404
409
|
runScript('e2e-status', args, projectName ? { CELILO_E2E_PROJECT: projectName } : undefined);
|
|
410
|
+
break;
|
|
405
411
|
}
|
|
406
412
|
|
|
407
413
|
case 'load':
|
|
408
414
|
runScript('e2e-load', args);
|
|
415
|
+
break;
|
|
409
416
|
|
|
410
417
|
case 'build-infra': {
|
|
411
418
|
const save = args.includes('--save');
|
|
@@ -18,7 +18,9 @@ const MAX_DEPTH = 2;
|
|
|
18
18
|
|
|
19
19
|
function readManifestTestsDir(manifestPath: string): string | undefined {
|
|
20
20
|
try {
|
|
21
|
-
const parsed = parseYaml(readFileSync(manifestPath, 'utf-8')) as {
|
|
21
|
+
const parsed = parseYaml(readFileSync(manifestPath, 'utf-8')) as {
|
|
22
|
+
e2e?: { tests_dir?: string };
|
|
23
|
+
};
|
|
22
24
|
return parsed?.e2e?.tests_dir;
|
|
23
25
|
} catch {
|
|
24
26
|
return undefined;
|