-
Notifications
You must be signed in to change notification settings - Fork 8
test(integration): harden pre-existing shard-5/6 CI flakes #1504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
abc95f8
28574cb
49179af
551c8f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -226,9 +226,49 @@ async function waitFor(predicate: () => Promise<boolean>, timeoutMs = 60_000, in | |||||||||||||||||||||||
| suite('HNSW vector-index data-integrity (integration)', (ctx: ContextWithHarper) => { | ||||||||||||||||||||||||
| let client: any; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // All four tables are deployed by a SINGLE component with ONE worker restart in `before`. | ||||||||||||||||||||||||
| // Previously every test deployed its own component and restarted the HTTP workers (~5 restarts | ||||||||||||||||||||||||
| // across the suite); on slow runners that restart is the dominant per-test cost (a test doing | ||||||||||||||||||||||||
| // 3 inserts still took ~265s on Windows — almost entirely restart/setup). Consolidating to one | ||||||||||||||||||||||||
| // shared restart here — plus the single restart `reindex` needs for its mid-test index-add — is | ||||||||||||||||||||||||
| // the bulk of this suite's wall-time. The tables use distinct names/databases so the tests stay | ||||||||||||||||||||||||
| // isolated despite sharing one component; `reindex` runs last and is the only test that mutates | ||||||||||||||||||||||||
| // the schema afterward, so re-asserting the others on its redeploy is a harmless no-op. | ||||||||||||||||||||||||
| const PROJECT = 'vecintegsuite'; | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The shared component/schema state creates an implicit test-ordering dependency: At minimum, a comment near the test declarations noting "order matters — reindex must run last" (or a runtime guard that asserting the |
||||||||||||||||||||||||
| // ReindexTable starts WITHOUT an index — the reindex test adds it mid-test to exercise backfill. | ||||||||||||||||||||||||
| const SCHEMA_INITIAL = [ | ||||||||||||||||||||||||
| makeSchemaWithIndex('DelEPTable', 'vecinteg1'), | ||||||||||||||||||||||||
| makeSchemaWithIndex('ChurnTable', 'vecinteg2'), | ||||||||||||||||||||||||
| makeSchemaWithIndex('ThreshTable', 'vecinteg3'), | ||||||||||||||||||||||||
| makeSchemaWithoutIndex('ReindexTable', 'vecinteg4'), | ||||||||||||||||||||||||
| ].join('\n'); | ||||||||||||||||||||||||
| // Same schema with ReindexTable now indexed — redeployed by the reindex test to trigger backfill. | ||||||||||||||||||||||||
| const SCHEMA_REINDEX_INDEXED = [ | ||||||||||||||||||||||||
| makeSchemaWithIndex('DelEPTable', 'vecinteg1'), | ||||||||||||||||||||||||
| makeSchemaWithIndex('ChurnTable', 'vecinteg2'), | ||||||||||||||||||||||||
| makeSchemaWithIndex('ThreshTable', 'vecinteg3'), | ||||||||||||||||||||||||
| makeSchemaWithIndex('ReindexTable', 'vecinteg4'), | ||||||||||||||||||||||||
| ].join('\n'); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| before(async () => { | ||||||||||||||||||||||||
| await startHarper(ctx, { config: {}, env: {} }); | ||||||||||||||||||||||||
| client = createApiClient(ctx.harper); | ||||||||||||||||||||||||
| await client | ||||||||||||||||||||||||
| .req() | ||||||||||||||||||||||||
| .send({ operation: 'add_component', project: PROJECT }) | ||||||||||||||||||||||||
| .expect((r: any) => { | ||||||||||||||||||||||||
| ok( | ||||||||||||||||||||||||
| JSON.stringify(r.body).includes('Successfully added project') || | ||||||||||||||||||||||||
| JSON.stringify(r.body).includes('Project already exists'), | ||||||||||||||||||||||||
| r.text | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| await client | ||||||||||||||||||||||||
| .req() | ||||||||||||||||||||||||
| .send({ operation: 'set_component_file', project: PROJECT, file: 'schema.graphql', payload: SCHEMA_INITIAL }) | ||||||||||||||||||||||||
| .expect(200); | ||||||||||||||||||||||||
| // One restart loads all four tables; probing one ready endpoint confirms the workers are back. | ||||||||||||||||||||||||
| await restartHttpWorkers(client, '/DelEPTable/'); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| after(async () => { | ||||||||||||||||||||||||
|
|
@@ -241,39 +281,13 @@ suite('HNSW vector-index data-integrity (integration)', (ctx: ContextWithHarper) | |||||||||||||||||||||||
| // getRange and skips the node being deleted. | ||||||||||||||||||||||||
| // Pre-fix: getRange ran outside the write-set → re-elected the deleted node | ||||||||||||||||||||||||
| // as EP → dangling entry point → subsequent searches returned []. | ||||||||||||||||||||||||
| const DB = 'vecinteg1'; | ||||||||||||||||||||||||
| const TABLE = 'DelEPTable'; | ||||||||||||||||||||||||
| const PROJECT = 'vecintegsuite1'; | ||||||||||||||||||||||||
| const DIMS = 8; | ||||||||||||||||||||||||
| const N = 50; | ||||||||||||||||||||||||
| const SURVIVORS = 10; | ||||||||||||||||||||||||
| const deleteCount = N - SURVIVORS; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| await client | ||||||||||||||||||||||||
| .req() | ||||||||||||||||||||||||
| .send({ operation: 'add_component', project: PROJECT }) | ||||||||||||||||||||||||
| .expect((r: any) => { | ||||||||||||||||||||||||
| ok( | ||||||||||||||||||||||||
| JSON.stringify(r.body).includes('Successfully added project') || | ||||||||||||||||||||||||
| JSON.stringify(r.body).includes('Project already exists'), | ||||||||||||||||||||||||
| r.text | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| await client | ||||||||||||||||||||||||
| .req() | ||||||||||||||||||||||||
| .send({ | ||||||||||||||||||||||||
| operation: 'set_component_file', | ||||||||||||||||||||||||
| project: PROJECT, | ||||||||||||||||||||||||
| file: 'schema.graphql', | ||||||||||||||||||||||||
| payload: makeSchemaWithIndex(TABLE, DB), | ||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||
| .expect(200); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| await restartHttpWorkers(client, `/${TABLE}/`); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const httpURL = ctx.harper.httpURL; | ||||||||||||||||||||||||
| const headers = client.headers; | ||||||||||||||||||||||||
| const path = `/${TABLE}/`; | ||||||||||||||||||||||||
| const path = '/DelEPTable/'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Insert N records — the first-inserted node becomes the initial entry point. | ||||||||||||||||||||||||
| for (let i = 0; i < N; i++) { | ||||||||||||||||||||||||
|
|
@@ -312,38 +326,12 @@ suite('HNSW vector-index data-integrity (integration)', (ctx: ContextWithHarper) | |||||||||||||||||||||||
| // where the old connection existed, not the full 0..l sweep (correct for DELETE). | ||||||||||||||||||||||||
| // The broader sweep destroyed reverse edges that addConnection had just re-added, | ||||||||||||||||||||||||
| // accumulating asymmetry with every re-embed round. | ||||||||||||||||||||||||
| const DB = 'vecinteg2'; | ||||||||||||||||||||||||
| const TABLE = 'ChurnTable'; | ||||||||||||||||||||||||
| const PROJECT = 'vecintegsuite2'; | ||||||||||||||||||||||||
| const DIMS = 8; | ||||||||||||||||||||||||
| const N = 30; | ||||||||||||||||||||||||
| const ROUNDS = 5; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| await client | ||||||||||||||||||||||||
| .req() | ||||||||||||||||||||||||
| .send({ operation: 'add_component', project: PROJECT }) | ||||||||||||||||||||||||
| .expect((r: any) => { | ||||||||||||||||||||||||
| ok( | ||||||||||||||||||||||||
| JSON.stringify(r.body).includes('Successfully added project') || | ||||||||||||||||||||||||
| JSON.stringify(r.body).includes('Project already exists'), | ||||||||||||||||||||||||
| r.text | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| await client | ||||||||||||||||||||||||
| .req() | ||||||||||||||||||||||||
| .send({ | ||||||||||||||||||||||||
| operation: 'set_component_file', | ||||||||||||||||||||||||
| project: PROJECT, | ||||||||||||||||||||||||
| file: 'schema.graphql', | ||||||||||||||||||||||||
| payload: makeSchemaWithIndex(TABLE, DB), | ||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||
| .expect(200); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| await restartHttpWorkers(client, `/${TABLE}/`); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const httpURL = ctx.harper.httpURL; | ||||||||||||||||||||||||
| const headers = client.headers; | ||||||||||||||||||||||||
| const path = `/${TABLE}/`; | ||||||||||||||||||||||||
| const path = '/ChurnTable/'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| for (let i = 0; i < N; i++) { | ||||||||||||||||||||||||
| await insertRecord(httpURL, headers, path, { id: `churn-${i}`, embedding: seedVector(i, DIMS) }); | ||||||||||||||||||||||||
|
|
@@ -360,12 +348,17 @@ suite('HNSW vector-index data-integrity (integration)', (ctx: ContextWithHarper) | |||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Each record's final vector is seedVector(i*100 + ROUNDS, dims). | ||||||||||||||||||||||||
| // Searching for that exact vector must return it in the top-5. | ||||||||||||||||||||||||
| // Searching for that exact vector must return it in the top-5. These are | ||||||||||||||||||||||||
| // independent read-only searches, so run them concurrently rather than | ||||||||||||||||||||||||
| // serializing N round-trips. | ||||||||||||||||||||||||
| const churnResults = await Promise.all( | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Launching 30 concurrent QUERY requests via
Suggested change
|
||||||||||||||||||||||||
| Array.from({ length: N }, (_, i) => | ||||||||||||||||||||||||
| vectorSearch(httpURL, headers, path, seedVector(i * 100 + ROUNDS, DIMS), { limit: 5 }) | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| let misses = 0; | ||||||||||||||||||||||||
| for (let i = 0; i < N; i++) { | ||||||||||||||||||||||||
| const finalVec = seedVector(i * 100 + ROUNDS, DIMS); | ||||||||||||||||||||||||
| const results = await vectorSearch(httpURL, headers, path, finalVec, { limit: 5 }); | ||||||||||||||||||||||||
| if (!results.some((r: any) => r.id === `churn-${i}`)) misses++; | ||||||||||||||||||||||||
| if (!churnResults[i].some((r: any) => r.id === `churn-${i}`)) misses++; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| const allowed = Math.ceil(N * 0.1); | ||||||||||||||||||||||||
| ok(misses <= allowed, `expected ≤${allowed} misses after ${ROUNDS} churn rounds; got ${misses}/${N}`); | ||||||||||||||||||||||||
|
|
@@ -375,35 +368,9 @@ suite('HNSW vector-index data-integrity (integration)', (ctx: ContextWithHarper) | |||||||||||||||||||||||
| test('threshold queries: le includes exact boundary; le(~0) returns exact-match only', async () => { | ||||||||||||||||||||||||
| // Guards fix #6b: le comparator now uses <= (was <). | ||||||||||||||||||||||||
| // Pre-fix: records at exactly the threshold distance were excluded by le. | ||||||||||||||||||||||||
| const DB = 'vecinteg3'; | ||||||||||||||||||||||||
| const TABLE = 'ThreshTable'; | ||||||||||||||||||||||||
| const PROJECT = 'vecintegsuite3'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| await client | ||||||||||||||||||||||||
| .req() | ||||||||||||||||||||||||
| .send({ operation: 'add_component', project: PROJECT }) | ||||||||||||||||||||||||
| .expect((r: any) => { | ||||||||||||||||||||||||
| ok( | ||||||||||||||||||||||||
| JSON.stringify(r.body).includes('Successfully added project') || | ||||||||||||||||||||||||
| JSON.stringify(r.body).includes('Project already exists'), | ||||||||||||||||||||||||
| r.text | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| await client | ||||||||||||||||||||||||
| .req() | ||||||||||||||||||||||||
| .send({ | ||||||||||||||||||||||||
| operation: 'set_component_file', | ||||||||||||||||||||||||
| project: PROJECT, | ||||||||||||||||||||||||
| file: 'schema.graphql', | ||||||||||||||||||||||||
| payload: makeSchemaWithIndex(TABLE, DB), | ||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||
| .expect(200); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| await restartHttpWorkers(client, `/${TABLE}/`); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const httpURL = ctx.harper.httpURL; | ||||||||||||||||||||||||
| const headers = client.headers; | ||||||||||||||||||||||||
| const path = `/${TABLE}/`; | ||||||||||||||||||||||||
| const path = '/ThreshTable/'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // 2-D vectors at three well-separated cosine distances from [1,0]: | ||||||||||||||||||||||||
| // [1,0] → distance 0 (exact; representable in float32, so exactly 0) | ||||||||||||||||||||||||
|
|
@@ -464,42 +431,26 @@ suite('HNSW vector-index data-integrity (integration)', (ctx: ContextWithHarper) | |||||||||||||||||||||||
| // on structural index change so runIndexing starts clean). | ||||||||||||||||||||||||
| const DB = 'vecinteg4'; | ||||||||||||||||||||||||
| const TABLE = 'ReindexTable'; | ||||||||||||||||||||||||
| const PROJECT = 'vecintegsuite4'; | ||||||||||||||||||||||||
| const DIMS = 8; | ||||||||||||||||||||||||
| const N = 40; | ||||||||||||||||||||||||
| const httpURL = ctx.harper.httpURL; | ||||||||||||||||||||||||
| const headers = client.headers; | ||||||||||||||||||||||||
| const path = '/ReindexTable/'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Step 1: Deploy schema WITHOUT the HNSW index. | ||||||||||||||||||||||||
| await client | ||||||||||||||||||||||||
| .req() | ||||||||||||||||||||||||
| .send({ operation: 'add_component', project: PROJECT }) | ||||||||||||||||||||||||
| .expect((r: any) => { | ||||||||||||||||||||||||
| ok( | ||||||||||||||||||||||||
| JSON.stringify(r.body).includes('Successfully added project') || | ||||||||||||||||||||||||
| JSON.stringify(r.body).includes('Project already exists'), | ||||||||||||||||||||||||
| r.text | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| // Step 1: ReindexTable was deployed WITHOUT an HNSW index by the shared `before` setup. | ||||||||||||||||||||||||
| // Step 2: Insert N records (plain [Float], no HNSW index yet). Bulk-insert in a | ||||||||||||||||||||||||
| // single request — there is no index during insert, so per-record ordering is | ||||||||||||||||||||||||
| // irrelevant here; the backfill (Step 3) builds the index from the stored rows. | ||||||||||||||||||||||||
| await client | ||||||||||||||||||||||||
| .req() | ||||||||||||||||||||||||
| .send({ | ||||||||||||||||||||||||
| operation: 'set_component_file', | ||||||||||||||||||||||||
| project: PROJECT, | ||||||||||||||||||||||||
| file: 'schema.graphql', | ||||||||||||||||||||||||
| payload: makeSchemaWithoutIndex(TABLE, DB), | ||||||||||||||||||||||||
| operation: 'insert', | ||||||||||||||||||||||||
| database: DB, | ||||||||||||||||||||||||
| table: TABLE, | ||||||||||||||||||||||||
| records: Array.from({ length: N }, (_, i) => ({ id: `reindex-${i}`, embedding: seedVector(i, DIMS) })), | ||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||
| .expect(200); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| await restartHttpWorkers(client, `/${TABLE}/`); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const httpURL = ctx.harper.httpURL; | ||||||||||||||||||||||||
| const headers = client.headers; | ||||||||||||||||||||||||
| const path = `/${TABLE}/`; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Step 2: Insert N records (plain [Float], no HNSW index yet). | ||||||||||||||||||||||||
| for (let i = 0; i < N; i++) { | ||||||||||||||||||||||||
| await insertRecord(httpURL, headers, path, { id: `reindex-${i}`, embedding: seedVector(i, DIMS) }); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Sanity-check: records exist. | ||||||||||||||||||||||||
| const hashCheck = await client | ||||||||||||||||||||||||
| .req() | ||||||||||||||||||||||||
|
|
@@ -513,14 +464,16 @@ suite('HNSW vector-index data-integrity (integration)', (ctx: ContextWithHarper) | |||||||||||||||||||||||
| .expect(200); | ||||||||||||||||||||||||
| ok(Array.isArray(hashCheck.body) && hashCheck.body.length === 1, 'reindex-0 must exist before reindex'); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Step 3: Alter schema to ADD the HNSW index → triggers runIndexing backfill. | ||||||||||||||||||||||||
| // Step 3: Alter schema to ADD the HNSW index on ReindexTable → triggers runIndexing | ||||||||||||||||||||||||
| // backfill. Redeploy the full schema (the other three tables are unchanged, so this is | ||||||||||||||||||||||||
| // a no-op re-assert for them); reindex runs last, so restarting them here is harmless. | ||||||||||||||||||||||||
| await client | ||||||||||||||||||||||||
| .req() | ||||||||||||||||||||||||
| .send({ | ||||||||||||||||||||||||
| operation: 'set_component_file', | ||||||||||||||||||||||||
| project: PROJECT, | ||||||||||||||||||||||||
| file: 'schema.graphql', | ||||||||||||||||||||||||
| payload: makeSchemaWithIndex(TABLE, DB), | ||||||||||||||||||||||||
| payload: SCHEMA_REINDEX_INDEXED, | ||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||
| .expect(200); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -537,12 +490,14 @@ suite('HNSW vector-index data-integrity (integration)', (ctx: ContextWithHarper) | |||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| }, 60_000); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Step 5: All N pre-existing records must be reachable. | ||||||||||||||||||||||||
| // Step 5: All N pre-existing records must be reachable. Independent read-only | ||||||||||||||||||||||||
| // searches — run them concurrently rather than serializing N round-trips. | ||||||||||||||||||||||||
| const reindexResults = await Promise.all( | ||||||||||||||||||||||||
| Array.from({ length: N }, (_, i) => vectorSearch(httpURL, headers, path, seedVector(i, DIMS), { limit: 5 })) | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| let misses = 0; | ||||||||||||||||||||||||
| for (let i = 0; i < N; i++) { | ||||||||||||||||||||||||
| const vec = seedVector(i, DIMS); | ||||||||||||||||||||||||
| const results = await vectorSearch(httpURL, headers, path, vec, { limit: 5 }); | ||||||||||||||||||||||||
| if (!results.some((r: any) => r.id === `reindex-${i}`)) misses++; | ||||||||||||||||||||||||
| if (!reindexResults[i].some((r: any) => r.id === `reindex-${i}`)) misses++; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| const allowed = Math.ceil(N * 0.1); | ||||||||||||||||||||||||
| ok(misses <= allowed, `expected ≤${allowed} misses after backfill; got ${misses}/${N}`); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.