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
22 changes: 0 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,6 @@ on:
branches: [main]
workflow_dispatch:

# TEMPORARY (backlog merge): the type check, tests and build are set to
# continue-on-error so a red result cannot block merging the open contributor
# PRs. The steps still run and their output is still in the logs — only the
# gate is lifted.
#
# The workflow is deliberately NOT deleted: if a branch protection rule requires
# the "build" check, removing the workflow means the check never reports at all
# and merges stay blocked permanently. Keeping it green is what actually unblocks.
#
# TO RESTORE: delete the four `continue-on-error: true` lines below.
jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -33,22 +23,10 @@ jobs:
run: npm ci

- name: Type check
continue-on-error: true
run: npx tsc --noEmit

- name: Test
continue-on-error: true
run: npm test

- name: Build
continue-on-error: true
run: npm run build

# Reports the real status without failing the job, so the gate stays open
# while the actual state of the branch is still visible at a glance.
- name: Report status
continue-on-error: true
run: |
echo "::notice::CI gating is temporarily disabled for the backlog merge."
npx tsc --noEmit 2>&1 | grep -c "error TS" \
| xargs -I{} echo "::notice::tsc reports {} error(s)"
12 changes: 10 additions & 2 deletions __tests__/database-writer-pool-migration-hooks.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import Database from "better-sqlite3";
import { setDb, runMigrations, closeDb } from "../src/indexer/db.js";
import {
setDb,
runMigrations,
closeDb,
getShippedMigrationVersions,
} from "../src/indexer/db.js";
import {
WriterPoolSchemaError,
assertWriterPoolSchemaReady,
Expand Down Expand Up @@ -63,7 +68,10 @@ describe("database_writer_pool – migration verification hooks (#331)", () => {
const report = verifyWriterPoolSchema();

expect(report.valid).toBe(false);
expect(report.missingVersions).toEqual([5, 6, 7]);
// Derived from the shipped list rather than hardcoded, so adding a
// migration does not require editing this expectation.
const expectedMissing = getShippedMigrationVersions().filter((v) => v >= 5);
expect(report.missingVersions).toEqual(expectedMissing);
expect(report.issues.join(" ")).toContain("out of sync");
});

Expand Down
17 changes: 11 additions & 6 deletions __tests__/duplicate-prevention-index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,12 @@ describe("SQLite index optimization — duplicate_prevention lookups", () => {
.prepare("SELECT name, \"unique\" FROM pragma_index_list('events')")
.all() as Array<{ name: string; unique: number }>;

// Only the auto-index backing the UNIQUE constraint should exist - no
// speculative index was added for this ticket.
expect(indexes).toHaveLength(1);
expect(indexes[0].name).toBe("sqlite_autoindex_events_1");
expect(indexes[0].unique).toBe(1);
// Later migrations added non-unique lookup indexes to `events`, so this
// asserts what the ticket actually cares about: the UNIQUE constraint's
// auto-index is still the one and only unique index on the table.
const uniqueIndexes = indexes.filter((i) => i.unique === 1);
expect(uniqueIndexes).toHaveLength(1);
expect(uniqueIndexes[0].name).toBe("sqlite_autoindex_events_1");

const columns = testDb
.prepare("SELECT name FROM pragma_index_info('sqlite_autoindex_events_1') ORDER BY seqno")
Expand Down Expand Up @@ -108,7 +109,11 @@ describe("SQLite index optimization — duplicate_prevention lookups", () => {
.all("C1", 10, 0) as QueryPlanRow[];

const detail = plan.map((row) => row.detail).join(" | ");
expect(detail).toContain("USING INDEX sqlite_autoindex_events_1");
// Later migrations gave the planner a better-suited composite index for
// this predicate, so assert the property that matters — the lookup is
// index-backed rather than a full table scan — instead of naming one.
expect(detail).toMatch(/USING (COVERING )?INDEX/);
expect(detail).not.toMatch(/SCAN events\b/);
});
});

Expand Down
57 changes: 35 additions & 22 deletions __tests__/event-type-filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,31 @@ function makeSuccessResult(
};
}

/**
* Stand-in for the slice of `Server` that `fetchEventsWithRetry` needs.
*
* A bare `jest.fn()` infers `Mock<UnknownFunction>`, which makes
* `mockResolvedValue` expect `never` and leaves the object unassignable to the
* `Pick<Server, "getEvents">` parameter. Typing the mock explicitly keeps both
* the call sites and the `.mock.calls` assertions well typed.
*/
type MockEventsFn = jest.Mock<(params: GetEventsParams) => Promise<RpcGetEventsResult>>;
type MockServer = Parameters<typeof fetchEventsWithRetry>[0] & {
getEvents: MockEventsFn;
};

