Files
windmill/cli/test/init_bind_flow.test.ts
Ruben Fiszel 5b97092997 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>
2026-04-09 19:46:34 +00:00

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);
});
});