11import { describe , expect , it } from "@effect/vitest"
22import { Effect } from "effect"
3+ import * as fc from "fast-check"
34import { beforeEach , vi } from "vitest"
45
56import type { CreateInputs } from "../../src/docker-git/menu-types.js"
@@ -72,16 +73,24 @@ const project = {
7273 targetDir : "/home/dev/project"
7374} satisfies ProjectDetails
7475
75- const projectCreatedEvent : ApiEvent = {
76+ const projectDetailsWithId = ( projectId : string ) =>
77+ ( {
78+ ...project ,
79+ id : projectId
80+ } ) satisfies ProjectDetails
81+
82+ const projectCreatedEventFor = (
83+ createdProject : ReturnType < typeof projectDetailsWithId >
84+ ) : ApiEvent => ( {
7685 at : "2026-05-13T00:00:01.000Z" ,
7786 payload : {
78- project,
79- projectId : project . id
87+ project : createdProject ,
88+ projectId : createdProject . id
8089 } ,
81- projectId : project . id ,
90+ projectId : createdProject . id ,
8291 seq : 8 ,
8392 type : "project.created"
84- }
93+ } )
8594
8695const readCreateEventHandler = ( ) => {
8796 const handler = openProjectEventStreamMock . mock . calls [ 0 ] ?. [ 1 ] ?. onEvent
@@ -91,47 +100,108 @@ const readCreateEventHandler = () => {
91100 return handler
92101}
93102
103+ const resetCreateMocks = (
104+ projectId = project . id ,
105+ cursor = 7
106+ ) => {
107+ eventStreamCloseMock . mockReset ( )
108+ loadProjectDetailsMock . mockReset ( )
109+ openProjectEventStreamMock . mockReset ( )
110+ startCreateProjectMock . mockReset ( )
111+ startCreateProjectMock . mockImplementation ( ( ) =>
112+ Effect . succeed ( {
113+ accepted : true ,
114+ cursor,
115+ projectId
116+ } )
117+ )
118+ openProjectEventStreamMock . mockImplementation ( ( ) => ( { close : eventStreamCloseMock } ) )
119+ }
120+
121+ const runCreateFlow = (
122+ createdProject : ReturnType < typeof projectDetailsWithId >
123+ ) =>
124+ Effect . gen ( function * ( _ ) {
125+ const { context, output, reloadDashboard, setMessage } = makeBrowserActionContext ( )
126+
127+ submitCreateInputs ( createInputs , context )
128+
129+ yield * _ ( waitForAssertion ( ( ) => {
130+ expect ( openProjectEventStreamMock ) . toHaveBeenCalledTimes ( 1 )
131+ } ) )
132+ readCreateEventHandler ( ) ( projectCreatedEventFor ( createdProject ) )
133+
134+ yield * _ ( waitForAssertion ( ( ) => {
135+ expect ( context . setSelectedProject ) . toHaveBeenCalledWith ( createdProject )
136+ } ) )
137+
138+ return { context, createdProject, output, reloadDashboard, setMessage }
139+ } )
140+
141+ const expectCreateFlowInvariants = (
142+ {
143+ context,
144+ createdProject,
145+ cursor,
146+ reloadDashboard
147+ } : {
148+ readonly context : ReturnType < typeof makeBrowserActionContext > [ "context" ]
149+ readonly createdProject : ReturnType < typeof projectDetailsWithId >
150+ readonly cursor : number
151+ readonly reloadDashboard : ReturnType < typeof makeBrowserActionContext > [ "reloadDashboard" ]
152+ }
153+ ) => {
154+ expect ( openProjectEventStreamMock ) . toHaveBeenCalledWith (
155+ createdProject . id ,
156+ expect . objectContaining ( { initialCursor : cursor } )
157+ )
158+ expect ( eventStreamCloseMock ) . toHaveBeenCalledTimes ( 1 )
159+ expect ( loadProjectDetailsMock ) . not . toHaveBeenCalled ( )
160+ expect ( reloadDashboard ) . toHaveBeenCalledTimes ( 1 )
161+ expect ( context . setSelectedProjectId ) . toHaveBeenCalledWith ( createdProject . id )
162+ expect ( context . setSelectedProject ) . toHaveBeenCalledWith ( createdProject )
163+ }
164+
94165describe ( "browser create project action" , ( ) => {
95166 beforeEach ( ( ) => {
96- eventStreamCloseMock . mockReset ( )
97- loadProjectDetailsMock . mockReset ( )
98- openProjectEventStreamMock . mockReset ( )
99- startCreateProjectMock . mockReset ( )
100- startCreateProjectMock . mockImplementation ( ( ) =>
101- Effect . succeed ( {
102- accepted : true ,
103- cursor : 7 ,
104- projectId : project . id
105- } )
106- )
107- openProjectEventStreamMock . mockImplementation ( ( ) => ( { close : eventStreamCloseMock } ) )
167+ resetCreateMocks ( )
108168 } )
109169
110170 it . effect ( "clones a project through the browser menu create flow" , ( ) =>
111171 Effect . gen ( function * ( _ ) {
112- const { context, output, reloadDashboard, setMessage } = makeBrowserActionContext ( )
113-
114- submitCreateInputs ( createInputs , context )
115-
116- yield * _ ( waitForAssertion ( ( ) => {
117- expect ( openProjectEventStreamMock ) . toHaveBeenCalledTimes ( 1 )
118- } ) )
119- readCreateEventHandler ( ) ( projectCreatedEvent )
120-
121- yield * _ ( waitForAssertion ( ( ) => {
122- expect ( context . setSelectedProject ) . toHaveBeenCalledWith ( project )
123- } ) )
172+ const { context, createdProject, output, reloadDashboard, setMessage } = yield * _ (
173+ runCreateFlow ( projectDetailsWithId ( project . id ) )
174+ )
124175
125176 expect ( startCreateProjectMock ) . toHaveBeenCalledWith ( expectedCreateDraft )
126- expect ( openProjectEventStreamMock ) . toHaveBeenCalledWith ( project . id , expect . objectContaining ( { initialCursor : 7 } ) )
127- expect ( eventStreamCloseMock ) . toHaveBeenCalledTimes ( 1 )
128- expect ( loadProjectDetailsMock ) . not . toHaveBeenCalled ( )
129- expect ( reloadDashboard ) . toHaveBeenCalledTimes ( 1 )
130- expect ( context . setSelectedProjectId ) . toHaveBeenCalledWith ( project . id )
177+ expectCreateFlowInvariants ( { context, createdProject, cursor : 7 , reloadDashboard } )
131178 expect ( context . setSelectedMenuIndex ) . toHaveBeenCalledWith ( 1 )
132179 expect ( setMessage ) . toHaveBeenLastCalledWith ( "Created octocat/Hello-World." )
133180 expect ( output ( ) ) . toContain ( "[create] Project creation requested" )
134181 expect ( output ( ) ) . toContain ( "[create] Project accepted: project-1" )
135182 expect ( output ( ) ) . toContain ( "[create] Project created" )
136183 } ) )
184+
185+ it . effect ( "preserves create event invariants for generated project ids and cursors" , ( ) =>
186+ Effect . tryPromise ( {
187+ catch : ( error ) => error ,
188+ try : ( ) =>
189+ fc . assert (
190+ fc . asyncProperty (
191+ fc . uuid ( ) ,
192+ fc . integer ( { min : 0 , max : 10_000 } ) ,
193+ ( projectId , cursor ) =>
194+ Effect . runPromise (
195+ Effect . gen ( function * ( _ ) {
196+ resetCreateMocks ( projectId , cursor )
197+ const createdProject = projectDetailsWithId ( projectId )
198+ const { context, reloadDashboard } = yield * _ ( runCreateFlow ( createdProject ) )
199+
200+ expectCreateFlowInvariants ( { context, createdProject, cursor, reloadDashboard } )
201+ } )
202+ )
203+ ) ,
204+ { numRuns : 25 }
205+ )
206+ } ) )
137207} )
0 commit comments