Skip to content

Commit 2eae0d7

Browse files
authored
test(e2e): cover Git Bash npm and handle missing ARM64 musl releases (#34)
1 parent dd4283c commit 2eae0d7

8 files changed

Lines changed: 126 additions & 15 deletions

File tree

‎docs/e2e.md‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,15 @@ driven from Go through `docker exec`-style calls):
2020
3. A pinned Node.js release installs with checksum verification and runs
2121
(`node --version`, `npm --version`, `process.platform`, `process.arch`),
2222
so the `linux-x64` / `linux-arm64` / `win-x64` / `win-arm64` package
23-
selection is checked for real. The LTS release (`LTS_NODE_VERSION`)
24-
installs as well, backing the codename probe below.
23+
selection is checked for real. On Windows, Git Bash also runs `node`,
24+
`npm.cmd`, and extensionless `npm` after activation to cover
25+
[issue #293](https://github.com/version-fox/vfox/issues/293). The LTS
26+
release (`LTS_NODE_VERSION`) installs as well, backing the codename probe
27+
below.
2528
4. `nodejs@latest` resolves through the plugin's `Available` list (the
26-
first, newest entry) and installs.
29+
first, newest entry) and installs when that platform's binary is published.
30+
Alpine ARM64 skips this probe only while the matching upstream unofficial
31+
musl build is absent; it still verifies the pinned and LTS releases.
2732
5. The major and major.minor shorthands resolve through `PreUse` against the
2833
installed versions.
2934
6. `.nvmrc` and `.node-version` drive `vfox env` selection: the exact

‎tests/e2e/linux/e2e_test.go‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,9 @@ func runOne(t *testing.T, ctx context.Context, image, platform, arch string, c c
203203
lts := common.ResolveLTSVersion(t)
204204
installNodeVersion(ctx, t, ctr, lts)
205205

206-
latest := installLatestNode(ctx, t, ctr)
207-
execOK(ctx, t, ctr, activated(fmt.Sprintf("vfox use --global nodejs@%s", latest)))
208-
verifyToolchain(ctx, t, ctr, latest, arch)
206+
if latest, ok := installLatestNode(ctx, t, ctr); ok {
207+
execOK(ctx, t, ctr, activated(fmt.Sprintf("vfox use --global nodejs@%s", latest)))
208+
verifyToolchain(ctx, t, ctr, latest, arch)
209+
}
209210
verifyLegacyFiles(ctx, t, ctr, version, lts)
210211
}

