Slug-based scenarios, quests and offers per the environments contract; regenerated models; e2e seed passes environment to admin mirrors

Claude-Session: https://claude.ai/code/session_01SMCvdwDmuxoaqGgvGBLk1V
This commit is contained in:
edmand46
2026-09-06 21:44:54 +03:00
parent e863cc3bf7
commit a7401b534a
23 changed files with 127 additions and 115 deletions
+8 -8
View File
@@ -24,7 +24,7 @@ describe('E2E: boundary nodes (server-side continuation)', () => {
stores: [{
slug: 'starter',
name: 'Starter',
offers: [{ id: 'pack_1', name: 'Starter Pack' }],
offers: [{ id: 'pack_1', slug: 'pack-1', name: 'Starter Pack' }],
}],
});
gateway.install();
@@ -35,12 +35,12 @@ describe('E2E: boundary nodes (server-side continuation)', () => {
it('purchase crosses the boundary → callback fires and continues the scenario', async () => {
const offerEffect = effect('store', { storeSlug: 'starter', message: 'Buy the starter pack!' }, {
scenarioId: 'offer_flow',
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'offer',
});
const reward = effect('notification', { message: 'Reward granted: 500 gems!' }, {
scenarioId: 'offer_flow',
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'reward',
});
@@ -52,7 +52,7 @@ describe('E2E: boundary nodes (server-side continuation)', () => {
let offer: StoreOfferEffect | undefined;
client.effects.onStoreOffer((e) => { offer = e; });
client.effects.onNotification((e) => { messages.push(e.message); });
client.effects.onScenarioCompleted((e) => { completed.push(e.scenarioId); });
client.effects.onScenarioCompleted((e) => { completed.push(e.scenarioSlug); });
const token = (await client.auth.loginWithDevice()).accessToken;
await vi.waitFor(() => expect(offer).toBeDefined());
@@ -61,7 +61,7 @@ describe('E2E: boundary nodes (server-side continuation)', () => {
const callback = gateway.recorded.find((r) => r.path === '/sdk/v1/scenarios/callback');
expect(callback?.body).toEqual({
scenarioId: 'offer_flow',
scenarioSlug: 'offer_flow',
nodeId: 'offer',
handle: 'onPurchase',
runId: 'offer_flow-run',
@@ -75,12 +75,12 @@ describe('E2E: boundary nodes (server-side continuation)', () => {
it('decline posts callback and continues with the server-supplied next effect', async () => {
const offerEffect = effect('store', { storeSlug: 'starter', message: 'Buy the starter pack!' }, {
scenarioId: 'offer_flow',
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'offer',
});
const consolation = effect('notification', { message: 'Maybe next time!' }, {
scenarioId: 'offer_flow',
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'consolation',
});
@@ -102,7 +102,7 @@ describe('E2E: boundary nodes (server-side continuation)', () => {
it('a failing callback does not fail or crash the run', async () => {
const offerEffect = effect('store', { storeSlug: 'starter' }, {
scenarioId: 'offer_flow',
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'offer',
});
+8 -8
View File
@@ -18,29 +18,29 @@ function makeClient(): RudderClient {
function offerScenario(now: number) {
const waitIntro = effect('wait', {}, {
scenarioId: 'offer_flow',
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'wait_intro',
waitDeadline: new Date(now + MINUTE).toISOString(),
});
const offer = effect('store', { storeSlug: 'starter', offerId: 'pack_1', message: 'Limited starter pack!' }, {
scenarioId: 'offer_flow',
const offer = effect('store', { storeSlug: 'starter', offerSlug: 'pack-1', message: 'Limited starter pack!' }, {
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'offer',
});
const waitReminder = effect('wait', {}, {
scenarioId: 'offer_flow',
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'wait_reminder',
waitDeadline: new Date(now + 2 * MINUTE).toISOString(),
});
const reminder = effect('notification', { message: 'Your offer is still available!' }, {
scenarioId: 'offer_flow',
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'reminder',
});
const thanks = effect('notification', { message: 'Thanks for your purchase!' }, {
scenarioId: 'offer_flow',
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'thanks',
});
@@ -58,7 +58,7 @@ describe('E2E: player offer journey', () => {
stores: [{
slug: 'starter',
name: 'Starter',
offers: [{ id: 'pack_1', name: 'Starter Pack' }],
offers: [{ id: 'pack_1', slug: 'pack-1', name: 'Starter Pack' }],
}],
});
gateway.install();
@@ -139,7 +139,7 @@ describe('E2E: player offer journey', () => {
expect(purchase.success).toBe(true);
expect(gateway.purchases).toEqual([
{ storeSlug: 'starter', offerId: 'pack_1', idempotencyKey: 'idem-key-123', authToken: accessToken },
{ storeSlug: 'starter', offerSlug: 'pack-1', idempotencyKey: 'idem-key-123', authToken: accessToken },
]);
expect(notifications).toHaveLength(1);
+3 -3
View File
@@ -24,17 +24,17 @@ describe('E2E: pending effects resume after a new login', () => {
stubDeterministicUuid();
vi.useFakeTimers();
gateway = createFakeGateway({
stores: [{ slug: 'starter', name: 'Starter', offers: [{ id: 'pack_1', name: 'Pack' }] }],
stores: [{ slug: 'starter', name: 'Starter', offers: [{ id: 'pack_1', slug: 'pack-1', name: 'Pack' }] }],
});
const now = Date.now();
const wait = effect('wait', {}, {
scenarioId: 'offer_flow',
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'wait_intro',
waitDeadline: new Date(now + MINUTE).toISOString(),
});
const offer = effect('store', { message: 'Limited starter pack!', storeSlug: 'starter' }, {
scenarioId: 'offer_flow',
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'offer',
});