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/); });