diff --git a/tests/hash-backend-integration/package.json b/tests/hash-backend-integration/package.json
index 8df50911c8a..3eb829df0b1 100644
--- a/tests/hash-backend-integration/package.json
+++ b/tests/hash-backend-integration/package.json
@@ -10,7 +10,7 @@
"fix:eslint": "eslint --fix .",
"lint:eslint": "eslint --report-unused-disable-directives .",
"lint:tsc": "tsc --noEmit",
- "test:integration": "vitest --run"
+ "test:integration": "vitest --run && vitest --run --config vitest.snapshot.config.ts"
},
"dependencies": {
"@apps/hash-api": "workspace:*",
diff --git a/tests/hash-backend-integration/src/tests/admin-server.ts b/tests/hash-backend-integration/src/tests/admin-server.ts
index 5ae6dbbefe1..d884473f3e4 100644
--- a/tests/hash-backend-integration/src/tests/admin-server.ts
+++ b/tests/hash-backend-integration/src/tests/admin-server.ts
@@ -6,6 +6,25 @@ import { StatusCode } from "@local/status";
import type { GraphStatus } from "@rust/hash-graph-type-defs/typescript/status";
+/**
+ * Throw unless running in the snapshot group of the backend integration
+ * tests.
+ *
+ * Destructive graph operations wipe the shared system graph that the seeded
+ * group (`vitest.config.ts`) seeds once per run via `globalSetup`, so they
+ * may only run in the snapshot group (`vitest.snapshot.config.ts`), which
+ * runs as a separate vitest invocation after the seeded group and owns the
+ * wipe. The snapshot group's config sets the `HASH_TEST_GROUP` marker checked
+ * here.
+ */
+const assertRunningInSnapshotGroup = (operation: string) => {
+ if (process.env.HASH_TEST_GROUP !== "snapshot") {
+ throw new Error(
+ `\`${operation}\` wipes the graph and may only run in the snapshot group of the backend integration tests, which runs as a separate vitest invocation after the seeded group so it cannot destroy the system graph seeded by \`globalSetup\`. To make a test file destructive, place it under \`src/tests/subgraph/\` so that \`vitest.snapshot.config.ts\` picks it up.`,
+ );
+ }
+};
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const port = process.env.HASH_GRAPH_ADMIN_PORT || "4001";
@@ -96,8 +115,13 @@ export const deleteEntities = async () => {
/**
* Restore a snapshot from a file.
+ *
+ * May only be called from the snapshot group of the backend integration
+ * tests (`vitest.snapshot.config.ts`).
*/
export const restoreSnapshot = async (snapshotPath: string) => {
+ assertRunningInSnapshotGroup("restoreSnapshot");
+
await fetch(`http://127.0.0.1:${port}/snapshot`, {
method: "POST",
body: createReadStream(snapshotPath),
@@ -137,8 +161,13 @@ export const deleteUser = async (
* Reset the Graph.
*
* This is a convenience function for deleting all entities, entity types, property types, data types, and accounts.
+ *
+ * May only be called from the snapshot group of the backend integration
+ * tests (`vitest.snapshot.config.ts`).
*/
export const resetGraph = async () => {
+ assertRunningInSnapshotGroup("resetGraph");
+
await deleteEntities();
await deleteEntityTypes();
await deletePropertyTypes();
diff --git a/tests/hash-backend-integration/tsconfig.json b/tests/hash-backend-integration/tsconfig.json
index 8ff51e490d6..31ae08d6037 100644
--- a/tests/hash-backend-integration/tsconfig.json
+++ b/tests/hash-backend-integration/tsconfig.json
@@ -1,4 +1,9 @@
{
"extends": "@local/tsconfig/legacy-base-tsconfig-to-refactor.json",
- "include": ["./src/", "codegen.config.ts", "vitest.config.ts"]
+ "include": [
+ "./src/",
+ "codegen.config.ts",
+ "vitest.config.ts",
+ "vitest.snapshot.config.ts"
+ ]
}
diff --git a/tests/hash-backend-integration/vitest.config.ts b/tests/hash-backend-integration/vitest.config.ts
index 83ada709987..9339a6e9bcd 100644
--- a/tests/hash-backend-integration/vitest.config.ts
+++ b/tests/hash-backend-integration/vitest.config.ts
@@ -1,29 +1,48 @@
///
import { defineConfig } from "vitest/config";
-import { BaseSequencer } from "vitest/node";
-import type { TestSpecification } from "vitest/node";
+import type { TestUserConfig } from "vitest/config";
/**
- * The subgraph tests reset the graph and restore standalone snapshots,
- * destroying the shared system graph seeded once per run by `globalSetup`.
- * Sort them after all other test files so that they cannot break tests which
- * rely on the shared seed – the next run's `globalSetup` re-seeds the graph
- * from scratch.
+ * The backend integration tests are split into two groups which run as
+ * separate, sequential vitest invocations (see the `test:integration` script
+ * in `package.json`):
+ *
+ * - the seeded group (this config, `src/tests/graph/`): runs against the
+ * shared system graph seeded once per run by `globalSetup` and must never
+ * wipe it, and
+ * - the snapshot group (`vitest.snapshot.config.ts`, `src/tests/subgraph/`):
+ * wipes the graph and restores standalone snapshots, so it runs after the
+ * seeded group.
+ *
+ * These test options are shared between the two configs.
*/
-class DestructiveTestsLastSequencer extends BaseSequencer {
- override async sort(files: TestSpecification[]) {
- const isDestructive = (file: TestSpecification) =>
- file.moduleId.includes("/src/tests/subgraph/");
-
- const sorted = await super.sort(files);
-
- return [
- ...sorted.filter((file) => !isDestructive(file)),
- ...sorted.filter(isDestructive),
- ];
- }
-}
+export const sharedTestConfig = {
+ coverage: {
+ enabled: process.env.TEST_COVERAGE === "true",
+ provider: "istanbul",
+ reporter: ["lcov", "text"],
+ include: ["**/*.{c,m,}{j,t}s{x,}"],
+ exclude: ["**/node_modules/**", "**/dist/**"],
+ },
+ setupFiles: [
+ "@local/hash-backend-utils/environment",
+ "./src/tests/setup-opentelemetry.ts",
+ ],
+ environment: "node",
+ testTimeout: 60_000,
+ hookTimeout: 120_000,
+ sequence: {
+ hooks: "list",
+ },
+ /**
+ * These integration tests share a single graph instance, so running files
+ * in parallel causes graph state races.
+ */
+ fileParallelism: false,
+ maxWorkers: 1,
+ maxConcurrency: 1,
+} satisfies TestUserConfig;
export default defineConfig({
plugins: [],
@@ -31,35 +50,8 @@ export default defineConfig({
target: "esnext",
},
test: {
- coverage: {
- enabled: process.env.TEST_COVERAGE === "true",
- provider: "istanbul",
- reporter: ["lcov", "text"],
- include: ["**/*.{c,m,}{j,t}s{x,}"],
- exclude: ["**/node_modules/**", "**/dist/**"],
- },
+ ...sharedTestConfig,
globalSetup: ["./src/tests/global-setup.ts"],
- setupFiles: [
- "@local/hash-backend-utils/environment",
- "./src/tests/setup-opentelemetry.ts",
- ],
- include: [
- "src/tests/graph/**/*.test.ts",
- "src/tests/subgraph/**/*.test.ts",
- ],
- environment: "node",
- testTimeout: 60_000,
- hookTimeout: 120_000,
- sequence: {
- hooks: "list",
- sequencer: DestructiveTestsLastSequencer,
- },
- /**
- * These integration tests share a single graph instance, so running files
- * in parallel causes graph state races.
- */
- fileParallelism: false,
- maxWorkers: 1,
- maxConcurrency: 1,
+ include: ["src/tests/graph/**/*.test.ts"],
},
});
diff --git a/tests/hash-backend-integration/vitest.snapshot.config.ts b/tests/hash-backend-integration/vitest.snapshot.config.ts
new file mode 100644
index 00000000000..c3f6530a690
--- /dev/null
+++ b/tests/hash-backend-integration/vitest.snapshot.config.ts
@@ -0,0 +1,44 @@
+///
+import { defineConfig } from "vitest/config";
+
+import { sharedTestConfig } from "./vitest.config";
+
+/**
+ * Config for the snapshot group of the backend integration tests: the test
+ * files under `src/tests/subgraph/` wipe the graph via `resetGraph` and
+ * restore standalone snapshots via `restoreSnapshot`, so they cannot share
+ * the system graph seeded by the seeded group's `globalSetup`.
+ *
+ * The `test:integration` script in `package.json` runs this config as a
+ * separate vitest invocation after the seeded group (`vitest.config.ts`), so
+ * the seeded group structurally cannot lose its seed to these tests. There is
+ * deliberately no `globalSetup` here – each test file restores the snapshot
+ * it needs, and the next seeded run re-seeds the graph from scratch.
+ *
+ * To make a test file destructive, place it under `src/tests/subgraph/` so
+ * this config picks it up. Destructive graph operations refuse to run outside
+ * this group: `resetGraph`/`restoreSnapshot` in `src/tests/admin-server.ts`
+ * throw unless the `HASH_TEST_GROUP` marker below is set.
+ */
+export default defineConfig({
+ plugins: [],
+ build: {
+ target: "esnext",
+ },
+ test: {
+ ...sharedTestConfig,
+ coverage: {
+ ...sharedTestConfig.coverage,
+ /**
+ * Written next to the seeded group's `./coverage` – vitest cleans its
+ * reports directory at the start of a run, so sharing one directory
+ * would discard the seeded group's report.
+ */
+ reportsDirectory: "./coverage-snapshot",
+ },
+ include: ["src/tests/subgraph/**/*.test.ts"],
+ env: {
+ HASH_TEST_GROUP: "snapshot",
+ },
+ },
+});