Files
windmill/cli/test/folder_missing_meta.test.ts
2026-04-02 10:05:02 +00:00

436 lines
15 KiB
TypeScript

/**
* Tests for missing folder.meta.yaml detection during sync push,
* the `folder add-missing` command, and the simplified `folder push` command.
*/
import { expect, test, describe } from "bun:test";
import { writeFile, mkdir, readFile, rm, mkdtemp } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { getTestBackend, createNonAdminUser } from "./test_backend.ts";
type IsolatedWorkspaceTestContext = {
backend: any;
tempDir: string;
workspaceId: string;
runCLICommand: (
args: string[],
opts?: { token?: string }
) => Promise<{ stdout: string; stderr: string; code: number }>;
apiRequest: (path: string, options?: RequestInit) => Promise<Response>;
};
async function createWorkspace(backend: any, workspaceId: string): Promise<void> {
const response = await backend.apiRequest!("/api/workspaces/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: workspaceId,
// Workspace name has a 50-char DB limit; keep it identical to the short ID.
name: workspaceId,
}),
});
if (!response.ok) {
const error = await response.text();
if (!error.includes("already exists") && !error.includes("duplicate")) {
throw new Error(`Failed to create workspace ${workspaceId}: ${error}`);
}
return;
}
await response.text();
}
async function withIsolatedWorkspace(
testFn: (ctx: IsolatedWorkspaceTestContext) => Promise<void>
): Promise<void> {
const backend = await getTestBackend();
const tempDir = await mkdtemp(join(tmpdir(), "windmill_cli_test_"));
const workspaceId = `fmeta_${Date.now().toString(36)}_${Math.random()
.toString(36)
.slice(2, 6)}`;
let workspaceCreated = false;
try {
await createWorkspace(backend, workspaceId);
workspaceCreated = true;
await testFn({
backend,
tempDir,
workspaceId,
runCLICommand: (args: string[], opts?: { token?: string }) =>
backend.runCLICommand(args, tempDir, {
workspace: workspaceId,
token: opts?.token,
}),
apiRequest: (path: string, options?: RequestInit) =>
backend.apiRequest!(`/api/w/${workspaceId}${path}`, options),
});
} finally {
if (workspaceCreated) {
try {
const archiveResponse = await backend.apiRequest!(
`/api/w/${workspaceId}/workspaces/archive`,
{ method: "POST" }
);
await archiveResponse.text();
} catch {
// Best-effort cleanup to avoid exceeding non-enterprise workspace limits.
}
}
await rm(tempDir, { recursive: true, force: true });
}
}
function wmillYaml(): string {
return `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`;
}
// =============================================================================
// folder new — creates folder.meta.yaml with summary and display_name
// =============================================================================
describe("folder new", () => {
test("creates folder.meta.yaml with summary and display_name", async () => {
await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
const folderName = `newfolder${Date.now()}`;
const result = await runCLICommand(
["folder", "new", folderName, "--summary", "My summary"],
);
expect(result.code).toEqual(0);
const metaPath = join(tempDir, "f", folderName, "folder.meta.yaml");
const content = await readFile(metaPath, "utf-8");
expect(content).toContain("summary: My summary");
expect(content).toContain(`display_name: ${folderName}`);
expect(content).toContain("owners:");
expect(content).toContain("extra_perms:");
});
});
test("creates folder.meta.yaml with empty summary when none provided", async () => {
await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
const folderName = `nosummary${Date.now()}`;
const result = await runCLICommand(
["folder", "new", folderName],
);
expect(result.code).toEqual(0);
const content = await readFile(
join(tempDir, "f", folderName, "folder.meta.yaml"),
"utf-8"
);
expect(content).toContain('summary: ""');
expect(content).toContain(`display_name: ${folderName}`);
});
});
test("fails if folder.meta.yaml already exists", async () => {
await withIsolatedWorkspace(async ({ runCLICommand }) => {
const folderName = `dupfolder${Date.now()}`;
// Create first
await runCLICommand(["folder", "new", folderName]);
// Try again — should fail
const result = await runCLICommand(
["folder", "new", folderName],
);
expect(result.code).not.toEqual(0);
});
});
});
// =============================================================================
// folder add-missing — scaffolds missing folder.meta.yaml files
// =============================================================================
describe("folder add-missing", () => {
test("creates folder.meta.yaml for directories missing one", async () => {
await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
// Create two folders: one with meta, one without
const withMeta = `withmeta${Date.now()}`;
const withoutMeta = `withoutmeta${Date.now()}`;
await mkdir(join(tempDir, "f", withMeta), { recursive: true });
await writeFile(
join(tempDir, "f", withMeta, "folder.meta.yaml"),
'summary: ""\ndisplay_name: existing\nowners: []\nextra_perms: {}\n',
"utf-8"
);
await mkdir(join(tempDir, "f", withoutMeta), { recursive: true });
// No folder.meta.yaml for withoutMeta
const result = await runCLICommand(
["folder", "add-missing", "-y"],
);
expect(result.code).toEqual(0);
// withoutMeta should now have a folder.meta.yaml
const createdMeta = await readFile(
join(tempDir, "f", withoutMeta, "folder.meta.yaml"),
"utf-8"
);
expect(createdMeta).toContain(`display_name: ${withoutMeta}`);
expect(createdMeta).toContain("owners:");
// withMeta should be unchanged
const existingMeta = await readFile(
join(tempDir, "f", withMeta, "folder.meta.yaml"),
"utf-8"
);
expect(existingMeta).toContain("display_name: existing");
});
});
test("reports nothing to do when all folders have meta", async () => {
await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
const folderName = `alldone${Date.now()}`;
await mkdir(join(tempDir, "f", folderName), { recursive: true });
await writeFile(
join(tempDir, "f", folderName, "folder.meta.yaml"),
'summary: ""\ndisplay_name: done\nowners: []\nextra_perms: {}\n',
"utf-8"
);
const result = await runCLICommand(
["folder", "add-missing", "-y"],
);
expect(result.code).toEqual(0);
expect(result.stdout + result.stderr).toContain("Nothing to do");
});
});
test("reports nothing to do when no f/ directory exists", async () => {
await withIsolatedWorkspace(async ({ runCLICommand }) => {
const result = await runCLICommand(
["folder", "add-missing", "-y"],
);
expect(result.code).toEqual(0);
expect(result.stdout + result.stderr).toContain("Nothing to do");
});
});
});
// =============================================================================
// folder push — simplified single-arg signature
// =============================================================================
describe("folder push", () => {
test("pushes a folder by name", async () => {
await withIsolatedWorkspace(async ({ tempDir, runCLICommand, apiRequest }) => {
const folderName = `pushbyname${Date.now()}`;
// Create local folder meta
await mkdir(join(tempDir, "f", folderName), { recursive: true });
await writeFile(
join(tempDir, "f", folderName, "folder.meta.yaml"),
`summary: "pushed"\ndisplay_name: "${folderName}"\nowners:\n - "admin@windmill.dev"\nextra_perms: {}\n`,
"utf-8"
);
const result = await runCLICommand(
["folder", "push", folderName],
);
expect(result.code).toEqual(0);
expect(result.stdout + result.stderr).toContain("Folder pushed");
// Verify via API
const apiResp = await apiRequest(`/folders/get/${folderName}`);
expect(apiResp.status).toEqual(200);
});
});
test("fails when folder does not exist locally", async () => {
await withIsolatedWorkspace(async ({ runCLICommand }) => {
const result = await runCLICommand(
["folder", "push", "nonexistent"],
);
expect(result.code).not.toEqual(0);
});
});
});
// =============================================================================
// sync push — missing folder.meta.yaml detection
// =============================================================================
describe("sync push missing folder detection", () => {
test("admin user gets warning but push succeeds", async () => {
await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
const uniqueId = Date.now();
const folderName = `nometaadmin${uniqueId}`;
await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8");
// Create a script inside a folder WITHOUT folder.meta.yaml
await mkdir(join(tempDir, "f", folderName), { recursive: true });
await writeFile(
join(tempDir, "f", folderName, "test_script.ts"),
'export async function main() { return "hello"; }',
"utf-8"
);
const result = await runCLICommand(
["sync", "push", "--yes", "--includes", `f/${folderName}/**`],
);
// Admin should get a warning but push succeeds (exit 0)
expect(result.code).toEqual(0);
const output = result.stdout + result.stderr;
expect(output).toContain("Missing folder.meta.yaml");
expect(output).toContain(folderName);
expect(output).toContain("wmill folder add-missing");
});
});
test("no warning when folder.meta.yaml exists", async () => {
await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
const uniqueId = Date.now();
const folderName = `withmeta${uniqueId}`;
await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8");
// Create folder WITH folder.meta.yaml
await mkdir(join(tempDir, "f", folderName), { recursive: true });
await writeFile(
join(tempDir, "f", folderName, "folder.meta.yaml"),
`summary: ""\ndisplay_name: "${folderName}"\nowners: []\nextra_perms: {}\n`,
"utf-8"
);
await writeFile(
join(tempDir, "f", folderName, "test_script.ts"),
'export async function main() { return "hello"; }',
"utf-8"
);
const result = await runCLICommand(
["sync", "push", "--yes", "--includes", `f/${folderName}/**`],
);
expect(result.code).toEqual(0);
const output = result.stdout + result.stderr;
expect(output).not.toContain("Missing folder.meta.yaml");
});
});
test.skipIf(!process.env["EE_LICENSE_KEY"])("non-admin user gets error and exit code 1", async () => {
await withIsolatedWorkspace(async ({ backend, tempDir, workspaceId, runCLICommand, apiRequest }) => {
const nonAdminToken = await createNonAdminUser(backend, workspaceId);
const uniqueId = Date.now();
const folderName = `nometanonadmin${uniqueId}`;
await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8");
// Create a script inside a folder WITHOUT folder.meta.yaml
// First create the folder on remote so the non-admin has somewhere to push
await apiRequest(
"/folders/create",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: folderName,
extra_perms: { "g/all": true },
}),
}
);
await mkdir(join(tempDir, "f", folderName), { recursive: true });
await writeFile(
join(tempDir, "f", folderName, "test_script.ts"),
'export async function main() { return "hello"; }',
"utf-8"
);
const result = await runCLICommand(
["sync", "push", "--yes", "--includes", `f/${folderName}/**`],
{ token: nonAdminToken }
);
// Non-admin should get exit code 1
expect(result.code).toEqual(1);
const output = result.stdout + result.stderr;
expect(output).toContain("Missing folder.meta.yaml");
expect(output).toContain("wmill folder add-missing");
});
});
test("no warning for deleted changes without folder.meta.yaml", async () => {
await withIsolatedWorkspace(async ({ tempDir, runCLICommand, apiRequest }) => {
const uniqueId = Date.now();
const folderName = `delfolder${uniqueId}`;
// Create folder and script on remote via API
await apiRequest(
"/folders/create",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: folderName }),
}
);
await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8");
// Pull to get remote state, then delete the folder locally
await runCLICommand(["sync", "pull", "--yes"]);
// Remove the folder locally to trigger a "deleted" change
await rm(join(tempDir, "f", folderName), { recursive: true, force: true });
const result = await runCLICommand(
["sync", "push", "--yes", "--includes", `f/${folderName}/**`],
);
// Should not warn about missing meta for deleted items
const output = result.stdout + result.stderr;
expect(output).not.toContain("Missing folder.meta.yaml");
});
});
test("no warning when branch-specific folder.meta.yaml exists", async () => {
await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
const uniqueId = Date.now();
const folderName = `branchmeta${uniqueId}`;
// wmill.yaml with branch-specific folders configured
await writeFile(
join(tempDir, "wmill.yaml"),
`defaultTs: bun\nincludes:\n - "**"\nexcludes: []\ngitBranches:\n dev:\n specificItems:\n folders:\n - "f/${folderName}"\n`,
"utf-8"
);
// Create folder with branch-specific meta only (no base folder.meta.yaml)
await mkdir(join(tempDir, "f", folderName), { recursive: true });
await writeFile(
join(tempDir, "f", folderName, "folder.dev.meta.yaml"),
`summary: ""\ndisplay_name: "${folderName}"\nowners: []\nextra_perms: {}\n`,
"utf-8"
);
await writeFile(
join(tempDir, "f", folderName, "test_script.ts"),
'export async function main() { return "hello"; }',
"utf-8"
);
const result = await runCLICommand(
["sync", "push", "--yes", "--branch", "dev", "--includes", `f/${folderName}/**`],
);
expect(result.code).toEqual(0);
const output = result.stdout + result.stderr;
expect(output).not.toContain("Missing folder.meta.yaml");
});
});
});