From 646fa370d228a96aadce8d87148d33fe6485b6d8 Mon Sep 17 00:00:00 2001 From: Denis Redozubov Date: Fri, 29 May 2026 14:07:33 +0400 Subject: [PATCH] fix(daemon): honor artifact manifest entry refs --- apps/daemon/src/artifact-manifest.ts | 11 ++++-- apps/daemon/src/import-export-routes.ts | 18 ++++++--- .../tests/export-manifest-route.test.ts | 38 +++++++++++++++++++ 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/apps/daemon/src/artifact-manifest.ts b/apps/daemon/src/artifact-manifest.ts index ead9b55e0..49b8c6f1c 100644 --- a/apps/daemon/src/artifact-manifest.ts +++ b/apps/daemon/src/artifact-manifest.ts @@ -201,10 +201,15 @@ export function validateArtifactManifestInput( } } - const safeEntry = typeof entry === 'string' ? entry : ''; - if (!safeEntry || safeEntry.length > MAX_ENTRY_LENGTH) { - return { ok: false, error: `artifact entry exceeds max length (${MAX_ENTRY_LENGTH})` }; + const manifestEntry = + typeof manifest.entry === 'string' && manifest.entry.trim() + ? manifest.entry.trim() + : entry; + const entryErr = validateSupportingPath(manifestEntry); + if (entryErr) { + return { ok: false, error: `artifactManifest.entry ${entryErr}` }; } + const safeEntry = (manifestEntry as string).replace(/\\/g, '/'); return { ok: true, value: sanitizeManifest(manifest, safeEntry, options) }; } diff --git a/apps/daemon/src/import-export-routes.ts b/apps/daemon/src/import-export-routes.ts index a6489e5f8..fbd06bbff 100644 --- a/apps/daemon/src/import-export-routes.ts +++ b/apps/daemon/src/import-export-routes.ts @@ -731,7 +731,7 @@ function buildProjectExportManifestResponse({ artifactSupporting.add(normalized); note(normalized, reason); }; - addManifestRef(manifest.entry, 'artifact-entry'); + addManifestRef(manifest.entry, 'artifact-entry', { preferProjectRoot: true }); if (typeof manifest.primary === 'string') { addManifestRef(manifest.primary, 'artifact-primary', { preferProjectRoot: true }); } @@ -798,12 +798,18 @@ function chooseExportManifestEntryFile( for (const file of files) { const manifest = file.artifactManifest; if (!manifest || typeof manifest !== 'object') continue; + if (isInferredArtifactManifest(manifest)) continue; if (manifest.primary === true) return file.name; - if (typeof manifest.primary !== 'string') continue; - const rootPrimary = normalizeManifestProjectRootRef(manifest.primary); - if (rootPrimary && filesByName.has(rootPrimary)) return rootPrimary; - const ownerRelativePrimary = normalizeManifestProjectRef(manifest.primary, file.name); - if (ownerRelativePrimary && filesByName.has(ownerRelativePrimary)) return ownerRelativePrimary; + if (typeof manifest.primary === 'string') { + const rootPrimary = normalizeManifestProjectRootRef(manifest.primary); + if (rootPrimary && filesByName.has(rootPrimary)) return rootPrimary; + const ownerRelativePrimary = normalizeManifestProjectRef(manifest.primary, file.name); + if (ownerRelativePrimary && filesByName.has(ownerRelativePrimary)) return ownerRelativePrimary; + } + const rootEntry = normalizeManifestProjectRootRef(manifest.entry); + if (rootEntry && filesByName.has(rootEntry)) return rootEntry; + const ownerRelativeEntry = normalizeManifestProjectRef(manifest.entry, file.name); + if (ownerRelativeEntry && filesByName.has(ownerRelativeEntry)) return ownerRelativeEntry; } return files.find((file) => /(^|\/)index\.html?$/i.test(file.name))?.name ?? files.find((file) => file.kind === 'html')?.name diff --git a/apps/daemon/tests/export-manifest-route.test.ts b/apps/daemon/tests/export-manifest-route.test.ts index edea6b60c..2ae5a924c 100644 --- a/apps/daemon/tests/export-manifest-route.test.ts +++ b/apps/daemon/tests/export-manifest-route.test.ts @@ -176,6 +176,44 @@ describe('project export manifest route', () => { }); }); + it('uses artifact entry strings as project-relative entry refs without primary hints', async () => { + const projectId = await createProject({ kind: 'prototype' }); + await writeFile(projectId, { + name: 'index.html', + content: '
fallback
', + }); + await writeFile(projectId, { + name: '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'], + }, + }); + + 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[] }>; + }; + + expect(body.entryFile).toBe('reviewed.html'); + expect(body.files.find((file) => file.name === 'reviewed.html')).toMatchObject({ + role: 'entry', + reasons: expect.arrayContaining(['artifact-entry', 'project-entry-file']), + }); + }); + 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);