Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bab5208
plan 84: mark in-progress
claude Apr 22, 2026
75715bd
plan 84: red — assert symlinks are skipped by default (--follow-symli…
claude Apr 22, 2026
74aa0af
plan 84: green — symlinks skipped by default across walks
claude Apr 22, 2026
b79dee0
plan 84: close — docs, plan AC, security note
claude Apr 22, 2026
9564b18
plan 84: address Copilot review — cover explicit-arg symlinks
claude Apr 22, 2026
4b925d0
plan 84: address second Copilot review
claude Apr 22, 2026
5066fcf
plan 84: address third Copilot review — clarify symlink-to-dir
claude Apr 22, 2026
3c9dd52
plan 84: address fourth Copilot review — .md-named dir symlinks
claude Apr 22, 2026
003f551
plan 84: address fifth Copilot review — symlinked ancestors
claude Apr 22, 2026
96bac7d
plan 84: address sixth Copilot review
claude Apr 22, 2026
879c4bc
plan 84: address seventh Copilot review — .git project root
claude Apr 22, 2026
fd686cc
plan 84: rebase fixups and round-8 portability skips
claude Apr 22, 2026
1d17e3b
plan 84: address ninth Copilot review — ..-relative bypass
claude Apr 22, 2026
8fe77d4
plan 84: remove --no-follow-symlinks CLI flag outright
claude Apr 22, 2026
1d31511
plan 84: tri-state --follow-symlinks + tighten test
claude Apr 23, 2026
fbc75a8
plan 84: require regular-file target + document tri-state flag
claude Apr 23, 2026
3d56f6d
plan 84: consistent broken-symlink skip + symlinked-dir in test
claude Apr 23, 2026
2751cf2
plan 84: address Copilot round 13 + codecov gap
claude Apr 23, 2026
fdd5451
plan 84: help text reflects tri-state --follow-symlinks
claude Apr 23, 2026
998a7e1
plan 84: reject non-regular entries + portable test
claude Apr 23, 2026
1767d24
plan 84: gate FIFO test on !windows + cover more branches
claude Apr 23, 2026
7c65ade
plan 84: probe both file and directory symlinks in skip helper
claude Apr 23, 2026
ef6ec17
plan 84: actually use precomputed cwd for relative paths
claude Apr 23, 2026
5ec698a
plan 84: reject paths with '..' after a named segment
claude Apr 23, 2026
09c8ada
plan 84: component-walk for symlink-ancestor detection
claude Apr 24, 2026
14c26f4
plan 84: cover new walker branches for codecov/patch
claude Apr 24, 2026
f6ae3cb
plan 84: Windows-safe component walk and boundary check
claude Apr 24, 2026
9bce83b
plan 84: cover more plan-84 branches for codecov/patch
claude Apr 24, 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: 1 addition & 1 deletion PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ footer: |
| 65 | 🔲 | [Spike WASM-Embedded Weasel Inference](plan/65_spike-wasm-embedded-inference.md) |
| 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) |
| 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) |
| 86 | 🔳 | [Markdown flavor validation](plan/86_markdown-flavor-validation.md) |
| 89 | 🔲 | [TOC generator directive and MDS035 auto-fix](plan/89_toc-generator-directive.md) |
Expand Down
89 changes: 15 additions & 74 deletions cmd/mdsmith/e2e_coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,55 +136,6 @@ func TestE2E_Fix_Discovered_BadConfig_ExitsTwo(t *testing.T) {
"expected error in stderr, got: %s", stderr)
}

// =============================================================
// 9. resolveOpts — --no-follow-symlinks flag
// =============================================================

func TestE2E_Check_NoFollowSymlinks(t *testing.T) {
dir := t.TempDir()
// Use a config with rules enabled and file discovery patterns.
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".git"), 0o755))
writeFixture(t, dir, ".mdsmith.yml",
"files:\n - \"**/*.md\"\nrules:\n no-trailing-spaces: true\n")

// Create a subdirectory with a dirty file, and a symlink to it.
subDir := filepath.Join(dir, "real")
require.NoError(t, os.MkdirAll(subDir, 0o755))
writeFixture(t, subDir, "dirty.md", "# Title\n\nHello \n")
require.NoError(t, os.Symlink(subDir, filepath.Join(dir, "linked")))

// Without --no-follow-symlinks, discovery finds dirty.md via symlink.
// Use --no-gitignore to avoid ancestor .gitignore interference.
_, _, exitWithout := runBinaryInDir(t, dir, "", "check", "--no-color", "--no-gitignore")
assert.Equal(t, 1, exitWithout,
"expected exit 1 without --no-follow-symlinks (dirty file found via discovery)")

