Skip to content

Add chasm test engine for unit tests#9882

Open
awln-temporal wants to merge 7 commits intomainfrom
awln/chasm-test-engine-base
Open

Add chasm test engine for unit tests#9882
awln-temporal wants to merge 7 commits intomainfrom
awln/chasm-test-engine-base

Conversation

@awln-temporal
Copy link
Copy Markdown
Contributor

@awln-temporal awln-temporal commented Apr 9, 2026

What changed?

Add chasm test engine for unit tests

Why?

Currently, CHASM library unit tests need to create mock chasm engine behavior, manually wire expected framework calls to the CHASM tree/node to CloseTransaction/generate any state needed for validation. Instead, unit tests should behave similar to how they are implemented, and delegate any chasm tree creation and reading component state to chasm engine methods.

This change introduces an implementation of the test CHASM engine that mocks the NodeBackend, but implements all methods that mutate or create Component state.

How did you test it?

  • built
  • run locally and tested manually
  • covered by existing tests
  • added new unit test(s)
  • added new functional test(s)

@awln-temporal awln-temporal requested review from a team as code owners April 9, 2026 00:11
@awln-temporal awln-temporal force-pushed the awln/chasm-test-engine-base branch from 19cf028 to 171cdf0 Compare April 9, 2026 16:41
)

type (
Option[T chasm.RootComponent] func(*Engine[T])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Call this EngineOption

type (
Option[T chasm.RootComponent] func(*Engine[T])

Engine[T chasm.RootComponent] struct {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the engine need to be typed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't, it was only added for the helper func to get the Root component, but definitely not needed, removing this along with other accessor methods as mentioned below

return e
}

func WithRoot[T chasm.RootComponent](
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed, StartExecution should be used here instead to create a root.

}
}

func WithExecutionKey[T chasm.RootComponent](key chasm.ExecutionKey) Option[T] {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That will also be part of StartExecution.

}
}

func (e *Engine[T]) Root() T {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use ReadComponent to extract whatever we need from the engine.

@awln-temporal awln-temporal changed the title Add chasm test engine for engine-backed tests Add chasm test engine for unit tests Apr 10, 2026
registry *chasm.Registry
logger log.Logger
metrics metrics.Handler
executions map[executionLookupKey]*execution
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can start with this but you'll want to expand it to handle multiple runs and have a way to locate the current run for completeness.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This limits what can be tested today and we'll have to rely on functional test coverage for ID policy enforcement for example.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm wondering if this these cases should be in scope of functional tests. In this case, it's testing for the real behavior of handling conflicting IDs, while the TestEngine and real chasm_engine.go have technically different implementations.

Comment on lines +69 to +71
func (e *Engine) EngineContext() context.Context {
return chasm.NewEngineContext(context.Background(), e)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't add this, tests have their own context that they'll want to install the engine on.

return chasm.StartExecutionResult{
ExecutionKey: execution.key,
ExecutionRef: serializedRef,
Created: true,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be false when using UseExisting policy.

return serviceerror.NewUnimplemented("chasmtest.Engine.DeleteExecution")
}

func (e *Engine) NotifyExecution(chasm.ExecutionKey) {}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentionally not implemented?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, I wanted to review the important methods first to make sure we're aligned on the contract, will add DeleteExecution impl.

Comment on lines +235 to +246
HandleNextTransitionCount: func() int64 { return 2 },
HandleGetCurrentVersion: func() int64 { return 1 },
HandleGetWorkflowKey: func() definition.WorkflowKey {
return definition.NewWorkflowKey(key.NamespaceID, key.BusinessID, key.RunID)
},
HandleIsWorkflow: func() bool { return false },
HandleCurrentVersionedTransition: func() *persistencespb.VersionedTransition {
return &persistencespb.VersionedTransition{
NamespaceFailoverVersion: 1,
TransitionCount: 1,
}
},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're going to want to increment the current version on every transaction (UpdateComponent, StartExecution, UpdateWithStartExecution).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm ok, yeah I left this part out since the backend is not even accessible right now. Should we discuss what verifications would be useful to check on the MockNodeBackend?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't really care about verifications on transition counts and rather that be in scope of functional tests, should we just leave the MockNodeBackend impl as is?

key.NamespaceID = "test-namespace-id"
}
if key.BusinessID == "" {
key.BusinessID = "test-workflow-id"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
key.BusinessID = "test-workflow-id"
key.BusinessID = "test-business-id"

}
}

func normalizeExecutionKey(key chasm.ExecutionKey) chasm.ExecutionKey {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just ask test authors to create a valid execution key instead of implicitly adding defaults.

return chasm.NewEngineContext(context.Background(), e)
}

func (e *Engine) Ref(component chasm.Component) chasm.ComponentRef {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed IMHO. Test authors should already have refs to components they create using start execution.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how would they get the ref to the component within StartExecution or after? Subcomponents only get set in valueToNode map after syncSubComponents when SetRootComponent is called, so we'd either need to return this map as a result of StartExecution or something similar.

Comment on lines +83 to +87
func readCallbackFromEngine(
t *testing.T,
testEngine *chasmtest.Engine,
callback *Callback,
) *Callback {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this supposed to do? If I already have a callback component, isn't this just returning the same callback? Seems confusing to me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah not sure what I was thinking, removed


// Verify the outcome and tasks
tc.assertOutcome(t, callback, err)
tc.assertOutcome(t, readCallbackFromEngine(t, testEngine, callback), err)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just wrap this call in chasm.ReadComponent

awln-temporal and others added 3 commits April 10, 2026 17:42
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@awln-temporal awln-temporal force-pushed the awln/chasm-test-engine-base branch from 8a834bc to f502701 Compare April 13, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants