Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
!cmd/
!internal/
!schemas/
!web/embed.go
!web/dist/
!web/
Comment thread
chlowell marked this conversation as resolved.
!docs.go
!docs/graders/
!version.txt

# Re-exclude test files pulled in by the allowlist above
# Re-exclude some files pulled in by the allowlist above
**/*_test.go
**/testdata/
web/node_modules/
web/dist/
web/test-results/
web/playwright-report/
15 changes: 15 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# Dockerfile for waza - AI agent skill evaluation CLI
# This provides a containerized environment for running waza in CI/CD pipelines

# Stage 1: Build web dashboard
FROM node:22-alpine AS web-builder

WORKDIR /build/web

COPY web/package.json web/package-lock.json ./
RUN npm ci --silent

COPY web/ ./
RUN npm run build

# Stage 2: Build Go binary
FROM golang:1.26-alpine AS builder

WORKDIR /build
Expand All @@ -12,8 +24,11 @@
RUN go mod download

# Copy source code
COPY . .

Check warning on line 27 in Dockerfile

View workflow job for this annotation

GitHub Actions / Build and Verify Docker Image

Attempting to Copy file that is excluded by .dockerignore

CopyIgnoredFile: Attempting to Copy file "." that is excluded by .dockerignore More info: https://docs.docker.com/go/dockerfile/rule/copy-ignored-file/

# Copy built web assets from web-builder stage
COPY --from=web-builder /build/web/dist/ web/dist/

# Build the binary with static linking
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-s -w' -o waza ./cmd/waza

Expand Down
25 changes: 16 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Waza Build System
.PHONY: all build clean test lint fmt install help
.PHONY: all build build-web clean test lint fmt install help

# Build configuration
BINARY_NAME=waza
Expand All @@ -9,15 +9,20 @@ VERSION?=0.1.0
LDFLAGS=-ldflags "-X main.version=$(VERSION)"

# Default target
all: fmt lint test build
all: fmt lint build test

# Build the binary
build:
# Build the web dashboard (React SPA)
build-web:
@echo "Building web dashboard..."
@cd web && npm ci --silent && npm run build

# Build the binary (includes web dashboard)
build: build-web
Comment thread
chlowell marked this conversation as resolved.
@echo "Building $(BINARY_NAME)..."
@go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/waza
Comment thread
chlowell marked this conversation as resolved.

# Run tests
test:
# Run tests (requires web assets for embedded FS tests)
test: build-web
@echo "Running tests..."
@go test -v -race -coverprofile=coverage.out ./...
@go tool cover -func=coverage.out | tail -1
Expand All @@ -38,8 +43,8 @@ fmt:
@gofmt -w $(GO_FILES)
@go mod tidy

# Install binary to GOPATH
install:
# Install binary to GOPATH (builds web dashboard first)
install: build-web
@echo "Installing $(BINARY_NAME)..."
@go install $(LDFLAGS) ./cmd/waza

Expand All @@ -48,16 +53,18 @@ clean:
@echo "Cleaning..."
@rm -f $(BUILD_DIR)/$(BINARY_NAME)
@rm -f coverage.out
@rm -rf web/dist/assets
@go clean -cache -testcache

# Show help
help:
@echo "Waza Makefile Targets:"
@echo " all - Format, lint, test, and build (default)"
@echo " all - Format, lint, build, and test (default)"
@echo " build - Compile the binary"
@echo " test - Run all tests with coverage"
@echo " lint - Run golangci-lint"
@echo " fmt - Format Go code and tidy modules"
@echo " install - Install binary to GOPATH"
@echo " build-web - Build the web dashboard (React SPA)"
@echo " clean - Remove build artifacts"
@echo " help - Show this help message"
7 changes: 6 additions & 1 deletion build.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/bin/bash
set -euo pipefail
Comment thread
chlowell marked this conversation as resolved.

# Get the directory of the script
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
Expand All @@ -15,13 +16,17 @@ OUTPUT_DIR="${OUTPUT_DIR:-$SCRIPT_DIR/bin}"
# Create output directory if it doesn't exist
mkdir -p "$OUTPUT_DIR"

# Build web dashboard
echo "Building web dashboard..."
(cd "$SCRIPT_DIR/web" && npm ci --silent && npm run build)
Comment thread
chlowell marked this conversation as resolved.

# Get Git commit hash and build date
COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
VERSION="${VERSION:-0.1.0}"

