* 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>
198 lines
7.7 KiB
TypeScript
198 lines
7.7 KiB
TypeScript
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);
|
|
});
|
|
});
|