hivecast-sdk/packages/cli/tests/integration/fp30-publish-preflight.spec.ts

343 lines
10 KiB
TypeScript
Raw Permalink Normal View History

import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
import { createMatrixPackage } from '../helpers/package-fixtures.js';
describe('FP-30 mx publish preflight', () => {
const tempDirs: string[] = [];
afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});
it('requires login and enforces immutable version publish', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-'));
tempDirs.push(root);
const fixture = createMatrixPackage(root, {
packageName: '@acme/fp30-publish',
version: '1.0.0',
componentType: 'PublishActor',
mount: 'fp30.publish',
});
const registryDir = path.join(root, 'registry');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const unauthenticatedPublish = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(unauthenticatedPublish, 1);
assert.equal(
unauthenticatedPublish.errors.some((line) => line.includes('MX_AUTH_REQUIRED')),
true
);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-abc',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishResult, 0);
const publishPayload = parseLastJson(publishResult.logs) as {
packageName: string;
version: string;
registryTarballPath?: string;
};
assert.equal(publishPayload.packageName, fixture.packageName);
assert.equal(publishPayload.version, fixture.version);
assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);
const republishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(republishResult, 1);
assert.equal(republishResult.errors.some((line) => line.includes('MX_VERSION_EXISTS')), true);
});
it('allows webapp-only packages without actor components', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-webapp-'));
tempDirs.push(root);
const packageDir = path.join(root, 'webapp-only');
const registryDir = path.join(root, 'registry');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
fs.mkdirSync(path.join(packageDir, 'dist'), { recursive: true });
fs.writeFileSync(path.join(packageDir, 'dist', 'index.html'), '<!doctype html><title>Webapp only</title>\n', 'utf8');
fs.writeFileSync(
path.join(packageDir, 'matrix.json'),
JSON.stringify(
{
name: '@acme/fp30-webapp-only',
version: '1.0.0',
runtime: { language: 'typescript', entry: 'dist/index.html' },
webapp: { distDir: 'dist', entry: 'index.html', appName: 'webapp-only' },
components: [],
permissions: { fsPolicy: 'none' },
},
null,
2,
),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(
{
name: '@acme/fp30-webapp-only',
version: '1.0.0',
type: 'module',
files: ['dist', 'matrix.json'],
},
null,
2,
),
'utf8',
);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-webapp-only',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: packageDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: packageDir });
assertExitCode(publishResult, 0);
const publishPayload = parseLastJson(publishResult.logs) as {
packageName: string;
version: string;
registryTarballPath?: string;
};
assert.equal(publishPayload.packageName, '@acme/fp30-webapp-only');
assert.equal(publishPayload.version, '1.0.0');
assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);
});
it('allows runtime-only packages with a root actor and no components', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-runtime-only-'));
tempDirs.push(root);
const packageDir = path.join(root, 'runtime-only');
const registryDir = path.join(root, 'registry');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
fs.mkdirSync(path.join(packageDir, 'dist'), { recursive: true });
fs.writeFileSync(path.join(packageDir, 'dist', 'index.js'), 'export class RuntimeOnlyActor {}\n', 'utf8');
fs.writeFileSync(
path.join(packageDir, 'matrix.json'),
JSON.stringify(
{
name: '@acme/fp30-runtime-only',
version: '1.0.0',
root: {
type: 'RuntimeOnlyActor',
export: 'RuntimeOnlyActor',
description: 'Runtime-only root actor',
},
runtime: { language: 'typescript', entry: 'dist/index.js' },
components: [],
permissions: { fsPolicy: 'none' },
},
null,
2,
),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(
{
name: '@acme/fp30-runtime-only',
version: '1.0.0',
type: 'module',
files: ['dist', 'matrix.json'],
},
null,
2,
),
'utf8',
);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-runtime-only',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: packageDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: packageDir });
assertExitCode(publishResult, 0);
const publishPayload = parseLastJson(publishResult.logs) as {
packageName: string;
version: string;
registryTarballPath?: string;
};
assert.equal(publishPayload.packageName, '@acme/fp30-runtime-only');
assert.equal(publishPayload.version, '1.0.0');
assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);
});
it('allows service factory packages declared by matrix.json runtime.factory', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-service-'));
tempDirs.push(root);
const packageDir = path.join(root, 'service-factory');
const registryDir = path.join(root, 'registry');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
fs.mkdirSync(path.join(packageDir, 'dist'), { recursive: true });
fs.writeFileSync(
path.join(packageDir, 'dist', 'index.js'),
'export async function createServiceFactory() { return { runtime: { shutdown() {} } }; }\n',
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'matrix.json'),
JSON.stringify(
{
name: '@acme/fp30-service-factory',
version: '1.0.0',
runtime: {
language: 'typescript',
entry: 'dist/index.js',
factory: {
kind: 'factory',
export: 'createServiceFactory',
instance: {
id: 'RUNTIME-SERVICE-FACTORY',
class: 'service',
rootKind: 'package-root',
autoStart: true,
},
},
},
components: [],
permissions: { fsPolicy: 'none' },
},
null,
2,
),
'utf8',
);
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(
{
name: '@acme/fp30-service-factory',
version: '1.0.0',
type: 'module',
files: ['dist', 'matrix.json'],
},
null,
2,
),
'utf8',
);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-service-factory',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: packageDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: packageDir });
assertExitCode(publishResult, 0);
const publishPayload = parseLastJson(publishResult.logs) as {
packageName: string;
version: string;
registryTarballPath?: string;
};
assert.equal(publishPayload.packageName, '@acme/fp30-service-factory');
assert.equal(publishPayload.version, '1.0.0');
assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);
});
});