/** Build a mock `Server.getEvents` that returns `result`. */
function makeSuccessServer(result: RpcGetEventsResult = makeSuccessResult()) {
return {
getEvents: jest.fn().mockResolvedValue(result),
};
function makeSuccessServer(result: RpcGetEventsResult = makeSuccessResult()): MockServer {
const getEvents = jest.fn() as MockEventsFn;
getEvents.mockResolvedValue(result);
return { getEvents } as unknown as MockServer;
}

/** Build a mock `Server.getEvents` that always throws `err`. */
function makeFailingServer(err: Error) {
return {
getEvents: jest.fn().mockRejectedValue(err),
};
function makeFailingServer(err: Error): MockServer {
const getEvents = jest.fn() as MockEventsFn;
getEvents.mockRejectedValue(err);
return { getEvents } as unknown as MockServer;
}

/**
Expand All @@ -58,13 +71,13 @@ function makePartialFailServer(
successResult: RpcGetEventsResult = makeSuccessResult(1)
) {
let calls = 0;
return {
getEvents: jest.fn().mockImplementation(() => {
calls += 1;
if (calls <= failCount) return Promise.reject(err);
return Promise.resolve(successResult);
}),
};
const getEvents = jest.fn() as MockEventsFn;
getEvents.mockImplementation(() => {
calls += 1;
if (calls <= failCount) return Promise.reject(err);
return Promise.resolve(successResult);
});
return { getEvents } as unknown as MockServer;
}

const BASE_PARAMS: GetEventsParams = {
Expand Down Expand Up @@ -196,7 +209,7 @@ describe("fetchEventsWithRetry – success path", () => {

await fetchEventsWithRetry(server, BASE_PARAMS, { sleep: noopSleep });

const callArgs = server.getEvents.mock.calls[0][0] as {
const callArgs = server.getEvents.mock.calls[0][0] as unknown as {
filters: Array<{ topics: string[][] }>;
};
expect(callArgs.filters[0].topics[0]).toEqual([...EVENT_TYPES]);
Expand All @@ -212,7 +225,7 @@ describe("fetchEventsWithRetry – success path", () => {

await fetchEventsWithRetry(server, params, { sleep: noopSleep });

const callArgs = server.getEvents.mock.calls[0][0] as {
const callArgs = server.getEvents.mock.calls[0][0] as unknown as {
startLedger: number;
limit: number;
filters: Array<{ contractIds: string[] }>;
Expand All @@ -231,7 +244,7 @@ describe("fetchEventsWithRetry – success path", () => {
{ sleep: noopSleep }
);

const callArgs = server.getEvents.mock.calls[0][0] as { limit: number };
const callArgs = server.getEvents.mock.calls[0][0] as unknown as { limit: number };
expect(callArgs.limit).toBe(100);
});

Expand Down Expand Up @@ -581,7 +594,7 @@ describe("fetchEventsWithRetry – retry frequency increases up to max attempts"
fakeNow += 1;
return Promise.reject(err);
}),
};
} as unknown as MockServer;

await fetchEventsWithRetry(server, BASE_PARAMS, {
maxAttempts: MAX_ATTEMPTS,
Expand All @@ -603,7 +616,7 @@ describe("fetchEventsWithRetry – retry frequency increases up to max attempts"
callCount++;
return Promise.reject(err);
}),
};
} as unknown as MockServer;

const maxAttempts = 4;

Expand Down Expand Up @@ -634,7 +647,7 @@ describe("fetchEventsWithRetry – mixed error scenarios", () => {
if (callCount <= 2) return Promise.reject(connectionErr);
return Promise.reject(nonConnectionErr);
}),
};
} as unknown as MockServer;

await expect(
fetchEventsWithRetry(server, BASE_PARAMS, { sleep: noopSleep })
Expand All @@ -656,7 +669,7 @@ describe("fetchEventsWithRetry – mixed error scenarios", () => {
if (callCount === 1) return Promise.reject(connErr);
return Promise.reject(badErr);
}),
};
} as unknown as MockServer;

await expect(
fetchEventsWithRetry(server, BASE_PARAMS, { sleep: noopSleep })
Expand Down
1 change: 0 additions & 1 deletion __tests__/failover-recovery-poll-diagnostics.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { jest } from "@jest/globals";
import Database from "better-sqlite3";
import { jest } from "@jest/globals";
import { setDb, runMigrations } from "../src/indexer/db.js";
import {
initializeNodeHealthTables,
Expand Down
6 changes: 1 addition & 5 deletions __tests__/indexer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,9 @@ describe("Indexer Database", () => {
});

it("does not re-apply already-applied migrations (idempotent)", () => {
const before = testDb
.prepare("SELECT version FROM schema_migrations ORDER BY version")
.all() as Array<{ version: number }>;

// Running again should not throw and should not duplicate rows
const before = testDb
.prepare("SELECT version FROM schema_migrations")
.prepare("SELECT version FROM schema_migrations ORDER BY version")
.all() as Array<{ version: number }>;

runMigrations();
Expand Down
1 change: 0 additions & 1 deletion __tests__/sqlite-schema-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
} from "../src/indexer/db.js";
import { jest } from "@jest/globals";
import logger from "../src/utils/logger.js";
import { SCHEMA_MANAGER_INDEXES } from "../src/indexer/db.js";

describe("SQLite Schema Manager – in-memory integration tests", () => {
let testDb: Database.Database;
Expand Down
8 changes: 8 additions & 0 deletions __tests__/sqlite_vacuum_cleaner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,14 @@ describe("sqlite_vacuum_cleaner — failure alerting (#347)", () => {
describe("runVacuumCleanup integration", () => {
let testDb: Database.Database;

// Near-zero backoff so the retry path is exercised without real delays.
const fastConfig = {
maxRetries: 3,
initialBackoffMs: 1,
backoffMultiplier: 2,
maxBackoffMs: 5,
};

beforeEach(() => {
testDb = new Database(":memory:");
setDb(testDb);
Expand Down
6 changes: 6 additions & 0 deletions src/indexer/database-writer-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
getDb,
getLastIndexedLedger,
getShippedMigrationVersions,
insertEvent,
verifySchemaIntegrity,
verifySchemaUpToDate,
type EventRow,
Expand Down Expand Up @@ -1531,6 +1532,11 @@ export function resetWriterPoolStartState(): void {
lastSchemaReport = null;
migrationHooks.clear();
resetWriterPoolHistoricalRangeConfig();

// The queue's "already persisted" cache describes one specific database.
// Carrying it across a restart (or a setDb swap) would make the pool skip
// inserts for rows the new database has never seen.
defaultEventQueue.reset();
}

// ---------------------------------------------------------------------------
Expand Down
Loading
Loading