Files
windmill/cli/test/app_inline_script_delete.test.ts
Ruben Fiszel 943fe9c6cc fix: handle inline script deletion in sync push + flow new nonDottedPaths (#8553)
* fix: handle inline script file deletions in app/flow folders during sync push

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add regression test for app inline script deletion during sync push

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: flow new respects nonDottedPaths setting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add flow new nonDottedPaths test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: separate stat from pushObj in delete handler to avoid masking errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:54:45 +00:00

159 lines
5.3 KiB
TypeScript

import { expect, test } from "bun:test";
import { withTestBackend } from "./test_backend.ts";
import { addWorkspace } from "../workspace.ts";
import * as path from "node:path";
import { writeFile, readdir, stat, rm } from "node:fs/promises";
import { getFolderSuffix, getMetadataFileName } from "../src/utils/resource_folders.ts";
// =============================================================================
// APP INLINE SCRIPT DELETION TESTS
// Regression tests for: deleting inline script files within .app/ folders
// during sync push should re-push the app, not crash with TypeError.
// =============================================================================
async function fileExists(filePath: string): Promise<boolean> {
try {
await stat(filePath);
return true;
} catch {
return false;
}
}
test("App: delete inline script file and push does not crash", async () => {
await withTestBackend(async (backend, tempDir) => {
const testWorkspace = {
remote: backend.baseUrl,
workspaceId: backend.workspace,
name: "app_inline_delete_test",
token: backend.token,
};
await addWorkspace(testWorkspace, {
force: true,
configDir: backend.testConfigDir,
});
await writeFile(
`${tempDir}/wmill.yaml`,
`defaultTs: bun\nincludes:\n - "**"\nexcludes: []`,
"utf-8"
);
// Create an app with an inline script via the API
const appPath = "f/test/inline_delete_app";
const inlineContent = `export async function main() {\n return "hello";\n}`;
// Create the folder first
await backend.apiRequest!(
`/api/w/${backend.workspace}/folders/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "test" }),
}
).then((r) => r.text());
await backend.createAppWithInlineScript!(appPath, inlineContent, "bun");
// =========================================================================
// STEP 1: Pull — get the app folder with inline script files
// =========================================================================
const pullResult1 = await backend.runCLICommand(
["sync", "pull", "--yes"],
tempDir,
"app_inline_delete_test"
);
expect(pullResult1.code).toEqual(0);
// Find the app folder and its inline script files
const appSuffix = getFolderSuffix("app");
const appDir = path.join(tempDir, appPath + appSuffix);
expect(await fileExists(appDir)).toBeTruthy();
// List files in the app folder to find inline script files
const appFiles = await readdir(appDir);
const inlineScriptFiles = appFiles.filter(
(f) => f.endsWith(".ts") || f.endsWith(".js")
);
expect(inlineScriptFiles.length).toBeGreaterThan(0);
const inlineScriptPath = path.join(appDir, inlineScriptFiles[0]);
expect(await fileExists(inlineScriptPath)).toBeTruthy();
const metadataFile = getMetadataFileName("app", "yaml");
const appYamlPath = path.join(appDir, metadataFile);
// =========================================================================
// STEP 2: Remove the inline script from app.yaml and delete the .ts file
// =========================================================================
// Replace the inline script with a static text component (no inline scripts)
const updatedAppYaml = `summary: Test app with inline script
value:
type: app
grid:
- id: text1
data:
type: textcomponent
componentInput:
type: static
value: hello world
hiddenInlineScripts: []
css: {}
norefreshbar: false
policy:
on_behalf_of: null
on_behalf_of_email: null
triggerables: {}
execution_mode: viewer
`;
await writeFile(appYamlPath, updatedAppYaml, "utf-8");
// Delete the inline script file
await rm(inlineScriptPath);
expect(await fileExists(inlineScriptPath)).toBeFalsy();
// Also delete any lock files for the inline script
for (const f of appFiles) {
if (f.endsWith(".lock")) {
await rm(path.join(appDir, f));
}
}
// =========================================================================
// STEP 3: Push — should succeed, NOT crash with TypeError
// =========================================================================
const pushResult = await backend.runCLICommand(
["sync", "push", "--yes"],
tempDir,
"app_inline_delete_test"
);
// The critical assertion: push should not crash
expect(pushResult.code).toEqual(0);
// =========================================================================
// STEP 4: Verify by pulling again — inline script should be gone
// =========================================================================
await rm(appDir, { recursive: true });
const pullResult2 = await backend.runCLICommand(
["sync", "pull", "--yes"],
tempDir,
"app_inline_delete_test"
);
expect(pullResult2.code).toEqual(0);
// App should still exist
expect(await fileExists(appDir)).toBeTruthy();
// But no inline script files should be present
const finalFiles = await readdir(appDir);
const finalInlineScripts = finalFiles.filter(
(f) =>
(f.endsWith(".ts") || f.endsWith(".js")) &&
f.includes("inline_script")
);
expect(finalInlineScripts.length).toEqual(0);
});
});