/** * Sync Pull/Push Integration Tests * * Tests the sync pull and push functionality with a simulated filesystem * containing every kind of Windmill resource type. */ import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts"; import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts"; import * as path from "https://deno.land/std@0.224.0/path/mod.ts"; import { JSZip } from "../deps.ts"; import { getFolderSuffix, getMetadataFileName, buildFolderPath, } from "../src/utils/resource_folders.ts"; // ============================================================================= // Test Fixtures - Every Type of Windmill Resource // ============================================================================= /** * Creates a mock script file structure */ function createScriptFixture( name: string, language: "python3" | "deno" | "bun" | "bash" | "go" | "postgresql", content?: string, ): { contentFile: { path: string; content: string }; metadataFile: { path: string; content: string } } { const extensions: Record = { python3: ".py", deno: ".ts", bun: ".ts", bash: ".sh", go: ".go", postgresql: ".sql", }; const ext = extensions[language]; const defaultContent: Record = { python3: `def main():\n return "Hello from ${name}"`, deno: `export async function main() {\n return "Hello from ${name}";\n}`, bun: `export async function main() {\n return "Hello from ${name}";\n}`, bash: `#!/bin/bash\necho "Hello from ${name}"`, go: `package inner\n\nfunc main() string {\n return "Hello from ${name}"\n}`, postgresql: `-- ${name}\nSELECT 'Hello from ${name}';`, }; return { contentFile: { path: `${name}${ext}`, content: content ?? defaultContent[language], }, metadataFile: { path: `${name}.script.yaml`, content: `summary: "${name} script" description: "A ${language} script for testing" schema: $schema: "https://json-schema.org/draft/2020-12/schema" type: object properties: {} required: [] is_template: false lock: [] kind: script `, }, }; } /** * Creates a mock flow file structure */ function createFlowFixture(name: string): Record { const flowSuffix = getFolderSuffix("flow"); const metadataFile = getMetadataFileName("flow", "yaml"); return { metadata: { path: `${name}${flowSuffix}/${metadataFile}`, content: `summary: "${name} flow" description: "A flow for testing" value: modules: - id: a value: type: rawscript content: "!inline ${name}${flowSuffix}/a.ts" language: bun input_transforms: {} schema: $schema: "https://json-schema.org/draft/2020-12/schema" type: object properties: {} required: [] `, }, inlineScript: { path: `${name}${flowSuffix}/a.ts`, content: `export async function main() {\n return "Hello from flow ${name}";\n}`, }, }; } /** * Creates a mock app file structure */ function createAppFixture(name: string): Record { const appSuffix = getFolderSuffix("app"); const metadataFile = getMetadataFileName("app", "yaml"); return { metadata: { path: `${name}${appSuffix}/${metadataFile}`, content: `summary: "${name} app" value: grid: - data: id: "text_1" type: textcomponent componentInput: type: static value: "Hello from ${name}" id: "a" hiddenInlineScripts: [] policy: execution_mode: viewer `, }, }; } /** * Creates a mock raw_app file structure */ function createRawAppFixture(name: string): Record { const rawAppSuffix = getFolderSuffix("raw_app"); const metadataFile = getMetadataFileName("raw_app", "yaml"); return { metadata: { path: `${name}${rawAppSuffix}/${metadataFile}`, content: `summary: "${name} raw app" runnables: a: path: u/test/my_script type: script `, }, indexHtml: { path: `${name}${rawAppSuffix}/index.html`, content: ` ${name}

Hello from ${name}

