import assert from 'node:assert/strict'; import test from 'node:test'; import { classifyReferrerHost } from './mindspace-analytics-discovery.mjs'; import { listAdminSeoGeoPublicationCatalog, resolveAdminCatalogPublicUrl, } from './mindspace-seo-geo-admin-catalog.mjs'; test('classifyReferrerHost maps search and ai referrers', () => { assert.deepEqual(classifyReferrerHost('www.google.com'), { discovery_channel: 'seo', discovery_source: 'google', }); assert.deepEqual(classifyReferrerHost('chatgpt.com'), { discovery_channel: 'geo', discovery_source: 'chatgpt', }); }); test('resolveAdminCatalogPublicUrl normalizes relative public urls', () => { assert.equal( resolveAdminCatalogPublicUrl('/u/john/pages/demo'), 'https://m.tkmind.cn/u/john/pages/demo', ); }); test('listAdminSeoGeoPublicationCatalog merges zero-traffic pages with view stats', async () => { const calls = []; const pool = { async query(sql, params = []) { calls.push({ sql, params }); const normalized = String(sql).replace(/\s+/g, ' ').trim(); if (normalized.startsWith('SELECT COUNT(*) AS total')) { return [[{ total: 2 }]]; } if (normalized.includes('FROM h5_publish_records pr') && normalized.includes('JOIN h5_users')) { return [[ { publicationId: 'pub-1', pageId: 'page-1', userId: 'user-1', publicUrl: '/u/john/pages/demo', accessMode: 'public', status: 'online', userConfirmedAt: 1000, expiresAt: null, publishedAt: 2000, lifetimeViewCount: 12, pageTitle: 'Demo', pageSummary: 'Summary', username: 'john', displayName: 'John', }, { publicationId: 'pub-2', pageId: 'page-2', userId: 'user-2', publicUrl: '/u/jane/pages/empty', accessMode: 'public', status: 'online', userConfirmedAt: null, expiresAt: null, publishedAt: 1500, lifetimeViewCount: 0, pageTitle: 'Empty', pageSummary: '', username: 'jane', displayName: 'Jane', }, ]]; } if (normalized.includes('FROM h5_publication_views')) { return [[ { publicationId: 'pub-1', device_type: 'desktop', referrer_host: 'www.google.com', views: 3, }, { publicationId: 'pub-1', device_type: 'bot', referrer_host: null, views: 2, }, ]]; } return [[]]; }, }; const result = await listAdminSeoGeoPublicationCatalog(pool, { page: 1, pageSize: 10, startAt: 1, endAt: 9_999_999_999_999, sortBy: 'totalViews', sortOrder: 'desc', }); assert.equal(result.total, 2); assert.equal(result.data.length, 2); assert.equal(result.data[0].publicationId, 'pub-1'); assert.equal(result.data[0].seoViews, 3); assert.equal(result.data[0].crawlerViews, 2); assert.equal(result.data[0].totalViews, 5); assert.equal(result.data[0].indexable, true); assert.equal(result.data[1].publicationId, 'pub-2'); assert.equal(result.data[1].totalViews, 0); assert.equal(result.data[1].indexable, true); assert.match(result.data[1].publicUrl, /^https:\/\/m\.tkmind\.cn\//); }); test('listAdminSeoGeoPublicationCatalog search matches workspace path and defaults to publishedAt desc', async () => { const pool = { async query(sql, params = []) { const normalized = String(sql).replace(/\s+/g, ' ').trim(); if (normalized.startsWith('SELECT COUNT(*) AS total')) { assert.match(normalized, /workspace_relative_path LIKE \?/); assert.equal(params[0], '%daily-news-0917%'); return [[{ total: 1 }]]; } if (normalized.includes('FROM h5_publish_records pr') && normalized.includes('JOIN h5_users')) { return [[ { publicationId: 'pub-news', pageId: 'page-news', userId: 'user-1', publicUrl: '/u/john/pages/news', accessMode: 'public', status: 'online', userConfirmedAt: 1000, expiresAt: null, publishedAt: 5000, lifetimeViewCount: 99, pageTitle: '每日新闻早报', pageSummary: 'Summary', username: 'john', displayName: 'John', }, { publicationId: 'pub-old', pageId: 'page-old', userId: 'user-1', publicUrl: '/u/john/pages/old', accessMode: 'public', status: 'online', userConfirmedAt: 1000, expiresAt: null, publishedAt: 1000, lifetimeViewCount: 999, pageTitle: 'Old', pageSummary: '', username: 'john', displayName: 'John', }, ]]; } return [[]]; }, }; const result = await listAdminSeoGeoPublicationCatalog(pool, { page: 1, pageSize: 10, startAt: 1, endAt: 9_999_999_999_999, search: 'daily-news-0917', sortBy: 'publishedAt', sortOrder: 'desc', }); assert.equal(result.total, 1); assert.equal(result.data[0].publicationId, 'pub-news'); }); test('listAdminSeoGeoPublicationCatalog merges Umami stats and appends workspace-only pages', async () => { const pool = { async query(sql) { const normalized = String(sql).replace(/\s+/g, ' ').trim(); if (normalized.startsWith('SELECT COUNT(*) AS total')) { return [[{ total: 1 }]]; } if (normalized.includes('FROM h5_publish_records pr') && normalized.includes('JOIN h5_users')) { return [[ { publicationId: 'pub-1', pageId: 'page-1', userId: 'user-1', publicUrl: '/u/john/pages/news', accessMode: 'public', status: 'online', userConfirmedAt: 1000, expiresAt: null, publishedAt: 2000, lifetimeViewCount: 1, pageTitle: 'Published News', pageSummary: '', workspaceRelativePath: 'public/daily-news-0916.html', username: 'john', displayName: 'John', }, ]]; } if (normalized.includes('FROM h5_publication_views')) { return [[]]; } return [[]]; }, }; const umamiPath = '/MindSpace/user-1/public/daily-news-0916.html'; const umamiOnlyPath = '/MindSpace/user-1/public/daily-news-0917.html'; const statsByPath = new Map([ [umamiPath, { totalViews: 20, seoViews: 12, geoViews: 2 }], [umamiOnlyPath, { totalViews: 9, seoViews: 7, geoViews: 0, pageTitle: '每日新闻早报 · 2026年9月17日', ownerLabel: 'John', generatedAt: 9000, }], ]); const result = await listAdminSeoGeoPublicationCatalog(pool, { page: 1, pageSize: 10, startAt: 1, endAt: 9_999_999_999_999, sortBy: 'seoViews', sortOrder: 'desc', fetchUmamiStats: async () => ({ statsByPath, available: true, reason: 'ok', }), }); assert.equal(result.total, 2); assert.equal(result.umami.available, true); assert.equal(result.data[0].publicationId, 'pub-1'); assert.equal(result.data[0].seoViews, 12); assert.equal(result.data[0].statsSource, 'umami+publication'); assert.equal(result.data[1].publicationId, `umami:${umamiOnlyPath}`); assert.equal(result.data[1].seoViews, 7); });