# List of OS and architecture combinations
if [ -n "$PLATFORM" ]; then
if [ -n "${PLATFORM:-}" ]; then
PLATFORMS=("$PLATFORM")
else
PLATFORMS=(
Expand Down
95 changes: 95 additions & 0 deletions internal/webserver/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ package webserver
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"log/slog"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"regexp"
"runtime"
"testing"
"time"

"github.com/microsoft/waza/web"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -129,6 +133,97 @@ func TestWebSocketUpgradeRequestFallsBackToSPA(t *testing.T) {
assert.Contains(t, string(body), "<!doctype html>")
}

// TestEmbeddedAssetsDirectoryContainsBundles verifies that the embedded
// web/dist filesystem includes at least one JS and one CSS bundle inside
// the assets/ subdirectory. A missing assets/ directory (or empty one) is
// exactly the bug that caused the blank-page issue.
func TestEmbeddedAssetsDirectoryContainsBundles(t *testing.T) {
distFS, err := fs.Sub(web.Assets, "dist")
require.NoError(t, err, "fs.Sub for dist should succeed")

entries, err := fs.ReadDir(distFS, "assets")
if errors.Is(err, fs.ErrNotExist) {
t.Skip("skipping: web/dist/assets not built (run 'cd web && npm run build' or 'make build-web')")

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests currently t.Skip when dist/assets is missing. That means the suite can still pass in environments that don’t run the web build step (e.g., CI/Windows or developers running go test ./... directly), which undermines the goal of catching the blank-page regression. Consider failing (require) when dist/assets is missing, or only skipping when an explicit opt-out env var is set (and default to failing in CI).

Suggested change
t.Skip("skipping: web/dist/assets not built (run 'cd web && npm run build' or 'make build-web')")
if os.Getenv("WAZA_SKIP_WEB_ASSET_TESTS") != "" {
t.Skip("skipping: web/dist/assets not built (run 'cd web && npm run build' or 'make build-web'; set WAZA_SKIP_WEB_ASSET_TESTS='' to enforce failure)")
}
require.FailNow(t, "web/dist/assets not built (run 'cd web && npm run build' or 'make build-web'; or set WAZA_SKIP_WEB_ASSET_TESTS=1 to skip this test)")

Copilot uses AI. Check for mistakes.
}
require.NoError(t, err, "unexpected error reading dist/assets")

var hasJS, hasCSS bool
for _, e := range entries {
if !e.IsDir() {
switch {
case filepath.Ext(e.Name()) == ".js":
hasJS = true
case filepath.Ext(e.Name()) == ".css":
hasCSS = true
}
}
}
assert.True(t, hasJS, "dist/assets must contain at least one .js bundle")
assert.True(t, hasCSS, "dist/assets must contain at least one .css bundle")
}

// TestIndexHTMLReferencesExistingAssets parses the embedded index.html for
// <script src="..."> and <link href="..."> tags pointing at /assets/*, then
// verifies that every referenced file actually exists in the embedded FS and
// is served with the correct content type — not the SPA HTML fallback.
func TestIndexHTMLReferencesExistingAssets(t *testing.T) {
distFS, err := fs.Sub(web.Assets, "dist")
require.NoError(t, err)

if _, err := fs.ReadDir(distFS, "assets"); errors.Is(err, fs.ErrNotExist) {
t.Skip("skipping: web/dist/assets not built (run 'cd web && npm run build' or 'make build-web')")
} else if err != nil {
Comment on lines +173 to +175

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: t.Skip when dist/assets doesn’t exist means this check won’t enforce the embed/build invariant in setups that don’t build the web UI first. If this is intended to prevent regressions in packaging, it should fail by default (or at least fail under CI) so missing bundles are surfaced.

Copilot uses AI. Check for mistakes.
require.NoError(t, err, "unexpected error reading dist/assets")
}

indexBytes, err := fs.ReadFile(distFS, "index.html")
require.NoError(t, err, "index.html must be readable")
html := string(indexBytes)

// Extract asset paths referenced in index.html.
scriptRe := regexp.MustCompile(`<script[^>]+src="(/assets/[^"]+)"`)
linkRe := regexp.MustCompile(`<link[^>]+href="(/assets/[^"]+)"`)

var refs []string
for _, m := range scriptRe.FindAllStringSubmatch(html, -1) {
refs = append(refs, m[1])
}
for _, m := range linkRe.FindAllStringSubmatch(html, -1) {
refs = append(refs, m[1])
}
require.NotEmpty(t, refs, "index.html must reference at least one asset in /assets/")

handler := newTestServer(t)

for _, ref := range refs {
t.Run(ref, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, ref, nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)

assert.Equal(t, http.StatusOK, rec.Code)

ct := rec.Header().Get("Content-Type")

// The critical assertion: the Content-Type must NOT be
// text/html. Before the fix, missing assets would
// silently return index.html via the SPA fallback,
// causing a blank page.
assert.NotContains(t, ct, "text/html",
"asset %s was served as text/html (SPA fallback); bundle is missing", ref)

switch filepath.Ext(ref) {
case ".js":
assert.Contains(t, ct, "javascript",
"JS bundle should be served with a javascript content type")
case ".css":
assert.Contains(t, ct, "css",
"CSS bundle should be served with a css content type")
}
})
}
}

func TestNewAppliesDefaults(t *testing.T) {
srv, err := New(Config{
NoBrowser: true,
Expand Down
4 changes: 2 additions & 2 deletions web/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>waza — eval dashboard</title>
<script type="module" crossorigin src="/assets/index-Dm42lpxa.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-eAT5_Cc2.css">
<script type="module" crossorigin src="/assets/index-C3jXWGPS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DUl_5EUI.css">
</head>
<body class="bg-zinc-900 text-zinc-100 antialiased">
<div id="root"></div>
Expand Down
Loading