diff --git a/apps/web/lib/product-overview-persistence.test.ts b/apps/web/lib/product-overview-persistence.test.ts index 7673df6..811f6bd 100644 --- a/apps/web/lib/product-overview-persistence.test.ts +++ b/apps/web/lib/product-overview-persistence.test.ts @@ -7,6 +7,35 @@ test('does not persist an empty remote overview', () => { assert.equal(shouldPersistRemoteOverview([]), false); }); -test('persists a non-empty remote overview', () => { - assert.equal(shouldPersistRemoteOverview([{ id: 'product-1' }]), true); +test('does not persist product-only remote overview', () => { + assert.equal(shouldPersistRemoteOverview([{ id: 'product-1' }]), false); +}); + +test('does not persist relational products without frontend project or version data', () => { + assert.equal( + shouldPersistRemoteOverview([ + { + id: 'cmqs07s3900001450gwu9lesi', + name: '测试AI任务拆解', + projects: [], + versions: [], + _count: { requirements: 0, projects: 0, versions: 0 }, + }, + ]), + false, + ); +}); + +test('persists overview with frontend project and version ids', () => { + assert.equal( + shouldPersistRemoteOverview([ + { + id: 'local-1782301366735', + name: '测试AI任务拆解', + projects: [{ id: 'proj-1782301375710', name: '任务拆解' }], + versions: [{ id: 'ver-1782301431670', name: '任务拆解V1.0', status: 'developing' }], + }, + ]), + true, + ); }); diff --git a/apps/web/lib/product-overview-persistence.ts b/apps/web/lib/product-overview-persistence.ts index ad7e95c..cfad48d 100644 --- a/apps/web/lib/product-overview-persistence.ts +++ b/apps/web/lib/product-overview-persistence.ts @@ -1,3 +1,35 @@ export function shouldPersistRemoteOverview(overview: unknown): boolean { - return Array.isArray(overview) && overview.length > 0; + if (!Array.isArray(overview) || overview.length === 0) return false; + return overview.some(hasFrontendTreeSignal); +} + +function hasFrontendTreeSignal(product: unknown): boolean { + if (!isRecord(product)) return false; + const projects = Array.isArray(product.projects) ? product.projects : []; + const versions = Array.isArray(product.versions) ? product.versions : []; + return projects.some(isFrontendProject) || versions.some(isFrontendVersion); +} + +function isFrontendProject(project: unknown): boolean { + return ( + isRecord(project) && + typeof project.id === 'string' && + project.id.startsWith('proj-') && + typeof project.name === 'string' + ); +} + +function isFrontendVersion(version: unknown): boolean { + if (!isRecord(version) || typeof version.id !== 'string' || typeof version.name !== 'string') return false; + return ( + version.id.startsWith('ver-') || + typeof version.status === 'string' || + typeof version.expectedReleaseDate === 'string' || + Array.isArray(version.members) || + typeof version.priority === 'string' + ); +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; }