2026-10-07 23:17:10 +00:00
package validation
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
func TestCheckMarkdownLinksResolvesCommittedDestinationsAndFragments ( t * testing . T ) {
guide := [ ] byte ( "" +
"# Start\n" +
"[target](target.md?view=1#TARGET)\n" +
"[same](#START)\n" +
2026-10-08 01:31:18 +00:00
"[url heading](autolinks.md#httpsexampleinvalid)\n" +
"[email heading](autolinks.md#maintainerexampleinvalid)\n" +
2026-10-07 23:17:10 +00:00
"[top](#)\n" +
"[directory](assets/?view=1)\n" +
"[binary](image.bin#not-an-anchor)\n" +
"[encoded](docs/a%20b.md#CAF%C3%89)\n" +
"[query](target.md?view=%ZZ)\n" +
"[external](https://example.invalid/%ZZ#also%ZZ)\n" +
"[protocol-relative](//example.invalid/file.md)\n" )
repository , tree := committedMarkdownTree ( t , map [ string ] [ ] byte {
"guide.md" : guide ,
"target.md" : [ ] byte ( "# Target\n" ) ,
2026-10-08 01:31:18 +00:00
"autolinks.md" : [ ] byte ( "# <https://example.invalid>\n# <maintainer@example.invalid>\n" ) ,
2026-10-07 23:17:10 +00:00
"docs/a b.md" : [ ] byte ( "# Café\n" ) ,
"assets/file.md" : [ ] byte ( "# Asset\n" ) ,
"nested/guide.md" : [ ] byte ( "# Nested\n[same](#NESTED)\n[query](?view=1#nested)\n" ) ,
"image.bin" : [ ] byte { 0 , 1 , 2 } ,
} )
if err := checkMarkdownLinks ( context . Background ( ) , tree , false , nil ) ; err != nil {
t . Fatalf ( "checkMarkdownLinks() error = %v" , err )
}
if got := readCommitted ( t , tree , "guide.md" ) ; ! bytes . Equal ( got , guide ) {
t . Fatal ( "checker changed committed Markdown bytes" )
}
if got , err := os . ReadFile ( filepath . Join ( repository , "guide.md" ) ) ; err != nil || ! bytes . Equal ( got , guide ) {
t . Fatalf ( "worktree Markdown changed: got %q, err %v" , got , err )
}
}
2026-10-08 01:32:29 +00:00
func TestCheckMarkdownLinksTreatsImageLabelsAsAlternativeText ( t * testing . T ) {
_ , tree := committedMarkdownTree ( t , map [ string ] [ ] byte {
"guide.md" : [ ] byte ( "" +
"](image.png)\n" +
"](image.png)\n" +
"](image.png)\n" +
"](image.png)\n" +
"[](target.md#present)\n" +
"[](absent.md)\n" ) ,
"image.png" : [ ] byte ( "image" ) ,
"target.md" : [ ] byte ( "# Present\n" ) ,
} )
for _ , enforceBoundary := range [ ] bool { false , true } {
t . Run ( fmt . Sprintf ( "boundary=%t" , enforceBoundary ) , func ( t * testing . T ) {
err := checkMarkdownLinks ( context . Background ( ) , tree , enforceBoundary , nil )
var failure * Failure
if ! errors . As ( err , & failure ) || failure . Check != MarkdownLinks || len ( failure . Findings ) != 2 {
t . Fatalf ( "error = %v, want only the missing linked-image source and link target" , err )
}
want := map [ string ] bool { "absent.png" : true , "absent.md" : true }
for _ , finding := range failure . Findings {
if ! want [ finding . Target ] {
t . Errorf ( "unexpected destination finding: %#v" , finding )
}
delete ( want , finding . Target )
}
if len ( want ) != 0 {
t . Errorf ( "missing findings for real destinations: %v" , want )
}
} )
}
}
2026-10-07 23:17:10 +00:00
func TestCheckMarkdownLinksReportsMissingPathsAnchorsAndCycles ( t * testing . T ) {
_ , tree := committedMarkdownTree ( t , map [ string ] [ ] byte {
"guide.md" : [ ] byte ( "" +
"[missing](absent.md)\n" +
"[anchor](target.md#missing)\n" +
"[cycle](cycle-a)\n" ) ,
"target.md" : [ ] byte ( "# Present\n" ) ,
} )
repository := tree . repositoryDir
if err := os . Symlink ( "cycle-b" , filepath . Join ( repository , "cycle-a" ) ) ; err != nil {
t . Fatal ( err )
}
if err := os . Symlink ( "cycle-a" , filepath . Join ( repository , "cycle-b" ) ) ; err != nil {
t . Fatal ( err )
}
gitTree ( t , repository , "add" , "cycle-a" , "cycle-b" )
gitTree ( t , repository , "commit" , "-m" , "add markdown link cycle" )
tree = candidateTreeAtHead ( t , repository )
err := checkMarkdownLinks ( context . Background ( ) , tree , false , nil )
var failure * Failure
if ! errors . As ( err , & failure ) {
t . Fatalf ( "checkMarkdownLinks() error = %v, want validation Failure" , err )
}
if failure . Check != MarkdownLinks {
t . Fatalf ( "failure check = %q, want %q" , failure . Check , MarkdownLinks )
}
if len ( failure . Findings ) != 3 {
t . Fatalf ( "findings = %#v, want missing path, anchor, and cycle" , failure . Findings )
}
want := map [ string ] string {
"absent.md" : "destination does not exist" ,
"target.md#missing" : "heading anchor does not exist" ,
"cycle-a" : "destination contains a symlink cycle" ,
}
for _ , finding := range failure . Findings {
if finding . Path != "guide.md" || finding . Line < 1 || finding . Column < 1 {
t . Errorf ( "finding lacks source location: %#v" , finding )
}
if want [ finding . Target ] != finding . Message {
t . Errorf ( "finding = %#v" , finding )
}
delete ( want , finding . Target )
}
if len ( want ) != 0 {
t . Fatalf ( "missing expected findings for %#v" , want )
}
}
func TestCheckMarkdownLinksUsesResolvedMarkdownIdentity ( t * testing . T ) {
repository , _ := committedMarkdownTree ( t , map [ string ] [ ] byte {
"guide.md" : [ ] byte ( "[valid](alias.md#present)\n[missing](alias.md#missing)\n" ) ,
"target.md" : [ ] byte ( "# Present\n" ) ,
} )
if err := os . Symlink ( "target.md" , filepath . Join ( repository , "alias.md" ) ) ; err != nil {
t . Fatal ( err )
}
gitTree ( t , repository , "add" , "alias.md" )
gitTree ( t , repository , "commit" , "-m" , "add Markdown alias" )
tree := candidateTreeAtHead ( t , repository )
err := checkMarkdownLinks ( context . Background ( ) , tree , false , nil )
var failure * Failure
if ! errors . As ( err , & failure ) || failure . Check != MarkdownLinks || len ( failure . Findings ) != 1 {
t . Fatalf ( "resolved alias error = %v, want one missing-anchor finding" , err )
}
if finding := failure . Findings [ 0 ] ; finding . Path != "guide.md" || finding . Target != "alias.md#missing" || finding . Message != "heading anchor does not exist" {
t . Fatalf ( "resolved alias finding = %#v" , finding )
}
}
2026-10-09 01:37:33 +00:00
func TestCheckMarkdownLinksUsesCurrentPathForSharedDocuments ( t * testing . T ) {
shared := [ ] byte ( "[target](target.md)\n" )
_ , tree := committedMarkdownTree ( t , map [ string ] [ ] byte {
"one/guide.md" : shared ,
"one/target.md" : [ ] byte ( "# Target\n" ) ,
"two/guide.md" : shared ,
} )
err := checkMarkdownLinks ( context . Background ( ) , tree , false , nil )
var failure * Failure
if ! errors . As ( err , & failure ) || failure . Check != MarkdownLinks || len ( failure . Findings ) != 1 {
t . Fatalf ( "checkMarkdownLinks() error = %v, want one path-specific finding" , err )
}
if finding := failure . Findings [ 0 ] ; finding . Path != "two/guide.md" || finding . Target != "target.md" {
t . Fatalf ( "shared-document finding = %#v, want missing target relative to two/guide.md" , finding )
}
}
2026-10-07 23:17:10 +00:00
func TestCheckMarkdownLinksReportsBoundaryAndSubmoduleObservations ( t * testing . T ) {
repository , tree := committedMarkdownTree ( t , map [ string ] [ ] byte {
"guide.md" : [ ] byte ( "[outside](../../outside.md)\n[submodule](deps/module/README.md)\n" ) ,
} )
commit := strings . TrimSpace ( gitTree ( t , repository , "rev-parse" , "HEAD" ) )
gitTree ( t , repository , "update-index" , "--add" , "--cacheinfo" , fmt . Sprintf ( "160000,%s,deps/module" , commit ) )
gitTree ( t , repository , "commit" , "-m" , "add submodule entry" )
tree = candidateTreeAtHead ( t , repository )
var observed bytes . Buffer
if err := checkMarkdownLinks ( context . Background ( ) , tree , false , & observed ) ; err != nil {
t . Fatalf ( "checkMarkdownLinks() error = %v" , err )
}
if got := observed . String ( ) ; ! strings . Contains ( got , "outside the checkout" ) || ! strings . Contains ( got , "uninspected submodule content" ) || strings . Count ( got , "out of scope" ) != 2 {
t . Fatalf ( "out-of-scope observations = %q" , got )
}
observed . Reset ( )
err := checkMarkdownLinks ( context . Background ( ) , tree , true , & observed )
var failure * Failure
if ! errors . As ( err , & failure ) || failure . Check != MarkdownLinks || len ( failure . Findings ) != 1 {
t . Fatalf ( "enforced boundary error = %v, want one Markdown finding" , err )
}
if finding := failure . Findings [ 0 ] ; finding . Target != "../../outside.md" || finding . Message != "destination is outside the checkout" {
t . Fatalf ( "boundary finding = %#v" , finding )
}
if got := observed . String ( ) ; ! strings . Contains ( got , "uninspected submodule content" ) || strings . Contains ( got , "outside the checkout" ) {
t . Fatalf ( "enforced observations = %q" , got )
}
}
func TestCheckMarkdownLinksPreservesResolverCauseAndCancellation ( t * testing . T ) {
repository , tree := committedMarkdownTree ( t , map [ string ] [ ] byte {
"guide.md" : [ ] byte ( "[target](broken.md)\n" ) ,
} )
if err := os . Symlink ( "target.md" , filepath . Join ( repository , "broken.md" ) ) ; err != nil {
t . Fatal ( err )
}
gitTree ( t , repository , "add" , "broken.md" )
gitTree ( t , repository , "commit" , "-m" , "add Markdown symlink" )
tree = candidateTreeAtHead ( t , repository )
index := tree . byPath [ "broken.md" ]
tree . entries [ index ] . ObjectID = strings . Repeat ( "0" , 40 )
err := checkMarkdownLinks ( context . Background ( ) , tree , false , nil )
var failure * Failure
if ! errors . As ( err , & failure ) || failure . Check != MarkdownLinks || failure . Cause == nil {
t . Fatalf ( "resolver inspection error = %v, want Markdown failure with cause" , err )
}
if len ( failure . Findings ) != 1 || failure . Findings [ 0 ] . Path != "guide.md" || failure . Findings [ 0 ] . Line != 1 || failure . Findings [ 0 ] . Target != "broken.md" {
t . Fatalf ( "resolver inspection finding = %#v" , failure . Findings )
}
ctx , cancel := context . WithCancel ( context . Background ( ) )
cancel ( )
err = checkMarkdownLinks ( ctx , tree , false , nil )
if ! errors . As ( err , & failure ) || failure . Check != MarkdownLinks || ! errors . Is ( err , context . Canceled ) {
t . Fatalf ( "canceled Markdown check error = %v, want preserved cancellation" , err )
}
}
func TestCheckMarkdownLinksPreservesAnchorReadCause ( t * testing . T ) {
_ , tree := committedMarkdownTree ( t , map [ string ] [ ] byte {
"guide.md" : [ ] byte ( "[target](target.md#present)\n" ) ,
"target.md" : [ ] byte ( "# Present\n" ) ,
} )
tree . entries [ tree . byPath [ "target.md" ] ] . ObjectID = strings . Repeat ( "0" , 40 )
err := checkMarkdownLinks ( context . Background ( ) , tree , false , nil )
var failure * Failure
if ! errors . As ( err , & failure ) || failure . Check != MarkdownLinks || failure . Cause == nil {
t . Fatalf ( "anchor inspection error = %v, want Markdown failure with cause" , err )
}
if len ( failure . Findings ) != 1 || failure . Findings [ 0 ] . Path != "guide.md" || failure . Findings [ 0 ] . Target != "target.md#present" || failure . Findings [ 0 ] . Message != "could not inspect destination anchor" {
t . Fatalf ( "anchor inspection finding = %#v" , failure . Findings )
}
}
2026-10-07 23:26:48 +00:00
func TestRunSelectsMarkdownWithCombinedGoChecksAndReportsOutOfScopeLinks ( t * testing . T ) {
repository , _ := committedMarkdownTree ( t , map [ string ] [ ] byte {
"guide.md" : [ ] byte ( "[outside](../../outside.md)\n" ) ,
} )
commit := strings . TrimSpace ( gitTree ( t , repository , "rev-parse" , "HEAD" ) )
goLog := installGoShim ( t , "" , false )
var output bytes . Buffer
err := Run ( context . Background ( ) , Request {
RepositoryDir : repository ,
Commit : commit ,
Output : & output ,
Settings : Settings {
Race : true ,
FullVet : true ,
CompileTargets : [ ] Target { { OS : "linux" , Arch : "amd64" } } ,
Markdown : & MarkdownSettings { } ,
} ,
} )
if err != nil {
t . Fatalf ( "Run() error = %v, want out-of-scope observation to succeed" , err )
}
if got := output . String ( ) ; ! strings . Contains ( got , "outside the checkout" ) || ! strings . Contains ( got , "out of scope" ) {
t . Fatalf ( "Run() output = %q, want outside-checkout observation" , got )
}
commands := readGoShimLog ( t , goLog )
var testCommands [ ] string
var validationCommands [ ] string
for _ , command := range commands {
if strings . HasPrefix ( command , "test " ) {
testCommands = append ( testCommands , command )
}
if strings . HasPrefix ( command , "test " ) || strings . HasPrefix ( command , "vet " ) || strings . HasPrefix ( command , "build " ) {
validationCommands = append ( validationCommands , command )
}
}
if len ( testCommands ) != 1 || ! strings . Contains ( testCommands [ 0 ] , "-race" ) || ! strings . Contains ( testCommands [ 0 ] , "-vet=off" ) {
t . Fatalf ( "Go test commands = %#v, want one race-enabled suite with test-time vet disabled" , testCommands )
}
if len ( validationCommands ) != 3 || ! strings . HasPrefix ( validationCommands [ 0 ] , "test " ) || ! strings . HasPrefix ( validationCommands [ 1 ] , "vet " ) || ! strings . HasPrefix ( validationCommands [ 2 ] , "build " ) {
t . Fatalf ( "Go validation commands = %#v, want test, full vet, and target compilation before Markdown" , validationCommands )
}
}
2026-10-07 23:17:10 +00:00
func committedMarkdownTree ( t * testing . T , files map [ string ] [ ] byte ) ( string , * CandidateTree ) {
t . Helper ( )
repository := newTreeRepository ( t )
for name , content := range files {
writeTreeFile ( t , repository , name , content )
}
gitTree ( t , repository , "add" , "--" , "." )
gitTree ( t , repository , "commit" , "-m" , "add Markdown fixture" )
return repository , candidateTreeAtHead ( t , repository )
}
func candidateTreeAtHead ( t * testing . T , repository string ) * CandidateTree {
t . Helper ( )
commit := strings . TrimSpace ( gitTree ( t , repository , "rev-parse" , "HEAD" ) )
tree , err := ReadCandidateTree ( context . Background ( ) , repository , commit )
if err != nil {
t . Fatal ( err )
}
2026-10-09 01:30:30 +00:00
cleanupCandidateTree ( t , tree )
2026-10-07 23:17:10 +00:00
return tree
}
func readCommitted ( t * testing . T , tree * CandidateTree , name string ) [ ] byte {
t . Helper ( )
contents , err := tree . ReadFile ( context . Background ( ) , name )
if err != nil {
t . Fatal ( err )
}
return contents
}