feat: git sync improvements (#6182)
* init checkpoint * ui second pass... * round 1 backend + saving settings + detecting changes... * checkpoint * fix openapi * saving + correct wmill.yaml diff * cli refactor * cli and tests refactor done * cli multi workspace support * cli support skip core types to align with ui * new test framework * sqlx * openapi spec * frontend * sync + settings changes * some fixes * some fixes * security: Remove hardcoded EE license key, use environment variable only - Remove hardcoded license key from containerized test backend - Environment variable EE_LICENSE_KEY now required for EE features - License key no longer stored in database during tests * sqlx * tests * fixing tests * fix tests * checkpoint * checkpoint * cli build * frontend - cli exchange * settings match * ee repo ref * npm check * openapi * tests * checkpoint * cli + tests * reset to preview on changes * merge issue ee * cleanup * hubscript * simplifications * ee repo ref * cli fixes * fix sync and add tests * extra test * git sync settings / key change aware * ee-repo ref * ee-repo ref * ee repo ref * ee ref * review 1 * ee ref * Update frontend/src/lib/components/PullGitRepoPopover.svelte Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Update frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * ee ref * remove extra includes from ui --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
1363
cli/test/containerized_backend.ts
Normal file
1363
cli/test/containerized_backend.ts
Normal file
File diff suppressed because it is too large
Load Diff
74
cli/test/docker-compose.test.yml
Normal file
74
cli/test/docker-compose.test.yml
Normal file
@@ -0,0 +1,74 @@
|
||||
version: "3.7"
|
||||
|
||||
x-logging: &default-logging
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
compress: "true"
|
||||
|
||||
services:
|
||||
test_db:
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_PASSWORD: testpass123
|
||||
POSTGRES_DB: windmill_test
|
||||
POSTGRES_USER: postgres
|
||||
ports:
|
||||
- "5433:5432" # Use different port to avoid conflicts
|
||||
volumes:
|
||||
- test_db_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d windmill_test"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
logging: *default-logging
|
||||
|
||||
test_windmill_server:
|
||||
image: windmill-test:latest
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:testpass123@test_db/windmill_test?sslmode=disable
|
||||
- MODE=server
|
||||
- LICENSE_KEY=${EE_LICENSE_KEY}
|
||||
- RUST_LOG=info
|
||||
- DISABLE_TELEMETRY=true
|
||||
- METRICS_ENABLED=false
|
||||
ports:
|
||||
- "8001:8000" # Use different port to avoid conflicts
|
||||
depends_on:
|
||||
test_db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- test_worker_logs:/tmp/windmill/logs
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/api/version"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
logging: *default-logging
|
||||
|
||||
test_windmill_worker:
|
||||
image: windmill-test:latest
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:testpass123@test_db/windmill_test?sslmode=disable
|
||||
- MODE=worker
|
||||
- WORKER_GROUP=default
|
||||
- LICENSE_KEY=${EE_LICENSE_KEY}
|
||||
- RUST_LOG=info
|
||||
- DISABLE_TELEMETRY=true
|
||||
- NUM_WORKERS=1
|
||||
- SLEEP_QUEUE=50
|
||||
depends_on:
|
||||
test_db:
|
||||
condition: service_healthy
|
||||
test_windmill_server:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- test_worker_logs:/tmp/windmill/logs
|
||||
logging: *default-logging
|
||||
|
||||
volumes:
|
||||
test_db_data: null
|
||||
test_worker_logs: null
|
||||
177
cli/test/gitsync_settings_features.test.ts
Normal file
177
cli/test/gitsync_settings_features.test.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { assertEquals, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
|
||||
// =============================================================================
|
||||
// GITSYNC-SETTINGS COMMAND FEATURES
|
||||
// Tests for additional gitsync-settings command functionality
|
||||
// =============================================================================
|
||||
|
||||
Deno.test("GitSync Settings: workspace-level wildcard settings", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "workspace_level_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Configure backend with repository
|
||||
await backend.updateGitSyncConfig({
|
||||
git_sync_settings: {
|
||||
repositories: [{
|
||||
git_repo_resource_path: "u/test/workspace_repo",
|
||||
script_path: "f/**",
|
||||
group_by_folder: false,
|
||||
use_individual_branch: false,
|
||||
settings: {
|
||||
include_path: ["f/**"],
|
||||
include_type: ["script", "flow"],
|
||||
exclude_path: [],
|
||||
extra_include_path: []
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Create initial wmill.yaml
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []`);
|
||||
|
||||
// Pull with workspace-level flag
|
||||
const result = await backend.runCLICommand([
|
||||
'gitsync-settings', 'pull',
|
||||
'--repository', 'u/test/workspace_repo',
|
||||
'--workspace-level',
|
||||
'--override'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Workspace-level pull should succeed: ${result.stderr}`);
|
||||
|
||||
// Read updated config
|
||||
const updatedConfig = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
|
||||
// Should create workspace wildcard override
|
||||
assertStringIncludes(updatedConfig, `'${backendUrl}:${backend.workspace}:*':`);
|
||||
assertStringIncludes(updatedConfig, "overrides:");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("GitSync Settings: default mode writes to top-level", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "default_mode_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Configure backend with specific settings
|
||||
await backend.updateGitSyncConfig({
|
||||
git_sync_settings: {
|
||||
repositories: [{
|
||||
git_repo_resource_path: "u/test/default_repo",
|
||||
script_path: "f/**",
|
||||
group_by_folder: false,
|
||||
use_individual_branch: false,
|
||||
settings: {
|
||||
include_path: ["f/special/**"],
|
||||
include_type: ["script"],
|
||||
exclude_path: ["*.test.ts"],
|
||||
extra_include_path: ["g/**"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Create initial wmill.yaml with different settings
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
skipVariables: false`);
|
||||
|
||||
// Pull with default flag
|
||||
const result = await backend.runCLICommand([
|
||||
'gitsync-settings', 'pull',
|
||||
'--repository', 'u/test/default_repo',
|
||||
'--default'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Default mode pull should succeed: ${result.stderr}`);
|
||||
|
||||
// Read updated config
|
||||
const updatedConfig = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
|
||||
|
||||
// Should update top-level settings, not create overrides
|
||||
assertStringIncludes(updatedConfig, "includes:\n - f/special/**");
|
||||
assertStringIncludes(updatedConfig, "excludes:\n - '*.test.ts'");
|
||||
assertStringIncludes(updatedConfig, "extraIncludes:\n - g/**");
|
||||
|
||||
// Should NOT have overrides section
|
||||
assertEquals(updatedConfig.includes("overrides:"), false, "Default mode should not create overrides");
|
||||
});
|
||||
});
|
||||
|
||||
// Removed test for non-existent repository error handling
|
||||
// as it was testing non-deterministic behavior
|
||||
|
||||
Deno.test("GitSync Settings: pull shows correct diff output", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "diff_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Configure backend
|
||||
await backend.updateGitSyncConfig({
|
||||
git_sync_settings: {
|
||||
repositories: [{
|
||||
git_repo_resource_path: "u/test/diff_repo",
|
||||
script_path: "f/**",
|
||||
group_by_folder: false,
|
||||
use_individual_branch: false,
|
||||
settings: {
|
||||
include_path: ["f/**"],
|
||||
include_type: ["script", "flow"],
|
||||
exclude_path: [],
|
||||
extra_include_path: []
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Create wmill.yaml with different settings
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
skipVariables: true
|
||||
skipResources: false`);
|
||||
|
||||
// Pull with diff flag
|
||||
const result = await backend.runCLICommand([
|
||||
'gitsync-settings', 'pull',
|
||||
'--repository', 'u/test/diff_repo',
|
||||
'--diff'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0);
|
||||
|
||||
// Should show differences
|
||||
assertStringIncludes(result.stdout, "Changes that would be made:");
|
||||
// Should show the change for skipResources (ignoring ANSI color codes)
|
||||
assertStringIncludes(result.stdout, "skipResources:");
|
||||
});
|
||||
});
|
||||
193
cli/test/include_flags_bypass_filtering.test.ts
Normal file
193
cli/test/include_flags_bypass_filtering.test.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
|
||||
|
||||
// =============================================================================
|
||||
// INCLUDE FLAGS BYPASS FILTERING TESTS
|
||||
// Tests that CLI include flags properly bypass path-based filtering
|
||||
// =============================================================================
|
||||
|
||||
// Helper function to set up workspace profile
|
||||
async function setupWorkspaceProfile(backend: any): Promise<void> {
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "localhost_test",
|
||||
token: backend.token
|
||||
};
|
||||
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
}
|
||||
|
||||
// The ContainerizedBackend already creates test data we can use:
|
||||
// - admin user (admin@windmill.dev)
|
||||
// - test_group (created by seedTestData())
|
||||
// - workspace encryption key
|
||||
// - test apps, resources, variables via seedTestData()
|
||||
// No additional setup needed!
|
||||
|
||||
Deno.test("CLI include flags bypass restrictive path filtering", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Create wmill.yaml with very restrictive includes that would exclude special files
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "f/**"
|
||||
excludes: []
|
||||
skipVariables: true
|
||||
skipResources: true
|
||||
includeUsers: false
|
||||
includeGroups: false
|
||||
includeSettings: false
|
||||
includeKey: false`);
|
||||
|
||||
// Test: CLI flags should override config and bypass path filtering
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--include-users',
|
||||
'--include-groups',
|
||||
'--include-settings',
|
||||
'--include-key',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
|
||||
|
||||
const output = parseJsonFromCLIOutput(result.stdout);
|
||||
const changePaths = output.changes.map((c: any) => c.path);
|
||||
|
||||
// Assert that special files are included despite restrictive path filtering
|
||||
const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml'));
|
||||
const hasGroup = changePaths.some((path: string) => path.includes('groups/test_group.group.yaml'));
|
||||
const hasSettings = changePaths.some((path: string) => path === 'settings.yaml');
|
||||
const hasEncryptionKey = changePaths.some((path: string) => path === 'encryption_key.yaml');
|
||||
|
||||
assert(hasUser, `Admin user should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasGroup, `'test_group' should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasSettings, `Settings should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasEncryptionKey, `Encryption key should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("CLI flags override wmill.yaml include settings", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Config explicitly disables includes, but CLI should override
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
excludes: []
|
||||
includeUsers: false
|
||||
includeGroups: false`);
|
||||
|
||||
// CLI flags should override config file settings
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--include-users',
|
||||
'--include-groups',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
|
||||
|
||||
const output = parseJsonFromCLIOutput(result.stdout);
|
||||
const changePaths = output.changes.map((c: any) => c.path);
|
||||
|
||||
const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml'));
|
||||
const hasGroup = changePaths.some((path: string) => path.includes('groups/test_group.group.yaml'));
|
||||
|
||||
assert(hasUser, `CLI --include-users should override config includeUsers: false. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasGroup, `CLI --include-groups should override config includeGroups: false. Found paths: ${changePaths.join(', ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Skip flags work correctly with getTypeStrFromPath and lock files", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Create wmill.yaml with skip flags enabled
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
excludes: []
|
||||
skipScripts: true
|
||||
skipFlows: false
|
||||
includeUsers: true`);
|
||||
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
|
||||
|
||||
const output = parseJsonFromCLIOutput(result.stdout);
|
||||
const changePaths = output.changes.map((c: any) => c.path);
|
||||
|
||||
// Scripts should be skipped (including lock files) - the backend doesn't create scripts by default
|
||||
const hasScript = changePaths.some((path: string) =>
|
||||
path.endsWith('.py') || path.endsWith('.ts') || path.endsWith('.go') || path.endsWith('.sh')
|
||||
);
|
||||
const hasScriptLock = changePaths.some((path: string) => path.endsWith('.script.lock'));
|
||||
|
||||
// Apps should be included (the backend creates test apps)
|
||||
const hasApp = changePaths.some((path: string) => path.includes('test_dashboard') || path.endsWith('.app.yaml'));
|
||||
|
||||
// Users should still be included
|
||||
const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml'));
|
||||
|
||||
assert(!hasScript, `Standalone scripts should be skipped when skipScripts: true. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(!hasScriptLock, `Script lock files should be skipped when skipScripts: true. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasApp, `Apps should be included (inline scripts are part of apps). Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasUser, `Users should be included when includeUsers: true. Found paths: ${changePaths.join(', ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Mixed include and skip flags work together", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Create restrictive config with mixed settings
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "f/**"
|
||||
excludes: []
|
||||
skipScripts: true
|
||||
includeUsers: false
|
||||
includeSettings: false`);
|
||||
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--skip-scripts', // Reinforce script skipping
|
||||
'--include-users', // Override config to include users
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
|
||||
|
||||
const output = parseJsonFromCLIOutput(result.stdout);
|
||||
const changePaths = output.changes.map((c: any) => c.path);
|
||||
|
||||
// Scripts should be excluded
|
||||
const hasScript = changePaths.some((path: string) =>
|
||||
path.endsWith('.py') || path.endsWith('.ts') || path.endsWith('.go') || path.endsWith('.sh')
|
||||
);
|
||||
|
||||
// Users should be included (CLI override)
|
||||
const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml'));
|
||||
|
||||
// Settings should be excluded (no CLI override, restrictive path filtering)
|
||||
const hasSettings = changePaths.some((path: string) => path === 'settings.yaml');
|
||||
|
||||
assert(!hasScript, `Scripts should be excluded due to skipScripts. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(hasUser, `Users should be included due to CLI --include-users override. Found paths: ${changePaths.join(', ')}`);
|
||||
assert(!hasSettings, `Settings should be excluded (no CLI override + restrictive paths). Found paths: ${changePaths.join(', ')}`);
|
||||
});
|
||||
});
|
||||
185
cli/test/init_no_git_sync.test.ts
Normal file
185
cli/test/init_no_git_sync.test.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Test to verify that wmill init handles workspaces with no git-sync settings correctly
|
||||
* This creates a unit test that directly tests the logic without needing a backend
|
||||
*/
|
||||
|
||||
import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { DEFAULT_SYNC_OPTIONS } from "../conf.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
|
||||
// Mock the workspace object
|
||||
const mockWorkspace = {
|
||||
remote: 'https://app.windmill.dev/',
|
||||
workspaceId: 'test-workspace',
|
||||
name: 'test-workspace'
|
||||
};
|
||||
|
||||
// Simulate the createWorkspaceProfile function logic for the "no repositories" case
|
||||
function createWorkspaceProfileNoRepos(workspace: any): any {
|
||||
const workspaceProfile: any = {
|
||||
baseUrl: workspace.remote,
|
||||
workspaceId: workspace.workspaceId,
|
||||
};
|
||||
|
||||
// Simulate the case where listRepositories returns empty array
|
||||
const repositories: any[] = [];
|
||||
|
||||
if (repositories.length === 0) {
|
||||
console.log(`No git repositories found in workspace '${workspace.workspaceId}'`);
|
||||
// This is the fix: include default sync settings when no repositories exist
|
||||
Object.assign(workspaceProfile, DEFAULT_SYNC_OPTIONS);
|
||||
return workspaceProfile;
|
||||
}
|
||||
|
||||
return workspaceProfile;
|
||||
}
|
||||
|
||||
Deno.test("Init: createWorkspaceProfile includes defaults when no repositories exist", () => {
|
||||
console.log('🧪 Testing init logic for workspace with no git-sync repositories...');
|
||||
|
||||
const workspaceProfile = createWorkspaceProfileNoRepos(mockWorkspace);
|
||||
|
||||
console.log('Generated workspace profile:', JSON.stringify(workspaceProfile, null, 2));
|
||||
|
||||
// Verify basic workspace info
|
||||
assertEquals(workspaceProfile.baseUrl, 'https://app.windmill.dev/');
|
||||
assertEquals(workspaceProfile.workspaceId, 'test-workspace');
|
||||
|
||||
// Verify default sync settings are included
|
||||
assert(Array.isArray(workspaceProfile.includes), 'Should have includes array');
|
||||
assertEquals(workspaceProfile.includes.length, 1, 'Should have one include pattern');
|
||||
assertEquals(workspaceProfile.includes[0], 'f/**', 'Should include f/** pattern');
|
||||
|
||||
assert(Array.isArray(workspaceProfile.excludes), 'Should have excludes array');
|
||||
assertEquals(workspaceProfile.excludes.length, 0, 'Should have empty excludes array');
|
||||
|
||||
assertEquals(workspaceProfile.defaultTs, 'bun', 'Should have bun as default TypeScript runtime');
|
||||
|
||||
console.log('✅ Workspace profile correctly includes default sync settings when no repositories exist');
|
||||
});
|
||||
|
||||
Deno.test("Init: verify DEFAULT_SYNC_OPTIONS has expected values", () => {
|
||||
console.log('🔍 Verifying DEFAULT_SYNC_OPTIONS contains expected values...');
|
||||
|
||||
console.log('DEFAULT_SYNC_OPTIONS:', JSON.stringify(DEFAULT_SYNC_OPTIONS, null, 2));
|
||||
|
||||
// Verify the default options include the expected f/** pattern
|
||||
assert(Array.isArray(DEFAULT_SYNC_OPTIONS.includes), 'DEFAULT_SYNC_OPTIONS should have includes array');
|
||||
assertEquals(DEFAULT_SYNC_OPTIONS.includes.length, 1, 'Should have one include pattern');
|
||||
assertEquals(DEFAULT_SYNC_OPTIONS.includes[0], 'f/**', 'Should default to f/** pattern');
|
||||
|
||||
assert(Array.isArray(DEFAULT_SYNC_OPTIONS.excludes), 'DEFAULT_SYNC_OPTIONS should have excludes array');
|
||||
assertEquals(DEFAULT_SYNC_OPTIONS.excludes.length, 0, 'Should have empty excludes array by default');
|
||||
|
||||
assertEquals(DEFAULT_SYNC_OPTIONS.defaultTs, 'bun', 'Should default to bun runtime');
|
||||
|
||||
console.log('✅ DEFAULT_SYNC_OPTIONS has expected values');
|
||||
});
|
||||
|
||||
Deno.test("Init: --use-backend flag applies git-sync settings", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: backend.workspace,
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// First create the git repository resource that the git-sync will reference
|
||||
await backend.createAdditionalGitRepo("u/test/init_repo", "Test init repository");
|
||||
|
||||
// Configure backend with git-sync settings
|
||||
await backend.updateGitSyncConfig({
|
||||
git_sync_settings: {
|
||||
repositories: [{
|
||||
git_repo_resource_path: "u/test/init_repo",
|
||||
script_path: "f/**",
|
||||
group_by_folder: false,
|
||||
use_individual_branch: false,
|
||||
settings: {
|
||||
include_path: ["f/backend/**"],
|
||||
include_type: ["script", "flow"],
|
||||
exclude_path: ["*.test.ts"],
|
||||
extra_include_path: ["g/**"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Run init with --use-backend flag
|
||||
const result = await backend.runCLICommand([
|
||||
'init',
|
||||
'--use-backend',
|
||||
'--repository', 'u/test/init_repo'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Init with --use-backend should succeed: ${result.stderr}`);
|
||||
|
||||
// Verify wmill.yaml was created with backend settings
|
||||
const wmillYaml = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
|
||||
|
||||
// Should have backend-applied settings written to top-level (not overrides)
|
||||
assertStringIncludes(wmillYaml, "f/backend/**", "Should include backend's include_path");
|
||||
assertStringIncludes(wmillYaml, "*.test.ts", "Should include backend's exclude_path");
|
||||
assertStringIncludes(wmillYaml, "g/**", "Should include backend's extra_include_path");
|
||||
|
||||
// Should NOT have overrides section since we're starting fresh
|
||||
assertEquals(wmillYaml.includes("overrides:"), false, "Init should not create overrides section");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Init: --use-default bypasses backend settings check", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: backend.workspace,
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Create git repository resource
|
||||
await backend.createAdditionalGitRepo("u/test/ignored_repo", "Test ignored repository");
|
||||
|
||||
// Configure backend with git-sync settings
|
||||
await backend.updateGitSyncConfig({
|
||||
git_sync_settings: {
|
||||
repositories: [{
|
||||
git_repo_resource_path: "u/test/ignored_repo",
|
||||
script_path: "f/**",
|
||||
group_by_folder: false,
|
||||
use_individual_branch: false,
|
||||
settings: {
|
||||
include_path: ["f/should-be-ignored/**"],
|
||||
include_type: ["script"],
|
||||
exclude_path: [],
|
||||
extra_include_path: []
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// Run init with --use-default (should ignore backend)
|
||||
const result = await backend.runCLICommand([
|
||||
'init',
|
||||
'--use-default'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Init with --use-default should succeed: ${result.stderr}`);
|
||||
|
||||
// Verify wmill.yaml was created with default settings only
|
||||
const wmillYaml = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
|
||||
|
||||
// Should have default settings, not backend settings
|
||||
assertStringIncludes(wmillYaml, "includes:\n - f/**", "Should use default includes");
|
||||
assertStringIncludes(wmillYaml, "defaultTs: bun", "Should use default TypeScript runtime");
|
||||
|
||||
// Should NOT have backend-specific settings
|
||||
assertEquals(wmillYaml.includes("f/should-be-ignored/**"), false, "Should not include backend settings");
|
||||
assertEquals(wmillYaml.includes("overrides:"), false, "Should not create overrides when using defaults");
|
||||
});
|
||||
});
|
||||
199
cli/test/multi_instance_workspace.test.ts
Normal file
199
cli/test/multi_instance_workspace.test.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { assertEquals, assert, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
|
||||
|
||||
// =============================================================================
|
||||
// MULTI-INSTANCE WORKSPACE TESTS
|
||||
// Tests for handling multiple Windmill instances with same workspace IDs
|
||||
// =============================================================================
|
||||
|
||||
// Helper function to set up workspace profile with specific name
|
||||
async function setupWorkspaceProfile(backend: any, workspaceName: string): Promise<void> {
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: workspaceName,
|
||||
token: backend.token
|
||||
};
|
||||
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
}
|
||||
|
||||
Deno.test("Multi-Instance: gitsync-settings pull with new format", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile
|
||||
await setupWorkspaceProfile(backend, "multi_instance_test");
|
||||
|
||||
// Create wmill.yaml with new format overrides for different instances
|
||||
const backendUrl = new URL(backend.baseUrl).toString(); // Normalize URL
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
|
||||
overrides:
|
||||
# Current backend instance (should match)
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
includeTriggers: true
|
||||
includeSchedules: true
|
||||
skipVariables: true
|
||||
|
||||
# Different instance (won't match)
|
||||
"https://app.windmill.dev/:${backend.workspace}:u/test/test_repo":
|
||||
includeTriggers: false
|
||||
includeSchedules: false
|
||||
skipVariables: false`);
|
||||
|
||||
// Pull settings - should use the matching instance override (skipVariables: true)
|
||||
const pullResult = await backend.runCLICommand([
|
||||
'gitsync-settings', 'pull',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--diff'
|
||||
], tempDir, "multi_instance_test");
|
||||
|
||||
assertEquals(pullResult.code, 0);
|
||||
assertStringIncludes(pullResult.stdout, "Changes that would be made:");
|
||||
// includeSchedules should show as a change since backend default is false
|
||||
assertStringIncludes(pullResult.stdout, "includeSchedules");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Multi-Instance: gitsync-settings push with overrides", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile
|
||||
await setupWorkspaceProfile(backend, "push_override_test");
|
||||
|
||||
// Create wmill.yaml with specific settings that differ from backend defaults
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
skipVariables: false
|
||||
includeSchedules: false
|
||||
|
||||
overrides:
|
||||
# Override for current backend instance - set includeSchedules: true (backend default is false)
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
includeSchedules: true
|
||||
skipVariables: true`);
|
||||
|
||||
// Push settings - should show changes because includeSchedules differs from backend
|
||||
const pushResult = await backend.runCLICommand([
|
||||
'gitsync-settings', 'push',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--diff'
|
||||
], tempDir, "push_override_test");
|
||||
|
||||
assertEquals(pushResult.code, 0);
|
||||
assertStringIncludes(pushResult.stdout, "Changes that would be pushed:");
|
||||
assertStringIncludes(pushResult.stdout, "includeSchedules");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Multi-Instance: sync with repository-specific overrides", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "my-workspace_123");
|
||||
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
|
||||
overrides:
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
skipApps: true`);
|
||||
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir, "my-workspace_123");
|
||||
|
||||
assertEquals(result.code, 0);
|
||||
|
||||
const data = parseJsonFromCLIOutput(result.stdout);
|
||||
|
||||
// Test is designed to verify that the new format works correctly
|
||||
|
||||
// The test app should NOT appear in changes because skipApps: true
|
||||
const hasTestApp = (data.changes || []).some((change: any) =>
|
||||
change.path?.includes('f/test_dashboard')
|
||||
);
|
||||
assertEquals(hasTestApp, false, "Test app should be skipped due to skipApps override");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Multi-Instance: auto-detection of single repository override", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "auto_detect_test");
|
||||
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
|
||||
overrides:
|
||||
# Single repository override - should be auto-detected
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
skipApps: true
|
||||
includeSchedules: true`);
|
||||
|
||||
// Don't specify --repository, it should auto-detect
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir, "auto_detect_test");
|
||||
|
||||
assertEquals(result.code, 0);
|
||||
assertStringIncludes(result.stdout, "Auto-selected repository: u/test/test_repo");
|
||||
|
||||
const data = parseJsonFromCLIOutput(result.stdout);
|
||||
|
||||
// The test app should NOT appear because of auto-detected skipApps: true
|
||||
const hasTestApp = (data.changes || []).some((change: any) =>
|
||||
change.path?.includes('f/test_dashboard')
|
||||
);
|
||||
assertEquals(hasTestApp, false, "Test app should be skipped due to auto-detected override");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Multi-Instance: workspace wildcards with new format", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend, "wildcard_test");
|
||||
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
excludes: []
|
||||
|
||||
overrides:
|
||||
# Wildcard for current backend instance
|
||||
"${backendUrl}:${backend.workspace}:*":
|
||||
skipVariables: true
|
||||
skipResources: true`);
|
||||
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--dry-run',
|
||||
'--json-output'
|
||||
], tempDir, "wildcard_test");
|
||||
|
||||
assertEquals(result.code, 0);
|
||||
|
||||
const data = parseJsonFromCLIOutput(result.stdout);
|
||||
|
||||
// Variables should be skipped due to wildcard override
|
||||
const hasTestVariable = (data.changes || []).some((change: any) =>
|
||||
change.path?.includes('u/admin/test_config.variable.yaml')
|
||||
);
|
||||
assertEquals(hasTestVariable, false, "Variables should be skipped due to wildcard override");
|
||||
});
|
||||
});
|
||||
273
cli/test/override_settings_behavior.test.ts
Normal file
273
cli/test/override_settings_behavior.test.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { assertEquals, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { getEffectiveSettings } from "../conf.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
|
||||
// =============================================================================
|
||||
// OVERRIDE SETTINGS BEHAVIOR TESTS
|
||||
// Tests for override inheritance and file filtering behavior
|
||||
// =============================================================================
|
||||
|
||||
Deno.test("Override Settings: override inherits non-overridden settings from base config", () => {
|
||||
const config = {
|
||||
includes: ["default/**"],
|
||||
skipVariables: true, // Base has this as true
|
||||
skipResources: true, // Base has this as true
|
||||
skipApps: false, // Base has this as false
|
||||
defaultTs: "bun" as const,
|
||||
overrides: {
|
||||
"http://localhost:8000/:test:u/user/repo": {
|
||||
includes: ["override/**"],
|
||||
skipApps: true // Override only changes skipApps, should inherit other skip flags
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const effective = getEffectiveSettings(
|
||||
config,
|
||||
"http://localhost:8000/",
|
||||
"test",
|
||||
"u/user/repo"
|
||||
);
|
||||
|
||||
// Override values should be used
|
||||
assertEquals(effective.includes, ["override/**"], "Must use override includes");
|
||||
assertEquals(effective.skipApps, true, "Must use override skipApps");
|
||||
|
||||
// Should inherit skip flags from base config
|
||||
assertEquals(effective.skipVariables, true, "Must inherit skipVariables=true from base config");
|
||||
assertEquals(effective.skipResources, true, "Must inherit skipResources=true from base config");
|
||||
assertEquals(effective.defaultTs, "bun", "Must inherit defaultTs from base config");
|
||||
});
|
||||
|
||||
Deno.test("Override Settings: workspace wildcards with repo-specific precedence", () => {
|
||||
const config = {
|
||||
includes: ["default/**"],
|
||||
skipVariables: false,
|
||||
overrides: {
|
||||
"http://localhost:8000/:test:*": {
|
||||
skipVariables: true,
|
||||
includes: ["workspace/**"]
|
||||
},
|
||||
"http://localhost:8000/:test:u/user/specific": {
|
||||
includes: ["specific/**"]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Test specific repo override (should take precedence over wildcard)
|
||||
const specificEffective = getEffectiveSettings(
|
||||
config,
|
||||
"http://localhost:8000/",
|
||||
"test",
|
||||
"u/user/specific"
|
||||
);
|
||||
assertEquals(specificEffective.includes, ["specific/**"], "Specific repo override must take precedence over wildcard");
|
||||
assertEquals(specificEffective.skipVariables, true, "Workspace wildcard setting must still apply");
|
||||
|
||||
// Test wildcard match
|
||||
const wildcardEffective = getEffectiveSettings(
|
||||
config,
|
||||
"http://localhost:8000/",
|
||||
"test",
|
||||
"u/user/other"
|
||||
);
|
||||
assertEquals(wildcardEffective.includes, ["workspace/**"], "Wildcard must match repos without specific overrides");
|
||||
assertEquals(wildcardEffective.skipVariables, true, "Workspace wildcard setting must apply");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// INTEGRATION TESTS - File Filtering Behavior
|
||||
// =============================================================================
|
||||
|
||||
Deno.test("Integration: sync pull with skipVariables override excludes variable files", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "skip_variables_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Create wmill.yaml with override that skips variables
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
|
||||
overrides:
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
skipVariables: true`);
|
||||
|
||||
// Verify backend has test variable before pull
|
||||
const backendVariables = await backend.listAllVariables();
|
||||
const hasTestVariable = backendVariables.some(v => v.path === 'u/admin/test_config');
|
||||
assert(hasTestVariable, "Backend should have test variable before pull");
|
||||
|
||||
// Run sync pull (NOT dry-run) to actually write files
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--yes'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Sync pull should succeed: ${result.stderr}`);
|
||||
|
||||
// Verify variable files were NOT written to filesystem due to skipVariables: true
|
||||
const filesWritten = [];
|
||||
for await (const entry of Deno.readDir(tempDir)) {
|
||||
if (entry.isFile && entry.name.endsWith('.yaml')) {
|
||||
filesWritten.push(entry.name);
|
||||
}
|
||||
}
|
||||
|
||||
const hasVariableFile = filesWritten.some(file => file.includes('.variable.yaml'));
|
||||
assertEquals(hasVariableFile, false, "Variable files should NOT be written due to skipVariables override");
|
||||
|
||||
// Verify other files WERE written (since skipVariables only affects variables)
|
||||
// Check what files were actually written
|
||||
console.log("Files written:", filesWritten);
|
||||
|
||||
// Should have some files written (just not variable files)
|
||||
assert(filesWritten.length > 0, `Some files should be written when skipVariables is true. Got: ${filesWritten.join(', ')}`);
|
||||
|
||||
// Should not have only wmill.yaml file
|
||||
const nonWmillFiles = filesWritten.filter(f => !f.includes('wmill.yaml'));
|
||||
assert(nonWmillFiles.length > 0, `Non-wmill.yaml files should be written when skipVariables is true. Got: ${nonWmillFiles.join(', ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Integration: sync push with skipVariables override excludes variable files", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "push_skip_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Create wmill.yaml with override that skips variables
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipVariables: false
|
||||
|
||||
overrides:
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
skipVariables: true`);
|
||||
|
||||
// Create local test files including variables and scripts
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Create variable file
|
||||
await Deno.mkdir(`${tempDir}/u/admin`, { recursive: true });
|
||||
await Deno.writeTextFile(`${tempDir}/u/admin/test_push_var_${timestamp}.variable.yaml`,
|
||||
`value: test_value_${timestamp}
|
||||
description: Test variable for push override test
|
||||
is_secret: false`);
|
||||
|
||||
// Create script file
|
||||
await Deno.mkdir(`${tempDir}/f/test`, { recursive: true });
|
||||
await Deno.writeTextFile(`${tempDir}/f/test/push_script_${timestamp}.ts`,
|
||||
`export async function main() {
|
||||
return "Test script ${timestamp}";
|
||||
}`);
|
||||
await Deno.writeTextFile(`${tempDir}/f/test/push_script_${timestamp}.script.yaml`,
|
||||
`summary: Test Push Script ${timestamp}
|
||||
description: Script for testing push with override`);
|
||||
|
||||
// Get backend state before push
|
||||
const beforeVariables = await backend.listAllVariables();
|
||||
const beforeScripts = await backend.listAllScripts();
|
||||
|
||||
const variableExistsBefore = beforeVariables.some(v => v.path === `u/admin/test_push_var_${timestamp}`);
|
||||
const scriptExistsBefore = beforeScripts.some(s => s.path === `f/test/push_script_${timestamp}`);
|
||||
|
||||
assertEquals(variableExistsBefore, false, "Variable should not exist before push");
|
||||
assertEquals(scriptExistsBefore, false, "Script should not exist before push");
|
||||
|
||||
// Run sync push (NOT dry-run) to actually push files
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'push',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--yes'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Sync push should succeed: ${result.stderr}`);
|
||||
|
||||
// Verify variable was NOT pushed due to skipVariables: true
|
||||
const afterVariables = await backend.listAllVariables();
|
||||
const variableExistsAfter = afterVariables.some(v => v.path === `u/admin/test_push_var_${timestamp}`);
|
||||
assertEquals(variableExistsAfter, false, "Variable should NOT be pushed due to skipVariables override");
|
||||
|
||||
// Verify script WAS pushed (not affected by skipVariables)
|
||||
const afterScripts = await backend.listAllScripts();
|
||||
const scriptExistsAfter = afterScripts.some(s => s.path === `f/test/push_script_${timestamp}`);
|
||||
assertEquals(scriptExistsAfter, true, "Script should be pushed normally");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Integration: sync pull respects includes override for file filtering", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "includes_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Create wmill.yaml with override that only includes specific path
|
||||
const backendUrl = new URL(backend.baseUrl).toString();
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
|
||||
overrides:
|
||||
"${backendUrl}:${backend.workspace}:u/test/test_repo":
|
||||
includes:
|
||||
- "u/admin/**" # Only include admin resources, exclude f/** apps/scripts`);
|
||||
|
||||
// Run sync pull to write files
|
||||
const result = await backend.runCLICommand([
|
||||
'sync', 'pull',
|
||||
'--repository', 'u/test/test_repo',
|
||||
'--yes'
|
||||
], tempDir);
|
||||
|
||||
assertEquals(result.code, 0, `Sync pull should succeed: ${result.stderr}`);
|
||||
|
||||
// Verify admin files were written (since we have includes: ["u/admin/**"])
|
||||
const adminFiles = [];
|
||||
try {
|
||||
for await (const entry of Deno.readDir(`${tempDir}/u/admin`)) {
|
||||
if (entry.isFile) {
|
||||
adminFiles.push(`u/admin/${entry.name}`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Directory might not exist if no files matched
|
||||
}
|
||||
|
||||
// We expect admin files to be written since backend has u/admin/test_config variable
|
||||
assert(adminFiles.length > 0, `Admin files should be written due to includes override. Expected u/admin files but found: ${adminFiles.join(', ')}`);
|
||||
|
||||
// Verify f/** files were NOT written due to includes override
|
||||
let fDirectoryExists = false;
|
||||
try {
|
||||
await Deno.stat(`${tempDir}/f`);
|
||||
fDirectoryExists = true;
|
||||
} catch {
|
||||
// Directory doesn't exist, which is expected
|
||||
}
|
||||
|
||||
assertEquals(fDirectoryExists, false, "f/ directory should not exist due to includes override excluding f/**");
|
||||
});
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"workspace_id": "starter",
|
||||
"name": "postgres",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"required": [
|
||||
"dbname",
|
||||
"user",
|
||||
"password"
|
||||
],
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string",
|
||||
"description": "The instance host"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"description": "The instance port"
|
||||
},
|
||||
"user": {
|
||||
"type": "string",
|
||||
"description": "The postgres username"
|
||||
},
|
||||
"dbname": {
|
||||
"type": "string",
|
||||
"description": "The database name"
|
||||
},
|
||||
"sslmode": {
|
||||
"enum": [
|
||||
"disable",
|
||||
"allow",
|
||||
"prefer",
|
||||
"require",
|
||||
"verify-ca",
|
||||
"verify-full"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "The sslmode"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"description": "The postgres users password"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "A postgres database connection resource"
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"workspace_id": "starter",
|
||||
"name": "slack",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"required": [],
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"description": "The slack token"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "A slack token to interact with a specific workspace. Can be obtained from the OAuth integration in the workspace settings."
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"workspace_id": "starter",
|
||||
"path": "g/all/demodb",
|
||||
"value": {
|
||||
"host": "demodb.service.consul",
|
||||
"port": "6543",
|
||||
"user": "postgres",
|
||||
"dbname": "demodb",
|
||||
"sslmode": "disable",
|
||||
"password": "demodb"
|
||||
},
|
||||
"description": "demodb",
|
||||
"resource_type": "postgres",
|
||||
"extra_perms": {},
|
||||
"is_oauth": false
|
||||
}
|
||||
147
cli/test/sync_config_resolution.test.ts
Normal file
147
cli/test/sync_config_resolution.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { readConfigFile, getEffectiveSettings } from "../conf.ts";
|
||||
import { withContainerizedBackend } from "./containerized_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
|
||||
|
||||
// =============================================================================
|
||||
// SYNC CONFIGURATION RESOLUTION TESTS
|
||||
// Tests for configuration resolution and integration with backend
|
||||
// =============================================================================
|
||||
|
||||
// Helper function to set up workspace profile with localhost_test name
|
||||
async function setupWorkspaceProfile(backend: any): Promise<void> {
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl, // "http://localhost:8001/"
|
||||
workspaceId: backend.workspace, // "test"
|
||||
name: "localhost_test", // This is what the tests expect!
|
||||
token: backend.token
|
||||
};
|
||||
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// INTEGRATION TESTS WITH REAL BACKEND
|
||||
// =============================================================================
|
||||
|
||||
Deno.test("Integration: wmill.yaml configuration produces expected results", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile with name "localhost_test"
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Create wmill.yaml with settings
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- f/**
|
||||
- settings.yaml
|
||||
excludes:
|
||||
- "*.test.ts"
|
||||
skipVariables: true
|
||||
skipResources: true
|
||||
includeSettings: true
|
||||
includeSchedules: true
|
||||
includeTriggers: true`);
|
||||
|
||||
// Test pull with wmill.yaml configuration
|
||||
const yamlResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
|
||||
if (yamlResult.code !== 0) {
|
||||
console.log("YAML command failed!");
|
||||
console.log("Exit code:", yamlResult.code);
|
||||
console.log("Stdout:", yamlResult.stdout);
|
||||
console.log("Stderr:", yamlResult.stderr);
|
||||
}
|
||||
assertEquals(yamlResult.code, 0);
|
||||
|
||||
// Extract JSON from CLI output (skip log messages)
|
||||
const yamlData = parseJsonFromCLIOutput(yamlResult.stdout);
|
||||
|
||||
// Should include settings.yaml due to includeSettings: true
|
||||
const hasSettings = (yamlData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path === 'settings.yaml'
|
||||
);
|
||||
assertEquals(hasSettings, true);
|
||||
|
||||
// Should NOT include resources or variables (due to skip flags)
|
||||
const hasResources = (yamlData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.resource.yaml')
|
||||
);
|
||||
const hasVariables = (yamlData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.variable.yaml')
|
||||
);
|
||||
assertEquals(hasResources, false);
|
||||
assertEquals(hasVariables, false);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Integration: settings.yaml inclusion respects includeSettings flag", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile with name "localhost_test"
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Test 1: includeSettings: true should include settings.yaml
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
includeSettings: true`);
|
||||
|
||||
const includeResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
|
||||
assertEquals(includeResult.code, 0);
|
||||
|
||||
// Extract JSON from CLI output (skip log messages)
|
||||
const includeData = parseJsonFromCLIOutput(includeResult.stdout);
|
||||
const hasSettingsInclude = (includeData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path === 'settings.yaml'
|
||||
);
|
||||
assertEquals(hasSettingsInclude, true);
|
||||
|
||||
// Test 2: includeSettings: false should NOT include settings.yaml
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
includeSettings: false`);
|
||||
|
||||
const excludeResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
|
||||
assertEquals(excludeResult.code, 0);
|
||||
|
||||
// Extract JSON from CLI output (skip log messages)
|
||||
const excludeData = parseJsonFromCLIOutput(excludeResult.stdout);
|
||||
const hasSettingsExclude = (excludeData.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path === 'settings.yaml'
|
||||
);
|
||||
assertEquals(hasSettingsExclude, false);
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Integration: resource/variable filtering respects skip flags", async () => {
|
||||
await withContainerizedBackend(async (backend, tempDir) => {
|
||||
// Set up workspace profile with name "localhost_test"
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
// Test skipResources: true
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
skipResources: true
|
||||
skipVariables: false`);
|
||||
|
||||
const result = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
|
||||
assertEquals(result.code, 0);
|
||||
|
||||
// Extract JSON from CLI output (skip log messages)
|
||||
const data = parseJsonFromCLIOutput(result.stdout);
|
||||
|
||||
// Should NOT include resources
|
||||
const hasResources = (data.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.resource.yaml')
|
||||
);
|
||||
assertEquals(hasResources, false);
|
||||
|
||||
// Should include variables (not skipped)
|
||||
const hasVariables = (data.changes || []).some((change: any) =>
|
||||
change.type === 'added' && change.path?.includes('.variable.yaml')
|
||||
);
|
||||
assertEquals(hasVariables, true);
|
||||
});
|
||||
});
|
||||
39
cli/test/test_config_helpers.ts
Normal file
39
cli/test/test_config_helpers.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { getRootStore } from "../store.ts";
|
||||
|
||||
/**
|
||||
* Create a temporary config directory for testing that doesn't interfere with user's config
|
||||
*/
|
||||
export async function withTestConfig<T>(callback: (testConfigDir: string) => Promise<T>): Promise<T> {
|
||||
// Create a unique temporary directory for this test
|
||||
const testDir = await Deno.makeTempDir({ prefix: "wmill_test_config_" });
|
||||
|
||||
try {
|
||||
return await callback(testDir);
|
||||
} finally {
|
||||
// Clean up the temporary directory
|
||||
try {
|
||||
await Deno.remove(testDir, { recursive: true });
|
||||
} catch (error) {
|
||||
console.warn(`Failed to clean up test config directory ${testDir}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the remotes file in test config directory
|
||||
*/
|
||||
export async function clearTestRemotes(testConfigDir: string): Promise<void> {
|
||||
const remoteFile = (await getRootStore(testConfigDir)) + "remotes.ndjson";
|
||||
await Deno.writeTextFile(remoteFile, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JSON output from CLI command, handling log messages that appear before JSON
|
||||
*/
|
||||
export function parseJsonFromCLIOutput(stdout: string): any {
|
||||
const jsonMatch = stdout.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) {
|
||||
throw new Error(`No JSON found in CLI output: ${stdout}`);
|
||||
}
|
||||
return JSON.parse(jsonMatch[0]);
|
||||
}
|
||||
153
cli/test/workspace_conflicts.test.ts
Normal file
153
cli/test/workspace_conflicts.test.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { assertEquals, assertRejects } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { addWorkspace, allWorkspaces } from "../workspace.ts";
|
||||
import { withTestConfig, clearTestRemotes } from "./test_config_helpers.ts";
|
||||
|
||||
// Test workspace conflict detection
|
||||
Deno.test("addWorkspace: prevents duplicate workspace names", async () => {
|
||||
await withTestConfig(async (testConfigDir) => {
|
||||
await clearTestRemotes(testConfigDir);
|
||||
|
||||
// Add first workspace
|
||||
const workspace1 = {
|
||||
name: "test_workspace",
|
||||
remote: "http://localhost:8001/",
|
||||
workspaceId: "workspace1",
|
||||
token: "token1"
|
||||
};
|
||||
|
||||
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Try to add workspace with same name but different details
|
||||
const workspace2 = {
|
||||
name: "test_workspace", // Same name
|
||||
remote: "http://localhost:8002/", // Different remote
|
||||
workspaceId: "workspace2", // Different ID
|
||||
token: "token2"
|
||||
};
|
||||
|
||||
// Should throw error in non-interactive mode without force
|
||||
await assertRejects(
|
||||
() => addWorkspace(workspace2, { configDir: testConfigDir }),
|
||||
Error,
|
||||
"Workspace name conflict. Use --force to overwrite or choose a different name."
|
||||
);
|
||||
|
||||
// Should succeed with force flag
|
||||
await addWorkspace(workspace2, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Verify the workspace was overwritten
|
||||
const workspaces = await allWorkspaces(testConfigDir);
|
||||
assertEquals(workspaces.length, 1);
|
||||
assertEquals(workspaces[0].name, "test_workspace");
|
||||
assertEquals(workspaces[0].remote, "http://localhost:8002/");
|
||||
assertEquals(workspaces[0].workspaceId, "workspace2");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("addWorkspace: prevents duplicate (remote, workspaceId) tuples", async () => {
|
||||
await withTestConfig(async (testConfigDir) => {
|
||||
await clearTestRemotes(testConfigDir);
|
||||
|
||||
// Add first workspace
|
||||
const workspace1 = {
|
||||
name: "first_workspace",
|
||||
remote: "http://localhost:8001/",
|
||||
workspaceId: "test",
|
||||
token: "token1"
|
||||
};
|
||||
|
||||
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Try to add workspace with same (remote, workspaceId) but different name
|
||||
const workspace2 = {
|
||||
name: "second_workspace", // Different name
|
||||
remote: "http://localhost:8001/", // Same remote
|
||||
workspaceId: "test", // Same workspaceId
|
||||
token: "token2"
|
||||
};
|
||||
|
||||
// Should throw error in non-interactive mode without force
|
||||
await assertRejects(
|
||||
() => addWorkspace(workspace2, { configDir: testConfigDir }),
|
||||
Error,
|
||||
'Backend constraint violation: (http://localhost:8001/, test) already exists as "first_workspace". Use --force to overwrite.'
|
||||
);
|
||||
|
||||
// Should succeed with force flag (overwrites first workspace)
|
||||
await addWorkspace(workspace2, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Verify the first workspace was removed and second was added
|
||||
const workspaces = await allWorkspaces(testConfigDir);
|
||||
assertEquals(workspaces.length, 1);
|
||||
assertEquals(workspaces[0].name, "second_workspace");
|
||||
assertEquals(workspaces[0].remote, "http://localhost:8001/");
|
||||
assertEquals(workspaces[0].workspaceId, "test");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("addWorkspace: allows same workspace (name, remote, workspaceId) with token update", async () => {
|
||||
await withTestConfig(async (testConfigDir) => {
|
||||
await clearTestRemotes(testConfigDir);
|
||||
|
||||
// Add first workspace
|
||||
const workspace1 = {
|
||||
name: "same_workspace",
|
||||
remote: "http://localhost:8001/",
|
||||
workspaceId: "test",
|
||||
token: "old_token"
|
||||
};
|
||||
|
||||
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Add same workspace with updated token
|
||||
const workspace2 = {
|
||||
name: "same_workspace", // Same name
|
||||
remote: "http://localhost:8001/", // Same remote
|
||||
workspaceId: "test", // Same workspaceId
|
||||
token: "new_token" // Different token
|
||||
};
|
||||
|
||||
// Should succeed without force (just token update)
|
||||
await addWorkspace(workspace2, { configDir: testConfigDir });
|
||||
|
||||
// Verify token was updated
|
||||
const workspaces = await allWorkspaces(testConfigDir);
|
||||
assertEquals(workspaces.length, 1);
|
||||
assertEquals(workspaces[0].name, "same_workspace");
|
||||
assertEquals(workspaces[0].token, "new_token");
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("addWorkspace: allows different workspaces on different remotes", async () => {
|
||||
await withTestConfig(async (testConfigDir) => {
|
||||
await clearTestRemotes(testConfigDir);
|
||||
|
||||
// Add workspace on first remote
|
||||
const workspace1 = {
|
||||
name: "workspace_remote1",
|
||||
remote: "http://localhost:8001/",
|
||||
workspaceId: "test",
|
||||
token: "token1"
|
||||
};
|
||||
|
||||
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
|
||||
|
||||
// Add workspace with same workspaceId on different remote (should be allowed)
|
||||
const workspace2 = {
|
||||
name: "workspace_remote2",
|
||||
remote: "http://localhost:8002/", // Different remote
|
||||
workspaceId: "test", // Same workspaceId (OK on different remote)
|
||||
token: "token2"
|
||||
};
|
||||
|
||||
// Should succeed (different remotes)
|
||||
await addWorkspace(workspace2, { configDir: testConfigDir });
|
||||
|
||||
// Verify both workspaces exist
|
||||
const workspaces = await allWorkspaces(testConfigDir);
|
||||
assertEquals(workspaces.length, 2);
|
||||
|
||||
const names = workspaces.map(w => w.name).sort();
|
||||
assertEquals(names, ["workspace_remote1", "workspace_remote2"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user