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
22 changes: 1 addition & 21 deletions cmd/ailang/main_run_pipe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -105,7 +104,6 @@ export func main() -> () ! {IO, Clock} {
// If the buffer is broken (events only flush at exit), they all arrive
// at ~the same time near the end (~1.5-2s).
gotByEvent := map[string]time.Duration{}
deadline := time.After(4 * time.Second)
collect:
for len(gotByEvent) < 3 {
select {
Expand All @@ -114,7 +112,7 @@ collect:
break collect
}
gotByEvent[ev.line] = ev.at
case <-deadline:
case <-ctx.Done():
break collect
}
}
Expand All @@ -140,24 +138,6 @@ collect:
gap, minGap)
}

// Belt-and-suspenders: also assert EVENT_1 arrived before total runtime
// elapsed (i.e. before all three sleeps would have completed sequentially).
//
// On Windows the ailang binary cold-start cost is ~1.7s vs <0.5s on
// Linux/macOS — runner-VM filesystem + process-launch overhead — so the
// budget is widened there. The load-bearing assertion is the gap check
// above (EVENT_1 → EVENT_2 ≥ 200ms); this check is redundant guardrail.
eventOneBudget := 1500 * time.Millisecond
if runtime.GOOS == "windows" {
eventOneBudget = 3500 * time.Millisecond
}
if gotByEvent["EVENT_1"] > eventOneBudget {
t.Errorf("EVENT_1 arrived at %s — too late (budget %s). Expected first println "+
"to appear before the program had time to call all three sleeps. "+
"Suggests stdout is buffered until exit.",
gotByEvent["EVENT_1"], eventOneBudget)
}

// Diagnostic output for debugging.
t.Logf("event timings: EVENT_1=%s, EVENT_2=%s, EVENT_3=%s",
gotByEvent["EVENT_1"],
Expand Down
53 changes: 11 additions & 42 deletions cmd/ailang/main_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package main

import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"

"github.com/sunholo-data/ailang/internal/effects"
"github.com/sunholo-data/ailang/internal/testutil"
)

// runCLI runs the ailang CLI with given arguments and returns stdout, stderr, and exit code
Expand All @@ -23,26 +24,7 @@ func runCLI(t *testing.T, args ...string) (stdout, stderr string, exitCode int)
t.Fatalf("Failed to get project root: %v", err)
}

cmd := exec.Command("go", append([]string{"run", "./cmd/ailang"}, args...)...)
cmd.Dir = projectRoot // Run from project root so paths resolve correctly

var outBuf, errBuf bytes.Buffer
cmd.Stdout = &outBuf
cmd.Stderr = &errBuf

err = cmd.Run()
stdout = outBuf.String()
stderr = errBuf.String()

if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
} else {
t.Fatalf("Failed to run CLI: %v", err)
}
}

return stdout, stderr, exitCode
return testutil.RunBounded(t, projectRoot, 120*time.Second, "go", append([]string{"run", "./cmd/ailang"}, args...)...)
}

func TestCLI_Version(t *testing.T) {
Expand Down Expand Up @@ -428,7 +410,7 @@ var (
ailangBinOnce sync.Once
ailangBinPath string
ailangBinErr error
ailangBinOutput []byte
ailangBinOutput string
)

// buildAilang builds the ailang binary once per test run and returns its path.
Expand Down Expand Up @@ -463,9 +445,11 @@ func buildAilang(t *testing.T) string {
return
}
ailangBinPath = filepath.Join(dir, binName)
cmd := exec.Command("go", "build", "-o", ailangBinPath, "./cmd/ailang")
cmd.Dir = projectRoot
ailangBinOutput, ailangBinErr = cmd.CombinedOutput()
stdout, stderr, exitCode := testutil.RunBounded(t, projectRoot, 120*time.Second, "go", "build", "-o", ailangBinPath, "./cmd/ailang")
ailangBinOutput = stdout + stderr
if exitCode != 0 {
ailangBinErr = fmt.Errorf("go build exited with code %d", exitCode)
}
})
if ailangBinErr != nil {
t.Fatalf("Failed to build ailang: %v\n%s", ailangBinErr, ailangBinOutput)
Expand All @@ -480,22 +464,7 @@ func runAilangBin(t *testing.T, binPath string, args ...string) (stdout, stderr
if err != nil {
t.Fatalf("Failed to get project root: %v", err)
}
cmd := exec.Command(binPath, args...)
cmd.Dir = projectRoot
var outBuf, errBuf bytes.Buffer
cmd.Stdout = &outBuf
cmd.Stderr = &errBuf
err = cmd.Run()
stdout = outBuf.String()
stderr = errBuf.String()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
} else {
t.Fatalf("Failed to run ailang: %v", err)
}
}
return stdout, stderr, exitCode
return testutil.RunBounded(t, projectRoot, 60*time.Second, binPath, args...)
}