// With --no-follow-symlinks, symlinked dir is skipped; only real/ found.
_, stderr, exitCode := runBinaryInDir(t, dir, "",
"check", "--no-color", "--no-gitignore", "--no-follow-symlinks")
// Should still find real/dirty.md (exit 1) but the flag exercises resolveOpts.
assert.Equal(t, 1, exitCode,
"expected exit 1 (real/dirty.md still found), got %d; stderr: %s", exitCode, stderr)
}

func TestE2E_Fix_NoFollowSymlinks(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".git"), 0o755))
writeFixture(t, dir, ".mdsmith.yml",
"files:\n - \"**/*.md\"\nrules:\n no-trailing-spaces: true\n")

subDir := filepath.Join(dir, "real")
require.NoError(t, os.MkdirAll(subDir, 0o755))
writeFixture(t, subDir, "fixme.md", "# Title\n\nHello \n")
require.NoError(t, os.Symlink(subDir, filepath.Join(dir, "linked")))

// --no-follow-symlinks exercises resolveOpts; real/fixme.md still found.
_, stderr, exitCode := runBinaryInDir(t, dir, "",
"fix", "--no-color", "--no-follow-symlinks")
assert.Equal(t, 0, exitCode,
"expected exit 0 after fix, got %d; stderr: %s", exitCode, stderr)
}

// =============================================================
// 10. rootDirFromConfig — empty cfgPath falls back to cwd
// =============================================================
Expand Down Expand Up @@ -486,31 +437,36 @@ func TestE2E_Query_Verbose_MixedResults(t *testing.T) {
}

// =============================================================
// 36. check with no-follow-symlinks via discovery
// 36. Legacy no-follow-symlinks config key emits a deprecation
// =============================================================

func TestE2E_Check_Discovered_NoFollowSymlinks(t *testing.T) {
// TestE2E_Check_LegacyNoFollowSymlinksConfig asserts that the old
// `no-follow-symlinks:` config key is still parsed (no hard error)
// and surfaces a deprecation warning. The new default-deny already
// achieves what the legacy key intended, so discovery results match
// what the user expects.
func TestE2E_Check_LegacyNoFollowSymlinksConfig(t *testing.T) {
skipIfSymlinkUnsupported(t)
dir := t.TempDir()
// Create config that enables no-follow-symlinks.
writeFixture(t, dir, ".mdsmith.yml", "no-follow-symlinks:\n - \"**\"\nrules:\n no-trailing-spaces: true\n")
writeFixture(t, dir, ".mdsmith.yml",
"no-follow-symlinks:\n - \"**\"\nrules:\n no-trailing-spaces: true\n")

// Create a real directory with a dirty file.
subDir := filepath.Join(dir, "real")
require.NoError(t, os.MkdirAll(subDir, 0o755))
writeFixture(t, subDir, "dirty.md", "# Title\n\nHello \n")

// Create a symlink to the subdirectory.
link := filepath.Join(dir, "linked")
require.NoError(t, os.Symlink(subDir, link))

Comment thread
jeduden marked this conversation as resolved.
// The real dir's file should be found, but the symlinked one should not.
_, stderr, exitCode := runBinaryInDir(t, dir, "", "check", "--no-color")
// We expect exit 1 (real/dirty.md found) but only once.
assert.Equal(t, 1, exitCode,
"expected exit 1 (real dirty.md found), got %d; stderr: %s", exitCode, stderr)
// Should not report the symlinked path.
assert.NotContains(t, stderr, "linked/",
"expected symlinked dir to be skipped, but found in stderr: %s", stderr)
"symlinked dir must be skipped under default-deny; stderr: %s", stderr)
assert.Contains(t, stderr, "no-follow-symlinks",
"expected deprecation warning, got stderr: %s", stderr)
assert.Contains(t, stderr, "deprecated",
"expected deprecation warning, got stderr: %s", stderr)
}

// =============================================================
Expand Down Expand Up @@ -685,21 +641,6 @@ func TestE2E_Init_HelpFlag(t *testing.T) {
"expected init usage, got: %s", stderr)
}

// =============================================================
// Additional: metrics rank with --no-follow-symlinks
// =============================================================

func TestE2E_MetricsRank_NoFollowSymlinks(t *testing.T) {
dir := t.TempDir()
writeFixture(t, dir, "a.md", "# Title\n\nSome content here.\n")

stdout, stderr, exitCode := runBinaryInDir(t, dir, "",
"metrics", "rank", "--by", "bytes", "--no-follow-symlinks", ".")
require.Equal(t, 0, exitCode,
"expected exit 0, got %d; stderr: %s", exitCode, stderr)
assert.Contains(t, stdout, "a.md", "expected a.md in output")
}

// =============================================================
// Additional: metrics rank with --no-gitignore
// =============================================================
Expand Down
Loading
Loading