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
2 changes: 1 addition & 1 deletion .github/workflows/sync-surface-verification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ name: Sync surface verification
# them continuously. The evidence existed; nothing carried it.
#
# The snapshot is COMMITTED rather than fetched during the build because
# tests/artifacts.test.ts asserts the artifact build is byte-identical across
# tests/artifacts-build-determinism.test.ts asserts the artifact build is byte-identical across
# rebuilds, and a network call inside the build would end that -- as well as
# making every build depend on the API being up. Same posture as
# registry/verification/promotions.json.
Expand Down
2 changes: 1 addition & 1 deletion scripts/sync-surface-verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// the evidence comes from the health prober rather than verify-candidates.ts,
// and for the promotion bar itself.
//
// COMMITTED, not fetched at build time. tests/artifacts.test.ts asserts the
// COMMITTED, not fetched at build time. tests/artifacts-build-determinism.test.ts asserts the
// artifact build is byte-identical across rebuilds; a network call inside the
// build would end that, and would also make every build depend on the API being
// up. Same posture as registry/verification/promotions.json.
Expand Down
89 changes: 89 additions & 0 deletions tests/artifacts-build-determinism.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Split out of tests/artifacts.test.ts (#8937 follow-up). Each test here runs a
// full scripts/build-artifacts.ts, which is 7-25s of CI on its own; six of them
// sharing one file made artifacts.test.ts 118.8s of a 144.3s pass and the floor
// for the whole shared-registry run, because vitest parallelizes across FILES
// and never within one. One file per build lets them run on separate workers.
//
// The per-file sandbox clone is ~0.6s against a ~20s build, so the split pays
// for itself many times over.
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import path from "node:path";
import { test } from "vitest";

import { createArtifactBuildHarness } from "./helpers/artifact-build-harness.ts";
import type { Row } from "./row-type.ts";

const harness = createArtifactBuildHarness("artifacts-build-determinism");

// #510 refactor invariant: the artifact build is deterministic, so two
// consecutive builds (epoch timestamp, no METAGRAPH_BUILD_TIMESTAMP) must emit a
// byte-identical R2 staging tree. This is the regression guard that lets the
// build-artifacts/lib decomposition stay safe — any future code-motion that
// silently reorders keys, changes a number, or drops an artifact flips this hash.
// It deliberately compares the whole staging tree (not a hardcoded golden), so it
// never needs touching when the committed source data legitimately refreshes.
function digestArtifactTree(root: string) {
const hash = createHash("sha256");
for (const file of harness
.walkFiles(root)
.filter((file) => path.basename(file) !== ".DS_Store") // OS noise, not an artifact
.sort()) {
hash.update(path.relative(root, file));
hash.update("\0");
hash.update(readFileSync(file));
hash.update("\0");
}
return hash.digest("hex");
}

test("artifact build is deterministic (byte-identical across rebuilds)", () => {
const supportArtifacts = harness.snapshotSupportArtifacts();
const buildEnv: Row = {
...harness.env,
METAGRAPH_PRESERVE_PROBE_HEALTH: "1",
};
delete buildEnv.METAGRAPH_BUILD_TIMESTAMP; // force the reproducible epoch
const runBuild = () =>
execFileSync(process.execPath, ["scripts/build-artifacts.ts"], {
cwd: harness.scriptCwd,
encoding: "utf8",
env: buildEnv as unknown as NodeJS.ProcessEnv,
stdio: "pipe",
});
try {
runBuild();
const firstDigest = digestArtifactTree(harness.r2StagingRoot);

// The build must actually produce the artifacts whose derivation was
// extracted to scripts/lib/ — a broken import would yield empty/missing
// output, which this asserts before the cheaper hash comparison.
for (const relativePath of [
"endpoints.json",
"rpc-endpoints.json",
"economics.json",
"endpoint-pools.json",
"endpoint-incidents.json",
]) {
const artifact = harness.readArtifact(relativePath);
assert.ok(
artifact && typeof artifact === "object",
`${relativePath} should build to a non-empty object`,
);
}

runBuild();
const secondDigest = digestArtifactTree(harness.r2StagingRoot);

assert.equal(
secondDigest,
firstDigest,
"two consecutive builds must emit a byte-identical R2 staging tree",
);
} finally {
runBuild();
harness.restoreSupportArtifacts(supportArtifacts);
}
}, 120_000);
109 changes: 109 additions & 0 deletions tests/artifacts-build-health.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Split out of tests/artifacts.test.ts (#8937 follow-up). Each test here runs a
// full scripts/build-artifacts.ts, which is 7-25s of CI on its own; six of them
// sharing one file made artifacts.test.ts 118.8s of a 144.3s pass and the floor
// for the whole shared-registry run, because vitest parallelizes across FILES
// and never within one. One file per build lets them run on separate workers.
//
// The per-file sandbox clone is ~0.6s against a ~20s build, so the split pays
// for itself many times over.
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import { test } from "vitest";

import { createArtifactBuildHarness } from "./helpers/artifact-build-harness.ts";
import type { Row } from "./row-type.ts";

const harness = createArtifactBuildHarness("artifacts-build-health");

test("artifact build does not preserve forged endpoint index health", () => {
const endpointsPath = harness.artifactFilePath("endpoints.json");
// Sandbox-rooted, for the same reason the harness roots its support-artifact
// paths. Relative, this pointed at the real repo, which also made the setup a
// no-op against its own purpose: the build reads the SANDBOX's .cache, so
// clearing the real one never removed the health cache the rebuild consults.
const cachePath = path.join(
harness.root,
".cache/metagraphed/health/latest.json",
);
const original = readFileSync(endpointsPath, "utf8");
const originalCache = existsSync(cachePath)
? readFileSync(cachePath, "utf8")
: null;
const supportArtifacts = harness.snapshotSupportArtifacts();
rmSync(cachePath, { force: true });
const tampered = JSON.parse(original);
const target = tampered.endpoints.find(
(endpoint: Row) => endpoint.public_safe === true,
);
assert(target, "expected a public-safe endpoint row to tamper");

target.health_source = "probe-derived";
target.monitoring_status = "monitored";
target.status = "ok";
target.classification = "live";
target.last_checked = "2999-01-01T00:00:00.000Z";
target.last_ok = "2999-01-01T00:00:00.000Z";
target.observed_at = "2999-01-01T00:00:00.000Z";
target.latency_ms = 7;
target.latest_block = 4242424242;
target.archive_support = true;

try {
writeFileSync(endpointsPath, `${JSON.stringify(tampered, null, 2)}\n`);
execFileSync(process.execPath, ["scripts/build-artifacts.ts"], {
cwd: harness.scriptCwd,
encoding: "utf8",
env: { ...harness.env, METAGRAPH_PRESERVE_PROBE_HEALTH: "1" },
stdio: "pipe",
});

const rebuilt = JSON.parse(readFileSync(endpointsPath, "utf8"));
const rebuiltTarget = rebuilt.endpoints.find(
(endpoint: Row) => endpoint.surface_id === target.surface_id,
);
assert.equal(rebuiltTarget.status, "unknown");
assert.equal(rebuiltTarget.classification, "unknown");
assert.equal(rebuiltTarget.last_checked, null);
assert.equal(rebuiltTarget.latency_ms, null);
assert.equal(rebuiltTarget.latest_block, null);
assert.equal(rebuiltTarget.archive_support, null);
assert.equal(rebuiltTarget.health_source, "missing-probe");
} finally {
writeFileSync(endpointsPath, original);
if (originalCache === null) {
rmSync(cachePath, { force: true });
} else {
writeFileSync(cachePath, originalCache);
}
execFileSync(process.execPath, ["scripts/build-artifacts.ts"], {
cwd: harness.scriptCwd,
encoding: "utf8",
env: {
...harness.env,
METAGRAPH_PRESERVE_PROBE_HEALTH: "1",
},
stdio: "pipe",
});
execFileSync(process.execPath, ["scripts/generate-types.ts"], {
cwd: harness.scriptCwd,
encoding: "utf8",
env: harness.env,
stdio: "pipe",
});
execFileSync(process.execPath, ["scripts/generate-client.ts", "--write"], {
cwd: harness.scriptCwd,
encoding: "utf8",
env: harness.env,
stdio: "pipe",
});
execFileSync(process.execPath, ["scripts/r2-manifest.ts", "--write"], {
cwd: harness.scriptCwd,
encoding: "utf8",
env: harness.env,
stdio: "pipe",
});
harness.restoreSupportArtifacts(supportArtifacts);
}
}, 120_000);
110 changes: 110 additions & 0 deletions tests/artifacts-build-schema-index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Split out of tests/artifacts.test.ts (#8937 follow-up). Each test here runs a
// full scripts/build-artifacts.ts, which is 7-25s of CI on its own; six of them
// sharing one file made artifacts.test.ts 118.8s of a 144.3s pass and the floor
// for the whole shared-registry run, because vitest parallelizes across FILES
// and never within one. One file per build lets them run on separate workers.
//
// The per-file sandbox clone is ~0.6s against a ~20s build, so the split pays
// for itself many times over.
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import {
cpSync,
existsSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { test } from "vitest";

import { createArtifactBuildHarness } from "./helpers/artifact-build-harness.ts";
import type { Row } from "./row-type.ts";

const harness = createArtifactBuildHarness("artifacts-build-schema-index");

test("artifact build preserves committed schema index without R2 schema details", () => {
const schemaIndexPath = harness.artifactFilePath("schemas/index.json");
const originalSchemaIndex = readFileSync(schemaIndexPath, "utf8");
const originalSchemaIndexJson = JSON.parse(originalSchemaIndex);
const supportArtifacts = harness.snapshotSupportArtifacts();
const backupDir = mkdtempSync(`${tmpdir()}/metagraphed-schema-r2-`);
const stagingBackup = `${backupDir}/metagraph-r2`;
const hadStagingRoot = existsSync(harness.r2StagingRoot);
if (hadStagingRoot) {
cpSync(harness.r2StagingRoot, stagingBackup, { recursive: true });
}

assert.equal(originalSchemaIndexJson.source, "openapi-snapshot");
assert.equal(originalSchemaIndexJson.schemas.length > 0, true);

try {
rmSync(harness.r2StagingRoot, { recursive: true, force: true });
execFileSync(process.execPath, ["scripts/build-artifacts.ts"], {
cwd: harness.scriptCwd,
encoding: "utf8",
env: harness.env,
stdio: "pipe",
});

const rebuiltSchemaIndex = readFileSync(schemaIndexPath, "utf8");
assert.deepEqual(JSON.parse(rebuiltSchemaIndex), originalSchemaIndexJson);
} finally {
writeFileSync(schemaIndexPath, originalSchemaIndex);
rmSync(harness.r2StagingRoot, { recursive: true, force: true });
if (hadStagingRoot) {
cpSync(stagingBackup, harness.r2StagingRoot, { recursive: true });
}
harness.restoreSupportArtifacts(supportArtifacts);
rmSync(backupDir, { recursive: true, force: true });
}
}, 120_000);

test("artifact build accepts an OpenAPI-vendor JSON content-type for a captured schema entry", () => {
const schemaIndexPath = harness.artifactFilePath("schemas/index.json");
const originalSchemaIndex = readFileSync(schemaIndexPath, "utf8");
const supportArtifacts = harness.snapshotSupportArtifacts();
const schemaIndex = JSON.parse(originalSchemaIndex);
const indexTarget = schemaIndex.schemas?.find(
(schema: Row) => schema.status === "captured",
);
assert(indexTarget, "expected a captured schema index entry to retype");

// A real subnet (SN-71 Leadpoet) serves its OpenAPI document as
// application/vnd.oai.openapi+json rather than plain application/json -- a
// spec-valid, OAI-registered media type. schemaIndexEntryMatchesSurface used
// to require an exact "application/json" match, so this one entry failed the
// reconciler's forgery/staleness guard and wholesale-discarded the entire
// committed index down to an empty placeholder (metagraphed#6411).
indexTarget.content_type = "application/vnd.oai.openapi+json; charset=utf-8";

try {
writeFileSync(schemaIndexPath, `${JSON.stringify(schemaIndex, null, 2)}\n`);
execFileSync(process.execPath, ["scripts/build-artifacts.ts"], {
cwd: harness.scriptCwd,
encoding: "utf8",
env: harness.env,
stdio: "pipe",
});

const rebuiltSchemaIndex = JSON.parse(
readFileSync(schemaIndexPath, "utf8"),
);
assert.equal(rebuiltSchemaIndex.source, "openapi-snapshot");
const rebuiltTarget = rebuiltSchemaIndex.schemas.find(
(schema: Row) => schema.surface_id === indexTarget.surface_id,
);
assert.equal(rebuiltTarget?.content_type, indexTarget.content_type);
assert.equal(rebuiltTarget?.hash, indexTarget.hash);
} finally {
writeFileSync(schemaIndexPath, originalSchemaIndex);
execFileSync(process.execPath, ["scripts/build-artifacts.ts"], {
cwd: harness.scriptCwd,
encoding: "utf8",
env: harness.env,
stdio: "pipe",
});
harness.restoreSupportArtifacts(supportArtifacts);
}
}, 120_000);
Loading