1033 lines
34 KiB
TypeScript
1033 lines
34 KiB
TypeScript
|
|
import { describe, it } from 'node:test';
|
||
|
|
import { strict as assert } from 'node:assert';
|
||
|
|
import * as fs from 'node:fs';
|
||
|
|
import * as net from 'node:net';
|
||
|
|
import * as os from 'node:os';
|
||
|
|
import * as path from 'node:path';
|
||
|
|
|
||
|
|
import { resolveNatsBinary } from '../../src/utils/runner-transport.js';
|
||
|
|
import { assertExitCode, parseLastJson, runMxCli } from '../helpers/cli-harness.js';
|
||
|
|
|
||
|
|
async function getAvailablePorts(count: number): Promise<number[]> {
|
||
|
|
const servers = await Promise.all(
|
||
|
|
Array.from({ length: count }, () => new Promise<net.Server>((resolve, reject) => {
|
||
|
|
const server = net.createServer();
|
||
|
|
server.once('error', reject);
|
||
|
|
server.listen(0, '127.0.0.1', () => resolve(server));
|
||
|
|
})),
|
||
|
|
);
|
||
|
|
const ports = servers.map((server) => {
|
||
|
|
const address = server.address();
|
||
|
|
assert.ok(address && typeof address === 'object');
|
||
|
|
return address.port;
|
||
|
|
});
|
||
|
|
await Promise.all(servers.map((server) => new Promise<void>((resolve, reject) => {
|
||
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
||
|
|
})));
|
||
|
|
return ports;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function createServiceFixture(rootDir: string, options?: {
|
||
|
|
readonly conflictingTopLevelFields?: boolean;
|
||
|
|
readonly topLevelSecretsFile?: boolean;
|
||
|
|
readonly withEnvironment?: boolean;
|
||
|
|
}): Promise<string> {
|
||
|
|
const packageDir = path.join(rootDir, 'sample-service');
|
||
|
|
const runtimeDir = path.join(packageDir, 'dist', 'runtime');
|
||
|
|
fs.mkdirSync(runtimeDir, { recursive: true });
|
||
|
|
const conflictingTopLevelFields = options?.conflictingTopLevelFields === true;
|
||
|
|
const topLevelSecretsFile = options?.topLevelSecretsFile === true;
|
||
|
|
const withEnvironment = options?.withEnvironment === true;
|
||
|
|
const runtimeFactory = {
|
||
|
|
kind: 'factory',
|
||
|
|
export: 'createSampleService',
|
||
|
|
...(conflictingTopLevelFields ? {
|
||
|
|
mount: 'old.mount',
|
||
|
|
config: {
|
||
|
|
file: './config/old-config.json',
|
||
|
|
},
|
||
|
|
dependsOn: ['old.identity'],
|
||
|
|
policyRef: 'policies/old',
|
||
|
|
} : {}),
|
||
|
|
...(topLevelSecretsFile ? {
|
||
|
|
secrets: {
|
||
|
|
file: './config/manifest-secrets.json',
|
||
|
|
},
|
||
|
|
} : {}),
|
||
|
|
bootstrap: {
|
||
|
|
root: 'local.root',
|
||
|
|
transport: {
|
||
|
|
mode: 'local-memory',
|
||
|
|
},
|
||
|
|
http: {
|
||
|
|
enabled: false,
|
||
|
|
},
|
||
|
|
overrides: {
|
||
|
|
config: {
|
||
|
|
source: 'manifest',
|
||
|
|
nested: { enabled: false },
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
instance: {
|
||
|
|
packageName: '@acme/sample-service',
|
||
|
|
packageRevision: '1.0.0',
|
||
|
|
class: 'service',
|
||
|
|
rootKind: 'package-root',
|
||
|
|
mount: 'sample-service.local',
|
||
|
|
configRef: './config/service-config.json',
|
||
|
|
secretRefs: [
|
||
|
|
{
|
||
|
|
name: 'apiKey',
|
||
|
|
provider: 'system.factotum',
|
||
|
|
ref: 'secret://sample/api-key',
|
||
|
|
required: true,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
dependsOn: ['system.identity'],
|
||
|
|
policyRef: 'policies/standalone',
|
||
|
|
autoStart: true,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(packageDir, 'package.json'),
|
||
|
|
JSON.stringify({
|
||
|
|
name: '@acme/sample-service',
|
||
|
|
version: '1.0.0',
|
||
|
|
type: 'module',
|
||
|
|
main: './dist/runtime/index.js',
|
||
|
|
}, null, 2),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(packageDir, 'matrix.json'),
|
||
|
|
JSON.stringify({
|
||
|
|
name: '@acme/sample-service',
|
||
|
|
version: '1.0.0',
|
||
|
|
namespace: 'sample',
|
||
|
|
root: {
|
||
|
|
type: 'SampleRoot',
|
||
|
|
export: 'SampleRoot',
|
||
|
|
mount: 'sample',
|
||
|
|
},
|
||
|
|
runtime: {
|
||
|
|
language: 'typescript',
|
||
|
|
entry: './dist/runtime/index.js',
|
||
|
|
factory: runtimeFactory,
|
||
|
|
},
|
||
|
|
components: [
|
||
|
|
{
|
||
|
|
type: 'SampleService',
|
||
|
|
export: 'SampleService',
|
||
|
|
mount: 'worker',
|
||
|
|
accepts: ['sample.run', 'sample.run'],
|
||
|
|
autoStart: true,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
permissions: {
|
||
|
|
fsPolicy: 'none',
|
||
|
|
},
|
||
|
|
}, null, 2),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
|
||
|
|
fs.mkdirSync(path.join(packageDir, 'config'), { recursive: true });
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(packageDir, 'config', 'old-config.json'),
|
||
|
|
JSON.stringify({
|
||
|
|
config: {
|
||
|
|
old: true,
|
||
|
|
},
|
||
|
|
}, null, 2),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(packageDir, 'config', 'service-config.json'),
|
||
|
|
JSON.stringify({
|
||
|
|
config: {
|
||
|
|
fromFile: true,
|
||
|
|
nested: { file: true },
|
||
|
|
},
|
||
|
|
}, null, 2),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(packageDir, 'config', 'cli-config.json'),
|
||
|
|
JSON.stringify({
|
||
|
|
config: {
|
||
|
|
fromCliFile: true,
|
||
|
|
nested: { file: 'cli' },
|
||
|
|
},
|
||
|
|
}, null, 2),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(packageDir, 'config', 'cli-secrets.json'),
|
||
|
|
JSON.stringify([
|
||
|
|
{
|
||
|
|
name: 'overrideKey',
|
||
|
|
provider: 'system.factotum',
|
||
|
|
ref: 'secret://sample/override-key',
|
||
|
|
},
|
||
|
|
], null, 2),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(packageDir, 'config', 'manifest-secrets.json'),
|
||
|
|
JSON.stringify([
|
||
|
|
{
|
||
|
|
name: 'manifestKey',
|
||
|
|
provider: 'system.factotum',
|
||
|
|
ref: 'secret://sample/manifest-key',
|
||
|
|
},
|
||
|
|
], null, 2),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(runtimeDir, 'index.js'),
|
||
|
|
`export async function createSampleService(overrides = {}, services = {}, _configResolution, bootstrapContext) {
|
||
|
|
return {
|
||
|
|
ok: true,
|
||
|
|
effectiveConfig: overrides,
|
||
|
|
bootContext: bootstrapContext,
|
||
|
|
runnerRuntimeProvided: Boolean(services.runtime),
|
||
|
|
runnerRuntimeRoot: services.runtime?.transport?.root ?? null,
|
||
|
|
runtime: {
|
||
|
|
async shutdown() {
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
|
||
|
|
if (withEnvironment) {
|
||
|
|
const [natsPort, wsPort] = await getAvailablePorts(2);
|
||
|
|
fs.mkdirSync(path.join(packageDir, '.matrix'), { recursive: true });
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(packageDir, '.matrix', 'dev.environment.json'),
|
||
|
|
JSON.stringify({
|
||
|
|
name: 'dev',
|
||
|
|
runtime: {
|
||
|
|
root: 'COM.TEST.PACKAGE-ENV',
|
||
|
|
runtimeId: 'sample-service-dev',
|
||
|
|
},
|
||
|
|
package: {
|
||
|
|
publicRoot: 'sample',
|
||
|
|
},
|
||
|
|
nats: {
|
||
|
|
mode: 'embedded',
|
||
|
|
port: natsPort,
|
||
|
|
wsPort,
|
||
|
|
binaryPath: resolveNatsBinary(process.cwd()),
|
||
|
|
},
|
||
|
|
}, null, 2),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return packageDir;
|
||
|
|
}
|
||
|
|
|
||
|
|
function createPackageRootFixture(rootDir: string): string {
|
||
|
|
const packageDir = path.join(rootDir, 'package-root-service');
|
||
|
|
const runtimeDir = path.join(packageDir, 'dist', 'runtime');
|
||
|
|
fs.mkdirSync(runtimeDir, { recursive: true });
|
||
|
|
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(packageDir, 'package.json'),
|
||
|
|
JSON.stringify({
|
||
|
|
name: '@acme/package-root-service',
|
||
|
|
version: '2.0.0',
|
||
|
|
type: 'module',
|
||
|
|
main: './dist/runtime/index.js',
|
||
|
|
}, null, 2),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(packageDir, 'matrix.json'),
|
||
|
|
JSON.stringify({
|
||
|
|
name: '@acme/package-root-service',
|
||
|
|
version: '2.0.0',
|
||
|
|
runtime: {
|
||
|
|
language: 'typescript',
|
||
|
|
entry: './dist/runtime/index.js',
|
||
|
|
factory: {
|
||
|
|
kind: 'factory',
|
||
|
|
export: 'createPackageRootService',
|
||
|
|
bootstrap: {
|
||
|
|
overrides: {
|
||
|
|
mode: 'package-root',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
instance: {
|
||
|
|
rootKind: 'package-root',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
components: [
|
||
|
|
{
|
||
|
|
type: 'NestedService',
|
||
|
|
export: 'NestedService',
|
||
|
|
mount: 'nested-component-mount',
|
||
|
|
autoStart: true,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
permissions: {
|
||
|
|
fsPolicy: 'none',
|
||
|
|
},
|
||
|
|
}, null, 2),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(runtimeDir, 'index.js'),
|
||
|
|
`export async function createPackageRootService(overrides = {}, _services, _configResolution, bootstrapContext) {
|
||
|
|
return {
|
||
|
|
ok: true,
|
||
|
|
effectiveConfig: overrides,
|
||
|
|
bootContext: bootstrapContext,
|
||
|
|
runtime: {
|
||
|
|
async shutdown() {
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
|
||
|
|
return packageDir;
|
||
|
|
}
|
||
|
|
|
||
|
|
function createComponentRootFixture(
|
||
|
|
rootDir: string,
|
||
|
|
options: {
|
||
|
|
name: string;
|
||
|
|
components: Array<{ type: string; mount?: string }>;
|
||
|
|
instance?: Record<string, unknown>;
|
||
|
|
},
|
||
|
|
): string {
|
||
|
|
const packageDir = path.join(rootDir, options.name);
|
||
|
|
const runtimeDir = path.join(packageDir, 'dist', 'runtime');
|
||
|
|
fs.mkdirSync(runtimeDir, { recursive: true });
|
||
|
|
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(packageDir, 'package.json'),
|
||
|
|
JSON.stringify({
|
||
|
|
name: `@acme/${options.name}`,
|
||
|
|
version: '1.0.0',
|
||
|
|
type: 'module',
|
||
|
|
main: './dist/runtime/index.js',
|
||
|
|
}, null, 2),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(packageDir, 'matrix.json'),
|
||
|
|
JSON.stringify({
|
||
|
|
name: `@acme/${options.name}`,
|
||
|
|
version: '1.0.0',
|
||
|
|
runtime: {
|
||
|
|
language: 'typescript',
|
||
|
|
entry: './dist/runtime/index.js',
|
||
|
|
factory: {
|
||
|
|
kind: 'factory',
|
||
|
|
export: 'createComponentRootService',
|
||
|
|
instance: {
|
||
|
|
rootKind: 'component',
|
||
|
|
...(options.instance ?? {}),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
components: options.components.map((component) => ({
|
||
|
|
type: component.type,
|
||
|
|
export: component.type,
|
||
|
|
...(component.mount ? { mount: component.mount } : {}),
|
||
|
|
autoStart: false,
|
||
|
|
})),
|
||
|
|
permissions: {
|
||
|
|
fsPolicy: 'none',
|
||
|
|
},
|
||
|
|
}, null, 2),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(runtimeDir, 'index.js'),
|
||
|
|
`export async function createComponentRootService(overrides = {}, _services, _configResolution, bootstrapContext) {
|
||
|
|
return {
|
||
|
|
ok: true,
|
||
|
|
effectiveConfig: overrides,
|
||
|
|
bootContext: bootstrapContext,
|
||
|
|
runtime: {
|
||
|
|
async shutdown() {
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
|
||
|
|
return packageDir;
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('mx service command', () => {
|
||
|
|
it('marks runtime control ready before registry registration', () => {
|
||
|
|
const source = fs.readFileSync(path.join(process.cwd(), 'src', 'commands', 'service.ts'), 'utf8');
|
||
|
|
const controlIndex = source.indexOf('const mountedControlMount = mounted.controlMount;');
|
||
|
|
const readyIndex = source.indexOf("lifecycleState = 'ready';");
|
||
|
|
const registerIndex = source.indexOf('const runtimeRegistration = await registerRunnerRuntime(');
|
||
|
|
const liveIndex = source.indexOf("lifecycleState = 'live';");
|
||
|
|
|
||
|
|
assert.notEqual(controlIndex, -1);
|
||
|
|
assert.notEqual(readyIndex, -1);
|
||
|
|
assert.notEqual(registerIndex, -1);
|
||
|
|
assert.notEqual(liveIndex, -1);
|
||
|
|
assert.ok(controlIndex < readyIndex, 'control actor mount result must precede ready state');
|
||
|
|
assert.ok(readyIndex < registerIndex, 'runtime.ready must not wait on self-registration');
|
||
|
|
assert.ok(registerIndex < liveIndex, 'live state remains after registry registration');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('loads matrix.json runtime.factory and invokes the declared bootstrap export in check mode', async () => {
|
||
|
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-service-'));
|
||
|
|
try {
|
||
|
|
const packageDir = await createServiceFixture(tmpRoot);
|
||
|
|
const result = await runMxCli([
|
||
|
|
'node',
|
||
|
|
'mx',
|
||
|
|
'service',
|
||
|
|
packageDir,
|
||
|
|
'--check',
|
||
|
|
'--json',
|
||
|
|
'--root',
|
||
|
|
'cli.root',
|
||
|
|
'--mount',
|
||
|
|
'cli.mount',
|
||
|
|
'--config',
|
||
|
|
'./config/cli-config.json',
|
||
|
|
'--secrets',
|
||
|
|
'./config/cli-secrets.json',
|
||
|
|
'--overrides',
|
||
|
|
JSON.stringify({
|
||
|
|
config: {
|
||
|
|
nested: { enabled: true },
|
||
|
|
cli: 'override',
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
]);
|
||
|
|
|
||
|
|
assertExitCode(result, 0);
|
||
|
|
const payload = parseLastJson(result.logs) as {
|
||
|
|
packageName: string;
|
||
|
|
exportName: string;
|
||
|
|
checked: boolean;
|
||
|
|
root?: string;
|
||
|
|
mount?: string;
|
||
|
|
configRef?: string;
|
||
|
|
secretsRef?: string;
|
||
|
|
instance?: {
|
||
|
|
id?: string;
|
||
|
|
class?: string;
|
||
|
|
mount?: string;
|
||
|
|
rootKind?: string;
|
||
|
|
configRef?: string;
|
||
|
|
policyRef?: string;
|
||
|
|
dependsOn?: string[];
|
||
|
|
secretRefs?: Array<{
|
||
|
|
name?: string;
|
||
|
|
provider?: string;
|
||
|
|
ref?: string;
|
||
|
|
required?: boolean;
|
||
|
|
}>;
|
||
|
|
registrationSource?: string;
|
||
|
|
};
|
||
|
|
serviceInstance?: {
|
||
|
|
id?: string;
|
||
|
|
class?: string;
|
||
|
|
mount?: string;
|
||
|
|
rootKind?: string;
|
||
|
|
configRef?: string;
|
||
|
|
policyRef?: string;
|
||
|
|
dependsOn?: string[];
|
||
|
|
secretRefs?: Array<{
|
||
|
|
name?: string;
|
||
|
|
provider?: string;
|
||
|
|
ref?: string;
|
||
|
|
required?: boolean;
|
||
|
|
}>;
|
||
|
|
registrationSource?: string;
|
||
|
|
};
|
||
|
|
runtimeRegistration?: {
|
||
|
|
runtimeId?: string;
|
||
|
|
runtimeRoot?: string;
|
||
|
|
registryMount?: string;
|
||
|
|
created?: boolean;
|
||
|
|
actorCount?: number;
|
||
|
|
runtimeCount?: number;
|
||
|
|
};
|
||
|
|
bindingRegistration?: {
|
||
|
|
runtimeId?: string;
|
||
|
|
registryMount?: string;
|
||
|
|
registeredCount?: number;
|
||
|
|
bindings?: Array<{
|
||
|
|
binding?: string;
|
||
|
|
localMount?: string;
|
||
|
|
packageName?: string;
|
||
|
|
ops?: string[];
|
||
|
|
}>;
|
||
|
|
};
|
||
|
|
runtimeDeregistration?: {
|
||
|
|
runtimeId?: string;
|
||
|
|
registryMount?: string;
|
||
|
|
removed?: boolean;
|
||
|
|
runtimeCount?: number;
|
||
|
|
};
|
||
|
|
bindingDeregistration?: {
|
||
|
|
runtimeId?: string;
|
||
|
|
registryMount?: string;
|
||
|
|
removedCount?: number;
|
||
|
|
};
|
||
|
|
result?: {
|
||
|
|
effectiveConfig?: {
|
||
|
|
config?: {
|
||
|
|
source?: string;
|
||
|
|
cli?: string;
|
||
|
|
nested?: { enabled?: boolean };
|
||
|
|
fromCliFile?: boolean;
|
||
|
|
};
|
||
|
|
};
|
||
|
|
runnerRuntimeProvided?: boolean;
|
||
|
|
runnerRuntimeRoot?: string | null;
|
||
|
|
bootContext?: {
|
||
|
|
root?: string;
|
||
|
|
mount?: string;
|
||
|
|
configRef?: string;
|
||
|
|
secretsRef?: string;
|
||
|
|
transport?: { mode?: string };
|
||
|
|
http?: { enabled?: boolean };
|
||
|
|
instance?: {
|
||
|
|
mount?: string;
|
||
|
|
configRef?: string;
|
||
|
|
dependsOn?: string[];
|
||
|
|
policyRef?: string;
|
||
|
|
secretRefs?: Array<{
|
||
|
|
name?: string;
|
||
|
|
provider?: string;
|
||
|
|
ref?: string;
|
||
|
|
}>;
|
||
|
|
registrationSource?: string;
|
||
|
|
};
|
||
|
|
serviceInstance?: {
|
||
|
|
mount?: string;
|
||
|
|
configRef?: string;
|
||
|
|
dependsOn?: string[];
|
||
|
|
policyRef?: string;
|
||
|
|
secretRefs?: Array<{
|
||
|
|
name?: string;
|
||
|
|
provider?: string;
|
||
|
|
ref?: string;
|
||
|
|
}>;
|
||
|
|
registrationSource?: string;
|
||
|
|
};
|
||
|
|
};
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
assert.equal(payload.packageName, '@acme/sample-service');
|
||
|
|
assert.equal(payload.exportName, 'createSampleService');
|
||
|
|
assert.equal(payload.checked, true);
|
||
|
|
assert.equal(payload.root, 'cli.root');
|
||
|
|
assert.equal(payload.mount, 'cli.mount');
|
||
|
|
assert.equal(payload.configRef, './config/cli-config.json');
|
||
|
|
assert.equal(payload.secretsRef, './config/cli-secrets.json');
|
||
|
|
assert.equal(payload.result?.effectiveConfig?.config?.source, 'manifest');
|
||
|
|
assert.equal(payload.result?.effectiveConfig?.config?.fromCliFile, true);
|
||
|
|
assert.equal(payload.result?.effectiveConfig?.config?.cli, 'override');
|
||
|
|
assert.equal(payload.result?.effectiveConfig?.config?.nested?.enabled, true);
|
||
|
|
assert.equal(payload.result?.runnerRuntimeProvided, true);
|
||
|
|
assert.equal(payload.result?.runnerRuntimeRoot, 'cli.root');
|
||
|
|
assert.equal(payload.runtimeRegistration?.runtimeId, '@acme/sample-service:sample-service.local');
|
||
|
|
assert.equal(payload.runtimeRegistration?.runtimeRoot, 'cli.root');
|
||
|
|
assert.equal(payload.runtimeRegistration?.registryMount, 'system.runtimes');
|
||
|
|
assert.equal(payload.runtimeRegistration?.created, true);
|
||
|
|
// Runtime control now has one RuntimeControlActor at
|
||
|
|
// system.runtimes.<runtimeId>. LoadedPackageActor adds one mount while
|
||
|
|
// the separate .control mount is absent, so actorCount stays at 5.
|
||
|
|
assert.equal(payload.runtimeRegistration?.actorCount, 5);
|
||
|
|
assert.equal(payload.runtimeRegistration?.runtimeCount, 1);
|
||
|
|
assert.equal(payload.bindingRegistration?.runtimeId, '@acme/sample-service:sample-service.local');
|
||
|
|
assert.equal(payload.bindingRegistration?.registryMount, 'system.bindings');
|
||
|
|
assert.equal(payload.bindingRegistration?.registeredCount, 2);
|
||
|
|
assert.deepEqual(payload.bindingRegistration?.bindings, [
|
||
|
|
{
|
||
|
|
binding: 'sample',
|
||
|
|
localMount: 'cli.mount',
|
||
|
|
packageName: '@acme/sample-service',
|
||
|
|
ops: [],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
binding: 'sample.worker',
|
||
|
|
localMount: 'cli.mount.worker',
|
||
|
|
packageName: '@acme/sample-service',
|
||
|
|
ops: ['sample.run'],
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
assert.equal(payload.runtimeDeregistration?.runtimeId, '@acme/sample-service:sample-service.local');
|
||
|
|
assert.equal(payload.runtimeDeregistration?.registryMount, 'system.runtimes');
|
||
|
|
assert.equal(payload.runtimeDeregistration?.removed, true);
|
||
|
|
assert.equal(payload.runtimeDeregistration?.runtimeCount, 0);
|
||
|
|
assert.equal(payload.bindingDeregistration?.runtimeId, '@acme/sample-service:sample-service.local');
|
||
|
|
assert.equal(payload.bindingDeregistration?.registryMount, 'system.bindings');
|
||
|
|
assert.equal(payload.bindingDeregistration?.removedCount, 2);
|
||
|
|
assert.equal(payload.instance?.autoStart, true);
|
||
|
|
assert.equal(payload.instance?.mount, 'cli.mount');
|
||
|
|
assert.equal(payload.instance?.configRef, './config/cli-config.json');
|
||
|
|
assert.equal(payload.instance?.rootKind, 'package-root');
|
||
|
|
assert.equal(payload.instance?.policyRef, 'policies/standalone');
|
||
|
|
assert.deepEqual(payload.instance?.dependsOn, ['system.identity']);
|
||
|
|
assert.equal(payload.instance?.registrationSource, 'matrix-service');
|
||
|
|
assert.equal(payload.instance?.secretRefs?.[0]?.name, 'overrideKey');
|
||
|
|
assert.equal(payload.instance?.secretRefs?.[0]?.provider, 'system.factotum');
|
||
|
|
assert.equal(payload.instance?.secretRefs?.[0]?.ref, 'secret://sample/override-key');
|
||
|
|
assert.equal(payload.serviceInstance?.mount, 'cli.mount');
|
||
|
|
assert.equal(payload.serviceInstance?.configRef, './config/cli-config.json');
|
||
|
|
assert.equal(payload.serviceInstance?.rootKind, 'package-root');
|
||
|
|
assert.equal(payload.serviceInstance?.autoStart, true);
|
||
|
|
assert.equal(payload.serviceInstance?.policyRef, 'policies/standalone');
|
||
|
|
assert.deepEqual(payload.serviceInstance?.dependsOn, ['system.identity']);
|
||
|
|
assert.equal(payload.serviceInstance?.registrationSource, 'matrix-service');
|
||
|
|
assert.equal(payload.serviceInstance?.secretRefs?.[0]?.name, 'overrideKey');
|
||
|
|
assert.equal(payload.serviceInstance?.secretRefs?.[0]?.provider, 'system.factotum');
|
||
|
|
assert.equal(payload.serviceInstance?.secretRefs?.[0]?.ref, 'secret://sample/override-key');
|
||
|
|
assert.equal(payload.result?.bootContext?.root, 'cli.root');
|
||
|
|
assert.equal(payload.result?.bootContext?.mount, 'cli.mount');
|
||
|
|
assert.equal(payload.result?.bootContext?.configRef, './config/cli-config.json');
|
||
|
|
assert.equal(payload.result?.bootContext?.secretsRef, './config/cli-secrets.json');
|
||
|
|
assert.equal(payload.result?.bootContext?.transport?.mode, 'local-memory');
|
||
|
|
assert.equal(payload.result?.bootContext?.http?.enabled, false);
|
||
|
|
assert.equal(payload.result?.bootContext?.instance?.mount, 'cli.mount');
|
||
|
|
assert.equal(payload.result?.bootContext?.instance?.configRef, './config/cli-config.json');
|
||
|
|
assert.equal(payload.result?.bootContext?.instance?.autoStart, true);
|
||
|
|
assert.deepEqual(payload.result?.bootContext?.instance?.dependsOn, ['system.identity']);
|
||
|
|
assert.equal(payload.result?.bootContext?.instance?.policyRef, 'policies/standalone');
|
||
|
|
assert.equal(payload.result?.bootContext?.instance?.registrationSource, 'matrix-service');
|
||
|
|
assert.equal(payload.result?.bootContext?.instance?.secretRefs?.[0]?.name, 'overrideKey');
|
||
|
|
assert.equal(payload.result?.bootContext?.serviceInstance?.mount, 'cli.mount');
|
||
|
|
assert.equal(payload.result?.bootContext?.serviceInstance?.configRef, './config/cli-config.json');
|
||
|
|
assert.equal(payload.result?.bootContext?.serviceInstance?.autoStart, true);
|
||
|
|
assert.deepEqual(payload.result?.bootContext?.serviceInstance?.dependsOn, ['system.identity']);
|
||
|
|
assert.equal(payload.result?.bootContext?.serviceInstance?.policyRef, 'policies/standalone');
|
||
|
|
assert.equal(payload.result?.bootContext?.serviceInstance?.registrationSource, 'matrix-service');
|
||
|
|
assert.equal(payload.result?.bootContext?.serviceInstance?.secretRefs?.[0]?.name, 'overrideKey');
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('loads package-local environment manifests and threads them into standalone bootstrap', async () => {
|
||
|
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-service-env-'));
|
||
|
|
const packageDir = await createServiceFixture(tempRoot, { withEnvironment: true });
|
||
|
|
|
||
|
|
const result = await runMxCli(['node', 'mx', 'service', packageDir, '--check', '--json', '--env', 'dev']);
|
||
|
|
assertExitCode(result, 0);
|
||
|
|
const payload = parseLastJson(result.logs) as {
|
||
|
|
environmentName?: string;
|
||
|
|
root?: string;
|
||
|
|
environmentPath?: string;
|
||
|
|
runnerTransport?: {
|
||
|
|
mode?: string;
|
||
|
|
root?: string;
|
||
|
|
url?: string;
|
||
|
|
wsUrl?: string;
|
||
|
|
dataDir?: string;
|
||
|
|
pidFile?: string;
|
||
|
|
};
|
||
|
|
runtimeRegistration?: {
|
||
|
|
runtimeId?: string;
|
||
|
|
runtimeRoot?: string;
|
||
|
|
runtimeCount?: number;
|
||
|
|
};
|
||
|
|
bindingRegistration?: {
|
||
|
|
runtimeId?: string;
|
||
|
|
registryMount?: string;
|
||
|
|
registeredCount?: number;
|
||
|
|
bindings?: Array<{
|
||
|
|
binding?: string;
|
||
|
|
localMount?: string;
|
||
|
|
packageName?: string;
|
||
|
|
ops?: string[];
|
||
|
|
}>;
|
||
|
|
};
|
||
|
|
runtimeDeregistration?: {
|
||
|
|
runtimeId?: string;
|
||
|
|
removed?: boolean;
|
||
|
|
runtimeCount?: number;
|
||
|
|
};
|
||
|
|
bindingDeregistration?: {
|
||
|
|
runtimeId?: string;
|
||
|
|
removedCount?: number;
|
||
|
|
};
|
||
|
|
result?: {
|
||
|
|
bootContext?: {
|
||
|
|
environment?: {
|
||
|
|
name?: string;
|
||
|
|
runtime?: { root?: string };
|
||
|
|
};
|
||
|
|
runnerTransport?: {
|
||
|
|
mode?: string;
|
||
|
|
url?: string;
|
||
|
|
wsUrl?: string;
|
||
|
|
};
|
||
|
|
};
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
assert.equal(payload.environmentName, 'dev');
|
||
|
|
assert.equal(payload.root, 'COM.TEST.PACKAGE-ENV');
|
||
|
|
assert.equal(
|
||
|
|
payload.environmentPath,
|
||
|
|
path.join(packageDir, '.matrix', 'dev.environment.json'),
|
||
|
|
);
|
||
|
|
assert.equal(payload.runnerTransport?.mode, 'embedded');
|
||
|
|
assert.equal(payload.runnerTransport?.root, 'COM.TEST.PACKAGE-ENV');
|
||
|
|
assert.match(String(payload.runnerTransport?.url), /^nats:\/\/127\.0\.0\.1:\d+$/);
|
||
|
|
assert.match(String(payload.runnerTransport?.wsUrl), /^ws:\/\/127\.0\.0\.1:\d+$/);
|
||
|
|
assert.equal(payload.runnerTransport?.dataDir, path.join(packageDir, '.matrix', 'state', 'dev', 'nats'));
|
||
|
|
assert.equal(payload.runnerTransport?.pidFile, path.join(packageDir, '.matrix', 'state', 'dev', 'nats-server.pid'));
|
||
|
|
assert.equal(payload.result?.bootContext?.environment?.name, 'dev');
|
||
|
|
assert.equal(payload.result?.bootContext?.environment?.runtime?.root, 'COM.TEST.PACKAGE-ENV');
|
||
|
|
assert.equal(payload.result?.bootContext?.runnerTransport?.mode, 'embedded');
|
||
|
|
assert.match(String(payload.result?.bootContext?.runnerTransport?.url), /^nats:\/\/127\.0\.0\.1:\d+$/);
|
||
|
|
assert.match(String(payload.result?.bootContext?.runnerTransport?.wsUrl), /^ws:\/\/127\.0\.0\.1:\d+$/);
|
||
|
|
assert.equal(payload.runtimeRegistration?.runtimeId, 'sample-service-dev');
|
||
|
|
assert.equal(payload.runtimeRegistration?.runtimeRoot, 'COM.TEST.PACKAGE-ENV');
|
||
|
|
assert.equal(payload.runtimeRegistration?.runtimeCount, 1);
|
||
|
|
assert.equal(payload.bindingRegistration?.runtimeId, 'sample-service-dev');
|
||
|
|
assert.equal(payload.bindingRegistration?.registryMount, 'system.bindings');
|
||
|
|
assert.equal(payload.bindingRegistration?.registeredCount, 2);
|
||
|
|
assert.deepEqual(payload.bindingRegistration?.bindings, [
|
||
|
|
{
|
||
|
|
binding: 'sample',
|
||
|
|
localMount: 'sample-service.local',
|
||
|
|
packageName: '@acme/sample-service',
|
||
|
|
ops: [],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
binding: 'sample.worker',
|
||
|
|
localMount: 'sample-service.local.worker',
|
||
|
|
packageName: '@acme/sample-service',
|
||
|
|
ops: ['sample.run'],
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
assert.equal(payload.runtimeDeregistration?.runtimeId, 'sample-service-dev');
|
||
|
|
assert.equal(payload.runtimeDeregistration?.removed, true);
|
||
|
|
assert.equal(payload.runtimeDeregistration?.runtimeCount, 0);
|
||
|
|
assert.equal(payload.bindingDeregistration?.runtimeId, 'sample-service-dev');
|
||
|
|
assert.equal(payload.bindingDeregistration?.removedCount, 2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('fails clearly when standalone top-level fields are present', async () => {
|
||
|
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-service-instance-conflict-'));
|
||
|
|
try {
|
||
|
|
const packageDir = await createServiceFixture(tmpRoot, { conflictingTopLevelFields: true });
|
||
|
|
const result = await runMxCli([
|
||
|
|
'node',
|
||
|
|
'mx',
|
||
|
|
'service',
|
||
|
|
packageDir,
|
||
|
|
'--check',
|
||
|
|
'--json',
|
||
|
|
]);
|
||
|
|
|
||
|
|
assertExitCode(result, 1);
|
||
|
|
assert.match(result.errors.join('\n'), /top-level "mount" is no longer supported; use "instance\.mount"/i);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('fails clearly when matrix.json runtime.factory uses top-level secrets.file', async () => {
|
||
|
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-service-secrets-field-'));
|
||
|
|
try {
|
||
|
|
const packageDir = await createServiceFixture(tmpRoot, { topLevelSecretsFile: true });
|
||
|
|
const result = await runMxCli([
|
||
|
|
'node',
|
||
|
|
'mx',
|
||
|
|
'service',
|
||
|
|
packageDir,
|
||
|
|
'--check',
|
||
|
|
'--json',
|
||
|
|
]);
|
||
|
|
|
||
|
|
assertExitCode(result, 1);
|
||
|
|
assert.match(result.errors.join('\n'), /top-level "secrets\.file" is no longer supported; use "instance\.secretRefs or CLI --secrets"/i);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('fails clearly when matrix.json runtime.factory uses top-level standalone bootstrap fields', async () => {
|
||
|
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-service-bootstrap-field-'));
|
||
|
|
try {
|
||
|
|
const packageDir = await createServiceFixture(tmpRoot);
|
||
|
|
const manifestPath = path.join(packageDir, 'matrix.json');
|
||
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as {
|
||
|
|
runtime?: { factory?: Record<string, unknown> };
|
||
|
|
};
|
||
|
|
fs.writeFileSync(
|
||
|
|
manifestPath,
|
||
|
|
JSON.stringify({
|
||
|
|
...manifest,
|
||
|
|
runtime: {
|
||
|
|
...(manifest.runtime ?? {}),
|
||
|
|
factory: {
|
||
|
|
...(manifest.runtime?.factory ?? {}),
|
||
|
|
root: 'old.root',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}, null, 2),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
|
||
|
|
const result = await runMxCli([
|
||
|
|
'node',
|
||
|
|
'mx',
|
||
|
|
'service',
|
||
|
|
packageDir,
|
||
|
|
'--check',
|
||
|
|
'--json',
|
||
|
|
]);
|
||
|
|
|
||
|
|
assertExitCode(result, 1);
|
||
|
|
assert.match(result.errors.join('\n'), /top-level "root" is no longer supported; use "bootstrap\.root"/i);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('fails clearly when matrix.json runtime.factory is missing', async () => {
|
||
|
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-service-missing-'));
|
||
|
|
try {
|
||
|
|
const packageDir = path.join(tmpRoot, 'broken-service');
|
||
|
|
fs.mkdirSync(packageDir, { recursive: true });
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(packageDir, 'matrix.json'),
|
||
|
|
JSON.stringify({
|
||
|
|
name: '@acme/broken-service',
|
||
|
|
version: '1.0.0',
|
||
|
|
runtime: { language: 'typescript', entry: './dist/runtime/index.js' },
|
||
|
|
components: [],
|
||
|
|
permissions: { fsPolicy: 'none' },
|
||
|
|
}, null, 2),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
|
||
|
|
const result = await runMxCli([
|
||
|
|
'node',
|
||
|
|
'mx',
|
||
|
|
'service',
|
||
|
|
packageDir,
|
||
|
|
'--check',
|
||
|
|
]);
|
||
|
|
|
||
|
|
assertExitCode(result, 1);
|
||
|
|
assert.match(result.errors.join('\n'), /matrix\.json runtime\.factory must be a JSON object/i);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('fails clearly when matrix.json runtime.factory instance identity disagrees with matrix.json', async () => {
|
||
|
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-service-package-identity-mismatch-'));
|
||
|
|
try {
|
||
|
|
const packageDir = await createServiceFixture(tmpRoot);
|
||
|
|
const manifestPath = path.join(packageDir, 'matrix.json');
|
||
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as {
|
||
|
|
runtime?: { factory?: { instance?: Record<string, unknown> } };
|
||
|
|
};
|
||
|
|
fs.writeFileSync(
|
||
|
|
manifestPath,
|
||
|
|
JSON.stringify({
|
||
|
|
...manifest,
|
||
|
|
runtime: {
|
||
|
|
...(manifest.runtime ?? {}),
|
||
|
|
factory: {
|
||
|
|
...(manifest.runtime?.factory ?? {}),
|
||
|
|
instance: {
|
||
|
|
...(manifest.runtime?.factory?.instance ?? {}),
|
||
|
|
packageName: '@acme/not-the-same-service',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}, null, 2),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
|
||
|
|
const result = await runMxCli([
|
||
|
|
'node',
|
||
|
|
'mx',
|
||
|
|
'service',
|
||
|
|
packageDir,
|
||
|
|
'--check',
|
||
|
|
'--json',
|
||
|
|
]);
|
||
|
|
|
||
|
|
assertExitCode(result, 1);
|
||
|
|
assert.match(
|
||
|
|
result.errors.join('\n'),
|
||
|
|
/instance\.packageName "@acme\/not-the-same-service" does not match matrix\.json name "@acme\/sample-service"/,
|
||
|
|
);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('does not invent a component mount for package-root standalone services', async () => {
|
||
|
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-service-package-root-'));
|
||
|
|
try {
|
||
|
|
const packageDir = createPackageRootFixture(tmpRoot);
|
||
|
|
const result = await runMxCli([
|
||
|
|
'node',
|
||
|
|
'mx',
|
||
|
|
'service',
|
||
|
|
packageDir,
|
||
|
|
'--check',
|
||
|
|
'--json',
|
||
|
|
]);
|
||
|
|
|
||
|
|
assertExitCode(result, 0);
|
||
|
|
const payload = parseLastJson(result.logs) as {
|
||
|
|
mount?: string;
|
||
|
|
instance?: {
|
||
|
|
id?: string;
|
||
|
|
mount?: string;
|
||
|
|
rootKind?: string;
|
||
|
|
};
|
||
|
|
serviceInstance?: {
|
||
|
|
id?: string;
|
||
|
|
mount?: string;
|
||
|
|
rootKind?: string;
|
||
|
|
};
|
||
|
|
result?: {
|
||
|
|
bootContext?: {
|
||
|
|
mount?: string;
|
||
|
|
instance?: {
|
||
|
|
mount?: string;
|
||
|
|
rootKind?: string;
|
||
|
|
};
|
||
|
|
serviceInstance?: {
|
||
|
|
mount?: string;
|
||
|
|
rootKind?: string;
|
||
|
|
};
|
||
|
|
};
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
assert.equal(payload.mount, undefined);
|
||
|
|
assert.equal(payload.instance?.id, '@acme/package-root-service');
|
||
|
|
assert.equal(payload.instance?.mount, undefined);
|
||
|
|
assert.equal(payload.instance?.rootKind, 'package-root');
|
||
|
|
assert.equal(payload.serviceInstance?.id, '@acme/package-root-service');
|
||
|
|
assert.equal(payload.serviceInstance?.mount, undefined);
|
||
|
|
assert.equal(payload.serviceInstance?.rootKind, 'package-root');
|
||
|
|
assert.equal(payload.result?.bootContext?.mount, undefined);
|
||
|
|
assert.equal(payload.result?.bootContext?.instance?.mount, undefined);
|
||
|
|
assert.equal(payload.result?.bootContext?.instance?.rootKind, 'package-root');
|
||
|
|
assert.equal(payload.result?.bootContext?.serviceInstance?.mount, undefined);
|
||
|
|
assert.equal(payload.result?.bootContext?.serviceInstance?.rootKind, 'package-root');
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('resolves component-root standalone services by declared componentType', async () => {
|
||
|
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-service-component-root-'));
|
||
|
|
try {
|
||
|
|
const packageDir = createComponentRootFixture(tmpRoot, {
|
||
|
|
name: 'component-root-service',
|
||
|
|
components: [
|
||
|
|
{ type: 'PrimaryService', mount: 'primary.mount' },
|
||
|
|
{ type: 'SecondaryService', mount: 'secondary.mount' },
|
||
|
|
],
|
||
|
|
instance: {
|
||
|
|
componentType: 'SecondaryService',
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const result = await runMxCli([
|
||
|
|
'node',
|
||
|
|
'mx',
|
||
|
|
'service',
|
||
|
|
packageDir,
|
||
|
|
'--check',
|
||
|
|
'--json',
|
||
|
|
]);
|
||
|
|
|
||
|
|
assertExitCode(result, 0);
|
||
|
|
const payload = parseLastJson(result.logs) as {
|
||
|
|
mount?: string;
|
||
|
|
instance?: {
|
||
|
|
componentType?: string;
|
||
|
|
mount?: string;
|
||
|
|
rootKind?: string;
|
||
|
|
};
|
||
|
|
result?: {
|
||
|
|
bootContext?: {
|
||
|
|
mount?: string;
|
||
|
|
instance?: {
|
||
|
|
componentType?: string;
|
||
|
|
mount?: string;
|
||
|
|
rootKind?: string;
|
||
|
|
};
|
||
|
|
};
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
assert.equal(payload.mount, 'secondary.mount');
|
||
|
|
assert.equal(payload.instance?.componentType, 'SecondaryService');
|
||
|
|
assert.equal(payload.instance?.mount, 'secondary.mount');
|
||
|
|
assert.equal(payload.instance?.rootKind, 'component');
|
||
|
|
assert.equal(payload.result?.bootContext?.mount, 'secondary.mount');
|
||
|
|
assert.equal(payload.result?.bootContext?.instance?.componentType, 'SecondaryService');
|
||
|
|
assert.equal(payload.result?.bootContext?.instance?.mount, 'secondary.mount');
|
||
|
|
assert.equal(payload.result?.bootContext?.instance?.rootKind, 'component');
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('fails clearly when component-root standalone bootstrap is ambiguous', async () => {
|
||
|
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-service-component-ambiguous-'));
|
||
|
|
try {
|
||
|
|
const packageDir = createComponentRootFixture(tmpRoot, {
|
||
|
|
name: 'ambiguous-component-root-service',
|
||
|
|
components: [
|
||
|
|
{ type: 'PrimaryService', mount: 'primary.mount' },
|
||
|
|
{ type: 'SecondaryService', mount: 'secondary.mount' },
|
||
|
|
],
|
||
|
|
});
|
||
|
|
const result = await runMxCli([
|
||
|
|
'node',
|
||
|
|
'mx',
|
||
|
|
'service',
|
||
|
|
packageDir,
|
||
|
|
'--check',
|
||
|
|
]);
|
||
|
|
|
||
|
|
assertExitCode(result, 1);
|
||
|
|
assert.match(result.errors.join('\n'), /component-root bootstrap is ambiguous/i);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|