‎tests/e2e/linux/install.go‎

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package linux
1717
import (
1818
"context"
1919
"fmt"
20+
"strings"
2021
"testing"
2122

2223
"github.com/stretchr/testify/require"
@@ -38,14 +39,68 @@ func installNodeVersion(ctx context.Context, t *testing.T, ctr tc.Container, ver
3839
execOK(ctx, t, ctr, activated(fmt.Sprintf("vfox use --global nodejs@%s", version)))
3940
}
4041

41-
// installLatestNode installs the newest published Node.js version. `vfox
42-
// install nodejs@latest` resolves @latest through the plugin's Available
43-
// (the first, newest entry) and prints the pick; the version is returned so
44-
// the caller can drive `vfox use` against it.
45-
func installLatestNode(ctx context.Context, t *testing.T, ctr tc.Container) string {
42+
// installLatestNode installs the newest published Node.js version. If the
43+
// latest official release has no unofficial ARM64 musl build yet, that one
44+
// platform-specific probe is skipped; the other latest probes still cover
45+
// @latest resolution and installation.
46+
func installLatestNode(ctx context.Context, t *testing.T, ctr tc.Container) (string, bool) {
4647
t.Helper()
47-
out := execRetry(ctx, t, ctr, "vfox install nodejs@latest", "vfox install nodejs@latest")
48+
const label = "vfox install nodejs@latest"
49+
code, out := execRun(ctx, t, ctr, label)
50+
if code != 0 {
51+
version, filename, missing := missingLatestArm64MuslBuild(out)
52+
if missing {
53+
available, known := unofficialArm64MuslBuildAvailable(ctx, t, ctr, version, filename)
54+
if known && !available {
55+
t.Logf("SKIP @latest install: unofficial musl ARM64 build %s is not published yet", filename)
56+
return "", false
57+
}
58+
}
59+
out = common.Retry(ctx, t, label, func() (int, string) {
60+
return execRun(ctx, t, ctr, label)
61+
})
62+
}
4863
latest := common.LatestUsed(out)
4964
require.NotEmptyf(t, latest, "@latest install did not report the picked version\n--- output ---\n%s", out)
50-
return latest
65+
return latest, true
66+
}
67+
68+
// missingLatestArm64MuslBuild recognizes only the specific error emitted
69+
// after the unofficial-builds checksum list also lacks the requested asset.
70+
func missingLatestArm64MuslBuild(output string) (version, filename string, missing bool) {
71+
output = common.StripANSI(output)
72+
version = common.LatestUsed(output)
73+
if version == "" {
74+
return "", "", false
75+
}
76+
filename = fmt.Sprintf("node-v%s-linux-arm64-musl.tar.gz", version)
77+
message := fmt.Sprintf("Node.js package %s is not listed in ", filename)
78+
missing = strings.Contains(output, message) && strings.Contains(output, "SHASUMS256.txt")
79+
return version, filename, missing
80+
}
81+
82+
// unofficialArm64MuslBuildAvailable checks the exact upstream checksum list
83+
// before treating a missing artifact as an expected release-publication lag.
84+
// The second return value is false when the HTTP result is inconclusive, so
85+
// network errors still fail through the normal retry path instead of skipping.
86+
func unofficialArm64MuslBuildAvailable(ctx context.Context, t *testing.T, ctr tc.Container, version, filename string) (available, known bool) {
87+
t.Helper()
88+
url := fmt.Sprintf("https://unofficial-builds.nodejs.org/download/release/v%s/SHASUMS256.txt", version)
89+
script := fmt.Sprintf("curl -fsSL --max-time 20 -w '\\n%%{http_code}' %q 2>/dev/null", url)
90+
code, out := execRun(ctx, t, ctr, script)
91+
trimmed := strings.TrimSpace(out)
92+
status := trimmed[strings.LastIndex(trimmed, "\n")+1:]
93+
switch status {
94+
case "404":
95+
return false, code == 22
96+
case "200":
97+
if code != 0 {
98+
return false, false
99+
}
100+
return strings.Contains(trimmed, filename), true
101+
default:
102+
// curl exits non-zero for HTTP errors and reports 000 for transport
103+
// failures. Do not skip a CI failure unless absence is confirmed.
104+
return false, false
105+
}
51106
}

‎tests/e2e/linux/matrix_test.go‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,17 @@ func TestReleasesIndex(t *testing.T) {
3737
t.Errorf("releasesIndex = %q, want %q", got, want)
3838
}
3939
}
40+
41+
func TestMissingLatestArm64MuslBuild(t *testing.T) {
42+
output := "Using latest version: \x1b[92m26.12.0\x1b[0m\n" +
43+
"Node.js package node-v26.12.0-linux-arm64-musl.tar.gz is not listed in " +
44+
"https://nodejs.org/dist/v26.12.0/SHASUMS256.txt"
45+
version, filename, missing := missingLatestArm64MuslBuild(output)
46+
if version != "26.12.0" || filename != "node-v26.12.0-linux-arm64-musl.tar.gz" || !missing {
47+
t.Fatalf("missingLatestArm64MuslBuild = (%q, %q, %v), want (26.12.0, node-v26.12.0-linux-arm64-musl.tar.gz, true)", version, filename, missing)
48+
}
49+
50+
if _, _, missing := missingLatestArm64MuslBuild("Node.js package node-v26.12.0-linux-x64-musl.tar.gz is not listed"); missing {
51+
t.Fatal("missingLatestArm64MuslBuild recognized an unrelated architecture")
52+
}
53+
}

‎tests/e2e/linux/verify.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func verifySearch(ctx context.Context, t *testing.T, ctr tc.Container, version,
107107
//
108108
// Every probe runs in its own freshly activated shell session; node may only
109109
// come from the env the legacy file selects, so a fallback to the global
110-
// (latest) release fails the assertion.
110+
// release fails the assertion.
111111
func verifyLegacyFiles(ctx context.Context, t *testing.T, ctr tc.Container, pinned, lts string) {
112112
t.Helper()
113113
// Each probe activates inside its own exec rather than relying on the
@@ -129,7 +129,7 @@ func verifyLegacyFiles(ctx context.Context, t *testing.T, ctr tc.Container, pinn
129129
assertProbe := func(got, want, what string) {
130130
t.Helper()
131131
require.Truef(t, strings.HasSuffix(got, "v"+want),
132-
"%s must resolve to %s over the global latest\n--- node path and version ---\n%s", what, want, got)
132+
"%s must resolve to %s over the global release\n--- node path and version ---\n%s", what, want, got)
133133
}
134134

135135
write("/tmp/e2e-legacy-nvmrc", ".nvmrc", pinned)

‎tests/e2e/windows/e2e_test.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ func runOne(t *testing.T, ctx context.Context, image, platform string, c common.
192192
activation := fetchActivation(ctx, t, ctr, box)
193193
installNodeVersion(ctx, t, ctr, box, activation, version)
194194
npm := verifyToolchain(ctx, t, ctr, box, activation, version)
195+
verifyGitBashNpm(ctx, t, ctr, box, version)
195196
verifyShorthand(ctx, t, ctr, box, activation, version)
196197
verifySearch(ctx, t, ctr, box, version, npm)
197198

‎tests/e2e/windows/exec.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package windows
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"io"
2021
"testing"
2122

@@ -51,6 +52,15 @@ func execRun(ctx context.Context, t *testing.T, ctr tc.Container, script string,
5152
return code, string(out)
5253
}
5354

55+
// execGitBashRun runs script through the Git for Windows login shell inside
56+
// the container, while using execRun to apply the isolated slot environment.
57+
func execGitBashRun(ctx context.Context, t *testing.T, ctr tc.Container, script string, extraEnv ...string) (int, string) {
58+
t.Helper()
59+
command := fmt.Sprintf("& %s --login -c %s",
60+
pwshQuote(`C:\Program Files\Git\bin\bash.exe`), pwshQuote(script))
61+
return execRun(ctx, t, ctr, command, extraEnv...)
62+
}
63+
5464
// execOK runs script and fatals unless it exits 0, returning the output.
5565
func execOK(ctx context.Context, t *testing.T, ctr tc.Container, script string, extraEnv ...string) string {
5666
t.Helper()

‎tests/e2e/windows/verify.go‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,31 @@ func verifyToolchain(ctx context.Context, t *testing.T, ctr tc.Container, box sl
7474
return npm
7575
}
7676

77+
// verifyGitBashNpm checks the Windows Node.js package from Git Bash. npm.cmd
78+
// is a control for the known-working Windows shim; the extensionless npm
79+
// command is the regression target from vfox#293.
80+
func verifyGitBashNpm(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox, version string) {
81+
t.Helper()
82+
script := `set -e
83+
eval "$(vfox activate bash)"
84+
eval "$(vfox env -s bash)"
85+
node --version
86+
npm.cmd --version
87+
npm --version`
88+
code, out := execGitBashRun(ctx, t, ctr, script, box.vars...)
89+
require.Equalf(t, 0, code,
90+
"Git Bash must run node, npm.cmd, and npm after vfox activation\n--- output ---\n%s", out)
91+
92+
versions := strings.Fields(out)
93+
require.Lenf(t, versions, 3,
94+
"Git Bash should print node, npm.cmd, and npm versions\n--- output ---\n%s", out)
95+
require.Equalf(t, "v"+version, versions[0], "Git Bash node --version\n--- output ---\n%s", out)
96+
require.Regexp(t, `^\d+\.\d+\.\d+$`, versions[1], "Git Bash npm.cmd --version")
97+
require.Regexp(t, `^\d+\.\d+\.\d+$`, versions[2], "Git Bash npm --version")
98+
require.Equal(t, versions[1], versions[2], "npm and npm.cmd should report the same bundled version")
99+
t.Logf("PASS Git Bash node v%s, npm.cmd and npm %s (issue #293)", version, versions[2])
100+
}
101+
77102
// verifyShorthand proves PreUse resolves the major and major.minor shorthands
78103
// against the installed versions, without another download.
79104
func verifyShorthand(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox, activation, version string) {

0 commit comments

Comments
 (0)