Files
base/core/tasks_embed_test.go
zeekayandHanzo Dev 52de10a14c tasks: the embedded engine listens on a socket, not a port
tasks v1.52.4 replaced EmbedConfig.ZAPPort with Address, where a filesystem
path binds a unix socket. Nothing off this host dials the embedded engine, so
there is no port to allocate and none to collide over — two Base processes on
one machine used to fight over 9999 and the loser came up with no durable
engine at all, silently.

The default socket lives under the app's own DataDir rather than a shared temp
dir, so the collision cannot reappear one layer down: the path names whose it
is. TASKS_EMBED_ADDRESS overrides it and takes a host:port just as well, for a
deployment that does want a remote listener.

client.Options.HostPort became Address on the same release; the two dial sites
and the embed test move with it, and the test now runs over a socket.

luxfi/vm moves v1.3.1 -> v1.3.2, which is what cloud already runs. The pinned
v1.3.1 hash was not the one the checksum database holds for that tag, and its
go.mod hash was v1.3.2's — a moved tag, so the module could not be verified at
all. v1.3.2 verifies.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 23:03:48 -07:00

82 lines
2.0 KiB
Go

// Copyright © 2026 Hanzo AI. MIT License.
package core_test
import (
"context"
"path/filepath"
"testing"
"time"
"github.com/hanzoai/base/core"
"github.com/hanzoai/tasks/pkg/sdk/client"
)
// TestTasksEmbed boots a BaseApp with TASKS_EMBED=true and verifies the
// in-process Hanzo Tasks daemon is reachable over native ZAP.
//
// One Go binary, full workflow engine inside. No gRPC. No proxy. No
// upstream temporal.io. The same path Base apps take when they want
// durable workflows without running a sidecar tasksd container.
func TestTasksEmbed(t *testing.T) {
// A socket, not a port: nothing off this host dials the embedded engine, so
// there is no port to allocate and none to collide over.
sock := filepath.Join(t.TempDir(), "tasks.sock")
t.Setenv("TASKS_EMBED", "true")
t.Setenv("TASKS_EMBED_ADDRESS", sock)
t.Setenv("TASKS_NAMESPACE", "default")
app := core.NewBaseApp(core.BaseAppConfig{DataDir: t.TempDir()})
if app == nil {
t.Fatal("NewBaseApp returned nil")
}
c, err := client.Dial(client.Options{
Address: sock,
Namespace: "default",
DialTimeout: 3 * time.Second,
CallTimeout: 3 * time.Second,
})
if err != nil {
t.Fatalf("client.Dial: %v", err)
}
defer c.Close()
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
svc, status, err := c.Health(ctx)
if err != nil {
t.Fatalf("Health: %v", err)
}
if svc != "tasks" || status != "ok" {
t.Fatalf("Health = %q/%q, want tasks/ok", svc, status)
}
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
ID: "embed-smoke",
TaskQueue: "default",
}, "EmbedSmoke")
if err != nil {
t.Fatalf("ExecuteWorkflow: %v", err)
}
if run.GetID() == "" || run.GetRunID() == "" {
t.Fatalf("missing IDs: %+v", run)
}
list, err := c.ListWorkflows(ctx, "", 10, nil)
if err != nil {
t.Fatalf("ListWorkflows: %v", err)
}
found := false
for _, e := range list.Executions {
if e.WorkflowID == run.GetID() {
found = true
break
}
}
if !found {
t.Fatalf("workflow %s not in list", run.GetID())
}
}