@xemahq/repo-build-tooling 0.8.2 → 0.9.0

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.
@@ -0,0 +1,79 @@
1
+ // `xema-composed-image smoke-refusal <image>`: boots the composed-process image's
2
+ // own host with a declaration it must refuse (two members on one port,
3
+ // `declaration-refusal.mjs`) and fails unless the host exits non-zero, names the
4
+ // collision, and constructed no member. Run after the image is pushed and before
5
+ // anything seals it.
6
+ //
7
+ // The members are `--member <biomeId>/<componentKey>` (twice), else the first two
8
+ // of the image's own member index. Their facts are derived inside the image by the
9
+ // kernel (`derive-member-facts.mjs`), never restated. Needs only `docker` and the
10
+ // image reference: no git history, no install.
11
+
12
+ import { spawnSync } from 'node:child_process';
13
+ import { readFileSync } from 'node:fs';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ import { gradeRefusal, refusalEnvironment } from './declaration-refusal.mjs';
17
+
18
+ const DERIVE_FACTS = fileURLToPath(new URL('./derive-member-facts.mjs', import.meta.url));
19
+
20
+ /**
21
+ * Bound on the host's run. Justified: a host that does NOT refuse starts and
22
+ * serves forever, so without a bound the failure this smoke exists for would
23
+ * hang the job instead of failing it. Refusal needs only two members' module
24
+ * graphs loaded; minutes is generous on any runner.
25
+ */
26
+ export const HOST_RUN_TIMEOUT_MS = 5 * 60 * 1000;
27
+
28
+ function dockerWith(spawn) {
29
+ return (args, options = {}) => {
30
+ const result = spawn('docker', args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, ...options });
31
+ if (result.error && result.error.code !== 'ETIMEDOUT') throw result.error;
32
+ return result;
33
+ };
34
+ }
35
+
36
+ /** Returns the grade's problems (empty when the declaration-level refusal holds) and the members used. */
37
+ export function runRefusalSmoke({ image, members = [], spawn = spawnSync, log = console.log, write = (text) => process.stdout.write(text) }) {
38
+ if (!image) throw new Error('usage: xema-composed-image smoke-refusal <image> [--member <biomeId>/<componentKey>]x2');
39
+ const docker = dockerWith(spawn);
40
+ const inImage = (nodeArgs, input) => {
41
+ const result = docker(['run', '--rm', '-i', '--entrypoint', 'node', image, ...nodeArgs], { input });
42
+ if (result.status !== 0) {
43
+ throw new Error(`docker run ${image} node ${nodeArgs.join(' ')} exited ${result.status}:\n${result.stderr}`);
44
+ }
45
+ return result.stdout.trim();
46
+ };
47
+
48
+ const keys =
49
+ members.length > 0
50
+ ? members
51
+ : JSON.parse(
52
+ inImage([
53
+ '-p',
54
+ 'JSON.stringify(Object.keys(require(process.env.XEMA_COMPOSED_MEMBER_INDEX).members).sort().slice(0, 2))',
55
+ ]),
56
+ );
57
+ if (keys.length !== 2) throw new Error(`the refusal scenario needs two members; got ${keys.length} (${keys.join(', ')})`);
58
+ const facts = JSON.parse(inImage(['--input-type=module', '-', ...keys], readFileSync(DERIVE_FACTS, 'utf8')));
59
+
60
+ const env = refusalEnvironment(keys, facts);
61
+ const name = `declaration-refusal-smoke-${process.pid}`;
62
+ const envArgs = Object.keys(env).flatMap((variable) => ['-e', variable]);
63
+ const run = docker(['run', '--rm', '--name', name, ...envArgs, image], {
64
+ env: { ...process.env, ...env },
65
+ timeout: HOST_RUN_TIMEOUT_MS,
66
+ });
67
+ if (run.status === null) docker(['rm', '--force', name]);
68
+ const output = `${run.stdout ?? ''}${run.stderr ?? ''}`;
69
+ write(output);
70
+
71
+ const problems = gradeRefusal({ status: run.status, output }, keys);
72
+ if (problems.length > 0) {
73
+ throw new Error(
74
+ `the declaration-level refusal did not hold for ${keys.join(' + ')}:\n${problems.map((problem) => ` - ${problem}`).join('\n')}`,
75
+ );
76
+ }
77
+ log(`declaration-level refusal holds: ${keys.join(' + ')} on one port, exit ${run.status}, no member constructed`);
78
+ return keys;
79
+ }
@@ -0,0 +1,57 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { portCollisionMessage } from './declaration-refusal.mjs';
5
+ import { runRefusalSmoke } from './smoke-refusal.mjs';
6
+
7
+ const KEYS = ['alpha/alpha-api', 'beta/beta-api'];
8
+ const REFUSED = `[xema-composed-process] refused to start — Error: ${portCollisionMessage(KEYS)}\n`;
9
+
10
+ /** A docker double: answers the in-image node calls and the host run. */
11
+ function fakeDocker({ hostStatus, hostOutput }) {
12
+ const calls = [];
13
+ const spawn = (command, args, options) => {
14
+ calls.push({ command, args, options });
15
+ if (args.includes('--entrypoint')) {
16
+ const script = args.slice(args.indexOf('node') + 2);
17
+ if (script[0] === '-p') return { status: 0, stdout: JSON.stringify(KEYS) };
18
+ if (script[0] === '--input-type=module' && script[1] === '-') return { status: 0, stdout: JSON.stringify([{ fact: 1 }]) };
19
+ }
20
+ if (args[0] === 'run' && args.includes('--name')) return { status: hostStatus, stdout: hostOutput, stderr: '' };
21
+ return { status: 0, stdout: '', stderr: '' };
22
+ };
23
+ return { spawn, calls };
24
+ }
25
+
26
+ test('the smoke passes when the host refuses the declaration, with members from the index', () => {
27
+ const { spawn, calls } = fakeDocker({ hostStatus: 1, hostOutput: REFUSED });
28
+ const lines = [];
29
+ const keys = runRefusalSmoke({ image: 'ghcr.io/o/img@sha256:x', spawn, log: (l) => lines.push(l), write() {} });
30
+ assert.deepEqual(keys, KEYS);
31
+ const host = calls.find((call) => call.args.includes('--name'));
32
+ assert.equal(host.args.at(-1), 'ghcr.io/o/img@sha256:x');
33
+ assert.ok(host.args.includes('XEMA_PROCESS_DEFINITION'));
34
+ assert.match(lines[0], /refusal holds/);
35
+ });
36
+
37
+ test('explicit members are used instead of the index, and need exactly two', () => {
38
+ const { spawn, calls } = fakeDocker({ hostStatus: 1, hostOutput: REFUSED });
39
+ runRefusalSmoke({ image: 'i', members: KEYS, spawn, log() {}, write() {} });
40
+ assert.ok(!calls.some((call) => call.args.includes('-p')));
41
+ assert.throws(() => runRefusalSmoke({ image: 'i', members: ['only/one'], spawn, log() {}, write() {} }), /needs two members/);
42
+ });
43
+
44
+ test('a host that starts fails the smoke', () => {
45
+ const { spawn } = fakeDocker({ hostStatus: 0, hostOutput: 'probing on 8080' });
46
+ assert.throws(() => runRefusalSmoke({ image: 'i', spawn, log() {}, write() {} }), /did not hold[\s\S]*did not refuse/);
47
+ });
48
+
49
+ test('a host that never exits is removed and fails the smoke', () => {
50
+ const { spawn, calls } = fakeDocker({ hostStatus: null, hostOutput: REFUSED });
51
+ assert.throws(() => runRefusalSmoke({ image: 'i', spawn, log() {}, write() {} }), /did not exit/);
52
+ assert.ok(calls.some((call) => call.args[0] === 'rm' && call.args.includes('--force')));
53
+ });
54
+
55
+ test('an image is required', () => {
56
+ assert.throws(() => runRefusalSmoke({ spawn() {} }), /usage/);
57
+ });