diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..bdae166c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.snap -text diff --git a/CLAUDE.md b/CLAUDE.md index 1aab3800..e9f84f0e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -233,6 +233,7 @@ When drafting Slack messages, PR descriptions, review replies, release notes, or # Testing - Prefer integration tests to cover most cases. Use unit tests when integration tests are not practical. +- Assertions use `github.com/stretchr/testify` (`require` for fatal checks). Snapshots use the in-house `internal/snap` helper: `snap.Match(t, s)` stores snapshots in one archive per test file (`__snapshots__/.snap`): a `[TestName_N]` header line per Match call, the value verbatim, and a `---` terminator on its own line (so a value and the same value plus one final newline are indistinguishable; other newlines are preserved exactly). Missing snapshots are created locally but fail in CI; `UPDATE_SNAPS=true go test` rewrites them. The format has no escaping, so a value containing a line that is exactly `---` is rejected (round-trip guard) — sanitize such values before matching. `snap.MatchJSON(t, raw, "data.currentVersion", ...)` snapshots a JSON document in canonical pretty-printed form, masking the values at the given dotted paths with `` (a path that doesn't resolve fails the test; objects only, no array indexing). A package using snapshots must wire `func TestMain(m *testing.M) { os.Exit(snap.Clean(m)) }` — obsolete snapshots then fail the run (or are deleted under `UPDATE_SNAPS=true`); cleanup is skipped on filtered (`-run`/`-skip`) or failed runs. Sanitize volatile values before matching with label-anchored regexes, not value-shaped ones (see `sanitizeSnapshot` in `internal/output`; RE2 has no lookahead, so a bare semver pattern corrupts IPv4 strings). - **When fixing a bug, always add an integration test** that fails before the fix and passes after. This prevents regressions and documents the exact scenario that was broken. - Integration tests that run the CLI binary with Bubble Tea must use a PTY since Bubble Tea requires a terminal. Use the cross-platform helpers in `test/integration/pty_helpers_test.go` — `runLstkInPTY` for run-to-completion, or `startLstkInPTY`/`startCmdInPTY` returning a `ptyProc` with `waitForOutput`/`write`/`wait`/`kill` for interactive prompts — never `creack/pty` directly. The helpers wrap `charmbracelet/x/xpty` (Unix PTY on macOS/Linux, ConPTY on Windows), so PTY tests run on Windows too; output from `wait()`/`output()` is ANSI-stripped because ConPTY injects its own repaint sequences. `creack/pty` remains only in `signal_forwarding_test.go` (build-tagged `!windows`; POSIX signal semantics don't port). - Never fake an external CLI (`aws`, `az`, `terraform`, `cdk`, `sam`, `aws_completer`, browser openers) with a shell script — scripts don't run on Windows. Use `writeFakeTool`/`installFakeTool` (`test/integration/faketool_test.go`), which copy a compiled Go stand-in (`test-samples/faketool`) onto PATH configured via a JSON sidecar (echo lines with `{argN}`/`{args}`/`{env:NAME}` placeholders, arg-prefix cases like `--version`, sleep, exit codes, record-to-file). diff --git a/go.mod b/go.mod index 349d86e5..24a92fc0 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/containerd/errdefs v1.0.0 github.com/creack/pty v1.1.24 github.com/docker/go-units v0.5.0 + github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/hashicorp/hcl/v2 v2.24.0 github.com/moby/moby/api v1.55.0 @@ -67,7 +68,6 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect diff --git a/internal/extension/resolve.go b/internal/extension/resolve.go index 98d34dc6..f6d9b8c9 100644 --- a/internal/extension/resolve.go +++ b/internal/extension/resolve.go @@ -6,6 +6,7 @@ import ( "os/exec" "path/filepath" goruntime "runtime" + "slices" "sort" "strings" @@ -128,7 +129,15 @@ func scanDir(dir string) []string { } name := strings.TrimPrefix(fileName, NamePrefix) if goruntime.GOOS == "windows" { - name = strings.TrimSuffix(name, filepath.Ext(name)) + // isExecutableFile accepts any regular file on Windows, so + // executability must be decided here by PATHEXT — otherwise a + // data file like lstk-extensions.toml lists as a phantom + // "extensions" extension. + ext := strings.ToLower(filepath.Ext(name)) + if !slices.Contains(windowsExts(), ext) { + continue + } + name = strings.TrimSuffix(name, ext) } if name == "" { continue diff --git a/internal/output/__snapshots__/envelope_sink_test.snap b/internal/output/__snapshots__/envelope_sink_test.snap new file mode 100644 index 00000000..d8305bcf --- /dev/null +++ b/internal/output/__snapshots__/envelope_sink_test.snap @@ -0,0 +1,17 @@ +Snapshots created by internal/snap. UPDATE_SNAPS=true go test rewrites +this file. + +[TestEnvelopeSink_UpdateCheckedEnvelopeJSON_1] +{ + "command": "update", + "data": { + "currentVersion": "", + "latestVersion": "", + "updateAvailable": true + }, + "error": null, + "schemaVersion": 1, + "status": "ok", + "warnings": [] +} +--- diff --git a/internal/output/__snapshots__/plain_sink_test.snap b/internal/output/__snapshots__/plain_sink_test.snap new file mode 100644 index 00000000..02748312 --- /dev/null +++ b/internal/output/__snapshots__/plain_sink_test.snap @@ -0,0 +1,66 @@ +Snapshots created by internal/snap. UPDATE_SNAPS=true go test rewrites +this file. + +[TestPlainSink_EmitsErrorEvent_1] +Error: Connection failed + Cannot connect to Docker + ==> Start Docker: open -a Docker +--- + +[TestPlainSink_EmitsInstanceInfoEvent_full_1] +✔︎ LocalStack AWS Emulator is running +• Endpoint: localhost.localstack.cloud:4566 +• Persistence: Enabled +• Container: localstack-aws +• Version: +• Uptime: