Files
Genarrative/apps/ai-game-creator-shell/scripts/release-oss.test.mjs
T
kdletters 623e007fae
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m36s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m37s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m42s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m51s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m55s
Project CI / AI game creator shell Rust crates (push) Successful in 4m15s
Project CI / Repository checks (push) Successful in 4m45s
Project CI / Frontend tests (push) Successful in 7m47s
Project CI / Native shell tests (push) Successful in 9m33s
Project CI / Backend tests (push) Successful in 10m9s
Project CI / AI game creator shell web tests (push) Successful in 4m38s
完善客户端发布渠道并接入模板库灰度
区分发布渠道与系统,支持 dev、release 和自定义渠道
允许网站通过服务端配置选择客户端下载检测渠道
接入模板库灰度权限并阻断退出和切号后的异步操作
补齐发布、下载、灰度与会话竞态测试及当前规范
2026-09-20 12:12:49 +08:00

238 lines
7.2 KiB
JavaScript

import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { test } from 'node:test';
import {
createReleaseUploadPlan,
formatOssutilCommand,
readReleaseDryRun,
uploadReleaseArtifacts,
} from './release-oss.mjs';
test('dry run only accepts explicit truthy values', () => {
assert.equal(readReleaseDryRun({}), false);
assert.equal(readReleaseDryRun({ AGC_RELEASE_DRY_RUN: '1' }), true);
assert.equal(readReleaseDryRun({ AGC_RELEASE_DRY_RUN: ' true ' }), true);
assert.equal(readReleaseDryRun({ AGC_RELEASE_DRY_RUN: '0' }), false);
assert.equal(readReleaseDryRun({ AGC_RELEASE_DRY_RUN: '' }), false);
});
test('printed upload command keeps arguments and hides credentials', () => {
const command = formatOssutilCommand({
binary: 'ossutil',
args: [
'cp',
'--force',
'陶泥儿 0.1.48.exe',
'oss://agc-dev/agc/dev-win/x.exe',
],
endpoint: 'oss-rg-china-mainland.aliyuncs.com',
credentials: true,
});
assert.match(command, /^ossutil cp --force /u);
assert.match(command, /"陶泥儿 0\.1\.48\.exe"/u);
assert.match(command, /oss:\/\/agc-dev\/agc\/dev-win\/x\.exe/u);
assert.match(
command,
/--access-key-id <redacted> --access-key-secret <redacted>/u,
);
});
function withReleaseFixture(channel, architecture, run, platform = 'macos') {
const root = mkdtempSync(path.join(os.tmpdir(), 'agc-upload-plan-'));
try {
const artifact = path.join(
root,
platform === 'windows'
? '陶泥儿_1.2.3_x64-setup.exe'
: '陶泥儿.app.tar.gz',
);
const downloadArtifact =
platform === 'windows'
? artifact
: path.join(root, `陶泥儿_1.2.3_${architecture}.dmg`);
const manifestPath = path.join(root, 'latest.json');
const legacyManifestPath =
channel === 'dev' && platform === 'windows'
? path.join(root, 'legacy-latest.json')
: null;
for (const file of [
artifact,
`${artifact}.sig`,
downloadArtifact,
manifestPath,
legacyManifestPath,
].filter(Boolean)) {
writeFileSync(file, 'fixture');
}
return run({
artifact,
downloadArtifact,
channel,
target:
platform === 'windows'
? 'x86_64-pc-windows-msvc'
: `${architecture === 'aarch64' ? 'aarch64' : 'x86_64'}-apple-darwin`,
manifest: { version: '1.2.3' },
manifestPath,
legacyManifestPath,
});
} finally {
rmSync(root, { recursive: true, force: true });
}
}
const uploadOptions = {
bucket: 'agc-dev',
endpoint: 'oss-rg-china-mainland.aliyuncs.com',
log: () => {},
};
for (const architecture of ['aarch64', 'x64']) {
test(`uploads every ${architecture} Mac object before the channel pointer`, () => {
withReleaseFixture('dev', architecture, (release) => {
const calls = [];
uploadReleaseArtifacts(release, {
...uploadOptions,
spawn: (binary, args, options) => {
assert.equal(binary, 'ossutil');
assert.equal(options.shell, false);
assert.deepEqual(args.slice(0, 2), ['cp', '--force']);
calls.push({ source: args[2], destination: args[3] });
return { status: 0 };
},
});
assert.deepEqual(
calls.map(({ source }) => source),
[
release.artifact,
`${release.artifact}.sig`,
release.downloadArtifact,
release.manifestPath,
],
);
assert.equal(
calls[2].destination,
`oss://agc-dev/agc/dev-mac/1.2.3/陶泥儿_1.2.3_${architecture}.dmg`,
);
assert.equal(
calls[3].destination,
'oss://agc-dev/agc/dev-mac/latest.json',
);
});
});
}
test('Windows uploads the shared installer once and publishes migration metadata last', () => {
withReleaseFixture(
'dev',
'x64',
(release) => {
const plan = createReleaseUploadPlan(release, 'agc-dev');
assert.deepEqual(
plan.map(({ source }) => source),
[
release.artifact,
`${release.artifact}.sig`,
release.manifestPath,
release.legacyManifestPath,
],
);
assert.equal(plan.at(-1).destination, 'oss://agc-dev/agc/latest.json');
const calls = [];
uploadReleaseArtifacts(release, {
...uploadOptions,
spawn: (_binary, args) => {
assert.deepEqual(args.slice(0, 2), ['cp', '--force']);
calls.push(args[3]);
return { status: 0 };
},
});
assert.deepEqual(
calls,
plan.map(({ destination }) => destination),
);
},
'windows',
);
});
for (const failedArtifactIndex of [0, 1, 2]) {
test(`failed Mac object ${failedArtifactIndex} prevents both later objects and latest publication`, () => {
withReleaseFixture('dev', 'aarch64', (release) => {
const destinations = [];
assert.throws(
() =>
uploadReleaseArtifacts(release, {
...uploadOptions,
spawn: (_binary, args) => {
destinations.push(args[3]);
return {
status: destinations.length - 1 === failedArtifactIndex ? 1 : 0,
};
},
}),
/OSS 上传失败/u,
);
assert.equal(destinations.length, failedArtifactIndex + 1);
assert.ok(
destinations.every(
(destination) => !destination.endsWith('/latest.json'),
),
);
});
});
}
test('dry run prints the complete plan without spawning uploads or exposing credentials', () => {
withReleaseFixture('dev', 'aarch64', (release) => {
const output = [];
uploadReleaseArtifacts(release, {
...uploadOptions,
dryRun: true,
accessKeyId: 'fixture-id',
accessKeySecret: 'fixture-secret',
spawn: () => assert.fail('dry run must never execute ossutil'),
log: (line) => output.push(line),
});
assert.equal(
output.filter((line) => line.startsWith('[dry-run]')).length,
4,
);
assert.match(output.join('\n'), /\.dmg/u);
assert.match(output.at(-1), /未写入任何 OSS 对象/u);
assert.doesNotMatch(output.join('\n'), /fixture-id|fixture-secret|已上传/u);
});
});
for (const channel of ['release', 'beta-2']) {
for (const platform of ['windows', 'macos']) {
test(`${channel} ${platform} uploads only its own partition and cannot write the dev bridge`, () => {
withReleaseFixture(
channel,
'x64',
(release) => {
const plan = createReleaseUploadPlan(release, 'agc-dev');
const suffix = platform === 'windows' ? 'win' : 'mac';
const prefix = `oss://agc-dev/agc/${channel}-${suffix}/`;
assert.ok(
plan.every(({ destination }) => destination.startsWith(prefix)),
);
assert.equal(plan.at(-1).destination, `${prefix}latest.json`);
assert.throws(
() =>
createReleaseUploadPlan(
{ ...release, legacyManifestPath: release.manifestPath },
'agc-dev',
),
/只属于 dev 渠道/u,
);
},
platform,
);
});
}
}