diff --git a/cli/test/conf_branch_override.test.ts b/cli/test/conf_branch_override.test.ts new file mode 100644 index 0000000000..e0976710bb --- /dev/null +++ b/cli/test/conf_branch_override.test.ts @@ -0,0 +1,144 @@ +import { assertEquals, assertExists } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { getEffectiveSettings, type SyncOptions } from "../src/core/conf.ts"; + +// ============================================================================= +// CONF.TS BRANCH OVERRIDE TESTS +// Tests for getEffectiveSettings with branchOverride parameter +// ============================================================================= + +Deno.test("getEffectiveSettings: applies branch overrides when branchOverride is provided", async () => { + const config: SyncOptions = { + defaultTs: "bun", + includes: ["f/**"], + gitBranches: { + staging: { + overrides: { + includes: ["staging/**"], + skipVariables: true, + }, + }, + production: { + overrides: { + includes: ["prod/**"], + skipSecrets: true, + }, + }, + }, + }; + + // Test with staging branch override + const stagingSettings = await getEffectiveSettings(config, undefined, true, true, "staging"); + assertEquals(stagingSettings.includes, ["staging/**"]); + assertEquals(stagingSettings.skipVariables, true); + assertEquals(stagingSettings.skipSecrets, undefined); + + // Test with production branch override + const prodSettings = await getEffectiveSettings(config, undefined, true, true, "production"); + assertEquals(prodSettings.includes, ["prod/**"]); + assertEquals(prodSettings.skipSecrets, true); + assertEquals(prodSettings.skipVariables, undefined); +}); + +Deno.test("getEffectiveSettings: uses top-level settings when branchOverride has no overrides", async () => { + const config: SyncOptions = { + defaultTs: "bun", + includes: ["f/**"], + skipVariables: true, + gitBranches: { + staging: { + // No overrides defined + }, + }, + }; + + const settings = await getEffectiveSettings(config, undefined, true, true, "staging"); + assertEquals(settings.includes, ["f/**"]); + assertEquals(settings.skipVariables, true); + assertEquals(settings.defaultTs, "bun"); +}); + +Deno.test("getEffectiveSettings: uses top-level settings for unknown branch", async () => { + const config: SyncOptions = { + defaultTs: "bun", + includes: ["f/**"], + gitBranches: { + staging: { + overrides: { + includes: ["staging/**"], + }, + }, + }, + }; + + const settings = await getEffectiveSettings(config, undefined, true, true, "nonexistent"); + assertEquals(settings.includes, ["f/**"]); + assertEquals(settings.defaultTs, "bun"); +}); + +Deno.test("getEffectiveSettings: promotionOverrides take precedence when promotion specified", async () => { + const config: SyncOptions = { + defaultTs: "bun", + includes: ["f/**"], + gitBranches: { + production: { + overrides: { + includes: ["prod/**"], + }, + promotionOverrides: { + includes: ["promoted/**"], + skipVariables: true, + }, + }, + }, + }; + + // Test without promotion flag - should use regular overrides + const normalSettings = await getEffectiveSettings(config, undefined, true, true, "production"); + assertEquals(normalSettings.includes, ["prod/**"]); + assertEquals(normalSettings.skipVariables, undefined); + + // Test with promotion flag - should use promotionOverrides + const promoSettings = await getEffectiveSettings(config, "production", true, true); + assertEquals(promoSettings.includes, ["promoted/**"]); + assertEquals(promoSettings.skipVariables, true); +}); + +Deno.test("getEffectiveSettings: branchOverride works without gitBranches config", async () => { + const config: SyncOptions = { + defaultTs: "bun", + includes: ["f/**"], + }; + + // Should not throw even with branchOverride but no gitBranches + const settings = await getEffectiveSettings(config, undefined, true, true, "staging"); + assertEquals(settings.includes, ["f/**"]); + assertEquals(settings.defaultTs, "bun"); +}); + +Deno.test("getEffectiveSettings: preserves all top-level settings in merged result", async () => { + const config: SyncOptions = { + defaultTs: "bun", + includes: ["f/**"], + excludes: ["*.test.ts"], + skipVariables: false, + skipResources: false, + skipFlows: false, + parallel: 4, + gitBranches: { + staging: { + overrides: { + skipVariables: true, // Override just this one + }, + }, + }, + }; + + const settings = await getEffectiveSettings(config, undefined, true, true, "staging"); + assertEquals(settings.defaultTs, "bun"); + assertEquals(settings.includes, ["f/**"]); + assertEquals(settings.excludes, ["*.test.ts"]); + assertEquals(settings.skipVariables, true); // Overridden + assertEquals(settings.skipResources, false); + assertEquals(settings.skipFlows, false); + assertEquals(settings.parallel, 4); +}); diff --git a/cli/test/containerized_backend.ts b/cli/test/containerized_backend.ts index b6facf945d..8ee7e064f6 100644 --- a/cli/test/containerized_backend.ts +++ b/cli/test/containerized_backend.ts @@ -20,7 +20,7 @@ export class ContainerizedBackend { constructor(config: Partial = {}) { this.config = { - composeFile: config.composeFile || '/home/alex/windmill/windmill/cli/test/docker-compose.test.yml', + composeFile: config.composeFile || new URL('./docker-compose.test.yml', import.meta.url).pathname, baseUrl: config.baseUrl || 'http://localhost:8001', workspace: config.workspace || 'test', // Use test workspace token: config.token || '', @@ -1023,10 +1023,13 @@ export async function main( ...args ]; - console.log('🔧 CLI Command:', ['/home/alex/.deno/bin/deno', 'run', '-A', '/home/alex/windmill/windmill/cli/src/main.ts', ...fullArgs].join(' ')); - - return new Deno.Command('/home/alex/.deno/bin/deno', { - args: ['run', '-A', '/home/alex/windmill/windmill/cli/src/main.ts', ...fullArgs], + const denoPath = Deno.execPath(); + const cliMainPath = new URL('../src/main.ts', import.meta.url).pathname; + + console.log('🔧 CLI Command:', [denoPath, 'run', '-A', cliMainPath, ...fullArgs].join(' ')); + + return new Deno.Command(denoPath, { + args: ['run', '-A', cliMainPath, ...fullArgs], cwd: workingDir, stdout: 'piped', stderr: 'piped', diff --git a/cli/test/specific_items.test.ts b/cli/test/specific_items.test.ts new file mode 100644 index 0000000000..4448f32db7 --- /dev/null +++ b/cli/test/specific_items.test.ts @@ -0,0 +1,338 @@ +import { assertEquals, assertExists, assert } from "https://deno.land/std@0.224.0/assert/mod.ts"; + +// ============================================================================= +// SPECIFIC ITEMS UNIT TESTS +// Tests for branch-specific file path functions (no Docker required) +// ============================================================================= + +// Import the functions we need to test +import { + isSpecificItem, + toBranchSpecificPath, + fromBranchSpecificPath, + isBranchSpecificFile, +} from "../src/core/specific_items.ts"; + +import type { SpecificItemsConfig } from "../src/core/specific_items.ts"; + +// ============================================================================= +// toBranchSpecificPath TESTS +// ============================================================================= + +Deno.test("toBranchSpecificPath: converts variable path to branch-specific", () => { + const result = toBranchSpecificPath("f/test.variable.yaml", "main"); + assertEquals(result, "f/test.main.variable.yaml"); +}); + +Deno.test("toBranchSpecificPath: converts resource path to branch-specific", () => { + const result = toBranchSpecificPath("u/admin/db.resource.yaml", "develop"); + assertEquals(result, "u/admin/db.develop.resource.yaml"); +}); + +Deno.test("toBranchSpecificPath: converts trigger path to branch-specific", () => { + const result = toBranchSpecificPath("f/my_trigger.http_trigger.yaml", "feature-x"); + assertEquals(result, "f/my_trigger.feature-x.http_trigger.yaml"); +}); + +Deno.test("toBranchSpecificPath: sanitizes branch names with slashes", () => { + const result = toBranchSpecificPath("f/test.variable.yaml", "feature/my-feature"); + assertEquals(result, "f/test.feature_my-feature.variable.yaml"); +}); + +Deno.test("toBranchSpecificPath: sanitizes branch names with dots", () => { + const result = toBranchSpecificPath("f/test.variable.yaml", "release.1.0"); + assertEquals(result, "f/test.release_1_0.variable.yaml"); +}); + +Deno.test("toBranchSpecificPath: leaves non-specific files unchanged", () => { + const result = toBranchSpecificPath("f/script.ts", "main"); + assertEquals(result, "f/script.ts"); +}); + +Deno.test("toBranchSpecificPath: handles resource files with extensions", () => { + const result = toBranchSpecificPath("f/config.resource.file.json", "main"); + assertEquals(result, "f/config.main.resource.file.json"); +}); + +// ============================================================================= +// fromBranchSpecificPath TESTS +// ============================================================================= + +Deno.test("fromBranchSpecificPath: converts branch-specific variable back to base", () => { + const result = fromBranchSpecificPath("f/test.main.variable.yaml", "main"); + assertEquals(result, "f/test.variable.yaml"); +}); + +Deno.test("fromBranchSpecificPath: converts branch-specific resource back to base", () => { + const result = fromBranchSpecificPath("u/admin/db.develop.resource.yaml", "develop"); + assertEquals(result, "u/admin/db.resource.yaml"); +}); + +Deno.test("fromBranchSpecificPath: converts branch-specific trigger back to base", () => { + const result = fromBranchSpecificPath("f/my_trigger.feature-x.http_trigger.yaml", "feature-x"); + assertEquals(result, "f/my_trigger.http_trigger.yaml"); +}); + +Deno.test("fromBranchSpecificPath: handles sanitized branch names", () => { + const result = fromBranchSpecificPath("f/test.feature_my-feature.variable.yaml", "feature/my-feature"); + assertEquals(result, "f/test.variable.yaml"); +}); + +Deno.test("fromBranchSpecificPath: returns unchanged if not branch-specific", () => { + const result = fromBranchSpecificPath("f/test.variable.yaml", "main"); + assertEquals(result, "f/test.variable.yaml"); +}); + +Deno.test("fromBranchSpecificPath: handles resource files with extensions", () => { + const result = fromBranchSpecificPath("f/config.main.resource.file.json", "main"); + assertEquals(result, "f/config.resource.file.json"); +}); + +// ============================================================================= +// isSpecificItem TESTS +// ============================================================================= + +Deno.test("isSpecificItem: returns false when specificItems is undefined", () => { + const result = isSpecificItem("f/test.variable.yaml", undefined); + assertEquals(result, false); +}); + +Deno.test("isSpecificItem: matches variable paths with glob pattern", () => { + const config: SpecificItemsConfig = { + variables: ["f/**"], + }; + assertEquals(isSpecificItem("f/test.variable.yaml", config), true); + assertEquals(isSpecificItem("u/admin/test.variable.yaml", config), false); +}); + +Deno.test("isSpecificItem: matches resource paths with glob pattern", () => { + const config: SpecificItemsConfig = { + resources: ["u/admin/**"], + }; + assertEquals(isSpecificItem("u/admin/db.resource.yaml", config), true); + assertEquals(isSpecificItem("f/db.resource.yaml", config), false); +}); + +Deno.test("isSpecificItem: matches trigger paths with glob pattern", () => { + const config: SpecificItemsConfig = { + triggers: ["f/triggers/**"], + }; + assertEquals(isSpecificItem("f/triggers/my.http_trigger.yaml", config), true); + assertEquals(isSpecificItem("u/admin/my.http_trigger.yaml", config), false); +}); + +Deno.test("isSpecificItem: matches multiple patterns", () => { + const config: SpecificItemsConfig = { + variables: ["f/**", "g/**"], + }; + assertEquals(isSpecificItem("f/test.variable.yaml", config), true); + assertEquals(isSpecificItem("g/test.variable.yaml", config), true); + assertEquals(isSpecificItem("u/admin/test.variable.yaml", config), false); +}); + +Deno.test("isSpecificItem: handles exact path patterns", () => { + const config: SpecificItemsConfig = { + variables: ["f/specific.variable.yaml"], + }; + assertEquals(isSpecificItem("f/specific.variable.yaml", config), true); + assertEquals(isSpecificItem("f/other.variable.yaml", config), false); +}); + +// ============================================================================= +// isBranchSpecificFile TESTS +// ============================================================================= + +Deno.test("isBranchSpecificFile: detects branch-specific variable files", () => { + assertEquals(isBranchSpecificFile("f/test.main.variable.yaml"), true); + assertEquals(isBranchSpecificFile("f/test.develop.variable.yaml"), true); + assertEquals(isBranchSpecificFile("f/test.feature_branch.variable.yaml"), true); +}); + +Deno.test("isBranchSpecificFile: detects branch-specific resource files", () => { + assertEquals(isBranchSpecificFile("u/admin/db.main.resource.yaml"), true); + assertEquals(isBranchSpecificFile("u/admin/db.staging.resource.yaml"), true); +}); + +Deno.test("isBranchSpecificFile: detects branch-specific trigger files", () => { + assertEquals(isBranchSpecificFile("f/my.main.http_trigger.yaml"), true); + assertEquals(isBranchSpecificFile("f/my.develop.kafka_trigger.yaml"), true); + assertEquals(isBranchSpecificFile("f/my.main.websocket_trigger.yaml"), true); +}); + +Deno.test("isBranchSpecificFile: returns false for non-branch-specific files", () => { + assertEquals(isBranchSpecificFile("f/test.variable.yaml"), false); + assertEquals(isBranchSpecificFile("u/admin/db.resource.yaml"), false); + assertEquals(isBranchSpecificFile("f/my.http_trigger.yaml"), false); + assertEquals(isBranchSpecificFile("f/script.ts"), false); +}); + +Deno.test("isBranchSpecificFile: handles resource files with extensions", () => { + assertEquals(isBranchSpecificFile("f/config.main.resource.file.json"), true); + assertEquals(isBranchSpecificFile("f/config.resource.file.json"), false); +}); + +// ============================================================================= +// ROUND-TRIP TESTS +// ============================================================================= + +Deno.test("round-trip: variable file path conversion", () => { + const original = "f/my/nested/config.variable.yaml"; + const branch = "feature/test-branch"; + const branchSpecific = toBranchSpecificPath(original, branch); + const restored = fromBranchSpecificPath(branchSpecific, branch); + assertEquals(restored, original); +}); + +Deno.test("round-trip: resource file path conversion", () => { + const original = "u/admin/database.resource.yaml"; + const branch = "develop"; + const branchSpecific = toBranchSpecificPath(original, branch); + const restored = fromBranchSpecificPath(branchSpecific, branch); + assertEquals(restored, original); +}); + +Deno.test("round-trip: trigger file path conversion", () => { + const original = "f/webhooks/handler.http_trigger.yaml"; + const branch = "main"; + const branchSpecific = toBranchSpecificPath(original, branch); + const restored = fromBranchSpecificPath(branchSpecific, branch); + assertEquals(restored, original); +}); + +Deno.test("round-trip: resource file with extension", () => { + const original = "f/configs/settings.resource.file.ini"; + const branch = "release/v1.0"; + const branchSpecific = toBranchSpecificPath(original, branch); + const restored = fromBranchSpecificPath(branchSpecific, branch); + assertEquals(restored, original); +}); + +// ============================================================================= +// BRANCH OVERRIDE TESTS (for --branch flag functionality) +// These tests validate that functions work correctly with explicit branch override +// ============================================================================= + +import { + getBranchSpecificPath, + isCurrentBranchFile, + getSpecificItemsForCurrentBranch, +} from "../src/core/specific_items.ts"; + +Deno.test("branchOverride: getBranchSpecificPath with override returns branch-specific path", () => { + // This test verifies that when branchOverride is provided, the function uses it + // instead of detecting the current git branch + const config: SpecificItemsConfig = { + variables: ["f/**"], + }; + + // When override is provided, it should return the branch-specific path even outside git repo + const result = getBranchSpecificPath("f/test.variable.yaml", config, "staging"); + assertEquals(result, "f/test.staging.variable.yaml"); +}); + +Deno.test("branchOverride: getBranchSpecificPath without override and not in git repo returns undefined", () => { + const config: SpecificItemsConfig = { + variables: ["f/**"], + }; + + // Without override and outside git repo (or if git returns null), should return undefined + // Note: This test's behavior depends on whether we're in a git repo + const result = getBranchSpecificPath("f/test.variable.yaml", config); + // In a git repo, this would return a branch-specific path; outside, it would be undefined + // We test the override case above which is deterministic +}); + +Deno.test("branchOverride: isCurrentBranchFile with override uses provided branch", () => { + // Test that isCurrentBranchFile uses the override branch instead of git detection + const result = isCurrentBranchFile("f/test.staging.variable.yaml", "staging"); + assertEquals(result, true); + + // Should return false for different branch + const resultOther = isCurrentBranchFile("f/test.staging.variable.yaml", "production"); + assertEquals(resultOther, false); + + // Should return false for non-branch-specific file + const resultNonSpecific = isCurrentBranchFile("f/test.variable.yaml", "staging"); + assertEquals(resultNonSpecific, false); +}); + +Deno.test("branchOverride: isCurrentBranchFile with override handles sanitized branch names", () => { + // Test with branch names that get sanitized + const result = isCurrentBranchFile("f/test.feature_my-branch.variable.yaml", "feature/my-branch"); + assertEquals(result, true); + + // Different sanitized branch should return false + const resultOther = isCurrentBranchFile("f/test.feature_my-branch.variable.yaml", "feature/other-branch"); + assertEquals(resultOther, false); +}); + +Deno.test("branchOverride: getSpecificItemsForCurrentBranch with override returns correct config", () => { + // Test that getSpecificItemsForCurrentBranch uses the override branch + const config = { + gitBranches: { + staging: { + specificItems: { + variables: ["f/**"], + resources: ["u/admin/**"], + }, + }, + production: { + specificItems: { + variables: ["g/**"], + }, + }, + commonSpecificItems: { + triggers: ["f/webhooks/**"], + }, + }, + }; + + const stagingItems = getSpecificItemsForCurrentBranch(config as any, "staging"); + assertEquals(stagingItems?.variables, ["f/**"]); + assertEquals(stagingItems?.resources, ["u/admin/**"]); + assertEquals(stagingItems?.triggers, ["f/webhooks/**"]); // From common + + const productionItems = getSpecificItemsForCurrentBranch(config as any, "production"); + assertEquals(productionItems?.variables, ["g/**"]); + assertEquals(productionItems?.resources, undefined); + assertEquals(productionItems?.triggers, ["f/webhooks/**"]); // From common +}); + +Deno.test("branchOverride: getSpecificItemsForCurrentBranch with non-existent branch returns undefined", () => { + const config = { + gitBranches: { + staging: { + specificItems: { + variables: ["f/**"], + }, + }, + }, + }; + + // When the branch doesn't have specific items (and there's no common), should return undefined + const result = getSpecificItemsForCurrentBranch(config as any, "nonexistent"); + assertEquals(result, undefined); +}); + +Deno.test("branchOverride: getSpecificItemsForCurrentBranch merges common and branch items", () => { + const config = { + gitBranches: { + commonSpecificItems: { + variables: ["common/**"], + resources: ["shared/**"], + }, + develop: { + specificItems: { + variables: ["dev/**"], + triggers: ["dev/triggers/**"], + }, + }, + }, + }; + + const result = getSpecificItemsForCurrentBranch(config as any, "develop"); + // Should merge common and branch-specific + assertEquals(result?.variables, ["common/**", "dev/**"]); + assertEquals(result?.resources, ["shared/**"]); + assertEquals(result?.triggers, ["dev/triggers/**"]); +});