fix(daemon): preserve export refs across directory moves

This commit is contained in:
Denis Redozubov 2026-05-30 22:29:01 +04:00
parent f2e04df500
commit 0bffe6ba40
2 changed files with 61 additions and 7 deletions

View file

@ -716,14 +716,16 @@ function buildProjectExportManifestResponse({
const addManifestRef = (
ref: unknown,
reason: string,
options: { preferProjectRoot?: boolean } = {},
options: { allowProjectRootFallback?: boolean; preferProjectRoot?: boolean } = {},
) => {
const ownerRelative = normalizeManifestProjectRef(ref, file.name);
const projectRoot = normalizeManifestProjectRootRef(ref);
const candidates = options.preferProjectRoot
? [
normalizeManifestProjectRootRef(ref),
normalizeManifestProjectRef(ref, file.name),
]
: [normalizeManifestProjectRef(ref, file.name)];
? [projectRoot, ownerRelative]
: [
ownerRelative,
...(options.allowProjectRootFallback ? [projectRoot] : []),
];
const normalized = candidates.find((candidate) => candidate && filesByName.has(candidate));
if (!normalized) return;
if (normalized === file.name) return;
@ -737,7 +739,7 @@ function buildProjectExportManifestResponse({
}
if (Array.isArray(manifest.supportingFiles)) {
for (const ref of manifest.supportingFiles) {
addManifestRef(ref, 'artifact-supporting-file');
addManifestRef(ref, 'artifact-supporting-file', { allowProjectRootFallback: true });
}
}

View file

@ -284,6 +284,58 @@ describe('project export manifest route', () => {
});
});
it('keeps artifact entry refs current when a referenced file moves out of the wrapper directory', async () => {
const projectId = await createProject({ kind: 'prototype' });
await writeFile(projectId, {
name: 'index.html',
content: '<!doctype html><main>fallback</main>',
});
await writeFile(projectId, {
name: 'preview/reviewed.html',
content: '<!doctype html><main>reviewed</main>',
});
await writeFile(projectId, {
name: 'preview/wrapper.html',
content: '<!doctype html><iframe src="reviewed.html"></iframe>',
artifactManifest: {
version: 1,
kind: 'html',
title: 'Review wrapper',
entry: 'reviewed.html',
renderer: 'html',
status: 'complete',
exports: ['html'],
primary: 'reviewed.html',
supportingFiles: ['reviewed.html'],
},
});
await renameFile(projectId, 'preview/reviewed.html', 'reviewed.html');
const response = await fetch(`${baseUrl}/api/projects/${projectId}/export/manifest`);
expect(response.ok).toBe(true);
const body = await response.json() as {
entryFile: string;
files: Array<{ name: string; role: string; reasons: string[] }>;
artifacts: Array<{ file: string; supportingFiles: string[] }>;
};
expect(body.entryFile).toBe('reviewed.html');
expect(body.files.find((file) => file.name === 'reviewed.html')).toMatchObject({
role: 'entry',
reasons: expect.arrayContaining([
'artifact-entry',
'artifact-primary',
'artifact-supporting-file',
'project-entry-file',
]),
});
expect(body.artifacts.find((artifact) => artifact.file === 'preview/wrapper.html'))
.toMatchObject({
supportingFiles: ['reviewed.html'],
});
});
it('rejects invalid project ids before listing files', async () => {
const response = await fetch(`${baseUrl}/api/projects/bad:id/export/manifest`);
expect(response.status).toBe(400);