Add vehicle-h2-fee-ledger, customer-management, lease and self-operated ledgers, annotation sources, agent skills, and vite annotation runtime support. Update vehicle management, contract templates, and lease contract flows. Co-authored-by: Cursor <cursoragent@cursor.com>
236 lines
8.5 KiB
TypeScript
236 lines
8.5 KiB
TypeScript
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
createAnnotationSourceMarkdownPlugin,
|
|
preprocessAnnotationSourceMarkdown,
|
|
resolveAnnotationMarkdownPath,
|
|
} from './annotationSourceMarkdown';
|
|
|
|
const tempRoots: string[] = [];
|
|
|
|
function createProjectRoot(): string {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'annotation-source-markdown-'));
|
|
tempRoots.push(root);
|
|
fs.mkdirSync(path.join(root, 'src/prototypes/order-review/docs/nested'), { recursive: true });
|
|
fs.writeFileSync(path.join(root, 'src/prototypes/order-review/index.tsx'), 'export default function Demo() { return null; }\n', 'utf8');
|
|
return root;
|
|
}
|
|
|
|
function writeSource(projectRoot: string, source: unknown): string {
|
|
const sourcePath = path.join(projectRoot, 'src/prototypes/order-review/annotation-source.json');
|
|
fs.mkdirSync(path.dirname(sourcePath), { recursive: true });
|
|
fs.writeFileSync(sourcePath, `${JSON.stringify(source, null, 2)}\n`, 'utf8');
|
|
return sourcePath;
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
for (const root of tempRoots.splice(0)) {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('annotation source markdown preprocessing', () => {
|
|
it('inlines directory markdownPath content without creating Make-specific edit URLs', () => {
|
|
const projectRoot = createProjectRoot();
|
|
const mdPath = path.join(projectRoot, 'src/prototypes/order-review/docs/prd-03-status.md');
|
|
fs.writeFileSync(mdPath, '# PRD 03\n\n状态说明', 'utf8');
|
|
const sourcePath = writeSource(projectRoot, {
|
|
documentVersion: 1,
|
|
format: 'axhub-annotation-source',
|
|
data: { version: 2, prototypeName: 'order-review', pageId: 'order-review', nodes: [], updatedAt: 1 },
|
|
markdownMap: {},
|
|
assetMap: {},
|
|
directory: {
|
|
nodes: [
|
|
{
|
|
type: 'markdown',
|
|
id: 'prd-03-status',
|
|
title: 'PRD 03 | 状态',
|
|
markdownPath: 'docs/prd-03-status.md',
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const result = preprocessAnnotationSourceMarkdown({
|
|
projectRoot,
|
|
sourceFilePath: sourcePath,
|
|
source: JSON.parse(fs.readFileSync(sourcePath, 'utf8')),
|
|
mode: 'serve',
|
|
});
|
|
|
|
const node = result.source.directory.nodes[0];
|
|
expect(node).toMatchObject({
|
|
type: 'markdown',
|
|
markdown: '# PRD 03\n\n状态说明',
|
|
markdownPath: 'docs/prd-03-status.md',
|
|
});
|
|
expect(node).not.toHaveProperty('markdownEditUrl');
|
|
expect(result.watchFiles).toEqual([mdPath]);
|
|
});
|
|
|
|
it('does not expose edit URLs when preprocessing for export builds', () => {
|
|
const projectRoot = createProjectRoot();
|
|
fs.writeFileSync(path.join(projectRoot, 'src/prototypes/order-review/docs/prd.md'), '# Exported PRD', 'utf8');
|
|
const sourcePath = writeSource(projectRoot, {
|
|
documentVersion: 1,
|
|
format: 'axhub-annotation-source',
|
|
data: { version: 2, prototypeName: 'order-review', pageId: 'order-review', nodes: [], updatedAt: 1 },
|
|
markdownMap: {},
|
|
assetMap: {},
|
|
directory: {
|
|
nodes: [{ type: 'markdown', id: 'prd', title: 'PRD', markdownPath: 'docs/prd.md' }],
|
|
},
|
|
});
|
|
|
|
const result = preprocessAnnotationSourceMarkdown({
|
|
projectRoot,
|
|
sourceFilePath: sourcePath,
|
|
source: JSON.parse(fs.readFileSync(sourcePath, 'utf8')),
|
|
mode: 'build',
|
|
});
|
|
|
|
expect(result.source.directory.nodes[0]).toMatchObject({
|
|
markdown: '# Exported PRD',
|
|
markdownPath: 'docs/prd.md',
|
|
});
|
|
expect(result.source.directory.nodes[0]).not.toHaveProperty('markdownEditUrl');
|
|
});
|
|
|
|
it('recursively inlines markdownPath documents nested in directory folders', () => {
|
|
const projectRoot = createProjectRoot();
|
|
fs.writeFileSync(path.join(projectRoot, 'src/prototypes/order-review/docs/nested/prd.md'), '# Nested PRD', 'utf8');
|
|
const sourcePath = writeSource(projectRoot, {
|
|
documentVersion: 1,
|
|
format: 'axhub-annotation-source',
|
|
data: { version: 2, prototypeName: 'order-review', pageId: 'order-review', nodes: [], updatedAt: 1 },
|
|
markdownMap: {},
|
|
assetMap: {},
|
|
directory: {
|
|
nodes: [
|
|
{
|
|
type: 'folder',
|
|
id: 'docs',
|
|
title: '文档',
|
|
children: [
|
|
{ type: 'markdown', id: 'nested-prd', title: 'Nested PRD', markdownPath: 'docs/nested/prd.md' },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const result = preprocessAnnotationSourceMarkdown({
|
|
projectRoot,
|
|
sourceFilePath: sourcePath,
|
|
source: JSON.parse(fs.readFileSync(sourcePath, 'utf8')),
|
|
mode: 'serve',
|
|
});
|
|
|
|
expect(result.source.directory.nodes[0].children[0]).toMatchObject({
|
|
type: 'markdown',
|
|
markdownPath: 'docs/nested/prd.md',
|
|
markdown: '# Nested PRD',
|
|
});
|
|
expect(result.source.directory.nodes[0].children[0]).not.toHaveProperty('markdownEditUrl');
|
|
});
|
|
|
|
it.each([
|
|
['docs/prd.md', 'src/prototypes/order-review/docs/prd.md'],
|
|
['docs/nested/prd.md', 'src/prototypes/order-review/docs/nested/prd.md'],
|
|
])('allows safe prototype-relative markdownPath %s', (markdownPath, expectedProjectRelativePath) => {
|
|
const projectRoot = createProjectRoot();
|
|
const sourcePath = path.join(projectRoot, 'src/prototypes/order-review/annotation-source.json');
|
|
|
|
const resolved = resolveAnnotationMarkdownPath(projectRoot, sourcePath, markdownPath);
|
|
|
|
expect(resolved).toMatchObject({
|
|
ok: true,
|
|
projectRelativePath: expectedProjectRelativePath,
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
'/abs.md',
|
|
'../secret.md',
|
|
'docs/../../secret.md',
|
|
'docs//prd.md',
|
|
'docs/%2e%2e/secret.md',
|
|
])('rejects unsafe markdownPath %s', (markdownPath) => {
|
|
const projectRoot = createProjectRoot();
|
|
const sourcePath = path.join(projectRoot, 'src/prototypes/order-review/annotation-source.json');
|
|
|
|
const resolved = resolveAnnotationMarkdownPath(projectRoot, sourcePath, markdownPath);
|
|
|
|
expect(resolved.ok).toBe(false);
|
|
});
|
|
|
|
it('falls back to existing inline markdown when the referenced file is missing and warns in dev', () => {
|
|
const projectRoot = createProjectRoot();
|
|
const sourcePath = writeSource(projectRoot, {
|
|
documentVersion: 1,
|
|
format: 'axhub-annotation-source',
|
|
data: { version: 2, prototypeName: 'order-review', pageId: 'order-review', nodes: [], updatedAt: 1 },
|
|
markdownMap: {},
|
|
assetMap: {},
|
|
directory: {
|
|
nodes: [{ type: 'markdown', id: 'prd', title: 'PRD', markdownPath: 'docs/missing.md', markdown: '旧内容' }],
|
|
},
|
|
});
|
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
|
|
const result = preprocessAnnotationSourceMarkdown({
|
|
projectRoot,
|
|
sourceFilePath: sourcePath,
|
|
source: JSON.parse(fs.readFileSync(sourcePath, 'utf8')),
|
|
mode: 'serve',
|
|
});
|
|
|
|
expect(result.source.directory.nodes[0]).toMatchObject({
|
|
markdown: '旧内容',
|
|
markdownPath: 'docs/missing.md',
|
|
});
|
|
expect(result.source.directory.nodes[0]).not.toHaveProperty('markdownEditUrl');
|
|
expect(warn).toHaveBeenCalledWith(expect.stringContaining('docs/missing.md'));
|
|
});
|
|
|
|
it('loads annotation-source.json as an inlined JavaScript module in Vite', async () => {
|
|
const projectRoot = createProjectRoot();
|
|
fs.writeFileSync(path.join(projectRoot, 'src/prototypes/order-review/docs/prd.md'), '# Vite PRD', 'utf8');
|
|
const sourcePath = writeSource(projectRoot, {
|
|
documentVersion: 1,
|
|
format: 'axhub-annotation-source',
|
|
data: { version: 2, prototypeName: 'order-review', pageId: 'order-review', nodes: [], updatedAt: 1 },
|
|
markdownMap: {},
|
|
assetMap: {},
|
|
directory: {
|
|
nodes: [{ type: 'markdown', id: 'prd', title: 'PRD', markdownPath: 'docs/prd.md' }],
|
|
},
|
|
});
|
|
const watched: string[] = [];
|
|
const plugin = createAnnotationSourceMarkdownPlugin(projectRoot, { mode: 'serve' });
|
|
|
|
const transform = plugin.transform as (
|
|
this: { addWatchFile: (file: string) => void },
|
|
code: string,
|
|
id: string,
|
|
) => string | Promise<string> | null;
|
|
const code = await transform.call(
|
|
{ addWatchFile: (file: string) => watched.push(file) },
|
|
fs.readFileSync(sourcePath, 'utf8'),
|
|
sourcePath,
|
|
);
|
|
|
|
expect(() => JSON.parse(String(code))).not.toThrow();
|
|
expect(code).not.toContain('export default');
|
|
expect(code).toContain('# Vite PRD');
|
|
expect(code).not.toContain('markdownEditUrl');
|
|
expect(watched).toEqual([path.join(projectRoot, 'src/prototypes/order-review/docs/prd.md')]);
|
|
});
|
|
});
|