feat: configure ready outreach funnel limit
This commit is contained in:
1 parent
22eb32e5bf
commit
e9083281a8
15 files changed
+66
-11
No files matched your search
@@ -26,6 +26,7 @@ export interface SettingsDraft {
|
|||||||
dryRun: boolean;
|
dryRun: boolean;
|
||||||
prioritizeRecentPosts: boolean;
|
prioritizeRecentPosts: boolean;
|
||||||
recentWindowMinutes: string;
|
recentWindowMinutes: string;
|
||||||
|
maxReadyCandidates: string;
|
||||||
subreddits: SubredditDraft[];
|
subreddits: SubredditDraft[];
|
||||||
advancedConfigJson: string;
|
advancedConfigJson: string;
|
||||||
}
|
}
|
||||||
@@ -53,6 +54,7 @@ const ValidatedSettingsSchema = z.strictObject({
|
|||||||
dryRun: z.boolean(),
|
dryRun: z.boolean(),
|
||||||
prioritizeRecentPosts: z.boolean(),
|
prioritizeRecentPosts: z.boolean(),
|
||||||
recentWindowMinutes: z.number().finite().positive('Recent window must be greater than 0 minutes.'),
|
recentWindowMinutes: z.number().finite().positive('Recent window must be greater than 0 minutes.'),
|
||||||
|
maxReadyCandidates: z.number().int().min(1, 'Ready-post limit must be at least 1.').max(500, 'Ready-post limit cannot exceed 500.'),
|
||||||
subreddits: z.array(EditableSubredditSchema).min(1, 'At least one subreddit is required.'),
|
subreddits: z.array(EditableSubredditSchema).min(1, 'At least one subreddit is required.'),
|
||||||
}).superRefine((value, context) => {
|
}).superRefine((value, context) => {
|
||||||
const names = value.subreddits.map((subreddit) => subreddit.name.trim().toLowerCase());
|
const names = value.subreddits.map((subreddit) => subreddit.name.trim().toLowerCase());
|
||||||
@@ -69,6 +71,7 @@ export function createSettingsDraft(config: ConfigResponse): SettingsDraft {
|
|||||||
dryRun: config.browser.dryRun,
|
dryRun: config.browser.dryRun,
|
||||||
prioritizeRecentPosts: config.reddit.prioritizeRecentPosts,
|
prioritizeRecentPosts: config.reddit.prioritizeRecentPosts,
|
||||||
recentWindowMinutes: String(config.reddit.recentWindowMinutes),
|
recentWindowMinutes: String(config.reddit.recentWindowMinutes),
|
||||||
|
maxReadyCandidates: String(config.reddit.maxReadyCandidates),
|
||||||
subreddits: config.subreddits.map((subreddit) => ({
|
subreddits: config.subreddits.map((subreddit) => ({
|
||||||
name: subreddit.name,
|
name: subreddit.name,
|
||||||
enabled: subreddit.enabled,
|
enabled: subreddit.enabled,
|
||||||
@@ -90,6 +93,7 @@ export function validateSettingsDraft(draft: SettingsDraft): SettingsValidationR
|
|||||||
if (draft.maxMessagesPerHour.trim() === '') requiredErrors.maxMessagesPerHour = 'Maximum messages per hour is required.';
|
if (draft.maxMessagesPerHour.trim() === '') requiredErrors.maxMessagesPerHour = 'Maximum messages per hour is required.';
|
||||||
if (draft.jitterSeconds.trim() === '') requiredErrors.jitterSeconds = 'Jitter is required.';
|
if (draft.jitterSeconds.trim() === '') requiredErrors.jitterSeconds = 'Jitter is required.';
|
||||||
if (draft.recentWindowMinutes.trim() === '') requiredErrors.recentWindowMinutes = 'Recent window is required.';
|
if (draft.recentWindowMinutes.trim() === '') requiredErrors.recentWindowMinutes = 'Recent window is required.';
|
||||||
|
if (draft.maxReadyCandidates.trim() === '') requiredErrors.maxReadyCandidates = 'Ready-post limit is required.';
|
||||||
draft.subreddits.forEach((subreddit, index) => {
|
draft.subreddits.forEach((subreddit, index) => {
|
||||||
if (subreddit.name.trim() === '') requiredErrors[`subreddits.${index}.name`] = 'Subreddit name is required.';
|
if (subreddit.name.trim() === '') requiredErrors[`subreddits.${index}.name`] = 'Subreddit name is required.';
|
||||||
if (subreddit.weight.trim() !== '' && Number(subreddit.weight) <= 0) {
|
if (subreddit.weight.trim() !== '' && Number(subreddit.weight) <= 0) {
|
||||||
@@ -125,6 +129,7 @@ export function validateSettingsDraft(draft: SettingsDraft): SettingsValidationR
|
|||||||
dryRun: draft.dryRun,
|
dryRun: draft.dryRun,
|
||||||
prioritizeRecentPosts: draft.prioritizeRecentPosts,
|
prioritizeRecentPosts: draft.prioritizeRecentPosts,
|
||||||
recentWindowMinutes: Number(draft.recentWindowMinutes),
|
recentWindowMinutes: Number(draft.recentWindowMinutes),
|
||||||
|
maxReadyCandidates: Number(draft.maxReadyCandidates),
|
||||||
subreddits: draft.subreddits.map((subreddit) => ({
|
subreddits: draft.subreddits.map((subreddit) => ({
|
||||||
name: subreddit.name.trim(),
|
name: subreddit.name.trim(),
|
||||||
enabled: subreddit.enabled,
|
enabled: subreddit.enabled,
|
||||||
@@ -165,6 +170,7 @@ export function validateSettingsDraft(draft: SettingsDraft): SettingsValidationR
|
|||||||
reddit: {
|
reddit: {
|
||||||
prioritizeRecentPosts: parsed.data.prioritizeRecentPosts,
|
prioritizeRecentPosts: parsed.data.prioritizeRecentPosts,
|
||||||
recentWindowMinutes: parsed.data.recentWindowMinutes,
|
recentWindowMinutes: parsed.data.recentWindowMinutes,
|
||||||
|
maxReadyCandidates: parsed.data.maxReadyCandidates,
|
||||||
},
|
},
|
||||||
subreddits: parsed.data.subreddits,
|
subreddits: parsed.data.subreddits,
|
||||||
...(advanced ? { advanced } : {}),
|
...(advanced ? { advanced } : {}),
|
||||||
|
|||||||
@@ -58,6 +58,14 @@ export function MonitoringSection({ draft, errors, onChange }: SectionProps) {
|
|||||||
onChange={(recentWindowMinutes) => onChange({ ...draft, recentWindowMinutes })}
|
onChange={(recentWindowMinutes) => onChange({ ...draft, recentWindowMinutes })}
|
||||||
value={draft.recentWindowMinutes}
|
value={draft.recentWindowMinutes}
|
||||||
/>
|
/>
|
||||||
|
<NumberField
|
||||||
|
error={errors.maxReadyCandidates}
|
||||||
|
label="Maximum ready posts"
|
||||||
|
min={1}
|
||||||
|
name="maximum-ready-posts"
|
||||||
|
onChange={(maxReadyCandidates) => onChange({ ...draft, maxReadyCandidates })}
|
||||||
|
value={draft.maxReadyCandidates}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ reddit:
|
|||||||
filterSellers: true # Skip profiles that appear to be OnlyFans/content sellers
|
filterSellers: true # Skip profiles that appear to be OnlyFans/content sellers
|
||||||
prioritizeRecentPosts: true # Prefer posts within recentWindowMinutes; older posts only as fallback
|
prioritizeRecentPosts: true # Prefer posts within recentWindowMinutes; older posts only as fallback
|
||||||
recentWindowMinutes: 30
|
recentWindowMinutes: 30
|
||||||
|
maxReadyCandidates: 25 # Pause screening when this many posts are ready to send
|
||||||
# sellerKeywords: [...] # Uncomment to override default keyword list
|
# sellerKeywords: [...] # Uncomment to override default keyword list
|
||||||
|
|
||||||
openrouter:
|
openrouter:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const RateLimitConfigSchema = z.strictObject({
|
|||||||
export const RuntimeRedditConfigSchema = z.strictObject({
|
export const RuntimeRedditConfigSchema = z.strictObject({
|
||||||
prioritizeRecentPosts: z.boolean(),
|
prioritizeRecentPosts: z.boolean(),
|
||||||
recentWindowMinutes: z.number(),
|
recentWindowMinutes: z.number(),
|
||||||
|
maxReadyCandidates: z.number().int().min(1).max(500),
|
||||||
});
|
});
|
||||||
export const RuntimeConfigSchema = z.strictObject({
|
export const RuntimeConfigSchema = z.strictObject({
|
||||||
rateLimit: RateLimitConfigSchema,
|
rateLimit: RateLimitConfigSchema,
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ describe('dashboard endpoint contracts', () => {
|
|||||||
const payload = {
|
const payload = {
|
||||||
rateLimit: { minDelayBetweenMessagesMs: 300_000, maxMessagesPerHour: 8, jitterMs: 60_000 },
|
rateLimit: { minDelayBetweenMessagesMs: 300_000, maxMessagesPerHour: 8, jitterMs: 60_000 },
|
||||||
browser: { dryRun: false },
|
browser: { dryRun: false },
|
||||||
reddit: { prioritizeRecentPosts: true, recentWindowMinutes: 60 },
|
reddit: { prioritizeRecentPosts: true, recentWindowMinutes: 60, maxReadyCandidates: 25 },
|
||||||
subreddits: [{ name: 'example', enabled: true, type: 'chat', weight: 2, tone: 'direct' }],
|
subreddits: [{ name: 'example', enabled: true, type: 'chat', weight: 2, tone: 'direct' }],
|
||||||
tones: ['default', 'direct'],
|
tones: ['default', 'direct'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export const ConfigSchema = z.object({
|
|||||||
filterSellers: z.boolean().default(true),
|
filterSellers: z.boolean().default(true),
|
||||||
prioritizeRecentPosts: z.boolean().default(false),
|
prioritizeRecentPosts: z.boolean().default(false),
|
||||||
recentWindowMinutes: z.number().positive().default(60),
|
recentWindowMinutes: z.number().positive().default(60),
|
||||||
|
maxReadyCandidates: z.number().int().min(1).max(500).default(25),
|
||||||
sellerKeywords: z
|
sellerKeywords: z
|
||||||
.array(z.string())
|
.array(z.string())
|
||||||
.default([
|
.default([
|
||||||
|
|||||||
@@ -191,12 +191,22 @@ export class CandidateQueueRepository {
|
|||||||
`).run(now, postId, workerId).changes === 1;
|
`).run(now, postId, workerId).changes === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
promoteDueRetries(now: number = Date.now()): number {
|
promoteDueRetries(maxReadyCandidates: number, now: number = Date.now()): number {
|
||||||
|
if (!Number.isInteger(maxReadyCandidates) || maxReadyCandidates < 1) {
|
||||||
|
throw new Error('Maximum ready candidates must be a positive integer');
|
||||||
|
}
|
||||||
|
const capacity = maxReadyCandidates - this.countReady();
|
||||||
|
if (capacity <= 0) return 0;
|
||||||
return this.db.prepare(`
|
return this.db.prepare(`
|
||||||
UPDATE candidate_queue
|
UPDATE candidate_queue
|
||||||
SET status = 'ready', updated_at = ?, assigned_account_id = NULL
|
SET status = 'ready', updated_at = ?, assigned_account_id = NULL
|
||||||
WHERE status = 'retryable' AND available_at <= ?
|
WHERE post_id IN (
|
||||||
`).run(now, now).changes;
|
SELECT post_id FROM candidate_queue
|
||||||
|
WHERE status = 'retryable' AND available_at <= ?
|
||||||
|
ORDER BY priority DESC, discovered_at ASC
|
||||||
|
LIMIT ?
|
||||||
|
)
|
||||||
|
`).run(now, now, capacity).changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
recoverExpiredLeases(now: number = Date.now(), sendingDelayMs = 10 * 60_000): number {
|
recoverExpiredLeases(now: number = Date.now(), sendingDelayMs = 10 * 60_000): number {
|
||||||
|
|||||||
+2
-3
@@ -429,7 +429,6 @@ async function runConcurrentAccountRuntimes(app: AppContainer): Promise<void> {
|
|||||||
const workerInstance = `candidate-funnel-${process.pid}-${Date.now()}`;
|
const workerInstance = `candidate-funnel-${process.pid}-${Date.now()}`;
|
||||||
const leaseMs = 5 * 60_000;
|
const leaseMs = 5 * 60_000;
|
||||||
const maxActiveCandidates = 500;
|
const maxActiveCandidates = 500;
|
||||||
const readyTarget = 100;
|
|
||||||
const recovered = candidateQueueRepo.recoverAllLeases();
|
const recovered = candidateQueueRepo.recoverAllLeases();
|
||||||
app.recipientDeduplicator.recoverStaleReservations(Date.now() - 10 * 60_000);
|
app.recipientDeduplicator.recoverStaleReservations(Date.now() - 10 * 60_000);
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -527,7 +526,7 @@ async function runConcurrentAccountRuntimes(app: AppContainer): Promise<void> {
|
|||||||
candidateQueueRepo.expireStale(
|
candidateQueueRepo.expireStale(
|
||||||
Date.now() - config.reddit.recentWindowMinutes * 60_000,
|
Date.now() - config.reddit.recentWindowMinutes * 60_000,
|
||||||
);
|
);
|
||||||
if (candidateQueueRepo.countReady() >= readyTarget) {
|
if (candidateQueueRepo.countReady() >= config.reddit.maxReadyCandidates) {
|
||||||
await sleep(2_000, controller.signal).catch(() => undefined);
|
await sleep(2_000, controller.signal).catch(() => undefined);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -590,7 +589,7 @@ async function runConcurrentAccountRuntimes(app: AppContainer): Promise<void> {
|
|||||||
await sleep(1_000, controller.signal).catch(() => undefined);
|
await sleep(1_000, controller.signal).catch(() => undefined);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
candidateQueueRepo.promoteDueRetries();
|
candidateQueueRepo.promoteDueRetries(config.reddit.maxReadyCandidates);
|
||||||
candidateQueueRepo.recoverExpiredLeases();
|
candidateQueueRepo.recoverExpiredLeases();
|
||||||
app.recipientDeduplicator.recoverStaleReservations(Date.now() - 10 * 60_000);
|
app.recipientDeduplicator.recoverStaleReservations(Date.now() - 10 * 60_000);
|
||||||
const recipients = await availableRecipients();
|
const recipients = await availableRecipients();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const AdvancedConfigSchema = z.strictObject({
|
|||||||
subreddits: true,
|
subreddits: true,
|
||||||
prioritizeRecentPosts: true,
|
prioritizeRecentPosts: true,
|
||||||
recentWindowMinutes: true,
|
recentWindowMinutes: true,
|
||||||
|
maxReadyCandidates: true,
|
||||||
}),
|
}),
|
||||||
openrouter: ConfigSchema.shape.openrouter.omit({ apiKey: true }),
|
openrouter: ConfigSchema.shape.openrouter.omit({ apiKey: true }),
|
||||||
browser: z.strictObject({ sessionDir: z.string() }),
|
browser: z.strictObject({ sessionDir: z.string() }),
|
||||||
@@ -19,7 +20,13 @@ const AdvancedConfigSchema = z.strictObject({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function advancedConfig(ctx: WebContext): z.infer<typeof AdvancedConfigSchema> {
|
function advancedConfig(ctx: WebContext): z.infer<typeof AdvancedConfigSchema> {
|
||||||
const { subreddits: _subreddits, prioritizeRecentPosts: _prioritize, recentWindowMinutes: _window, ...reddit } = ctx.config.reddit;
|
const {
|
||||||
|
subreddits: _subreddits,
|
||||||
|
prioritizeRecentPosts: _prioritize,
|
||||||
|
recentWindowMinutes: _window,
|
||||||
|
maxReadyCandidates: _maxReady,
|
||||||
|
...reddit
|
||||||
|
} = ctx.config.reddit;
|
||||||
const { apiKey: _apiKey, ...openrouter } = ctx.config.openrouter;
|
const { apiKey: _apiKey, ...openrouter } = ctx.config.openrouter;
|
||||||
const { dryRun: _dryRun, ...browser } = ctx.config.browser;
|
const { dryRun: _dryRun, ...browser } = ctx.config.browser;
|
||||||
return {
|
return {
|
||||||
@@ -98,6 +105,12 @@ export function createConfigRouter(ctx: WebContext): Router {
|
|||||||
if (typeof updates.reddit.recentWindowMinutes === 'number' && updates.reddit.recentWindowMinutes > 0) {
|
if (typeof updates.reddit.recentWindowMinutes === 'number' && updates.reddit.recentWindowMinutes > 0) {
|
||||||
ctx.runtimeConfig.reddit.recentWindowMinutes = updates.reddit.recentWindowMinutes;
|
ctx.runtimeConfig.reddit.recentWindowMinutes = updates.reddit.recentWindowMinutes;
|
||||||
}
|
}
|
||||||
|
if (typeof updates.reddit.maxReadyCandidates === 'number'
|
||||||
|
&& Number.isInteger(updates.reddit.maxReadyCandidates)
|
||||||
|
&& updates.reddit.maxReadyCandidates >= 1
|
||||||
|
&& updates.reddit.maxReadyCandidates <= 500) {
|
||||||
|
ctx.runtimeConfig.reddit.maxReadyCandidates = updates.reddit.maxReadyCandidates;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirror into the loaded config so getStats reflects new values.
|
// Mirror into the loaded config so getStats reflects new values.
|
||||||
@@ -107,6 +120,7 @@ export function createConfigRouter(ctx: WebContext): Router {
|
|||||||
ctx.config.browser.dryRun = ctx.runtimeConfig.browser.dryRun;
|
ctx.config.browser.dryRun = ctx.runtimeConfig.browser.dryRun;
|
||||||
ctx.config.reddit.prioritizeRecentPosts = ctx.runtimeConfig.reddit.prioritizeRecentPosts;
|
ctx.config.reddit.prioritizeRecentPosts = ctx.runtimeConfig.reddit.prioritizeRecentPosts;
|
||||||
ctx.config.reddit.recentWindowMinutes = ctx.runtimeConfig.reddit.recentWindowMinutes;
|
ctx.config.reddit.recentWindowMinutes = ctx.runtimeConfig.reddit.recentWindowMinutes;
|
||||||
|
ctx.config.reddit.maxReadyCandidates = ctx.runtimeConfig.reddit.maxReadyCandidates;
|
||||||
|
|
||||||
if (parsedSubreddits?.success) {
|
if (parsedSubreddits?.success) {
|
||||||
const next: SubredditConfig[] = parsedSubreddits.data;
|
const next: SubredditConfig[] = parsedSubreddits.data;
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export class WebServer {
|
|||||||
reddit: {
|
reddit: {
|
||||||
prioritizeRecentPosts: config.reddit.prioritizeRecentPosts,
|
prioritizeRecentPosts: config.reddit.prioritizeRecentPosts,
|
||||||
recentWindowMinutes: config.reddit.recentWindowMinutes,
|
recentWindowMinutes: config.reddit.recentWindowMinutes,
|
||||||
|
maxReadyCandidates: config.reddit.maxReadyCandidates,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.app = express();
|
this.app = express();
|
||||||
|
|||||||
@@ -65,5 +65,6 @@ export interface RuntimeConfig {
|
|||||||
reddit: {
|
reddit: {
|
||||||
prioritizeRecentPosts: boolean;
|
prioritizeRecentPosts: boolean;
|
||||||
recentWindowMinutes: number;
|
recentWindowMinutes: number;
|
||||||
|
maxReadyCandidates: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,7 @@ describe('CandidateQueueRepository', () => {
|
|||||||
queue.deferDelivery('p1', 'send-a', 'Matrix rate limited', 1_100, 1_004);
|
queue.deferDelivery('p1', 'send-a', 'Matrix rate limited', 1_100, 1_004);
|
||||||
|
|
||||||
expect(queue.claimReady('account-b', 'send-b', 100, 1_099)).toBeNull();
|
expect(queue.claimReady('account-b', 'send-b', 100, 1_099)).toBeNull();
|
||||||
expect(queue.promoteDueRetries(1_100)).toBe(1);
|
expect(queue.promoteDueRetries(25, 1_100)).toBe(1);
|
||||||
expect(queue.claimReady('account-b', 'send-b', 100, 1_101)).toMatchObject({
|
expect(queue.claimReady('account-b', 'send-b', 100, 1_101)).toMatchObject({
|
||||||
postId: 'p1',
|
postId: 'p1',
|
||||||
assignedAccountId: 'account-b',
|
assignedAccountId: 'account-b',
|
||||||
|
|||||||
@@ -41,6 +41,18 @@ describe('ConfigSchema', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('defaults the ready-post limit to 25 and requires a bounded integer', () => {
|
||||||
|
const result = ConfigSchema.safeParse(minimalConfig({ reddit: { subreddits: ['test'] } }));
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) expect(result.data.reddit.maxReadyCandidates).toBe(25);
|
||||||
|
expect(ConfigSchema.safeParse(minimalConfig({
|
||||||
|
reddit: { subreddits: ['test'], maxReadyCandidates: 0 },
|
||||||
|
})).success).toBe(false);
|
||||||
|
expect(ConfigSchema.safeParse(minimalConfig({
|
||||||
|
reddit: { subreddits: ['test'], maxReadyCandidates: 25.5 },
|
||||||
|
})).success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('applies default tone to subreddits', () => {
|
it('applies default tone to subreddits', () => {
|
||||||
const result = ConfigSchema.safeParse(minimalConfig({ reddit: { subreddits: ['test'] } }));
|
const result = ConfigSchema.safeParse(minimalConfig({ reddit: { subreddits: ['test'] } }));
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { SettingsRoute } from '../../apps/dashboard/src/features/settings/settin
|
|||||||
const config = {
|
const config = {
|
||||||
rateLimit: { minDelayBetweenMessagesMs: 300_000, maxMessagesPerHour: 8, jitterMs: 60_000 },
|
rateLimit: { minDelayBetweenMessagesMs: 300_000, maxMessagesPerHour: 8, jitterMs: 60_000 },
|
||||||
browser: { dryRun: true },
|
browser: { dryRun: true },
|
||||||
reddit: { prioritizeRecentPosts: false, recentWindowMinutes: 60 },
|
reddit: { prioritizeRecentPosts: false, recentWindowMinutes: 60, maxReadyCandidates: 25 },
|
||||||
subreddits: [{ name: 'casualconversation', type: 'chat', weight: 2, tone: 'default' }],
|
subreddits: [{ name: 'casualconversation', type: 'chat', weight: 2, tone: 'default' }],
|
||||||
tones: ['default', 'dominant', 'submissive', 'playful', 'direct', 'gentle', 'kink-friendly', 'teasing'],
|
tones: ['default', 'dominant', 'submissive', 'playful', 'direct', 'gentle', 'kink-friendly', 'teasing'],
|
||||||
advanced: { openrouter: { chatModel: 'example/chat-model' } },
|
advanced: { openrouter: { chatModel: 'example/chat-model' } },
|
||||||
@@ -173,7 +173,7 @@ describe('settings and access', () => {
|
|||||||
expect(screen.getByRole('button', { name: 'Saving settings' })).toBeDisabled();
|
expect(screen.getByRole('button', { name: 'Saving settings' })).toBeDisabled();
|
||||||
|
|
||||||
await waitFor(() => expect(resolveUpdate).toBeTypeOf('function'));
|
await waitFor(() => expect(resolveUpdate).toBeTypeOf('function'));
|
||||||
expect(requestBody).toMatchObject({ reddit: { prioritizeRecentPosts: true, recentWindowMinutes: 60 } });
|
expect(requestBody).toMatchObject({ reddit: { prioritizeRecentPosts: true, recentWindowMinutes: 60, maxReadyCandidates: 25 } });
|
||||||
resolveUpdate?.(HttpResponse.json({
|
resolveUpdate?.(HttpResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export function getTestConfig(overrides?: Partial<Config>): Config {
|
|||||||
sellerDomains: [],
|
sellerDomains: [],
|
||||||
prioritizeRecentPosts: false,
|
prioritizeRecentPosts: false,
|
||||||
recentWindowMinutes: 60,
|
recentWindowMinutes: 60,
|
||||||
|
maxReadyCandidates: 25,
|
||||||
...overrides?.reddit,
|
...overrides?.reddit,
|
||||||
},
|
},
|
||||||
openrouter: {
|
openrouter: {
|
||||||
|
|||||||
Reference in new issue
Block a user