critique-storage.test.mjs

  1/**
  2 * Tests for critique snapshot persistence.
  3 * Run with: node --test tests/critique-storage.test.mjs
  4 */
  5
  6import { describe, it, beforeEach, afterEach } from 'node:test';
  7import assert from 'node:assert/strict';
  8import { mkdtempSync, rmSync, symlinkSync } from 'node:fs';
  9import { join } from 'node:path';
 10import { tmpdir } from 'node:os';
 11import { spawnSync } from 'node:child_process';
 12import { fileURLToPath } from 'node:url';
 13
 14const SCRIPT = fileURLToPath(new URL('../skill/scripts/critique-storage.mjs', import.meta.url));
 15
 16import {
 17  slugFromTarget,
 18  writeSnapshot,
 19  readLatestSnapshot,
 20  readTrend,
 21  nowFilenameStamp,
 22} from '../skill/scripts/critique-storage.mjs';
 23
 24let cwd;
 25beforeEach(() => { cwd = mkdtempSync(join(tmpdir(), 'imp-critique-')); });
 26afterEach(() => { rmSync(cwd, { recursive: true, force: true }); });
 27
 28describe('slugFromTarget', () => {
 29  it('kebabs a relative file path', () => {
 30    assert.equal(slugFromTarget('site/pages/index.astro', { cwd }), 'site-pages-index-astro');
 31  });
 32
 33  it('kebabs an absolute path inside cwd by relativizing', () => {
 34    const abs = join(cwd, 'site/pages/index.astro');
 35    assert.equal(slugFromTarget(abs, { cwd }), 'site-pages-index-astro');
 36  });
 37
 38  it('uses basename for absolute paths outside cwd', () => {
 39    // Sibling path, not under cwd
 40    const abs = join(tmpdir(), 'somewhere', 'else', 'page.html');
 41    assert.equal(slugFromTarget(abs, { cwd }), 'page-html');
 42  });
 43
 44  it('drops port from URL', () => {
 45    assert.equal(slugFromTarget('http://localhost:3000/pricing', { cwd }), 'localhost-pricing');
 46  });
 47
 48  it('normalizes URL casing and trailing slash', () => {
 49    assert.equal(
 50      slugFromTarget('https://Impeccable.Style/docs/audit/', { cwd }),
 51      'impeccable-style-docs-audit',
 52    );
 53  });
 54
 55  it('strips query strings', () => {
 56    assert.equal(
 57      slugFromTarget('https://example.com/x?utm=1&foo=bar', { cwd }),
 58      'example-com-x',
 59    );
 60  });
 61
 62  it('returns null for empty / project-root inputs', () => {
 63    assert.equal(slugFromTarget('', { cwd }), null);
 64    assert.equal(slugFromTarget('.', { cwd }), null);
 65    assert.equal(slugFromTarget(null, { cwd }), null);
 66  });
 67
 68  it('caps overly long slugs from the tail', () => {
 69    const longPath = 'a/'.repeat(60) + 'file.tsx';   // way over 50
 70    const slug = slugFromTarget(longPath, { cwd });
 71    assert.ok(slug.length <= 50);
 72    assert.ok(slug.endsWith('file-tsx'));
 73  });
 74
 75  it('is stable: same input → same slug', () => {
 76    const a = slugFromTarget('site/pages/index.astro', { cwd });
 77    const b = slugFromTarget('site/pages/index.astro', { cwd });
 78    assert.equal(a, b);
 79  });
 80});
 81
 82describe('nowFilenameStamp', () => {
 83  it('is windows-safe (no colons or dots in the time fragment)', () => {
 84    const stamp = nowFilenameStamp(new Date('2026-05-12T18:30:00.123Z'));
 85    assert.equal(stamp, '2026-05-12T18-30-00Z');
 86  });
 87});
 88
 89describe('writeSnapshot + readLatestSnapshot', () => {
 90  it('round-trips body and frontmatter', () => {
 91    const out = writeSnapshot({
 92      slug: 'index-astro',
 93      meta: { target: 'the homepage', total_score: 28, p0_count: 1, p1_count: 3 },
 94      body: '# Critique\n\nP0: nested cards',
 95      cwd,
 96    });
 97    assert.ok(out.endsWith('__index-astro.md'));
 98    const latest = readLatestSnapshot('index-astro', { cwd });
 99    assert.equal(latest.meta.slug, 'index-astro');
100    assert.equal(latest.meta.target, 'the homepage');
101    assert.equal(latest.meta.total_score, 28);
102    assert.match(latest.body, /P0: nested cards/);
103  });
104
105  it('returns null when no snapshot for slug', () => {
106    assert.equal(readLatestSnapshot('nope', { cwd }), null);
107  });
108
109  it('picks the newest by filename when multiple exist', () => {
110    writeSnapshot({ slug: 'index-astro', meta: { total_score: 22 }, body: 'old', cwd, now: new Date('2026-05-01T00:00:00Z') });
111    writeSnapshot({ slug: 'index-astro', meta: { total_score: 30 }, body: 'new', cwd, now: new Date('2026-05-12T00:00:00Z') });
112    const latest = readLatestSnapshot('index-astro', { cwd });
113    assert.equal(latest.meta.total_score, 30);
114    assert.match(latest.body, /new/);
115  });
116
117  it('does not see snapshots for a different slug', () => {
118    writeSnapshot({ slug: 'pricing-astro', meta: { total_score: 10 }, body: 'b', cwd });
119    assert.equal(readLatestSnapshot('index-astro', { cwd }), null);
120  });
121
122  it('caller-supplied meta cannot override computed timestamp or slug', () => {
123    // Defends against a corrupt IMPECCABLE_CRITIQUE_META blob (parsed from
124    // an env var) silently rewriting fields that must agree with the
125    // filename. Otherwise readTrend would attribute scores to the wrong
126    // timestamps with no error.
127    const out = writeSnapshot({
128      slug: 'index-astro',
129      meta: { timestamp: 'NOT_A_REAL_STAMP', slug: 'somewhere-else', total_score: 50 },
130      body: 'b',
131      cwd,
132      now: new Date('2026-05-12T18:30:00Z'),
133    });
134    const latest = readLatestSnapshot('index-astro', { cwd });
135    assert.equal(latest.meta.slug, 'index-astro');
136    assert.equal(latest.meta.timestamp, '2026-05-12T18-30-00Z');
137    // The legit meta field still lands.
138    assert.equal(latest.meta.total_score, 50);
139    // The filename matches the computed slug.
140    assert.ok(out.endsWith('2026-05-12T18-30-00Z__index-astro.md'));
141  });
142
143  it('quotes values containing : or # to keep parsing simple', () => {
144    writeSnapshot({
145      slug: 'x',
146      meta: { target: 'docs: critique # main' },
147      body: '...',
148      cwd,
149    });
150    const latest = readLatestSnapshot('x', { cwd });
151    assert.equal(latest.meta.target, 'docs: critique # main');
152  });
153});
154
155describe('CLI entry point', () => {
156  // Why a subprocess test: the CLI guard at the bottom of the script
157  // previously compared import.meta.url to `file://${process.argv[1]}`,
158  // which silently broke on Windows (forward vs back slashes) — exit 0,
159  // no output, save skipped. The exported functions kept passing because
160  // tests never spawned the script as a process. See issue #155.
161  it('slug subcommand prints a slug and exits 0', () => {
162    const r = spawnSync(process.execPath, [SCRIPT, 'slug', 'site/pages/index.astro'], {
163      cwd,
164      encoding: 'utf-8',
165    });
166    assert.equal(r.status, 0, `stderr: ${r.stderr}`);
167    assert.equal(r.stdout.trim(), 'site-pages-index-astro');
168  });
169
170  it('slug subcommand exits 1 with a message for empty input', () => {
171    const r = spawnSync(process.execPath, [SCRIPT, 'slug', ''], { cwd, encoding: 'utf-8' });
172    assert.equal(r.status, 1);
173    assert.match(r.stderr, /no stable slug/);
174  });
175
176  it('runs when invoked through a symlinked harness path', () => {
177    const linkedScript = join(cwd, 'linked-critique-storage.mjs');
178    symlinkSync(SCRIPT, linkedScript);
179
180    const r = spawnSync(process.execPath, [linkedScript, 'slug', 'index.html'], {
181      cwd,
182      encoding: 'utf-8',
183    });
184
185    assert.equal(r.status, 0, `stderr: ${r.stderr}`);
186    assert.equal(r.stdout.trim(), 'index-html');
187  });
188
189  it('latest subcommand exits 2 when no snapshot exists', () => {
190    const r = spawnSync(process.execPath, [SCRIPT, 'latest', 'never-written'], {
191      cwd,
192      encoding: 'utf-8',
193    });
194    assert.equal(r.status, 2);
195  });
196});
197
198describe('readTrend', () => {
199  it('returns last N entries oldest → newest, filtered by slug', () => {
200    for (let i = 0; i < 6; i++) {
201      writeSnapshot({
202        slug: 'index-astro',
203        meta: { total_score: 20 + i },
204        body: `run ${i}`,
205        cwd,
206        now: new Date(2026, 4, i + 1),
207      });
208    }
209    writeSnapshot({ slug: 'pricing-astro', meta: { total_score: 99 }, body: 'unrelated', cwd });
210    const trend = readTrend('index-astro', { limit: 5, cwd });
211    assert.equal(trend.length, 5);
212    assert.equal(trend[0].total_score, 21);   // dropped the oldest
213    assert.equal(trend[4].total_score, 25);
214  });
215
216  it('returns empty when no snapshots', () => {
217    assert.deepEqual(readTrend('nope', { cwd }), []);
218  });
219});