mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-14 17:05:28 +02:00
The only migration without a dedicated test. Covers copy-when-absent (script must mkdir GSTACK_HOME itself), destination-wins (never overwrites), clean no-op, and two-run idempotence — asserting file existence and contents, not just exit codes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
118 lines
4.2 KiB
TypeScript
118 lines
4.2 KiB
TypeScript
/**
|
|
* v1.78.0.0 migration — carry feature-discovery acknowledgement markers
|
|
* (.feature-prompted-continuous-checkpoint, .feature-prompted-model-overlay)
|
|
* from the gstack install dir to GSTACK_HOME (#2728 absorption).
|
|
*
|
|
* Exercises the script in hermetic mkdtemp roots via GSTACK_INSTALL_DIR /
|
|
* GSTACK_HOME overrides. Covers: copy-when-absent, destination-wins,
|
|
* clean no-op, and idempotent re-run.
|
|
*/
|
|
|
|
import { describe, test, expect, beforeEach, afterAll } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { spawnSync } from 'child_process';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const MIGRATION = path.join(ROOT, 'gstack-upgrade', 'migrations', 'v1.78.0.0.sh');
|
|
|
|
const MARKERS = [
|
|
'.feature-prompted-continuous-checkpoint',
|
|
'.feature-prompted-model-overlay',
|
|
] as const;
|
|
|
|
const tmpRoots: string[] = [];
|
|
let tmpHome: string;
|
|
let installDir: string;
|
|
let gstackHome: string;
|
|
|
|
beforeEach(() => {
|
|
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'mig-v1.78-'));
|
|
tmpRoots.push(tmpHome);
|
|
installDir = path.join(tmpHome, 'install');
|
|
gstackHome = path.join(tmpHome, '.gstack');
|
|
fs.mkdirSync(installDir, { recursive: true });
|
|
// gstackHome deliberately NOT pre-created: the script's own `mkdir -p` is
|
|
// part of the contract (fresh installs have no ~/.gstack yet).
|
|
});
|
|
|
|
afterAll(() => {
|
|
for (const dir of tmpRoots) fs.rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
function run(): { code: number; stdout: string; stderr: string } {
|
|
const r = spawnSync('bash', [MIGRATION], {
|
|
env: {
|
|
PATH: '/usr/bin:/bin',
|
|
HOME: tmpHome,
|
|
GSTACK_INSTALL_DIR: installDir,
|
|
GSTACK_HOME: gstackHome,
|
|
},
|
|
encoding: 'utf-8',
|
|
cwd: tmpHome,
|
|
timeout: 30_000,
|
|
});
|
|
return { code: r.status ?? -1, stdout: r.stdout || '', stderr: r.stderr || '' };
|
|
}
|
|
|
|
describe('v1.78.0.0 migration — feature markers → GSTACK_HOME', () => {
|
|
test('markers in INSTALL_DIR, absent in GSTACK_HOME → created in GSTACK_HOME, source untouched', () => {
|
|
for (const m of MARKERS) fs.writeFileSync(path.join(installDir, m), 'source-content\n');
|
|
|
|
const r = run();
|
|
expect(r.code).toBe(0);
|
|
for (const m of MARKERS) {
|
|
expect(fs.existsSync(path.join(gstackHome, m))).toBe(true);
|
|
expect(r.stdout).toContain(`migrated: ${m}`);
|
|
// Copy, not move: the install-dir marker stays, content intact.
|
|
expect(fs.readFileSync(path.join(installDir, m), 'utf-8')).toBe('source-content\n');
|
|
}
|
|
});
|
|
|
|
test('marker already in GSTACK_HOME → NOT overwritten (destination wins); missing sibling still migrates', () => {
|
|
fs.mkdirSync(gstackHome, { recursive: true });
|
|
const [checkpoint, overlay] = MARKERS;
|
|
fs.writeFileSync(path.join(installDir, checkpoint), 'install-side\n');
|
|
fs.writeFileSync(path.join(gstackHome, checkpoint), 'dest-side\n');
|
|
fs.writeFileSync(path.join(installDir, overlay), '');
|
|
|
|
const r = run();
|
|
expect(r.code).toBe(0);
|
|
expect(fs.readFileSync(path.join(gstackHome, checkpoint), 'utf-8')).toBe('dest-side\n');
|
|
expect(r.stdout).not.toContain(`migrated: ${checkpoint}`);
|
|
expect(fs.existsSync(path.join(gstackHome, overlay))).toBe(true);
|
|
expect(r.stdout).toContain(`migrated: ${overlay}`);
|
|
});
|
|
|
|
test('no markers anywhere → clean no-op, exit 0, nothing created', () => {
|
|
const r = run();
|
|
expect(r.code).toBe(0);
|
|
expect(r.stdout).toBe('');
|
|
expect(r.stderr).toBe('');
|
|
for (const m of MARKERS) {
|
|
expect(fs.existsSync(path.join(gstackHome, m))).toBe(false);
|
|
}
|
|
});
|
|
|
|
test('idempotent: second run exits 0, migrates nothing new, end state unchanged', () => {
|
|
for (const m of MARKERS) fs.writeFileSync(path.join(installDir, m), '');
|
|
|
|
const r1 = run();
|
|
expect(r1.code).toBe(0);
|
|
const stateAfterFirst = MARKERS.map((m) => [
|
|
fs.existsSync(path.join(gstackHome, m)),
|
|
fs.readFileSync(path.join(gstackHome, m), 'utf-8'),
|
|
]);
|
|
|
|
const r2 = run();
|
|
expect(r2.code).toBe(0);
|
|
expect(r2.stdout).toBe(''); // destinations exist now — no "migrated:" lines
|
|
const stateAfterSecond = MARKERS.map((m) => [
|
|
fs.existsSync(path.join(gstackHome, m)),
|
|
fs.readFileSync(path.join(gstackHome, m), 'utf-8'),
|
|
]);
|
|
expect(stateAfterSecond).toEqual(stateAfterFirst);
|
|
});
|
|
});
|