From 0cd0ea42809fa31f0ec139a8709ee5685f3a626b Mon Sep 17 00:00:00 2001 From: i-xtsu-sixyou-ken-mei <9bmf4by27w@privaterelay.appleid.com> Date: Wed, 9 Sep 2026 17:31:08 +0900 Subject: [PATCH 1/7] test(control-center): preserve legacy waitlist attribution --- .../waitlist-growth-legacy-language.test.ts | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 apps/control-center/src/server/services/waitlist-growth-legacy-language.test.ts diff --git a/apps/control-center/src/server/services/waitlist-growth-legacy-language.test.ts b/apps/control-center/src/server/services/waitlist-growth-legacy-language.test.ts new file mode 100644 index 000000000..6eda94c39 --- /dev/null +++ b/apps/control-center/src/server/services/waitlist-growth-legacy-language.test.ts @@ -0,0 +1,119 @@ +import type { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { describe, expect, it, vi } from 'vitest'; + +import { loadWaitlistGrowth } from './waitlist-growth.js'; + +type QueryResult = { + count?: number | null; + data?: unknown[] | null; + error: Error | null; +}; + +function query(result: QueryResult) { + const promise = () => Promise.resolve(result); + return { + select() { + return this; + }, + lte() { + return this; + }, + gte() { + return this; + }, + in() { + return this; + }, + eq() { + return this; + }, + order() { + return this; + }, + range() { + return promise(); + }, + then( + onfulfilled?: + | ((value: QueryResult) => TResult1 | PromiseLike) + | null, + onrejected?: + | ((reason: unknown) => TResult2 | PromiseLike) + | null, + ) { + return promise().then(onfulfilled, onrejected); + }, + }; +} + +describe('loadWaitlistGrowth legacy attribution', () => { + it('keeps legacy jobs with missing language and social post attributable', async () => { + const from = vi + .fn() + .mockReturnValueOnce(query({ count: 1, error: null })) + .mockReturnValueOnce(query({ count: 1, error: null })) + .mockReturnValueOnce(query({ count: 1, error: null })) + .mockReturnValueOnce( + query({ + data: [ + { + id: 'signup-legacy', + created_at: '2026-09-08T12:00:00.000Z', + social_publish_job_id: 'job-legacy', + }, + ], + error: null, + }), + ); + const pipelineFrom = vi + .fn() + .mockReturnValueOnce( + query({ + data: [ + { + id: 'job-legacy', + episode_id: 'episode-legacy', + platform: 'youtube', + language_code: null, + social_post_id: null, + }, + ], + error: null, + }), + ) + .mockReturnValueOnce(query({ data: [], error: null })); + const schema = vi.fn(() => ({ from: pipelineFrom })); + const client = { from, schema } as unknown as SupabaseClient; + const create = (() => client) as unknown as typeof createClient; + + const result = await loadWaitlistGrowth({ + create, + url: 'https://example.supabase.co', + key: 'test-key', + now: new Date('2026-09-09T00:00:00.000Z'), + }); + + expect(result).toEqual({ + status: 'ok', + message: null, + total: 1, + signups7d: 1, + signups30d: 1, + attributedSocial7d: 1, + directOrUnknown7d: 0, + conversions: [ + { + socialPublishJobId: 'job-legacy', + episodeId: 'episode-legacy', + platform: 'youtube', + languageCode: 'unknown', + socialPostId: null, + signups: 1, + views24h: null, + signupRate: null, + }, + ], + }); + expect(pipelineFrom).toHaveBeenCalledTimes(2); + }); +}); From eaa20d94b7ae56e4d6af0b0370808347e2ccd063 Mon Sep 17 00:00:00 2001 From: i-xtsu-sixyou-ken-mei <9bmf4by27w@privaterelay.appleid.com> Date: Wed, 9 Sep 2026 18:36:02 +0900 Subject: [PATCH 2/7] fix(control-center): fail closed on duplicate metric pages --- apps/control-center/src/server/services/waitlist-growth.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/control-center/src/server/services/waitlist-growth.ts b/apps/control-center/src/server/services/waitlist-growth.ts index 95d2a2f70..2a0b20efa 100644 --- a/apps/control-center/src/server/services/waitlist-growth.ts +++ b/apps/control-center/src/server/services/waitlist-growth.ts @@ -188,6 +188,7 @@ async function readViews( until: string, ): Promise> { const views = new Map(); + const seenMetricIds = new Set(); const ids = [ ...new Set( jobs.flatMap((job) => (job.social_post_id ? [job.social_post_id] : [])), @@ -214,6 +215,10 @@ async function readViews( break; } for (const row of result.data) { + if (seenMetricIds.has(row.id)) { + throw new Error('Metric snapshot changed during collection'); + } + seenMetricIds.add(row.id); views.set(row.social_post_id, row.views); } fetched += result.data.length; From b5fa9b2ce4d0c661141eaf3f8647b0b2b6b256ab Mon Sep 17 00:00:00 2001 From: i-xtsu-sixyou-ken-mei <9bmf4by27w@privaterelay.appleid.com> Date: Wed, 9 Sep 2026 18:36:29 +0900 Subject: [PATCH 3/7] test(control-center): detect duplicate metric pagination --- ...waitlist-growth-metrics-pagination.test.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/apps/control-center/src/server/services/waitlist-growth-metrics-pagination.test.ts b/apps/control-center/src/server/services/waitlist-growth-metrics-pagination.test.ts index 0cdcd5f57..f851b8eb4 100644 --- a/apps/control-center/src/server/services/waitlist-growth-metrics-pagination.test.ts +++ b/apps/control-center/src/server/services/waitlist-growth-metrics-pagination.test.ts @@ -211,4 +211,89 @@ describe('loadWaitlistGrowth metric pagination', () => { }), ]); }); + + it('degrades views when offset pagination repeats a metric row', async () => { + const from = vi + .fn() + .mockReturnValueOnce(query({ count: 1, error: null })) + .mockReturnValueOnce(query({ count: 1, error: null })) + .mockReturnValueOnce(query({ count: 1, error: null })) + .mockReturnValueOnce( + query({ + data: [ + { + id: 'signup-1', + created_at: '2026-09-08T12:00:00.000Z', + social_publish_job_id: 'job-1', + }, + ], + error: null, + }), + ); + + const jobPage = query({ + data: [ + { + id: 'job-1', + episode_id: 'episode-1', + platform: 'youtube', + language_code: 'en', + social_post_id: 'post-1', + }, + ], + error: null, + }); + const jobPageDone = query({ data: [], error: null }); + const firstMetricPage = query({ + data: Array.from({ length: 500 }, (_, index) => ({ + id: `metric-${String(index + 1).padStart(3, '0')}`, + social_post_id: 'post-1', + views: index + 1, + captured_at: '2026-09-08T13:00:00.000Z', + })), + error: null, + }); + const repeatedMetricPage = query({ + data: [ + { + id: 'metric-500', + social_post_id: 'post-1', + views: 999, + captured_at: '2026-09-08T13:00:00.000Z', + }, + ], + error: null, + }); + const pipelineFrom = vi + .fn() + .mockReturnValueOnce(jobPage) + .mockReturnValueOnce(jobPageDone) + .mockReturnValueOnce(firstMetricPage) + .mockReturnValueOnce(repeatedMetricPage); + const schema = vi.fn(() => ({ from: pipelineFrom })); + const client = { from, schema } as unknown as SupabaseClient; + const create = (() => client) as unknown as typeof createClient; + + const result = await loadWaitlistGrowth({ + create, + url: 'https://example.supabase.co', + key: 'test-key', + now: new Date('2026-09-09T00:00:00.000Z'), + }); + + expect(result.status).toBe('ok'); + expect(result.message).toBe( + '24h views unavailable; persisted signup counts remain available.', + ); + expect(result.conversions).toEqual([ + expect.objectContaining({ + socialPostId: 'post-1', + signups: 1, + views24h: null, + signupRate: null, + }), + ]); + expect(firstMetricPage.range).toHaveBeenCalledWith(0, 499); + expect(repeatedMetricPage.range).toHaveBeenCalledWith(500, 999); + }); }); From 34444d70aafbfa173e480728abb5d549387c2dac Mon Sep 17 00:00:00 2001 From: i-xtsu-sixyou-ken-mei <9bmf4by27w@privaterelay.appleid.com> Date: Wed, 9 Sep 2026 19:31:53 +0900 Subject: [PATCH 4/7] fix(control-center): reject reordered metric pages --- .../src/server/services/waitlist-growth.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/control-center/src/server/services/waitlist-growth.ts b/apps/control-center/src/server/services/waitlist-growth.ts index 2a0b20efa..d6cc6f3a2 100644 --- a/apps/control-center/src/server/services/waitlist-growth.ts +++ b/apps/control-center/src/server/services/waitlist-growth.ts @@ -196,6 +196,7 @@ async function readViews( ]; for (let offset = 0; offset < ids.length; offset += JOB_BATCH_SIZE) { let fetched = 0; + let lastMetric: { captured_at: string; id: string } | null = null; while (true) { const result = await client .schema('from_fed_to_chain') @@ -215,14 +216,21 @@ async function readViews( break; } for (const row of result.data) { - if (seenMetricIds.has(row.id)) { + if ( + seenMetricIds.has(row.id) || + (lastMetric !== null && + (row.captured_at < lastMetric.captured_at || + (row.captured_at === lastMetric.captured_at && + row.id <= lastMetric.id))) + ) { throw new Error('Metric snapshot changed during collection'); } seenMetricIds.add(row.id); + lastMetric = { captured_at: row.captured_at, id: row.id }; views.set(row.social_post_id, row.views); } fetched += result.data.length; } } return views; -} +} \ No newline at end of file From aa6eabd060edd6289d8289d385802532fa163013 Mon Sep 17 00:00:00 2001 From: i-xtsu-sixyou-ken-mei <9bmf4by27w@privaterelay.appleid.com> Date: Wed, 9 Sep 2026 19:32:08 +0900 Subject: [PATCH 5/7] test(control-center): reject reordered metric pagination --- .../waitlist-growth-metrics-reorder.test.ts | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 apps/control-center/src/server/services/waitlist-growth-metrics-reorder.test.ts diff --git a/apps/control-center/src/server/services/waitlist-growth-metrics-reorder.test.ts b/apps/control-center/src/server/services/waitlist-growth-metrics-reorder.test.ts new file mode 100644 index 000000000..590a2b55f --- /dev/null +++ b/apps/control-center/src/server/services/waitlist-growth-metrics-reorder.test.ts @@ -0,0 +1,132 @@ +import type { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { describe, expect, it, vi } from 'vitest'; + +import { loadWaitlistGrowth } from './waitlist-growth.js'; + +type QueryResult = { + count?: number | null; + data?: unknown[] | null; + error: Error | null; +}; + +function query(result: QueryResult) { + const promise = () => Promise.resolve(result); + return { + select() { + return this; + }, + lte() { + return this; + }, + gte() { + return this; + }, + in() { + return this; + }, + eq() { + return this; + }, + order() { + return this; + }, + range: vi.fn(() => promise()), + then( + onfulfilled?: + | ((value: QueryResult) => TResult1 | PromiseLike) + | null, + onrejected?: + | ((reason: unknown) => TResult2 | PromiseLike) + | null, + ) { + return promise().then(onfulfilled, onrejected); + }, + }; +} + +describe('loadWaitlistGrowth metric snapshot consistency', () => { + it('degrades views when a later offset page moves backward in sort order', async () => { + const from = vi + .fn() + .mockReturnValueOnce(query({ count: 1, error: null })) + .mockReturnValueOnce(query({ count: 1, error: null })) + .mockReturnValueOnce(query({ count: 1, error: null })) + .mockReturnValueOnce( + query({ + data: [ + { + id: 'signup-1', + created_at: '2026-09-08T12:00:00.000Z', + social_publish_job_id: 'job-1', + }, + ], + error: null, + }), + ); + + const jobPage = query({ + data: [ + { + id: 'job-1', + episode_id: 'episode-1', + platform: 'youtube', + language_code: 'en', + social_post_id: 'post-1', + }, + ], + error: null, + }); + const jobPageDone = query({ data: [], error: null }); + const firstMetricPage = query({ + data: Array.from({ length: 500 }, (_, index) => ({ + id: `metric-${String(index + 1).padStart(3, '0')}`, + social_post_id: 'post-1', + views: index + 1, + captured_at: '2026-09-08T14:00:00.000Z', + })), + error: null, + }); + const reorderedMetricPage = query({ + data: [ + { + id: 'metric-new', + social_post_id: 'post-1', + views: 999, + captured_at: '2026-09-08T13:59:59.000Z', + }, + ], + error: null, + }); + const pipelineFrom = vi + .fn() + .mockReturnValueOnce(jobPage) + .mockReturnValueOnce(jobPageDone) + .mockReturnValueOnce(firstMetricPage) + .mockReturnValueOnce(reorderedMetricPage); + const schema = vi.fn(() => ({ from: pipelineFrom })); + const client = { from, schema } as unknown as SupabaseClient; + const create = (() => client) as unknown as typeof createClient; + + const result = await loadWaitlistGrowth({ + create, + url: 'https://example.supabase.co', + key: 'test-key', + now: new Date('2026-09-09T00:00:00.000Z'), + }); + + expect(result.status).toBe('ok'); + expect(result.message).toBe( + '24h views unavailable; persisted signup counts remain available.', + ); + expect(result.conversions).toEqual([ + expect.objectContaining({ + socialPostId: 'post-1', + signups: 1, + views24h: null, + signupRate: null, + }), + ]); + expect(firstMetricPage.range).toHaveBeenCalledWith(0, 499); + expect(reorderedMetricPage.range).toHaveBeenCalledWith(500, 999); + }); +}); From fdf9ce26d74bf227485c84e0b06aa93094a5468f Mon Sep 17 00:00:00 2001 From: i-xtsu-sixyou-ken-mei <9bmf4by27w@privaterelay.appleid.com> Date: Wed, 9 Sep 2026 20:31:25 +0900 Subject: [PATCH 6/7] fix(control-center): restore canonical file ending --- apps/control-center/src/server/services/waitlist-growth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/control-center/src/server/services/waitlist-growth.ts b/apps/control-center/src/server/services/waitlist-growth.ts index d6cc6f3a2..647872c57 100644 --- a/apps/control-center/src/server/services/waitlist-growth.ts +++ b/apps/control-center/src/server/services/waitlist-growth.ts @@ -233,4 +233,4 @@ async function readViews( } } return views; -} \ No newline at end of file +} From c039afe29ce98d9c0466f447c807ed7395a1ce3b Mon Sep 17 00:00:00 2001 From: i-xtsu-sixyou-ken-mei <9bmf4by27w@privaterelay.appleid.com> Date: Wed, 9 Sep 2026 21:32:09 +0900 Subject: [PATCH 7/7] test(control-center): pin seven-day attribution boundary --- .../services/waitlist-growth-batching.test.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/apps/control-center/src/server/services/waitlist-growth-batching.test.ts b/apps/control-center/src/server/services/waitlist-growth-batching.test.ts index c14e01111..108d2e9ac 100644 --- a/apps/control-center/src/server/services/waitlist-growth-batching.test.ts +++ b/apps/control-center/src/server/services/waitlist-growth-batching.test.ts @@ -141,4 +141,63 @@ describe('loadWaitlistGrowth batching', () => { expect(secondPage.range).toHaveBeenCalledWith(500, 999); expect(pipelineFrom).not.toHaveBeenCalled(); }); + + it('includes an attributed signup exactly on the seven-day boundary', async () => { + const signups = [ + { + id: 'signup-before-boundary', + created_at: '2026-09-01T23:59:59.999Z', + social_publish_job_id: 'job-1', + }, + { + id: 'signup-on-boundary', + created_at: '2026-09-02T00:00:00.000Z', + social_publish_job_id: 'job-1', + }, + ]; + const signupPage = query({ data: signups, error: null }); + const from = vi + .fn() + .mockReturnValueOnce(query({ count: 2, error: null })) + .mockReturnValueOnce(query({ count: 1, error: null })) + .mockReturnValueOnce(query({ count: 2, error: null })) + .mockReturnValueOnce(signupPage); + const jobPage = query({ + data: [ + { + id: 'job-1', + episode_id: 'episode-1', + platform: 'threads', + language_code: 'ja', + social_post_id: null, + }, + ], + error: null, + }); + const pipelineFrom = vi + .fn() + .mockReturnValueOnce(jobPage) + .mockReturnValueOnce(query({ data: [], error: null })); + const schema = vi.fn(() => ({ from: pipelineFrom })); + const client = { from, schema } as unknown as SupabaseClient; + const create = (() => client) as unknown as typeof createClient; + + const result = await loadWaitlistGrowth({ + create, + url: 'https://example.supabase.co', + key: 'test-key', + now: new Date('2026-09-09T00:00:00.000Z'), + }); + + expect(result.status).toBe('ok'); + expect(result.signups7d).toBe(1); + expect(result.attributedSocial7d).toBe(1); + expect(result.directOrUnknown7d).toBe(0); + expect(result.conversions).toEqual([ + expect.objectContaining({ + socialPublishJobId: 'job-1', + signups: 2, + }), + ]); + }); });