From 2f860ca9e48969df50abc0ef85c57d60c208afcd Mon Sep 17 00:00:00 2001 From: vickydotbat Date: Fri, 31 Jul 2026 18:42:20 +0000 Subject: [PATCH] fix(nwsync): write emit and assemble summaries to stderr (#82) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #81. `crucible nwsync emit` and `assemble` printed their summary line to **stdout**. Any caller that captures a script's stdout as a value gets the summary glued onto it — `pack-haks.sh` does `release_dir="$(...)"`, so all 11 emit summaries landed in `$release_dir` and `publish-release.sh` died with "release dir not found". Both lines move to stderr, where `lib.sh`'s own `nwsync: emitted $key` log already goes. Neither line is a machine-readable contract: sow-topdata's contract tests grep an `EMIT_LOG` their own fake-crucible stub writes, not real stdout, so nothing parses these. `runEmit`/`runAssemble` no longer take the stdout writer — a leak in those two functions is now impossible to write by accident. `Run` still passes stdout to `printRunUsage` for explicit `-h`/`help`, which is correct. Adds `TestRunKeepsSummariesOffStdout`: runs both verbs end to end against a local tree and asserts stdout stays empty while the summary reaches stderr. It asserts emptiness, not wording, so the summary text stays free to change. Full `go test ./...` green. Follow-up, outside this repo: sow-assets-manifest needs a `flake.lock` bump, then a re-run of the v0.2.1-rc1 tag. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: https://git.westgate.pw/ShadowsOverWestgate/sow-tools/pulls/82 Reviewed-by: xtul Co-authored-by: vickydotbat --- internal/nwsync/nwsync_test.go | 27 +++++++++++++++++++++++++++ internal/nwsync/run.go | 12 ++++++------ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/internal/nwsync/nwsync_test.go b/internal/nwsync/nwsync_test.go index 0eb5c5c..0673f25 100644 --- a/internal/nwsync/nwsync_test.go +++ b/internal/nwsync/nwsync_test.go @@ -458,6 +458,33 @@ func TestAssembleFailsClosedOnMissingIndex(t *testing.T) { } } +// Callers capture a script's stdout as a value: `dir="$(pack-haks.sh)"`. A +// summary line on stdout gets glued onto that value, so both summaries belong +// on stderr. +func TestRunKeepsSummariesOffStdout(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "sow_top.hak") + writeHak(t, path, map[string][]byte{"appearance.2da": []byte("2da from sow_top")}) + key := artifactKey(t, path) + out := filepath.Join(dir, "out") + + for _, args := range [][]string{ + {"emit", "--out", out, "--as", "sow_top.hak", key, path}, + {"assemble", "--out", out, key}, + } { + var stdout, stderr bytes.Buffer + if code := Run(args, &stdout, &stderr); code != exitOK { + t.Fatalf("Run(%v) exit=%d: %s", args, code, stderr.String()) + } + if stdout.Len() != 0 { + t.Errorf("Run(%v) wrote to stdout: %q", args, stdout.String()) + } + if stderr.Len() == 0 { + t.Errorf("Run(%v) reported no summary on stderr", args) + } + } +} + func TestRunUsageErrors(t *testing.T) { cases := [][]string{ nil, diff --git a/internal/nwsync/run.go b/internal/nwsync/run.go index de2a88c..a25b519 100644 --- a/internal/nwsync/run.go +++ b/internal/nwsync/run.go @@ -29,9 +29,9 @@ func Run(args []string, stdout, stderr io.Writer) int { } switch args[0] { case "emit": - return runEmit(args[1:], stdout, stderr) + return runEmit(args[1:], stderr) case "assemble": - return runAssemble(args[1:], stdout, stderr) + return runAssemble(args[1:], stderr) case "-h", "--help", "help": printRunUsage(stdout) return exitOK @@ -76,7 +76,7 @@ func parseArgs(fs *flag.FlagSet, args []string) ([]string, error) { } } -func runEmit(args []string, stdout, stderr io.Writer) int { +func runEmit(args []string, stderr io.Writer) int { fs := flag.NewFlagSet("emit", flag.ContinueOnError) fs.SetOutput(stderr) as := fs.String("as", "", "published name of the artifact, when it differs from the key") @@ -106,12 +106,12 @@ func runEmit(args []string, stdout, stderr io.Writer) int { fmt.Fprintf(stderr, "nwsync emit: %v\n", err) return exitInternal } - fmt.Fprintf(stdout, "emitted %s: %d resources, %d new blobs, index %s\n", + fmt.Fprintf(stderr, "emitted %s: %d resources, %d new blobs, index %s\n", result.Name, result.Entries, result.BlobsWritten, result.ManifestPath) return exitOK } -func runAssemble(args []string, stdout, stderr io.Writer) int { +func runAssemble(args []string, stderr io.Writer) int { fs := flag.NewFlagSet("assemble", flag.ContinueOnError) fs.SetOutput(stderr) tlkKey := fs.String("tlk-key", "", "depot key of the TLK, which shadows nothing and merges last") @@ -140,7 +140,7 @@ func runAssemble(args []string, stdout, stderr io.Writer) int { fmt.Fprintf(stderr, "nwsync assemble: %v\n", err) return exitInternal } - fmt.Fprintf(stdout, "assembled manifest %s: %d resources, %s\n", + fmt.Fprintf(stderr, "assembled manifest %s: %d resources, %s\n", result.SHA1, result.Entries, result.ManifestPath) return exitOK }