Files
dify/knowledge-fs/scripts/github-actions-workflow.test.mjs
2026-07-22 06:07:12 -04:00

371 lines
17 KiB
JavaScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { parse } from "yaml";
import {
KNOWLEDGE_FS_INTEGRATION_EXACT_PATHS,
KNOWLEDGE_FS_INTEGRATION_PREFIXES,
} from "./secret-scan.mjs";
const workflow = readFileSync(
new URL("../../.github/workflows/knowledge-fs-ci.yml", import.meta.url),
"utf8",
);
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
const dependabot = readFileSync(new URL("../../.github/dependabot.yml", import.meta.url), "utf8");
const workflowDocument = parse(workflow);
const pathsFilterStep = workflowDocument.jobs["check-changes"].steps.find((step) =>
step.uses?.startsWith("dorny/paths-filter@"),
);
const knowledgeFsWorkflowPaths = parse(pathsFilterStep.with.filters)["knowledge-fs"];
const qualitySteps = workflowDocument.jobs.quality.steps;
function qualityStep(name) {
const step = qualitySteps.find((candidate) => candidate.name === name);
assert.ok(step, `workflow is missing the ${name} step`);
return step;
}
test("root workflow always emits a stable PR and merge-queue gate", () => {
assert.match(workflow, /^name: KnowledgeFS CI$/m);
assert.match(workflow, /^ {2}pull_request:$/m);
assert.match(workflow, /^ {2}merge_group:$/m);
assert.match(workflow, /^ {4}types: \[checks_requested\]$/m);
assert.match(workflow, /^ {2}push:$/m);
assert.match(workflow, /^ {2}workflow_dispatch:$/m);
assert.doesNotMatch(workflow, /^ {4}paths:$/m);
assert.match(workflow, /^ {2}final:$/m);
assert.match(workflow, /^ {4}name: KnowledgeFS CI$/m);
assert.match(workflow, /^ {4}if: \$\{\{ always\(\) \}\}$/m);
assert.match(workflow, /^concurrency:$/m);
assert.match(workflow, /cancel-in-progress: true/);
});
test("root workflow scopes expensive checks internally", () => {
assert.match(workflow, /uses: dorny\/paths-filter@[0-9a-f]{40}/);
assert.match(workflow, /- 'knowledge-fs\/\*\*'/);
assert.match(workflow, /- 'knowledge-fs\/packages\/api\/src\/dify-capability-v2\.ts'/);
assert.match(workflow, /- 'knowledge-fs\/scripts\/export-openapi\.mjs'/);
assert.match(workflow, /- 'api\/dev\/generate_knowledge_fs_contract\.py'/);
assert.match(workflow, /- 'api\/dev\/knowledge_fs_product_contract\.py'/);
assert.match(workflow, /- 'api\/knowledge-fs-product-operation-gaps\.json'/);
assert.match(workflow, /- 'api\/knowledge-fs-product-operations\.json'/);
assert.match(workflow, /- 'api\/services\/knowledge_fs\/\*\*'/);
assert.match(workflow, /- 'api\/\*\*\/knowledge_fs\/\*\*'/);
assert.match(workflow, /- 'api\/\*\*\/\*knowledge_fs\*'/);
assert.match(workflow, /- 'api\/\*\*\/\*knowledge-fs\*'/);
assert.match(workflow, /- 'api\/app_factory\.py'/);
assert.match(workflow, /- 'api\/models\/__init__\.py'/);
assert.match(workflow, /- 'api\/controllers\/console\/__init__\.py'/);
assert.match(workflow, /- 'api\/controllers\/service_api\/__init__\.py'/);
assert.doesNotMatch(workflow, /- 'web\//);
assert.match(workflow, /- 'docker\/\.env\.example'/);
assert.match(workflow, /- 'docker\/README\.md'/);
assert.match(workflow, /- 'docker\/docker-compose-template\.yaml'/);
assert.match(workflow, /- 'docker\/docker-compose\.yaml'/);
assert.match(workflow, /- 'docker\/envs\/core-services\/api\.env\.example'/);
assert.match(workflow, /- 'docker\/envs\/core-services\/knowledge-fs\.env\.example'/);
assert.match(workflow, /- 'docker\/generate_docker_compose'/);
assert.match(workflow, /- 'docs\/design\/knowledge-fs\*'/);
assert.match(workflow, /- 'api\/pyproject\.toml'/);
assert.match(workflow, /- 'api\/uv\.lock'/);
assert.match(workflow, /- '\.github\/dependabot\.yml'/);
assert.match(workflow, /needs\.check-changes\.outputs\.knowledge-fs == 'true'/);
assert.match(workflow, /^ {2}skip:$/m);
});
test("workflow paths stay in parity with every auditable integration touchpoint", () => {
for (const path of KNOWLEDGE_FS_INTEGRATION_EXACT_PATHS) {
assert.ok(knowledgeFsWorkflowPaths.includes(path), `workflow path filter is missing ${path}`);
}
for (const prefix of KNOWLEDGE_FS_INTEGRATION_PREFIXES) {
assert.ok(
knowledgeFsWorkflowPaths.includes(prefix.workflowPattern),
`workflow path filter is missing ${prefix.workflowPattern}`,
);
}
});
test("root workflow and Dependabot configuration are valid YAML", () => {
assert.doesNotThrow(() => parse(workflow));
assert.doesNotThrow(() => parse(dependabot));
});
test("root workflow preserves the independent KnowledgeFS pnpm workspace", () => {
assert.equal(packageJson.packageManager, "pnpm@10.33.0");
assert.match(workflow, /working-directory: \.\/knowledge-fs/);
assert.match(workflow, /package_json_file: knowledge-fs\/package\.json/);
assert.match(workflow, /cache-dependency-path: knowledge-fs\/pnpm-lock\.yaml/);
assert.match(workflow, /node-version: 22/);
assert.match(workflow, /run: pnpm install --frozen-lockfile/);
assert.match(workflow, /run: pnpm check/);
assert.match(workflow, /run: pnpm build/);
assert.match(workflow, /run: pnpm lint/);
assert.doesNotMatch(workflow, /cache-dependency-path: pnpm-lock\.yaml/);
assert.match(packageJson.scripts.check, /pnpm compose:middleware:config/);
assert.match(packageJson.scripts.check, /pnpm compose:config/);
assert.match(packageJson.scripts.check, /pnpm dify:compose:config/);
assert.equal(packageJson.scripts["dify:compose:config"], "node scripts/dify-compose-config.mjs");
});
test("root workflow runs explicit local security gates", () => {
assert.match(workflow, /name: Scan KnowledgeFS secrets\s+run: pnpm security:secrets/);
assert.match(
workflow,
/name: Audit KnowledgeFS production dependencies\s+run: pnpm security:dependencies/,
);
assert.equal(packageJson.scripts["security:secrets"], "node scripts/secret-scan.mjs");
assert.equal(
packageJson.scripts["security:dependencies"],
"pnpm audit --prod --audit-level high",
);
assert.match(packageJson.scripts["ci:workflow:test"], /scripts\/secret-scan\.test\.mjs/);
});
test("Dependabot covers the independent KnowledgeFS pnpm workspace", () => {
const config = parse(dependabot);
const update = config.updates.find(
(candidate) =>
candidate["package-ecosystem"] === "npm" && candidate.directory === "/knowledge-fs",
);
assert.ok(update);
assert.equal(update.schedule.interval, "weekly");
assert.ok(update["open-pull-requests-limit"] > 0);
});
test("root workflow installs the locked Dify environment and checks the contract", () => {
assert.match(workflow, /uv lock --project api --check/);
assert.match(workflow, /uv sync --project api --locked --dev/);
assert.match(
qualityStep("Collect Dify KnowledgeFS gate targets").run,
/find api\/tests\/unit_tests[\s\S]*knowledge_fs/,
);
assert.match(
workflow,
/uv run --project api python api\/dev\/generate_knowledge_fs_contract\.py --check/,
);
assert.equal(
packageJson.scripts["openapi:export"],
"node --import tsx scripts/export-openapi.mjs",
);
});
test("root workflow fail-closed collects the complete Dify KnowledgeFS gate surface", () => {
const collectTargets = qualityStep("Collect Dify KnowledgeFS gate targets");
assert.equal(collectTargets["working-directory"], ".");
assert.match(collectTargets.run, /set -euo pipefail/);
assert.match(collectTargets.run, /find api[\s\S]*-path 'api\/tests'[\s\S]*-name '\*\.py'/);
assert.match(collectTargets.run, /\[\[ "\$path" == \*knowledge_fs\* \]\]/);
assert.match(collectTargets.run, /find api\/tests\/unit_tests[\s\S]*-name '\*\.py'/);
for (const path of [
"api/app_factory.py",
"api/commands/__init__.py",
"api/controllers/console/__init__.py",
"api/controllers/console/workspace/rbac.py",
"api/controllers/service_api/__init__.py",
"api/core/agent/base_agent_runner.py",
"api/core/app/apps/agent_app/runtime_request_builder.py",
"api/core/rbac/entities.py",
"api/core/tools/__base/tool_runtime.py",
"api/core/workflow/node_runtime.py",
"api/core/workflow/nodes/agent_v2/runtime_request_builder.py",
"api/extensions/ext_celery.py",
"api/extensions/ext_commands.py",
"api/models/__init__.py",
"api/services/account_service.py",
"api/services/agent_tool_inner_service.py",
"api/services/enterprise/rbac_service.py",
"api/services/entities/agent_tool_inner.py",
]) {
assert.match(collectTargets.run, new RegExp(path.replaceAll("/", "\\/")));
}
for (const path of [
"api/tests/unit_tests/controllers/console/workspace/test_rbac.py",
"api/tests/unit_tests/core/agent/test_base_agent_runner.py",
"api/tests/unit_tests/core/app/apps/agent_app/test_runtime_request_builder.py",
"api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py",
"api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node_runtime.py",
"api/tests/unit_tests/core/workflow/test_node_runtime.py",
"api/tests/unit_tests/services/enterprise/test_rbac_service.py",
"api/tests/unit_tests/services/test_account_service.py",
"api/tests/unit_tests/services/test_agent_tool_inner_service.py",
]) {
assert.match(collectTargets.run, new RegExp(path.replaceAll("/", "\\/")));
}
for (const requiredScope of [
"/commands/",
"/configs/",
"/controllers/",
"/core/agent/",
"/core/app/apps/agent_app/",
"/core/tools/builtin_tool/providers/knowledge_fs/",
"/core/workflow/",
"/dev/",
"/extensions/",
"/migrations/",
"/models/",
"/repositories/",
"/services/",
"/tasks/",
]) {
assert.match(collectTargets.run, new RegExp(requiredScope.replaceAll("/", "\\/")));
}
assert.match(collectTargets.run, /production target set is empty/);
assert.match(collectTargets.run, /production_touchpoints\[@\][\s\S]*glue-files/);
assert.match(collectTargets.run, /unit test target set is empty/);
assert.match(collectTargets.run, /required Dify KnowledgeFS test scope is empty/);
});
test("root workflow runs Dify Ruff, Pyrefly, Mypy, and the full KnowledgeFS unit surface", () => {
const lint = qualityStep("Lint Dify KnowledgeFS integration");
const typecheck = qualityStep("Type-check Dify KnowledgeFS integration");
const unitTests = qualityStep("Test Dify KnowledgeFS unit surface");
assert.equal(lint["working-directory"], ".");
assert.match(lint.run, /ruff format --check/);
assert.match(lint.run, /ruff check/);
assert.match(lint.run, /production-files/);
assert.equal(typecheck["working-directory"], ".");
assert.match(typecheck.run, /\.\/dev\/pyrefly-check-local/);
assert.match(typecheck.run, /PYREFLY_OUTPUT_FORMAT=github/);
assert.match(typecheck.run, /uv run --directory api --dev mypy/);
assert.match(typecheck.run, /--check-untyped-defs/);
assert.match(typecheck.run, /production-files/);
assert.equal(unitTests["working-directory"], ".");
assert.match(
unitTests.run,
/coverage run --branch --source=api -m pytest "\$\{targets\[@\]\}" --no-cov -q/,
);
assert.match(unitTests.run, /unit-test-files/);
assert.doesNotMatch(
unitTests.run,
/pytest\s+api\/tests\/unit_tests\/dev\/test_generate_knowledge_fs_contract\.py/,
);
});
test("root workflow enforces focused Dify line, branch, and changed-glue coverage", () => {
const changeCheckout = workflowDocument.jobs["check-changes"].steps.find(
(step) => step.name === "Checkout code",
);
const checkout = qualityStep("Checkout code");
const unitTests = qualityStep("Test Dify KnowledgeFS unit surface");
const coverage = qualityStep("Enforce Dify KnowledgeFS focused coverage");
assert.equal(changeCheckout.with["fetch-depth"], 0);
assert.equal(checkout.with["fetch-depth"], 0);
assert.match(unitTests.env.COVERAGE_FILE, /dify-knowledge-fs\.coverage/);
assert.match(coverage.env.COVERAGE_FILE, /dify-knowledge-fs\.coverage/);
assert.match(coverage.run, /coverage json --show-contexts/);
assert.match(coverage.run, /api\/dev\/check_knowledge_fs_coverage\.py/);
assert.match(coverage.run, /--glue-manifest[\s\S]*knowledge-fs-ci-targets\/glue-files/);
assert.match(coverage.run, /--minimum 90/);
assert.match(coverage.run, /--glue-minimum 90/);
assert.match(
coverage.env.KNOWLEDGE_FS_COVERAGE_BASE,
/github\.event\.pull_request\.base\.sha[\s\S]*github\.event\.before/,
);
});
test("root workflow directly gates the Dify Agent KnowledgeFS core-tool callback", () => {
const verifyLock = qualityStep("Verify Dify Agent dependency lock");
const install = qualityStep("Install Dify Agent gate dependencies");
const lint = qualityStep("Lint Dify Agent KnowledgeFS integration");
const typecheck = qualityStep("Type-check Dify Agent KnowledgeFS integration");
const unitTests = qualityStep("Test Dify Agent KnowledgeFS integration");
assert.match(verifyLock.run, /uv lock --project dify-agent --check/);
assert.match(install.run, /uv sync --project dify-agent --locked --dev/);
for (const step of [lint, typecheck, unitTests]) {
assert.equal(step["working-directory"], "./dify-agent");
assert.match(step.run, /tests\/local\/dify_agent\/layers\/dify_core_tools\/test_client\.py/);
}
for (const step of [lint, typecheck]) {
assert.match(step.run, /src\/dify_agent\/layers\/dify_core_tools\/client\.py/);
}
assert.match(lint.run, /ruff format --check/);
assert.match(lint.run, /ruff check/);
assert.match(typecheck.run, /basedpyright --level error/);
assert.match(unitTests.run, /pytest[\s\S]*-q/);
});
test("root workflow builds the KnowledgeFS API image and publishes only trusted revisions", () => {
const build = workflowDocument.jobs.build;
assert.ok(build, "workflow is missing the KnowledgeFS API image build job");
assert.equal(build.name, "Build KnowledgeFS API production image");
assert.equal(build.needs, "check-changes");
assert.match(build.if, /needs\.check-changes\.outputs\.knowledge-fs == 'true'/);
assert.match(
workflowDocument.env.DIFY_KNOWLEDGE_FS_API_IMAGE_NAME,
/vars\.DIFY_KNOWLEDGE_FS_API_IMAGE_NAME.*langgenius\/dify-knowledge-fs-api/,
);
const checkout = build.steps.find((step) => step.name === "Checkout code");
const setupBuildx = build.steps.find((step) => step.name === "Set up Docker Buildx");
const login = build.steps.find((step) => step.name === "Login to Docker Hub");
const metadata = build.steps.find((step) => step.name === "Extract KnowledgeFS image metadata");
const buildImage = build.steps.find((step) => step.name === "Build KnowledgeFS API image");
assert.equal(checkout.with["persist-credentials"], false);
assert.match(setupBuildx.uses, /^docker\/setup-buildx-action@[0-9a-f]{40}$/);
assert.match(login.uses, /^docker\/login-action@[0-9a-f]{40}$/);
assert.match(login.if, /workflow_dispatch.*push.*refs\/heads\/main/);
assert.equal(login.with.username, "${{ secrets.DOCKERHUB_USER }}");
assert.equal(login.with.password, "${{ secrets.DOCKERHUB_TOKEN }}");
assert.match(metadata.uses, /^docker\/metadata-action@[0-9a-f]{40}$/);
assert.equal(metadata.with.images, "${{ env.DIFY_KNOWLEDGE_FS_API_IMAGE_NAME }}");
assert.match(metadata.with.tags, /type=raw,value=latest/);
assert.match(metadata.with.tags, /type=sha,format=long/);
assert.match(buildImage.uses, /^docker\/build-push-action@[0-9a-f]{40}$/);
assert.equal(buildImage.with.context, "./knowledge-fs");
assert.equal(buildImage.with.file, "./knowledge-fs/apps/api/Dockerfile");
assert.equal(buildImage.with.platforms, "linux/amd64");
assert.match(buildImage.with.push, /workflow_dispatch.*push.*refs\/heads\/main/);
assert.equal(buildImage.with.tags, "${{ steps.meta.outputs.tags }}");
assert.equal(buildImage.with.labels, "${{ steps.meta.outputs.labels }}");
assert.doesNotMatch(JSON.stringify(build), /apps\/admin/);
const final = workflowDocument.jobs.final;
assert.ok(final.needs.includes("build"));
assert.equal(final.steps[0].env.BUILD_RESULT, "${{ needs.build.result }}");
assert.match(final.steps[0].run, /"\$BUILD_RESULT" == 'success'/);
});
test("root workflow uses least privilege and pinned third-party actions", () => {
assert.match(workflow, /^ {2}contents: read$/m);
assert.match(workflow, /^ {2}pull-requests: read$/m);
assert.equal(workflow.match(/persist-credentials: false/g)?.length, 3);
const usesLines = workflow
.split("\n")
.map((line) => line.trim())
.filter((line) => line.startsWith("uses:"));
const actionNames = usesLines.map((line) => line.slice("uses: ".length).split("@")[0]);
assert.deepEqual(
new Set(actionNames),
new Set([
"actions/checkout",
"actions/setup-node",
"astral-sh/setup-uv",
"dorny/paths-filter",
"docker/build-push-action",
"docker/login-action",
"docker/metadata-action",
"docker/setup-buildx-action",
"pnpm/action-setup",
]),
);
for (const line of usesLines) {
assert.match(line, /@[0-9a-f]{40}(?:\s+#.*)?$/);
}
});