Add manifest creation command

This commit is contained in:
2026-06-01 21:02:34 +00:00
parent 04557f610d
commit 8b1e5abf68
6 changed files with 538 additions and 1 deletions

View File

@@ -13,6 +13,7 @@ import (
"gitea.maximumdirect.net/eric/distributor/internal/storage"
"gitea.maximumdirect.net/eric/distributor/internal/testutil"
producerbundle "gitea.maximumdirect.net/eric/distributor/pkg/bundle"
)
func decodeEnvelope(t *testing.T, stdout *bytes.Buffer) map[string]any {
@@ -267,6 +268,206 @@ func TestExecuteInspectArgs(t *testing.T) {
}
}
func TestExecuteManifestCreateExplicitFiles(t *testing.T) {
root := t.TempDir()
writeCLIFile(t, root, "b.txt", "bravo")
writeCLIFile(t, root, "nested/a.txt", "alpha")
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{
"manifest", "create", root,
"--id", "reports.explicit",
"--created", "2026-06-01T11:00:00Z",
"--file", "b.txt",
"--file", "nested/a.txt",
}, &stdout, &stderr)
if code != exitOK {
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
}
for _, want := range []string{
"created manifest.json",
"bundle: reports.explicit",
"files: 2",
"digest: sha256:",
} {
if !strings.Contains(stdout.String(), want) {
t.Fatalf("stdout = %q, want substring %q", stdout.String(), want)
}
}
manifest, err := producerbundle.LoadManifest(root)
if err != nil {
t.Fatalf("LoadManifest() error = %v", err)
}
if got, want := manifestPaths(manifest), []string{"b.txt", "nested/a.txt"}; !equalStrings(got, want) {
t.Fatalf("manifest paths = %v, want %v", got, want)
}
if err := producerbundle.ValidateBundle(root, manifest); err != nil {
t.Fatalf("ValidateBundle() error = %v", err)
}
var validateStdout, validateStderr bytes.Buffer
validateCode := Execute(context.Background(), []string{"validate", root}, &validateStdout, &validateStderr)
if validateCode != exitOK {
t.Fatalf("validate exit code = %d, want %d; stderr = %q", validateCode, exitOK, validateStderr.String())
}
}
func TestExecuteManifestCreateScansBundle(t *testing.T) {
root := t.TempDir()
writeCLIFile(t, root, "z.txt", "zulu")
writeCLIFile(t, root, ".env", "dotfile")
writeCLIFile(t, root, "nested/report.md", "# Report\n")
writeCLIFile(t, root, storage.StateFileName, "destination state")
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{"manifest", "create", root, "--id", "reports.scan"}, &stdout, &stderr)
if code != exitOK {
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
}
manifest, err := producerbundle.LoadManifest(root)
if err != nil {
t.Fatalf("LoadManifest() error = %v", err)
}
if got, want := manifestPaths(manifest), []string{".env", "nested/report.md", "z.txt"}; !equalStrings(got, want) {
t.Fatalf("manifest paths = %v, want %v", got, want)
}
}
func TestExecuteManifestCreateJSON(t *testing.T) {
root := t.TempDir()
writeCLIFile(t, root, "report.md", "# Report\n")
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{"manifest", "create", root, "--id", "reports.json", "--file", "report.md", "--format", "json"}, &stdout, &stderr)
if code != exitOK {
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
}
envelope := decodeEnvelope(t, &stdout)
if envelope["command"] != "manifest create" || envelope["ok"] != true {
t.Fatalf("envelope = %#v, want manifest create ok", envelope)
}
result := envelopeResult(t, envelope)
if result["id"] != "reports.json" || result["file_count"] != float64(1) {
t.Fatalf("result = %#v, want manifest summary", result)
}
if stderr.Len() != 0 {
t.Fatalf("stderr = %q, want empty", stderr.String())
}
}
func TestExecuteManifestCreateOverwrite(t *testing.T) {
root := t.TempDir()
writeCLIFile(t, root, "report.md", "old\n")
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{"manifest", "create", root, "--id", "reports.old", "--file", "report.md"}, &stdout, &stderr)
if code != exitOK {
t.Fatalf("initial exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
}
writeCLIFile(t, root, "report.md", "new\n")
stdout.Reset()
stderr.Reset()
code = Execute(context.Background(), []string{"manifest", "create", root, "--id", "reports.new", "--file", "report.md"}, &stdout, &stderr)
if code != exitError {
t.Fatalf("overwrite exit code = %d, want %d", code, exitError)
}
if !strings.Contains(stderr.String(), "write manifest") {
t.Fatalf("stderr = %q, want write manifest error", stderr.String())
}
manifest, err := producerbundle.LoadManifest(root)
if err != nil {
t.Fatalf("LoadManifest() error = %v", err)
}
if manifest.ID != "reports.old" {
t.Fatalf("manifest id = %q, want reports.old", manifest.ID)
}
stdout.Reset()
stderr.Reset()
code = Execute(context.Background(), []string{"manifest", "create", root, "--id", "reports.new", "--file", "report.md", "--overwrite"}, &stdout, &stderr)
if code != exitOK {
t.Fatalf("overwrite exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
}
manifest, err = producerbundle.LoadManifest(root)
if err != nil {
t.Fatalf("LoadManifest() error = %v", err)
}
if manifest.ID != "reports.new" {
t.Fatalf("manifest id = %q, want reports.new", manifest.ID)
}
}
func TestExecuteManifestCreateRejectsSymlink(t *testing.T) {
root := t.TempDir()
writeCLIFile(t, root, "target.md", "# Report\n")
if err := os.Symlink("target.md", filepath.Join(root, "link.md")); err != nil {
t.Skipf("symlink unavailable: %v", err)
}
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{"manifest", "create", root, "--id", "reports.link", "--file", "link.md"}, &stdout, &stderr)
if code != exitError {
t.Fatalf("exit code = %d, want %d", code, exitError)
}
if !strings.Contains(stderr.String(), "regular file") {
t.Fatalf("stderr = %q, want regular file error", stderr.String())
}
}
func TestExecuteManifestCreateArgs(t *testing.T) {
root := t.TempDir()
writeCLIFile(t, root, "report.md", "# Report\n")
tests := []struct {
name string
args []string
wantCode int
wantStderr string
}{
{
name: "missing path",
args: []string{"manifest", "create", "--id", "reports.missing"},
wantCode: exitUsage,
wantStderr: "requires exactly one bundle path",
},
{
name: "missing id",
args: []string{"manifest", "create", root},
wantCode: exitError,
wantStderr: "requires --id",
},
{
name: "bad created",
args: []string{"manifest", "create", root, "--id", "reports.bad", "--created", "June 1"},
wantCode: exitError,
wantStderr: "created must be RFC3339",
},
{
name: "bad format",
args: []string{"manifest", "create", root, "--id", "reports.bad", "--format", "xml"},
wantCode: exitUsage,
wantStderr: "format must be text or json",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), tt.args, &stdout, &stderr)
if code != tt.wantCode {
t.Fatalf("exit code = %d, want %d; stderr = %q", code, tt.wantCode, stderr.String())
}
if stdout.Len() != 0 {
t.Fatalf("stdout = %q, want empty", stdout.String())
}
if !strings.Contains(stderr.String(), tt.wantStderr) {
t.Fatalf("stderr = %q, want substring %q", stderr.String(), tt.wantStderr)
}
})
}
}
func TestExecuteRunDryRun(t *testing.T) {
sourceRoot := t.TempDir()
testutil.WriteSourceBundle(t, sourceRoot, "", testutil.BundleOptions{})
@@ -512,3 +713,34 @@ func TestUnknownCommandIsUsageError(t *testing.T) {
t.Fatalf("stderr = %q, want unknown command error", stderr.String())
}
}
func manifestPaths(manifest producerbundle.Manifest) []string {
paths := make([]string, 0, len(manifest.Files))
for _, file := range manifest.Files {
paths = append(paths, file.Path)
}
return paths
}
func equalStrings(a, b []string) bool {
if len(a) != len(b) {
return false
}
for index := range a {
if a[index] != b[index] {
return false
}
}
return true
}
func writeCLIFile(t *testing.T, root, relative, body string) {
t.Helper()
path := filepath.Join(root, filepath.FromSlash(relative))
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir %s: %v", filepath.Dir(path), err)
}
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}