Files
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

309 lines
11 KiB
TypeScript

// @vitest-environment jsdom
import { renderHook, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { useDesignMdState, computeStale } from '../../src/hooks/useDesignMdState';
const FRESH_DESIGN_MD = `# DESIGN.md
## Provenance
- Project ID: p1
- Design system: alphatrace
- Current artifact: deck.html
- Transcript message count: 12
- Generated UTC timestamp: 2026-05-08T12:00:00Z
`;
const FRESH_GENERATED_MS = Date.parse('2026-05-08T12:00:00Z');
interface MockEndpoints {
files?: { ok?: boolean; body: unknown };
designMd?: { ok?: boolean; body: string };
conversations?: { ok?: boolean; body: unknown };
}
function installFetchMock(endpoints: MockEndpoints) {
return vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const url = typeof input === 'string' ? input : (input as Request).url;
if (url.endsWith('/files')) {
const m = endpoints.files;
return new Response(JSON.stringify(m?.body ?? { files: [] }), {
status: m?.ok === false ? 500 : 200,
headers: { 'content-type': 'application/json' },
});
}
if (url.endsWith('/conversations')) {
const m = endpoints.conversations;
return new Response(JSON.stringify(m?.body ?? { conversations: [] }), {
status: m?.ok === false ? 500 : 200,
headers: { 'content-type': 'application/json' },
});
}
if (url.includes('/files/DESIGN.md')) {
const m = endpoints.designMd;
return new Response(m?.body ?? FRESH_DESIGN_MD, {
status: m?.ok === false ? 500 : 200,
headers: { 'content-type': 'text/markdown' },
});
}
return new Response('not found', { status: 404 });
});
}
afterEach(() => {
vi.restoreAllMocks();
});
describe('useDesignMdState', () => {
it('returns { exists: false, isStale: false } when DESIGN.md is absent', async () => {
installFetchMock({
files: { body: { files: [{ name: 'index.html', size: 10, mtime: 1, kind: 'html', mime: 'text/html' }] } },
});
const { result } = renderHook(() => useDesignMdState('p1'));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.exists).toBe(false);
expect(result.current.isStale).toBe(false);
expect(result.current.staleReason).toBeNull();
});
it('returns { isStale: false } when DESIGN.md is present and nothing is newer', async () => {
const olderMs = FRESH_GENERATED_MS - 60_000;
installFetchMock({
files: {
body: {
files: [
{ name: 'DESIGN.md', size: 100, mtime: FRESH_GENERATED_MS, kind: 'text', mime: 'text/markdown' },
{ name: 'index.html', size: 10, mtime: olderMs, kind: 'html', mime: 'text/html' },
],
},
},
conversations: {
body: {
conversations: [
{ id: 'c1', projectId: 'p1', title: null, createdAt: olderMs, updatedAt: olderMs },
],
},
},
});
const { result } = renderHook(() => useDesignMdState('p1'));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.exists).toBe(true);
expect(result.current.isStale).toBe(false);
expect(result.current.transcriptMessageCount).toBe(12);
expect(result.current.designSystemId).toBe('alphatrace');
});
// Issue #1580: the synthesis prompt does not pin field-label syntax,
// so Claude emits Provenance with Markdown-bold labels in practice.
// This end-to-end test through useDesignMdState pins the parser fix
// at the hook layer — a regression that re-introduces the `** ` leak
// would surface `transcriptMessageCount === null` and trip the
// `unknown-provenance` fail-closed path instead of the fresh path.
it('reads bold-labelled Provenance correctly (issue #1580 end-to-end)', async () => {
const olderMs = FRESH_GENERATED_MS - 60_000;
const boldDesignMd = `# DESIGN.md
## Provenance
- **Project ID:** \`p1\`
- **Design system:** \`alphatrace\`
- **Current artifact:** \`deck.html\`
- **Transcript message count:** 12
- **Generated UTC timestamp:** 2026-05-08T12:00:00Z
`;
installFetchMock({
files: {
body: {
files: [
{ name: 'DESIGN.md', size: 100, mtime: FRESH_GENERATED_MS, kind: 'text', mime: 'text/markdown' },
{ name: 'index.html', size: 10, mtime: olderMs, kind: 'html', mime: 'text/html' },
],
},
},
designMd: { body: boldDesignMd },
conversations: { body: { conversations: [] } },
});
const { result } = renderHook(() => useDesignMdState('p1'));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.exists).toBe(true);
// Round 7 (mrcfps @ useDesignMdState.ts:160): a regression that
// leaks `** ` would null transcriptMessageCount / generatedAt and
// flip this to 'unknown-provenance'.
expect(result.current.staleReason).toBeNull();
expect(result.current.isStale).toBe(false);
expect(result.current.transcriptMessageCount).toBe(12);
// Backticks intentionally kept on the value (out of scope per
// #1580 spec); the `** ` bold prefix must be stripped.
expect(result.current.designSystemId).toBe('`alphatrace`');
});
it('marks stale with files-newer when a project file mtime exceeds generatedAt', async () => {
const newerMs = FRESH_GENERATED_MS + 60_000;
installFetchMock({
files: {
body: {
files: [
{ name: 'DESIGN.md', size: 100, mtime: FRESH_GENERATED_MS, kind: 'text', mime: 'text/markdown' },
{ name: 'index.html', size: 10, mtime: newerMs, kind: 'html', mime: 'text/html' },
],
},
},
});
const { result } = renderHook(() => useDesignMdState('p1'));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.isStale).toBe(true);
expect(result.current.staleReason).toBe('files-newer');
});
it('marks stale with conversations-newer when a conversation updatedAt exceeds generatedAt', async () => {
const olderMs = FRESH_GENERATED_MS - 60_000;
const newerMs = FRESH_GENERATED_MS + 60_000;
installFetchMock({
files: {
body: {
files: [
{ name: 'DESIGN.md', size: 100, mtime: FRESH_GENERATED_MS, kind: 'text', mime: 'text/markdown' },
{ name: 'index.html', size: 10, mtime: olderMs, kind: 'html', mime: 'text/html' },
],
},
},
conversations: {
body: {
conversations: [
{ id: 'c1', projectId: 'p1', title: null, createdAt: olderMs, updatedAt: newerMs },
],
},
},
});
const { result } = renderHook(() => useDesignMdState('p1'));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.isStale).toBe(true);
expect(result.current.staleReason).toBe('conversations-newer');
});
// Round 7 (mrcfps @ useDesignMdState.ts:131): the hook used to only
// recompute on mount or explicit refresh(). ProjectView now bumps a
// counter on file-changed / live_artifact / streaming-completion
// events; the hook accepts that counter as a `refreshKey` arg and
// recomputes when it changes, no remount required.
it('flips stale state after a refreshKey bump without remounting', async () => {
const olderMs = FRESH_GENERATED_MS - 60_000;
const newerMs = FRESH_GENERATED_MS + 60_000;
let fileMtime = olderMs;
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const url = typeof input === 'string' ? input : (input as Request).url;
if (url.endsWith('/files')) {
return new Response(
JSON.stringify({
files: [
{ name: 'DESIGN.md', size: 100, mtime: FRESH_GENERATED_MS, kind: 'text', mime: 'text/markdown' },
{ name: 'index.html', size: 10, mtime: fileMtime, kind: 'html', mime: 'text/html' },
],
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url.endsWith('/conversations')) {
return new Response(JSON.stringify({ conversations: [] }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
if (url.includes('/files/DESIGN.md')) {
return new Response(FRESH_DESIGN_MD, {
status: 200,
headers: { 'content-type': 'text/markdown' },
});
}
return new Response('not found', { status: 404 });
});
const { result, rerender } = renderHook(
({ refreshKey }) => useDesignMdState('p1', refreshKey),
{ initialProps: { refreshKey: 0 } },
);
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.isStale).toBe(false);
// Simulate a post-finalize file mutation: index.html mtime moves
// past generatedAt, then ProjectView bumps the refresh key.
fileMtime = newerMs;
rerender({ refreshKey: 1 });
await waitFor(() => expect(result.current.isStale).toBe(true));
expect(result.current.staleReason).toBe('files-newer');
});
});
describe('computeStale', () => {
// Round 7 (mrcfps @ useDesignMdState.ts:160): inverted from the
// pre-round-7 "fresh on null timestamp" behavior. A missing /
// malformed provenance timestamp now surfaces as the distinct
// 'unknown-provenance' degraded state instead of misleading fresh.
it('returns degraded unknown-provenance state when generatedMs is null (no usable timestamp parsed)', () => {
expect(
computeStale({ generatedMs: null, files: [], conversations: [] }),
).toEqual({ isStale: true, staleReason: 'unknown-provenance' });
});
it('ignores DESIGN.md mtime when comparing file ages', () => {
expect(
computeStale({
generatedMs: 1000,
files: [
{ name: 'DESIGN.md', size: 0, mtime: 5000, kind: 'text', mime: 'text/markdown' },
],
conversations: [],
}),
).toEqual({ isStale: false, staleReason: null });
});
});
describe('useDesignMdState — malformed provenance', () => {
// Round 7 (mrcfps @ useDesignMdState.ts:160): end-to-end through
// compute() so a regression that re-pins fresh-on-null at the hook
// level (not just computeStale) fails fast.
it('reports unknown-provenance after a malformed ## Provenance section in DESIGN.md', async () => {
const malformedDesignMd = `# DESIGN.md
## Provenance
- Project ID: p1
- Generated UTC timestamp: not-a-real-date
`;
installFetchMock({
files: {
body: {
files: [
{ name: 'DESIGN.md', size: 100, mtime: 1, kind: 'text', mime: 'text/markdown' },
{ name: 'index.html', size: 10, mtime: 1, kind: 'html', mime: 'text/html' },
],
},
},
designMd: { body: malformedDesignMd },
conversations: { body: { conversations: [] } },
});
const { result } = renderHook(() => useDesignMdState('p1'));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.exists).toBe(true);
expect(result.current.isStale).toBe(true);
expect(result.current.staleReason).toBe('unknown-provenance');
});
});