From 55db21f4640166c295e37f578cbf573133111c1f Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 13:47:18 +0000 Subject: [PATCH 1/5] Show missing commit fields as N/A in get trail table output Commit info on a trail may carry only a sha1 (the trails API accepts partial commit info), and the table renderer used unchecked type assertions on author, timestamp and message, so it panicked. Render missing fields as N/A, matching the markdown output. --- cmd/kosli/getTrail.go | 16 ++++++++--- cmd/kosli/getTrail_table_test.go | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 cmd/kosli/getTrail_table_test.go diff --git a/cmd/kosli/getTrail.go b/cmd/kosli/getTrail.go index ec3b2e322..73af8769c 100644 --- a/cmd/kosli/getTrail.go +++ b/cmd/kosli/getTrail.go @@ -368,9 +368,9 @@ func printTrailAsTable(raw string, out io.Writer, page int) error { rows = append(rows, fmt.Sprintf("Last modified at:\t%s", lastModifiedAt)) if commitInfo, ok := trail["git_commit_info"].(map[string]any); ok { rows = append(rows, "Git commit:\t") - rows = append(rows, fmt.Sprintf(" Sha1:\t%s", commitInfo["sha1"].(string))) - rows = append(rows, fmt.Sprintf(" Author:\t%s", commitInfo["author"].(string))) - commitTimestamp, err := formattedTimestamp(commitInfo["timestamp"].(float64), false) + rows = append(rows, fmt.Sprintf(" Sha1:\t%s", stringOrNA(commitInfo["sha1"]))) + rows = append(rows, fmt.Sprintf(" Author:\t%s", stringOrNA(commitInfo["author"]))) + commitTimestamp, err := formattedTimestamp(commitInfo["timestamp"], false) if err != nil { return err } @@ -378,7 +378,7 @@ func printTrailAsTable(raw string, out io.Writer, page int) error { if url, ok := commitInfo["url"]; ok { rows = append(rows, fmt.Sprintf(" url:\t%s", url)) } - rows = append(rows, fmt.Sprintf(" message:\t%s", prefixEachLine(commitInfo["message"].(string), "\t"))) + rows = append(rows, fmt.Sprintf(" message:\t%s", prefixEachLine(stringOrNA(commitInfo["message"]), "\t"))) } rows = append(rows, "Events:\n") @@ -508,3 +508,11 @@ func eventFields(event any) (trailEventFields, error) { attestationRef: eventAttestationRef, }, nil } + +// stringOrNA renders an optional string field, since commit info may carry only a sha1. +func stringOrNA(v any) string { + if s, ok := v.(string); ok && s != "" { + return s + } + return "N/A" +} diff --git a/cmd/kosli/getTrail_table_test.go b/cmd/kosli/getTrail_table_test.go new file mode 100644 index 000000000..eea0dd69f --- /dev/null +++ b/cmd/kosli/getTrail_table_test.go @@ -0,0 +1,47 @@ +package main + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestPrintTrailAsTableCommitInfo checks that the table renderer tolerates +// commit info with only some of its fields, which the API accepts for trails. +func TestPrintTrailAsTableCommitInfo(t *testing.T) { + t.Setenv("KOSLI_TESTS", "true") // makes formattedTimestamp use a fixed time + + for _, tc := range []struct { + name string + commitInfo string + want []string + }{ + { + name: "only sha1", + commitInfo: `{"sha1": "abc1234def"}`, + want: []string{"Sha1: abc1234def", "Author: N/A", "Timestamp: N/A", "message: N/A"}, + }, + { + name: "sha1 and branch", + commitInfo: `{"sha1": "abc1234def", "branch": "main"}`, + want: []string{"abc1234def", "Author: N/A", "Timestamp: N/A", "message: N/A"}, + }, + { + name: "full commit info", + commitInfo: `{"sha1": "abc1234def", "author": "Jane ", "timestamp": 1452902400, "url": "https://example.com/c/abc1234def", "message": "first line\nsecond line", "branch": "main"}`, + want: []string{"abc1234def", "Jane ", "Timestamp: Sat, 16 Jan 2016", "url: https://example.com/c/abc1234def", "message: first line\n second line"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + raw := `{"name": "t1", "description": "d", "compliance_state": "INCOMPLETE", + "last_modified_at": 1452902400, "git_commit_info": ` + tc.commitInfo + `, "events": []}` + + var buf bytes.Buffer + require.NoError(t, printTrailAsTable(raw, &buf, 0)) + for _, w := range tc.want { + require.Contains(t, buf.String(), w) + } + }) + } +} From 8777215731b5f086d39255e34c581db1afd4906b Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 13:53:12 +0000 Subject: [PATCH 2/5] no-mistakes(test): Hide url row when commit url is null or empty --- cmd/kosli/getTrail.go | 2 +- cmd/kosli/getTrail_table_test.go | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cmd/kosli/getTrail.go b/cmd/kosli/getTrail.go index 73af8769c..e36d386cc 100644 --- a/cmd/kosli/getTrail.go +++ b/cmd/kosli/getTrail.go @@ -375,7 +375,7 @@ func printTrailAsTable(raw string, out io.Writer, page int) error { return err } rows = append(rows, fmt.Sprintf(" Timestamp:\t%s", commitTimestamp)) - if url, ok := commitInfo["url"]; ok { + if url, ok := commitInfo["url"].(string); ok && url != "" { rows = append(rows, fmt.Sprintf(" url:\t%s", url)) } rows = append(rows, fmt.Sprintf(" message:\t%s", prefixEachLine(stringOrNA(commitInfo["message"]), "\t"))) diff --git a/cmd/kosli/getTrail_table_test.go b/cmd/kosli/getTrail_table_test.go index eea0dd69f..509c5246b 100644 --- a/cmd/kosli/getTrail_table_test.go +++ b/cmd/kosli/getTrail_table_test.go @@ -16,6 +16,7 @@ func TestPrintTrailAsTableCommitInfo(t *testing.T) { name string commitInfo string want []string + notWant []string }{ { name: "only sha1", @@ -27,6 +28,13 @@ func TestPrintTrailAsTableCommitInfo(t *testing.T) { commitInfo: `{"sha1": "abc1234def", "branch": "main"}`, want: []string{"abc1234def", "Author: N/A", "Timestamp: N/A", "message: N/A"}, }, + { + // the server returns absent commit fields as explicit nulls + name: "only sha1 with null fields", + commitInfo: `{"sha1": "abc1234def", "message": null, "author": null, "branch": null, "timestamp": null, "url": null}`, + want: []string{"Sha1: abc1234def", "Author: N/A", "Timestamp: N/A", "message: N/A"}, + notWant: []string{"%!", "url:"}, + }, { name: "full commit info", commitInfo: `{"sha1": "abc1234def", "author": "Jane ", "timestamp": 1452902400, "url": "https://example.com/c/abc1234def", "message": "first line\nsecond line", "branch": "main"}`, @@ -42,6 +50,9 @@ func TestPrintTrailAsTableCommitInfo(t *testing.T) { for _, w := range tc.want { require.Contains(t, buf.String(), w) } + for _, nw := range tc.notWant { + require.NotContains(t, buf.String(), nw) + } }) } } From 84b2cec4355e773f733e89beedd3986d15360020 Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 15:12:46 +0000 Subject: [PATCH 3/5] Show missing commit author and message as N/A in get trail markdown output --- cmd/kosli/getTrail.go | 4 ++-- cmd/kosli/getTrail_markdown_test.go | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/cmd/kosli/getTrail.go b/cmd/kosli/getTrail.go index e36d386cc..ec087ff2f 100644 --- a/cmd/kosli/getTrail.go +++ b/cmd/kosli/getTrail.go @@ -124,9 +124,9 @@ func (o *getTrailOptions) printTrailAsMarkdown(raw string, out io.Writer, page i b.WriteString("| | |\n") b.WriteString("| --- | --- |\n") fmt.Fprintf(&b, "| Sha1 | %s |\n", sha) - fmt.Fprintf(&b, "| Author | %s |\n", mdCell(commitInfo["author"])) + fmt.Fprintf(&b, "| Author | %s |\n", mdCell(stringOrNA(commitInfo["author"]))) fmt.Fprintf(&b, "| Timestamp | %s |\n", mdCell(commitTimestamp)) - fmt.Fprintf(&b, "| Message | %s |\n", mdCell(firstLine(commitInfo["message"]))) + fmt.Fprintf(&b, "| Message | %s |\n", mdCell(stringOrNA(firstLine(commitInfo["message"])))) } writeAttestationStatuses(&b, trail["compliance_status"], trailURL) diff --git a/cmd/kosli/getTrail_markdown_test.go b/cmd/kosli/getTrail_markdown_test.go index a961bcb1d..dc9cd4590 100644 --- a/cmd/kosli/getTrail_markdown_test.go +++ b/cmd/kosli/getTrail_markdown_test.go @@ -413,3 +413,16 @@ func TestPrintTrailAsMarkdownEnvironmentLinks(t *testing.T) { "artifact 'artifact' stopped running in ['prod-aws'](https://app.kosli.com/my-org/environments/prod-aws/)", "without a snapshot index the link falls back to the environment page") } + +func TestPrintTrailAsMarkdownPartialCommitInfo(t *testing.T) { + t.Setenv("KOSLI_TESTS", "true") + global = &GlobalOpts{Host: "http://localhost:8001", Org: "docs-cmd-test-user-shared"} + o := &getTrailOptions{flowName: "get-trail"} + raw := `{"name": "t1", "compliance_state": "INCOMPLETE", "last_modified_at": 1452902400, + "git_commit_info": {"sha1": "abc1234def"}, "events": []}` + + var buf bytes.Buffer + require.NoError(t, o.printTrailAsMarkdown(raw, &buf, 0)) + require.Contains(t, buf.String(), "| Author | N/A |") + require.Contains(t, buf.String(), "| Message | N/A |") +} From 04ff10b652d870647bac348753978467df5af6eb Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 15:13:01 +0000 Subject: [PATCH 4/5] Match get trail table test output with regexps instead of fixed padding --- cmd/kosli/getTrail_table_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/kosli/getTrail_table_test.go b/cmd/kosli/getTrail_table_test.go index 509c5246b..2852057d7 100644 --- a/cmd/kosli/getTrail_table_test.go +++ b/cmd/kosli/getTrail_table_test.go @@ -21,24 +21,24 @@ func TestPrintTrailAsTableCommitInfo(t *testing.T) { { name: "only sha1", commitInfo: `{"sha1": "abc1234def"}`, - want: []string{"Sha1: abc1234def", "Author: N/A", "Timestamp: N/A", "message: N/A"}, + want: []string{`Sha1:\s+abc1234def`, `Author:\s+N/A`, `Timestamp:\s+N/A`, `message:\s+N/A`}, }, { name: "sha1 and branch", commitInfo: `{"sha1": "abc1234def", "branch": "main"}`, - want: []string{"abc1234def", "Author: N/A", "Timestamp: N/A", "message: N/A"}, + want: []string{"abc1234def", `Author:\s+N/A`, `Timestamp:\s+N/A`, `message:\s+N/A`}, }, { // the server returns absent commit fields as explicit nulls name: "only sha1 with null fields", commitInfo: `{"sha1": "abc1234def", "message": null, "author": null, "branch": null, "timestamp": null, "url": null}`, - want: []string{"Sha1: abc1234def", "Author: N/A", "Timestamp: N/A", "message: N/A"}, + want: []string{`Sha1:\s+abc1234def`, `Author:\s+N/A`, `Timestamp:\s+N/A`, `message:\s+N/A`}, notWant: []string{"%!", "url:"}, }, { name: "full commit info", commitInfo: `{"sha1": "abc1234def", "author": "Jane ", "timestamp": 1452902400, "url": "https://example.com/c/abc1234def", "message": "first line\nsecond line", "branch": "main"}`, - want: []string{"abc1234def", "Jane ", "Timestamp: Sat, 16 Jan 2016", "url: https://example.com/c/abc1234def", "message: first line\n second line"}, + want: []string{"abc1234def", `Jane `, `Timestamp:\s+Sat, 16 Jan 2016`, `url:\s+https://example\.com/c/abc1234def`, `message:\s+first line\n\s+second line`}, }, } { t.Run(tc.name, func(t *testing.T) { @@ -48,7 +48,7 @@ func TestPrintTrailAsTableCommitInfo(t *testing.T) { var buf bytes.Buffer require.NoError(t, printTrailAsTable(raw, &buf, 0)) for _, w := range tc.want { - require.Contains(t, buf.String(), w) + require.Regexp(t, w, buf.String()) } for _, nw := range tc.notWant { require.NotContains(t, buf.String(), nw) From f31f718feffb757ae66ce742eb99add545a69fe3 Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 16:29:18 +0000 Subject: [PATCH 5/5] no-mistakes(ci): The markdown Sha1 cell now shows N/A when sha1 is missing, so markdown and table output agree. In `cmd/kosli/getTrail.go:119` the cell uses `mdCell(stringOrNA(commitInfo["sha1"]))`, the same pattern the Author and Message cells already use. The Timestamp cell already gave N/A through `formattedTimestamp`, so every field in the Git commit section now handles a missing value. When sha1 is present, `stringOrNA` returns it unchanged, so normal output and the commit-URL link are the same as before. `TestPrintTrailAsMarkdownPartialCommitInfo` (`cmd/kosli/getTrail_markdown_test.go`) has a new case: commit info with only an author renders `| Sha1 | N/A |`. With the production change reverted, that assertion fails; with the fix, it passes. All `TestPrintTrailAsMarkdown*` tests pass, and `go vet` and `golangci-lint` report no issues. The Snyk CI findings the user declined earlier were not touched. Nothing has been committed --- cmd/kosli/getTrail.go | 2 +- cmd/kosli/getTrail_markdown_test.go | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/kosli/getTrail.go b/cmd/kosli/getTrail.go index ec087ff2f..ad3392579 100644 --- a/cmd/kosli/getTrail.go +++ b/cmd/kosli/getTrail.go @@ -116,7 +116,7 @@ func (o *getTrailOptions) printTrailAsMarkdown(raw string, out io.Writer, page i if err != nil { return err } - sha := mdCell(commitInfo["sha1"]) + sha := mdCell(stringOrNA(commitInfo["sha1"])) if commitURL, ok := commitInfo["url"].(string); ok && commitURL != "" { sha = fmt.Sprintf("[%s](%s)", sha, commitURL) } diff --git a/cmd/kosli/getTrail_markdown_test.go b/cmd/kosli/getTrail_markdown_test.go index dc9cd4590..da0ba8894 100644 --- a/cmd/kosli/getTrail_markdown_test.go +++ b/cmd/kosli/getTrail_markdown_test.go @@ -425,4 +425,10 @@ func TestPrintTrailAsMarkdownPartialCommitInfo(t *testing.T) { require.NoError(t, o.printTrailAsMarkdown(raw, &buf, 0)) require.Contains(t, buf.String(), "| Author | N/A |") require.Contains(t, buf.String(), "| Message | N/A |") + + missingSha := `{"name": "t1", "compliance_state": "INCOMPLETE", "last_modified_at": 1452902400, + "git_commit_info": {"author": "Jane "}, "events": []}` + buf.Reset() + require.NoError(t, o.printTrailAsMarkdown(missingSha, &buf, 0)) + require.Contains(t, buf.String(), "| Sha1 | N/A |") }