Skip to content

Commit eab9e8a

Browse files
committed
feat(quality): add AI-assisted OSPS-QA-06.02 evaluation
Implement AI-assisted assessment for OSPS-QA-06.02 (test execution documentation). When AI is configured, the step feeds README and CONTRIBUTING content to the model and records a structured verdict with evidence. Falls back gracefully to manual review when AI is unavailable. Updated to work with the TypedStep refactor (#302) and SDK v1.28.0 bump (#333). Changes: - Add TestExecutionDocumentation step with AI-assisted evaluation - Add testExecutionDocumentationEvidence to gather README/CONTRIBUTING - Add reusable_steps.AIFallback for generic AI degradation handling Signed-off-by: Vinaya Damle <vinayada1@users.noreply.github.com>
1 parent 6db38ed commit eab9e8a

12 files changed

Lines changed: 1052 additions & 45 deletions

File tree

.gitignore

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# ignore the generated artifacts
2-
pvtr-github-repo
3-
github-repo
4-
badge-url
2+
/pvtr-github-repo
3+
/pvtr-github-repo-dev
4+
/github-repo
5+
/github-repo-dev
6+
/badge-url
57
evaluation_results
68

79
# ignore any local dev config file

cmd/badge-url/main_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
"testing"
9+
)
10+
11+
func TestRunMissingFileFlag(t *testing.T) {
12+
var stdout, stderr bytes.Buffer
13+
code := run([]string{}, &stdout, &stderr)
14+
if code != 2 {
15+
t.Fatalf("expected exit code 2, got %d", code)
16+
}
17+
if !strings.Contains(stderr.String(), "requires -f") {
18+
t.Fatalf("expected usage message on stderr, got %q", stderr.String())
19+
}
20+
if stdout.Len() != 0 {
21+
t.Fatalf("expected empty stdout, got %q", stdout.String())
22+
}
23+
}
24+
25+
func TestRunInvalidFlag(t *testing.T) {
26+
var stdout, stderr bytes.Buffer
27+
code := run([]string{"--nonexistent"}, &stdout, &stderr)
28+
if code != 2 {
29+
t.Fatalf("expected exit code 2, got %d", code)
30+
}
31+
}
32+
33+
func TestRunNonexistentFile(t *testing.T) {
34+
var stdout, stderr bytes.Buffer
35+
code := run([]string{"-f", "/nonexistent/results.yaml"}, &stdout, &stderr)
36+
if code != 1 {
37+
t.Fatalf("expected exit code 1, got %d", code)
38+
}
39+
if !strings.Contains(stderr.String(), "read results file") {
40+
t.Fatalf("expected file error on stderr, got %q", stderr.String())
41+
}
42+
}
43+
44+
func TestRunInvalidYAML(t *testing.T) {
45+
filePath := writeTestFile(t, "not valid yaml: [")
46+
var stdout, stderr bytes.Buffer
47+
code := run([]string{"-f", filePath}, &stdout, &stderr)
48+
if code != 1 {
49+
t.Fatalf("expected exit code 1, got %d", code)
50+
}
51+
}
52+
53+
func TestRunValidResults(t *testing.T) {
54+
filePath := writeTestFile(t, minimalResultsYAML())
55+
var stdout, stderr bytes.Buffer
56+
code := run([]string{"-f", filePath, "-badge", "baseline-1"}, &stdout, &stderr)
57+
if code != 0 {
58+
t.Fatalf("expected exit code 0, got %d; stderr: %s", code, stderr.String())
59+
}
60+
output := stdout.String()
61+
if !strings.Contains(output, "bestpractices.dev") {
62+
t.Fatalf("expected bestpractices.dev URL in stdout, got %q", output)
63+
}
64+
if !strings.Contains(stderr.String(), "Open this link") {
65+
t.Fatalf("expected user instruction on stderr, got %q", stderr.String())
66+
}
67+
}
68+
69+
func TestRunNoJustifications(t *testing.T) {
70+
filePath := writeTestFile(t, minimalResultsYAML())
71+
var stdout, stderr bytes.Buffer
72+
code := run([]string{"-f", filePath, "-justifications=false"}, &stdout, &stderr)
73+
if code != 0 {
74+
t.Fatalf("expected exit code 0, got %d; stderr: %s", code, stderr.String())
75+
}
76+
if strings.Contains(stdout.String(), "justification=") {
77+
t.Fatalf("expected no justifications in URL, got %q", stdout.String())
78+
}
79+
}
80+
81+
func TestRunInvalidBadge(t *testing.T) {
82+
filePath := writeTestFile(t, minimalResultsYAML())
83+
var stdout, stderr bytes.Buffer
84+
code := run([]string{"-f", filePath, "-badge", "gold"}, &stdout, &stderr)
85+
if code != 1 {
86+
t.Fatalf("expected exit code 1, got %d", code)
87+
}
88+
if !strings.Contains(stderr.String(), "invalid badge") {
89+
t.Fatalf("expected invalid badge error, got %q", stderr.String())
90+
}
91+
}
92+
93+
func writeTestFile(t *testing.T, content string) string {
94+
t.Helper()
95+
filePath := filepath.Join(t.TempDir(), "results.yaml")
96+
if err := os.WriteFile(filePath, []byte(content), 0o600); err != nil {
97+
t.Fatalf("write test file: %v", err)
98+
}
99+
return filePath
100+
}
101+
102+
func minimalResultsYAML() string {
103+
return `payload:
104+
config:
105+
vars:
106+
owner: acme
107+
repo: rocket
108+
evaluation-suites:
109+
- control-evaluations:
110+
evaluations:
111+
- assessment-logs:
112+
- requirement:
113+
entry-id: OSPS-AC-01.01
114+
result: Passed
115+
message: MFA required
116+
applicability:
117+
- Maturity Level 1
118+
`
119+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"strings"
7+
"testing"
8+
9+
"github.com/ossf/pvtr-github-repo-scanner/evaluation_plans"
10+
)
11+
12+
func TestListSupportedCatalogs(t *testing.T) {
13+
// Capture stdout by redirecting os.Stdout to a pipe.
14+
old := os.Stdout
15+
r, w, err := os.Pipe()
16+
if err != nil {
17+
t.Fatal(err)
18+
}
19+
os.Stdout = w
20+
21+
main()
22+
23+
_ = w.Close()
24+
os.Stdout = old
25+
26+
var buf bytes.Buffer
27+
if _, err := buf.ReadFrom(r); err != nil {
28+
t.Fatal(err)
29+
}
30+
output := buf.String()
31+
32+
for _, catalogID := range evaluation_plans.SupportedCatalogIDs {
33+
if !strings.Contains(output, catalogID) {
34+
t.Errorf("expected output to contain %q, got %q", catalogID, output)
35+
}
36+
}
37+
38+
lines := strings.Split(strings.TrimSpace(output), "\n")
39+
if len(lines) != len(evaluation_plans.SupportedCatalogIDs) {
40+
t.Errorf("expected %d lines, got %d", len(evaluation_plans.SupportedCatalogIDs), len(lines))
41+
}
42+
}

