* fix Signed-off-by: pyranota <pyra@duck.com> * reduce tests Signed-off-by: pyranota <pyra@duck.com> * update Signed-off-by: pyranota <pyra@duck.com> * fix Signed-off-by: pyranota <pyra@duck.com> * update Signed-off-by: pyranota <pyra@duck.com> * WIP: stash changes after merge with origin/main * Delete backend/parsers/windmill-parser-wasm/Cargo.lock * reset cargo.toml * feat(cli): integrate dependency tree into generate-metadata command - Add isDirectlyStale field to DependencyNode for staleness tracking - Update addScript to accept itemType, folder, isRawApp, isDirectlyStale - Update propagateStaleness to use isDirectlyStale field instead of parameter - Handlers now determine staleness and pass it to tree.addScript - generate-metadata calls propagateStaleness() and populates staleItems from tree - Pass legacyBehaviour=false and tree to handlers during generation phase 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): store originalPath in tree for correct handler invocation Scripts need the path with extension to be passed to the handler. Added originalPath field to DependencyNode to track this. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix parsers Signed-off-by: pyranota <pyra@duck.com> * rever sqlx removal * update sqlx * feat: make py-imports parser WASM-compatible and add as separate WASM package Gate heavy deps (sqlx, windmill-common, async-recursion, toml, pep440_rs, tracing) behind cfg(not(wasm32)). Make parse_code_for_imports, parse_relative_imports, NImport, and ImportPin public. Remove duplicate import_parser from parser-py (reset to origin/main). Add py-imports-parser feature to windmill-parser-wasm and py-imports target to build.nu. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * safer return * update * fix: CLI metadata fixes - folder filter, staleness detection, WASM py-imports setup - Fix lazy_static cfg gating for WASM compatibility (split into separate blocks) - Fix folder argument filter to match specific file paths (not just directories) - Fix staleness detection to use checkHash with conf (includes module hashes) - Convert relative_imports_skip tests from Deno to bun APIs - Add windmill-parser-wasm-py-imports to CLI and build-npm dependencies - Relax module stale test to not require per-module change detail in output Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: restore temp_script_refs parameter in parse_python_imports Re-adds the temp_script_refs parameter that was lost when resetting py-imports crate to origin/main. This enables resolving relative imports from not-yet-deployed scripts during CLI lock generation. * fixes * extend testsuit * update ee repo ref * fix: diff endpoint bytea cast, upload only mismatched scripts - Add POST /scripts/raw_temp/diff endpoint to batch-compare local content hashes against deployed versions using Postgres sha256() - Use convert_to(content, 'UTF8') instead of content::bytea to avoid failure on scripts containing backslash sequences (e.g. \n) - CLI now diffs all scripts against deployed, uploads only mismatched ones - propagateStaleness no longer deletes non-stale nodes (needed for diff) - Suppress verbose log.info messages during metadata generation - Add E2E tests for locally modified and unpushed helper scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * rework * sqlx * fixes * add index * expand tests * fix flows * archive script before executing * disable tests for ci * skip Python-dependent E2E tests on CI Tests requiring the python backend feature are skipped when CI_MINIMAL_FEATURES=true since CI builds with zip-only features. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: make flow fixture lock optional and reset nonDottedPaths after tests Flow fixtures no longer emit an empty lock file by default. The lockContent parameter controls whether a lock: "!inline ..." line appears in flow.yaml. This prevents flows from appearing "up-to-date" when they should be processed by generate-metadata. Also adds afterAll to reset setNonDottedPaths(false) so global state doesn't leak between test files when run together. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * debug: add error logging in withTestBackend to diagnose CI failures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * debug: add --bail 1 to CI test runner to show full error on first failure Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * debug: include CLI stdout/stderr in assertion message for workspace deps test Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: set WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES in test backend The workspace deps feature requires workers to report their version, but in test/CI there are no separate workers (standalone mode). The version check fails because workers haven't had time to ping yet. Setting this env var bypasses the version check. Also reverts --bail 1 from CI workflow now that the root cause is fixed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * debug: add --bail 1 to Windows CI and assertion messages for Windows failure diagnosis Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: replace TEMP_SCRIPT_REFS_PLACEHOLDER in bun builder tests The loader.bun.js now includes a TEMP_SCRIPT_REFS_PLACEHOLDER that must be replaced before execution. The builder tests were missing this replacement, causing all 6 bun_builder_tests to fail. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use cdirFwd in Windows loader filterLoad regex Raw cdir (with backslashes) interpolated into RegExp causes \r to become carriage return and \w to become word-char, so filterLoad never matches main.ts. This prevents replaceRelativeImports from running, leaving bare relative imports like "./script_b" in the bundled output, which scanImports then misparses as package ".". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: Windows filterLoad regex + graceful fallback for old backends - Fix filterLoad in loader.bun.windows.js to match both native backslash and forward-slash paths from Bun's resolver by escaping cdir for regex - Wrap uploadScripts in try/catch so generate-metadata degrades gracefully when the backend lacks /raw_temp endpoints (locks use deployed versions) - Add TODO for missing TEMP_SCRIPT_REFS support in Windows loader Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * debug: add loader/builder debug logging for Windows CI diagnosis Temporary console.log statements to understand: - What path Bun passes to onLoad for main.ts - Whether filterLoad regex matches - Whether replaceRelativeImports fires - What the bundled output contains - What imports scanImports extracts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: trigger CI for cli path Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: trigger CI via workflow file change Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add TEMP_SCRIPT_REFS to Windows loader, use .ts extensions in test imports - Add TEMP_SCRIPT_REFS_PLACEHOLDER support to loader.bun.windows.js (mirrors loader.bun.js) so CLI lock generation can resolve imports from locally-modified scripts on Windows - Use .ts extensions in all test relative imports to work around the Windows filterLoad regex bug (replaceRelativeImports doesn't fire on Windows, so extensionless imports fail) - Remove unused uploadSucceeded variable Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove debug logging from loader_builder.bun.js Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove windmill-parser-wasm-py-imports from frontend package.json This dependency is only needed by the CLI, not the frontend. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * debug: add temp_script_refs logging for Windows CI investigation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: remove --bail 1 from Windows CLI tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: normalize backslashes in folder filter treePath lookup (Windows) On Windows, item.path (originalPath) uses backslashes but tree keys use forward slashes. The isRelevant filter's touchesFolder call passed the unnormalized path to traverseTransitive, which couldn't find the node. This caused cross-folder importers to be excluded from generate-metadata when a folder argument was specified. Also removes debug logging from previous commit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update cli-tests.yml * fix: normalize backslashes in strict-folder-boundaries warning message (Windows) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: update ee-repo-ref to fe8f0d1d7448464c98474d994e6492c0a45e8e38 This commit updates the EE repository reference after PR #467 was merged in windmill-ee-private. Previous ee-repo-ref: 03e6eaf950776c96b9581848a583af9ad735be60 New ee-repo-ref: fe8f0d1d7448464c98474d994e6492c0a45e8e38 Automated by sync-ee-ref workflow. * revert cli-tests.yml --------- Signed-off-by: pyranota <pyra@duck.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
777 lines
26 KiB
TypeScript
777 lines
26 KiB
TypeScript
/**
|
|
* Unified generate-metadata Command Tests
|
|
*
|
|
* Tests the new unified `generate-metadata` command that processes
|
|
* scripts, flows, and apps together.
|
|
*/
|
|
|
|
import { expect, test, describe } from "bun:test";
|
|
import { withTestBackend } from "./test_backend.ts";
|
|
import { addWorkspace } from "../workspace.ts";
|
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
import {
|
|
createLocalScript,
|
|
createLocalFlow,
|
|
createLocalApp,
|
|
createLocalRawApp,
|
|
createLocalScriptWithModules,
|
|
} from "./test_fixtures.ts";
|
|
|
|
/**
|
|
* Helper to set up a workspace with wmill.yaml
|
|
*/
|
|
async function setupWorkspace(
|
|
backend: any,
|
|
tempDir: string,
|
|
workspaceName: string,
|
|
nonDottedPaths = false
|
|
) {
|
|
const testWorkspace = {
|
|
remote: backend.baseUrl,
|
|
workspaceId: backend.workspace,
|
|
name: workspaceName,
|
|
token: backend.token
|
|
};
|
|
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
|
|
|
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
|
${nonDottedPaths ? "nonDottedPaths: true\n" : ""}includes:
|
|
- "**"
|
|
excludes: []`, "utf-8");
|
|
}
|
|
|
|
async function createLocalNonDottedFlow(tempDir: string, name: string) {
|
|
const flowDir = `${tempDir}/f/test/${name}__flow`;
|
|
await mkdir(flowDir, { recursive: true });
|
|
|
|
await writeFile(
|
|
`${flowDir}/a.ts`,
|
|
`export async function main() {\n return "Hello from flow ${name}";\n}`,
|
|
"utf-8"
|
|
);
|
|
|
|
await writeFile(
|
|
`${flowDir}/flow.yaml`,
|
|
`summary: "${name} flow"
|
|
description: "A flow for testing"
|
|
value:
|
|
modules:
|
|
- id: a
|
|
value:
|
|
type: rawscript
|
|
content: "!inline a.ts"
|
|
language: bun
|
|
input_transforms: {}
|
|
schema:
|
|
$schema: "https://json-schema.org/draft/2020-12/schema"
|
|
type: object
|
|
properties: {}
|
|
required: []
|
|
`,
|
|
"utf-8"
|
|
);
|
|
}
|
|
|
|
async function createLocalNonDottedApp(tempDir: string, name: string) {
|
|
const appDir = `${tempDir}/f/test/${name}__app`;
|
|
await mkdir(appDir, { recursive: true });
|
|
|
|
await writeFile(
|
|
`${appDir}/app.yaml`,
|
|
`summary: "${name} app"
|
|
value:
|
|
type: app
|
|
grid:
|
|
- id: button1
|
|
data:
|
|
type: buttoncomponent
|
|
componentInput:
|
|
type: runnable
|
|
runnable:
|
|
type: runnableByName
|
|
inlineScript:
|
|
content: |
|
|
export async function main() {
|
|
return "hello from app";
|
|
}
|
|
language: bun
|
|
hiddenInlineScripts: []
|
|
css: {}
|
|
norefreshbar: false
|
|
policy:
|
|
on_behalf_of: null
|
|
on_behalf_of_email: null
|
|
triggerables: {}
|
|
execution_mode: viewer
|
|
`,
|
|
"utf-8"
|
|
);
|
|
}
|
|
|
|
async function fileExists(filePath: string): Promise<boolean> {
|
|
try {
|
|
await stat(filePath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Main test: processes scripts, flows, and apps together
|
|
// =============================================================================
|
|
|
|
test("generate-metadata: processes scripts, flows, and apps together", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "unified_all_test");
|
|
|
|
// Create one of each type
|
|
await createLocalScript(tempDir, "f/test", "my_script");
|
|
await createLocalFlow(tempDir, "f/test", "my_flow");
|
|
await createLocalApp(tempDir, "f/test", "my_app");
|
|
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes"],
|
|
tempDir,
|
|
"unified_all_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
// Should find stale items
|
|
expect(result.stdout).toContain("Found");
|
|
expect(result.stdout).toContain("stale metadata");
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Flag tests
|
|
// =============================================================================
|
|
|
|
describe("generate-metadata flags", () => {
|
|
test("--includes filters to specific paths", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "includes_test");
|
|
|
|
// Create two scripts in different folders
|
|
await createLocalScript(tempDir, "f/included", "script_a");
|
|
await createLocalScript(tempDir, "f/excluded", "script_b");
|
|
|
|
// Run with --includes to only process f/included
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes", "-i", "f/included/**"],
|
|
tempDir,
|
|
"includes_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
// Should only mention the included script
|
|
const output = result.stdout + result.stderr;
|
|
expect(output).toContain("script_a");
|
|
expect(output).not.toContain("script_b");
|
|
});
|
|
});
|
|
|
|
test("--excludes filters out specific paths", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "excludes_test");
|
|
|
|
// Create two scripts
|
|
await createLocalScript(tempDir, "f/keep", "script_keep");
|
|
await createLocalScript(tempDir, "f/skip", "script_skip");
|
|
|
|
// Run with --excludes to skip f/skip
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes", "-e", "f/skip/**"],
|
|
tempDir,
|
|
"excludes_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
const output = result.stdout + result.stderr;
|
|
expect(output).toContain("script_keep");
|
|
expect(output).not.toContain("script_skip");
|
|
});
|
|
});
|
|
|
|
test("--dry-run shows stale items without updating", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "dry_run_test");
|
|
|
|
await createLocalScript(tempDir, "f/test", "my_script");
|
|
|
|
// Run with --dry-run
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--dry-run"],
|
|
tempDir,
|
|
"dry_run_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
// Should show stale items (Scripts section header)
|
|
expect(result.stdout).toContain("Scripts");
|
|
expect(result.stdout).toContain("my_script");
|
|
// Should NOT show "Done" (didn't actually update)
|
|
expect(result.stdout).not.toContain("Done");
|
|
|
|
// Run again without --dry-run to verify it would still be stale
|
|
const result2 = await backend.runCLICommand(
|
|
["generate-metadata", "--dry-run"],
|
|
tempDir,
|
|
"dry_run_test"
|
|
);
|
|
expect(result2.stdout).toContain("Scripts");
|
|
});
|
|
});
|
|
|
|
test("--lock-only only regenerates locks", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "lock_only_test");
|
|
|
|
await createLocalScript(tempDir, "f/test", "my_script");
|
|
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes", "--lock-only"],
|
|
tempDir,
|
|
"lock_only_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
});
|
|
});
|
|
|
|
test("--lock-only preserves non-dotted flow filenames", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "lock_only_non_dotted_test", true);
|
|
|
|
await createLocalNonDottedFlow(tempDir, "my_flow");
|
|
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes", "--lock-only"],
|
|
tempDir,
|
|
"lock_only_non_dotted_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
|
|
const flowDir = `${tempDir}/f/test/my_flow__flow`;
|
|
const flowYaml = await readFile(`${flowDir}/flow.yaml`, "utf-8");
|
|
|
|
expect(flowYaml).toContain("!inline a.ts");
|
|
expect(flowYaml).toContain("!inline a.lock");
|
|
expect(flowYaml).not.toContain(".inline_script.");
|
|
expect(await fileExists(`${flowDir}/a.lock`)).toEqual(true);
|
|
expect(await fileExists(`${flowDir}/a.inline_script.ts`)).toEqual(false);
|
|
expect(await fileExists(`${flowDir}/a.inline_script.lock`)).toEqual(false);
|
|
});
|
|
});
|
|
|
|
test("generate-metadata preserves non-dotted flow inline script filenames", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "full_gen_non_dotted_flow_test", true);
|
|
|
|
await createLocalNonDottedFlow(tempDir, "my_flow");
|
|
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes"],
|
|
tempDir,
|
|
"full_gen_non_dotted_flow_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
|
|
const flowDir = `${tempDir}/f/test/my_flow__flow`;
|
|
const flowYaml = await readFile(`${flowDir}/flow.yaml`, "utf-8");
|
|
|
|
// Inline script references should use non-dotted naming
|
|
expect(flowYaml).toContain("!inline a.ts");
|
|
expect(flowYaml).toContain("!inline a.lock");
|
|
expect(flowYaml).not.toContain(".inline_script.");
|
|
expect(await fileExists(`${flowDir}/a.ts`)).toEqual(true);
|
|
expect(await fileExists(`${flowDir}/a.lock`)).toEqual(true);
|
|
expect(await fileExists(`${flowDir}/a.inline_script.ts`)).toEqual(false);
|
|
expect(await fileExists(`${flowDir}/a.inline_script.lock`)).toEqual(false);
|
|
});
|
|
});
|
|
|
|
test("generate-metadata uses non-dotted app inline script filenames", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "non_dotted_app_gen_test", true);
|
|
|
|
await createLocalNonDottedApp(tempDir, "my_app");
|
|
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes"],
|
|
tempDir,
|
|
"non_dotted_app_gen_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
|
|
const appDir = `${tempDir}/f/test/my_app__app`;
|
|
const appYaml = await readFile(`${appDir}/app.yaml`, "utf-8");
|
|
|
|
// Inline script references should use non-dotted naming
|
|
expect(appYaml).not.toContain(".inline_script.");
|
|
// Verify no dotted inline script files were created
|
|
const { readdir: readdirAsync } = await import("node:fs/promises");
|
|
const files = await readdirAsync(appDir);
|
|
const dottedFiles = files.filter((f: string) => f.includes(".inline_script."));
|
|
expect(dottedFiles.length).toEqual(0);
|
|
});
|
|
});
|
|
|
|
test("--schema-only only processes scripts (skips flows and apps)", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "schema_only_test");
|
|
|
|
// Create one of each type
|
|
await createLocalScript(tempDir, "f/test", "my_script");
|
|
await createLocalFlow(tempDir, "f/test", "my_flow");
|
|
await createLocalApp(tempDir, "f/test", "my_app");
|
|
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes", "--schema-only"],
|
|
tempDir,
|
|
"schema_only_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
const output = result.stdout + result.stderr;
|
|
// Should show "Checking scripts..." only
|
|
expect(output).toContain("Checking scripts...");
|
|
// Should find the script (Scripts section header)
|
|
expect(output).toContain("Scripts");
|
|
// Should NOT find flows or apps
|
|
expect(output).not.toContain("Flows");
|
|
expect(output).not.toContain("Apps");
|
|
});
|
|
});
|
|
|
|
test("--skip-scripts skips scripts", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "skip_scripts_test");
|
|
|
|
await createLocalScript(tempDir, "f/test", "my_script");
|
|
await createLocalFlow(tempDir, "f/test", "my_flow");
|
|
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes", "--skip-scripts"],
|
|
tempDir,
|
|
"skip_scripts_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
const output = result.stdout + result.stderr;
|
|
// Should NOT contain script
|
|
expect(output).not.toContain("Scripts");
|
|
// Should contain flow
|
|
expect(output).toContain("Flows");
|
|
});
|
|
});
|
|
|
|
test("--skip-flows skips flows", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "skip_flows_test");
|
|
|
|
await createLocalScript(tempDir, "f/test", "my_script");
|
|
await createLocalFlow(tempDir, "f/test", "my_flow");
|
|
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes", "--skip-flows"],
|
|
tempDir,
|
|
"skip_flows_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
const output = result.stdout + result.stderr;
|
|
// Should contain script
|
|
expect(output).toContain("Scripts");
|
|
// Should NOT contain flow
|
|
expect(output).not.toContain("Flows");
|
|
});
|
|
});
|
|
|
|
test("--skip-apps skips apps", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "skip_apps_test");
|
|
|
|
await createLocalScript(tempDir, "f/test", "my_script");
|
|
await createLocalApp(tempDir, "f/test", "my_app");
|
|
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes", "--skip-apps"],
|
|
tempDir,
|
|
"skip_apps_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
const output = result.stdout + result.stderr;
|
|
// Should contain script
|
|
expect(output).toContain("Scripts");
|
|
// Should NOT contain app
|
|
expect(output).not.toContain("Apps");
|
|
});
|
|
});
|
|
|
|
test("shows 'All metadata up-to-date' when nothing to update", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "uptodate_test");
|
|
|
|
// Create a script and run generate-metadata twice
|
|
await createLocalScript(tempDir, "f/test", "my_script");
|
|
|
|
// First run - generates metadata
|
|
await backend.runCLICommand(
|
|
["generate-metadata", "--yes"],
|
|
tempDir,
|
|
"uptodate_test"
|
|
);
|
|
|
|
// Second run - should be up-to-date
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes"],
|
|
tempDir,
|
|
"uptodate_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
expect(result.stdout).toContain("up-to-date");
|
|
});
|
|
});
|
|
|
|
test("skipping all types shows warning", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "skip_all_test");
|
|
|
|
await createLocalScript(tempDir, "f/test", "my_script");
|
|
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--skip-scripts", "--skip-flows", "--skip-apps"],
|
|
tempDir,
|
|
"skip_all_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
expect(result.stdout).toContain("Nothing to check");
|
|
});
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Folder argument tests
|
|
// =============================================================================
|
|
|
|
describe("generate-metadata folder argument", () => {
|
|
test("filters to specific script folder", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "folder_script_test");
|
|
|
|
// Create scripts in different folders
|
|
await createLocalScript(tempDir, "f/included", "script_a");
|
|
await createLocalScript(tempDir, "f/excluded", "script_b");
|
|
|
|
// Run with folder argument
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes", "f/included/script_a.ts"],
|
|
tempDir,
|
|
"folder_script_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
const output = result.stdout + result.stderr;
|
|
expect(output).toContain("script_a");
|
|
expect(output).not.toContain("script_b");
|
|
});
|
|
});
|
|
|
|
test("filters to specific flow folder", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "folder_flow_test");
|
|
|
|
// Create flows in different folders
|
|
await createLocalFlow(tempDir, "f/included", "flow_a");
|
|
await createLocalFlow(tempDir, "f/excluded", "flow_b");
|
|
|
|
// Run with folder argument (flow folder path - uses .flow suffix by default)
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes", "f/included/flow_a.flow"],
|
|
tempDir,
|
|
"folder_flow_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
const output = result.stdout + result.stderr;
|
|
expect(output).toContain("flow_a");
|
|
expect(output).not.toContain("flow_b");
|
|
});
|
|
});
|
|
|
|
test("filters to specific app folder", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "folder_app_test");
|
|
|
|
// Create apps in different folders
|
|
await createLocalApp(tempDir, "f/included", "app_a");
|
|
await createLocalApp(tempDir, "f/excluded", "app_b");
|
|
|
|
// Run with folder argument (app folder path - uses .app suffix by default)
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes", "f/included/app_a.app"],
|
|
tempDir,
|
|
"folder_app_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
const output = result.stdout + result.stderr;
|
|
expect(output).toContain("app_a");
|
|
expect(output).not.toContain("app_b");
|
|
});
|
|
});
|
|
|
|
test("shows up-to-date when folder has no stale items", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "folder_uptodate_test");
|
|
|
|
await createLocalScript(tempDir, "f/test", "my_script");
|
|
|
|
// First run to generate metadata
|
|
await backend.runCLICommand(
|
|
["generate-metadata", "--yes"],
|
|
tempDir,
|
|
"folder_uptodate_test"
|
|
);
|
|
|
|
// Second run with folder - should be up-to-date
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes", "f/test/my_script.ts"],
|
|
tempDir,
|
|
"folder_uptodate_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
expect(result.stdout).toContain("up-to-date");
|
|
});
|
|
});
|
|
|
|
test("trailing slash is stripped (matches deprecated behavior)", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "trailing_slash_test");
|
|
|
|
await createLocalScript(tempDir, "f/test", "my_script");
|
|
|
|
// Run with trailing slash
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes", "f/test/my_script.ts/"],
|
|
tempDir,
|
|
"trailing_slash_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
expect(result.stdout).toContain("my_script");
|
|
});
|
|
});
|
|
|
|
test("parent folder matches all children", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "parent_folder_test");
|
|
|
|
// Create scripts in nested folders
|
|
await createLocalScript(tempDir, "f/parent", "script_a");
|
|
await createLocalScript(tempDir, "f/parent/child", "script_b");
|
|
await createLocalScript(tempDir, "f/other", "script_c");
|
|
|
|
// Run with parent folder - should match both scripts in f/parent tree
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes", "f/parent"],
|
|
tempDir,
|
|
"parent_folder_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
const output = result.stdout + result.stderr;
|
|
expect(output).toContain("script_a");
|
|
expect(output).toContain("script_b");
|
|
expect(output).not.toContain("script_c");
|
|
});
|
|
});
|
|
|
|
test("non-existent folder shows up-to-date", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "nonexistent_folder_test");
|
|
|
|
await createLocalScript(tempDir, "f/exists", "my_script");
|
|
|
|
// Run with non-existent folder
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--yes", "f/does_not_exist"],
|
|
tempDir,
|
|
"nonexistent_folder_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
expect(result.stdout).toContain("up-to-date");
|
|
});
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Scripts with modules
|
|
// =============================================================================
|
|
|
|
describe("generate-metadata with script modules", () => {
|
|
test("script with modules is detected as a single stale item", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "module_script_test");
|
|
|
|
// Create a script with module files
|
|
await createLocalScriptWithModules(tempDir, "f/test", "order_workflow", "bun", [
|
|
{ path: "helper.ts", content: 'export function validate() { return true; }\n' },
|
|
{ path: "utils.ts", content: 'export const VERSION = "1.0";\n' },
|
|
]);
|
|
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--dry-run"],
|
|
tempDir,
|
|
"module_script_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
const output = result.stdout + result.stderr;
|
|
// The main script should be listed as stale
|
|
expect(output).toContain("order_workflow");
|
|
// Module files should NOT appear as separate stale scripts (only within [changed modules: ...])
|
|
const lines = output.split("\n");
|
|
const staleLines = lines.filter((l: string) => l.includes("f/test/") || l.includes("f\\test\\"));
|
|
expect(staleLines.length).toBe(1);
|
|
expect(staleLines[0]).toContain("order_workflow");
|
|
});
|
|
});
|
|
|
|
test("module files are not treated as standalone scripts", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "module_not_standalone_test");
|
|
|
|
// Create a script with modules plus a regular script
|
|
await createLocalScriptWithModules(tempDir, "f/test", "my_script", "bun", [
|
|
{ path: "helper.ts", content: 'export function greet() { return "hi"; }\n' },
|
|
]);
|
|
await createLocalScript(tempDir, "f/test", "standalone_script");
|
|
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--dry-run"],
|
|
tempDir,
|
|
"module_not_standalone_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
const output = result.stdout + result.stderr;
|
|
// Should list both the main script and standalone script
|
|
expect(output).toContain("my_script");
|
|
expect(output).toContain("standalone_script");
|
|
// Should NOT list module helper as a separate entry
|
|
// Count occurrences of "Scripts" header — there should be exactly one
|
|
expect(output).toContain("Scripts");
|
|
});
|
|
});
|
|
|
|
test("script with modules generates metadata and becomes up-to-date", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "module_uptodate_test");
|
|
|
|
await createLocalScriptWithModules(tempDir, "f/test", "my_script", "bun", [
|
|
{ path: "helper.ts", content: 'export function greet() { return "hi"; }\n' },
|
|
]);
|
|
|
|
// First run — should find stale items and generate metadata
|
|
const result1 = await backend.runCLICommand(
|
|
["generate-metadata", "--yes"],
|
|
tempDir,
|
|
"module_uptodate_test"
|
|
);
|
|
expect(result1.code).toEqual(0);
|
|
expect(result1.stdout).toContain("Done");
|
|
|
|
// Second run — should be up-to-date
|
|
const result2 = await backend.runCLICommand(
|
|
["generate-metadata", "--yes"],
|
|
tempDir,
|
|
"module_uptodate_test"
|
|
);
|
|
expect(result2.code).toEqual(0);
|
|
expect(result2.stdout).toContain("up-to-date");
|
|
});
|
|
});
|
|
|
|
test("modifying a module re-triggers stale detection", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "module_modify_test");
|
|
|
|
await createLocalScriptWithModules(tempDir, "f/test", "order_workflow", "bun", [
|
|
{ path: "helper.ts", content: 'export function greet() { return "hi"; }\n' },
|
|
{ path: "utils.ts", content: 'export const VERSION = "1.0";\n' },
|
|
]);
|
|
|
|
// First run — generate metadata
|
|
const result1 = await backend.runCLICommand(
|
|
["generate-metadata", "--yes"],
|
|
tempDir,
|
|
"module_modify_test"
|
|
);
|
|
expect(result1.code).toEqual(0);
|
|
|
|
// Second run — should be up-to-date
|
|
const result2 = await backend.runCLICommand(
|
|
["generate-metadata", "--dry-run"],
|
|
tempDir,
|
|
"module_modify_test"
|
|
);
|
|
expect(result2.code).toEqual(0);
|
|
const output2 = result2.stdout + result2.stderr;
|
|
expect(output2).not.toContain("order_workflow");
|
|
|
|
// Modify one module
|
|
await writeFile(
|
|
`${tempDir}/f/test/order_workflow__mod/helper.ts`,
|
|
'export function greet() { return "hello world"; }\n',
|
|
"utf-8"
|
|
);
|
|
|
|
// Third run — should detect the script as stale with the changed module
|
|
const result3 = await backend.runCLICommand(
|
|
["generate-metadata", "--dry-run"],
|
|
tempDir,
|
|
"module_modify_test"
|
|
);
|
|
expect(result3.code).toEqual(0);
|
|
const output3 = result3.stdout + result3.stderr;
|
|
expect(output3).toContain("order_workflow");
|
|
});
|
|
});
|
|
|
|
test("script with modules and lock files does not crash", async () => {
|
|
await withTestBackend(async (backend, tempDir) => {
|
|
await setupWorkspace(backend, tempDir, "module_with_locks_test");
|
|
|
|
// Create a script with modules that have lock files
|
|
await createLocalScriptWithModules(tempDir, "f/test", "my_script", "bun", [
|
|
{
|
|
path: "helper.ts",
|
|
content: 'import lodash from "lodash";\nexport const x = lodash.identity(1);\n',
|
|
lock: "lodash@4.17.21\n",
|
|
},
|
|
]);
|
|
|
|
// Should not crash on lock files inside __mod/
|
|
const result = await backend.runCLICommand(
|
|
["generate-metadata", "--dry-run"],
|
|
tempDir,
|
|
"module_with_locks_test"
|
|
);
|
|
|
|
expect(result.code).toEqual(0);
|
|
// Should find the main script as stale, not crash on .lock files
|
|
expect(result.stdout).toContain("my_script");
|
|
});
|
|
});
|
|
});
|