controllogic: debug value is Value (array-capable); lua get narrows to scalar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-06-24 19:50:47 +02:00
parent 088063d9cd
commit 9c1f2685a7
6 changed files with 28 additions and 23 deletions
+6 -6
View File
@@ -12,11 +12,11 @@ import (
// only meaningful when HasValue is true (e.g. an action.write's written value or // only meaningful when HasValue is true (e.g. an action.write's written value or
// a flow.if branch as 0/1); otherwise the event just marks the node as active. // a flow.if branch as 0/1); otherwise the event just marks the node as active.
type DebugEvent struct { type DebugEvent struct {
GraphID string `json:"graphId"` GraphID string `json:"graphId"`
NodeID string `json:"nodeId"` NodeID string `json:"nodeId"`
Value float64 `json:"value"` Value any `json:"value"`
HasValue bool `json:"hasValue"` HasValue bool `json:"hasValue"`
TS int64 `json:"ts"` // unix millis TS int64 `json:"ts"` // unix millis
} }
// DebugObserver receives node-execution events from running (and simulated) // DebugObserver receives node-execution events from running (and simulated)
@@ -83,7 +83,7 @@ func (e *Engine) FireTrigger(graphID, triggerID string) bool {
// emitDebug reports a node execution to the observer when the graph is watched // emitDebug reports a node execution to the observer when the graph is watched
// (or the graph is a simulate sandbox, which is always its own subscriber). // (or the graph is a simulate sandbox, which is always its own subscriber).
func (cg *compiledGraph) emitDebug(nodeID string, value float64, hasValue bool) { func (cg *compiledGraph) emitDebug(nodeID string, value Value, hasValue bool) {
if !cg.alwaysDebug { if !cg.alwaysDebug {
w, _ := cg.engine.debugWatch.Load().(map[string]bool) w, _ := cg.engine.debugWatch.Load().(map[string]bool)
if !w[cg.id] { if !w[cg.id] {
+3 -3
View File
@@ -79,7 +79,7 @@ func TestDebugObserverCapturesNodeValues(t *testing.T) {
if !ok { if !ok {
t.Fatal("no event for write node 'w'") t.Fatal("no event for write node 'w'")
} }
if !wEv.HasValue || wEv.Value != 42 { if !wEv.HasValue || wEv.Value != 42.0 {
t.Errorf("write node event = %+v, want value 42 hasValue true", wEv) t.Errorf("write node event = %+v, want value 42 hasValue true", wEv)
} }
if wEv.GraphID != "g1" { if wEv.GraphID != "g1" {
@@ -136,7 +136,7 @@ func TestFireTriggerForcesRun(t *testing.T) {
} }
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
} }
if !wEv.HasValue || wEv.Value != 42 { if !wEv.HasValue || wEv.Value != 42.0 {
t.Errorf("write node event = %+v, want value 42 hasValue true", wEv) t.Errorf("write node event = %+v, want value 42 hasValue true", wEv)
} }
@@ -187,7 +187,7 @@ func TestSimulateSuppressesWrites(t *testing.T) {
if !ok { if !ok {
t.Fatal("simulate produced no event for write node 'w'") t.Fatal("simulate produced no event for write node 'w'")
} }
if !wEv.HasValue || wEv.Value != 42 { if !wEv.HasValue || wEv.Value != 42.0 {
t.Errorf("simulate write event = %+v, want value 42 hasValue true", wEv) t.Errorf("simulate write event = %+v, want value 42 hasValue true", wEv)
} }
if wEv.GraphID != "sim" { if wEv.GraphID != "sim" {
+1 -5
View File
@@ -1053,11 +1053,7 @@ func (cg *compiledGraph) run(nodeID string, ctx *runCtx) {
case "action.write": case "action.write":
val := EvalValue(node.param("expr"), ctx.resolve) val := EvalValue(node.param("expr"), ctx.resolve)
f, isNum := val.(float64) cg.emitDebug(node.ID, val, true)
if !isNum {
f = math.NaN()
}
cg.emitDebug(node.ID, f, true)
cg.engine.write(cg, node.param("target"), val) cg.engine.write(cg, node.param("target"), val)
cg.follow(node.ID, "out", ctx) cg.follow(node.ID, "out", ctx)
+8
View File
@@ -110,3 +110,11 @@ func TestResolverReturnsLocalValue(t *testing.T) {
t.Fatalf("sum(buf) = %v", v) t.Fatalf("sum(buf) = %v", v)
} }
} }
func TestEmitDebugAcceptsArray(t *testing.T) {
g := Graph{ID: "g", Name: "n"}
cg := compile(g)
cg.engine = &Engine{} // no observer installed; emitDebug must not panic
cg.emitDebug("x", []Value{1.0, 2.0}, true)
cg.emitDebug("x", 3.0, true)
}
+4 -3
View File
@@ -1,6 +1,7 @@
package controllogic package controllogic
import ( import (
"math"
"sync" "sync"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
@@ -60,9 +61,9 @@ func (lr *luaRuntime) ensure() error {
L.SetGlobal("get", L.NewFunction(func(s *lua.LState) int { L.SetGlobal("get", L.NewFunction(func(s *lua.LState) int {
target := s.CheckString(1) target := s.CheckString(1)
ds, name, ok := parseRef(target) ds, name, ok := parseRef(target)
var v float64 v := math.NaN()
if ok && lr.curResolve != nil { // shim: Task 6 finalizes lua Value handling if ok && lr.curResolve != nil {
if f, fok := lr.curResolve(ds, name).(float64); fok { if f, isNum := lr.curResolve(ds, name).(float64); isNum {
v = f v = f
} }
} }
+6 -6
View File
@@ -55,12 +55,12 @@ func NewDebugHub(engine *controllogic.Engine, log *slog.Logger) *DebugHub {
// debugOut is the client-bound JSON form of a node-execution event. // debugOut is the client-bound JSON form of a node-execution event.
type debugOut struct { type debugOut struct {
Type string `json:"type"` // always "debugNode" Type string `json:"type"` // always "debugNode"
GraphID string `json:"graphId"` GraphID string `json:"graphId"`
NodeID string `json:"nodeId"` NodeID string `json:"nodeId"`
Value float64 `json:"value"` Value any `json:"value"`
HasValue bool `json:"hasValue"` HasValue bool `json:"hasValue"`
TS int64 `json:"ts"` TS int64 `json:"ts"`
} }
// Observe implements controllogic.DebugObserver: push the event to every client // Observe implements controllogic.DebugObserver: push the event to every client