Configure browsing session pacing and size
This commit is contained in:
1 parent
1fd154e1eb
commit
e890f74478
14 files changed
+201
-56
No files matched your search
@@ -122,8 +122,7 @@ describe('AccountBrowsingSimulationService', () => {
|
||||
const dueAt = repository.getSettings(ACCOUNT).nextDueAt;
|
||||
|
||||
expect(scheduled).toEqual({ kind: 'scheduled', nextDueAt: dueAt });
|
||||
expect(dueAt).toBeGreaterThanOrEqual(NOW + 45 * 60_000);
|
||||
expect(dueAt).toBeLessThanOrEqual(NOW + 12 * 60 * 60_000);
|
||||
expect(dueAt).toBe(NOW + 50 * 60_000);
|
||||
const restarted = service({ random: 0.1 });
|
||||
await expect(restarted.instance.tick()).resolves.toEqual({
|
||||
kind: 'idle', reason: 'Browsing simulation is not due',
|
||||
@@ -157,6 +156,37 @@ describe('AccountBrowsingSimulationService', () => {
|
||||
expect(voter.upvote).toHaveBeenCalledWith('t3_one');
|
||||
});
|
||||
|
||||
it('selects the configured number of distinct things in one session', async () => {
|
||||
enable({ postWeight: 100, commentWeight: 0, selectionsPerSession: 3 });
|
||||
const fixture = service({ posts: [post('one'), post('two'), post('three'), post('four')] });
|
||||
|
||||
const result = await fixture.instance.runNow();
|
||||
|
||||
expect(result).toMatchObject({ kind: 'simulated' });
|
||||
if (result.kind !== 'simulated') throw new Error('Expected a simulated session');
|
||||
expect(result.decisions).toHaveLength(3);
|
||||
expect(new Set(result.decisions.map((decision) => decision.thingFullname)).size).toBe(3);
|
||||
expect(repository.listRecentSelections(ACCOUNT)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('caps a scheduled multi-selection session at the remaining daily target', async () => {
|
||||
enable({ postWeight: 100, commentWeight: 0, selectionsPerSession: 3, dailyTargetMin: 2, dailyTargetMax: 2 });
|
||||
repository.recordDecision({
|
||||
accountId: ACCOUNT, thingFullname: 't3_prior', thingKind: 'post', postId: 'prior',
|
||||
subreddit: 'AskUK', author: 'prior_author', permalink: '/prior', decision: 'simulated-upvote',
|
||||
reason: 'test', discoveredAt: NOW - 1, decidedAt: NOW - 1, scheduledSessionId: 'prior',
|
||||
voteSubmitted: false, voteStatus: 'simulated',
|
||||
});
|
||||
repository.setNextDueAt(ACCOUNT, NOW, NOW - 1);
|
||||
|
||||
const result = await service({ posts: [post('one'), post('two'), post('three')] }).instance.tick();
|
||||
|
||||
expect(result).toMatchObject({ kind: 'simulated' });
|
||||
if (result.kind !== 'simulated') throw new Error('Expected a simulated session');
|
||||
expect(result.decisions).toHaveLength(1);
|
||||
expect(repository.listRecentSelections(ACCOUNT)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('keeps an immediate session simulation-only in browser dry-run', async () => {
|
||||
enable({ postWeight: 100, commentWeight: 0, autoVoteEnabled: true });
|
||||
const voter = { upvote: vi.fn().mockResolvedValue({ status: 'submitted' }) };
|
||||
@@ -305,13 +335,13 @@ describe('browsing migration', () => {
|
||||
runMigrations(db);
|
||||
const version = db.pragma('user_version', { simple: true }) as number;
|
||||
db.exec('DROP TABLE account_browsing_decisions; DROP TABLE account_browsing_settings;');
|
||||
db.pragma(`user_version = ${version - 3}`);
|
||||
db.pragma(`user_version = ${version - 4}`);
|
||||
|
||||
runMigrations(db);
|
||||
insertAccount(db, ACCOUNT, 'owner');
|
||||
|
||||
expect(new AccountBrowsingSimulationRepository(db).getSettings(ACCOUNT)).toMatchObject({
|
||||
enabled: false, dailyTargetMin: 3, dailyTargetMax: 6, nextDueAt: null,
|
||||
enabled: false, dailyTargetMin: 3, dailyTargetMax: 6, selectionsPerSession: 1, nextDueAt: null,
|
||||
});
|
||||
expect(db.prepare(`PRAGMA table_info(account_growth_settings)`).all())
|
||||
.toEqual(expect.arrayContaining([expect.objectContaining({ name: 'daily_limit' })]));
|
||||
|
||||
@@ -54,6 +54,7 @@ function browsing(includeRecent = false) {
|
||||
quietHours: { enabled: true, startHour: 23, endHour: 7 },
|
||||
minimumGapMs: 2_700_000,
|
||||
jitterMs: 900_000,
|
||||
selectionsPerSession: 1,
|
||||
allowNsfw: false,
|
||||
autoVoteEnabled: false,
|
||||
nextDueAt: null,
|
||||
@@ -266,6 +267,26 @@ describe('GrowthPage', () => {
|
||||
})));
|
||||
});
|
||||
|
||||
it('updates browsing session pacing and size', async () => {
|
||||
renderGrowth();
|
||||
const user = userEvent.setup();
|
||||
|
||||
const gap = await screen.findByLabelText('Minimum session gap');
|
||||
await user.clear(gap);
|
||||
await user.type(gap, '90');
|
||||
await user.click(screen.getByRole('button', { name: 'Save session gap' }));
|
||||
|
||||
const size = screen.getByLabelText('Selections per session');
|
||||
await user.clear(size);
|
||||
await user.type(size, '4');
|
||||
await user.click(screen.getByRole('button', { name: 'Save session size' }));
|
||||
|
||||
await waitFor(() => expect(requests).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ method: 'PATCH', body: { minimumGapMs: 5_400_000 } }),
|
||||
expect.objectContaining({ method: 'PATCH', body: { selectionsPerSession: 4 } }),
|
||||
])));
|
||||
});
|
||||
|
||||
it('shows an empty state when there are no accounts', async () => {
|
||||
overviewPayload = [];
|
||||
renderGrowth();
|
||||
|
||||
@@ -265,12 +265,13 @@ describe('growth route', () => {
|
||||
dailyTargetMin: 4,
|
||||
dailyTargetMax: 8,
|
||||
minimumGapMs: 3_600_000,
|
||||
selectionsPerSession: 4,
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
const parsed = GrowthAccountResponseSchema.parse(json);
|
||||
expect(parsed.account.browsing).toMatchObject({
|
||||
settings: { enabled: true, dailyTargetMin: 4, dailyTargetMax: 8 },
|
||||
settings: { enabled: true, dailyTargetMin: 4, dailyTargetMax: 8, selectionsPerSession: 4 },
|
||||
noVotesSubmitted: true,
|
||||
});
|
||||
expect(pushed).toContainEqual({
|
||||
@@ -281,6 +282,7 @@ describe('growth route', () => {
|
||||
it.each([
|
||||
{ dailyTargetMax: 13 },
|
||||
{ minimumGapMs: 44 * 60_000 },
|
||||
{ selectionsPerSession: 13 },
|
||||
{ postWeight: 0, commentWeight: 0 },
|
||||
{ nextDueAt: 123 },
|
||||
])('rejects unsafe or non-operator settings %o', async (body) => {
|
||||
|
||||
Reference in new issue
Block a user