feat: unify CLI config to workspaces, deprecate gitBranches/environments (#8767)
* refactor: unify CLI config to workspaces, deprecate gitBranches/environments Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: update frontend examples and regenerate system prompts for workspaces config Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: update test files to use workspaces config instead of gitBranches Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: handle --branch with --base-url correctly in sync pull/push Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: warn when --workspace overrides auto-detected branch or misses config entry Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: show reason why workspace was selected in log message Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: clarify specificItems file naming uses gitBranch as suffix Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: rename branch-specific to workspace-specific, use workspace name as file suffix Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: rename branch-specific to workspace-specific, add comprehensive integration tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: simplify bind and init to be workspace-centric Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: make bind/unbind interactive with --workspace and --branch flags Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: make bind interactive with profile selection, workspace name, and optional branch Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: init offers to bind workspace using same flow as wmill workspace bind Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: skip backend git-sync check in init when no workspace was bound Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: skip all API calls in init when no workspace was bound Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: log when RT namespace is skipped, offer to generate it after bind Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: warn when no workspace bound during init Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: init git-sync check uses bound workspace, not active profile Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: init uses selected profile directly, avoids re-resolving and duplicate prompt Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: init skips requireLogin, uses bound profile token directly Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: auto-pick or prompt workspace from config when no branch matches Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: show configured workspaces list and bind hint in resolution messages Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: cache bound profile to avoid duplicate profile selection prompts in init Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: hoist boundProfile scope, add 2 comprehensive integration tests covering all flows Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: rt.d.ts prompt defaults to no when file exists, better description Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: remove empty overrides from generated config, add specificItems hint Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: add inline comments for non-trivial fields, add overrides/promotionOverrides hints to bound workspaces Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: regenerate system prompts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,15 +2,15 @@ import { expect, test } from "bun:test";
|
||||
import { getEffectiveSettings, type SyncOptions } from "../src/core/conf.ts";
|
||||
|
||||
// =============================================================================
|
||||
// CONF.TS BRANCH OVERRIDE TESTS
|
||||
// Tests for getEffectiveSettings with branchOverride parameter
|
||||
// CONF.TS WORKSPACE OVERRIDE TESTS
|
||||
// Tests for getEffectiveSettings with workspaceNameOverride parameter
|
||||
// =============================================================================
|
||||
|
||||
test("getEffectiveSettings: applies branch overrides when branchOverride is provided", async () => {
|
||||
test("getEffectiveSettings: applies workspace overrides when workspaceNameOverride is provided", async () => {
|
||||
const config: SyncOptions = {
|
||||
defaultTs: "bun",
|
||||
includes: ["f/**"],
|
||||
gitBranches: {
|
||||
workspaces: {
|
||||
staging: {
|
||||
overrides: {
|
||||
includes: ["staging/**"],
|
||||
@@ -26,25 +26,25 @@ test("getEffectiveSettings: applies branch overrides when branchOverride is prov
|
||||
},
|
||||
};
|
||||
|
||||
// Test with staging branch override
|
||||
// Test with staging workspace override
|
||||
const stagingSettings = await getEffectiveSettings(config, undefined, true, true, "staging");
|
||||
expect(stagingSettings.includes).toEqual(["staging/**"]);
|
||||
expect(stagingSettings.skipVariables).toEqual(true);
|
||||
expect(stagingSettings.skipSecrets).toEqual(undefined);
|
||||
|
||||
// Test with production branch override
|
||||
// Test with production workspace override
|
||||
const prodSettings = await getEffectiveSettings(config, undefined, true, true, "production");
|
||||
expect(prodSettings.includes).toEqual(["prod/**"]);
|
||||
expect(prodSettings.skipSecrets).toEqual(true);
|
||||
expect(prodSettings.skipVariables).toEqual(undefined);
|
||||
});
|
||||
|
||||
test("getEffectiveSettings: uses top-level settings when branchOverride has no overrides", async () => {
|
||||
test("getEffectiveSettings: uses top-level settings when workspace has no overrides", async () => {
|
||||
const config: SyncOptions = {
|
||||
defaultTs: "bun",
|
||||
includes: ["f/**"],
|
||||
skipVariables: true,
|
||||
gitBranches: {
|
||||
workspaces: {
|
||||
staging: {
|
||||
// No overrides defined
|
||||
},
|
||||
@@ -57,11 +57,11 @@ test("getEffectiveSettings: uses top-level settings when branchOverride has no o
|
||||
expect(settings.defaultTs).toEqual("bun");
|
||||
});
|
||||
|
||||
test("getEffectiveSettings: uses top-level settings for unknown branch", async () => {
|
||||
test("getEffectiveSettings: uses top-level settings for unknown workspace", async () => {
|
||||
const config: SyncOptions = {
|
||||
defaultTs: "bun",
|
||||
includes: ["f/**"],
|
||||
gitBranches: {
|
||||
workspaces: {
|
||||
staging: {
|
||||
overrides: {
|
||||
includes: ["staging/**"],
|
||||
@@ -79,7 +79,7 @@ test("getEffectiveSettings: promotionOverrides take precedence when promotion sp
|
||||
const config: SyncOptions = {
|
||||
defaultTs: "bun",
|
||||
includes: ["f/**"],
|
||||
gitBranches: {
|
||||
workspaces: {
|
||||
production: {
|
||||
overrides: {
|
||||
includes: ["prod/**"],
|
||||
@@ -103,13 +103,13 @@ test("getEffectiveSettings: promotionOverrides take precedence when promotion sp
|
||||
expect(promoSettings.skipVariables).toEqual(true);
|
||||
});
|
||||
|
||||
test("getEffectiveSettings: branchOverride works without gitBranches config", async () => {
|
||||
test("getEffectiveSettings: workspaceNameOverride works without workspaces config", async () => {
|
||||
const config: SyncOptions = {
|
||||
defaultTs: "bun",
|
||||
includes: ["f/**"],
|
||||
};
|
||||
|
||||
// Should not throw even with branchOverride but no gitBranches
|
||||
// Should not throw even with workspaceNameOverride but no workspaces
|
||||
const settings = await getEffectiveSettings(config, undefined, true, true, "staging");
|
||||
expect(settings.includes).toEqual(["f/**"]);
|
||||
expect(settings.defaultTs).toEqual("bun");
|
||||
@@ -124,7 +124,7 @@ test("getEffectiveSettings: preserves all top-level settings in merged result",
|
||||
skipResources: false,
|
||||
skipFlows: false,
|
||||
parallel: 4,
|
||||
gitBranches: {
|
||||
workspaces: {
|
||||
staging: {
|
||||
overrides: {
|
||||
skipVariables: true, // Override just this one
|
||||
|
||||
197
cli/test/init_bind_flow.test.ts
Normal file
197
cli/test/init_bind_flow.test.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { writeFile, readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { withTestBackend } from "./test_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
|
||||
|
||||
async function setupProfile(backend: any, name: string): Promise<void> {
|
||||
await addWorkspace({
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name,
|
||||
token: backend.token,
|
||||
}, { force: true, configDir: backend.testConfigDir });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test 1: Full sync flow with new workspaces config — overrides, gitBranch ≠ key,
|
||||
// workspace-specific file suffix, legacy back-compat, and promotion
|
||||
// =============================================================================
|
||||
|
||||
test("Full workspaces config: overrides, gitBranch mapping, specific items suffix, legacy compat, promotion", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupProfile(backend, "test_profile");
|
||||
|
||||
// --- Part A: New workspaces config with gitBranch ≠ key ---
|
||||
await writeFile(join(tempDir, "wmill.yaml"), `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
|
||||
workspaces:
|
||||
production:
|
||||
gitBranch: prod_branch
|
||||
baseUrl: ${backend.baseUrl}
|
||||
workspaceId: ${backend.workspace}
|
||||
overrides:
|
||||
skipVariables: true
|
||||
promotionOverrides:
|
||||
skipVariables: true
|
||||
skipResources: true
|
||||
specificItems:
|
||||
variables:
|
||||
- "f/**"
|
||||
development:
|
||||
gitBranch: dev_branch
|
||||
baseUrl: ${backend.baseUrl}
|
||||
workspaceId: ${backend.workspace}
|
||||
overrides:
|
||||
skipVariables: false`, "utf-8");
|
||||
|
||||
// A1: --branch prod_branch → resolves to workspace "production", skipVariables: true
|
||||
const prodResult = await backend.runCLICommand([
|
||||
'sync', 'pull', '--branch', 'prod_branch', '--dry-run', '--json-output',
|
||||
], tempDir);
|
||||
expect(prodResult.code).toEqual(0);
|
||||
const prodChanges = parseJsonFromCLIOutput(prodResult.stdout).changes || [];
|
||||
const prodPaths = prodChanges.map((c: any) => c.path);
|
||||
expect(prodPaths.some((p: string) => p.includes('.variable.yaml'))).toEqual(false);
|
||||
|
||||
// A2: --branch dev_branch → resolves to workspace "development", skipVariables: false
|
||||
const devResult = await backend.runCLICommand([
|
||||
'sync', 'pull', '--branch', 'dev_branch', '--dry-run', '--json-output',
|
||||
], tempDir);
|
||||
expect(devResult.code).toEqual(0);
|
||||
const devPaths = (parseJsonFromCLIOutput(devResult.stdout).changes || []).map((c: any) => c.path);
|
||||
expect(devPaths.some((p: string) => p.includes('.variable.yaml'))).toEqual(true);
|
||||
|
||||
// A3: workspace-specific file suffix uses workspace name "production", not "prod_branch"
|
||||
const wsSpecificPaths = prodChanges
|
||||
.filter((c: any) => c.workspace_specific_path)
|
||||
.map((c: any) => c.workspace_specific_path);
|
||||
for (const p of wsSpecificPaths) {
|
||||
expect(p).toContain(".production.");
|
||||
expect(p).not.toContain(".prod_branch.");
|
||||
}
|
||||
|
||||
// A4: --promotion prod_branch applies promotionOverrides (skipResources: true)
|
||||
const promoResult = await backend.runCLICommand([
|
||||
'sync', 'pull', '--branch', 'prod_branch', '--promotion', 'prod_branch',
|
||||
'--dry-run', '--json-output',
|
||||
], tempDir);
|
||||
expect(promoResult.code).toEqual(0);
|
||||
const promoPaths = (parseJsonFromCLIOutput(promoResult.stdout).changes || []).map((c: any) => c.path);
|
||||
expect(promoPaths.some((p: string) => p.includes('.variable.yaml'))).toEqual(false);
|
||||
expect(promoPaths.some((p: string) => p.includes('.resource.yaml'))).toEqual(false);
|
||||
|
||||
// --- Part B: Legacy gitBranches config works via normalization ---
|
||||
await writeFile(join(tempDir, "wmill.yaml"), `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
|
||||
gitBranches:
|
||||
legacy_branch:
|
||||
overrides:
|
||||
skipVariables: true`, "utf-8");
|
||||
|
||||
const legacyResult = await backend.runCLICommand([
|
||||
'sync', 'pull', '--branch', 'legacy_branch', '--dry-run', '--json-output',
|
||||
], tempDir);
|
||||
expect(legacyResult.code).toEqual(0);
|
||||
const legacyPaths = (parseJsonFromCLIOutput(legacyResult.stdout).changes || []).map((c: any) => c.path);
|
||||
expect(legacyPaths.some((p: string) => p.includes('.variable.yaml'))).toEqual(false);
|
||||
|
||||
// --- Part C: Legacy environments config ---
|
||||
await writeFile(join(tempDir, "wmill.yaml"), `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
|
||||
environments:
|
||||
env_branch:
|
||||
overrides:
|
||||
skipVariables: true`, "utf-8");
|
||||
|
||||
const envResult = await backend.runCLICommand([
|
||||
'sync', 'pull', '--branch', 'env_branch', '--dry-run', '--json-output',
|
||||
], tempDir);
|
||||
expect(envResult.code).toEqual(0);
|
||||
const envPaths = (parseJsonFromCLIOutput(envResult.stdout).changes || []).map((c: any) => c.path);
|
||||
expect(envPaths.some((p: string) => p.includes('.variable.yaml'))).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Test 2: Config migrate and workspace resolution fallbacks
|
||||
// =============================================================================
|
||||
|
||||
test("Config migrate and workspace resolution fallbacks", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupProfile(backend, "fallback_test");
|
||||
|
||||
// --- Part A: config migrate converts gitBranches → workspaces ---
|
||||
await writeFile(join(tempDir, "wmill.yaml"), `defaultTs: bun
|
||||
includes:
|
||||
- "f/**"
|
||||
gitBranches:
|
||||
main:
|
||||
baseUrl: https://app.windmill.dev
|
||||
workspaceId: production
|
||||
overrides:
|
||||
skipSecrets: false
|
||||
commonSpecificItems:
|
||||
variables:
|
||||
- "f/shared/**"`, "utf-8");
|
||||
|
||||
const migrateResult = await backend.runCLICommand(['config', 'migrate'], tempDir);
|
||||
expect(migrateResult.code).toEqual(0);
|
||||
|
||||
const migrated = await readFile(join(tempDir, "wmill.yaml"), "utf-8");
|
||||
expect(migrated).toContain("workspaces:");
|
||||
expect(migrated).not.toContain("gitBranches:");
|
||||
expect(migrated).toContain("production");
|
||||
expect(migrated).toContain("commonSpecificItems");
|
||||
|
||||
// config migrate is idempotent
|
||||
const migrateAgain = await backend.runCLICommand(['config', 'migrate'], tempDir);
|
||||
expect(migrateAgain.code).toEqual(0);
|
||||
|
||||
// --- Part B: single workspace auto-selected ---
|
||||
await writeFile(join(tempDir, "wmill.yaml"), `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
workspaces:
|
||||
only_ws:
|
||||
baseUrl: ${backend.baseUrl}
|
||||
workspaceId: ${backend.workspace}
|
||||
overrides:
|
||||
skipVariables: true`, "utf-8");
|
||||
|
||||
// No --branch, no --workspace: should auto-select "only_ws"
|
||||
const singleResult = await backend.runCLICommand([
|
||||
'sync', 'pull', '--dry-run', '--json-output',
|
||||
], tempDir);
|
||||
expect(singleResult.code).toEqual(0);
|
||||
const singlePaths = (parseJsonFromCLIOutput(singleResult.stdout).changes || []).map((c: any) => c.path);
|
||||
// skipVariables: true should be applied from auto-selected workspace
|
||||
expect(singlePaths.some((p: string) => p.includes('.variable.yaml'))).toEqual(false);
|
||||
|
||||
// --- Part C: no workspaces config falls back to active profile ---
|
||||
await writeFile(join(tempDir, "wmill.yaml"), `defaultTs: bun
|
||||
includes:
|
||||
- "**"`, "utf-8");
|
||||
|
||||
const noWsResult = await backend.runCLICommand([
|
||||
'sync', 'pull', '--dry-run', '--json-output',
|
||||
], tempDir);
|
||||
expect(noWsResult.code).toEqual(0);
|
||||
// Should succeed using active profile, no overrides applied (all defaults)
|
||||
const noWsPaths = (parseJsonFromCLIOutput(noWsResult.stdout).changes || []).map((c: any) => c.path);
|
||||
expect(noWsPaths.length).toBeGreaterThan(0);
|
||||
// Variables should be included (no skipVariables override)
|
||||
expect(noWsPaths.some((p: string) => p.includes('.variable.yaml'))).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -129,7 +129,7 @@ test.skipIf(shouldSkipOnCI())("Init: --use-backend flag applies git-sync setting
|
||||
expect(wmillYaml).toContain("g/**");
|
||||
|
||||
// Should have empty overrides section for consistency
|
||||
expect(wmillYaml).toContain("gitBranches: {}");
|
||||
expect(wmillYaml).toContain("workspaces:");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -182,6 +182,6 @@ test.skipIf(shouldSkipOnCI())("Init: --use-default bypasses backend settings che
|
||||
|
||||
// Should NOT have backend-specific settings
|
||||
expect(wmillYaml.includes("f/should-be-ignored/**")).toEqual(false);
|
||||
expect(wmillYaml).toContain("gitBranches: {}");
|
||||
expect(wmillYaml).toContain("workspaces:");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,15 +25,14 @@ describe("generateCommentedTemplate", () => {
|
||||
expect(typeof config).toBe("object");
|
||||
});
|
||||
|
||||
test("uses provided branch name in gitBranches", () => {
|
||||
test("uses provided branch name in workspaces", () => {
|
||||
const config = parse(generateCommentedTemplate("my-feature"));
|
||||
expect(config.gitBranches["my-feature"]).toBeDefined();
|
||||
expect(config.gitBranches["my-feature"].overrides).toEqual({});
|
||||
expect(config.workspaces["my-feature"]).toBeDefined();
|
||||
});
|
||||
|
||||
test("defaults to 'main' when no branch name given", () => {
|
||||
const config = parse(generateCommentedTemplate());
|
||||
expect(config.gitBranches["main"]).toBeDefined();
|
||||
expect(config.workspaces["main"]).toBeDefined();
|
||||
});
|
||||
|
||||
test("quotes branch names with YAML-special characters", () => {
|
||||
@@ -41,7 +40,7 @@ describe("generateCommentedTemplate", () => {
|
||||
for (const branch of specialBranches) {
|
||||
const yaml = generateCommentedTemplate(branch);
|
||||
const config = parse(yaml);
|
||||
expect(config.gitBranches[branch]).toBeDefined();
|
||||
expect(config.workspaces[branch]).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -50,19 +49,19 @@ describe("generateCommentedTemplate", () => {
|
||||
expect(yaml.startsWith("# yaml-language-server: $schema=wmill.schema.json")).toBe(true);
|
||||
});
|
||||
|
||||
test("includes all non-commented CONFIG_REFERENCE entries as active YAML keys", () => {
|
||||
test("includes all non-commented, non-skipped CONFIG_REFERENCE entries as active YAML keys", () => {
|
||||
const config = parse(generateCommentedTemplate("main"));
|
||||
for (const opt of CONFIG_REFERENCE) {
|
||||
if (!opt.commented) {
|
||||
if (!opt.commented && !opt.skipInTemplate) {
|
||||
expect(config).toHaveProperty(opt.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("does not include commented entries as active YAML keys", () => {
|
||||
test("does not include commented or skipped entries as active YAML keys", () => {
|
||||
const config = parse(generateCommentedTemplate("main"));
|
||||
for (const opt of CONFIG_REFERENCE) {
|
||||
if (opt.commented && opt.name !== "environments") {
|
||||
if (opt.commented || opt.skipInTemplate) {
|
||||
expect(config[opt.name]).toBeUndefined();
|
||||
}
|
||||
}
|
||||
@@ -123,23 +122,31 @@ describe("generateJsonSchema", () => {
|
||||
expect(schema.properties.codebases.items.required).toContain("relative_path");
|
||||
});
|
||||
|
||||
test("includes gitBranches with branch config schema", () => {
|
||||
const branchSchema = schema.properties.gitBranches.additionalProperties;
|
||||
expect(branchSchema.properties.baseUrl).toBeDefined();
|
||||
expect(branchSchema.properties.workspaceId).toBeDefined();
|
||||
expect(branchSchema.properties.specificItems).toBeDefined();
|
||||
expect(branchSchema.properties.specificItems.properties.variables).toBeDefined();
|
||||
test("includes workspaces with workspace config schema", () => {
|
||||
const wsSchema = schema.properties.workspaces.additionalProperties;
|
||||
expect(wsSchema.properties.gitBranch).toBeDefined();
|
||||
expect(wsSchema.properties.baseUrl).toBeDefined();
|
||||
expect(wsSchema.properties.workspaceId).toBeDefined();
|
||||
expect(wsSchema.properties.specificItems).toBeDefined();
|
||||
expect(wsSchema.properties.specificItems.properties.variables).toBeDefined();
|
||||
});
|
||||
|
||||
test("includes environments as alias for gitBranches", () => {
|
||||
test("includes gitBranches as deprecated alias for workspaces", () => {
|
||||
expect(schema.properties.gitBranches).toBeDefined();
|
||||
expect(schema.properties.gitBranches.additionalProperties).toEqual(
|
||||
schema.properties.workspaces.additionalProperties
|
||||
);
|
||||
});
|
||||
|
||||
test("includes environments as deprecated alias for workspaces", () => {
|
||||
expect(schema.properties.environments).toBeDefined();
|
||||
expect(schema.properties.environments.additionalProperties).toEqual(
|
||||
schema.properties.gitBranches.additionalProperties
|
||||
schema.properties.workspaces.additionalProperties
|
||||
);
|
||||
});
|
||||
|
||||
test("does not contain template-only keys in schema output", () => {
|
||||
const templateKeys = ["section", "sectionNote", "commented", "templateValue", "example", "inlineComment", "groupNote"];
|
||||
const templateKeys = ["section", "sectionNote", "commented", "templateValue", "example", "inlineComment", "groupNote", "skipInTemplate"];
|
||||
const json = JSON.stringify(schema);
|
||||
for (const key of templateKeys) {
|
||||
expect(json).not.toContain(`"${key}"`);
|
||||
@@ -172,15 +179,23 @@ describe("formatConfigReference", () => {
|
||||
expect(output).toContain("codebases[].external");
|
||||
});
|
||||
|
||||
test("auto-expands gitBranches sub-fields", () => {
|
||||
expect(output).toContain("gitBranches.<branch>.baseUrl");
|
||||
expect(output).toContain("gitBranches.<branch>.workspaceId");
|
||||
expect(output).toContain("gitBranches.<branch>.specificItems.variables");
|
||||
test("auto-expands workspaces sub-fields", () => {
|
||||
expect(output).toContain("workspaces.<workspace>.gitBranch");
|
||||
expect(output).toContain("workspaces.<workspace>.baseUrl");
|
||||
expect(output).toContain("workspaces.<workspace>.workspaceId");
|
||||
expect(output).toContain("workspaces.<workspace>.specificItems.variables");
|
||||
});
|
||||
|
||||
test("auto-expands commonSpecificItems sub-fields", () => {
|
||||
expect(output).toContain("gitBranches.commonSpecificItems.variables");
|
||||
expect(output).toContain("gitBranches.commonSpecificItems.settings");
|
||||
expect(output).toContain("workspaces.commonSpecificItems.variables");
|
||||
expect(output).toContain("workspaces.commonSpecificItems.settings");
|
||||
});
|
||||
|
||||
test("deprecated entries are listed but not expanded", () => {
|
||||
expect(output).toContain("gitBranches");
|
||||
expect(output).toContain("[Deprecated]");
|
||||
// Should NOT have expanded sub-fields for deprecated entries
|
||||
expect(output).not.toContain("gitBranches.<workspace>");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -207,7 +222,7 @@ describe("formatConfigReferenceJson", () => {
|
||||
|
||||
test("does not contain template-only keys", () => {
|
||||
const parsed = JSON.parse(formatConfigReferenceJson());
|
||||
const templateKeys = ["section", "sectionNote", "commented", "templateValue", "example", "inlineComment", "groupNote"];
|
||||
const templateKeys = ["section", "sectionNote", "commented", "templateValue", "example", "inlineComment", "groupNote", "skipInTemplate"];
|
||||
for (const entry of parsed) {
|
||||
for (const key of templateKeys) {
|
||||
expect(entry).not.toHaveProperty(key);
|
||||
|
||||
@@ -17,7 +17,7 @@ test("Override Settings: branch override inherits non-overridden settings from b
|
||||
skipResources: true, // Base has this as true
|
||||
skipApps: false, // Base has this as false
|
||||
defaultTs: "bun" as const,
|
||||
gitBranches: {
|
||||
workspaces: {
|
||||
main: {
|
||||
overrides: {
|
||||
includes: ["override/**"],
|
||||
@@ -49,7 +49,7 @@ test("Override Settings: branch-specific settings take precedence", async () =>
|
||||
const config = {
|
||||
includes: ["default/**"],
|
||||
skipVariables: false,
|
||||
gitBranches: {
|
||||
workspaces: {
|
||||
main: {
|
||||
overrides: {
|
||||
skipVariables: true,
|
||||
|
||||
@@ -2,93 +2,93 @@ import { expect, test } from "bun:test";
|
||||
|
||||
// =============================================================================
|
||||
// SPECIFIC ITEMS UNIT TESTS
|
||||
// Tests for branch-specific file path functions (no Docker required)
|
||||
// Tests for workspace-specific file path functions (no Docker required)
|
||||
// =============================================================================
|
||||
|
||||
// Import the functions we need to test
|
||||
import {
|
||||
isSpecificItem,
|
||||
isItemTypeConfigured,
|
||||
toBranchSpecificPath,
|
||||
fromBranchSpecificPath,
|
||||
isBranchSpecificFile,
|
||||
isCurrentBranchFile,
|
||||
getBranchSpecificPath,
|
||||
toWorkspaceSpecificPath,
|
||||
fromWorkspaceSpecificPath,
|
||||
isWorkspaceSpecificFile,
|
||||
isCurrentWorkspaceFile,
|
||||
getWorkspaceSpecificPath,
|
||||
getSpecificItemsForCurrentBranch,
|
||||
} from "../src/core/specific_items.ts";
|
||||
|
||||
import type { SpecificItemsConfig } from "../src/core/specific_items.ts";
|
||||
|
||||
// =============================================================================
|
||||
// toBranchSpecificPath TESTS
|
||||
// toWorkspaceSpecificPath TESTS
|
||||
// =============================================================================
|
||||
|
||||
test("toBranchSpecificPath: converts variable path to branch-specific", () => {
|
||||
const result = toBranchSpecificPath("f/test.variable.yaml", "main");
|
||||
test("toWorkspaceSpecificPath: converts variable path to workspace-specific", () => {
|
||||
const result = toWorkspaceSpecificPath("f/test.variable.yaml", "main");
|
||||
expect(result).toEqual("f/test.main.variable.yaml");
|
||||
});
|
||||
|
||||
test("toBranchSpecificPath: converts resource path to branch-specific", () => {
|
||||
const result = toBranchSpecificPath("u/admin/db.resource.yaml", "develop");
|
||||
test("toWorkspaceSpecificPath: converts resource path to workspace-specific", () => {
|
||||
const result = toWorkspaceSpecificPath("u/admin/db.resource.yaml", "develop");
|
||||
expect(result).toEqual("u/admin/db.develop.resource.yaml");
|
||||
});
|
||||
|
||||
test("toBranchSpecificPath: converts trigger path to branch-specific", () => {
|
||||
const result = toBranchSpecificPath("f/my_trigger.http_trigger.yaml", "feature-x");
|
||||
test("toWorkspaceSpecificPath: converts trigger path to workspace-specific", () => {
|
||||
const result = toWorkspaceSpecificPath("f/my_trigger.http_trigger.yaml", "feature-x");
|
||||
expect(result).toEqual("f/my_trigger.feature-x.http_trigger.yaml");
|
||||
});
|
||||
|
||||
test("toBranchSpecificPath: sanitizes branch names with slashes", () => {
|
||||
const result = toBranchSpecificPath("f/test.variable.yaml", "feature/my-feature");
|
||||
test("toWorkspaceSpecificPath: sanitizes branch names with slashes", () => {
|
||||
const result = toWorkspaceSpecificPath("f/test.variable.yaml", "feature/my-feature");
|
||||
expect(result).toEqual("f/test.feature_my-feature.variable.yaml");
|
||||
});
|
||||
|
||||
test("toBranchSpecificPath: sanitizes branch names with dots", () => {
|
||||
const result = toBranchSpecificPath("f/test.variable.yaml", "release.1.0");
|
||||
test("toWorkspaceSpecificPath: sanitizes branch names with dots", () => {
|
||||
const result = toWorkspaceSpecificPath("f/test.variable.yaml", "release.1.0");
|
||||
expect(result).toEqual("f/test.release_1_0.variable.yaml");
|
||||
});
|
||||
|
||||
test("toBranchSpecificPath: leaves non-specific files unchanged", () => {
|
||||
const result = toBranchSpecificPath("f/script.ts", "main");
|
||||
test("toWorkspaceSpecificPath: leaves non-specific files unchanged", () => {
|
||||
const result = toWorkspaceSpecificPath("f/script.ts", "main");
|
||||
expect(result).toEqual("f/script.ts");
|
||||
});
|
||||
|
||||
test("toBranchSpecificPath: handles resource files with extensions", () => {
|
||||
const result = toBranchSpecificPath("f/config.resource.file.json", "main");
|
||||
test("toWorkspaceSpecificPath: handles resource files with extensions", () => {
|
||||
const result = toWorkspaceSpecificPath("f/config.resource.file.json", "main");
|
||||
expect(result).toEqual("f/config.main.resource.file.json");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// fromBranchSpecificPath TESTS
|
||||
// fromWorkspaceSpecificPath TESTS
|
||||
// =============================================================================
|
||||
|
||||
test("fromBranchSpecificPath: converts branch-specific variable back to base", () => {
|
||||
const result = fromBranchSpecificPath("f/test.main.variable.yaml", "main");
|
||||
test("fromWorkspaceSpecificPath: converts workspace-specific variable back to base", () => {
|
||||
const result = fromWorkspaceSpecificPath("f/test.main.variable.yaml", "main");
|
||||
expect(result).toEqual("f/test.variable.yaml");
|
||||
});
|
||||
|
||||
test("fromBranchSpecificPath: converts branch-specific resource back to base", () => {
|
||||
const result = fromBranchSpecificPath("u/admin/db.develop.resource.yaml", "develop");
|
||||
test("fromWorkspaceSpecificPath: converts workspace-specific resource back to base", () => {
|
||||
const result = fromWorkspaceSpecificPath("u/admin/db.develop.resource.yaml", "develop");
|
||||
expect(result).toEqual("u/admin/db.resource.yaml");
|
||||
});
|
||||
|
||||
test("fromBranchSpecificPath: converts branch-specific trigger back to base", () => {
|
||||
const result = fromBranchSpecificPath("f/my_trigger.feature-x.http_trigger.yaml", "feature-x");
|
||||
test("fromWorkspaceSpecificPath: converts workspace-specific trigger back to base", () => {
|
||||
const result = fromWorkspaceSpecificPath("f/my_trigger.feature-x.http_trigger.yaml", "feature-x");
|
||||
expect(result).toEqual("f/my_trigger.http_trigger.yaml");
|
||||
});
|
||||
|
||||
test("fromBranchSpecificPath: handles sanitized branch names", () => {
|
||||
const result = fromBranchSpecificPath("f/test.feature_my-feature.variable.yaml", "feature/my-feature");
|
||||
test("fromWorkspaceSpecificPath: handles sanitized branch names", () => {
|
||||
const result = fromWorkspaceSpecificPath("f/test.feature_my-feature.variable.yaml", "feature/my-feature");
|
||||
expect(result).toEqual("f/test.variable.yaml");
|
||||
});
|
||||
|
||||
test("fromBranchSpecificPath: returns unchanged if not branch-specific", () => {
|
||||
const result = fromBranchSpecificPath("f/test.variable.yaml", "main");
|
||||
test("fromWorkspaceSpecificPath: returns unchanged if not workspace-specific", () => {
|
||||
const result = fromWorkspaceSpecificPath("f/test.variable.yaml", "main");
|
||||
expect(result).toEqual("f/test.variable.yaml");
|
||||
});
|
||||
|
||||
test("fromBranchSpecificPath: handles resource files with extensions", () => {
|
||||
const result = fromBranchSpecificPath("f/config.main.resource.file.json", "main");
|
||||
test("fromWorkspaceSpecificPath: handles resource files with extensions", () => {
|
||||
const result = fromWorkspaceSpecificPath("f/config.main.resource.file.json", "main");
|
||||
expect(result).toEqual("f/config.resource.file.json");
|
||||
});
|
||||
|
||||
@@ -143,36 +143,36 @@ test("isSpecificItem: handles exact path patterns", () => {
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// isBranchSpecificFile TESTS
|
||||
// isWorkspaceSpecificFile TESTS
|
||||
// =============================================================================
|
||||
|
||||
test("isBranchSpecificFile: detects branch-specific variable files", () => {
|
||||
expect(isBranchSpecificFile("f/test.main.variable.yaml")).toEqual(true);
|
||||
expect(isBranchSpecificFile("f/test.develop.variable.yaml")).toEqual(true);
|
||||
expect(isBranchSpecificFile("f/test.feature_branch.variable.yaml")).toEqual(true);
|
||||
test("isWorkspaceSpecificFile: detects workspace-specific variable files", () => {
|
||||
expect(isWorkspaceSpecificFile("f/test.main.variable.yaml")).toEqual(true);
|
||||
expect(isWorkspaceSpecificFile("f/test.develop.variable.yaml")).toEqual(true);
|
||||
expect(isWorkspaceSpecificFile("f/test.feature_branch.variable.yaml")).toEqual(true);
|
||||
});
|
||||
|
||||
test("isBranchSpecificFile: detects branch-specific resource files", () => {
|
||||
expect(isBranchSpecificFile("u/admin/db.main.resource.yaml")).toEqual(true);
|
||||
expect(isBranchSpecificFile("u/admin/db.staging.resource.yaml")).toEqual(true);
|
||||
test("isWorkspaceSpecificFile: detects workspace-specific resource files", () => {
|
||||
expect(isWorkspaceSpecificFile("u/admin/db.main.resource.yaml")).toEqual(true);
|
||||
expect(isWorkspaceSpecificFile("u/admin/db.staging.resource.yaml")).toEqual(true);
|
||||
});
|
||||
|
||||
test("isBranchSpecificFile: detects branch-specific trigger files", () => {
|
||||
expect(isBranchSpecificFile("f/my.main.http_trigger.yaml")).toEqual(true);
|
||||
expect(isBranchSpecificFile("f/my.develop.kafka_trigger.yaml")).toEqual(true);
|
||||
expect(isBranchSpecificFile("f/my.main.websocket_trigger.yaml")).toEqual(true);
|
||||
test("isWorkspaceSpecificFile: detects workspace-specific trigger files", () => {
|
||||
expect(isWorkspaceSpecificFile("f/my.main.http_trigger.yaml")).toEqual(true);
|
||||
expect(isWorkspaceSpecificFile("f/my.develop.kafka_trigger.yaml")).toEqual(true);
|
||||
expect(isWorkspaceSpecificFile("f/my.main.websocket_trigger.yaml")).toEqual(true);
|
||||
});
|
||||
|
||||
test("isBranchSpecificFile: returns false for non-branch-specific files", () => {
|
||||
expect(isBranchSpecificFile("f/test.variable.yaml")).toEqual(false);
|
||||
expect(isBranchSpecificFile("u/admin/db.resource.yaml")).toEqual(false);
|
||||
expect(isBranchSpecificFile("f/my.http_trigger.yaml")).toEqual(false);
|
||||
expect(isBranchSpecificFile("f/script.ts")).toEqual(false);
|
||||
test("isWorkspaceSpecificFile: returns false for non-workspace-specific files", () => {
|
||||
expect(isWorkspaceSpecificFile("f/test.variable.yaml")).toEqual(false);
|
||||
expect(isWorkspaceSpecificFile("u/admin/db.resource.yaml")).toEqual(false);
|
||||
expect(isWorkspaceSpecificFile("f/my.http_trigger.yaml")).toEqual(false);
|
||||
expect(isWorkspaceSpecificFile("f/script.ts")).toEqual(false);
|
||||
});
|
||||
|
||||
test("isBranchSpecificFile: handles resource files with extensions", () => {
|
||||
expect(isBranchSpecificFile("f/config.main.resource.file.json")).toEqual(true);
|
||||
expect(isBranchSpecificFile("f/config.resource.file.json")).toEqual(false);
|
||||
test("isWorkspaceSpecificFile: handles resource files with extensions", () => {
|
||||
expect(isWorkspaceSpecificFile("f/config.main.resource.file.json")).toEqual(true);
|
||||
expect(isWorkspaceSpecificFile("f/config.resource.file.json")).toEqual(false);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
@@ -182,32 +182,32 @@ test("isBranchSpecificFile: handles resource files with extensions", () => {
|
||||
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);
|
||||
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
||||
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
||||
expect(restored).toEqual(original);
|
||||
});
|
||||
|
||||
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);
|
||||
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
||||
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
||||
expect(restored).toEqual(original);
|
||||
});
|
||||
|
||||
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);
|
||||
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
||||
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
||||
expect(restored).toEqual(original);
|
||||
});
|
||||
|
||||
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);
|
||||
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
||||
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
||||
expect(restored).toEqual(original);
|
||||
});
|
||||
|
||||
@@ -216,58 +216,58 @@ test("round-trip: resource file with extension", () => {
|
||||
// These tests validate that functions work correctly with explicit branch override
|
||||
// =============================================================================
|
||||
|
||||
test("branchOverride: getBranchSpecificPath with override returns branch-specific path", () => {
|
||||
test("branchOverride: getWorkspaceSpecificPath with override returns workspace-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");
|
||||
// When override is provided, it should return the workspace-specific path even outside git repo
|
||||
const result = getWorkspaceSpecificPath("f/test.variable.yaml", config, "staging");
|
||||
expect(result).toEqual("f/test.staging.variable.yaml");
|
||||
});
|
||||
|
||||
test("branchOverride: getBranchSpecificPath without override and not in git repo returns undefined", () => {
|
||||
test("branchOverride: getWorkspaceSpecificPath 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
|
||||
const result = getWorkspaceSpecificPath("f/test.variable.yaml", config);
|
||||
// In a git repo, this would return a workspace-specific path; outside, it would be undefined
|
||||
// We test the override case above which is deterministic
|
||||
});
|
||||
|
||||
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");
|
||||
test("branchOverride: isCurrentWorkspaceFile with override uses provided branch", () => {
|
||||
// Test that isCurrentWorkspaceFile uses the override branch instead of git detection
|
||||
const result = isCurrentWorkspaceFile("f/test.staging.variable.yaml", "staging");
|
||||
expect(result).toEqual(true);
|
||||
|
||||
// Should return false for different branch
|
||||
const resultOther = isCurrentBranchFile("f/test.staging.variable.yaml", "production");
|
||||
const resultOther = isCurrentWorkspaceFile("f/test.staging.variable.yaml", "production");
|
||||
expect(resultOther).toEqual(false);
|
||||
|
||||
// Should return false for non-branch-specific file
|
||||
const resultNonSpecific = isCurrentBranchFile("f/test.variable.yaml", "staging");
|
||||
// Should return false for non-workspace-specific file
|
||||
const resultNonSpecific = isCurrentWorkspaceFile("f/test.variable.yaml", "staging");
|
||||
expect(resultNonSpecific).toEqual(false);
|
||||
});
|
||||
|
||||
test("branchOverride: isCurrentBranchFile with override handles sanitized branch names", () => {
|
||||
test("branchOverride: isCurrentWorkspaceFile 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");
|
||||
const result = isCurrentWorkspaceFile("f/test.feature_my-branch.variable.yaml", "feature/my-branch");
|
||||
expect(result).toEqual(true);
|
||||
|
||||
// Different sanitized branch should return false
|
||||
const resultOther = isCurrentBranchFile("f/test.feature_my-branch.variable.yaml", "feature/other-branch");
|
||||
const resultOther = isCurrentWorkspaceFile("f/test.feature_my-branch.variable.yaml", "feature/other-branch");
|
||||
expect(resultOther).toEqual(false);
|
||||
});
|
||||
|
||||
test("branchOverride: getSpecificItemsForCurrentBranch with override returns correct config", () => {
|
||||
// Test that getSpecificItemsForCurrentBranch uses the override branch
|
||||
const config = {
|
||||
gitBranches: {
|
||||
workspaces: {
|
||||
staging: {
|
||||
specificItems: {
|
||||
variables: ["f/**"],
|
||||
@@ -298,7 +298,7 @@ test("branchOverride: getSpecificItemsForCurrentBranch with override returns cor
|
||||
|
||||
test("branchOverride: getSpecificItemsForCurrentBranch with non-existent branch returns undefined", () => {
|
||||
const config = {
|
||||
gitBranches: {
|
||||
workspaces: {
|
||||
staging: {
|
||||
specificItems: {
|
||||
variables: ["f/**"],
|
||||
@@ -314,7 +314,7 @@ test("branchOverride: getSpecificItemsForCurrentBranch with non-existent branch
|
||||
|
||||
test("branchOverride: getSpecificItemsForCurrentBranch merges common and branch items", () => {
|
||||
const config = {
|
||||
gitBranches: {
|
||||
workspaces: {
|
||||
commonSpecificItems: {
|
||||
variables: ["common/**"],
|
||||
resources: ["shared/**"],
|
||||
@@ -329,7 +329,7 @@ test("branchOverride: getSpecificItemsForCurrentBranch merges common and branch
|
||||
};
|
||||
|
||||
const result = getSpecificItemsForCurrentBranch(config as any, "develop");
|
||||
// Should merge common and branch-specific
|
||||
// Should merge common and workspace-specific
|
||||
expect(result?.variables).toEqual(["common/**", "dev/**"]);
|
||||
expect(result?.resources).toEqual(["shared/**"]);
|
||||
expect(result?.triggers).toEqual(["dev/triggers/**"]);
|
||||
@@ -340,34 +340,34 @@ test("branchOverride: getSpecificItemsForCurrentBranch merges common and branch
|
||||
// Format: f/folder/folder.branchName.meta.yaml
|
||||
// =============================================================================
|
||||
|
||||
test("toBranchSpecificPath: converts folder meta path to branch-specific", () => {
|
||||
test("toWorkspaceSpecificPath: converts folder meta path to workspace-specific", () => {
|
||||
// f/my_folder/folder.meta.yaml -> f/my_folder/folder.main.meta.yaml
|
||||
const result = toBranchSpecificPath("f/my_folder/folder.meta.yaml", "main");
|
||||
const result = toWorkspaceSpecificPath("f/my_folder/folder.meta.yaml", "main");
|
||||
expect(result).toEqual("f/my_folder/folder.main.meta.yaml");
|
||||
});
|
||||
|
||||
test("toBranchSpecificPath: converts nested folder meta path to branch-specific", () => {
|
||||
const result = toBranchSpecificPath("f/parent/child/folder.meta.yaml", "develop");
|
||||
test("toWorkspaceSpecificPath: converts nested folder meta path to workspace-specific", () => {
|
||||
const result = toWorkspaceSpecificPath("f/parent/child/folder.meta.yaml", "develop");
|
||||
expect(result).toEqual("f/parent/child/folder.develop.meta.yaml");
|
||||
});
|
||||
|
||||
test("toBranchSpecificPath: sanitizes branch name in folder path", () => {
|
||||
const result = toBranchSpecificPath("f/env/folder.meta.yaml", "feature/test");
|
||||
test("toWorkspaceSpecificPath: sanitizes branch name in folder path", () => {
|
||||
const result = toWorkspaceSpecificPath("f/env/folder.meta.yaml", "feature/test");
|
||||
expect(result).toEqual("f/env/folder.feature_test.meta.yaml");
|
||||
});
|
||||
|
||||
test("fromBranchSpecificPath: converts branch-specific folder back to base", () => {
|
||||
const result = fromBranchSpecificPath("f/my_folder/folder.main.meta.yaml", "main");
|
||||
test("fromWorkspaceSpecificPath: converts workspace-specific folder back to base", () => {
|
||||
const result = fromWorkspaceSpecificPath("f/my_folder/folder.main.meta.yaml", "main");
|
||||
expect(result).toEqual("f/my_folder/folder.meta.yaml");
|
||||
});
|
||||
|
||||
test("fromBranchSpecificPath: handles nested branch-specific folder", () => {
|
||||
const result = fromBranchSpecificPath("f/parent/child/folder.develop.meta.yaml", "develop");
|
||||
test("fromWorkspaceSpecificPath: handles nested workspace-specific folder", () => {
|
||||
const result = fromWorkspaceSpecificPath("f/parent/child/folder.develop.meta.yaml", "develop");
|
||||
expect(result).toEqual("f/parent/child/folder.meta.yaml");
|
||||
});
|
||||
|
||||
test("fromBranchSpecificPath: handles sanitized branch names for folders", () => {
|
||||
const result = fromBranchSpecificPath("f/env/folder.feature_test.meta.yaml", "feature/test");
|
||||
test("fromWorkspaceSpecificPath: handles sanitized branch names for folders", () => {
|
||||
const result = fromWorkspaceSpecificPath("f/env/folder.feature_test.meta.yaml", "feature/test");
|
||||
expect(result).toEqual("f/env/folder.meta.yaml");
|
||||
});
|
||||
|
||||
@@ -388,43 +388,43 @@ test("isSpecificItem: matches folder paths with exact pattern", () => {
|
||||
expect(isSpecificItem("f/other/folder.meta.yaml", config)).toEqual(false);
|
||||
});
|
||||
|
||||
test("isBranchSpecificFile: detects branch-specific folder files", () => {
|
||||
expect(isBranchSpecificFile("f/my_folder/folder.main.meta.yaml")).toEqual(true);
|
||||
expect(isBranchSpecificFile("f/my_folder/folder.develop.meta.yaml")).toEqual(true);
|
||||
expect(isBranchSpecificFile("f/nested/path/folder.staging.meta.yaml")).toEqual(true);
|
||||
test("isWorkspaceSpecificFile: detects workspace-specific folder files", () => {
|
||||
expect(isWorkspaceSpecificFile("f/my_folder/folder.main.meta.yaml")).toEqual(true);
|
||||
expect(isWorkspaceSpecificFile("f/my_folder/folder.develop.meta.yaml")).toEqual(true);
|
||||
expect(isWorkspaceSpecificFile("f/nested/path/folder.staging.meta.yaml")).toEqual(true);
|
||||
});
|
||||
|
||||
test("isBranchSpecificFile: returns false for non-branch-specific folder files", () => {
|
||||
expect(isBranchSpecificFile("f/my_folder/folder.meta.yaml")).toEqual(false);
|
||||
expect(isBranchSpecificFile("f/nested/path/folder.meta.yaml")).toEqual(false);
|
||||
test("isWorkspaceSpecificFile: returns false for non-workspace-specific folder files", () => {
|
||||
expect(isWorkspaceSpecificFile("f/my_folder/folder.meta.yaml")).toEqual(false);
|
||||
expect(isWorkspaceSpecificFile("f/nested/path/folder.meta.yaml")).toEqual(false);
|
||||
});
|
||||
|
||||
test("isCurrentBranchFile: detects branch-specific folder for current branch", () => {
|
||||
expect(isCurrentBranchFile("f/my_folder/folder.staging.meta.yaml", "staging")).toEqual(true);
|
||||
expect(isCurrentBranchFile("f/my_folder/folder.staging.meta.yaml", "production")).toEqual(false);
|
||||
expect(isCurrentBranchFile("f/my_folder/folder.meta.yaml", "staging")).toEqual(false);
|
||||
test("isCurrentWorkspaceFile: detects workspace-specific folder for current branch", () => {
|
||||
expect(isCurrentWorkspaceFile("f/my_folder/folder.staging.meta.yaml", "staging")).toEqual(true);
|
||||
expect(isCurrentWorkspaceFile("f/my_folder/folder.staging.meta.yaml", "production")).toEqual(false);
|
||||
expect(isCurrentWorkspaceFile("f/my_folder/folder.meta.yaml", "staging")).toEqual(false);
|
||||
});
|
||||
|
||||
test("isCurrentBranchFile: handles sanitized branch for folders", () => {
|
||||
expect(isCurrentBranchFile("f/env/folder.feature_test.meta.yaml", "feature/test")).toEqual(true);
|
||||
expect(isCurrentBranchFile("f/env/folder.feature_test.meta.yaml", "feature/other")).toEqual(false);
|
||||
test("isCurrentWorkspaceFile: handles sanitized branch for folders", () => {
|
||||
expect(isCurrentWorkspaceFile("f/env/folder.feature_test.meta.yaml", "feature/test")).toEqual(true);
|
||||
expect(isCurrentWorkspaceFile("f/env/folder.feature_test.meta.yaml", "feature/other")).toEqual(false);
|
||||
});
|
||||
|
||||
test("round-trip: folder meta path conversion", () => {
|
||||
const original = "f/configs/env_folder/folder.meta.yaml";
|
||||
const branch = "main";
|
||||
const branchSpecific = toBranchSpecificPath(original, branch);
|
||||
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
||||
expect(branchSpecific).toEqual("f/configs/env_folder/folder.main.meta.yaml");
|
||||
const restored = fromBranchSpecificPath(branchSpecific, branch);
|
||||
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
||||
expect(restored).toEqual(original);
|
||||
});
|
||||
|
||||
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);
|
||||
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
||||
expect(branchSpecific).toEqual("f/env/folder.feature_new-env.meta.yaml");
|
||||
const restored = fromBranchSpecificPath(branchSpecific, branch);
|
||||
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
||||
expect(restored).toEqual(original);
|
||||
});
|
||||
|
||||
@@ -432,23 +432,23 @@ test("round-trip: folder meta with sanitized branch", () => {
|
||||
// SETTINGS BRANCH-SPECIFIC TESTS
|
||||
// =============================================================================
|
||||
|
||||
test("toBranchSpecificPath: converts settings.yaml to branch-specific", () => {
|
||||
const result = toBranchSpecificPath("settings.yaml", "main");
|
||||
test("toWorkspaceSpecificPath: converts settings.yaml to workspace-specific", () => {
|
||||
const result = toWorkspaceSpecificPath("settings.yaml", "main");
|
||||
expect(result).toEqual("settings.main.yaml");
|
||||
});
|
||||
|
||||
test("toBranchSpecificPath: sanitizes branch name in settings path", () => {
|
||||
const result = toBranchSpecificPath("settings.yaml", "feature/test");
|
||||
test("toWorkspaceSpecificPath: sanitizes branch name in settings path", () => {
|
||||
const result = toWorkspaceSpecificPath("settings.yaml", "feature/test");
|
||||
expect(result).toEqual("settings.feature_test.yaml");
|
||||
});
|
||||
|
||||
test("fromBranchSpecificPath: converts branch-specific settings back to base", () => {
|
||||
const result = fromBranchSpecificPath("settings.main.yaml", "main");
|
||||
test("fromWorkspaceSpecificPath: converts workspace-specific settings back to base", () => {
|
||||
const result = fromWorkspaceSpecificPath("settings.main.yaml", "main");
|
||||
expect(result).toEqual("settings.yaml");
|
||||
});
|
||||
|
||||
test("fromBranchSpecificPath: handles sanitized branch names for settings", () => {
|
||||
const result = fromBranchSpecificPath("settings.feature_test.yaml", "feature/test");
|
||||
test("fromWorkspaceSpecificPath: handles sanitized branch names for settings", () => {
|
||||
const result = fromWorkspaceSpecificPath("settings.feature_test.yaml", "feature/test");
|
||||
expect(result).toEqual("settings.yaml");
|
||||
});
|
||||
|
||||
@@ -473,49 +473,49 @@ test("isSpecificItem: does not match settings.yaml when settings is undefined",
|
||||
expect(isSpecificItem("settings.yaml", config)).toEqual(false);
|
||||
});
|
||||
|
||||
test("isBranchSpecificFile: detects branch-specific settings files", () => {
|
||||
expect(isBranchSpecificFile("settings.main.yaml")).toEqual(true);
|
||||
expect(isBranchSpecificFile("settings.develop.yaml")).toEqual(true);
|
||||
expect(isBranchSpecificFile("settings.feature_test.yaml")).toEqual(true);
|
||||
test("isWorkspaceSpecificFile: detects workspace-specific settings files", () => {
|
||||
expect(isWorkspaceSpecificFile("settings.main.yaml")).toEqual(true);
|
||||
expect(isWorkspaceSpecificFile("settings.develop.yaml")).toEqual(true);
|
||||
expect(isWorkspaceSpecificFile("settings.feature_test.yaml")).toEqual(true);
|
||||
});
|
||||
|
||||
test("isBranchSpecificFile: returns false for non-branch-specific settings", () => {
|
||||
expect(isBranchSpecificFile("settings.yaml")).toEqual(false);
|
||||
test("isWorkspaceSpecificFile: returns false for non-workspace-specific settings", () => {
|
||||
expect(isWorkspaceSpecificFile("settings.yaml")).toEqual(false);
|
||||
});
|
||||
|
||||
test("isCurrentBranchFile: detects branch-specific settings for current branch", () => {
|
||||
expect(isCurrentBranchFile("settings.staging.yaml", "staging")).toEqual(true);
|
||||
expect(isCurrentBranchFile("settings.staging.yaml", "production")).toEqual(false);
|
||||
expect(isCurrentBranchFile("settings.yaml", "staging")).toEqual(false);
|
||||
test("isCurrentWorkspaceFile: detects workspace-specific settings for current branch", () => {
|
||||
expect(isCurrentWorkspaceFile("settings.staging.yaml", "staging")).toEqual(true);
|
||||
expect(isCurrentWorkspaceFile("settings.staging.yaml", "production")).toEqual(false);
|
||||
expect(isCurrentWorkspaceFile("settings.yaml", "staging")).toEqual(false);
|
||||
});
|
||||
|
||||
test("isCurrentBranchFile: handles sanitized branch for settings", () => {
|
||||
expect(isCurrentBranchFile("settings.feature_test.yaml", "feature/test")).toEqual(true);
|
||||
expect(isCurrentBranchFile("settings.feature_test.yaml", "feature/other")).toEqual(false);
|
||||
test("isCurrentWorkspaceFile: handles sanitized branch for settings", () => {
|
||||
expect(isCurrentWorkspaceFile("settings.feature_test.yaml", "feature/test")).toEqual(true);
|
||||
expect(isCurrentWorkspaceFile("settings.feature_test.yaml", "feature/other")).toEqual(false);
|
||||
});
|
||||
|
||||
test("round-trip: settings path conversion", () => {
|
||||
const original = "settings.yaml";
|
||||
const branch = "main";
|
||||
const branchSpecific = toBranchSpecificPath(original, branch);
|
||||
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
||||
expect(branchSpecific).toEqual("settings.main.yaml");
|
||||
const restored = fromBranchSpecificPath(branchSpecific, branch);
|
||||
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
||||
expect(restored).toEqual(original);
|
||||
});
|
||||
|
||||
test("round-trip: settings with sanitized branch", () => {
|
||||
const original = "settings.yaml";
|
||||
const branch = "release/v1.0";
|
||||
const branchSpecific = toBranchSpecificPath(original, branch);
|
||||
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
||||
expect(branchSpecific).toEqual("settings.release_v1_0.yaml");
|
||||
const restored = fromBranchSpecificPath(branchSpecific, branch);
|
||||
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
||||
expect(restored).toEqual(original);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// isItemTypeConfigured TESTS
|
||||
// This function checks if the TYPE is configured, not whether it matches pattern.
|
||||
// Used to determine if branch-specific files should be used for this type.
|
||||
// Used to determine if workspace-specific files should be used for this type.
|
||||
// =============================================================================
|
||||
|
||||
test("isItemTypeConfigured: returns false when specificItems is undefined", () => {
|
||||
@@ -628,11 +628,11 @@ test("isItemTypeConfigured: returns false for resource files when resources is N
|
||||
// =============================================================================
|
||||
// BRANCH-SPECIFIC FILE FILTERING TESTS
|
||||
// These tests verify the expected filtering behavior:
|
||||
// - When type IS configured: use branch-specific files, skip base files
|
||||
// - When type is NOT configured: skip branch-specific files, use base files
|
||||
// - When type IS configured: use workspace-specific files, skip base files
|
||||
// - When type is NOT configured: skip workspace-specific files, use base files
|
||||
// =============================================================================
|
||||
|
||||
test("filtering logic: folders - when NOT configured, branch-specific should be ignored", () => {
|
||||
test("filtering logic: folders - when NOT configured, workspace-specific should be ignored", () => {
|
||||
// Config has variables but NOT folders
|
||||
const config: SpecificItemsConfig = {
|
||||
variables: ["f/**"],
|
||||
@@ -644,15 +644,15 @@ test("filtering logic: folders - when NOT configured, branch-specific should be
|
||||
// Folder type is NOT configured
|
||||
expect(isItemTypeConfigured(basePath, config)).toEqual(false);
|
||||
|
||||
// Therefore, branch-specific file detection should not apply to this type
|
||||
// Therefore, workspace-specific file detection should not apply to this type
|
||||
// The sync logic should:
|
||||
// 1. Skip branch-specific folder files (isBranchSpecificFile returns true)
|
||||
// 1. Skip workspace-specific folder files (isWorkspaceSpecificFile returns true)
|
||||
// 2. Use the base file
|
||||
expect(isBranchSpecificFile(branchSpecificPath)).toEqual(true);
|
||||
expect(isBranchSpecificFile(basePath)).toEqual(false);
|
||||
expect(isWorkspaceSpecificFile(branchSpecificPath)).toEqual(true);
|
||||
expect(isWorkspaceSpecificFile(basePath)).toEqual(false);
|
||||
});
|
||||
|
||||
test("filtering logic: folders - when IS configured and matches, use branch-specific", () => {
|
||||
test("filtering logic: folders - when IS configured and matches, use workspace-specific", () => {
|
||||
const config: SpecificItemsConfig = {
|
||||
folders: ["f/my_folder"],
|
||||
};
|
||||
@@ -667,15 +667,15 @@ test("filtering logic: folders - when IS configured and matches, use branch-spec
|
||||
expect(isSpecificItem(basePath, config)).toEqual(true);
|
||||
|
||||
// The sync logic should:
|
||||
// 1. Use branch-specific folder file (map to base path)
|
||||
// 1. Use workspace-specific folder file (map to base path)
|
||||
// 2. Skip the base file
|
||||
expect(isBranchSpecificFile(branchSpecificPath)).toEqual(true);
|
||||
expect(fromBranchSpecificPath(branchSpecificPath, "main")).toEqual(basePath);
|
||||
expect(isWorkspaceSpecificFile(branchSpecificPath)).toEqual(true);
|
||||
expect(fromWorkspaceSpecificPath(branchSpecificPath, "main")).toEqual(basePath);
|
||||
});
|
||||
|
||||
test("filtering logic: folders - when IS configured but doesn't match, skip branch-specific", () => {
|
||||
test("filtering logic: folders - when IS configured but doesn't match, skip workspace-specific", () => {
|
||||
const config: SpecificItemsConfig = {
|
||||
folders: ["f/env_*"], // Only env_ folders are branch-specific
|
||||
folders: ["f/env_*"], // Only env_ folders are workspace-specific
|
||||
};
|
||||
|
||||
const basePath = "f/other_folder/folder.meta.yaml";
|
||||
@@ -688,11 +688,11 @@ test("filtering logic: folders - when IS configured but doesn't match, skip bran
|
||||
expect(isSpecificItem(basePath, config)).toEqual(false);
|
||||
|
||||
// The sync logic should:
|
||||
// 1. Skip the branch-specific file (type configured but doesn't match)
|
||||
// 1. Skip the workspace-specific file (type configured but doesn't match)
|
||||
// 2. Use the base file
|
||||
});
|
||||
|
||||
test("filtering logic: settings - when NOT configured, branch-specific should be ignored", () => {
|
||||
test("filtering logic: settings - when NOT configured, workspace-specific should be ignored", () => {
|
||||
// Config has variables but NOT settings
|
||||
const config: SpecificItemsConfig = {
|
||||
variables: ["f/**"],
|
||||
@@ -704,12 +704,12 @@ test("filtering logic: settings - when NOT configured, branch-specific should be
|
||||
// Settings type is NOT configured
|
||||
expect(isItemTypeConfigured(basePath, config)).toEqual(false);
|
||||
|
||||
// Therefore, branch-specific file detection should not apply to this type
|
||||
expect(isBranchSpecificFile(branchSpecificPath)).toEqual(true);
|
||||
expect(isBranchSpecificFile(basePath)).toEqual(false);
|
||||
// Therefore, workspace-specific file detection should not apply to this type
|
||||
expect(isWorkspaceSpecificFile(branchSpecificPath)).toEqual(true);
|
||||
expect(isWorkspaceSpecificFile(basePath)).toEqual(false);
|
||||
});
|
||||
|
||||
test("filtering logic: settings - when IS configured (true), use branch-specific", () => {
|
||||
test("filtering logic: settings - when IS configured (true), use workspace-specific", () => {
|
||||
const config: SpecificItemsConfig = {
|
||||
settings: true,
|
||||
};
|
||||
@@ -723,12 +723,12 @@ test("filtering logic: settings - when IS configured (true), use branch-specific
|
||||
// And settings: true means it matches
|
||||
expect(isSpecificItem(basePath, config)).toEqual(true);
|
||||
|
||||
// The sync logic should use branch-specific file
|
||||
expect(isBranchSpecificFile(branchSpecificPath)).toEqual(true);
|
||||
expect(fromBranchSpecificPath(branchSpecificPath, "main")).toEqual(basePath);
|
||||
// The sync logic should use workspace-specific file
|
||||
expect(isWorkspaceSpecificFile(branchSpecificPath)).toEqual(true);
|
||||
expect(fromWorkspaceSpecificPath(branchSpecificPath, "main")).toEqual(basePath);
|
||||
});
|
||||
|
||||
test("filtering logic: settings - when IS configured (false), skip branch-specific", () => {
|
||||
test("filtering logic: settings - when IS configured (false), skip workspace-specific", () => {
|
||||
// settings: false means type is configured but explicitly disabled
|
||||
const config: SpecificItemsConfig = {
|
||||
settings: false,
|
||||
@@ -743,10 +743,10 @@ test("filtering logic: settings - when IS configured (false), skip branch-specif
|
||||
// But settings: false means it doesn't match (not a specific item)
|
||||
expect(isSpecificItem(basePath, config)).toEqual(false);
|
||||
|
||||
// The sync logic should skip branch-specific file and use base
|
||||
// The sync logic should skip workspace-specific file and use base
|
||||
});
|
||||
|
||||
test("filtering logic: variables - when NOT configured, branch-specific should be ignored", () => {
|
||||
test("filtering logic: variables - when NOT configured, workspace-specific should be ignored", () => {
|
||||
// Config has folders but NOT variables
|
||||
const config: SpecificItemsConfig = {
|
||||
folders: ["f/env_*"],
|
||||
@@ -759,11 +759,11 @@ test("filtering logic: variables - when NOT configured, branch-specific should b
|
||||
expect(isItemTypeConfigured(basePath, config)).toEqual(false);
|
||||
|
||||
// Branch-specific variable files should be ignored
|
||||
expect(isBranchSpecificFile(branchSpecificPath)).toEqual(true);
|
||||
expect(isBranchSpecificFile(basePath)).toEqual(false);
|
||||
expect(isWorkspaceSpecificFile(branchSpecificPath)).toEqual(true);
|
||||
expect(isWorkspaceSpecificFile(basePath)).toEqual(false);
|
||||
});
|
||||
|
||||
test("filtering logic: resources - when NOT configured, branch-specific should be ignored", () => {
|
||||
test("filtering logic: resources - when NOT configured, workspace-specific should be ignored", () => {
|
||||
// Config has folders but NOT resources
|
||||
const config: SpecificItemsConfig = {
|
||||
folders: ["f/env_*"],
|
||||
@@ -775,11 +775,11 @@ test("filtering logic: resources - when NOT configured, branch-specific should b
|
||||
// Resource type is NOT configured
|
||||
expect(isItemTypeConfigured(basePath, config)).toEqual(false);
|
||||
|
||||
expect(isBranchSpecificFile(branchSpecificPath)).toEqual(true);
|
||||
expect(isBranchSpecificFile(basePath)).toEqual(false);
|
||||
expect(isWorkspaceSpecificFile(branchSpecificPath)).toEqual(true);
|
||||
expect(isWorkspaceSpecificFile(basePath)).toEqual(false);
|
||||
});
|
||||
|
||||
test("filtering logic: triggers - when NOT configured, branch-specific should be ignored", () => {
|
||||
test("filtering logic: triggers - when NOT configured, workspace-specific should be ignored", () => {
|
||||
// Config has folders but NOT triggers
|
||||
const config: SpecificItemsConfig = {
|
||||
folders: ["f/env_*"],
|
||||
@@ -791,8 +791,8 @@ test("filtering logic: triggers - when NOT configured, branch-specific should be
|
||||
// Trigger type is NOT configured
|
||||
expect(isItemTypeConfigured(basePath, config)).toEqual(false);
|
||||
|
||||
expect(isBranchSpecificFile(branchSpecificPath)).toEqual(true);
|
||||
expect(isBranchSpecificFile(basePath)).toEqual(false);
|
||||
expect(isWorkspaceSpecificFile(branchSpecificPath)).toEqual(true);
|
||||
expect(isWorkspaceSpecificFile(basePath)).toEqual(false);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
|
||||
520
cli/test/workspaces_config.test.ts
Normal file
520
cli/test/workspaces_config.test.ts
Normal file
@@ -0,0 +1,520 @@
|
||||
import { expect, test, describe } from "bun:test";
|
||||
import { writeFile, mkdir, readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { withTestBackend } from "./test_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
|
||||
import { getEffectiveSettings, readConfigFile, convertGitBranchesToWorkspaces, findWorkspaceByGitBranch, getEffectiveWorkspaceId, getEffectiveGitBranch, getWorkspaceNames, type SyncOptions } from "../src/core/conf.ts";
|
||||
import { getSpecificItemsForCurrentBranch } from "../src/core/specific_items.ts";
|
||||
|
||||
// =============================================================================
|
||||
// Helper
|
||||
// =============================================================================
|
||||
|
||||
async function setupWorkspaceProfile(backend: any, workspaceName: string): Promise<void> {
|
||||
await addWorkspace({
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: workspaceName,
|
||||
token: backend.token,
|
||||
}, { force: true, configDir: backend.testConfigDir });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UNIT TESTS: workspaces config helpers
|
||||
// =============================================================================
|
||||
|
||||
describe("findWorkspaceByGitBranch", () => {
|
||||
test("finds workspace by key when gitBranch not set (default)", () => {
|
||||
const ws = { staging: { baseUrl: "https://staging.wm.dev" } };
|
||||
const match = findWorkspaceByGitBranch(ws as any, "staging");
|
||||
expect(match).toBeDefined();
|
||||
expect(match![0]).toEqual("staging");
|
||||
});
|
||||
|
||||
test("finds workspace by explicit gitBranch", () => {
|
||||
const ws = { production: { gitBranch: "main", baseUrl: "https://app.wm.dev" } };
|
||||
const match = findWorkspaceByGitBranch(ws as any, "main");
|
||||
expect(match).toBeDefined();
|
||||
expect(match![0]).toEqual("production");
|
||||
});
|
||||
|
||||
test("does not find workspace when gitBranch differs", () => {
|
||||
const ws = { production: { gitBranch: "main", baseUrl: "https://app.wm.dev" } };
|
||||
const match = findWorkspaceByGitBranch(ws as any, "production");
|
||||
expect(match).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returns undefined for undefined workspaces", () => {
|
||||
expect(findWorkspaceByGitBranch(undefined, "main")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("skips commonSpecificItems", () => {
|
||||
const ws = { commonSpecificItems: { variables: ["f/**"] }, staging: { baseUrl: "x" } };
|
||||
const match = findWorkspaceByGitBranch(ws as any, "commonSpecificItems");
|
||||
expect(match).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returns first match when multiple map to same gitBranch", () => {
|
||||
const ws = {
|
||||
alpha: { gitBranch: "develop", baseUrl: "a" },
|
||||
beta: { gitBranch: "develop", baseUrl: "b" },
|
||||
};
|
||||
const match = findWorkspaceByGitBranch(ws as any, "develop");
|
||||
expect(match![0]).toEqual("alpha");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEffectiveWorkspaceId", () => {
|
||||
test("returns workspaceId when set", () => {
|
||||
expect(getEffectiveWorkspaceId("prod", { workspaceId: "production" })).toEqual("production");
|
||||
});
|
||||
|
||||
test("defaults to workspace name when workspaceId not set", () => {
|
||||
expect(getEffectiveWorkspaceId("staging", {})).toEqual("staging");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEffectiveGitBranch", () => {
|
||||
test("returns gitBranch when set", () => {
|
||||
expect(getEffectiveGitBranch("production", { gitBranch: "main" })).toEqual("main");
|
||||
});
|
||||
|
||||
test("defaults to workspace name when gitBranch not set", () => {
|
||||
expect(getEffectiveGitBranch("staging", {})).toEqual("staging");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWorkspaceNames", () => {
|
||||
test("excludes commonSpecificItems", () => {
|
||||
const ws = { staging: {}, production: {}, commonSpecificItems: { variables: [] } };
|
||||
expect(getWorkspaceNames(ws as any)).toEqual(["staging", "production"]);
|
||||
});
|
||||
|
||||
test("returns empty for undefined", () => {
|
||||
expect(getWorkspaceNames(undefined)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// UNIT TESTS: convertGitBranchesToWorkspaces
|
||||
// =============================================================================
|
||||
|
||||
describe("convertGitBranchesToWorkspaces", () => {
|
||||
test("preserves all fields from old branch entries", () => {
|
||||
const old = {
|
||||
main: {
|
||||
baseUrl: "https://app.wm.dev",
|
||||
workspaceId: "production",
|
||||
overrides: { skipSecrets: false },
|
||||
promotionOverrides: { skipSecrets: true },
|
||||
specificItems: { variables: ["f/**"] },
|
||||
stateful: true,
|
||||
message: "auto",
|
||||
},
|
||||
};
|
||||
const ws = convertGitBranchesToWorkspaces(old as any);
|
||||
const entry = (ws as any).main;
|
||||
expect(entry.baseUrl).toEqual("https://app.wm.dev");
|
||||
expect(entry.workspaceId).toEqual("production");
|
||||
expect(entry.overrides).toEqual({ skipSecrets: false });
|
||||
expect(entry.promotionOverrides).toEqual({ skipSecrets: true });
|
||||
expect(entry.specificItems).toEqual({ variables: ["f/**"] });
|
||||
expect(entry.stateful).toEqual(true);
|
||||
expect(entry.message).toEqual("auto");
|
||||
});
|
||||
|
||||
test("preserves commonSpecificItems", () => {
|
||||
const old = {
|
||||
commonSpecificItems: { variables: ["f/shared/**"], settings: true },
|
||||
main: { overrides: {} },
|
||||
};
|
||||
const ws = convertGitBranchesToWorkspaces(old as any);
|
||||
expect(ws.commonSpecificItems).toEqual({ variables: ["f/shared/**"], settings: true });
|
||||
});
|
||||
|
||||
test("handles empty entries", () => {
|
||||
const ws = convertGitBranchesToWorkspaces({ main: {} } as any);
|
||||
expect((ws as any).main).toEqual({});
|
||||
});
|
||||
|
||||
test("does not set gitBranch (defaults to key name)", () => {
|
||||
const ws = convertGitBranchesToWorkspaces({ main: { baseUrl: "x" } } as any);
|
||||
expect((ws as any).main.gitBranch).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// UNIT TESTS: getEffectiveSettings with workspaces config
|
||||
// =============================================================================
|
||||
|
||||
describe("getEffectiveSettings with workspaces", () => {
|
||||
test("applies overrides by workspace name", async () => {
|
||||
const config: SyncOptions = {
|
||||
includes: ["f/**"],
|
||||
workspaces: {
|
||||
staging: { overrides: { includes: ["staging/**"], skipVariables: true } },
|
||||
production: { overrides: { skipSecrets: false } },
|
||||
},
|
||||
};
|
||||
const s = await getEffectiveSettings(config, undefined, true, true, "staging");
|
||||
expect(s.includes).toEqual(["staging/**"]);
|
||||
expect(s.skipVariables).toEqual(true);
|
||||
});
|
||||
|
||||
test("returns top-level settings for unknown workspace", async () => {
|
||||
const config: SyncOptions = { includes: ["f/**"], workspaces: { staging: { overrides: {} } } };
|
||||
const s = await getEffectiveSettings(config, undefined, true, true, "nonexistent");
|
||||
expect(s.includes).toEqual(["f/**"]);
|
||||
});
|
||||
|
||||
test("promotion resolves by gitBranch", async () => {
|
||||
const config: SyncOptions = {
|
||||
workspaces: {
|
||||
production: {
|
||||
gitBranch: "main",
|
||||
promotionOverrides: { skipSecrets: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
// --promotion main should find workspace "production" via gitBranch match
|
||||
const s = await getEffectiveSettings(config, "main", true, true);
|
||||
expect(s.skipSecrets).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// UNIT TESTS: getSpecificItemsForCurrentBranch with workspaces
|
||||
// =============================================================================
|
||||
|
||||
describe("getSpecificItemsForCurrentBranch with workspaces", () => {
|
||||
test("looks up by workspace name override", () => {
|
||||
const config: SyncOptions = {
|
||||
workspaces: {
|
||||
staging: { specificItems: { variables: ["f/staging/**"] } },
|
||||
production: { specificItems: { variables: ["f/prod/**"] } },
|
||||
commonSpecificItems: { resources: ["f/shared/**"] },
|
||||
},
|
||||
};
|
||||
const items = getSpecificItemsForCurrentBranch(config, "staging");
|
||||
expect(items?.variables).toEqual(["f/staging/**"]);
|
||||
expect(items?.resources).toEqual(["f/shared/**"]);
|
||||
});
|
||||
|
||||
test("returns undefined for unknown workspace", () => {
|
||||
const config: SyncOptions = {
|
||||
workspaces: { staging: { specificItems: { variables: ["f/**"] } } },
|
||||
};
|
||||
const items = getSpecificItemsForCurrentBranch(config, "nonexistent");
|
||||
expect(items).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returns undefined when no workspaces config", () => {
|
||||
expect(getSpecificItemsForCurrentBranch({}, "staging")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("merges common and workspace-specific items", () => {
|
||||
const config: SyncOptions = {
|
||||
workspaces: {
|
||||
commonSpecificItems: { variables: ["common/**"], resources: ["shared/**"] },
|
||||
dev: { specificItems: { variables: ["dev/**"], triggers: ["dev/triggers/**"] } },
|
||||
},
|
||||
};
|
||||
const items = getSpecificItemsForCurrentBranch(config, "dev");
|
||||
expect(items?.variables).toEqual(["common/**", "dev/**"]);
|
||||
expect(items?.resources).toEqual(["shared/**"]);
|
||||
expect(items?.triggers).toEqual(["dev/triggers/**"]);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// INTEGRATION TESTS: new workspaces config format
|
||||
// =============================================================================
|
||||
|
||||
test("Integration: workspaces config with --branch applies correct overrides", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "ws_test");
|
||||
|
||||
// New workspaces config with workspace name ≠ gitBranch
|
||||
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
|
||||
workspaces:
|
||||
production:
|
||||
gitBranch: prod_branch
|
||||
overrides:
|
||||
skipVariables: true
|
||||
staging:
|
||||
overrides:
|
||||
skipVariables: false`, "utf-8");
|
||||
|
||||
// --branch prod_branch should resolve to workspace "production" and skip variables
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--branch', 'prod_branch',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const output = parseJsonFromCLIOutput(result.stdout);
|
||||
const paths = (output.changes || []).map((c: any) => c.path);
|
||||
const hasVariables = paths.some((p: string) => p.includes('.variable.yaml'));
|
||||
expect(hasVariables).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("Integration: workspaces config with --branch resolves default gitBranch", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "ws_test");
|
||||
|
||||
// Workspace name = "staging", gitBranch defaults to "staging"
|
||||
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
|
||||
workspaces:
|
||||
staging:
|
||||
overrides:
|
||||
skipVariables: true`, "utf-8");
|
||||
|
||||
// --branch staging resolves to workspace "staging"
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--branch', 'staging',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const output = parseJsonFromCLIOutput(result.stdout);
|
||||
const paths = (output.changes || []).map((c: any) => c.path);
|
||||
const hasVariables = paths.some((p: string) => p.includes('.variable.yaml'));
|
||||
expect(hasVariables).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("Integration: different workspaces have different overrides", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "ws_test");
|
||||
|
||||
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
skipResources: false
|
||||
|
||||
workspaces:
|
||||
production:
|
||||
gitBranch: prod
|
||||
overrides:
|
||||
skipVariables: true
|
||||
skipResources: true
|
||||
development:
|
||||
gitBranch: dev
|
||||
overrides:
|
||||
skipVariables: false
|
||||
skipResources: false`, "utf-8");
|
||||
|
||||
// prod should skip both
|
||||
const prodResult = await backend.runCLICommand([
|
||||
'sync', 'pull', '--branch', 'prod', '--dry-run', '--json-output'
|
||||
], tempDir);
|
||||
expect(prodResult.code).toEqual(0);
|
||||
const prodPaths = (parseJsonFromCLIOutput(prodResult.stdout).changes || []).map((c: any) => c.path);
|
||||
expect(prodPaths.some((p: string) => p.includes('.variable.yaml'))).toEqual(false);
|
||||
expect(prodPaths.some((p: string) => p.includes('.resource.yaml'))).toEqual(false);
|
||||
|
||||
// dev should include both
|
||||
const devResult = await backend.runCLICommand([
|
||||
'sync', 'pull', '--branch', 'dev', '--dry-run', '--json-output'
|
||||
], tempDir);
|
||||
expect(devResult.code).toEqual(0);
|
||||
const devPaths = (parseJsonFromCLIOutput(devResult.stdout).changes || []).map((c: any) => c.path);
|
||||
expect(devPaths.some((p: string) => p.includes('.variable.yaml'))).toEqual(true);
|
||||
expect(devPaths.some((p: string) => p.includes('.resource.yaml'))).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("Integration: legacy gitBranches config still works via normalization", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "legacy_test");
|
||||
|
||||
// Old gitBranches format on disk
|
||||
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
|
||||
gitBranches:
|
||||
legacy_branch:
|
||||
overrides:
|
||||
skipVariables: true`, "utf-8");
|
||||
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull', '--branch', 'legacy_branch', '--dry-run', '--json-output'
|
||||
], tempDir);
|
||||
expect(result.code).toEqual(0);
|
||||
const paths = (parseJsonFromCLIOutput(result.stdout).changes || []).map((c: any) => c.path);
|
||||
expect(paths.some((p: string) => p.includes('.variable.yaml'))).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("Integration: legacy environments config still works via normalization", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "env_test");
|
||||
|
||||
// Old environments format on disk
|
||||
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
|
||||
environments:
|
||||
env_branch:
|
||||
overrides:
|
||||
skipVariables: true`, "utf-8");
|
||||
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull', '--branch', 'env_branch', '--dry-run', '--json-output'
|
||||
], tempDir);
|
||||
expect(result.code).toEqual(0);
|
||||
const paths = (parseJsonFromCLIOutput(result.stdout).changes || []).map((c: any) => c.path);
|
||||
expect(paths.some((p: string) => p.includes('.variable.yaml'))).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("Integration: workspace-specific files use workspace name as suffix", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "specific_test");
|
||||
|
||||
// Workspace "production" with gitBranch "main" and specificItems
|
||||
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
|
||||
workspaces:
|
||||
production:
|
||||
gitBranch: prod_branch
|
||||
specificItems:
|
||||
variables:
|
||||
- "f/**"`, "utf-8");
|
||||
|
||||
// Pull with --branch prod_branch → resolves to workspace "production"
|
||||
// workspace-specific files should use "production" as suffix (the workspace name)
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull', '--branch', 'prod_branch', '--dry-run', '--json-output'
|
||||
], tempDir);
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const output = parseJsonFromCLIOutput(result.stdout);
|
||||
const changes = output.changes || [];
|
||||
|
||||
// Check that workspace-specific paths use the workspace name "production" as suffix
|
||||
const wsSpecificPaths = changes
|
||||
.filter((c: any) => c.workspace_specific_path)
|
||||
.map((c: any) => c.workspace_specific_path);
|
||||
|
||||
for (const p of wsSpecificPaths) {
|
||||
expect(p).toContain(".production.");
|
||||
expect(p).not.toContain(".prod_branch.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("Integration: promotion with workspaces config", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "promo_test");
|
||||
|
||||
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
|
||||
workspaces:
|
||||
staging:
|
||||
gitBranch: staging_branch
|
||||
overrides:
|
||||
skipVariables: false
|
||||
promotionOverrides:
|
||||
skipVariables: true`, "utf-8");
|
||||
|
||||
// --promotion staging_branch should find workspace "staging" via gitBranch
|
||||
// and apply promotionOverrides (skipVariables: true)
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--branch', 'staging_branch',
|
||||
'--promotion', 'staging_branch',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir);
|
||||
expect(result.code).toEqual(0);
|
||||
const paths = (parseJsonFromCLIOutput(result.stdout).changes || []).map((c: any) => c.path);
|
||||
expect(paths.some((p: string) => p.includes('.variable.yaml'))).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("Integration: config migrate converts gitBranches to workspaces", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "migrate_test");
|
||||
|
||||
// Write old format
|
||||
const yamlPath = `${tempDir}/wmill.yaml`;
|
||||
await writeFile(yamlPath, `defaultTs: bun
|
||||
includes:
|
||||
- "f/**"
|
||||
|
||||
gitBranches:
|
||||
main:
|
||||
baseUrl: https://app.windmill.dev
|
||||
workspaceId: production
|
||||
overrides:
|
||||
skipSecrets: false
|
||||
staging:
|
||||
baseUrl: https://staging.windmill.dev
|
||||
overrides:
|
||||
includeSchedules: true
|
||||
commonSpecificItems:
|
||||
variables:
|
||||
- "f/shared/**"`, "utf-8");
|
||||
|
||||
// Run migrate
|
||||
const result = await backend.runCLICommand([
|
||||
'config', 'migrate',
|
||||
], tempDir);
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
// Read back and verify
|
||||
const migrated = await readFile(yamlPath, "utf-8");
|
||||
expect(migrated).toContain("workspaces:");
|
||||
expect(migrated).not.toContain("gitBranches:");
|
||||
expect(migrated).toContain("production");
|
||||
expect(migrated).toContain("staging");
|
||||
expect(migrated).toContain("commonSpecificItems");
|
||||
expect(migrated).toContain("f/shared/**");
|
||||
});
|
||||
});
|
||||
|
||||
test("Integration: config migrate is idempotent", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "idempotent_test");
|
||||
|
||||
const yamlPath = `${tempDir}/wmill.yaml`;
|
||||
await writeFile(yamlPath, `defaultTs: bun
|
||||
workspaces:
|
||||
staging:
|
||||
baseUrl: https://staging.wm.dev
|
||||
overrides: {}`, "utf-8");
|
||||
|
||||
const result = await backend.runCLICommand(['config', 'migrate'], tempDir);
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
// File should be unchanged
|
||||
const content = await readFile(yamlPath, "utf-8");
|
||||
expect(content).toContain("workspaces:");
|
||||
expect(content).not.toContain("gitBranches:");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user