From 1ff43335fc322fe6f075deebc192ca1145bc6a29 Mon Sep 17 00:00:00 2001 From: aliasunder <32620895+aliasunder@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:21:26 -0500 Subject: [PATCH 01/10] test(parsers): check linear growth, not fixed wall-clock ceilings, in the parser timing tests The timing tests asserted medians under fixed millisecond ceilings next to a growth-ratio check. A slow CI runner exceeds a fixed ceiling and the fixed child deadlines with no regression in the code. The growth check alone catches the superlinear behaviour these tests guard, so the ceilings go, the packed-hashtag test gains a growth check scaled by line length, and every timing child shares one deadline with room for slow runners. Co-Authored-By: Claude Opus 5.5 --- .../__tests__/headings-linear.test.ts | 53 ++-- .../__tests__/helpers/linear-timing.ts | 12 + .../__tests__/tasks-clear-linear.test.ts | 138 +++++----- .../__tests__/tasks-linear.test.ts | 240 ++++++++++-------- .../__tests__/tasks-tag-dedup.test.ts | 51 ++-- 5 files changed, 274 insertions(+), 220 deletions(-) create mode 100644 src/vault-mcp/obsidian-markdown/__tests__/helpers/linear-timing.ts diff --git a/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts index 2cb6d679c..8c4ffc89d 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts @@ -6,9 +6,11 @@ import { describe, expect, it } from "vitest" import { parseHeadings, type HeadingInfo } from "../headings.js" import * as lineParsers from "../lines.js" import { tasks } from "../tasks.js" - -/** Allows scheduling noise across the full input range. */ -const LINEAR_GROWTH_ALLOWANCE = 2 +import { + CHILD_DEADLINE_MS, + CHILD_TEST_TIMEOUT_MS, + LINEAR_GROWTH_ALLOWANCE, +} from "./helpers/linear-timing.js" type HeadingParser = (lines: readonly string[]) => HeadingInfo[] @@ -171,17 +173,19 @@ describe("heading traversal compatibility", () => { ]) }) - it("bounds closing-hash and complete heading-task traversal adversaries in a child", () => { - const headingsUrl = new URL("../headings.ts", import.meta.url).href - const tasksUrl = new URL("../tasks.ts", import.meta.url).href - const output = execFileSync( - process.execPath, - [ - "--import", - "tsx", - "--input-type=module", - "--eval", - ` + it( + "bounds closing-hash and complete heading-task traversal adversaries in a child", + () => { + const headingsUrl = new URL("../headings.ts", import.meta.url).href + const tasksUrl = new URL("../tasks.ts", import.meta.url).href + const output = execFileSync( + process.execPath, + [ + "--import", + "tsx", + "--input-type=module", + "--eval", + ` import assert from 'node:assert/strict'; import { parseHeadings } from ${JSON.stringify(headingsUrl)}; import { tasks } from ${JSON.stringify(tasksUrl)}; @@ -201,19 +205,20 @@ describe("heading traversal compatibility", () => { const samples=Array.from({length:5},()=>{const start=performance.now();check();return performance.now()-start}).toSorted((a,b)=>a-b); medians.push(samples[2]); } - assert.ok(medians.every(value=>value<1000),variant+' exceeded ceiling '+medians); assert.ok(medians.at(-1)<=Math.max(10,medians[0])*(sizes.at(-1)/sizes[0])*${LINEAR_GROWTH_ALLOWANCE},variant+' scaled superlinearly '+medians); measurements.push({variant,sizes,medians}); } process.stdout.write(JSON.stringify(measurements)); `, - ], - { encoding: "utf8", timeout: 30_000, killSignal: "SIGKILL" }, - ) - - expect( - JSON.parse(output).map((measurement: { variant: string }) => measurement.variant), - ).toEqual(["closing", "traversal"]) - process.stdout.write(`Heading parser timings ${output}\n`) - }, 35_000) + ], + { encoding: "utf8", timeout: CHILD_DEADLINE_MS, killSignal: "SIGKILL" }, + ) + + expect( + JSON.parse(output).map((measurement: { variant: string }) => measurement.variant), + ).toEqual(["closing", "traversal"]) + process.stdout.write(`Heading parser timings ${output}\n`) + }, + CHILD_TEST_TIMEOUT_MS, + ) }) diff --git a/src/vault-mcp/obsidian-markdown/__tests__/helpers/linear-timing.ts b/src/vault-mcp/obsidian-markdown/__tests__/helpers/linear-timing.ts new file mode 100644 index 000000000..91bf173eb --- /dev/null +++ b/src/vault-mcp/obsidian-markdown/__tests__/helpers/linear-timing.ts @@ -0,0 +1,12 @@ +/** How far a timing series may exceed linear growth before a test calls it superlinear, to allow for scheduling noise. */ +export const LINEAR_GROWTH_ALLOWANCE = 2 + +/** + * Stops a timing child that never finishes. The growth check is the speed + * test, so this only has to outlast a slow CI runner: the slowest child takes + * about 7 s on a fast laptop, and CI runners have taken over four times as long. + */ +export const CHILD_DEADLINE_MS = 60_000 + +/** Gives a killed child time to report its failure before vitest abandons the test. */ +export const CHILD_TEST_TIMEOUT_MS = CHILD_DEADLINE_MS + 10_000 diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts index 3cbf30cab..00fa24779 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts @@ -3,9 +3,12 @@ import { readFileSync } from "node:fs" import { runInNewContext } from "node:vm" import ts from "typescript" import { describe, expect, it } from "vitest" +import { + CHILD_DEADLINE_MS, + CHILD_TEST_TIMEOUT_MS, + LINEAR_GROWTH_ALLOWANCE, +} from "./helpers/linear-timing.js" -/** Allows scheduling noise across the full input range. */ -const LINEAR_GROWTH_ALLOWANCE = 2 /** Below this many milliseconds a median is mostly scheduling noise, so it sets no tighter growth bound. */ const TIMING_FLOOR_MS = 25 @@ -140,16 +143,18 @@ describe("task metadata clear compatibility", () => { } }, 20_000) - it("clears twenty to eighty thousand priority fields within a child deadline", () => { - const parserUrl = new URL("../tasks.ts", import.meta.url).href - const output = execFileSync( - process.execPath, - [ - "--import", - "tsx", - "--input-type=module", - "--eval", - ` + it( + "clears twenty to eighty thousand priority fields within a child deadline", + () => { + const parserUrl = new URL("../tasks.ts", import.meta.url).href + const output = execFileSync( + process.execPath, + [ + "--import", + "tsx", + "--input-type=module", + "--eval", + ` import assert from 'node:assert/strict'; import { tasks, DEFAULT_STATUS_REGISTRY } from ${JSON.stringify(parserUrl)}; const config={taskFormat:'emoji',setDoneDate:true,setCancelledDate:true,setCreatedDate:false,recurrenceOnNextLine:false,removeScheduledDateOnRecurrence:false,statusRegistry:DEFAULT_STATUS_REGISTRY}; @@ -162,29 +167,32 @@ describe("task metadata clear compatibility", () => { const samples=Array.from({length:5},()=>{const start=performance.now();check();return performance.now()-start;}).toSorted((a,b)=>a-b); medians.push(samples[2]); } - assert.ok(medians.every(value=>value<1000),'metadata clear exceeded ceiling '+medians); assert.ok(medians.at(-1)<=Math.max(${TIMING_FLOOR_MS},medians[0])*(sizes.at(-1)/sizes[0])*${LINEAR_GROWTH_ALLOWANCE},'metadata clear scaled superlinearly '+medians); process.stdout.write(JSON.stringify({sizes,medians})); `, - ], - { encoding: "utf8", timeout: 15_000, killSignal: "SIGKILL" }, - ) + ], + { encoding: "utf8", timeout: CHILD_DEADLINE_MS, killSignal: "SIGKILL" }, + ) - expect(JSON.parse(output)).toEqual({ - sizes: [20000, 40000, 80000], - medians: [expect.any(Number), expect.any(Number), expect.any(Number)], - }) - process.stdout.write(`Metadata clear timings ${output}\n`) - }, 20_000) + expect(JSON.parse(output)).toEqual({ + sizes: [20000, 40000, 80000], + medians: [expect.any(Number), expect.any(Number), expect.any(Number)], + }) + process.stdout.write(`Metadata clear timings ${output}\n`) + }, + CHILD_TEST_TIMEOUT_MS, + ) - it("bounds every field grammar and newly exposed nested fields at increasing sizes", () => { - const compiled = compileRemovalGrammar() - const output = execFileSync( - process.execPath, - [ - "--input-type=module", - "--eval", - ` + it( + "bounds every field grammar and newly exposed nested fields at increasing sizes", + () => { + const compiled = compileRemovalGrammar() + const output = execFileSync( + process.execPath, + [ + "--input-type=module", + "--eval", + ` import assert from 'node:assert/strict'; import { runInThisContext } from 'node:vm'; const grammar=runInThisContext(${JSON.stringify(compiled)}); @@ -213,7 +221,6 @@ describe("task metadata clear compatibility", () => { const samples=Array.from({length:5},()=>{const start=performance.now();check();return performance.now()-start;}).toSorted((a,b)=>a-b); medians.push(samples[2]); } - assert.ok(medians.every(value=>value<2000),name+' exceeded ceiling '+medians); assert.ok(medians.at(-1)<=Math.max(${TIMING_FLOOR_MS},medians[0])*(sizes.at(-1)/sizes[0])*${LINEAR_GROWTH_ALLOWANCE},name+' scaled superlinearly '+medians); timings.push({name,medians}); }; @@ -222,32 +229,36 @@ describe("task metadata clear compatibility", () => { checkFixture('nested-dataview-date',2,size=>'#'+'[due::'.repeat(size)+'2026-01-02'+']2026-01-02'.repeat(size-1)+']'); process.stdout.write(JSON.stringify({sizes,timings})); `, - ], - { encoding: "utf8", timeout: 30_000, killSignal: "SIGKILL" }, - ) + ], + { encoding: "utf8", timeout: CHILD_DEADLINE_MS, killSignal: "SIGKILL" }, + ) - const result: unknown = JSON.parse(output) + const result: unknown = JSON.parse(output) - expect(result).toEqual({ - sizes: [10000, 20000, 40000], - timings: Array.from({ length: 24 }, () => ({ - name: expect.any(String), - medians: [expect.any(Number), expect.any(Number), expect.any(Number)], - })), - }) - process.stdout.write(`All metadata grammar timings ${output}\n`) - }, 35_000) + expect(result).toEqual({ + sizes: [10000, 20000, 40000], + timings: Array.from({ length: 24 }, () => ({ + name: expect.any(String), + medians: [expect.any(Number), expect.any(Number), expect.any(Number)], + })), + }) + process.stdout.write(`All metadata grammar timings ${output}\n`) + }, + CHILD_TEST_TIMEOUT_MS, + ) - it("clears duplicate completion stamps through status mutations within a child deadline", () => { - const parserUrl = new URL("../tasks.ts", import.meta.url).href - const output = execFileSync( - process.execPath, - [ - "--import", - "tsx", - "--input-type=module", - "--eval", - ` + it( + "clears duplicate completion stamps through status mutations within a child deadline", + () => { + const parserUrl = new URL("../tasks.ts", import.meta.url).href + const output = execFileSync( + process.execPath, + [ + "--import", + "tsx", + "--input-type=module", + "--eval", + ` import assert from 'node:assert/strict'; import { tasks, DEFAULT_STATUS_REGISTRY } from ${JSON.stringify(parserUrl)}; const config={taskFormat:'emoji',setDoneDate:true,setCancelledDate:true,setCreatedDate:false,recurrenceOnNextLine:false,removeScheduledDateOnRecurrence:false,statusRegistry:DEFAULT_STATUS_REGISTRY}; @@ -260,17 +271,18 @@ describe("task metadata clear compatibility", () => { const samples=Array.from({length:5},()=>{const start=performance.now();check();return performance.now()-start;}).toSorted((a,b)=>a-b); medians.push(samples[2]); } - assert.ok(medians.every(value=>value<2000),'status stamp clear exceeded ceiling '+medians); assert.ok(medians.at(-1)<=Math.max(${TIMING_FLOOR_MS},medians[0])*(sizes.at(-1)/sizes[0])*${LINEAR_GROWTH_ALLOWANCE},'status stamp clear scaled superlinearly '+medians); process.stdout.write(JSON.stringify({sizes,medians})); `, - ], - { encoding: "utf8", timeout: 15_000, killSignal: "SIGKILL" }, - ) + ], + { encoding: "utf8", timeout: CHILD_DEADLINE_MS, killSignal: "SIGKILL" }, + ) - expect(JSON.parse(output)).toEqual({ - sizes: [20000, 40000, 80000], - medians: [expect.any(Number), expect.any(Number), expect.any(Number)], - }) - }, 20_000) + expect(JSON.parse(output)).toEqual({ + sizes: [20000, 40000, 80000], + medians: [expect.any(Number), expect.any(Number), expect.any(Number)], + }) + }, + CHILD_TEST_TIMEOUT_MS, + ) }) diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tasks-linear.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tasks-linear.test.ts index 4adb66a16..0da700a60 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/tasks-linear.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/tasks-linear.test.ts @@ -6,9 +6,12 @@ import ts from "typescript" import { describe, expect, it } from "vitest" import { tasks } from "../tasks.js" import { createScannerDocuments } from "./helpers/link-scanner-corpus.js" +import { + CHILD_DEADLINE_MS, + CHILD_TEST_TIMEOUT_MS, + LINEAR_GROWTH_ALLOWANCE, +} from "./helpers/linear-timing.js" -/** Allows scheduling noise across the full input range. */ -const LINEAR_GROWTH_ALLOWANCE = 2 /** Below this many milliseconds a median is mostly scheduling noise, so it sets no tighter growth bound. */ const TIMING_FLOOR_MS = 25 @@ -194,16 +197,18 @@ describe("task recognizer compatibility", () => { expect(grammar.trailingTags("#a\t#b #a")).toEqual(["#a", "#b", "#a"]) }) - it("bounds deduplication with tens of thousands of trailing description tags", () => { - const parserUrl = new URL("../tasks.ts", import.meta.url).href - const output = execFileSync( - process.execPath, - [ - "--import", - "tsx", - "--input-type=module", - "--eval", - ` + it( + "bounds deduplication with tens of thousands of trailing description tags", + () => { + const parserUrl = new URL("../tasks.ts", import.meta.url).href + const output = execFileSync( + process.execPath, + [ + "--import", + "tsx", + "--input-type=module", + "--eval", + ` import assert from 'node:assert/strict'; import { tasks } from ${JSON.stringify(parserUrl)}; const sizes=[20000,40000,80000]; @@ -215,20 +220,21 @@ describe("task recognizer compatibility", () => { const samples=Array.from({length:5},()=>{const start=performance.now();check();return performance.now()-start;}).toSorted((a,b)=>a-b); medians.push(samples[2]); } - assert.ok(medians.every(value=>value<500),'trailing tag deduplication exceeded ceiling '+medians); assert.ok(medians.at(-1)<=Math.max(${TIMING_FLOOR_MS},medians[0])*(sizes.at(-1)/sizes[0])*${LINEAR_GROWTH_ALLOWANCE},'trailing tag deduplication scaled superlinearly '+medians); process.stdout.write(JSON.stringify({sizes,medians})); `, - ], - { encoding: "utf8", timeout: 10_000, killSignal: "SIGKILL" }, - ) + ], + { encoding: "utf8", timeout: CHILD_DEADLINE_MS, killSignal: "SIGKILL" }, + ) - expect(JSON.parse(output)).toEqual({ - sizes: [20000, 40000, 80000], - medians: [expect.any(Number), expect.any(Number), expect.any(Number)], - }) - process.stdout.write(`Trailing tag deduplication timings ${output}\n`) - }, 15_000) + expect(JSON.parse(output)).toEqual({ + sizes: [20000, 40000, 80000], + medians: [expect.any(Number), expect.any(Number), expect.any(Number)], + }) + process.stdout.write(`Trailing tag deduplication timings ${output}\n`) + }, + CHILD_TEST_TIMEOUT_MS, + ) it("preserves metadata boundaries over fixed-seed bodies and every literal task fixture", () => { const grammar = readGrammar() @@ -390,23 +396,27 @@ describe("task recognizer compatibility", () => { expect(grammar.metadataStart("#๐Ÿ“…2026-01-02 (repeat:: every day)")).toBe(1) }) - it("bounds task edits whose hashtags each pack hundreds of signifiers", () => { - const parserUrl = new URL("../tasks.ts", import.meta.url).href - const output = execFileSync( - process.execPath, - [ - "--import", - "tsx", - "--input-type=module", - "--eval", - ` + it( + "bounds task edits whose hashtags each pack hundreds of signifiers", + () => { + const parserUrl = new URL("../tasks.ts", import.meta.url).href + const output = execFileSync( + process.execPath, + [ + "--import", + "tsx", + "--input-type=module", + "--eval", + ` import assert from 'node:assert/strict'; import { tasks } from ${JSON.stringify(parserUrl)}; const config = {taskFormat:'emoji',setDoneDate:true,setCancelledDate:true}; const sizes = [120,240,480]; + const lineLengths = []; const medians = []; for (const size of sizes) { const taskLine = '- [ ] ' + Array.from({length:size},()=> '#'+ '๐Ÿ“…'.repeat(size+2)).join(' '); + lineLengths.push(taskLine.length); const check = () => { assert.equal(tasks.updateTaskLineTaskId({taskLine,taskId:'probe',config}),taskLine+' ๐Ÿ†” probe'); assert.deepEqual(tasks.diffTaskRoundTrip({taskLine,priorTaskLine:taskLine,submitted:{}}),[]); @@ -415,30 +425,36 @@ describe("task recognizer compatibility", () => { const samples = Array.from({length:3},()=>{const start=performance.now();check();return performance.now()-start;}).toSorted((a,b)=>a-b); medians.push(samples[1]); } - assert.ok(medians.every(value=>value<1000),'packed hashtag edits exceeded ceiling '+medians); + // Size sets both the hashtag count and each hashtag's length, so the line grows with + // the square of size and the growth bound follows the line's length instead. + assert.ok(medians.at(-1)<=Math.max(${TIMING_FLOOR_MS},medians[0])*(lineLengths.at(-1)/lineLengths[0])*${LINEAR_GROWTH_ALLOWANCE},'packed hashtag edits scaled superlinearly '+medians); process.stdout.write(JSON.stringify({sizes,medians})); `, - ], - { encoding: "utf8", timeout: 10_000, killSignal: "SIGKILL" }, - ) + ], + { encoding: "utf8", timeout: CHILD_DEADLINE_MS, killSignal: "SIGKILL" }, + ) - expect(JSON.parse(output)).toEqual({ - sizes: [120, 240, 480], - medians: [expect.any(Number), expect.any(Number), expect.any(Number)], - }) - process.stdout.write(`Packed hashtag edit timings ${output}\n`) - }, 15_000) - - it("bounds signifier-dense task edits and their growth in a child process", () => { - const parserUrl = new URL("../tasks.ts", import.meta.url).href - const output = execFileSync( - process.execPath, - [ - "--import", - "tsx", - "--input-type=module", - "--eval", - ` + expect(JSON.parse(output)).toEqual({ + sizes: [120, 240, 480], + medians: [expect.any(Number), expect.any(Number), expect.any(Number)], + }) + process.stdout.write(`Packed hashtag edit timings ${output}\n`) + }, + CHILD_TEST_TIMEOUT_MS, + ) + + it( + "bounds signifier-dense task edits and their growth in a child process", + () => { + const parserUrl = new URL("../tasks.ts", import.meta.url).href + const output = execFileSync( + process.execPath, + [ + "--import", + "tsx", + "--input-type=module", + "--eval", + ` import assert from 'node:assert/strict'; import { tasks } from ${JSON.stringify(parserUrl)}; const config = {taskFormat:'emoji',setDoneDate:true,setCancelledDate:true}; @@ -455,21 +471,22 @@ describe("task recognizer compatibility", () => { const samples = Array.from({length:5}, () => {const start=performance.now();check();return performance.now()-start;}).toSorted((first,second)=>first-second); medians.push(samples[2]); } - assert.ok(medians.every(value => value < 250), 'dense edits exceeded 250ms ' + medians); assert.ok(medians.at(-1) <= Math.max(${TIMING_FLOOR_MS},medians[0])*(sizes.at(-1)/sizes[0])*${LINEAR_GROWTH_ALLOWANCE}, 'dense edits scaled superlinearly ' + medians); process.stdout.write(JSON.stringify({sizes,medians})); `, - ], - { encoding: "utf8", timeout: 10_000, killSignal: "SIGKILL" }, - ) - const measurements: unknown = JSON.parse(output) + ], + { encoding: "utf8", timeout: CHILD_DEADLINE_MS, killSignal: "SIGKILL" }, + ) + const measurements: unknown = JSON.parse(output) - expect(measurements).toEqual({ - sizes: [1000, 2000, 4000, 5000], - medians: [expect.any(Number), expect.any(Number), expect.any(Number), expect.any(Number)], - }) - process.stdout.write(`Task metadata edit timings ${output}\n`) - }, 15_000) + expect(measurements).toEqual({ + sizes: [1000, 2000, 4000, 5000], + medians: [expect.any(Number), expect.any(Number), expect.any(Number), expect.any(Number)], + }) + process.stdout.write(`Task metadata edit timings ${output}\n`) + }, + CHILD_TEST_TIMEOUT_MS, + ) it("preserves Dataview field spans and consumed captures over fixed-seed variants", () => { const grammar = readGrammar() @@ -523,7 +540,7 @@ describe("task recognizer compatibility", () => { }) } } - }) + }, 30_000) it("preserves emoji recurrence spans and consumed captures over fixed-seed variants", () => { const grammar = readGrammar() @@ -629,16 +646,18 @@ describe("task recognizer compatibility", () => { ).toEqual([]) }) - it("bounds task, recurrence and Dataview adversaries in a child process", () => { - const parserUrl = new URL("../tasks.ts", import.meta.url).href - const output = execFileSync( - process.execPath, - [ - "--import", - "tsx", - "--input-type=module", - "--eval", - ` + it( + "bounds task, recurrence and Dataview adversaries in a child process", + () => { + const parserUrl = new URL("../tasks.ts", import.meta.url).href + const output = execFileSync( + process.execPath, + [ + "--import", + "tsx", + "--input-type=module", + "--eval", + ` import assert from 'node:assert/strict'; import { tasks } from ${JSON.stringify(parserUrl)}; const fixtures = [ @@ -663,44 +682,45 @@ describe("task recognizer compatibility", () => { const samples = Array.from({length:5}, () => { const start=performance.now(); check(); return performance.now()-start; }).toSorted((first,second)=>first-second); medians.push(samples[2]); } - assert.ok(medians.every(value => value < 250), fixture.name + ' exceeded 250ms ' + medians); assert.ok(medians.at(-1) <= Math.max(${TIMING_FLOOR_MS},medians[0])*(fixture.sizes.at(-1)/fixture.sizes[0])*${LINEAR_GROWTH_ALLOWANCE}, fixture.name + ' scaled superlinearly ' + medians); measurements.push({name:fixture.name,sizes:fixture.sizes,medians}); } process.stdout.write(JSON.stringify(measurements)); `, - ], - { encoding: "utf8", timeout: 30_000, killSignal: "SIGKILL" }, - ) - const measurements: unknown = JSON.parse(output) - - expect(measurements).toEqual([ - { - name: "task-u2028", - sizes: [10_000, 20_000, 40_000], - medians: [expect.any(Number), expect.any(Number), expect.any(Number)], - }, - { - name: "task-u2029", - sizes: [10_000, 20_000, 40_000], - medians: [expect.any(Number), expect.any(Number), expect.any(Number)], - }, - { - name: "emoji-recurrence", - sizes: [20_000, 40_000, 80_000], - medians: [expect.any(Number), expect.any(Number), expect.any(Number)], - }, - { - name: "dataview-recurrence", - sizes: [1_000, 2_000, 4_000], - medians: [expect.any(Number), expect.any(Number), expect.any(Number)], - }, - { - name: "dataview-lookahead", - sizes: [25_000, 50_000, 100_000], - medians: [expect.any(Number), expect.any(Number), expect.any(Number)], - }, - ]) - process.stdout.write(`Task parser timings ${output}\n`) - }, 35_000) + ], + { encoding: "utf8", timeout: CHILD_DEADLINE_MS, killSignal: "SIGKILL" }, + ) + const measurements: unknown = JSON.parse(output) + + expect(measurements).toEqual([ + { + name: "task-u2028", + sizes: [10_000, 20_000, 40_000], + medians: [expect.any(Number), expect.any(Number), expect.any(Number)], + }, + { + name: "task-u2029", + sizes: [10_000, 20_000, 40_000], + medians: [expect.any(Number), expect.any(Number), expect.any(Number)], + }, + { + name: "emoji-recurrence", + sizes: [20_000, 40_000, 80_000], + medians: [expect.any(Number), expect.any(Number), expect.any(Number)], + }, + { + name: "dataview-recurrence", + sizes: [1_000, 2_000, 4_000], + medians: [expect.any(Number), expect.any(Number), expect.any(Number)], + }, + { + name: "dataview-lookahead", + sizes: [25_000, 50_000, 100_000], + medians: [expect.any(Number), expect.any(Number), expect.any(Number)], + }, + ]) + process.stdout.write(`Task parser timings ${output}\n`) + }, + CHILD_TEST_TIMEOUT_MS, + ) }) diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts index bd75dd52c..5a5bdba6a 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts @@ -5,9 +5,11 @@ import { DateTime } from "luxon" import ts from "typescript" import { describe, expect, it } from "vitest" import { tasks } from "../tasks.js" - -/** Allows scheduling noise across the full input range. */ -const LINEAR_GROWTH_ALLOWANCE = 2 +import { + CHILD_DEADLINE_MS, + CHILD_TEST_TIMEOUT_MS, + LINEAR_GROWTH_ALLOWANCE, +} from "./helpers/linear-timing.js" type Deduplication = ReturnType @@ -82,16 +84,18 @@ describe("task description tag deduplication", () => { } }) - it("bounds distinct description tags against a long metadata tail in a child process", () => { - const parserUrl = new URL("../tasks.ts", import.meta.url).href - const output = execFileSync( - process.execPath, - [ - "--import", - "tsx", - "--input-type=module", - "--eval", - ` + it( + "bounds distinct description tags against a long metadata tail in a child process", + () => { + const parserUrl = new URL("../tasks.ts", import.meta.url).href + const output = execFileSync( + process.execPath, + [ + "--import", + "tsx", + "--input-type=module", + "--eval", + ` import assert from 'node:assert/strict'; import { tasks } from ${JSON.stringify(parserUrl)}; const sizes = [20000,40000,80000]; @@ -104,18 +108,19 @@ describe("task description tag deduplication", () => { const samples = Array.from({length:5},()=>{const start=performance.now();check();return performance.now()-start;}).toSorted((a,b)=>a-b); medians.push(samples[2]); } - assert.ok(medians.every(value=>value<350),'description tag searches exceeded ceiling '+medians); assert.ok(medians.at(-1)<=Math.max(10,medians[0])*(sizes.at(-1)/sizes[0])*${LINEAR_GROWTH_ALLOWANCE},'description tag searches scaled superlinearly '+medians); process.stdout.write(JSON.stringify({sizes,medians})); `, - ], - { encoding: "utf8", timeout: 10_000, killSignal: "SIGKILL" }, - ) + ], + { encoding: "utf8", timeout: CHILD_DEADLINE_MS, killSignal: "SIGKILL" }, + ) - expect(JSON.parse(output)).toEqual({ - sizes: [20000, 40000, 80000], - medians: [expect.any(Number), expect.any(Number), expect.any(Number)], - }) - process.stdout.write(`Distinct description tag timings ${output}\n`) - }, 15_000) + expect(JSON.parse(output)).toEqual({ + sizes: [20000, 40000, 80000], + medians: [expect.any(Number), expect.any(Number), expect.any(Number)], + }) + process.stdout.write(`Distinct description tag timings ${output}\n`) + }, + CHILD_TEST_TIMEOUT_MS, + ) }) From befd76b164afb5287002dc600972fd32dd3b3821 Mon Sep 17 00:00:00 2001 From: aliasunder <32620895+aliasunder@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:49:57 -0500 Subject: [PATCH 02/10] test(parsers): assert every timing series' identity in the heading and field-grammar timing tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The heading timing test checked only the variant names, and the field-grammar timing test matched each fixture name with expect.any(String). Both outputs are deterministic apart from the medians, so each test now matches the whole result: the variant, sizes and three medians per heading series, and the 24 fixture names in the order the child times them. Ship-Check: test-audit ยท claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .../__tests__/headings-linear.test.ts | 17 ++++++++++++++--- .../__tests__/tasks-clear-linear.test.ts | 15 +++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts index 8c4ffc89d..ee9955325 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts @@ -214,9 +214,20 @@ describe("heading traversal compatibility", () => { { encoding: "utf8", timeout: CHILD_DEADLINE_MS, killSignal: "SIGKILL" }, ) - expect( - JSON.parse(output).map((measurement: { variant: string }) => measurement.variant), - ).toEqual(["closing", "traversal"]) + const measurements: unknown = JSON.parse(output) + + expect(measurements).toEqual([ + { + variant: "closing", + sizes: [25_000, 50_000, 100_000], + medians: [expect.any(Number), expect.any(Number), expect.any(Number)], + }, + { + variant: "traversal", + sizes: [16_000, 32_000, 64_000], + medians: [expect.any(Number), expect.any(Number), expect.any(Number)], + }, + ]) process.stdout.write(`Heading parser timings ${output}\n`) }, CHILD_TEST_TIMEOUT_MS, diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts index 00fa24779..c866fe1ed 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts @@ -235,10 +235,21 @@ describe("task metadata clear compatibility", () => { const result: unknown = JSON.parse(output) + // The child times each of the eleven fields in emoji format (suffix 0) and Dataview format + // (suffix 1), then the two nested fixtures. + const fixtureNames = [ + ...Array.from({ length: 11 }, (_, fieldIndex) => [ + `${fieldIndex}-0`, + `${fieldIndex}-1`, + ]).flat(), + "nested-emoji-date", + "nested-dataview-date", + ] + expect(result).toEqual({ sizes: [10000, 20000, 40000], - timings: Array.from({ length: 24 }, () => ({ - name: expect.any(String), + timings: fixtureNames.map((name) => ({ + name, medians: [expect.any(Number), expect.any(Number), expect.any(Number)], })), }) From 7c9d9f158d70ed342accfa1f7062b2f31580e674 Mon Sep 17 00:00:00 2001 From: aliasunder <32620895+aliasunder@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:57:27 -0500 Subject: [PATCH 03/10] test(parsers): state why the vitest timeout sits above the timing child's deadline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vitest cannot interrupt a test blocked in execFileSync, and a killed child's own error is what vitest reports whatever the test timeout. The margin keeps a child that finishes inside its deadline from failing as a vitest timeout, which vitest checks once a synchronous test returns. Ship-Check: bug-check ยท claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .../obsidian-markdown/__tests__/helpers/linear-timing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vault-mcp/obsidian-markdown/__tests__/helpers/linear-timing.ts b/src/vault-mcp/obsidian-markdown/__tests__/helpers/linear-timing.ts index 91bf173eb..96f15a4dc 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/helpers/linear-timing.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/helpers/linear-timing.ts @@ -8,5 +8,5 @@ export const LINEAR_GROWTH_ALLOWANCE = 2 */ export const CHILD_DEADLINE_MS = 60_000 -/** Gives a killed child time to report its failure before vitest abandons the test. */ +/** Sits above the child deadline, so vitest never reports a child that finished within it as a test timeout. */ export const CHILD_TEST_TIMEOUT_MS = CHILD_DEADLINE_MS + 10_000 From e13e98544c1e66cfc0c0c0ad4312ee5137739690 Mon Sep 17 00:00:00 2001 From: aliasunder <32620895+aliasunder@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:59:19 -0500 Subject: [PATCH 04/10] test(parsers): name the growth floor in every timing file and print the status-stamp timings The heading and tag-deduplication timing tests passed a bare 10 as the growth check's noise floor; it is now a named constant like the other two files' floor, with the same value. The status-stamp test was the only timing test that never printed its medians. Co-Authored-By: Claude Opus 5.5 --- .../obsidian-markdown/__tests__/headings-linear.test.ts | 5 ++++- .../obsidian-markdown/__tests__/tasks-clear-linear.test.ts | 1 + .../obsidian-markdown/__tests__/tasks-tag-dedup.test.ts | 5 ++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts index ee9955325..384fd7c2d 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts @@ -12,6 +12,9 @@ import { LINEAR_GROWTH_ALLOWANCE, } from "./helpers/linear-timing.js" +/** Below this many milliseconds a median is mostly scheduling noise, so it sets no tighter growth bound. */ +const TIMING_FLOOR_MS = 10 + type HeadingParser = (lines: readonly string[]) => HeadingInfo[] /** Keeps the former suffix and boundary rules as a small-input oracle. */ @@ -205,7 +208,7 @@ describe("heading traversal compatibility", () => { const samples=Array.from({length:5},()=>{const start=performance.now();check();return performance.now()-start}).toSorted((a,b)=>a-b); medians.push(samples[2]); } - assert.ok(medians.at(-1)<=Math.max(10,medians[0])*(sizes.at(-1)/sizes[0])*${LINEAR_GROWTH_ALLOWANCE},variant+' scaled superlinearly '+medians); + assert.ok(medians.at(-1)<=Math.max(${TIMING_FLOOR_MS},medians[0])*(sizes.at(-1)/sizes[0])*${LINEAR_GROWTH_ALLOWANCE},variant+' scaled superlinearly '+medians); measurements.push({variant,sizes,medians}); } process.stdout.write(JSON.stringify(measurements)); diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts index c866fe1ed..243815f19 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts @@ -293,6 +293,7 @@ describe("task metadata clear compatibility", () => { sizes: [20000, 40000, 80000], medians: [expect.any(Number), expect.any(Number), expect.any(Number)], }) + process.stdout.write(`Status stamp clear timings ${output}\n`) }, CHILD_TEST_TIMEOUT_MS, ) diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts index 5a5bdba6a..abe2fc042 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts @@ -11,6 +11,9 @@ import { LINEAR_GROWTH_ALLOWANCE, } from "./helpers/linear-timing.js" +/** Below this many milliseconds a median is mostly scheduling noise, so it sets no tighter growth bound. */ +const TIMING_FLOOR_MS = 10 + type Deduplication = ReturnType /** Freezes the original per-tag search while sharing the separately verified boundary parser. */ @@ -108,7 +111,7 @@ describe("task description tag deduplication", () => { const samples = Array.from({length:5},()=>{const start=performance.now();check();return performance.now()-start;}).toSorted((a,b)=>a-b); medians.push(samples[2]); } - assert.ok(medians.at(-1)<=Math.max(10,medians[0])*(sizes.at(-1)/sizes[0])*${LINEAR_GROWTH_ALLOWANCE},'description tag searches scaled superlinearly '+medians); + assert.ok(medians.at(-1)<=Math.max(${TIMING_FLOOR_MS},medians[0])*(sizes.at(-1)/sizes[0])*${LINEAR_GROWTH_ALLOWANCE},'description tag searches scaled superlinearly '+medians); process.stdout.write(JSON.stringify({sizes,medians})); `, ], From 0198539eff2f9853eee2648c0a90f06d963fd63d Mon Sep 17 00:00:00 2001 From: aliasunder <32620895+aliasunder@users.noreply.github.com> Date: Thu, 12 Nov 2026 20:04:07 -0500 Subject: [PATCH 05/10] test(parsers): name the two clear tests for the growth they check, not their deadline Co-Authored-By: Claude Opus 5.5 --- .../obsidian-markdown/__tests__/tasks-clear-linear.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts index 243815f19..d13a4e05f 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts @@ -144,7 +144,7 @@ describe("task metadata clear compatibility", () => { }, 20_000) it( - "clears twenty to eighty thousand priority fields within a child deadline", + "clears twenty to eighty thousand priority fields in time linear in their count", () => { const parserUrl = new URL("../tasks.ts", import.meta.url).href const output = execFileSync( @@ -259,7 +259,7 @@ describe("task metadata clear compatibility", () => { ) it( - "clears duplicate completion stamps through status mutations within a child deadline", + "clears duplicate completion stamps through status mutations in time linear in their count", () => { const parserUrl = new URL("../tasks.ts", import.meta.url).href const output = execFileSync( From 47a35a15b350c04fab4bc178f9a94356c822a2bf Mon Sep 17 00:00:00 2001 From: aliasunder <32620895+aliasunder@users.noreply.github.com> Date: Thu, 12 Nov 2026 20:11:39 -0500 Subject: [PATCH 06/10] test(parsers): say why two timing files keep a 10 ms growth floor Co-Authored-By: Claude Opus 5.5 --- .../obsidian-markdown/__tests__/headings-linear.test.ts | 7 ++++++- .../obsidian-markdown/__tests__/tasks-tag-dedup.test.ts | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts index 384fd7c2d..ef3f1f307 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts @@ -12,7 +12,12 @@ import { LINEAR_GROWTH_ALLOWANCE, } from "./helpers/linear-timing.js" -/** Below this many milliseconds a median is mostly scheduling noise, so it sets no tighter growth bound. */ +/** + * Below this many milliseconds a median is mostly scheduling noise, so it sets + * no tighter growth bound. It is lower than the other timing files' 25 ms + * because the traversal fixture's smallest median sits near 10 ms: at a 25 ms + * floor its bound would pass a quadratic slowdown. + */ const TIMING_FLOOR_MS = 10 type HeadingParser = (lines: readonly string[]) => HeadingInfo[] diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts index abe2fc042..b6eb5697a 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts @@ -11,7 +11,12 @@ import { LINEAR_GROWTH_ALLOWANCE, } from "./helpers/linear-timing.js" -/** Below this many milliseconds a median is mostly scheduling noise, so it sets no tighter growth bound. */ +/** + * Below this many milliseconds a median is mostly scheduling noise, so it sets + * no tighter growth bound. It is lower than the other timing files' 25 ms + * because this fixture's smallest median sits near 14 ms: at a 25 ms floor its + * bound would pass a quadratic slowdown. + */ const TIMING_FLOOR_MS = 10 type Deduplication = ReturnType From 32339fc0a7061e1650ed2d127a2b4ff0be750ec5 Mon Sep 17 00:00:00 2001 From: aliasunder <32620895+aliasunder@users.noreply.github.com> Date: Thu, 12 Nov 2026 20:22:17 -0500 Subject: [PATCH 07/10] test(parsers): give every fixed-seed corpus test an explicit timeout and correct the floor comments' arithmetic The four fixed-seed corpus tests that still relied on vitest's 5 s default get the same 30 s as their siblings. The 10 ms floor comments now state each file's real margin: the heading traversal's quadratic would pass a 25 ms floor, while the tag-dedup fixture would fail it by only about 10%. Co-Authored-By: Claude Opus 5.5 --- .../obsidian-markdown/__tests__/headings-linear.test.ts | 9 +++++---- .../obsidian-markdown/__tests__/tasks-linear.test.ts | 4 ++-- .../obsidian-markdown/__tests__/tasks-tag-dedup.test.ts | 9 +++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts index ef3f1f307..0e1bca808 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts @@ -14,9 +14,10 @@ import { /** * Below this many milliseconds a median is mostly scheduling noise, so it sets - * no tighter growth bound. It is lower than the other timing files' 25 ms - * because the traversal fixture's smallest median sits near 10 ms: at a 25 ms - * floor its bound would pass a quadratic slowdown. + * no tighter growth bound. It is lower than the 25 ms in tasks-linear and + * tasks-clear-linear because the traversal fixture's smallest median sits near + * 10 ms: a 25 ms floor would set the bound at 200 ms, above a quadratic + * slowdown's 165 ms, so the slowdown would pass. */ const TIMING_FLOOR_MS = 10 @@ -122,7 +123,7 @@ describe("heading traversal compatibility", () => { }) } } - }) + }, 30_000) it.each([ { newline: "\n", label: "LF" }, diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tasks-linear.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tasks-linear.test.ts index 0da700a60..275355167 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/tasks-linear.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/tasks-linear.test.ts @@ -195,7 +195,7 @@ describe("task recognizer compatibility", () => { }) } expect(grammar.trailingTags("#a\t#b #a")).toEqual(["#a", "#b", "#a"]) - }) + }, 30_000) it( "bounds deduplication with tens of thousands of trailing description tags", @@ -560,7 +560,7 @@ describe("task recognizer compatibility", () => { match: consumedMatch(oldEmoji.exec(input)), }) } - }) + }, 30_000) it("compares real Dataview dependency fields with the frozen suffix grammar", () => { const grammar = readGrammar() diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts index b6eb5697a..57d2605d0 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts @@ -13,9 +13,10 @@ import { /** * Below this many milliseconds a median is mostly scheduling noise, so it sets - * no tighter growth bound. It is lower than the other timing files' 25 ms - * because this fixture's smallest median sits near 14 ms: at a 25 ms floor its - * bound would pass a quadratic slowdown. + * no tighter growth bound. It is lower than the 25 ms in tasks-linear and + * tasks-clear-linear because this fixture's smallest median sits near 14 ms: a + * 25 ms floor would set the bound at 200 ms, about 10% under a quadratic + * slowdown's 224 ms, while this floor keeps it at about half. */ const TIMING_FLOOR_MS = 10 @@ -90,7 +91,7 @@ describe("task description tag deduplication", () => { result: legacyDeduplication(taskLine), }) } - }) + }, 30_000) it( "bounds distinct description tags against a long metadata tail in a child process", From 3179ec6868e4ad25709af1ca2b5a6e857b6bbb13 Mon Sep 17 00:00:00 2001 From: aliasunder <32620895+aliasunder@users.noreply.github.com> Date: Thu, 12 Nov 2026 20:47:57 -0500 Subject: [PATCH 08/10] test(parsers): scope the floor comments' stated medians to a fast laptop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 10 ms and 14 ms smallest medians hold on an idle fast machine; a loaded or slower runner measures several times more, and there the smallest median clears the 25 ms floor anyway. The arithmetic is unchanged. Ship-Check: test-audit ยท claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- .../obsidian-markdown/__tests__/headings-linear.test.ts | 4 ++-- .../obsidian-markdown/__tests__/tasks-tag-dedup.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts index 0e1bca808..5f53e2987 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts @@ -16,8 +16,8 @@ import { * Below this many milliseconds a median is mostly scheduling noise, so it sets * no tighter growth bound. It is lower than the 25 ms in tasks-linear and * tasks-clear-linear because the traversal fixture's smallest median sits near - * 10 ms: a 25 ms floor would set the bound at 200 ms, above a quadratic - * slowdown's 165 ms, so the slowdown would pass. + * 10 ms on a fast laptop: a 25 ms floor would set the bound at 200 ms, above a + * quadratic slowdown's 165 ms, so the slowdown would pass. */ const TIMING_FLOOR_MS = 10 diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts index 57d2605d0..0eaa1df8d 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts @@ -14,9 +14,9 @@ import { /** * Below this many milliseconds a median is mostly scheduling noise, so it sets * no tighter growth bound. It is lower than the 25 ms in tasks-linear and - * tasks-clear-linear because this fixture's smallest median sits near 14 ms: a - * 25 ms floor would set the bound at 200 ms, about 10% under a quadratic - * slowdown's 224 ms, while this floor keeps it at about half. + * tasks-clear-linear because this fixture's smallest median sits near 14 ms on + * a fast laptop: a 25 ms floor would set the bound at 200 ms, about 10% under a + * quadratic slowdown's 224 ms, while this floor keeps it at about half. */ const TIMING_FLOOR_MS = 10 From 923448694defbaa6b0a5740d34b99af30596b9eb Mon Sep 17 00:00:00 2001 From: aliasunder <32620895+aliasunder@users.noreply.github.com> Date: Thu, 12 Nov 2026 20:49:37 -0500 Subject: [PATCH 09/10] test(parsers): give the field-removal corpus test the same 30 s timeout as its siblings Co-Authored-By: Claude Opus 5.5 --- .../obsidian-markdown/__tests__/tasks-clear-linear.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts index d13a4e05f..3c612f164 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts @@ -141,7 +141,7 @@ describe("task metadata clear compatibility", () => { }) } } - }, 20_000) + }, 30_000) it( "clears twenty to eighty thousand priority fields in time linear in their count", From 90a22462eb75d358900dc3c2480467a53f12e27d Mon Sep 17 00:00:00 2001 From: aliasunder <32620895+aliasunder@users.noreply.github.com> Date: Thu, 12 Nov 2026 21:05:45 -0500 Subject: [PATCH 10/10] test(parsers): state each 10 ms floor's reason as the consequence and its two facts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Ship-Check: code-quality ยท claude-opus-5-5 --- .../obsidian-markdown/__tests__/headings-linear.test.ts | 7 ++++--- .../obsidian-markdown/__tests__/tasks-tag-dedup.test.ts | 9 ++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts index 5f53e2987..c04261c85 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts @@ -15,9 +15,10 @@ import { /** * Below this many milliseconds a median is mostly scheduling noise, so it sets * no tighter growth bound. It is lower than the 25 ms in tasks-linear and - * tasks-clear-linear because the traversal fixture's smallest median sits near - * 10 ms on a fast laptop: a 25 ms floor would set the bound at 200 ms, above a - * quadratic slowdown's 165 ms, so the slowdown would pass. + * tasks-clear-linear, because a 25 ms floor would let a quadratic slowdown pass: + * - The traversal fixture's smallest median sits near 10 ms on a fast laptop. + * - A 25 ms floor would set the bound at 200 ms, above a quadratic slowdown's + * 165 ms. */ const TIMING_FLOOR_MS = 10 diff --git a/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts index 0eaa1df8d..253c67817 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/tasks-tag-dedup.test.ts @@ -14,9 +14,12 @@ import { /** * Below this many milliseconds a median is mostly scheduling noise, so it sets * no tighter growth bound. It is lower than the 25 ms in tasks-linear and - * tasks-clear-linear because this fixture's smallest median sits near 14 ms on - * a fast laptop: a 25 ms floor would set the bound at 200 ms, about 10% under a - * quadratic slowdown's 224 ms, while this floor keeps it at about half. + * tasks-clear-linear, because a 25 ms floor would barely catch a quadratic + * slowdown: + * - This fixture's smallest median sits near 14 ms on a fast laptop. + * - A 25 ms floor would set the bound at 200 ms, only about 10% under a + * quadratic slowdown's 224 ms, so scheduling noise could let the slowdown pass. + * - This floor sets the bound at about half of 224 ms. */ const TIMING_FLOOR_MS = 10