import { afterEach, describe, expect, test } from 'bun:test'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { execFile as execFileCallback } from 'node:child_process'; import { promisify } from 'node:util'; import { main } from '../runtime/cli.js'; const temporaryRoots: string[] = []; const execFile = promisify(execFileCallback); function sink() { let value = ''; return { write(chunk: unknown) { value += Buffer.from(chunk as any).toString('utf8'); }, value() { return value; }, }; } async function fixture() { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'gstack effect cli ')); temporaryRoots.push(root); const cwd = path.join(root, 'project'); const home = path.join(root, 'home'); await fs.mkdir(cwd); return { root, cwd, home, env: { ...process.env, GSTACK_HOME: home } }; } afterEach(async () => { await Promise.all(temporaryRoots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 50, }))); }); describe('gstack state external-effect CLI', () => { test('an actual local git push ship effect is executed at most once', async () => { const { root, cwd, env } = await fixture(); const remote = path.join(root, 'origin.git'); const git = async (args: string[], workingDirectory = cwd) => (await execFile('git', args, { cwd: workingDirectory, encoding: 'utf8', timeout: 15_000, })).stdout.trim(); await git(['init']); await git(['config', 'user.name', 'GStack Test']); await git(['config', 'user.email', 'gstack@example.invalid']); await fs.writeFile(path.join(cwd, 'release.txt'), 'first\n'); await git(['add', 'release.txt']); await git(['commit', '-m', 'first release']); await git(['init', '--bare', remote], root); await git(['remote', 'add', 'origin', remote]); const out = sink(); const err = sink(); expect(await main(['state', 'begin', 'ship', '--run-id', 'run_ship_once'], { cwd, env, stdout: out, stderr: err })).toBe(0); const command = [ 'state', 'effect', 'run_ship_once', 'git.push.origin', '--', 'git', 'push', 'origin', 'HEAD:refs/heads/main', ]; expect(await main(command, { cwd, env, stdout: out, stderr: err })).toBe(0); const firstCommit = await git(['rev-parse', 'HEAD']); expect(await git(['rev-parse', 'refs/heads/main'], remote)).toBe(firstCommit); // A wrongly repeated push would advance the remote to this second commit. await fs.writeFile(path.join(cwd, 'release.txt'), 'second\n'); await git(['add', 'release.txt']); await git(['commit', '-m', 'second release']); expect(await git(['rev-parse', 'HEAD'])).not.toBe(firstCommit); expect(await main(command, { cwd, env, stdout: out, stderr: err })).toBe(0); expect(await git(['rev-parse', 'refs/heads/main'], remote)).toBe(firstCommit); expect(await main(['state', 'complete', 'run_ship_once'], { cwd, env, stdout: out, stderr: err })).toBe(0); }); test('resume refuses to repeat an effect whose command may already have happened', async () => { const { cwd, env } = await fixture(); const marker = path.join(cwd, 'deploys.txt'); const out = sink(); const err = sink(); await main(['state', 'begin', 'ship', '--run-id', 'run_ship_crash'], { cwd, env, stdout: out, stderr: err }); const command = [ 'state', 'effect', 'run_ship_crash', 'deploy.production', '--', process.execPath, '-e', `require('node:fs').appendFileSync(${JSON.stringify(marker)}, 'deploy\\n');process.exit(7)`, ]; expect(await main(command, { cwd, env, stdout: out, stderr: err })).toBe(1); expect(await main(['state', 'resume', 'run_ship_crash'], { cwd, env, stdout: out, stderr: err })).toBe(0); expect(await main(command, { cwd, env, stdout: out, stderr: err })).toBe(1); expect((await fs.readFile(marker, 'utf8')).trim().split('\n')).toEqual(['deploy']); expect(err.value()).toContain('was already claimed'); expect(await main( ['state', 'reconcile-not-applied', 'run_ship_crash', 'deploy.production'], { cwd, env, stdout: out, stderr: err }, )).toBe(2); }); });