Skip to content

Add chasm test engine for unit tests#9882

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

Add chasm test engine for unit tests#9882
awln-temporal wants to merge 10 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
Comment thread chasm/chasmtest/test_engine.go Outdated
)

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

Comment thread chasm/chasmtest/test_engine.go Outdated
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

Comment thread chasm/chasmtest/test_engine.go Outdated
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.

Comment thread chasm/chasmtest/test_engine.go Outdated
}
}

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.

Comment thread chasm/chasmtest/test_engine.go Outdated
}
}

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
Comment thread chasm/chasmtest/test_engine.go Outdated
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 thread chasm/chasmtest/test_engine.go Outdated
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.

Comment thread chasm/chasmtest/test_engine.go Outdated
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.

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 right now the implementation just assumes there can be one execution at a time, ig same discussion as below, do we want to handle conflict policies in this engine, or should we just go with a simpler solution? When would unit tests want to test out conflicting execution behavior?

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 implement a more complete solution. You basically are implementing a full in-memory CHASM engine. It should have the same semantics as the history chasm engine.

Comment thread chasm/chasmtest/test_engine.go Outdated
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.

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.

for NotifyExecution, I don't see a case where we need to signal PollingComponent especially in a unit test. I'm also not sure we need PollComponent if callers can just validate state they need? Could be missing something for PollComponent though.

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.

Put a comment in code please.

Comment thread chasm/chasmtest/test_engine.go Outdated
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?

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.

The engine should be as close to the real engine as possible. Ideally it would have the exact same behavior.

Comment thread chasm/chasmtest/test_engine.go Outdated
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"

Comment thread chasm/chasmtest/test_engine.go Outdated
}
}

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.

Comment thread chasm/chasmtest/test_engine.go Outdated
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.

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 expect chasm.Ref(c) to work.

Comment thread chasm/lib/callback/tasks_test.go Outdated
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

Comment thread chasm/lib/callback/tasks_test.go Outdated

// 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 awln-temporal force-pushed the awln/chasm-test-engine-base branch from 8a834bc to f502701 Compare April 13, 2026 14:14
@awln-temporal awln-temporal force-pushed the awln/chasm-test-engine-base branch from f502701 to 2e57fc9 Compare April 14, 2026 15:07
@awln-temporal awln-temporal requested a review from bergundy April 14, 2026 16:27
Copy link
Copy Markdown
Member

@bergundy bergundy left a comment

Choose a reason for hiding this comment

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

I would expect the test framework to be as comprehensive as our SDKs' test frameworks. Library authors should be able to test their code and get the same behavior as if they were using the "real thing".

I can approve because this is far better than what we had previously but it still feels incomplete to me and I would rather spend more time on this before declaring this task done.

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.

These helpers are a bit redundant to me but no strong objection to keep them.

Comment thread chasm/chasmtest/test_engine.go Outdated
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.

I would expect chasm.Ref(c) to work.

Comment thread chasm/chasmtest/test_engine.go Outdated
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.

I would implement a more complete solution. You basically are implementing a full in-memory CHASM engine. It should have the same semantics as the history chasm engine.

Comment thread chasm/chasmtest/test_engine.go Outdated
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.

Put a comment in code please.

Comment thread chasm/chasmtest/test_engine.go Outdated
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.

The engine should be as close to the real engine as possible. Ideally it would have the exact same behavior.

awln-temporal and others added 2 commits April 15, 2026 12:24
Use ctx.Ref(component) + DeserializeComponentRef inside a ReadComponent
callback instead, which is the idiomatic approach available on the public
chasm.Context interface.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@awln-temporal awln-temporal force-pushed the awln/chasm-test-engine-base branch from db2c277 to e7b13aa Compare April 15, 2026 17:49
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