mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 08:02:40 +00:00
feat(cli): add consistent get/list/new subcommands for all item types (#8047)
* feat(cli): add consistent get/list/new subcommands for all item types Make the CLI consistent so every item type (script, flow, app, resource, resource-type, variable, schedule, folder, trigger) supports get/list/new subcommands, enabling the CLI to be used as a full API client in bash scripts with jq piping. - Add --json flag to all list commands for machine-readable output - Register explicit "list" subcommand alongside default action - Add "get <path> [--json]" subcommand to fetch single items from API - Rename "bootstrap" to "new" for script/flow, keep "bootstrap" as alias - Add "new" subcommand for resource, resource-type, variable, schedule, folder, and trigger to create local template YAML files - Update cli-commands skill documentation for wmill init - Add integration tests for all new commands Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * all * feat: install wmill CLI in Docker images and use it for bash variable/resource access - Install windmill-cli via bun in all Dockerfiles that include bun - DockerfileCli: switch from node:slim to oven/bun:slim - CLI: auto-configure from WM_WORKSPACE/WM_TOKEN/BASE_INTERNAL_URL env vars as last-resort fallback when no workspace is configured - Frontend: replace curl-based bash snippets with wmill variable/resource get - Add backend integration tests for wmill CLI in bash scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ci): install windmill-cli in backend test workflow Ensures wmill is available on PATH for bash integration tests that use `wmill variable get` and `wmill resource get`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(cli): replace @std/* Deno dependencies with Node.js equivalents Replace @std/log with a lightweight custom logger (core/log.ts), @std/path with node:path, and @std/yaml with the yaml npm package. Also fix process hang on exit, add --node option to install_dev.sh, and add missing hasRequiredPermissions to NpmProvider. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * all * all * all * refactor(cli): replace @ayonli/jsext and @std/encoding with lightweight alternatives Replace @ayonli/jsext (8.4MB) with tar-stream (32kB) for tar creation, replace @std/encoding with Node.js Buffer.toString("hex"), and fix @windmill-labs/shared-utils to use direct npm instead of JSR mirror. Also resolve merge conflicts in sync.ts and fix pre-existing type errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(cli): use singleQuote YAML output and pass yamlOptions in gitsync pull The yaml library defaults to double quotes, but the codebase (and tests) expect single-quoted strings. Add singleQuote: true to yamlOptions and pass yamlOptions to gitsync-settings pull writeFile calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * all * all * fix(cli): address code review feedback - Install CLI from source in backend tests instead of npm - Fix script bootstrap catch block to re-throw "File already exists" - Add type-safe local variable after trigger kind validation - Use created_by instead of policy.on_behalf_of for app get output - Note --kind is recommended for faster trigger lookup in help text - Document node symlink purpose in Dockerfiles Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ci): use /usr/bin for wmill wrapper to ensure it's in PATH Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ci): install wmill to ~/.local/bin to avoid permission issues Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci(backend): switch to Blacksmith runner and add cargo caching - Switch from ubicloud-standard-16 to blacksmith-16vcpu-ubuntu-2404 for faster NVMe-backed builds - Add stickydisk for cargo target directory (persistent NVMe cache across runs) - Add cache for cargo registry and git dependencies - Upgrade DuckDB FFI cache from actions/cache@v3 to useblacksmith/cache@v1 - Enable CARGO_INCREMENTAL=1 to benefit from persistent target cache Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix ci --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a91c532eca
commit
4fedfdfd11
@@ -1,7 +1,7 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import * as path from "@std/path";
|
||||
import * as path from "node:path";
|
||||
import {
|
||||
formatValidationError,
|
||||
runLint,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect, test, describe } from "bun:test";
|
||||
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import * as path from "@std/path";
|
||||
import * as path from "node:path";
|
||||
import { checkMissingLocks, runLint } from "../src/commands/lint/lint.ts";
|
||||
|
||||
async function withTempDir(
|
||||
|
||||
@@ -0,0 +1,639 @@
|
||||
/**
|
||||
* Integration tests for the new list/get/new CLI commands.
|
||||
*
|
||||
* Tests:
|
||||
* - `list --json` for all item types
|
||||
* - `get <path>` and `get <path> --json` for all item types
|
||||
* - `new` (bootstrap) for script, flow, resource, resource-type, variable, schedule, folder, trigger
|
||||
* - `bootstrap` alias for script and flow
|
||||
*/
|
||||
|
||||
import { expect, test, describe } from "bun:test";
|
||||
import { writeFile, mkdir, stat, readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { withTestBackend, type TestBackend } from "./test_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
|
||||
async function setupWorkspaceProfile(backend: TestBackend): Promise<void> {
|
||||
await addWorkspace(
|
||||
{
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "localhost_test",
|
||||
token: backend.token!,
|
||||
},
|
||||
{ force: true, configDir: backend.testConfigDir }
|
||||
);
|
||||
}
|
||||
|
||||
async function createRemoteScript(
|
||||
backend: TestBackend,
|
||||
scriptPath: string,
|
||||
content: string = 'export async function main() { return "hello"; }'
|
||||
): Promise<void> {
|
||||
const resp = await backend.apiRequest!(
|
||||
`/api/w/${backend.workspace}/scripts/create`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
path: scriptPath,
|
||||
content,
|
||||
language: "bun",
|
||||
summary: "Test script summary",
|
||||
description: "Test script description",
|
||||
schema: {
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
expect(resp.status).toBeLessThan(300);
|
||||
await resp.text();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// list --json
|
||||
// =============================================================================
|
||||
|
||||
describe("list --json flag", () => {
|
||||
test("script list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const uniqueId = Date.now();
|
||||
const scriptPath = `f/test/list_json_script_${uniqueId}`;
|
||||
await createRemoteScript(backend, scriptPath);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
expect(parsed.some((s: any) => s.path === scriptPath)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("flow list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["flow", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("resource list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["resource", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
// seedTestData creates f/test/my_resource
|
||||
expect(parsed.some((r: any) => r.path === "f/test/my_resource")).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("variable list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["variable", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("folder list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["folder", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
expect(parsed.some((f: any) => f.name === "test")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("schedule list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["schedule", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("resource-type list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["resource-type", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("trigger list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["trigger", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("app list --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["app", "list", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("default action with --json works (e.g. wmill script --json)", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// get <path> and get <path> --json
|
||||
// =============================================================================
|
||||
|
||||
describe("get command", () => {
|
||||
test("script get pretty-prints details", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const uniqueId = Date.now();
|
||||
const scriptPath = `f/test/get_script_${uniqueId}`;
|
||||
await createRemoteScript(backend, scriptPath);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "get", scriptPath],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const output = result.stdout;
|
||||
expect(output).toContain("Path:");
|
||||
expect(output).toContain(scriptPath);
|
||||
expect(output).toContain("Summary:");
|
||||
expect(output).toContain("Language:");
|
||||
expect(output).toContain("bun");
|
||||
});
|
||||
});
|
||||
|
||||
test("script get --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const uniqueId = Date.now();
|
||||
const scriptPath = `f/test/get_json_script_${uniqueId}`;
|
||||
await createRemoteScript(backend, scriptPath);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "get", scriptPath, "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(parsed.path).toBe(scriptPath);
|
||||
expect(parsed.language).toBe("bun");
|
||||
expect(parsed.summary).toBe("Test script summary");
|
||||
});
|
||||
});
|
||||
|
||||
test("resource get --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["resource", "get", "f/test/my_resource", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(parsed.path).toBe("f/test/my_resource");
|
||||
expect(parsed.resource_type).toBe("any");
|
||||
});
|
||||
});
|
||||
|
||||
test("resource get pretty-prints details", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["resource", "get", "f/test/my_resource"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const output = result.stdout;
|
||||
expect(output).toContain("Path:");
|
||||
expect(output).toContain("f/test/my_resource");
|
||||
expect(output).toContain("Resource Type:");
|
||||
});
|
||||
});
|
||||
|
||||
test("variable get --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["variable", "get", "f/test/my_variable", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(parsed.path).toBe("f/test/my_variable");
|
||||
});
|
||||
});
|
||||
|
||||
test("folder get --json outputs valid JSON", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["folder", "get", "test", "--json"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
expect(parsed.name).toBe("test");
|
||||
});
|
||||
});
|
||||
|
||||
test("folder get pretty-prints details", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["folder", "get", "test"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
const output = result.stdout;
|
||||
expect(output).toContain("Name:");
|
||||
expect(output).toContain("test");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// new command
|
||||
// =============================================================================
|
||||
|
||||
describe("new command", () => {
|
||||
test("script new creates files (same as bootstrap)", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await writeFile(
|
||||
join(tempDir, "wmill.yaml"),
|
||||
`defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`,
|
||||
"utf-8"
|
||||
);
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "new", "f/test/new_cmd_script", "bun", "--summary", "Test new"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const codeStat = await stat(join(tempDir, "f/test/new_cmd_script.ts"));
|
||||
expect(codeStat.isFile()).toBe(true);
|
||||
|
||||
const metaStat = await stat(
|
||||
join(tempDir, "f/test/new_cmd_script.script.yaml")
|
||||
);
|
||||
expect(metaStat.isFile()).toBe(true);
|
||||
|
||||
const metaContent = await readFile(
|
||||
join(tempDir, "f/test/new_cmd_script.script.yaml"),
|
||||
"utf-8"
|
||||
);
|
||||
expect(metaContent).toContain("Test new");
|
||||
});
|
||||
});
|
||||
|
||||
test("script bootstrap still works as alias", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await writeFile(
|
||||
join(tempDir, "wmill.yaml"),
|
||||
`defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`,
|
||||
"utf-8"
|
||||
);
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "bootstrap", "f/test/alias_script", "bun"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const codeStat = await stat(join(tempDir, "f/test/alias_script.ts"));
|
||||
expect(codeStat.isFile()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("flow new creates flow directory and flow.yaml", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["flow", "new", "f/test/new_flow", "--summary", "My flow"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const flowYamlStat = await stat(
|
||||
join(tempDir, "f/test/new_flow.flow/flow.yaml")
|
||||
);
|
||||
expect(flowYamlStat.isFile()).toBe(true);
|
||||
|
||||
const flowContent = await readFile(
|
||||
join(tempDir, "f/test/new_flow.flow/flow.yaml"),
|
||||
"utf-8"
|
||||
);
|
||||
expect(flowContent).toContain("My flow");
|
||||
});
|
||||
});
|
||||
|
||||
test("flow bootstrap still works as alias", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["flow", "bootstrap", "f/test/alias_flow"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const flowYamlStat = await stat(
|
||||
join(tempDir, "f/test/alias_flow.flow/flow.yaml")
|
||||
);
|
||||
expect(flowYamlStat.isFile()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("resource new creates resource yaml template", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["resource", "new", "f/test/new_resource"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const filePath = join(tempDir, "f/test/new_resource.resource.yaml");
|
||||
const fileStat = await stat(filePath);
|
||||
expect(fileStat.isFile()).toBe(true);
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
expect(content).toContain("resource_type");
|
||||
expect(content).toContain("value");
|
||||
});
|
||||
});
|
||||
|
||||
test("resource-type new creates resource-type yaml template", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["resource-type", "new", "my_custom_type"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const filePath = join(tempDir, "my_custom_type.resource-type.yaml");
|
||||
const fileStat = await stat(filePath);
|
||||
expect(fileStat.isFile()).toBe(true);
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
expect(content).toContain("schema");
|
||||
expect(content).toContain("description");
|
||||
});
|
||||
});
|
||||
|
||||
test("variable new creates variable yaml template", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["variable", "new", "f/test/new_var"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const filePath = join(tempDir, "f/test/new_var.variable.yaml");
|
||||
const fileStat = await stat(filePath);
|
||||
expect(fileStat.isFile()).toBe(true);
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
expect(content).toContain("is_secret");
|
||||
expect(content).toContain("value");
|
||||
});
|
||||
});
|
||||
|
||||
test("schedule new creates schedule yaml template", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["schedule", "new", "f/test/new_sched"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const filePath = join(tempDir, "f/test/new_sched.schedule.yaml");
|
||||
const fileStat = await stat(filePath);
|
||||
expect(fileStat.isFile()).toBe(true);
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
expect(content).toContain("schedule");
|
||||
expect(content).toContain("script_path");
|
||||
expect(content).toContain("timezone");
|
||||
});
|
||||
});
|
||||
|
||||
test("folder new creates folder.meta.yaml in f/<name>/", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["folder", "new", "new_folder"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const filePath = join(tempDir, "f/new_folder/folder.meta.yaml");
|
||||
const fileStat = await stat(filePath);
|
||||
expect(fileStat.isFile()).toBe(true);
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
expect(content).toContain("owners");
|
||||
expect(content).toContain("extra_perms");
|
||||
});
|
||||
});
|
||||
|
||||
test("trigger new --kind http creates http trigger yaml template", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["trigger", "new", "f/test/new_trigger", "--kind", "http"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const filePath = join(
|
||||
tempDir,
|
||||
"f/test/new_trigger.http_trigger.yaml"
|
||||
);
|
||||
const fileStat = await stat(filePath);
|
||||
expect(fileStat.isFile()).toBe(true);
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
expect(content).toContain("script_path");
|
||||
expect(content).toContain("route_path");
|
||||
});
|
||||
});
|
||||
|
||||
test("trigger new without --kind fails with error", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["trigger", "new", "f/test/fail_trigger"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).not.toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
test("trigger new --kind kafka creates kafka trigger yaml template", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["trigger", "new", "f/test/kafka_trigger", "--kind", "kafka"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
|
||||
const filePath = join(
|
||||
tempDir,
|
||||
"f/test/kafka_trigger.kafka_trigger.yaml"
|
||||
);
|
||||
const fileStat = await stat(filePath);
|
||||
expect(fileStat.isFile()).toBe(true);
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
expect(content).toContain("kafka_resource_path");
|
||||
expect(content).toContain("topics");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
import { expect, test } from "bun:test";
|
||||
import { encodeHex } from "@std/encoding";
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mirrors extractWorkspaceDepsAnnotation + computeLockCacheKey from
|
||||
@@ -110,7 +110,7 @@ async function computeLockCacheKey(
|
||||
.join(";");
|
||||
const content = `${language}|${annotationStr}|${depsStr}`;
|
||||
const buf = new TextEncoder().encode(content);
|
||||
return encodeHex(await crypto.subtle.digest("SHA-256", buf));
|
||||
return Buffer.from(await crypto.subtle.digest("SHA-256", buf)).toString("hex");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
*/
|
||||
|
||||
import { expect, test } from "bun:test";
|
||||
import * as path from "@std/path";
|
||||
import * as path from "node:path";
|
||||
import { writeFile, readFile, stat } from "node:fs/promises";
|
||||
import { withTestBackend } from "./test_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { withTestBackend } from "./test_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import * as path from "@std/path";
|
||||
import * as path from "node:path";
|
||||
import { writeFile, readFile, stat, rm, mkdir } from "node:fs/promises";
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
|
||||
import { expect, test, describe } from "bun:test";
|
||||
import * as path from "@std/path";
|
||||
import { SEPARATOR as SEP } from "@std/path";
|
||||
import * as path from "node:path";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { writeFile, readFile, readdir, rm, mkdir, mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Unit tests for the tar creation utility.
|
||||
* These tests require no backend — they test standalone tar logic.
|
||||
*/
|
||||
|
||||
import { expect, test, describe } from "bun:test";
|
||||
import { createTarBlob, type TarEntry } from "../src/utils/tar.ts";
|
||||
import { extract, type Headers } from "tar-stream";
|
||||
import { Readable } from "node:stream";
|
||||
|
||||
/** Extract all entries from a tarball Blob into a map of name -> content string */
|
||||
async function extractTar(
|
||||
blob: Blob
|
||||
): Promise<Map<string, { content: string; header: Headers }>> {
|
||||
const result = new Map<string, { content: string; header: Headers }>();
|
||||
const ex = extract();
|
||||
const buffer = Buffer.from(await blob.arrayBuffer());
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ex.on("entry", (header, stream, next) => {
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
stream.on("end", () => {
|
||||
result.set(header.name, {
|
||||
content: Buffer.concat(chunks).toString("utf-8"),
|
||||
header,
|
||||
});
|
||||
next();
|
||||
});
|
||||
stream.on("error", reject);
|
||||
stream.resume();
|
||||
});
|
||||
ex.on("finish", () => resolve(result));
|
||||
ex.on("error", reject);
|
||||
|
||||
Readable.from(buffer).pipe(ex);
|
||||
});
|
||||
}
|
||||
|
||||
describe("createTarBlob", () => {
|
||||
test("single file tarball", async () => {
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: 'console.log("hello");' },
|
||||
];
|
||||
|
||||
const blob = await createTarBlob(entries);
|
||||
const extracted = await extractTar(blob);
|
||||
|
||||
expect(extracted.size).toBe(1);
|
||||
expect(extracted.has("main.js")).toBe(true);
|
||||
expect(extracted.get("main.js")!.content).toBe('console.log("hello");');
|
||||
});
|
||||
|
||||
test("multiple output files", async () => {
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: 'import "./chunk-abc.js";' },
|
||||
{ name: "chunk-abc.js", content: "export const x = 42;" },
|
||||
{ name: "chunk-def.js", content: "export const y = 99;" },
|
||||
];
|
||||
|
||||
const blob = await createTarBlob(entries);
|
||||
const extracted = await extractTar(blob);
|
||||
|
||||
expect(extracted.size).toBe(3);
|
||||
expect(extracted.get("main.js")!.content).toBe(
|
||||
'import "./chunk-abc.js";'
|
||||
);
|
||||
expect(extracted.get("chunk-abc.js")!.content).toBe(
|
||||
"export const x = 42;"
|
||||
);
|
||||
expect(extracted.get("chunk-def.js")!.content).toBe(
|
||||
"export const y = 99;"
|
||||
);
|
||||
});
|
||||
|
||||
test("single file with assets", async () => {
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: "const data = require('./data.json');" },
|
||||
{ name: "data.json", content: '{"key":"value"}' },
|
||||
];
|
||||
|
||||
const blob = await createTarBlob(entries);
|
||||
const extracted = await extractTar(blob);
|
||||
|
||||
expect(extracted.size).toBe(2);
|
||||
expect(extracted.has("main.js")).toBe(true);
|
||||
expect(extracted.has("data.json")).toBe(true);
|
||||
expect(extracted.get("data.json")!.content).toBe('{"key":"value"}');
|
||||
});
|
||||
|
||||
test("produces a valid Blob", async () => {
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: "module.exports = {};" },
|
||||
];
|
||||
|
||||
const blob = await createTarBlob(entries);
|
||||
|
||||
expect(blob).toBeInstanceOf(Blob);
|
||||
expect(blob.size).toBeGreaterThan(0);
|
||||
// Tar blocks are 512-byte aligned
|
||||
expect(blob.size % 512).toBe(0);
|
||||
});
|
||||
|
||||
test("file naming — entries have exact names given", async () => {
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: "entry point" },
|
||||
{ name: "lib/utils.js", content: "utils" },
|
||||
];
|
||||
|
||||
const blob = await createTarBlob(entries);
|
||||
const extracted = await extractTar(blob);
|
||||
|
||||
// Names should be exactly as provided (no leading slash)
|
||||
expect(extracted.has("main.js")).toBe(true);
|
||||
expect(extracted.has("lib/utils.js")).toBe(true);
|
||||
});
|
||||
|
||||
test("handles Buffer content", async () => {
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content: Buffer.from("buffer content") },
|
||||
];
|
||||
|
||||
const blob = await createTarBlob(entries);
|
||||
const extracted = await extractTar(blob);
|
||||
|
||||
expect(extracted.get("main.js")!.content).toBe("buffer content");
|
||||
});
|
||||
|
||||
test("handles Uint8Array content", async () => {
|
||||
const content = new TextEncoder().encode("uint8 content");
|
||||
const entries: TarEntry[] = [
|
||||
{ name: "main.js", content },
|
||||
];
|
||||
|
||||
const blob = await createTarBlob(entries);
|
||||
const extracted = await extractTar(blob);
|
||||
|
||||
expect(extracted.get("main.js")!.content).toBe("uint8 content");
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { expect, test } from "bun:test";
|
||||
import * as path from "@std/path";
|
||||
import * as path from "node:path";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import {
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
clearGlobalLock,
|
||||
} from "../src/utils/metadata.ts";
|
||||
import { generateHash } from "../src/utils/utils.ts";
|
||||
import { stringify as yamlStringify } from "@std/yaml";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { yamlParseFile } from "../src/utils/yaml.ts";
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -14,7 +14,7 @@ import { expect, test } from "bun:test";
|
||||
import { withTestBackend } from "./test_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import { writeFile, mkdir } from "node:fs/promises";
|
||||
import { stringify as stringifyYaml } from "@std/yaml";
|
||||
import { stringify as stringifyYaml } from "yaml";
|
||||
|
||||
// Import hash generation utilities from CLI
|
||||
import { generateHash } from "../src/utils/utils.ts";
|
||||
|
||||
Reference in New Issue
Block a user