feat(addons): add the add-on runtime, navigation and first-party add-ons

Add the add-on runtime to LocalAI: manifests, the host operation broker,
admin gating by account role, the add-on UI bundle builder and the SDK
for add-on pages. Move the navigation to Chat, Models, Add-ons and
Settings, with pinned add-ons in the sidebar and an Add-ons drawer.

Ship the first-party add-ons (voice, face, fine-tuning, quantization) as
packages for linux/amd64, linux/arm64 and darwin/arm64. Intel macOS
(darwin/amd64) is enabled in the platform files.

Squashed from the review branch. The tree is unchanged; the pre-squash
history is kept on a local backup branch.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-10-20 20:54:43 +00:00
1 parent 6fc2bc4967
commit 1604a67c1f
733 files changed
+149964 -323

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Prints what one workflow event may publish, as key=value lines:
# publish, reason, immutable, aliases, channel, signer_ref, version
#
# sh scripts/addons/channel.sh --event <name> --ref <full ref> --sha <40 hex>
# [--draft] [--prerelease]
#
# Pull requests and branch pushes never publish, and master alone publishes
# development. A tag push prepares the immutable version and moves no alias.
# A published stable release moves latest.
# Anything the rules do not name fails.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
if [ -n "${ADDONPKG:-}" ]; then
exec "$ADDONPKG" channel "$@"
fi
exec go run ./scripts/addons/cmd/addonpkg channel "$@"
+20
View File
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
// Command addonpkg is the command line of pkg/addons/packaging: build, verify
// and publish first-party add-on OCI packages. scripts/addons/package.sh and
// the add-on package workflow call it.
package main
import (
"fmt"
"os"
"github.com/mudler/LocalAI/pkg/addons/packaging"
)
func main() {
if err := packaging.Run(os.Args[1:], os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "addonpkg: %v\n", err)
os.Exit(1)
}
}
+80
View File
@@ -0,0 +1,80 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Runs the add-on UI runtime checks and prints one JSON case line per test:
# 1. the node unit tests of the SDK, loader, style scoping and registry;
# 2. the Go specs of the immutable asset route, the apps UI entry and the
# owner's lifecycle rules, through real stored keys and sessions;
# 3. the Playwright spec against the UI test server running with real
# authentication and a real add-on owner, which installs the packages of
# tests/addons/fixture/ui AFTER the host build, from a catalog on
# loopback, with the protocol fixture as their process.
# verify.sh compares the lines with the exact inventory of the esm-ui suite and
# rejects a skipped, flaky or missing case.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
cache="$root/.cache/addons"
ui="$root/core/http/react-ui"
mkdir -p "$cache/tmp" "$cache/logs"
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
export PATH="$cache/bin:$PATH"
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
done
# 1. Pure SDK, loader, style scoping and registry rules.
(cd "$ui" && node tests/unit/run.mjs esm.test.mjs)
# 2. Go: ginkgo specs of this feature, reported as case lines. The label
# "assets" selects them; every other spec of these packages belongs to the
# suites that own them.
i=0
for pkg in ./core/services/addons/apps ./core/services/addons ./core/services/addons/gateway ./core/http/endpoints/addons; do
i=$((i + 1))
go_report="$cache/logs/esm-ui-go-$i.json"
rm -f "$go_report"
go test -count=1 -p 2 -tags auth "$pkg" -ginkgo.label-filter=assets -ginkgo.json-report="$go_report" >"$cache/logs/esm-ui-go-$i.log" 2>&1 || { tail -40 "$cache/logs/esm-ui-go-$i.log" >&2; exit 1; }
python3 -I - "$go_report" <<'PY'
import json, sys
for report in json.load(open(sys.argv[1])):
for spec in report['SpecReports']:
if spec['LeafNodeType'] != 'It':
continue
labels = set(spec.get('LeafNodeLabels') or [])
for group in spec.get('ContainerHierarchyLabels') or []:
labels.update(group or [])
# A spec the label filter left out is another suite's, not skipped here.
if 'assets' not in labels:
continue
status = {'passed': 'passed', 'skipped': 'skipped'}.get(spec['State'], 'failed')
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
PY
done
# 3. The real owner and the browser.
fixture_exe="$cache/tmp/fixture-addon"
go build -p 2 -tags auth -o "$fixture_exe" ./tests/addons/fixture/cmd
port=$(python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])')
creds="$cache/logs/esm-ui-credentials.json"
rm -f "$creds"
log="$cache/logs/esm-ui-server.log"
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$port" \
--addons-fixture="$root/tests/addons/fixture/ui" --addon-executable="$fixture_exe" --fixture-out="$creds" \
--fixture-enable=uifixture@1.0.0,cssb@1.0.0,bare@1.0.0 >"$log" 2>&1 &
server=$!
trap 'kill "$server" 2>/dev/null || true; wait "$server" 2>/dev/null || true' EXIT HUP INT TERM
i=0
until [ -s "$creds" ] && python3 -I -c "import sys,urllib.request;urllib.request.urlopen('http://127.0.0.1:$port/readyz',timeout=1)" 2>/dev/null; do
i=$((i + 1))
kill -0 "$server" 2>/dev/null || { echo 'UI test server exited' >&2; tail -20 "$log" >&2; exit 1; }
[ "$i" -lt 120 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
sleep 1
done
report="$cache/logs/esm-ui-report.json"
rm -f "$report"
status=0
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$port" ADDONS_FIXTURE="$creds" ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.esm.config.js >"$cache/logs/esm-ui-playwright.log" 2>&1) || status=$?
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
exit "$status"
+212
View File
@@ -0,0 +1,212 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Runs the add-ons foundation acceptance gate and prints one JSON case line per
# check:
# 1. Go: the integration specs of tests/addons/foundation. They start the
# real owner and two frontends as separate server processes with a shared
# account store, the real mTLS carrier and the protocol fixture as the
# add-on process, and drive them through real sessions and API keys;
# 2. regression: the existing model and backend gallery specs and the process
# adapter specs, which the add-on lifecycle must not disturb. They run
# offline: core/gallery (gallery, backend gallery, OCI install paths),
# core/services/galleryop (the operations the gallery routes enqueue) and
# pkg/model (the process adapter in process_runtime.go);
# 3. the Playwright spec against the same three servers, which proves server
# authorization from the browser.
# verify.sh compares the lines with the exact inventory of the
# foundation-acceptance suite and rejects a skipped, flaky or missing case.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
cache="$root/.cache/addons"
ui="$root/core/http/react-ui"
mkdir -p "$cache/tmp" "$cache/logs"
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
export PATH="$cache/bin:$PATH"
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
for file in tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
done
[ -f "$ui/dist/index.html" ] || { echo 'missing UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
work=$(mktemp -d "$cache/tmp/foundation-gate.XXXXXX")
cluster_pid=
. "$root/scripts/addons/leaks.sh"
leaked=0
cleanup() {
code=$?
trap - EXIT HUP INT TERM
if [ -n "$cluster_pid" ]; then
# The cluster tears its servers down on SIGTERM; wait for it, bounded.
kill "$cluster_pid" 2>/dev/null || true
n=0
while kill -0 "$cluster_pid" 2>/dev/null && [ "$n" -lt 60 ]; do
n=$((n + 1))
sleep 0.5
done
kill -KILL "$cluster_pid" 2>/dev/null || true
wait "$cluster_pid" 2>/dev/null || true
fi
# Nothing that names the work or scratch directories may survive. Whatever
# does is killed, and the gate fails.
reap_leaks "$work" foundation || leaked=1
reap_leaks "$cache/tmp/" foundation paths-only || leaked=1
find "$work" -type d -exec chmod u+rwx {} + 2>/dev/null || true
rm -rf "$work"
if [ "$leaked" -ne 0 ] && [ "$code" -eq 0 ]; then
code=1
fi
exit "$code"
}
trap cleanup EXIT
trap 'exit 143' HUP INT TERM
# The binaries are built here, never taken from an earlier build: the server
# embeds the UI build and every other part of the fixture.
go build -p 2 -tags auth -o "$work/ui-test-server" ./tests/e2e-ui
go build -p 2 -tags auth -o "$work/fixture-addon" ./tests/addons/fixture/cmd
go build -p 2 -tags auth -o "$work/foundation-cluster" ./tests/addons/foundation/cmd/foundation-cluster
# One case line per spec of a Ginkgo report. $2 prefixes the case.
report_cases() {
python3 -I - "$1" "$2" <<'PY'
import json, sys
for report in json.load(open(sys.argv[1])):
for spec in report['SpecReports']:
if spec['LeafNodeType'] != 'It':
continue
status = {'passed': 'passed', 'skipped': 'skipped'}.get(spec['State'], 'failed')
print(json.dumps({'case': sys.argv[2] + spec['LeafNodeText'], 'status': status}))
PY
}
# 1. The integration specs.
go_report="$cache/logs/foundation-go.json"
rm -f "$go_report"
FOUNDATION_SERVER="$work/ui-test-server" FOUNDATION_FIXTURE="$work/fixture-addon" FOUNDATION_MOCK="$root/tests/e2e/mock-backend/mock-backend" \
go test -count=1 -p 2 -tags auth -timeout 20m ./tests/addons/foundation -ginkgo.json-report="$go_report" >"$cache/logs/foundation-go.log" 2>&1 || {
# Name the failed specs for the runner, then show the end of the log.
report_cases "$go_report" 'go: ' 2>/dev/null | grep -v '"status": "passed"\|"status": "skipped"' || true
tail -60 "$cache/logs/foundation-go.log" >&2
exit 1
}
report_cases "$go_report" 'go: '
# 2. The existing gallery and process adapter specs. A package case passes when
# its suite passed with at least one spec and no failed or pending one. The only
# skips allowed are the specs that say they need macOS: they cannot run here.
# core/gallery gets its fixture directory, so its model install specs run.
# A package may name one spec to run alone (see below).
regression() {
pkg=$1
label=$2
isolate=${3:-}
base="$cache/logs/foundation-regression-$(printf %s "$pkg" | tr '/.' '--')"
rm -f "$base.json" "$base.main.json" "$base.isolated.json" "$base.plan.json"
if [ -z "$isolate" ]; then
FIXTURES="$root/tests/fixtures" go test -count=1 -p 2 -tags auth -timeout 15m "$pkg" -ginkgo.json-report="$base.json" >"$base.log" 2>&1 || { tail -40 "$base.log" >&2; exit 1; }
python3 -I - "$base.json" "" "$label" <<'PY'
import json, sys
specs = platform = failed = 0
for report in json.load(open(sys.argv[1])):
for spec in report['SpecReports']:
if spec['LeafNodeType'] != 'It':
continue
specs += 1
if spec['State'] == 'passed':
continue
message = (spec.get('Failure') or {}).get('Message', '').lower()
if spec['State'] == 'skipped' and 'darwin' in message:
platform += 1
else:
failed += 1
ok = specs > 0 and failed == 0
print(json.dumps({'case': sys.argv[3], 'status': 'passed' if ok else 'failed', 'specs': specs, 'platformSkipped': platform}))
PY
return
fi
# One spec of the package replaces the process-wide logger. Specs that log
# from their own goroutines at the same time make it fail at random, so it
# runs alone, once, and the rest of the package runs without it. A dry run
# fixes the inventory: the two runs must account for every spec exactly
# once, the isolated run for exactly one.
FIXTURES="$root/tests/fixtures" go test -count=1 -tags auth "$pkg" -ginkgo.dry-run -ginkgo.json-report="$base.plan.json" >"$base.plan.log" 2>&1 || { tail -40 "$base.plan.log" >&2; exit 1; }
FIXTURES="$root/tests/fixtures" go test -count=1 -p 2 -tags auth -timeout 15m "$pkg" -ginkgo.skip="$isolate" -ginkgo.json-report="$base.main.json" >"$base.main.log" 2>&1 || { tail -40 "$base.main.log" >&2; exit 1; }
FIXTURES="$root/tests/fixtures" go test -count=1 -p 2 -tags auth -timeout 15m "$pkg" -ginkgo.focus="$isolate" -ginkgo.json-report="$base.isolated.json" >"$base.isolated.log" 2>&1 || { tail -40 "$base.isolated.log" >&2; exit 1; }
python3 -I - "$base" "$isolate" "$label" <<'PY'
import json, sys
base, isolate, label = sys.argv[1:4]
def its(path):
return [s for r in json.load(open(path)) for s in r['SpecReports'] if s['LeafNodeType'] == 'It']
plan = its(base + '.plan.json')
main = its(base + '.main.json')
alone = its(base + '.isolated.json')
isolated = [s for s in plan if isolate in s['LeafNodeText']]
problems = []
if len(isolated) != 1:
problems.append('the isolated spec must match exactly one spec, matched %d' % len(isolated))
if len(main) != len(plan) or len(alone) != len(plan):
problems.append('a run reported %d and %d specs for a plan of %d' % (len(main), len(alone), len(plan)))
platform = ran = 0
for spec in main:
if isolate in spec['LeafNodeText']:
if spec['State'] != 'skipped':
problems.append('the isolated spec ran in the main run')
continue
if spec['State'] == 'passed':
ran += 1
continue
message = (spec.get('Failure') or {}).get('Message', '').lower()
if spec['State'] == 'skipped' and 'darwin' in message:
platform += 1
ran += 1
else:
problems.append('%s: %s' % (spec['State'], spec['LeafNodeText']))
once = 0
for spec in alone:
if isolate in spec['LeafNodeText']:
if spec['State'] == 'passed':
once += 1
else:
problems.append('isolated spec %s' % spec['State'])
elif spec['State'] != 'skipped':
problems.append('a spec outside the isolation ran in the isolated run')
if once != 1 or ran != len(plan) - 1:
problems.append('accounted %d of %d specs, isolated run %d times' % (ran + once, len(plan), once))
ok = not problems
for p in problems:
print(p, file=sys.stderr)
print(json.dumps({'case': label, 'status': 'passed' if ok else 'failed', 'specs': ran + once, 'platformSkipped': platform, 'isolated': once}))
PY
}
regression ./core/gallery 'regression: model and backend gallery specs (core/gallery)'
regression ./core/services/galleryop 'regression: gallery operation specs (core/services/galleryop)'
regression ./pkg/model 'regression: process adapter specs (pkg/model)' 'includes the exit code and final stderr line for an unexpected exit'
# 3. The browser against the real cluster.
ready="$work/ready.json"
"$work/foundation-cluster" -dir "$work/cluster" -server "$work/ui-test-server" -mock-backend "$root/tests/e2e/mock-backend/mock-backend" \
-fixture "$work/fixture-addon" -ready "$ready" >"$cache/logs/foundation-cluster.log" 2>&1 &
cluster_pid=$!
i=0
until [ -s "$ready" ]; do
i=$((i + 1))
kill -0 "$cluster_pid" 2>/dev/null || { echo 'foundation cluster exited' >&2; tail -20 "$cache/logs/foundation-cluster.log" >&2; exit 1; }
[ "$i" -lt 180 ] || { echo 'foundation cluster readiness timed out' >&2; exit 1; }
sleep 1
done
report="$cache/logs/foundation-report.json"
rm -f "$report"
status=0
(cd "$ui" && ADDONS_FOUNDATION="$ready" ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.foundation.config.js >"$cache/logs/foundation-playwright.log" 2>&1) || status=1
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
if [ "$status" -ne 0 ]; then
# Keep the evidence in the runner output: the end of the browser log and
# of the cluster log name the failing step and what the servers saw.
echo '--- playwright log (tail) ---' >&2
tail -60 "$cache/logs/foundation-playwright.log" >&2 || true
echo '--- cluster log (tail) ---' >&2
tail -30 "$cache/logs/foundation-cluster.log" >&2 || true
fi
exit "$status"
+66
View File
@@ -0,0 +1,66 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Runs every foundation suite in order and prints the final inventory:
# suite -> passed count, skipped count, status
# It exits nonzero when any suite fails or reports a skipped case, and it never
# stops at the first failure, so one run shows every broken suite.
#
# The set is explicit. A suite that is added to scripts/addons/suites.json and
# not named here is reported as missing from the gate, so the gate cannot
# silently shrink or fall behind the inventory.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
cache="$root/.cache/addons"
mkdir -p "$cache/logs" "$cache/tmp"
. "$root/scripts/addons/leaks.sh"
suites='prerequisites contracts catalog packages lifecycle runtime gateway policy proposals artifacts apps gallery-ui esm-ui host-slots recovery updates foundation-acceptance packaging'
# Tables suites are not foundation suites: scripts/addons/tables_gate.sh runs
# them. They are named here only so the inventory check does not report them
# as missing from the foundation gate.
tables_suites='tables-databases tables-schema tables-rows tables-tools tables-admin tables-ui tables-csv tables-acceptance'
results="$cache/logs/foundation-gate-$$.jsonl"
: >"$results"
for suite in $suites; do
out="$cache/logs/foundation-gate-$suite.out"
# A suite that leaves a process that names the scratch directory behind
# fails, whatever its own result was; the leftover is killed.
leak=0
ok=0
sh scripts/addons/verify.sh "$suite" >"$out" 2>&1 || ok=1
reap_leaks "$cache/tmp/" "$suite" paths-only >>"$out" 2>&1 || leak=1
if [ "$leak" -eq 1 ]; then
printf '{"suite":"%s","passed":0,"skipped":0,"status":"leaked-processes","log":"%s"}\n' "$suite" "$out" >>"$results"
elif [ "$ok" -eq 0 ]; then
line=$(tail -n 1 "$out")
printf '%s\n' "$line" | python3 -I -c 'import json,sys; d=json.loads(sys.stdin.read()); assert d["suite"]=="'"$suite"'"; print(json.dumps({"suite":d["suite"],"passed":d["passed"],"skipped":d["skipped"],"status":"passed" if d["skipped"]==0 else "skipped"}))' >>"$results" 2>/dev/null \
|| printf '{"suite":"%s","passed":0,"skipped":0,"status":"no-result"}\n' "$suite" >>"$results"
else
printf '{"suite":"%s","passed":0,"skipped":0,"status":"failed","log":"%s"}\n' "$suite" "$out" >>"$results"
fi
done
python3 -I - "$results" "$root/scripts/addons/suites.json" "$suites" "$tables_suites" <<'PY'
import json, sys
rows = [json.loads(l) for l in open(sys.argv[1]) if l.strip()]
inventory = json.load(open(sys.argv[2]))
named = sys.argv[3].split()
tables = sys.argv[4].split()
missing = sorted(set(inventory) - set(named) - set(tables))
absent = sorted(set(named) - set(inventory))
width = max(len(r['suite']) for r in rows)
print('foundation gate inventory')
total = skipped = 0
bad = False
for r in rows:
print(' %-*s passed=%-4d skipped=%-2d %s' % (width, r['suite'], r['passed'], r['skipped'], r['status']))
total += r['passed']
skipped += r['skipped']
if r['status'] != 'passed':
bad = True
for name in missing:
print(' %-*s not part of the gate: add it to scripts/addons/foundation_gate.sh' % (width, name))
for name in absent:
print(' %-*s named by the gate and absent from suites.json' % (width, name))
print('total passed=%d skipped=%d suites=%d' % (total, skipped, len(rows)))
sys.exit(1 if bad or missing or absent else 0)
PY
+41
View File
@@ -0,0 +1,41 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Runs the add-ons gallery UI checks and prints one JSON case line per test:
# the node unit tests of the pure state, then the Playwright spec against the
# real UI test server on an isolated port. verify.sh compares the lines with
# the exact inventory of the gallery-ui suite and rejects a skipped, flaky or
# missing case.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
cache="$root/.cache/addons"
ui="$root/core/http/react-ui"
mkdir -p "$cache/tmp" "$cache/logs"
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
export PATH="$cache/bin:$PATH"
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
done
# 1. Pure state and client: node's own runner, no dependencies.
(cd "$ui" && node tests/unit/run.mjs addons.test.mjs)
# 2. Browser specs against the real server, which embeds the built UI.
port=$(python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])')
log="$cache/logs/gallery-ui-server.log"
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$port" >"$log" 2>&1 &
server=$!
trap 'kill "$server" 2>/dev/null || true; wait "$server" 2>/dev/null || true' EXIT HUP INT TERM
i=0
until python3 -I -c "import sys,urllib.request;urllib.request.urlopen('http://127.0.0.1:$port/readyz',timeout=1)" 2>/dev/null; do
i=$((i + 1))
kill -0 "$server" 2>/dev/null || { echo 'UI test server exited' >&2; exit 1; }
[ "$i" -lt 90 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
sleep 1
done
report="$cache/logs/gallery-ui-report.json"
rm -f "$report"
status=0
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$port" ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.addons.config.js >"$cache/logs/gallery-ui-playwright.log" 2>&1) || status=$?
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
exit "$status"
+72
View File
@@ -0,0 +1,72 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Runs the host UI slot checks and prints one JSON case line per test:
# 1. the node unit tests of the slot registry, the loader's component
# validation, the approval frame rules and the SDK approvals call;
# 2. the Go specs of the manifest declarations (unknown slot, unknown
# placement, remote icon, undeclared tool, duplicates, markup, bounds) and
# of the apps list that carries them;
# 3. the Playwright spec against the UI test server running with real
# authentication and a real add-on owner, which installs the "slots"
# fixture package AFTER the host build, from a catalog on loopback.
# verify.sh compares the lines with the exact inventory of the host-slots suite
# and rejects a skipped, flaky or missing case.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
cache="$root/.cache/addons"
ui="$root/core/http/react-ui"
mkdir -p "$cache/tmp" "$cache/logs"
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
export PATH="$cache/bin:$PATH"
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
done
# The server embeds the UI build: a build made before the last UI change would
# test the old pages.
[ "$root/tests/e2e-ui/ui-test-server" -nt "$ui/dist/index.html" ] || { echo 'ui-test-server is older than the UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
# 1. Pure slot, loader, approval and SDK rules.
(cd "$ui" && node tests/unit/run.mjs slots.test.mjs)
# 2. Go: ginkgo specs of the declarations, reported as case lines.
go_report="$cache/logs/host-slots-go.json"
rm -f "$go_report"
go test -count=1 -p 2 -tags auth ./tests/addons/hostslots -ginkgo.json-report="$go_report" >"$cache/logs/host-slots-go.log" 2>&1 || { tail -40 "$cache/logs/host-slots-go.log" >&2; exit 1; }
python3 -I - "$go_report" <<'PY'
import json, sys
for report in json.load(open(sys.argv[1])):
for spec in report['SpecReports']:
if spec['LeafNodeType'] != 'It':
continue
status = {'passed': 'passed', 'skipped': 'skipped'}.get(spec['State'], 'failed')
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
PY
# 3. The real owner and the browser.
fixture_exe="$cache/tmp/fixture-addon"
go build -p 2 -tags auth -o "$fixture_exe" ./tests/addons/fixture/cmd
port=$(python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])')
creds="$cache/logs/host-slots-credentials.json"
rm -f "$creds"
log="$cache/logs/host-slots-server.log"
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$port" \
--addons-fixture="$root/tests/addons/fixture/ui" --addon-executable="$fixture_exe" --fixture-out="$creds" \
--fixture-enable=slots@1.0.0 >"$log" 2>&1 &
server=$!
trap 'kill "$server" 2>/dev/null || true; wait "$server" 2>/dev/null || true' EXIT HUP INT TERM
i=0
until [ -s "$creds" ] && python3 -I -c "import sys,urllib.request;urllib.request.urlopen('http://127.0.0.1:$port/readyz',timeout=1)" 2>/dev/null; do
i=$((i + 1))
kill -0 "$server" 2>/dev/null || { echo 'UI test server exited' >&2; tail -20 "$log" >&2; exit 1; }
[ "$i" -lt 120 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
sleep 1
done
report="$cache/logs/host-slots-report.json"
rm -f "$report"
status=0
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$port" ADDONS_FIXTURE="$creds" ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.slots.config.js >"$cache/logs/host-slots-playwright.log" 2>&1) || status=$?
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
exit "$status"
+48
View File
@@ -0,0 +1,48 @@
# SPDX-License-Identifier: MIT
# Sourced by the foundation scripts. leaked_pids DIR prints the id of every
# live process, except this shell and its ancestors, whose executable, working
# directory or command line names DIR. It reads /proc, so it finds a process
# that was orphaned (parent 1) as well as a child.
leaked_pids() {
dir=$1
# paths-only: a process is a leak only when its executable or working
# directory is under DIR. The command line is not read, so an unrelated
# shell that merely names the shared scratch directory (a log tail or a
# wait loop of another session) is never killed.
mode=${2:-}
skip=" $$ "
p=$$
while [ "$p" -gt 1 ] 2>/dev/null; do
p=$(awk '{ s = $0; sub(/^.*\) /, "", s); split(s, f, " "); print f[2] }' "/proc/$p/stat" 2>/dev/null) || break
[ -n "$p" ] || break
skip="$skip$p "
done
for d in /proc/[0-9]*; do
pid=${d#/proc/}
case "$skip" in *" $pid "*) continue ;; esac
[ -r "$d/cmdline" ] || continue
state=$(awk '{ s = $0; sub(/^.*\) /, "", s); print substr(s, 1, 1) }' "$d/stat" 2>/dev/null) || continue
[ "$state" != Z ] || continue
hit=
case "$(readlink "$d/exe" 2>/dev/null)" in "$dir"*) hit=1 ;; esac
case "$(readlink "$d/cwd" 2>/dev/null)" in "$dir"*) hit=1 ;; esac
[ -n "$hit" ] || [ "$mode" = paths-only ] || case "$(tr '\0' ' ' <"$d/cmdline" 2>/dev/null)" in *"$dir"*) hit=1 ;; esac
[ -z "$hit" ] || echo "$pid"
done
}
# reap_leaks DIR LABEL kills what leaked_pids finds, reports it on stderr and
# returns 1 when anything was found, so a leak is never silent.
reap_leaks() {
found=$(leaked_pids "$1" "${3:-}")
[ -n "$found" ] || return 0
for pid in $found; do
echo "LEAK ($2): process $pid survived: $(tr '\0' ' ' <"/proc/$pid/cmdline" 2>/dev/null | cut -c1-200)" >&2
done
# shellcheck disable=SC2086
kill -TERM $found 2>/dev/null || true
sleep 1
# shellcheck disable=SC2086
kill -KILL $found 2>/dev/null || true
return 1
}
+82
View File
@@ -0,0 +1,82 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Builds the first-party add-on OCI package of one platform (the add-on is
# named by the platform file, Tables by default) as a local OCI
# layout. It never pushes, tags, signs or logs in to anything.
#
# sh scripts/addons/package.sh --layout <dir> --version <semver> --platform <os/arch>
# [--executable <file>] [--runtime-dir <dir>] [--revision <sha>]
# [--config <platforms.yaml>] [--local-check]
#
# The layout holds one platform image: an executable layer (the entrypoint and
# the runtime files of the platform), a ui layer when the manifest declares a
# UI, and the manifest layer, plus receipt.json with the digests. The layout is
# verified before the script succeeds.
#
# --executable is the built binary of the platform. Without it the script
# builds the executable of the add-on for the platform of this host and refuses any
# other platform: a package is never made from a binary of another platform.
# --local-check allows a platform that the platform matrix has not enabled yet;
# the receipt then says publishable=false and the publish steps refuse it.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
layout= version= platform= executable= runtime_dir= revision= local_check=
config=addons/tables/package/platforms.yaml
while [ "$#" -gt 0 ]; do
case "$1" in
--layout) layout=${2-}; shift 2 ;;
--version) version=${2-}; shift 2 ;;
--platform) platform=${2-}; shift 2 ;;
--executable) executable=${2-}; shift 2 ;;
--runtime-dir) runtime_dir=${2-}; shift 2 ;;
--revision) revision=${2-}; shift 2 ;;
--config) config=${2-}; shift 2 ;;
--local-check) local_check=1; shift ;;
*) echo "package.sh: unknown argument $1" >&2; exit 2 ;;
esac
done
[ -n "$layout" ] && [ -n "$version" ] && [ -n "$platform" ] || {
echo 'usage: package.sh --layout <dir> --version <version> --platform <os/arch>' >&2
exit 2
}
scratch=$(mktemp -d "${TMPDIR:-/tmp}/addon-package.XXXXXX")
trap 'chmod -R u+rwx "$scratch" 2>/dev/null; rm -rf "$scratch"' EXIT HUP INT TERM
tool=${ADDONPKG:-}
if [ -z "$tool" ]; then
go build -o "$scratch/addonpkg" ./scripts/addons/cmd/addonpkg
tool="$scratch/addonpkg"
fi
# The add-on, its executable and its UI directory come from the platform file.
[ -f "$config" ] || { echo "package.sh: platform file $config not found" >&2; exit 2; }
addon=$(sed -n 's/^addon: //p' "$config")
exe_name=$(sed -n 's/^executable: //p' "$config")
ui_dir=$(sed -n 's/^ui: //p' "$config")
for v in "$addon" "$exe_name"; do
case "$v" in
'' | *[!a-z0-9-]*) echo "package.sh: $config needs a plain addon and executable name" >&2; exit 2 ;;
esac
done
if [ -z "$executable" ]; then
host="$(go env GOOS)/$(go env GOARCH)"
[ "$platform" = "$host" ] || {
echo "package.sh: platform $platform is not this host ($host); pass the native --executable" >&2
exit 2
}
# Tables links a C library. The other add-ons are pure Go, so their
# executable needs no C toolchain on any platform.
cgo=0
[ "$addon" != tables ] || cgo=1
CGO_ENABLED=$cgo go build -trimpath -o "$scratch/$exe_name" "./addons/$addon/cmd/$exe_name"
executable="$scratch/$exe_name"
fi
# The UI is a build output. Build it only when it is absent.
if [ -n "$ui_dir" ] && [ ! -f "$ui_dir/ui/$addon.js" ]; then
node "addons/$addon/ui/build.mjs"
fi
set -- --config "$config" --layout "$layout" --version "$version" --platform "$platform" --executable "$executable"
[ -z "$runtime_dir" ] || set -- "$@" --runtime-dir "$runtime_dir"
[ -z "$revision" ] || set -- "$@" --revision "$revision"
[ -z "$local_check" ] || set -- "$@" --local-check
"$tool" build "$@"
"$tool" verify --config "$config" --layout "$layout" --platform "$platform" --version "$version"
+172
View File
@@ -0,0 +1,172 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Packaging checks of the first-party add-on OCI package. It prints one JSON
# line per case; scripts/addons/verify.sh compares the lines with the exact
# inventory of the "packaging" suite and rejects a skipped or missing case.
#
# 1. Go specs of pkg/addons/packaging: layout rules, rejected packages,
# event and channel policy, merge completeness, publication and alias
# promotion against an in-process registry, catalog template, and the
# workflow policy with one failing mutant per rule.
# 2. The installer contract: the package built by package.sh is installed
# by the real installer from a real local OCI distribution and its
# executable is started by the real supervisor.
# 3. Shell entry points: package.sh, channel.sh, the workflow policy
# command and static checks that nothing here pushes, tags or signs.
#
# Nothing here pushes, tags, signs or logs in: registries are loopback
# distributions inside the test process and signatures come from a fixture
# authority.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
cache="$root/.cache/addons"
mkdir -p "$cache/tmp" "$cache/logs"
export TMPDIR="$cache/tmp"
export PATH="$cache/bin:$PATH"
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
work=$(mktemp -d "$cache/tmp/packaging.XXXXXX")
trap 'chmod -R u+rwx "$work" 2>/dev/null; rm -rf "$work"' EXIT HUP INT TERM
# The UI is a build output of this checkout; build it so the package holds
# the current files.
node addons/tables/ui/build.mjs >"$cache/logs/packaging-ui-build.log" 2>&1 || { cat "$cache/logs/packaging-ui-build.log" >&2; exit 1; }
go_cases() {
python3 -I - "$1" <<'PY'
import json, sys
for report in json.load(open(sys.argv[1])):
for spec in report['SpecReports']:
if spec['LeafNodeType'] != 'It' or spec['State'] == 'skipped' and not spec.get('Failure'):
continue
status = {'passed': 'passed'}.get(spec['State'], 'failed')
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
PY
}
run_go() {
name=$1
shift
report="$cache/logs/packaging-$name.json"
rm -f "$report"
go test -count=1 -p 2 -tags auth "$@" -ginkgo.json-report="$report" >"$cache/logs/packaging-$name.log" 2>&1 || { tail -60 "$cache/logs/packaging-$name.log" >&2; exit 1; }
go_cases "$report"
}
run_go specs ./pkg/addons/packaging
run_go installer ./tests/addons/packaging
# Shell entry points.
tool="$work/addonpkg"
go build -o "$tool" ./scripts/addons/cmd/addonpkg
export ADDONPKG="$tool"
case_ok() { printf '{"case":"%s","status":"passed"}\n' "$1"; }
expect_fail() { # name expected-text command...
name=$1; expected=$2; shift 2
if "$@" >"$work/out" 2>&1; then echo "FAIL $name: unexpectedly succeeded" >&2; cat "$work/out" >&2; exit 1; fi
grep -F -- "$expected" "$work/out" >/dev/null || { echo "FAIL $name: wrong failure (expected $expected)" >&2; cat "$work/out" >&2; exit 1; }
case_ok "$name"
}
sha=0123456789abcdef0123456789abcdef01234567
host="$(go env GOOS)/$(go env GOARCH)"
printf '#!/bin/sh\nexit 2\n' >"$work/stub"
chmod 755 "$work/stub"
# package.sh: a local check layout for the host builds and verifies.
sh scripts/addons/package.sh --layout "$work/leg" --version 1.2.3 --platform "$host" --executable "$work/stub" --local-check --revision "$sha" >"$work/out" 2>&1 || { cat "$work/out" >&2; exit 1; }
grep -F 'publishable=false' "$work/out" >/dev/null && grep -F 'verified=tables 1.2.3' "$work/out" >/dev/null
test -f "$work/leg/receipt.json" && test -f "$work/leg/index.json" && test -f "$work/leg/oci-layout"
case_ok 'package.sh builds and verifies a local check layout'
expect_fail 'package.sh refuses a platform the matrix has not enabled' 'not enabled' sh scripts/addons/package.sh --layout "$work/leg2" --version 1.2.3 --platform "$host" --executable "$work/stub"
expect_fail 'package.sh refuses an existing non-empty layout' 'not empty' sh scripts/addons/package.sh --layout "$work/leg" --version 1.2.3 --platform "$host" --executable "$work/stub" --local-check
expect_fail 'package.sh requires its arguments' 'usage:' sh scripts/addons/package.sh --version 1.2.3
expect_fail 'package.sh refuses an invalid version' 'semantic version' sh scripts/addons/package.sh --layout "$work/leg3" --version v1 --platform "$host" --executable "$work/stub" --local-check
# package.sh reads the add-on from its platform file. The voice add-on is built
# for the host as a local check, and its layout verifies.
sh scripts/addons/package.sh --config addons/voice/package/platforms.yaml --layout "$work/voice" --version 1.2.3 --platform "$host" --local-check --revision "$sha" >"$work/out" 2>&1 || { cat "$work/out" >&2; exit 1; }
grep -F 'verified=voice 1.2.3' "$work/out" >/dev/null
test -f "$work/voice/receipt.json"
case_ok 'package.sh builds and verifies the voice add-on for the host'
# A platform other than the host is refused for every add-on, whatever the config.
if [ "$host" = linux/amd64 ]; then foreign=linux/arm64; else foreign=linux/amd64; fi
for id in tables voice face fine-tuning quantization; do
expect_fail "package.sh refuses a foreign platform without an executable: $id" 'is not this host' sh scripts/addons/package.sh --config "addons/$id/package/platforms.yaml" --layout "$work/foreign-$id" --version 1.2.3 --platform "$foreign" --local-check
done
case_ok 'package.sh refuses a foreign platform without an executable for every add-on'
# The build line: cgo for Tables, none for the pure Go add-ons. A stub go
# records the environment and the package it was asked to build.
realgo=$(command -v go)
mkdir "$work/stubbin"
cat >"$work/stubbin/go" <<STUB
#!/bin/sh
if [ "\$1" = build ]; then
out=
for a in "\$@"; do
if [ "\$prev" = -o ]; then out=\$a; fi
prev=\$a
last=\$a
done
printf '%s CGO_ENABLED=%s\n' "\$last" "\${CGO_ENABLED-unset}" >>"$work/gobuild.log"
printf '#!/bin/sh\nexit 2\n' >"\$out"
chmod 755 "\$out"
exit 0
fi
exec "$realgo" "\$@"
STUB
chmod 755 "$work/stubbin/go"
: >"$work/gobuild.log"
for id in tables voice face fine-tuning quantization; do
PATH="$work/stubbin:$PATH" sh scripts/addons/package.sh --config "addons/$id/package/platforms.yaml" --layout "$work/stub-$id" --version 1.2.3 --platform "$host" --local-check >"$work/out" 2>&1 || { cat "$work/out" >&2; exit 1; }
done
grep -x './addons/tables/cmd/tables CGO_ENABLED=1' "$work/gobuild.log" >/dev/null
for id in voice face fine-tuning quantization; do
grep -x "./addons/$id/cmd/$id CGO_ENABLED=0" "$work/gobuild.log" >/dev/null || { echo "FAIL: $id is not built with CGO_ENABLED=0" >&2; cat "$work/gobuild.log" >&2; exit 1; }
done
case_ok 'package.sh builds Tables with cgo and the pure Go add-ons without'
expect_fail 'matrix fails closed while no platform is enabled' 'no enabled platform' "$tool" matrix --config addons/tables/package/platforms.yaml
expect_fail 'matrix fails closed without a file' 'platform matrix' "$tool" matrix --config "$work/absent.yaml"
# channel.sh.
channel() { sh scripts/addons/channel.sh "$@"; }
out=$(channel --event pull_request --ref refs/pull/7/merge --sha "$sha")
printf '%s\n' "$out" | grep -x 'publish=false' >/dev/null && printf '%s\n' "$out" | grep -x 'aliases=' >/dev/null
case_ok 'channel.sh: a pull request never publishes'
out=$(channel --event push --ref refs/heads/master --sha "$sha")
printf '%s\n' "$out" | grep -x 'aliases=development' >/dev/null && printf '%s\n' "$out" | grep -x 'immutable=sha-0123456789ab' >/dev/null
case_ok 'channel.sh: master publishes development'
out=$(channel --event push --ref refs/heads/latest --sha "$sha")
printf '%s\n' "$out" | grep -x 'publish=false' >/dev/null && printf '%s\n' "$out" | grep -x 'aliases=' >/dev/null && printf '%s\n' "$out" | grep -x 'channel=' >/dev/null
case_ok 'channel.sh: a branch push never publishes, even a branch named latest'
out=$(channel --event push --ref refs/tags/v1.2.3 --sha "$sha")
printf '%s\n' "$out" | grep -x 'aliases=' >/dev/null && printf '%s\n' "$out" | grep -x 'immutable=1.2.3' >/dev/null
case_ok 'channel.sh: a tag push prepares the version and moves no alias'
out=$(channel --event release --ref refs/tags/v1.2.3 --sha "$sha")
printf '%s\n' "$out" | grep -x 'aliases=latest' >/dev/null
case_ok 'channel.sh: a published stable release promotes latest'
out=$(channel --event release --ref refs/tags/v1.2.3 --sha "$sha" --prerelease)
printf '%s\n' "$out" | grep -x 'aliases=' >/dev/null
case_ok 'channel.sh: a pre-release never promotes latest'
expect_fail 'channel.sh: a draft release fails' 'draft' channel --event release --ref refs/tags/v1.2.3 --sha "$sha" --draft
expect_fail 'channel.sh: an unknown event fails' 'not allowed to publish' channel --event workflow_dispatch --ref refs/heads/master --sha "$sha"
expect_fail 'channel.sh: a tag that is not a version fails' 'release tag' channel --event push --ref refs/tags/latest --sha "$sha"
# Workflow policy.
"$tool" policy --workflow .github/workflows/addons-package.yml >"$work/out" 2>&1 || { cat "$work/out" >&2; exit 1; }
case_ok 'workflow policy accepts the checked-in workflow'
sed 's/^ release:$/ pull_request_target:\n types: [opened]\n release:/' .github/workflows/addons-package.yml >"$work/mutant.yml"
expect_fail 'workflow policy rejects pull_request_target' 'P_PR_TARGET' "$tool" policy --workflow "$work/mutant.yml"
sed 's/^ needs: \[plan, validate, package\]$/ needs: [plan, package]/' .github/workflows/addons-package.yml >"$work/mutant.yml"
expect_fail 'workflow policy rejects a publish job that does not wait for every leg' 'P_NEEDS' "$tool" policy --workflow "$work/mutant.yml"
sed 's/^ name: validate package rules$/ name: validate package rules\n continue-on-error: true/' .github/workflows/addons-package.yml >"$work/mutant.yml"
expect_fail 'workflow policy rejects a validate job that cannot stop the release' 'P_VALIDATE_GATE' "$tool" policy --workflow "$work/mutant.yml"
# Nothing in the packaging scripts pushes, tags, signs or logs in.
if grep -n -E 'docker push|docker login|git tag|git push|oras (push|copy)|cosign (sign|attest)|crane push|gh release' scripts/addons/package.sh scripts/addons/channel.sh scripts/addons/package_test.sh | grep -v '^[^:]*:[0-9]*:#' | grep -v 'grep -n -E' | grep -v 'static checks'; then
echo 'FAIL: a packaging script pushes, tags or signs' >&2
exit 1
fi
case_ok 'scripts never push, tag, sign or log in'
if grep -n -i -E 'quay\.io|go-skynet|backend_merge|backend_build|backend\.yml|ci-cache|docker\.io' .github/workflows/addons-package.yml scripts/addons/package.sh scripts/addons/channel.sh addons/*/package/platforms.yaml gallery/addons/tables.yaml.tmpl; then
echo 'FAIL: a packaging file references the backend pipeline' >&2
exit 1
fi
case_ok 'packaging files never reference backend registries or workflows'
+84
View File
@@ -0,0 +1,84 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
export GOTOOLCHAIN=local
export GOPATH="$root/.cache/addons/go" GOMODCACHE="$root/.cache/addons/go/pkg/mod"
export GOCACHE="$root/.cache/addons/go-build" GOBIN="$root/.cache/addons/bin"
export npm_config_cache="$root/.cache/addons/npm" PLAYWRIGHT_BROWSERS_PATH="$root/.cache/addons/browsers"
export PATH="$GOBIN:$PATH"
mode=${1:-}
case "$mode" in --check|--prepare|--integration) ;; *) echo 'usage: prepare.sh --check|--prepare|--integration' >&2; exit 2;; esac
[ "$#" -eq 1 ] || exit 2
fail() { echo "$*" >&2; exit 1; }
go version 2>/dev/null | grep -Eq '^go version go1\.26\.[0-9]+ ' || fail 'Go 1.26.x required (GOTOOLCHAIN=local)'
node --version 2>/dev/null | grep -Eq '^v22\.([1-9][2-9]|[2-9][0-9]|[1-9][0-9]{2,})\.[0-9]+$' || fail 'Node 22.12 or later within Node 22 required'
[ "$(protoc --version 2>/dev/null)" = 'libprotoc 26.1' ] || fail 'protoc 26.1 required; provision explicitly (Makefile downloader uses a different version)'
if [ "$mode" = --check ]; then
# Fail before invoking compilers or package managers if generated inputs are absent.
for file in pkg/grpc/proto/backend.pb.go pkg/grpc/proto/backend_grpc.pb.go core/http/react-ui/dist/index.html; do
[ -s "$file" ] || fail "missing generated file: $file; run --integration explicitly"
done
fi
command -v npm >/dev/null || fail 'npm required'
command -v "${CC:-cc}" >/dev/null || fail 'CGO C compiler required'
[ "$(go env CGO_ENABLED)" = 1 ] || fail 'CGO_ENABLED=1 required'
space() {
available=$(df -Pk "$root" | awk 'END {print $4}')
[ "$available" -ge 20971520 ] || fail 'stop: at least 20 GiB free required before setup/build'
}
if [ "$mode" = --prepare ]; then
space
mkdir -p "$GOBIN" "$root/.cache/addons/tmp"
export TMPDIR="$root/.cache/addons/tmp"
# Exact existing Makefile pins; Go module checksums are authenticated by sum.golang.org.
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2
space
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@1958fcbe2ca8bd93af633f11e97d44e567e945af
space
go mod download
space
(cd core/http/react-ui && npm ci --no-audit --no-fund)
# Explicit setup only: the lockfile-pinned local CLI selects the browser
# revision. Never use npx or install host OS packages here.
if [ -z "${PLAYWRIGHT_CHROMIUM_PATH:-}" ]; then
space
core/http/react-ui/node_modules/.bin/playwright install chromium
else
[ -x "$PLAYWRIGHT_CHROMIUM_PATH" ] || fail 'configured Chromium is not executable'
fi
exit 0
fi
# Go resolves the Makefile commit to cmd/protoc-gen-go-grpc/v1.4.0.
# Bind the tag to the checksum authenticated by sum.golang.org, not its label.
check_plugin() {
metadata=$(go version -m "$(command -v "$1")") || fail "$1 build metadata unavailable"
printf '%s\n' "$metadata" | awk -v module="$2" -v version="$3" -v sum="$4" '
$1 == "mod" && $2 == module && $3 == version && $4 == sum { found=1 }
$1 == "=>" { replaced=1 }
END { exit !(found && !replaced) }
' || fail "$1 exact module version/checksum required"
}
check_plugin protoc-gen-go google.golang.org/protobuf v1.34.2 'h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg='
check_plugin protoc-gen-go-grpc google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.4.0 'h1:9SxA29VM43MF5Z9dQu694wmY5t8E/Gxr7s+RSxiIDmc='
export GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
if [ "$mode" = --integration ]; then
space
mkdir -p pkg/grpc/proto "$root/.cache/addons/tmp"
export TMPDIR="$root/.cache/addons/tmp"
protoc --experimental_allow_proto3_optional -Ibackend/ --go_out=pkg/grpc/proto/ --go_opt=paths=source_relative --go-grpc_out=pkg/grpc/proto/ --go-grpc_opt=paths=source_relative backend/backend.proto
space
(cd core/http/react-ui && npm run build)
space
go build -p 2 -tags auth -o tests/e2e/mock-backend/mock-backend ./tests/e2e/mock-backend
space
go build -p 2 -tags auth -o tests/e2e-ui/ui-test-server ./tests/e2e-ui
exit 0
fi
for file in tests/e2e/mock-backend/mock-backend tests/e2e-ui/ui-test-server core/http/react-ui/node_modules/.bin/playwright; do
[ -x "$file" ] || fail "missing executable: $file"
done
(cd core/http/react-ui && npm ls --depth=0 --offline >/dev/null) || fail 'installed npm dependencies do not match package/lock requirements'
# --check inspects only; verify performs native compile/runtime and browser probes.
printf '{"case":"toolchain","status":"passed"}\n'
+210
View File
@@ -0,0 +1,210 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Fixtures are isolated copies: never substitute fake tools in a real readiness run.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
export TMPDIR="$root/.cache/addons/tmp"
mkdir -p "$TMPDIR"
fixture=$(mktemp -d)
trap 'rm -rf "$fixture"' EXIT HUP INT TERM
mkdir -p "$fixture/scripts/addons" "$fixture/bin"
for file in prepare.sh verify.sh suites.json ui_cases.py; do
test -f "$root/scripts/addons/$file" || { echo "missing implementation: $file" >&2; exit 1; }
cp "$root/scripts/addons/$file" "$fixture/scripts/addons/$file"
done
cat > "$fixture/bin/go" <<'TOOL'
#!/bin/sh
case "$*" in
version) echo "go version ${FIXTURE_GO:-go1.26.1} linux/amd64";;
*) exit 91;;
esac
TOOL
cat > "$fixture/bin/node" <<'TOOL'
#!/bin/sh
echo "${FIXTURE_NODE:-v22.22.0}"
TOOL
cat > "$fixture/bin/protoc" <<'TOOL'
#!/bin/sh
echo "${FIXTURE_PROTOC:-libprotoc 26.1}"
TOOL
chmod +x "$fixture/bin/"*
count=0
reject() {
name=$1; expected=$2; shift 2
if "$@" > "$fixture/output" 2>&1; then
echo "FAIL $name: unexpectedly succeeded" >&2; exit 1
fi
if ! grep -F "$expected" "$fixture/output" >/dev/null; then
echo "FAIL $name: wrong failure (expected $expected)" >&2
cat "$fixture/output" >&2; exit 1
fi
count=$((count + 1))
printf '{"case":"%s","status":"passed"}\n' "$name"
}
prepare="$fixture/scripts/addons/prepare.sh"
verify="$fixture/scripts/addons/verify.sh"
reject missing-go 'Go 1.26' env PATH="$fixture/bin:/usr/bin:/bin" FIXTURE_GO=missing sh "$prepare" --check
reject old-go 'Go 1.26' env PATH="$fixture/bin:$PATH" FIXTURE_GO=go1.25.9 sh "$prepare" --check
reject future-go 'Go 1.26' env PATH="$fixture/bin:$PATH" FIXTURE_GO=go1.27.0 sh "$prepare" --check
reject old-node 'Node 22.12' env PATH="$fixture/bin:$PATH" FIXTURE_NODE=v22.11.0 sh "$prepare" --check
reject wrong-node 'Node 22.12' env PATH="$fixture/bin:$PATH" FIXTURE_NODE=v24.0.0 sh "$prepare" --check
reject wrong-protoc 'protoc 26.1' env PATH="$fixture/bin:$PATH" FIXTURE_PROTOC='libprotoc 31.1' sh "$prepare" --check
reject absent-generated 'generated file' env PATH="$fixture/bin:$PATH" sh "$prepare" --check
reject empty-name 'suite' sh "$verify" ''
reject future-suite 'suite' sh "$verify" unregistered
# Replace the copied inventory and children, not production behavior.
cat > "$fixture/scripts/addons/suites.json" <<'JSON'
{"prerequisites":{"kind":"shell","commands":[],"cases":[]}}
JSON
reject empty-selection 'empty' sh "$verify" prerequisites
cat > "$fixture/scripts/addons/suites.json" <<'JSON'
{"prerequisites":{"kind":"shell","commands":[["sh","scripts/addons/prerequisites_test.sh"]],"cases":["required"]}}
JSON
printf '#!/bin/sh\nexit 42\n' > "$fixture/scripts/addons/prerequisites_test.sh"
reject failed-child '42' sh "$verify" prerequisites
# A failing child must leave its failing case and the end of its output in the
# runner output, so a flake in a long gate run can be named afterwards.
printf '#!/bin/sh\necho one; echo '\''{"case":"required","status":"failed"}'\''; echo tail-marker-line >&2; exit 7\n' > "$fixture/scripts/addons/prerequisites_test.sh"
reject failed-case-named 'failed case: required' sh "$verify" prerequisites
reject failed-tail-kept '| tail-marker-line' sh "$verify" prerequisites
printf '#!/bin/sh\nexit 0\n' > "$fixture/scripts/addons/prerequisites_test.sh"
reject zero-tests 'required' sh "$verify" prerequisites
printf '#!/bin/sh\necho '\''{"case":"required","status":"skipped"}'\''\n' > "$fixture/scripts/addons/prerequisites_test.sh"
reject skipped-tests 'required' sh "$verify" prerequisites
# The Playwright report converter must never turn a failed, skipped, retried,
# empty or missing report into a passing case.
cat > "$fixture/scripts/addons/suites.json" <<'JSON'
{"prerequisites":{"kind":"shell","commands":[["python3","-I","scripts/addons/ui_cases.py","playwright","report.json"]],"cases":["first","second"]}}
JSON
ui_report() {
printf '{"suites":[{"title":"f.spec.js","specs":[{"title":"first","tests":[{"status":"expected","results":[{"status":"passed"}]}]},{"title":"second","tests":[{"status":"%s","results":%s}]}]}]}' "$1" "$2" > "$fixture/report.json"
}
ui_report unexpected '[{"status":"failed"}]'
reject ui-report-failed 'required case not passed: second' sh "$verify" prerequisites
ui_report skipped '[]'
reject ui-report-skipped 'required case not passed: second' sh "$verify" prerequisites
ui_report flaky '[{"status":"failed"},{"status":"passed"}]'
reject ui-report-flaky 'required case not passed: second' sh "$verify" prerequisites
printf '{"suites":[]}' > "$fixture/report.json"
reject ui-report-empty 'playwright-report-empty' sh "$verify" prerequisites
rm -f "$fixture/report.json"
reject ui-report-missing 'playwright-report' sh "$verify" prerequisites
# Machine-readable Ginkgo reports must reject empty and skipped selections.
cat > "$fixture/scripts/addons/suites.json" <<'JSON'
{"prerequisites":{"kind":"ginkgo","commands":[["go","test","-count=1","-tags","auth","./fixture","-ginkgo.json-report=.cache/report.json"]],"cases":["sqlite transaction"],"report":".cache/report.json"}}
JSON
cat > "$fixture/bin/go" <<'TOOL'
#!/bin/sh
mkdir -p .cache
printf '%s' "$FIXTURE_REPORT" > .cache/report.json
TOOL
reject ginkgo-empty 'Ginkgo report' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[]' sh "$verify" prerequisites
reject ginkgo-skipped 'Ginkgo report' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[{"SuiteSucceeded":true,"SpecReports":[{"State":"skipped","LeafNodeType":"It","LeafNodeText":"sqlite transaction"}]}]' sh "$verify" prerequisites
reject ginkgo-missing 'Ginkgo report' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='{}' sh "$verify" prerequisites
# A failed Ginkgo run names its failed spec and the first line of its failure.
cat > "$fixture/bin/go" <<'TOOL'
#!/bin/sh
mkdir -p .cache
printf '%s' "$FIXTURE_REPORT" > .cache/report.json
exit 1
TOOL
reject ginkgo-failed-spec-named 'failed spec: sqlite transaction' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[{"SuiteSucceeded":false,"SpecReports":[{"State":"failed","LeafNodeType":"It","LeafNodeText":"sqlite transaction","Failure":{"Message":"Expected 1 to equal 2\nmore"}}]}]' sh "$verify" prerequisites
reject ginkgo-failure-message 'Expected 1 to equal 2' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[{"SuiteSucceeded":false,"SpecReports":[{"State":"failed","LeafNodeType":"It","LeafNodeText":"sqlite transaction","Failure":{"Message":"Expected 1 to equal 2\nmore"}}]}]' sh "$verify" prerequisites
cat > "$fixture/bin/go" <<'TOOL'
#!/bin/sh
mkdir -p .cache
printf '%s' "$FIXTURE_REPORT" > .cache/report.json
TOOL
# Registered contracts must reach the same report gate, never bypass it.
python3 - "$fixture/scripts/addons/suites.json" <<'JSON'
import json, sys
p=sys.argv[1]
v=json.load(open(p)); v['contracts']=v.pop('prerequisites')
open(p,'w').write(json.dumps(v))
JSON
reject contracts-empty 'Ginkgo report' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[]' sh "$verify" contracts
reject contracts-skipped 'Ginkgo report' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[{"SuiteSucceeded":true,"SpecReports":[{"State":"skipped","LeafNodeType":"It","LeafNodeText":"sqlite transaction"}]}]' sh "$verify" contracts
# Every gateway report is mandatory, including a missing second report.
cat > "$fixture/scripts/addons/suites.json" <<'JSON'
{"gateway":{"kind":"ginkgo","commands":[["go","test","-count=1","-tags","auth","./fixture"]],"cases":["sqlite transaction","http"],"report":".cache/report.json","reports":[{"path":".cache/report.json","cases":["sqlite transaction"]},{"path":".cache/second.json","cases":["http"]}]}}
JSON
reject gateway-missing-report 'Ginkgo report' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[{"SuiteSucceeded":true,"SpecReports":[{"State":"passed","LeafNodeType":"It","LeafNodeText":"sqlite transaction"}]}]' sh "$verify" gateway
reject gateway-skipped-report 'Ginkgo report' env PATH="$fixture/bin:$PATH" FIXTURE_REPORT='[{"SuiteSucceeded":true,"SpecReports":[{"State":"skipped","LeafNodeType":"It","LeafNodeText":"sqlite transaction"}]}]' sh "$verify" gateway
# A commit at a tagged module resolves to the tag, with an exact module sum.
cat > "$fixture/bin/go" <<'TOOL'
#!/bin/sh
case "$*" in
version) echo 'go version go1.26.1 linux/amd64';;
'env CGO_ENABLED') echo 1;;
'version -m '*protoc-gen-go-grpc) printf '\tmod\tgoogle.golang.org/grpc/cmd/protoc-gen-go-grpc\tv1.4.0\t%s\n' "${FIXTURE_SUM:-h1:9SxA29VM43MF5Z9dQu694wmY5t8E/Gxr7s+RSxiIDmc=}";;
'version -m '*protoc-gen-go) printf '\tmod\tgoogle.golang.org/protobuf\tv1.34.2\th1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\n';;
*) exit 91;;
esac
TOOL
for tool in npm cc protoc-gen-go protoc-gen-go-grpc; do
printf '#!/bin/sh\nexit 0\n' > "$fixture/bin/$tool"
chmod +x "$fixture/bin/$tool"
done
cat > "$fixture/bin/protoc" <<'TOOL'
#!/bin/sh
if [ "$*" = --version ]; then echo 'libprotoc 26.1'; else echo 'pin accepted: generation sentinel' >&2; exit 92; fi
TOOL
reject exact-tag-pin 'generation sentinel' env PATH="$fixture/bin:$PATH" sh "$prepare" --integration
reject wrong-tag-sum 'protoc-gen-go-grpc' env PATH="$fixture/bin:$PATH" FIXTURE_SUM=h1:wrong sh "$prepare" --integration
# Explicit setup must use the local CLI and worktree browser cache, and
# propagate an installer failure rather than reporting incomplete setup ready.
cat > "$fixture/bin/go" <<'TOOL'
#!/bin/sh
case "$*" in
version) echo 'go version go1.26.1 linux/amd64';;
'env CGO_ENABLED') echo 1;;
*) exit 0;;
esac
TOOL
mkdir -p "$fixture/core/http/react-ui/node_modules/.bin"
cat > "$fixture/core/http/react-ui/node_modules/.bin/playwright" <<'TOOL'
#!/bin/sh
[ "$*" = 'install chromium' ] || exit 93
[ "$PLAYWRIGHT_BROWSERS_PATH" = "$EXPECTED_BROWSER_CACHE" ] || exit 94
echo 'browser installation sentinel' >&2
exit 95
TOOL
chmod +x "$fixture/core/http/react-ui/node_modules/.bin/playwright"
# Select the download branch explicitly, regardless of the caller's browser.
# This is scoped to the fixture child; verify must retain the real browser path.
reject browser-setup-failure 'browser installation sentinel' env PATH="$fixture/bin:$PATH" PLAYWRIGHT_CHROMIUM_PATH= EXPECTED_BROWSER_CACHE="$fixture/.cache/addons/browsers" sh "$prepare" --prepare
reject configured-browser-missing 'configured Chromium is not executable' env PATH="$fixture/bin:$PATH" PLAYWRIGHT_CHROMIUM_PATH="$fixture/missing-chromium" sh "$prepare" --prepare
# Setup only checks executability and must bypass the failing installer above.
# This stub is not browser readiness evidence: verify launches real Chromium.
printf '#!/bin/sh\nexit 96\n' > "$fixture/bin/chromium"
chmod +x "$fixture/bin/chromium"
if ! env PATH="$fixture/bin:$PATH" PLAYWRIGHT_CHROMIUM_PATH="$fixture/bin/chromium" sh "$prepare" --prepare > "$fixture/output" 2>&1; then
echo 'FAIL configured-browser-setup: unexpectedly failed' >&2
cat "$fixture/output" >&2; exit 1
fi
count=$((count + 1))
printf '{"case":"configured-browser-setup","status":"passed"}\n'
# The leak scan of the shared scratch directory looks at executables and
# working directories only. A shell of another session that merely names the
# directory in its command line (a log tail, a wait loop) is not a leak and is
# never killed; a process that runs in or from the directory still is.
leaks="$root/scripts/addons/leaks.sh"
[ -n "${LEAKS_SCRIPT:-}" ] && leaks=$LEAKS_SCRIPT
scratch="$fixture/shared-scratch"
mkdir -p "$scratch/run"
sh -c ': '"$scratch"'/named-only; sleep 10' &
named=$!
(cd "$scratch/run" && exec sleep 10) &
inside=$!
sleep 0.3
found=$(. "$leaks"; leaked_pids "$scratch/" paths-only)
pkill -P "$named" 2>/dev/null || true
kill "$named" "$inside" 2>/dev/null || true
wait "$named" "$inside" 2>/dev/null || true
case " $(echo $found) " in *" $named "*) echo 'FAIL leak-ignores-command-line: a shell that only names the directory was reported' >&2; exit 1 ;; esac
count=$((count + 1))
printf '{"case":"leak-ignores-command-line","status":"passed"}\n'
case " $(echo $found) " in *" $inside "*) ;; *) echo 'FAIL leak-finds-working-directory: a process running in the directory was missed' >&2; exit 1 ;; esac
count=$((count + 1))
printf '{"case":"leak-finds-working-directory","status":"passed"}\n'
printf '{"total":%s,"passed":%s,"skipped":0}\n' "$count" "$count"
+104
View File
@@ -0,0 +1,104 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Runs the add-on data recovery checks and prints one JSON case line per test:
# 1. the node unit tests of the recovery rules, the typed client and the
# locale keys of the panel;
# 2. the Go specs of the recovery package (WAL-consistent backup, corrupt and
# oversized backups, cancellation, retention, every journal phase of an
# upgrade and of a restore, grant resurrection, purge), of the policy
# (hold, restore filter, revocation log, ownership migration), of the
# Tables store (format migration) and of the real owner with the real
# Tables executable over real HTTP (typed refusals, owner and frontend
# parity, upgrade with data, reinstall identity, documentation);
# 3. the Playwright spec against a UI test server that runs the real owner,
# the real Tables executable and the real built UI package.
# verify.sh compares the lines with the exact inventory of the recovery suite
# and rejects a skipped, flaky or missing case.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
cache="$root/.cache/addons"
ui="$root/core/http/react-ui"
pkg="$root/addons/tables/ui"
mkdir -p "$cache/tmp" "$cache/logs"
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
export PATH="$cache/bin:$PATH"
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
done
# The server embeds the UI build: a build made before the last change would
# test the old host code.
[ "$root/tests/e2e-ui/ui-test-server" -nt "$ui/dist/index.html" ] || { echo 'ui-test-server is older than the UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
for src in "$ui/src/components/addons/RecoveryPanel.jsx" "$ui/src/components/addons/recoveryState.js" "$ui/src/utils/addonsApi.js"; do
[ "$ui/dist/index.html" -nt "$src" ] || { echo "the UI build is older than $src; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
done
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/core/services/addons/recovery/backup.go" ] || { echo 'ui-test-server is older than the recovery package; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/core/http/endpoints/addons/recovery.go" ] || { echo 'ui-test-server is older than the recovery routes; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
[ "$root/tests/e2e/mock-backend/mock-backend" -nt "$root/tests/e2e/mock-backend/main.go" ] || { echo 'mock-backend is older than its source; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
# 1. Pure rules, the typed client and the locale keys.
(cd "$ui" && node tests/unit/run.mjs recovery.test.mjs)
# 2. Go: ginkgo specs reported as case lines. Specs that a focus filtered out
# are not part of the run and are not printed; a failed or skipped spec is.
go_cases() {
python3 -I - "$1" <<'PY'
import json, sys
for report in json.load(open(sys.argv[1])):
for spec in report['SpecReports']:
if spec['LeafNodeType'] != 'It' or spec['State'] == 'skipped' and not spec.get('Failure'):
continue
status = {'passed': 'passed'}.get(spec['State'], 'failed')
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
PY
}
run_go() {
name=$1
shift
report="$cache/logs/recovery-$name.json"
rm -f "$report"
go test -count=1 -p 2 -tags auth "$@" -ginkgo.json-report="$report" >"$cache/logs/recovery-$name.log" 2>&1 || { tail -40 "$cache/logs/recovery-$name.log" >&2; exit 1; }
go_cases "$report"
}
run_go package ./core/services/addons/recovery
run_go policy ./core/services/addons/policy -ginkgo.focus='Addon recovery state of the policy'
run_go store ./addons/tables/internal/store -ginkgo.focus='Format migration'
run_go owner ./tests/addons/recovery
# 3. The real owner, the real Tables executable, the built package.
node "$pkg/build.mjs" >"$cache/logs/recovery-build.log" 2>&1 || { cat "$cache/logs/recovery-build.log" >&2; exit 1; }
tables_exe="$cache/tmp/recovery-exe"
CGO_ENABLED=1 go build -p 2 -o "$tables_exe" ./addons/tables/cmd/tables
fixture_dir="$cache/tmp/recovery-fixture"
rm -rf "$fixture_dir"
mkdir -p "$fixture_dir/tables"
cp "$root/addons/tables/manifest.json" "$fixture_dir/tables/manifest.json"
cp -R "$pkg/dist/ui" "$fixture_dir/tables/ui"
free_port() { python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])'; }
named_port=$(free_port)
creds="$cache/logs/recovery-credentials.json"
rm -f "$creds"
shots=${RECOVERY_SHOTS:-}
trap 'kill "$named" 2>/dev/null || true; wait "$named" 2>/dev/null || true; rm -rf "$fixture_dir" "$tables_exe"' EXIT HUP INT TERM
named=0
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$named_port" \
--addons-fixture="$fixture_dir" --addon-executable="$tables_exe" --fixture-out="$creds" \
--fixture-enable=tables@0.1.0 >"$cache/logs/recovery-server.log" 2>&1 &
named=$!
i=0
until [ -s "$creds" ] && python3 -I -c "import urllib.request;urllib.request.urlopen('http://127.0.0.1:$named_port/readyz',timeout=1)" 2>/dev/null; do
i=$((i + 1))
kill -0 "$named" 2>/dev/null || { echo 'UI test server exited' >&2; tail -20 "$cache/logs/recovery-server.log" >&2; exit 1; }
[ "$i" -lt 180 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
sleep 1
done
report="$cache/logs/recovery-report.json"
rm -f "$report"
status=0
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$named_port" ADDONS_FIXTURE="$creds" ADDONS_SHOTS="$shots" \
ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.recovery.config.js >"$cache/logs/recovery-playwright.log" 2>&1) || status=$?
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
exit "$status"
File diff suppressed because it is too large. Load diff
+170
View File
@@ -0,0 +1,170 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Runs the Tables acceptance and prints one JSON case line per check:
# 1. Go: the end-to-end specs of tests/addons/tablesacceptance. They start
# the real owner and two forwarding frontends as separate server
# processes over the real mTLS carrier with one shared account store, and
# install the real packaged Tables (built by scripts/addons/package.sh)
# from a local catalog. Every call uses a real named session or API key,
# the public HTTP routes and the real SQLite databases of the add-on;
# 2. the Playwright spec against the same kind of cluster, started by the
# tables-cluster command;
# 3. the evidence manifest, a JSON file the run writes under .cache/addons:
# commands, case inventory, native platform, package digests and the
# traces retained for a failure. A Go test checks it.
# Nothing is signed, tagged, pushed or published. verify.sh compares the lines
# with the exact inventory of the tables-acceptance suite and rejects a
# skipped, flaky or missing case.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
cache="$root/.cache/addons"
ui="$root/core/http/react-ui"
mkdir -p "$cache/tmp" "$cache/logs"
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
export PATH="$cache/bin:$PATH"
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
for file in tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
done
[ -f "$ui/dist/index.html" ] || { echo 'missing UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
[ "$root/tests/e2e/mock-backend/mock-backend" -nt "$root/tests/e2e/mock-backend/main.go" ] || { echo 'mock-backend is older than its source; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
work=$(mktemp -d "$cache/tmp/tables-acceptance.XXXXXX")
cluster_pid=
. "$root/scripts/addons/leaks.sh"
leaked=0
cleanup() {
code=$?
trap - EXIT HUP INT TERM
if [ -n "$cluster_pid" ]; then
# The cluster tears its servers down on SIGTERM; wait, bounded.
kill "$cluster_pid" 2>/dev/null || true
n=0
while kill -0 "$cluster_pid" 2>/dev/null && [ "$n" -lt 60 ]; do
n=$((n + 1))
sleep 0.5
done
kill -KILL "$cluster_pid" 2>/dev/null || true
wait "$cluster_pid" 2>/dev/null || true
fi
# Nothing that names the work or scratch directories may survive. Whatever
# does is killed, and the run fails.
reap_leaks "$work" tables-acceptance || leaked=1
reap_leaks "$cache/tmp/" tables-acceptance paths-only || leaked=1
find "$work" -type d -exec chmod u+rwx {} + 2>/dev/null || true
rm -rf "$work"
if [ "$leaked" -ne 0 ] && [ "$code" -eq 0 ]; then
code=1
fi
exit "$code"
}
trap cleanup EXIT
trap 'exit 143' HUP INT TERM
# The binaries are built here, never taken from an earlier build: the server
# embeds the UI build and every other part of the fixture.
go build -p 2 -tags auth -o "$work/ui-test-server" ./tests/e2e-ui
CGO_ENABLED=1 go build -p 2 -trimpath -o "$work/tables" ./addons/tables/cmd/tables
go build -p 2 -tags auth -o "$work/tables-cluster" ./tests/addons/tablesacceptance/cmd/tables-cluster
evidence="$cache/tables-acceptance-evidence.json"
rm -f "$evidence"
rm -rf "$cache/logs/tables-acceptance-traces"
# 1. The end-to-end specs.
go_report="$cache/logs/tables-acceptance-go.json"
rm -f "$go_report"
TABLES_ACCEPTANCE_SERVER="$work/ui-test-server" TABLES_ACCEPTANCE_TABLES="$work/tables" \
TABLES_ACCEPTANCE_MOCK="$root/tests/e2e/mock-backend/mock-backend" TABLES_ACCEPTANCE_EVIDENCE="$evidence" \
go test -count=1 -p 2 -tags auth -timeout 30m ./tests/addons/tablesacceptance -ginkgo.json-report="$go_report" >"$cache/logs/tables-acceptance-go.log" 2>&1 || {
python3 -I - "$go_report" <<'PY' >&2 || true
import json, sys
try:
for report in json.load(open(sys.argv[1])):
for spec in report['SpecReports']:
if spec['LeafNodeType'] == 'It' and spec['State'] != 'passed':
print('failed case: go: %s (%s)' % (spec['LeafNodeText'], spec['State']))
except (OSError, ValueError):
pass
PY
tail -60 "$cache/logs/tables-acceptance-go.log" >&2
exit 1
}
python3 -I - "$go_report" <<'PY'
import json, sys
for report in json.load(open(sys.argv[1])):
for spec in report['SpecReports']:
if spec['LeafNodeType'] != 'It':
continue
status = {'passed': 'passed', 'skipped': 'skipped'}.get(spec['State'], 'failed')
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
PY
# 2. The browser against the real cluster.
ready="$work/ready.json"
"$work/tables-cluster" -dir "$work/state" -root "$root" -server "$work/ui-test-server" -mock-backend "$root/tests/e2e/mock-backend/mock-backend" \
-tables "$work/tables" -ready "$ready" >"$cache/logs/tables-acceptance-cluster.log" 2>&1 &
cluster_pid=$!
i=0
until [ -s "$ready" ]; do
i=$((i + 1))
kill -0 "$cluster_pid" 2>/dev/null || { echo 'acceptance cluster exited' >&2; tail -20 "$cache/logs/tables-acceptance-cluster.log" >&2; exit 1; }
[ "$i" -lt 300 ] || { echo 'acceptance cluster readiness timed out' >&2; exit 1; }
sleep 1
done
report="$cache/logs/tables-acceptance-report.json"
rm -f "$report"
status=0
(cd "$ui" && ADDONS_TABLES_CLUSTER="$ready" ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.tables-acceptance.config.js >"$cache/logs/tables-acceptance-playwright.log" 2>&1) || status=1
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
if [ "$status" -ne 0 ]; then
echo '--- playwright log (tail) ---' >&2
tail -60 "$cache/logs/tables-acceptance-playwright.log" >&2 || true
echo '--- cluster log (tail) ---' >&2
tail -30 "$cache/logs/tables-acceptance-cluster.log" >&2 || true
mkdir -p "$cache/logs/tables-acceptance-traces"
cp "$cache/logs/tables-acceptance-playwright.log" "$cache/logs/tables-acceptance-cluster.log" "$cache/logs/tables-acceptance-traces/" 2>/dev/null || true
fi
# 3. The evidence manifest: add the browser cases and run the check.
python3 -I - "$evidence" "$report" "$cache/logs/tables-acceptance-traces" "$cache/logs/tables-acceptance-playwright.log" <<'PY' || status=1
import json, os, sys
evidence, report, traces, log = sys.argv[1:5]
ev = json.load(open(evidence))
cases = []
def walk(suite):
for spec in suite.get('specs', []):
for test in spec.get('tests', []):
results = test.get('results', [])
ok = test.get('status') == 'expected' and len(results) == 1 and results[0].get('status') == 'passed'
case = {'name': spec['title'], 'state': 'passed' if ok else 'failed',
'millis': int(sum(r.get('duration', 0) for r in results))}
if not ok:
case['trace'] = log
case['error'] = ((results[-1].get('error') or {}).get('message') or '')[:2000] if results else ''
cases.append(case)
for child in suite.get('suites', []):
walk(child)
try:
for suite in json.load(open(report)).get('suites', []):
walk(suite)
except (OSError, ValueError):
pass
ev['browser'] = cases
ev['commands'].append(['playwright', 'test', '--config', 'playwright.tables-acceptance.config.js'])
tmp = evidence + '.tmp'
json.dump(ev, open(tmp, 'w'), indent=2)
os.replace(tmp, evidence)
PY
if TABLES_ACCEPTANCE_CHECK_EVIDENCE=1 TABLES_ACCEPTANCE_EVIDENCE="$evidence" \
go test -count=1 -tags auth -run 'TestEvidenceManifest$' -v ./tests/addons/tablesacceptance >"$cache/logs/tables-acceptance-evidence.log" 2>&1 \
&& grep -q -- '--- PASS: TestEvidenceManifest' "$cache/logs/tables-acceptance-evidence.log"; then
printf '{"case": "manifest: the evidence manifest names the platform, the commands, the package digests and every case", "status": "passed"}\n'
else
printf '{"case": "manifest: the evidence manifest names the platform, the commands, the package digests and every case", "status": "failed"}\n'
tail -30 "$cache/logs/tables-acceptance-evidence.log" >&2
status=1
fi
exit "$status"
+113
View File
@@ -0,0 +1,113 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Runs the Tables CSV export checks and prints one JSON case line per test:
# 1. the node unit tests of the file card rules and of the export call of
# the Tables page;
# 2. the Go specs of the CSV store (snapshot, quoting, formula guard,
# bounds), of the runtime output handles, of the real owner with the real
# Tables executable (private artifacts, failures, expiry, quota) and of
# the owner with two real frontend processes (identical bytes and
# headers from every replica);
# 3. the Playwright spec against two UI test servers that run the real
# owner, the real Tables executable and the real built UI package: one
# with named accounts and one in local-owner mode.
# verify.sh compares the lines with the exact inventory of the tables-csv
# suite and rejects a skipped, flaky or missing case.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
cache="$root/.cache/addons"
ui="$root/core/http/react-ui"
pkg="$root/addons/tables/ui"
mkdir -p "$cache/tmp" "$cache/logs"
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
export PATH="$cache/bin:$PATH"
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
done
# The server embeds the UI build: a build made before the last change would
# test the old host code.
[ "$root/tests/e2e-ui/ui-test-server" -nt "$ui/dist/index.html" ] || { echo 'ui-test-server is older than the UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/core/services/addons/resources/service.go" ] || { echo 'ui-test-server is older than the resource service; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
[ "$root/tests/e2e/mock-backend/mock-backend" -nt "$root/tests/e2e/mock-backend/main.go" ] || { echo 'mock-backend is older than its source; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/tests/e2e-ui/addons_fixture.go" ] || { echo 'ui-test-server is older than its fixture; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
# 1. Pure rules: the file card and the export call.
(cd "$ui" && node tests/unit/run.mjs tablesartifacts.test.mjs)
# 2. Go: ginkgo specs reported as case lines. Specs that a focus filtered out
# are not part of the run and are not printed; a failed or skipped spec is.
go_cases() {
python3 -I - "$1" <<'PY'
import json, sys
for report in json.load(open(sys.argv[1])):
for spec in report['SpecReports']:
if spec['LeafNodeType'] != 'It' or spec['State'] == 'skipped' and not spec.get('Failure'):
continue
status = {'passed': 'passed'}.get(spec['State'], 'failed')
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
PY
}
run_go() {
name=$1
shift
report="$cache/logs/tables-csv-$name.json"
rm -f "$report"
go test -count=1 -p 2 -tags auth "$@" -ginkgo.json-report="$report" >"$cache/logs/tables-csv-$name.log" 2>&1 || { tail -40 "$cache/logs/tables-csv-$name.log" >&2; exit 1; }
go_cases "$report"
}
run_go store ./addons/tables/internal/store -ginkgo.focus='CSV export'
run_go server ./addons/tables/internal/server
run_go owner ./tests/addons/tablesadmin -ginkgo.focus='CSV export'
FOUNDATION_SERVER="$root/tests/e2e-ui/ui-test-server" run_go replicas ./tests/addons/tablesexport
# 3. The real owner, the real Tables executable, the built package.
node "$pkg/build.mjs" >"$cache/logs/tables-csv-build.log" 2>&1 || { cat "$cache/logs/tables-csv-build.log" >&2; exit 1; }
tables_exe="$cache/tmp/tables-csv-exe"
CGO_ENABLED=1 go build -p 2 -o "$tables_exe" ./addons/tables/cmd/tables
fixture_dir="$cache/tmp/tables-csv-fixture"
rm -rf "$fixture_dir"
mkdir -p "$fixture_dir/tables"
cp "$root/addons/tables/manifest.json" "$fixture_dir/tables/manifest.json"
cp -R "$pkg/dist/ui" "$fixture_dir/tables/ui"
free_port() { python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])'; }
named_port=$(free_port)
local_port=$(free_port)
creds="$cache/logs/tables-csv-credentials.json"
local_creds="$cache/logs/tables-csv-local-credentials.json"
rm -f "$creds" "$local_creds"
trap 'kill "$named" "$localsrv" 2>/dev/null || true; wait "$named" "$localsrv" 2>/dev/null || true; rm -rf "$fixture_dir"' EXIT HUP INT TERM
named=0
localsrv=0
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$named_port" \
--addons-fixture="$fixture_dir" --addon-executable="$tables_exe" --fixture-out="$creds" \
--fixture-enable=tables@0.1.0 >"$cache/logs/tables-csv-server.log" 2>&1 &
named=$!
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$local_port" \
--addons-fixture="$fixture_dir" --addon-executable="$tables_exe" --fixture-out="$local_creds" \
--addons-local-owner --fixture-enable=tables@0.1.0 >"$cache/logs/tables-csv-local-server.log" 2>&1 &
localsrv=$!
wait_for() {
file=$1
port=$2
pid=$3
i=0
until [ -s "$file" ] && python3 -I -c "import sys,urllib.request;urllib.request.urlopen('http://127.0.0.1:$port/readyz',timeout=1)" 2>/dev/null; do
i=$((i + 1))
kill -0 "$pid" 2>/dev/null || { echo 'UI test server exited' >&2; tail -20 "$cache/logs/tables-csv-server.log" "$cache/logs/tables-csv-local-server.log" >&2; exit 1; }
[ "$i" -lt 180 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
sleep 1
done
}
wait_for "$creds" "$named_port" "$named"
wait_for "$local_creds" "$local_port" "$localsrv"
report="$cache/logs/tables-csv-report.json"
rm -f "$report"
status=0
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$named_port" ADDONS_LOCAL_URL="http://127.0.0.1:$local_port" ADDONS_FIXTURE="$creds" \
ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.csv.config.js >"$cache/logs/tables-csv-playwright.log" 2>&1) || status=$?
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
exit "$status"
+65
View File
@@ -0,0 +1,65 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Runs every Tables suite in order, the acceptance last, and prints the final
# inventory:
# suite -> passed count, skipped count, status
# It exits nonzero when any suite fails, reports a skipped case, leaves a
# process behind or is missing from the set, and it never stops at the first
# failure, so one run shows every broken suite.
#
# The set is explicit. A suite named tables-* that is added to
# scripts/addons/suites.json and not named here is reported as missing from the
# gate, so the gate cannot silently fall behind the inventory. The foundation
# suites are not part of this gate: scripts/addons/foundation_gate.sh runs them.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
cache="$root/.cache/addons"
mkdir -p "$cache/logs" "$cache/tmp"
. "$root/scripts/addons/leaks.sh"
suites='tables-databases tables-schema tables-rows tables-tools tables-admin tables-ui tables-csv tables-acceptance'
results="$cache/logs/tables-gate-$$.jsonl"
: >"$results"
for suite in $suites; do
out="$cache/logs/tables-gate-$suite.out"
# A suite that leaves a process that names the scratch directory behind
# fails, whatever its own result was; the leftover is killed.
leak=0
ok=0
sh scripts/addons/verify.sh "$suite" >"$out" 2>&1 || ok=1
reap_leaks "$cache/tmp/" "$suite" paths-only >>"$out" 2>&1 || leak=1
if [ "$leak" -eq 1 ]; then
printf '{"suite":"%s","passed":0,"skipped":0,"status":"leaked-processes","log":"%s"}\n' "$suite" "$out" >>"$results"
elif [ "$ok" -eq 0 ]; then
line=$(tail -n 1 "$out")
printf '%s\n' "$line" | python3 -I -c 'import json,sys; d=json.loads(sys.stdin.read()); assert d["suite"]=="'"$suite"'"; print(json.dumps({"suite":d["suite"],"passed":d["passed"],"skipped":d["skipped"],"status":"passed" if d["skipped"]==0 else "skipped"}))' >>"$results" 2>/dev/null \
|| printf '{"suite":"%s","passed":0,"skipped":0,"status":"no-result"}\n' "$suite" >>"$results"
else
printf '{"suite":"%s","passed":0,"skipped":0,"status":"failed","log":"%s"}\n' "$suite" "$out" >>"$results"
fi
done
python3 -I - "$results" "$root/scripts/addons/suites.json" "$suites" <<'PY'
import json, sys
rows = [json.loads(l) for l in open(sys.argv[1]) if l.strip()]
inventory = json.load(open(sys.argv[2]))
named = sys.argv[3].split()
tables = sorted(k for k in inventory if k.startswith('tables-'))
missing = sorted(set(tables) - set(named))
absent = sorted(set(named) - set(inventory))
width = max(len(r['suite']) for r in rows)
print('tables gate inventory')
total = skipped = 0
bad = False
for r in rows:
print(' %-*s passed=%-4d skipped=%-2d %s' % (width, r['suite'], r['passed'], r['skipped'], r['status']))
total += r['passed']
skipped += r['skipped']
if r['status'] != 'passed':
bad = True
for name in missing:
print(' %-*s not part of the gate: add it to scripts/addons/tables_gate.sh' % (width, name))
for name in absent:
print(' %-*s named by the gate and absent from suites.json' % (width, name))
print('total passed=%d skipped=%d suites=%d' % (total, skipped, len(rows)))
sys.exit(1 if bad or missing or absent else 0)
PY
+95
View File
@@ -0,0 +1,95 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Runs the Tables tool checks and prints one JSON case line per test:
# 1. the node unit tests of the proposal card rules (reference parsing, the
# stored history, the facts the card draws, the confirmation call);
# 2. the Go specs of the tool adapter (discovery, forged fields, approval
# aliases, the executors, the consumer matrix), of the pending proposal
# list across the owner and forwarding frontends, and of the real owner
# with the real Tables executable (tool calls to proposals to
# confirmation);
# 3. the Playwright spec against the UI test server running with real
# authentication, the real owner, the real Tables executable and a
# scripted model that answers with the tool call a spec encodes.
# verify.sh compares the lines with the exact inventory of the tables-tools
# suite and rejects a skipped, flaky or missing case.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
cache="$root/.cache/addons"
ui="$root/core/http/react-ui"
mkdir -p "$cache/tmp" "$cache/logs"
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
export PATH="$cache/bin:$PATH"
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
done
# The server embeds the UI build and the mock model embeds its script
# markers: a build made before the last change would test the old code.
[ "$root/tests/e2e-ui/ui-test-server" -nt "$ui/dist/index.html" ] || { echo 'ui-test-server is older than the UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
[ "$root/tests/e2e/mock-backend/mock-backend" -nt "$root/tests/e2e/mock-backend/main.go" ] || { echo 'mock-backend is older than its source; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/core/services/addons/tools/adapter.go" ] || { echo 'ui-test-server is older than the adapter; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
# 1. Pure card, history and chat action rules.
(cd "$ui" && node tests/unit/run.mjs tablestools.test.mjs)
# 2. Go: ginkgo specs reported as case lines. Specs that a focus filtered out
# are not part of the run and are not printed; a failed or skipped spec is.
go_cases() {
python3 -I - "$1" <<'PY'
import json, sys
for report in json.load(open(sys.argv[1])):
for spec in report['SpecReports']:
if spec['LeafNodeType'] != 'It' or spec['State'] == 'skipped' and not spec.get('Failure'):
continue
status = {'passed': 'passed'}.get(spec['State'], 'failed')
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
PY
}
run_go() {
name=$1
shift
report="$cache/logs/tables-tools-$name.json"
rm -f "$report"
go test -count=1 -p 2 -tags auth "$@" -ginkgo.json-report="$report" >"$cache/logs/tables-tools-$name.log" 2>&1 || { tail -40 "$cache/logs/tables-tools-$name.log" >&2; exit 1; }
go_cases "$report"
}
run_go adapter ./core/services/addons/tools
run_go cli ./core/cli -ginkgo.focus='mcpJobSessionOption'
run_go pending ./core/http/endpoints/addons -ginkgo.focus='lists the caller.s own pending proposals'
run_go owner ./tests/addons/tables -ginkgo.focus='Tables tools under the real owner'
# 3. The real owner, the real Tables executable and the browser.
tables_exe="$cache/tmp/tables-exe"
CGO_ENABLED=1 go build -p 2 -o "$tables_exe" ./addons/tables/cmd/tables
fixture_dir="$cache/tmp/tables-tools-fixture"
rm -rf "$fixture_dir"
mkdir -p "$fixture_dir/tables"
cp "$root/addons/tables/manifest.json" "$fixture_dir/tables/manifest.json"
# The manifest declares the pages of the package, so the package carries them.
node "$root/addons/tables/ui/build.mjs" >"$cache/logs/tables-tools-ui-build.log" 2>&1 || { cat "$cache/logs/tables-tools-ui-build.log" >&2; exit 1; }
cp -R "$root/addons/tables/ui/dist/ui" "$fixture_dir/tables/ui"
port=$(python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])')
creds="$cache/logs/tables-tools-credentials.json"
rm -f "$creds"
log="$cache/logs/tables-tools-server.log"
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$port" \
--addons-fixture="$fixture_dir" --addon-executable="$tables_exe" --fixture-out="$creds" \
--fixture-enable=tables@0.1.0 >"$log" 2>&1 &
server=$!
trap 'kill "$server" 2>/dev/null || true; wait "$server" 2>/dev/null || true; rm -rf "$fixture_dir"' EXIT HUP INT TERM
i=0
until [ -s "$creds" ] && python3 -I -c "import sys,urllib.request;urllib.request.urlopen('http://127.0.0.1:$port/readyz',timeout=1)" 2>/dev/null; do
i=$((i + 1))
kill -0 "$server" 2>/dev/null || { echo 'UI test server exited' >&2; tail -20 "$log" >&2; exit 1; }
[ "$i" -lt 120 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
sleep 1
done
report="$cache/logs/tables-tools-report.json"
rm -f "$report"
status=0
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$port" ADDONS_FIXTURE="$creds" ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.tools.config.js >"$cache/logs/tables-tools-playwright.log" 2>&1) || status=$?
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
exit "$status"
+110
View File
@@ -0,0 +1,110 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Runs the Tables UI checks and prints one JSON case line per test:
# 1. the node unit tests of the host SDK resource calls and of the Tables UI
# package (field codecs, filter builder, batch bounds, error mapping, the
# build output and a render of the first states);
# 2. the Go specs of the UI-intent change route against the real owner and
# the real Tables executable;
# 3. the Playwright spec against two UI test servers that run the real
# owner, the real Tables executable and the real built UI package: one
# with named accounts and one in local-owner mode.
# verify.sh compares the lines with the exact inventory of the tables-ui suite
# and rejects a skipped, flaky or missing case.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
cache="$root/.cache/addons"
ui="$root/core/http/react-ui"
pkg="$root/addons/tables/ui"
mkdir -p "$cache/tmp" "$cache/logs"
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
export PATH="$cache/bin:$PATH"
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
done
# The server embeds the UI build: a build made before the last change would
# test the old host code.
[ "$root/tests/e2e-ui/ui-test-server" -nt "$ui/dist/index.html" ] || { echo 'ui-test-server is older than the UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/core/services/addons/resources/service.go" ] || { echo 'ui-test-server is older than the resource service; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/tests/e2e-ui/addons_fixture.go" ] || { echo 'ui-test-server is older than its fixture; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
# 1. Pure rules: the SDK resource calls and the package.
(cd "$ui" && node tests/unit/run.mjs resources.test.mjs)
(cd "$pkg" && node test/run.mjs)
# 2. Go: the UI-intent change route.
go_cases() {
python3 -I - "$1" <<'PY'
import json, sys
for report in json.load(open(sys.argv[1])):
for spec in report['SpecReports']:
if spec['LeafNodeType'] != 'It' or spec['State'] == 'skipped' and not spec.get('Failure'):
continue
status = {'passed': 'passed'}.get(spec['State'], 'failed')
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
PY
}
report="$cache/logs/tables-ui-apply.json"
rm -f "$report"
go test -count=1 -p 2 -tags auth ./tests/addons/tablesadmin -ginkgo.focus='UI changes of a database' -ginkgo.json-report="$report" >"$cache/logs/tables-ui-apply.log" 2>&1 || { tail -40 "$cache/logs/tables-ui-apply.log" >&2; exit 1; }
go_cases "$report"
# 3. The real owner, the real Tables executable, the built package.
node "$pkg/build.mjs" >"$cache/logs/tables-ui-build.log" 2>&1 || { cat "$cache/logs/tables-ui-build.log" >&2; exit 1; }
tables_exe="$cache/tmp/tables-ui-exe"
CGO_ENABLED=1 go build -p 2 -o "$tables_exe" ./addons/tables/cmd/tables
fixture_dir="$cache/tmp/tables-ui-fixture"
rm -rf "$fixture_dir"
mkdir -p "$fixture_dir/tables" "$fixture_dir/tables-next"
cp "$root/addons/tables/manifest.json" "$fixture_dir/tables/manifest.json"
cp -R "$pkg/dist/ui" "$fixture_dir/tables/ui"
# A second release, for the stale-tab checks: the same package, one patch up.
python3 -I - "$root/addons/tables/manifest.json" "$fixture_dir/tables-next/manifest.json" <<'PY'
import json, sys
m = json.load(open(sys.argv[1]))
m['version'] = '0.1.1'
json.dump(m, open(sys.argv[2], 'w'))
PY
cp -R "$pkg/dist/ui" "$fixture_dir/tables-next/ui"
free_port() { python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])'; }
named_port=$(free_port)
local_port=$(free_port)
creds="$cache/logs/tables-ui-credentials.json"
local_creds="$cache/logs/tables-ui-local-credentials.json"
rm -f "$creds" "$local_creds"
trap 'kill "$named" "$localsrv" 2>/dev/null || true; wait "$named" "$localsrv" 2>/dev/null || true; rm -rf "$fixture_dir"' EXIT HUP INT TERM
named=0
localsrv=0
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$named_port" \
--addons-fixture="$fixture_dir" --addon-executable="$tables_exe" --fixture-out="$creds" \
--fixture-enable=tables@0.1.0 >"$cache/logs/tables-ui-server.log" 2>&1 &
named=$!
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$local_port" \
--addons-fixture="$fixture_dir" --addon-executable="$tables_exe" --fixture-out="$local_creds" \
--addons-local-owner --fixture-enable=tables@0.1.0 >"$cache/logs/tables-ui-local-server.log" 2>&1 &
localsrv=$!
wait_for() {
file=$1
port=$2
pid=$3
i=0
until [ -s "$file" ] && python3 -I -c "import sys,urllib.request;urllib.request.urlopen('http://127.0.0.1:$port/readyz',timeout=1)" 2>/dev/null; do
i=$((i + 1))
kill -0 "$pid" 2>/dev/null || { echo 'UI test server exited' >&2; tail -20 "$cache/logs/tables-ui-server.log" "$cache/logs/tables-ui-local-server.log" >&2; exit 1; }
[ "$i" -lt 180 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
sleep 1
done
}
wait_for "$creds" "$named_port" "$named"
wait_for "$local_creds" "$local_port" "$localsrv"
report="$cache/logs/tables-ui-report.json"
rm -f "$report"
status=0
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$named_port" ADDONS_LOCAL_URL="http://127.0.0.1:$local_port" ADDONS_FIXTURE="$creds" \
ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.tables.config.js >"$cache/logs/tables-ui-playwright.log" 2>&1) || status=$?
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
exit "$status"
+58
View File
@@ -0,0 +1,58 @@
# SPDX-License-Identifier: MIT
"""Turns a Playwright JSON report into the case lines the verify runner reads.
usage: ui_cases.py playwright REPORT.json
One line {"case": <spec title>, "status": passed|failed|skipped|flaky} per
test. A test is passed only when it passed on its first attempt: a retry that
passed is flaky, and a skipped, fixme or todo test is skipped. Any error the
runner reports outside a test is a failed case of its own, so an empty or
partial run can never look like success.
"""
import json
import sys
def walk(suite, out):
for spec in suite.get('specs', []):
for test in spec.get('tests', []):
results = test.get('results', [])
status = test.get('status')
if status == 'expected' and len(results) == 1 and results[0].get('status') == 'passed':
state = 'passed'
elif status == 'skipped' or not results or all(r.get('status') == 'skipped' for r in results):
state = 'skipped'
elif status == 'flaky':
state = 'flaky'
else:
state = 'failed'
out.append({'case': spec['title'], 'status': state})
for child in suite.get('suites', []):
walk(child, out)
def main(argv):
if len(argv) != 3 or argv[1] != 'playwright':
print('usage: ui_cases.py playwright REPORT.json', file=sys.stderr)
return 2
try:
with open(argv[2]) as f:
report = json.load(f)
assert isinstance(report, dict)
except (OSError, ValueError, AssertionError):
print(json.dumps({'case': 'playwright-report', 'status': 'failed'}))
return 1
cases = []
for suite in report.get('suites', []):
walk(suite, cases)
for error in report.get('errors', []) or []:
cases.append({'case': 'playwright-error: ' + str(error.get('message', ''))[:120], 'status': 'failed'})
if not cases:
print(json.dumps({'case': 'playwright-report-empty', 'status': 'failed'}))
return 1
for case in cases:
print(json.dumps(case))
return 0
sys.exit(main(sys.argv))
+121
View File
@@ -0,0 +1,121 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Runs the add-on update detection and upgrade checks and prints one JSON case
# line per test:
# 1. the node unit tests of the update rules, the typed client and the
# locale keys;
# 2. the Go specs of the checker (moving channels, immutable versions,
# compatibility, failed checks, cache bounds, rate limit), of the
# catalog reads it uses, of the real lifecycle manager (pinned upgrade,
# duplicate and conflicting operations, last good generation) and of the
# HTTP routes over the owner and two forwarding frontends;
# 3. the Playwright spec against a UI test server that runs the real owner,
# the real Tables executable and the real built UI package, with a
# library whose channel the spec moves.
# verify.sh compares the lines with the exact inventory of the updates suite
# and rejects a skipped, flaky or missing case.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
cache="$root/.cache/addons"
ui="$root/core/http/react-ui"
pkg="$root/addons/tables/ui"
mkdir -p "$cache/tmp" "$cache/logs"
export TMPDIR="$cache/tmp" PLAYWRIGHT_BROWSERS_PATH="$cache/browsers"
export PATH="$cache/bin:$PATH"
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
for file in tests/e2e-ui/ui-test-server tests/e2e/mock-backend/mock-backend core/http/react-ui/node_modules/.bin/playwright; do
[ -x "$file" ] || { echo "missing $file; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
done
# The server embeds the UI build: a build made before the last change would
# test the old host code.
[ "$root/tests/e2e-ui/ui-test-server" -nt "$ui/dist/index.html" ] || { echo 'ui-test-server is older than the UI build; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
for src in "$ui/src/components/addons/InstalledView.jsx" "$ui/src/components/addons/updateState.js" "$ui/src/components/addons/AppsView.jsx" "$ui/src/utils/addonsApi.js"; do
[ "$ui/dist/index.html" -nt "$src" ] || { echo "the UI build is older than $src; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
done
for src in core/services/addons/updates/checker.go core/services/addons/updates.go core/http/endpoints/addons/updates.go tests/e2e-ui/addons_fixture.go; do
[ "$root/tests/e2e-ui/ui-test-server" -nt "$root/$src" ] || { echo "ui-test-server is older than $src; run scripts/addons/prepare.sh --integration" >&2; exit 1; }
done
[ "$root/tests/e2e/mock-backend/mock-backend" -nt "$root/tests/e2e/mock-backend/main.go" ] || { echo 'mock-backend is older than its source; run scripts/addons/prepare.sh --integration' >&2; exit 1; }
# 1. Pure rules, the typed client and the locale keys.
(cd "$ui" && node tests/unit/run.mjs updates.test.mjs)
# 2. Go: ginkgo specs reported as case lines. Specs that a focus filtered out
# are not part of the run and are not printed; a failed or skipped spec is.
go_cases() {
python3 -I - "$1" <<'PY'
import json, sys
for report in json.load(open(sys.argv[1])):
for spec in report['SpecReports']:
if spec['LeafNodeType'] != 'It' or spec['State'] == 'skipped' and not spec.get('Failure'):
continue
status = {'passed': 'passed'}.get(spec['State'], 'failed')
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
PY
}
run_go() {
name=$1
shift
report="$cache/logs/updates-$name.json"
rm -f "$report"
go test -count=1 -p 2 -tags auth "$@" -ginkgo.json-report="$report" >"$cache/logs/updates-$name.log" 2>&1 || { tail -40 "$cache/logs/updates-$name.log" >&2; exit 1; }
go_cases "$report"
}
run_go checker ./core/services/addons/updates
run_go catalog ./core/services/addons/catalog -ginkgo.focus='what the update check reads'
run_go manager ./core/services/addons -ginkgo.focus='explicit upgrade from an authoritative update check'
run_go routes ./core/http/endpoints/addons -ginkgo.focus='update routes'
# 3. The real owner, the real Tables executable, the built package. The
# fixture library has one moving channel: each directory is one generation of
# it, with the same version label and other bytes. The last generation asks
# for a host this one is not.
node "$pkg/build.mjs" >"$cache/logs/updates-build.log" 2>&1 || { cat "$cache/logs/updates-build.log" >&2; exit 1; }
tables_exe="$cache/tmp/updates-exe"
CGO_ENABLED=1 go build -p 2 -o "$tables_exe" ./addons/tables/cmd/tables
fixture_dir="$cache/tmp/updates-fixture"
rm -rf "$fixture_dir"
mkdir -p "$fixture_dir"
for n in 1 2 3 4 5; do
d="$fixture_dir/tables-dev$n"
mkdir -p "$d"
python3 -I - "$root/addons/tables/manifest.json" "$d/manifest.json" <<'PY'
import json, sys
m = json.load(open(sys.argv[1]))
m['version'] = '0.1.0-dev'
json.dump(m, open(sys.argv[2], 'w'))
PY
cp -R "$pkg/dist/ui" "$d/ui"
printf 'build %s\n' "$n" >"$d/build.txt"
printf 'development\n' >"$d/.fixture-channel"
printf '%s\n' "$n" >"$d/.fixture-generation"
done
printf '{"host":{"min":9,"max":9}}\n' >"$fixture_dir/tables-dev5/.fixture-requires"
free_port() { python3 -I -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1])'; }
named_port=$(free_port)
creds="$cache/logs/updates-credentials.json"
rm -f "$creds"
shots=${UPDATES_SHOTS:-}
trap 'kill "$named" 2>/dev/null || true; wait "$named" 2>/dev/null || true; rm -rf "$fixture_dir" "$tables_exe"' EXIT HUP INT TERM
named=0
"$root/tests/e2e-ui/ui-test-server" --mock-backend="$root/tests/e2e/mock-backend/mock-backend" --port="$named_port" \
--addons-fixture="$fixture_dir" --addon-executable="$tables_exe" --fixture-out="$creds" \
--fixture-enable=tables@development >"$cache/logs/updates-server.log" 2>&1 &
named=$!
i=0
until [ -s "$creds" ] && python3 -I -c "import urllib.request;urllib.request.urlopen('http://127.0.0.1:$named_port/readyz',timeout=1)" 2>/dev/null; do
i=$((i + 1))
kill -0 "$named" 2>/dev/null || { echo 'UI test server exited' >&2; tail -20 "$cache/logs/updates-server.log" >&2; exit 1; }
[ "$i" -lt 180 ] || { echo 'UI test server readiness timed out' >&2; exit 1; }
sleep 1
done
report="$cache/logs/updates-report.json"
rm -f "$report"
status=0
(cd "$ui" && ADDONS_UI_URL="http://127.0.0.1:$named_port" ADDONS_FIXTURE="$creds" ADDONS_SHOTS="$shots" \
ADDONS_UI_REPORT="$report" ./node_modules/.bin/playwright test --config playwright.updates.config.js >"$cache/logs/updates-playwright.log" 2>&1) || status=$?
python3 -I "$root/scripts/addons/ui_cases.py" playwright "$report" || status=1
exit "$status"
+156
View File
@@ -0,0 +1,156 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
[ "$#" -eq 1 ] && [ -n "$1" ] || { echo 'one nonempty suite required' >&2; exit 2; }
exec python3 - "$1" <<'PY'
import json, os, pathlib, subprocess, sys, tempfile, time, socket, urllib.request
root = pathlib.Path.cwd()
suite = sys.argv[1]
inventory = json.loads((root / 'scripts/addons/suites.json').read_text())
if suite not in inventory:
sys.exit('unknown or unavailable suite: ' + suite)
entry = inventory[suite]
if not entry.get('commands') or not entry.get('cases'):
sys.exit('empty command/test inventory')
cache = root / '.cache/addons'
(cache / 'logs').mkdir(parents=True, exist_ok=True)
(cache / 'tmp').mkdir(exist_ok=True)
env = dict(os.environ, GOTOOLCHAIN='local', GOPROXY='off', GOSUMDB='off', GOFLAGS='-mod=readonly',
GOPATH=str(cache/'go'), GOMODCACHE=str(cache/'go/pkg/mod'), GOCACHE=str(cache/'go-build'),
TMPDIR=str(cache/'tmp'), npm_config_cache=str(cache/'npm'), PLAYWRIGHT_BROWSERS_PATH=str(cache/'browsers'))
env['PATH'] = str(cache/'bin') + os.pathsep + env['PATH']
log = cache / 'logs' / ('verify-' + str(time.time_ns()) + '.jsonl')
def run(command, cwd=root):
result = subprocess.run(command, cwd=cwd, env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
with log.open('a') as f:
f.write(json.dumps(dict(command=command, status=result.returncode, output=result.stdout))+'\n')
print(result.stdout, end='', flush=True)
if result.returncode:
sys.exit(failure_summary(command, result.stdout, result.returncode))
return result.stdout
def failure_summary(command, output, status):
"""Names what failed, so a flake in a long gate run can be diagnosed later.
It reads only: failed case lines of the child, failed specs of the Ginkgo
reports the command names, and the last lines of the output. It never
turns a failure into a pass."""
names, lines = [], []
for line in output.splitlines():
try: item = json.loads(line)
except ValueError: continue
if isinstance(item, dict) and 'case' in item and item.get('status') not in (None, 'passed'):
names.append(str(item['case']))
lines.append('failed case: %s (%s)' % (item['case'], item['status']))
for arg in command:
if not arg.startswith('-ginkgo.json-report='): continue
path = pathlib.Path(arg.split('=', 1)[1])
path = path if path.is_absolute() else root / path
try: reports = json.loads(path.read_text())
except (OSError, ValueError): continue
for report in reports if isinstance(reports, list) else []:
for spec in report.get('SpecReports', []):
if spec.get('State') in ('passed', 'skipped', 'pending'): continue
message = ((spec.get('Failure') or {}).get('Message') or '').strip().splitlines()
names.append(str(spec.get('LeafNodeText')))
lines.append('failed spec: %s (%s)%s' % (spec.get('LeafNodeText'), spec.get('State'),
': ' + message[0][:200] if message else ''))
tail = [l.strip() for l in output.splitlines() if l.strip()][-5:]
if tail: lines.append('last output: ' + ' | '.join(l[:200] for l in tail))
lines.append('child status %d; failed: %s; log: %s' % (status, ', '.join(names[:10]) or 'unknown', log))
return '\n'.join(lines)
def ginkgo_report(path, expected):
try:
reports = json.loads(path.read_text())
assert isinstance(reports, list) and reports
cases = []
for report in reports:
assert report['SuiteSucceeded'] is True
specs = report['SpecReports']
assert specs
for spec in specs:
assert spec['State'] == 'passed'
if spec['LeafNodeType'] == 'It': cases.append(spec['LeafNodeText'])
assert cases and sorted(cases) == sorted(expected)
except (OSError, ValueError, KeyError, TypeError, AssertionError):
sys.exit('Ginkgo report missing, empty, skipped, failed or inventory mismatch: '+str(path))
return cases
seen = []
if entry['kind'] == 'ginkgo':
reports = entry.get('reports', [dict(path=entry['report'], cases=entry['cases'])])
if not reports: sys.exit('empty report inventory')
for report in reports: (root/report['path']).unlink(missing_ok=True)
for command in entry['commands']:
if entry['kind'] == 'ginkgo' and (command[:3] != ['go', 'test', '-count=1'] or '-tags' not in command or 'auth' not in command):
sys.exit('Ginkgo command requires go test -count=1 -tags auth')
for line in run(command).splitlines():
try: item = json.loads(line)
except ValueError: continue
if 'case' in item:
if item.get('status') != 'passed': sys.exit('required case not passed: '+item['case'])
seen.append(item['case'])
if entry['kind'] == 'ginkgo':
seen = []
for report in reports: seen.extend(ginkgo_report(root/report['path'], report['cases']))
if sorted(seen) != sorted(entry['cases']):
sys.exit('required case inventory mismatch: '+repr(seen))
# Run real readiness only after the shell test inventory has passed.
run(['sh', 'scripts/addons/prepare.sh', '--check'])
with tempfile.TemporaryDirectory(dir=cache/'tmp') as scratch:
scratch = pathlib.Path(scratch)
probe = scratch/'sqlite_test.go'
probe.write_text('''package readiness_test
import ("database/sql"; "testing"; _ "github.com/mattn/go-sqlite3"
. "github.com/onsi/ginkgo/v2"; . "github.com/onsi/gomega")
func TestReadiness(t *testing.T) { RegisterFailHandler(Fail); RunSpecs(t, "Native readiness") }
var _ = Describe("SQLite", func() { It("sqlite transaction", func() {
db,e:=sql.Open("sqlite3", ":memory:"); Expect(e).NotTo(HaveOccurred()); defer db.Close()
tx,e:=db.Begin(); Expect(e).NotTo(HaveOccurred())
_,e=tx.Exec("create table readiness(value integer); insert into readiness values (42)"); Expect(e).NotTo(HaveOccurred())
Expect(tx.Commit()).To(Succeed()); var n int
Expect(db.QueryRow("select value from readiness").Scan(&n)).To(Succeed()); Expect(n).To(Equal(42))
}) })
''')
report = cache/'logs'/('ginkgo-'+str(time.time_ns())+'.json')
run(['go', 'test', '-count=1', '-p', '2', '-tags', 'auth', str(probe), '-ginkgo.json-report='+str(report)])
ginkgo_report(report, ['sqlite transaction'])
# OS-selected isolated port, bounded health wait, private server temporary data.
with socket.socket() as sock:
sock.bind(('127.0.0.1', 0)); port = sock.getsockname()[1]
env['TMPDIR'] = str(scratch)
with (scratch/'server.log').open('w+') as output:
command = [str(root/'tests/e2e-ui/ui-test-server'), '--mock-backend='+str(root/'tests/e2e/mock-backend/mock-backend'), '--port='+str(port)]
server = subprocess.Popen(command, env=env, stdout=output, stderr=subprocess.STDOUT)
try:
url = 'http://127.0.0.1:'+str(port)
for _ in range(120):
if server.poll() is not None: raise RuntimeError('UI server exited '+str(server.returncode))
try:
with urllib.request.urlopen(url+'/readyz', timeout=1) as response:
if response.status == 200: break
except OSError: pass
time.sleep(1)
else: raise RuntimeError('UI server readiness timed out')
# Local Playwright CLI and library, never npx or an implicit install.
ui = root/'core/http/react-ui'
run([str(ui/'node_modules/.bin/playwright'), '--version'])
run(['node', '--input-type=module', '-e', '''
import { chromium } from '@playwright/test';
const browser = await chromium.launch({executablePath: process.env.PLAYWRIGHT_CHROMIUM_PATH || undefined});
try { const page=await browser.newPage(); const response=await page.goto(process.argv[1]);
if (!response.ok()) throw new Error('UI HTTP failure');
await page.locator('#root').waitFor();
await page.waitForFunction(()=>document.querySelector('#root')?.children.length > 0);
console.log(JSON.stringify({case:'browser',status:'passed'}));
} finally { await browser.close(); }
''', url], cwd=ui)
finally:
server.terminate()
try: server.wait(timeout=15)
except subprocess.TimeoutExpired: server.kill(); server.wait()
output.seek(0)
with log.open('a') as f: f.write(json.dumps(dict(command=command,status=server.returncode,output=output.read()))+'\n')
print(json.dumps(dict(suite=suite, passed=len(seen)+3, skipped=0, log=str(log))))
PY