Support Codex CLI as a second agent harness alongside opencode, with per-harness credential mounting, model routing, and reasoning effort handling.
188 lines
5.0 KiB
Go
188 lines
5.0 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
|
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
|
|
|
package lint
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// resolvedIssue is one record of status/archive/issues-resolved.yaml, the
|
|
// frozen archive of issues closed before the tracker moved to the forge.
|
|
type resolvedIssue struct {
|
|
Title string `yaml:"title"`
|
|
Kind string `yaml:"kind"`
|
|
Areas []string `yaml:"areas"`
|
|
Outcome string `yaml:"outcome"`
|
|
Resolved string `yaml:"resolved"`
|
|
By string `yaml:"by"`
|
|
Summary string `yaml:"summary"`
|
|
Body string `yaml:"body"`
|
|
}
|
|
|
|
// archiveOutcomes is the closed set of outcome values. The forge label
|
|
// set has no outcome scope, so the archive carries its own.
|
|
var archiveOutcomes = map[string]bool{
|
|
"fixed": true,
|
|
"superseded": true,
|
|
"wontfix": true,
|
|
"duplicate": true,
|
|
"moved-upstream": true,
|
|
}
|
|
|
|
// archiveAreas is the area/ label set the archive was written against. The
|
|
// forge's labels are the live registry; the archive is frozen history, so
|
|
// a later rename there does not rewrite these records.
|
|
var archiveAreas = map[string]bool{
|
|
"identity": true,
|
|
"entitlements": true,
|
|
"catalog": true,
|
|
"billing": true,
|
|
"domains": true,
|
|
"fedwiki": true,
|
|
"discourse": true,
|
|
"integrations": true,
|
|
"operator-ui": true,
|
|
"member-ui": true,
|
|
"ops": true,
|
|
"testing": true,
|
|
"licensing": true,
|
|
"meta": true,
|
|
}
|
|
|
|
var archiveResolvedRe = regexp.MustCompile(`^\d{4}-\d{2}(-\d{2})?$`)
|
|
|
|
// repoRoot walks up from the test's working directory until it finds go.mod.
|
|
func repoRoot(t *testing.T) string {
|
|
t.Helper()
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for {
|
|
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
|
return dir
|
|
} else if !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatal(err)
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
t.Fatalf("no go.mod above %s", dir)
|
|
}
|
|
dir = parent
|
|
}
|
|
}
|
|
|
|
// formKinds reads every issue form under .gitea/ISSUE_TEMPLATE and returns
|
|
// the kind/ label each one applies, with the scope prefix removed. The forms
|
|
// are the in-repo statement of which kinds exist.
|
|
func formKinds(t *testing.T, dir string) map[string]bool {
|
|
t.Helper()
|
|
paths, err := filepath.Glob(filepath.Join(dir, "*.yaml"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
kinds := map[string]bool{}
|
|
for _, path := range paths {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var form struct {
|
|
Labels []string `yaml:"labels"`
|
|
}
|
|
if err := yaml.Unmarshal(raw, &form); err != nil {
|
|
t.Fatalf("%s: %v", path, err)
|
|
}
|
|
for _, l := range form.Labels {
|
|
if rest, ok := strings.CutPrefix(l, "kind/"); ok {
|
|
kinds[rest] = true
|
|
}
|
|
}
|
|
}
|
|
if len(kinds) == 0 {
|
|
t.Fatalf("%s: no issue form applies a kind/ label", dir)
|
|
}
|
|
return kinds
|
|
}
|
|
|
|
// TestResolvedIssuesArchiveSchema enforces the record schema of
|
|
// status/archive/issues-resolved.yaml: every field but `by` present and
|
|
// non-empty, kind one the issue forms apply, areas from the frozen
|
|
// archiveAreas set, outcome from the archive's own set, resolved a
|
|
// YYYY-MM-DD or YYYY-MM date, at least one area, and no two records with
|
|
// the same title.
|
|
func TestResolvedIssuesArchiveSchema(t *testing.T) {
|
|
root := repoRoot(t)
|
|
kinds := formKinds(t, filepath.Join(root, ".gitea", "ISSUE_TEMPLATE"))
|
|
areas := archiveAreas
|
|
|
|
path := filepath.Join(root, "status", "archive", "issues-resolved.yaml")
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
dec := yaml.NewDecoder(f)
|
|
dec.KnownFields(true)
|
|
var records []resolvedIssue
|
|
if err := dec.Decode(&records); err != nil {
|
|
t.Fatalf("%s: %v", path, err)
|
|
}
|
|
if len(records) == 0 {
|
|
t.Fatalf("%s: no records", path)
|
|
}
|
|
|
|
seen := map[string]int{}
|
|
for i, r := range records {
|
|
n := i + 1
|
|
fail := func(format string, args ...any) {
|
|
t.Helper()
|
|
t.Errorf("record %d %q: %s", n, r.Title, fmt.Sprintf(format, args...))
|
|
}
|
|
required := []struct{ name, value string }{
|
|
{"title", r.Title},
|
|
{"kind", r.Kind},
|
|
{"outcome", r.Outcome},
|
|
{"resolved", r.Resolved},
|
|
{"summary", r.Summary},
|
|
{"body", r.Body},
|
|
}
|
|
for _, field := range required {
|
|
if strings.TrimSpace(field.value) == "" {
|
|
fail("%s is missing or empty", field.name)
|
|
}
|
|
}
|
|
if r.Kind != "" && !kinds[r.Kind] {
|
|
fail("kind %q is not a kind/ label any issue form applies", r.Kind)
|
|
}
|
|
if len(r.Areas) == 0 {
|
|
fail("areas is empty")
|
|
}
|
|
for _, a := range r.Areas {
|
|
if !areas[a] {
|
|
fail("area %q is not in the archive area set", a)
|
|
}
|
|
}
|
|
if r.Outcome != "" && !archiveOutcomes[r.Outcome] {
|
|
fail("outcome %q is not one of fixed, superseded, wontfix, duplicate, moved-upstream", r.Outcome)
|
|
}
|
|
if r.Resolved != "" && !archiveResolvedRe.MatchString(r.Resolved) {
|
|
fail("resolved %q is not YYYY-MM-DD or YYYY-MM", r.Resolved)
|
|
}
|
|
if first, dup := seen[r.Title]; dup {
|
|
fail("duplicate title, first at record %d", first)
|
|
} else {
|
|
seen[r.Title] = n
|
|
}
|
|
}
|
|
}
|