chore: import upstream snapshot with attribution
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/qualitygate/publiccontent"
|
||||
"github.com/larksuite/cli/internal/qualitygate/report"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/internal/vfs"
|
||||
)
|
||||
|
||||
type eventPayload struct {
|
||||
Comment *struct {
|
||||
Body string `json:"body"`
|
||||
} `json:"comment"`
|
||||
Review *struct {
|
||||
Body string `json:"body"`
|
||||
} `json:"review"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
eventPath := flag.String("event", os.Getenv("GITHUB_EVENT_PATH"), "GitHub event payload path")
|
||||
kind := flag.String("kind", os.Getenv("GITHUB_EVENT_NAME"), "GitHub event kind")
|
||||
flag.Parse()
|
||||
|
||||
if *eventPath == "" {
|
||||
fmt.Fprintln(os.Stderr, "comment-audit: --event or GITHUB_EVENT_PATH is required")
|
||||
os.Exit(2)
|
||||
}
|
||||
body, err := commentBody(*eventPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "comment-audit: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
diags := diagnostics(publiccontent.ScanComment(*kind, body))
|
||||
if len(diags) > 0 {
|
||||
fmt.Fprintln(os.Stderr, auditFailureSummary(len(diags)))
|
||||
}
|
||||
report.Print(os.Stderr, diags)
|
||||
os.Exit(report.ExitCode(diags))
|
||||
}
|
||||
|
||||
func auditFailureSummary(count int) string {
|
||||
return fmt.Sprintf("post-publication audit found public content findings: %d", count)
|
||||
}
|
||||
|
||||
func commentBody(path string) (string, error) {
|
||||
safePath, err := validate.SafeInputPath(path)
|
||||
if err != nil {
|
||||
return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid --event: %v", err).
|
||||
WithParam("--event").
|
||||
WithCause(err)
|
||||
}
|
||||
data, err := vfs.ReadFile(safePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var payload eventPayload
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch {
|
||||
case payload.Comment != nil:
|
||||
return payload.Comment.Body, nil
|
||||
case payload.Review != nil:
|
||||
return payload.Review.Body, nil
|
||||
default:
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
func diagnostics(items []publiccontent.Finding) []report.Diagnostic {
|
||||
out := make([]report.Diagnostic, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, report.Diagnostic{
|
||||
Rule: item.Rule,
|
||||
Action: item.Action,
|
||||
File: item.File,
|
||||
Line: item.Line,
|
||||
Message: item.Message,
|
||||
Suggestion: item.Suggestion,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
)
|
||||
|
||||
func TestCommentBodyReadsSafeRelativeEventPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := writeTestFile(filepath.Join(dir, "event.json"), `{"comment":{"body":"clean comment"}}`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chdir(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = os.Chdir(origDir)
|
||||
})
|
||||
|
||||
got, err := commentBody("event.json")
|
||||
if err != nil {
|
||||
t.Fatalf("commentBody() error = %v", err)
|
||||
}
|
||||
if got != "clean comment" {
|
||||
t.Fatalf("comment body = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommentBodyRejectsUnsafeEventPath(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "event.json")
|
||||
if err := writeTestFile(path, `{"comment":{"body":"clean"}}`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := commentBody(path)
|
||||
problem, ok := errs.ProblemOf(err)
|
||||
if err == nil || !ok {
|
||||
t.Fatalf("commentBody(%q) error = %v, want unsafe path validation error", path, err)
|
||||
}
|
||||
if problem.Category != errs.CategoryValidation || problem.Subtype != errs.SubtypeInvalidArgument {
|
||||
t.Fatalf("commentBody(%q) problem = %#v, want invalid argument validation", path, problem)
|
||||
}
|
||||
var validationErr *errs.ValidationError
|
||||
if !errors.As(err, &validationErr) || validationErr.Param != "--event" {
|
||||
t.Fatalf("commentBody(%q) error = %v, want --event validation param", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuditFailureSummaryStatesPostPublicationAudit(t *testing.T) {
|
||||
got := auditFailureSummary(2)
|
||||
want := "post-publication audit found public content findings: 2"
|
||||
if got != want {
|
||||
t.Fatalf("auditFailureSummary() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func writeTestFile(path, data string) error {
|
||||
return os.WriteFile(path, []byte(data), 0o644)
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
rootcmd "github.com/larksuite/cli/cmd"
|
||||
"github.com/larksuite/cli/internal/cmdmeta"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/qualitygate/manifest"
|
||||
"github.com/larksuite/cli/internal/registry"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func collectHandAuthored(ctx context.Context) (manifest.Manifest, error) {
|
||||
root := rootcmd.Build(ctx, cmdutil.InvocationContext{},
|
||||
rootcmd.WithIO(strings.NewReader(""), io.Discard, io.Discard),
|
||||
rootcmd.WithoutPlugins(),
|
||||
rootcmd.WithoutStrictMode(),
|
||||
rootcmd.WithoutServiceCommands(),
|
||||
)
|
||||
|
||||
return collectFromRoot(root), nil
|
||||
}
|
||||
|
||||
func collectCommandIndex(ctx context.Context) (manifest.Manifest, error) {
|
||||
root := rootcmd.Build(ctx, cmdutil.InvocationContext{},
|
||||
rootcmd.WithIO(strings.NewReader(""), io.Discard, io.Discard),
|
||||
rootcmd.WithoutPlugins(),
|
||||
rootcmd.WithoutStrictMode(),
|
||||
rootcmd.WithServiceCatalog(registry.EmbeddedCatalog()),
|
||||
)
|
||||
|
||||
idx := collectFromRoot(root)
|
||||
handAuthored, err := collectHandAuthored(ctx)
|
||||
if err != nil {
|
||||
return manifest.Manifest{}, err
|
||||
}
|
||||
return overlayHandAuthoredCommands(idx, handAuthored), nil
|
||||
}
|
||||
|
||||
func collectFromRoot(root *cobra.Command) manifest.Manifest {
|
||||
var commands []manifest.Command
|
||||
walkCommands(root, func(c *cobra.Command) {
|
||||
if c == root {
|
||||
return
|
||||
}
|
||||
commands = append(commands, commandFromCobra(c, nil))
|
||||
})
|
||||
sort.Slice(commands, func(i, j int) bool {
|
||||
return commands[i].Path < commands[j].Path
|
||||
})
|
||||
|
||||
return manifest.Manifest{SchemaVersion: 1, Commands: commands}
|
||||
}
|
||||
|
||||
func overlayHandAuthoredCommands(idx, handAuthored manifest.Manifest) manifest.Manifest {
|
||||
byPath := make(map[string]manifest.Command, len(handAuthored.Commands))
|
||||
for _, cmd := range handAuthored.Commands {
|
||||
byPath[cmd.Path] = cmd
|
||||
}
|
||||
for i, cmd := range idx.Commands {
|
||||
if handCmd, ok := byPath[cmd.Path]; ok {
|
||||
idx.Commands[i] = handCmd
|
||||
}
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
func walkCommands(root *cobra.Command, visit func(*cobra.Command)) {
|
||||
visit(root)
|
||||
for _, child := range root.Commands() {
|
||||
walkCommands(child, visit)
|
||||
}
|
||||
}
|
||||
|
||||
func commandFromCobra(c *cobra.Command, defaultFields map[string][]string) manifest.Command {
|
||||
path := strings.TrimPrefix(c.CommandPath(), "lark-cli ")
|
||||
source := manifest.SourceBuiltin
|
||||
if s, ok := cmdmeta.SourceOf(c); ok {
|
||||
source = manifest.Source(s)
|
||||
}
|
||||
entry := manifest.Command{
|
||||
Path: path,
|
||||
CanonicalPath: manifest.CanonicalCommandPath(path),
|
||||
Domain: commandDomain(c, path, source),
|
||||
Use: c.Use,
|
||||
Short: c.Short,
|
||||
Example: c.Example,
|
||||
Hidden: c.Hidden,
|
||||
Runnable: c.Runnable(),
|
||||
Source: source,
|
||||
Generated: cmdmeta.Generated(c),
|
||||
Identities: cmdmeta.Identities(c),
|
||||
DefaultFields: defaultFields[path],
|
||||
}
|
||||
if risk, ok := cmdmeta.Risk(c); ok {
|
||||
entry.Risk = risk
|
||||
}
|
||||
|
||||
c.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
entry.Flags = append(entry.Flags, flagFromPFlag(f))
|
||||
})
|
||||
c.InheritedFlags().VisitAll(func(f *pflag.Flag) {
|
||||
if findFlag(entry.Flags, f.Name) == nil {
|
||||
entry.Flags = append(entry.Flags, flagFromPFlag(f))
|
||||
}
|
||||
})
|
||||
sort.Slice(entry.Flags, func(i, j int) bool {
|
||||
return entry.Flags[i].Name < entry.Flags[j].Name
|
||||
})
|
||||
return entry
|
||||
}
|
||||
|
||||
func commandDomain(c *cobra.Command, path string, source manifest.Source) string {
|
||||
if domain := cmdmeta.Domain(c); domain != "" {
|
||||
return domain
|
||||
}
|
||||
if source == manifest.SourceService {
|
||||
if first, _, ok := strings.Cut(path, " "); ok {
|
||||
return first
|
||||
}
|
||||
return path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func flagFromPFlag(f *pflag.Flag) manifest.Flag {
|
||||
return manifest.Flag{
|
||||
Name: f.Name,
|
||||
Shorthand: f.Shorthand,
|
||||
Usage: f.Usage,
|
||||
Hidden: f.Hidden,
|
||||
Required: hasAnnotation(f, cobra.BashCompOneRequiredFlag),
|
||||
TakesValue: f.NoOptDefVal == "",
|
||||
DefValue: f.DefValue,
|
||||
NoOptValue: f.NoOptDefVal,
|
||||
Annotations: cloneAnnotations(f.Annotations),
|
||||
}
|
||||
}
|
||||
|
||||
func findFlag(flags []manifest.Flag, name string) *manifest.Flag {
|
||||
for i := range flags {
|
||||
if flags[i].Name == name {
|
||||
return &flags[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasAnnotation(f *pflag.Flag, key string) bool {
|
||||
if f.Annotations == nil {
|
||||
return false
|
||||
}
|
||||
values, ok := f.Annotations[key]
|
||||
return ok && len(values) > 0
|
||||
}
|
||||
|
||||
func cloneAnnotations(in map[string][]string) map[string][]string {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string][]string, len(in))
|
||||
for key, values := range in {
|
||||
out[key] = append([]string(nil), values...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/larksuite/cli/internal/qualitygate/manifest"
|
||||
"github.com/larksuite/cli/internal/vfs"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(runManifestExport(os.Args[1:], os.Stderr))
|
||||
}
|
||||
|
||||
func runManifestExport(args []string, stderr io.Writer) int {
|
||||
configureManifestExportEnvironment()
|
||||
|
||||
fs := flag.NewFlagSet("manifest-export", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
var manifestOut string
|
||||
var commandIndexOut string
|
||||
fs.StringVar(&manifestOut, "manifest-out", "", "write hand-authored command manifest JSON to this path")
|
||||
fs.StringVar(&commandIndexOut, "command-index-out", "", "write full command index JSON to this path")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
fmt.Fprintf(stderr, "manifest-export: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
if manifestOut == "" || commandIndexOut == "" {
|
||||
fmt.Fprintln(stderr, "manifest-export: --manifest-out and --command-index-out are required")
|
||||
return 2
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
m, err := collectHandAuthored(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "manifest-export: collect command manifest: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
idx, err := collectCommandIndex(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "manifest-export: collect command index: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
if err := ensureParentDir(manifestOut); err != nil {
|
||||
fmt.Fprintf(stderr, "manifest-export: create manifest output directory: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
if err := ensureParentDir(commandIndexOut); err != nil {
|
||||
fmt.Fprintf(stderr, "manifest-export: create command index output directory: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
if err := manifest.WriteFile(manifestOut, manifest.KindCommandManifest, m); err != nil {
|
||||
fmt.Fprintf(stderr, "manifest-export: write command manifest: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
if err := manifest.WriteFile(commandIndexOut, manifest.KindCommandIndex, idx); err != nil {
|
||||
fmt.Fprintf(stderr, "manifest-export: write command index: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func ensureParentDir(path string) error {
|
||||
dir := filepath.Dir(path)
|
||||
if dir == "." || dir == "" {
|
||||
return nil
|
||||
}
|
||||
return vfs.MkdirAll(dir, 0o755)
|
||||
}
|
||||
|
||||
func configureManifestExportEnvironment() {
|
||||
_ = os.Setenv("LARKSUITE_CLI_REMOTE_META", "off")
|
||||
if os.Getenv("LARKSUITE_CLI_CONFIG_DIR") == "" {
|
||||
_ = os.Setenv("LARKSUITE_CLI_CONFIG_DIR", filepath.Join(os.TempDir(), "quality-gate-cli-config"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/qualitygate/manifest"
|
||||
)
|
||||
|
||||
func TestManifestExportWritesManifestAndCommandIndex(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
manifestPath := filepath.Join(dir, "command-manifest.json")
|
||||
indexPath := filepath.Join(dir, "command-index.json")
|
||||
|
||||
code := runManifestExport([]string{
|
||||
"--manifest-out", manifestPath,
|
||||
"--command-index-out", indexPath,
|
||||
}, &bytes.Buffer{})
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d", code)
|
||||
}
|
||||
|
||||
m, err := manifest.ReadFile(manifestPath, manifest.KindCommandManifest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
idx, err := manifest.ReadFile(indexPath, manifest.KindCommandIndex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(m.Commands) == 0 || len(idx.Commands) == 0 {
|
||||
t.Fatalf("empty export: manifest=%d index=%d", len(m.Commands), len(idx.Commands))
|
||||
}
|
||||
if hasServiceCommand(m) {
|
||||
t.Fatal("command-manifest should not include service commands")
|
||||
}
|
||||
if !hasServiceCommand(idx) {
|
||||
t.Fatal("command-index should include service commands")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestExportRequiresOutputPaths(t *testing.T) {
|
||||
var stderr bytes.Buffer
|
||||
code := runManifestExport(nil, &stderr)
|
||||
if code != 2 {
|
||||
t.Fatalf("exit code = %d", code)
|
||||
}
|
||||
if got := stderr.String(); !bytes.Contains([]byte(got), []byte("--manifest-out and --command-index-out are required")) {
|
||||
t.Fatalf("stderr = %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureManifestExportEnvironmentForcesDeterministicRegistry(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_REMOTE_META", "on")
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", "")
|
||||
|
||||
configureManifestExportEnvironment()
|
||||
|
||||
if got := os.Getenv("LARKSUITE_CLI_REMOTE_META"); got != "off" {
|
||||
t.Fatalf("LARKSUITE_CLI_REMOTE_META = %q, want off", got)
|
||||
}
|
||||
if got := os.Getenv("LARKSUITE_CLI_CONFIG_DIR"); got == "" {
|
||||
t.Fatal("LARKSUITE_CLI_CONFIG_DIR was not set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectContainsDocsFetchAndDryRunFlag(t *testing.T) {
|
||||
got, err := collectHandAuthored(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("collectHandAuthored() error = %v", err)
|
||||
}
|
||||
cmd := findManifestCommand(&got, "docs +fetch")
|
||||
if cmd == nil {
|
||||
t.Fatalf("docs +fetch not found")
|
||||
}
|
||||
if !cmd.Runnable {
|
||||
t.Fatalf("docs +fetch should be runnable")
|
||||
}
|
||||
if findManifestFlag(cmd, "dry-run") == nil {
|
||||
t.Fatalf("docs +fetch should expose --dry-run")
|
||||
}
|
||||
if cmd.Source != manifest.SourceShortcut {
|
||||
t.Fatalf("docs +fetch source = %q, want shortcut", cmd.Source)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectExcludesGeneratedServiceCommands(t *testing.T) {
|
||||
got, err := collectHandAuthored(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("collectHandAuthored() error = %v", err)
|
||||
}
|
||||
for _, cmd := range got.Commands {
|
||||
if cmd.Source == manifest.SourceService || cmd.Generated {
|
||||
t.Fatalf("quality-gate manifest should not include generated service command: %#v", cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectCommandIndexIncludesEmbeddedServiceCommand(t *testing.T) {
|
||||
got, err := collectCommandIndex(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("collectCommandIndex() error = %v", err)
|
||||
}
|
||||
cmd := findManifestCommand(&got, "drive file.comments create_v2")
|
||||
if cmd == nil {
|
||||
t.Fatalf("drive file.comments create_v2 not found")
|
||||
}
|
||||
if cmd.Source != manifest.SourceService {
|
||||
t.Fatalf("source = %q, want service", cmd.Source)
|
||||
}
|
||||
if !cmd.Generated {
|
||||
t.Fatalf("service command should be marked generated")
|
||||
}
|
||||
if !cmd.Runnable {
|
||||
t.Fatalf("service method command should be runnable")
|
||||
}
|
||||
for _, name := range []string{"file-token", "params", "data", "dry-run"} {
|
||||
if findManifestFlag(cmd, name) == nil {
|
||||
t.Fatalf("drive file.comments create_v2 should expose --%s", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectCommandIndexDoesNotInheritGeneratedFromServiceParentForShortcut(t *testing.T) {
|
||||
got, err := collectCommandIndex(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("collectCommandIndex() error = %v", err)
|
||||
}
|
||||
cmd := findManifestCommand(&got, "docs +fetch")
|
||||
if cmd == nil {
|
||||
t.Fatalf("docs +fetch not found")
|
||||
}
|
||||
if cmd.Source != manifest.SourceShortcut {
|
||||
t.Fatalf("docs +fetch source = %q, want shortcut", cmd.Source)
|
||||
}
|
||||
if cmd.Generated {
|
||||
t.Fatalf("shortcut under service parent must not inherit generated=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectCommandIndexPreservesHandAuthoredMetadataForOverlappingCommands(t *testing.T) {
|
||||
handAuthored, err := collectHandAuthored(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("collectHandAuthored() error = %v", err)
|
||||
}
|
||||
idx, err := collectCommandIndex(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("collectCommandIndex() error = %v", err)
|
||||
}
|
||||
for _, handCmd := range handAuthored.Commands {
|
||||
indexCmd := findManifestCommand(&idx, handCmd.Path)
|
||||
if indexCmd == nil {
|
||||
t.Fatalf("command-index missing hand-authored command %q", handCmd.Path)
|
||||
}
|
||||
if indexCmd.Source != handCmd.Source || indexCmd.Generated != handCmd.Generated {
|
||||
t.Fatalf("command-index metadata for %q = source:%s generated:%v, want source:%s generated:%v", handCmd.Path, indexCmd.Source, indexCmd.Generated, handCmd.Source, handCmd.Generated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectDoesNotInheritGeneratedFromServiceParentForShortcut(t *testing.T) {
|
||||
got, err := collectHandAuthored(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("collectHandAuthored() error = %v", err)
|
||||
}
|
||||
cmd := findManifestCommand(&got, "docs +fetch")
|
||||
if cmd == nil {
|
||||
t.Fatalf("docs +fetch not found")
|
||||
}
|
||||
if cmd.Source != manifest.SourceShortcut {
|
||||
t.Fatalf("docs +fetch source = %q, want shortcut", cmd.Source)
|
||||
}
|
||||
if cmd.Generated {
|
||||
t.Fatalf("shortcut under service parent must not inherit generated=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectIgnoresRuntimeStrictMode(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
t.Setenv("LARKSUITE_CLI_APP_ID", "dry-run")
|
||||
t.Setenv("LARKSUITE_CLI_APP_SECRET", "dry-run")
|
||||
t.Setenv("LARKSUITE_CLI_BRAND", "feishu")
|
||||
|
||||
got, err := collectHandAuthored(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("collectHandAuthored() error = %v", err)
|
||||
}
|
||||
if cmd := findManifestCommand(&got, "contact +search-user"); cmd == nil {
|
||||
t.Fatal("user-only shortcut missing; manifest collection should not apply runtime strict mode")
|
||||
}
|
||||
}
|
||||
|
||||
func hasServiceCommand(m manifest.Manifest) bool {
|
||||
for _, cmd := range m.Commands {
|
||||
if cmd.Source == manifest.SourceService {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func findManifestCommand(m *manifest.Manifest, path string) *manifest.Command {
|
||||
for i := range m.Commands {
|
||||
if m.Commands[i].Path == path {
|
||||
return &m.Commands[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findManifestFlag(cmd *manifest.Command, name string) *manifest.Flag {
|
||||
for i := range cmd.Flags {
|
||||
if cmd.Flags[i].Name == name {
|
||||
return &cmd.Flags[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/larksuite/cli/internal/qualitygate/manifest"
|
||||
"github.com/larksuite/cli/internal/qualitygate/report"
|
||||
"github.com/larksuite/cli/internal/qualitygate/rules"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usageAndExit(2)
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case "check":
|
||||
os.Exit(runCheck(os.Args[2:]))
|
||||
default:
|
||||
usageAndExit(2)
|
||||
}
|
||||
}
|
||||
|
||||
func runCheck(args []string) int {
|
||||
configureQualityGateEnvironment()
|
||||
|
||||
fs := flag.NewFlagSet("check", flag.ContinueOnError)
|
||||
opts := rules.Options{}
|
||||
var printLegacyCommandCandidates bool
|
||||
var printLegacyFlagCandidates bool
|
||||
fs.StringVar(&opts.Repo, "repo", ".", "repository root")
|
||||
fs.StringVar(&opts.CLIBin, "cli-bin", "./lark-cli", "lark-cli binary used for dry-run validation")
|
||||
fs.StringVar(&opts.ChangedFrom, "changed-from", "", "base revision for incremental checks")
|
||||
fs.StringVar(&opts.FactsOut, "facts-out", "", "write facts JSON to this path")
|
||||
fs.StringVar(&opts.ManifestPath, "manifest", "", "hand-authored command manifest JSON")
|
||||
fs.StringVar(&opts.CommandIndexPath, "command-index", "", "full command index JSON")
|
||||
fs.StringVar(&opts.PublicContentMetadataPath, "public-content-metadata", "", "PR title/body metadata JSON for public content checks")
|
||||
fs.BoolVar(&printLegacyCommandCandidates, "print-legacy-command-candidates", false, "print current non-kebab-case hand-authored command candidates")
|
||||
fs.BoolVar(&printLegacyFlagCandidates, "print-legacy-flag-candidates", false, "print current non-kebab-case flag candidates")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "quality-gate check: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
|
||||
if opts.PublicContentMetadataPath != "" {
|
||||
safePath, err := validate.SafeInputPath(opts.PublicContentMetadataPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "quality-gate check: --public-content-metadata: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
opts.PublicContentMetadataPath = safePath
|
||||
}
|
||||
|
||||
if opts.ManifestPath == "" || opts.CommandIndexPath == "" {
|
||||
fmt.Fprintln(os.Stderr, "quality-gate check: --manifest and --command-index are required")
|
||||
return 2
|
||||
}
|
||||
|
||||
if printLegacyCommandCandidates || printLegacyFlagCandidates {
|
||||
m, err := manifest.ReadFile(opts.ManifestPath, manifest.KindCommandManifest)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "quality-gate check: --manifest: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
if printLegacyCommandCandidates {
|
||||
for _, line := range rules.LegacyCommandCandidates(m) {
|
||||
fmt.Fprintln(os.Stdout, line)
|
||||
}
|
||||
}
|
||||
if printLegacyFlagCandidates {
|
||||
for _, line := range rules.LegacyFlagCandidates(m) {
|
||||
fmt.Fprintln(os.Stdout, line)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
diags, facts, err := rules.Run(context.Background(), opts)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "quality-gate check: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
report.Print(os.Stderr, diags)
|
||||
if opts.FactsOut != "" {
|
||||
if err := facts.WriteFile(opts.FactsOut); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "quality-gate check: write facts: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
return report.ExitCode(diags)
|
||||
}
|
||||
|
||||
func configureQualityGateEnvironment() {
|
||||
_ = os.Setenv("LARKSUITE_CLI_REMOTE_META", "off")
|
||||
if os.Getenv("LARKSUITE_CLI_CONFIG_DIR") == "" {
|
||||
_ = os.Setenv("LARKSUITE_CLI_CONFIG_DIR", filepath.Join(os.TempDir(), "quality-gate-cli-config"))
|
||||
}
|
||||
}
|
||||
|
||||
func usageAndExit(code int) {
|
||||
fmt.Fprintln(os.Stderr, "usage: quality-gate check")
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/qualitygate/manifest"
|
||||
)
|
||||
|
||||
func TestConfigureQualityGateEnvironmentForcesDeterministicRegistry(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_REMOTE_META", "on")
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", "")
|
||||
|
||||
configureQualityGateEnvironment()
|
||||
|
||||
if got := os.Getenv("LARKSUITE_CLI_REMOTE_META"); got != "off" {
|
||||
t.Fatalf("LARKSUITE_CLI_REMOTE_META = %q, want off", got)
|
||||
}
|
||||
if got := os.Getenv("LARKSUITE_CLI_CONFIG_DIR"); got == "" {
|
||||
t.Fatal("LARKSUITE_CLI_CONFIG_DIR was not set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRequiresManifestInputs(t *testing.T) {
|
||||
code, stderr := runCheckCaptureStderr(t, []string{"--repo", t.TempDir(), "--cli-bin", "./lark-cli"})
|
||||
if code != 2 {
|
||||
t.Fatalf("exit code = %d, stderr=%s", code, stderr)
|
||||
}
|
||||
if !strings.Contains(stderr, "--manifest and --command-index are required") {
|
||||
t.Fatalf("stderr = %s", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckAcceptsPublicContentMetadataFlag(t *testing.T) {
|
||||
code, stderr := runCheckCaptureStderr(t, []string{
|
||||
"--repo", t.TempDir(),
|
||||
"--cli-bin", "./lark-cli",
|
||||
"--public-content-metadata", ".tmp/quality-gate/pr.json",
|
||||
})
|
||||
if code != 2 {
|
||||
t.Fatalf("exit code = %d, stderr=%s", code, stderr)
|
||||
}
|
||||
if strings.Contains(stderr, "flag provided but not defined") {
|
||||
t.Fatalf("public content metadata flag was not registered: %s", stderr)
|
||||
}
|
||||
if !strings.Contains(stderr, "--manifest and --command-index are required") {
|
||||
t.Fatalf("stderr = %s", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRejectsUnsafePublicContentMetadataPath(t *testing.T) {
|
||||
code, stderr := runCheckCaptureStderr(t, []string{
|
||||
"--repo", t.TempDir(),
|
||||
"--cli-bin", "./lark-cli",
|
||||
"--public-content-metadata", filepath.Join(t.TempDir(), "pr.json"),
|
||||
})
|
||||
if code != 2 {
|
||||
t.Fatalf("exit code = %d, stderr=%s", code, stderr)
|
||||
}
|
||||
if !strings.Contains(stderr, "--public-content-metadata") || !strings.Contains(stderr, "--file") {
|
||||
t.Fatalf("stderr = %s, want unsafe public content metadata path error", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckReportsManifestReadErrorsWithFlagName(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
manifestPath := filepath.Join(dir, "command-manifest.json")
|
||||
indexPath := filepath.Join(dir, "command-index.json")
|
||||
if err := os.WriteFile(manifestPath, []byte(`{"schema_version":999,"commands":[]}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := manifest.WriteFile(indexPath, manifest.KindCommandIndex, manifest.Manifest{
|
||||
SchemaVersion: 1,
|
||||
Commands: []manifest.Command{{
|
||||
Path: "drive file.comments create_v2",
|
||||
CanonicalPath: "drive file-comments create-v2",
|
||||
Source: manifest.SourceService,
|
||||
Generated: true,
|
||||
}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
code, stderr := runCheckCaptureStderr(t, []string{
|
||||
"--repo", dir,
|
||||
"--cli-bin", "./lark-cli",
|
||||
"--manifest", manifestPath,
|
||||
"--command-index", indexPath,
|
||||
})
|
||||
if code != 2 {
|
||||
t.Fatalf("exit code = %d, stderr=%s", code, stderr)
|
||||
}
|
||||
if !strings.Contains(stderr, "--manifest:") {
|
||||
t.Fatalf("stderr = %s", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func runCheckCaptureStderr(t *testing.T, args []string) (int, string) {
|
||||
t.Helper()
|
||||
original := os.Stderr
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Stderr = w
|
||||
code := runCheck(args)
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Stderr = original
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return code, string(data)
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/larksuite/cli/internal/qualitygate/facts"
|
||||
"github.com/larksuite/cli/internal/qualitygate/report"
|
||||
"github.com/larksuite/cli/internal/qualitygate/semantic"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(run(os.Args[1:]))
|
||||
}
|
||||
|
||||
func run(args []string) int {
|
||||
fs := flag.NewFlagSet("semantic-review", flag.ContinueOnError)
|
||||
var repo, factsPath, reviewPath, waiversPath, decisionOut, markdownOut string
|
||||
var block bool
|
||||
fs.StringVar(&repo, "repo", ".", "repository root")
|
||||
fs.StringVar(&factsPath, "facts", "", "facts.json path")
|
||||
fs.StringVar(&reviewPath, "review-json", "", "optional precomputed review JSON")
|
||||
fs.StringVar(&waiversPath, "waivers-file", "", "optional semantic waiver TSV file")
|
||||
fs.StringVar(&decisionOut, "decision-out", "", "optional decision JSON output path")
|
||||
fs.StringVar(&markdownOut, "markdown-out", "", "optional markdown output path")
|
||||
fs.BoolVar(&block, "block", false, "exit 1 when gatekeeper finds blockers")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
if factsPath == "" && fs.NArg() == 1 {
|
||||
factsPath = fs.Arg(0)
|
||||
}
|
||||
if factsPath == "" {
|
||||
fmt.Fprintln(os.Stderr, "semantic-review: --facts is required")
|
||||
return 2
|
||||
}
|
||||
|
||||
f, err := facts.ReadFile(factsPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "semantic-review: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
policy, waivers, waiverDiags, modelConfig, err := loadSemanticConfig(repo, waiversPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "semantic-review: %v\n", err)
|
||||
decision := semantic.InfrastructureFailureDecision(err)
|
||||
decision.BlockMode = block
|
||||
_ = semantic.WriteDecision(decisionOut, decision)
|
||||
_ = semantic.WriteMarkdown(markdownOut, decision)
|
||||
return 0
|
||||
}
|
||||
if reviewPath == "" && !semantic.BuildInputView(f).HasReviewableFacts() {
|
||||
decision := finalizeDecision(block, waiverDiags, semantic.Decision{})
|
||||
if err := writeSemanticOutputs(decisionOut, markdownOut, decision); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "semantic-review: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
return decisionExitCode(decision)
|
||||
}
|
||||
review, err := semantic.LoadOrReviewWithConfig(context.Background(), f, reviewPath, modelConfig)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "semantic-review: %v\n", err)
|
||||
decision := semantic.DegradedDecision(err)
|
||||
switch {
|
||||
case errors.Is(err, semantic.ErrReviewerUnavailable):
|
||||
decision = semantic.SkippedDecision(err)
|
||||
case errors.Is(err, semantic.ErrReviewerConfiguration):
|
||||
decision = semantic.InfrastructureFailureDecision(err)
|
||||
}
|
||||
decision.BlockMode = block
|
||||
_ = semantic.WriteDecision(decisionOut, decision)
|
||||
_ = semantic.WriteMarkdown(markdownOut, decision)
|
||||
return 0
|
||||
}
|
||||
decision := semantic.DecideWithWaivers(f, review, policy, waivers)
|
||||
decision = finalizeDecision(block, waiverDiags, decision)
|
||||
if err := writeSemanticOutputs(decisionOut, markdownOut, decision); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "semantic-review: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
return decisionExitCode(decision)
|
||||
}
|
||||
|
||||
func finalizeDecision(block bool, waiverDiags []report.Diagnostic, decision semantic.Decision) semantic.Decision {
|
||||
decision.BlockMode = block
|
||||
if !block && len(decision.Blockers) > 0 {
|
||||
for i := range decision.Blockers {
|
||||
decision.Blockers[i].ReviewAction = semantic.ReviewActionObserve
|
||||
}
|
||||
decision.Warnings = append(decision.Warnings, decision.Blockers...)
|
||||
decision.Blockers = nil
|
||||
}
|
||||
decision.SystemWarnings = append(diagnosticSystemWarnings(waiverDiags), decision.SystemWarnings...)
|
||||
return decision
|
||||
}
|
||||
|
||||
func writeSemanticOutputs(decisionOut, markdownOut string, decision semantic.Decision) error {
|
||||
if err := semantic.WriteDecision(decisionOut, decision); err != nil {
|
||||
return fmt.Errorf("write decision: %w", err)
|
||||
}
|
||||
if err := semantic.WriteMarkdown(markdownOut, decision); err != nil {
|
||||
return fmt.Errorf("write markdown: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decisionExitCode(decision semantic.Decision) int {
|
||||
if decision.BlockMode && len(decision.Blockers) > 0 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func loadSemanticConfig(repo, waiversPath string) (semantic.Policy, semantic.Waivers, []report.Diagnostic, semantic.ModelConfig, error) {
|
||||
policy, err := semantic.LoadPolicy(repo)
|
||||
if err != nil {
|
||||
return semantic.Policy{}, semantic.Waivers{}, nil, semantic.ModelConfig{}, fmt.Errorf("load policy: %w", err)
|
||||
}
|
||||
var (
|
||||
waivers semantic.Waivers
|
||||
waiverDiags []report.Diagnostic
|
||||
)
|
||||
if waiversPath != "" {
|
||||
waivers, waiverDiags, err = semantic.LoadWaiversFile(waiversPath, now())
|
||||
} else {
|
||||
waivers, waiverDiags, err = semantic.LoadWaivers(repo, now())
|
||||
}
|
||||
if err != nil {
|
||||
return semantic.Policy{}, semantic.Waivers{}, nil, semantic.ModelConfig{}, fmt.Errorf("load waivers: %w", err)
|
||||
}
|
||||
modelConfig, err := semantic.LoadModelConfig(repo)
|
||||
if err != nil {
|
||||
return semantic.Policy{}, semantic.Waivers{}, nil, semantic.ModelConfig{}, fmt.Errorf("load model config: %w", err)
|
||||
}
|
||||
return policy, waivers, waiverDiags, modelConfig, nil
|
||||
}
|
||||
|
||||
var now = func() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func diagnosticSystemWarnings(diags []report.Diagnostic) []semantic.SystemWarning {
|
||||
if len(diags) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]semantic.SystemWarning, 0, len(diags))
|
||||
for _, diag := range diags {
|
||||
out = append(out, semantic.SystemWarning{
|
||||
Severity: "minor",
|
||||
Message: diag.Message,
|
||||
SuggestedAction: diag.Suggestion,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/qualitygate/facts"
|
||||
"github.com/larksuite/cli/internal/qualitygate/semantic"
|
||||
)
|
||||
|
||||
func TestRunLoadsPolicyAndWaivers(t *testing.T) {
|
||||
repo := t.TempDir()
|
||||
writeSemanticConfig(t, repo, `{
|
||||
"schema_version": 1,
|
||||
"default_enforcement": "observe",
|
||||
"block_categories": ["skill_quality"],
|
||||
"rollout_groups": [{
|
||||
"id": "changed-only",
|
||||
"enforcement": "blocking",
|
||||
"scope": {"changed_only": true},
|
||||
"categories": ["skill_quality"],
|
||||
"owner": "cli-owner",
|
||||
"reason": "test rollout"
|
||||
}]
|
||||
}`, `{
|
||||
"allowed": ["semantic-review-v1"],
|
||||
"allowed_base_urls": ["https://ark.ap-southeast.bytepluses.com/api/v3"]
|
||||
}`, "wiki-move\tskill_quality\tskill\tskills/lark-wiki/SKILL.md\t30\t\twiki-owner\tmigration\t2026-06-08\t2026-07-15\n")
|
||||
|
||||
factsPath := filepath.Join(t.TempDir(), "facts.json")
|
||||
f := facts.Facts{
|
||||
SchemaVersion: 1,
|
||||
Skills: []facts.SkillFact{{
|
||||
SourceFile: "skills/lark-wiki/SKILL.md",
|
||||
Line: 30,
|
||||
Changed: true,
|
||||
ReferencesInvalidCommand: true,
|
||||
}},
|
||||
}
|
||||
if err := f.WriteFile(factsPath); err != nil {
|
||||
t.Fatalf("write facts: %v", err)
|
||||
}
|
||||
reviewPath := filepath.Join(t.TempDir(), "review.json")
|
||||
if err := os.WriteFile(reviewPath, []byte(`{"verdict":"warn","findings":[{"category":"skill_quality","severity":"major","evidence":["facts.skills[0]"],"message":"bad","suggested_action":"fix"}]}`), 0o644); err != nil {
|
||||
t.Fatalf("write review: %v", err)
|
||||
}
|
||||
decisionPath := filepath.Join(t.TempDir(), "decision.json")
|
||||
code := run([]string{"--repo", repo, "--facts", factsPath, "--review-json", reviewPath, "--decision-out", decisionPath, "--block"})
|
||||
if code != 0 {
|
||||
t.Fatalf("run() = %d, want waived success", code)
|
||||
}
|
||||
decision := readDecision(t, decisionPath)
|
||||
if len(decision.Blockers) != 0 || len(decision.Warnings) != 1 || decision.Warnings[0].WaiverID != "wiki-move" {
|
||||
t.Fatalf("unexpected decision: %#v", decision)
|
||||
}
|
||||
if decision.Warnings[0].ReviewAction != semantic.ReviewActionConfirm {
|
||||
t.Fatalf("review action = %q, want %q", decision.Warnings[0].ReviewAction, semantic.ReviewActionConfirm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLoadsWaiversFromOverrideFile(t *testing.T) {
|
||||
repo := t.TempDir()
|
||||
writeSemanticConfig(t, repo, `{
|
||||
"schema_version": 1,
|
||||
"default_enforcement": "observe",
|
||||
"block_categories": ["error_hint"],
|
||||
"rollout_groups": [{
|
||||
"id": "changed-only",
|
||||
"enforcement": "blocking",
|
||||
"scope": {"changed_only": true},
|
||||
"categories": ["error_hint"],
|
||||
"owner": "cli-owner",
|
||||
"reason": "test rollout"
|
||||
}]
|
||||
}`, `{
|
||||
"allowed": ["semantic-review-v1"],
|
||||
"allowed_base_urls": ["https://ark.ap-southeast.bytepluses.com/api/v3"]
|
||||
}`, "")
|
||||
|
||||
factsPath := filepath.Join(t.TempDir(), "facts.json")
|
||||
f := facts.Facts{
|
||||
SchemaVersion: 1,
|
||||
Errors: []facts.ErrorFact{{
|
||||
File: "shortcuts/contact/contact_search_user.go",
|
||||
Line: 199,
|
||||
CommandPath: "contact +search-user",
|
||||
Changed: true,
|
||||
Boundary: true,
|
||||
RequiredHint: true,
|
||||
HintActionCount: 0,
|
||||
Code: "validation",
|
||||
}},
|
||||
}
|
||||
if err := f.WriteFile(factsPath); err != nil {
|
||||
t.Fatalf("write facts: %v", err)
|
||||
}
|
||||
reviewPath := filepath.Join(t.TempDir(), "review.json")
|
||||
if err := os.WriteFile(reviewPath, []byte(`{"verdict":"warn","findings":[{"category":"error_hint","severity":"major","evidence":["facts.errors[0]"],"message":"bad","suggested_action":"fix"}]}`), 0o644); err != nil {
|
||||
t.Fatalf("write review: %v", err)
|
||||
}
|
||||
waiversPath := filepath.Join(t.TempDir(), "waivers.txt")
|
||||
if err := os.WriteFile(waiversPath, []byte("semantic-error-hint-confirm\terror_hint\terror\tshortcuts/contact/contact_search_user.go\t199\t\tcli-owner\tsandbox confirm case\t2026-06-11\t2026-07-11\n"), 0o644); err != nil {
|
||||
t.Fatalf("write override waivers: %v", err)
|
||||
}
|
||||
decisionPath := filepath.Join(t.TempDir(), "decision.json")
|
||||
code := run([]string{"--repo", repo, "--facts", factsPath, "--review-json", reviewPath, "--waivers-file", waiversPath, "--decision-out", decisionPath, "--block"})
|
||||
if code != 0 {
|
||||
t.Fatalf("run() = %d, want waived success", code)
|
||||
}
|
||||
decision := readDecision(t, decisionPath)
|
||||
if len(decision.Blockers) != 0 || len(decision.Warnings) != 1 {
|
||||
t.Fatalf("unexpected decision: %#v", decision)
|
||||
}
|
||||
if decision.Warnings[0].ReviewAction != semantic.ReviewActionConfirm || decision.Warnings[0].WaiverID != "semantic-error-hint-confirm" {
|
||||
t.Fatalf("override waiver was not used: %#v", decision.Warnings[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCommentOnlyDowngradesPolicyBlockers(t *testing.T) {
|
||||
repo := t.TempDir()
|
||||
writeSemanticConfig(t, repo, `{
|
||||
"schema_version": 1,
|
||||
"default_enforcement": "observe",
|
||||
"block_categories": ["skill_quality"],
|
||||
"rollout_groups": [{
|
||||
"id": "changed-only",
|
||||
"enforcement": "blocking",
|
||||
"scope": {"changed_only": true},
|
||||
"categories": ["skill_quality"],
|
||||
"owner": "cli-owner",
|
||||
"reason": "test rollout"
|
||||
}]
|
||||
}`, `{
|
||||
"allowed": ["semantic-review-v1"],
|
||||
"allowed_base_urls": ["https://ark.ap-southeast.bytepluses.com/api/v3"]
|
||||
}`, "")
|
||||
|
||||
factsPath := filepath.Join(t.TempDir(), "facts.json")
|
||||
f := facts.Facts{
|
||||
SchemaVersion: 1,
|
||||
Skills: []facts.SkillFact{{
|
||||
SourceFile: "skills/lark-wiki/SKILL.md",
|
||||
Line: 30,
|
||||
Changed: true,
|
||||
ReferencesInvalidCommand: true,
|
||||
}},
|
||||
}
|
||||
if err := f.WriteFile(factsPath); err != nil {
|
||||
t.Fatalf("write facts: %v", err)
|
||||
}
|
||||
reviewPath := filepath.Join(t.TempDir(), "review.json")
|
||||
if err := os.WriteFile(reviewPath, []byte(`{"verdict":"warn","findings":[{"category":"skill_quality","severity":"major","evidence":["facts.skills[0]"],"message":"bad","suggested_action":"fix"}]}`), 0o644); err != nil {
|
||||
t.Fatalf("write review: %v", err)
|
||||
}
|
||||
decisionPath := filepath.Join(t.TempDir(), "decision.json")
|
||||
code := run([]string{"--repo", repo, "--facts", factsPath, "--review-json", reviewPath, "--decision-out", decisionPath})
|
||||
if code != 0 {
|
||||
t.Fatalf("run() = %d, want success", code)
|
||||
}
|
||||
decision := readDecision(t, decisionPath)
|
||||
if decision.BlockMode || len(decision.Blockers) != 0 || len(decision.Warnings) != 1 {
|
||||
t.Fatalf("comment-only should warn only: %#v", decision)
|
||||
}
|
||||
if decision.Warnings[0].ReviewAction != semantic.ReviewActionObserve {
|
||||
t.Fatalf("review action = %q, want %q", decision.Warnings[0].ReviewAction, semantic.ReviewActionObserve)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWritesInfrastructureFailureDecisionForMissingPolicy(t *testing.T) {
|
||||
repo := t.TempDir()
|
||||
writeSemanticConfig(t, repo, "", `{
|
||||
"allowed": ["semantic-review-v1"],
|
||||
"allowed_base_urls": ["https://ark.ap-southeast.bytepluses.com/api/v3"]
|
||||
}`, "")
|
||||
factsPath := filepath.Join(t.TempDir(), "facts.json")
|
||||
if err := (facts.Facts{SchemaVersion: 1}).WriteFile(factsPath); err != nil {
|
||||
t.Fatalf("write facts: %v", err)
|
||||
}
|
||||
reviewPath := filepath.Join(t.TempDir(), "review.json")
|
||||
if err := os.WriteFile(reviewPath, []byte(`{"verdict":"warn","findings":[]}`), 0o644); err != nil {
|
||||
t.Fatalf("write review: %v", err)
|
||||
}
|
||||
decisionPath := filepath.Join(t.TempDir(), "decision.json")
|
||||
code := run([]string{"--repo", repo, "--facts", factsPath, "--review-json", reviewPath, "--decision-out", decisionPath, "--block"})
|
||||
if code != 0 {
|
||||
t.Fatalf("run() = %d, want infrastructure handoff", code)
|
||||
}
|
||||
decision := readDecision(t, decisionPath)
|
||||
if !decision.InfrastructureFailure || !decision.Degraded || !decision.BlockMode {
|
||||
t.Fatalf("expected infrastructure blocking decision: %#v", decision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWritesSkippedDecisionForUnavailableReviewer(t *testing.T) {
|
||||
t.Setenv("ARK_API_KEY", "")
|
||||
t.Setenv("ARK_BASE_URL", "")
|
||||
t.Setenv("ARK_MODEL", "")
|
||||
|
||||
repo := t.TempDir()
|
||||
writeSemanticConfig(t, repo, `{
|
||||
"schema_version": 1,
|
||||
"default_enforcement": "observe",
|
||||
"block_categories": ["skill_quality"]
|
||||
}`, `{
|
||||
"allowed": ["semantic-review-v1"],
|
||||
"allowed_base_urls": ["https://ark.ap-southeast.bytepluses.com/api/v3"]
|
||||
}`, "")
|
||||
factsPath := filepath.Join(t.TempDir(), "facts.json")
|
||||
f := facts.Facts{
|
||||
SchemaVersion: 1,
|
||||
Skills: []facts.SkillFact{{
|
||||
SourceFile: "skills/lark-wiki/SKILL.md",
|
||||
Line: 30,
|
||||
Changed: true,
|
||||
ReferencesInvalidCommand: true,
|
||||
}},
|
||||
}
|
||||
if !semantic.BuildInputView(f).HasReviewableFacts() {
|
||||
t.Fatal("test setup must contain reviewable facts")
|
||||
}
|
||||
if err := f.WriteFile(factsPath); err != nil {
|
||||
t.Fatalf("write facts: %v", err)
|
||||
}
|
||||
decisionPath := filepath.Join(t.TempDir(), "decision.json")
|
||||
code := run([]string{"--repo", repo, "--facts", factsPath, "--decision-out", decisionPath, "--block"})
|
||||
if code != 0 {
|
||||
t.Fatalf("run() = %d, want skipped handoff", code)
|
||||
}
|
||||
decision := readDecision(t, decisionPath)
|
||||
if !decision.Skipped || decision.Degraded || decision.InfrastructureFailure || !decision.BlockMode {
|
||||
t.Fatalf("expected skipped non-infrastructure decision: %#v", decision)
|
||||
}
|
||||
if len(decision.SystemWarnings) != 1 || len(decision.Warnings) != 0 || len(decision.Blockers) != 0 {
|
||||
t.Fatalf("skipped decision should only carry system warnings: %#v", decision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunShortCircuitsEmptySemanticInputWithoutReviewer(t *testing.T) {
|
||||
t.Setenv("ARK_API_KEY", "")
|
||||
t.Setenv("ARK_BASE_URL", "")
|
||||
t.Setenv("ARK_MODEL", "")
|
||||
|
||||
repo := t.TempDir()
|
||||
writeSemanticConfig(t, repo, `{
|
||||
"schema_version": 1,
|
||||
"default_enforcement": "observe",
|
||||
"block_categories": ["skill_quality"]
|
||||
}`, `{
|
||||
"allowed": ["semantic-review-v1"],
|
||||
"allowed_base_urls": ["https://ark.ap-southeast.bytepluses.com/api/v3"]
|
||||
}`, "")
|
||||
factsPath := filepath.Join(t.TempDir(), "facts.json")
|
||||
f := facts.Facts{
|
||||
SchemaVersion: 1,
|
||||
Commands: []facts.CommandFact{{
|
||||
Path: "service command 1",
|
||||
Domain: "service",
|
||||
Changed: true,
|
||||
Source: "service",
|
||||
}},
|
||||
Outputs: []facts.OutputFact{{
|
||||
Command: "service command 1",
|
||||
Domain: "service",
|
||||
Changed: true,
|
||||
Source: "service",
|
||||
IsList: true,
|
||||
HasDefaultLimit: true,
|
||||
HasDecisionField: true,
|
||||
}},
|
||||
}
|
||||
if semantic.BuildInputView(f).HasReviewableFacts() {
|
||||
t.Fatal("test setup must not contain reviewable facts")
|
||||
}
|
||||
if err := f.WriteFile(factsPath); err != nil {
|
||||
t.Fatalf("write facts: %v", err)
|
||||
}
|
||||
decisionPath := filepath.Join(t.TempDir(), "decision.json")
|
||||
markdownPath := filepath.Join(t.TempDir(), "semantic.md")
|
||||
code := run([]string{"--repo", repo, "--facts", factsPath, "--decision-out", decisionPath, "--markdown-out", markdownPath, "--block"})
|
||||
if code != 0 {
|
||||
t.Fatalf("run() = %d, want clean pass", code)
|
||||
}
|
||||
decision := readDecision(t, decisionPath)
|
||||
if decision.Skipped || decision.Degraded || decision.InfrastructureFailure || !decision.BlockMode {
|
||||
t.Fatalf("expected non-degraded pass decision: %#v", decision)
|
||||
}
|
||||
if len(decision.SystemWarnings) != 0 || len(decision.Warnings) != 0 || len(decision.Blockers) != 0 {
|
||||
t.Fatalf("empty semantic view should not produce findings: %#v", decision)
|
||||
}
|
||||
data, err := os.ReadFile(markdownPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read markdown: %v", err)
|
||||
}
|
||||
markdown := string(data)
|
||||
if !strings.Contains(markdown, "No semantic blockers.") {
|
||||
t.Fatalf("markdown missing pass summary: %s", markdown)
|
||||
}
|
||||
if strings.Contains(strings.ToLower(markdown), "skipped") || strings.Contains(strings.ToLower(markdown), "degraded") {
|
||||
t.Fatalf("markdown should not report semantic review as skipped/degraded: %s", markdown)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWritesInfrastructureFailureDecisionForInvalidReviewerConfig(t *testing.T) {
|
||||
t.Setenv("ARK_API_KEY", "test-key")
|
||||
t.Setenv("ARK_BASE_URL", "")
|
||||
t.Setenv("ARK_MODEL", "not-allowed-model")
|
||||
|
||||
repo := t.TempDir()
|
||||
writeSemanticConfig(t, repo, `{
|
||||
"schema_version": 1,
|
||||
"default_enforcement": "observe",
|
||||
"block_categories": ["skill_quality"]
|
||||
}`, `{
|
||||
"allowed": ["semantic-review-v1"],
|
||||
"allowed_base_urls": ["https://ark.ap-southeast.bytepluses.com/api/v3"]
|
||||
}`, "")
|
||||
factsPath := filepath.Join(t.TempDir(), "facts.json")
|
||||
f := facts.Facts{
|
||||
SchemaVersion: 1,
|
||||
Skills: []facts.SkillFact{{
|
||||
SourceFile: "skills/lark-wiki/SKILL.md",
|
||||
Line: 30,
|
||||
Changed: true,
|
||||
ReferencesInvalidCommand: true,
|
||||
}},
|
||||
}
|
||||
if !semantic.BuildInputView(f).HasReviewableFacts() {
|
||||
t.Fatal("test setup must contain reviewable facts")
|
||||
}
|
||||
if err := f.WriteFile(factsPath); err != nil {
|
||||
t.Fatalf("write facts: %v", err)
|
||||
}
|
||||
decisionPath := filepath.Join(t.TempDir(), "decision.json")
|
||||
code := run([]string{"--repo", repo, "--facts", factsPath, "--decision-out", decisionPath, "--block"})
|
||||
if code != 0 {
|
||||
t.Fatalf("run() = %d, want infrastructure handoff", code)
|
||||
}
|
||||
decision := readDecision(t, decisionPath)
|
||||
if !decision.InfrastructureFailure || !decision.Degraded || decision.Skipped || !decision.BlockMode {
|
||||
t.Fatalf("expected infrastructure failure decision: %#v", decision)
|
||||
}
|
||||
}
|
||||
|
||||
func writeSemanticConfig(t *testing.T, repo, policy, models, waivers string) {
|
||||
t.Helper()
|
||||
dir := filepath.Join(repo, "internal", "qualitygate", "config", "semantic")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
if policy != "" {
|
||||
if err := os.WriteFile(filepath.Join(dir, "policy.json"), []byte(policy), 0o644); err != nil {
|
||||
t.Fatalf("write policy: %v", err)
|
||||
}
|
||||
}
|
||||
if models != "" {
|
||||
if err := os.WriteFile(filepath.Join(dir, "models.json"), []byte(models), 0o644); err != nil {
|
||||
t.Fatalf("write models: %v", err)
|
||||
}
|
||||
}
|
||||
if waivers != "" {
|
||||
if err := os.WriteFile(filepath.Join(dir, "waivers.txt"), []byte(waivers), 0o644); err != nil {
|
||||
t.Fatalf("write waivers: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readDecision(t *testing.T, path string) semantic.Decision {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read decision: %v", err)
|
||||
}
|
||||
var decision semantic.Decision
|
||||
if err := json.Unmarshal(data, &decision); err != nil {
|
||||
t.Fatalf("decode decision: %v", err)
|
||||
}
|
||||
return decision
|
||||
}
|
||||
Reference in New Issue
Block a user