mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
* fix(lbug): pin Ladybug core so Dependabot cannot ship a skewed FTS artifact The extension version is a separate upstream constant. Ignore daily core bumps and fail the pairing gate when the committed manifest does not name the installed core. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): make doctor and CI FTS gates resolve the packaged artifact Doctor and the REQUIRE_FTS file gates still treated an empty ~/.lbdb as unavailable, which would turn three CI jobs red once analyze stops installing into that tree. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): name native-abort and tuple-missing so analyze cannot mis-advise The CLI summary's trailing else treated every unknown skip reason as a missing extension. New crash and platform causes must get their own remedies, not a network-install hint. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): delete the dead read-path FTS index create ensureFTSIndex had no production callers and swallowed read-only CREATE_FTS_INDEX failures, which hid the only signal that a reader tried to write. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): vendor per-platform FTS artifacts so analyze needs no host install Keyword search depended on a CDN fetch into ~/.lbdb. Shipping the five published tuples inside the package makes air-gapped and ignore-scripts installs load the same artifact the publish gate checksums. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): load the packaged FTS artifact before any network install Analyze still required a CDN fetch into ~/.lbdb even when the package already shipped the file. FTS now path-loads the vendored tuple first and records source labels so a later truncated home copy cannot steal the diagnosis. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): diagnose a core/extension version skew instead of a missing runtime A structurally valid FTS artifact whose path version disagrees with the packaged pin must name both versions, not prescribe VC++ or OpenSSL. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): stamp an FTS phase so repair stays usable after an in-place abort A native CREATE_FTS_INDEX abort leaves no skip reason; the next run infers it from the dirty flag, and --repair-fts must not treat that phase as a half-written graph. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): park an in-place FTS crash WAL without wiping the graph An FTS abort after a successful checkpoint must reopen the live index on macOS, Windows, and Linux. Staging never parks the live WAL; readers keep today's large-WAL refusal. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): refuse read-only opens of an FTS-poisoned WAL MCP and serve cannot repair a leftover in-place abort. Fail before the native open and name --repair-fts, on macOS, Windows, and Linux. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): name a vendor-neutral Windows OpenSSL prerequisite OQ1 is unanswered here so GitNexus does not ship OpenSSL DLLs. Windows FTS now asks for a system OpenSSL 3 runtime instead of Git Bash PATH. Co-authored-by: Cursor <cursoragent@cursor.com> * test(lbug): inject the FTS vendor root and redact it on HTTP and MCP Path-loaded artifacts no longer vary with HOME. Tests pass an injected vendor tree and assert search warnings never leak a filesystem path. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(lbug): document load-only as the global FTS install default Analyze still overrides to auto. Packaged per-platform artifacts load before any network install on macOS, Windows, and Linux. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(lbug): format the FTS install-policy README table Prettier does not run on Markdown in pre-commit, so the U10 table wrap needs its own formatting commit. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): skip FTS CREATE after a persisted native abort A recovered analyze run was retrying CREATE_FTS_INDEX from skipReason alone. Keep that skip until --repair-fts, fail closed on unsupported tuples, and honor the checkpoint warrant for park/repair. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): honor checkpoint flushed warrant and align FTS tests with packaged vendor A no-op CHECKPOINT must not satisfy the FTS park warrant, and CI still asserted HOME-only FTS isolation after analyze started path-LOADing the packaged artifact. Co-authored-by: Cursor <cursoragent@cursor.com> * test(lbug): accept a nonempty incremental write set in the #2790 recovery check FTS-phase recovery can incremental-add files (changed=0, added=1). That is not the #2790 empty-diff wipe skip. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): compare FTS home versions to the core pin and tighten the publish filename gate Ladybug's ~/.lbdb/extension directory is the runtime/core version; treating it as the artifact version false-diagnosed skew. The publish guard now rejects a path-escaping filename the same way the fetch script does. Co-authored-by: Cursor <cursoragent@cursor.com> * test(lbug): seed FTS e2e fixtures from the packaged vendor artifact A machine with no ~/.lbdb copy should still run the vendor-survivorship cases; the seed no longer depends on HOME or a network install. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3274) Keep in-place FTS abort evidence after persist so a second CREATE abort cannot fail-open readers, and close the CLI, loader, embed, and e2e gaps the review called out. Note: full npm test hit Ladybug worker-pool startup failures under memory pressure; tsc and 180 targeted unit tests passed. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3274) Run the vendored-path symlink guard on the OS matrix, put e2e HOME fixtures on Ladybug's real extension layout, pin the embed crash-WAL gate before the writable open, and let analyze writers park through missing-shadow recovery. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): keep --repair-fts CI green after vendored-first FTS Never-installed warning fixtures must not inspect a packaged vendor binary, and a failed dirty restamp must not abort an otherwise successful --repair-fts run. Co-authored-by: Cursor <cursoragent@cursor.com> * test(cli): give the #1169 analyze e2e the same 90s Windows budget as its sibling The first #1169 persist-meta case was still on a 60s spawn/it budget and was killed banner-only on windows-latest after the FTS warning fixture no longer failed the shard first. Co-authored-by: Cursor <cursoragent@cursor.com> * test(ci): reweight Windows shards after the FTS e2e grew Vendored-first HOME fixtures pushed fts-extension-e2e to ~6 minutes on windows-latest, so the old 146s weight packed it with skills-e2e and blew the 20-minute watchdog. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
168 lines
7.6 KiB
TypeScript
168 lines
7.6 KiB
TypeScript
/**
|
|
* Pins the weight-aware split behind the cross-platform matrix (#2449).
|
|
*
|
|
* The regression this guards is specific and was expensive: three CHEAP files
|
|
* were registered in `SPAWN_CLI`, vitest re-partitioned the list by file COUNT,
|
|
* and the reshuffle clustered `cli-e2e` (now 621 s on Windows) with `cli-limit-e2e`
|
|
* (75 s) and `analyze-heap-oom-e2e` (23 s) on one shard, which then blew the
|
|
* 20-minute watchdog. The added files cost nothing; the COUNT-split did it.
|
|
*
|
|
* So the load-bearing case here is not "the split is even" — it is
|
|
* "adding a cheap file does not move a heavy one". A partition that merely
|
|
* balanced totals could still reshuffle everything on every insertion and would
|
|
* reproduce the outage exactly.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
shardFiles,
|
|
shardWeight,
|
|
weightOf,
|
|
WINDOWS_WEIGHTS_SEC,
|
|
} from '../../scripts/cross-platform-shard.js';
|
|
import { ALL_CROSS_PLATFORM } from '../../scripts/cross-platform-tests.js';
|
|
|
|
const SHARD_TOTAL = 3;
|
|
|
|
/** Every shard of a split, as file lists. */
|
|
const allShards = (files: readonly string[], total: number): readonly (readonly string[])[] =>
|
|
Array.from({ length: total }, (_unused, i) => shardFiles(files, i + 1, total));
|
|
|
|
describe('cross-platform shard partition', () => {
|
|
it('does not recreate the overloaded Windows shard from the #3190 CI run', () => {
|
|
// Run 34014266125: these serialized DB suites were missing or undercharged
|
|
// in the scheduling table. Keep the observed profile independent of the
|
|
// table so deleting a weight cannot make this regression pass again.
|
|
const observed: Readonly<Record<string, number>> = {
|
|
'test/integration/skills-e2e.test.ts': 550,
|
|
'test/unit/incremental-index-extension-dml-gate.test.ts': 414,
|
|
'test/integration/fts-extension-e2e.test.ts': 380,
|
|
'test/integration/analyze-wal-checkpoint-failure.test.ts': 86,
|
|
'test/integration/skip-fts.test.ts': 110,
|
|
};
|
|
const floor = weightOf('test/unmeasured.test.ts');
|
|
const loads = allShards(ALL_CROSS_PLATFORM, SHARD_TOTAL).map((files) =>
|
|
files.reduce((sum, file) => sum + Math.max(weightOf(file), (observed[file] ?? 0) + floor), 0),
|
|
);
|
|
// This is a deterministic replay of recorded weights, not a timing test.
|
|
// The runner's existing watchdog remains 20 minutes.
|
|
expect(Math.max(...loads)).toBeLessThan(20 * 60);
|
|
const shards = allShards(ALL_CROSS_PLATFORM, SHARD_TOTAL);
|
|
const heavyweightLocations = [
|
|
'test/integration/cli-e2e.test.ts',
|
|
'test/integration/skills-e2e.test.ts',
|
|
'test/unit/incremental-index-extension-dml-gate.test.ts',
|
|
].map((file) => shards.findIndex((files) => files.includes(file)));
|
|
expect(heavyweightLocations).not.toContain(-1);
|
|
expect(new Set(heavyweightLocations).size).toBe(SHARD_TOTAL);
|
|
expect(
|
|
shards.filter(
|
|
(s) =>
|
|
s.includes('test/integration/skills-e2e.test.ts') &&
|
|
s.includes('test/integration/fts-extension-e2e.test.ts'),
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it('covers every file exactly once, with no overlap between shards', () => {
|
|
const shards = allShards(ALL_CROSS_PLATFORM, SHARD_TOTAL);
|
|
const seen = shards.flatMap((s) => [...s]);
|
|
|
|
expect(seen.slice().sort()).toEqual([...ALL_CROSS_PLATFORM].sort());
|
|
expect(new Set(seen).size).toBe(ALL_CROSS_PLATFORM.length);
|
|
});
|
|
|
|
it('keeps each shard within a shard of the ideal weight', () => {
|
|
const shards = allShards(ALL_CROSS_PLATFORM, SHARD_TOTAL);
|
|
const weights = shards.map(shardWeight);
|
|
const ideal = shardWeight(ALL_CROSS_PLATFORM) / SHARD_TOTAL;
|
|
|
|
// LPT's guarantee is 4/3 of optimal, and optimal is at least the ideal
|
|
// average. A hard 1.34x ceiling on the busiest shard is what keeps the
|
|
// matrix inside its watchdog no matter how the list is edited.
|
|
expect(Math.max(...weights)).toBeLessThanOrEqual(ideal * 1.34);
|
|
});
|
|
|
|
it('keeps the CLI and worker-pool suites on different shards', () => {
|
|
// The exact shape of the outage: cli-e2e and worker-pool are 621 s and
|
|
// 222 s, so together they are most of a shard's budget before anything else
|
|
// is scheduled.
|
|
const shards = allShards(ALL_CROSS_PLATFORM, SHARD_TOTAL);
|
|
const withBoth = shards.filter(
|
|
(s) =>
|
|
s.includes('test/integration/cli-e2e.test.ts') &&
|
|
s.includes('test/integration/worker-pool.test.ts'),
|
|
);
|
|
|
|
expect(withBoth).toEqual([]);
|
|
});
|
|
|
|
it('does not move a heavy file when a cheap file is added — the #2449 regression', () => {
|
|
const heavy = Object.keys(WINDOWS_WEIGHTS_SEC);
|
|
const placementOf = (files: readonly string[]): ReadonlyMap<string, number> => {
|
|
const shards = allShards(files, SHARD_TOTAL);
|
|
return new Map(
|
|
heavy
|
|
.map((f) => [f, shards.findIndex((s) => s.includes(f))] as const)
|
|
.filter(([, i]) => i >= 0),
|
|
);
|
|
};
|
|
|
|
const before = placementOf(ALL_CROSS_PLATFORM);
|
|
|
|
// The inserted names sort EARLY, and there is a case that is NOT a multiple
|
|
// of the shard count. Both details are load-bearing, and getting them wrong
|
|
// made earlier versions of this test vacuous:
|
|
// - names that sort last cannot disturb anything under any scheme;
|
|
// - adding exactly `total` files leaves an equal-weight round-robin in the
|
|
// same rotation, so a count-split would pass too.
|
|
// Under the real weighted split, heavy files are scheduled before every
|
|
// light one, so no number of cheap insertions can move them.
|
|
const afterOne = placementOf([...ALL_CROSS_PLATFORM, 'test/aaa-new-cheap-a.test.ts']);
|
|
const afterTwo = placementOf([
|
|
...ALL_CROSS_PLATFORM,
|
|
'test/aaa-new-cheap-a.test.ts',
|
|
'test/aaa-new-cheap-b.test.ts',
|
|
]);
|
|
|
|
expect(Object.fromEntries(afterOne)).toMatchObject(Object.fromEntries(before));
|
|
expect(Object.fromEntries(afterTwo)).toMatchObject(Object.fromEntries(before));
|
|
});
|
|
|
|
it('is deterministic, so every runner computes the same split independently', () => {
|
|
// Each matrix job resolves its own slice on its own machine with no shared
|
|
// state, so an unstable sort would silently drop or duplicate files.
|
|
const once = allShards(ALL_CROSS_PLATFORM, SHARD_TOTAL).map((s) => [...s]);
|
|
const twice = allShards([...ALL_CROSS_PLATFORM].reverse(), SHARD_TOTAL).map((s) =>
|
|
[...s].sort(),
|
|
);
|
|
|
|
expect(twice).toEqual(once.map((s) => [...s].sort()));
|
|
});
|
|
|
|
it('returns every file for a single-shard run, and rejects an out-of-range shard', () => {
|
|
expect(shardFiles(ALL_CROSS_PLATFORM, 1, 1)).toEqual([...ALL_CROSS_PLATFORM]);
|
|
expect(() => shardFiles(ALL_CROSS_PLATFORM, 0, 3)).toThrow(/shard index/);
|
|
expect(() => shardFiles(ALL_CROSS_PLATFORM, 4, 3)).toThrow(/shard index/);
|
|
expect(() => shardFiles(ALL_CROSS_PLATFORM, 1, 0)).toThrow(/shard total/);
|
|
});
|
|
|
|
it('charges every file the per-file floor, so light files are never free', () => {
|
|
// Without this, the balancer isolates the monsters and then piles all the
|
|
// light files onto the remaining shards — a count imbalance that costs just
|
|
// as much wall clock as the runtime one it just fixed.
|
|
expect(weightOf('test/unit/zzz-does-not-exist.test.ts')).toBeGreaterThan(0);
|
|
expect(weightOf('test/integration/cli-e2e.test.ts')).toBeGreaterThan(
|
|
WINDOWS_WEIGHTS_SEC['test/integration/cli-e2e.test.ts'] ?? 0,
|
|
);
|
|
});
|
|
|
|
it('weights only files that are actually registered', () => {
|
|
// A weight entry for a file no longer in the list is dead config that the
|
|
// balancer silently ignores; catching it here keeps the table honest.
|
|
const registered = new Set(ALL_CROSS_PLATFORM);
|
|
const stale = Object.keys(WINDOWS_WEIGHTS_SEC).filter((f) => !registered.has(f));
|
|
|
|
expect(stale).toEqual([]);
|
|
});
|
|
});
|