maestro/scripts/lib/design-index.test.mjs
oss-sync b1292e34b2
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (edc775f2)
2026-07-06 01:04:12 +00:00

168 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it, expect } from 'vitest';
import { docParts, deriveTopicGuess, extractTitle, hasSupersededBy, STATUSES, scaffoldEntryObject, findUnregistered } from './design-index.mjs';
describe('docParts', () => {
it('spec のパスを分解', () => {
expect(docParts('specs/2026-07-02-foo-design.md')).toEqual({ type: 'spec', date: '2026-07-02', base: '2026-07-02-foo-design', archived: false });
});
it('archived plan', () => {
expect(docParts('archived/plans/2026-03-14-bar.md')).toEqual({ type: 'plan', date: '2026-03-14', base: '2026-03-14-bar', archived: true });
});
});
describe('deriveTopicGuess', () => {
it('spec の -design を除く', () => {
expect(deriveTopicGuess('specs/2026-07-01-workspace-task-search-design.md')).toBe('workspace-task-search');
});
it('plan は接尾なし', () => {
expect(deriveTopicGuess('plans/2026-07-02-workspace-task-search.md')).toBe('workspace-task-search');
});
it('redesign を誤って削らない', () => {
expect(deriveTopicGuess('specs/2026-03-25-memory-redesign.md')).toBe('memory-redesign');
});
});
describe('extractTitle', () => {
it('見出しから接尾を除く', () => {
expect(extractTitle('# ワークスペース内タスク横断検索 実装計画\n\n本文')).toBe('ワークスペース内タスク横断検索');
expect(extractTitle('# A2A プロトコル対応 — 設計書\n')).toBe('A2A プロトコル対応');
});
it('見出し無しは空', () => { expect(extractTitle('本文だけ')).toBe(''); });
});
describe('hasSupersededBy', () => {
it('null/空はなし', () => { expect(hasSupersededBy(null)).toBe(false); expect(hasSupersededBy('')).toBe(false); });
it('文字列はあり', () => { expect(hasSupersededBy('specs/x.md')).toBe(true); });
});
it('STATUSES は6値', () => { expect(STATUSES).toHaveLength(6); });
describe('scaffoldEntryObject', () => {
it('active は unknown, archived は shipped', () => {
const a = scaffoldEntryObject('plans/2026-07-02-foo.md', '# Foo 実装計画\n');
expect(a).toMatchObject({ path: 'plans/2026-07-02-foo.md', type: 'plan', topic: 'foo', status: 'unknown', pr: [], superseded_by: null });
expect(a.title).toBe('Foo');
const b = scaffoldEntryObject('archived/specs/2026-03-14-bar-design.md', '# Bar — 設計書\n');
expect(b.status).toBe('shipped');
expect(b.type).toBe('spec');
});
});
describe('findUnregistered', () => {
it('manifest に無いものだけ返す', () => {
expect(findUnregistered(['a', 'b', 'c'], new Set(['b']))).toEqual(['a', 'c']);
});
});
import { renderIndex, escapeCell, prUrl, BADGE } from './design-index.mjs';
describe('escapeCell', () => {
it('パイプと改行を無害化', () => {
expect(escapeCell('a|b\nc')).toBe('a\\|b c');
});
});
describe('renderIndex', () => {
const entries = [
{ path: 'specs/2026-07-01-x-design.md', title: 'X', type: 'spec', topic: 'x', status: 'design', pr: [], superseded_by: null },
{ path: 'plans/2026-07-02-x.md', title: 'X 計画', type: 'plan', topic: 'x', status: 'design', pr: [712], superseded_by: null },
{ path: 'archived/plans/2026-03-14-y.md', title: 'Y', type: 'plan', topic: 'y', status: 'shipped', pr: [], superseded_by: null },
];
const md = renderIndex(entries);
it('サマリに件数', () => { expect(md).toMatch(/🔵\s*2/); expect(md).toMatch(/🟢\s*1/); });
it('topic 見出しで spec/plan が同居', () => {
const xSection = md.slice(md.indexOf('x'));
expect(xSection).toContain('X');
expect(xSection).toContain('X 計画');
});
it('PR リンクを描く', () => { expect(md).toContain(prUrl(712)); });
it('archived は別セクション', () => { expect(md).toMatch(/archived|アーカイブ/i); });
it('決定的2回同じ', () => { expect(renderIndex(entries)).toBe(md); });
it('superseded_by があると後継リンクを描く', () => {
const supersededEntry = {
path: 'specs/2026-01-01-old-design.md',
title: 'Old',
type: 'spec',
topic: 'old',
status: 'superseded',
pr: [],
superseded_by: 'specs/2026-01-01-z-design.md',
};
const successor = {
path: 'specs/2026-01-01-z-design.md',
title: 'Z',
type: 'spec',
topic: 'old',
status: 'shipped',
pr: [],
superseded_by: null,
};
const out = renderIndex([supersededEntry, successor]);
expect(out).toContain('[→](./specs/2026-01-01-z-design.md)');
});
it('同日・同トピック 2エントリを逆順に渡しても同じ出力C1 order-independence', () => {
const e1 = { path: 'specs/2026-05-01-alpha-design.md', title: 'Alpha', type: 'spec', topic: 'tie', status: 'design', pr: [], superseded_by: null };
const e2 = { path: 'specs/2026-05-01-beta-design.md', title: 'Beta', type: 'spec', topic: 'tie', status: 'design', pr: [], superseded_by: null };
expect(renderIndex([e1, e2])).toBe(renderIndex([e2, e1]));
});
it('未知の status は ❔ バッジにフォールバック', () => {
const weirdEntry = { path: 'specs/2026-06-01-foo-design.md', title: 'Foo', type: 'spec', topic: 'foo', status: 'weird', pr: [], superseded_by: null };
const out = renderIndex([weirdEntry]);
expect(out).toContain('❔');
});
});
import { validateManifest } from './design-index.mjs';
describe('validateManifest', () => {
const ok = { path: 'specs/2026-01-01-a-design.md', title: 'A', type: 'spec', topic: 'a', status: 'shipped', pr: [], superseded_by: null };
it('整合していれば error 無し', () => {
const r = validateManifest([ok], ['specs/2026-01-01-a-design.md']);
expect(r.errors).toEqual([]);
});
it('孤児(ファイルはあるが manifest に無い)を error に', () => {
const r = validateManifest([ok], ['specs/2026-01-01-a-design.md', 'plans/2026-01-02-b.md']);
expect(r.errors.some((e) => e.includes('2026-01-02-b.md'))).toBe(true);
});
it('欠落manifest にあるがファイルが無い)を error に', () => {
const r = validateManifest([ok], []);
expect(r.errors.some((e) => e.includes('does not exist') || e.includes('欠落'))).toBe(true);
});
it('不正 status を error に', () => {
const bad = { ...ok, status: 'done' };
const r = validateManifest([bad], ['specs/2026-01-01-a-design.md']);
expect(r.errors.some((e) => e.includes('status'))).toBe(true);
});
it('superseded_by あり + status!=superseded は warning', () => {
const s = { ...ok, superseded_by: 'specs/2026-01-01-a-design.md' };
const r = validateManifest([s], ['specs/2026-01-01-a-design.md']);
expect(r.warnings.some((w) => w.includes('superseded'))).toBe(true);
});
it('必須フィールド欠如title が空)を error に', () => {
const bad = { ...ok, title: '' };
const r = validateManifest([bad], ['specs/2026-01-01-a-design.md']);
expect(r.errors.some((e) => e.includes("'title'"))).toBe(true);
});
it('不正 type を error に', () => {
const bad = { ...ok, type: 'note' };
const r = validateManifest([bad], ['specs/2026-01-01-a-design.md']);
expect(r.errors.some((e) => e.includes('type'))).toBe(true);
});
it('superseded_by が manifest 内に無い path を error に', () => {
const bad = { ...ok, status: 'superseded', superseded_by: 'specs/9999-nonexistent.md' };
const r = validateManifest([bad], ['specs/2026-01-01-a-design.md']);
expect(r.errors.some((e) => e.includes('superseded_by') && e.includes('9999-nonexistent'))).toBe(true);
});
it('status=superseded かつ superseded_by 空は warning', () => {
const bad = { ...ok, status: 'superseded', superseded_by: null };
const r = validateManifest([bad], ['specs/2026-01-01-a-design.md']);
expect(r.warnings.some((w) => w.includes('superseded_by'))).toBe(true);
});
it('status=unknown のエントリは warning に件数を含む', () => {
const u1 = { ...ok, path: 'specs/2026-01-01-a-design.md', status: 'unknown' };
const u2 = { ...ok, path: 'plans/2026-01-03-c.md', status: 'unknown' };
const r = validateManifest([u1, u2], ['specs/2026-01-01-a-design.md', 'plans/2026-01-03-c.md']);
expect(r.warnings.some((w) => w.includes('2') && w.includes('unknown'))).toBe(true);
});
});