Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
fed4910
add check_consensus_api task
pk910 May 14, 2026
3818e0d
add generate_api_compatibility_matrix task and inline markdown rendering
pk910 May 14, 2026
2596aa3
add gloas-api-check compatibility playbook
pk910 May 14, 2026
65c5659
playbook: emit numeric outputs as json so check_consensus_slot_range …
pk910 May 15, 2026
0815da7
matrix: clean json, title formatting, result_files on details endpoint
pk910 May 15, 2026
7d69188
add run_javascript task
pk910 May 18, 2026
9991551
drop generate_api_compatibility_matrix; render matrix with run_javasc…
pk910 May 18, 2026
3c398bb
playbook: prefix '.tasks' for jq query (matches existing convention)
pk910 May 18, 2026
8d28b59
playbook: walk descendant scopes so SSE rows nested in run_tasks_conc…
pk910 May 18, 2026
a925799
playbook: revert to flat .tasks walk; recursive '..' triggered jq pan…
pk910 May 18, 2026
2fe899f
playbook: collect matrix rows via per-row envVar path expressions
pk910 May 18, 2026
1b22d14
DEBUG: dump task ids
pk910 May 18, 2026
232d2d1
playbook: unnest SSE checks so they register in the root .tasks scope
pk910 May 18, 2026
ecd2004
check_consensus_api: 'subscription opened, no events' is pass
pk910 May 18, 2026
a957fc4
add test-run-level Result panel ($ASSERTOOR_TEST_RESULT)
pk910 May 18, 2026
efc8b2d
playbook: write matrix to ASSERTOOR_TEST_RESULT for run-level visibility
pk910 May 18, 2026
40ca842
api: GET /api/v1/test/{testId}/latest_result
pk910 May 18, 2026
723eb11
web-ui: api hook for latest test result
pk910 May 18, 2026
57ef477
web-ui: dashboard tile system
pk910 May 18, 2026
d7d7949
web-ui: rewrite Dashboard as configurable tile grid
pk910 May 18, 2026
bdd860f
web-ui: split-view Tests & Runs page at /runs
pk910 May 18, 2026
bd1dbd6
dashboard: compact edit-strip on small tiles
pk910 May 18, 2026
f226918
docker: bump node to 24 and stub-image gains node too
pk910 May 18, 2026
8577ae9
api: server-side dashboard config + network_status
pk910 May 18, 2026
5831eb8
dashboard: rows + drag-and-drop + palette sidebar + new tiles
pk910 May 18, 2026
1426719
dashboard: explicit save instead of live PUT
pk910 May 18, 2026
5e4150d
dashboard: optional max-height + scroll for unbounded tiles
pk910 May 18, 2026
eaf72ad
api: schedulable cron + queue position endpoints
pk910 May 18, 2026
a6de745
web-ui: prominent schedule editor in Library + Runs page
pk910 May 18, 2026
4123fba
web-ui: visually-separated StartTestModal + QueuePicker
pk910 May 18, 2026
48d762b
Merge branch 'master' into research/dashboard-rework
pk910 May 18, 2026
a8c5fa7
api: persist dashboard config via assertoor_state KV store
pk910 May 18, 2026
53d1751
lint: clear pre-existing golangci-lint + gofmt findings
pk910 May 18, 2026
d8e502b
lint: match CI's golangci-lint v2.11.3 ruleset
pk910 May 18, 2026
6f2e316
update default dashboard
pk910 May 19, 2026
d871bfe
check_consensus_api: add {recent_*} placeholders
pk910 May 19, 2026
c11a690
playbook: harden GLOAS API matrix inputs + accept varying error shapes
pk910 May 19, 2026
657a500
Merge remote-tracking branch 'origin/research/dashboard-rework' into …
pk910 May 19, 2026
eee33f1
check_consensus_api: drain Stream.Ready to unblock SSE receive loop
pk910 May 19, 2026
cdf1bbc
tasks: add small get_consensus_* primitives for live chain context
pk910 May 19, 2026
827cb50
playbook: real GLOAS chain inputs + strict response schemas + paralle…
pk910 May 19, 2026
59b0ff6
playbook: fix configVars jq object-constructor syntax
pk910 May 19, 2026
f49aa41
check_consensus_api: substitute pathParams when no chain lookups + cl…
pk910 May 19, 2026
3495152
check_consensus_api: classify generic 'route not found' replies as ❌
pk910 May 19, 2026
c907f83
check_consensus_api: tighten NOT_FOUND heuristic to distinguish frame…
pk910 May 19, 2026
d5f0667
playbook: row 2 schema matches updated PR #580 (BeaconBlock|BlockCont…
pk910 May 19, 2026
bcf9895
playbook: row 2 skip_randao_verification uses empty value
pk910 May 19, 2026
49fd6c1
playbook: tighten recent-block window to head-1 for better envelope h…
pk910 May 19, 2026
e61beb6
playbook: bump recent maxLookback to 16 to survive missed-slot bursts
pk910 May 19, 2026
1e1d240
playbook: row 7 — use spec-conformant {slot} + beacon_block_root quer…
pk910 May 19, 2026
3923009
Merge branch 'master' into research/dashboard-rework
pk910 May 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
FROM node:20-slim AS ui-builder
FROM node:24-slim AS ui-builder
WORKDIR /src
COPY web-ui/package.json web-ui/package-lock.json ./web-ui/
RUN cd web-ui && npm ci
Expand All @@ -24,6 +24,9 @@ RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-reco
curl \
make \
sudo \
gnupg \
&& curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& update-ca-certificates
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile-local
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
FROM node:20-slim AS ui-builder
FROM node:24-slim AS ui-builder
WORKDIR /src
COPY web-ui/package.json web-ui/package-lock.json ./web-ui/
RUN cd web-ui && npm ci
Expand Down Expand Up @@ -27,6 +27,9 @@ RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-reco
curl \
make \
sudo \
gnupg \
&& curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& update-ca-certificates
Expand Down
3 changes: 3 additions & 0 deletions Dockerfile-stub
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-reco
curl \
make \
sudo \
gnupg \
&& curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& update-ca-certificates
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ require (
github.com/protolambda/bls12-381-util v0.1.0 // indirect
github.com/r3labs/sse/v2 v2.10.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/supranational/blst v0.3.16 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
Expand Down
4 changes: 4 additions & 0 deletions pkg/assertoor/coordinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,10 @@ func (c *Coordinator) ScheduleTest(descriptor types.TestDescriptor, configOverri
return c.runner.ScheduleTest(descriptor, configOverrides, allowDuplicate, skipQueue)
}

func (c *Coordinator) ScheduleTestWithOptions(descriptor types.TestDescriptor, configOverrides map[string]any, opts types.ScheduleOptions) (types.TestRunner, error) {
return c.runner.ScheduleTestWithOptions(descriptor, configOverrides, opts)
}

func (c *Coordinator) startMetrics() error {
c.log.GetLogger().
Info(fmt.Sprintf("Starting metrics server on :%v", c.metricsPort))
Expand Down
67 changes: 67 additions & 0 deletions pkg/assertoor/testregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/ethpandaops/assertoor/pkg/test"
"github.com/ethpandaops/assertoor/pkg/types"
"github.com/ethpandaops/assertoor/pkg/vars"
"github.com/gorhill/cronexpr"
"github.com/jmoiron/sqlx"
"gopkg.in/yaml.v3"
)
Expand Down Expand Up @@ -393,6 +394,72 @@ func (c *TestRegistry) externalTestCfgToDB(cfgExternalTest *types.ExternalTestCo
return dbTestCfg, nil
}

// UpdateTestSchedule validates the schedule's cron expressions, then
// swaps the schedule in on the registered descriptor and writes it
// to the test_configs row. Returns an error without touching state
// if any cron expression is invalid.
func (c *TestRegistry) UpdateTestSchedule(testID string, schedule *types.TestSchedule) error {
// Validate up-front so we don't half-apply.
if schedule != nil {
for _, expr := range schedule.Cron {
if _, err := cronexpr.Parse(expr); err != nil {
return fmt.Errorf("invalid cron expression %q: %w", expr, err)
}
}
}

c.testDescriptorsMutex.Lock()
entry, ok := c.testDescriptors[testID]
c.testDescriptorsMutex.Unlock()

if !ok {
return fmt.Errorf("test %q not registered", testID)
}

// SetSchedule is on the concrete *test.Descriptor — the registry
// only handles its own type so the cast is safe here.
type scheduleSetter interface {
SetSchedule(s *types.TestSchedule)
}

setter, ok := entry.descriptor.(scheduleSetter)
if !ok {
return fmt.Errorf("descriptor for %q does not support schedule updates", testID)
}

setter.SetSchedule(schedule)

// Persist the change. Fetch existing row, mutate schedule fields,
// re-upsert — we never want to clobber yaml/config/configVars.
dbCfg, err := c.coordinator.Database().GetTestConfig(testID)
if err != nil || dbCfg == nil {
// No existing row — the test was registered without going
// through the DB path. Skip persistence; the in-memory change
// is still in effect for this process's lifetime.
return nil
}

dbCfg.ScheduleStartup = false
dbCfg.ScheduleCronYaml = ""

if schedule != nil {
dbCfg.ScheduleStartup = schedule.Startup

if len(schedule.Cron) > 0 {
cronYaml, err := yaml.Marshal(schedule.Cron)
if err != nil {
return fmt.Errorf("error encoding cron schedule: %w", err)
}

dbCfg.ScheduleCronYaml = string(cronYaml)
}
}

return c.coordinator.Database().RunTransaction(func(tx *sqlx.Tx) error {
return c.coordinator.Database().InsertTestConfig(tx, dbCfg)
})
}

func (c *TestRegistry) DeleteTest(testID string) error {
c.testDescriptorsMutex.Lock()

Expand Down
53 changes: 47 additions & 6 deletions pkg/assertoor/testrunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,38 @@ func (c *TestRunner) RemoveTestFromQueue(runID uint64) bool {
return false
}

// ScheduleTest is the legacy 4-arg entry point. New callers should
// use ScheduleTestWithOptions; this signature is kept so any external
// code (and the deprecated `skip_queue` request field) continues to
// work unchanged.
func (c *TestRunner) ScheduleTest(descriptor types.TestDescriptor, configOverrides map[string]any, allowDuplicate, skipQueue bool) (types.TestRunner, error) {
return c.ScheduleTestWithOptions(descriptor, configOverrides, types.ScheduleOptions{
AllowDuplicate: allowDuplicate,
SkipQueue: skipQueue,
})
}

// ScheduleTestWithOptions adds explicit support for inserting at a
// chosen position in the pending queue (opts.AfterRunID). Mutually
// exclusive with SkipQueue — when AfterRunID is non-zero the test is
// inserted into the regular queue regardless of SkipQueue.
func (c *TestRunner) ScheduleTestWithOptions(descriptor types.TestDescriptor, configOverrides map[string]any, opts types.ScheduleOptions) (types.TestRunner, error) {
if descriptor.Err() != nil {
return nil, fmt.Errorf("cannot create test from failed test descriptor: %w", descriptor.Err())
}

testRef, err := c.createTestRun(descriptor, configOverrides, allowDuplicate, skipQueue)
// AfterRunID always wins over SkipQueue: the user explicitly asked
// to slot the test into the queue at a known position.
if opts.AfterRunID > 0 {
opts.SkipQueue = false
}

testRef, err := c.createTestRun(descriptor, configOverrides, opts)
if err != nil {
return nil, err
}

if skipQueue {
if opts.SkipQueue {
c.offQueueNotificationChan <- testRef
} else {
select {
Expand All @@ -98,11 +119,11 @@ func (c *TestRunner) ScheduleTest(descriptor types.TestDescriptor, configOverrid
return testRef, nil
}

func (c *TestRunner) createTestRun(descriptor types.TestDescriptor, configOverrides map[string]any, allowDuplicate, skipQueue bool) (types.TestRunner, error) {
func (c *TestRunner) createTestRun(descriptor types.TestDescriptor, configOverrides map[string]any, opts types.ScheduleOptions) (types.TestRunner, error) {
c.testSchedulerMutex.Lock()
defer c.testSchedulerMutex.Unlock()

if !allowDuplicate {
if !opts.AllowDuplicate {
for _, queuedTest := range c.GetTestQueue() {
if queuedTest.TestID() == descriptor.ID() {
return nil, fmt.Errorf("test already in queue")
Expand All @@ -120,8 +141,28 @@ func (c *TestRunner) createTestRun(descriptor types.TestDescriptor, configOverri

c.testRegistryMutex.Lock()

if !skipQueue {
c.testQueue = append(c.testQueue, testRef)
if !opts.SkipQueue {
insertIdx := -1

// When the user explicitly asked to slot in after a known
// queued run, locate it and insert just behind. If the target
// isn't queued any more (already started, was cancelled, etc.)
// we fall through to "append" — never silently drop the
// schedule request.
if opts.AfterRunID > 0 {
for idx, qt := range c.testQueue {
if qt.RunID() == opts.AfterRunID {
insertIdx = idx + 1
break
}
}
}

if insertIdx < 0 || insertIdx > len(c.testQueue) {
c.testQueue = append(c.testQueue, testRef)
} else {
c.testQueue = append(c.testQueue[:insertIdx], append([]types.TestRunner{testRef}, c.testQueue[insertIdx:]...)...)
}
}

c.testRunMap[runID] = testRef
Expand Down
40 changes: 39 additions & 1 deletion pkg/db/task_results.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,46 @@ func (db *Database) GetAllTaskResultHeaders(runID uint64) ([]TaskResultHeader, e

err := db.reader.Select(&headers, `
SELECT task_id, result_type, result_index, name, size FROM task_results
WHERE run_id = $1`,
WHERE run_id = $1 AND task_id != 0`,
runID)

return headers, err
}

// testResultTaskID and testResultType are the sentinel coordinates we use
// to store the run-level "test result" markdown in the shared
// task_results table. TaskID 0 is unused by real tasks (real indices
// start at 1).
const (
testResultTaskID = 0
testResultType = "test_result"
)

// UpsertTestResult stores the run-level markdown blob (set by tasks that
// write to $ASSERTOOR_TEST_RESULT). One blob per test run.
func (db *Database) UpsertTestResult(runID uint64, data []byte) error {
return db.RunTransaction(func(tx *sqlx.Tx) error {
return db.UpsertTaskResult(tx, &TaskResult{
RunID: runID,
TaskID: testResultTaskID,
Type: testResultType,
Index: 0,
Name: "result.md",
Size: uint64(len(data)),
Data: data,
})
})
}

// GetTestResult returns the run-level markdown blob, or nil + nil error
// when none is set.
func (db *Database) GetTestResult(runID uint64) (*TaskResult, error) {
result, err := db.GetTaskResultByIndex(runID, testResultTaskID, testResultType, 0)
if err != nil {
// Surface "no rows" as nil, nil so callers can distinguish "not
// set" from "fetch failed".
return nil, nil //nolint:nilerr // sql.ErrNoRows-style miss
}

return result, nil
}
Loading