test: integration tests for gstack-review-log and gstack-review-read

- Full temp-dir integration tests via GSTACK_HOME env var override
- Tests: file creation, append behavior, slash sanitization, NO_REVIEWS
  fallback, review content read, CONFIG separator
- Regression tests: generated SKILL.md files use new helpers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-03-17 20:56:36 -07:00
parent 77929c3dd5
commit c940cb5fb3
2 changed files with 142 additions and 2 deletions
+16 -2
View File
@@ -329,14 +329,14 @@ describe('REVIEW_DASHBOARD resolver', () => {
for (const skill of REVIEW_SKILLS) {
test(`review dashboard appears in ${skill} generated file`, () => {
const content = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf-8');
expect(content).toContain('reviews.jsonl');
expect(content).toContain('gstack-review-read');
expect(content).toContain('REVIEW READINESS DASHBOARD');
});
}
test('review dashboard appears in ship generated file', () => {
const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
expect(content).toContain('reviews.jsonl');
expect(content).toContain('gstack-review-read');
expect(content).toContain('REVIEW READINESS DASHBOARD');
});
@@ -349,4 +349,18 @@ describe('REVIEW_DASHBOARD resolver', () => {
expect(content).toContain('Design Review');
expect(content).toContain('skip_eng_review');
});
test('review dashboard uses gstack-review-read (not multi-line eval+cat pattern)', () => {
for (const skill of [...REVIEW_SKILLS, 'ship']) {
const content = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf-8');
expect(content).toContain('gstack-review-read');
}
});
test('review log uses gstack-review-log (not multi-line eval+mkdir+echo pattern)', () => {
for (const skill of REVIEW_SKILLS) {
const content = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf-8');
expect(content).toContain('gstack-review-log');
}
});
});
+126
View File
@@ -775,6 +775,132 @@ describe('gstack-slug', () => {
});
});
// --- gstack-review-log helper ---
describe('gstack-review-log', () => {
const REVIEW_LOG_BIN = path.join(ROOT, 'bin', 'gstack-review-log');
test('binary exists and is executable', () => {
expect(fs.existsSync(REVIEW_LOG_BIN)).toBe(true);
const stat = fs.statSync(REVIEW_LOG_BIN);
expect(stat.mode & 0o111).toBeGreaterThan(0);
});
test('creates review log file and appends JSON payload', () => {
const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'gstack-review-log-'));
const payload = '{"skill":"test","timestamp":"2026-03-17T00:00:00Z","status":"clean"}';
const result = Bun.spawnSync([REVIEW_LOG_BIN, payload], {
cwd: ROOT,
env: { ...process.env, GSTACK_HOME: tmpDir },
stdout: 'pipe',
stderr: 'pipe',
});
expect(result.exitCode).toBe(0);
// Determine the written file path via gstack-slug
const slugResult = Bun.spawnSync([path.join(ROOT, 'bin', 'gstack-slug')], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' });
const slug = slugResult.stdout.toString().match(/SLUG=(.*)/)?.[1] ?? '';
const branch = slugResult.stdout.toString().match(/BRANCH=(.*)/)?.[1] ?? '';
const filePath = path.join(tmpDir, 'projects', slug, `${branch}-reviews.jsonl`);
expect(fs.existsSync(filePath)).toBe(true);
expect(fs.readFileSync(filePath, 'utf-8').trim()).toBe(payload);
// File path should not contain forward slashes in slug or branch segments
expect(slug).not.toContain('/');
expect(branch).not.toContain('/');
fs.rmSync(tmpDir, { recursive: true });
});
test('appending a second entry adds to same file (not overwrites)', () => {
const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'gstack-review-log-'));
const payload1 = '{"skill":"test","entry":1}';
const payload2 = '{"skill":"test","entry":2}';
Bun.spawnSync([REVIEW_LOG_BIN, payload1], {
cwd: ROOT,
env: { ...process.env, GSTACK_HOME: tmpDir },
stdout: 'pipe',
stderr: 'pipe',
});
Bun.spawnSync([REVIEW_LOG_BIN, payload2], {
cwd: ROOT,
env: { ...process.env, GSTACK_HOME: tmpDir },
stdout: 'pipe',
stderr: 'pipe',
});
const slugResult = Bun.spawnSync([path.join(ROOT, 'bin', 'gstack-slug')], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' });
const slug = slugResult.stdout.toString().match(/SLUG=(.*)/)?.[1] ?? '';
const branch = slugResult.stdout.toString().match(/BRANCH=(.*)/)?.[1] ?? '';
const filePath = path.join(tmpDir, 'projects', slug, `${branch}-reviews.jsonl`);
const lines = fs.readFileSync(filePath, 'utf-8').trim().split('\n');
expect(lines.length).toBe(2);
expect(lines[0]).toBe(payload1);
expect(lines[1]).toBe(payload2);
fs.rmSync(tmpDir, { recursive: true });
});
});
// --- gstack-review-read helper ---
describe('gstack-review-read', () => {
const REVIEW_READ_BIN = path.join(ROOT, 'bin', 'gstack-review-read');
test('binary exists and is executable', () => {
expect(fs.existsSync(REVIEW_READ_BIN)).toBe(true);
const stat = fs.statSync(REVIEW_READ_BIN);
expect(stat.mode & 0o111).toBeGreaterThan(0);
});
test('outputs NO_REVIEWS when no log file exists', () => {
const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'gstack-review-read-'));
const result = Bun.spawnSync([REVIEW_READ_BIN], {
cwd: ROOT,
env: { ...process.env, GSTACK_HOME: tmpDir },
stdout: 'pipe',
stderr: 'pipe',
});
expect(result.exitCode).toBe(0);
const output = result.stdout.toString();
expect(output).toContain('NO_REVIEWS');
expect(output).toContain('---CONFIG---');
fs.rmSync(tmpDir, { recursive: true });
});
test('outputs review content when log file exists', () => {
const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'gstack-review-read-'));
const logBin = path.join(ROOT, 'bin', 'gstack-review-log');
const payload = '{"skill":"plan-eng-review","status":"clean"}';
// Write a review first
Bun.spawnSync([logBin, payload], {
cwd: ROOT,
env: { ...process.env, GSTACK_HOME: tmpDir },
stdout: 'pipe',
stderr: 'pipe',
});
// Now read
const result = Bun.spawnSync([REVIEW_READ_BIN], {
cwd: ROOT,
env: { ...process.env, GSTACK_HOME: tmpDir },
stdout: 'pipe',
stderr: 'pipe',
});
expect(result.exitCode).toBe(0);
const output = result.stdout.toString();
expect(output).toContain(payload);
expect(output).toContain('---CONFIG---');
fs.rmSync(tmpDir, { recursive: true });
});
});
// --- Test Bootstrap validation ---
describe('Test Bootstrap ({{TEST_BOOTSTRAP}}) integration', () => {