dify/knowledge-fs/scripts/semantic-compilation-rollout.test.mjs

187 lines
6.9 KiB
JavaScript

import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { readFile } from "node:fs/promises";
import { createServer } from "node:http";
import { test } from "node:test";
const rootPackage = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
const scriptUrl = new URL("./semantic-compilation-rollout.mjs", import.meta.url);
const spaceId = "018f0d60-7a49-7cc2-9c1b-5b36f18f2c40";
const documentId = "018f0d60-7a49-7cc2-9c1b-5b36f18f2c41";
test("static rollout evidence is safe and includes migration 0043", async () => {
const result = await runScript({ SEMANTIC_ROLLOUT_MODE: "static" });
assert.equal(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.equal(payload.mode, "static");
assert.equal(payload.staticEvidence.migrationId, "0043_semantic_generation_receipts");
});
test("mutating rollout modes require an exact space-scoped confirmation", async () => {
const result = await runScript({
SEMANTIC_ROLLOUT_DOCUMENT_IDS: documentId,
SEMANTIC_ROLLOUT_MODE: "canary",
SEMANTIC_ROLLOUT_SPACE_ID: spaceId,
});
assert.notEqual(result.code, 0);
assert.match(result.stderr, /SEMANTIC_ROLLOUT_APPLY=1 is required/u);
});
test("preflight is read-only, bounded, and reports durable state", async (context) => {
const requests = [];
const server = createServer((request, response) => {
requests.push({ method: request.method, url: request.url });
response.setHeader("content-type", "application/json");
if (request.url === "/health") {
response.end(JSON.stringify({ components: { database: true, objectStorage: true } }));
return;
}
if (request.url?.endsWith("/settings")) {
response.end(JSON.stringify({ configurationState: "active" }));
return;
}
if (request.url?.includes("/documents?")) {
response.end(JSON.stringify({ items: [{ id: documentId }] }));
return;
}
if (request.url?.includes("/background-tasks?")) {
response.end(JSON.stringify({ items: [] }));
return;
}
response.statusCode = 404;
response.end(JSON.stringify({ error: "not found" }));
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
context.after(() => new Promise((resolve) => server.close(resolve)));
const address = server.address();
assert(address && typeof address === "object");
const result = await runScript({
SEMANTIC_ROLLOUT_API_BASE: `http://127.0.0.1:${address.port}`,
SEMANTIC_ROLLOUT_MODE: "preflight",
SEMANTIC_ROLLOUT_SPACE_ID: spaceId,
});
assert.equal(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.equal(payload.preflight.configurationState, "active");
assert.equal(payload.preflight.documentCount, 1);
assert.deepEqual(
requests.map((request) => request.method),
["GET", "GET", "GET", "GET"],
);
});
test("canary reindexes only explicit documents and verifies the published outline", async (context) => {
const requests = [];
const server = createServer(async (request, response) => {
let body = "";
for await (const chunk of request) body += chunk;
requests.push({ body, method: request.method, url: request.url });
response.setHeader("content-type", "application/json");
if (request.url === "/health") {
response.end(JSON.stringify({ components: { database: true, objectStorage: true } }));
return;
}
if (request.url?.endsWith("/settings")) {
response.end(JSON.stringify({ configurationState: "active" }));
return;
}
if (request.url?.includes("/documents?")) {
response.end(JSON.stringify({ items: [{ id: documentId }] }));
return;
}
if (request.url?.includes("/background-tasks?")) {
response.end(JSON.stringify({ items: [] }));
return;
}
if (request.method === "POST" && request.url?.endsWith("/documents/bulk/reindex")) {
response.statusCode = 202;
response.end(
JSON.stringify({
bulkJobId: "bulk-canary",
items: [
{
asset: { id: documentId },
status: "queued",
statusUrl: "/jobs/canary",
},
],
}),
);
return;
}
if (request.url === "/jobs/canary") {
response.end(JSON.stringify({ stage: "published" }));
return;
}
if (request.url?.endsWith(`/documents/${documentId}/outline`)) {
response.end(
JSON.stringify({
nodes: [{ sectionPath: ["Canary"], sourceNodeIds: ["node-1"] }],
}),
);
return;
}
response.statusCode = 404;
response.end(JSON.stringify({ error: "not found" }));
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
context.after(() => new Promise((resolve) => server.close(resolve)));
const address = server.address();
assert(address && typeof address === "object");
const result = await runScript({
SEMANTIC_ROLLOUT_API_BASE: `http://127.0.0.1:${address.port}`,
SEMANTIC_ROLLOUT_APPLY: "1",
SEMANTIC_ROLLOUT_CONFIRM: `semantic:canary:${spaceId}`,
SEMANTIC_ROLLOUT_DOCUMENT_IDS: documentId,
SEMANTIC_ROLLOUT_MAX_POLLS: "1",
SEMANTIC_ROLLOUT_MODE: "canary",
SEMANTIC_ROLLOUT_SPACE_ID: spaceId,
});
assert.equal(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.equal(payload.result.documentsQueued, 1);
const mutation = requests.find((request) => request.method === "POST");
assert.deepEqual(JSON.parse(mutation?.body ?? "{}"), { documentIds: [documentId] });
});
test("package scripts expose explicit rollout phases and keep their tests in check", () => {
assert.equal(
rootPackage.scripts["semantic:rollout:static"],
"node scripts/semantic-compilation-rollout.mjs",
);
assert.match(
rootPackage.scripts["semantic:rollout:preflight"],
/SEMANTIC_ROLLOUT_MODE=preflight/u,
);
assert.match(rootPackage.scripts["semantic:rollout:canary"], /SEMANTIC_ROLLOUT_MODE=canary/u);
assert.match(rootPackage.scripts["semantic:rollout:backfill"], /SEMANTIC_ROLLOUT_MODE=backfill/u);
assert.match(rootPackage.scripts["semantic:rollout:rollback"], /SEMANTIC_ROLLOUT_MODE=rollback/u);
assert.equal(
rootPackage.scripts["semantic:rollout:test"],
"node --test scripts/semantic-compilation-rollout.test.mjs",
);
assert.match(rootPackage.scripts.check, /semantic:rollout:test/u);
});
function runScript(extraEnv) {
return new Promise((resolve) => {
const child = spawn(process.execPath, [scriptUrl.pathname], {
env: { ...process.env, ...extraEnv },
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
child.on("close", (code) => resolve({ code, stderr, stdout }));
});
}