From 7f736165bb5df61cba77d2d193a633591a7444b5 Mon Sep 17 00:00:00 2001 From: nicodes Date: Thu, 20 Aug 2026 22:38:16 -0600 Subject: [PATCH 1/2] Generate new games against SDK v0.0.2 and smoke-test released modules Scaffolds pinned sdk v0.0.1 while v0.0.2 is the current release. Pin the template to v0.0.2 via a scaffoldSDK constant and add a consumer gate: a generated project in a temp dir outside the repository resolves only released modules (GOPROXY=off against a go.sum-verified cache preload, no replace), builds with GOWORK=off, and packages/ABI-validates as WASI. Ref aviorstudio/termcade-be#47 --- scaffold.go | 7 ++++- scaffold_test.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 scaffold_test.go diff --git a/scaffold.go b/scaffold.go index 1419c47..f34b28f 100644 --- a/scaffold.go +++ b/scaffold.go @@ -104,11 +104,16 @@ height = 40 "←/→" = "move paddle" ` +// scaffoldSDK is the released SDK version new games are generated against. +// It must be a published release: a scaffolded project resolves it from the +// module proxy with no replace, exactly like any other consumer. +const scaffoldSDK = "v0.0.2" + const goModTmpl = `module example.com/%s/%s go 1.26.2 -require github.com/aviorstudio/termcade/sdk v0.0.1 +require github.com/aviorstudio/termcade/sdk ` + scaffoldSDK + ` // Developing against a local termcade checkout? Point the sdk there: // replace github.com/aviorstudio/termcade/sdk => /path/to/termcade/sdk diff --git a/scaffold_test.go b/scaffold_test.go new file mode 100644 index 0000000..bf38d2e --- /dev/null +++ b/scaffold_test.go @@ -0,0 +1,81 @@ +package main + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +// TestScaffoldRequiresReleasedSDK pins the acceptance criterion: a new +// scaffold requires the released SDK v0.0.2, resolved like any other +// consumer — never a workspace path and never an unreleased version. +func TestScaffoldRequiresReleasedSDK(t *testing.T) { + if scaffoldSDK != "v0.0.2" { + t.Fatalf("scaffoldSDK = %s, want v0.0.2", scaffoldSDK) + } + if !strings.Contains(goModTmpl, "require github.com/aviorstudio/termcade/sdk "+scaffoldSDK+"\n") { + t.Fatalf("go.mod template does not require sdk %s:\n%s", scaffoldSDK, goModTmpl) + } +} + +// TestScaffoldedGameBuildsAsAReleasedConsumer is the released-consumer gate +// for the scaffold: a generated project in a temporary directory outside +// this repository must resolve only released modules, build with GOWORK=off, +// and package into a WASI .tcade that passes the same ABI validation an +// install would do. GOPROXY=off confines resolution to the local module +// cache, which the preload below fills through this repository's own +// go.sum-verified download — so the test proves a stranger's offline build +// works and cannot silently fall back to the workspace the way v0.0.3 did. +func TestScaffoldedGameBuildsAsAReleasedConsumer(t *testing.T) { + // Preload the released SDK into the module cache, verified against this + // repository's go.sum, so the generated project below resolves offline. + preload := exec.Command("go", "mod", "download", "github.com/aviorstudio/termcade/sdk") + preload.Env = append(os.Environ(), "GOWORK=off") + if out, err := preload.CombinedOutput(); err != nil { + t.Fatalf("preload released sdk: %v\n%s", err, out) + } + + // Everything after this point resolves offline, outside the workspace. + t.Setenv("GOWORK", "off") + t.Setenv("GOPROXY", "off") + + dir := filepath.Join(t.TempDir(), "pong") + if err := cmdDevNew([]string{"test/pong", dir}); err != nil { + t.Fatalf("dev new: %v", err) + } + + run := func(name string, args ...string) string { + t.Helper() + cmd := exec.Command(name, args...) + cmd.Dir = dir + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("%s %s: %v\n%s", name, strings.Join(args, " "), err, out) + } + return string(out) + } + run("go", "mod", "tidy") + + // Only released modules: no replace may be in effect, and the SDK must + // resolve to exactly the pinned release. + mods := run("go", "list", "-m", + "-f", `{{.Path}} {{.Version}} {{with .Replace}}REPLACED{{end}}`, "all") + if strings.Contains(mods, "REPLACED") { + t.Fatalf("generated project resolves through a replace:\n%s", mods) + } + if !strings.Contains(mods, "github.com/aviorstudio/termcade/sdk "+scaffoldSDK+" ") { + t.Fatalf("sdk did not resolve to released %s:\n%s", scaffoldSDK, mods) + } + + // dev build compiles the WASI plugin and ABI-validates it the way an + // install would. + if err := cmdDevBuild([]string{dir}); err != nil { + t.Fatalf("dev build: %v", err) + } + pkg := filepath.Join(dir, "build", "pong.tcade") + if info, err := os.Stat(pkg); err != nil || info.Size() == 0 { + t.Fatalf("expected non-empty %s, stat err: %v", pkg, err) + } +} From 5da2fa9846cd907fef151a720bc03ac3ffb36f05 Mon Sep 17 00:00:00 2001 From: nicodes Date: Thu, 20 Aug 2026 23:59:38 -0600 Subject: [PATCH 2/2] Isolate the consumer gate's module cache from ambient state The preload inherited the shared GOMODCACHE and default GOPROXY, so the offline consumer phase could silently resolve from ambient cache state. The test now owns a fresh GOMODCACHE; the preload runs against it with explicit proxy and checksum policy (verified against the repo's go.sum), and the test asserts the released SDK zip landed in that isolated cache before the consumer phase runs GOPROXY=off against it. -modcacherw keeps the test-owned cache writable so t.TempDir can remove it. Ref aviorstudio/termcade-be#47 --- scaffold_test.go | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/scaffold_test.go b/scaffold_test.go index bf38d2e..0d82136 100644 --- a/scaffold_test.go +++ b/scaffold_test.go @@ -24,22 +24,49 @@ func TestScaffoldRequiresReleasedSDK(t *testing.T) { // for the scaffold: a generated project in a temporary directory outside // this repository must resolve only released modules, build with GOWORK=off, // and package into a WASI .tcade that passes the same ABI validation an -// install would do. GOPROXY=off confines resolution to the local module -// cache, which the preload below fills through this repository's own -// go.sum-verified download — so the test proves a stranger's offline build -// works and cannot silently fall back to the workspace the way v0.0.3 did. +// install would do. The preload runs against a test-owned GOMODCACHE with +// explicit proxy/checksum policy, verified against this repository's go.sum; +// the consumer phase then runs with GOPROXY=off against that same isolated +// cache, so it proves a stranger's offline build works and cannot silently +// fall back to ambient module state or to the workspace the way v0.0.3 did. func TestScaffoldedGameBuildsAsAReleasedConsumer(t *testing.T) { // Preload the released SDK into the module cache, verified against this // repository's go.sum, so the generated project below resolves offline. + // The consumer gate owns its module cache: a fresh GOMODCACHE means the + // generated project can only ever resolve what the preload below fetched, + // never ambient state from a shared cache. + cache := t.TempDir() + + // Preload the released SDK into the test-owned cache, resolved through + // the public proxy and verified against this repository's go.sum + // (GOWORK=off makes the repo's go.mod the preload's context). Proxy and + // checksum policy are explicit rather than inherited. -modcacherw keeps + // the test-owned cache writable so t.TempDir can remove it. preload := exec.Command("go", "mod", "download", "github.com/aviorstudio/termcade/sdk") - preload.Env = append(os.Environ(), "GOWORK=off") + preload.Env = append(os.Environ(), + "GOWORK=off", + "GOMODCACHE="+cache, + "GOPROXY=https://proxy.golang.org,direct", + "GOSUMDB=sum.golang.org", + "GOFLAGS=-modcacherw", + ) if out, err := preload.CombinedOutput(); err != nil { t.Fatalf("preload released sdk: %v\n%s", err, out) } - // Everything after this point resolves offline, outside the workspace. + // Prove the preload landed in the isolated cache before trusting the + // consumer phase below to run offline. + zip := filepath.Join(cache, "cache", "download", "github.com", "aviorstudio", + "termcade", "sdk", "@v", scaffoldSDK+".zip") + if _, err := os.Stat(zip); err != nil { + t.Fatalf("isolated cache missing %s: %v", zip, err) + } + + // Everything after this point resolves offline from the isolated cache, + // outside the workspace. t.Setenv("GOWORK", "off") t.Setenv("GOPROXY", "off") + t.Setenv("GOMODCACHE", cache) dir := filepath.Join(t.TempDir(), "pong") if err := cmdDevNew([]string{"test/pong", dir}); err != nil {