Compare commits

...

5 Commits

Author SHA1 Message Date
Ruben Fiszel
bbeed2ccef fix(cli): fix branch-specific item filtering to check if type is configured
The previous logic incorrectly skipped branch-specific files when:
- specificItems was configured but didn't include the item type
- e.g., folders: ["f/**"] in config, but a folder.dev.meta.yaml file
  would be skipped because isSpecificItem returned false

New logic with isItemTypeConfigured:
- If item type is NOT configured in specificItems -> process normally
- If item type IS configured but doesn't match pattern -> skip
- If item type IS configured and matches pattern -> process

Added tests for:
- isItemTypeConfigured function
- Filtering behavior with different config combinations
- getSpecificItemsForCurrentBranch with branch override

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 12:22:28 +00:00
Ruben Fiszel
6869e4c3e2 fix(cli): always skip branch-specific files when not on matching branch
Branch-specific files (folder.dev.meta.yaml, settings.main.yaml, etc.)
should ALWAYS be skipped if:
1. Not on any git branch
2. On a different branch than the file's branch name

Previously, this filtering only happened when specificItems was configured.
Now it applies unconditionally based on the file naming convention.

Also refactored the path mapping logic to be clearer:
- Branch-specific files for current branch -> map to base path
- Regular base files -> add to map (unless it's a specific item)
- Branch-specific files for other branches -> filtered out earlier

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 12:09:22 +00:00
Ruben Fiszel
fd2cb93cac fix(cli): skip base files that are branch-specific items
When a file (folder.meta.yaml, settings.yaml, etc.) is configured as
a branch-specific item, the base file should be skipped and only the
branch-specific version (folder.branchName.meta.yaml) should be used.

Previously, base files were only skipped if a branch-specific version
was already processed, but they should always be skipped for specific
items regardless of processing order.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 12:05:48 +00:00
Ruben Fiszel
23cf2834b3 feat(cli): add settings as branch-specific item and skip validation with --branch
- Add settings.yaml as a branch-specific item (settings: true in config)
  - settings.yaml -> settings.branchName.yaml conversion
- Skip "Create empty branch configuration" prompt when using --branch flag
  - User explicitly specifies branch, so skip validation prompts
- Add folders and settings fields to gitBranches type definitions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 17:32:52 +00:00
Ruben Fiszel
77d59d7371 feat(cli): add folders as branch-specific items
Folders can now be configured as branch-specific items in wmill.yaml:

```yaml
gitBranches:
  staging:
    specificItems:
      folders:
        - "f/env_*"
        - "f/config"
```

Branch-specific folder format: f/folder/folder.branchName.meta.yaml
(consistent with other item types where branch goes before the type suffix)

Example:
- Base: f/env_staging/folder.meta.yaml
- Branch-specific: f/env_staging/folder.main.meta.yaml

Changes:
- Add `folders?: string[]` to SpecificItemsConfig
- Add folder handling in toBranchSpecificPath()
- Add folder handling in fromBranchSpecificPath()
- Add folder pattern matching in isSpecificItem()
- Add folder detection in isBranchSpecificFile()
- Add folder detection in isCurrentBranchFile()
- Add 13 new tests for folder functionality

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 17:20:17 +00:00
4 changed files with 449 additions and 50 deletions

View File

@@ -53,6 +53,7 @@ import {
getSpecificItemsForCurrentBranch,
isBranchSpecificFile,
isCurrentBranchFile,
isItemTypeConfigured,
isSpecificItem,
SpecificItemsConfig,
} from "../../core/specific_items.ts";
@@ -1350,10 +1351,12 @@ export async function elementsToMap(
// If getTypeStrFromPath can't determine the type, continue processing the file
}
// Handle branch-specific files - skip files for other branches
if (specificItems && isBranchSpecificFile(path)) {
// Handle branch-specific files - ALWAYS skip files that don't match current branch
// This applies regardless of specificItems config - branch-specific files by naming
// convention should only be processed when on the matching branch
if (isBranchSpecificFile(path)) {
if (!isCurrentBranchFile(path, branchOverride)) {
// Skip branch-specific files for other branches
// Skip branch-specific files for other branches (or when not on any branch)
continue;
}
}
@@ -1387,32 +1390,36 @@ export async function elementsToMap(
}
// Handle branch-specific path mapping after all filtering
if (specificItems) {
if (isCurrentBranchFile(path, branchOverride)) {
// This is a branch-specific file for current branch
// Safe to compute branch here since isCurrentBranchFile already validated it exists
const currentBranch = branchOverride || getCurrentGitBranch()!;
const basePath = fromBranchSpecificPath(path, currentBranch);
if (isSpecificItem(basePath, specificItems)) {
// Map to base path for push operations
map[basePath] = content;
processedBasePaths.add(basePath);
} else {
// Branch-specific file doesn't match pattern, skip it
continue;
}
} else if (!isBranchSpecificFile(path)) {
// This is a regular base file, check if we should skip it
if (processedBasePaths.has(path)) {
// Skip base file, we already processed branch-specific version
continue;
}
map[path] = content;
if (isCurrentBranchFile(path, branchOverride)) {
// This is a branch-specific file for current branch - map to base path
const currentBranch = branchOverride || getCurrentGitBranch()!;
const basePath = fromBranchSpecificPath(path, currentBranch);
// Only apply specificItems filtering if the item TYPE is configured
// If the type is configured but doesn't match the pattern, skip it
// If the type is NOT configured, process the file normally
if (isItemTypeConfigured(basePath, specificItems) && !isSpecificItem(basePath, specificItems)) {
// Branch-specific file doesn't match the configured pattern, skip it
continue;
}
} else {
// No specific items configuration, use regular path
map[entry.path] = content;
// Map to base path for push operations
map[basePath] = content;
processedBasePaths.add(basePath);
} else if (!isBranchSpecificFile(path)) {
// This is a regular base file
if (processedBasePaths.has(path)) {
// Skip base file, we already processed branch-specific version
continue;
}
// Only skip base files if this item type IS configured as branch-specific
// AND it matches the pattern (meaning we expect a branch-specific version)
if (isSpecificItem(path, specificItems)) {
continue;
}
map[path] = content;
}
// Note: branch-specific files for other branches are already filtered out above
}
return map;
}
@@ -1866,9 +1873,9 @@ export async function pull(
const originalCliOpts = { ...opts };
opts = await mergeConfigWithConfigFile(opts);
// Validate branch configuration early
// Validate branch configuration early (skipped when --branch is used)
try {
await validateBranchConfiguration(opts);
await validateBranchConfiguration(opts, opts.branch);
} catch (error) {
if (error instanceof Error && error.message.includes("overrides")) {
log.error(error.message);
@@ -2351,9 +2358,9 @@ export async function push(
// Load configuration from wmill.yaml and merge with CLI options
opts = await mergeConfigWithConfigFile(opts);
// Validate branch configuration early
// Validate branch configuration early (skipped when --branch is used)
try {
await validateBranchConfiguration(opts);
await validateBranchConfiguration(opts, opts.branch);
} catch (error) {
if (error instanceof Error && error.message.includes("overrides")) {
log.error(error.message);

View File

@@ -53,6 +53,8 @@ export interface SyncOptions {
variables?: string[];
resources?: string[];
triggers?: string[];
folders?: string[];
settings?: boolean;
};
} & {
[branchName: string]: SyncOptions & {
@@ -64,6 +66,8 @@ export interface SyncOptions {
variables?: string[];
resources?: string[];
triggers?: string[];
folders?: string[];
settings?: boolean;
};
};
};
@@ -73,6 +77,8 @@ export interface SyncOptions {
variables?: string[];
resources?: string[];
triggers?: string[];
folders?: string[];
settings?: boolean;
};
} & {
[branchName: string]: SyncOptions & {
@@ -84,6 +90,8 @@ export interface SyncOptions {
variables?: string[];
resources?: string[];
triggers?: string[];
folders?: string[];
settings?: boolean;
};
};
};
@@ -370,9 +378,11 @@ export async function mergeConfigWithConfigFile<T>(
// Validate branch configuration early in the process
export async function validateBranchConfiguration(
opts: Pick<SyncOptions, "skipBranchValidation" | "yes">
opts: Pick<SyncOptions, "skipBranchValidation" | "yes">,
branchOverride?: string
): Promise<void> {
if (opts.skipBranchValidation || !isGitRepository()) {
// When branch override is provided, skip validation - user is explicitly specifying the branch
if (opts.skipBranchValidation || branchOverride || !isGitRepository()) {
return;
}

View File

@@ -8,6 +8,8 @@ export interface SpecificItemsConfig {
variables?: string[];
resources?: string[];
triggers?: string[];
folders?: string[];
settings?: boolean;
}
// Define all branch-specific file types (computed lazily)
@@ -98,6 +100,12 @@ export function getSpecificItemsForCurrentBranch(config: SyncOptions, branchOver
if (commonItems?.triggers) {
merged.triggers = [...commonItems.triggers];
}
if (commonItems?.folders) {
merged.folders = [...commonItems.folders];
}
if (commonItems?.settings !== undefined) {
merged.settings = commonItems.settings;
}
// Add branch-specific items (extending common items)
if (branchItems?.variables) {
@@ -109,6 +117,13 @@ export function getSpecificItemsForCurrentBranch(config: SyncOptions, branchOver
if (branchItems?.triggers) {
merged.triggers = [...(merged.triggers || []), ...branchItems.triggers];
}
if (branchItems?.folders) {
merged.folders = [...(merged.folders || []), ...branchItems.folders];
}
// For settings (boolean), branch-specific overrides common
if (branchItems?.settings !== undefined) {
merged.settings = branchItems.settings;
}
return merged;
}
@@ -120,6 +135,42 @@ function matchesPatterns(path: string, patterns: string[]): boolean {
return patterns.some(pattern => minimatch(path, pattern));
}
/**
* Check if the item type for a given path is configured in specificItems
* This is different from isSpecificItem which checks if it MATCHES the pattern
*/
export function isItemTypeConfigured(path: string, specificItems: SpecificItemsConfig | undefined): boolean {
if (!specificItems) {
return false;
}
if (path.endsWith('.variable.yaml')) {
return specificItems.variables !== undefined;
}
if (path.endsWith('.resource.yaml')) {
return specificItems.resources !== undefined;
}
if (isTriggerFile(path)) {
return specificItems.triggers !== undefined;
}
if (path.endsWith('/folder.meta.yaml')) {
return specificItems.folders !== undefined;
}
if (path === 'settings.yaml') {
return specificItems.settings !== undefined;
}
if (isFileResource(path)) {
return specificItems.resources !== undefined;
}
return false;
}
/**
* Check if a file path should be treated as branch-specific
*/
@@ -142,6 +193,21 @@ export function isSpecificItem(path: string, specificItems: SpecificItemsConfig
return specificItems.triggers ? matchesPatterns(path, specificItems.triggers) : false;
}
// Check for folder meta files
if (path.endsWith('/folder.meta.yaml')) {
if (specificItems.folders) {
// Match against the folder path (without /folder.meta.yaml)
const folderPath = path.slice(0, -'/folder.meta.yaml'.length);
return matchesPatterns(folderPath, specificItems.folders);
}
return false;
}
// Check for settings.yaml (root-level file)
if (path === 'settings.yaml') {
return specificItems.settings === true;
}
// Check for resource files using the standard detection function
if (isFileResource(path)) {
// Extract the base path without the file extension to match against patterns
@@ -159,6 +225,25 @@ export function isSpecificItem(path: string, specificItems: SpecificItemsConfig
* Convert a base path to a branch-specific path
*/
export function toBranchSpecificPath(basePath: string, branchName: string): string {
// Sanitize branch name to be filesystem-safe
const sanitizedBranchName = branchName.replace(/[\/\\:*?"<>|.]/g, '_');
// Warn about potential collisions if sanitization occurred
if (sanitizedBranchName !== branchName) {
console.warn(`Warning: Branch name "${branchName}" contains filesystem-unsafe characters (/ \\ : * ? " < > | .) and was sanitized to "${sanitizedBranchName}". This may cause collisions with other similarly named branches.`);
}
// Check for folder meta file pattern: folder.meta.yaml -> folder.branchName.meta.yaml
if (basePath.endsWith('/folder.meta.yaml')) {
const pathWithoutMeta = basePath.substring(0, basePath.length - '/folder.meta.yaml'.length);
return `${pathWithoutMeta}/folder.${sanitizedBranchName}.meta.yaml`;
}
// Check for settings.yaml: settings.yaml -> settings.branchName.yaml
if (basePath === 'settings.yaml') {
return `settings.${sanitizedBranchName}.yaml`;
}
// Check for resource file pattern (e.g., .resource.file.ini)
const resourceFileMatch = basePath.match(/^(.+?)(\.resource\.file\..+)$/);
@@ -178,14 +263,6 @@ export function toBranchSpecificPath(basePath: string, branchName: string): stri
pathWithoutExtension = basePath.substring(0, basePath.length - extension.length);
}
// Sanitize branch name to be filesystem-safe
const sanitizedBranchName = branchName.replace(/[\/\\:*?"<>|.]/g, '_');
// Warn about potential collisions if sanitization occurred
if (sanitizedBranchName !== branchName) {
console.warn(`Warning: Branch name "${branchName}" contains filesystem-unsafe characters (/ \\ : * ? " < > | .) and was sanitized to "${sanitizedBranchName}". This may cause collisions with other similarly named branches.`);
}
return `${pathWithoutExtension}.${sanitizedBranchName}${extension}`;
}
@@ -197,7 +274,19 @@ export function fromBranchSpecificPath(branchSpecificPath: string, branchName: s
const sanitizedBranchName = branchName.replace(/[\/\\:*?"<>|.]/g, '_');
const escapedBranchName = sanitizedBranchName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Check for resource file pattern first
// Check for folder meta file pattern: /folder.branchName.meta.yaml -> /folder.meta.yaml
const folderPattern = new RegExp(`/folder\\.${escapedBranchName}\\.meta\\.yaml$`);
if (folderPattern.test(branchSpecificPath)) {
return branchSpecificPath.replace(folderPattern, '/folder.meta.yaml');
}
// Check for settings file pattern: settings.branchName.yaml -> settings.yaml
const settingsPattern = new RegExp(`^settings\\.${escapedBranchName}\\.yaml$`);
if (settingsPattern.test(branchSpecificPath)) {
return 'settings.yaml';
}
// Check for resource file pattern
const resourceFilePattern = new RegExp(`\\.${escapedBranchName}(\\.resource\\.file\\..+)$`);
const resourceFileMatch = branchSpecificPath.match(resourceFilePattern);
@@ -283,7 +372,12 @@ export function isCurrentBranchFile(path: string, branchOverride?: string): bool
// Use cached pattern or create and cache new one
let pattern = branchPatternCache.get(currentBranch);
if (!pattern) {
pattern = new RegExp(`\\.${escapedBranchName}\\.${buildYamlTypePattern()}\\.yaml$|\\.${escapedBranchName}\\.resource\\.file\\..+$`);
pattern = new RegExp(
`\\.${escapedBranchName}\\.${buildYamlTypePattern()}\\.yaml$|` +
`\\.${escapedBranchName}\\.resource\\.file\\..+$|` +
`/folder\\.${escapedBranchName}\\.meta\\.yaml$|` +
`^settings\\.${escapedBranchName}\\.yaml$`
);
branchPatternCache.set(currentBranch, pattern);
}
@@ -296,5 +390,10 @@ export function isCurrentBranchFile(path: string, branchOverride?: string): bool
*/
export function isBranchSpecificFile(path: string): boolean {
const yamlTypePattern = buildYamlTypePattern();
return new RegExp(`\\.[^.]+\\.${yamlTypePattern}\\.yaml$|\\.[^.]+\\.resource\\.file\\..+$`).test(path);
return new RegExp(
`\\.[^.]+\\.${yamlTypePattern}\\.yaml$|` +
`\\.[^.]+\\.resource\\.file\\..+$|` +
`/folder\\.[^.]+\\.meta\\.yaml$|` +
`^settings\\.[^.]+\\.yaml$`
).test(path);
}

View File

@@ -8,9 +8,13 @@ import { assertEquals, assertExists, assert } from "https://deno.land/std@0.224.
// Import the functions we need to test
import {
isSpecificItem,
isItemTypeConfigured,
toBranchSpecificPath,
fromBranchSpecificPath,
isBranchSpecificFile,
isCurrentBranchFile,
getBranchSpecificPath,
getSpecificItemsForCurrentBranch,
} from "../src/core/specific_items.ts";
import type { SpecificItemsConfig } from "../src/core/specific_items.ts";
@@ -212,12 +216,6 @@ Deno.test("round-trip: resource file with extension", () => {
// 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
@@ -336,3 +334,288 @@ Deno.test("branchOverride: getSpecificItemsForCurrentBranch merges common and br
assertEquals(result?.resources, ["shared/**"]);
assertEquals(result?.triggers, ["dev/triggers/**"]);
});
// =============================================================================
// FOLDER BRANCH-SPECIFIC TESTS
// Format: f/folder/folder.branchName.meta.yaml
// =============================================================================
Deno.test("toBranchSpecificPath: converts folder meta path to branch-specific", () => {
// f/my_folder/folder.meta.yaml -> f/my_folder/folder.main.meta.yaml
const result = toBranchSpecificPath("f/my_folder/folder.meta.yaml", "main");
assertEquals(result, "f/my_folder/folder.main.meta.yaml");
});
Deno.test("toBranchSpecificPath: converts nested folder meta path to branch-specific", () => {
const result = toBranchSpecificPath("f/parent/child/folder.meta.yaml", "develop");
assertEquals(result, "f/parent/child/folder.develop.meta.yaml");
});
Deno.test("toBranchSpecificPath: sanitizes branch name in folder path", () => {
const result = toBranchSpecificPath("f/env/folder.meta.yaml", "feature/test");
assertEquals(result, "f/env/folder.feature_test.meta.yaml");
});
Deno.test("fromBranchSpecificPath: converts branch-specific folder back to base", () => {
const result = fromBranchSpecificPath("f/my_folder/folder.main.meta.yaml", "main");
assertEquals(result, "f/my_folder/folder.meta.yaml");
});
Deno.test("fromBranchSpecificPath: handles nested branch-specific folder", () => {
const result = fromBranchSpecificPath("f/parent/child/folder.develop.meta.yaml", "develop");
assertEquals(result, "f/parent/child/folder.meta.yaml");
});
Deno.test("fromBranchSpecificPath: handles sanitized branch names for folders", () => {
const result = fromBranchSpecificPath("f/env/folder.feature_test.meta.yaml", "feature/test");
assertEquals(result, "f/env/folder.meta.yaml");
});
Deno.test("isSpecificItem: matches folder paths with glob pattern", () => {
const config: SpecificItemsConfig = {
folders: ["f/env_*"],
};
assertEquals(isSpecificItem("f/env_staging/folder.meta.yaml", config), true);
assertEquals(isSpecificItem("f/env_production/folder.meta.yaml", config), true);
assertEquals(isSpecificItem("f/other/folder.meta.yaml", config), false);
});
Deno.test("isSpecificItem: matches folder paths with exact pattern", () => {
const config: SpecificItemsConfig = {
folders: ["f/config"],
};
assertEquals(isSpecificItem("f/config/folder.meta.yaml", config), true);
assertEquals(isSpecificItem("f/other/folder.meta.yaml", config), false);
});
Deno.test("isBranchSpecificFile: detects branch-specific folder files", () => {
assertEquals(isBranchSpecificFile("f/my_folder/folder.main.meta.yaml"), true);
assertEquals(isBranchSpecificFile("f/my_folder/folder.develop.meta.yaml"), true);
assertEquals(isBranchSpecificFile("f/nested/path/folder.staging.meta.yaml"), true);
});
Deno.test("isBranchSpecificFile: returns false for non-branch-specific folder files", () => {
assertEquals(isBranchSpecificFile("f/my_folder/folder.meta.yaml"), false);
assertEquals(isBranchSpecificFile("f/nested/path/folder.meta.yaml"), false);
});
Deno.test("isCurrentBranchFile: detects branch-specific folder for current branch", () => {
assertEquals(isCurrentBranchFile("f/my_folder/folder.staging.meta.yaml", "staging"), true);
assertEquals(isCurrentBranchFile("f/my_folder/folder.staging.meta.yaml", "production"), false);
assertEquals(isCurrentBranchFile("f/my_folder/folder.meta.yaml", "staging"), false);
});
Deno.test("isCurrentBranchFile: handles sanitized branch for folders", () => {
assertEquals(isCurrentBranchFile("f/env/folder.feature_test.meta.yaml", "feature/test"), true);
assertEquals(isCurrentBranchFile("f/env/folder.feature_test.meta.yaml", "feature/other"), false);
});
Deno.test("round-trip: folder meta path conversion", () => {
const original = "f/configs/env_folder/folder.meta.yaml";
const branch = "main";
const branchSpecific = toBranchSpecificPath(original, branch);
assertEquals(branchSpecific, "f/configs/env_folder/folder.main.meta.yaml");
const restored = fromBranchSpecificPath(branchSpecific, branch);
assertEquals(restored, original);
});
Deno.test("round-trip: folder meta with sanitized branch", () => {
const original = "f/env/folder.meta.yaml";
const branch = "feature/new-env";
const branchSpecific = toBranchSpecificPath(original, branch);
assertEquals(branchSpecific, "f/env/folder.feature_new-env.meta.yaml");
const restored = fromBranchSpecificPath(branchSpecific, branch);
assertEquals(restored, original);
});
// =============================================================================
// SETTINGS BRANCH-SPECIFIC TESTS
// =============================================================================
Deno.test("toBranchSpecificPath: converts settings.yaml to branch-specific", () => {
const result = toBranchSpecificPath("settings.yaml", "main");
assertEquals(result, "settings.main.yaml");
});
Deno.test("toBranchSpecificPath: sanitizes branch name in settings path", () => {
const result = toBranchSpecificPath("settings.yaml", "feature/test");
assertEquals(result, "settings.feature_test.yaml");
});
Deno.test("fromBranchSpecificPath: converts branch-specific settings back to base", () => {
const result = fromBranchSpecificPath("settings.main.yaml", "main");
assertEquals(result, "settings.yaml");
});
Deno.test("fromBranchSpecificPath: handles sanitized branch names for settings", () => {
const result = fromBranchSpecificPath("settings.feature_test.yaml", "feature/test");
assertEquals(result, "settings.yaml");
});
Deno.test("isSpecificItem: matches settings.yaml when settings is true", () => {
const config: SpecificItemsConfig = {
settings: true,
};
assertEquals(isSpecificItem("settings.yaml", config), true);
});
Deno.test("isSpecificItem: does not match settings.yaml when settings is false", () => {
const config: SpecificItemsConfig = {
settings: false,
};
assertEquals(isSpecificItem("settings.yaml", config), false);
});
Deno.test("isSpecificItem: does not match settings.yaml when settings is undefined", () => {
const config: SpecificItemsConfig = {
variables: ["f/**"],
};
assertEquals(isSpecificItem("settings.yaml", config), false);
});
Deno.test("isBranchSpecificFile: detects branch-specific settings files", () => {
assertEquals(isBranchSpecificFile("settings.main.yaml"), true);
assertEquals(isBranchSpecificFile("settings.develop.yaml"), true);
assertEquals(isBranchSpecificFile("settings.feature_test.yaml"), true);
});
Deno.test("isBranchSpecificFile: returns false for non-branch-specific settings", () => {
assertEquals(isBranchSpecificFile("settings.yaml"), false);
});
Deno.test("isCurrentBranchFile: detects branch-specific settings for current branch", () => {
assertEquals(isCurrentBranchFile("settings.staging.yaml", "staging"), true);
assertEquals(isCurrentBranchFile("settings.staging.yaml", "production"), false);
assertEquals(isCurrentBranchFile("settings.yaml", "staging"), false);
});
Deno.test("isCurrentBranchFile: handles sanitized branch for settings", () => {
assertEquals(isCurrentBranchFile("settings.feature_test.yaml", "feature/test"), true);
assertEquals(isCurrentBranchFile("settings.feature_test.yaml", "feature/other"), false);
});
Deno.test("round-trip: settings path conversion", () => {
const original = "settings.yaml";
const branch = "main";
const branchSpecific = toBranchSpecificPath(original, branch);
assertEquals(branchSpecific, "settings.main.yaml");
const restored = fromBranchSpecificPath(branchSpecific, branch);
assertEquals(restored, original);
});
Deno.test("round-trip: settings with sanitized branch", () => {
const original = "settings.yaml";
const branch = "release/v1.0";
const branchSpecific = toBranchSpecificPath(original, branch);
assertEquals(branchSpecific, "settings.release_v1_0.yaml");
const restored = fromBranchSpecificPath(branchSpecific, branch);
assertEquals(restored, original);
});
// =============================================================================
// isItemTypeConfigured TESTS
// =============================================================================
Deno.test("isItemTypeConfigured: returns true when folders is configured", () => {
const config: SpecificItemsConfig = { folders: ["f/**"] };
assertEquals(isItemTypeConfigured("f/test/folder.meta.yaml", config), true);
});
Deno.test("isItemTypeConfigured: returns false when folders is NOT configured", () => {
const config: SpecificItemsConfig = { variables: ["f/**"] };
assertEquals(isItemTypeConfigured("f/test/folder.meta.yaml", config), false);
});
Deno.test("isItemTypeConfigured: returns true when variables is configured", () => {
const config: SpecificItemsConfig = { variables: ["f/**"] };
assertEquals(isItemTypeConfigured("f/test.variable.yaml", config), true);
});
Deno.test("isItemTypeConfigured: returns false when variables is NOT configured", () => {
const config: SpecificItemsConfig = { folders: ["f/**"] };
assertEquals(isItemTypeConfigured("f/test.variable.yaml", config), false);
});
Deno.test("isItemTypeConfigured: returns true when settings is configured", () => {
const config: SpecificItemsConfig = { settings: true };
assertEquals(isItemTypeConfigured("settings.yaml", config), true);
});
Deno.test("isItemTypeConfigured: returns false when settings is NOT configured", () => {
const config: SpecificItemsConfig = { folders: ["f/**"] };
assertEquals(isItemTypeConfigured("settings.yaml", config), false);
});
Deno.test("isItemTypeConfigured: returns false for undefined config", () => {
assertEquals(isItemTypeConfigured("f/test/folder.meta.yaml", undefined), false);
});
// =============================================================================
// Branch-specific item filtering behavior tests
// =============================================================================
Deno.test("filtering: folder type configured and matches - should process branch-specific file", () => {
const config: SpecificItemsConfig = { folders: ["f/**"] };
const basePath = "f/compliance/folder.meta.yaml";
// Type is configured AND matches pattern = should process
const typeConfigured = isItemTypeConfigured(basePath, config);
const matchesPattern = isSpecificItem(basePath, config);
assertEquals(typeConfigured, true);
assertEquals(matchesPattern, true);
// Skip condition: typeConfigured && !matchesPattern = false (should NOT skip)
assertEquals(typeConfigured && !matchesPattern, false);
});
Deno.test("filtering: folder type configured but does NOT match - should skip branch-specific file", () => {
const config: SpecificItemsConfig = { folders: ["f/other/**"] };
const basePath = "f/compliance/folder.meta.yaml";
// Type is configured but does NOT match pattern = should skip
const typeConfigured = isItemTypeConfigured(basePath, config);
const matchesPattern = isSpecificItem(basePath, config);
assertEquals(typeConfigured, true);
assertEquals(matchesPattern, false);
// Skip condition: typeConfigured && !matchesPattern = true (should skip)
assertEquals(typeConfigured && !matchesPattern, true);
});
Deno.test("filtering: folder type NOT configured - should process branch-specific file", () => {
const config: SpecificItemsConfig = { variables: ["f/**"] };
const basePath = "f/compliance/folder.meta.yaml";
// Type is NOT configured = should process (not subject to specificItems rules)
const typeConfigured = isItemTypeConfigured(basePath, config);
assertEquals(typeConfigured, false);
// Skip condition: typeConfigured && !matchesPattern = false (should NOT skip)
assertEquals(typeConfigured && !isSpecificItem(basePath, config), false);
});
Deno.test("filtering: getSpecificItemsForCurrentBranch returns correct config", () => {
const config = {
gitBranches: {
dev: {
specificItems: {
folders: ["f/**"]
}
},
prod: {
specificItems: {
folders: ["f/prod/**"]
}
}
}
};
const devItems = getSpecificItemsForCurrentBranch(config as any, "dev");
assertEquals(devItems?.folders, ["f/**"]);
const prodItems = getSpecificItemsForCurrentBranch(config as any, "prod");
assertEquals(prodItems?.folders, ["f/prod/**"]);
const unknownItems = getSpecificItemsForCurrentBranch(config as any, "unknown");
assertEquals(unknownItems, undefined);
});