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',
]);
});
});