* 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>
159 lines
5.3 KiB
TypeScript
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);
|
|
});
|
|
});
|