Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
78 changes: 52 additions & 26 deletions src/vault-mcp/obsidian-markdown/__tests__/headings-linear.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,21 @@ 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"

/**
* 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 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

type HeadingParser = (lines: readonly string[]) => HeadingInfo[]

Expand Down Expand Up @@ -112,7 +124,7 @@ describe("heading traversal compatibility", () => {
})
}
}
})
}, 30_000)

it.each([
{ newline: "\n", label: "LF" },
Expand Down Expand Up @@ -171,17 +183,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)};
Expand All @@ -201,19 +215,31 @@ 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);
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));
`,
],
{ 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" },
)

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,
)
})
Original file line number Diff line number Diff line change
@@ -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

/** 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
152 changes: 88 additions & 64 deletions src/vault-mcp/obsidian-markdown/__tests__/tasks-clear-linear.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
aliasunder marked this conversation as resolved.
Resolved

Expand Down Expand Up @@ -138,18 +141,20 @@ describe("task metadata clear compatibility", () => {
})
}
}
}, 20_000)
}, 30_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 in time linear in their count",
() => {
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};
Expand All @@ -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)});
Expand Down Expand Up @@ -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});
};
Expand All @@ -222,32 +229,47 @@ 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)
// 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",
]

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",
`
expect(result).toEqual({
sizes: [10000, 20000, 40000],
timings: fixtureNames.map((name) => ({
name,
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 in time linear in their count",
() => {
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};
Expand All @@ -260,17 +282,19 @@ 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);
Comment thread
aliasunder marked this conversation as resolved.
Resolved
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)],
})
process.stdout.write(`Status stamp clear timings ${output}\n`)
},
CHILD_TEST_TIMEOUT_MS,
)
})
Loading
Loading