test: keep explicit regressions without legacy algorithm copies (#32218)

* test: keep explicit regressions without legacy algorithm copies

* test: use Number.NaN in latency fixtures

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-23 00:25:30 -07:00
committed by GitHub
co-authored by OrcaWin
parent 0841e09b64
commit c43ec4ddbb
7 changed files with 144 additions and 606 deletions
+60 -154
View File
@@ -821,109 +821,7 @@ describe('relay observability', () => {
})
})
// The pre-change implementation, kept verbatim as the differential oracle. Both
// ranks sorted their own copy and the maximum was a zero-seeded fold.
function legacyPercentile(values: number[], percentileRank: number): number {
if (values.length === 0) return 0
const sorted = [...values].sort((left, right) => left - right)
return sorted[Math.ceil(percentileRank * sorted.length) - 1] ?? 0
}
function legacyLatencySummary(samples: number[]): { p50: number; p95: number; max: number } {
const round = (value: number): number => Number(value.toFixed(3))
return {
p50: round(legacyPercentile(samples, 0.5)),
p95: round(legacyPercentile(samples, 0.95)),
max: round(samples.reduce((highest, sample) => Math.max(highest, sample), 0))
}
}
// `-0` and `NaN` both survive a string round trip, unlike a bare equality check.
function describeNumber(value: number): string {
return Object.is(value, -0) ? '-0' : String(value)
}
function expectSameNumber(actual: number, expected: number, label: string): void {
expect(`${label} = ${describeNumber(actual)}`).toBe(`${label} = ${describeNumber(expected)}`)
}
function sparseWindow(size: number, filled: Record<number, number>): number[] {
const values: number[] = new Array<number>(size)
for (const [index, value] of Object.entries(filled)) values[Number(index)] = value
return values
}
// Lehmer generator: stays inside the safe-integer range so the window is
// byte-identical on every engine the relay runs on.
function deterministicWindow(size: number): number[] {
let seed = 20_260_912
return Array.from({ length: size }, () => {
seed = (seed * 48_271) % 2_147_483_647
return (seed % 4_000_000) / 1_000
})
}
const DENSE_WINDOWS: Array<{ name: string; values: number[] }> = [
{ name: 'empty', values: [] },
{ name: 'single', values: [7.5] },
{ name: 'single negative', values: [-7.5] },
{ name: 'ascending', values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] },
{ name: 'descending', values: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] },
{ name: 'duplicates', values: [4, 4, 4, 4, 4] },
// The trap: a sorted last element reads -1 here, the zero-seeded fold reads 0.
{ name: 'all negative', values: [-5, -1, -9, -3, -2] },
{ name: 'mixed signs', values: [-2, 3, -7, 0, 11, -0.5] },
{ name: 'signed zero', values: [0, -0, -0, 0] },
{ name: 'negative then signed zero', values: [-3, -0, -1] },
{ name: 'nan leading', values: [NaN, 5, 1, 9] },
{ name: 'nan trailing', values: [5, 1, 9, NaN] },
{ name: 'nan interleaved', values: [5, NaN, 1, NaN, 9] },
{ name: 'all nan', values: [NaN, NaN, NaN] },
{ name: 'positive infinity', values: [Infinity, 3, 1] },
{ name: 'negative infinity', values: [-Infinity, -3, -1] },
{ name: 'both infinities', values: [Infinity, -Infinity, 3, -Infinity] },
{ name: 'infinities and nan', values: [Infinity, NaN, -Infinity, 0] },
{ name: 'sub-millisecond rounding', values: [0.00049, 0.0005, 0.00051, 0.9995] },
{ name: 'reservoir sized', values: deterministicWindow(CONTROL_RTT_RESERVOIR_LIMIT) }
]
// Holes cannot reach the recorders, so they are exercised through `percentile`
// alone — the surface `host-session-registry` also calls.
const SPARSE_WINDOWS: Array<{ name: string; values: number[] }> = [
{ name: 'all holes', values: sparseWindow(4, {}) },
{ name: 'leading hole', values: sparseWindow(5, { 3: 8, 4: 2 }) },
{ name: 'trailing hole', values: sparseWindow(5, { 0: 8, 1: 2 }) },
{ name: 'interleaved holes', values: sparseWindow(6, { 0: 3, 2: -4, 5: 1 }) },
{ name: 'holes with nan', values: sparseWindow(5, { 1: NaN, 3: 6 }) }
]
const PERCENTILE_RANKS = [0, 0.05, 0.5, 0.9, 0.95, 0.99, 1]
type SortWork = { sorts: number; comparisons: number; copiedElements: number }
// Every sorted array here is a fresh spread copy, so its length is the number of
// elements copied to produce it.
function countSortWork(run: () => void): SortWork {
const work: SortWork = { sorts: 0, comparisons: 0, copiedElements: 0 }
const original = Array.prototype.sort
const patched = Array.prototype as { sort: unknown }
patched.sort = function <T>(this: T[], compare?: (left: T, right: T) => number): T[] {
work.sorts++
work.copiedElements += this.length
return original.call(this, (left: T, right: T) => {
work.comparisons++
return compare ? compare(left, right) : String(left) < String(right) ? -1 : 1
})
}
try {
run()
} finally {
patched.sort = original
}
return work
}
function summaryThroughFlush(samples: number[]): { p50: number; p95: number; max: number } {
function summaryThroughFlush(samples: number[]) {
const entries: Array<Record<string, unknown>> = []
const observability = new RelayObservability(
{ role: 'cell', cellId: 'staging-c1', region: 'us-central1' },
@@ -933,68 +831,76 @@ function summaryThroughFlush(samples: number[]): { p50: number; p95: number; max
observability.flush(counts)
const entry = entries[0]!
return {
p50: entry.controlRenewalLatencyMsP50 as number,
p95: entry.controlRenewalLatencyMsP95 as number,
max: entry.controlRenewalLatencyMsMax as number
p50: entry.controlRenewalLatencyMsP50,
p95: entry.controlRenewalLatencyMsP95,
max: entry.controlRenewalLatencyMsMax
}
}
describe('latency window summarisation', () => {
it('matches the pre-change percentile on every edge-case window', () => {
let compared = 0
for (const { name, values } of [...DENSE_WINDOWS, ...SPARSE_WINDOWS]) {
for (const rank of PERCENTILE_RANKS) {
expectSameNumber(
percentile(values, rank),
legacyPercentile(values, rank),
`${name} @ p${rank}`
)
compared++
}
}
expect(compared).toBe((DENSE_WINDOWS.length + SPARSE_WINDOWS.length) * PERCENTILE_RANKS.length)
it.each([
{ values: [], rank: 0.5, expected: 0 },
{ values: [10, 1, 5, 3], rank: 0.5, expected: 3 },
{ values: [10, 1, 5, 3], rank: 0.95, expected: 10 },
{ values: [10, 1, 5, 3], rank: 0, expected: 0 },
{ values: [-5, -1, -9, -3, -2], rank: 1, expected: -1 },
{ values: [0, -0, -0, 0], rank: 0.5, expected: -0 },
{ values: new Array<number>(4), rank: 0.5, expected: 0 },
{ values: [Number.NaN, Number.NaN], rank: 1, expected: Number.NaN },
{ values: [Infinity, -Infinity], rank: 0.5, expected: -Infinity }
])('selects the nearest rank without interpolating (case %#)', ({ values, rank, expected }) => {
expect(percentile(values, rank)).toBe(expected)
})
it('matches the pre-change p50, p95 and maximum through a flush', () => {
let compared = 0
for (const { name, values } of DENSE_WINDOWS) {
const actual = summaryThroughFlush(values)
const expected = legacyLatencySummary(values)
expectSameNumber(actual.p50, expected.p50, `${name} p50`)
expectSameNumber(actual.p95, expected.p95, `${name} p95`)
// The zero-seeded fold, not the sorted last element: all-negative and NaN
// windows disagree between the two.
expectSameNumber(actual.max, expected.max, `${name} max`)
compared += 3
}
expect(compared).toBe(DENSE_WINDOWS.length * 3)
// The trap, spelled out: the sorted window ends at -1 but the fold reports 0.
expect(summaryThroughFlush([-5, -1, -9, -3, -2]).max).toBe(0)
expect(Number.isNaN(summaryThroughFlush([5, NaN, 1]).max)).toBe(true)
it.each([
{ name: 'empty', values: [], expected: { p50: 0, p95: 0, max: 0 } },
{ name: 'single', values: [7.5], expected: { p50: 7.5, p95: 7.5, max: 7.5 } },
{ name: 'unsorted', values: [10, 1, 5, 3], expected: { p50: 3, p95: 10, max: 10 } },
{ name: 'duplicates', values: [4, 4, 4], expected: { p50: 4, p95: 4, max: 4 } },
{ name: 'negative', values: [-5, -1, -9, -3, -2], expected: { p50: -3, p95: -1, max: 0 } },
{ name: 'mixed signs', values: [-2, 3, -7, 0, 11, -0.5], expected: { p50: -0.5, p95: 11, max: 11 } },
{ name: 'signed zero', values: [0, -0, -0, 0], expected: { p50: 0, p95: 0, max: 0 } },
{
name: 'NaN',
values: [Number.NaN, Number.NaN],
expected: { p50: Number.NaN, p95: Number.NaN, max: Number.NaN }
},
{ name: 'positive infinity', values: [Infinity, 3, 1], expected: { p50: 3, p95: Infinity, max: Infinity } },
{ name: 'negative infinity', values: [-Infinity, -3, -1], expected: { p50: -3, p95: -1, max: 0 } },
{ name: 'both infinities', values: [Infinity, -Infinity, 3, -Infinity], expected: { p50: -Infinity, p95: Infinity, max: Infinity } },
{ name: 'rounding', values: [0.00049, 0.0005, 0.00051, 0.9995], expected: { p50: 0.001, p95: 1, max: 1 } }
])('publishes explicit p50, p95 and maximum for $name samples', ({ values, expected }) => {
expect(summaryThroughFlush(values)).toEqual(expected)
})
it('sorts each latency window once instead of once per rank', () => {
const samples = deterministicWindow(CONTROL_RTT_RESERVOIR_LIMIT)
const before = countSortWork(() => legacyLatencySummary(samples))
const after = countSortWork(() => summaryThroughFlush(samples))
expect(before.sorts).toBe(2)
expect(after.sorts).toBe(1)
expect(before.copiedElements).toBe(2 * CONTROL_RTT_RESERVOIR_LIMIT)
expect(after.copiedElements).toBe(CONTROL_RTT_RESERVOIR_LIMIT)
// Identical input and comparator, so the dropped sort is exactly half the
// comparator calls rather than an engine-specific constant.
expect(before.comparisons).toBeGreaterThan(CONTROL_RTT_RESERVOIR_LIMIT)
expect(after.comparisons).toBe(before.comparisons / 2)
it('sorts a full latency reservoir once for all ranks', () => {
const samples = Array.from({ length: CONTROL_RTT_RESERVOIR_LIMIT }, (_, index) =>
CONTROL_RTT_RESERVOIR_LIMIT - index
)
const sort = vi.spyOn(Array.prototype, 'sort')
try {
summaryThroughFlush(samples)
const sortedWindows = [...sort.mock.contexts]
expect(sortedWindows).toHaveLength(1)
expect(sortedWindows[0]).toHaveLength(CONTROL_RTT_RESERVOIR_LIMIT)
} finally {
sort.mockRestore()
}
})
it('never sorts an empty window and leaves the caller window untouched', () => {
const samples = [5, -1, NaN, 3, -0]
const before = samples.map(describeNumber)
expect(countSortWork(() => summaryThroughFlush([])).sorts).toBe(0)
expect(countSortWork(() => percentile([], 0.95)).sorts).toBe(0)
countSortWork(() => summaryThroughFlush(samples))
const sort = vi.spyOn(Array.prototype, 'sort')
try {
summaryThroughFlush([])
percentile([], 0.95)
expect(sort).not.toHaveBeenCalled()
} finally {
sort.mockRestore()
}
const samples = [5, -1, Number.NaN, 3, -0]
const before = [...samples]
summaryThroughFlush(samples)
percentile(samples, 0.5)
expect(samples.map(describeNumber)).toEqual(before)
expect(samples).toEqual(before)
})
})
@@ -12,11 +12,6 @@ afterEach(() => {
document.body.innerHTML = ''
})
function originalFence(code: string): string {
const longest = (code.match(/`+/g) ?? []).reduce((run, match) => Math.max(run, match.length), 0)
return '`'.repeat(Math.max(3, longest + 1))
}
describe('code-block fence sizing', () => {
it.each([
{ code: '', length: 3 },
@@ -30,23 +25,6 @@ describe('code-block fence sizing', () => {
expect(codeFenceFor(code)).toBe('`'.repeat(length))
})
it('matches the previous result across raw UTF-16 text and consecutive calls', () => {
let seed = 0x823517
const random = () => {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
return seed
}
const atoms = ['`', '`', '````', 'a', '\n', '\r', '\u0000', '\uD800', '\uDC00', '😀', 'é', ' ']
for (let sample = 0; sample < 3000; sample += 1) {
let code = ''
const length = random() % 500
for (let index = 0; index < length; index += 1) {
code += atoms[random() % atoms.length]
}
expect(codeFenceFor(code)).toBe(originalFence(code))
}
})
it('serializes an admitted editable document without collecting every backtick run', () => {
document.body.innerHTML = RICH_MARKDOWN_EDITOR_MARKUP
const posted: MobileRichMarkdownEditorMessage[] = []
@@ -1,43 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { findTextMatchRanges } from './markdown-preview-search'
function referenceMatchRanges(text: string, query: string): { start: number; end: number }[] {
let normalized = ''
let originalOffset = 0
const starts: number[] = []
const ends: number[] = []
for (const char of text) {
const lowercase = char.toLocaleLowerCase()
for (let index = 0; index < lowercase.length; index += 1) {
starts.push(originalOffset)
ends.push(originalOffset + char.length)
}
normalized += lowercase
originalOffset += char.length
}
const normalizedQuery = Array.from(query, (char) => char.toLocaleLowerCase()).join('')
const matches: { start: number; end: number }[] = []
let searchStart = 0
while (searchStart <= normalized.length - normalizedQuery.length) {
const start = normalized.indexOf(normalizedQuery, searchStart)
if (start === -1) {
break
}
const end = start + normalizedQuery.length
matches.push({ start: starts[start] ?? text.length, end: ends[end - 1] ?? text.length })
searchStart = end + (normalizedQuery.length === 0 ? 1 : 0)
}
return matches
}
function createRandom(seed: number): (max: number) => number {
let state = seed
return (max) => {
state = (Math.imul(state, 1664525) + 1013904223) >>> 0
return state % max
}
}
function mockLowercaseLocale(locale: string): void {
const lowercase = String.prototype.toLocaleLowerCase
vi.spyOn(String.prototype, 'toLocaleLowerCase').mockImplementation(function (this: string) {
@@ -57,9 +20,16 @@ describe('case-insensitive Markdown search offsets', () => {
])
})
it.each(['İ', '😊', '𐐀'])('keeps offsets across an ordinary prefix and %s', (character) => {
it.each([
{ character: 'İ', first: 9, second: 18 },
{ character: '😊', first: 10, second: 20 },
{ character: '𐐀', first: 10, second: 20 }
])('keeps offsets across an ordinary prefix and $character', ({ character, first, second }) => {
const text = `Prefix ${character} NEEDLE ${character} needle`
expect(findTextMatchRanges(text, 'needle')).toEqual(referenceMatchRanges(text, 'needle'))
expect(findTextMatchRanges(text, 'needle')).toEqual([
{ start: first, end: first + 6 },
{ start: second, end: second + 6 }
])
})
it('preserves whole code-point ranges for lone surrogate queries', () => {
@@ -75,49 +45,19 @@ describe('case-insensitive Markdown search offsets', () => {
])
})
it.each(['en-US', 'tr', 'az', 'lt', 'el'])(
'preserves every ASCII character in locale %s',
(locale) => {
mockLowercaseLocale(locale)
const ascii = Array.from({ length: 128 }, (_, index) => String.fromCharCode(index)).join('')
const text = `${ascii} IJI iIj ${ascii}`
for (const query of [...ascii, 'IJI', 'iij', 'j']) {
expect(findTextMatchRanges(text, query)).toEqual(referenceMatchRanges(text, query))
}
}
)
it.each(['en-US', 'tr', 'az', 'lt', 'el'])(
'matches reference code-point folding in locale %s',
(locale) => {
mockLowercaseLocale(locale)
const random = createRandom(101)
const characters = [
'a',
'B',
' ',
'İ',
'I',
'ı',
'\u0307',
'Σ',
'σ',
'ς',
'𐐀',
'𐐨',
'😊',
'\ud800',
'\udc00'
]
for (let sample = 0; sample < 250; sample += 1) {
let text = ''
for (let index = 0; index < 100; index += 1) {
text += characters[random(characters.length)]
}
const start = random(text.length)
const query = text.slice(start, start + random(4) + 1)
expect(findTextMatchRanges(text, query)).toEqual(referenceMatchRanges(text, query))
}
}
)
it.each([
{ locale: 'en-US', dotted: [0, 2, 4], dotless: [6] },
{ locale: 'tr', dotted: [2, 4], dotless: [0, 6] },
{ locale: 'az', dotted: [2, 4], dotless: [0, 6] },
{ locale: 'lt', dotted: [0, 2, 4], dotless: [6] },
{ locale: 'el', dotted: [0, 2, 4], dotless: [6] }
])('respects dotted and dotless I in $locale', ({ locale, dotted, dotless }) => {
mockLowercaseLocale(locale)
expect(findTextMatchRanges('I i İ ı', 'i')).toEqual(
dotted.map((start) => ({ start, end: start + 1 }))
)
expect(findTextMatchRanges('I i İ ı', 'ı')).toEqual(
dotless.map((start) => ({ start, end: start + 1 }))
)
})
})
@@ -1,38 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import {
expandDraggedWorktreeIdsForVisibleLineage,
type WorktreeDragLineageRow
} from './worktree-manual-order'
// Preserve the original traversal as an independent ordering oracle.
function referenceExpansion(
rows: readonly WorktreeDragLineageRow[],
draggedIds: readonly string[]
) {
const dragged = new Set(draggedIds)
const expanded = new Set(draggedIds)
const rowIds = new Set(rows.map((row) => row.worktreeId))
for (let index = 0; index < rows.length; index++) {
const row = rows[index]
if (!dragged.has(row.worktreeId)) {
continue
}
for (let cursor = index + 1; cursor < rows.length; cursor++) {
const child = rows[cursor]
if (child.depth <= row.depth) {
break
}
expanded.add(child.worktreeId)
}
}
const result = rows.filter((row) => expanded.has(row.worktreeId)).map((row) => row.worktreeId)
for (const id of draggedIds) {
if (!rowIds.has(id) && !result.includes(id)) {
result.push(id)
}
}
return result
}
import { expandDraggedWorktreeIdsForVisibleLineage } from './worktree-manual-order'
describe('visible lineage expansion scaling', () => {
it('reads each depth once even when every ancestor is selected', () => {
@@ -45,9 +12,6 @@ describe('visible lineage expansion scaling', () => {
}
}))
const selected = rows.map((row) => row.worktreeId)
expect(referenceExpansion(rows, selected)).toEqual(selected)
expect(depthReads).toBe(rows.length * (rows.length - 1))
depthReads = 0
expect(expandDraggedWorktreeIdsForVisibleLineage(rows, selected)).toEqual(selected)
expect(depthReads).toBe(rows.length)
})
@@ -66,23 +30,4 @@ describe('visible lineage expansion scaling', () => {
expect(result).toEqual(selected)
expect(calls).toBe(0)
})
it('matches the previous algorithm across duplicate rows, missing IDs and depth boundaries', () => {
let seed = 12345
function random(limit: number): number {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
return seed % limit
}
const depths = [-1, 0, 1, 2, 3, 20, Infinity, -Infinity, Number.NaN]
for (let sample = 0; sample < 500; sample++) {
const rows = Array.from({ length: random(40) }, () => ({
worktreeId: `wt-${random(20)}`,
depth: depths[random(depths.length)]
}))
const selected = Array.from({ length: random(20) }, () => `wt-${random(25)}`)
expect(expandDraggedWorktreeIdsForVisibleLineage(rows, selected)).toEqual(
referenceExpansion(rows, selected)
)
}
})
})
@@ -1,176 +0,0 @@
import { expect, it } from 'vitest'
import {
selectForegroundProcessCandidate,
type ForegroundProcessCandidate
} from './foreground-process-selection'
import { recognizeAgentProcessFromCommandLine } from './agent-process-recognition'
/** The pre-memo lineage walk, with a step cap standing in for its missing cycle guard. */
function referenceIsAncestorOrSelf(
ancestorPid: number,
descendant: ForegroundProcessCandidate,
byPid: ReadonlyMap<number, ForegroundProcessCandidate>
): boolean {
let currentPid = descendant.pid
for (let steps = 0; steps <= byPid.size + 1; steps += 1) {
if (currentPid === ancestorPid) {
return true
}
const current = byPid.get(currentPid)
if (!current) {
return false
}
currentPid = current.ppid
}
// Only reachable on a ppid cycle, where the original spun forever.
return false
}
function referenceSelect(
candidates: readonly ForegroundProcessCandidate[],
ancestryCandidates: readonly ForegroundProcessCandidate[] = candidates
): ForegroundProcessCandidate | null {
const recognized = candidates.flatMap((candidate) => {
const agent = recognizeAgentProcessFromCommandLine(candidate.command)
return agent ? [{ candidate, agent }] : []
})
if (recognized.length === 0) {
return null
}
const names = new Set(recognized.map((entry) => entry.agent.agent))
if (names.size > 1) {
const byPid = new Map(ancestryCandidates.map((candidate) => [candidate.pid, candidate]))
const outer = [...recognized].sort(
(left, right) => left.candidate.depth - right.candidate.depth
)[0]
if (
!outer ||
!recognized.every((entry) =>
referenceIsAncestorOrSelf(outer.candidate.pid, entry.candidate, byPid)
)
) {
return null
}
return outer.candidate
}
const score = (candidate: ForegroundProcessCandidate): number =>
(candidate.stat?.includes('+') ? 10_000 : 0) + candidate.depth
return recognized.reduce((best, current) =>
score(current.candidate) > score(best.candidate) ? current : best
).candidate
}
function makeRandom(seed: number): () => number {
let state = seed >>> 0
return () => {
state = (state * 1664525 + 1013904223) >>> 0
return state / 0x100000000
}
}
const COMMANDS = ['claude', 'codex', 'opencode', 'bash -lc build', 'node server.js']
/** A random process table: some rows reparented, some parents missing, some cycles. */
function makeTable(random: () => number, size: number): ForegroundProcessCandidate[] {
const rows: ForegroundProcessCandidate[] = []
for (let index = 0; index < size; index += 1) {
const pid = index + 1
const roll = random()
let ppid: number
if (index === 0) {
ppid = 0
} else if (roll < 0.15) {
ppid = 9000 + index // parent absent from the table
} else if (roll < 0.25) {
ppid = 1 + Math.floor(random() * size) // arbitrary reparent, may form a cycle
} else {
ppid = index // straight chain
}
rows.push({
pid,
ppid,
depth: index,
stat: random() < 0.5 ? 'S+' : 'S',
command: COMMANDS[Math.floor(random() * COMMANDS.length)]!
})
}
return rows
}
it('picks the same foreground agent as the unmemoized lineage walk', () => {
let multiAgentCases = 0
let selectedCases = 0
for (let seed = 1; seed <= 3000; seed += 1) {
const random = makeRandom(seed)
const table = makeTable(random, 1 + Math.floor(random() * 10))
// Also exercise the split candidate/ancestry inputs the batch caller uses.
const foreground = table.filter((_, index) => index % 3 !== 2)
for (const [candidates, ancestry] of [
[table, table],
[foreground, table]
] as const) {
const actual = selectForegroundProcessCandidate(candidates, ancestry)
const expected = referenceSelect(candidates, ancestry)
expect(actual?.candidate ?? null, `seed ${seed}`).toEqual(expected)
if (
new Set(candidates.map((row) => recognizeAgentProcessFromCommandLine(row.command)?.agent))
.size > 2
) {
multiAgentCases += 1
}
if (actual) {
selectedCases += 1
}
}
}
// The mixed-agent ancestry branch (the only path the memo touches) must be hit.
expect(multiAgentCases).toBeGreaterThan(500)
expect(selectedCases).toBeGreaterThan(500)
})
// The pre-memo walk had no cycle guard, so a ppid loop that never reaches the outer
// agent spun forever and hung the caller reporting the foreground process.
it('terminates on a ppid cycle that never reaches the outer agent', () => {
const cycle: ForegroundProcessCandidate[] = [
{ pid: 1, ppid: 0, depth: 0, stat: 'S+', command: 'claude' },
{ pid: 2, ppid: 3, depth: 1, stat: 'S+', command: 'codex' },
{ pid: 3, ppid: 2, depth: 2, stat: 'S+', command: 'bash -lc build' }
]
expect(selectForegroundProcessCandidate(cycle)).toBeNull()
})
it('reflects a reparent, a spawn and an exit on the next capture', () => {
const shell: ForegroundProcessCandidate = {
pid: 10,
ppid: 1,
depth: 0,
stat: 'S+',
command: 'claude'
}
const helper: ForegroundProcessCandidate = {
pid: 11,
ppid: 10,
depth: 1,
stat: 'S+',
command: 'codex'
}
// Nested lineage: the outer agent wins.
expect(selectForegroundProcessCandidate([shell, helper])?.candidate.pid).toBe(10)
// Reparent the helper to a pid outside the capture: the lineage no longer holds.
const reparented = { ...helper, ppid: 999 }
expect(selectForegroundProcessCandidate([shell, reparented])).toBeNull()
// Spawn a sibling agent under an unrelated parent: still untrustworthy.
const sibling: ForegroundProcessCandidate = {
pid: 12,
ppid: 1,
depth: 1,
stat: 'S+',
command: 'opencode'
}
expect(selectForegroundProcessCandidate([shell, helper, sibling])).toBeNull()
// The sibling exits: the surviving nested lineage resolves again on the new capture.
expect(selectForegroundProcessCandidate([shell, helper])?.candidate.pid).toBe(10)
})
@@ -1,5 +1,8 @@
import { describe, expect, it } from 'vitest'
import { selectForegroundProcessCandidate } from './foreground-process-selection'
import {
selectForegroundProcessCandidate,
type ForegroundProcessCandidate
} from './foreground-process-selection'
describe('selectForegroundProcessCandidate', () => {
it('keeps a recognized ancestor over a different agent helper below a non-agent', () => {
@@ -37,14 +40,17 @@ describe('selectForegroundProcessCandidate', () => {
expect(selectForegroundProcessCandidate(candidates)).toBeNull()
})
it('keeps the deepest process when one recognized agent owns the lineage', () => {
it.each([
{ helperStat: 'S+', selectedPid: 102 },
{ helperStat: 'S', selectedPid: 101 }
])('prefers foreground processes, then depth ($helperStat)', ({ helperStat, selectedPid }) => {
const candidates = [
{ pid: 101, ppid: 100, depth: 1, stat: 'S+', command: 'node /opt/bin/codex' },
{ pid: 102, ppid: 101, depth: 2, stat: 'S+', command: '/opt/vendor/bin/codex' }
{ pid: 102, ppid: 101, depth: 2, stat: helperStat, command: '/opt/vendor/bin/codex' }
]
expect(selectForegroundProcessCandidate(candidates)).toMatchObject({
candidate: { pid: 102 },
candidate: { pid: selectedPid },
recognized: { agent: 'codex' }
})
})
@@ -65,3 +71,44 @@ it('memoizes shared ancestry while validating a long agent/helper lineage', () =
expect(selectForegroundProcessCandidate(candidates)?.candidate.pid).toBe(1)
expect(reads).toBeLessThanOrEqual(1000)
})
it('terminates on a ppid cycle that never reaches the outer agent', () => {
const cycle: ForegroundProcessCandidate[] = [
{ pid: 1, ppid: 0, depth: 0, stat: 'S+', command: 'claude' },
{ pid: 2, ppid: 3, depth: 1, stat: 'S+', command: 'codex' },
{ pid: 3, ppid: 2, depth: 2, stat: 'S+', command: 'bash -lc build' }
]
expect(selectForegroundProcessCandidate(cycle)).toBeNull()
})
it('does not reuse ancestry after reparenting, spawning or exit', () => {
const shell: ForegroundProcessCandidate = {
pid: 10,
ppid: 1,
depth: 0,
stat: 'S+',
command: 'claude'
}
const helper: ForegroundProcessCandidate = {
pid: 11,
ppid: 10,
depth: 1,
stat: 'S+',
command: 'codex'
}
expect(selectForegroundProcessCandidate([shell, helper])?.candidate.pid).toBe(10)
const reparented = { ...helper, ppid: 999 }
expect(selectForegroundProcessCandidate([shell, reparented])).toBeNull()
const sibling: ForegroundProcessCandidate = {
pid: 12,
ppid: 1,
depth: 1,
stat: 'S+',
command: 'opencode'
}
expect(selectForegroundProcessCandidate([shell, helper, sibling])).toBeNull()
expect(selectForegroundProcessCandidate([shell, helper])?.candidate.pid).toBe(10)
})
+8 -110
View File
@@ -1,35 +1,12 @@
import { expect, it } from 'vitest'
import { compareFileNames } from './file-name-sort'
import {
prepareQuickOpenFiles,
QuickOpenPathRanker,
rankQuickOpenFiles,
type QuickOpenIndexedFile
} from './quick-open-path-search'
import { prepareQuickOpenFiles, rankQuickOpenFiles } from './quick-open-path-search'
function referenceScore(query: string, file: QuickOpenIndexedFile): number | null {
let qi = 0
let score = 0
let lastMatch = -1
for (let ti = 0; ti < file.lowerPath.length && qi < query.length; ti++) {
if (file.lowerPath[ti] !== query[qi]) {
continue
}
score += lastMatch === -1 ? 0 : ti - lastMatch - 1
if (ti > 0 && '/.-'.includes(file.lowerPath[ti - 1])) {
score -= 5
}
lastMatch = ti
qi++
}
if (qi < query.length) {
return null
}
return score - (file.lowerFilename.includes(query) ? 100 : 0)
}
it.each(['az', 'aq'])('skips nonmatching path spans for %s', (query) => {
const [prepared] = prepareQuickOpenFiles([`a/${'x'.repeat(4000)}/z.ts`])
it.each([
{ query: 'az', score: 3997 },
{ query: 'aq', score: null }
])('skips nonmatching path spans for $query', ({ query, score }) => {
const path = `a/${'x'.repeat(4000)}/z.ts`
const [prepared] = prepareQuickOpenFiles([path])
let pathReads = 0
const measured = {
...prepared,
@@ -38,85 +15,6 @@ it.each(['az', 'aq'])('skips nonmatching path spans for %s', (query) => {
return prepared.lowerPath
}
}
expect(rankQuickOpenFiles(query, [measured])).toEqual(rankQuickOpenFiles(query, [prepared]))
expect(rankQuickOpenFiles(query, [measured])).toEqual(score === null ? [] : [{ path, score }])
expect(pathReads).toBeLessThanOrEqual(20)
})
it('preserves code-unit scores and ordering for generated Unicode paths and queries', () => {
const alphabet = [
'a',
'b',
'c',
'/',
'\\',
'.',
'-',
'2',
'0',
'é',
'e\u0301',
'İ',
'😀',
'\ud800',
'\udc00'
]
let seed = 43
const next = () => {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
return seed
}
const paths = ['a.../b', 'a/x/z', 'a/z', '😀.ts', 'a'.repeat(110), 'aa', 'same', 'same', '']
for (let index = 0; index < 1000; index++) {
const length = next() % 80
let path = ''
for (let offset = 0; offset < length; offset++) {
path += alphabet[next() % alphabet.length]
}
paths.push(path)
}
const files = prepareQuickOpenFiles(paths)
const queries = [
'',
' ',
'az',
'ab',
'a',
'aa',
'q',
'😀',
'\ud800',
'\udc00',
'é',
'e\u0301',
'İ',
'a\\b',
' A '
]
for (let index = 0; index < 100; index++) {
queries.push(`${alphabet[next() % alphabet.length]}${alphabet[next() % alphabet.length]}`)
}
for (const query of queries) {
// Separator queries now include word-boundary matches.
if (/[-_]/.test(query)) {
continue
}
const normalized = query.trim().replace(/\\/g, '/').toLowerCase()
const expected = files
.map((file) => ({ ...file, score: normalized ? referenceScore(normalized, file) : 0 }))
.filter((file): file is typeof file & { score: number } => file.score !== null)
.sort(
(a, b) =>
a.score - b.score || compareFileNames(a.path, b.path) || a.inputIndex - b.inputIndex
)
for (const limit of [1, 16, 50]) {
const selected = expected.slice(0, limit).map(({ path, score }) => ({ path, score }))
expect(rankQuickOpenFiles(query, files, limit)).toEqual(selected)
const streamed = new QuickOpenPathRanker(query, limit)
paths.forEach((path) => streamed.consider(path))
expect(streamed.result()).toEqual({
paths: selected.map((entry) => entry.path),
totalCount: expected.length
})
}
}
})