test(cli): add unit tests for branch detection and --branch override

- Add specific_items.test.ts with 35 tests covering:
  - toBranchSpecificPath and fromBranchSpecificPath conversions
  - isSpecificItem pattern matching
  - isBranchSpecificFile detection
  - Round-trip path conversions
  - branchOverride parameter functionality

- Add conf_branch_override.test.ts with 6 tests covering:
  - getEffectiveSettings with branchOverride parameter
  - Branch-specific overrides application
  - promotionOverrides precedence
  - Fallback to top-level settings

- Fix containerized_backend.ts to use dynamic paths instead of
  hardcoded user home directories

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-01-16 19:31:44 +00:00
parent 56c18b813b
commit 639e320267
3 changed files with 490 additions and 5 deletions
+144
View File
@@ -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);
});
+8 -5
View File
@@ -20,7 +20,7 @@ export class ContainerizedBackend {
constructor(config: Partial<ContainerConfig> = {}) {
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',
+338
View File
@@ -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/**"]);
});