Ver Fonte

test: 添加配置与设置系统单元测试 (测试计划 09)

为 SettingsSchema、PermissionsSchema、AllowedMcpServerEntrySchema
验证,MCP 类型守卫,设置常量函数,以及 validation 工具函数添加
62 个测试用例。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
claude-code-best há 3 semanas atrás
pai
commit
183421361e
1 ficheiros alterados com 476 adições e 0 exclusões
  1. 476 0
      src/utils/settings/__tests__/config.test.ts

+ 476 - 0
src/utils/settings/__tests__/config.test.ts

@@ -0,0 +1,476 @@
+import { describe, expect, test } from "bun:test";
+import {
+  SettingsSchema,
+  EnvironmentVariablesSchema,
+  PermissionsSchema,
+  AllowedMcpServerEntrySchema,
+  DeniedMcpServerEntrySchema,
+  isMcpServerNameEntry,
+  isMcpServerCommandEntry,
+  isMcpServerUrlEntry,
+  CUSTOMIZATION_SURFACES,
+} from "../types";
+import {
+  SETTING_SOURCES,
+  SOURCES,
+  CLAUDE_CODE_SETTINGS_SCHEMA_URL,
+  getSettingSourceName,
+  getSourceDisplayName,
+  getSettingSourceDisplayNameLowercase,
+  getSettingSourceDisplayNameCapitalized,
+  parseSettingSourcesFlag,
+} from "../constants";
+import {
+  formatZodError,
+  filterInvalidPermissionRules,
+  validateSettingsFileContent,
+} from "../validation";
+
+// ─── Settings Schema Validation ──────────────────────────────────────────
+
+describe("SettingsSchema", () => {
+  test("accepts empty object", () => {
+    const result = SettingsSchema().safeParse({});
+    expect(result.success).toBe(true);
+  });
+
+  test("accepts model string", () => {
+    const result = SettingsSchema().safeParse({ model: "sonnet" });
+    expect(result.success).toBe(true);
+  });
+
+  test("accepts permissions block with allow rules", () => {
+    const result = SettingsSchema().safeParse({
+      permissions: { allow: ["Bash(npm install)"] },
+    });
+    expect(result.success).toBe(true);
+  });
+
+  test("accepts permissions block with deny rules", () => {
+    const result = SettingsSchema().safeParse({
+      permissions: { deny: ["Bash(rm -rf *)"] },
+    });
+    expect(result.success).toBe(true);
+  });
+
+  test("accepts env variables", () => {
+    const result = SettingsSchema().safeParse({
+      env: { FOO: "bar", DEBUG: "1" },
+    });
+    expect(result.success).toBe(true);
+  });
+
+  test("accepts hooks configuration", () => {
+    const result = SettingsSchema().safeParse({
+      hooks: {
+        PreToolUse: [
+          {
+            matcher: "Bash",
+            hooks: [{ type: "command", command: "echo test" }],
+          },
+        ],
+      },
+    });
+    expect(result.success).toBe(true);
+  });
+
+  test("accepts attribution settings", () => {
+    const result = SettingsSchema().safeParse({
+      attribution: {
+        commit: "Generated by AI",
+        pr: "AI-generated PR",
+      },
+    });
+    expect(result.success).toBe(true);
+  });
+
+  test("accepts worktree settings", () => {
+    const result = SettingsSchema().safeParse({
+      worktree: {
+        symlinkDirectories: ["node_modules", ".cache"],
+        sparsePaths: ["src/"],
+      },
+    });
+    expect(result.success).toBe(true);
+  });
+
+  test("accepts $schema field", () => {
+    const result = SettingsSchema().safeParse({
+      $schema: CLAUDE_CODE_SETTINGS_SCHEMA_URL,
+    });
+    expect(result.success).toBe(true);
+  });
+
+  test("passes through unknown keys (passthrough mode)", () => {
+    const result = SettingsSchema().safeParse({ unknownKey: "value" });
+    expect(result.success).toBe(true);
+    if (result.success) {
+      expect((result.data as any).unknownKey).toBe("value");
+    }
+  });
+
+  test("coerces env var numbers to strings", () => {
+    const result = EnvironmentVariablesSchema().safeParse({ PORT: 3000 });
+    expect(result.success).toBe(true);
+    if (result.success) {
+      expect(result.data.PORT).toBe("3000");
+    }
+  });
+
+  test("accepts boolean settings", () => {
+    const result = SettingsSchema().safeParse({
+      includeCoAuthoredBy: true,
+      respectGitignore: false,
+      disableAllHooks: true,
+    });
+    expect(result.success).toBe(true);
+  });
+
+  test("accepts cleanupPeriodDays", () => {
+    const result = SettingsSchema().safeParse({ cleanupPeriodDays: 30 });
+    expect(result.success).toBe(true);
+  });
+
+  test("rejects negative cleanupPeriodDays", () => {
+    const result = SettingsSchema().safeParse({ cleanupPeriodDays: -1 });
+    expect(result.success).toBe(false);
+  });
+
+  test("accepts statusLine configuration", () => {
+    const result = SettingsSchema().safeParse({
+      statusLine: { type: "command", command: "echo status" },
+    });
+    expect(result.success).toBe(true);
+  });
+
+  test("accepts sshConfigs", () => {
+    const result = SettingsSchema().safeParse({
+      sshConfigs: [
+        {
+          id: "dev-server",
+          name: "Development Server",
+          sshHost: "dev.example.com",
+          sshPort: 22,
+        },
+      ],
+    });
+    expect(result.success).toBe(true);
+  });
+});
+
+// ─── Permissions Schema ─────────────────────────────────────────────────
+
+describe("PermissionsSchema", () => {
+  test("accepts defaultMode", () => {
+    const result = PermissionsSchema().safeParse({
+      defaultMode: "acceptEdits",
+    });
+    expect(result.success).toBe(true);
+  });
+
+  test("accepts additionalDirectories", () => {
+    const result = PermissionsSchema().safeParse({
+      additionalDirectories: ["/tmp/extra"],
+    });
+    expect(result.success).toBe(true);
+  });
+
+  test("accepts disableBypassPermissionsMode", () => {
+    const result = PermissionsSchema().safeParse({
+      disableBypassPermissionsMode: "disable",
+    });
+    expect(result.success).toBe(true);
+  });
+});
+
+// ─── AllowedMcpServerEntrySchema ────────────────────────────────────────
+
+describe("AllowedMcpServerEntrySchema", () => {
+  test("accepts serverName entry", () => {
+    const result = AllowedMcpServerEntrySchema().safeParse({
+      serverName: "my-server",
+    });
+    expect(result.success).toBe(true);
+  });
+
+  test("accepts serverCommand entry", () => {
+    const result = AllowedMcpServerEntrySchema().safeParse({
+      serverCommand: ["npx", "mcp-server"],
+    });
+    expect(result.success).toBe(true);
+  });
+
+  test("accepts serverUrl entry", () => {
+    const result = AllowedMcpServerEntrySchema().safeParse({
+      serverUrl: "https://*.example.com/*",
+    });
+    expect(result.success).toBe(true);
+  });
+
+  test("rejects entry with no fields", () => {
+    const result = AllowedMcpServerEntrySchema().safeParse({});
+    expect(result.success).toBe(false);
+  });
+
+  test("rejects entry with multiple fields", () => {
+    const result = AllowedMcpServerEntrySchema().safeParse({
+      serverName: "my-server",
+      serverUrl: "https://example.com",
+    });
+    expect(result.success).toBe(false);
+  });
+
+  test("rejects invalid serverName characters", () => {
+    const result = AllowedMcpServerEntrySchema().safeParse({
+      serverName: "my server with spaces",
+    });
+    expect(result.success).toBe(false);
+  });
+
+  test("rejects empty serverCommand array", () => {
+    const result = AllowedMcpServerEntrySchema().safeParse({
+      serverCommand: [],
+    });
+    expect(result.success).toBe(false);
+  });
+});
+
+// ─── Type guards ─────────────────────────────────────────────────────────
+
+describe("MCP server entry type guards", () => {
+  test("isMcpServerNameEntry identifies name entry", () => {
+    expect(isMcpServerNameEntry({ serverName: "test" })).toBe(true);
+  });
+
+  test("isMcpServerNameEntry rejects non-name entry", () => {
+    expect(isMcpServerNameEntry({ serverUrl: "https://example.com" })).toBe(
+      false
+    );
+  });
+
+  test("isMcpServerCommandEntry identifies command entry", () => {
+    expect(isMcpServerCommandEntry({ serverCommand: ["npx", "srv"] })).toBe(
+      true
+    );
+  });
+
+  test("isMcpServerCommandEntry rejects non-command entry", () => {
+    expect(isMcpServerCommandEntry({ serverName: "test" })).toBe(false);
+  });
+
+  test("isMcpServerUrlEntry identifies url entry", () => {
+    expect(
+      isMcpServerUrlEntry({ serverUrl: "https://example.com" })
+    ).toBe(true);
+  });
+
+  test("isMcpServerUrlEntry rejects non-url entry", () => {
+    expect(isMcpServerUrlEntry({ serverName: "test" })).toBe(false);
+  });
+});
+
+// ─── Constants ──────────────────────────────────────────────────────────
+
+describe("SETTING_SOURCES", () => {
+  test("contains all five sources in order", () => {
+    expect(SETTING_SOURCES).toEqual([
+      "userSettings",
+      "projectSettings",
+      "localSettings",
+      "flagSettings",
+      "policySettings",
+    ]);
+  });
+});
+
+describe("SOURCES (editable)", () => {
+  test("contains three editable sources", () => {
+    expect(SOURCES).toEqual([
+      "localSettings",
+      "projectSettings",
+      "userSettings",
+    ]);
+  });
+});
+
+describe("CUSTOMIZATION_SURFACES", () => {
+  test("contains expected surfaces", () => {
+    expect(CUSTOMIZATION_SURFACES).toEqual([
+      "skills",
+      "agents",
+      "hooks",
+      "mcp",
+    ]);
+  });
+});
+
+describe("getSettingSourceName", () => {
+  test("maps userSettings to user", () => {
+    expect(getSettingSourceName("userSettings")).toBe("user");
+  });
+
+  test("maps projectSettings to project", () => {
+    expect(getSettingSourceName("projectSettings")).toBe("project");
+  });
+
+  test("maps localSettings to project, gitignored", () => {
+    expect(getSettingSourceName("localSettings")).toBe("project, gitignored");
+  });
+
+  test("maps flagSettings to cli flag", () => {
+    expect(getSettingSourceName("flagSettings")).toBe("cli flag");
+  });
+
+  test("maps policySettings to managed", () => {
+    expect(getSettingSourceName("policySettings")).toBe("managed");
+  });
+});
+
+describe("getSourceDisplayName", () => {
+  test("maps userSettings to User", () => {
+    expect(getSourceDisplayName("userSettings")).toBe("User");
+  });
+
+  test("maps plugin to Plugin", () => {
+    expect(getSourceDisplayName("plugin")).toBe("Plugin");
+  });
+
+  test("maps built-in to Built-in", () => {
+    expect(getSourceDisplayName("built-in")).toBe("Built-in");
+  });
+});
+
+describe("getSettingSourceDisplayNameLowercase", () => {
+  test("maps policySettings correctly", () => {
+    expect(getSettingSourceDisplayNameLowercase("policySettings")).toBe(
+      "enterprise managed settings"
+    );
+  });
+
+  test("maps cliArg correctly", () => {
+    expect(getSettingSourceDisplayNameLowercase("cliArg")).toBe("CLI argument");
+  });
+
+  test("maps session correctly", () => {
+    expect(getSettingSourceDisplayNameLowercase("session")).toBe(
+      "current session"
+    );
+  });
+});
+
+describe("getSettingSourceDisplayNameCapitalized", () => {
+  test("maps userSettings correctly", () => {
+    expect(getSettingSourceDisplayNameCapitalized("userSettings")).toBe(
+      "User settings"
+    );
+  });
+
+  test("maps command correctly", () => {
+    expect(getSettingSourceDisplayNameCapitalized("command")).toBe(
+      "Command configuration"
+    );
+  });
+});
+
+describe("parseSettingSourcesFlag", () => {
+  test("parses comma-separated sources", () => {
+    expect(parseSettingSourcesFlag("user,project,local")).toEqual([
+      "userSettings",
+      "projectSettings",
+      "localSettings",
+    ]);
+  });
+
+  test("parses single source", () => {
+    expect(parseSettingSourcesFlag("user")).toEqual(["userSettings"]);
+  });
+
+  test("returns empty array for empty string", () => {
+    expect(parseSettingSourcesFlag("")).toEqual([]);
+  });
+
+  test("trims whitespace", () => {
+    expect(parseSettingSourcesFlag("user , project")).toEqual([
+      "userSettings",
+      "projectSettings",
+    ]);
+  });
+
+  test("throws for invalid source name", () => {
+    expect(() => parseSettingSourcesFlag("invalid")).toThrow(
+      "Invalid setting source"
+    );
+  });
+});
+
+// ─── Validation ─────────────────────────────────────────────────────────
+
+describe("filterInvalidPermissionRules", () => {
+  test("returns empty for non-object input", () => {
+    expect(filterInvalidPermissionRules(null, "test.json")).toEqual([]);
+    expect(filterInvalidPermissionRules("string", "test.json")).toEqual([]);
+  });
+
+  test("returns empty when no permissions", () => {
+    expect(filterInvalidPermissionRules({}, "test.json")).toEqual([]);
+  });
+
+  test("filters non-string rules and returns warnings", () => {
+    const data = { permissions: { allow: ["Bash", 123, "Read"] } };
+    const warnings = filterInvalidPermissionRules(data, "test.json");
+    expect(warnings.length).toBe(1);
+    expect(warnings[0]!.path).toBe("permissions.allow");
+    expect((data.permissions as any).allow).toEqual(["Bash", "Read"]);
+  });
+
+  test("preserves valid rules", () => {
+    const data = {
+      permissions: { allow: ["Bash(npm install)", "Read", "Write"] },
+    };
+    const warnings = filterInvalidPermissionRules(data, "test.json");
+    expect(warnings).toEqual([]);
+    expect((data.permissions as any).allow).toEqual([
+      "Bash(npm install)",
+      "Read",
+      "Write",
+    ]);
+  });
+});
+
+describe("validateSettingsFileContent", () => {
+  test("accepts valid JSON settings", () => {
+    const result = validateSettingsFileContent('{"model": "sonnet"}');
+    expect(result.isValid).toBe(true);
+  });
+
+  test("accepts empty object", () => {
+    const result = validateSettingsFileContent("{}");
+    expect(result.isValid).toBe(true);
+  });
+
+  test("rejects invalid JSON", () => {
+    const result = validateSettingsFileContent("not json");
+    expect(result.isValid).toBe(false);
+    if (!result.isValid) {
+      expect(result.error).toContain("Invalid JSON");
+    }
+  });
+
+  test("rejects unknown keys in strict mode", () => {
+    const result = validateSettingsFileContent('{"unknownField": true}');
+    expect(result.isValid).toBe(false);
+  });
+});
+
+describe("formatZodError", () => {
+  test("formats invalid type error", () => {
+    const result = SettingsSchema().safeParse({ model: 123 });
+    expect(result.success).toBe(false);
+    if (!result.success) {
+      const errors = formatZodError(result.error, "settings.json");
+      expect(errors.length).toBeGreaterThan(0);
+      expect(errors[0]!.file).toBe("settings.json");
+      expect(errors[0]!.path).toContain("model");
+    }
+  });
+});