`, }, indexJs: { path: `${name}${rawAppSuffix}/index.js`, content: `console.log("Hello from ${name}");`, }, }; } /** * Creates a mock resource file */ function createResourceFixture( name: string, resourceType: string, value: Record, ): { path: string; content: string } { return { path: `${name}.resource.yaml`, content: `resource_type: "${resourceType}" value: ${Object.entries(value).map(([k, v]) => ` ${k}: ${JSON.stringify(v)}`).join("\n")} `, }; } /** * Creates a mock variable file */ function createVariableFixture( name: string, value: string, isSecret: boolean = false, ): { path: string; content: string } { return { path: `${name}.variable.yaml`, content: `value: "${value}" is_secret: ${isSecret} description: "Variable ${name} for testing" `, }; } /** * Creates a mock schedule file */ function createScheduleFixture( name: string, scriptPath: string, schedule: string = "0 * * * *", ): { path: string; content: string } { return { path: `${name}.schedule.yaml`, content: `path: "${name}" schedule: "${schedule}" script_path: "${scriptPath}" is_flow: false args: {} enabled: true timezone: "UTC" `, }; } /** * Creates a mock HTTP trigger file */ function createHttpTriggerFixture( name: string, routePath: string, scriptPath: string, ): { path: string; content: string } { return { path: `${name}.http_trigger.yaml`, content: `path: "${name}" route_path: "${routePath}" script_path: "${scriptPath}" is_flow: false http_method: post authentication_method: none is_async: false `, }; } /** * Creates a mock WebSocket trigger file */ function createWebsocketTriggerFixture( name: string, url: string, scriptPath: string, ): { path: string; content: string } { return { path: `${name}.websocket_trigger.yaml`, content: `path: "${name}" url: "${url}" script_path: "${scriptPath}" is_flow: false enabled: true filters: [] initial_messages: [] `, }; } /** * Creates a mock folder meta file */ function createFolderFixture(name: string): { path: string; content: string } { return { path: `${name}/folder.meta.yaml`, content: `display_name: "${name}" owners: [] extra_perms: {} `, }; } /** * Creates a mock user file */ function createUserFixture( username: string, email: string, isAdmin: boolean = false, ): { path: string; content: string } { return { path: `${username}.user.yaml`, content: `username: "${username}" email: "${email}" is_admin: ${isAdmin} operator: false disabled: false `, }; } /** * Creates a mock group file */ function createGroupFixture(name: string, members: string[] = []): { path: string; content: string } { return { path: `${name}.group.yaml`, content: `name: "${name}" summary: "Group ${name} for testing" members: ${members.map((m) => ` - ${m}`).join("\n") || " []"} `, }; } // ============================================================================= // Test Helper Functions // ============================================================================= /** * Creates a complete local filesystem with all resource types */ async function createLocalFilesystem(baseDir: string): Promise { // Create folder structure const folders = ["f/scripts", "f/flows", "f/apps", "f/resources"]; for (const folder of folders) { await ensureDir(path.join(baseDir, folder)); } // Create scripts const scripts = [ createScriptFixture("f/scripts/python_script", "python3"), createScriptFixture("f/scripts/deno_script", "deno"), createScriptFixture("f/scripts/bash_script", "bash"), createScriptFixture("f/scripts/go_script", "go"), createScriptFixture("f/scripts/sql_script", "postgresql"), ]; for (const script of scripts) { await Deno.writeTextFile( path.join(baseDir, script.contentFile.path), script.contentFile.content, ); await Deno.writeTextFile( path.join(baseDir, script.metadataFile.path), script.metadataFile.content, ); } // Create flows const flowFixture = createFlowFixture("f/flows/test_flow"); await ensureDir(path.join(baseDir, `f/flows/test_flow${getFolderSuffix("flow")}`)); for (const file of Object.values(flowFixture)) { await Deno.writeTextFile(path.join(baseDir, file.path), file.content); } // Create apps const appFixture = createAppFixture("f/apps/test_app"); await ensureDir(path.join(baseDir, `f/apps/test_app${getFolderSuffix("app")}`)); for (const file of Object.values(appFixture)) { await Deno.writeTextFile(path.join(baseDir, file.path), file.content); } // Create raw apps const rawAppFixture = createRawAppFixture("f/apps/test_raw_app"); await ensureDir(path.join(baseDir, `f/apps/test_raw_app${getFolderSuffix("raw_app")}`)); for (const file of Object.values(rawAppFixture)) { await Deno.writeTextFile(path.join(baseDir, file.path), file.content); } // Create resources const resources = [ createResourceFixture("f/resources/postgres_db", "postgresql", { host: "localhost", port: 5432, user: "admin", dbname: "test_db", }), createResourceFixture("f/resources/api_config", "c_api_config", { base_url: "https://api.example.com", api_key: "$var:f/secrets/api_key", }), ]; for (const resource of resources) { await Deno.writeTextFile(path.join(baseDir, resource.path), resource.content); } // Create variables const variables = [ createVariableFixture("f/resources/config_value", "test_value", false), createVariableFixture("f/resources/secret_key", "secret123", true), ]; for (const variable of variables) { await Deno.writeTextFile(path.join(baseDir, variable.path), variable.content); } // Create folder metadata await ensureDir(path.join(baseDir, "f")); const folderMeta = createFolderFixture("f"); await Deno.writeTextFile(path.join(baseDir, folderMeta.path), folderMeta.content); } /** * Creates a mock zip file representing remote workspace state */ async function createMockRemoteZip(items: Record): Promise { const zip = new JSZip(); for (const [filePath, content] of Object.entries(items)) { zip.file(filePath, content); } return zip; } /** * Reads all files from a directory recursively */ async function readDirRecursive( dir: string, baseDir: string = dir, ): Promise> { const files: Record = {}; for await (const entry of Deno.readDir(dir)) { const fullPath = path.join(dir, entry.name); const relativePath = fullPath.substring(baseDir.length + 1); if (entry.isDirectory) { const subFiles = await readDirRecursive(fullPath, baseDir); Object.assign(files, subFiles); } else { files[relativePath] = await Deno.readTextFile(fullPath); } } return files; } /** * Creates a temporary directory for testing */ async function createTempDir(): Promise { return await Deno.makeTempDir({ prefix: "wmill_sync_test_" }); } /** * Cleans up a temporary directory */ async function cleanupTempDir(dir: string): Promise { try { await Deno.remove(dir, { recursive: true }); } catch { // Ignore cleanup errors } } // ============================================================================= // Tests // ============================================================================= Deno.test("Resource folder suffixes are correct", () => { assertEquals(getFolderSuffix("flow"), ".flow"); assertEquals(getFolderSuffix("app"), ".app"); assertEquals(getFolderSuffix("raw_app"), ".raw_app"); }); Deno.test("Metadata file names are correct", () => { assertEquals(getMetadataFileName("flow", "yaml"), "flow.yaml"); assertEquals(getMetadataFileName("flow", "json"), "flow.json"); assertEquals(getMetadataFileName("app", "yaml"), "app.yaml"); assertEquals(getMetadataFileName("raw_app", "yaml"), "raw_app.yaml"); }); Deno.test("buildFolderPath creates correct paths", () => { assertEquals(buildFolderPath("my_flow", "flow"), "my_flow.flow"); assertEquals(buildFolderPath("f/test/my_app", "app"), "f/test/my_app.app"); assertEquals(buildFolderPath("u/admin/raw_app", "raw_app"), "u/admin/raw_app.raw_app"); }); Deno.test("Script fixture creates valid structure", () => { const pythonScript = createScriptFixture("test_script", "python3"); assertEquals(pythonScript.contentFile.path, "test_script.py"); assertEquals(pythonScript.metadataFile.path, "test_script.script.yaml"); assertStringIncludes(pythonScript.contentFile.content, "def main()"); assertStringIncludes(pythonScript.metadataFile.content, "summary:"); assertStringIncludes(pythonScript.metadataFile.content, "kind: script"); }); Deno.test("Flow fixture creates valid structure", () => { const flow = createFlowFixture("test_flow"); assertEquals(flow.metadata.path, "test_flow.flow/flow.yaml"); assertEquals(flow.inlineScript.path, "test_flow.flow/a.ts"); assertStringIncludes(flow.metadata.content, "summary:"); assertStringIncludes(flow.metadata.content, "modules:"); assertStringIncludes(flow.inlineScript.content, "export async function main"); }); Deno.test("App fixture creates valid structure", () => { const app = createAppFixture("test_app"); assertEquals(app.metadata.path, "test_app.app/app.yaml"); assertStringIncludes(app.metadata.content, "summary:"); assertStringIncludes(app.metadata.content, "grid:"); assertStringIncludes(app.metadata.content, "policy:"); }); Deno.test("Raw app fixture creates valid structure", () => { const rawApp = createRawAppFixture("test_raw_app"); assertEquals(rawApp.metadata.path, "test_raw_app.raw_app/raw_app.yaml"); assertEquals(rawApp.indexHtml.path, "test_raw_app.raw_app/index.html"); assertEquals(rawApp.indexJs.path, "test_raw_app.raw_app/index.js"); assertStringIncludes(rawApp.metadata.content, "summary:"); assertStringIncludes(rawApp.metadata.content, "runnables:"); }); Deno.test("Resource fixture creates valid YAML", () => { const resource = createResourceFixture("postgres", "postgresql", { host: "localhost", port: 5432, }); assertEquals(resource.path, "postgres.resource.yaml"); assertStringIncludes(resource.content, 'resource_type: "postgresql"'); assertStringIncludes(resource.content, "value:"); }); Deno.test("Variable fixture creates valid YAML", () => { const variable = createVariableFixture("my_var", "test_value", false); assertEquals(variable.path, "my_var.variable.yaml"); assertStringIncludes(variable.content, 'value: "test_value"'); assertStringIncludes(variable.content, "is_secret: false"); }); Deno.test("Schedule fixture creates valid YAML", () => { const schedule = createScheduleFixture("hourly_job", "u/admin/my_script", "0 * * * *"); assertEquals(schedule.path, "hourly_job.schedule.yaml"); assertStringIncludes(schedule.content, 'schedule: "0 * * * *"'); assertStringIncludes(schedule.content, 'script_path: "u/admin/my_script"'); }); Deno.test("HTTP trigger fixture creates valid YAML", () => { const trigger = createHttpTriggerFixture("webhook", "/api/webhook", "u/admin/handler"); assertEquals(trigger.path, "webhook.http_trigger.yaml"); assertStringIncludes(trigger.content, 'route_path: "/api/webhook"'); assertStringIncludes(trigger.content, "http_method: post"); }); Deno.test("Folder fixture creates valid YAML", () => { const folder = createFolderFixture("my_folder"); assertEquals(folder.path, "my_folder/folder.meta.yaml"); assertStringIncludes(folder.content, 'display_name: "my_folder"'); }); Deno.test("User fixture creates valid YAML", () => { const user = createUserFixture("test_user", "test@example.com", true); assertEquals(user.path, "test_user.user.yaml"); assertStringIncludes(user.content, 'username: "test_user"'); assertStringIncludes(user.content, 'email: "test@example.com"'); assertStringIncludes(user.content, "is_admin: true"); }); Deno.test("Group fixture creates valid YAML", () => { const group = createGroupFixture("developers", ["user1", "user2"]); assertEquals(group.path, "developers.group.yaml"); assertStringIncludes(group.content, 'name: "developers"'); assertStringIncludes(group.content, "- user1"); assertStringIncludes(group.content, "- user2"); }); Deno.test("Local filesystem creation creates all expected files", async () => { const tempDir = await createTempDir(); try { await createLocalFilesystem(tempDir); const files = await readDirRecursive(tempDir); // Check scripts exist assert("f/scripts/python_script.py" in files, "Python script content should exist"); assert("f/scripts/python_script.script.yaml" in files, "Python script metadata should exist"); assert("f/scripts/deno_script.ts" in files, "Deno script content should exist"); assert("f/scripts/bash_script.sh" in files, "Bash script content should exist"); assert("f/scripts/go_script.go" in files, "Go script content should exist"); assert("f/scripts/sql_script.sql" in files, "SQL script content should exist"); // Check flows exist assert("f/flows/test_flow.flow/flow.yaml" in files, "Flow metadata should exist"); assert("f/flows/test_flow.flow/a.ts" in files, "Flow inline script should exist"); // Check apps exist assert("f/apps/test_app.app/app.yaml" in files, "App metadata should exist"); // Check raw apps exist assert("f/apps/test_raw_app.raw_app/raw_app.yaml" in files, "Raw app metadata should exist"); assert("f/apps/test_raw_app.raw_app/index.html" in files, "Raw app HTML should exist"); assert("f/apps/test_raw_app.raw_app/index.js" in files, "Raw app JS should exist"); // Check resources exist assert("f/resources/postgres_db.resource.yaml" in files, "PostgreSQL resource should exist"); assert("f/resources/api_config.resource.yaml" in files, "API config resource should exist"); // Check variables exist assert("f/resources/config_value.variable.yaml" in files, "Config variable should exist"); assert("f/resources/secret_key.variable.yaml" in files, "Secret variable should exist"); // Check folder metadata assert("f/folder.meta.yaml" in files, "Folder metadata should exist"); } finally { await cleanupTempDir(tempDir); } }); Deno.test("Mock remote zip can be created and read", async () => { const items = { "test_script.py": 'def main():\n return "hello"', "test_script.script.json": '{"summary":"test","schema":{}}', "test_flow.flow.json": '{"summary":"flow","value":{"modules":[]}}', "test_app.app.json": '{"summary":"app","value":{}}', }; const zip = await createMockRemoteZip(items); // Verify files exist in zip const scriptContent = await zip.file("test_script.py")?.async("text"); assertEquals(scriptContent, 'def main():\n return "hello"'); const flowContent = await zip.file("test_flow.flow.json")?.async("text"); assertStringIncludes(flowContent!, '"summary":"flow"'); }); Deno.test("readDirRecursive reads all files correctly", async () => { const tempDir = await createTempDir(); try { // Create a simple structure await ensureDir(path.join(tempDir, "subdir")); await Deno.writeTextFile(path.join(tempDir, "file1.txt"), "content1"); await Deno.writeTextFile(path.join(tempDir, "subdir", "file2.txt"), "content2"); const files = await readDirRecursive(tempDir); assertEquals(files["file1.txt"], "content1"); assertEquals(files["subdir/file2.txt"], "content2"); assertEquals(Object.keys(files).length, 2); } finally { await cleanupTempDir(tempDir); } }); // ============================================================================= // Integration Tests (require Windmill server at localhost:8000) // ============================================================================= import { addWorkspace } from "../workspace.ts"; // Configuration for local Windmill server const WINDMILL_BASE_URL = Deno.env.get("WINDMILL_BASE_URL") || "http://localhost:8000"; const WINDMILL_EMAIL = Deno.env.get("WINDMILL_EMAIL") || "admin@windmill.dev"; const WINDMILL_PASSWORD = Deno.env.get("WINDMILL_PASSWORD") || "changeme"; const WINDMILL_WORKSPACE = Deno.env.get("WINDMILL_WORKSPACE") || "admins"; // Set to true to run integration tests (requires Windmill server) const RUN_INTEGRATION_TESTS = Deno.env.get("RUN_INTEGRATION_TESTS") === "true"; /** * Get an authentication token from the Windmill server */ async function getAuthToken(): Promise { let response: Response; try { response = await fetch(`${WINDMILL_BASE_URL}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: WINDMILL_EMAIL, password: WINDMILL_PASSWORD }), }); } catch (e) { throw new Error( `Failed to connect to Windmill server at ${WINDMILL_BASE_URL}. ` + `Make sure the server is running. Error: ${e instanceof Error ? e.message : e}` ); } const responseText = await response.text(); if (!response.ok) { throw new Error( `Failed to authenticate with ${WINDMILL_EMAIL} at ${WINDMILL_BASE_URL}. ` + `Status: ${response.status}. Response: ${responseText}` ); } // The auth endpoint returns the token as plain text return responseText; } /** * Check if the Windmill server is available */ async function isServerAvailable(): Promise { try { const response = await fetch(`${WINDMILL_BASE_URL}/api/version`, { signal: AbortSignal.timeout(5000), }); // Consume the response body to avoid resource leaks await response.text(); return response.ok; } catch { return false; } } /** * Run a CLI command and return the result */ async function runCLICommand( args: string[], cwd: string, configDir: string, ): Promise<{ code: number; stdout: string; stderr: string }> { const cmd = new Deno.Command("deno", { args: [ "run", "-A", "--no-check", path.join(Deno.cwd(), "src/main.ts"), "--config-dir", configDir, ...args, ], cwd, env: { ...Deno.env.toObject(), SKIP_DENO_DEPRECATION_WARNING: "true", }, stdout: "piped", stderr: "piped", }); const output = await cmd.output(); return { code: output.code, stdout: new TextDecoder().decode(output.stdout), stderr: new TextDecoder().decode(output.stderr), }; } /** * Set up a test environment with workspace configured */ async function setupTestEnvironment(): Promise<{ tempDir: string; configDir: string; token: string; cleanup: () => Promise; }> { const tempDir = await Deno.makeTempDir({ prefix: "wmill_sync_test_" }); const configDir = await Deno.makeTempDir({ prefix: "wmill_config_" }); const token = await getAuthToken(); // Configure workspace - addWorkspace will create configDir/windmill/ structure const workspace = { remote: WINDMILL_BASE_URL + "/", workspaceId: WINDMILL_WORKSPACE, name: "test_workspace", token, }; await addWorkspace(workspace, { force: true, configDir }); // Set active workspace - needs to go in configDir/windmill/activeWorkspace const windmillConfigDir = path.join(configDir, "windmill"); await ensureDir(windmillConfigDir); await Deno.writeTextFile(path.join(windmillConfigDir, "activeWorkspace"), "test_workspace"); return { tempDir, configDir, token, cleanup: async () => { await cleanupTempDir(tempDir); await cleanupTempDir(configDir); }, }; } Deno.test({ name: "Integration: Pull creates correct local structure", ignore: !RUN_INTEGRATION_TESTS, async fn() { if (!await isServerAvailable()) { console.log(`Skipping: Windmill server not available at ${WINDMILL_BASE_URL}`); return; } const { tempDir, configDir, cleanup } = await setupTestEnvironment(); try { // Create wmill.yaml await Deno.writeTextFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" excludes: [] `, ); // Run sync pull const result = await runCLICommand(["sync", "pull", "--yes"], tempDir, configDir); assertEquals( result.code, 0, `Pull should succeed.\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, ); // Verify files were created const files = await readDirRecursive(tempDir); const hasYamlFiles = Object.keys(files).some((f) => f.endsWith(".yaml") && f !== "wmill.yaml"); assert(hasYamlFiles || Object.keys(files).length > 1, "Should have pulled files from server"); } finally { await cleanup(); } }, }); Deno.test({ name: "Integration: Push uploads local changes correctly", ignore: !RUN_INTEGRATION_TESTS, async fn() { if (!await isServerAvailable()) { console.log(`Skipping: Windmill server not available at ${WINDMILL_BASE_URL}`); return; } const { tempDir, configDir, cleanup } = await setupTestEnvironment(); try { // Create wmill.yaml await Deno.writeTextFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" excludes: [] `, ); // Create a test script locally with a unique name // Path must have at least 2 segments after prefix (e.g., f/folder/name) const uniqueId = Date.now(); await ensureDir(`${tempDir}/f/test`); const script = createScriptFixture(`f/test/push_script_${uniqueId}`, "deno"); await Deno.writeTextFile(`${tempDir}/${script.contentFile.path}`, script.contentFile.content); await Deno.writeTextFile(`${tempDir}/${script.metadataFile.path}`, script.metadataFile.content); // Run sync push with dry-run first (only push our test script, not everything) const dryRunResult = await runCLICommand( ["sync", "push", "--dry-run", "--includes", `f/test/push_script_${uniqueId}**`], tempDir, configDir, ); assertEquals( dryRunResult.code, 0, `Dry run should succeed.\nstdout: ${dryRunResult.stdout}\nstderr: ${dryRunResult.stderr}`, ); assertStringIncludes( dryRunResult.stdout + dryRunResult.stderr, `push_script_${uniqueId}`, "Should detect the new script", ); // Run actual push (only push our test script) const pushResult = await runCLICommand( ["sync", "push", "--yes", "--includes", `f/test/push_script_${uniqueId}**`], tempDir, configDir, ); assertEquals( pushResult.code, 0, `Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`, ); } finally { await cleanup(); } }, }); Deno.test({ name: "Integration: Pull then Push is idempotent", ignore: !RUN_INTEGRATION_TESTS, async fn() { if (!await isServerAvailable()) { console.log(`Skipping: Windmill server not available at ${WINDMILL_BASE_URL}`); return; } const { tempDir, configDir, cleanup } = await setupTestEnvironment(); try { // Create wmill.yaml await Deno.writeTextFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" excludes: [] `, ); // Pull from remote const pullResult = await runCLICommand(["sync", "pull", "--yes"], tempDir, configDir); assertEquals( pullResult.code, 0, `Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`, ); // Push back without changes (should be no-op) const pushResult = await runCLICommand(["sync", "push", "--dry-run"], tempDir, configDir); assertEquals(pushResult.code, 0, `Push dry-run should succeed: ${pushResult.stderr}`); // Should report 0 changes (check both stdout and stderr) const output = (pushResult.stdout + pushResult.stderr).toLowerCase(); assert( output.includes("0 change") || output.includes("no change") || output.includes("nothing"), `Should have no changes after pull without modifications. Output: ${output}`, ); } finally { await cleanup(); } }, }); Deno.test({ name: "Integration: Include/exclude filters work correctly", ignore: !RUN_INTEGRATION_TESTS, async fn() { if (!await isServerAvailable()) { console.log(`Skipping: Windmill server not available at ${WINDMILL_BASE_URL}`); return; } const { tempDir, configDir, cleanup } = await setupTestEnvironment(); try { // Create wmill.yaml with restrictive filters await Deno.writeTextFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "f/scripts/**" excludes: - "**/*.test.ts" skipVariables: true skipResources: true `, ); // Run sync pull const result = await runCLICommand(["sync", "pull", "--yes"], tempDir, configDir); assertEquals( result.code, 0, `Pull should succeed.\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, ); // Verify only scripts in f/scripts/ were pulled (if any exist) const files = await readDirRecursive(tempDir); // Should not have variables or resources const hasVariables = Object.keys(files).some((f) => f.includes(".variable.")); const hasResources = Object.keys(files).some((f) => f.includes(".resource.")); assert(!hasVariables, "Should not have pulled variables (skipVariables: true)"); assert(!hasResources, "Should not have pulled resources (skipResources: true)"); } finally { await cleanup(); } }, }); Deno.test({ name: "Integration: Flow folder structure is created correctly", ignore: !RUN_INTEGRATION_TESTS, async fn() { if (!await isServerAvailable()) { console.log(`Skipping: Windmill server not available at ${WINDMILL_BASE_URL}`); return; } const { tempDir, configDir, cleanup } = await setupTestEnvironment(); try { // Create wmill.yaml await Deno.writeTextFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" excludes: [] `, ); // Create a local flow with unique name // Path must have at least 2 segments after prefix (e.g., f/folder/name) const uniqueId = Date.now(); const flowName = `f/test/flow_${uniqueId}`; const flowFixture = createFlowFixture(flowName); await ensureDir(`${tempDir}/f/test/flow_${uniqueId}${getFolderSuffix("flow")}`); for (const file of Object.values(flowFixture)) { await Deno.writeTextFile(`${tempDir}/${file.path}`, file.content); } // Push the flow (only push our test flow, not everything) // Pattern needs to match the .flow folder, so use * to match the suffix const pushResult = await runCLICommand( ["sync", "push", "--yes", "--includes", `f/test/flow_${uniqueId}*/**`], tempDir, configDir, ); assertEquals( pushResult.code, 0, `Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`, ); // Pull back and verify structure is preserved const tempDir2 = await Deno.makeTempDir({ prefix: "wmill_flow_verify_" }); try { // Use template literal properly for the includes pattern const wmillConfig = `defaultTs: bun includes: - "f/test/flow_${uniqueId}*/**" excludes: [] `; await Deno.writeTextFile(`${tempDir2}/wmill.yaml`, wmillConfig); const pullResult = await runCLICommand(["sync", "pull", "--yes"], tempDir2, configDir); assertEquals( pullResult.code, 0, `Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`, ); // Verify flow folder structure const files = await readDirRecursive(tempDir2); const allFiles = Object.keys(files); const flowFiles = allFiles.filter((f) => f.includes(`flow_${uniqueId}`)); assert(flowFiles.length > 0, `Should have pulled the flow. Files found: ${allFiles.join(", ")}`); assert( flowFiles.some((f) => f.includes(".flow/")), "Flow should be in a .flow folder", ); } finally { await cleanupTempDir(tempDir2); } } finally { await cleanup(); } }, }); Deno.test({ name: "Integration: Raw app folder structure is handled correctly", ignore: !RUN_INTEGRATION_TESTS, async fn() { if (!await isServerAvailable()) { console.log(`Skipping: Windmill server not available at ${WINDMILL_BASE_URL}`); return; } const { tempDir, configDir, cleanup } = await setupTestEnvironment(); try { // Create wmill.yaml await Deno.writeTextFile( `${tempDir}/wmill.yaml`, `defaultTs: bun includes: - "**" excludes: [] `, ); // Create a local raw app with unique name // Path must have at least 2 segments after prefix (e.g., f/folder/name) const uniqueId = Date.now(); const rawAppFixture = createRawAppFixture(`f/test/raw_app_${uniqueId}`); await ensureDir(`${tempDir}/f/test/raw_app_${uniqueId}${getFolderSuffix("raw_app")}`); for (const file of Object.values(rawAppFixture)) { await Deno.writeTextFile(`${tempDir}/${file.path}`, file.content); } // Push the raw app (only push our test raw app, not everything) const pushResult = await runCLICommand( ["sync", "push", "--yes", "--includes", `f/test/raw_app_${uniqueId}**`], tempDir, configDir, ); // Note: This may fail if raw apps require specific validation // The test verifies the CLI handles the folder structure correctly if (pushResult.code === 0) { assertStringIncludes( pushResult.stdout + pushResult.stderr, "", "Push completed", ); } } finally { await cleanup(); } }, });