Files
openmaic/OpenMAIC/tests/ci/publish-openmaic-skill.test.ts
2026-08-16 14:58:47 +08:00

255 lines
8.4 KiB
TypeScript

import { spawnSync } from 'node:child_process';
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { tmpdir } from 'node:os';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterAll, describe, expect, it } from 'vitest';
const repositoryRoot = fileURLToPath(new URL('../..', import.meta.url));
const publishScript = resolve(repositoryRoot, '.github/scripts/publish-openmaic-skill.sh');
const packageJsonPath = resolve(repositoryRoot, 'package.json');
const semverPackageJsonPath = createRequire(packageJsonPath).resolve('semver/package.json');
const fixtureRoot = mkdtempSync(resolve(tmpdir(), 'clawhub-publish-test-'));
const stubClawhub = resolve(fixtureRoot, 'clawhub-stub.sh');
let runIndex = 0;
writeFileSync(
stubClawhub,
`#!/bin/bash
set -euo pipefail
printf '%s\\t' "$@" >> "$STUB_CALLS"
printf '\\n' >> "$STUB_CALLS"
if [[ -n "\${STUB_PREFLIGHT_JSON:-}" && " $* " != *" --version "* ]]; then
printf '%s\\n' "$STUB_PREFLIGHT_JSON"
else
printf '%s\\n' '{"published":true}'
fi
`,
'utf8',
);
chmodSync(stubClawhub, 0o755);
afterAll(() => {
rmSync(fixtureRoot, { recursive: true, force: true });
});
type RunOptions = {
args?: string[];
clawhub?: string;
omittedEnvironment?: 'SOURCE_REPO' | 'PUBLISH_VERSION' | 'CLAWHUB' | 'RUNNER_TEMP';
pathPrefix?: string;
preflight?: unknown;
publishVersion?: string;
sourceRepo?: string;
};
function runPublish(options: RunOptions = {}) {
const callsPath = resolve(fixtureRoot, `calls-${runIndex++}.txt`);
const env = Object.create(null) as NodeJS.ProcessEnv;
for (const name of ['PATH', 'HOME', 'TMPDIR', 'TMP', 'TEMP', 'SystemRoot', 'WINDIR']) {
if (process.env[name] !== undefined) env[name] = process.env[name];
}
Object.assign(env, {
CLAWHUB: options.clawhub ?? stubClawhub,
PUBLISH_VERSION: options.publishVersion ?? '',
RUNNER_TEMP: fixtureRoot,
SEMVER_PACKAGE_JSON: semverPackageJsonPath,
SOURCE_REPO: options.sourceRepo ?? 'THU-MAIC/OpenMAIC',
STUB_CALLS: callsPath,
});
if (options.preflight !== undefined) {
env.STUB_PREFLIGHT_JSON = JSON.stringify(options.preflight);
}
if (options.pathPrefix) env.PATH = `${options.pathPrefix}:${env.PATH ?? ''}`;
if (options.omittedEnvironment) delete env[options.omittedEnvironment];
const result = spawnSync('/bin/bash', [publishScript, ...(options.args ?? [])], {
cwd: repositoryRoot,
encoding: 'utf8',
env,
});
const calls = (() => {
try {
return readFileSync(callsPath, 'utf8')
.trimEnd()
.split('\n')
.map((line) => line.split('\t'));
} catch {
return [];
}
})();
return { calls, result };
}
function expectSuccess(result: ReturnType<typeof runPublish>['result'], stdout: string) {
expect(result.error).toBeUndefined();
expect(result.signal).toBeNull();
expect(result.status).toBe(0);
expect(result.stdout).toBe(stdout);
expect(result.stderr).toBe('');
}
function expectFailure(result: ReturnType<typeof runPublish>['result'], message: string) {
expect(result.error).toBeUndefined();
expect(result.signal).toBeNull();
expect(result.status).toBe(1);
expect(result.stdout).toBe('');
expect(result.stderr).toBe(`::error::${message}\n`);
}
describe('publish-openmaic-skill shell contract', () => {
it('performs a real publish with no positional argument under macOS Bash 3.2', () => {
const { calls, result } = runPublish();
expectSuccess(result, '{"published":true}\n');
expect(calls).toHaveLength(1);
expect(calls[0]).toContain('publish');
expect(calls[0]).not.toContain('--dry-run');
});
it('adds dry-run only for a preview publish', () => {
const { calls, result } = runPublish({ args: ['--dry-run'] });
expectSuccess(result, '{"published":true}\n');
expect(calls).toHaveLength(1);
expect(calls[0]).toContain('--dry-run');
});
it('preflights a manual version and publishes the canonical stable version', () => {
const preflight = {
status: 'would-publish',
version: '0.4.0',
latestVersion: '0.3.1',
fingerprint: 'fixture-fingerprint',
};
const { calls, result } = runPublish({
preflight,
publishVersion: ' v0.4.0 ',
});
expectSuccess(result, `${JSON.stringify(preflight)}\n{"published":true}\n`);
expect(calls).toHaveLength(2);
expect(calls[0]).toContain('--dry-run');
expect(calls[0]).not.toContain('--version');
expect(calls[1]).not.toContain('--dry-run');
expect(calls[1]).toContain('--version');
expect(calls[1][calls[1].indexOf('--version') + 1]).toBe('0.4.0');
});
it('keeps a manual version preview in dry-run mode after preflight', () => {
const preflight = {
status: 'would-publish',
version: '0.4.0',
latestVersion: '0.3.1',
fingerprint: 'fixture-fingerprint',
};
const { calls, result } = runPublish({
args: ['--dry-run'],
preflight,
publishVersion: ' v0.4.0 ',
});
expectSuccess(result, `${JSON.stringify(preflight)}\n{"published":true}\n`);
expect(calls).toHaveLength(2);
expect(calls[1]).toContain('--dry-run');
expect(calls[1]).toContain('--version');
expect(calls[1][calls[1].indexOf('--version') + 1]).toBe('0.4.0');
});
it('stops after a noop preflight', () => {
const preflight = {
status: 'unchanged',
version: '0.3.1',
latestVersion: '0.3.1',
fingerprint: 'fixture-fingerprint',
};
const { calls, result } = runPublish({ preflight, publishVersion: '0.3.1' });
expectSuccess(
result,
`${JSON.stringify(preflight)}\n::notice::The requested version already has identical content.\n`,
);
expect(calls).toHaveLength(1);
expect(calls[0]).toContain('--dry-run');
});
it('rejects unsupported arguments', () => {
const usage = runPublish({ args: ['--publish'] });
expectFailure(usage.result, 'Usage: publish-openmaic-skill.sh [--dry-run]');
expect(usage.calls).toEqual([]);
});
it.each([
['an empty argument before dry-run', ['', '--dry-run']],
['a trailing argument after dry-run', ['--dry-run', '--anything']],
])('rejects %s without invoking ClawHub', (_name, args) => {
const { calls, result } = runPublish({ args });
expectFailure(result, 'Usage: publish-openmaic-skill.sh [--dry-run]');
expect(calls).toEqual([]);
});
it.each([
['SOURCE_REPO', 'SOURCE_REPO is required.'],
['PUBLISH_VERSION', 'PUBLISH_VERSION is required.'],
['CLAWHUB', 'CLAWHUB is required.'],
] as const)('rejects an unset %s environment variable', (name, message) => {
const { calls, result } = runPublish({ omittedEnvironment: name });
expectFailure(result, message);
expect(calls).toEqual([]);
});
it.each([
['SOURCE_REPO', { sourceRepo: '' }, 'SOURCE_REPO is required.'],
['CLAWHUB', { clawhub: '' }, 'CLAWHUB is required.'],
] as const)('rejects an empty %s environment variable', (_name, options, message) => {
const { calls, result } = runPublish(options);
expectFailure(result, message);
expect(calls).toEqual([]);
});
it('requires RUNNER_TEMP when an explicit version reaches preflight', () => {
const { calls, result } = runPublish({
omittedEnvironment: 'RUNNER_TEMP',
publishVersion: '0.4.0',
});
expectFailure(result, 'RUNNER_TEMP is required for version preflight.');
expect(calls).toEqual([]);
});
it('accepts an explicitly empty PUBLISH_VERSION for automatic versioning', () => {
const { calls, result } = runPublish({ publishVersion: '' });
expectSuccess(result, '{"published":true}\n');
expect(calls).toHaveLength(1);
expect(calls[0]).not.toContain('--version');
});
it('rejects a malformed checker decision', () => {
const fakeBin = resolve(fixtureRoot, `fake-bin-${runIndex++}`);
const fakeNode = resolve(fakeBin, 'node');
mkdirSync(fakeBin);
writeFileSync(fakeNode, '#!/bin/bash\nprintf "malformed-decision\\n"\n', 'utf8');
chmodSync(fakeNode, 0o755);
const preflight = {
status: 'would-publish',
version: '0.4.0',
latestVersion: '0.3.1',
fingerprint: 'fixture-fingerprint',
};
const { calls, result } = runPublish({
pathPrefix: fakeBin,
preflight,
publishVersion: '0.4.0',
});
expect(result.status).toBe(1);
expect(result.stderr).toBe('::error::Invalid version preflight decision.\n');
expect(calls).toHaveLength(1);
});
});