Add control logic engine, panel logic dialogs, logic-edit restriction
Introduce server-side control-logic flow graphs (cron/alarm triggers, Lua blocks) with CRUD endpoints, panel-logic lifecycle triggers and user-interaction dialog nodes, and a synthetic node-graph editor. Add an optional logic-editor allowlist (server.logic_editors) gating who may add/edit panel logic and control logic, surfaced via /api/v1/me and enforced in the API; hide logic affordances in the UI accordingly. Update README, example config, and functional/technical specs to cover all current features (plot panels, panel/control logic, local variables, access control) and refresh the in-app manual and contextual help. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package controllogic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func mustSched(t *testing.T, spec string) *Schedule {
|
||||
t.Helper()
|
||||
s, err := ParseSchedule(spec)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSchedule(%q): %v", spec, err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func TestCronEveryMinute(t *testing.T) {
|
||||
s := mustSched(t, "* * * * *")
|
||||
if !s.Match(time.Date(2026, 6, 18, 12, 34, 0, 0, time.UTC)) {
|
||||
t.Error("* * * * * should match any time")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronSpecificTime(t *testing.T) {
|
||||
s := mustSched(t, "30 9 * * *")
|
||||
if !s.Match(time.Date(2026, 6, 18, 9, 30, 0, 0, time.UTC)) {
|
||||
t.Error("should match 09:30")
|
||||
}
|
||||
if s.Match(time.Date(2026, 6, 18, 9, 31, 0, 0, time.UTC)) {
|
||||
t.Error("should not match 09:31")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronStep(t *testing.T) {
|
||||
s := mustSched(t, "*/15 * * * *")
|
||||
for _, m := range []int{0, 15, 30, 45} {
|
||||
if !s.Match(time.Date(2026, 6, 18, 1, m, 0, 0, time.UTC)) {
|
||||
t.Errorf("*/15 should match minute %d", m)
|
||||
}
|
||||
}
|
||||
if s.Match(time.Date(2026, 6, 18, 1, 7, 0, 0, time.UTC)) {
|
||||
t.Error("*/15 should not match minute 7")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronRangeAndList(t *testing.T) {
|
||||
s := mustSched(t, "0 9-17 * * 1,2,3,4,5")
|
||||
// 2026-06-18 is a Thursday (weekday 4).
|
||||
if !s.Match(time.Date(2026, 6, 18, 10, 0, 0, 0, time.UTC)) {
|
||||
t.Error("should match Thursday 10:00")
|
||||
}
|
||||
if s.Match(time.Date(2026, 6, 18, 18, 0, 0, 0, time.UTC)) {
|
||||
t.Error("should not match 18:00 (out of 9-17)")
|
||||
}
|
||||
// 2026-06-20 is a Saturday (weekday 6).
|
||||
if s.Match(time.Date(2026, 6, 20, 10, 0, 0, 0, time.UTC)) {
|
||||
t.Error("should not match Saturday")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronDOWSunday7(t *testing.T) {
|
||||
s := mustSched(t, "0 0 * * 7")
|
||||
// 2026-06-21 is a Sunday.
|
||||
if !s.Match(time.Date(2026, 6, 21, 0, 0, 0, 0, time.UTC)) {
|
||||
t.Error("7 should mean Sunday")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronDomOrDow(t *testing.T) {
|
||||
// When both day fields restricted, match either (Vixie semantics).
|
||||
s := mustSched(t, "0 0 1 * 5")
|
||||
// 2026-06-01 is a Monday — matches via day-of-month=1.
|
||||
if !s.Match(time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)) {
|
||||
t.Error("should match day-of-month 1")
|
||||
}
|
||||
// 2026-06-19 is a Friday (weekday 5) — matches via day-of-week.
|
||||
if !s.Match(time.Date(2026, 6, 19, 0, 0, 0, 0, time.UTC)) {
|
||||
t.Error("should match Friday")
|
||||
}
|
||||
// 2026-06-18 Thursday, not the 1st — no match.
|
||||
if s.Match(time.Date(2026, 6, 18, 0, 0, 0, 0, time.UTC)) {
|
||||
t.Error("should not match Thursday the 18th")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronInvalid(t *testing.T) {
|
||||
for _, bad := range []string{"* * * *", "60 * * * *", "* 24 * * *", "* * 0 * *", "a * * * *", "*/0 * * * *"} {
|
||||
if _, err := ParseSchedule(bad); err == nil {
|
||||
t.Errorf("ParseSchedule(%q) should error", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user