data/payload.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"net/http"
77

8+
"github.com/gemaraproj/go-gemara"
89
"github.com/google/go-github/v74/github"
910
"github.com/privateerproj/privateer-sdk/config"
1011
"github.com/shurcooL/githubv4"
@@ -14,6 +15,7 @@ import (
1415
type Payload struct {
1516
*GraphqlRepoData
1617
*RestData
18+
Evidence *gemara.EvidenceCollector
1719
Config *config.Config
1820
RepositoryMetadata RepositoryMetadata
1921
DependencyManifestsCount int
@@ -24,6 +26,27 @@ type Payload struct {
2426
cachedTree *GraphqlRepoTree
2527
}
2628

29+
// AddEvidence, GetEvidence, and ClearEvidence implement gemara.HasEvidence.
30+
// Nil-safe: a payload without a collector silently drops evidence.
31+
func (p Payload) AddEvidence(evidence gemara.Evidence) {
32+
if p.Evidence != nil {
33+
p.Evidence.AddEvidence(evidence)
34+
}
35+
}
36+
37+
func (p Payload) GetEvidence() []gemara.Evidence {
38+
if p.Evidence == nil {
39+
return nil
40+
}
41+
return p.Evidence.GetEvidence()
42+
}
43+
44+
func (p Payload) ClearEvidence() {
45+
if p.Evidence != nil {
46+
p.Evidence.ClearEvidence()
47+
}
48+
}
49+
2750
func Loader(config *config.Config) (payload any, err error) {
2851
graphql, client, httpClient, err := getGraphqlRepoData(config)
2952
if err != nil {
@@ -62,6 +85,7 @@ func Loader(config *config.Config) (payload any, err error) {
6285
return any(Payload{
6386
GraphqlRepoData: graphql,
6487
RestData: rest,
88+
Evidence: &gemara.EvidenceCollector{},
6589
Config: config,
6690
RepositoryMetadata: repositoryMetadata,
6791
DependencyManifestsCount: dependencyManifestsCount,

evaluation_plans/evaluation-plans.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ var (
183183
quality.HasOneOrMoreStatusChecks,
184184
},
185185
"OSPS-QA-06.02": {
186-
quality.DocumentsTestExecution,
186+
quality.TestExecutionDocumentation,
187187
},
188188
"OSPS-QA-06.03": {
189189
reusable_steps.IsCodeRepo,

0 commit comments

Comments
 (0)