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 --access-key-secret /u, ); }); function withReleaseFixture(channel, architecture, run) { const root = mkdtempSync(path.join(os.tmpdir(), 'agc-upload-plan-')); try { const artifact = path.join( root, channel === 'dev-win' ? '陶泥儿_1.2.3_x64-setup.exe' : '陶泥儿.app.tar.gz', ); const downloadArtifact = channel === 'dev-win' ? artifact : path.join(root, `陶泥儿_1.2.3_${architecture}.dmg`); const manifestPath = path.join(root, 'latest.json'); const legacyManifestPath = channel === 'dev-win' ? 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, 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-mac', 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-win', '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), ); }); }); for (const failedArtifactIndex of [0, 1, 2]) { test(`failed Mac object ${failedArtifactIndex} prevents both later objects and latest publication`, () => { withReleaseFixture('dev-mac', '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-mac', '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); }); });