From c940cb5fb3e33dee6aff4b1fd2df48cc1b75bc1e Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Tue, 17 Mar 2026 20:56:36 -0700 Subject: [PATCH] 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) --- test/gen-skill-docs.test.ts | 18 ++++- test/skill-validation.test.ts | 126 ++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 2 deletions(-) diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index c3861e8d..77798e73 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -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'); + } + }); }); diff --git a/test/skill-validation.test.ts b/test/skill-validation.test.ts index 81d97d31..46c404be 100644 --- a/test/skill-validation.test.ts +++ b/test/skill-validation.test.ts @@ -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', () => {