diff --git a/apps/daemon/src/import-export-routes.ts b/apps/daemon/src/import-export-routes.ts index fbd06bbff..fe8203f71 100644 --- a/apps/daemon/src/import-export-routes.ts +++ b/apps/daemon/src/import-export-routes.ts @@ -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 }); } } diff --git a/apps/daemon/tests/export-manifest-route.test.ts b/apps/daemon/tests/export-manifest-route.test.ts index fe79e7d1d..c56bafc2a 100644 --- a/apps/daemon/tests/export-manifest-route.test.ts +++ b/apps/daemon/tests/export-manifest-route.test.ts @@ -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: '
fallback
', + }); + await writeFile(projectId, { + name: 'preview/reviewed.html', + content: '
reviewed
', + }); + await writeFile(projectId, { + name: 'preview/wrapper.html', + content: '', + 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);