1- import { describe , it , expect , beforeEach , afterEach } from "vitest" ;
1+ import { describe , it , expect , beforeEach , afterEach , vi } from "vitest" ;
22import {
33 mkdtempSync ,
44 writeFileSync ,
@@ -15,6 +15,13 @@ import { checkCrossFile } from "../src/drift/checkers/cross-file.js";
1515import { checkIndexSync } from "../src/drift/checkers/index-sync.js" ;
1616import type { Claim , ScaffoldFrontmatter } from "../src/types.js" ;
1717
18+ vi . mock ( "../src/git.js" , ( ) => ( {
19+ daysSinceLastChange : vi . fn ( ) ,
20+ commitsSinceLastChange : vi . fn ( ) ,
21+ } ) ) ;
22+ const gitMock = await import ( "../src/git.js" ) ;
23+ const { checkStaleness } = await import ( "../src/drift/checkers/staleness.js" ) ;
24+
1825let tmpDir : string ;
1926
2027beforeEach ( ( ) => {
@@ -299,3 +306,65 @@ describe("checkIndexSync", () => {
299306 expect ( issues ) . toHaveLength ( 0 ) ;
300307 } ) ;
301308} ) ;
309+
310+ // ── Staleness Checker ──
311+
312+ describe ( "checkStaleness" , ( ) => {
313+ const daysFn = gitMock . daysSinceLastChange as unknown as ReturnType < typeof vi . fn > ;
314+ const commitsFn = gitMock . commitsSinceLastChange as unknown as ReturnType < typeof vi . fn > ;
315+
316+ beforeEach ( ( ) => {
317+ daysFn . mockReset ( ) ;
318+ commitsFn . mockReset ( ) ;
319+ } ) ;
320+
321+ it ( "returns no issues when both thresholds are clean" , async ( ) => {
322+ daysFn . mockResolvedValue ( 10 ) ;
323+ commitsFn . mockResolvedValue ( 5 ) ;
324+ const issues = await checkStaleness ( "file.md" , "source.md" , "." ) ;
325+ expect ( issues ) . toHaveLength ( 0 ) ;
326+ } ) ;
327+
328+ it ( "returns a single issue when only the day threshold is exceeded" , async ( ) => {
329+ daysFn . mockResolvedValue ( 100 ) ;
330+ commitsFn . mockResolvedValue ( 5 ) ;
331+ const issues = await checkStaleness ( "file.md" , "source.md" , "." ) ;
332+ expect ( issues ) . toHaveLength ( 1 ) ;
333+ expect ( issues [ 0 ] . severity ) . toBe ( "error" ) ;
334+ expect ( issues [ 0 ] . message ) . toContain ( "100 days" ) ;
335+ } ) ;
336+
337+ it ( "collapses day + commit thresholds into a single compound issue" , async ( ) => {
338+ daysFn . mockResolvedValue ( 100 ) ;
339+ commitsFn . mockResolvedValue ( 250 ) ;
340+ const issues = await checkStaleness ( "file.md" , "source.md" , "." ) ;
341+ expect ( issues ) . toHaveLength ( 1 ) ;
342+ expect ( issues [ 0 ] . code ) . toBe ( "STALE_FILE" ) ;
343+ expect ( issues [ 0 ] . severity ) . toBe ( "error" ) ;
344+ expect ( issues [ 0 ] . message ) . toContain ( "100 days" ) ;
345+ expect ( issues [ 0 ] . message ) . toContain ( "250 commits" ) ;
346+ } ) ;
347+
348+ it ( "uses the higher severity when one threshold is warning and the other error" , async ( ) => {
349+ daysFn . mockResolvedValue ( 40 ) ;
350+ commitsFn . mockResolvedValue ( 250 ) ;
351+ const issues = await checkStaleness ( "file.md" , "source.md" , "." ) ;
352+ expect ( issues ) . toHaveLength ( 1 ) ;
353+ expect ( issues [ 0 ] . severity ) . toBe ( "error" ) ;
354+ } ) ;
355+
356+ it ( "keeps warning severity when neither threshold reaches error" , async ( ) => {
357+ daysFn . mockResolvedValue ( 40 ) ;
358+ commitsFn . mockResolvedValue ( 60 ) ;
359+ const issues = await checkStaleness ( "file.md" , "source.md" , "." ) ;
360+ expect ( issues ) . toHaveLength ( 1 ) ;
361+ expect ( issues [ 0 ] . severity ) . toBe ( "warning" ) ;
362+ } ) ;
363+
364+ it ( "returns empty when git history is unavailable" , async ( ) => {
365+ daysFn . mockResolvedValue ( null ) ;
366+ commitsFn . mockResolvedValue ( null ) ;
367+ const issues = await checkStaleness ( "file.md" , "source.md" , "." ) ;
368+ expect ( issues ) . toHaveLength ( 0 ) ;
369+ } ) ;
370+ } ) ;
0 commit comments