Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
]);
});
});
Original file line number Diff line number Diff line change
@@ -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<TResult1 = QueryResult, TResult2 = never>(
onfulfilled?:
| ((value: QueryResult) => TResult1 | PromiseLike<TResult1>)
| null,
onrejected?:
| ((reason: unknown) => TResult2 | PromiseLike<TResult2>)
| 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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Loading
Loading