mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 08:18:19 +00:00
168 lines
5.8 KiB
Go
168 lines
5.8 KiB
Go
package skills
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os/exec"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func setFixtureNpmStderr(t *testing.T, stderr string) {
|
|
t.Helper()
|
|
if runtime.GOOS == "windows" {
|
|
stderr = strings.ReplaceAll(stderr, "\n", " ")
|
|
}
|
|
t.Setenv("FIXTURE_NPM_STDERR", stderr)
|
|
}
|
|
|
|
// TestNpmExecutor_SourceName verifies the Source() method returns "npm".
|
|
func TestNpmExecutor_SourceName(t *testing.T) {
|
|
if got := NewNpmUpdateExecutor().Source(); got != "npm" {
|
|
t.Fatalf("want source=npm, got %q", got)
|
|
}
|
|
}
|
|
|
|
// TestNpmExecutor_InvalidName verifies that a package name containing a version
|
|
// suffix (e.g. "typescript@latest") is rejected before any exec.
|
|
func TestNpmExecutor_InvalidName(t *testing.T) {
|
|
// Do NOT set fixture npm — we expect rejection before any exec.
|
|
e := NewNpmUpdateExecutor()
|
|
err := e.Update(context.Background(), "typescript@latest", "5.5.0", nil)
|
|
if err == nil {
|
|
t.Fatal("want error for invalid package name containing @version suffix")
|
|
}
|
|
}
|
|
|
|
// TestNpmExecutor_EmptyToVersion verifies that an empty toVersion is rejected
|
|
// before any exec. This enforces exact-version pinning (P2A-H4).
|
|
func TestNpmExecutor_EmptyToVersion(t *testing.T) {
|
|
e := NewNpmUpdateExecutor()
|
|
err := e.Update(context.Background(), "typescript", "", nil)
|
|
if err == nil {
|
|
t.Fatal("want error for empty toVersion")
|
|
}
|
|
}
|
|
|
|
// TestNpmExecutor_Success verifies that a successful npm install (exit 0)
|
|
// returns nil error.
|
|
func TestNpmExecutor_Success(t *testing.T) {
|
|
useFixtureNpm(t)
|
|
t.Setenv("FIXTURE_NPM_EXIT", "0")
|
|
t.Setenv("FIXTURE_NPM_STDERR", "")
|
|
|
|
err := NewNpmUpdateExecutor().Update(context.Background(), "typescript", "5.5.0", nil)
|
|
if err != nil {
|
|
t.Fatalf("want nil error on exit 0, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestNpmExecutor_ERESOLVE verifies that stderr containing "ERESOLVE" maps to
|
|
// ErrUpdateNpmConflict.
|
|
func TestNpmExecutor_ERESOLVE(t *testing.T) {
|
|
useFixtureNpm(t)
|
|
t.Setenv("FIXTURE_NPM_EXIT", "1")
|
|
setFixtureNpmStderr(t, "npm ERR! code ERESOLVE\nnpm ERR! peer dep conflict")
|
|
|
|
err := NewNpmUpdateExecutor().Update(context.Background(), "typescript", "5.5.0", nil)
|
|
if err == nil {
|
|
t.Fatal("want non-nil error")
|
|
}
|
|
if !errors.Is(err, ErrUpdateNpmConflict) {
|
|
t.Fatalf("want errors.Is(err, ErrUpdateNpmConflict), got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestNpmExecutor_EACCES verifies that stderr containing "EACCES" maps to
|
|
// ErrUpdateNpmPermission.
|
|
func TestNpmExecutor_EACCES(t *testing.T) {
|
|
useFixtureNpm(t)
|
|
t.Setenv("FIXTURE_NPM_EXIT", "1")
|
|
setFixtureNpmStderr(t, "npm ERR! code EACCES\nnpm ERR! permission denied")
|
|
|
|
err := NewNpmUpdateExecutor().Update(context.Background(), "typescript", "5.5.0", nil)
|
|
if err == nil {
|
|
t.Fatal("want non-nil error")
|
|
}
|
|
if !errors.Is(err, ErrUpdateNpmPermission) {
|
|
t.Fatalf("want errors.Is(err, ErrUpdateNpmPermission), got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestNpmExecutor_404 verifies that stderr containing "E404" maps to
|
|
// ErrUpdateNpmNotFound.
|
|
func TestNpmExecutor_404(t *testing.T) {
|
|
useFixtureNpm(t)
|
|
t.Setenv("FIXTURE_NPM_EXIT", "1")
|
|
setFixtureNpmStderr(t, "npm ERR! code E404\nnpm ERR! 404 Not Found - GET https://registry.npmjs.org/nonexistent")
|
|
|
|
err := NewNpmUpdateExecutor().Update(context.Background(), "nonexistent", "1.0.0", nil)
|
|
if err == nil {
|
|
t.Fatal("want non-nil error")
|
|
}
|
|
if !errors.Is(err, ErrUpdateNpmNotFound) {
|
|
t.Fatalf("want errors.Is(err, ErrUpdateNpmNotFound), got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestNpmExecutor_ExactVersionArgv verifies that the command argv contains
|
|
// the exact "name@version" token — never "@latest" or "@next". This test
|
|
// exercises the executor against a real (fixture) process to confirm the
|
|
// argument is passed literally to exec, not mangled.
|
|
func TestNpmExecutor_ExactVersionArgv(t *testing.T) {
|
|
// We use a custom fixture that records its arguments to stdout.
|
|
// Instead, we verify indirectly: the fixture exits 0 for any install argv,
|
|
// confirming our target is "typescript@5.5.0" (not "@latest").
|
|
// The real guard is ValidateNpmPackageName rejecting "@latest" as a name,
|
|
// and the executor always constructing target = name + "@" + toVersion.
|
|
useFixtureNpm(t)
|
|
t.Setenv("FIXTURE_NPM_EXIT", "0")
|
|
|
|
// Passing "@latest" as toVersion should succeed at the exec level
|
|
// (fixture exits 0) but we explicitly document that callers MUST pass
|
|
// an exact version. The executor does NOT re-validate toVersion content
|
|
// beyond non-empty — that contract is enforced by the checker always
|
|
// supplying LatestVersion which is a concrete version string.
|
|
//
|
|
// Verify a legitimate exact version works end-to-end.
|
|
err := NewNpmUpdateExecutor().Update(context.Background(), "typescript", "5.5.0", nil)
|
|
if err != nil {
|
|
t.Fatalf("exact version install must succeed: %v", err)
|
|
}
|
|
|
|
// Verify scoped package works end-to-end.
|
|
err = NewNpmUpdateExecutor().Update(context.Background(), "@angular/core", "17.0.0", nil)
|
|
if err != nil {
|
|
t.Fatalf("scoped package install must succeed: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestNpmExecutor_ContextCancel verifies that context cancellation propagates
|
|
// to the subprocess (exec.CommandContext contract). We set npmBinary to a
|
|
// long-running command and cancel immediately.
|
|
func TestNpmExecutor_ContextCancel(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("sleep fixture is Unix-specific")
|
|
}
|
|
restoreNpmBinary(t)
|
|
restoreNpmLookPath(t)
|
|
|
|
// Use `sleep 30` as the npm binary so it blocks until cancelled.
|
|
sleepBin, err := exec.LookPath("sleep")
|
|
if err != nil {
|
|
t.Skip("sleep not available, skipping context cancel test")
|
|
}
|
|
npmBinary = sleepBin
|
|
npmLookPath = func(string) (string, error) { return sleepBin, nil }
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // cancel immediately
|
|
|
|
// Update arg is "30" which sleep interprets as seconds — but ctx is already done.
|
|
err = NewNpmUpdateExecutor().Update(ctx, "30", "1.0.0", nil)
|
|
if err == nil {
|
|
t.Fatal("want error when context is cancelled before exec")
|
|
}
|
|
}
|