regression_test.go GO 635 lines View on github.com → Search inside
1// SPDX-License-Identifier: MIT23package main45import (6	"bytes"7	"context"8	"os"9	"os/exec"10	"path/filepath"11	"strings"12	"testing"13	"time"1415	"github.com/spf13/pflag"16)1718// The tests in this file guard against regressions to pre-existing behaviour19// introduced by the config-dotfile work (spec/01-config-dotfile). They focus on20// functionality that existed before the feature and could have been silently21// changed by the registerFlags refactor, the shared @file tokenizer and the22// config discovery/merge pipeline - cases the feature's own tests do not cover.23//24// They run with noGlobalConfig (defined in config_test.go) so a SCC_CONFIG_PATH25// in the developer's environment cannot pollute the results.2627// TestRegressionExcludeDirPreservesDefaults exercises the phase-02 slice-default28// change on the ordinary no-config CLI path (the ~99% case). pflag replaces a29// slice's default on the first Set, so before this work `--exclude-dir vendor`30// dropped the built-in .git/.hg/.svn and scc descended into them. The post-parse31// union must keep the defaults as a non-removable safety net. .svn is the canary:32// nothing else skips it, so if preservation breaks it reappears in the output.33func TestRegressionExcludeDirPreservesDefaults(t *testing.T) {34	dir := t.TempDir()35	layout := map[string]string{".svn": "x.go", "vendor": "y.go", "keep": "z.go"}36	for sub, file := range layout {37		if err := os.Mkdir(filepath.Join(dir, sub), 0755); err != nil {38			t.Fatal(err)39		}40		if err := os.WriteFile(filepath.Join(dir, sub, file), []byte("package x\n"), 0644); err != nil {41			t.Fatal(err)42		}43	}4445	out, err := runSCCDir(t, dir, noGlobalConfig, "-f", "csv", "--by-file", "--exclude-dir", "vendor")46	if err != nil {47		t.Fatal(err)48	}49	if !strings.Contains(out, "z.go") {50		t.Errorf("keep/z.go should be counted, output:\n%s", out)51	}52	if strings.Contains(out, "y.go") {53		t.Errorf("vendor/y.go should be excluded by --exclude-dir, output:\n%s", out)54	}55	if strings.Contains(out, "x.go") {56		t.Errorf(".svn/x.go reappeared: a CLI --exclude-dir must not replace the built-in defaults (union, not replace), output:\n%s", out)57	}58}5960// TestRegressionExcludeFilePreservesDefaults is the --exclude-file counterpart of61// the above: adding one ignored filename on the CLI must not drop the built-in62// lockfile defaults. package-lock.json is the canary - before the fix pflag's63// replace-on-first-Set would have let it through.64func TestRegressionExcludeFilePreservesDefaults(t *testing.T) {65	dir := t.TempDir()66	files := map[string]string{67		"main.go":           "package main\n",68		"package-lock.json": "{\"a\":1}\n",69		"myignore.txt":      "hello\n",70	}71	for name, body := range files {72		if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0644); err != nil {73			t.Fatal(err)74		}75	}7677	out, err := runSCCDir(t, dir, noGlobalConfig, "-f", "csv", "--by-file", "--exclude-file", "myignore.txt")78	if err != nil {79		t.Fatal(err)80	}81	if !strings.Contains(out, "main.go") {82		t.Errorf("main.go should be counted, output:\n%s", out)83	}84	if strings.Contains(out, "myignore.txt") {85		t.Errorf("myignore.txt should be excluded by --exclude-file, output:\n%s", out)86	}87	if strings.Contains(out, "package-lock.json") {88		t.Errorf("package-lock.json reappeared: a CLI --exclude-file must not replace the built-in lockfile defaults, output:\n%s", out)89	}90}9192// TestRegressionGeneratedMarkersPreservesDefaults is the --generated-markers93// counterpart of the exclude-dir/exclude-file default-preservation tests above -94// the third (and easiest-to-miss) StringSlice flag with a non-empty built-in95// default. Supplying a custom marker on the CLI must not replace the built-in96// "do not edit" / "<auto-generated />" markers (pflag's replace-on-first-Set),97// so a file carrying only a default marker must still be flagged generated.98// examples/generated/test.h ("DO NOT EDIT") and test.cs ("<auto-generated />")99// are the canaries: both should remain "(gen)" despite the custom marker.100func TestRegressionGeneratedMarkersPreservesDefaults(t *testing.T) {101	t.Parallel()102	out, err := runSCC("-z", "--generated-markers", "zzz-not-a-real-marker", "--no-scc-ignore", "examples/generated/")103	if err != nil {104		t.Fatal(err)105	}106	if strings.Count(out, "(gen)") < 2 {107		t.Errorf("a custom --generated-markers must not drop the built-in default markers; both example files should still be flagged (gen), output:\n%s", out)108	}109}110111// TestRegressionConfigWithPositionalDir guards the merged argument ordering: a112// positional path must still be scanned when a project .sccconfig is present. Config113// tokens are prepended and the genuine CLI (including the path) comes last, so114// cobra must still receive the path as a positional. The .sccconfig sets --by-file, so115// the per-file row for the scanned path proves both that config applied and that116// the positional argument survived the prepend.117func TestRegressionConfigWithPositionalDir(t *testing.T) {118	dir := t.TempDir()119	if err := os.Mkdir(filepath.Join(dir, "code"), 0755); err != nil {120		t.Fatal(err)121	}122	if err := os.WriteFile(filepath.Join(dir, "code", "a.go"), []byte("package a\n"), 0644); err != nil {123		t.Fatal(err)124	}125	if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte("--by-file\n"), 0644); err != nil {126		t.Fatal(err)127	}128129	out, err := runSCCDir(t, dir, noGlobalConfig, "-f", "csv", "code")130	if err != nil {131		t.Fatal(err)132	}133	// a.go only appears in --by-file output, so its presence proves both the134	// config (--by-file) applied and the positional 'code' dir was scanned.135	if !strings.Contains(out, "a.go") {136		t.Errorf("positional 'code' dir should be scanned with a project .sccconfig present, output:\n%s", out)137	}138}139140// TestRegressionAtFileCommentsAndMultiToken confirms the shared tokenizer's new141// capabilities reach the existing @file syntax: multiple tokens per line, plus142// whole-line and inline '#' comments. Under the old whole-line splitter "-f csv"143// was a single unknown token and "main.go # x" an unreadable path, so this both144// proves the improvement and pins the new @file contract.145func TestRegressionAtFileCommentsAndMultiToken(t *testing.T) {146	dir := t.TempDir()147	if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {148		t.Fatal(err)149	}150	atFile := filepath.Join(dir, "flags.txt")151	content := "# count the project\n\n-f csv\n--by-file\nmain.go # the entrypoint\n"152	if err := os.WriteFile(atFile, []byte(content), 0644); err != nil {153		t.Fatal(err)154	}155156	// @file must be the sole argument; the sole-argument trigger is preserved.157	out, err := runSCCDir(t, dir, noGlobalConfig, "@"+atFile)158	if err != nil {159		t.Fatalf("@file errored: %v\n%s", err, out)160	}161	if strings.Contains(out, "could not be read") {162		t.Errorf("inline '#' comment was not stripped from the path line, output:\n%s", out)163	}164	if !strings.Contains(out, "main.go") {165		t.Errorf("@file multi-token (-f csv --by-file) + main.go path not honoured, output:\n%s", out)166	}167}168169// TestRegressionHelpShowsSliceDefaults guards the user-visible default display.170// The three slice flags are registered with an empty runtime default (to defuse171// pflag's replace-on-first-Set); their "(default ...)" text is restored via172// DefValue. Skipping that restoration would silently drop the signal of what scc173// excludes out of the box, so assert --help still advertises the defaults.174func TestRegressionHelpShowsSliceDefaults(t *testing.T) {175	out, err := runSCC("--help")176	if err != nil {177		t.Fatalf("--help should exit 0: %v\n%s", err, out)178	}179	wants := []string{180		"[.git,.hg,.svn]",181		"[package-lock.json,Cargo.lock,yarn.lock,pubspec.lock,Podfile.lock,pnpm-lock.yaml]",182		"[do not edit,<auto-generated />]",183	}184	for _, w := range wants {185		if !strings.Contains(out, w) {186			t.Errorf("--help should display slice default %q, output:\n%s", w, out)187		}188	}189}190191// TestRegressionCommentOnlyConfigAllowsWrite documents an edge of the security192// model: a .sccconfig that contributes zero tokens (only comments/blanks) must not193// engage the write-blocking config path. With nothing injected there is nothing194// to defend against, so the no-config fast path runs and a CLI -o still writes.195func TestRegressionCommentOnlyConfigAllowsWrite(t *testing.T) {196	dir := t.TempDir()197	if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {198		t.Fatal(err)199	}200	if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte("# just a comment\n\n"), 0644); err != nil {201		t.Fatal(err)202	}203204	target := filepath.Join(dir, "out.csv")205	if _, err := runSCCDir(t, dir, noGlobalConfig, "-f", "csv", "-o", target); err != nil {206		t.Fatal(err)207	}208	if info, statErr := os.Stat(target); statErr != nil || info.Size() == 0 {209		t.Errorf("a comment-only .sccconfig should not block a genuine CLI -o write")210	}211}212213// TestRegressionShorthandsPreserved guards the phase-02 registerFlags refactor.214// ~60 flag registrations were lifted out of main() into registerFlags; the215// existing exhaustive test only checks long names, so a shorthand silently216// dropped or rebound to the wrong flag during the move would slip through except217// for the handful of shorthands the integration tests happen to exercise. This218// pins the complete shorthand -> long-name map. The config-control flags219// (registered separately by registerConfigControlFlags) carry no shorthand: -r220// is deliberately unbound, reserved for a future find-root that relocates the221// scan directory like cs.222func TestRegressionShorthandsPreserved(t *testing.T) {223	t.Parallel()224	fs := pflag.NewFlagSet("test", pflag.ContinueOnError)225	var out, report, multi string226	registerFlags(fs, &flagBindings{output: &out, report: &report, formatMulti: &multi, inert: true})227	registerConfigControlFlags(fs)228229	want := map[string]string{230		"m": "character",231		"p": "percent",232		"u": "uloc",233		"a": "dryness",234		"f": "format",235		"i": "include-ext",236		"x": "exclude-ext",237		"n": "exclude-file",238		"l": "languages",239		"c": "no-complexity",240		"d": "no-duplicates",241		"z": "min-gen",242		"M": "not-match",243		"o": "output",244		"s": "sort",245		"t": "trace",246		"v": "verbose",247		"w": "wide",248	}249	for short, longName := range want {250		f := fs.ShorthandLookup(short)251		if f == nil {252			t.Errorf("shorthand -%s is not registered (expected --%s)", short, longName)253			continue254		}255		if f.Name != longName {256			t.Errorf("shorthand -%s bound to --%s, want --%s", short, f.Name, longName)257		}258	}259260	// Inverse guard: no shorthand exists that the map above does not account for,261	// so a stray/renamed shorthand introduced by the refactor is also caught.262	fs.VisitAll(func(f *pflag.Flag) {263		if f.Shorthand == "" {264			return265		}266		if _, ok := want[f.Shorthand]; !ok {267			t.Errorf("unexpected shorthand -%s on --%s (not in the locked map)", f.Shorthand, f.Name)268		}269	})270}271272// TestRegressionScalarDefaultsPreserved locks a representative sample of scalar273// flag defaults through the registerFlags refactor (the slice defaults are274// covered by TestRegisterFlagsSliceDefValue). It also pins the three write275// flags' empty defaults and, crucially, the --report NoOptDefVal: runReport276// compares ReportOut to that sentinel to tell a bare --report from --report=path,277// so a refactor that dropped it would silently break the bare-flag form.278func TestRegressionScalarDefaultsPreserved(t *testing.T) {279	t.Parallel()280	fs := pflag.NewFlagSet("test", pflag.ContinueOnError)281	var out, report, multi string282	registerFlags(fs, &flagBindings{output: &out, report: &report, formatMulti: &multi, inert: true})283284	wantDef := map[string]string{285		"format":              "tabular",286		"sort":                "files",287		"size-unit":           "si",288		"currency-symbol":     "$",289		"cocomo-project-type": "organic",290		"avg-wage":            "56286",291		"large-line-count":    "40000",292		"large-byte-count":    "1000000",293		"output":              "",294		"report":              "",295		"format-multi":        "",296	}297	for name, def := range wantDef {298		f := fs.Lookup(name)299		if f == nil {300			t.Errorf("--%s missing after refactor", name)301			continue302		}303		if f.DefValue != def {304			t.Errorf("--%s DefValue = %q, want %q", name, f.DefValue, def)305		}306	}307308	if f := fs.Lookup("report"); f == nil || f.NoOptDefVal != processorDefaultReportName() {309		got := "<nil flag>"310		if f != nil {311			got = f.NoOptDefVal312		}313		t.Errorf("--report NoOptDefVal = %q, want %q (bare --report relies on it)", got, processorDefaultReportName())314	}315}316317// processorDefaultReportName mirrors the sentinel used to wire --report's318// NoOptDefVal, kept as a tiny indirection so the test reads clearly.319func processorDefaultReportName() string {320	fs := pflag.NewFlagSet("probe", pflag.ContinueOnError)321	var out, report, multi string322	registerFlags(fs, &flagBindings{output: &out, report: &report, formatMulti: &multi, inert: true})323	return fs.Lookup("report").NoOptDefVal324}325326// TestRegressionGenuineReportWritesWithConfig fills the spec-04 checklist gap:327// "Genuine CLI ... --report ... -> do write" is only exercised for -o and328// --format-multi. With a project .sccconfig present the two-mode write split engages329// (write flags bound to discards in the merged parse, resolved from the genuine330// CLI alone), so this proves the --report arm of resolveWriteFlags still sources331// the real ReportOut from the command line. --report=path overwrites silently,332// avoiding the bare-flag interactive prompt.333func TestRegressionGenuineReportWritesWithConfig(t *testing.T) {334	dir := writeSccConfig(t, "--no-cocomo\n")335	target := filepath.Join(dir, "report.html")336	out, err := runSCCDir(t, dir, noGlobalConfig, "--report="+target)337	if err != nil {338		t.Fatalf("scc errored: %v\n%s", err, out)339	}340	if info, statErr := os.Stat(target); statErr != nil || info.Size() == 0 {341		t.Errorf("genuine CLI --report=path should write the report even with config present, output:\n%s", out)342	}343	// The genuine CLI set --report, so the "ignoring --report from config" notice344	// must NOT fire.345	if strings.Contains(out, "ignoring --report from config") {346		t.Errorf("genuine CLI --report was wrongly treated as config-sourced, output:\n%s", out)347	}348}349350// TestRegressionAtFileWritesWithProjectConfigPresent locks the spec-§6 @file ⨉351// config interaction. Running `scc @file` rewrites os.Args before discovery, so352// a ./.sccconfig in the directory is still discovered and layered beneath the @file353// tokens - and because the genuine-CLI slice is captured AFTER @file expansion,354// the @file is allowed to write even though config is present (config is not).355// Here the project .sccconfig supplies --format csv (proving config layered into the356// merged parse) while the @file supplies -o target (proving @file kept its write357// capability): a single CSV-content assertion on the written file proves both.358func TestRegressionAtFileWritesWithProjectConfigPresent(t *testing.T) {359	dir := writeSccConfig(t, "--format csv\n")360	target := filepath.Join(dir, "out.txt")361	atFile := filepath.Join(dir, "flags.txt")362	if err := os.WriteFile(atFile, []byte("-o "+target+"\n"), 0644); err != nil {363		t.Fatal(err)364	}365366	out, err := runSCCDir(t, dir, noGlobalConfig, "@"+atFile)367	if err != nil {368		t.Fatalf("scc errored: %v\n%s", err, out)369	}370	info, statErr := os.Stat(target)371	if statErr != nil || info.Size() == 0 {372		t.Fatalf("@file -o should write even with a project .sccconfig present, stdout:\n%s", out)373	}374	body, err := os.ReadFile(target)375	if err != nil {376		t.Fatal(err)377	}378	if !strings.Contains(string(body), "Language,Lines,Code") {379		t.Errorf("project .sccconfig --format csv should have layered into the @file run, file contents:\n%s", body)380	}381}382383// TestRegressionMcpSkipsConfigDiscovery guards the run-order guarantee that the384// --mcp short-circuit fires before any config discovery (spec §3.3 step 0 / the385// "--mcp does not load config" checklist item). If discovery ran first, an386// unreadable explicit SCC_CONFIG_PATH global would exit non-zero with a "could387// not read config" error before the server ever started. With the correct388// ordering --mcp wins, the server starts, reads the empty stdin, hits EOF and389// exits - never touching config. Empty stdin guarantees a prompt EOF so the390// process cannot hang; a context deadline is a belt-and-suspenders backstop.391func TestRegressionMcpSkipsConfigDiscovery(t *testing.T) {392	bin, err := filepath.Abs(sccBinPath)393	if err != nil {394		t.Fatal(err)395	}396	missing := filepath.Join(t.TempDir(), "does-not-exist.sccconfig")397398	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)399	defer cancel()400401	cmd := exec.CommandContext(ctx, bin, sccTestFlag, "--mcp")402	cmd.Env = append(os.Environ(), SccConfigEnv+"="+missing)403	cmd.Stdin = bytes.NewReader(nil) // immediate EOF -> server exits cleanly404	out, _ := cmd.CombinedOutput()405406	if ctx.Err() == context.DeadlineExceeded {407		t.Fatalf("scc --mcp did not exit on EOF, output:\n%s", out)408	}409	if strings.Contains(string(out), "could not read config") {410		t.Errorf("--mcp must short-circuit before config discovery; config was read, output:\n%s", out)411	}412}413414// TestRegressionVersionFlagWithConfig guards cobra's built-in meta flags through415// the new arg pipeline. main() no longer hands the genuine argv to cobra; it416// builds a merged list and passes it via rootCmd.SetArgs(merged[1:]). A bug in417// that wiring (e.g. an off-by-one slice, or config tokens shifting the version418// flag) could break --version even though it is not a real scc flag. The output419// must be byte-identical with and without a project .sccconfig present, and a .sccconfig420// must never leak a config error onto the version path.421func TestRegressionVersionFlagWithConfig(t *testing.T) {422	bare := t.TempDir()423	noCfg, err := runSCCDir(t, bare, noGlobalConfig, "--version")424	if err != nil {425		t.Fatalf("scc --version should exit 0 with no config: %v\n%s", err, noCfg)426	}427	if !strings.Contains(noCfg, "scc version") {428		t.Fatalf("--version output missing version banner, output:\n%s", noCfg)429	}430431	dir := writeSccConfig(t, "--format csv\n--no-cocomo\n")432	withCfg, err := runSCCDir(t, dir, noGlobalConfig, "--version")433	if err != nil {434		t.Fatalf("scc --version should exit 0 with a project .sccconfig present: %v\n%s", err, withCfg)435	}436	if withCfg != noCfg {437		t.Errorf("--version output changed when a project .sccconfig was present\nno config:\n%s\nwith config:\n%s", noCfg, withCfg)438	}439}440441// TestRegressionSccDirectoryDoesNotBreakRun guards the no-panics / robustness442// policy against a real-world surprise the feature introduces: a path named443// ".sccconfig" that is a *directory* (another tool's data dir, an accidental mkdir).444// Project discovery does os.Stat("./.sccconfig"), which succeeds for a directory, then445// os.ReadFile fails with "is a directory". That is the non-explicit project arm,446// so it must degrade to a stderr warning and let the run finish (exit 0 with447// real output) - never abort, panic, or swallow the scan.448func TestRegressionSccDirectoryDoesNotBreakRun(t *testing.T) {449	dir := t.TempDir()450	if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {451		t.Fatal(err)452	}453	if err := os.Mkdir(filepath.Join(dir, ".sccconfig"), 0755); err != nil {454		t.Fatal(err)455	}456457	out, err := runSCCDir(t, dir, noGlobalConfig, "-f", "csv", "--by-file")458	if err != nil {459		t.Fatalf("a .sccconfig *directory* must not make scc exit non-zero: %v\n%s", err, out)460	}461	if !strings.Contains(out, "main.go") {462		t.Errorf("scc should still scan and emit output when ./.sccconfig is a directory, output:\n%s", out)463	}464}465466// TestRegressionConfigGlobalProjectPrecedence locks the middle rung of the467// precedence ladder (global < project) for a scalar flag. The existing suite468// proves CLI beats config and that each source loads, but not that the project469// .sccconfig overrides the SCC_CONFIG_PATH global when the two disagree - the ordering470// that falls out of prepending global tokens ahead of project tokens. Asserted471// in both directions so a swapped prepend order cannot pass.472func TestRegressionConfigGlobalProjectPrecedence(t *testing.T) {473	check := func(globalContent, projectContent string, wantCSV bool) {474		t.Helper()475		dir := t.TempDir()476		if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {477			t.Fatal(err)478		}479		global := filepath.Join(dir, "global.sccconfig")480		if err := os.WriteFile(global, []byte(globalContent), 0644); err != nil {481			t.Fatal(err)482		}483		if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte(projectContent), 0644); err != nil {484			t.Fatal(err)485		}486		out, err := runSCCDir(t, dir, []string{SccConfigEnv + "=" + global})487		if err != nil {488			t.Fatal(err)489		}490		isCSV := strings.Contains(out, "Language,Lines,Code")491		if isCSV != wantCSV {492			t.Errorf("global=%q project=%q: wantCSV=%v gotCSV=%v, output:\n%s", globalContent, projectContent, wantCSV, isCSV, out)493		}494	}495	// project (json) overrides global (csv) -> not CSV496	check("--format csv\n", "--format json\n", false)497	// project (csv) overrides global (json) -> CSV498	check("--format json\n", "--format csv\n", true)499}500501// TestRegressionConfigGlobalProjectSliceUnion extends the §7 union semantics to502// the global+project pair. TestConfigSliceUnion covers project ∪ CLI ∪ defaults;503// this proves a slice flag set in the SCC_CONFIG_PATH global unions with the same504// flag set in the project .sccconfig (rather than one source replacing the other),505// which is the natural pflag append behaviour that the empty-default mechanism506// must preserve across both config sources.507func TestRegressionConfigGlobalProjectSliceUnion(t *testing.T) {508	dir := t.TempDir()509	for _, d := range []string{"ga", "pb", "keep"} {510		if err := os.Mkdir(filepath.Join(dir, d), 0755); err != nil {511			t.Fatal(err)512		}513		if err := os.WriteFile(filepath.Join(dir, d, "f.go"), []byte("package x\n"), 0644); err != nil {514			t.Fatal(err)515		}516	}517	global := filepath.Join(dir, "global.sccconfig")518	if err := os.WriteFile(global, []byte("--exclude-dir ga\n"), 0644); err != nil {519		t.Fatal(err)520	}521	if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte("--exclude-dir pb\n"), 0644); err != nil {522		t.Fatal(err)523	}524525	out, err := runSCCDir(t, dir, []string{SccConfigEnv + "=" + global}, "-f", "csv", "--by-file")526	if err != nil {527		t.Fatal(err)528	}529	if !strings.Contains(out, "keep/f.go") {530		t.Errorf("keep/f.go should be counted, output:\n%s", out)531	}532	// Both the global-excluded (ga) and project-excluded (pb) dirs must be gone:533	// a union, not one source clobbering the other. Key on the path column so the534	// shared basename (f.go) doesn't mask a leak.535	if strings.Contains(out, "ga/f.go") || strings.Contains(out, "pb/f.go") {536		t.Errorf("global+project --exclude-dir should union (ga and pb both excluded), output:\n%s", out)537	}538}539540// TestRegressionConfigFormatMultiIgnoredForStdout hardens the §5.3 guarantee541// that --format-multi is ignored *entirely* when it comes from config, not just542// blocked from writing files. TestConfigNeverWritesFile only proves no file543// appears; this proves config cannot even change the on-screen format via a544// stdout-only --format-multi. The merged parse binds --format-multi to a discard,545// so processor.FormatMulti stays empty and output keeps the default tabular form.546func TestRegressionConfigFormatMultiIgnoredForStdout(t *testing.T) {547	dir := writeSccConfig(t, "--format-multi json:stdout\n")548	out, err := runSCCDir(t, dir, noGlobalConfig)549	if err != nil {550		t.Fatalf("scc errored: %v\n%s", err, out)551	}552	// JSON would start with '[{'; tabular has the box-drawing header rule.553	if strings.Contains(out, "[{\"Name\"") {554		t.Errorf("config --format-multi must be ignored; output switched to JSON, output:\n%s", out)555	}556	if !strings.Contains(out, "Language") {557		t.Errorf("expected the default tabular output, got:\n%s", out)558	}559}560561// TestRegressionConfigMinFlagClassifies fills a behavioural gap the existing562// suite leaves open. TestConfigCoupledMinFlags only asserts the run does not563// error; nothing proves a coupled min/gen flag (-z/--min/--gen, registered as564// BoolFuncs whose closures mutate processor state during parse) actually reaches565// Process when it comes from config. These are the flags spec-04 repeatedly566// flags as fragile under the two-mode write split. With a project .sccconfig present567// the split engages: the merged parse fires the -z closure (real binding), then568// resolveWriteFlags re-parses the genuine CLI with the closures made inert so it569// cannot undo that. A long single line trips the minified heuristic, so the file570// must surface as "(min)" - proving config's coupled flag survived end to end.571func TestRegressionConfigMinFlagClassifies(t *testing.T) {572	dir := t.TempDir()573	// One ~415-byte line: avg bytes/line well over the 255 default, so -z flags574	// it minified.575	long := "var x = '" + strings.Repeat("a", 400) + "';\n"576	if err := os.WriteFile(filepath.Join(dir, "app.js"), []byte(long), 0644); err != nil {577		t.Fatal(err)578	}579	// Everything needed is in the config; the genuine CLI is empty, so the580	// effect is purely config-sourced. -f csv keeps the "(min)" suffix off the581	// truncating tabular language column.582	if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte("-z\n-i js\n--no-scc-ignore\n-f csv\n"), 0644); err != nil {583		t.Fatal(err)584	}585586	out, err := runSCCDir(t, dir, noGlobalConfig)587	if err != nil {588		t.Fatalf("scc errored: %v\n%s", err, out)589	}590	if !strings.Contains(out, "(min)") {591		t.Errorf("config-supplied -z should flag the minified file as (min) through the two-mode pipeline, output:\n%s", out)592	}593}594595// TestRegressionConfigCannotStartMcpServer guards a security invariant that596// parallels the "config can never write a file" tests: a --mcp inside a project597// .sccconfig must NOT hijack stdio and start an MCP server. The interception reads598// os.Args ONLY (before config is discovered), so config's --mcp lands in the599// merged parse as the inert dummy flag registerFlags registers and the normal600// scan runs instead. This pins that ordering so a future refactor that moves the601// --mcp detection after the config merge - letting a checked-out .sccconfig silently602// turn a scan into a server - is caught. Empty stdin means even the bad case603// EOFs rather than hanging; the context deadline is a backstop.604func TestRegressionConfigCannotStartMcpServer(t *testing.T) {605	dir := t.TempDir()606	if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {607		t.Fatal(err)608	}609	if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte("--mcp\n"), 0644); err != nil {610		t.Fatal(err)611	}612613	bin, err := filepath.Abs(sccBinPath)614	if err != nil {615		t.Fatal(err)616	}617	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)618	defer cancel()619620	cmd := exec.CommandContext(ctx, bin, sccTestFlag, "-f", "csv", "--by-file")621	cmd.Dir = dir622	cmd.Env = append(os.Environ(), noGlobalConfig...)623	cmd.Stdin = bytes.NewReader(nil) // a started server would EOF, not hang624	out, _ := cmd.CombinedOutput()625626	if ctx.Err() == context.DeadlineExceeded {627		t.Fatalf("config --mcp appears to have started a server (timed out), output:\n%s", out)628	}629	// The by-file scan ran: the source file appears in the CSV. An MCP server630	// would speak JSON-RPC and never emit this.631	if !strings.Contains(string(out), "main.go") {632		t.Errorf("config --mcp must be an inert dummy flag and let the scan run, output:\n%s", string(out))633	}634}