func TestCLI_Exit_Code0(t *testing.T) {
Expand Down
3 changes: 0 additions & 3 deletions cmd/ailang/serve_api_mcp_surface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ import (
)

func TestServeAPI_MCPToolSurface(t *testing.T) {
if testing.Short() {
t.Skip("builds and drives the serve-api stdio MCP binary")
}
binary := buildAilang(t)

moduleRoot := t.TempDir()
Expand Down
4 changes: 0 additions & 4 deletions internal/ai/ollama/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,6 @@ func TestGuessProvider(t *testing.T) {
// TestCheckConnection is an integration test that requires Ollama running.
// Skip if Ollama is not available.
func TestCheckConnection(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}

client, err := NewClient()
if err != nil {
t.Fatalf("NewClient() error = %v", err)
Expand Down
4 changes: 4 additions & 0 deletions internal/coordinator/provider_script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ func TestScriptProvider_Execute(t *testing.T) {
})

t.Run("timeout kills script", func(t *testing.T) {
// Gatelint R2 allowlist reason: this is a Unix shell/grandchild signal
// semantics test, not a live-network test, so the network opt-in helper
// would misstate its requirement. CI runners skip the known flaky shell
// behavior while local Unix runs retain coverage.
// Skip in CI - exec.CommandContext signal handling is unreliable
// on Linux when using "bash -c" because the shell doesn't forward
// signals to child processes. This causes the test to be flaky.
Expand Down
60 changes: 50 additions & 10 deletions internal/effects/net_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package effects

import (
"net"
"os"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/sunholo-data/ailang/internal/eval"
"github.com/sunholo-data/ailang/internal/testutil"
)

// TestValidateIP_MetadataServer tests the cloud metadata server exception.
Expand Down Expand Up @@ -359,10 +361,7 @@ func TestNetHttpPost(t *testing.T) {
})

t.Run("httpPost to httpbin.org", func(t *testing.T) {
// Skip in CI environments due to unreliable external network access
if os.Getenv("SKIP_NET_TESTS") != "" || os.Getenv("CI") != "" || os.Getenv("GITHUB_ACTIONS") != "" {
t.Skip("Skipping network test in CI environment (unreliable external access)")
}
testutil.RequiresLiveNetwork(t)

url := &eval.StringValue{Value: "https://httpbin.org/post"}
body := &eval.StringValue{Value: `{"test": "data", "value": 42}`}
Expand All @@ -377,13 +376,57 @@ func TestNetHttpPost(t *testing.T) {
if !ok {
t.Errorf("Expected StringValue, got %T", result)
} else if !strings.Contains(strResult.Value, "httpbin.org") {
t.Errorf("Expected response containing 'httpbin.org', got: %s", strResult.Value)
t.Logf("Live endpoint returned a non-canonical response (possibly non-2xx); deterministic response assertions are covered by local-server subtests: %s", strResult.Value)
}
}
} else {
t.Logf("Network error (expected in some environments): %v", err)
}
})

for _, tc := range []struct {
name string
statusCode int
response string
}{
{name: "local success response", statusCode: http.StatusOK, response: `{"ok":true}`},
{name: "local non-2xx response", statusCode: http.StatusServiceUnavailable, response: `{"error":"unavailable"}`},
} {
t.Run(tc.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("method = %s, want POST", r.Method)
}
w.WriteHeader(tc.statusCode)
_, _ = w.Write([]byte(tc.response))
}))
defer server.Close()

localCtx := NewEffContext([]string{})
localCtx.Grant(NewCapability("Net"))
localCtx.Net = NewNetContext()
// Local test servers use plain HTTP on loopback, so both capabilities
// must be explicit. This deterministic coverage therefore exercises a
// different capability posture from the HTTPS live-endpoint subtest.
localCtx.Net.AllowHTTP = true
localCtx.Net.AllowLocalhost = true

result, err := netHTTPPost(localCtx, []eval.Value{
&eval.StringValue{Value: server.URL},
&eval.StringValue{Value: `{"request":"body"}`},
})
if err != nil {
t.Fatalf("netHTTPPost: %v", err)
}
got, ok := result.(*eval.StringValue)
if !ok {
t.Fatalf("result type = %T, want *eval.StringValue", result)
}
if got.Value != tc.response {
t.Errorf("response = %q, want %q", got.Value, tc.response)
}
})
}
}

