@celilo/e2e 0.10.2 → 0.10.3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/e2e",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
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",
|
|
@@ -113,6 +113,14 @@ function handleSend(id: unknown, params: Record<string, unknown>): Response {
|
|
|
113
113
|
if (recipients.length === 0) return rpcError(id, -32602, 'No recipient given');
|
|
114
114
|
|
|
115
115
|
for (const recipient of recipients) {
|
|
116
|
+
// The linked account is ALWAYS a valid recipient: messaging yourself is
|
|
117
|
+
// note-to-self, a first-class Signal feature and the default
|
|
118
|
+
// single-operator setup (design R2) — the route points at the very number
|
|
119
|
+
// the transport is a secondary device of. Rejecting it as "unregistered"
|
|
120
|
+
// is something no real daemon does, and it made the note-to-self ack path
|
|
121
|
+
// untestable end to end: the page could not be sent, so no token ever
|
|
122
|
+
// reached the operator to reply with.
|
|
123
|
+
if (recipient === ACCOUNT) continue;
|
|
116
124
|
// Push back like Signal: an unregistered number is a hard failure, not a
|
|
117
125
|
// silent no-op.
|
|
118
126
|
if (KNOWN_RECIPIENTS.size > 0 && !KNOWN_RECIPIENTS.has(recipient)) {
|
package/src/container-manager.ts
CHANGED
|
@@ -75,6 +75,56 @@ const activeProjects: Set<string> = new Set();
|
|
|
75
75
|
* MUST stay fully synchronous: it runs from an `exit` handler, where the
|
|
76
76
|
* event loop is already closed and any async work is silently dropped.
|
|
77
77
|
*/
|
|
78
|
+
/**
|
|
79
|
+
* The commands that remove a per-test project WITHOUT needing its compose file.
|
|
80
|
+
* Pure (Rule 10.1) so the no-compose-dependency property is testable.
|
|
81
|
+
*
|
|
82
|
+
* `docker compose -p <project> down` — what this used to run — resolves the
|
|
83
|
+
* project from a compose file in the CURRENT DIRECTORY. An exit handler has no
|
|
84
|
+
* reliable cwd, so compose exited "no configuration file provided", the error
|
|
85
|
+
* was swallowed by the surrounding catch, and nothing was removed. The handler
|
|
86
|
+
* fired correctly and tore down nothing.
|
|
87
|
+
*
|
|
88
|
+
* Removing by name needs only the project name, which we already have.
|
|
89
|
+
* Per-test projects are `celilo-e2e-<timestamp>`; the shared project is
|
|
90
|
+
* `celilo-e2e-shared`, which contains no timestamp and so is never matched —
|
|
91
|
+
* shared infra stays up, as it must.
|
|
92
|
+
*/
|
|
93
|
+
export function projectTeardownCommands(project: string): {
|
|
94
|
+
listContainers: string;
|
|
95
|
+
listNetworks: string;
|
|
96
|
+
} {
|
|
97
|
+
return {
|
|
98
|
+
listContainers: `docker ps -aq --filter name=${project}`,
|
|
99
|
+
listNetworks: `docker network ls --format {{.Name}} --filter name=${project}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Force-remove a project's containers and networks by name. Never throws. */
|
|
104
|
+
function forceRemoveProject(project: string): void {
|
|
105
|
+
const cmds = projectTeardownCommands(project);
|
|
106
|
+
try {
|
|
107
|
+
const ids = execSync(cmds.listContainers, { timeout: 15_000, stdio: 'pipe' })
|
|
108
|
+
.toString()
|
|
109
|
+
.split('\n')
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
if (ids.length > 0) {
|
|
112
|
+
execSync(`docker rm -f ${ids.join(' ')}`, { timeout: 30_000, stdio: 'pipe' });
|
|
113
|
+
}
|
|
114
|
+
} catch {}
|
|
115
|
+
try {
|
|
116
|
+
const nets = execSync(cmds.listNetworks, { timeout: 15_000, stdio: 'pipe' })
|
|
117
|
+
.toString()
|
|
118
|
+
.split('\n')
|
|
119
|
+
.filter(Boolean);
|
|
120
|
+
for (const net of nets) {
|
|
121
|
+
try {
|
|
122
|
+
execSync(`docker network rm ${net}`, { timeout: 10_000, stdio: 'pipe' });
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
} catch {}
|
|
126
|
+
}
|
|
127
|
+
|
|
78
128
|
function cleanupOnExit() {
|
|
79
129
|
// `--keep` / `--reuse` mean the operator wants the stack to survive for
|
|
80
130
|
// debugging — and they want it MOST after a failure, which is exactly the
|
|
@@ -82,12 +132,17 @@ function cleanupOnExit() {
|
|
|
82
132
|
if (process.env.CELILO_E2E_KEEP === '1' || process.env.CELILO_E2E_REUSE === '1') return;
|
|
83
133
|
|
|
84
134
|
for (const project of activeProjects) {
|
|
135
|
+
// Compose first when it can work — it also drops volumes and orphans — but
|
|
136
|
+
// it is best-effort, so never rely on it having done anything.
|
|
85
137
|
try {
|
|
86
|
-
execSync(`docker compose -p ${project} down --volumes --remove-orphans`, {
|
|
138
|
+
execSync(`docker compose -f ${COMPOSE_FILE} -p ${project} down --volumes --remove-orphans`, {
|
|
139
|
+
cwd: PACKAGE_ROOT,
|
|
87
140
|
timeout: 30_000,
|
|
88
141
|
stdio: 'pipe',
|
|
89
142
|
});
|
|
90
143
|
} catch {}
|
|
144
|
+
// Authoritative: needs no compose file, no cwd, no working directory state.
|
|
145
|
+
forceRemoveProject(project);
|
|
91
146
|
}
|
|
92
147
|
activeProjects.clear();
|
|
93
148
|
try {
|
package/src/exit-cleanup.test.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { readFileSync } from 'node:fs';
|
|
|
15
15
|
import { dirname, join } from 'node:path';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
import { expect, test } from 'bun:test';
|
|
18
|
+
import { projectTeardownCommands } from './container-manager';
|
|
18
19
|
|
|
19
20
|
const SRC = readFileSync(
|
|
20
21
|
join(dirname(fileURLToPath(import.meta.url)), 'container-manager.ts'),
|
|
@@ -39,6 +40,31 @@ test('cleanup honors --keep / --reuse so a failed run stays debuggable', () => {
|
|
|
39
40
|
expect(body).toContain('CELILO_E2E_REUSE');
|
|
40
41
|
});
|
|
41
42
|
|
|
43
|
+
// The first version of this fix wired the handler correctly and still leaked,
|
|
44
|
+
// because the teardown it ran was `docker compose -p <project> down` with no
|
|
45
|
+
// `-f` and no cwd. Compose resolves the project from a compose file in the
|
|
46
|
+
// CURRENT DIRECTORY; an exit handler has no reliable cwd, so it exited "no
|
|
47
|
+
// configuration file provided", the surrounding catch swallowed it, and nothing
|
|
48
|
+
// was removed. Asserting the hook exists was not enough — assert it can work.
|
|
49
|
+
test('teardown does not depend on a compose file being findable', () => {
|
|
50
|
+
const cmds = projectTeardownCommands('celilo-e2e-1785546029645');
|
|
51
|
+
for (const cmd of Object.values(cmds)) {
|
|
52
|
+
expect(cmd).not.toContain('docker compose');
|
|
53
|
+
expect(cmd).toContain('celilo-e2e-1785546029645');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('teardown cannot match the shared project', () => {
|
|
58
|
+
// Per-test projects carry a timestamp; `celilo-e2e-shared` does not, so a
|
|
59
|
+
// name filter for one can never match the other. Shared infra must survive —
|
|
60
|
+
// it is torn down separately, and removing it mid-suite breaks every
|
|
61
|
+
// subsequent test.
|
|
62
|
+
const cmds = projectTeardownCommands('celilo-e2e-1785546029645');
|
|
63
|
+
for (const cmd of Object.values(cmds)) {
|
|
64
|
+
expect(cmd).not.toContain('celilo-e2e-shared');
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
42
68
|
test("the premise holds: process.exit() fires 'exit' but NOT 'beforeExit'", async () => {
|
|
43
69
|
// The whole fix rests on this runtime behavior. If it ever changes, the
|
|
44
70
|
// reasoning above is void and this should fail loudly rather than silently
|