mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 00:02:03 +00:00
5d5b853f70
* fix(cli): only preserve case for raw-app runnableIds, not app/flow summaries PR #8940 stopped lowercasing in sanitizeForFilesystem to fix #8939, where a raw-app runnableId like CamelCaseTSRunnable produced a CamelCase YAML metadata file but a lowercased code file, making them desync and register as duplicate runnables on push. That fix overshot. sanitizeForFilesystem is also reached by newPathAssigner, which serves normal apps and flows where the input is the script's human summary ("Get Users Data") rather than an identifier. There the on-disk filename is the only artifact — there's no companion YAML to keep in sync — so lowercasing was the right behavior. Removing it changed both the on-disk filename and the !inline reference in app.yaml / flow.yaml from get_users_data.inline_script.ts to Get_Users_Data.inline_script.ts on the next pull, surfacing as unwanted case churn for users updating to 1.693.x. Add a preserveCase option to sanitizeForFilesystem (default false → lowercase). newRawAppPathAssigner opts in; newPathAssigner stays on the default. Update unit tests accordingly and add an end-to-end raw-app round-trip in raw_app_sync.test.ts that pushes a CamelCase backend runnable, pulls it back, and asserts both YAML and code file preserve case with no lowercase orphan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(cli): use readdir for exact-case orphan check on Windows The CamelCase round-trip test used fileExists("camelcasetsrunnable.ts") to assert no lowercase orphan was produced, which false-positives on Windows since the filesystem is case-insensitive and resolves the lookup to the existing CamelCaseTSRunnable.ts. Switch to readdir + toContain so the exact on-disk casing is compared identically on Linux and Windows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
596 lines
21 KiB
TypeScript
596 lines
21 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, readFile, stat, rm, mkdir, readdir } from "node:fs/promises";
|
|
|
|
// =============================================================================
|
|
// RAW APP SYNC TESTS
|
|
// Tests for raw app sync pull/push workflow
|
|
// =============================================================================
|
|
|
|
// Raw app file contents
|
|
const APP_TSX = `import React, { useState } from 'react'
|
|
import { backend } from './wmill'
|
|
import './index.css'
|
|
|
|
const App = () => {
|
|
const [value, setValue] = useState(undefined as string | undefined)
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
async function runA() {
|
|
setLoading(true)
|
|
try {
|
|
setValue(await backend.a({ x: 42 }))
|
|
} catch (e) {
|
|
console.error()
|
|
}
|
|
setLoading(false)
|
|
}
|
|
|
|
return <div style={{ width: "100%" }}>
|
|
<h1>hello world</h1>
|
|
|
|
<button style={{ marginTop: "2px" }} onClick={runA}>Run 'a'</button>
|
|
|
|
<div style={{ marginTop: "20px", width: '250px' }} className='myclass'>
|
|
{loading ? 'Loading ...' : value ?? 'Click button to see value here'}
|
|
</div>
|
|
</div>;
|
|
};
|
|
|
|
export default App;
|
|
`;
|
|
|
|
const INDEX_CSS = `.myclass {
|
|
border: 1px solid gray;
|
|
padding: 2px;
|
|
}`;
|
|
|
|
const INDEX_TSX = `
|
|
import React from 'react'
|
|
|
|
import { createRoot } from 'react-dom/client'
|
|
import App from './App'
|
|
|
|
const root = createRoot(document.getElementById('root')!);
|
|
root.render(<App/>);
|
|
`;
|
|
|
|
const PACKAGE_JSON = `{
|
|
"dependencies": {
|
|
"react": "19.0.0",
|
|
"react-dom": "19.0.0",
|
|
"windmill-client": "^1"
|
|
},
|
|
"devDependencies": {
|
|
"@types/react-dom": "^19.0.0",
|
|
"@types/react": "^19.0.0"
|
|
}
|
|
}`;
|
|
|
|
const INLINE_SCRIPT_A = `// import * as wmill from "windmill-client"
|
|
|
|
export async function main(x: string) {
|
|
return x
|
|
}
|
|
`;
|
|
|
|
const INLINE_SCRIPT_A_LOCK = `{
|
|
"dependencies": {}
|
|
}
|
|
//bun.lock
|
|
<empty>`;
|
|
|
|
// raw_app.yaml metadata file
|
|
const RAW_APP_YAML = `summary: Test Raw App
|
|
policy:
|
|
execution_mode: publisher
|
|
triggerables: {}
|
|
triggerables_v2: {}
|
|
`;
|
|
|
|
async function fileExists(filePath: string): Promise<boolean> {
|
|
try {
|
|
await stat(filePath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function readFileContent(filePath: string): Promise<string> {
|
|
return await readFile(filePath, "utf-8");
|
|
}
|
|
|
|
/**
|
|
* Create a raw app directory structure on disk
|
|
* Uses .raw_app folder suffix with raw_app.yaml metadata
|
|
*/
|
|
async function createRawAppOnDisk(appDir: string, includeBackend: boolean = false): Promise<void> {
|
|
await mkdir(appDir, { recursive: true });
|
|
await mkdir(path.join(appDir, "inline_scripts"), { recursive: true });
|
|
|
|
// Create raw_app.yaml metadata file
|
|
await writeFile(path.join(appDir, "raw_app.yaml"), RAW_APP_YAML, "utf-8");
|
|
|
|
// Create app source files
|
|
await writeFile(path.join(appDir, "App.tsx"), APP_TSX, "utf-8");
|
|
await writeFile(path.join(appDir, "index.css"), INDEX_CSS, "utf-8");
|
|
await writeFile(path.join(appDir, "index.tsx"), INDEX_TSX, "utf-8");
|
|
await writeFile(path.join(appDir, "package.json"), PACKAGE_JSON, "utf-8");
|
|
|
|
// Create inline script in inline_scripts folder
|
|
await writeFile(
|
|
path.join(appDir, "inline_scripts", "a.inline_script.ts"),
|
|
INLINE_SCRIPT_A,
|
|
"utf-8"
|
|
);
|
|
await writeFile(
|
|
path.join(appDir, "inline_scripts", "a.inline_script.lock"),
|
|
INLINE_SCRIPT_A_LOCK,
|
|
"utf-8"
|
|
);
|
|
|
|
// Optionally create backend runnable (type: inline)
|
|
if (includeBackend) {
|
|
await mkdir(path.join(appDir, "backend"), { recursive: true });
|
|
await writeFile(path.join(appDir, "backend", "query.yaml"), "type: inline\n", "utf-8");
|
|
await writeFile(path.join(appDir, "backend", "query.ts"), `export async function main(x: number): Promise<string> {
|
|
return \`Result: \${x}\`;
|
|
}
|
|
`, "utf-8");
|
|
}
|
|
}
|
|
|
|
test("Raw App: full sync workflow - push, pull, modify, push, clear, pull", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
// Set up workspace
|
|
const testWorkspace = {
|
|
remote: backend.baseUrl,
|
|
workspaceId: backend.workspace,
|
|
name: "raw_app_test",
|
|
token: backend.token
|
|
};
|
|
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
|
|
|
// Create wmill.yaml
|
|
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
|
includes:
|
|
- "**"
|
|
excludes: []`, "utf-8");
|
|
|
|
// Create folder structure
|
|
const appDir = path.join(tempDir, "f", "test", "my_raw_app.raw_app");
|
|
await mkdir(path.join(tempDir, "f", "test"), { recursive: true });
|
|
await createRawAppOnDisk(appDir, true); // Include backend for metadata test
|
|
|
|
// =========================================================================
|
|
// STEP 1: Initial push - create raw app on backend
|
|
// =========================================================================
|
|
const pushResult1 = await backend.runCLICommand([
|
|
'sync', 'push',
|
|
'--yes'
|
|
], tempDir, "raw_app_test");
|
|
|
|
expect(pushResult1.code).toEqual(0);
|
|
|
|
// =========================================================================
|
|
// STEP 2: Clear disk and pull - verify raw app is pulled correctly
|
|
// =========================================================================
|
|
await rm(appDir, { recursive: true });
|
|
expect(!(await fileExists(appDir))).toBeTruthy();
|
|
|
|
const pullResult1 = await backend.runCLICommand([
|
|
'sync', 'pull',
|
|
'--yes'
|
|
], tempDir, "raw_app_test");
|
|
|
|
expect(pullResult1.code).toEqual(0);
|
|
|
|
// Verify raw app directory structure was created
|
|
expect(await fileExists(appDir)).toBeTruthy();
|
|
|
|
// Verify files were pulled
|
|
const appTsxPath = path.join(appDir, "App.tsx");
|
|
const indexCssPath = path.join(appDir, "index.css");
|
|
const indexTsxPath = path.join(appDir, "index.tsx");
|
|
const packageJsonPath = path.join(appDir, "package.json");
|
|
const inlineScriptPath = path.join(appDir, "inline_scripts", "a.inline_script.ts");
|
|
|
|
expect(await fileExists(appTsxPath)).toBeTruthy();
|
|
expect(await fileExists(indexCssPath)).toBeTruthy();
|
|
expect(await fileExists(indexTsxPath)).toBeTruthy();
|
|
expect(await fileExists(packageJsonPath)).toBeTruthy();
|
|
expect(await fileExists(inlineScriptPath)).toBeTruthy();
|
|
|
|
// Verify file contents
|
|
const appTsxContent = await readFileContent(appTsxPath);
|
|
expect(appTsxContent).toContain("hello world");
|
|
expect(appTsxContent).toContain("backend.a");
|
|
|
|
const indexCssContent = await readFileContent(indexCssPath);
|
|
expect(indexCssContent).toContain(".myclass");
|
|
|
|
const inlineScriptContent = await readFileContent(inlineScriptPath);
|
|
expect(inlineScriptContent).toContain("export async function main");
|
|
|
|
// =========================================================================
|
|
// STEP 3: Modify files locally
|
|
// =========================================================================
|
|
|
|
// Modify App.tsx - change the heading
|
|
const modifiedAppTsx = appTsxContent.replace("hello world", "hello modified world");
|
|
await writeFile(appTsxPath, modifiedAppTsx, "utf-8");
|
|
|
|
// Modify index.css - change the border color
|
|
const modifiedIndexCss = indexCssContent.replace("gray", "blue");
|
|
await writeFile(indexCssPath, modifiedIndexCss, "utf-8");
|
|
|
|
// Modify inline script - change the return value
|
|
const modifiedInlineScript = inlineScriptContent.replace("return x", "return `modified: ${x}`");
|
|
await writeFile(inlineScriptPath, modifiedInlineScript, "utf-8");
|
|
|
|
// =========================================================================
|
|
// STEP 4: Push changes
|
|
// =========================================================================
|
|
const pushResult2 = await backend.runCLICommand([
|
|
'sync', 'push',
|
|
'--yes'
|
|
], tempDir, "raw_app_test");
|
|
|
|
expect(pushResult2.code).toEqual(0);
|
|
|
|
// =========================================================================
|
|
// STEP 5: Clear disk (delete the app directory)
|
|
// =========================================================================
|
|
await rm(appDir, { recursive: true });
|
|
expect(!(await fileExists(appDir))).toBeTruthy();
|
|
|
|
// =========================================================================
|
|
// STEP 6: Pull again and verify modifications persisted
|
|
// =========================================================================
|
|
const pullResult2 = await backend.runCLICommand([
|
|
'sync', 'pull',
|
|
'--yes'
|
|
], tempDir, "raw_app_test");
|
|
|
|
expect(pullResult2.code).toEqual(0);
|
|
|
|
// Verify app directory exists again
|
|
expect(await fileExists(appDir)).toBeTruthy();
|
|
|
|
// Verify all files were pulled again
|
|
expect(await fileExists(appTsxPath)).toBeTruthy();
|
|
expect(await fileExists(indexCssPath)).toBeTruthy();
|
|
expect(await fileExists(indexTsxPath)).toBeTruthy();
|
|
expect(await fileExists(packageJsonPath)).toBeTruthy();
|
|
expect(await fileExists(inlineScriptPath)).toBeTruthy();
|
|
|
|
// Verify modifications were persisted
|
|
const pulledAppTsx = await readFileContent(appTsxPath);
|
|
expect(pulledAppTsx).toContain("hello modified world");
|
|
|
|
const pulledIndexCss = await readFileContent(indexCssPath);
|
|
expect(pulledIndexCss).toContain("blue");
|
|
|
|
const pulledInlineScript = await readFileContent(inlineScriptPath);
|
|
expect(pulledInlineScript).toContain("modified:");
|
|
|
|
// =========================================================================
|
|
// STEP 7: Test that script generate-metadata does NOT process backend runnables
|
|
// =========================================================================
|
|
|
|
// Create a standalone script (should be processed by script generate-metadata)
|
|
await writeFile(path.join(tempDir, "f", "test", "standalone.ts"), `export async function main(): Promise<string> {
|
|
return "hello";
|
|
}
|
|
`, "utf-8");
|
|
|
|
// Run script generate-metadata
|
|
const metaResult1 = await backend.runCLICommand(
|
|
['script', 'generate-metadata', '--yes'],
|
|
tempDir, "raw_app_test"
|
|
);
|
|
expect(metaResult1.code).toEqual(0);
|
|
|
|
// Run generate-metadata --skip-flows --skip-apps
|
|
const metaResult2 = await backend.runCLICommand(
|
|
['generate-metadata', '--skip-flows', '--skip-apps', '--yes'],
|
|
tempDir, "raw_app_test"
|
|
);
|
|
expect(metaResult2.code).toEqual(0);
|
|
|
|
// Backend runnables should NOT have .script.yaml files
|
|
const backendDir = path.join(appDir, "backend");
|
|
expect(await fileExists(path.join(backendDir, "query.yaml"))).toBeTruthy();
|
|
expect(await fileExists(path.join(backendDir, "query.ts"))).toBeTruthy();
|
|
expect(await fileExists(path.join(backendDir, "query.script.yaml"))).toBeFalsy();
|
|
expect(await fileExists(path.join(backendDir, "query.script.lock"))).toBeFalsy();
|
|
|
|
// Bug: raw app backend files get misprocessed and create script files at wrong location
|
|
// The path f/test/my_raw_app.raw_app/backend/query.ts gets truncated at first "."
|
|
// becoming f/test/my_raw_app.script.yaml (stripping .raw_app/backend/query.ts)
|
|
expect(await fileExists(path.join(tempDir, "f", "test", "my_raw_app.script.yaml"))).toBeFalsy();
|
|
expect(await fileExists(path.join(tempDir, "f", "test", "my_raw_app.script.lock"))).toBeFalsy();
|
|
|
|
// Standalone script SHOULD have metadata
|
|
expect(await fileExists(path.join(tempDir, "f", "test", "standalone.script.yaml"))).toBeTruthy();
|
|
expect(await fileExists(path.join(tempDir, "f", "test", "standalone.script.lock"))).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
test("Raw App: add new file and push", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
// Set up workspace
|
|
const testWorkspace = {
|
|
remote: backend.baseUrl,
|
|
workspaceId: backend.workspace,
|
|
name: "raw_app_new_file_test",
|
|
token: backend.token
|
|
};
|
|
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
|
|
|
// Create wmill.yaml
|
|
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
|
includes:
|
|
- "**"
|
|
excludes: []`, "utf-8");
|
|
|
|
// Create initial raw app
|
|
const appDir = path.join(tempDir, "f", "test", "new_file_app.raw_app");
|
|
await mkdir(path.join(tempDir, "f", "test"), { recursive: true });
|
|
await createRawAppOnDisk(appDir);
|
|
|
|
// Initial push
|
|
const pushResult1 = await backend.runCLICommand([
|
|
'sync', 'push',
|
|
'--yes'
|
|
], tempDir, "raw_app_new_file_test");
|
|
|
|
expect(pushResult1.code).toEqual(0);
|
|
|
|
// Add a new file
|
|
const newFilePath = path.join(appDir, "utils.ts");
|
|
await writeFile(newFilePath, `export function formatValue(val: string): string {
|
|
return \`Formatted: \${val}\`;
|
|
}
|
|
`, "utf-8");
|
|
|
|
// Push changes
|
|
const pushResult2 = await backend.runCLICommand([
|
|
'sync', 'push',
|
|
'--yes'
|
|
], tempDir, "raw_app_new_file_test");
|
|
|
|
expect(pushResult2.code).toEqual(0);
|
|
|
|
// Clear and pull again
|
|
await rm(appDir, { recursive: true });
|
|
|
|
const pullResult = await backend.runCLICommand([
|
|
'sync', 'pull',
|
|
'--yes'
|
|
], tempDir, "raw_app_new_file_test");
|
|
|
|
expect(pullResult.code).toEqual(0);
|
|
|
|
// Verify new file was persisted
|
|
expect(await fileExists(newFilePath)).toBeTruthy();
|
|
const newFileContent = await readFileContent(newFilePath);
|
|
expect(newFileContent).toContain("formatValue");
|
|
});
|
|
});
|
|
|
|
test("Raw App: delete file and push", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
// Set up workspace
|
|
const testWorkspace = {
|
|
remote: backend.baseUrl,
|
|
workspaceId: backend.workspace,
|
|
name: "raw_app_delete_file_test",
|
|
token: backend.token
|
|
};
|
|
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
|
|
|
// Create wmill.yaml
|
|
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
|
includes:
|
|
- "**"
|
|
excludes: []`, "utf-8");
|
|
|
|
// Create initial raw app
|
|
const appDir = path.join(tempDir, "f", "test", "delete_file_app.raw_app");
|
|
await mkdir(path.join(tempDir, "f", "test"), { recursive: true });
|
|
await createRawAppOnDisk(appDir);
|
|
|
|
// Initial push
|
|
const pushResult1 = await backend.runCLICommand([
|
|
'sync', 'push',
|
|
'--yes'
|
|
], tempDir, "raw_app_delete_file_test");
|
|
|
|
expect(pushResult1.code).toEqual(0);
|
|
|
|
const indexCssPath = path.join(appDir, "index.css");
|
|
const appTsxPath = path.join(appDir, "App.tsx");
|
|
expect(await fileExists(indexCssPath)).toBeTruthy();
|
|
|
|
// First, update App.tsx to remove the CSS import (otherwise bundle will fail)
|
|
const appTsxContent = await readFileContent(appTsxPath);
|
|
const updatedAppTsx = appTsxContent.replace("import './index.css'\n", "");
|
|
await writeFile(appTsxPath, updatedAppTsx, "utf-8");
|
|
|
|
// Delete the CSS file
|
|
await rm(indexCssPath);
|
|
expect(!(await fileExists(indexCssPath))).toBeTruthy();
|
|
|
|
// Push changes
|
|
const pushResult2 = await backend.runCLICommand([
|
|
'sync', 'push',
|
|
'--yes'
|
|
], tempDir, "raw_app_delete_file_test");
|
|
|
|
expect(pushResult2.code).toEqual(0);
|
|
|
|
// Clear and pull again
|
|
await rm(appDir, { recursive: true });
|
|
|
|
const pullResult = await backend.runCLICommand([
|
|
'sync', 'pull',
|
|
'--yes'
|
|
], tempDir, "raw_app_delete_file_test");
|
|
|
|
expect(pullResult.code).toEqual(0);
|
|
|
|
// Verify the deleted file is NOT pulled (it was deleted from backend)
|
|
expect(!(await fileExists(indexCssPath))).toBeTruthy();
|
|
|
|
// But other files should still exist
|
|
expect(await fileExists(appTsxPath)).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
test("Raw App: dry-run push shows expected changes", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
// Set up workspace
|
|
const testWorkspace = {
|
|
remote: backend.baseUrl,
|
|
workspaceId: backend.workspace,
|
|
name: "raw_app_dry_run_test",
|
|
token: backend.token
|
|
};
|
|
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
|
|
|
// Create wmill.yaml
|
|
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
|
includes:
|
|
- "**"
|
|
excludes: []`, "utf-8");
|
|
|
|
// Create raw app
|
|
const appDir = path.join(tempDir, "f", "test", "dry_run_app.raw_app");
|
|
await mkdir(path.join(tempDir, "f", "test"), { recursive: true });
|
|
await createRawAppOnDisk(appDir);
|
|
|
|
// Dry-run push
|
|
const dryRunResult = await backend.runCLICommand([
|
|
'sync', 'push',
|
|
'--dry-run',
|
|
'--json-output'
|
|
], tempDir, "raw_app_dry_run_test");
|
|
|
|
expect(dryRunResult.code).toEqual(0);
|
|
|
|
// Parse JSON output (may be pretty-printed across multiple lines)
|
|
let jsonOutput = null;
|
|
try {
|
|
// Try parsing the entire stdout as JSON
|
|
jsonOutput = JSON.parse(dryRunResult.stdout.trim());
|
|
} catch {
|
|
// If that fails, try to find JSON object in the output
|
|
const jsonMatch = dryRunResult.stdout.match(/\{[\s\S]*\}/);
|
|
if (jsonMatch) {
|
|
try {
|
|
jsonOutput = JSON.parse(jsonMatch[0]);
|
|
} catch {
|
|
// Ignore parse errors
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(jsonOutput !== null).toBeTruthy();
|
|
expect(Array.isArray(jsonOutput.changes)).toBeTruthy();
|
|
|
|
// Should include raw app in changes
|
|
const changePaths = jsonOutput.changes.map((c: any) => c.path);
|
|
const hasRawApp = changePaths.some((p: string) => p.includes("dry_run_app"));
|
|
expect(hasRawApp).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
test("Raw App: CamelCase backend runnableId round-trips without duplicates", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
const testWorkspace = {
|
|
remote: backend.baseUrl,
|
|
workspaceId: backend.workspace,
|
|
name: "raw_app_camelcase_test",
|
|
token: backend.token
|
|
};
|
|
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
|
|
|
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
|
includes:
|
|
- "**"
|
|
excludes: []`, "utf-8");
|
|
|
|
const appDir = path.join(tempDir, "f", "test", "camelcase_app.raw_app");
|
|
const backendDir = path.join(appDir, "backend");
|
|
await mkdir(path.join(tempDir, "f", "test"), { recursive: true });
|
|
await createRawAppOnDisk(appDir);
|
|
|
|
// Add a backend runnable whose id contains uppercase letters. The YAML
|
|
// metadata file is named `${runnableId}.yaml` and must stay in sync with
|
|
// the code file's casing — the bug fixed here desynced them.
|
|
await mkdir(backendDir, { recursive: true });
|
|
await writeFile(path.join(backendDir, "CamelCaseTSRunnable.yaml"), "type: inline\n", "utf-8");
|
|
await writeFile(
|
|
path.join(backendDir, "CamelCaseTSRunnable.ts"),
|
|
`export async function main(x: number): Promise<string> {\n return \`Result: \${x}\`;\n}\n`,
|
|
"utf-8"
|
|
);
|
|
|
|
const pushResult1 = await backend.runCLICommand(
|
|
["sync", "push", "--yes"],
|
|
tempDir, "raw_app_camelcase_test"
|
|
);
|
|
expect(pushResult1.code).toEqual(0);
|
|
|
|
await rm(appDir, { recursive: true });
|
|
|
|
const pullResult = await backend.runCLICommand(
|
|
["sync", "pull", "--yes"],
|
|
tempDir, "raw_app_camelcase_test"
|
|
);
|
|
expect(pullResult.code).toEqual(0);
|
|
|
|
// YAML and code file must both come back with the original case so
|
|
// loadRunnablesFromBackend pairs them as one runnable, not two.
|
|
// Use readdir for exact-case comparison: Windows is case-insensitive at
|
|
// the filesystem level, so fileExists("camelcasetsrunnable.ts") would
|
|
// resolve to CamelCaseTSRunnable.ts and false-positive the orphan check.
|
|
const backendEntries = await readdir(backendDir);
|
|
expect(backendEntries).toContain("CamelCaseTSRunnable.yaml");
|
|
expect(backendEntries).toContain("CamelCaseTSRunnable.ts");
|
|
expect(backendEntries).not.toContain("camelcasetsrunnable.ts");
|
|
expect(backendEntries).not.toContain("camelcasetsrunnable.yaml");
|
|
|
|
const pulledContent = await readFileContent(path.join(backendDir, "CamelCaseTSRunnable.ts"));
|
|
expect(pulledContent).toContain("Result:");
|
|
|
|
// A second push must not surface a duplicate lowercase runnable: the
|
|
// dry-run change list should only mention the one app, no orphan code
|
|
// file getting registered as a separate inline runnable.
|
|
const dryRun = await backend.runCLICommand(
|
|
["sync", "push", "--dry-run", "--json-output"],
|
|
tempDir, "raw_app_camelcase_test"
|
|
);
|
|
expect(dryRun.code).toEqual(0);
|
|
|
|
let jsonOutput: any = null;
|
|
try {
|
|
jsonOutput = JSON.parse(dryRun.stdout.trim());
|
|
} catch {
|
|
const jsonMatch = dryRun.stdout.match(/\{[\s\S]*\}/);
|
|
if (jsonMatch) {
|
|
try { jsonOutput = JSON.parse(jsonMatch[0]); } catch { /* ignore */ }
|
|
}
|
|
}
|
|
expect(jsonOutput !== null).toBeTruthy();
|
|
const changes = (jsonOutput.changes ?? []) as Array<{ path: string }>;
|
|
// No change should reference a lowercased runnable filename.
|
|
const lowercased = changes.filter((c) => c.path.includes("camelcasetsrunnable"));
|
|
expect(lowercased).toEqual([]);
|
|
});
|
|
});
|