chore: establish learning module baseline
This commit is contained in:
commit
2d04197f3f
2409 files changed
+533050
No files matched your search
@@ -0,0 +1,254 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user