Files
dify/knowledge-fs/scripts/compose-apps.test.mjs

194 lines
8.8 KiB
JavaScript

import assert from "node:assert/strict";
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { parse, parseAllDocuments } from "yaml";
import { materializeDifyComposeEnv } from "./dify-compose-config.mjs";
const compose = readFileSync(new URL("../infra/local/compose.yaml", import.meta.url), "utf8");
const difyComposeFiles = [
readFileSync(new URL("../../docker/docker-compose-template.yaml", import.meta.url), "utf8"),
readFileSync(new URL("../../docker/docker-compose.yaml", import.meta.url), "utf8"),
];
const difyApiEnv = readFileSync(
new URL("../../docker/envs/core-services/api.env.example", import.meta.url),
"utf8",
);
const difyKnowledgeFsEnv = readFileSync(
new URL("../../docker/envs/core-services/knowledge-fs.env.example", import.meta.url),
"utf8",
);
const kubernetesBaseline = readFileSync(
new URL("../infra/kubernetes/dify-integration-baseline.yaml", import.meta.url),
"utf8",
);
function serviceBlock(source, serviceName) {
const lines = source.split("\n");
const start = lines.findIndex((line) => line === ` ${serviceName}:`);
assert.notEqual(start, -1, `missing ${serviceName} service`);
const relativeEnd = lines
.slice(start + 1)
.findIndex((line) => /^ {2}[a-zA-Z0-9_-]+:$/.test(line));
const end = relativeEnd === -1 ? lines.length : start + 1 + relativeEnd;
return lines.slice(start, end).join("\n");
}
test("deployment Compose and Kubernetes artifacts are valid YAML", () => {
for (const source of [compose, ...difyComposeFiles]) {
const document = parse(source);
assert.equal(typeof document?.services, "object");
}
const documents = parseAllDocuments(kubernetesBaseline);
assert.equal(documents.length, 4);
for (const document of documents) {
assert.deepEqual(document.errors, []);
}
assert.deepEqual(
documents.map((document) => document.toJS().kind),
["ConfigMap", "Deployment", "Service", "NetworkPolicy"],
);
});
test("Dify Compose config materializes a missing ignored env without replacing user config", () => {
const directory = mkdtempSync(join(tmpdir(), "dify-compose-config-"));
const envPath = join(directory, ".env");
const examplePath = join(directory, ".env.example");
try {
writeFileSync(examplePath, "BASELINE=true\n");
const removeMaterializedEnv = materializeDifyComposeEnv({ envPath, examplePath });
assert.equal(readFileSync(envPath, "utf8"), "BASELINE=true\n");
removeMaterializedEnv();
assert.equal(existsSync(envPath), false);
writeFileSync(envPath, "USER_CONFIG=true\n");
const preserveUserEnv = materializeDifyComposeEnv({ envPath, examplePath });
preserveUserEnv();
assert.equal(readFileSync(envPath, "utf8"), "USER_CONFIG=true\n");
} finally {
rmSync(directory, { force: true, recursive: true });
}
});
test("app compose profile builds the API image with development-only static auth", () => {
assert.match(compose, /^ {2}api:$/m);
assert.match(compose, /^ {4}build:$/m);
assert.match(compose, /^ {6}context: \.\.\/\.\.$/m);
assert.match(compose, /^ {6}dockerfile: apps\/api\/Dockerfile$/m);
assert.match(compose, /^ {4}image: knowledge-fs-api:local$/m);
assert.match(compose, /^ {4}profiles: \["apps"\]$/m);
assert.match(compose, /^ {6}NODE_ENV: development$/m);
assert.match(compose, /^ {6}PORT: 8787$/m);
assert.match(
compose,
/^ {6}KNOWLEDGE_DEV_AUTH_TOKEN: \$\{KNOWLEDGE_DEV_AUTH_TOKEN:-dev-token\}$/m,
);
assert.match(
compose,
/^ {6}KNOWLEDGE_EMBEDDING_PROVIDER: \$\{KNOWLEDGE_EMBEDDING_PROVIDER:-\}$/m,
);
assert.match(compose, /^ {6}KNOWLEDGE_EMBEDDING_MODEL: \$\{KNOWLEDGE_EMBEDDING_MODEL:-\}$/m);
assert.match(compose, /^ {6}OPENAI_EMBEDDING_BASE_URL: \$\{OPENAI_EMBEDDING_BASE_URL:-\}$/m);
});
test("app compose profile waits for durable middleware readiness before API startup", () => {
assert.match(compose, /^ {4}depends_on:$/m);
assert.match(compose, /^ {6}postgres:$/m);
assert.match(compose, /^ {8}condition: service_healthy$/m);
assert.match(compose, /^ {6}minio-bootstrap:$/m);
assert.match(compose, /^ {8}condition: service_completed_successfully$/m);
assert.match(compose, /^ {6}unstructured:$/m);
assert.match(compose, /^ {8}condition: service_started$/m);
});
test("app compose profile uses service-local middleware URLs inside the API container", () => {
assert.match(
compose,
/^ {6}DATABASE_URL: postgresql:\/\/\$\{POSTGRES_USER:-knowledge_fs\}:\$\{POSTGRES_PASSWORD:-knowledge_fs\}@postgres:5432\/\$\{POSTGRES_DB:-knowledge_fs\}$/m,
);
assert.match(compose, /^ {6}MINIO_ENDPOINT: http:\/\/minio:9000$/m);
assert.match(compose, /^ {6}UNSTRUCTURED_API_URL: http:\/\/unstructured:8000$/m);
});
test("app compose profile builds Admin as a production image after API readiness", () => {
assert.match(compose, /^ {2}admin:$/m);
assert.match(compose, /^ {6}dockerfile: apps\/admin\/Dockerfile$/m);
assert.match(compose, /^ {4}image: knowledge-fs-admin:local$/m);
assert.match(compose, /^ {4}profiles: \["apps"\]$/m);
assert.match(compose, /^ {6}api:$/m);
assert.match(compose, /^ {8}condition: service_healthy$/m);
assert.match(compose, /^ {6}HOSTNAME: 0\.0\.0\.0$/m);
assert.match(compose, /^ {6}KNOWLEDGE_API_BASE_URL: http:\/\/api:8787$/m);
assert.match(compose, /^ {6}NEXT_PUBLIC_API_BASE_URL: http:\/\/localhost:\$\{API_PORT:-8788\}$/m);
assert.match(compose, /^ {6}NODE_ENV: production$/m);
assert.match(compose, /^ {6}PORT: 3000$/m);
assert.doesNotMatch(compose, /pnpm --filter @knowledge\/admin dev/);
assert.doesNotMatch(compose, /^ {6}- \.:\/workspace$/m);
assert.doesNotMatch(compose, /^ {2}pnpm-store:$/m);
});
test("Dify compose keeps the integrated KnowledgeFS API internal and disabled by profile", () => {
for (const difyCompose of difyComposeFiles) {
const knowledgeFs = serviceBlock(difyCompose, "knowledge_fs");
assert.match(knowledgeFs, /^ {4}profiles: \["knowledge-fs"\]$/m);
assert.match(knowledgeFs, /^ {6}context: \.\.\/knowledge-fs$/m);
assert.match(knowledgeFs, /^ {6}dockerfile: apps\/api\/Dockerfile$/m);
assert.match(knowledgeFs, /^ {4}expose:$/m);
assert.match(knowledgeFs, /^ {6}- "8787"$/m);
assert.doesNotMatch(knowledgeFs, /^ {4}ports:$/m);
assert.match(
knowledgeFs,
/^ {6}KNOWLEDGE_INTEGRATED_MODE_ENABLED: \$\{KNOWLEDGE_INTEGRATED_MODE_ENABLED:-true\}$/m,
);
assert.doesNotMatch(knowledgeFs, /^ {6}PLUGIN_DAEMON_(?:URL|KEY):/m);
assert.doesNotMatch(knowledgeFs, /^ {6}plugin_daemon:$/m);
assert.match(knowledgeFs, /http:\/\/127\.0\.0\.1:8787\/ready/);
assert.match(knowledgeFs, /^ {6}- default$/m);
}
assert.match(difyApiEnv, /^KNOWLEDGE_FS_ENABLED=\$\{KNOWLEDGE_FS_ENABLED:-false\}$/m);
});
test("deployment examples expose every guarded KnowledgeFS rollout capability as disabled", () => {
for (const variable of [
"KNOWLEDGE_FS_LIFECYCLE_WORKER_ENABLED",
"KNOWLEDGE_FS_INTEGRATED_PROVISION_READY",
"KNOWLEDGE_FS_LEGACY_ACL_FREEZE_READY",
]) {
assert.match(difyApiEnv, new RegExp(`^${variable}=false$`, "m"));
}
for (const variable of [
"KNOWLEDGE_FS_CAPABILITY_V2_ENABLED",
"KNOWLEDGE_INTEGRATED_MODE_ENABLED",
"KNOWLEDGE_LEGACY_ACL_READ_ONLY",
"KNOWLEDGE_LEGACY_AUTHORIZATION_REMOVED",
]) {
assert.match(difyKnowledgeFsEnv, new RegExp(`^${variable}=false$`, "m"));
}
assert.match(difyKnowledgeFsEnv, /^KNOWLEDGE_DIRECT_UPLOAD_ENABLED=off$/m);
assert.match(difyKnowledgeFsEnv, /^KNOWLEDGE_DIRECT_UPLOAD_ALLOWED_ORIGINS=$/m);
assert.match(difyKnowledgeFsEnv, /^KNOWLEDGE_DIRECT_STREAM_ENABLED=off$/m);
assert.match(kubernetesBaseline, /^ {2}KNOWLEDGE_INTEGRATED_MODE_ENABLED: "false"$/m);
assert.match(kubernetesBaseline, /^ {2}KNOWLEDGE_LEGACY_AUTHORIZATION_REMOVED: "false"$/m);
assert.match(kubernetesBaseline, /^ {2}KNOWLEDGE_DIRECT_UPLOAD_ENABLED: "off"$/m);
assert.match(kubernetesBaseline, /^ {2}KNOWLEDGE_DIRECT_UPLOAD_ALLOWED_ORIGINS: ""$/m);
assert.match(kubernetesBaseline, /^ {2}KNOWLEDGE_DIRECT_STREAM_ENABLED: "off"$/m);
});
test("Kubernetes baseline starts at zero replicas with internal-only service and fail-closed probes", () => {
assert.match(kubernetesBaseline, /^kind: Deployment$/m);
assert.match(kubernetesBaseline, /^ {2}replicas: 0$/m);
assert.match(kubernetesBaseline, /^ {14}path: \/health$/m);
assert.match(kubernetesBaseline, /^ {14}path: \/ready$/m);
assert.match(kubernetesBaseline, /^kind: Service$/m);
assert.match(kubernetesBaseline, /^ {2}type: ClusterIP$/m);
assert.match(kubernetesBaseline, /^kind: NetworkPolicy$/m);
assert.match(kubernetesBaseline, /^ {2}policyTypes:$/m);
assert.match(kubernetesBaseline, /^ {4}- Ingress$/m);
assert.doesNotMatch(kubernetesBaseline, /^kind: Ingress$/m);
assert.doesNotMatch(kubernetesBaseline, /\b(?:LoadBalancer|NodePort)\b/);
});