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
16 changes: 11 additions & 5 deletions services/control-plane/src/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,17 @@ function firstDevnetMap(state: LoadedControlPlaneState, keys: string[]): Record<
}

function devnetBlocksArray(state: LoadedControlPlaneState): JsonObject[] {
const candidate = asJsonArray(state.devnet?.blocks).length > 0
? asJsonArray(state.devnet?.blocks)
: asJsonArray(state.devnetControlPlaneHandoff?.blocks).length > 0
? asJsonArray(state.devnetControlPlaneHandoff?.blocks)
: asJsonArray(state.devnetIndexerHandoff?.blocks);
const sources = [
asJsonArray(state.devnet?.blocks),
asJsonArray(state.devnetControlPlaneHandoff?.blocks),
asJsonArray(state.devnetIndexerHandoff?.blocks),
];
const candidate = sources.find((blocks) =>
blocks.some((entry) => {
const block = asJsonObject(entry);
return block !== null && stringList(block.txIds).length > 0;
}),
) ?? sources.find((blocks) => blocks.length > 0) ?? [];
return candidate
.map((entry) => asJsonObject(entry))
.filter((entry): entry is JsonObject => entry !== null);
Expand Down
14 changes: 12 additions & 2 deletions services/control-plane/src/smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,18 @@ import { loadControlPlaneState } from "./fixture-state.ts";
import type { ControlPlanePaths, JsonObject, RpcErrorResponse, RpcSuccessResponse } from "./types.ts";

function firstDevnetBlock(state: ReturnType<typeof loadControlPlaneState>): JsonObject {
const blocks = Array.isArray(state.devnet?.blocks) ? state.devnet.blocks : [];
const block = blocks[0];
const blocksResponse = dispatchJsonRpc(
{ jsonrpc: "2.0", id: "blocks-prefetch", method: "block_list", params: { limit: 10 } },
{ state },
) as RpcSuccessResponse;
const blocks = (blocksResponse.result as JsonObject).blocks;
const blockRows = Array.isArray(blocks) ? blocks : [];
const block = blockRows.find((entry) => {
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
return false;
}
return Array.isArray((entry as JsonObject).txIds) && ((entry as JsonObject).txIds as unknown[]).length > 0;
}) ?? blockRows[0];
if (block === null || typeof block !== "object" || Array.isArray(block)) {
throw new Error("control-plane smoke requires at least one local devnet block");
}
Expand Down
29 changes: 29 additions & 0 deletions services/control-plane/test/control-plane.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,35 @@ test("smoke client queries the complete local lifecycle surface", () => {
rmSync(dir, { recursive: true, force: true });
});

test("smoke client ignores stale local devnet blocks without transactions", () => {
const dir = mkdtempSync(join(tmpdir(), "flowmemory-control-plane-stale-devnet-"));
try {
const staleDevnetPath = join(dir, "state.json");
writeFileSync(staleDevnetPath, JSON.stringify({
schema: "flowmemory.local_devnet.state.v0",
blocks: [{
schema: "flowmemory.local_devnet.block.v0",
blockNumber: "1",
blockHash: "0x1909a47bfaaabbfe51d371173d550fcdaff1abaedeea1045bfb77a496bdb8695",
txIds: [],
receipts: [],
}],
}));

const smoke = runControlPlaneSmoke({
localDevnetPath: staleDevnetPath,
txIntakePath: join(dir, "transactions.ndjson"),
bridgeObservationIntakePath: join(dir, "bridge-observations.ndjson"),
});

assert.equal(smoke.schema, "flowmemory.control_plane.smoke.v0");
assert.equal(smoke.ok, true);
assert.equal(typeof (smoke.queried as Record<string, unknown>).txId, "string");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

test("HTTP server exposes browser-safe health and state endpoints", async () => {
const server = startControlPlaneServer({ host: "127.0.0.1", port: 0 });

Expand Down
Loading