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

239 lines
7.1 KiB
Go

package languages
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
)
// Java extractor tests for the Temporal annotation surface. The
// resolver-side tagging (`temporal_role`) is exercised in
// internal/resolver/temporal_calls_test.go; this file pins the
// extractor's contract that every @ActivityInterface /
// @WorkflowInterface / @ActivityMethod / @SignalMethod / @QueryMethod
// annotation is materialised as an EdgeAnnotated edge pointing at a
// well-known synthetic annotation node.
// hasAnnotationEdge reports whether the extraction emitted an
// EdgeAnnotated edge from `fromID` to the canonical annotation node
// for `annoName`.
func hasAnnotationEdge(t *testing.T, edges []*graph.Edge, fromID, annoName string) bool {
t.Helper()
want := AnnotationNodeID("java", annoName)
for _, e := range edges {
if e.Kind == graph.EdgeAnnotated && e.From == fromID && e.To == want {
return true
}
}
return false
}
func TestJavaTemporal_ActivityInterfaceAnnotationEdge(t *testing.T) {
src := []byte(`@ActivityInterface
public interface OrderActivities {
void chargeCard(String id);
void shipOrder(String id);
}
`)
e := NewJavaExtractor()
result, err := e.Extract("OrderActivities.java", src)
require.NoError(t, err)
ifaces := nodesOfKind(result.Nodes, graph.KindInterface)
require.Len(t, ifaces, 1)
iface := ifaces[0]
assert.Equal(t, "OrderActivities", iface.Name)
assert.True(t, hasAnnotationEdge(t, result.Edges, iface.ID, "ActivityInterface"),
"interface must carry an EdgeAnnotated edge to annotation::java::ActivityInterface")
}
func TestJavaTemporal_WorkflowInterfaceAnnotationEdge(t *testing.T) {
src := []byte(`@WorkflowInterface
public interface OrderWorkflow {
void processOrder(String id);
}
`)
e := NewJavaExtractor()
result, err := e.Extract("OrderWorkflow.java", src)
require.NoError(t, err)
ifaces := nodesOfKind(result.Nodes, graph.KindInterface)
require.Len(t, ifaces, 1)
assert.True(t, hasAnnotationEdge(t, result.Edges, ifaces[0].ID, "WorkflowInterface"))
}
func TestJavaTemporal_MethodLevelAnnotationsCarried(t *testing.T) {
src := []byte(`public class OrderWorkflowImpl {
@SignalMethod
public void cancel() {}
@QueryMethod
public String status() { return null; }
@UpdateMethod
public void retry() {}
}
`)
e := NewJavaExtractor()
result, err := e.Extract("OrderWorkflowImpl.java", src)
require.NoError(t, err)
methods := nodesOfKind(result.Nodes, graph.KindMethod)
byName := map[string]*graph.Node{}
for _, m := range methods {
byName[m.Name] = m
}
require.Contains(t, byName, "cancel")
require.Contains(t, byName, "status")
require.Contains(t, byName, "retry")
assert.True(t, hasAnnotationEdge(t, result.Edges, byName["cancel"].ID, "SignalMethod"))
assert.True(t, hasAnnotationEdge(t, result.Edges, byName["status"].ID, "QueryMethod"))
assert.True(t, hasAnnotationEdge(t, result.Edges, byName["retry"].ID, "UpdateMethod"))
}
func TestJavaTemporal_ActivityMethodAnnotation(t *testing.T) {
src := []byte(`@ActivityInterface
public interface OrderActivities {
@ActivityMethod(name = "ChargeCard")
void chargeCard(String id);
}
`)
e := NewJavaExtractor()
result, err := e.Extract("OrderActivities.java", src)
require.NoError(t, err)
// The method-level @ActivityMethod annotation must travel
// alongside the interface-level @ActivityInterface annotation —
// both edges are needed by the resolver, neither replaces the
// other.
methods := nodesOfKind(result.Nodes, graph.KindMethod)
require.Len(t, methods, 1)
method := methods[0]
assert.True(t, hasAnnotationEdge(t, result.Edges, method.ID, "ActivityMethod"),
"method-level @ActivityMethod must emit its own EdgeAnnotated edge")
}
// temporalStartEdge returns the via=temporal.start edge originating at
// fromID, or nil.
func temporalStartEdge(edges []*graph.Edge, fromID string) *graph.Edge {
for _, e := range edges {
if e.From == fromID && e.Meta != nil && e.Meta["via"] == "temporal.start" {
return e
}
}
return nil
}
func TestJavaTemporal_NewWorkflowStubStart(t *testing.T) {
src := []byte(`public class OrderService {
public void start(WorkflowClient client) {
OrderWorkflow wf = client.newWorkflowStub(OrderWorkflow.class, options);
wf.processOrder("id");
}
}
`)
e := NewJavaExtractor()
result, err := e.Extract("OrderService.java", src)
require.NoError(t, err)
var startMethod string
for _, n := range result.Nodes {
if n.Name == "start" {
startMethod = n.ID
}
}
require.NotEmpty(t, startMethod, "start method must be indexed")
edge := temporalStartEdge(result.Edges, startMethod)
require.NotNil(t, edge, "newWorkflowStub must emit a via=temporal.start edge")
assert.Equal(t, "workflow", edge.Meta["temporal_kind"])
assert.Equal(t, "OrderWorkflow", edge.Meta["temporal_name"],
"the class literal's simple name is the canonical workflow type")
}
func TestJavaTemporal_NewUntypedWorkflowStubStart(t *testing.T) {
src := []byte(`public class OrderService {
public void start(WorkflowClient client) {
client.newUntypedWorkflowStub("OrderWorkflow");
}
}
`)
e := NewJavaExtractor()
result, err := e.Extract("OrderService.java", src)
require.NoError(t, err)
var startMethod string
for _, n := range result.Nodes {
if n.Name == "start" {
startMethod = n.ID
}
}
require.NotEmpty(t, startMethod)
edge := temporalStartEdge(result.Edges, startMethod)
require.NotNil(t, edge)
assert.Equal(t, "OrderWorkflow", edge.Meta["temporal_name"])
}
func temporalEdgeByViaFrom(edges []*graph.Edge, fromID, via string) *graph.Edge {
for _, e := range edges {
if e.From == fromID && e.Meta != nil && e.Meta["via"] == via {
return e
}
}
return nil
}
func TestJavaTemporal_UntypedStubSignalSend(t *testing.T) {
src := []byte(`public class Canceller {
public void cancel(WorkflowClient client) {
WorkflowStub stub = client.newUntypedWorkflowStub("OrderWorkflow");
stub.signal("cancel-request", null);
}
}
`)
e := NewJavaExtractor()
result, err := e.Extract("Canceller.java", src)
require.NoError(t, err)
var fromID string
for _, n := range result.Nodes {
if n.Name == "cancel" {
fromID = n.ID
}
}
require.NotEmpty(t, fromID)
edge := temporalEdgeByViaFrom(result.Edges, fromID, "temporal.signal-send")
require.NotNil(t, edge, "stub.signal on a WorkflowStub must emit a signal-send edge")
assert.Equal(t, "signal", edge.Meta["temporal_kind"])
assert.Equal(t, "cancel-request", edge.Meta["temporal_name"])
}
func TestJavaTemporal_SignalOnNonStubIgnored(t *testing.T) {
// `signal` on a receiver that is NOT a WorkflowStub must not be
// detected — the type gate keeps the common method name precise.
src := []byte(`public class Light {
public void flip(Lamp lamp) {
lamp.signal("on");
}
}
`)
e := NewJavaExtractor()
result, err := e.Extract("Light.java", src)
require.NoError(t, err)
var fromID string
for _, n := range result.Nodes {
if n.Name == "flip" {
fromID = n.ID
}
}
require.NotEmpty(t, fromID)
assert.Nil(t, temporalEdgeByViaFrom(result.Edges, fromID, "temporal.signal-send"),
"signal on a non-WorkflowStub receiver must not be detected")
}