Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 36 additions & 45 deletions libs/game/idle-client/src/resync/run-resync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
createMockAvatarData,
createMockEnemyData,
} from '@vers/idle-core/test-utils';
import { createAuthedServiceClient } from '@vers/mock-services';
import { createAuthedServiceClient, createViewer } from '@vers/mock-services';
import { mockActivityService } from '@vers/mock-services/activity';
import * as db from '@vers/mock-services/db';
import { waitFor } from '@vers/test-utils';
Expand Down Expand Up @@ -44,12 +44,11 @@ async function setupTest(config: Readonly<SetupTestConfig>) {
}

test('it resolves to none for an avatar with no activity history', async () => {
const user = await db.userCollection.create({});
const avatar = await db.avatarCollection.create({ userID: user.id });
const ctx = await setupTest({ userID: user.id });
const viewer = await createViewer();
const ctx = await setupTest({ userID: viewer.user.id });

const result = await runResync({
avatarID: avatar.id,
avatarID: viewer.avatar.id,
buildSimulationInput: (started) => ({
activity: createMockActivityInput({
encounter: {
Expand All @@ -74,19 +73,18 @@ test('it resolves to none for an avatar with no activity history', async () => {
});

test('it rebases from the stop index without simulating when the activity is capped', async () => {
const user = await db.userCollection.create({});
const avatar = await db.avatarCollection.create({ userID: user.id });
const ctx = await setupTest({ userID: user.id });
const viewer = await createViewer();
const ctx = await setupTest({ userID: viewer.user.id });

const activity = await db.activityCollection.create({
appendedHead: 5,
avatarID: avatar.id,
avatarID: viewer.avatar.id,
status: 'capped',
verifiedHead: 3,
});

const result = await runResync({
avatarID: avatar.id,
avatarID: viewer.avatar.id,
buildSimulationInput: (started) => ({
activity: createMockActivityInput({
encounter: {
Expand Down Expand Up @@ -121,20 +119,19 @@ test('it rebases from the stop index without simulating when the activity is cap
});

test('it attaches live when the gap is negligible, leaving the submitter untouched', async () => {
const user = await db.userCollection.create({});
const avatar = await db.avatarCollection.create({ userID: user.id });
const ctx = await setupTest({ userID: user.id });
const viewer = await createViewer();
const ctx = await setupTest({ userID: viewer.user.id });

const activity = await db.activityCollection.create({
appendedAt: new Date(Date.now() - 2000),
appendedHead: 0,
avatarID: avatar.id,
avatarID: viewer.avatar.id,
status: 'active',
verifiedHead: 0,
});

const result = await runResync({
avatarID: avatar.id,
avatarID: viewer.avatar.id,
buildSimulationInput: (started) => ({
activity: createMockActivityInput({
encounter: {
Expand Down Expand Up @@ -168,20 +165,19 @@ test('it attaches live when the gap is negligible, leaving the submitter untouch
});

test('it fast-forwards a real offline gap and reports the outcome', async () => {
const user = await db.userCollection.create({});
const avatar = await db.avatarCollection.create({ userID: user.id });
const ctx = await setupTest({ userID: user.id });
const viewer = await createViewer();
const ctx = await setupTest({ userID: viewer.user.id });

await db.activityCollection.create({
appendedAt: new Date(Date.now() - 60_000),
appendedHead: 0,
avatarID: avatar.id,
avatarID: viewer.avatar.id,
status: 'active',
verifiedHead: 0,
});

const result = await runResync({
avatarID: avatar.id,
avatarID: viewer.avatar.id,
buildSimulationInput: (started) => ({
activity: createMockActivityInput({
encounter: {
Expand Down Expand Up @@ -213,14 +209,13 @@ test('it fast-forwards a real offline gap and reports the outcome', async () =>
});

test('it awaits onProgressFetched with the settled progress before planning a fast-forward', async () => {
const user = await db.userCollection.create({});
const avatar = await db.avatarCollection.create({ userID: user.id });
const ctx = await setupTest({ userID: user.id });
const viewer = await createViewer();
const ctx = await setupTest({ userID: viewer.user.id });

const activity = await db.activityCollection.create({
appendedAt: new Date(Date.now() - 60_000),
appendedHead: 0,
avatarID: avatar.id,
avatarID: viewer.avatar.id,
status: 'active',
verifiedHead: 0,
});
Expand All @@ -237,7 +232,7 @@ test('it awaits onProgressFetched with the settled progress before planning a fa
});

const resync = runResync({
avatarID: avatar.id,
avatarID: viewer.avatar.id,
buildSimulationInput: (started) => ({
activity: createMockActivityInput({
encounter: {
Expand Down Expand Up @@ -276,14 +271,13 @@ test('it awaits onProgressFetched with the settled progress before planning a fa
});

test('it delivers checkpoints a previous worker left queued and plans against the head they advanced', async () => {
const user = await db.userCollection.create({});
const avatar = await db.avatarCollection.create({ userID: user.id });
const ctx = await setupTest({ userID: user.id });
const viewer = await createViewer();
const ctx = await setupTest({ userID: viewer.user.id });

const activity = await db.activityCollection.create({
appendedAt: new Date(Date.now() - 60_000),
appendedHead: 5,
avatarID: avatar.id,
avatarID: viewer.avatar.id,
status: 'active',
verifiedHead: 0,
});
Expand All @@ -299,7 +293,7 @@ test('it delivers checkpoints a previous worker left queued and plans against th
);

const result = await runResync({
avatarID: avatar.id,
avatarID: viewer.avatar.id,
buildSimulationInput: (started) => ({
activity: createMockActivityInput({
encounter: {
Expand Down Expand Up @@ -333,14 +327,13 @@ test('it delivers checkpoints a previous worker left queued and plans against th
});

test('it refuses to plan while stranded checkpoints cannot be delivered', async () => {
const user = await db.userCollection.create({});
const avatar = await db.avatarCollection.create({ userID: user.id });
const ctx = await setupTest({ scheduleRetry: () => {}, userID: user.id });
const viewer = await createViewer();
const ctx = await setupTest({ scheduleRetry: () => {}, userID: viewer.user.id });

const activity = await db.activityCollection.create({
appendedAt: new Date(Date.now() - 60_000),
appendedHead: 5,
avatarID: avatar.id,
avatarID: viewer.avatar.id,
status: 'active',
verifiedHead: 0,
});
Expand All @@ -358,7 +351,7 @@ test('it refuses to plan while stranded checkpoints cannot be delivered', async

expect(
runResync({
avatarID: avatar.id,
avatarID: viewer.avatar.id,
buildSimulationInput: (started) => ({
activity: createMockActivityInput({
encounter: {
Expand All @@ -382,20 +375,19 @@ test('it refuses to plan while stranded checkpoints cannot be delivered', async
});

test('it downgrades a fast-forward to attach-live when the activity is already simulating live', async () => {
const user = await db.userCollection.create({});
const avatar = await db.avatarCollection.create({ userID: user.id });
const ctx = await setupTest({ userID: user.id });
const viewer = await createViewer();
const ctx = await setupTest({ userID: viewer.user.id });

const activity = await db.activityCollection.create({
appendedAt: new Date(Date.now() - 60_000),
appendedHead: 3,
avatarID: avatar.id,
avatarID: viewer.avatar.id,
status: 'active',
verifiedHead: 0,
});

const result = await runResync({
avatarID: avatar.id,
avatarID: viewer.avatar.id,
buildSimulationInput: (started) => ({
activity: createMockActivityInput({
encounter: {
Expand All @@ -422,14 +414,13 @@ test('it downgrades a fast-forward to attach-live when the activity is already s
});

test('it leaves a live activity queue untouched instead of draining it', async () => {
const user = await db.userCollection.create({});
const avatar = await db.avatarCollection.create({ userID: user.id });
const ctx = await setupTest({ userID: user.id });
const viewer = await createViewer();
const ctx = await setupTest({ userID: viewer.user.id });

const activity = await db.activityCollection.create({
appendedAt: new Date(Date.now() - 2000),
appendedHead: 5,
avatarID: avatar.id,
avatarID: viewer.avatar.id,
status: 'active',
verifiedHead: 0,
});
Expand All @@ -441,7 +432,7 @@ test('it leaves a live activity queue untouched instead of draining it', async (
);

const result = await runResync({
avatarID: avatar.id,
avatarID: viewer.avatar.id,
buildSimulationInput: (started) => ({
activity: createMockActivityInput({
encounter: {
Expand Down
44 changes: 20 additions & 24 deletions libs/game/idle-client/src/worker/create-worker-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect, onTestFinished, test } from 'bun:test';
import type { ErrorEvent } from '@sentry/browser';
import { createMockActivityData } from '@vers/contract-activity/test-utils';
import { ActivityFailureAction } from '@vers/idle-core';
import { createAuthedServiceClient, resolveServiceURL } from '@vers/mock-services';
import { createAuthedServiceClient, createViewer, resolveServiceURL } from '@vers/mock-services';
import { mockActivityService } from '@vers/mock-services/activity';
import * as db from '@vers/mock-services/db';
import { waitFor } from '@vers/test-utils';
Expand Down Expand Up @@ -63,31 +63,30 @@ test('it seeds the boot state from the device-local failure-action cache before
});

test('it retains the cached dirty flag across boot so the next resync flushes it to the server', async () => {
const user = await db.userCollection.create({});
const avatar = await db.avatarCollection.create({ userID: user.id });
const viewer = await createViewer();

await db.activityCollection.create({
appendedHead: 0,
avatarID: avatar.id,
avatarID: viewer.avatar.id,
startedAt: new Date(),
});

await writeFailureActionCache({
avatarID: avatar.id,
avatarID: viewer.avatar.id,
dirty: true,
failureAction: ActivityFailureAction.Retry,
});

const client = await createAuthedServiceClient<ActivityServiceClient>('activity', user.id);
const client = await createAuthedServiceClient<ActivityServiceClient>('activity', viewer.user.id);

using runtime = createWorkerRuntime({ client });

const connection = createConnection(runtime);

connection.post({ avatarID: avatar.id, type: ClientMessageType.RequestResync });
connection.post({ avatarID: viewer.avatar.id, type: ClientMessageType.RequestResync });

await waitFor(() => {
const updatedAvatar = db.avatarCollection.findFirst((q) => q.where({ id: avatar.id }));
const updatedAvatar = db.avatarCollection.findFirst((q) => q.where({ id: viewer.avatar.id }));

expect(updatedAvatar?.failureAction).toBe('retry');
});
Expand Down Expand Up @@ -185,14 +184,13 @@ test('it reports a fault to the error backend when a message makes its handler t
});

test('it resumes into a fresh row once a same-row CONFLICT resync drains a held terminal append', async () => {
const user = await db.userCollection.create({});
const avatar = await db.avatarCollection.create({ userID: user.id });
const client = await createAuthedServiceClient<ActivityServiceClient>('activity', user.id);
const viewer = await createViewer();
const client = await createAuthedServiceClient<ActivityServiceClient>('activity', viewer.user.id);

// this seed's placeholder encounter completes in exactly 60s of simulated time; the fast clock
// below collapses that wait into a single tick-loop frame
const activity = await db.activityCollection.create({
avatarID: avatar.id,
avatarID: viewer.avatar.id,
encounterNode: { difficulty: 1 },
seed: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaa6072',
});
Expand Down Expand Up @@ -225,7 +223,7 @@ test('it resumes into a fresh row once a same-row CONFLICT resync drains a held
clock.jump(65_000);

const minted = db.activityCollection.findFirst((q) =>
q.where({ avatarID: avatar.id, status: 'active' }),
q.where({ avatarID: viewer.avatar.id, status: 'active' }),
);

invariant(minted !== undefined, 'expected the same-row CONFLICT resync to mint a fresh row');
Expand All @@ -244,14 +242,13 @@ test('it resumes into a fresh row once a same-row CONFLICT resync drains a held
});

test('it resumes into a fresh row once a reconnect drains a held terminal append behind an offline continuation', async () => {
const user = await db.userCollection.create({});
const avatar = await db.avatarCollection.create({ userID: user.id });
const client = await createAuthedServiceClient<ActivityServiceClient>('activity', user.id);
const viewer = await createViewer();
const client = await createAuthedServiceClient<ActivityServiceClient>('activity', viewer.user.id);

// this seed's placeholder encounter completes in exactly 60s of simulated time; the fast clock
// below collapses that wait into a single tick-loop frame
const activity = await db.activityCollection.create({
avatarID: avatar.id,
avatarID: viewer.avatar.id,
encounterNode: { difficulty: 1 },
seed: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaa6072',
});
Expand All @@ -265,7 +262,7 @@ test('it resumes into a fresh row once a reconnect drains a held terminal append
),
http.post(
`${resolveServiceURL('activity')}/rpc/startActivity`,
makeFailFirstMatchHandler((input) => input['avatarID'] === avatar.id),
makeFailFirstMatchHandler((input) => input['avatarID'] === viewer.avatar.id),
),
);

Expand Down Expand Up @@ -302,7 +299,7 @@ test('it resumes into a fresh row once a reconnect drains a held terminal append

await waitFor(() => {
const minted = db.activityCollection.findFirst((q) =>
q.where({ avatarID: avatar.id, status: 'active' }),
q.where({ avatarID: viewer.avatar.id, status: 'active' }),
);

invariant(minted !== undefined, 'expected the reconnect resync to mint a fresh row');
Expand All @@ -316,10 +313,9 @@ test('it resumes into a fresh row once a reconnect drains a held terminal append
});

test("it resumes the pending continuation's avatar on reconnect over an earlier avatar it resynced", async () => {
const user = await db.userCollection.create({});
const earlierAvatar = await db.avatarCollection.create({ userID: user.id });
const avatar = await db.avatarCollection.create({ userID: user.id });
const client = await createAuthedServiceClient<ActivityServiceClient>('activity', user.id);
const viewer = await createViewer();
const avatar = await db.avatarCollection.create({ userID: viewer.user.id });
const client = await createAuthedServiceClient<ActivityServiceClient>('activity', viewer.user.id);

// this seed's placeholder encounter completes in exactly 60s of simulated time; the fast clock
// below collapses that wait into a single tick-loop frame
Expand Down Expand Up @@ -354,7 +350,7 @@ test("it resumes the pending continuation's avatar on reconnect over an earlier

// the worker remembers this history-free avatar as its last resync target; the boundary failure
// below must outrank that memory on reconnect or the pending continuation strands
connection.post({ avatarID: earlierAvatar.id, type: ClientMessageType.RequestResync });
connection.post({ avatarID: viewer.avatar.id, type: ClientMessageType.RequestResync });
connection.post({ activity, type: ClientMessageType.SetActivity });

await waitFor(
Expand Down
Loading