Files
pulse/scripts/installtests/native_agent_frontend_test.go
T
pulse-triage[bot] 246646090a fix(ci): qualify the Actions patch and minor refresh
Own PR2936's stale pin and download-admission failures at b7e774d4. Refresh all current workflow consumers, including the newer pre-token issue-label job, with their exact static expectations and lifecycle/deployment contracts. Preserve Node 24, scoped lockfile caching, credential isolation and artifact identity/retention; reject old, mutable and arbitrary download pins.

Contract-Neutral: Immutable Actions pin refresh and coupled consumer/trust expectations only; workflow inputs, permissions, cache and credential controls, artifact identities and application/runtime contracts are unchanged.
Change-source: pulse-maintainer
2026-10-14 17:38:18 +01:00

266 lines
9.2 KiB
Go

package installtests
import (
"fmt"
"os"
"strings"
"testing"
"gopkg.in/yaml.v3"
)
type nativeAgentFrontendStep struct {
Name string
If string
Uses string
With map[string]string
WorkingDirectory string `yaml:"working-directory"`
Run string
ContinueOnError bool `yaml:"continue-on-error"`
}
func validateNativeImagePreflight(steps []nativeAgentFrontendStep) error {
goAt, checkAt, nodeAt := -1, -1, -1
for i, step := range steps {
switch step.Name {
case "Set up Go":
goAt = i
case "Check immutable integration images before frontend preparation":
if checkAt >= 0 {
return fmt.Errorf("immutable integration image preflight must run once")
}
checkAt = i
case "Set up Node.js for native runtime test assets":
nodeAt = i
}
}
if goAt < 0 || checkAt <= goAt || nodeAt <= checkAt {
return fmt.Errorf("immutable image checks must follow Go setup and precede frontend preparation")
}
check := steps[checkAt]
if check.If != "matrix.unix" || check.ContinueOnError || check.WorkingDirectory != "" ||
strings.TrimSpace(check.Run) != "go test -count=1 -timeout 3m -run '^TestIntegrationContainer' ./scripts/installtests" {
return fmt.Errorf("immutable image preflight must execute the uncached bounded source checks on every Unix platform and retain failure")
}
return validateNativeAgentFrontendPreparation(steps)
}
func TestNativeAgentRejectsBrokenImagesBeforeFrontendPreparation(t *testing.T) {
content, err := os.ReadFile(repoFile(".github", "workflows", "unified-agent-native.yml"))
if err != nil {
t.Fatal(err)
}
var workflow struct {
Jobs map[string]struct{ Steps []nativeAgentFrontendStep }
}
if err := yaml.Unmarshal(content, &workflow); err != nil {
t.Fatal(err)
}
steps := workflow.Jobs["native-agent"].Steps
if err := validateNativeImagePreflight(steps); err != nil {
t.Fatal(err)
}
checkAt, nodeAt := -1, -1
for i, step := range steps {
switch step.Name {
case "Check immutable integration images before frontend preparation":
checkAt = i
case "Set up Node.js for native runtime test assets":
nodeAt = i
}
}
for _, scenario := range []string{"missing", "late", "before Go", "Linux only", "hidden failure", "cached", "empty selection", "stub assets", "missing real build"} {
t.Run(scenario, func(t *testing.T) {
changed := append([]nativeAgentFrontendStep(nil), steps...)
switch scenario {
case "missing":
changed = append(changed[:checkAt], changed[checkAt+1:]...)
case "late":
changed[checkAt], changed[nodeAt] = changed[nodeAt], changed[checkAt]
case "before Go":
changed[checkAt], changed[checkAt-1] = changed[checkAt-1], changed[checkAt]
case "Linux only":
changed[checkAt].If = "runner.os == 'Linux'"
case "hidden failure":
changed[checkAt].ContinueOnError = true
case "cached":
changed[checkAt].Run = strings.Replace(changed[checkAt].Run, "-count=1 ", "", 1)
case "empty selection":
changed[checkAt].Run = strings.Replace(changed[checkAt].Run, "^TestIntegrationContainer", "^TestNonexistent", 1)
case "stub assets", "missing real build":
for i, step := range changed {
if step.Name == "Build real frontend assets for API-linked native tests" {
if scenario == "stub assets" {
changed[i].Run = "touch index.html"
} else {
changed = append(changed[:i], changed[i+1:]...)
}
break
}
}
}
if err := validateNativeImagePreflight(changed); err == nil {
t.Fatalf("accepted %s immutable-image preflight", scenario)
}
})
}
}
func validateNativeAgentFrontendTriggers(content []byte) error {
var workflow struct {
On map[string]struct{ Paths []string }
}
if err := yaml.Unmarshal(content, &workflow); err != nil {
return err
}
for _, event := range []string{"push", "pull_request"} {
paths := workflow.On[event].Paths
for _, required := range []string{"frontend-modern/package.json", "frontend-modern/package-lock.json"} {
found := false
for _, pattern := range paths {
if strings.HasPrefix(pattern, "!") {
return fmt.Errorf("%s must not exclude native frontend inputs", event)
}
found = found || pattern == required
}
if !found {
return fmt.Errorf("%s must admit native frontend graph changes in %s", event, required)
}
}
}
return nil
}
func TestNativeAgentWorkflowAdmitsFrontendGraphChanges(t *testing.T) {
content, err := os.ReadFile(repoFile(".github", "workflows", "unified-agent-native.yml"))
if err != nil {
t.Fatal(err)
}
if err := validateNativeAgentFrontendTriggers(content); err != nil {
t.Fatal(err)
}
// Decode each event, rather than accepting a matching path elsewhere in
// the file. Both PR qualification and containing-main checks need it.
for _, event := range []string{"push", "pull_request"} {
for _, required := range []string{"frontend-modern/package.json", "frontend-modern/package-lock.json"} {
t.Run(event+" missing "+required, func(t *testing.T) {
var workflow map[string]interface{}
if err := yaml.Unmarshal(content, &workflow); err != nil {
t.Fatal(err)
}
trigger := workflow["on"].(map[string]interface{})[event].(map[string]interface{})
paths := trigger["paths"].([]interface{})
var changed []interface{}
for _, pattern := range paths {
if pattern != required {
changed = append(changed, pattern)
}
}
if len(changed) != len(paths)-1 {
t.Fatal("control must remove exactly one original input")
}
trigger["paths"] = changed
mutant, err := yaml.Marshal(workflow)
if err != nil {
t.Fatal(err)
}
if err := validateNativeAgentFrontendTriggers(mutant); err == nil {
t.Fatal("accepted frontend graph change without native verification")
}
})
}
t.Run(event+" excluded frontend graph", func(t *testing.T) {
var workflow map[string]interface{}
if err := yaml.Unmarshal(content, &workflow); err != nil {
t.Fatal(err)
}
trigger := workflow["on"].(map[string]interface{})[event].(map[string]interface{})
trigger["paths"] = append(trigger["paths"].([]interface{}), "!frontend-modern/**")
mutant, err := yaml.Marshal(workflow)
if err != nil {
t.Fatal(err)
}
if err := validateNativeAgentFrontendTriggers(mutant); err == nil {
t.Fatal("accepted a later filter excluding the native frontend graph")
}
})
}
}
func validateNativeAgentFrontendPreparation(steps []nativeAgentFrontendStep) error {
indices := map[string]int{}
for i, step := range steps {
indices[step.Name] = i
}
names := []string{
"Set up Node.js for native runtime test assets",
"Install frontend test dependencies",
"Build real frontend assets for API-linked native tests",
"Test native agent runtime",
}
previous := -1
for _, name := range names {
i, ok := indices[name]
if !ok || i <= previous || steps[i].If != "" {
return fmt.Errorf("%s must cover every native platform, in preparation order", name)
}
previous = i
}
node, dependencies, build := steps[indices[names[0]]], steps[indices[names[1]]], steps[indices[names[2]]]
if node.Uses != "actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1" ||
node.With["node-version"] != "24" || node.With["cache"] != "npm" ||
node.With["cache-dependency-path"] != "frontend-modern/package-lock.json" {
return fmt.Errorf("native frontend preparation must use the pinned Node tool and locked cache")
}
if dependencies.WorkingDirectory != "frontend-modern" || strings.TrimSpace(dependencies.Run) != "npm ci" ||
build.WorkingDirectory != "frontend-modern" || strings.TrimSpace(build.Run) != "npm run build" {
return fmt.Errorf("native tests need the locked real frontend build, not placeholder assets")
}
return nil
}
func TestNativeAgentWorkflowBuildsRealFrontendBeforeRuntime(t *testing.T) {
content, err := os.ReadFile(repoFile(".github", "workflows", "unified-agent-native.yml"))
if err != nil {
t.Fatal(err)
}
var workflow struct {
Jobs map[string]struct{ Steps []nativeAgentFrontendStep }
}
if err := yaml.Unmarshal(content, &workflow); err != nil {
t.Fatal(err)
}
steps := workflow.Jobs["native-agent"].Steps
if err := validateNativeAgentFrontendPreparation(steps); err != nil {
t.Fatal(err)
}
for _, scenario := range []string{"missing build", "Windows-only preparation", "late build", "stub build"} {
t.Run(scenario, func(t *testing.T) {
changed := append([]nativeAgentFrontendStep(nil), steps...)
buildIndex, runtimeIndex := -1, -1
for i, step := range changed {
switch step.Name {
case "Build real frontend assets for API-linked native tests":
buildIndex = i
case "Test native agent runtime":
runtimeIndex = i
}
}
switch scenario {
case "missing build":
changed = append(changed[:buildIndex], changed[buildIndex+1:]...)
case "Windows-only preparation":
changed[buildIndex].If = "${{ !matrix.unix }}"
case "late build":
changed[buildIndex], changed[runtimeIndex] = changed[runtimeIndex], changed[buildIndex]
case "stub build":
changed[buildIndex].Run = "mkdir -p ../internal/api/frontend-modern/dist && touch ../internal/api/frontend-modern/dist/index.html"
}
if err := validateNativeAgentFrontendPreparation(changed); err == nil {
t.Fatalf("accepted %s instead of real all-platform preparation", scenario)
}
})
}
}