158 lines
4.4 KiB
TypeScript
158 lines
4.4 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import { transformDocument } from '@/lib/document';
|
|
import type { DocumentArtifact, DocumentTransform } from '@/lib/document';
|
|
|
|
function artifact(text = 'Original'): DocumentArtifact {
|
|
return {
|
|
metadata: { fileName: 'manual.md', mimeType: 'text/markdown' },
|
|
blocks: [{ id: 'text_1', type: 'markdown', text }],
|
|
assets: [],
|
|
};
|
|
}
|
|
|
|
const context = {
|
|
purpose: 'course-generation' as const,
|
|
budget: { maxTextChars: 50_000, maxVisionImages: 20 },
|
|
};
|
|
|
|
describe('document transform pipeline', () => {
|
|
it('runs transforms in order, records lineage, metrics, and keeps the input immutable', async () => {
|
|
const input = artifact();
|
|
const append = (id: string, suffix: string): DocumentTransform => ({
|
|
id,
|
|
displayName: id,
|
|
version: '1.0.0',
|
|
capabilities: {},
|
|
apply(current) {
|
|
current.blocks[0].text += suffix;
|
|
return { artifact: current };
|
|
},
|
|
});
|
|
|
|
const result = await transformDocument(
|
|
input,
|
|
[append('first', ' A'), append('second', ' B')],
|
|
context,
|
|
);
|
|
|
|
expect(input.blocks[0].text).toBe('Original');
|
|
expect(result.artifact.blocks[0].text).toBe('Original A B');
|
|
expect(result.artifact.transforms?.map((record) => record.transformId)).toEqual([
|
|
'first',
|
|
'second',
|
|
]);
|
|
expect(result.artifact.transforms?.every((record) => record.status === 'applied')).toBe(true);
|
|
expect(result.metrics).toMatchObject({
|
|
inputChars: 8,
|
|
outputChars: 12,
|
|
inputAssets: 0,
|
|
outputAssets: 0,
|
|
});
|
|
});
|
|
|
|
it('continues after a failed transform in best-effort mode and records the failure', async () => {
|
|
const failing: DocumentTransform = {
|
|
id: 'failing',
|
|
displayName: 'Failing',
|
|
version: '1.0.0',
|
|
capabilities: {},
|
|
apply() {
|
|
throw new Error('broken input');
|
|
},
|
|
};
|
|
const following: DocumentTransform = {
|
|
id: 'following',
|
|
displayName: 'Following',
|
|
version: '1.0.0',
|
|
capabilities: {},
|
|
apply(current) {
|
|
current.blocks[0].text = 'Recovered';
|
|
return { artifact: current };
|
|
},
|
|
};
|
|
|
|
const result = await transformDocument(artifact(), [failing, following], context, {
|
|
failurePolicy: 'best-effort',
|
|
});
|
|
|
|
expect(result.artifact.blocks[0].text).toBe('Recovered');
|
|
expect(result.artifact.transforms?.map((record) => record.status)).toEqual([
|
|
'failed',
|
|
'applied',
|
|
]);
|
|
expect(result.diagnostics[0].message).toContain('broken input');
|
|
});
|
|
|
|
it('fails fast by default and does not run later transforms', async () => {
|
|
const laterApply = vi.fn();
|
|
const transforms: DocumentTransform[] = [
|
|
{
|
|
id: 'failing',
|
|
displayName: 'Failing',
|
|
version: '1.0.0',
|
|
capabilities: {},
|
|
apply() {
|
|
throw new Error('stop');
|
|
},
|
|
},
|
|
{
|
|
id: 'later',
|
|
displayName: 'Later',
|
|
version: '1.0.0',
|
|
capabilities: {},
|
|
apply(current) {
|
|
laterApply();
|
|
return { artifact: current };
|
|
},
|
|
},
|
|
];
|
|
|
|
await expect(transformDocument(artifact(), transforms, context)).rejects.toThrow('stop');
|
|
expect(laterApply).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('honors an already-aborted signal', async () => {
|
|
const controller = new AbortController();
|
|
controller.abort();
|
|
|
|
await expect(
|
|
transformDocument(artifact(), [], { ...context, signal: controller.signal }),
|
|
).rejects.toMatchObject({ name: 'AbortError' });
|
|
});
|
|
|
|
it('preserves pipeline-owned history when a transform rebuilds the artifact', async () => {
|
|
const rebuild: DocumentTransform = {
|
|
id: 'rebuild',
|
|
displayName: 'Rebuild',
|
|
version: '1.0.0',
|
|
capabilities: {},
|
|
apply(current) {
|
|
return {
|
|
artifact: {
|
|
metadata: { ...current.metadata },
|
|
blocks: current.blocks.map((block) => ({ ...block, text: `${block.text} rebuilt` })),
|
|
assets: [],
|
|
},
|
|
};
|
|
},
|
|
};
|
|
const first: DocumentTransform = {
|
|
id: 'first',
|
|
displayName: 'First',
|
|
version: '1.0.0',
|
|
capabilities: {},
|
|
apply(current) {
|
|
return { artifact: current };
|
|
},
|
|
};
|
|
|
|
const result = await transformDocument(artifact(), [first, rebuild], context);
|
|
|
|
expect(result.artifact.transforms?.map((record) => record.transformId)).toEqual([
|
|
'first',
|
|
'rebuild',
|
|
]);
|
|
});
|
|
});
|