Files
zip/js/runner_test.go
zeekayandHanzo Dev 5dfa4ef616 plugin: a repo declares its routes, and the host discovers them
HIP-0106's plugin contract, in the framework rather than in 116 repositories.

Declaration projects the LIVE ROUTER — every method+pattern, complete by
construction, so a route a plugin serves and does not publish is still
declared. That is the analytics-ingestion outage class: the published paths
were listed, the four ingestion doors were not, and every product beacon in
the fleet answered 405.

  Declaration/Route/(*App).Declaration   what a host discovers
  /.well-known/zip/plugin.json           the same value off a running child
  (*App).Described                       `<bin> openapi <f>` / `<bin> declare <f>`
  Tenant                                 the tenancy read, with the real rule
  Delegate                               a continuation for another org
  Ask                                    dial a peer by name, invoke one op
  (*App).Peer                            peer ops on their own app, own socket
  (*App).Ops                             /healthz + /readyz + /metrics
  Config.Eager                           the one fact a router cannot show
  App.claim                              a second claim of a prefix is an ERROR
  zip/runtime -> zip/js                  a language runtime is an adaptor

Four things the reference plugin falsified while being built:

- Describe was already taken. zip.Describe(methodPath, Doc) is the doc
  registry generated code calls. The projection writer is (*App).Described,
  one call, because argv is read in one place and a main cannot forget to
  forward it.

- zip's control plane is NOT all under /.well-known/. /docs and the MCP path
  are not, so a prefix filter let a child declare them — paths outside /v1/
  that every plugin serves and whose first claimant takes the HOST's document
  and agent door. App.control now registers and records in one call, so
  leaking one is impossible rather than unlikely.

- OPS_PORT appeared in no Go file in the fleet. HIP-0119 named the paths and
  nobody implemented them, so conformance meant 116 repos hand-rolling three
  routes and a listener. It is one sibling app now, brought up by Listen.

- There is deliberately NO default ops port. A default made every second app
  in a process die on "address already in use" — a plugin has two apps in it
  and a test process has many. The deployment states OPS_PORT; a host states
  none on a child, so a child correctly owns none.

Load's duplicate claim used to return nil while the first registration kept
answering: the losing plugin's whole surface served another plugin's 404 and
nothing said so. It is now an error naming both claimants, all-or-nothing
across a plugin's prefixes, taken before anything is spawned.

a.ops was the op registry and is now a.registry — the ONE registry the
OpenAPI document, the MCP tool, the CLI command and the plane op are all
projected from. a.ops is the ops sibling.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-29 17:54:28 -07:00

140 lines
3.8 KiB
Go

package js
import (
"context"
"errors"
"fmt"
"math/rand"
"sync"
"testing"
"time"
)
// newJSRunner builds a Runner with zip's real goja engine registered as
// "js". No stubs: this is the same *JSRuntime base/gojavm wraps.
func newJSRunner(t *testing.T) Runner {
t.Helper()
rt, err := NewJSRuntime(JSOptions{PoolSize: 8})
if err != nil {
t.Fatalf("NewJSRuntime: %v", err)
}
r := NewRunner()
if err := r.Register("js", rt.Engine()); err != nil {
t.Fatalf("Register js: %v", err)
}
return r
}
// TestRunnerJSHappyPath: real goja, real arithmetic, real Export() result.
func TestRunnerJSHappyPath(t *testing.T) {
r := newJSRunner(t)
got, err := r.Run(context.Background(), "js", []byte("1+2"))
if err != nil {
t.Fatalf("Run: %v", err)
}
if got != int64(3) {
t.Fatalf("1+2 = %#v (%T), want int64(3)", got, got)
}
}
// TestRunnerJSFunctionCall defines a function and returns the result of
// calling it via a top-level expression — proving a defined function is
// reachable and invocable in the same eval.
func TestRunnerJSFunctionCall(t *testing.T) {
r := newJSRunner(t)
got, err := r.Run(context.Background(), "js",
[]byte("function add(a,b){return a+b}; add(40,2)"))
if err != nil {
t.Fatalf("Run: %v", err)
}
if got != int64(42) {
t.Fatalf("add(40,2) = %#v (%T), want int64(42)", got, got)
}
// And capture the function value itself, then confirm it round-trips
// as a callable (Export of a goja function is a Go func wrapper).
fnVal, err := r.Run(context.Background(), "js",
[]byte("function add(a,b){return a+b}; add"))
if err != nil {
t.Fatalf("Run capture fn: %v", err)
}
if fnVal == nil {
t.Fatalf("captured function is nil")
}
}
// TestRunnerJSDeadline: an infinite loop must be interrupted by ctx
// deadline. Asserts the error is context.DeadlineExceeded and the wall
// time is bounded (interrupt actually fired, not a hung worker).
func TestRunnerJSDeadline(t *testing.T) {
r := newJSRunner(t)
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
start := time.Now()
_, err := r.Run(ctx, "js", []byte("while(true){}"))
elapsed := time.Since(start)
t.Logf("deadline test elapsed: %v", elapsed)
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("err = %v, want context.DeadlineExceeded", err)
}
if elapsed > 200*time.Millisecond {
t.Fatalf("elapsed %v > 200ms: interrupt did not fire promptly", elapsed)
}
}
// TestRunnerUnknownLanguage: dispatch to an unregistered language returns
// the typed sentinel, not a panic.
func TestRunnerUnknownLanguage(t *testing.T) {
r := newJSRunner(t)
_, err := r.Run(context.Background(), "cobol", []byte("DISPLAY 'HI'."))
if !errors.Is(err, ErrUnknownLanguage) {
t.Fatalf("err = %v, want ErrUnknownLanguage", err)
}
}
// TestRunnerConcurrent: 100 goroutines each evaluate a distinct random
// expression a+b and assert the result. Run under -race this proves the
// registry RWMutex and the per-call VM isolation are sound.
func TestRunnerConcurrent(t *testing.T) {
r := newJSRunner(t)
const n = 100
var wg sync.WaitGroup
errs := make(chan error, n)
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
a := rand.Intn(10000)
b := rand.Intn(10000)
src := fmt.Sprintf("%d+%d", a, b)
got, err := r.Run(context.Background(), "js", []byte(src))
if err != nil {
errs <- fmt.Errorf("%s: %w", src, err)
return
}
if got != int64(a+b) {
errs <- fmt.Errorf("%s = %#v, want %d", src, got, a+b)
}
}()
}
wg.Wait()
close(errs)
for err := range errs {
t.Error(err)
}
}
// TestRunnerLanguages: registry reports registered languages, sorted.
func TestRunnerLanguages(t *testing.T) {
r := newJSRunner(t)
langs := r.Languages()
if len(langs) != 1 || langs[0] != "js" {
t.Fatalf("Languages() = %v, want [js]", langs)
}
}