Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e921f91
test: add targeted coverage tests for rule packages and shared utilities
claude Apr 25, 2026
1a0d349
test: add coverage tests for fix, metrics, lint, and rule packages
claude Apr 25, 2026
549aa88
test: add Category() tests and headingstyle explore tests
claude Apr 25, 2026
439da04
test: add Category() tests and branch coverage across rule packages
claude Apr 25, 2026
1215064
test: update headingstyle coverage tests
claude Apr 25, 2026
513fcde
test: add coverage tests for catalog, requiredstructure, metrics, cro…
claude Apr 25, 2026
81b04b7
chore: ignore local coverage run artifacts
claude Apr 25, 2026
379415f
test: add edge-case tests to reach 95% combined coverage (plan 85)
claude Apr 26, 2026
a6680a5
test: address Copilot review comments on PR #172
claude Apr 26, 2026
0a2b5b8
test: address second round of Copilot review comments on PR #172
claude Apr 26, 2026
db5c9cd
test: fix misleading test names and comments (PR #172 Copilot round 3)
claude Apr 26, 2026
70e9e8e
ci: install mdsmith merge driver in merge-queue workflow
claude Apr 26, 2026
7750e53
test: fix misleading names and comments (PR #172 Copilot round 4)
claude Apr 26, 2026
5f67156
fix(merge-queue): use go run to install merge driver
claude Apr 26, 2026
a1cb584
fix(merge-queue): build merge driver from trusted base ref
claude Apr 26, 2026
62b6bf3
fix(merge-queue): download pinned release binary to install merge driver
claude Apr 26, 2026
dbbb90d
test(requiredstructure): rename misleading test to match what it veri…
claude Apr 26, 2026
e46248c
docs(plan-85): clarify toInt acceptance criterion lists intentional e…
claude Apr 26, 2026
9841d74
test: fix inaccurate comments on two tests
claude Apr 26, 2026
6c143ac
fix(merge-driver): store absolute binary path in git config
claude Apr 26, 2026
372740c
fix: resolve Go 1.25 covdata regression and harden isTemporaryBinary
claude Apr 26, 2026
3a91345
ci: install mdsmith before tests so merge-driver install tests pass
claude Apr 26, 2026
533b65a
fix(merge-driver): shell-quote exe path, add coverage, fix test names
claude Apr 26, 2026
0787345
test(merge-driver): cover registerMergeDriver error path and isTempor…
claude Apr 26, 2026
e3e85b8
test(merge-driver): cover resolveInstalledBinary $GOPATH/bin fallback
claude Apr 26, 2026
287728a
fix(merge-driver): split multi-entry GOPATH for $GOPATH/bin lookup
claude Apr 26, 2026
3bd89ae
fix(merge-driver): narrow isTemporaryBinary to go-build/go-run dirs only
claude Apr 26, 2026
d1febe6
docs(merge-driver): update usage text to match actual install output
claude Apr 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ jobs:
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version-file: go.mod
- name: Install mdsmith
run: go install ./cmd/mdsmith
- name: Run tests with coverage
run: |
mkdir -p "$GITHUB_WORKSPACE/e2e-cover"
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/merge-queue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ jobs:
fetch-depth: 0
token: ${{ secrets.MERGE_QUEUE_TOKEN }}

- name: Install mdsmith merge driver
env:
MDSMITH_VERSION: v0.5.0
MDSMITH_SHA256: 87519781aa7b5ab147d5ab1d75d4e0a1c6213479110055972a21025b537ce171
run: |
curl -fsSL "https://github.com/jeduden/mdsmith/releases/download/${MDSMITH_VERSION}/mdsmith-linux-amd64" \
-o "$RUNNER_TEMP/mdsmith"
echo "${MDSMITH_SHA256} $RUNNER_TEMP/mdsmith" | sha256sum -c
chmod +x "$RUNNER_TEMP/mdsmith"
"$RUNNER_TEMP/mdsmith" merge-driver install
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
Comment thread
jeduden marked this conversation as resolved.

- uses: jeduden/merge-queue-action@3be8077b142e4057d2fc097635d1ab6ada2bbbf5 # v0.7.1
with:
token: ${{ secrets.MERGE_QUEUE_TOKEN }}
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ cover.out
/.tmp/
docs/research/conciseness/spikes/wasm-embedded-inference/classifier.wasm
internal/rules/concisenessscoring/wasmclassifier/classifier.wasm
e2e-cover/
unit.cov
merged.cov
2 changes: 1 addition & 1 deletion PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ footer: |
| 78 | ✅ | [Query subcommand for front-matter filtering](plan/78_query-command.md) |
| 83 | ✅ | [Security hardening batch](plan/83_security-hardening-batch.md) |
| 84 | ✅ | [Symlink default-deny for file discovery](plan/84_symlink-default-deny.md) |
| 85 | 🔳 | [Increase test coverage to 95% by extracting shared rule helpers](plan/85_coverage-to-95-percent.md) |
| 85 | | [Increase test coverage to 95% by extracting shared rule helpers](plan/85_coverage-to-95-percent.md) |
Comment thread
jeduden marked this conversation as resolved.
| 86 | ✅ | [Markdown flavor validation](plan/86_markdown-flavor-validation.md) |
| 89 | ✅ | [TOC generator directive and MDS035 auto-fix](plan/89_toc-generator-directive.md) |
| 90 | ✅ | [Isolate corpus test git config from host signing](plan/90_corpus-test-git-config-isolation.md) |
Expand Down
5 changes: 3 additions & 2 deletions cmd/mdsmith/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1227,7 +1227,8 @@ func TestE2E_MergeDriver_Install(t *testing.T) {
out, err := exec.Command("git", "-C", dir, "config", "merge.mdsmith.driver").Output()
require.NoError(t, err, "reading git config: %v", err)
driver := strings.TrimSpace(string(out))
assert.Contains(t, driver, "mdsmith merge-driver run", "expected merge driver config with 'run', got: %s", driver)
assert.Contains(t, driver, "merge-driver run %O %A %B %P",
"expected merge driver config with run+placeholders, got: %s", driver)

// Verify .gitattributes.
attrs, err := os.ReadFile(filepath.Join(dir, ".gitattributes"))
Expand Down Expand Up @@ -1268,7 +1269,7 @@ func TestE2E_MergeDriver_Install_CustomFiles(t *testing.T) {
out, err := exec.Command("git", "-C", dir, "config", "merge.mdsmith.driver").Output()
require.NoError(t, err, "reading git config: %v", err)
driver := strings.TrimSpace(string(out))
assert.Contains(t, driver, "mdsmith merge-driver run", "expected merge driver config, got: %s", driver)
assert.Contains(t, driver, "merge-driver run %O %A %B %P", "expected merge driver config, got: %s", driver)

// Verify .gitattributes has custom files, not defaults.
attrs, err := os.ReadFile(filepath.Join(dir, ".gitattributes"))
Expand Down
73 changes: 71 additions & 2 deletions cmd/mdsmith/mergedriver.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,15 @@ func runMergeDriverInstall(args []string) int {
}

// registerMergeDriver writes the merge.mdsmith.* keys to local
// git config.
// git config. It uses the absolute path of the current executable
// so the driver works regardless of whether the install directory
// is in PATH.
func registerMergeDriver() error {
driver := "mdsmith merge-driver run %O %A %B %P"
exe, err := resolveInstalledBinary()
if err != nil {
return fmt.Errorf("cannot locate mdsmith binary: %w", err)
}
driver := shellQuote(exe) + " merge-driver run %O %A %B %P"
cmds := [][]string{
Comment thread
jeduden marked this conversation as resolved.
Comment thread
jeduden marked this conversation as resolved.
{"git", "config", "merge.mdsmith.name",
"mdsmith section-aware Markdown merge"},
Expand All @@ -411,6 +417,69 @@ func registerMergeDriver() error {
return nil
}

// executableFunc is the function used to locate the current binary.
// Overridden in tests to exercise the non-temporary-exe branch.
var executableFunc = os.Executable

// resolveInstalledBinary returns the absolute path to the mdsmith
// binary to use as the git merge driver. It prefers the current
// executable when it lives outside the OS temp directory (i.e. it
// was installed via "go install" or a release download). When the
// current executable is a transient "go run" binary it falls back
// to searching PATH and then $GOPATH/bin.
func resolveInstalledBinary() (string, error) {
if exe, err := executableFunc(); err == nil {
if !isTemporaryBinary(exe) {
return filepath.Clean(exe), nil
}
}
// Transient go-run binary — try PATH first, then $GOPATH/bin.
if p, err := exec.LookPath("mdsmith"); err == nil {
if abs, err := filepath.Abs(p); err == nil {
return abs, nil
}
}
gopath, err := goEnvPath()
Comment thread
jeduden marked this conversation as resolved.
if err == nil {
candidate := filepath.Join(gopath, "bin", "mdsmith")
if p, err := exec.LookPath(candidate); err == nil {
return p, nil
Comment thread
jeduden marked this conversation as resolved.
Outdated
}
}
return "", fmt.Errorf(
"mdsmith not found in PATH or $GOPATH/bin; " +
"run \"go install ./cmd/mdsmith\" first",
)
}

// isTemporaryBinary reports whether path looks like a transient
// binary created by "go run" (lives under the OS temp directory).
func isTemporaryBinary(path string) bool {
tmp := filepath.Clean(os.TempDir())
path = filepath.Clean(path)
rel, err := filepath.Rel(tmp, path)
if err != nil {
return false
}
Comment thread
jeduden marked this conversation as resolved.
return rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)) && !filepath.IsAbs(rel)
}
Comment thread
jeduden marked this conversation as resolved.
Outdated

// shellQuote wraps s in single quotes, escaping any embedded single
// quotes, so that it is safe to embed in a POSIX shell command such as
// the git merge.*.driver value.
func shellQuote(s string) string {
return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
}

// goEnvPath returns the value of GOPATH by running "go env GOPATH".
func goEnvPath() (string, error) {
out, err := exec.Command("go", "env", "GOPATH").Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}

// ensureGitattributes reads .gitattributes, adds any missing
// merge driver entries for the given files, and writes it back.
func ensureGitattributes(path string, files []string) error {
Expand Down
148 changes: 148 additions & 0 deletions cmd/mdsmith/mergedriver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
Expand Down Expand Up @@ -296,3 +297,150 @@ func TestRunMergeDriverInstall_HelpFlag_ExitsZero(t *testing.T) {
assert.Equal(t, 0, code)
})
}

// --- resolveInstalledBinary ---

func TestResolveInstalledBinary_NonTemporaryExe(t *testing.T) {
// Override executableFunc to return a path that is NOT under os.TempDir()
// so isTemporaryBinary returns false. resolveInstalledBinary should use
// that path directly without falling through to the PATH/GOPATH lookup.
fakePermanent := "/usr/local/bin-test-fake/mdsmith"

orig := executableFunc
t.Cleanup(func() { executableFunc = orig })
executableFunc = func() (string, error) { return fakePermanent, nil }

got, err := resolveInstalledBinary()
require.NoError(t, err)
assert.Equal(t, fakePermanent, got)
}

func TestResolveInstalledBinary_FromPATH(t *testing.T) {
// Place a fake "mdsmith" binary in a directory added to PATH.
// resolveInstalledBinary should find it after the temp-binary fallback.
dir := t.TempDir()
fakeBin := filepath.Join(dir, "mdsmith")
require.NoError(t, os.WriteFile(fakeBin, []byte("#!/bin/sh\n"), 0o755))

// Point executableFunc at a temporary path so the exe-based path is skipped.
orig := executableFunc
t.Cleanup(func() { executableFunc = orig })
executableFunc = func() (string, error) {
return filepath.Join(os.TempDir(), "fake-go-run", "mdsmith"), nil
}

origPath := os.Getenv("PATH")
t.Setenv("PATH", dir+string(os.PathListSeparator)+origPath)

got, err := resolveInstalledBinary()
require.NoError(t, err)
assert.Equal(t, fakeBin, got)
}

func TestResolveInstalledBinary_FromGopathBin(t *testing.T) {
// When the current exe is a transient go-run binary and "mdsmith" is
// not in PATH, resolveInstalledBinary must fall back to $GOPATH/bin.
// Limit PATH to the directory containing "go" so goEnvPath succeeds
// but exec.LookPath("mdsmith") fails (no other dirs to search).
goBin, err := exec.LookPath("go")
require.NoError(t, err)

gopathDir := t.TempDir()
gopathBinDir := filepath.Join(gopathDir, "bin")
require.NoError(t, os.MkdirAll(gopathBinDir, 0o755))
fakeBin := filepath.Join(gopathBinDir, "mdsmith")
require.NoError(t, os.WriteFile(fakeBin, []byte("#!/bin/sh\n"), 0o755))

orig := executableFunc
t.Cleanup(func() { executableFunc = orig })
executableFunc = func() (string, error) {
return filepath.Join(os.TempDir(), "fake-go-run", "mdsmith"), nil
}

t.Setenv("PATH", filepath.Dir(goBin))
t.Setenv("GOPATH", gopathDir)

got, err := resolveInstalledBinary()
require.NoError(t, err)
assert.Equal(t, fakeBin, got)
}

func TestResolveInstalledBinary_NotFound(t *testing.T) {
// When the exe is temporary, mdsmith is not in PATH, and GOPATH/bin has
// no mdsmith, resolveInstalledBinary should return an error.
orig := executableFunc
t.Cleanup(func() { executableFunc = orig })
executableFunc = func() (string, error) {
return filepath.Join(os.TempDir(), "fake-go-run", "mdsmith"), nil
}

// Empty PATH so LookPath("mdsmith") fails and go env GOPATH also fails.
t.Setenv("PATH", "")

_, err := resolveInstalledBinary()
require.Error(t, err)
assert.Contains(t, err.Error(), "mdsmith not found")
}

// --- goEnvPath ---

func TestGoEnvPath_GoNotInPATH(t *testing.T) {
// When PATH is empty "go" cannot be found, so goEnvPath returns an error.
t.Setenv("PATH", "")
_, err := goEnvPath()
require.Error(t, err)
}

// --- isTemporaryBinary ---

func TestIsTemporaryBinary_NonTempPath(t *testing.T) {
// A path outside os.TempDir() should NOT be considered temporary.
// Use a path that is definitely not under /tmp.
assert.False(t, isTemporaryBinary("/usr/local/bin/mdsmith"))
}

func TestIsTemporaryBinary_TempPath(t *testing.T) {
// A path under os.TempDir() IS temporary.
tmp := os.TempDir()
assert.True(t, isTemporaryBinary(filepath.Join(tmp, "go-run-123", "exe", "main")))
}

func TestIsTemporaryBinary_RelativePath_RelErrorReturnsFalse(t *testing.T) {
// filepath.Rel returns an error when basepath is absolute (os.TempDir
// is always absolute) and targpath is relative — filepath.Clean does
// not promote a relative path to absolute. The function must treat
// that as "not temporary" rather than panicking or returning true.
assert.False(t, isTemporaryBinary("relative/path/mdsmith"))
}

// --- registerMergeDriver ---

func TestRegisterMergeDriver_BinaryNotFound_ReturnsError(t *testing.T) {
// When resolveInstalledBinary cannot locate a binary, registerMergeDriver
// must surface that error instead of writing a broken git config entry.
orig := executableFunc
t.Cleanup(func() { executableFunc = orig })
executableFunc = func() (string, error) {
return filepath.Join(os.TempDir(), "fake-go-run", "mdsmith"), nil
}
t.Setenv("PATH", "")

err := registerMergeDriver()
require.Error(t, err)
assert.Contains(t, err.Error(), "cannot locate mdsmith binary")
}

// --- shellQuote ---

func TestShellQuote_NoSpecialChars(t *testing.T) {
assert.Equal(t, "'/usr/local/bin/mdsmith'", shellQuote("/usr/local/bin/mdsmith"))
}

func TestShellQuote_ContainsSingleQuote(t *testing.T) {
// A single quote in the path must be escaped as '\''.
assert.Equal(t, "'/path/it'\\''s/mdsmith'", shellQuote("/path/it's/mdsmith"))
}

func TestShellQuote_PathWithSpaces(t *testing.T) {
assert.Equal(t, "'/home/my user/bin/mdsmith'", shellQuote("/home/my user/bin/mdsmith"))
}
18 changes: 18 additions & 0 deletions internal/archetype/gensection/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,3 +523,21 @@ func TestParseColumnConfig_DefaultWrap(t *testing.T) {
t.Errorf("expected default wrap 'truncate', got %q", cols["desc"].Wrap)
}
}

// =====================================================================
// Phase 5: additional branch coverage
// =====================================================================

// TestEngine_Fix_SkipsOnInvalidYAML exercises the generateContent
// `dir == nil || len(diags) > 0` branch in Fix.
// When the YAML body is invalid, generateContent returns (_, false) and Fix skips.
func TestEngine_Fix_SkipsOnInvalidYAML(t *testing.T) {
src := "<?mock\n: invalid : yaml ::: [\n?>\nold content\n<?/mock?>\n"
f := newTestFile(t, "test.md", src)
d := &mockDirective{content: "new content\n"}
e := NewEngine(d)
result := string(e.Fix(f))
// Fix should leave old content intact when YAML parsing fails.
assert.Contains(t, result, "old content", "expected old content preserved when YAML is invalid")
assert.NotContains(t, result, "new content")
}
Loading
Loading