Files
windmill/cli/test/new_commands_helpers.ts
Alexander Petric 6c3c971af5 feat: improve CLI flow log streaming and job inspection (#8644)
* fix: improve CLI flow log streaming, sub-job listing, and failure handling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add hierarchical flow status in job get and aggregated flow logs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove duplicate ansi color hint in job logs output

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: update cli-commands skill with new job/flow features

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add integration tests for flow job inspection and log aggregation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove internal friction discovery doc from branch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: trim cli-commands skill to reduce context bloat

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: update job command descriptions and regenerate skills.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: commit auto-generated files from system_prompts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address review comments on flow streaming and test assertions

- Move for-loop waiting logic outside --silent guard (Cubic #2)
- Break outer loop when for-loop module fails (Cubic #3)
- Strengthen test assertion: toContain("a") -> toContain("a: Generate data") (Cubic #1)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: generator regex truncating descriptions with parentheses

The .command() regex used [^)]+ for the second arg, stopping at the
first ')' inside description strings like "(machine-friendly)".
Now matches quoted strings properly before falling back.

Fixes 6 truncated descriptions across job, flow, and script commands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 18:22:18 +00:00

326 lines
8.7 KiB
TypeScript

/**
* Shared helpers for new CLI command tests.
*/
import { expect } from "bun:test";
import { type TestBackend } from "./test_backend.ts";
import { addWorkspace } from "../workspace.ts";
export 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 }
);
}
export async function ensureFolder(backend: TestBackend, name: string): Promise<void> {
const resp = await backend.apiRequest!(
`/api/w/${backend.workspace}/folders/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
}
);
await resp.text();
}
export async function createRemoteScript(
backend: TestBackend,
scriptPath: string,
content: string = 'export async function main() { return "hello"; }'
): Promise<void> {
const parts = scriptPath.split("/");
if (parts[0] === "f" && parts.length > 2) {
await ensureFolder(backend, parts[1]);
}
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",
schema: {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {},
required: [],
},
}),
}
);
expect(resp.status).toBeLessThan(300);
await resp.text();
}
export async function runRemoteScript(
backend: TestBackend,
scriptPath: string,
retries: number = 10
): Promise<string> {
for (let i = 0; i < retries; i++) {
const resp = await backend.apiRequest!(
`/api/w/${backend.workspace}/jobs/run/p/${scriptPath}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
}
);
if (resp.status < 300) {
return (await resp.text()).replace(/"/g, "");
}
await resp.text();
if (i < retries - 1) {
await new Promise((r) => setTimeout(r, 1000));
}
}
throw new Error(`Failed to run script ${scriptPath} after ${retries} retries`);
}
export async function waitForJob(
backend: TestBackend,
jobId: string,
timeoutMs: number = 15000
): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const resp = await backend.apiRequest!(
`/api/w/${backend.workspace}/jobs_u/completed/get/${jobId}`
);
if (resp.ok) return;
await new Promise((r) => setTimeout(r, 500));
}
throw new Error(`Job ${jobId} did not complete within ${timeoutMs}ms`);
}
export async function createRemoteFlow(
backend: TestBackend,
flowPath: string
): Promise<void> {
const parts = flowPath.split("/");
if (parts[0] === "f" && parts.length > 2) {
await ensureFolder(backend, parts[1]);
}
const resp = await backend.apiRequest!(
`/api/w/${backend.workspace}/flows/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: flowPath,
summary: "Test flow",
description: "A test flow",
value: {
modules: [
{
id: "a",
value: {
type: "rawscript",
content: 'export async function main() { return "flow done"; }',
language: "bun",
input_transforms: {},
},
},
],
},
schema: {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {},
required: [],
},
}),
}
);
expect(resp.status).toBeLessThan(300);
await resp.text();
}
export async function runRemoteFlow(
backend: TestBackend,
flowPath: string,
retries: number = 10
): Promise<string> {
for (let i = 0; i < retries; i++) {
const resp = await backend.apiRequest!(
`/api/w/${backend.workspace}/jobs/run/f/${flowPath}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
}
);
if (resp.status < 300) {
return (await resp.text()).replace(/"/g, "");
}
await new Promise((r) => setTimeout(r, 500));
}
throw new Error(`Failed to run flow ${flowPath} after ${retries} retries`);
}
/**
* Create a multi-step flow with 2 steps (a prints, b returns result).
* Useful for testing hierarchical job get and aggregated logs.
*/
export async function createRemoteMultiStepFlow(
backend: TestBackend,
flowPath: string
): Promise<void> {
const parts = flowPath.split("/");
if (parts[0] === "f" && parts.length > 2) {
await ensureFolder(backend, parts[1]);
}
const resp = await backend.apiRequest!(
`/api/w/${backend.workspace}/flows/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: flowPath,
summary: "Multi-step test flow",
description: "A flow with two steps for testing",
value: {
modules: [
{
id: "a",
summary: "Generate data",
value: {
type: "rawscript",
content:
'export async function main() { console.log("step a running"); return { value: 42 }; }',
language: "bun",
input_transforms: {},
},
},
{
id: "b",
summary: "Process data",
value: {
type: "rawscript",
content:
'export async function main(data: any) { console.log("step b running"); return "done"; }',
language: "bun",
input_transforms: {
data: { type: "javascript", expr: "results.a" },
},
},
},
],
},
schema: {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {},
required: [],
},
}),
}
);
expect(resp.status).toBeLessThan(300);
await resp.text();
}
/**
* Create a flow where step b throws an error.
* Useful for testing failure handling.
*/
export async function createRemoteFailingFlow(
backend: TestBackend,
flowPath: string
): Promise<void> {
const parts = flowPath.split("/");
if (parts[0] === "f" && parts.length > 2) {
await ensureFolder(backend, parts[1]);
}
const resp = await backend.apiRequest!(
`/api/w/${backend.workspace}/flows/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: flowPath,
summary: "Failing test flow",
description: "A flow where step b fails",
value: {
modules: [
{
id: "a",
summary: "Succeeding step",
value: {
type: "rawscript",
content:
'export async function main() { return "ok"; }',
language: "bun",
input_transforms: {},
},
},
{
id: "b",
summary: "Failing step",
value: {
type: "rawscript",
content:
'export async function main() { throw new Error("simulated failure"); }',
language: "bun",
input_transforms: {},
},
},
],
},
schema: {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {},
required: [],
},
}),
}
);
expect(resp.status).toBeLessThan(300);
await resp.text();
}
export async function createRemoteSchedule(
backend: TestBackend,
schedulePath: string,
scriptPath: string
): Promise<void> {
const parts = schedulePath.split("/");
if (parts[0] === "f" && parts.length > 2) {
await ensureFolder(backend, parts[1]);
}
const resp = await backend.apiRequest!(
`/api/w/${backend.workspace}/schedules/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: schedulePath,
schedule: "0 0 */6 * * *",
timezone: "Etc/UTC",
script_path: scriptPath,
is_flow: false,
args: {},
enabled: false,
}),
}
);
expect(resp.status).toBeLessThan(300);
await resp.text();
}