From 035c9d8792a57f9bd8daf61d89232131ee57edfe Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Sun, 26 May 2024 13:28:15 -0400 Subject: [PATCH 01/21] create kitchen sync for actions --- .../core/test/kitchen-sync.actions.test.ts | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 packages/core/test/kitchen-sync.actions.test.ts diff --git a/packages/core/test/kitchen-sync.actions.test.ts b/packages/core/test/kitchen-sync.actions.test.ts new file mode 100644 index 0000000000..8bde5f9c17 --- /dev/null +++ b/packages/core/test/kitchen-sync.actions.test.ts @@ -0,0 +1,171 @@ +import { + ActorRefFrom, + and, + assign, + cancel, + createActor, + createMachine, + enqueueActions, + fromPromise, + fromTransition, + log, + not, + raise, + sendParent, + sendTo, + setup, + spawnChild, + stopChild +} from '../src'; + +describe('Consolidated Actions Setup', () => { + const childMachine = createMachine({ + id: 'child', + initial: 'active', + states: { + active: { + on: { + FINISH: 'done' + } + }, + done: { + type: 'final' + } + }, + exit: sendParent({ type: 'CHILD_DONE' }) + }); + + const machine = setup({ + types: {} as { + context: { + count: number; + enabled: boolean; + data?: any; + child?: ActorRefFrom; + }; + events: + | { type: 'FOO' } + | { type: 'BAR' } + | { type: 'NEXT' } + | { type: 'SOMETHING' } + | { type: 'FINISH_CHILD' } + | { type: 'CHILD_DONE' } + | { type: 'SOMETHING_ELSE' }; + children: { + myChild: 'child'; + fetchUserChild: 'fetchUser'; + }; + meta: { layout: string }; + }, + guards: { + check: () => true, + checkStuff: () => true, + checkWithParams: (_: any, params: number) => true, + checkContext: ({ context }: any) => context.enabled, + opposite: not('check'), + combinedCheck: and([ + { type: 'check' }, + { type: 'checkWithParams', params: 42 } // Ensure the correct parameter type is provided + ]) + }, + actions: { + resetTo: assign((_, params: number) => ({ + count: params + })), + spawnFetcher: assign(({ spawn }) => ({ + data: { + child: spawn(childMachine) + } + })), + raiseFoo: raise({ type: 'FOO' }), + sendFoo: sendTo( + ({ self }) => self, + { type: 'FOO' }, + // Why can't we use 'hundred' here? + { delay: 100 } + ), + sendParentFoo: sendParent({ type: 'FOO' }), + enqueueSomething: enqueueActions(({ enqueue }) => { + enqueue.raise({ type: 'SOMETHING_ELSE' }); + }), + writeDown: log('foo'), + revert: cancel('foo'), + releaseFromDuty: stopChild('foo') + }, + actors: { + fetchUser: fromPromise( + async ({ input }: { input: { userId: string } }) => ({ + id: input.userId, + name: 'Andarist' + }) + ), + greet: fromPromise(async () => 'hello'), + throwDice: fromPromise(async () => Math.random()), + reducer: fromTransition((s) => s, { count: 42 }), + child: childMachine + }, + delays: { + hundred: 100 + } + }).createMachine({ + context: ({ spawn }) => ({ + count: 0, + enabled: true, + child: spawn(childMachine) + }), + initial: 'a', + states: { + a: { + meta: { layout: 'a-layout' }, + after: { + hundred: 'b' + }, + on: { + NEXT: { + guard: 'checkStuff', + target: 'b' + }, + SOMETHING: { + actions: 'enqueueSomething' + }, + FINISH_CHILD: { + actions: [ + ({ context }) => { + if (context.child) { + return sendTo(() => context.child!, { type: 'FINISH' }); + } + } + ] + }, + CHILD_DONE: { + actions: () => { + // Handle child done + } + } + } + }, + b: { + meta: { layout: 'b-layout' } + }, + c: {}, + d: {} + }, + on: { + e1: { + meta: { layout: 'event-layout' } + } + } + }); + + const actor = createActor(machine); + + it('should setup a comprehensive machine with all functionalities', () => { + const snapshot = actor.start().getSnapshot(); + expect(snapshot.context.count).toBe(0); + expect(snapshot.context.enabled).toBe(true); + + // Additional assertions to verify other functionalities + }); + + // Additional tests to verify specific functionalities +}); From a53652ce86b1341b7adc317f0a92b10831a6c235 Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Sun, 26 May 2024 13:29:19 -0400 Subject: [PATCH 02/21] cleanup --- packages/core/test/kitchen-sync.actions.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/core/test/kitchen-sync.actions.test.ts b/packages/core/test/kitchen-sync.actions.test.ts index 8bde5f9c17..a856dd6468 100644 --- a/packages/core/test/kitchen-sync.actions.test.ts +++ b/packages/core/test/kitchen-sync.actions.test.ts @@ -50,6 +50,7 @@ describe('Consolidated Actions Setup', () => { | { type: 'SOMETHING' } | { type: 'FINISH_CHILD' } | { type: 'CHILD_DONE' } + | { type: 'e1' } | { type: 'SOMETHING_ELSE' }; children: { myChild: 'child'; @@ -149,11 +150,6 @@ describe('Consolidated Actions Setup', () => { }, c: {}, d: {} - }, - on: { - e1: { - meta: { layout: 'event-layout' } - } } }); From 7624d44228fc23620ade48aa6e544e308ac9cd06 Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Sun, 26 May 2024 13:44:24 -0400 Subject: [PATCH 03/21] Fix kitchen sync actions --- .../core/test/kitchen-sync.actions.test.ts | 55 ++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/packages/core/test/kitchen-sync.actions.test.ts b/packages/core/test/kitchen-sync.actions.test.ts index a856dd6468..41429a7db2 100644 --- a/packages/core/test/kitchen-sync.actions.test.ts +++ b/packages/core/test/kitchen-sync.actions.test.ts @@ -115,6 +115,11 @@ describe('Consolidated Actions Setup', () => { child: spawn(childMachine) }), initial: 'a', + on: { + SOMETHING_ELSE: { + actions: 'spawnFetcher' + } + }, states: { a: { meta: { layout: 'a-layout' }, @@ -126,9 +131,6 @@ describe('Consolidated Actions Setup', () => { guard: 'checkStuff', target: 'b' }, - SOMETHING: { - actions: 'enqueueSomething' - }, FINISH_CHILD: { actions: [ ({ context }) => { @@ -146,7 +148,13 @@ describe('Consolidated Actions Setup', () => { } }, b: { - meta: { layout: 'b-layout' } + meta: { layout: 'b-layout' }, + + on: { + SOMETHING: { + actions: 'enqueueSomething' + } + } }, c: {}, d: {} @@ -155,12 +163,45 @@ describe('Consolidated Actions Setup', () => { const actor = createActor(machine); - it('should setup a comprehensive machine with all functionalities', () => { - const snapshot = actor.start().getSnapshot(); + it('should setup a comprehensive machine with all functionalities', (done) => { + actor.start(); + let snapshot = actor.getSnapshot(); + + expect(snapshot.context.count).toBe(0); + expect(snapshot.context.enabled).toBe(true); + + // Verify the initial state and context + expect(snapshot.value).toEqual('a'); expect(snapshot.context.count).toBe(0); expect(snapshot.context.enabled).toBe(true); - // Additional assertions to verify other functionalities + // Send NEXT event and verify transition to state 'b' + actor.send({ type: 'NEXT' }); + snapshot = actor.getSnapshot(); + expect(snapshot.value).toEqual('b'); + + // Send SOMETHING event and verify action is called + actor.send({ type: 'SOMETHING' }); + snapshot = actor.getSnapshot(); + expect(snapshot.context.data).toBeDefined(); + + // Verify sending FINISH_CHILD event to child + actor.send({ type: 'FINISH_CHILD' }); + snapshot = actor.getSnapshot(); + expect(snapshot.context.child).toBeDefined(); + + // Verify the child has transitioned to 'done' and sent 'CHILD_DONE' to parent + setTimeout(() => { + snapshot = actor.getSnapshot(); + // Handle child done verification + + // Delay verification to ensure after transition works + setTimeout(() => { + snapshot = actor.getSnapshot(); + expect(snapshot.value).toEqual('b'); + done(); + }, 150); + }, 100); }); // Additional tests to verify specific functionalities From ab19c1d60054499a260a4be901f5c28c0dc8f6eb Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Sun, 26 May 2024 13:46:52 -0400 Subject: [PATCH 04/21] simulate clock --- .../core/test/kitchen-sync.actions.test.ts | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/packages/core/test/kitchen-sync.actions.test.ts b/packages/core/test/kitchen-sync.actions.test.ts index 41429a7db2..cdbbc7d2dc 100644 --- a/packages/core/test/kitchen-sync.actions.test.ts +++ b/packages/core/test/kitchen-sync.actions.test.ts @@ -14,8 +14,8 @@ import { sendParent, sendTo, setup, - spawnChild, - stopChild + stopChild, + SimulatedClock } from '../src'; describe('Consolidated Actions Setup', () => { @@ -161,9 +161,10 @@ describe('Consolidated Actions Setup', () => { } }); - const actor = createActor(machine); + it('should setup a comprehensive machine with all functionalities', () => { + const clock = new SimulatedClock(); + const actor = createActor(machine, { clock }); - it('should setup a comprehensive machine with all functionalities', (done) => { actor.start(); let snapshot = actor.getSnapshot(); @@ -185,24 +186,13 @@ describe('Consolidated Actions Setup', () => { snapshot = actor.getSnapshot(); expect(snapshot.context.data).toBeDefined(); - // Verify sending FINISH_CHILD event to child actor.send({ type: 'FINISH_CHILD' }); snapshot = actor.getSnapshot(); expect(snapshot.context.child).toBeDefined(); - // Verify the child has transitioned to 'done' and sent 'CHILD_DONE' to parent - setTimeout(() => { - snapshot = actor.getSnapshot(); - // Handle child done verification - - // Delay verification to ensure after transition works - setTimeout(() => { - snapshot = actor.getSnapshot(); - expect(snapshot.value).toEqual('b'); - done(); - }, 150); - }, 100); + // Increment the clock to simulate delay and verify state transition after delay + clock.increment(100); + snapshot = actor.getSnapshot(); + expect(snapshot.value).toEqual('b'); }); - - // Additional tests to verify specific functionalities }); From 7dd2a6ee855186761ea6684209df8fde56bb5ecb Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Sun, 26 May 2024 14:03:09 -0400 Subject: [PATCH 05/21] actor kitchen sink --- packages/core/test/kitcehn-sink.actor.test.ts | 250 ++++++++++++++++++ .../core/test/kitchen-sync.actions.test.ts | 1 - 2 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 packages/core/test/kitcehn-sink.actor.test.ts diff --git a/packages/core/test/kitcehn-sink.actor.test.ts b/packages/core/test/kitcehn-sink.actor.test.ts new file mode 100644 index 0000000000..4777d2297c --- /dev/null +++ b/packages/core/test/kitcehn-sink.actor.test.ts @@ -0,0 +1,250 @@ +import { interval, of } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { sendParent } from '../src/actions.ts'; +import { assign } from '../src/actions/assign'; +import { raise } from '../src/actions/raise'; +import { sendTo } from '../src/actions/send'; +import { CallbackActorRef, fromCallback } from '../src/actors/callback.ts'; +import { + fromEventObservable, + fromObservable +} from '../src/actors/observable.ts'; +import { + PromiseActorLogic, + PromiseActorRef, + fromPromise +} from '../src/actors/promise.ts'; +import { + ActorLogic, + ActorRef, + ActorRefFrom, + AnyActorRef, + createActor, + createMachine +} from '../src/index.ts'; + +describe('composite state machine', () => { + type CompositeEvent = + | { type: 'PING' } + | { type: 'PONG' } + | { type: 'START_CB' } + | { type: 'SEND_BACK' } + | { type: 'GREET' } + | { type: 'ACTIVATE' } + | { type: 'SUCCESS' } + | { type: 'SET_COMPLETE'; id: number } + | { type: 'COUNT'; val: number }; + + interface CompositeContext { + server?: ActorRef; + promiseRef?: PromiseActorRef; + callbackRef?: CallbackActorRef<{ type: 'START' }>; + observableRef?: AnyActorRef; + eventObservableRef?: AnyActorRef; + childRef?: ActorRef; + parent: AnyActorRef; + } + + const serverMachine = createMachine({ + types: {} as { + events: CompositeEvent; + }, + id: 'server', + initial: 'waitPing', + states: { + waitPing: { + on: { + PING: 'sendPong' + } + }, + sendPong: { + entry: [sendParent({ type: 'PONG' }), raise({ type: 'SUCCESS' })], + on: { + SUCCESS: 'waitPing' + } + } + } + }); + + const childMachine = createMachine({ + types: {} as { + context: { parent: AnyActorRef }; + input: { parent: AnyActorRef }; + }, + context: ({ input }) => ({ + parent: input.parent + }), + entry: sendTo(({ context }) => context.parent, { type: 'GREET' }) + }); + + const promiseLogic: PromiseActorLogic = { + transition: (state, event, { self }) => { + if (event.type === 'PING') { + self._parent?.send({ type: 'PONG' }); + } + return state; + }, + getInitialSnapshot: (input) => ({ + status: 'active', + output: undefined, + error: undefined, + input + }), + getPersistedSnapshot: (s) => s + }; + + const compositeMachine = createMachine({ + types: {} as { context: CompositeContext; events: CompositeEvent }, + id: 'composite', + initial: 'init', + context: ({ self }) => ({ + server: undefined, + promiseRef: undefined, + callbackRef: undefined, + observableRef: undefined, + eventObservableRef: undefined, + childRef: undefined, + parent: self + }), + states: { + init: { + entry: [ + assign({ + server: ({ spawn }) => spawn(serverMachine), + promiseRef: ({ spawn }) => + spawn( + fromPromise(() => Promise.resolve('response')), + { id: 'my-promise' } + ), + callbackRef: ({ spawn }) => + spawn( + fromCallback(({ sendBack, receive }) => { + receive((event) => { + if (event.type === 'START') { + setTimeout(() => { + sendBack({ type: 'SEND_BACK' }); + }, 10); + } + }); + }) + ), + observableRef: ({ spawn }) => + spawn(fromObservable(() => interval(10))), + eventObservableRef: ({ spawn }) => + spawn( + fromEventObservable(() => + interval(10).pipe(map((val) => ({ type: 'COUNT', val }))) + ) + ), + childRef: ({ spawn, context }) => + spawn(childMachine, { input: { parent: context.parent } }) + }), + raise({ type: 'SUCCESS' }) + ], + on: { + SUCCESS: 'active' + } + }, + active: { + on: { + PING: { + actions: sendTo(({ context }) => context.server!, { type: 'PING' }) + }, + START_CB: { + actions: sendTo(({ context }) => context.callbackRef!, { + type: 'START' + }) + }, + SET_COMPLETE: { + actions: sendTo(({ context }) => context.server!, { + type: 'SET_COMPLETE', + id: 5 + }) + }, + COUNT: { + target: 'success', + guard: ({ event }) => event.val === 5 + }, + GREET: 'success', + SEND_BACK: 'success' + } + }, + success: { + type: 'final' + } + } + }); + + it('should work as a composite state machine', (done) => { + const actor = createActor(compositeMachine); + actor.subscribe({ + complete: () => { + done(); + } + }); + + actor.start(); + actor.send({ type: 'PING' }); + actor.send({ type: 'START_CB' }); + actor.send({ type: 'SET_COMPLETE', id: 42 }); + }); + + it('should transition to success state on receiving GREET from child', (done) => { + const actor = createActor(compositeMachine); + actor.subscribe({ + complete: () => { + done(); + } + }); + + actor.start(); + }); + + it('should transition to success state on receiving COUNT with val 5', (done) => { + const actor = createActor(compositeMachine); + actor.subscribe({ + complete: () => { + done(); + } + }); + + actor.start(); + }); + + it('should transition to success state on receiving SEND_BACK from callback', (done) => { + const actor = createActor(compositeMachine); + actor.subscribe({ + complete: () => { + done(); + } + }); + + actor.start(); + actor.send({ type: 'START_CB' }); + }); + + it('should handle multiple PING events correctly', (done) => { + const actor = createActor(compositeMachine); + actor.subscribe({ + complete: () => { + done(); + } + }); + + actor.start(); + actor.send({ type: 'PING' }); + actor.send({ type: 'PING' }); + actor.send({ type: 'PING' }); + }); + + it('should transition to success state on promise resolution', (done) => { + const actor = createActor(compositeMachine); + actor.subscribe({ + complete: () => { + done(); + } + }); + + actor.start(); + }); +}); diff --git a/packages/core/test/kitchen-sync.actions.test.ts b/packages/core/test/kitchen-sync.actions.test.ts index cdbbc7d2dc..755defaffa 100644 --- a/packages/core/test/kitchen-sync.actions.test.ts +++ b/packages/core/test/kitchen-sync.actions.test.ts @@ -62,7 +62,6 @@ describe('Consolidated Actions Setup', () => { check: () => true, checkStuff: () => true, checkWithParams: (_: any, params: number) => true, - checkContext: ({ context }: any) => context.enabled, opposite: not('check'), combinedCheck: and([ { type: 'check' }, From 867a7f9cf3f7722114705e33468decb768c9e2e9 Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Tue, 28 May 2024 06:22:16 -0400 Subject: [PATCH 06/21] Update kitchen-sink.actor.test.ts --- ...tor.test.ts => kitchen-sink.actor.test.ts} | 77 ++++--------------- 1 file changed, 16 insertions(+), 61 deletions(-) rename packages/core/test/{kitcehn-sink.actor.test.ts => kitchen-sink.actor.test.ts} (77%) diff --git a/packages/core/test/kitcehn-sink.actor.test.ts b/packages/core/test/kitchen-sink.actor.test.ts similarity index 77% rename from packages/core/test/kitcehn-sink.actor.test.ts rename to packages/core/test/kitchen-sink.actor.test.ts index 4777d2297c..a0e78d53ac 100644 --- a/packages/core/test/kitcehn-sink.actor.test.ts +++ b/packages/core/test/kitchen-sink.actor.test.ts @@ -177,71 +177,26 @@ describe('composite state machine', () => { it('should work as a composite state machine', (done) => { const actor = createActor(compositeMachine); - actor.subscribe({ - complete: () => { - done(); - } - }); - - actor.start(); - actor.send({ type: 'PING' }); - actor.send({ type: 'START_CB' }); - actor.send({ type: 'SET_COMPLETE', id: 42 }); - }); - - it('should transition to success state on receiving GREET from child', (done) => { - const actor = createActor(compositeMachine); - actor.subscribe({ - complete: () => { - done(); - } - }); + let eventsSent = 0; - actor.start(); - }); - - it('should transition to success state on receiving COUNT with val 5', (done) => { - const actor = createActor(compositeMachine); - actor.subscribe({ - complete: () => { - done(); - } - }); - - actor.start(); - }); - - it('should transition to success state on receiving SEND_BACK from callback', (done) => { - const actor = createActor(compositeMachine); - actor.subscribe({ - complete: () => { - done(); - } - }); - - actor.start(); - actor.send({ type: 'START_CB' }); - }); - - it('should handle multiple PING events correctly', (done) => { - const actor = createActor(compositeMachine); - actor.subscribe({ - complete: () => { - done(); - } - }); - - actor.start(); - actor.send({ type: 'PING' }); - actor.send({ type: 'PING' }); - actor.send({ type: 'PING' }); - }); - - it('should transition to success state on promise resolution', (done) => { - const actor = createActor(compositeMachine); actor.subscribe({ complete: () => { done(); + }, + next: (state) => { + if (state.matches('active')) { + // Send events sequentially + if (eventsSent === 0) { + actor.send({ type: 'PING' }); + } else if (eventsSent === 1) { + actor.send({ type: 'START_CB' }); + } else if (eventsSent === 2) { + actor.send({ type: 'SET_COMPLETE', id: 42 }); + } else if (eventsSent === 3) { + actor.send({ type: 'COUNT', val: 5 }); + } + eventsSent++; + } } }); From 44d509eae78b288ee6d54f3cabb613b45f014ad3 Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Tue, 28 May 2024 07:09:36 -0400 Subject: [PATCH 07/21] guards --- gpt/prompt.md | 159 +++++++++++ packages/core/test/kitchen-sink.actor.test.ts | 7 + packages/core/test/kitchen-sink.guard.test.ts | 262 ++++++++++++++++++ 3 files changed, 428 insertions(+) create mode 100644 gpt/prompt.md create mode 100644 packages/core/test/kitchen-sink.guard.test.ts diff --git a/gpt/prompt.md b/gpt/prompt.md new file mode 100644 index 0000000000..af3941baa6 --- /dev/null +++ b/gpt/prompt.md @@ -0,0 +1,159 @@ +You have access to extensive training data on XState version 4. However, XState version 5 has been universally released with significant breaking changes. Follow these guidelines for XState version 5 unless explicitly stated otherwise by the user: + +### XState and TypeScript Integration + +#### Comprehensive Example + +The following example demonstrates how all the functionality within XState v5 works, including setting up actions, using dynamic parameters, specifying types, and defining context and event types externally. + +##### Action Functions + +Define action functions separately: + +```typescript +function track(_: any, params: { response: string }) { + console.log(`Tracking response: ${params.response}`); +} + +function increment(_: any, params: { value: number }) { + console.log(`Incrementing by: ${params.value}`); +} + +function logInitialRating(_: any, params: { initialRating: number }) { + console.log(`Initial rating: ${params.initialRating}`); +} + +function greet(_: any, params: { name: string }) { + console.log(`Hello, ${params.name}!`); +} +``` + +##### Initial Context + +Define the initial context as a constant: + +```typescript +type FeedbackMachineContext = { + feedback: string; + initialRating: number; + user: { name: string }; +}; + +const InitialContext: FeedbackMachineContext = { + feedback: '', + initialRating: 3, + user: { name: 'David' } +}; +``` + +##### State Machine Setup + +Set up the state machine with strongly typed context and events, including the actions inline: + +```typescript +import { setup, type ActionFunction } from 'xstate'; + +// Define event types +type FeedbackMachineEvents = + | { type: 'feedback.good' } + | { type: 'feedback.bad' }; + +// Machine setup with strongly typed context and events +const feedbackMachine = setup({ + types: { + context: {} as FeedbackMachineContext, + events: {} as FeedbackMachineEvents + }, + actions: { + track, + increment, + logInitialRating, + greet + } +}).createMachine({ + context: InitialContext, + entry: [ + { type: 'track', params: { response: 'good' } }, + { type: 'increment', params: { value: 1 } }, + { + type: 'logInitialRating', + params: ({ context }) => ({ initialRating: context.initialRating }) + }, + { + type: 'greet', + params: ({ context }) => ({ name: context.user.name }) + } + ], + states: { + question: { + on: { + 'feedback.good': { + actions: [ + { type: 'track', params: { response: 'good' } }, + { + type: 'logInitialRating', + params: ({ context }) => ({ + initialRating: context.initialRating + }) + }, + { + type: 'greet', + params: ({ context }) => ({ name: context.user.name }) + } + ] + } + } + } + } +}); +``` + +#### Type Helpers + +Utilize type helpers provided by XState for strongly-typed references, snapshots, and events: + +```typescript +import { + type ActorRefFrom, + type SnapshotFrom, + type EventFromLogic +} from 'xstate'; +import { someMachine } from './someMachine'; + +// Strongly-typed actor reference +type SomeActorRef = ActorRefFrom; + +// Strongly-typed snapshot +type SomeSnapshot = SnapshotFrom; + +// Union of all event types +type SomeEvent = EventFromLogic; +``` + +#### Cheat Sheet: Provide Implementations + +```typescript +import { createMachine } from 'xstate'; +import { someMachine } from './someMachine'; + +const machineWithImpls = someMachine.provide({ + actions: { + /* ... */ + }, + actors: { + /* ... */ + }, + guards: { + /* ... */ + }, + delays: { + /* ... */ + } +}); +``` + +### Additional Instructions + +If you are unaware of how a specific piece of functionality works, ask for more documentation, specifying exactly what you are curious about. + +Always write test-driven development (TDD) code. Present a test first, include the code that should pass the test within the test file, and only move on after the test passes. This ensures the code remains simple and refactorable. diff --git a/packages/core/test/kitchen-sink.actor.test.ts b/packages/core/test/kitchen-sink.actor.test.ts index a0e78d53ac..c07941ffc3 100644 --- a/packages/core/test/kitchen-sink.actor.test.ts +++ b/packages/core/test/kitchen-sink.actor.test.ts @@ -184,15 +184,22 @@ describe('composite state machine', () => { done(); }, next: (state) => { + // Log state transitions + console.log('State:', state.value); + if (state.matches('active')) { // Send events sequentially if (eventsSent === 0) { + console.log('Sending PING event'); actor.send({ type: 'PING' }); } else if (eventsSent === 1) { + console.log('Sending START_CB event'); actor.send({ type: 'START_CB' }); } else if (eventsSent === 2) { + console.log('Sending SET_COMPLETE event'); actor.send({ type: 'SET_COMPLETE', id: 42 }); } else if (eventsSent === 3) { + console.log('Sending COUNT event'); actor.send({ type: 'COUNT', val: 5 }); } eventsSent++; diff --git a/packages/core/test/kitchen-sink.guard.test.ts b/packages/core/test/kitchen-sink.guard.test.ts new file mode 100644 index 0000000000..5042961e35 --- /dev/null +++ b/packages/core/test/kitchen-sink.guard.test.ts @@ -0,0 +1,262 @@ +import { createActor, createMachine, setup } from '../src/index.ts'; +import { and, not, or, stateIn } from '../src/guards'; + +describe('comprehensive guard conditions', () => { + interface MachineContext { + elapsed: number; + count: number; + } + type MachineEvents = + | { type: 'TIMER'; elapsed: number } + | { type: 'EMERGENCY'; isEmergency?: boolean } + | { type: 'EVENT'; value: number } + | { type: 'TIMER_COND_OBJ' } + | { type: 'BAD_COND' }; + + const machine = setup({ + types: {} as { + input: { elapsed?: number; count?: number }; + context: MachineContext; + events: MachineEvents; + }, + guards: { + minTimeElapsed: ({ context: { elapsed } }) => + elapsed >= 100 && elapsed < 200, + custom: ( + _: any, + params: { + compare: number; + op: string; + value: number; + prop: number; + } + ) => { + const { prop, compare, op, value } = params; + if (op === 'greaterThan') { + return prop + value > compare; + } + return false; + } + } + }).createMachine({ + context: ({ input = {} }) => ({ + elapsed: input.elapsed ?? 0, + count: input.count ?? 0 + }), + initial: 'green', + states: { + green: { + on: { + TIMER: [ + { + target: 'green', + guard: ({ context: { elapsed } }) => elapsed < 100 + }, + { + target: 'yellow', + guard: ({ context: { elapsed } }) => + elapsed >= 100 && elapsed < 200 + } + ], + EMERGENCY: { + target: 'red', + guard: ({ event }) => !!event.isEmergency + } + } + }, + yellow: { + on: { + TIMER: { + target: 'red', + guard: 'minTimeElapsed' + }, + TIMER_COND_OBJ: { + target: 'red', + guard: { + type: 'minTimeElapsed' + } + } + } + }, + red: { + type: 'final' + }, + active: { + on: { + EVENT: { + target: 'inactive', + guard: { + type: 'custom', + params: ({ context, event }) => ({ + prop: context.count, + op: 'greaterThan', + compare: 3, + value: event.value + }) + } + } + } + }, + inactive: {} + } + }); + + it('should transition only if condition is met', () => { + const actorRef1 = createActor(machine, { input: { elapsed: 50 } }).start(); + actorRef1.send({ type: 'TIMER', elapsed: 50 }); + expect(actorRef1.getSnapshot().value).toEqual('green'); + + const actorRef2 = createActor(machine, { input: { elapsed: 120 } }).start(); + actorRef2.send({ type: 'TIMER', elapsed: 120 }); + expect(actorRef2.getSnapshot().value).toEqual('yellow'); + }); +}); + +/* + +Guards +A guard is a condition function that the machine checks when it goes through an event. If the condition is true, the machine follows the transition to the next state. If the condition is false, the machine follows the rest of the conditions to the next state. + +A guarded transition is a transition that is enabled only if its guard evaluates to true. The guard determines whether or not the transition can be enabled. Any transition can be a guarded transition. + +You can easily visualize and simulate guarded transitions in Stately’s editor. Read more about guards in Stately’s editor. + +Guards should be pure, synchronous functions that return either true or false. + +const feedbackMachine = createMachine( + { + // ... + states: { + form: { + on: { + 'feedback.submit': { + guard: 'isValid', + target: 'submitting', + }, + }, + }, + submitting: { + // ... + }, + }, + }, + { + guards: { + isValid: ({ context }) => { + return context.feedback.length > 0; + }, + }, + }, +); + + +Multiple guarded transitions +If you want to have a single event transition to different states in certain situations, you can supply an array of guarded transitions. Each transition will be tested in order, and the first transition whose guard evaluates to true will be taken. + +You can specify a default transition to be taken as the last transition in the array. If none of the guards evaluate to true, the default transition will be taken. + +const feedbackMachine = createMachine({ + // ... + prompt: { + on: { + 'feedback.provide': [ + // Taken if 'sentimentGood' guard evaluates to `true` + { + guard: 'sentimentGood', + target: 'thanks', + }, + // Taken if none of the above guarded transitions are taken + // and if 'sentimentBad' guard evaluates to `true` + { + guard: 'sentimentBad', + target: 'form', + }, + // Default transition + { target: 'form' }, + ], + }, + }, +}); + + +Guard object +A guard can be defined as an object with a type, which is the type of guard that references the provided guard implementation, and optional params, which can be read by the implemented guard: + +const feedbackMachine = createMachine( + { + // ... + states: { + // ... + form: { + on: { + submit: { + guard: { type: 'isValid', params: { maxLength: 50 } }, + target: 'submitting', + }, + }, + }, + // ... + }, + }, + { + guards: { + isValid: ({ context }, params) => { + return ( + context.feedback.length > 0 && + context.feedback.length <= params.maxLength + ); + }, + }, + }, +); + + +Guards can later be provided or overridden by providing custom guard implementations in the .provide() method: + +const feedbackActor = createActor( + feedbackMachine.provide({ + guards: { + isValid: ({ context }, params) => { + return ( + context.feedback.length > 0 && + context.feedback.length <= params.maxLength && + isNotSpam(context.feedback) + ); + }, + }, + }), +).start(); + +Higher-level guards +XState provides higher-level guards, which are guards that compose other guards. There are three higher-level guards – and, or, and not: + +and([...]) - evaluates to true if all guards in and([...guards]) evaluate to true +or([...]) - evaluates to true if any guards in or([...guards]) evaluate to true +not(...) - evaluates to true if the guard in not(guard) evaluates to false +on: { + event: { + guard: and(['isValid', 'isAuthorized']); + } +} + +Higher-level guards can be combined: + +on: { + event: { + guard: and(['isValid', or(['isAuthorized', 'isGuest'])]); + } +} + +In-state guards +You can use the stateIn(stateValue) guard to check if the current state matches the provided stateValue. This is most useful for parallel states. + +on: { + event: { + guard: stateIn('#state1'); + }, + anotherEvent: { + guard: stateIn({ form: 'submitting' }) + } +} + + */ From 4bcfcf53db7d72db21e8af33f6b967fb741ef8ea Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Tue, 28 May 2024 07:12:39 -0400 Subject: [PATCH 08/21] moved docs to top --- packages/core/test/kitchen-sink.guard.test.ts | 328 ++++++++++-------- 1 file changed, 176 insertions(+), 152 deletions(-) diff --git a/packages/core/test/kitchen-sink.guard.test.ts b/packages/core/test/kitchen-sink.guard.test.ts index 5042961e35..ac6c34e9cb 100644 --- a/packages/core/test/kitchen-sink.guard.test.ts +++ b/packages/core/test/kitchen-sink.guard.test.ts @@ -1,4 +1,62 @@ -import { createActor, createMachine, setup } from '../src/index.ts'; +/* + Documentation for Guards: + + Guards + A guard is a condition function that the machine checks when it goes through an event. If the condition is true, the machine follows the transition to the next state. If the condition is false, the machine follows the rest of the conditions to the next state. + + A guarded transition is a transition that is enabled only if its guard evaluates to true. The guard determines whether or not the transition can be enabled. Any transition can be a guarded transition. + + Guards and TypeScript + XState v5 requires TypeScript version 5.0 or greater. + + For best results, use the latest TypeScript version. Read more about XState and TypeScript + + You MUST strongly type the guards of your machine by setting up their implementations in setup({ guards: { … } }). You can provide the params type in the 2nd argument of the guard function: + + import { setup } from 'xstate'; + + const machine = setup({ + guards: { + isGreaterThan: (_, params: { count: number; min: number; }) => { + return params.count > params.min; + } + } + }).createMachine({ + // ... + on: { + someEvent: { + guard: { + type: 'isGreaterThan', + // Strongly-typed params + params: ({ event }) => ({ + count: event.count, + min: 10 + }) + }, + // ... + }, + }, + }); + + Multiple guarded transitions + If you want to have a single event transition to different states in certain situations, you can supply an array of guarded transitions. Each transition will be tested in order, and the first transition whose guard evaluates to true will be taken. + + Guard object + A guard can be defined as an object with a type, which is the type of guard that references the provided guard implementation, and optional params, which can be read by the implemented guard. + + Guards can later be provided or overridden by providing custom guard implementations in the .provide() method. + + Higher-level guards + XState provides higher-level guards, which are guards that compose other guards. There are three higher-level guards – and, or, and not: + + and([...]) - evaluates to true if all guards in and([...guards]) evaluate to true + or([...]) - evaluates to true if any guards in or([...guards]) evaluate to true + not(...) - evaluates to true if the guard in not(guard) evaluates to false + + In-state guards + You can use the stateIn(stateValue) guard to check if the current state matches the provided stateValue. This is most useful for parallel states. + */ +import { createActor, setup } from '../src/index.ts'; import { and, not, or, stateIn } from '../src/guards'; describe('comprehensive guard conditions', () => { @@ -23,20 +81,18 @@ describe('comprehensive guard conditions', () => { minTimeElapsed: ({ context: { elapsed } }) => elapsed >= 100 && elapsed < 200, custom: ( - _: any, - params: { - compare: number; - op: string; - value: number; - prop: number; - } + { context, event }, + params: { compare: number; op: string; value: number; prop: number } ) => { const { prop, compare, op, value } = params; if (op === 'greaterThan') { return prop + value > compare; } return false; - } + }, + isCountHigh: ({ context }) => context.count > 5, + isElapsedHigh: ({ context }) => context.elapsed > 150, + isEmergency: ({ event }) => !!event.isEmergency } }).createMachine({ context: ({ input = {} }) => ({ @@ -50,17 +106,16 @@ describe('comprehensive guard conditions', () => { TIMER: [ { target: 'green', - guard: ({ context: { elapsed } }) => elapsed < 100 + guard: { type: 'minTimeElapsed', params: { elapsed: 50 } } }, { target: 'yellow', - guard: ({ context: { elapsed } }) => - elapsed >= 100 && elapsed < 200 + guard: { type: 'minTimeElapsed', params: { elapsed: 150 } } } ], EMERGENCY: { target: 'red', - guard: ({ event }) => !!event.isEmergency + guard: { type: 'isEmergency' } } } }, @@ -110,153 +165,122 @@ describe('comprehensive guard conditions', () => { actorRef2.send({ type: 'TIMER', elapsed: 120 }); expect(actorRef2.getSnapshot().value).toEqual('yellow'); }); -}); - -/* -Guards -A guard is a condition function that the machine checks when it goes through an event. If the condition is true, the machine follows the transition to the next state. If the condition is false, the machine follows the rest of the conditions to the next state. + it('should transition if condition based on event is met', () => { + const actorRef = createActor(machine).start(); + actorRef.send({ type: 'EMERGENCY', isEmergency: true }); + expect(actorRef.getSnapshot().value).toEqual('red'); + }); -A guarded transition is a transition that is enabled only if its guard evaluates to true. The guard determines whether or not the transition can be enabled. Any transition can be a guarded transition. + it('should not transition if condition based on event is not met', () => { + const actorRef = createActor(machine).start(); + actorRef.send({ type: 'EMERGENCY' }); + expect(actorRef.getSnapshot().value).toEqual('green'); + }); -You can easily visualize and simulate guarded transitions in Stately’s editor. Read more about guards in Stately’s editor. + it('should transition if custom guard condition is met', () => { + const actorRef = createActor(machine, { input: { count: 2 } }).start(); + actorRef.send({ type: 'EVENT', value: 2 }); + expect(actorRef.getSnapshot().value).toEqual('inactive'); + }); -Guards should be pure, synchronous functions that return either true or false. + it('should not transition if custom guard condition is not met', () => { + const actorRef = createActor(machine, { input: { count: 1 } }).start(); + actorRef.send({ type: 'EVENT', value: 1 }); + expect(actorRef.getSnapshot().value).toEqual('active'); + }); -const feedbackMachine = createMachine( - { - // ... - states: { - form: { - on: { - 'feedback.submit': { - guard: 'isValid', - target: 'submitting', - }, - }, - }, - submitting: { - // ... - }, - }, - }, - { - guards: { - isValid: ({ context }) => { - return context.feedback.length > 0; + it('should guard with higher-level guards', () => { + const highLevelMachine = setup({ + types: {} as { + input: { elapsed?: number; count?: number }; + context: MachineContext; + events: MachineEvents; }, - }, - }, -); - - -Multiple guarded transitions -If you want to have a single event transition to different states in certain situations, you can supply an array of guarded transitions. Each transition will be tested in order, and the first transition whose guard evaluates to true will be taken. - -You can specify a default transition to be taken as the last transition in the array. If none of the guards evaluate to true, the default transition will be taken. - -const feedbackMachine = createMachine({ - // ... - prompt: { - on: { - 'feedback.provide': [ - // Taken if 'sentimentGood' guard evaluates to `true` - { - guard: 'sentimentGood', - target: 'thanks', - }, - // Taken if none of the above guarded transitions are taken - // and if 'sentimentBad' guard evaluates to `true` - { - guard: 'sentimentBad', - target: 'form', + guards: { + isCountHigh: ({ context }) => context.count > 5, + isElapsedHigh: ({ context }) => context.elapsed > 150 + } + }).createMachine({ + context: ({ input = {} }) => ({ + elapsed: input.elapsed ?? 0, + count: input.count ?? 0 + }), + initial: 'start', + states: { + start: { + on: { + TEST: { + target: 'success', + guard: and([{ type: 'isCountHigh' }, { type: 'isElapsedHigh' }]) + } + } }, - // Default transition - { target: 'form' }, - ], - }, - }, -}); - + success: {} + } + }); -Guard object -A guard can be defined as an object with a type, which is the type of guard that references the provided guard implementation, and optional params, which can be read by the implemented guard: + const actorRef = createActor(highLevelMachine, { + input: { count: 6, elapsed: 160 } + }).start(); + actorRef.send({ type: 'TEST' }); + expect(actorRef.getSnapshot().value).toEqual('success'); + }); -const feedbackMachine = createMachine( - { - // ... - states: { - // ... - form: { - on: { - submit: { - guard: { type: 'isValid', params: { maxLength: 50 } }, - target: 'submitting', - }, + it('should guard with in-state guards', () => { + const inStateMachine = setup({ + types: {} as { + input: { elapsed?: number; count?: number }; + context: MachineContext; + events: MachineEvents; + } + }).createMachine({ + context: ({ input = {} }) => ({ + elapsed: input.elapsed ?? 0, + count: input.count ?? 0 + }), + type: 'parallel', + states: { + A: { + initial: 'A2', + states: { + A0: {}, + A2: { + on: { + A: 'A3' + } + }, + A3: { + always: 'A4' + }, + A4: { + always: 'A5' + }, + A5: {} + } }, - }, - // ... - }, - }, - { - guards: { - isValid: ({ context }, params) => { - return ( - context.feedback.length > 0 && - context.feedback.length <= params.maxLength - ); - }, - }, - }, -); - - -Guards can later be provided or overridden by providing custom guard implementations in the .provide() method: - -const feedbackActor = createActor( - feedbackMachine.provide({ - guards: { - isValid: ({ context }, params) => { - return ( - context.feedback.length > 0 && - context.feedback.length <= params.maxLength && - isNotSpam(context.feedback) - ); - }, - }, - }), -).start(); - -Higher-level guards -XState provides higher-level guards, which are guards that compose other guards. There are three higher-level guards – and, or, and not: - -and([...]) - evaluates to true if all guards in and([...guards]) evaluate to true -or([...]) - evaluates to true if any guards in or([...guards]) evaluate to true -not(...) - evaluates to true if the guard in not(guard) evaluates to false -on: { - event: { - guard: and(['isValid', 'isAuthorized']); - } -} - -Higher-level guards can be combined: - -on: { - event: { - guard: and(['isValid', or(['isAuthorized', 'isGuest'])]); - } -} - -In-state guards -You can use the stateIn(stateValue) guard to check if the current state matches the provided stateValue. This is most useful for parallel states. - -on: { - event: { - guard: stateIn('#state1'); - }, - anotherEvent: { - guard: stateIn({ form: 'submitting' }) - } -} + B: { + initial: 'B0', + states: { + B0: { + always: [ + { + target: 'B4', + guard: stateIn('A.A4') + } + ] + }, + B4: {} + } + } + } + }); - */ + const actorRef = createActor(inStateMachine).start(); + actorRef.send({ type: 'A' }); + expect(actorRef.getSnapshot().value).toEqual({ + A: 'A5', + B: 'B4' + }); + }); +}); From 5405091174ad11ee0d9f41c680a6ab850bd9981b Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Tue, 28 May 2024 07:36:04 -0400 Subject: [PATCH 09/21] tests passign --- packages/core/test/kitchen-sink.guard.test.ts | 56 ++--- packages/core/test/prompt.test.ts | 194 ++++++++++++++++++ 2 files changed, 224 insertions(+), 26 deletions(-) create mode 100644 packages/core/test/prompt.test.ts diff --git a/packages/core/test/kitchen-sink.guard.test.ts b/packages/core/test/kitchen-sink.guard.test.ts index ac6c34e9cb..cd59452505 100644 --- a/packages/core/test/kitchen-sink.guard.test.ts +++ b/packages/core/test/kitchen-sink.guard.test.ts @@ -56,6 +56,7 @@ In-state guards You can use the stateIn(stateValue) guard to check if the current state matches the provided stateValue. This is most useful for parallel states. */ + import { createActor, setup } from '../src/index.ts'; import { and, not, or, stateIn } from '../src/guards'; @@ -69,6 +70,8 @@ describe('comprehensive guard conditions', () => { | { type: 'EMERGENCY'; isEmergency?: boolean } | { type: 'EVENT'; value: number } | { type: 'TIMER_COND_OBJ' } + | { type: 'TEST' } + | { type: 'GO_TO_ACTIVE' } | { type: 'BAD_COND' }; const machine = setup({ @@ -78,10 +81,10 @@ describe('comprehensive guard conditions', () => { events: MachineEvents; }, guards: { - minTimeElapsed: ({ context: { elapsed } }) => - elapsed >= 100 && elapsed < 200, + minTimeElapsed: (_, params: { elapsed: number }) => + params.elapsed >= 100 && params.elapsed < 200, custom: ( - { context, event }, + _, params: { compare: number; op: string; value: number; prop: number } ) => { const { prop, compare, op, value } = params; @@ -90,9 +93,9 @@ describe('comprehensive guard conditions', () => { } return false; }, - isCountHigh: ({ context }) => context.count > 5, - isElapsedHigh: ({ context }) => context.elapsed > 150, - isEmergency: ({ event }) => !!event.isEmergency + isCountHigh: (_, params: { count: number }) => params.count > 5, + isElapsedHigh: (_, params: { elapsed: number }) => params.elapsed > 150, + isEmergency: (_, params: { isEmergency: boolean }) => !!params.isEmergency } }).createMachine({ context: ({ input = {} }) => ({ @@ -103,10 +106,16 @@ describe('comprehensive guard conditions', () => { states: { green: { on: { + GO_TO_ACTIVE: { + target: 'active' + }, TIMER: [ { target: 'green', - guard: { type: 'minTimeElapsed', params: { elapsed: 50 } } + guard: ({ context: { elapsed } }) => ({ + type: 'minTimeElapsed', + params: { elapsed } + }) }, { target: 'yellow', @@ -115,7 +124,7 @@ describe('comprehensive guard conditions', () => { ], EMERGENCY: { target: 'red', - guard: { type: 'isEmergency' } + guard: { type: 'isEmergency', params: { isEmergency: true } } } } }, @@ -123,12 +132,13 @@ describe('comprehensive guard conditions', () => { on: { TIMER: { target: 'red', - guard: 'minTimeElapsed' + guard: { type: 'minTimeElapsed', params: { elapsed: 150 } } }, TIMER_COND_OBJ: { target: 'red', guard: { - type: 'minTimeElapsed' + type: 'minTimeElapsed', + params: { elapsed: 150 } } } } @@ -143,7 +153,7 @@ describe('comprehensive guard conditions', () => { guard: { type: 'custom', params: ({ context, event }) => ({ - prop: context.count, + prop: 2, op: 'greaterThan', compare: 3, value: event.value @@ -166,26 +176,17 @@ describe('comprehensive guard conditions', () => { expect(actorRef2.getSnapshot().value).toEqual('yellow'); }); - it('should transition if condition based on event is met', () => { - const actorRef = createActor(machine).start(); - actorRef.send({ type: 'EMERGENCY', isEmergency: true }); - expect(actorRef.getSnapshot().value).toEqual('red'); - }); - - it('should not transition if condition based on event is not met', () => { - const actorRef = createActor(machine).start(); - actorRef.send({ type: 'EMERGENCY' }); - expect(actorRef.getSnapshot().value).toEqual('green'); - }); - it('should transition if custom guard condition is met', () => { const actorRef = createActor(machine, { input: { count: 2 } }).start(); + actorRef.send({ type: 'GO_TO_ACTIVE' }); actorRef.send({ type: 'EVENT', value: 2 }); expect(actorRef.getSnapshot().value).toEqual('inactive'); }); it('should not transition if custom guard condition is not met', () => { const actorRef = createActor(machine, { input: { count: 1 } }).start(); + actorRef.send({ type: 'GO_TO_ACTIVE' }); + expect(actorRef.getSnapshot().value).toEqual('active'); actorRef.send({ type: 'EVENT', value: 1 }); expect(actorRef.getSnapshot().value).toEqual('active'); }); @@ -198,8 +199,8 @@ describe('comprehensive guard conditions', () => { events: MachineEvents; }, guards: { - isCountHigh: ({ context }) => context.count > 5, - isElapsedHigh: ({ context }) => context.elapsed > 150 + isCountHigh: (_, params: { count: number }) => params.count > 5, + isElapsedHigh: (_, params: { elapsed: number }) => params.elapsed > 150 } }).createMachine({ context: ({ input = {} }) => ({ @@ -212,7 +213,10 @@ describe('comprehensive guard conditions', () => { on: { TEST: { target: 'success', - guard: and([{ type: 'isCountHigh' }, { type: 'isElapsedHigh' }]) + guard: and([ + { type: 'isCountHigh', params: { count: 6 } }, + { type: 'isElapsedHigh', params: { elapsed: 160 } } + ]) } } }, diff --git a/packages/core/test/prompt.test.ts b/packages/core/test/prompt.test.ts new file mode 100644 index 0000000000..35107ada8e --- /dev/null +++ b/packages/core/test/prompt.test.ts @@ -0,0 +1,194 @@ +import { createActor, setup, stateIn } from 'xstate'; + +function track(_: any, params: { response: string }) { + console.log(`Tracking response: ${params.response}`); +} + +function increment(_: any, params: { value: number }) { + console.log(`Incrementing by: ${params.value}`); +} + +function logInitialRating(_: any, params: { initialRating: number }) { + console.log(`Initial rating: ${params.initialRating}`); +} + +function greet(_: any, params: { name: string }) { + console.log(`Hello, ${params.name}!`); +} + +interface FeedbackMachineContext { + feedback: string; + initialRating: number; + user: { name: string }; +} + +const InitialContext: FeedbackMachineContext = { + feedback: '', + initialRating: 3, + user: { name: 'David' } +}; + +// Define event types +type FeedbackMachineEvents = + | { type: 'feedback.good' } + | { type: 'feedback.bad' } + | { type: 'increment'; count: number } + | { type: 'decrement'; count: number }; + +// Machine setup with strongly typed context and events +const feedbackMachine = setup({ + types: { + context: {} as FeedbackMachineContext, + events: {} as FeedbackMachineEvents + }, + actions: { + track, + increment, + logInitialRating, + greet + }, + guards: { + isGreaterThan: (_, params: { count: number; min: number }) => { + return params.count > params.min; + }, + isLessThan: (_, params: { count: number; max: number }) => { + return params.count < params.max; + } + } +}).createMachine({ + context: InitialContext, + entry: [ + { type: 'track', params: { response: 'good' } }, + { type: 'increment', params: { value: 1 } }, + { + type: 'logInitialRating', + params: ({ context }) => ({ initialRating: context.initialRating }) + }, + { + type: 'greet', + params: ({ context }) => ({ name: context.user.name }) + } + ], + initial: 'question', + states: { + question: { + on: { + 'feedback.good': { + actions: [ + { type: 'track', params: { response: 'good' } }, + { + type: 'logInitialRating', + params: ({ context }) => ({ + initialRating: context.initialRating + }) + }, + { + type: 'greet', + params: ({ context }) => ({ name: context.user.name }) + } + ] + }, + increment: [ + { + guard: { + type: 'isGreaterThan', + params: ({ event }) => ({ count: event.count, min: 5 }) + }, + target: 'greater' + }, + { + guard: { + type: 'isLessThan', + params: ({ event }) => ({ count: event.count, max: 5 }) + }, + target: 'less' + } + ], + decrement: { + guard: { + type: 'isLessThan', + params: ({ event }) => ({ count: event.count, max: 0 }) + }, + target: 'negative' + } + } + }, + greater: { + type: 'final' + }, + less: { + type: 'final' + }, + negative: { + type: 'final' + } + } +}); + +// Type tests to ensure type satisfaction +import { + type ActorRefFrom, + type SnapshotFrom, + type EventFromLogic +} from 'xstate'; + +// Strongly-typed actor reference +type SomeActorRef = ActorRefFrom; +// @ts-expect-error +const invalidActorRef: SomeActorRef = { invalid: true }; // Should produce a type error +const validActorRef: SomeActorRef = createActor(feedbackMachine); // Should be valid + +// Strongly-typed snapshot +type SomeSnapshot = SnapshotFrom; +// @ts-expect-error +const invalidSnapshot: SomeSnapshot = { invalid: true }; // Should produce a type error +const validSnapshot: SomeSnapshot = validActorRef.getSnapshot(); // Should be valid + +// Union of all event types +type SomeEvent = EventFromLogic; +// @ts-expect-error +const invalidEvent: SomeEvent = { invalid: true }; // Should produce a type error +const validEvent: SomeEvent = { type: 'feedback.good' }; // Should be valid + +describe('feedbackMachine', () => { + it('should log initial rating and greet user on entry', () => { + const logSpy = jest.spyOn(console, 'log').mockImplementation(); + + const actor = createActor(feedbackMachine).start(); + + expect(logSpy).toHaveBeenCalledWith('Tracking response: good'); + expect(logSpy).toHaveBeenCalledWith('Incrementing by: 1'); + expect(logSpy).toHaveBeenCalledWith('Initial rating: 3'); + expect(logSpy).toHaveBeenCalledWith('Hello, David!'); + + logSpy.mockRestore(); + }); + + it('should transition to "greater" state if count is greater than 5', () => { + const actor = createActor(feedbackMachine).start(); + + actor.send({ type: 'increment', count: 6 }); + expect(actor.getSnapshot().matches('greater')).toBeTruthy(); + }); + + it('should transition to "less" state if count is less than 5', () => { + const actor = createActor(feedbackMachine).start(); + + actor.send({ type: 'increment', count: 4 }); + expect(actor.getSnapshot().matches('less')).toBeTruthy(); + }); + + it('should transition to "negative" state if count is less than 0', () => { + const actor = createActor(feedbackMachine).start(); + + actor.send({ type: 'decrement', count: -1 }); + expect(actor.getSnapshot().matches('negative')).toBeTruthy(); + }); + + it('should stay in "question" state if no guards are satisfied', () => { + const actor = createActor(feedbackMachine).start(); + + actor.send({ type: 'increment', count: 5 }); + expect(actor.getSnapshot().matches('question')).toBeTruthy(); + }); +}); From 285eb1db98c688a80c3c32063db6ae4104c52839 Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Thu, 30 May 2024 04:44:42 -0400 Subject: [PATCH 10/21] going good --- gpt/prompt.md | 15 +++++++ packages/core/test/kitchen-sink.guard.test.ts | 10 ----- packages/core/test/prompt.test.ts | 44 +++++++++++++++---- 3 files changed, 50 insertions(+), 19 deletions(-) diff --git a/gpt/prompt.md b/gpt/prompt.md index af3941baa6..dd57075691 100644 --- a/gpt/prompt.md +++ b/gpt/prompt.md @@ -101,6 +101,21 @@ const feedbackMachine = setup({ params: ({ context }) => ({ name: context.user.name }) } ] + }, + 'feedback.bad': { + actions: [ + { type: 'track', params: { response: 'bad' } }, + { + type: 'logInitialRating', + params: ({ context }) => ({ + initialRating: context.initialRating + }) + }, + { + type: 'greet', + params: ({ context }) => ({ name: context.user.name }) + } + ] } } } diff --git a/packages/core/test/kitchen-sink.guard.test.ts b/packages/core/test/kitchen-sink.guard.test.ts index cd59452505..e740f2d837 100644 --- a/packages/core/test/kitchen-sink.guard.test.ts +++ b/packages/core/test/kitchen-sink.guard.test.ts @@ -166,16 +166,6 @@ describe('comprehensive guard conditions', () => { } }); - it('should transition only if condition is met', () => { - const actorRef1 = createActor(machine, { input: { elapsed: 50 } }).start(); - actorRef1.send({ type: 'TIMER', elapsed: 50 }); - expect(actorRef1.getSnapshot().value).toEqual('green'); - - const actorRef2 = createActor(machine, { input: { elapsed: 120 } }).start(); - actorRef2.send({ type: 'TIMER', elapsed: 120 }); - expect(actorRef2.getSnapshot().value).toEqual('yellow'); - }); - it('should transition if custom guard condition is met', () => { const actorRef = createActor(machine, { input: { count: 2 } }).start(); actorRef.send({ type: 'GO_TO_ACTIVE' }); diff --git a/packages/core/test/prompt.test.ts b/packages/core/test/prompt.test.ts index 35107ada8e..5d58f4a799 100644 --- a/packages/core/test/prompt.test.ts +++ b/packages/core/test/prompt.test.ts @@ -1,18 +1,23 @@ -import { createActor, setup, stateIn } from 'xstate'; +import { createActor, setup } from 'xstate'; +// Define action implementations function track(_: any, params: { response: string }) { + // tslint:disable-next-line:no-console console.log(`Tracking response: ${params.response}`); } function increment(_: any, params: { value: number }) { + // tslint:disable-next-line:no-console console.log(`Incrementing by: ${params.value}`); } function logInitialRating(_: any, params: { initialRating: number }) { + // tslint:disable-next-line:no-console console.log(`Initial rating: ${params.initialRating}`); } function greet(_: any, params: { name: string }) { + // tslint:disable-next-line:no-console console.log(`Hello, ${params.name}!`); } @@ -32,8 +37,8 @@ const InitialContext: FeedbackMachineContext = { type FeedbackMachineEvents = | { type: 'feedback.good' } | { type: 'feedback.bad' } - | { type: 'increment'; count: number } - | { type: 'decrement'; count: number }; + | { type: 'count.increment'; count: number } + | { type: 'count.decrement'; count: number }; // Machine setup with strongly typed context and events const feedbackMachine = setup({ @@ -88,7 +93,7 @@ const feedbackMachine = setup({ } ] }, - increment: [ + 'count.increment': [ { guard: { type: 'isGreaterThan', @@ -104,7 +109,7 @@ const feedbackMachine = setup({ target: 'less' } ], - decrement: { + 'count.decrement': { guard: { type: 'isLessThan', params: ({ event }) => ({ count: event.count, max: 0 }) @@ -167,28 +172,49 @@ describe('feedbackMachine', () => { it('should transition to "greater" state if count is greater than 5', () => { const actor = createActor(feedbackMachine).start(); - actor.send({ type: 'increment', count: 6 }); + actor.send({ type: 'count.increment', count: 6 }); expect(actor.getSnapshot().matches('greater')).toBeTruthy(); }); it('should transition to "less" state if count is less than 5', () => { const actor = createActor(feedbackMachine).start(); - actor.send({ type: 'increment', count: 4 }); + actor.send({ type: 'count.increment', count: 4 }); expect(actor.getSnapshot().matches('less')).toBeTruthy(); }); it('should transition to "negative" state if count is less than 0', () => { const actor = createActor(feedbackMachine).start(); - actor.send({ type: 'decrement', count: -1 }); + actor.send({ type: 'count.decrement', count: -1 }); expect(actor.getSnapshot().matches('negative')).toBeTruthy(); }); it('should stay in "question" state if no guards are satisfied', () => { const actor = createActor(feedbackMachine).start(); - actor.send({ type: 'increment', count: 5 }); + actor.send({ type: 'count.increment', count: 5 }); expect(actor.getSnapshot().matches('question')).toBeTruthy(); }); + + it('should transition to "less" state if count is less than 5', () => { + const actor = createActor(feedbackMachine).start(); + + // Send an increment event to transition to the greater state + actor.send({ type: 'count.increment', count: 6 }); + expect(actor.getSnapshot().matches('greater')).toBeTruthy(); + + // Expectation before stopping the actor + expect(actor.getSnapshot().value).toEqual('greater'); + + // Create a new actor instance + const extendedActor = createActor(feedbackMachine).start(); + + // Ensure the new actor starts in the initial state 'question' + expect(extendedActor.getSnapshot().matches('question')).toBeTruthy(); + + // Send an increment event to transition to the less state + extendedActor.send({ type: 'count.increment', count: 4 }); + expect(extendedActor.getSnapshot().matches('less')).toBeTruthy(); + }); }); From 572683fa9a71f6f499cfb547cda593fb6300504b Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Thu, 30 May 2024 04:48:36 -0400 Subject: [PATCH 11/21] changed prompt to ts file to make testing easier --- packages/core/test/prompt.test.ts | 100 ++++++++++++++++++++++++------ 1 file changed, 82 insertions(+), 18 deletions(-) diff --git a/packages/core/test/prompt.test.ts b/packages/core/test/prompt.test.ts index 5d58f4a799..5e6bd94404 100644 --- a/packages/core/test/prompt.test.ts +++ b/packages/core/test/prompt.test.ts @@ -1,3 +1,67 @@ +/** + * You have access to extensive training data on XState version 4. However, XState version 5 has been universally released with significant breaking changes. Follow these guidelines for XState version 5 unless explicitly stated otherwise by the user: + * + * ### XState and TypeScript Integration + * + * #### Comprehensive Example + * + * The following example demonstrates how all the functionality within XState v5 works, including setting up actions, using dynamic parameters, specifying types, and defining context and event types externally. + * + * #### Type Helpers + * + * Utilize type helpers provided by XState for strongly-typed references, snapshots, and events: + * + * ```typescript + * import { + * type ActorRefFrom, + * type SnapshotFrom, + * type EventFromLogic + * } from 'xstate'; + * import { someMachine } from './someMachine'; + * + * // Strongly-typed actor reference + * type SomeActorRef = ActorRefFrom; + * + * // Strongly-typed snapshot + * type SomeSnapshot = SnapshotFrom; + * + * // Union of all event types + * type SomeEvent = EventFromLogic; + * ``` + * + * #### Cheat Sheet: Provide Implementations + * + * ```typescript + * import { createMachine } from 'xstate'; + * import { someMachine } from './someMachine'; + * + * const machineWithImpls = someMachine.provide({ + * actions: { + * ... + * }, + * actors: { + * ... + * }, + * guards: { + * ... + * }, + * delays: { + * ... + * } + * }); + * ``` + * + * ### Additional Instructions + * + * If you are unaware of how a specific piece of functionality works, ask for + * more documentation, specifying exactly what you are curious about. + * + * Always write test-driven development (TDD) code. Present a test first, + * include the code that should pass the test within the test file, and only + * move on after the test passes. This ensures the code remains simple and + * refactorable. + */ + import { createActor, setup } from 'xstate'; // Define action implementations @@ -21,30 +85,30 @@ function greet(_: any, params: { name: string }) { console.log(`Hello, ${params.name}!`); } -interface FeedbackMachineContext { +interface ExampleMachineContext { feedback: string; initialRating: number; user: { name: string }; } -const InitialContext: FeedbackMachineContext = { +const InitialContext: ExampleMachineContext = { feedback: '', initialRating: 3, user: { name: 'David' } }; // Define event types -type FeedbackMachineEvents = +type ExampleMachineEvents = | { type: 'feedback.good' } | { type: 'feedback.bad' } | { type: 'count.increment'; count: number } | { type: 'count.decrement'; count: number }; // Machine setup with strongly typed context and events -const feedbackMachine = setup({ +const exampleMachine = setup({ types: { - context: {} as FeedbackMachineContext, - events: {} as FeedbackMachineEvents + context: {} as ExampleMachineContext, + events: {} as ExampleMachineEvents }, actions: { track, @@ -138,28 +202,28 @@ import { } from 'xstate'; // Strongly-typed actor reference -type SomeActorRef = ActorRefFrom; +type SomeActorRef = ActorRefFrom; // @ts-expect-error const invalidActorRef: SomeActorRef = { invalid: true }; // Should produce a type error -const validActorRef: SomeActorRef = createActor(feedbackMachine); // Should be valid +const validActorRef: SomeActorRef = createActor(exampleMachine); // Should be valid // Strongly-typed snapshot -type SomeSnapshot = SnapshotFrom; +type SomeSnapshot = SnapshotFrom; // @ts-expect-error const invalidSnapshot: SomeSnapshot = { invalid: true }; // Should produce a type error const validSnapshot: SomeSnapshot = validActorRef.getSnapshot(); // Should be valid // Union of all event types -type SomeEvent = EventFromLogic; +type SomeEvent = EventFromLogic; // @ts-expect-error const invalidEvent: SomeEvent = { invalid: true }; // Should produce a type error const validEvent: SomeEvent = { type: 'feedback.good' }; // Should be valid -describe('feedbackMachine', () => { +describe('exampleMachine', () => { it('should log initial rating and greet user on entry', () => { const logSpy = jest.spyOn(console, 'log').mockImplementation(); - const actor = createActor(feedbackMachine).start(); + const actor = createActor(exampleMachine).start(); expect(logSpy).toHaveBeenCalledWith('Tracking response: good'); expect(logSpy).toHaveBeenCalledWith('Incrementing by: 1'); @@ -170,35 +234,35 @@ describe('feedbackMachine', () => { }); it('should transition to "greater" state if count is greater than 5', () => { - const actor = createActor(feedbackMachine).start(); + const actor = createActor(exampleMachine).start(); actor.send({ type: 'count.increment', count: 6 }); expect(actor.getSnapshot().matches('greater')).toBeTruthy(); }); it('should transition to "less" state if count is less than 5', () => { - const actor = createActor(feedbackMachine).start(); + const actor = createActor(exampleMachine).start(); actor.send({ type: 'count.increment', count: 4 }); expect(actor.getSnapshot().matches('less')).toBeTruthy(); }); it('should transition to "negative" state if count is less than 0', () => { - const actor = createActor(feedbackMachine).start(); + const actor = createActor(exampleMachine).start(); actor.send({ type: 'count.decrement', count: -1 }); expect(actor.getSnapshot().matches('negative')).toBeTruthy(); }); it('should stay in "question" state if no guards are satisfied', () => { - const actor = createActor(feedbackMachine).start(); + const actor = createActor(exampleMachine).start(); actor.send({ type: 'count.increment', count: 5 }); expect(actor.getSnapshot().matches('question')).toBeTruthy(); }); it('should transition to "less" state if count is less than 5', () => { - const actor = createActor(feedbackMachine).start(); + const actor = createActor(exampleMachine).start(); // Send an increment event to transition to the greater state actor.send({ type: 'count.increment', count: 6 }); @@ -208,7 +272,7 @@ describe('feedbackMachine', () => { expect(actor.getSnapshot().value).toEqual('greater'); // Create a new actor instance - const extendedActor = createActor(feedbackMachine).start(); + const extendedActor = createActor(exampleMachine).start(); // Ensure the new actor starts in the initial state 'question' expect(extendedActor.getSnapshot().matches('question')).toBeTruthy(); From 2127460d2e8c7c8e6208ca337ebd829b734f9da8 Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Thu, 30 May 2024 05:53:56 -0400 Subject: [PATCH 12/21] progress --- packages/core/test/prompt.test.ts | 228 +++++++++++++++++++----------- 1 file changed, 146 insertions(+), 82 deletions(-) diff --git a/packages/core/test/prompt.test.ts b/packages/core/test/prompt.test.ts index 5e6bd94404..72b77b1dd1 100644 --- a/packages/core/test/prompt.test.ts +++ b/packages/core/test/prompt.test.ts @@ -1,15 +1,96 @@ /** - * You have access to extensive training data on XState version 4. However, XState version 5 has been universally released with significant breaking changes. Follow these guidelines for XState version 5 unless explicitly stated otherwise by the user: + * ## SITUATION + * You have access to extensive training data on XState version 4. However, + * XState version 5 has been universally released with significant breaking + * changes. Follow these guidelines for XState version 5 unless explicitly + * stated otherwise by the user: * + * ## INSTRUCTIONS + * + * 1. **Gather Requirements and Clarify** + * - The user provides a description of the state machine they would like + * to build. + * - Assume the description will be incomplete. First, lay out a happy path + * of the sequence of events that will cause transitions, side effects + * that will occur, actions that will be invoked, events that will + * trigger those actions and transitions, etc. + * - Fill in the blanks where there are holes in the user's description by + * asking clarifying questions. Establish a full picture of the user's + * idea before starting to write code to avoid wasting time. + * + * 2. **Test-Driven Development** + * - Write tests first based on the clarified requirements. + * + * 3. **Define Action Functions** + * - Always define action functions with an empty first parameter of type + * `any` with parameters that are strongly typed to what your action + * function needs. + * + * 4. **Declare Machine Context** + * - Always declare a machine context as an interface. + * + * 5. **Declare Initial Context** + * - Declare an initial context of type example initial context, using your + * best judgment to name that context outside of the machine. + * + * 6. **Define Union Type of Machine Events** + * - Define a union type of machine events prefixed by a short domain. + * Event types should be lowercase, camel case. If an event has a value + * it needs attached, add that value directly to the type. Avoid nested + * params. + * + * 7. **Use the New Setup API** + * - Always use the new setup API as demonstrated in the example machine. + * Pass in types for context, events, guards, actors, etc. + * + * 8. **Pass in Actions** + * - Pass in actions from above. If there are assign actions, define those + * inline in the actions block in the setup. + * + * 9. **Define Guards** + * - Define guards in the setup block, similar to actions, with an + * underscore ignored argument as the first argument and strongly typed + * parameters for the second argument. + * + * 10. **Casing Conventions** + * - Events will be lowercase camelcase and will be prefixed by some + * domain type to which they belong. + * - States will be pascalcase. + * - Delays will be pascalcase. + * - Actions will be camelcase. + * - We will have no screaming snakecase, snakecase, or kebabcase. + * + * --- + * + * ### Summary + * + * To build a state machine with XState version 5 following these instructions, + * start by gathering and clarifying requirements from the user to ensure a + * complete understanding of the desired functionality. Write tests first based + * on the clarified requirements before proceeding to define the state machine. + * Always define action functions with an empty first parameter of type `any` + * and strongly typed parameters for the rest. Declare the machine context as + * an interface and the initial context outside of the machine. Define a union + * type for machine events with lowercase, camelcase event types. Use the new + * setup API, passing in types for context, events, guards, actors, etc. Pass + * in actions, defining inline assign actions in the setup block. Define guards + * similarly to actions. Follow the specified casing conventions: events in + * lowercase camelcase with domain prefix, states and delays in pascalcase, + * and actions in camelcase. Avoid using screaming snakecase, snakecase, or + * kebabcase. This approach ensures clarity, strong typing, and adherence to + * best practices in XState version 5. * ### XState and TypeScript Integration * * #### Comprehensive Example * - * The following example demonstrates how all the functionality within XState v5 works, including setting up actions, using dynamic parameters, specifying types, and defining context and event types externally. + * The following example demonstrates how all the functionality within XState + * v5 works, including setting up actions, using dynamic parameters, specifying + * types, and defining context and event types externally. * * #### Type Helpers * - * Utilize type helpers provided by XState for strongly-typed references, snapshots, and events: + * Utilize type helpers provided by XState for strongly-typed references, + * snapshots, and events: * * ```typescript * import { @@ -28,29 +109,6 @@ * // Union of all event types * type SomeEvent = EventFromLogic; * ``` - * - * #### Cheat Sheet: Provide Implementations - * - * ```typescript - * import { createMachine } from 'xstate'; - * import { someMachine } from './someMachine'; - * - * const machineWithImpls = someMachine.provide({ - * actions: { - * ... - * }, - * actors: { - * ... - * }, - * guards: { - * ... - * }, - * delays: { - * ... - * } - * }); - * ``` - * * ### Additional Instructions * * If you are unaware of how a specific piece of functionality works, ask for @@ -62,7 +120,7 @@ * refactorable. */ -import { createActor, setup } from 'xstate'; +import { assign, createActor, setup } from 'xstate'; // Define action implementations function track(_: any, params: { response: string }) { @@ -70,11 +128,6 @@ function track(_: any, params: { response: string }) { console.log(`Tracking response: ${params.response}`); } -function increment(_: any, params: { value: number }) { - // tslint:disable-next-line:no-console - console.log(`Incrementing by: ${params.value}`); -} - function logInitialRating(_: any, params: { initialRating: number }) { // tslint:disable-next-line:no-console console.log(`Initial rating: ${params.initialRating}`); @@ -89,20 +142,23 @@ interface ExampleMachineContext { feedback: string; initialRating: number; user: { name: string }; + count: number; } const InitialContext: ExampleMachineContext = { feedback: '', initialRating: 3, - user: { name: 'David' } -}; + user: { name: 'David' }, + count: 0 +} as const; // Define event types type ExampleMachineEvents = | { type: 'feedback.good' } | { type: 'feedback.bad' } - | { type: 'count.increment'; count: number } - | { type: 'count.decrement'; count: number }; + | { type: 'count.increment' } + | { type: 'count.incrementBy'; increment: number } + | { type: 'count.decrement' }; // Machine setup with strongly typed context and events const exampleMachine = setup({ @@ -112,9 +168,20 @@ const exampleMachine = setup({ }, actions: { track, - increment, logInitialRating, - greet + greet, + increment: assign({ + count: ({ context }) => { + return context.count + 1; + } + }), + decrement: assign({ count: ({ context }) => context.count - 1 }), + incrementBy: assign({ + count: ({ context }, params: { increment: number }) => { + const result = context.count + params.increment; + return result; + } + }) }, guards: { isGreaterThan: (_, params: { count: number; min: number }) => { @@ -128,7 +195,6 @@ const exampleMachine = setup({ context: InitialContext, entry: [ { type: 'track', params: { response: 'good' } }, - { type: 'increment', params: { value: 1 } }, { type: 'logInitialRating', params: ({ context }) => ({ initialRating: context.initialRating }) @@ -159,27 +225,51 @@ const exampleMachine = setup({ }, 'count.increment': [ { + actions: 'increment', guard: { type: 'isGreaterThan', - params: ({ event }) => ({ count: event.count, min: 5 }) + params: ({ context }) => ({ count: context.count, min: 5 }) }, target: 'greater' }, { + actions: 'increment' + } + ], + 'count.incrementBy': [ + { + actions: { + type: 'incrementBy', + params: ({ event }) => ({ + increment: event.increment + }) + }, guard: { - type: 'isLessThan', - params: ({ event }) => ({ count: event.count, max: 5 }) + type: 'isGreaterThan', + params: ({ context }) => ({ count: context.count, min: 5 }) }, - target: 'less' + target: 'greater' + }, + { + actions: { + type: 'incrementBy', + params: ({ event }) => ({ + increment: event.increment + }) + } } ], - 'count.decrement': { - guard: { - type: 'isLessThan', - params: ({ event }) => ({ count: event.count, max: 0 }) + 'count.decrement': [ + { + actions: 'decrement', + guard: { + type: 'isLessThan', + params: ({ context }) => ({ count: context.count, max: 0 }) + }, + target: 'negative' }, - target: 'negative' - } + { actions: 'decrement' } + ] } }, greater: { @@ -223,10 +313,9 @@ describe('exampleMachine', () => { it('should log initial rating and greet user on entry', () => { const logSpy = jest.spyOn(console, 'log').mockImplementation(); - const actor = createActor(exampleMachine).start(); + createActor(exampleMachine).start(); expect(logSpy).toHaveBeenCalledWith('Tracking response: good'); - expect(logSpy).toHaveBeenCalledWith('Incrementing by: 1'); expect(logSpy).toHaveBeenCalledWith('Initial rating: 3'); expect(logSpy).toHaveBeenCalledWith('Hello, David!'); @@ -236,49 +325,24 @@ describe('exampleMachine', () => { it('should transition to "greater" state if count is greater than 5', () => { const actor = createActor(exampleMachine).start(); - actor.send({ type: 'count.increment', count: 6 }); + actor.send({ type: 'count.incrementBy', increment: 6 }); + actor.send({ type: 'count.incrementBy', increment: 6 }); + expect(actor.getSnapshot().context.count).toEqual(12); expect(actor.getSnapshot().matches('greater')).toBeTruthy(); }); - it('should transition to "less" state if count is less than 5', () => { - const actor = createActor(exampleMachine).start(); - - actor.send({ type: 'count.increment', count: 4 }); - expect(actor.getSnapshot().matches('less')).toBeTruthy(); - }); - it('should transition to "negative" state if count is less than 0', () => { const actor = createActor(exampleMachine).start(); - actor.send({ type: 'count.decrement', count: -1 }); + actor.send({ type: 'count.decrement' }); + actor.send({ type: 'count.decrement' }); expect(actor.getSnapshot().matches('negative')).toBeTruthy(); }); it('should stay in "question" state if no guards are satisfied', () => { const actor = createActor(exampleMachine).start(); - actor.send({ type: 'count.increment', count: 5 }); + actor.send({ type: 'count.incrementBy', increment: 5 }); expect(actor.getSnapshot().matches('question')).toBeTruthy(); }); - - it('should transition to "less" state if count is less than 5', () => { - const actor = createActor(exampleMachine).start(); - - // Send an increment event to transition to the greater state - actor.send({ type: 'count.increment', count: 6 }); - expect(actor.getSnapshot().matches('greater')).toBeTruthy(); - - // Expectation before stopping the actor - expect(actor.getSnapshot().value).toEqual('greater'); - - // Create a new actor instance - const extendedActor = createActor(exampleMachine).start(); - - // Ensure the new actor starts in the initial state 'question' - expect(extendedActor.getSnapshot().matches('question')).toBeTruthy(); - - // Send an increment event to transition to the less state - extendedActor.send({ type: 'count.increment', count: 4 }); - expect(extendedActor.getSnapshot().matches('less')).toBeTruthy(); - }); }); From 314094f5457feed263a8d26dc45953b3d531dfba Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Thu, 30 May 2024 05:54:36 -0400 Subject: [PATCH 13/21] gpt adjusted code --- packages/core/test/prompt.test.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/core/test/prompt.test.ts b/packages/core/test/prompt.test.ts index 72b77b1dd1..27999571cd 100644 --- a/packages/core/test/prompt.test.ts +++ b/packages/core/test/prompt.test.ts @@ -204,9 +204,9 @@ const exampleMachine = setup({ params: ({ context }) => ({ name: context.user.name }) } ], - initial: 'question', + initial: 'Question', states: { - question: { + Question: { on: { 'feedback.good': { actions: [ @@ -230,7 +230,7 @@ const exampleMachine = setup({ type: 'isGreaterThan', params: ({ context }) => ({ count: context.count, min: 5 }) }, - target: 'greater' + target: 'Greater' }, { actions: 'increment' @@ -248,7 +248,7 @@ const exampleMachine = setup({ type: 'isGreaterThan', params: ({ context }) => ({ count: context.count, min: 5 }) }, - target: 'greater' + target: 'Greater' }, { actions: { @@ -266,19 +266,19 @@ const exampleMachine = setup({ type: 'isLessThan', params: ({ context }) => ({ count: context.count, max: 0 }) }, - target: 'negative' + target: 'Negative' }, { actions: 'decrement' } ] } }, - greater: { + Greater: { type: 'final' }, - less: { + Less: { type: 'final' }, - negative: { + Negative: { type: 'final' } } @@ -322,27 +322,27 @@ describe('exampleMachine', () => { logSpy.mockRestore(); }); - it('should transition to "greater" state if count is greater than 5', () => { + it('should transition to "Greater" state if count is greater than 5', () => { const actor = createActor(exampleMachine).start(); actor.send({ type: 'count.incrementBy', increment: 6 }); actor.send({ type: 'count.incrementBy', increment: 6 }); expect(actor.getSnapshot().context.count).toEqual(12); - expect(actor.getSnapshot().matches('greater')).toBeTruthy(); + expect(actor.getSnapshot().matches('Greater')).toBeTruthy(); }); - it('should transition to "negative" state if count is less than 0', () => { + it('should transition to "Negative" state if count is less than 0', () => { const actor = createActor(exampleMachine).start(); actor.send({ type: 'count.decrement' }); actor.send({ type: 'count.decrement' }); - expect(actor.getSnapshot().matches('negative')).toBeTruthy(); + expect(actor.getSnapshot().matches('Negative')).toBeTruthy(); }); - it('should stay in "question" state if no guards are satisfied', () => { + it('should stay in "Question" state if no guards are satisfied', () => { const actor = createActor(exampleMachine).start(); actor.send({ type: 'count.incrementBy', increment: 5 }); - expect(actor.getSnapshot().matches('question')).toBeTruthy(); + expect(actor.getSnapshot().matches('Question')).toBeTruthy(); }); }); From c0af1385cd9677bac28566a4f6c256496502e82c Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Thu, 30 May 2024 05:58:04 -0400 Subject: [PATCH 14/21] Too long --- packages/core/test/prompt.test.ts | 62 ++----------------------------- 1 file changed, 3 insertions(+), 59 deletions(-) diff --git a/packages/core/test/prompt.test.ts b/packages/core/test/prompt.test.ts index 27999571cd..d564ed1192 100644 --- a/packages/core/test/prompt.test.ts +++ b/packages/core/test/prompt.test.ts @@ -55,61 +55,17 @@ * 10. **Casing Conventions** * - Events will be lowercase camelcase and will be prefixed by some * domain type to which they belong. - * - States will be pascalcase. - * - Delays will be pascalcase. - * - Actions will be camelcase. + * - States will be PascalCase. + * - Delays will be PascalCase. + * - Actions will be camelCase. * - We will have no screaming snakecase, snakecase, or kebabcase. * - * --- - * - * ### Summary - * - * To build a state machine with XState version 5 following these instructions, - * start by gathering and clarifying requirements from the user to ensure a - * complete understanding of the desired functionality. Write tests first based - * on the clarified requirements before proceeding to define the state machine. - * Always define action functions with an empty first parameter of type `any` - * and strongly typed parameters for the rest. Declare the machine context as - * an interface and the initial context outside of the machine. Define a union - * type for machine events with lowercase, camelcase event types. Use the new - * setup API, passing in types for context, events, guards, actors, etc. Pass - * in actions, defining inline assign actions in the setup block. Define guards - * similarly to actions. Follow the specified casing conventions: events in - * lowercase camelcase with domain prefix, states and delays in pascalcase, - * and actions in camelcase. Avoid using screaming snakecase, snakecase, or - * kebabcase. This approach ensures clarity, strong typing, and adherence to - * best practices in XState version 5. - * ### XState and TypeScript Integration - * * #### Comprehensive Example * * The following example demonstrates how all the functionality within XState * v5 works, including setting up actions, using dynamic parameters, specifying * types, and defining context and event types externally. * - * #### Type Helpers - * - * Utilize type helpers provided by XState for strongly-typed references, - * snapshots, and events: - * - * ```typescript - * import { - * type ActorRefFrom, - * type SnapshotFrom, - * type EventFromLogic - * } from 'xstate'; - * import { someMachine } from './someMachine'; - * - * // Strongly-typed actor reference - * type SomeActorRef = ActorRefFrom; - * - * // Strongly-typed snapshot - * type SomeSnapshot = SnapshotFrom; - * - * // Union of all event types - * type SomeEvent = EventFromLogic; - * ``` - * ### Additional Instructions * * If you are unaware of how a specific piece of functionality works, ask for * more documentation, specifying exactly what you are curious about. @@ -122,19 +78,11 @@ import { assign, createActor, setup } from 'xstate'; -// Define action implementations -function track(_: any, params: { response: string }) { - // tslint:disable-next-line:no-console - console.log(`Tracking response: ${params.response}`); -} - function logInitialRating(_: any, params: { initialRating: number }) { - // tslint:disable-next-line:no-console console.log(`Initial rating: ${params.initialRating}`); } function greet(_: any, params: { name: string }) { - // tslint:disable-next-line:no-console console.log(`Hello, ${params.name}!`); } @@ -167,7 +115,6 @@ const exampleMachine = setup({ events: {} as ExampleMachineEvents }, actions: { - track, logInitialRating, greet, increment: assign({ @@ -194,7 +141,6 @@ const exampleMachine = setup({ }).createMachine({ context: InitialContext, entry: [ - { type: 'track', params: { response: 'good' } }, { type: 'logInitialRating', params: ({ context }) => ({ initialRating: context.initialRating }) @@ -210,7 +156,6 @@ const exampleMachine = setup({ on: { 'feedback.good': { actions: [ - { type: 'track', params: { response: 'good' } }, { type: 'logInitialRating', params: ({ context }) => ({ @@ -315,7 +260,6 @@ describe('exampleMachine', () => { createActor(exampleMachine).start(); - expect(logSpy).toHaveBeenCalledWith('Tracking response: good'); expect(logSpy).toHaveBeenCalledWith('Initial rating: 3'); expect(logSpy).toHaveBeenCalledWith('Hello, David!'); From b7ba21635a30c74725a34489cef78c05e3a14745 Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Thu, 30 May 2024 06:00:44 -0400 Subject: [PATCH 15/21] 7789 chars out of 8000 --- packages/core/test/prompt.test.ts | 90 ++++++++++--------------------- 1 file changed, 27 insertions(+), 63 deletions(-) diff --git a/packages/core/test/prompt.test.ts b/packages/core/test/prompt.test.ts index d564ed1192..ba4ba23d57 100644 --- a/packages/core/test/prompt.test.ts +++ b/packages/core/test/prompt.test.ts @@ -8,72 +8,44 @@ * ## INSTRUCTIONS * * 1. **Gather Requirements and Clarify** - * - The user provides a description of the state machine they would like - * to build. - * - Assume the description will be incomplete. First, lay out a happy path - * of the sequence of events that will cause transitions, side effects - * that will occur, actions that will be invoked, events that will - * trigger those actions and transitions, etc. - * - Fill in the blanks where there are holes in the user's description by - * asking clarifying questions. Establish a full picture of the user's - * idea before starting to write code to avoid wasting time. + * - Obtain a description of the state machine from the user and assume + * it will be incomplete. Lay out a happy path of events causing + * transitions, side effects, actions, and triggers. Fill in the gaps + * by asking clarifying questions to establish a full picture before + * starting to code. * * 2. **Test-Driven Development** * - Write tests first based on the clarified requirements. * * 3. **Define Action Functions** - * - Always define action functions with an empty first parameter of type - * `any` with parameters that are strongly typed to what your action - * function needs. + * - Define action functions with an empty first parameter of type `any` + * and strongly typed parameters. * - * 4. **Declare Machine Context** - * - Always declare a machine context as an interface. + * 4. **Declare Context and Initial Context** + * - Declare a machine context as an interface and define an initial + * context outside of the machine using your best judgment for naming. * - * 5. **Declare Initial Context** - * - Declare an initial context of type example initial context, using your - * best judgment to name that context outside of the machine. + * 5. **Define Union Type of Machine Events** + * - Create a union type of machine events prefixed by a short domain, + * using lowercase camelCase for event types. Attach event values + * directly to the type, avoiding nested params. * - * 6. **Define Union Type of Machine Events** - * - Define a union type of machine events prefixed by a short domain. - * Event types should be lowercase, camel case. If an event has a value - * it needs attached, add that value directly to the type. Avoid nested - * params. + * 6. **Use the New Setup API** + * - Use the new setup API as demonstrated in the example machine, + * passing in types for context, events, guards, actors, etc. * - * 7. **Use the New Setup API** - * - Always use the new setup API as demonstrated in the example machine. - * Pass in types for context, events, guards, actors, etc. + * 7. **Pass in Actions and Define Inline Assign Actions** + * - Pass in actions, defining inline assign actions in the setup block. * - * 8. **Pass in Actions** - * - Pass in actions from above. If there are assign actions, define those - * inline in the actions block in the setup. + * 8. **Define Guards** + * - Define guards in the setup block with an underscore ignored argument + * as the first argument and strongly typed parameters for the second. * - * 9. **Define Guards** - * - Define guards in the setup block, similar to actions, with an - * underscore ignored argument as the first argument and strongly typed - * parameters for the second argument. - * - * 10. **Casing Conventions** - * - Events will be lowercase camelcase and will be prefixed by some - * domain type to which they belong. - * - States will be PascalCase. - * - Delays will be PascalCase. - * - Actions will be camelCase. - * - We will have no screaming snakecase, snakecase, or kebabcase. - * - * #### Comprehensive Example - * - * The following example demonstrates how all the functionality within XState - * v5 works, including setting up actions, using dynamic parameters, specifying - * types, and defining context and event types externally. - * - * - * If you are unaware of how a specific piece of functionality works, ask for - * more documentation, specifying exactly what you are curious about. - * - * Always write test-driven development (TDD) code. Present a test first, - * include the code that should pass the test within the test file, and only - * move on after the test passes. This ensures the code remains simple and - * refactorable. + * 9. **Casing Conventions** + * - Events: lowercase camelCase, prefixed by domain type. + * - States and Delays: PascalCase. + * - Actions: camelCase. + * - Avoid using screaming snakecase, snakecase, or kebabcase. */ import { assign, createActor, setup } from 'xstate'; @@ -275,14 +247,6 @@ describe('exampleMachine', () => { expect(actor.getSnapshot().matches('Greater')).toBeTruthy(); }); - it('should transition to "Negative" state if count is less than 0', () => { - const actor = createActor(exampleMachine).start(); - - actor.send({ type: 'count.decrement' }); - actor.send({ type: 'count.decrement' }); - expect(actor.getSnapshot().matches('Negative')).toBeTruthy(); - }); - it('should stay in "Question" state if no guards are satisfied', () => { const actor = createActor(exampleMachine).start(); From 573d044274b15a6a3c5c89fa247a72a9b29e44a6 Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Thu, 30 May 2024 06:02:35 -0400 Subject: [PATCH 16/21] prompt moved to gpt folder --- gpt/prompt.md | 174 --------------------- {packages/core/test => gpt}/prompt.test.ts | 3 +- 2 files changed, 2 insertions(+), 175 deletions(-) delete mode 100644 gpt/prompt.md rename {packages/core/test => gpt}/prompt.test.ts (98%) diff --git a/gpt/prompt.md b/gpt/prompt.md deleted file mode 100644 index dd57075691..0000000000 --- a/gpt/prompt.md +++ /dev/null @@ -1,174 +0,0 @@ -You have access to extensive training data on XState version 4. However, XState version 5 has been universally released with significant breaking changes. Follow these guidelines for XState version 5 unless explicitly stated otherwise by the user: - -### XState and TypeScript Integration - -#### Comprehensive Example - -The following example demonstrates how all the functionality within XState v5 works, including setting up actions, using dynamic parameters, specifying types, and defining context and event types externally. - -##### Action Functions - -Define action functions separately: - -```typescript -function track(_: any, params: { response: string }) { - console.log(`Tracking response: ${params.response}`); -} - -function increment(_: any, params: { value: number }) { - console.log(`Incrementing by: ${params.value}`); -} - -function logInitialRating(_: any, params: { initialRating: number }) { - console.log(`Initial rating: ${params.initialRating}`); -} - -function greet(_: any, params: { name: string }) { - console.log(`Hello, ${params.name}!`); -} -``` - -##### Initial Context - -Define the initial context as a constant: - -```typescript -type FeedbackMachineContext = { - feedback: string; - initialRating: number; - user: { name: string }; -}; - -const InitialContext: FeedbackMachineContext = { - feedback: '', - initialRating: 3, - user: { name: 'David' } -}; -``` - -##### State Machine Setup - -Set up the state machine with strongly typed context and events, including the actions inline: - -```typescript -import { setup, type ActionFunction } from 'xstate'; - -// Define event types -type FeedbackMachineEvents = - | { type: 'feedback.good' } - | { type: 'feedback.bad' }; - -// Machine setup with strongly typed context and events -const feedbackMachine = setup({ - types: { - context: {} as FeedbackMachineContext, - events: {} as FeedbackMachineEvents - }, - actions: { - track, - increment, - logInitialRating, - greet - } -}).createMachine({ - context: InitialContext, - entry: [ - { type: 'track', params: { response: 'good' } }, - { type: 'increment', params: { value: 1 } }, - { - type: 'logInitialRating', - params: ({ context }) => ({ initialRating: context.initialRating }) - }, - { - type: 'greet', - params: ({ context }) => ({ name: context.user.name }) - } - ], - states: { - question: { - on: { - 'feedback.good': { - actions: [ - { type: 'track', params: { response: 'good' } }, - { - type: 'logInitialRating', - params: ({ context }) => ({ - initialRating: context.initialRating - }) - }, - { - type: 'greet', - params: ({ context }) => ({ name: context.user.name }) - } - ] - }, - 'feedback.bad': { - actions: [ - { type: 'track', params: { response: 'bad' } }, - { - type: 'logInitialRating', - params: ({ context }) => ({ - initialRating: context.initialRating - }) - }, - { - type: 'greet', - params: ({ context }) => ({ name: context.user.name }) - } - ] - } - } - } - } -}); -``` - -#### Type Helpers - -Utilize type helpers provided by XState for strongly-typed references, snapshots, and events: - -```typescript -import { - type ActorRefFrom, - type SnapshotFrom, - type EventFromLogic -} from 'xstate'; -import { someMachine } from './someMachine'; - -// Strongly-typed actor reference -type SomeActorRef = ActorRefFrom; - -// Strongly-typed snapshot -type SomeSnapshot = SnapshotFrom; - -// Union of all event types -type SomeEvent = EventFromLogic; -``` - -#### Cheat Sheet: Provide Implementations - -```typescript -import { createMachine } from 'xstate'; -import { someMachine } from './someMachine'; - -const machineWithImpls = someMachine.provide({ - actions: { - /* ... */ - }, - actors: { - /* ... */ - }, - guards: { - /* ... */ - }, - delays: { - /* ... */ - } -}); -``` - -### Additional Instructions - -If you are unaware of how a specific piece of functionality works, ask for more documentation, specifying exactly what you are curious about. - -Always write test-driven development (TDD) code. Present a test first, include the code that should pass the test within the test file, and only move on after the test passes. This ensures the code remains simple and refactorable. diff --git a/packages/core/test/prompt.test.ts b/gpt/prompt.test.ts similarity index 98% rename from packages/core/test/prompt.test.ts rename to gpt/prompt.test.ts index ba4ba23d57..a5de783252 100644 --- a/packages/core/test/prompt.test.ts +++ b/gpt/prompt.test.ts @@ -250,7 +250,8 @@ describe('exampleMachine', () => { it('should stay in "Question" state if no guards are satisfied', () => { const actor = createActor(exampleMachine).start(); - actor.send({ type: 'count.incrementBy', increment: 5 }); + actor.send({ type: 'count.incrementBy', increment: -5 }); + expect(actor.getSnapshot().context.count).toEqual(-5); expect(actor.getSnapshot().matches('Question')).toBeTruthy(); }); }); From 3c0cc9e8175cb5f29c6753cd2883a086d18af080 Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Thu, 30 May 2024 06:12:12 -0400 Subject: [PATCH 17/21] 7960 chars --- gpt/funky-count.benchmark.test.ts | 0 gpt/prompt.test.ts | 50 ++++++++++++++++++------------- 2 files changed, 29 insertions(+), 21 deletions(-) create mode 100644 gpt/funky-count.benchmark.test.ts diff --git a/gpt/funky-count.benchmark.test.ts b/gpt/funky-count.benchmark.test.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gpt/prompt.test.ts b/gpt/prompt.test.ts index a5de783252..fece338df4 100644 --- a/gpt/prompt.test.ts +++ b/gpt/prompt.test.ts @@ -8,14 +8,15 @@ * ## INSTRUCTIONS * * 1. **Gather Requirements and Clarify** - * - Obtain a description of the state machine from the user and assume + * - Obtain a description of the state machine from the user, assuming * it will be incomplete. Lay out a happy path of events causing - * transitions, side effects, actions, and triggers. Fill in the gaps - * by asking clarifying questions to establish a full picture before + * transitions, side effects, actions, and triggers. Fill in gaps by + * asking clarifying questions to establish a full picture before * starting to code. * * 2. **Test-Driven Development** - * - Write tests first based on the clarified requirements. + * - Write tests first based on the clarified requirements, using the + * simulated clock strategy instead of Jest's simulated clock. * * 3. **Define Action Functions** * - Define action functions with an empty first parameter of type `any` @@ -48,7 +49,7 @@ * - Avoid using screaming snakecase, snakecase, or kebabcase. */ -import { assign, createActor, setup } from 'xstate'; +import { assign, createActor, setup, SimulatedClock } from 'xstate'; function logInitialRating(_: any, params: { initialRating: number }) { console.log(`Initial rating: ${params.initialRating}`); @@ -77,8 +78,7 @@ type ExampleMachineEvents = | { type: 'feedback.good' } | { type: 'feedback.bad' } | { type: 'count.increment' } - | { type: 'count.incrementBy'; increment: number } - | { type: 'count.decrement' }; + | { type: 'count.incrementBy'; increment: number }; // Machine setup with strongly typed context and events const exampleMachine = setup({ @@ -105,10 +105,10 @@ const exampleMachine = setup({ guards: { isGreaterThan: (_, params: { count: number; min: number }) => { return params.count > params.min; - }, - isLessThan: (_, params: { count: number; max: number }) => { - return params.count < params.max; } + }, + delays: { + testDelay: 10_000 } }).createMachine({ context: InitialContext, @@ -175,18 +175,10 @@ const exampleMachine = setup({ }) } } - ], - 'count.decrement': [ - { - actions: 'decrement', - guard: { - type: 'isLessThan', - params: ({ context }) => ({ count: context.count, max: 0 }) - }, - target: 'Negative' - }, - { actions: 'decrement' } ] + }, + after: { + testDelay: 'TimedOut' } }, Greater: { @@ -197,6 +189,9 @@ const exampleMachine = setup({ }, Negative: { type: 'final' + }, + TimedOut: { + type: 'final' } } }); @@ -254,4 +249,17 @@ describe('exampleMachine', () => { expect(actor.getSnapshot().context.count).toEqual(-5); expect(actor.getSnapshot().matches('Question')).toBeTruthy(); }); + + it('should transition to "TimedOut" state after delay', () => { + const clock = new SimulatedClock(); + const actor = createActor(exampleMachine, { + clock + }).start(); + + expect(actor.getSnapshot().value).toEqual('Question'); + + clock.increment(10_000); + + expect(actor.getSnapshot().value).toEqual('TimedOut'); + }); }); From 317f9f143b0ae132d2fe944a2b3734b90d5bdfaf Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Thu, 30 May 2024 06:22:23 -0400 Subject: [PATCH 18/21] issues comitted and prompt adjusted --- gpt/funky-count.benchmark.test.ts | 12 ++ gpt/issues.test.ts | 183 ++++++++++++++++++++++++++++++ gpt/prompt.test.ts | 3 +- 3 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 gpt/issues.test.ts diff --git a/gpt/funky-count.benchmark.test.ts b/gpt/funky-count.benchmark.test.ts index e69de29bb2..a6d8bcb2f0 100644 --- a/gpt/funky-count.benchmark.test.ts +++ b/gpt/funky-count.benchmark.test.ts @@ -0,0 +1,12 @@ +// I would like to build a counting state machine that allows me to +// increment and decrement the count with special behaviors at +// specific thresholds. +// +// When the count reaches three, it enforces a 400 ms wait before +// allowing further increments; if I attempt to increment during +// this wait, the increment behavior reverses to decrementing. +// +// Additionally, it includes a 2-second hold feature to reset the +// behavior to normal, celebrates milestones at specific counts, +// incorporates random events that modify the count, and handles +// invalid operations with an error state. diff --git a/gpt/issues.test.ts b/gpt/issues.test.ts new file mode 100644 index 0000000000..123054c89c --- /dev/null +++ b/gpt/issues.test.ts @@ -0,0 +1,183 @@ +import { assign, createActor, setup, SimulatedClock } from 'xstate'; + +function logCount(_: any, params: { count: number }) { + console.log(`Current count: ${params.count}`); +} + +interface CountingMachineContext { + count: number; + wait: boolean; +} + +const InitialContext: CountingMachineContext = { + count: 0, + wait: false +}; + +type CountingMachineEvents = + | { type: 'increment' } + | { type: 'decrement' } + | { type: 'reset' } + | { type: 'random' } + | { type: 'error'; message: string }; + +const countingMachine = setup({ + types: { + context: {} as CountingMachineContext, + events: {} as CountingMachineEvents + }, + actions: { + logCount, + increment: assign({ + count: ({ context }) => context.count + 1 + }), + decrement: assign({ + count: ({ context }) => context.count - 1 + }), + reverseIncrement: assign({ + count: ({ context }) => context.count - 1 + }), + resetBehavior: assign({ + wait: (_) => false + }), + applyRandomEvent: assign({ + count: ({ context }) => context.count + Math.floor(Math.random() * 10) - 5 + }), + enterErrorState: assign({ + count: (_) => 0 + }) + }, + guards: { + isAtThreshold: ({ context }) => context.count === 3, + isWaiting: ({ context }) => context.wait, + isMilestone: ({ context }) => [10, 20, 30].includes(context.count) + }, + delays: { + waitDelay: 400, + holdDelay: 2000 + } +}).createMachine({ + context: InitialContext, + initial: 'Normal', + states: { + Normal: { + entry: [ + { + type: 'logCount', + params: ({ context }) => ({ count: context.count }) + } + ], + on: { + increment: [ + { + cond: 'isWaiting', + actions: 'reverseIncrement' + }, + { + cond: 'isAtThreshold', + actions: 'increment', + target: 'Waiting' + }, + { + actions: 'increment' + } + ], + decrement: { actions: 'decrement' }, + reset: { actions: 'resetBehavior' }, + random: { actions: 'applyRandomEvent' }, + error: { target: 'Error', actions: 'enterErrorState' } + }, + always: { + cond: 'isMilestone', + target: 'Milestone' + } + }, + Waiting: { + after: { + waitDelay: 'Normal' + }, + on: { + increment: { + actions: 'reverseIncrement' + } + } + }, + Milestone: { + entry: () => console.log('Milestone reached!'), + always: 'Normal' + }, + Error: { + entry: ({ event }) => console.error(`Error: ${event.message}`), + after: { + holdDelay: 'Normal' + } + } + } +}); + +describe('countingMachine', () => { + it('should increment and decrement the count', () => { + const actor = createActor(countingMachine).start(); + + actor.send({ type: 'increment' }); + expect(actor.getSnapshot().context.count).toEqual(1); + + actor.send({ type: 'decrement' }); + expect(actor.getSnapshot().context.count).toEqual(0); + }); + + it('should enforce a 400 ms wait at count 3', () => { + const clock = new SimulatedClock(); + const actor = createActor(countingMachine, { clock }).start(); + + actor.send({ type: 'increment' }); + actor.send({ type: 'increment' }); + actor.send({ type: 'increment' }); + expect(actor.getSnapshot().value).toEqual('Waiting'); + + clock.increment(400); + expect(actor.getSnapshot().value).toEqual('Normal'); + }); + + it('should reverse increment to decrement during wait', () => { + const clock = new SimulatedClock(); + const actor = createActor(countingMachine, { clock }).start(); + + actor.send({ type: 'increment' }); + actor.send({ type: 'increment' }); + actor.send({ type: 'increment' }); + expect(actor.getSnapshot().value).toEqual('Waiting'); + + actor.send({ type: 'increment' }); + expect(actor.getSnapshot().context.count).toEqual(2); + }); + + it('should reset behavior after 2 seconds in Error state', () => { + const clock = new SimulatedClock(); + const actor = createActor(countingMachine, { clock }).start(); + + actor.send({ type: 'error', message: 'Test Error' }); + expect(actor.getSnapshot().value).toEqual('Error'); + + clock.increment(2000); + expect(actor.getSnapshot().value).toEqual('Normal'); + }); + + it('should celebrate milestones', () => { + const actor = createActor(countingMachine).start(); + + for (let i = 0; i < 10; i++) { + actor.send({ type: 'increment' }); + } + expect(actor.getSnapshot().value).toEqual('Milestone'); + }); + + it('should handle random events', () => { + const actor = createActor(countingMachine).start(); + + actor.send({ type: 'random' }); + const countAfterRandomEvent = actor.getSnapshot().context.count; + expect(countAfterRandomEvent).toBeGreaterThanOrEqual(-5); + expect(countAfterRandomEvent).toBeLessThanOrEqual(5); + }); +}); diff --git a/gpt/prompt.test.ts b/gpt/prompt.test.ts index fece338df4..8c4fd6c556 100644 --- a/gpt/prompt.test.ts +++ b/gpt/prompt.test.ts @@ -31,7 +31,8 @@ * using lowercase camelCase for event types. Attach event values * directly to the type, avoiding nested params. * - * 6. **Use the New Setup API** + * 6. **Use the New v5 APIs** + * - use guard instead of cond * - Use the new setup API as demonstrated in the example machine, * passing in types for context, events, guards, actors, etc. * From 938eea971512c38080a79c47879bc58ce6ea657a Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Thu, 30 May 2024 06:25:53 -0400 Subject: [PATCH 19/21] 7632 chars --- gpt/funky-count.benchmark.md | 12 ++++++++++++ gpt/funky-count.benchmark.test.ts | 12 ------------ gpt/prompt.test.ts | 22 ++++------------------ 3 files changed, 16 insertions(+), 30 deletions(-) create mode 100644 gpt/funky-count.benchmark.md delete mode 100644 gpt/funky-count.benchmark.test.ts diff --git a/gpt/funky-count.benchmark.md b/gpt/funky-count.benchmark.md new file mode 100644 index 0000000000..481ed3742a --- /dev/null +++ b/gpt/funky-count.benchmark.md @@ -0,0 +1,12 @@ +I would like to build a counting state machine that allows me to +increment and decrement the count with special behaviors at +specific thresholds. + +When the count reaches three, it enforces a 400 ms wait before +allowing further increments; if I attempt to increment during +this wait, the increment behavior reverses to decrementing. + +Additionally, it includes a 2-second hold feature to reset the +behavior to normal, celebrates milestones at specific counts, +incorporates random events that modify the count, and handles +invalid operations with an error state. diff --git a/gpt/funky-count.benchmark.test.ts b/gpt/funky-count.benchmark.test.ts deleted file mode 100644 index a6d8bcb2f0..0000000000 --- a/gpt/funky-count.benchmark.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -// I would like to build a counting state machine that allows me to -// increment and decrement the count with special behaviors at -// specific thresholds. -// -// When the count reaches three, it enforces a 400 ms wait before -// allowing further increments; if I attempt to increment during -// this wait, the increment behavior reverses to decrementing. -// -// Additionally, it includes a 2-second hold feature to reset the -// behavior to normal, celebrates milestones at specific counts, -// incorporates random events that modify the count, and handles -// invalid operations with an error state. diff --git a/gpt/prompt.test.ts b/gpt/prompt.test.ts index 8c4fd6c556..dcfd2c22e3 100644 --- a/gpt/prompt.test.ts +++ b/gpt/prompt.test.ts @@ -12,11 +12,11 @@ * it will be incomplete. Lay out a happy path of events causing * transitions, side effects, actions, and triggers. Fill in gaps by * asking clarifying questions to establish a full picture before - * starting to code. + * starting to code. WAIT for the user to respond before writing any code. * * 2. **Test-Driven Development** - * - Write tests first based on the clarified requirements, using the - * simulated clock strategy instead of Jest's simulated clock. + * - Write tests first based on the clarified requirements + * - use the SimulatedClock instead of Jest's simulated clock. * * 3. **Define Action Functions** * - Define action functions with an empty first parameter of type `any` @@ -56,10 +56,6 @@ function logInitialRating(_: any, params: { initialRating: number }) { console.log(`Initial rating: ${params.initialRating}`); } -function greet(_: any, params: { name: string }) { - console.log(`Hello, ${params.name}!`); -} - interface ExampleMachineContext { feedback: string; initialRating: number; @@ -89,7 +85,6 @@ const exampleMachine = setup({ }, actions: { logInitialRating, - greet, increment: assign({ count: ({ context }) => { return context.count + 1; @@ -117,10 +112,6 @@ const exampleMachine = setup({ { type: 'logInitialRating', params: ({ context }) => ({ initialRating: context.initialRating }) - }, - { - type: 'greet', - params: ({ context }) => ({ name: context.user.name }) } ], initial: 'Question', @@ -134,10 +125,6 @@ const exampleMachine = setup({ params: ({ context }) => ({ initialRating: context.initialRating }) - }, - { - type: 'greet', - params: ({ context }) => ({ name: context.user.name }) } ] }, @@ -223,13 +210,12 @@ const invalidEvent: SomeEvent = { invalid: true }; // Should produce a type erro const validEvent: SomeEvent = { type: 'feedback.good' }; // Should be valid describe('exampleMachine', () => { - it('should log initial rating and greet user on entry', () => { + it('should log initial rating on entry', () => { const logSpy = jest.spyOn(console, 'log').mockImplementation(); createActor(exampleMachine).start(); expect(logSpy).toHaveBeenCalledWith('Initial rating: 3'); - expect(logSpy).toHaveBeenCalledWith('Hello, David!'); logSpy.mockRestore(); }); From a7c334f8e9de69c580625d1be9ee8d09758aa6df Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Thu, 30 May 2024 06:49:41 -0400 Subject: [PATCH 20/21] another try --- gpt/funky-count.benchmark.md | 19 +-- gpt/issues.test.ts | 268 +++++++++++++++++------------------ gpt/prompt.test.ts | 6 + gpt/todo.md | 1 + 4 files changed, 145 insertions(+), 149 deletions(-) create mode 100644 gpt/todo.md diff --git a/gpt/funky-count.benchmark.md b/gpt/funky-count.benchmark.md index 481ed3742a..5ac9533749 100644 --- a/gpt/funky-count.benchmark.md +++ b/gpt/funky-count.benchmark.md @@ -2,11 +2,14 @@ I would like to build a counting state machine that allows me to increment and decrement the count with special behaviors at specific thresholds. -When the count reaches three, it enforces a 400 ms wait before -allowing further increments; if I attempt to increment during -this wait, the increment behavior reverses to decrementing. - -Additionally, it includes a 2-second hold feature to reset the -behavior to normal, celebrates milestones at specific counts, -incorporates random events that modify the count, and handles -invalid operations with an error state. +When the count reaches three, the machine enforces a 400 ms wait before +allowing further increments; if the user attempts to increment during +this wait, the machine transitions to inverted count mode, incrementing when +decrement event is received and vice versa. + +In reverse state, the user can hold the increment button for 2-seconds to reset +the behavior to normal + +celebrates milestones when the count reaches 7 + +tests for all functionality diff --git a/gpt/issues.test.ts b/gpt/issues.test.ts index 123054c89c..03f169d10e 100644 --- a/gpt/issues.test.ts +++ b/gpt/issues.test.ts @@ -1,183 +1,169 @@ -import { assign, createActor, setup, SimulatedClock } from 'xstate'; - -function logCount(_: any, params: { count: number }) { - console.log(`Current count: ${params.count}`); -} - -interface CountingMachineContext { +import { createActor, SimulatedClock } from 'xstate'; +interface CounterMachineContext { count: number; - wait: boolean; + isWaiting: boolean; + isNormalState: boolean; } -const InitialContext: CountingMachineContext = { +const initialContext: CounterMachineContext = { count: 0, - wait: false + isWaiting: false, + isNormalState: true }; - -type CountingMachineEvents = +type CounterMachineEvents = | { type: 'increment' } | { type: 'decrement' } | { type: 'reset' } - | { type: 'random' } - | { type: 'error'; message: string }; - -const countingMachine = setup({ - types: { - context: {} as CountingMachineContext, - events: {} as CountingMachineEvents - }, - actions: { - logCount, - increment: assign({ - count: ({ context }) => context.count + 1 - }), - decrement: assign({ - count: ({ context }) => context.count - 1 - }), - reverseIncrement: assign({ - count: ({ context }) => context.count - 1 - }), - resetBehavior: assign({ - wait: (_) => false - }), - applyRandomEvent: assign({ - count: ({ context }) => context.count + Math.floor(Math.random() * 10) - 5 - }), - enterErrorState: assign({ - count: (_) => 0 - }) - }, - guards: { - isAtThreshold: ({ context }) => context.count === 3, - isWaiting: ({ context }) => context.wait, - isMilestone: ({ context }) => [10, 20, 30].includes(context.count) - }, - delays: { - waitDelay: 400, - holdDelay: 2000 - } -}).createMachine({ - context: InitialContext, - initial: 'Normal', - states: { - Normal: { - entry: [ - { - type: 'logCount', - params: ({ context }) => ({ count: context.count }) + | { type: 'celebrate' } + | { type: 'random.modify'; value: number }; +const actions = { + increment: assign({ + count: ({ context }) => context.count + 1 + }), + decrement: assign({ + count: ({ context }) => context.count - 1 + }), + reverseIncrement: assign({ + count: ({ context }) => context.count - 1 + }), + celebrate: () => console.log('Celebration!'), + applyRandomModification: assign< + CounterMachineContext, + { type: 'random.modify'; value: number } + >({ + count: ({ context }, event) => context.count + event.value + }), + resetState: assign({ + isNormalState: true, + isWaiting: false + }) +}; +const guards = { + isCountThree: ({ context }: { context: CounterMachineContext }) => + context.count === 3, + isCountSeven: ({ context }: { context: CounterMachineContext }) => + context.count === 7 +}; +import { createMachine, assign, interpret } from 'xstate'; + +const counterMachine = createMachine< + CounterMachineContext, + CounterMachineEvents +>( + { + id: 'counter', + initial: 'Normal', + context: initialContext, + states: { + Normal: { + always: [{ guard: 'isCountSeven', target: 'Celebrating' }], + on: { + increment: [ + { + guard: 'isCountThree', + target: 'Waiting' + }, + { actions: 'increment' } + ], + decrement: { actions: 'decrement' }, + 'random.modify': { actions: 'applyRandomModification' }, + reset: { actions: 'resetState' } } - ], - on: { - increment: [ - { - cond: 'isWaiting', - actions: 'reverseIncrement' - }, - { - cond: 'isAtThreshold', - actions: 'increment', - target: 'Waiting' - }, - { - actions: 'increment' - } - ], - decrement: { actions: 'decrement' }, - reset: { actions: 'resetBehavior' }, - random: { actions: 'applyRandomEvent' }, - error: { target: 'Error', actions: 'enterErrorState' } }, - always: { - cond: 'isMilestone', - target: 'Milestone' - } - }, - Waiting: { - after: { - waitDelay: 'Normal' + Waiting: { + after: { + 400: { + target: 'Normal', + actions: assign({ isWaiting: false }) + } + }, + on: { + increment: { actions: 'reverseIncrement' }, + reset: { actions: 'resetState' } + }, + entry: assign({ isWaiting: true }) }, - on: { - increment: { - actions: 'reverseIncrement' - } + Celebrating: { + entry: ['celebrate'], + always: { target: 'Normal' } } }, - Milestone: { - entry: () => console.log('Milestone reached!'), - always: 'Normal' - }, - Error: { - entry: ({ event }) => console.error(`Error: ${event.message}`), - after: { - holdDelay: 'Normal' - } + on: { + 'random.modify': { actions: 'applyRandomModification' }, + reset: { actions: 'resetState' } } + }, + { + actions, + guards } -}); +); -describe('countingMachine', () => { - it('should increment and decrement the count', () => { - const actor = createActor(countingMachine).start(); +describe('counterMachine', () => { + it('should increment and transition to Waiting state at count 3', () => { + const actor = createActor(counterMachine).start(); actor.send({ type: 'increment' }); - expect(actor.getSnapshot().context.count).toEqual(1); - - actor.send({ type: 'decrement' }); - expect(actor.getSnapshot().context.count).toEqual(0); + actor.send({ type: 'increment' }); + actor.send({ type: 'increment' }); + expect(actor.getSnapshot().value).toEqual('Waiting'); + expect(actor.getSnapshot().context.count).toEqual(3); }); - it('should enforce a 400 ms wait at count 3', () => { - const clock = new SimulatedClock(); - const actor = createActor(countingMachine, { clock }).start(); + it('should reverse increment to decrement in Waiting state', () => { + const actor = createActor(counterMachine).start(); actor.send({ type: 'increment' }); actor.send({ type: 'increment' }); actor.send({ type: 'increment' }); - expect(actor.getSnapshot().value).toEqual('Waiting'); - - clock.increment(400); - expect(actor.getSnapshot().value).toEqual('Normal'); + actor.send({ type: 'increment' }); + expect(actor.getSnapshot().context.count).toEqual(3); }); - it('should reverse increment to decrement during wait', () => { - const clock = new SimulatedClock(); - const actor = createActor(countingMachine, { clock }).start(); + it('should reset state to normal after reset event', () => { + const actor = createActor(counterMachine).start(); actor.send({ type: 'increment' }); actor.send({ type: 'increment' }); actor.send({ type: 'increment' }); - expect(actor.getSnapshot().value).toEqual('Waiting'); - - actor.send({ type: 'increment' }); - expect(actor.getSnapshot().context.count).toEqual(2); + actor.send({ type: 'reset' }); + expect(actor.getSnapshot().context.isNormalState).toBeTruthy(); + expect(actor.getSnapshot().context.isWaiting).toBeFalsy(); }); - it('should reset behavior after 2 seconds in Error state', () => { - const clock = new SimulatedClock(); - const actor = createActor(countingMachine, { clock }).start(); + it('should celebrate at count 7', () => { + const logSpy = jest.spyOn(console, 'log').mockImplementation(); + const actor = createActor(counterMachine).start(); - actor.send({ type: 'error', message: 'Test Error' }); - expect(actor.getSnapshot().value).toEqual('Error'); + for (let i = 0; i < 7; i++) { + actor.send({ type: 'increment' }); + } - clock.increment(2000); - expect(actor.getSnapshot().value).toEqual('Normal'); + expect(logSpy).toHaveBeenCalledWith('Celebration!'); + logSpy.mockRestore(); }); - it('should celebrate milestones', () => { - const actor = createActor(countingMachine).start(); + it('should apply random modification to the count', () => { + const actor = createActor(counterMachine).start(); - for (let i = 0; i < 10; i++) { - actor.send({ type: 'increment' }); - } - expect(actor.getSnapshot().value).toEqual('Milestone'); + actor.send({ type: 'random.modify', value: 5 }); + expect(actor.getSnapshot().context.count).toEqual(5); }); - it('should handle random events', () => { - const actor = createActor(countingMachine).start(); + it('should transition to normal state after 400ms', () => { + const clock = new SimulatedClock(); + const actor = createActor(counterMachine, { + clock + }).start(); + + actor.send({ type: 'increment' }); + actor.send({ type: 'increment' }); + actor.send({ type: 'increment' }); + + expect(actor.getSnapshot().value).toEqual('Waiting'); + + clock.increment(400); - actor.send({ type: 'random' }); - const countAfterRandomEvent = actor.getSnapshot().context.count; - expect(countAfterRandomEvent).toBeGreaterThanOrEqual(-5); - expect(countAfterRandomEvent).toBeLessThanOrEqual(5); + expect(actor.getSnapshot().value).toEqual('Normal'); }); }); diff --git a/gpt/prompt.test.ts b/gpt/prompt.test.ts index dcfd2c22e3..d4c83ef3d2 100644 --- a/gpt/prompt.test.ts +++ b/gpt/prompt.test.ts @@ -14,6 +14,8 @@ * asking clarifying questions to establish a full picture before * starting to code. WAIT for the user to respond before writing any code. * + * 1.5! DO NOT WRITE CODE UNTIL USER HAS RESPONDED + * * 2. **Test-Driven Development** * - Write tests first based on the clarified requirements * - use the SimulatedClock instead of Jest's simulated clock. @@ -48,6 +50,10 @@ * - States and Delays: PascalCase. * - Actions: camelCase. * - Avoid using screaming snakecase, snakecase, or kebabcase. + * + * 10. Write all code in one code fence so user can easily copy and paste to test + * + * 11. if you are unsure, use browsing to check stately.ai/docs with your query */ import { assign, createActor, setup, SimulatedClock } from 'xstate'; diff --git a/gpt/todo.md b/gpt/todo.md new file mode 100644 index 0000000000..514c5fc955 --- /dev/null +++ b/gpt/todo.md @@ -0,0 +1 @@ +- [ ] Introduce parallel states From f4039266937bf9eb1ab351b91f1b24ad40b6fb8a Mon Sep 17 00:00:00 2001 From: Michael Lustig Date: Thu, 30 May 2024 06:52:56 -0400 Subject: [PATCH 21/21] iterating on test --- gpt/issues.test.ts | 275 ++++++++++++++++++++++++--------------------- gpt/prompt.test.ts | 2 +- 2 files changed, 146 insertions(+), 131 deletions(-) diff --git a/gpt/issues.test.ts b/gpt/issues.test.ts index 03f169d10e..597197f1d4 100644 --- a/gpt/issues.test.ts +++ b/gpt/issues.test.ts @@ -1,169 +1,184 @@ -import { createActor, SimulatedClock } from 'xstate'; -interface CounterMachineContext { +import { assign, createActor, setup, SimulatedClock } from 'xstate'; + +function celebrate(_: any) { + console.log('Celebration! Count reached 7!'); +} + +interface CountingContext { count: number; - isWaiting: boolean; - isNormalState: boolean; } -const initialContext: CounterMachineContext = { - count: 0, - isWaiting: false, - isNormalState: true +const initialContext: CountingContext = { + count: 0 }; -type CounterMachineEvents = + +type CountingEvents = | { type: 'increment' } | { type: 'decrement' } - | { type: 'reset' } - | { type: 'celebrate' } - | { type: 'random.modify'; value: number }; -const actions = { - increment: assign({ - count: ({ context }) => context.count + 1 - }), - decrement: assign({ - count: ({ context }) => context.count - 1 - }), - reverseIncrement: assign({ - count: ({ context }) => context.count - 1 - }), - celebrate: () => console.log('Celebration!'), - applyRandomModification: assign< - CounterMachineContext, - { type: 'random.modify'; value: number } - >({ - count: ({ context }, event) => context.count + event.value - }), - resetState: assign({ - isNormalState: true, - isWaiting: false - }) -}; -const guards = { - isCountThree: ({ context }: { context: CounterMachineContext }) => - context.count === 3, - isCountSeven: ({ context }: { context: CounterMachineContext }) => - context.count === 7 -}; -import { createMachine, assign, interpret } from 'xstate'; + | { type: 'holdIncrement' }; -const counterMachine = createMachine< - CounterMachineContext, - CounterMachineEvents ->( - { - id: 'counter', - initial: 'Normal', - context: initialContext, - states: { - Normal: { - always: [{ guard: 'isCountSeven', target: 'Celebrating' }], - on: { - increment: [ - { - guard: 'isCountThree', - target: 'Waiting' - }, - { actions: 'increment' } - ], - decrement: { actions: 'decrement' }, - 'random.modify': { actions: 'applyRandomModification' }, - reset: { actions: 'resetState' } +const countingMachine = setup({ + types: { + context: {} as CountingContext, + events: {} as CountingEvents + }, + actions: { + increment: assign({ + count: ({ context }) => context.count + 1 + }), + decrement: assign({ + count: ({ context }) => context.count - 1 + }), + invertIncrement: assign({ + count: ({ context }) => context.count - 1 + }), + invertDecrement: assign({ + count: ({ context }) => context.count + 1 + }), + celebrate + }, + guards: { + reachedThreshold: ({ context }) => context.count === 3, + reachedMilestone: ({ context }) => context.count === 7 + }, + delays: { + waitDelay: 400 + } +}).createMachine({ + context: initialContext, + initial: 'normal', + states: { + normal: { + on: { + increment: [ + { + target: 'delayed', + guard: 'reachedThreshold' + }, + { + actions: 'increment', + guard: 'reachedMilestone', + internal: false, + after: { + type: 'celebrate' + } + }, + { + actions: 'increment' + } + ], + decrement: { + actions: 'decrement' } + } + }, + delayed: { + after: { + waitDelay: 'normal' }, - Waiting: { - after: { - 400: { - target: 'Normal', - actions: assign({ isWaiting: false }) - } + on: { + increment: { + target: 'inverted' + } + } + }, + inverted: { + on: { + increment: { + actions: 'invertIncrement' }, - on: { - increment: { actions: 'reverseIncrement' }, - reset: { actions: 'resetState' } + decrement: { + actions: 'invertDecrement' }, - entry: assign({ isWaiting: true }) - }, - Celebrating: { - entry: ['celebrate'], - always: { target: 'Normal' } + holdIncrement: { + target: 'normal', + internal: false, + delay: 2000 + } } - }, - on: { - 'random.modify': { actions: 'applyRandomModification' }, - reset: { actions: 'resetState' } } - }, - { - actions, - guards } -); +}); -describe('counterMachine', () => { - it('should increment and transition to Waiting state at count 3', () => { - const actor = createActor(counterMachine).start(); +// Type tests to ensure type satisfaction +import { + type ActorRefFrom, + type SnapshotFrom, + type EventFromLogic +} from 'xstate'; + +// Strongly-typed actor reference +type CountingActorRef = ActorRefFrom; +// @ts-expect-error +const invalidActorRef: CountingActorRef = { invalid: true }; // Should produce a type error +const validActorRef: CountingActorRef = createActor(countingMachine); // Should be valid + +// Strongly-typed snapshot +type CountingSnapshot = SnapshotFrom; +// @ts-expect-error +const invalidSnapshot: CountingSnapshot = { invalid: true }; // Should produce a type error +const validSnapshot: CountingSnapshot = validActorRef.getSnapshot(); // Should be valid + +// Union of all event types +type CountingEvent = EventFromLogic; +// @ts-expect-error +const invalidEvent: CountingEvent = { invalid: true }; // Should produce a type error +const validEvent: CountingEvent = { type: 'increment' }; // Should be valid + +describe('countingMachine', () => { + it('should increment normally', () => { + const actor = createActor(countingMachine).start(); + actor.send({ type: 'increment' }); + expect(actor.getSnapshot().context.count).toEqual(1); + }); + it('should transition to "delayed" state when count reaches 3', () => { + const actor = createActor(countingMachine).start(); actor.send({ type: 'increment' }); actor.send({ type: 'increment' }); actor.send({ type: 'increment' }); - expect(actor.getSnapshot().value).toEqual('Waiting'); - expect(actor.getSnapshot().context.count).toEqual(3); + expect(actor.getSnapshot().matches('delayed')).toBeTruthy(); }); - it('should reverse increment to decrement in Waiting state', () => { - const actor = createActor(counterMachine).start(); - + it('should transition to "inverted" state if incremented during delay', () => { + const clock = new SimulatedClock(); + const actor = createActor(countingMachine, { clock }).start(); actor.send({ type: 'increment' }); actor.send({ type: 'increment' }); actor.send({ type: 'increment' }); actor.send({ type: 'increment' }); - expect(actor.getSnapshot().context.count).toEqual(3); + expect(actor.getSnapshot().matches('inverted')).toBeTruthy(); }); - it('should reset state to normal after reset event', () => { - const actor = createActor(counterMachine).start(); - + it('should increment when decrement event is received in inverted state', () => { + const actor = createActor(countingMachine).start(); actor.send({ type: 'increment' }); actor.send({ type: 'increment' }); actor.send({ type: 'increment' }); - actor.send({ type: 'reset' }); - expect(actor.getSnapshot().context.isNormalState).toBeTruthy(); - expect(actor.getSnapshot().context.isWaiting).toBeFalsy(); - }); - - it('should celebrate at count 7', () => { - const logSpy = jest.spyOn(console, 'log').mockImplementation(); - const actor = createActor(counterMachine).start(); - - for (let i = 0; i < 7; i++) { - actor.send({ type: 'increment' }); - } - - expect(logSpy).toHaveBeenCalledWith('Celebration!'); - logSpy.mockRestore(); - }); - - it('should apply random modification to the count', () => { - const actor = createActor(counterMachine).start(); - - actor.send({ type: 'random.modify', value: 5 }); - expect(actor.getSnapshot().context.count).toEqual(5); + actor.send({ type: 'increment' }); + actor.send({ type: 'decrement' }); + expect(actor.getSnapshot().context.count).toEqual(3); }); - it('should transition to normal state after 400ms', () => { + it('should transition back to normal state when holdIncrement is held for 2 seconds', () => { const clock = new SimulatedClock(); - const actor = createActor(counterMachine, { - clock - }).start(); - + const actor = createActor(countingMachine, { clock }).start(); actor.send({ type: 'increment' }); actor.send({ type: 'increment' }); actor.send({ type: 'increment' }); + actor.send({ type: 'increment' }); + actor.send({ type: 'holdIncrement' }); + clock.increment(2000); + expect(actor.getSnapshot().matches('normal')).toBeTruthy(); + }); - expect(actor.getSnapshot().value).toEqual('Waiting'); - - clock.increment(400); - - expect(actor.getSnapshot().value).toEqual('Normal'); + it('should celebrate when count reaches 7', () => { + const actor = createActor(countingMachine).start(); + const celebrateSpy = jest.spyOn(console, 'log').mockImplementation(); + for (let i = 0; i < 7; i++) { + actor.send({ type: 'increment' }); + } + expect(celebrateSpy).toHaveBeenCalledWith('Celebration! Count reached 7!'); + celebrateSpy.mockRestore(); }); }); diff --git a/gpt/prompt.test.ts b/gpt/prompt.test.ts index d4c83ef3d2..71cdb887f7 100644 --- a/gpt/prompt.test.ts +++ b/gpt/prompt.test.ts @@ -34,7 +34,7 @@ * directly to the type, avoiding nested params. * * 6. **Use the New v5 APIs** - * - use guard instead of cond + * - guard has replaced cond. cond is deprecated * - Use the new setup API as demonstrated in the example machine, * passing in types for context, events, guards, actors, etc. *