/** * Mixed Case Paths Sync Tests * * Tests sync pull/push operations with folder paths containing capital letters. * This is critical for Windows compatibility testing since Windows has * case-insensitive file systems which can cause issues with paths like: * f/MyFolder/MyScript * * The test verifies that: * 1. Resources with mixed-case paths can be pulled correctly * 2. Modifications to those files can be pushed back * 3. The modifications are correctly applied on the server */ import { assertEquals, 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 { withTestBackend } from "./test_backend.ts"; import { addWorkspace } from "../workspace.ts"; import { parseJsonFromCLIOutput } from "./test_config_helpers.ts"; // ============================================================================= // HELPER FUNCTIONS // ============================================================================= async function setupWorkspaceProfile(backend: any): Promise { const testWorkspace = { remote: backend.baseUrl, workspaceId: backend.workspace, name: "localhost_test", token: backend.token, }; await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir }); } async function createFolder(backend: any, name: string): Promise { const response = await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name }), }); if (!response.ok) { const error = await response.text(); if (!error.includes("already exists")) { throw new Error(`Failed to create folder ${name}: ${error}`); } } else { await response.text(); } } async function createScript( backend: any, scriptPath: string, content: string, summary: string = "Test script" ): Promise { const script = { path: scriptPath, summary, description: `Script at ${scriptPath}`, content, language: "bun", is_template: false, kind: "script", schema: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: {}, required: [], }, }; const response = await backend.apiRequest!(`/api/w/${backend.workspace}/scripts/create`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(script), }); if (!response.ok) { const error = await response.text(); throw new Error(`Failed to create script ${scriptPath}: ${error}`); } await response.text(); } async function createFlow( backend: any, flowPath: string, inlineScriptContent: string, summary: string = "Test flow" ): Promise { const flow = { path: flowPath, summary, description: `Flow at ${flowPath}`, value: { modules: [ { id: "a", value: { type: "rawscript", content: inlineScriptContent, language: "bun", input_transforms: {}, }, }, ], }, schema: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: {}, required: [], }, }; const response = await backend.apiRequest!(`/api/w/${backend.workspace}/flows/create`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(flow), }); if (!response.ok) { const error = await response.text(); throw new Error(`Failed to create flow ${flowPath}: ${error}`); } await response.text(); } async function createApp( backend: any, appPath: string, summary: string = "Test app" ): Promise { const app = { path: appPath, summary, policy: { execution_mode: "viewer", }, value: { grid: [], hiddenInlineScripts: [], css: {}, norefreshbar: false, }, }; const response = await backend.apiRequest!(`/api/w/${backend.workspace}/apps/create`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(app), }); if (!response.ok) { const error = await response.text(); throw new Error(`Failed to create app ${appPath}: ${error}`); } await response.text(); } async function createVariable( backend: any, varPath: string, value: string, description: string = "Test variable" ): Promise { const response = await backend.apiRequest!(`/api/w/${backend.workspace}/variables/create`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: varPath, value, is_secret: false, description, }), }); if (!response.ok) { const error = await response.text(); throw new Error(`Failed to create variable ${varPath}: ${error}`); } await response.text(); } async function createResource( backend: any, resourcePath: string, resourceType: string, value: Record, description: string = "Test resource" ): Promise { const response = await backend.apiRequest!(`/api/w/${backend.workspace}/resources/create`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: resourcePath, resource_type: resourceType, value, description, }), }); if (!response.ok) { const error = await response.text(); throw new Error(`Failed to create resource ${resourcePath}: ${error}`); } await response.text(); } async function getScript(backend: any, scriptPath: string): Promise { const response = await backend.apiRequest!(`/api/w/${backend.workspace}/scripts/get/p/${encodeURIComponent(scriptPath)}`); if (!response.ok) { throw new Error(`Failed to get script ${scriptPath}: ${response.status}`); } return response.json(); } async function getFlow(backend: any, flowPath: string): Promise { const response = await backend.apiRequest!(`/api/w/${backend.workspace}/flows/get/${encodeURIComponent(flowPath)}`); if (!response.ok) { throw new Error(`Failed to get flow ${flowPath}: ${response.status}`); } return response.json(); } async function getApp(backend: any, appPath: string): Promise { const response = await backend.apiRequest!(`/api/w/${backend.workspace}/apps/get/p/${encodeURIComponent(appPath)}`); if (!response.ok) { throw new Error(`Failed to get app ${appPath}: ${response.status}`); } return response.json(); } async function getVariable(backend: any, varPath: string): Promise { const response = await backend.apiRequest!(`/api/w/${backend.workspace}/variables/get/${encodeURIComponent(varPath)}`); if (!response.ok) { throw new Error(`Failed to get variable ${varPath}: ${response.status}`); } return response.json(); } /** * Helper to verify that a subsequent pull detects no changes (idempotency check) */ async function verifyNoDiffOnPull(backend: any, tempDir: string): Promise { const pullResult = await backend.runCLICommand( ["sync", "pull", "--yes", "--dry-run", "--json-output"], tempDir ); assertEquals(pullResult.code, 0, `Pull for diff check should succeed: ${pullResult.stderr}`); const output = parseJsonFromCLIOutput(pullResult.stdout); const changes = output.changes || []; assertEquals( changes.length, 0, `Should have no changes after push, but found: ${JSON.stringify(changes.map((c: any) => c.path))}` ); } // ============================================================================= // TESTS // ============================================================================= Deno.test({ name: "Mixed Case Paths: pull and push script with capitalized folder", sanitizeResources: false, sanitizeOps: false, fn: async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); // Create folder with mixed case await createFolder(backend, "MyFolder"); // Create a script with mixed-case path const scriptPath = "f/MyFolder/MyScript"; const originalContent = `export async function main() { return "original content"; }`; await createScript(backend, scriptPath, originalContent, "My Test Script"); // Create wmill.yaml await Deno.writeTextFile( path.join(tempDir, "wmill.yaml"), `defaultTs: bun includes: - "**" excludes: [] ` ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`); // Verify file exists with correct path (normalized for comparison) const expectedScriptPath = path.join(tempDir, "f", "MyFolder", "MyScript.ts"); const scriptExists = await Deno.stat(expectedScriptPath).then(() => true).catch(() => false); assert(scriptExists, `Script file should exist at ${expectedScriptPath}`); // Read and verify content const pulledContent = await Deno.readTextFile(expectedScriptPath); assert(pulledContent.includes("original content"), "Pulled content should match original"); // Modify the script const modifiedContent = `export async function main() { return "modified content from test"; }`; await Deno.writeTextFile(expectedScriptPath, modifiedContent); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`); // Verify modification on server const updatedScript = await getScript(backend, scriptPath); assert( updatedScript.content.includes("modified content from test"), `Server should have modified content. Got: ${updatedScript.content}` ); // Verify no diff on subsequent pull (idempotency) await verifyNoDiffOnPull(backend, tempDir); }); }, }); Deno.test({ name: "Mixed Case Paths: pull and push flow with capitalized folder", sanitizeResources: false, sanitizeOps: false, fn: async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); // Create folder with mixed case await createFolder(backend, "MyFlows"); // Create a flow with mixed-case path const flowPath = "f/MyFlows/DataProcessor"; const originalContent = `export async function main() { return "original flow step"; }`; await createFlow(backend, flowPath, originalContent, "Data Processor Flow"); // Create wmill.yaml await Deno.writeTextFile( path.join(tempDir, "wmill.yaml"), `defaultTs: bun includes: - "**" excludes: [] ` ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`); // Verify flow directory exists const flowDir = path.join(tempDir, "f", "MyFlows", "DataProcessor.flow"); const flowDirExists = await Deno.stat(flowDir).then(s => s.isDirectory).catch(() => false); assert(flowDirExists, `Flow directory should exist at ${flowDir}`); // Modify the flow metadata (summary) instead of inline script const flowMetadataPath = path.join(flowDir, "flow.yaml"); const flowMetadataExists = await Deno.stat(flowMetadataPath).then(() => true).catch(() => false); assert(flowMetadataExists, `Flow metadata should exist at ${flowMetadataPath}`); const flowMetadata = await Deno.readTextFile(flowMetadataPath); const modifiedMetadata = flowMetadata.replace( /summary:.*$/m, 'summary: "Modified Data Processor Flow from test"' ); await Deno.writeTextFile(flowMetadataPath, modifiedMetadata); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`); // Verify modification on server const updatedFlow = await getFlow(backend, flowPath); assertEquals( updatedFlow.summary, "Modified Data Processor Flow from test", `Server should have modified flow summary. Got: ${updatedFlow.summary}` ); // Verify no diff on subsequent pull (idempotency) await verifyNoDiffOnPull(backend, tempDir); }); }, }); Deno.test({ name: "Mixed Case Paths: pull and push app with capitalized folder", sanitizeResources: false, sanitizeOps: false, fn: async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); // Create folder with mixed case await createFolder(backend, "MyApps"); // Create an app with mixed-case path const appPath = "f/MyApps/Dashboard"; await createApp(backend, appPath, "My Dashboard App"); // Create wmill.yaml await Deno.writeTextFile( path.join(tempDir, "wmill.yaml"), `defaultTs: bun includes: - "**" excludes: [] ` ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`); // Verify app directory exists const appDir = path.join(tempDir, "f", "MyApps", "Dashboard.app"); const appDirExists = await Deno.stat(appDir).then(s => s.isDirectory).catch(() => false); assert(appDirExists, `App directory should exist at ${appDir}`); // Modify the app metadata const appMetadataPath = path.join(appDir, "app.yaml"); const appMetadata = await Deno.readTextFile(appMetadataPath); const modifiedMetadata = appMetadata.replace( /summary:.*$/m, 'summary: "Modified Dashboard App from test"' ); await Deno.writeTextFile(appMetadataPath, modifiedMetadata); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`); // Verify modification on server const updatedApp = await getApp(backend, appPath); assertEquals( updatedApp.summary, "Modified Dashboard App from test", `Server should have modified app summary. Got: ${updatedApp.summary}` ); // Verify no diff on subsequent pull (idempotency) await verifyNoDiffOnPull(backend, tempDir); }); }, }); Deno.test({ name: "Mixed Case Paths: pull and push variable with capitalized folder", sanitizeResources: false, sanitizeOps: false, fn: async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); // Create folder with mixed case await createFolder(backend, "MyVars"); // Create a variable with mixed-case path const varPath = "f/MyVars/ApiKey"; await createVariable(backend, varPath, "original-api-key-value", "API Key Variable"); // Create wmill.yaml await Deno.writeTextFile( path.join(tempDir, "wmill.yaml"), `defaultTs: bun includes: - "**" excludes: [] ` ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`); // Verify variable file exists const varFilePath = path.join(tempDir, "f", "MyVars", "ApiKey.variable.yaml"); const varExists = await Deno.stat(varFilePath).then(() => true).catch(() => false); assert(varExists, `Variable file should exist at ${varFilePath}`); // Modify the variable const varContent = await Deno.readTextFile(varFilePath); const modifiedVarContent = varContent.replace( /value:.*$/m, 'value: "modified-api-key-from-test"' ); await Deno.writeTextFile(varFilePath, modifiedVarContent); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`); // Verify modification on server const updatedVar = await getVariable(backend, varPath); assertEquals( updatedVar.value, "modified-api-key-from-test", `Server should have modified variable value. Got: ${updatedVar.value}` ); // Verify no diff on subsequent pull (idempotency) await verifyNoDiffOnPull(backend, tempDir); }); }, }); Deno.test({ name: "Mixed Case Paths: deeply nested capitalized folders", sanitizeResources: false, sanitizeOps: false, fn: async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); // Create nested folders with mixed case // Note: Windmill folders are flat, so we create a folder with slashes in the name // The actual nesting is in the path structure await createFolder(backend, "MyProject"); // Create a script with deeply nested mixed-case path const scriptPath = "f/MyProject/SubFolder_A"; const originalContent = `export async function main() { return "deeply nested original"; }`; await createScript(backend, scriptPath, originalContent, "Nested Script"); // Create wmill.yaml await Deno.writeTextFile( path.join(tempDir, "wmill.yaml"), `defaultTs: bun includes: - "**" excludes: [] ` ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`); // Verify file exists const scriptFilePath = path.join(tempDir, "f", "MyProject", "SubFolder_A.ts"); const scriptExists = await Deno.stat(scriptFilePath).then(() => true).catch(() => false); assert(scriptExists, `Nested script should exist at ${scriptFilePath}`); // Modify const modifiedContent = `export async function main() { return "deeply nested modified from test"; }`; await Deno.writeTextFile(scriptFilePath, modifiedContent); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`); // Verify on server const updatedScript = await getScript(backend, scriptPath); assert( updatedScript.content.includes("deeply nested modified from test"), `Server should have modified nested content` ); // Verify no diff on subsequent pull (idempotency) await verifyNoDiffOnPull(backend, tempDir); }); }, }); Deno.test({ name: "Mixed Case Paths: multiple resources in same capitalized folder", sanitizeResources: false, sanitizeOps: false, fn: async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); // Create folder with mixed case await createFolder(backend, "SharedFolder"); // Create multiple resources in the same folder await createScript( backend, "f/SharedFolder/ScriptOne", 'export async function main() { return "script one original"; }', "Script One" ); await createScript( backend, "f/SharedFolder/ScriptTwo", 'export async function main() { return "script two original"; }', "Script Two" ); await createVariable(backend, "f/SharedFolder/VarOne", "var-one-original"); await createResource(backend, "f/SharedFolder/ResourceOne", "any", { key: "original" }); // Create wmill.yaml await Deno.writeTextFile( path.join(tempDir, "wmill.yaml"), `defaultTs: bun includes: - "**" excludes: [] ` ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`); // Verify all files exist const folderPath = path.join(tempDir, "f", "SharedFolder"); const script1Exists = await Deno.stat(path.join(folderPath, "ScriptOne.ts")).then(() => true).catch(() => false); const script2Exists = await Deno.stat(path.join(folderPath, "ScriptTwo.ts")).then(() => true).catch(() => false); const var1Exists = await Deno.stat(path.join(folderPath, "VarOne.variable.yaml")).then(() => true).catch(() => false); const res1Exists = await Deno.stat(path.join(folderPath, "ResourceOne.resource.yaml")).then(() => true).catch(() => false); assert(script1Exists, "ScriptOne should exist"); assert(script2Exists, "ScriptTwo should exist"); assert(var1Exists, "VarOne should exist"); assert(res1Exists, "ResourceOne should exist"); // Modify script one await Deno.writeTextFile( path.join(folderPath, "ScriptOne.ts"), 'export async function main() { return "script one MODIFIED"; }' ); // Modify script two await Deno.writeTextFile( path.join(folderPath, "ScriptTwo.ts"), 'export async function main() { return "script two MODIFIED"; }' ); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`); // Verify modifications on server const script1 = await getScript(backend, "f/SharedFolder/ScriptOne"); const script2 = await getScript(backend, "f/SharedFolder/ScriptTwo"); assert(script1.content.includes("script one MODIFIED"), "Script one should be modified on server"); assert(script2.content.includes("script two MODIFIED"), "Script two should be modified on server"); // Verify no diff on subsequent pull (idempotency) await verifyNoDiffOnPull(backend, tempDir); }); }, }); Deno.test({ name: "Mixed Case Paths: CamelCase folder names with numbers", sanitizeResources: false, sanitizeOps: false, fn: async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); // Create folder with CamelCase and numbers await createFolder(backend, "Project2024"); // Create script const scriptPath = "f/Project2024/DataHandler_V2"; await createScript( backend, scriptPath, 'export async function main() { return "handler v2 original"; }', "Data Handler V2" ); // Create wmill.yaml await Deno.writeTextFile( path.join(tempDir, "wmill.yaml"), `defaultTs: bun includes: - "**" excludes: [] ` ); // Pull const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`); // Verify file exists const scriptFilePath = path.join(tempDir, "f", "Project2024", "DataHandler_V2.ts"); const scriptExists = await Deno.stat(scriptFilePath).then(() => true).catch(() => false); assert(scriptExists, `Script should exist at ${scriptFilePath}`); // Modify await Deno.writeTextFile( scriptFilePath, 'export async function main() { return "handler v2 MODIFIED"; }' ); // Push const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`); // Verify on server const updatedScript = await getScript(backend, scriptPath); assert(updatedScript.content.includes("handler v2 MODIFIED"), "Server should have modified content"); // Verify no diff on subsequent pull (idempotency) await verifyNoDiffOnPull(backend, tempDir); }); }, });