juanfont/headscale · error
test(s) failed
Error message
test(s) failed
What it means
Wraps the failure of the policy's 'tests' block assertions (hscontrol/policy/v2/test.go:3228): one or more PolicyTest entries evaluated against the compiled global filter rules did not pass, and the individual failures are joined with multierr underneath this sentinel. It is produced at policy write boundaries — 'headscale policy set', file-mode reload after a change, 'headscale policy check' — so a bad policy never becomes live.
Source
Thrown at hscontrol/policy/v2/test.go:32
"tailscale.com/types/views"
)
// Tailscale's policy file `tests` block validates a policy against operator
// assertions: from a given src, named dst:port pairs must be accepted, and
// (optionally) other dst:port pairs must be denied. They run at user-write
// boundaries — `headscale policy set`, file-mode reload after a change,
// `headscale policy check` — and reject the write if any assertion fails.
// Boot-time reload of an already-stored policy does not run them, so a
// stale referenced entity (e.g. a deleted user) cannot lock the server out.
//
// The tests evaluate against the compiled global filter rules, which fold in
// both `acls` and `grants`, so the `tests` block validates the whole policy.
// errPolicyTestsFailed and errSSHPolicyTestsFailed share the
// "test(s) failed" prefix but stay distinct so callers can use
// [errors.Is] to tell ACL-test and SSH-test failures apart.
var (
errPolicyTestsFailed = errors.New("test(s) failed")
errSSHPolicyTestsFailed = errors.New("test(s) failed")
errTestDestinationNoIP = errors.New("destination resolved to no IP addresses")
)
// PolicyTest is one entry in the policy's `tests` block.
type PolicyTest struct {
// Src is a single source alias (user, group, tag, host, autogroup, or IP).
// Tailscale only supports a single src per test entry.
Src string `json:"src"`
// Proto restricts the test to one protocol. Empty matches the default
// set the client applies when proto is omitted (TCP/UDP/ICMP).
Proto Protocol `json:"proto,omitempty"`
// Accept lists destinations in `host:port` form that must be reachable
// from Src. A test fails if any entry is denied by the compiled filter.
Accept []string `json:"accept,omitempty"`
View on GitHub (pinned to 565fd254d0)
Solutions
- Read the joined per-test errors under the 'test(s) failed:' prefix — each names the failing src and destination
- Fix the grants to match the intended assertion, or fix the test to match the actual rules
- Iterate with 'headscale policy check -f policy.hujson' until the tests pass, then apply
Example fix
// before: test expects access the grants do not grant
"tests": [{"src": "user-a", "accept": ["tag:web:80", "tag:web:443"]}]
// after: grant it, then keep the test
"grants": [{"src": ["user-a"], "dst": ["tag:web"], "ip": ["tcp/80", "tcp/443"]}] Defensive patterns
Strategy: try-catch
Try / catch
err := pm.Compile(); if errors.Is(err, errPolicyTestsFailed) { // iterate: print the multierr detail lines, fix grants or tests, re-run 'policy check' } Prevention
- Keep the tests block in lockstep with grants during refactors
- Gate policy deploys on 'headscale policy check' in CI
- Note boot-time reload skips tests — always validate at write time
When it happens
Trigger: A policy whose tests block contains an assertion contradicting the acls/grants, e.g. {'src': 'user-a', 'accept': ['tag:web:80']} when no grant actually allows user-a to tag:web on port 80. RunTests collects each failing entry and returns fmt.Errorf wrapping errPolicyTestsFailed.
Common situations: Refactoring grants without updating tests; tests written against stale users/tags after deletions; port/proto mismatch between the test's 'accept' entries and the grants.
Related errors
- nodeAttrs target does not support this autogroup
- username must contain @
- group must start with 'group:'
- tag must start with 'tag:'
- invalid hostname
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/84fb383f2653aa62.
Report an issue: GitHub.