Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

1209 lines
38 KiB
Go

package indexer
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"github.com/zzet/gortex/internal/config"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
"github.com/zzet/gortex/internal/parser/languages"
)
// TestTemporalE2E_GoWorkflowToActivity exercises the full pipeline —
// parser detection → graph emission → resolver rewriting — on a tiny
// Go fixture that registers an activity + a workflow and dispatches
// the activity from the workflow body. After indexing, the
// EdgeCalls placeholder must point at the real activity function
// node and both the activity and the workflow must carry
// `temporal_role` Meta tags.
func TestTemporalE2E_GoWorkflowToActivity(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "workflow.go"), `package wf
import "go.temporal.io/sdk/workflow"
func OrderWorkflow(ctx workflow.Context, id string) error {
return workflow.ExecuteActivity(ctx, ChargeCard, id).Get(ctx, nil)
}
`)
writeFile(t, filepath.Join(dir, "activity.go"), `package wf
import "context"
func ChargeCard(ctx context.Context, id string) error {
return nil
}
`)
writeFile(t, filepath.Join(dir, "main.go"), `package wf
func setupWorker(w Worker) {
w.RegisterWorkflow(OrderWorkflow)
w.RegisterActivity(ChargeCard)
}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
// The activity function node was discovered via the
// `worker.RegisterActivity` edge and stamped temporal_role.
activityNodes := g.FindNodesByName("ChargeCard")
require.Len(t, activityNodes, 1)
activity := activityNodes[0]
assert.Equal(t, "activity", activity.Meta["temporal_role"],
"registered activity must carry temporal_role meta")
assert.Equal(t, "ChargeCard", activity.Meta["temporal_name"])
// The workflow was stamped too.
workflowNodes := g.FindNodesByName("OrderWorkflow")
require.Len(t, workflowNodes, 1)
wf := workflowNodes[0]
assert.Equal(t, "workflow", wf.Meta["temporal_role"])
// The workflow.ExecuteActivity call edge was rewritten from the
// placeholder to the real activity function.
var stubCall *graph.Edge
for _, e := range g.GetOutEdges(wf.ID) {
if e == nil || e.Meta == nil {
continue
}
if e.Meta["via"] == "temporal.stub" {
stubCall = e
break
}
}
require.NotNil(t, stubCall, "workflow must have an outbound temporal.stub edge")
assert.Equal(t, activity.ID, stubCall.To,
"stub-call edge must land on the registered activity, not the placeholder")
assert.Equal(t, graph.OriginASTResolved, stubCall.Origin)
}
// TestTemporalE2E_GoChildWorkflow exercises the same pipeline on a
// child-workflow dispatch — a different temporal_kind, same resolver
// path.
func TestTemporalE2E_GoChildWorkflow(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "parent.go"), `package wf
import "go.temporal.io/sdk/workflow"
func ParentWorkflow(ctx workflow.Context) error {
return workflow.ExecuteChildWorkflow(ctx, ChildWorkflow, 42).Get(ctx, nil)
}
`)
writeFile(t, filepath.Join(dir, "child.go"), `package wf
import "go.temporal.io/sdk/workflow"
func ChildWorkflow(ctx workflow.Context, n int) error {
return nil
}
`)
writeFile(t, filepath.Join(dir, "main.go"), `package wf
func setup(w Worker) {
w.RegisterWorkflow(ParentWorkflow)
w.RegisterWorkflow(ChildWorkflow)
}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
parent := g.FindNodesByName("ParentWorkflow")[0]
child := g.FindNodesByName("ChildWorkflow")[0]
assert.Equal(t, "workflow", parent.Meta["temporal_role"])
assert.Equal(t, "workflow", child.Meta["temporal_role"])
var stubCall *graph.Edge
for _, e := range g.GetOutEdges(parent.ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.stub" {
stubCall = e
break
}
}
require.NotNil(t, stubCall, "parent workflow must have an outbound temporal.stub edge")
assert.Equal(t, child.ID, stubCall.To)
assert.Equal(t, "workflow", stubCall.Meta["temporal_kind"])
}
// TestTemporalE2E_GoEnvDefaultActivity exercises the env-var-with-literal
// -default dispatch name: the workflow names its activity through a
// variable read from os.Getenv with a literal fallback. The pipeline must
// land the call on the default activity but at the speculative tier.
func TestTemporalE2E_GoEnvDefaultActivity(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "workflow.go"), `package wf
import (
"cmp"
"os"
"go.temporal.io/sdk/workflow"
)
func OrderWorkflow(ctx workflow.Context, id string) error {
actName := cmp.Or(os.Getenv("CHARGE_ACTIVITY"), "ChargeCard")
return workflow.ExecuteActivity(ctx, actName, id).Get(ctx, nil)
}
`)
writeFile(t, filepath.Join(dir, "activity.go"), `package wf
import "context"
func ChargeCard(ctx context.Context, id string) error {
return nil
}
`)
writeFile(t, filepath.Join(dir, "main.go"), `package wf
func setupWorker(w Worker) {
w.RegisterWorkflow(OrderWorkflow)
w.RegisterActivity(ChargeCard)
}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
wf := g.FindNodesByName("OrderWorkflow")[0]
activity := g.FindNodesByName("ChargeCard")[0]
var stubCall *graph.Edge
for _, e := range g.GetOutEdges(wf.ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.stub" {
stubCall = e
break
}
}
require.NotNil(t, stubCall, "workflow must have an outbound temporal.stub edge")
assert.Equal(t, activity.ID, stubCall.To,
"env-default dispatch must land on the default activity")
assert.Equal(t, "env_default", stubCall.Meta["temporal_name_origin"])
assert.Equal(t, "os_getenv", stubCall.Meta["temporal_env_source"])
assert.Equal(t, graph.OriginASTInferred, stubCall.Origin,
"provable os.Getenv env-default lands at the inferred (visible) tier")
_, hidden := stubCall.Meta[graph.MetaSpeculative]
assert.False(t, hidden, "os.Getenv env-default is visible by default")
}
// TestEnvFallbackResolution (G1) exercises the env-with-fallback-via-helper
// dispatch name: the workflow names its activity through a variable read
// from a project-local helper (`wfutils.GetEnvOrDefault(KEY, "Default")`)
// rather than os.Getenv directly. The helper body lives in another package
// and is invisible at extract time, so the literal 2nd argument is taken as
// the default and the call lands on the default activity at the speculative
// tier.
func TestEnvFallbackResolution(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "workflow.go"), `package wf
import (
"go.temporal.io/sdk/workflow"
)
func GetEnvOrDefault(key, def string) string { return def }
func OrderWorkflow(ctx workflow.Context, id string) error {
actName := GetEnvOrDefault("CHARGE_ACTIVITY", "ChargeCard")
return workflow.ExecuteActivity(ctx, actName, id).Get(ctx, nil)
}
`)
writeFile(t, filepath.Join(dir, "activity.go"), `package wf
import "context"
func ChargeCard(ctx context.Context, id string) error {
return nil
}
`)
writeFile(t, filepath.Join(dir, "main.go"), `package wf
func setupWorker(w Worker) {
w.RegisterWorkflow(OrderWorkflow)
w.RegisterActivity(ChargeCard)
}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
wf := g.FindNodesByName("OrderWorkflow")[0]
activity := g.FindNodesByName("ChargeCard")[0]
var stubCall *graph.Edge
for _, e := range g.GetOutEdges(wf.ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.stub" {
stubCall = e
break
}
}
require.NotNil(t, stubCall, "workflow must have an outbound temporal.stub edge")
assert.Equal(t, activity.ID, stubCall.To,
"helper env-default dispatch must land on the default activity")
assert.Equal(t, "env_default", stubCall.Meta["temporal_name_origin"])
assert.Equal(t, "allowlist", stubCall.Meta["temporal_env_source"])
assert.Equal(t, graph.OriginASTInferred, stubCall.Origin,
"allow-list helper env-default lands at the inferred (visible) tier")
_, hidden := stubCall.Meta[graph.MetaSpeculative]
assert.False(t, hidden, "allow-list env-default is visible by default")
}
// TestTemporalE2E_GoEnvHeuristicSpeculative exercises the generic recall
// layer end-to-end: a helper NOT in the allow-list but whose name contains
// "env" is recognised by the heuristic, still resolves to the default
// activity, but lands at the HIDDEN speculative tier (source=heuristic) — the
// LLM cleaning pass is what later confirms or prunes it.
func TestTemporalE2E_GoEnvHeuristicSpeculative(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "workflow.go"), `package wf
import "go.temporal.io/sdk/workflow"
func ActivityFromEnv(key, def string) string { return def }
func OrderWorkflow(ctx workflow.Context, id string) error {
actName := ActivityFromEnv("CHARGE_ACTIVITY", "ChargeCard")
return workflow.ExecuteActivity(ctx, actName, id).Get(ctx, nil)
}
`)
writeFile(t, filepath.Join(dir, "activity.go"), `package wf
import "context"
func ChargeCard(ctx context.Context, id string) error { return nil }
`)
writeFile(t, filepath.Join(dir, "main.go"), `package wf
func setupWorker(w Worker) {
w.RegisterWorkflow(OrderWorkflow)
w.RegisterActivity(ChargeCard)
}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
wf := g.FindNodesByName("OrderWorkflow")[0]
activity := g.FindNodesByName("ChargeCard")[0]
var stubCall *graph.Edge
for _, e := range g.GetOutEdges(wf.ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.stub" {
stubCall = e
break
}
}
require.NotNil(t, stubCall, "workflow must have an outbound temporal.stub edge")
assert.Equal(t, activity.ID, stubCall.To,
"heuristic env-default still resolves to the default activity")
assert.Equal(t, "heuristic", stubCall.Meta["temporal_env_source"])
assert.Equal(t, graph.OriginSpeculative, stubCall.Origin,
"heuristic env-default stays at the speculative tier")
assert.Equal(t, true, stubCall.Meta[graph.MetaSpeculative],
"heuristic env-default is hidden by default")
}
// TestTemporalE2E_GoQueryHandler exercises in-workflow handler detection:
// a workflow.SetQueryHandler call must surface as a via=temporal.handler
// edge from the enclosing workflow carrying its kind + name.
func TestTemporalE2E_GoQueryHandler(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "workflow.go"), `package wf
import "go.temporal.io/sdk/workflow"
func StatusWorkflow(ctx workflow.Context) error {
workflow.SetQueryHandler(ctx, "status", func() (string, error) { return "ok", nil })
return nil
}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
wf := g.FindNodesByName("StatusWorkflow")[0]
var handler *graph.Edge
for _, e := range g.GetOutEdges(wf.ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.handler" {
handler = e
break
}
}
require.NotNil(t, handler, "workflow must have an outbound temporal.handler edge")
assert.Equal(t, "query", handler.Meta["temporal_kind"])
assert.Equal(t, "status", handler.Meta["temporal_name"])
}
// TestTemporalE2E_GoOutboundSignalQuery exercises the consumer side of the
// signal/query namespaces through the real indexer: a workflow that signals
// an external workflow and a service that queries a running workflow must
// surface via=temporal.signal-send / via=temporal.query-call edges carrying
// the signal/query name (the 4th positional string literal).
func TestTemporalE2E_GoOutboundSignalQuery(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "orchestrator.go"), `package wf
import "go.temporal.io/sdk/workflow"
func Orchestrator(ctx workflow.Context) error {
return workflow.SignalExternalWorkflow(ctx, "order-123", "", "cancel-request", nil).Get(ctx, nil)
}
`)
writeFile(t, filepath.Join(dir, "service.go"), `package wf
type Client interface {
QueryWorkflow(ctx any, wid, rid, queryType string, args ...any) (any, error)
}
func CheckStatus(ctx any, c Client) {
c.QueryWorkflow(ctx, "order-123", "", "get-status")
}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
findOut := func(fnName, via string) *graph.Edge {
fn := g.FindNodesByName(fnName)
require.NotEmpty(t, fn, "function %s must be indexed", fnName)
for _, e := range g.GetOutEdges(fn[0].ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == via {
return e
}
}
return nil
}
sig := findOut("Orchestrator", "temporal.signal-send")
require.NotNil(t, sig, "Orchestrator must have an outbound temporal.signal-send edge")
assert.Equal(t, "signal", sig.Meta["temporal_kind"])
assert.Equal(t, "cancel-request", sig.Meta["temporal_name"])
qry := findOut("CheckStatus", "temporal.query-call")
require.NotNil(t, qry, "CheckStatus must have an outbound temporal.query-call edge")
assert.Equal(t, "query", qry.Meta["temporal_kind"])
assert.Equal(t, "get-status", qry.Meta["temporal_name"])
}
// TestTemporalE2E_GoRegisterActivitiesPlural exercises struct registration:
// w.RegisterActivities(&Activities{}) must promote every exported method of
// the struct to a temporal activity, so a workflow that dispatches one of
// those methods by name resolves to the method node.
func TestTemporalE2E_GoRegisterActivitiesPlural(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "activities.go"), `package wf
import "context"
type Activities struct{}
func (a *Activities) ChargeCard(ctx context.Context, id string) error { return nil }
func (a *Activities) Refund(ctx context.Context, id string) error { return nil }
func (a *Activities) internalHelper() {}
`)
writeFile(t, filepath.Join(dir, "workflow.go"), `package wf
import "go.temporal.io/sdk/workflow"
func OrderWorkflow(ctx workflow.Context, id string) error {
return workflow.ExecuteActivity(ctx, "ChargeCard", id).Get(ctx, nil)
}
`)
writeFile(t, filepath.Join(dir, "main.go"), `package wf
func setup(w Worker) {
w.RegisterActivities(&Activities{})
}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
wf := g.FindNodesByName("OrderWorkflow")[0]
var stubCall *graph.Edge
for _, e := range g.GetOutEdges(wf.ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.stub" {
stubCall = e
break
}
}
require.NotNil(t, stubCall, "workflow must have an outbound temporal.stub edge")
// The stub must land on the promoted ChargeCard method, which must
// carry the activity role.
charge := g.FindNodesByName("ChargeCard")
require.NotEmpty(t, charge, "ChargeCard method must be indexed")
assert.Equal(t, charge[0].ID, stubCall.To,
"dispatch must resolve to the struct's promoted method")
assert.Equal(t, "activity", charge[0].Meta["temporal_role"])
}
// TestTemporalE2E_GoServiceStartsWorkflow exercises the workflow-start
// family: a service that calls client.ExecuteWorkflow(ctx, opts, WorkflowFn)
// must get a via=temporal.start edge resolved to the registered workflow —
// the "who starts this workflow" relationship.
func TestTemporalE2E_GoServiceStartsWorkflow(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "workflow.go"), `package wf
import "go.temporal.io/sdk/workflow"
func OrderWorkflow(ctx workflow.Context, id string) error { return nil }
`)
writeFile(t, filepath.Join(dir, "service.go"), `package wf
import "go.temporal.io/sdk/client"
func StartOrder(ctx any, c client.Client, id string) error {
_, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{}, OrderWorkflow, id)
return err
}
`)
writeFile(t, filepath.Join(dir, "main.go"), `package wf
func setup(w Worker) {
w.RegisterWorkflow(OrderWorkflow)
}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
starter := g.FindNodesByName("StartOrder")
require.NotEmpty(t, starter)
wf := g.FindNodesByName("OrderWorkflow")
require.NotEmpty(t, wf)
var start *graph.Edge
for _, e := range g.GetOutEdges(starter[0].ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.start" {
start = e
break
}
}
require.NotNil(t, start, "StartOrder must have an outbound temporal.start edge")
assert.Equal(t, "workflow", start.Meta["temporal_kind"])
assert.Equal(t, "OrderWorkflow", start.Meta["temporal_name"])
assert.Equal(t, wf[0].ID, start.To,
"the start edge must resolve to the registered workflow")
}
// TestTemporalE2E_GoConstNamedActivity exercises cross-file const-value
// retention + dereference: the activity name is a string const declared in
// a separate file (the dominant real-world shape), and the dispatch names
// it through the const identifier. The pipeline must persist the const
// value and dereference it so the stub resolves to the activity.
func TestTemporalE2E_GoConstNamedActivity(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "constants.go"), `package wf
const ChargeCardActivity = "ChargeCard"
`)
writeFile(t, filepath.Join(dir, "workflow.go"), `package wf
import "go.temporal.io/sdk/workflow"
func OrderWorkflow(ctx workflow.Context, id string) error {
return workflow.ExecuteActivity(ctx, ChargeCardActivity, id).Get(ctx, nil)
}
`)
writeFile(t, filepath.Join(dir, "activity.go"), `package wf
import "context"
func ChargeCard(ctx context.Context, id string) error { return nil }
`)
writeFile(t, filepath.Join(dir, "main.go"), `package wf
func setup(w Worker) {
w.RegisterWorkflow(OrderWorkflow)
w.RegisterActivityWithOptions(ChargeCard, RegisterOptions{Name: "ChargeCard"})
}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
wf := g.FindNodesByName("OrderWorkflow")[0]
activity := g.FindNodesByName("ChargeCard")
require.NotEmpty(t, activity)
var stubCall *graph.Edge
for _, e := range g.GetOutEdges(wf.ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.stub" {
stubCall = e
break
}
}
require.NotNil(t, stubCall, "workflow must have an outbound temporal.stub edge")
assert.Equal(t, "ChargeCardActivity", stubCall.Meta["temporal_name"],
"the stub keeps the const identifier as its name")
assert.Equal(t, activity[0].ID, stubCall.To,
"the const-named dispatch must dereference to the activity")
assert.Equal(t, "ChargeCard", stubCall.Meta["temporal_const_deref"],
"the dereferenced literal value is recorded on the edge")
}
// TestTemporalE2E_CrossLangJavaStartsGoWorkflow exercises the full
// cross-language join: a Java service that creates a workflow stub for a
// workflow implemented (and registered) in Go must get a via=temporal.start
// edge that resolves to the Go workflow node, at the speculative tier.
func TestTemporalE2E_CrossLangJavaStartsGoWorkflow(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "workflow.go"), `package wf
import "go.temporal.io/sdk/workflow"
func OrderWorkflow(ctx workflow.Context, id string) error { return nil }
`)
writeFile(t, filepath.Join(dir, "main.go"), `package wf
func setup(w Worker) {
w.RegisterWorkflow(OrderWorkflow)
}
`)
writeFile(t, filepath.Join(dir, "OrderService.java"), `public class OrderService {
public void start(WorkflowClient client) {
OrderWorkflow wf = client.newWorkflowStub(OrderWorkflow.class, options);
wf.processOrder("id");
}
}
`)
g := graph.New()
idx := newTestIndexerGoJava(g)
_, err := idx.Index(dir)
require.NoError(t, err)
javaStart := g.FindNodesByName("start")
require.NotEmpty(t, javaStart, "Java start method must be indexed")
goWf := g.FindNodesByName("OrderWorkflow")
require.NotEmpty(t, goWf)
var goWfID string
for _, n := range goWf {
if n.Language == "go" {
goWfID = n.ID
}
}
require.NotEmpty(t, goWfID)
var start *graph.Edge
for _, e := range g.GetOutEdges(javaStart[0].ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.start" {
start = e
break
}
}
require.NotNil(t, start, "Java service must have an outbound temporal.start edge")
assert.Equal(t, goWfID, start.To,
"the Java start must cross-resolve to the Go workflow")
assert.Equal(t, true, start.Meta["temporal_cross_lang"])
assert.Equal(t, graph.OriginSpeculative, start.Origin)
}
// TestTemporalE2E_WrapperFollowing exercises the full wrapper-following pipeline:
// a thin dispatch wrapper forwards its `name` parameter to ExecuteActivity,
// and a workflow calls the wrapper with a literal activity name. The pipeline
// must produce a resolved temporal.stub edge from the workflow caller to the
// registered ChargeCard activity.
func TestTemporalE2E_WrapperFollowing(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "wrapper.go"), `package wf
import "go.temporal.io/sdk/workflow"
func execAct(ctx workflow.Context, name string, in any) workflow.Future {
return workflow.ExecuteActivity(ctx, name, in)
}
`)
writeFile(t, filepath.Join(dir, "workflow.go"), `package wf
import "go.temporal.io/sdk/workflow"
func OrderWorkflow(ctx workflow.Context, id string) error {
return execAct(ctx, "ChargeCard", id).Get(ctx, nil)
}
`)
writeFile(t, filepath.Join(dir, "activity.go"), `package wf
import "context"
func ChargeCard(ctx context.Context, id string) error {
return nil
}
`)
writeFile(t, filepath.Join(dir, "main.go"), `package wf
func setupWorker(w Worker) {
w.RegisterWorkflow(OrderWorkflow)
w.RegisterActivity(ChargeCard)
}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
wf := g.FindNodesByName("OrderWorkflow")
require.NotEmpty(t, wf)
activity := g.FindNodesByName("ChargeCard")
require.NotEmpty(t, activity)
// Find the temporal.stub edge from OrderWorkflow that names ChargeCard
var stubCall *graph.Edge
for _, e := range g.GetOutEdges(wf[0].ID) {
if e == nil || e.Meta == nil {
continue
}
if e.Meta["via"] == "temporal.stub" && e.Meta["temporal_name"] == "ChargeCard" {
stubCall = e
break
}
}
require.NotNil(t, stubCall,
"workflow caller must have a wrapper-synthesized temporal.stub edge for ChargeCard")
assert.Equal(t, activity[0].ID, stubCall.To,
"the wrapper-following stub must resolve to the registered ChargeCard activity")
}
// TestTemporalE2E_GoExecutorFieldDispatch exercises the full pipeline for
// executor struct-field dispatch: a struct method that dispatches via a
// field, constructed with a string literal, must resolve through the
// real indexer to the registered activity.
func TestTemporalE2E_GoExecutorFieldDispatch(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "executor.go"), `package wf
import "go.temporal.io/sdk/workflow"
type ActivityExecutor struct{ ActivityName string }
func (e ActivityExecutor) Run(ctx workflow.Context) {
workflow.ExecuteActivity(ctx, e.ActivityName)
}
`)
writeFile(t, filepath.Join(dir, "activity.go"), `package wf
import "context"
func ChargeCard(ctx context.Context) error { return nil }
`)
writeFile(t, filepath.Join(dir, "main.go"), `package wf
func setup(w Worker) {
w.RegisterActivity(ChargeCard)
_ = ActivityExecutor{ActivityName: "ChargeCard"}
}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
// Find the Run method node.
runners := g.FindNodesByName("Run")
require.NotEmpty(t, runners, "Run method must be indexed")
var runNode *graph.Node
for _, n := range runners {
if n.Language == "go" {
runNode = n
break
}
}
require.NotNil(t, runNode)
activity := g.FindNodesByName("ChargeCard")
require.NotEmpty(t, activity)
// The stub from Run must resolve to ChargeCard.
var stubCall *graph.Edge
for _, e := range g.GetOutEdges(runNode.ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.stub" {
stubCall = e
break
}
}
require.NotNil(t, stubCall, "Run must have an outbound temporal.stub edge")
assert.Equal(t, activity[0].ID, stubCall.To,
"executor-field dispatch must resolve to ChargeCard")
}
// TestTemporalE2E_GoFuncReturningConstantDispatch exercises the full pipeline
// for G2: a func that returns a string literal used as Temporal dispatch arg.
//
// func GetChargeActivityName() string { return "ChargeActivity" }
// workflow.ExecuteActivity(ctx, constants.GetChargeActivityName())
//
// After indexing, the stub edge must point at the real ChargeActivity node.
func TestTemporalE2E_GoFuncReturningConstantDispatch(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "constants.go"), `package wf
func GetChargeActivityName() string { return "ChargeActivity" }
`)
writeFile(t, filepath.Join(dir, "workflow.go"), `package wf
import "go.temporal.io/sdk/workflow"
func OrderWorkflow(ctx workflow.Context) error {
return workflow.ExecuteActivity(ctx, GetChargeActivityName()).Get(ctx, nil)
}
`)
writeFile(t, filepath.Join(dir, "activity.go"), `package wf
import "context"
func ChargeActivity(ctx context.Context) error {
return nil
}
`)
writeFile(t, filepath.Join(dir, "main.go"), `package wf
func setupWorker(w Worker) {
w.RegisterWorkflow(OrderWorkflow)
w.RegisterActivity(ChargeActivity)
}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
// ChargeActivity must be stamped as a registered activity
actNodes := g.FindNodesByName("ChargeActivity")
require.Len(t, actNodes, 1)
activity := actNodes[0]
assert.Equal(t, "activity", activity.Meta["temporal_role"])
assert.Equal(t, "ChargeActivity", activity.Meta["temporal_name"])
// OrderWorkflow must have its stub edge resolved to ChargeActivity
wfNodes := g.FindNodesByName("OrderWorkflow")
require.Len(t, wfNodes, 1)
wf := wfNodes[0]
var stubCall *graph.Edge
for _, e := range g.GetOutEdges(wf.ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.stub" {
stubCall = e
break
}
}
require.NotNil(t, stubCall, "OrderWorkflow must have an outbound temporal.stub edge")
assert.Equal(t, activity.ID, stubCall.To,
"func-returning-constant dispatch must resolve to ChargeActivity")
assert.Equal(t, graph.OriginASTResolved, stubCall.Origin)
}
// TestTemporalE2E_GoWrapperDepth2 exercises depth>1 wrapper-following: the
// dispatch name is forwarded through TWO wrapper hops (runStep -> execActivity
// -> workflow.ExecuteActivity). The iterative wrapper pass must record the
// forwarded lowercase parameter as an arg_name at each hop and chase the chain
// to a fixpoint so the caller's literal ("ProcessCancel") reaches the registered
// activity.
func TestTemporalE2E_GoWrapperDepth2(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "wrappers.go"), `package wf
import "go.temporal.io/sdk/workflow"
func runStep(ctx workflow.Context, name string, args ...any) error {
return execActivity(ctx, name, args)
}
func execActivity(ctx workflow.Context, name string, args ...any) error {
return workflow.ExecuteActivity(ctx, name, args).Get(ctx, nil)
}
`)
writeFile(t, filepath.Join(dir, "workflow.go"), `package wf
import "go.temporal.io/sdk/workflow"
func CancelWorkflow(ctx workflow.Context) error {
return runStep(ctx, "ProcessCancel", 1)
}
`)
writeFile(t, filepath.Join(dir, "activity.go"), `package wf
import "context"
func ProcessCancel(ctx context.Context, n int) error { return nil }
`)
writeFile(t, filepath.Join(dir, "main.go"), `package wf
func setupWorker(w Worker) {
w.RegisterWorkflow(CancelWorkflow)
w.RegisterActivity(ProcessCancel)
}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
wf := g.FindNodesByName("CancelWorkflow")[0]
activity := g.FindNodesByName("ProcessCancel")[0]
targets := map[string]bool{}
for _, e := range g.GetOutEdges(wf.ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.stub" && e.Meta["temporal_via_wrapper"] != nil {
targets[e.To] = true
}
}
assert.True(t, targets[activity.ID],
"depth-2 wrapper dispatch must follow runStep->execActivity and reach ProcessCancel")
}
// TestTemporalE2E_GoUnregisteredActivityByConvention exercises Pattern 2 /
// Stage 1.2: the activity function lives here but is registered by a
// separate worker-runner (no RegisterActivity in this workspace). The
// dispatch names it through a two-part const (ActivityFuncName), and the
// resolver must fall back to the function-name convention.
func TestTemporalE2E_GoUnregisteredActivityByConvention(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "activity.go"), `package wf
import "context"
// Registered elsewhere (a separate worker-runner); no RegisterActivity here.
func GetProductOfferingActivity(ctx context.Context) error { return nil }
`)
writeFile(t, filepath.Join(dir, "constants.go"), `package wf
const (
ActivityPackageName = "browse-product-catalog-activities"
ActivityFuncName = "GetProductOfferingActivity"
)
`)
writeFile(t, filepath.Join(dir, "workflow.go"), `package wf
import "go.temporal.io/sdk/workflow"
func BrowseWorkflow(ctx workflow.Context) error {
return workflow.ExecuteActivity(ctx, ActivityFuncName).Get(ctx, nil)
}
`)
writeFile(t, filepath.Join(dir, "main.go"), `package wf
func setupWorker(w Worker) {
w.RegisterWorkflow(BrowseWorkflow)
}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
wf := g.FindNodesByName("BrowseWorkflow")[0]
act := g.FindNodesByName("GetProductOfferingActivity")[0]
var stub *graph.Edge
for _, e := range g.GetOutEdges(wf.ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.stub" {
stub = e
}
}
require.NotNil(t, stub)
assert.Equal(t, act.ID, stub.To, "unregistered activity must resolve by func-name convention")
assert.Equal(t, "convention", stub.Meta["temporal_resolution_via"])
assert.Equal(t, graph.OriginASTInferred, stub.Origin, "convention match is inferred-tier")
}
// TestTemporalE2E_JavaToGoBridge links a Java @WorkflowInterface (start +
// signal + query) to the Go workflow that implements them, by canonical
// name — the cross-language bridge.
func TestTemporalE2E_JavaToGoBridge(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "order_workflow.go"), `package wf
import "go.temporal.io/sdk/workflow"
func OrderWorkflow(ctx workflow.Context, id string) error {
workflow.GetSignalChannel(ctx, "cancel-order")
workflow.SetQueryHandler(ctx, "order-status", func() (string, error) { return "ok", nil })
return nil
}
`)
writeFile(t, filepath.Join(dir, "main.go"), `package wf
func setup(w Worker) { w.RegisterWorkflow(OrderWorkflow) }
`)
writeFile(t, filepath.Join(dir, "OrderWorkflowApi.java"), `package com.example.orders;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
import io.temporal.workflow.SignalMethod;
import io.temporal.workflow.QueryMethod;
@WorkflowInterface
public interface OrderWorkflowApi {
@WorkflowMethod(name = "OrderWorkflow")
String process(String orderId);
@SignalMethod(name = "cancel-order")
void cancel(String reason);
@QueryMethod(name = "order-status")
String getStatus();
}
`)
g := graph.New()
idx := newTestIndexerGoJava(g)
_, err := idx.Index(dir)
require.NoError(t, err)
goWf := g.FindNodesByName("OrderWorkflow")[0]
// Find the Java method nodes by name.
javaMethod := func(name string) *graph.Node {
for _, n := range g.FindNodesByName(name) {
if n.Language == "java" {
return n
}
}
return nil
}
process := javaMethod("process")
cancel := javaMethod("cancel")
getStatus := javaMethod("getStatus")
require.NotNil(t, process)
require.NotNil(t, cancel)
require.NotNil(t, getStatus)
hasCrossLink := func(fromID, via string) bool {
for _, e := range g.GetOutEdges(fromID) {
if e != nil && e.Meta != nil && e.Meta["via"] == via &&
e.Meta["cross_language"] == true && e.To == goWf.ID {
return true
}
}
return false
}
assert.True(t, hasCrossLink(process.ID, "temporal.start-workflow"),
"Java @WorkflowMethod(name=OrderWorkflow) must link to the Go OrderWorkflow")
assert.True(t, hasCrossLink(cancel.ID, "temporal.signal-link"),
"Java @SignalMethod(cancel-order) must link to the Go workflow handling it")
assert.True(t, hasCrossLink(getStatus.ID, "temporal.query-link"),
"Java @QueryMethod(order-status) must link to the Go workflow handling it")
}
// TestTemporalE2E_JavaInvokerToGoBridge exercises the Java invoker detector: a
// Java class dispatches a Go workflow through a configured custom invoker
// (`invoker.invokeAsync("ProcessOrderWorkflow", …)`), both as a string literal
// (exact, register-tier) and through an env property with a literal default
// (heuristic, hidden tier). Both must emit via=temporal.stub edges the resolver
// lands on the registered Go workflow.
func TestTemporalE2E_JavaInvokerToGoBridge(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "OrderManager.java"), `package com.example;
import io.temporal.workflow.WorkflowOptions;
public class OrderManager {
private final Invoker invoker;
public String startOrder(Object input) {
WorkflowOptions options = WorkflowOptions.newBuilder()
.setTaskQueue("order-workflow").build();
return invoker.invokeAsync("ProcessOrderWorkflow", options, input).block();
}
public String startWithDefault(Object input) {
return invoker.invokeAsync(
env.getProperty("order.workflow.type", "ProcessOrderWorkflow"),
options, input).block();
}
}
`)
writeFile(t, filepath.Join(dir, "workflow.go"), `package main
import "go.temporal.io/sdk/workflow"
func ProcessOrderWorkflow(ctx workflow.Context, input string) error { return nil }
`)
writeFile(t, filepath.Join(dir, "main.go"), `package main
func setup(w Worker) { w.RegisterWorkflow(ProcessOrderWorkflow) }
`)
g := graph.New()
reg := parser.NewRegistry()
reg.Register(languages.NewGoExtractor())
reg.Register(languages.NewJavaExtractor())
languages.ConfigureTemporalJavaInvokers(reg, []string{"Invoker"}, nil)
cfg := config.Default().Index
cfg.Workers = 2
idx := New(g, reg, cfg, zap.NewNop())
_, err := idx.Index(dir)
require.NoError(t, err)
goWf := g.FindNodesByName("ProcessOrderWorkflow")[0]
javaStub := func(method string) *graph.Edge {
nodes := g.FindNodesByName(method)
require.NotEmpty(t, nodes, "java method %s must be a node", method)
for _, e := range g.GetOutEdges(nodes[0].ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.stub" {
return e
}
}
return nil
}
// Literal name → exact tier, resolves to the Go workflow, visible.
exact := javaStub("startOrder")
require.NotNil(t, exact, "startOrder must emit a temporal.stub")
assert.Equal(t, "ProcessOrderWorkflow", exact.Meta["temporal_name"])
assert.Equal(t, "workflow", exact.Meta["temporal_kind"])
assert.Equal(t, true, exact.Meta["cross_language"])
assert.Equal(t, goWf.ID, exact.To, "literal invoker dispatch resolves to the Go workflow")
// Env-property default → heuristic tier, carries the env key, also resolves.
heur := javaStub("startWithDefault")
require.NotNil(t, heur, "startWithDefault must emit a temporal.stub")
assert.Equal(t, "ProcessOrderWorkflow", heur.Meta["temporal_name"])
assert.Equal(t, "heuristic", heur.Meta["temporal_env_source"])
assert.Equal(t, "order.workflow.type", heur.Meta["temporal_env_key"])
assert.Equal(t, goWf.ID, heur.To, "env-default invoker dispatch resolves to the Go workflow")
}
// TestTemporalE2E_JavaInvokerOffWhenUnconfigured asserts zero behavioural
// change when java_temporal_invokers is not configured: the invoker call is
// left to the generic path, emitting no temporal.stub.
func TestTemporalE2E_JavaInvokerOffWhenUnconfigured(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "OrderManager.java"), `package com.example;
public class OrderManager {
private final Invoker invoker;
public String startOrder(Object input) {
return invoker.invokeAsync("ProcessOrderWorkflow", input).block();
}
}
`)
g := graph.New()
idx := newTestIndexerGoJava(g) // NOT configured with any invoker
_, err := idx.Index(dir)
require.NoError(t, err)
for _, n := range g.FindNodesByName("startOrder") {
for _, e := range g.GetOutEdges(n.ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.stub" {
t.Fatal("invoker detection must be OFF when java_temporal_invokers is unconfigured")
}
}
}
}
// TestTemporalE2E_JavaInvokerValueField exercises Java invoker priority 3: the
// dispatch name is a Spring `@Value("${key:Default}")`-injected field. The
// detector resolves the field to its SpEL literal default and lands the edge on
// the Go workflow at the heuristic tier.
func TestTemporalE2E_JavaInvokerValueField(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "OrderManager.java"), `package com.example;
import org.springframework.beans.factory.annotation.Value;
public class OrderManager {
private final Invoker invoker;
@Value("${order.workflow.type:ProcessOrderWorkflow}")
private String workflowType;
public String startConfigured(Object input) {
return invoker.invokeAsync(workflowType, options, input).block();
}
}
`)
writeFile(t, filepath.Join(dir, "workflow.go"), `package main
import "go.temporal.io/sdk/workflow"
func ProcessOrderWorkflow(ctx workflow.Context, input string) error { return nil }
`)
writeFile(t, filepath.Join(dir, "main.go"), `package main
func setup(w Worker) { w.RegisterWorkflow(ProcessOrderWorkflow) }
`)
g := graph.New()
reg := parser.NewRegistry()
reg.Register(languages.NewGoExtractor())
reg.Register(languages.NewJavaExtractor())
languages.ConfigureTemporalJavaInvokers(reg, []string{"Invoker"}, nil)
cfg := config.Default().Index
cfg.Workers = 2
idx := New(g, reg, cfg, zap.NewNop())
_, err := idx.Index(dir)
require.NoError(t, err)
goWf := g.FindNodesByName("ProcessOrderWorkflow")[0]
nodes := g.FindNodesByName("startConfigured")
require.NotEmpty(t, nodes)
var stub *graph.Edge
for _, e := range g.GetOutEdges(nodes[0].ID) {
if e != nil && e.Meta != nil && e.Meta["via"] == "temporal.stub" {
stub = e
break
}
}
require.NotNil(t, stub, "@Value-field invoker dispatch must emit a temporal.stub")
assert.Equal(t, "ProcessOrderWorkflow", stub.Meta["temporal_name"])
assert.Equal(t, "heuristic", stub.Meta["temporal_env_source"])
assert.Equal(t, goWf.ID, stub.To, "@Value default resolves to the Go workflow")
}