diff --git a/.changeset/tidy-states-omit-input.md b/.changeset/tidy-states-omit-input.md new file mode 100644 index 0000000..2b2b9de --- /dev/null +++ b/.changeset/tidy-states-omit-input.md @@ -0,0 +1,8 @@ +--- +"@typeonce/effect-machine": minor +--- + +Allow state builder `.from()` calls to omit the constructor input when the +selected schema accepts `{}`. Required fields and compound or parallel child +selection remain type-safe, and omitted inputs still run through schema +construction during planning. diff --git a/README.md b/README.md index dfbcd02..dbdfecc 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,11 @@ const Counter = Machine.make({ id: "Counter", states: States.states, events: [Event.cases.Start], - initial: () => States.initial.Idle.from({}) + initial: () => States.initial.Idle.from() }).handle({ Idle: { on: { - Start: ({ target }) => target.full.Running.from({}) + Start: ({ target }) => target.full.Running.from() } }, Running: {} @@ -86,6 +86,16 @@ form is available on initial, local, branch, full, compound, parallel, and final builders. A `.from` builder result is therefore a machine construction instruction; it becomes a validated public snapshot when planning succeeds. +When `{}` is valid constructor input, omit it. Required state fields remain +required, while compound and parallel states still require their active-child +callback: + +```ts +States.initial.Idle.from() +States.initial.Form.from({ draft: "" }, (form) => form.Editing.from()) +States.initial.Flow.from((flow) => flow.Idle.from()) +``` + Tagged classes are equally valid when cases need class methods or nominal identity: @@ -112,7 +122,7 @@ const machine = Machine.make({ states: States.states, events: [Command.cases.Save], internalEvents: [InternalEvent.cases.Saved, InternalEvent.cases.SaveFailed], - initial: () => States.initial.Idle(State.cases.Idle.make({})) + initial: () => States.initial.Idle.from() }) ``` @@ -174,7 +184,7 @@ Handlers implement behavior and output computation without repeating it: const machine = Machine.make({ states: States.states, events: [], - initial: () => States.initial.Form.from({ draft: "" }, (form) => form.Editing.from({})) + initial: () => States.initial.Form.from({ draft: "" }, (form) => form.Editing.from()) }).handle({ Form: { states: { @@ -246,7 +256,7 @@ effects in `Machine.action`; actions are staged during planning and run by the managed runtime before it publishes the next state. ```ts -Save: ({ target }) => Machine.action(writeAuditLog, target.local.Saving(State.cases.Saving.make({}))) +Save: ({ target }) => Machine.action(writeAuditLog, target.local.Saving.from()) ``` The one-argument form returns `void` after staging. The two-argument form diff --git a/docs/agent-guide.md b/docs/agent-guide.md index 0219cf9..568ed59 100644 --- a/docs/agent-guide.md +++ b/docs/agent-guide.md @@ -246,6 +246,19 @@ boundary rather than throwing synchronously. This applies recursively to initial, full, local, branch, compound, parallel, final, and `local.with` builders. +If `{}` satisfies the schema's constructor input, omit it: + +```ts +target.local.Idle.from() +target.local.Flow.from((flow) => flow.Idle.from()) +``` + +This shorthand also applies to schemas whose constructor fields are all +optional or defaulted. It does not make required fields optional. Compound and +parallel builders still require a callback selecting their active child or +every active region. Omitted input is normalized to `{}` and still passes +through `schema.makeEffect`, including refinements. + ## Reading state and parents `Machine.defineStates` returns typed helpers: diff --git a/src/Machine.ts b/src/Machine.ts index 747bd8b..18dc225 100644 --- a/src/Machine.ts +++ b/src/Machine.ts @@ -536,20 +536,43 @@ type SnapshotBuilderComplete = { readonly [SnapshotBuilderConstructionTypeId]: Constructed } +type FromCallable, Result> = Arguments extends + readonly [infer Input, ...infer Rest extends ReadonlyArray] ? {} extends Input ? { + (...args: Rest): Result + (...args: Arguments): Result + } + : (...args: Arguments) => Result + : (...args: Arguments) => Result + type FromMethod, Result> = { /** * Constructs the selected state from its schema make input while the - * machine plans the resulting configuration. + * machine plans the resulting configuration. The input may be omitted when + * the schema accepts an empty constructor object. * * @since 4.0.0 */ - readonly from: (...args: Arguments) => Machine.StateConstruction + readonly from: FromCallable> } type ConstructionResult = Result | Machine.StateConstruction type UnwrapConstruction = Result extends Machine.StateConstruction ? Value : Result +type ConstructionSelectorFromCallable = {} extends Input ? { + >( + state: (builder: Builder) => Selected + ): Machine.StateConstruction> + >( + input: Input, + state: (builder: Builder) => Selected + ): Machine.StateConstruction> + } + : >( + input: Input, + state: (builder: Builder) => Selected + ) => Machine.StateConstruction> + type InitialSnapshotBuilderWithPrefix< States extends Machine.StateSchemas, Prefix extends string = "" @@ -665,14 +688,15 @@ type InitialParallelBuilder< Constructed >) & { - readonly from: ( - ...args: InitialSnapshotFromArguments - ) => InitialParallelBuilder< - States, - Prefix, - Exclude, - Regions & { readonly [Region in Key]: InitialSnapshotResult }, - true + readonly from: FromCallable< + InitialSnapshotFromArguments, + InitialParallelBuilder< + States, + Prefix, + Exclude, + Regions & { readonly [Region in Key]: InitialSnapshotResult }, + true + > > } } @@ -766,14 +790,15 @@ type FullParallelBuilder< Constructed >) & { - readonly from: ( - ...args: FullSnapshotFromArguments - ) => FullParallelBuilder< - States, - Prefix, - Exclude, - Regions & { readonly [Region in Key]: FullSnapshotResult }, - true + readonly from: FromCallable< + FullSnapshotFromArguments, + FullParallelBuilder< + States, + Prefix, + Exclude, + Regions & { readonly [Region in Key]: FullSnapshotResult }, + true + > > } } @@ -852,12 +877,11 @@ type LocalTargetMethod< ) => Result ) => Result) & { - readonly from: >>( - input: Machine.NodeSchema["~type.make.in"], - state: ( - builder: LocalTargetBuilderWithPrefix - ) => Result - ) => Machine.StateConstruction> + readonly from: ConstructionSelectorFromCallable< + Machine.NodeSchema["~type.make.in"], + LocalTargetBuilderWithPrefix, + LocalTargetResultWithPrefix + > } : & (( @@ -883,12 +907,11 @@ type LocalTargetMethod< ) => Result ) => Result) & { - readonly from: >>( - input: Machine.NodeSchema["~type.make.in"], - state: ( - builder: LocalTargetBuilderWithPrefix - ) => Result - ) => Machine.StateConstruction> + readonly from: ConstructionSelectorFromCallable< + Machine.NodeSchema["~type.make.in"], + LocalTargetBuilderWithPrefix, + LocalTargetResultWithPrefix + > } : & ((value: Machine.NodeSchema["Type"]) => Machine.Target< @@ -922,12 +945,11 @@ type LocalTargetBuilderForScope< ) => Result ) => Result) & { - readonly from: >>( - input: Machine.SchemaByIdentifier["~type.make.in"], - state: ( - builder: LocalTargetBuilderWithPrefix - ) => Result - ) => Machine.StateConstruction> + readonly from: ConstructionSelectorFromCallable< + Machine.SchemaByIdentifier["~type.make.in"], + LocalTargetBuilderWithPrefix, + LocalTargetResultWithPrefix + > } } : {} @@ -978,12 +1000,11 @@ type BranchTargetMethod< ) => Result ) => Result) & { - readonly from: >>( - input: Machine.NodeSchema["~type.make.in"], - state: ( - builder: BranchTargetBuilderWithPrefix - ) => Result - ) => Machine.StateConstruction> + readonly from: ConstructionSelectorFromCallable< + Machine.NodeSchema["~type.make.in"], + BranchTargetBuilderWithPrefix, + BranchTargetResultWithPrefix + > } & BranchTargetBuilderWithPrefix : @@ -1010,12 +1031,11 @@ type BranchTargetMethod< ) => Result ) => Result) & { - readonly from: >>( - input: Machine.NodeSchema["~type.make.in"], - state: ( - builder: BranchTargetBuilderWithPrefix - ) => Result - ) => Machine.StateConstruction> + readonly from: ConstructionSelectorFromCallable< + Machine.NodeSchema["~type.make.in"], + BranchTargetBuilderWithPrefix, + BranchTargetResultWithPrefix + > } & BranchTargetBuilderWithPrefix : @@ -4058,15 +4078,22 @@ type SnapshotBuilderOptions = { readonly prefix: string } +type FromMethodKind = "leaf" | "nested" + const withFrom = ) => unknown>( - method: Method -): Method & { readonly from: (input: unknown, ...args: ReadonlyArray) => unknown } => { + method: Method, + kind: FromMethodKind +): Method & { readonly from: (...args: ReadonlyArray) => unknown } => { Object.defineProperty(method, "from", { - value: (input: unknown, ...args: ReadonlyArray) => - Model.markStateConstruction(method(Model.makeStateInput(input), ...args)), + value: (...args: ReadonlyArray) => { + const omitted = args.length === 0 || (kind === "nested" && args.length === 1 && typeof args[0] === "function") + const input = omitted ? {} : args[0] + const rest = omitted ? args : args.slice(1) + return Model.markStateConstruction(method(Model.makeStateInput(input), ...rest)) + }, enumerable: false }) - return method as Method & { readonly from: (input: unknown, ...args: ReadonlyArray) => unknown } + return method as Method & { readonly from: (...args: ReadonlyArray) => unknown } } const makeSnapshotBuilder = ( @@ -4075,8 +4102,12 @@ const makeSnapshotBuilder = ( ): unknown => { const builder: Record = {} for (const key of Object.keys(states)) { - builder[key] = withFrom((value: unknown, selector?: (builder: unknown) => unknown) => - makeSnapshotForNode(states[key], key, value, selector, options) + const path = options.prefix === "" ? key : `${options.prefix}.${key}` + const node = Model.getStateNodeDefinition(path, states[key]) + builder[key] = withFrom( + (value: unknown, selector?: (builder: unknown) => unknown) => + makeSnapshotForNode(states[key], key, value, selector, options), + node.states === undefined ? "leaf" : "nested" ) } return builder @@ -4100,6 +4131,8 @@ const makeParallelSnapshotBuilder = ( if (hasProperty(regions, key)) { continue } + const path = options.prefix === "" ? key : `${options.prefix}.${key}` + const node = Model.getStateNodeDefinition(path, states[key]) builder[key] = withFrom((value: unknown, selector?: (builder: unknown) => unknown) => { const nextRegions: Record = {} for (const regionKey of Object.keys(regions)) { @@ -4108,7 +4141,7 @@ const makeParallelSnapshotBuilder = ( nextRegions[key] = makeSnapshotForNode(states[key], key, value, selector, options) const next = makeParallelSnapshotBuilder(states, options, nextRegions) return Model.isStateConstruction(builder) ? Model.markStateConstruction(next) : next - }) + }, node.states === undefined ? "leaf" : "nested") } return builder } @@ -4304,7 +4337,7 @@ const makeLocalTargetChildBuilder = ( extendTargetValues(values, child.path, value), source )) - }) + }, child.type === "atomic" || child.type === "final" ? "leaf" : "nested") } return builder } @@ -4324,7 +4357,7 @@ const makeLocalTargetBuilder = ( throw new Error(`Machine expected target "${scope}" builder to provide an active child state`) } return selector(makeLocalTargetChildBuilder(states, stateNodes, scope, { [scope]: value }, source)) - }) + }, "nested") return builder } @@ -4352,7 +4385,7 @@ const makeBranchTargetNodeBuilder = ( ): unknown => { const node = getTargetBuilderNode(stateNodes, path) if (node.type === "atomic" || node.type === "final") { - return withFrom((value: unknown) => makeTargetWithValues(node.path, value, values)) + return withFrom((value: unknown) => makeTargetWithValues(node.path, value, values), "leaf") } const builder = withFrom((value: unknown, selector?: (builder: unknown) => unknown) => { if (node.type === "parallel") { @@ -4386,7 +4419,7 @@ const makeBranchTargetNodeBuilder = ( source ) return selector(nextBuilder) - }) as unknown as Record + }, "nested") as unknown as Record if (node.type !== "parallel" || source === node.path || source.startsWith(`${node.path}.`)) { addBranchTargetChildren(builder, states, stateNodes, node.path, values, source) } @@ -4445,7 +4478,7 @@ const makeTargetBuilder = ( * Machine.make({ * states: States.states, * events: [], - * initial: () => States.initial.idle(new Idle({})) + * initial: () => States.initial.idle.from() * }) * ``` * diff --git a/test/Machine.test.ts b/test/Machine.test.ts index 2d7588c..0846c89 100644 --- a/test/Machine.test.ts +++ b/test/Machine.test.ts @@ -589,7 +589,7 @@ describe("Machine", () => { const machine = Machine.make({ states: states.states, events: [Event], - initial: () => states.initial.Idle.from({}) + initial: () => states.initial.Idle.from() }).handle({ Idle: { on: { @@ -611,6 +611,192 @@ describe("Machine", () => { assert.isTrue(planned.done) })) + it.effect("constructs default-only TaggedClass state without an input argument", () => + Effect.gen(function*() { + class DefaultOnly extends Schema.TaggedClass("DefaultOnly")("DefaultOnly", { + label: Schema.String.pipe( + Schema.optionalKey, + Schema.withConstructorDefault(Effect.succeed("default-label")) + ) + }) {} + const states = Machine.defineStates({ DefaultOnly }) + const machine = Machine.make({ + id: "from-default-only", + states: states.states, + events: [], + initial: () => states.initial.DefaultOnly.from() + }) + + const planned = yield* Machine.planInitial(machine) + + assert.instanceOf(planned.state.value, DefaultOnly) + assert.strictEqual(planned.state.value.label, "default-label") + })) + + it.effect("constructs nested empty compound targets across local, branch, and full builders", () => + Effect.gen(function*() { + const State = Schema.TaggedUnion({ + Flow: {}, + Idle: {}, + Running: {}, + Nested: {}, + NestedIdle: {}, + Done: {} + }) + const Event = Schema.TaggedUnion({ + Local: {}, + LocalWith: {}, + Branch: {}, + Full: {}, + Finish: {} + }) + const states = Machine.defineStates({ + Flow: { + schema: State.cases.Flow, + initial: "Idle", + states: { + Idle: State.cases.Idle, + Running: State.cases.Running, + Nested: { + schema: State.cases.Nested, + initial: "NestedIdle", + states: { + NestedIdle: State.cases.NestedIdle + } + }, + Done: { + schema: State.cases.Done, + type: "final" + } + } + } + }) + const machine = Machine.make({ + id: "from-empty-targets", + states: states.states, + events: [ + Event.cases.Local, + Event.cases.LocalWith, + Event.cases.Branch, + Event.cases.Full, + Event.cases.Finish + ], + initial: () => states.initial.Flow.from((flow) => flow.Idle.from()) + }).handle({ + Flow: { + states: { + Idle: { + on: { + Local: ({ target }) => target.local.Running.from(), + LocalWith: ({ target }) => target.local.with.from((flow) => flow.Running.from()), + Branch: ({ target }) => target.branch.Flow.Nested.from((nested) => nested.NestedIdle.from()), + Full: ({ target }) => + target.full.Flow.from((flow) => flow.Nested.from((nested) => nested.NestedIdle.from())), + Finish: ({ target }) => target.local.Done.from() + } + }, + Running: {}, + Nested: { + states: { + NestedIdle: {} + } + }, + Done: {} + } + } + }) + const initial = yield* Machine.planInitial(machine) + + const local = yield* Machine.plan(machine, initial.state, Event.cases.Local.make({})) + const localWith = yield* Machine.plan(machine, initial.state, Event.cases.LocalWith.make({})) + const branch = yield* Machine.plan(machine, initial.state, Event.cases.Branch.make({})) + const full = yield* Machine.plan(machine, initial.state, Event.cases.Full.make({})) + const final = yield* Machine.plan(machine, initial.state, Event.cases.Finish.make({})) + + assert.strictEqual((local.next as any).state.path, "Flow.Running") + assert.strictEqual((localWith.next as any).state.path, "Flow.Running") + assert.strictEqual((branch.next as any).state.state.path, "Flow.Nested.NestedIdle") + assert.strictEqual((full.next as any).state.state.path, "Flow.Nested.NestedIdle") + assert.strictEqual((final.next as any).state.path, "Flow.Done") + assert.deepStrictEqual((initial.state as any).value, State.cases.Flow.make({})) + assert.deepStrictEqual((local.next as any).state.value, State.cases.Running.make({})) + assert.deepStrictEqual((branch.next as any).state.value, State.cases.Nested.make({})) + assert.deepStrictEqual((final.next as any).state.value, State.cases.Done.make({})) + })) + + it.effect("constructs every empty region of an initial parallel state", () => + Effect.gen(function*() { + const State = Schema.TaggedUnion({ + Parallel: {}, + Left: {}, + LeftIdle: {}, + Right: {}, + RightIdle: {} + }) + const states = Machine.defineStates({ + Parallel: { + schema: State.cases.Parallel, + type: "parallel", + states: { + left: { + schema: State.cases.Left, + initial: "LeftIdle", + states: { + LeftIdle: State.cases.LeftIdle + } + }, + right: { + schema: State.cases.Right, + initial: "RightIdle", + states: { + RightIdle: State.cases.RightIdle + } + } + } + } + }) + const machine = Machine.make({ + id: "from-empty-parallel", + states: states.states, + events: [], + initial: () => + states.initial.Parallel.from((parallel) => + parallel + .left.from((left) => left.LeftIdle.from()) + .right.from((right) => right.RightIdle.from()) + ) + }) + + const planned = yield* Machine.planInitial(machine) + + assert.strictEqual((planned.state as any).states.left.state.path, "Parallel.left.LeftIdle") + assert.strictEqual((planned.state as any).states.right.state.path, "Parallel.right.RightIdle") + assert.deepStrictEqual((planned.state as any).value, State.cases.Parallel.make({})) + assert.deepStrictEqual((planned.state as any).states.left.value, State.cases.Left.make({})) + assert.deepStrictEqual((planned.state as any).states.right.value, State.cases.Right.make({})) + })) + + it.effect("fails an omitted empty input refinement through MachineSchemaDecodeError", () => + Effect.gen(function*() { + const State = Schema.TaggedUnion({ Blocked: {} }) + const Blocked = State.cases.Blocked.check( + Schema.makeFilter(() => "blocked state cannot be entered") + ) + const states = Machine.defineStates({ Blocked }) + const invalid = states.initial.Blocked.from() + const machine = Machine.make({ + id: "from-empty-refinement", + states: states.states, + events: [], + initial: () => invalid + }) + + const error = yield* Effect.flip(Machine.planInitial(machine)) + + assertMachineSchemaDecodeError(error, "state", { state: "Blocked" }) + assert.strictEqual((error as Machine.MachineSchemaDecodeError).machineId, "from-empty-refinement") + })) + it.effect("fails invalid refinement input through MachineSchemaDecodeError without throwing in the builder", () => Effect.gen(function*() { const states = Machine.defineStates({ NonEmptyIdle }) diff --git a/typetest/Machine.tst.ts b/typetest/Machine.tst.ts index 8eb6580..0628090 100644 --- a/typetest/Machine.tst.ts +++ b/typetest/Machine.tst.ts @@ -1730,6 +1730,165 @@ describe("Machine", () => { ) }) + it("state builders omit empty constructor inputs across every target surface", () => { + const State = Schema.TaggedUnion({ + Flow: {}, + Idle: {}, + Running: {}, + Nested: {}, + NestedIdle: {}, + Done: {}, + Required: { value: Schema.String }, + Parallel: {}, + Left: {}, + LeftIdle: {}, + Right: {}, + RightIdle: {} + }) + class DefaultOnly extends Schema.TaggedClass("DefaultOnly")("DefaultOnly", { + label: Schema.String.pipe( + Schema.optionalKey, + Schema.withConstructorDefault(Effect.succeed("default")) + ) + }) {} + const States = Machine.defineStates({ + Flow: { + schema: State.cases.Flow, + initial: "Idle", + states: { + Idle: State.cases.Idle, + Running: State.cases.Running, + Nested: { + schema: State.cases.Nested, + initial: "NestedIdle", + states: { + NestedIdle: State.cases.NestedIdle + } + }, + Done: { + schema: State.cases.Done, + type: "final" + } + } + }, + Required: State.cases.Required, + DefaultOnly + }) + const ParallelStates = Machine.defineStates({ + Parallel: { + schema: State.cases.Parallel, + type: "parallel", + states: { + left: { + schema: State.cases.Left, + initial: "LeftIdle", + states: { + LeftIdle: State.cases.LeftIdle + } + }, + right: { + schema: State.cases.Right, + initial: "RightIdle", + states: { + RightIdle: State.cases.RightIdle + } + } + } + } + }) + type Context = Machine.Machine.HandlerContext< + typeof States.states, + readonly [typeof SignIn], + [], + "Flow.Idle", + "SignIn", + never, + never + > + type ParallelContext = Machine.Machine.HandlerContext< + typeof ParallelStates.states, + readonly [typeof SignIn], + [], + "Parallel.left.LeftIdle", + "SignIn", + never, + never + > + const context = null as unknown as Context + const parallelContext = null as unknown as ParallelContext + + const initial = States.initial.Flow.from((flow) => flow.Idle.from()) + const defaulted = States.initial.DefaultOnly.from() + const local = context.target.local.Running.from() + const localWith = context.target.local.with.from((flow) => flow.Running.from()) + const branch = context.target.branch.Flow.Nested.from((nested) => nested.NestedIdle.from()) + const full = context.target.full.Flow.from((flow) => flow.Nested.from((nested) => nested.NestedIdle.from())) + const final = context.target.local.Done.from() + const parallel = ParallelStates.initial.Parallel.from((root) => + root + .left.from((left) => left.LeftIdle.from()) + .right.from((right) => right.RightIdle.from()) + ) + const fullParallel = parallelContext.target.full.Parallel.from((root) => + root + .left.from((left) => left.LeftIdle.from()) + .right.from((right) => right.RightIdle.from()) + ) + + expect(initial).type.toBeAssignableTo< + Machine.Machine.StateConstruction> + >() + expect(defaulted).type.toBeAssignableTo< + Machine.Machine.StateConstruction> + >() + expect(local).type.toBeAssignableTo< + Machine.Machine.StateConstruction> + >() + expect(localWith).type.toBeAssignableTo< + Machine.Machine.StateConstruction> + >() + expect(branch).type.toBeAssignableTo< + Machine.Machine.StateConstruction> + >() + expect(full).type.toBeAssignableTo< + Machine.Machine.StateConstruction> + >() + expect(final).type.toBeAssignableTo< + Machine.Machine.StateConstruction> + >() + expect(parallel).type.toBeAssignableTo< + Machine.Machine.StateConstruction> + >() + expect(fullParallel).type.toBeAssignableTo< + Machine.Machine.StateConstruction> + >() + + expect(States.initial.Required.from).type.not.toBeCallableWith() + expect(context.target.full.Required.from).type.not.toBeCallableWith() + expect(States.initial.Flow.from).type.not.toBeCallableWith() + expect(ParallelStates.initial.Parallel.from).type.not.toBeCallableWith() + + const requiredContext = null as unknown as SignedOutContext + expect(UpStates.initial.up.from).type.not.toBeCallableWith( + (up: ChildBuilder) => + up + .auth.from({ userId: "guest" }, (auth) => auth.signedOut.from()) + .sync.from({ enabled: true }, (sync) => sync.idle.from()) + ) + expect(requiredContext.target.full.up.from).type.not.toBeCallableWith( + (up: ChildBuilder) => + up + .auth.from({ userId: "guest" }, (auth) => auth.signedOut.from()) + .sync.from({ enabled: true }, (sync) => sync.idle.from()) + ) + expect(requiredContext.target.local.with.from).type.not.toBeCallableWith( + (auth: ChildBuilder) => auth.signedOut.from() + ) + expect(requiredContext.target.branch.up.from).type.not.toBeCallableWith( + (up: ChildBuilder) => up.auth.signedOut.from() + ) + }) + it("from preserves parallel-region exhaustiveness", () => { const context = null as unknown as NestedIdleContext const activeContext = null as unknown as NestedActiveContext @@ -1738,12 +1897,15 @@ describe("Machine", () => { { userId: "guest" }, (auth) => auth.signedOut.from({}) ) - const target = context.target.local.work.from( - {}, - (work) => - work - .auth.from({ userId: "guest" }, (auth) => auth.signedOut.from({})) - .sync.from({ enabled: true }, (sync) => sync.idle.from({})) + const target = context.target.local.work.from((work) => + work + .auth.from({ userId: "guest" }, (auth) => auth.signedOut.from({})) + .sync.from({ enabled: true }, (sync) => sync.idle.from({})) + ) + const branch = context.target.branch.root.work.from((work) => + work + .auth.from({ userId: "guest" }, (auth) => auth.signedOut.from()) + .sync.from({ enabled: true }, (sync) => sync.idle.from()) ) const partial = activeContext.target.branch.root.work.sync.from( { enabled: true }, @@ -1758,6 +1920,11 @@ describe("Machine", () => { > >() expect(target).type.not.toHaveProperty("path") + expect(branch).type.toBeAssignableTo< + Machine.Machine.StateConstruction< + Machine.Machine.Target + > + >() expect(partial).type.toBeAssignableTo< Machine.Machine.StateConstruction< Machine.Machine.Target