Code quality findings 37

Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for sub, file := range layout {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.Mkdir(filepath.Join(dir, sub), 0755); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, sub, file), []byte("package x\n"), 0644); err != nil {
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
if err := os.WriteFile(filepath.Join(dir, sub, file), []byte("package x\n"), 0644); err != nil {
Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for name, body := range files {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0644); err != nil {
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.Mkdir(filepath.Join(dir, "code"), 0755); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "code", "a.go"), []byte("package a\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte("--by-file\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
atFile := filepath.Join(dir, "flags.txt")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte("# just a comment\n\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
target := filepath.Join(dir, "out.csv")
Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for short, longName := range want {
Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for name, def := range wantDef {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
target := filepath.Join(dir, "report.html")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
target := filepath.Join(dir, "out.txt")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
atFile := filepath.Join(dir, "flags.txt")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
missing := filepath.Join(t.TempDir(), "does-not-exist.sccconfig")
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
cmd.Env = append(os.Environ(), SccConfigEnv+"="+missing)
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.Mkdir(filepath.Join(dir, ".sccconfig"), 0755); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
global := filepath.Join(dir, "global.sccconfig")
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte(projectContent), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.Mkdir(filepath.Join(dir, d), 0755); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, d, "f.go"), []byte("package x\n"), 0644); err != nil {
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
if err := os.WriteFile(filepath.Join(dir, d, "f.go"), []byte("package x\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
global := filepath.Join(dir, "global.sccconfig")
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
if err := os.WriteFile(global, []byte("--exclude-dir ga\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte("--exclude-dir pb\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "app.js"), []byte(long), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte("-z\n-i js\n--no-scc-ignore\n-f csv\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n"), 0644); err != nil {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
if err := os.WriteFile(filepath.Join(dir, ".sccconfig"), []byte("--mcp\n"), 0644); err != nil {

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.