// TestNetBodySizeLimit verifies response size limiting
Expand All @@ -395,10 +438,7 @@ func TestNetBodySizeLimit(t *testing.T) {

t.Run("small response under limit", func(t *testing.T) {
// httpbin.org/get returns ~270 bytes, should exceed 100 byte limit
// Skip in CI environments due to unreliable external network access
if os.Getenv("SKIP_NET_TESTS") != "" || os.Getenv("CI") != "" || os.Getenv("GITHUB_ACTIONS") != "" {
t.Skip("Skipping network test in CI environment (unreliable external access)")
}
testutil.RequiresLiveNetwork(t)

url := &eval.StringValue{Value: "https://httpbin.org/get"}
_, err := netHTTPGet(ctx, []eval.Value{url})
Expand Down
3 changes: 0 additions & 3 deletions internal/effects/process_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,9 +434,6 @@ func TestProcessExec_WaitDelay_OrphanGrandchildNoHang(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("bash test requires unix")
}
if testing.Short() {
t.Skip("WaitDelay timing test (~5s)")
}
ctx := newProcessCtx()
args := []eval.Value{
&eval.StringValue{Value: "bash"},
Expand Down
27 changes: 17 additions & 10 deletions internal/eval_harness/reference_solutions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"strings"
"testing"
"time"

"github.com/sunholo-data/ailang/internal/testutil"
)

// referenceSolution describes one benchmark/language pair.
Expand Down Expand Up @@ -67,6 +69,20 @@ func testReferenceSolutions(t *testing.T, lang string) {
t.Skipf("cannot find repo root: %v", err)
}

// Pay each language runtime's cold-start cost once outside the asserted
// cases. The warm-up result is intentionally ignored: these tests assert
// the checked-in reference programs, not runtime startup behavior.
warmupCode := `console.log("warmup")`
if lang == "go" {
warmupCode = "package main\nimport \"fmt\"\nfunc main() { fmt.Println(\"warmup\") }\n"
}
for _, rs := range referenceSolutionsTable {
if rs.lang == lang {
_, _ = rs.runner().Run(warmupCode, testutil.HangGuard(t, 120*time.Second))
break
}
}

for _, rs := range referenceSolutionsTable {
if rs.lang != lang {
continue
Expand All @@ -79,17 +95,8 @@ func testReferenceSolutions(t *testing.T, lang string) {
t.Fatalf("reference solution not found: %s: %v", srcPath, err)
}

// Reference solutions are tiny programs; wall-clock is dominated
// by interpreter startup (~slow on Windows CI runners — node alone
// can take >20s cold). recursion_fibonacci was getting the only
// 60s slot, but fizzbuzz hits the same 30s cliff on Windows. Give
// every benchmark the same generous slot — the only thing the
// shorter limit was buying was faster failure on a hang, and any
// real hang would still time out well before 60s of useful work.
timeout := 60 * time.Second

runner := rs.runner()
result, err := runner.Run(string(code), timeout)
result, err := runner.Run(string(code), testutil.HangGuard(t, 120*time.Second))
if err != nil {
t.Fatalf("runner error: %v", err)
}
Expand Down
9 changes: 0 additions & 9 deletions internal/gen/golang/contracts_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@ import (
// 3. Runs tests that trigger contract violations
// 4. Verifies panics occur with correct messages
func TestContractViolation_Integration(t *testing.T) {
// Skip in short mode (these tests compile and run Go code)
if testing.Short() {
t.Skip("skipping integration test in short mode")
}

// Create temp directory for test
tmpDir, err := os.MkdirTemp("", "contract_test_*")
if err != nil {
Expand Down Expand Up @@ -246,10 +241,6 @@ func TestIncrement_EnsuresViolation(t *testing.T) {
// TestContractViolation_NoVerify verifies that without --verify-contracts,
// contract violations do NOT cause panics (contracts are just comments)
func TestContractViolation_NoVerify(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}

tmpDir, err := os.MkdirTemp("", "contract_noverify_*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
Expand Down
4 changes: 0 additions & 4 deletions internal/pipeline/validate_effects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,6 @@ func TestValidateEffects_LargeArrayPerformance(t *testing.T) {
// TestValidateEffects_LinearScaling verifies that effect checking scales linearly with input size.
// Note: This test uses warmup iterations and takes minimum times to be robust on CI.
func TestValidateEffects_LinearScaling(t *testing.T) {
if testing.Short() {
t.Skip("skipping performance test in short mode")
}

sizes := []int{10, 50, 100}
var times []time.Duration
const iterations = 5 // Run multiple times and take minimum
Expand Down
6 changes: 3 additions & 3 deletions internal/pkg/gitcache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"path/filepath"
"strings"
"testing"

"github.com/sunholo-data/ailang/internal/testutil"
)

func TestGitCache_CacheDir_Deterministic(t *testing.T) {
Expand Down Expand Up @@ -46,9 +48,7 @@ func TestGitCache_Resolve_RequiresTagOrRev(t *testing.T) {

// Integration test — requires git and network access
func TestGitCache_Resolve_RealRepo(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
testutil.RequiresLiveNetwork(t)

cache := &GitCache{baseDir: t.TempDir()}

Expand Down
Loading
Loading