feat: 跨平台命令助手 kopsc 初始实现

- 意图命令按 OS/架构、Linux 发行版与容器运行时解析并执行对应底层命令
- 内置 docker.clearsafe、net.ports、pkg.refreshindex
- --dry-run 预览、list/explain/platform/version 辅助命令
- 纯 Go 跨平台构建脚本 scripts/build
This commit is contained in:
2026-10-31 13:01:39 +08:00
commit 7d9852958f
18 changed files with 2650 additions and 0 deletions
+221
View File
@@ -0,0 +1,221 @@
package executor
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"gitea.odboy.cn/odboy/kenaito-kopsc/internal/platform"
"gitea.odboy.cn/odboy/kenaito-kopsc/internal/registry"
"gitea.odboy.cn/odboy/kenaito-kopsc/internal/resolve"
)
func samplePlan(argv ...string) Plan {
if len(argv) == 0 {
argv = []string{"docker", "system", "prune", "-f"}
}
return Plan{Result: resolve.Result{
Command: &registry.Command{ID: "docker.clearsafe", Summary: "安全清理"},
Info: platform.Info{
OS: "linux", Arch: "amd64", Distro: "centos",
DistroLike: []string{"rhel"}, Runtimes: []string{"docker"},
},
Variant: registry.Variant{Argv: argv, Note: "CentOS 上的 Docker 运行时"},
VariantIdx: 0,
Score: 5,
Candidates: []resolve.Candidate{{
Index: 0, Matched: true, Score: 5,
Reason: "匹配 os=linux, distro=centos (+3), runtime=docker (+2)",
}},
}}
}
func TestDryRunOnlyPrints(t *testing.T) {
var out bytes.Buffer
err := samplePlan().Run(context.Background(), Options{DryRun: true, Stdout: &out})
if err != nil {
t.Fatalf("dry-run returned error: %v", err)
}
text := out.String()
for _, want := range []string{
"[dry-run]",
"docker system prune -f",
"linux/amd64",
"distro=centos",
"变体 #0",
"CentOS 上的 Docker 运行时",
"依据",
} {
if !strings.Contains(text, want) {
t.Errorf("dry-run output missing %q:\n%s", want, text)
}
}
}
func TestDryRunNeverExecutes(t *testing.T) {
// A missing program must not matter in dry-run mode.
err := samplePlan("kopsc-program-that-does-not-exist").Run(context.Background(), Options{
DryRun: true,
Stdout: io.Discard,
})
if err != nil {
t.Fatalf("dry-run should not attempt execution: %v", err)
}
}
func TestArgvAppendsExtraArgs(t *testing.T) {
plan := samplePlan("docker", "system", "prune", "-f")
plan.ExtraArgs = []string{"--filter", "until=24h"}
want := []string{"docker", "system", "prune", "-f", "--filter", "until=24h"}
if got := plan.Argv(); strings.Join(got, " ") != strings.Join(want, " ") {
t.Fatalf("Argv() = %v, want %v", got, want)
}
if !strings.Contains(plan.Display(), "--filter until=24h") {
t.Fatalf("Display() = %q", plan.Display())
}
}
// TestHelperProcess is re-executed as a child by the tests below.
func TestHelperProcess(t *testing.T) {
if os.Getenv("KOPSC_TEST_HELPER") != "1" {
return
}
fmt.Fprintln(os.Stdout, "helper-stdout", strings.Join(os.Args[1:], " "))
fmt.Fprintln(os.Stderr, "helper-stderr")
os.Exit(7)
}
func TestRunPropagatesStdioAndExitCode(t *testing.T) {
t.Setenv("KOPSC_TEST_HELPER", "1")
plan := samplePlan(os.Args[0], "-test.run=TestHelperProcess", "--")
plan.ExtraArgs = []string{"forwarded"}
var stdout, stderr bytes.Buffer
err := plan.Run(context.Background(), Options{Stdout: &stdout, Stderr: &stderr})
if err == nil {
t.Fatal("expected a non-zero exit error")
}
var exitErr *ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("error type = %T, want *ExitError", err)
}
if exitErr.Code != 7 {
t.Errorf("exit code = %d, want 7", exitErr.Code)
}
if ExitCode(err) != 7 {
t.Errorf("ExitCode(err) = %d, want 7", ExitCode(err))
}
if !strings.Contains(stdout.String(), "helper-stdout") {
t.Errorf("child stdout not forwarded:\n%s", stdout.String())
}
if !strings.Contains(stdout.String(), "forwarded") {
t.Errorf("extra args not forwarded:\n%s", stdout.String())
}
if !strings.Contains(stderr.String(), "helper-stderr") {
t.Errorf("child stderr not forwarded:\n%s", stderr.String())
}
}
func TestRunFindsProgramOnPATH(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("shell script fixture is not executable on Windows")
}
dir := t.TempDir()
script := filepath.Join(dir, "docker")
body := "#!/bin/sh\necho fake-docker \"$@\"\nexit 3\n"
if err := os.WriteFile(script, []byte(body), 0o755); err != nil {
t.Fatalf("write fixture: %v", err)
}
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
var out bytes.Buffer
err := samplePlan().Run(context.Background(), Options{Stdout: &out, Stderr: io.Discard})
if ExitCode(err) != 3 {
t.Fatalf("exit code = %d, want 3 (err=%v)", ExitCode(err), err)
}
if !strings.Contains(out.String(), "fake-docker system prune -f") {
t.Errorf("fixture output missing:\n%s", out.String())
}
}
func TestRunNotFound(t *testing.T) {
err := samplePlan("kopsc-program-that-does-not-exist").Run(context.Background(), Options{
Stdout: io.Discard,
})
if err == nil {
t.Fatal("expected an error for a missing program")
}
if ExitCode(err) != 1 {
t.Errorf("ExitCode() = %d, want 1", ExitCode(err))
}
}
func TestShellVariant(t *testing.T) {
plan := samplePlan("echo", "hello from shell")
plan.Result.Variant.Shell = true
var out bytes.Buffer
if err := plan.Run(context.Background(), Options{Stdout: &out, Stderr: io.Discard}); err != nil {
t.Fatalf("shell run failed: %v", err)
}
if !strings.Contains(out.String(), "hello from shell") {
t.Errorf("shell output missing:\n%s", out.String())
}
display := plan.Display()
wantShell := "sh -c"
if runtime.GOOS == "windows" {
wantShell = "cmd /c"
}
if !strings.HasPrefix(display, wantShell) {
t.Errorf("Display() = %q, want prefix %q", display, wantShell)
}
}
func TestQuoteForShell(t *testing.T) {
unixCases := map[string]string{
"plain": "plain",
"with space": "'with space'",
"it's": `'it'\''s'`,
"": "''",
"/usr/bin/x": "/usr/bin/x",
"--filter=a,b": "--filter=a,b",
}
for in, want := range unixCases {
if got := quoteForShell(in, "linux"); got != want {
t.Errorf("quoteForShell(%q, linux) = %q, want %q", in, got, want)
}
}
windowsCases := map[string]string{
"plain": "plain",
"with space": `"with space"`,
}
for in, want := range windowsCases {
if got := quoteForShell(in, "windows"); got != want {
t.Errorf("quoteForShell(%q, windows) = %q, want %q", in, got, want)
}
}
}
func TestExitCode(t *testing.T) {
if got := ExitCode(nil); got != 0 {
t.Errorf("ExitCode(nil) = %d, want 0", got)
}
if got := ExitCode(errors.New("boom")); got != 1 {
t.Errorf("ExitCode(generic) = %d, want 1", got)
}
if got := ExitCode(&ExitError{Code: 42}); got != 42 {
t.Errorf("ExitCode(ExitError) = %d, want 42", got)
}
}