mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
4427a3d37f
* feat: add workspace-specific flag for resources and variables Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove set_ws_specific endpoint and fix rust-client compilation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: fall back to workspace name for ws_specific file naming When wsNameForFiles is not set (no wmill.yaml workspace config), ws_specific items would not get workspace-suffixed filenames during pull. Now falls back to workspace.name/workspaceId. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use workspace ID instead of CLI name for ws_specific file naming Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pass workspace ID fallback to elementsToMap for ws_specific push Without this, workspace-specific files (e.g., a.admins.resource.yaml) were not recognized during push when no wmill.yaml or git branch was available, causing spurious deletions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ui nits * nit * Fix variable edit when only editing ws_specific * mark_linked_variables_ws_specific * Helper label * Support json format alongside yaml * Fix file naming push/pull asymetry & ws_specific orphans * Revert all CLI diffs * CLI now appends the remote ws_specific list to the local specificItems * UI for Env switcher * Refactor Resource/Variable editors to use dumb component * Refactor side effects * Editor works with multi workspaces * Fix can_save * Fix As JSON * nit * UI nits * list_ws_specific_versions as pl sql function to avoid round trips * UI Nits * Per-workspace version read-only check * fix: reset session context in list_ws_specific_versions to prevent RLS leakage The function calls set_session_context() in a loop. Although SET LOCAL is transaction-scoped (so settings revert at autocommit), defending against the function being invoked inside a longer outer transaction: - wrap the loop in a sub-block with EXCEPTION WHEN OTHERS that resets the session to a deny-default (windmill_user, empty session.* GUCs) before re-raising, - on the happy path, reset to the same deny-default at the end of the function. * feat: audit auto-marked ws_specific variables When a resource is saved as ws_specific, every variable referenced via $var: inside its value is auto-INSERTed into ws_specific. Previously this happened silently. Now: - mark_linked_variables_ws_specific takes the authed user, - the INSERT uses RETURNING path so we know exactly which variables were freshly flipped (not the ones already ws_specific), - each newly flipped variable gets a 'variables.set_ws_specific' audit entry pointing at the resource that triggered it. * perf: skip mark_linked_variables_ws_specific when nothing relevant changed update_resource was calling mark_linked_variables_ws_specific on every save when the resource was ws_specific, even on a description-only or label-only edit. Gate the call on `ns.value.is_some() || ns.ws_specific == Some(true)` so we only re-mark when the $var: refs could actually have changed or ws_specific was freshly enabled. * docs: explain asymmetric ws_specific toggle in resource tooltip Enabling the resource's 'Workspace specific' toggle silently marks every variable referenced via $var: inside the value as ws_specific, but disabling it does not un-mark those variables (they may be referenced by other resources). Surface this in the tooltip so users know what to expect. * fix: surface non-404 errors when fetching ws_specific items in CLI sync mergeWsSpecificFromServer was catching every error from listWsSpecific and logging it at debug. That's correct for old servers without the endpoint (404), but a 401/403/network failure would silently produce an incomplete sync. Now distinguish 404 (debug, expected) from everything else (warn with status + message) so users notice when the merge fails for real reasons. * perf: collapse compare_two_variables presence checks into one round-trip The early-return path was issuing four sequential EXISTS queries (ws_specific × {source, fork}, variable × {source, fork}). Combine them into a single SELECT so the per-variable diff cost drops ~4x. * sqlx prepare * docs: clarify has_sql_updates invariant in update_variable The else branch of the npath resolution is only reachable for non-rename edits (labels-only, ws_specific-only) because ns.path being Some always forces has_sql_updates=true at the top of the function. Add a debug_assert and a comment explaining the invariant so a future change that decouples ns.path from has_sql_updates trips immediately. Also use `path` directly instead of unwrap_or_default-ing ns.path, since we know it's None here. * chore: drop redundant ws_specific type augmentations ListableResource and ListableVariable from $lib/gen now include `ws_specific?: boolean` after the openapi.yaml additions in this branch were regenerated. The intersection types in resources/+page and variables/+page were duplicating the field — drop them. * Put WsSpecificVersions toggle in top drawer bar * nit size * feat: detect local-only ws_specific items on sync push When wmill.yaml lists a resource/variable in specificItems but the remote isn't yet marked ws_specific for that item, sync push silently dropped the flag because: 1. file-content diff alone never noticed (ws_specific is metadata, not YAML body) — push{Resource,Variable} were never called for those items; 2. even when called, isSuperset(local, remote) returned true and the early-return skipped the API call. Now: - mergeWsSpecificFromServer returns the raw server list alongside the merged config so push can compare 'in local' vs 'in server'; - a new computeWsSpecificFlagOnlyPushes helper walks the local file map, finds ws_specific-flagged paths absent from the server list, and the push function injects them as synthetic 'edited' changes (same before and after content) so the standard display + apply pipeline picks them up; - push{Resource,Variable} no longer early-return when content matches but the ws_specific flag differs. Pull is unaffected — only the push-side caller of mergeWsSpecific takes the new (merged, serverItems) tuple. * getDeployTo for selected ws * refactor: ws_specific kind handling, support .json files The ws_specific helpers had two warts: 1. computeWsSpecificFlagOnlyPushes hardcoded `.resource.{yaml,json}` / `.variable.{yaml,json}` magic strings, even though the existing getTypeStrFromPath / removeType helpers already do that work and already cover both extensions. 2. isSpecificItem / isItemTypeConfigured only matched `.yaml` paths, so users with opts.json local files got no specificItems coverage at all — patterns from wmill.yaml (and from mergeWsSpecificFromServer) are expressed with `.yaml`, and a `.json` file never matched. Changes: - Replace WS_SPECIFIC_KIND_MAP (a closed enum of resource+variable) with configKeyForItemKind, a generic kind→SpecificItemsConfig key mapping. Triggers fold into 'triggers' via the `_trigger` suffix, so adding a kind to the backend's list_ws_specific_versions doesn't require a CLI change. - mergeWsSpecificFromServer now appends `${item.path}.${item.item_kind}.yaml` through the same helper. - computeWsSpecificFlagOnlyPushes uses getTypeStrFromPath + removeType, gated by configKeyForItemKind. No more magic strings. - isSpecificItem and isItemTypeConfigured normalize trailing `.json` to `.yaml` once at the entry, so a single set of patterns covers both extensions for the same logical item. * refactor: dedicated change type for ws_specific flag-only pushes Previously the sync push code injected a synthetic 'edited' Change with before === after to nudge the apply loop into calling pushResource / pushVariable for ws_specific-flag-only diffs, and a guard inside those two functions skipped the early-return when the flag differed. The contract was implicit and easy to break — any future 'skip identical edits' optimization in the change pipeline would silently drop these pushes. Replace with an explicit Change variant: type WsSpecificFlag = { name: 'ws_specific_flag'; path: string; kind: string; wsSpecific: boolean; }; The push apply loop now has a dedicated branch for it that calls wmill.updateResource / updateVariable with just the ws_specific flag. prettyChanges renders it on its own line. The dry-run JSON output picks it up via the existing change.name / change.path passthrough. The defensive wsSpecificMatches check inside push{Resource,Variable} is no longer needed (sync push doesn't go through them for flag-only diffs) and is reverted. * drop folders * feat(cli): warn on remote ws_specific items missing from local config When 'wmill sync pull' fetches the server's ws_specific list, items the server marks as ws_specific but that aren't matched by the local wmill.yaml's specificItems patterns now produce a warning. The merge already preserves correctness (those items are still treated as ws_specific during this pull), but the user's config drifts from the remote — and a later push from another machine without that config would push the item as non-ws_specific. Surface the drift so the user can update wmill.yaml. Also filter ws_specific_flag changes out before preCheckPermissionedAs (it expects added/edited/deleted only and they have no content payload so on_behalf_of resolution doesn't apply). * fix(cli): scope ws_specific drift warning to items in this pull's changes Previously the warning iterated every ws_specific item the server returned, producing log spam for items unrelated to the current pull (items that exist locally with no change, or items the user has nothing to do with this round). Move the loop after compareDynFSElement and only warn for items whose path appears in the changes list — i.e., items the user is actually pulling right now. * fix: clean up linked-side ws_specific rows on resource/variable delete Three places left orphaned ws_specific rows behind: 1. delete_resource deleted the resource's own ws_specific row and the linked variables, but never the ws_specific 'variable' rows that mark_linked_variables_ws_specific had auto-inserted for those variable paths. 2. delete_variable deleted its own ws_specific row and the linked resource at the same path, but never a ws_specific 'resource' row at that path. 3. delete_resources_bulk didn't even cascade to linked variables, let alone clean up their ws_specific rows. A new resource or variable later created at one of those paths would silently inherit a stale ws_specific flag — list_ws_specific would report it as workspace-specific, workspace diffs would treat it as 'no changes', and CLI sync would skip it. Fix: - delete_resource: DELETE FROM ws_specific WHERE item_kind = 'variable' AND path = ANY(linked_var_paths) before the linked-variable delete. - delete_variable: DELETE FROM ws_specific WHERE item_kind = 'resource' AND path = path before the linked-resource delete. - delete_resources_bulk: collect $var: refs from each bulk-deleted resource (mirror of single delete), then delete ws_specific 'variable' rows AND the variable rows themselves. Brings bulk delete in line with single delete semantics, including the orphan cleanup. * fix: gate list_ws_specific by resource/variable RLS The endpoint queried ws_specific directly under user_db, but ws_specific itself has no per-item RLS — only a workspace-level column. Any workspace member could enumerate every ws_specific path including those in folders they lack read access to (e.g. f/finance/prod_db_creds), revealing path existence that list_resources / list_variables would have hidden. Add EXISTS clauses against resource and variable so the same path-based RLS policies that govern those tables (see_own / see_member / see_extra_perms_user / see_extra_perms_groups / see_folder_extra_perms_user) also gate visibility here. The user transaction already establishes the session context; the joins make the policies apply. * only resources and variables * fix(cli): make workspace-specific path mapping handle .json files isSpecificItem() was extended to normalize .json -> .yaml so .json files could be matched against patterns, but the surrounding helpers remained yaml-only: - toWorkspaceSpecificPath only mapped folder.meta.yaml / settings.yaml / .X.yaml — a foo.resource.json went through unchanged, so the workspace-specific filename was never produced. - fromWorkspaceSpecificPath only matched .yaml extensions — pushing foo.dev.resource.json could not map back to foo.resource.json. - isCurrentWorkspaceFile / isWorkspaceSpecificFile regexes ended in \.yaml$, missing every branch-specific .json file. Replace the literal '.yaml' anchors with '(yaml|json)' alternations, preserve the actual extension on round-trips, and rename the helper buildYamlTypePattern -> buildItemTypePattern (it never had anything extension-specific in it). getFileTypeSuffix now returns the matching suffix for either extension. Changed: - getFileTypeSuffix - toWorkspaceSpecificPath / fromWorkspaceSpecificPath - isCurrentWorkspaceFile / isWorkspaceSpecificFile - isTriggerFile / isScheduleFile isItemTypeConfigured / isSpecificItem don't need touching — their checks run after normalizeJsonToYaml(), which already collapses both extensions to .yaml at the entry. * fix: create_resource?update_if_exists=true honors ws_specific=false The upsert path matched on `unwrap_or(false)`, so an explicit `ws_specific: false` and an absent flag were indistinguishable — both fell through with no DELETE on the existing ws_specific row. Callers trying to clear the flag via PUT-with-update_if_exists silently saw their request ignored. Mirror update_resource's three-way handling: Some(true) -> INSERT (+ mark linked variables) Some(false) -> DELETE (only when update_if_exists, since a pure create has no existing row anyway) None -> leave the existing flag alone create_variable doesn't have an upsert path (no ON CONFLICT), so the same bug doesn't apply there. * sqlx prepare * test: cover ws_specific cleanup, RLS filtering, upsert clearing, and CLI .json paths Backend (backend/tests/ws_specific.rs + fixture): - test_linked_delete_cleanup: creates a ws_specific resource that references a variable via $var:, deletes the resource, asserts the cross-kind ws_specific row for the auto-marked variable is also removed. Then does the inverse for delete_variable, verifying the ws_specific 'resource' row at the same path is cleaned by variable delete. - test_list_ws_specific_filters_by_rls: admin creates ws_specific items in u/test-user/ and u/test-user-2/; verifies admin sees both via list_ws_specific while a non-admin (test-user-2) only sees their own path — the RLS see_own policy on the joined resource/variable tables hides the other. - test_create_resource_upsert_clears_ws_specific: covers the three-way Option<bool> handling on the upsert path: Some(true) inserts, Some(false) clears the existing row, None leaves it alone. CLI: - specific_items_unit.test.ts: add 14 tests covering toWorkspaceSpecificPath / fromWorkspaceSpecificPath / isWorkspaceSpecificFile / isCurrentWorkspaceFile / isSpecificItem / isItemTypeConfigured for .json files (variable, resource, trigger, schedule, folder.meta, settings). - ws_specific_flag_only_unit.test.ts (new): covers computeWsSpecificFlagOnlyPushes — emits flag-only changes only for resource and variable kinds (the backend's list_ws_specific_versions scope), does not emit for schedules or triggers, returns empty when serverItems is null (older server), respects existing server entries, preserves .json extension on filePath. - Export computeWsSpecificFlagOnlyPushes so it can be unit-tested. * perf: index workspace_settings.deploy_to for the recursive CTE list_ws_specific_versions's recursive CTE probes WHERE ws.deploy_to = r.ws_id every iteration; without an index on workspace_settings.deploy_to each iteration seq-scans the table — at 10M workspaces with the depth cap of 32 that's up to 320M row reads per call. deploy_to is sparse (most workspaces don't deploy anywhere), so a partial index WHERE deploy_to IS NOT NULL stays small while still covering every probe. Tucked into the existing migration since the function and the index ship together. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
988 lines
40 KiB
TypeScript
988 lines
40 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
|
|
// =============================================================================
|
|
// SPECIFIC ITEMS UNIT TESTS
|
|
// Tests for workspace-specific file path functions (no Docker required)
|
|
// =============================================================================
|
|
|
|
// Import the functions we need to test
|
|
import {
|
|
isSpecificItem,
|
|
isItemTypeConfigured,
|
|
toWorkspaceSpecificPath,
|
|
fromWorkspaceSpecificPath,
|
|
isWorkspaceSpecificFile,
|
|
isCurrentWorkspaceFile,
|
|
getWorkspaceSpecificPath,
|
|
getSpecificItemsForCurrentBranch,
|
|
} from "../src/core/specific_items.ts";
|
|
|
|
import type { SpecificItemsConfig } from "../src/core/specific_items.ts";
|
|
|
|
// =============================================================================
|
|
// toWorkspaceSpecificPath TESTS
|
|
// =============================================================================
|
|
|
|
test("toWorkspaceSpecificPath: converts variable path to workspace-specific", () => {
|
|
const result = toWorkspaceSpecificPath("f/test.variable.yaml", "main");
|
|
expect(result).toEqual("f/test.main.variable.yaml");
|
|
});
|
|
|
|
test("toWorkspaceSpecificPath: converts resource path to workspace-specific", () => {
|
|
const result = toWorkspaceSpecificPath("u/admin/db.resource.yaml", "develop");
|
|
expect(result).toEqual("u/admin/db.develop.resource.yaml");
|
|
});
|
|
|
|
test("toWorkspaceSpecificPath: converts trigger path to workspace-specific", () => {
|
|
const result = toWorkspaceSpecificPath("f/my_trigger.http_trigger.yaml", "feature-x");
|
|
expect(result).toEqual("f/my_trigger.feature-x.http_trigger.yaml");
|
|
});
|
|
|
|
test("toWorkspaceSpecificPath: sanitizes branch names with slashes", () => {
|
|
const result = toWorkspaceSpecificPath("f/test.variable.yaml", "feature/my-feature");
|
|
expect(result).toEqual("f/test.feature_my-feature.variable.yaml");
|
|
});
|
|
|
|
test("toWorkspaceSpecificPath: sanitizes branch names with dots", () => {
|
|
const result = toWorkspaceSpecificPath("f/test.variable.yaml", "release.1.0");
|
|
expect(result).toEqual("f/test.release_1_0.variable.yaml");
|
|
});
|
|
|
|
test("toWorkspaceSpecificPath: leaves non-specific files unchanged", () => {
|
|
const result = toWorkspaceSpecificPath("f/script.ts", "main");
|
|
expect(result).toEqual("f/script.ts");
|
|
});
|
|
|
|
test("toWorkspaceSpecificPath: handles resource files with extensions", () => {
|
|
const result = toWorkspaceSpecificPath("f/config.resource.file.json", "main");
|
|
expect(result).toEqual("f/config.main.resource.file.json");
|
|
});
|
|
|
|
// =============================================================================
|
|
// fromWorkspaceSpecificPath TESTS
|
|
// =============================================================================
|
|
|
|
test("fromWorkspaceSpecificPath: converts workspace-specific variable back to base", () => {
|
|
const result = fromWorkspaceSpecificPath("f/test.main.variable.yaml", "main");
|
|
expect(result).toEqual("f/test.variable.yaml");
|
|
});
|
|
|
|
test("fromWorkspaceSpecificPath: converts workspace-specific resource back to base", () => {
|
|
const result = fromWorkspaceSpecificPath("u/admin/db.develop.resource.yaml", "develop");
|
|
expect(result).toEqual("u/admin/db.resource.yaml");
|
|
});
|
|
|
|
test("fromWorkspaceSpecificPath: converts workspace-specific trigger back to base", () => {
|
|
const result = fromWorkspaceSpecificPath("f/my_trigger.feature-x.http_trigger.yaml", "feature-x");
|
|
expect(result).toEqual("f/my_trigger.http_trigger.yaml");
|
|
});
|
|
|
|
test("fromWorkspaceSpecificPath: handles sanitized branch names", () => {
|
|
const result = fromWorkspaceSpecificPath("f/test.feature_my-feature.variable.yaml", "feature/my-feature");
|
|
expect(result).toEqual("f/test.variable.yaml");
|
|
});
|
|
|
|
test("fromWorkspaceSpecificPath: returns unchanged if not workspace-specific", () => {
|
|
const result = fromWorkspaceSpecificPath("f/test.variable.yaml", "main");
|
|
expect(result).toEqual("f/test.variable.yaml");
|
|
});
|
|
|
|
test("fromWorkspaceSpecificPath: handles resource files with extensions", () => {
|
|
const result = fromWorkspaceSpecificPath("f/config.main.resource.file.json", "main");
|
|
expect(result).toEqual("f/config.resource.file.json");
|
|
});
|
|
|
|
// =============================================================================
|
|
// isSpecificItem TESTS
|
|
// =============================================================================
|
|
|
|
test("isSpecificItem: returns false when specificItems is undefined", () => {
|
|
const result = isSpecificItem("f/test.variable.yaml", undefined);
|
|
expect(result).toEqual(false);
|
|
});
|
|
|
|
test("isSpecificItem: matches variable paths with glob pattern", () => {
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/**"],
|
|
};
|
|
expect(isSpecificItem("f/test.variable.yaml", config)).toEqual(true);
|
|
expect(isSpecificItem("u/admin/test.variable.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
test("isSpecificItem: matches resource paths with glob pattern", () => {
|
|
const config: SpecificItemsConfig = {
|
|
resources: ["u/admin/**"],
|
|
};
|
|
expect(isSpecificItem("u/admin/db.resource.yaml", config)).toEqual(true);
|
|
expect(isSpecificItem("f/db.resource.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
test("isSpecificItem: matches trigger paths with glob pattern", () => {
|
|
const config: SpecificItemsConfig = {
|
|
triggers: ["f/triggers/**"],
|
|
};
|
|
expect(isSpecificItem("f/triggers/my.http_trigger.yaml", config)).toEqual(true);
|
|
expect(isSpecificItem("u/admin/my.http_trigger.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
test("isSpecificItem: matches multiple patterns", () => {
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/**", "g/**"],
|
|
};
|
|
expect(isSpecificItem("f/test.variable.yaml", config)).toEqual(true);
|
|
expect(isSpecificItem("g/test.variable.yaml", config)).toEqual(true);
|
|
expect(isSpecificItem("u/admin/test.variable.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
test("isSpecificItem: handles exact path patterns", () => {
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/specific.variable.yaml"],
|
|
};
|
|
expect(isSpecificItem("f/specific.variable.yaml", config)).toEqual(true);
|
|
expect(isSpecificItem("f/other.variable.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
// =============================================================================
|
|
// isWorkspaceSpecificFile TESTS
|
|
// =============================================================================
|
|
|
|
test("isWorkspaceSpecificFile: detects workspace-specific variable files", () => {
|
|
expect(isWorkspaceSpecificFile("f/test.main.variable.yaml")).toEqual(true);
|
|
expect(isWorkspaceSpecificFile("f/test.develop.variable.yaml")).toEqual(true);
|
|
expect(isWorkspaceSpecificFile("f/test.feature_branch.variable.yaml")).toEqual(true);
|
|
});
|
|
|
|
test("isWorkspaceSpecificFile: detects workspace-specific resource files", () => {
|
|
expect(isWorkspaceSpecificFile("u/admin/db.main.resource.yaml")).toEqual(true);
|
|
expect(isWorkspaceSpecificFile("u/admin/db.staging.resource.yaml")).toEqual(true);
|
|
});
|
|
|
|
test("isWorkspaceSpecificFile: detects workspace-specific trigger files", () => {
|
|
expect(isWorkspaceSpecificFile("f/my.main.http_trigger.yaml")).toEqual(true);
|
|
expect(isWorkspaceSpecificFile("f/my.develop.kafka_trigger.yaml")).toEqual(true);
|
|
expect(isWorkspaceSpecificFile("f/my.main.websocket_trigger.yaml")).toEqual(true);
|
|
});
|
|
|
|
test("isWorkspaceSpecificFile: returns false for non-workspace-specific files", () => {
|
|
expect(isWorkspaceSpecificFile("f/test.variable.yaml")).toEqual(false);
|
|
expect(isWorkspaceSpecificFile("u/admin/db.resource.yaml")).toEqual(false);
|
|
expect(isWorkspaceSpecificFile("f/my.http_trigger.yaml")).toEqual(false);
|
|
expect(isWorkspaceSpecificFile("f/script.ts")).toEqual(false);
|
|
});
|
|
|
|
test("isWorkspaceSpecificFile: handles resource files with extensions", () => {
|
|
expect(isWorkspaceSpecificFile("f/config.main.resource.file.json")).toEqual(true);
|
|
expect(isWorkspaceSpecificFile("f/config.resource.file.json")).toEqual(false);
|
|
});
|
|
|
|
// =============================================================================
|
|
// ROUND-TRIP TESTS
|
|
// =============================================================================
|
|
|
|
test("round-trip: variable file path conversion", () => {
|
|
const original = "f/my/nested/config.variable.yaml";
|
|
const branch = "feature/test-branch";
|
|
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
|
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
|
expect(restored).toEqual(original);
|
|
});
|
|
|
|
test("round-trip: resource file path conversion", () => {
|
|
const original = "u/admin/database.resource.yaml";
|
|
const branch = "develop";
|
|
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
|
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
|
expect(restored).toEqual(original);
|
|
});
|
|
|
|
test("round-trip: trigger file path conversion", () => {
|
|
const original = "f/webhooks/handler.http_trigger.yaml";
|
|
const branch = "main";
|
|
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
|
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
|
expect(restored).toEqual(original);
|
|
});
|
|
|
|
test("round-trip: resource file with extension", () => {
|
|
const original = "f/configs/settings.resource.file.ini";
|
|
const branch = "release/v1.0";
|
|
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
|
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
|
expect(restored).toEqual(original);
|
|
});
|
|
|
|
// =============================================================================
|
|
// BRANCH OVERRIDE TESTS (for --branch flag functionality)
|
|
// These tests validate that functions work correctly with explicit branch override
|
|
// =============================================================================
|
|
|
|
test("branchOverride: getWorkspaceSpecificPath with override returns workspace-specific path", () => {
|
|
// This test verifies that when branchOverride is provided, the function uses it
|
|
// instead of detecting the current git branch
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/**"],
|
|
};
|
|
|
|
// When override is provided, it should return the workspace-specific path even outside git repo
|
|
const result = getWorkspaceSpecificPath("f/test.variable.yaml", config, "staging");
|
|
expect(result).toEqual("f/test.staging.variable.yaml");
|
|
});
|
|
|
|
test("branchOverride: getWorkspaceSpecificPath without override and not in git repo returns undefined", () => {
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/**"],
|
|
};
|
|
|
|
// Without override and outside git repo (or if git returns null), should return undefined
|
|
// Note: This test's behavior depends on whether we're in a git repo
|
|
const result = getWorkspaceSpecificPath("f/test.variable.yaml", config);
|
|
// In a git repo, this would return a workspace-specific path; outside, it would be undefined
|
|
// We test the override case above which is deterministic
|
|
});
|
|
|
|
test("branchOverride: isCurrentWorkspaceFile with override uses provided branch", () => {
|
|
// Test that isCurrentWorkspaceFile uses the override branch instead of git detection
|
|
const result = isCurrentWorkspaceFile("f/test.staging.variable.yaml", "staging");
|
|
expect(result).toEqual(true);
|
|
|
|
// Should return false for different branch
|
|
const resultOther = isCurrentWorkspaceFile("f/test.staging.variable.yaml", "production");
|
|
expect(resultOther).toEqual(false);
|
|
|
|
// Should return false for non-workspace-specific file
|
|
const resultNonSpecific = isCurrentWorkspaceFile("f/test.variable.yaml", "staging");
|
|
expect(resultNonSpecific).toEqual(false);
|
|
});
|
|
|
|
test("branchOverride: isCurrentWorkspaceFile with override handles sanitized branch names", () => {
|
|
// Test with branch names that get sanitized
|
|
const result = isCurrentWorkspaceFile("f/test.feature_my-branch.variable.yaml", "feature/my-branch");
|
|
expect(result).toEqual(true);
|
|
|
|
// Different sanitized branch should return false
|
|
const resultOther = isCurrentWorkspaceFile("f/test.feature_my-branch.variable.yaml", "feature/other-branch");
|
|
expect(resultOther).toEqual(false);
|
|
});
|
|
|
|
test("branchOverride: getSpecificItemsForCurrentBranch with override returns correct config", () => {
|
|
// Test that getSpecificItemsForCurrentBranch uses the override branch
|
|
const config = {
|
|
workspaces: {
|
|
staging: {
|
|
specificItems: {
|
|
variables: ["f/**"],
|
|
resources: ["u/admin/**"],
|
|
},
|
|
},
|
|
production: {
|
|
specificItems: {
|
|
variables: ["g/**"],
|
|
},
|
|
},
|
|
commonSpecificItems: {
|
|
triggers: ["f/webhooks/**"],
|
|
},
|
|
},
|
|
};
|
|
|
|
const stagingItems = getSpecificItemsForCurrentBranch(config as any, "staging");
|
|
expect(stagingItems?.variables).toEqual(["f/**"]);
|
|
expect(stagingItems?.resources).toEqual(["u/admin/**"]);
|
|
expect(stagingItems?.triggers).toEqual(["f/webhooks/**"]); // From common
|
|
|
|
const productionItems = getSpecificItemsForCurrentBranch(config as any, "production");
|
|
expect(productionItems?.variables).toEqual(["g/**"]);
|
|
expect(productionItems?.resources).toEqual(undefined);
|
|
expect(productionItems?.triggers).toEqual(["f/webhooks/**"]); // From common
|
|
});
|
|
|
|
test("branchOverride: getSpecificItemsForCurrentBranch with non-existent branch returns undefined", () => {
|
|
const config = {
|
|
workspaces: {
|
|
staging: {
|
|
specificItems: {
|
|
variables: ["f/**"],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
// When the branch doesn't have specific items (and there's no common), should return undefined
|
|
const result = getSpecificItemsForCurrentBranch(config as any, "nonexistent");
|
|
expect(result).toEqual(undefined);
|
|
});
|
|
|
|
test("branchOverride: getSpecificItemsForCurrentBranch merges common and branch items", () => {
|
|
const config = {
|
|
workspaces: {
|
|
commonSpecificItems: {
|
|
variables: ["common/**"],
|
|
resources: ["shared/**"],
|
|
},
|
|
develop: {
|
|
specificItems: {
|
|
variables: ["dev/**"],
|
|
triggers: ["dev/triggers/**"],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = getSpecificItemsForCurrentBranch(config as any, "develop");
|
|
// Should merge common and workspace-specific
|
|
expect(result?.variables).toEqual(["common/**", "dev/**"]);
|
|
expect(result?.resources).toEqual(["shared/**"]);
|
|
expect(result?.triggers).toEqual(["dev/triggers/**"]);
|
|
});
|
|
|
|
// =============================================================================
|
|
// FOLDER BRANCH-SPECIFIC TESTS
|
|
// Format: f/folder/folder.branchName.meta.yaml
|
|
// =============================================================================
|
|
|
|
test("toWorkspaceSpecificPath: converts folder meta path to workspace-specific", () => {
|
|
// f/my_folder/folder.meta.yaml -> f/my_folder/folder.main.meta.yaml
|
|
const result = toWorkspaceSpecificPath("f/my_folder/folder.meta.yaml", "main");
|
|
expect(result).toEqual("f/my_folder/folder.main.meta.yaml");
|
|
});
|
|
|
|
test("toWorkspaceSpecificPath: converts nested folder meta path to workspace-specific", () => {
|
|
const result = toWorkspaceSpecificPath("f/parent/child/folder.meta.yaml", "develop");
|
|
expect(result).toEqual("f/parent/child/folder.develop.meta.yaml");
|
|
});
|
|
|
|
test("toWorkspaceSpecificPath: sanitizes branch name in folder path", () => {
|
|
const result = toWorkspaceSpecificPath("f/env/folder.meta.yaml", "feature/test");
|
|
expect(result).toEqual("f/env/folder.feature_test.meta.yaml");
|
|
});
|
|
|
|
test("fromWorkspaceSpecificPath: converts workspace-specific folder back to base", () => {
|
|
const result = fromWorkspaceSpecificPath("f/my_folder/folder.main.meta.yaml", "main");
|
|
expect(result).toEqual("f/my_folder/folder.meta.yaml");
|
|
});
|
|
|
|
test("fromWorkspaceSpecificPath: handles nested workspace-specific folder", () => {
|
|
const result = fromWorkspaceSpecificPath("f/parent/child/folder.develop.meta.yaml", "develop");
|
|
expect(result).toEqual("f/parent/child/folder.meta.yaml");
|
|
});
|
|
|
|
test("fromWorkspaceSpecificPath: handles sanitized branch names for folders", () => {
|
|
const result = fromWorkspaceSpecificPath("f/env/folder.feature_test.meta.yaml", "feature/test");
|
|
expect(result).toEqual("f/env/folder.meta.yaml");
|
|
});
|
|
|
|
test("isSpecificItem: matches folder paths with glob pattern", () => {
|
|
const config: SpecificItemsConfig = {
|
|
folders: ["f/env_*"],
|
|
};
|
|
expect(isSpecificItem("f/env_staging/folder.meta.yaml", config)).toEqual(true);
|
|
expect(isSpecificItem("f/env_production/folder.meta.yaml", config)).toEqual(true);
|
|
expect(isSpecificItem("f/other/folder.meta.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
test("isSpecificItem: matches folder paths with exact pattern", () => {
|
|
const config: SpecificItemsConfig = {
|
|
folders: ["f/config"],
|
|
};
|
|
expect(isSpecificItem("f/config/folder.meta.yaml", config)).toEqual(true);
|
|
expect(isSpecificItem("f/other/folder.meta.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
test("isWorkspaceSpecificFile: detects workspace-specific folder files", () => {
|
|
expect(isWorkspaceSpecificFile("f/my_folder/folder.main.meta.yaml")).toEqual(true);
|
|
expect(isWorkspaceSpecificFile("f/my_folder/folder.develop.meta.yaml")).toEqual(true);
|
|
expect(isWorkspaceSpecificFile("f/nested/path/folder.staging.meta.yaml")).toEqual(true);
|
|
});
|
|
|
|
test("isWorkspaceSpecificFile: returns false for non-workspace-specific folder files", () => {
|
|
expect(isWorkspaceSpecificFile("f/my_folder/folder.meta.yaml")).toEqual(false);
|
|
expect(isWorkspaceSpecificFile("f/nested/path/folder.meta.yaml")).toEqual(false);
|
|
});
|
|
|
|
test("isCurrentWorkspaceFile: detects workspace-specific folder for current branch", () => {
|
|
expect(isCurrentWorkspaceFile("f/my_folder/folder.staging.meta.yaml", "staging")).toEqual(true);
|
|
expect(isCurrentWorkspaceFile("f/my_folder/folder.staging.meta.yaml", "production")).toEqual(false);
|
|
expect(isCurrentWorkspaceFile("f/my_folder/folder.meta.yaml", "staging")).toEqual(false);
|
|
});
|
|
|
|
test("isCurrentWorkspaceFile: handles sanitized branch for folders", () => {
|
|
expect(isCurrentWorkspaceFile("f/env/folder.feature_test.meta.yaml", "feature/test")).toEqual(true);
|
|
expect(isCurrentWorkspaceFile("f/env/folder.feature_test.meta.yaml", "feature/other")).toEqual(false);
|
|
});
|
|
|
|
test("round-trip: folder meta path conversion", () => {
|
|
const original = "f/configs/env_folder/folder.meta.yaml";
|
|
const branch = "main";
|
|
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
|
expect(branchSpecific).toEqual("f/configs/env_folder/folder.main.meta.yaml");
|
|
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
|
expect(restored).toEqual(original);
|
|
});
|
|
|
|
test("round-trip: folder meta with sanitized branch", () => {
|
|
const original = "f/env/folder.meta.yaml";
|
|
const branch = "feature/new-env";
|
|
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
|
expect(branchSpecific).toEqual("f/env/folder.feature_new-env.meta.yaml");
|
|
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
|
expect(restored).toEqual(original);
|
|
});
|
|
|
|
// =============================================================================
|
|
// SETTINGS BRANCH-SPECIFIC TESTS
|
|
// =============================================================================
|
|
|
|
test("toWorkspaceSpecificPath: converts settings.yaml to workspace-specific", () => {
|
|
const result = toWorkspaceSpecificPath("settings.yaml", "main");
|
|
expect(result).toEqual("settings.main.yaml");
|
|
});
|
|
|
|
test("toWorkspaceSpecificPath: sanitizes branch name in settings path", () => {
|
|
const result = toWorkspaceSpecificPath("settings.yaml", "feature/test");
|
|
expect(result).toEqual("settings.feature_test.yaml");
|
|
});
|
|
|
|
test("fromWorkspaceSpecificPath: converts workspace-specific settings back to base", () => {
|
|
const result = fromWorkspaceSpecificPath("settings.main.yaml", "main");
|
|
expect(result).toEqual("settings.yaml");
|
|
});
|
|
|
|
test("fromWorkspaceSpecificPath: handles sanitized branch names for settings", () => {
|
|
const result = fromWorkspaceSpecificPath("settings.feature_test.yaml", "feature/test");
|
|
expect(result).toEqual("settings.yaml");
|
|
});
|
|
|
|
test("isSpecificItem: matches settings.yaml when settings is true", () => {
|
|
const config: SpecificItemsConfig = {
|
|
settings: true,
|
|
};
|
|
expect(isSpecificItem("settings.yaml", config)).toEqual(true);
|
|
});
|
|
|
|
test("isSpecificItem: does not match settings.yaml when settings is false", () => {
|
|
const config: SpecificItemsConfig = {
|
|
settings: false,
|
|
};
|
|
expect(isSpecificItem("settings.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
test("isSpecificItem: does not match settings.yaml when settings is undefined", () => {
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/**"],
|
|
};
|
|
expect(isSpecificItem("settings.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
test("isWorkspaceSpecificFile: detects workspace-specific settings files", () => {
|
|
expect(isWorkspaceSpecificFile("settings.main.yaml")).toEqual(true);
|
|
expect(isWorkspaceSpecificFile("settings.develop.yaml")).toEqual(true);
|
|
expect(isWorkspaceSpecificFile("settings.feature_test.yaml")).toEqual(true);
|
|
});
|
|
|
|
test("isWorkspaceSpecificFile: returns false for non-workspace-specific settings", () => {
|
|
expect(isWorkspaceSpecificFile("settings.yaml")).toEqual(false);
|
|
});
|
|
|
|
test("isCurrentWorkspaceFile: detects workspace-specific settings for current branch", () => {
|
|
expect(isCurrentWorkspaceFile("settings.staging.yaml", "staging")).toEqual(true);
|
|
expect(isCurrentWorkspaceFile("settings.staging.yaml", "production")).toEqual(false);
|
|
expect(isCurrentWorkspaceFile("settings.yaml", "staging")).toEqual(false);
|
|
});
|
|
|
|
test("isCurrentWorkspaceFile: handles sanitized branch for settings", () => {
|
|
expect(isCurrentWorkspaceFile("settings.feature_test.yaml", "feature/test")).toEqual(true);
|
|
expect(isCurrentWorkspaceFile("settings.feature_test.yaml", "feature/other")).toEqual(false);
|
|
});
|
|
|
|
test("round-trip: settings path conversion", () => {
|
|
const original = "settings.yaml";
|
|
const branch = "main";
|
|
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
|
expect(branchSpecific).toEqual("settings.main.yaml");
|
|
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
|
expect(restored).toEqual(original);
|
|
});
|
|
|
|
test("round-trip: settings with sanitized branch", () => {
|
|
const original = "settings.yaml";
|
|
const branch = "release/v1.0";
|
|
const branchSpecific = toWorkspaceSpecificPath(original, branch);
|
|
expect(branchSpecific).toEqual("settings.release_v1_0.yaml");
|
|
const restored = fromWorkspaceSpecificPath(branchSpecific, branch);
|
|
expect(restored).toEqual(original);
|
|
});
|
|
|
|
// =============================================================================
|
|
// isItemTypeConfigured TESTS
|
|
// This function checks if the TYPE is configured, not whether it matches pattern.
|
|
// Used to determine if workspace-specific files should be used for this type.
|
|
// =============================================================================
|
|
|
|
test("isItemTypeConfigured: returns false when specificItems is undefined", () => {
|
|
expect(isItemTypeConfigured("f/test.variable.yaml", undefined)).toEqual(false);
|
|
expect(isItemTypeConfigured("f/test.resource.yaml", undefined)).toEqual(false);
|
|
expect(isItemTypeConfigured("f/folder/folder.meta.yaml", undefined)).toEqual(false);
|
|
expect(isItemTypeConfigured("settings.yaml", undefined)).toEqual(false);
|
|
});
|
|
|
|
test("isItemTypeConfigured: returns true for variables when variables is configured", () => {
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/**"],
|
|
};
|
|
// Type is configured (even if path doesn't match the pattern)
|
|
expect(isItemTypeConfigured("f/test.variable.yaml", config)).toEqual(true);
|
|
expect(isItemTypeConfigured("g/other.variable.yaml", config)).toEqual(true);
|
|
});
|
|
|
|
test("isItemTypeConfigured: returns false for variables when variables is NOT configured", () => {
|
|
const config: SpecificItemsConfig = {
|
|
resources: ["f/**"],
|
|
};
|
|
expect(isItemTypeConfigured("f/test.variable.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
test("isItemTypeConfigured: returns true for resources when resources is configured", () => {
|
|
const config: SpecificItemsConfig = {
|
|
resources: ["f/**"],
|
|
};
|
|
expect(isItemTypeConfigured("f/test.resource.yaml", config)).toEqual(true);
|
|
expect(isItemTypeConfigured("g/other.resource.yaml", config)).toEqual(true);
|
|
});
|
|
|
|
test("isItemTypeConfigured: returns false for resources when resources is NOT configured", () => {
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/**"],
|
|
};
|
|
expect(isItemTypeConfigured("f/test.resource.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
test("isItemTypeConfigured: returns true for triggers when triggers is configured", () => {
|
|
const config: SpecificItemsConfig = {
|
|
triggers: ["f/**"],
|
|
};
|
|
expect(isItemTypeConfigured("f/my.http_trigger.yaml", config)).toEqual(true);
|
|
expect(isItemTypeConfigured("f/my.kafka_trigger.yaml", config)).toEqual(true);
|
|
expect(isItemTypeConfigured("g/other.websocket_trigger.yaml", config)).toEqual(true);
|
|
});
|
|
|
|
test("isItemTypeConfigured: returns false for triggers when triggers is NOT configured", () => {
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/**"],
|
|
};
|
|
expect(isItemTypeConfigured("f/my.http_trigger.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
test("isItemTypeConfigured: returns true for folders when folders is configured", () => {
|
|
const config: SpecificItemsConfig = {
|
|
folders: ["f/env_*"],
|
|
};
|
|
// Type is configured (even if path doesn't match the pattern)
|
|
expect(isItemTypeConfigured("f/env_staging/folder.meta.yaml", config)).toEqual(true);
|
|
expect(isItemTypeConfigured("f/other/folder.meta.yaml", config)).toEqual(true);
|
|
});
|
|
|
|
test("isItemTypeConfigured: returns false for folders when folders is NOT configured", () => {
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/**"],
|
|
};
|
|
expect(isItemTypeConfigured("f/my_folder/folder.meta.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
test("isItemTypeConfigured: returns true for settings when settings is configured (true)", () => {
|
|
const config: SpecificItemsConfig = {
|
|
settings: true,
|
|
};
|
|
expect(isItemTypeConfigured("settings.yaml", config)).toEqual(true);
|
|
});
|
|
|
|
test("isItemTypeConfigured: returns true for settings when settings is configured (false)", () => {
|
|
// settings: false still means the type is "configured" (explicitly disabled)
|
|
const config: SpecificItemsConfig = {
|
|
settings: false,
|
|
};
|
|
expect(isItemTypeConfigured("settings.yaml", config)).toEqual(true);
|
|
});
|
|
|
|
test("isItemTypeConfigured: returns false for settings when settings is NOT configured", () => {
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/**"],
|
|
};
|
|
expect(isItemTypeConfigured("settings.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
test("isItemTypeConfigured: returns true for resource files (with extension) when resources is configured", () => {
|
|
const config: SpecificItemsConfig = {
|
|
resources: ["f/**"],
|
|
};
|
|
expect(isItemTypeConfigured("f/config.resource.file.json", config)).toEqual(true);
|
|
expect(isItemTypeConfigured("f/data.resource.file.ini", config)).toEqual(true);
|
|
});
|
|
|
|
test("isItemTypeConfigured: returns false for resource files when resources is NOT configured", () => {
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/**"],
|
|
};
|
|
expect(isItemTypeConfigured("f/config.resource.file.json", config)).toEqual(false);
|
|
});
|
|
|
|
// =============================================================================
|
|
// BRANCH-SPECIFIC FILE FILTERING TESTS
|
|
// These tests verify the expected filtering behavior:
|
|
// - When type IS configured: use workspace-specific files, skip base files
|
|
// - When type is NOT configured: skip workspace-specific files, use base files
|
|
// =============================================================================
|
|
|
|
test("filtering logic: folders - when NOT configured, workspace-specific should be ignored", () => {
|
|
// Config has variables but NOT folders
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/**"],
|
|
};
|
|
|
|
const basePath = "f/my_folder/folder.meta.yaml";
|
|
const branchSpecificPath = "f/my_folder/folder.main.meta.yaml";
|
|
|
|
// Folder type is NOT configured
|
|
expect(isItemTypeConfigured(basePath, config)).toEqual(false);
|
|
|
|
// Therefore, workspace-specific file detection should not apply to this type
|
|
// The sync logic should:
|
|
// 1. Skip workspace-specific folder files (isWorkspaceSpecificFile returns true)
|
|
// 2. Use the base file
|
|
expect(isWorkspaceSpecificFile(branchSpecificPath)).toEqual(true);
|
|
expect(isWorkspaceSpecificFile(basePath)).toEqual(false);
|
|
});
|
|
|
|
test("filtering logic: folders - when IS configured and matches, use workspace-specific", () => {
|
|
const config: SpecificItemsConfig = {
|
|
folders: ["f/my_folder"],
|
|
};
|
|
|
|
const basePath = "f/my_folder/folder.meta.yaml";
|
|
const branchSpecificPath = "f/my_folder/folder.main.meta.yaml";
|
|
|
|
// Folder type IS configured
|
|
expect(isItemTypeConfigured(basePath, config)).toEqual(true);
|
|
|
|
// And path matches the pattern
|
|
expect(isSpecificItem(basePath, config)).toEqual(true);
|
|
|
|
// The sync logic should:
|
|
// 1. Use workspace-specific folder file (map to base path)
|
|
// 2. Skip the base file
|
|
expect(isWorkspaceSpecificFile(branchSpecificPath)).toEqual(true);
|
|
expect(fromWorkspaceSpecificPath(branchSpecificPath, "main")).toEqual(basePath);
|
|
});
|
|
|
|
test("filtering logic: folders - when IS configured but doesn't match, skip workspace-specific", () => {
|
|
const config: SpecificItemsConfig = {
|
|
folders: ["f/env_*"], // Only env_ folders are workspace-specific
|
|
};
|
|
|
|
const basePath = "f/other_folder/folder.meta.yaml";
|
|
const branchSpecificPath = "f/other_folder/folder.main.meta.yaml";
|
|
|
|
// Folder type IS configured
|
|
expect(isItemTypeConfigured(basePath, config)).toEqual(true);
|
|
|
|
// But this path doesn't match the pattern
|
|
expect(isSpecificItem(basePath, config)).toEqual(false);
|
|
|
|
// The sync logic should:
|
|
// 1. Skip the workspace-specific file (type configured but doesn't match)
|
|
// 2. Use the base file
|
|
});
|
|
|
|
test("filtering logic: settings - when NOT configured, workspace-specific should be ignored", () => {
|
|
// Config has variables but NOT settings
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/**"],
|
|
};
|
|
|
|
const basePath = "settings.yaml";
|
|
const branchSpecificPath = "settings.main.yaml";
|
|
|
|
// Settings type is NOT configured
|
|
expect(isItemTypeConfigured(basePath, config)).toEqual(false);
|
|
|
|
// Therefore, workspace-specific file detection should not apply to this type
|
|
expect(isWorkspaceSpecificFile(branchSpecificPath)).toEqual(true);
|
|
expect(isWorkspaceSpecificFile(basePath)).toEqual(false);
|
|
});
|
|
|
|
test("filtering logic: settings - when IS configured (true), use workspace-specific", () => {
|
|
const config: SpecificItemsConfig = {
|
|
settings: true,
|
|
};
|
|
|
|
const basePath = "settings.yaml";
|
|
const branchSpecificPath = "settings.main.yaml";
|
|
|
|
// Settings type IS configured
|
|
expect(isItemTypeConfigured(basePath, config)).toEqual(true);
|
|
|
|
// And settings: true means it matches
|
|
expect(isSpecificItem(basePath, config)).toEqual(true);
|
|
|
|
// The sync logic should use workspace-specific file
|
|
expect(isWorkspaceSpecificFile(branchSpecificPath)).toEqual(true);
|
|
expect(fromWorkspaceSpecificPath(branchSpecificPath, "main")).toEqual(basePath);
|
|
});
|
|
|
|
test("filtering logic: settings - when IS configured (false), skip workspace-specific", () => {
|
|
// settings: false means type is configured but explicitly disabled
|
|
const config: SpecificItemsConfig = {
|
|
settings: false,
|
|
};
|
|
|
|
const basePath = "settings.yaml";
|
|
const branchSpecificPath = "settings.main.yaml";
|
|
|
|
// Settings type IS configured (even though value is false)
|
|
expect(isItemTypeConfigured(basePath, config)).toEqual(true);
|
|
|
|
// But settings: false means it doesn't match (not a specific item)
|
|
expect(isSpecificItem(basePath, config)).toEqual(false);
|
|
|
|
// The sync logic should skip workspace-specific file and use base
|
|
});
|
|
|
|
test("filtering logic: variables - when NOT configured, workspace-specific should be ignored", () => {
|
|
// Config has folders but NOT variables
|
|
const config: SpecificItemsConfig = {
|
|
folders: ["f/env_*"],
|
|
};
|
|
|
|
const basePath = "f/test.variable.yaml";
|
|
const branchSpecificPath = "f/test.main.variable.yaml";
|
|
|
|
// Variable type is NOT configured
|
|
expect(isItemTypeConfigured(basePath, config)).toEqual(false);
|
|
|
|
// Branch-specific variable files should be ignored
|
|
expect(isWorkspaceSpecificFile(branchSpecificPath)).toEqual(true);
|
|
expect(isWorkspaceSpecificFile(basePath)).toEqual(false);
|
|
});
|
|
|
|
test("filtering logic: resources - when NOT configured, workspace-specific should be ignored", () => {
|
|
// Config has folders but NOT resources
|
|
const config: SpecificItemsConfig = {
|
|
folders: ["f/env_*"],
|
|
};
|
|
|
|
const basePath = "f/db.resource.yaml";
|
|
const branchSpecificPath = "f/db.main.resource.yaml";
|
|
|
|
// Resource type is NOT configured
|
|
expect(isItemTypeConfigured(basePath, config)).toEqual(false);
|
|
|
|
expect(isWorkspaceSpecificFile(branchSpecificPath)).toEqual(true);
|
|
expect(isWorkspaceSpecificFile(basePath)).toEqual(false);
|
|
});
|
|
|
|
test("filtering logic: triggers - when NOT configured, workspace-specific should be ignored", () => {
|
|
// Config has folders but NOT triggers
|
|
const config: SpecificItemsConfig = {
|
|
folders: ["f/env_*"],
|
|
};
|
|
|
|
const basePath = "f/webhook.http_trigger.yaml";
|
|
const branchSpecificPath = "f/webhook.main.http_trigger.yaml";
|
|
|
|
// Trigger type is NOT configured
|
|
expect(isItemTypeConfigured(basePath, config)).toEqual(false);
|
|
|
|
expect(isWorkspaceSpecificFile(branchSpecificPath)).toEqual(true);
|
|
expect(isWorkspaceSpecificFile(basePath)).toEqual(false);
|
|
});
|
|
|
|
// =============================================================================
|
|
// MIXED CONFIGURATION TESTS
|
|
// Tests for configs that have some types configured but not others
|
|
// =============================================================================
|
|
|
|
test("mixed config: only folders configured - other types use base files", () => {
|
|
const config: SpecificItemsConfig = {
|
|
folders: ["f/env_*"],
|
|
};
|
|
|
|
// Folders IS configured
|
|
expect(isItemTypeConfigured("f/env_staging/folder.meta.yaml", config)).toEqual(true);
|
|
expect(isSpecificItem("f/env_staging/folder.meta.yaml", config)).toEqual(true);
|
|
|
|
// Variables, resources, triggers, settings are NOT configured
|
|
expect(isItemTypeConfigured("f/test.variable.yaml", config)).toEqual(false);
|
|
expect(isItemTypeConfigured("f/db.resource.yaml", config)).toEqual(false);
|
|
expect(isItemTypeConfigured("f/hook.http_trigger.yaml", config)).toEqual(false);
|
|
expect(isItemTypeConfigured("settings.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
test("mixed config: only settings configured - other types use base files", () => {
|
|
const config: SpecificItemsConfig = {
|
|
settings: true,
|
|
};
|
|
|
|
// Settings IS configured
|
|
expect(isItemTypeConfigured("settings.yaml", config)).toEqual(true);
|
|
expect(isSpecificItem("settings.yaml", config)).toEqual(true);
|
|
|
|
// Other types are NOT configured
|
|
expect(isItemTypeConfigured("f/test.variable.yaml", config)).toEqual(false);
|
|
expect(isItemTypeConfigured("f/db.resource.yaml", config)).toEqual(false);
|
|
expect(isItemTypeConfigured("f/hook.http_trigger.yaml", config)).toEqual(false);
|
|
expect(isItemTypeConfigured("f/my_folder/folder.meta.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
test("mixed config: variables and folders configured - resources and triggers use base", () => {
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/**"],
|
|
folders: ["f/env_*"],
|
|
};
|
|
|
|
// Variables IS configured
|
|
expect(isItemTypeConfigured("f/test.variable.yaml", config)).toEqual(true);
|
|
expect(isSpecificItem("f/test.variable.yaml", config)).toEqual(true);
|
|
|
|
// Folders IS configured (path matches)
|
|
expect(isItemTypeConfigured("f/env_staging/folder.meta.yaml", config)).toEqual(true);
|
|
expect(isSpecificItem("f/env_staging/folder.meta.yaml", config)).toEqual(true);
|
|
|
|
// Folders IS configured but path doesn't match
|
|
expect(isItemTypeConfigured("f/other/folder.meta.yaml", config)).toEqual(true);
|
|
expect(isSpecificItem("f/other/folder.meta.yaml", config)).toEqual(false);
|
|
|
|
// Resources and triggers are NOT configured
|
|
expect(isItemTypeConfigured("f/db.resource.yaml", config)).toEqual(false);
|
|
expect(isItemTypeConfigured("f/hook.http_trigger.yaml", config)).toEqual(false);
|
|
expect(isItemTypeConfigured("settings.yaml", config)).toEqual(false);
|
|
});
|
|
|
|
// =============================================================================
|
|
// JSON workspace-specific path conversion
|
|
// (every helper that previously hard-coded .yaml must also handle .json)
|
|
// =============================================================================
|
|
|
|
test("toWorkspaceSpecificPath: converts .json variable path", () => {
|
|
expect(toWorkspaceSpecificPath("f/test.variable.json", "main")).toEqual(
|
|
"f/test.main.variable.json",
|
|
);
|
|
});
|
|
|
|
test("toWorkspaceSpecificPath: converts .json resource path", () => {
|
|
expect(toWorkspaceSpecificPath("u/admin/db.resource.json", "develop")).toEqual(
|
|
"u/admin/db.develop.resource.json",
|
|
);
|
|
});
|
|
|
|
test("toWorkspaceSpecificPath: converts .json trigger path", () => {
|
|
expect(
|
|
toWorkspaceSpecificPath("f/my_trigger.http_trigger.json", "feature-x"),
|
|
).toEqual("f/my_trigger.feature-x.http_trigger.json");
|
|
});
|
|
|
|
test("toWorkspaceSpecificPath: converts .json schedule path", () => {
|
|
expect(toWorkspaceSpecificPath("f/cron.schedule.json", "main")).toEqual(
|
|
"f/cron.main.schedule.json",
|
|
);
|
|
});
|
|
|
|
test("toWorkspaceSpecificPath: converts folder.meta.json", () => {
|
|
expect(toWorkspaceSpecificPath("f/myfolder/folder.meta.json", "dev")).toEqual(
|
|
"f/myfolder/folder.dev.meta.json",
|
|
);
|
|
});
|
|
|
|
test("toWorkspaceSpecificPath: converts settings.json", () => {
|
|
expect(toWorkspaceSpecificPath("settings.json", "prod")).toEqual(
|
|
"settings.prod.json",
|
|
);
|
|
});
|
|
|
|
test("fromWorkspaceSpecificPath: round-trips .json variable", () => {
|
|
expect(fromWorkspaceSpecificPath("f/test.main.variable.json", "main")).toEqual(
|
|
"f/test.variable.json",
|
|
);
|
|
});
|
|
|
|
test("fromWorkspaceSpecificPath: round-trips .json resource", () => {
|
|
expect(
|
|
fromWorkspaceSpecificPath("u/admin/db.develop.resource.json", "develop"),
|
|
).toEqual("u/admin/db.resource.json");
|
|
});
|
|
|
|
test("fromWorkspaceSpecificPath: round-trips .json trigger", () => {
|
|
expect(
|
|
fromWorkspaceSpecificPath(
|
|
"f/my_trigger.feature-x.http_trigger.json",
|
|
"feature-x",
|
|
),
|
|
).toEqual("f/my_trigger.http_trigger.json");
|
|
});
|
|
|
|
test("fromWorkspaceSpecificPath: round-trips folder.<branch>.meta.json", () => {
|
|
expect(
|
|
fromWorkspaceSpecificPath("f/myfolder/folder.dev.meta.json", "dev"),
|
|
).toEqual("f/myfolder/folder.meta.json");
|
|
});
|
|
|
|
test("fromWorkspaceSpecificPath: round-trips settings.<branch>.json", () => {
|
|
expect(fromWorkspaceSpecificPath("settings.prod.json", "prod")).toEqual(
|
|
"settings.json",
|
|
);
|
|
});
|
|
|
|
test("isWorkspaceSpecificFile: detects .json branch-specific files", () => {
|
|
expect(isWorkspaceSpecificFile("f/test.main.variable.json")).toEqual(true);
|
|
expect(isWorkspaceSpecificFile("u/admin/db.develop.resource.json")).toEqual(
|
|
true,
|
|
);
|
|
expect(
|
|
isWorkspaceSpecificFile("f/my_trigger.feature.http_trigger.json"),
|
|
).toEqual(true);
|
|
expect(isWorkspaceSpecificFile("f/myfolder/folder.dev.meta.json")).toEqual(
|
|
true,
|
|
);
|
|
expect(isWorkspaceSpecificFile("settings.prod.json")).toEqual(true);
|
|
});
|
|
|
|
test("isWorkspaceSpecificFile: rejects base .json files", () => {
|
|
expect(isWorkspaceSpecificFile("f/test.variable.json")).toEqual(false);
|
|
expect(isWorkspaceSpecificFile("settings.json")).toEqual(false);
|
|
expect(isWorkspaceSpecificFile("f/myfolder/folder.meta.json")).toEqual(false);
|
|
});
|
|
|
|
test("isCurrentWorkspaceFile: detects .json branch-specific files for given workspace", () => {
|
|
expect(
|
|
isCurrentWorkspaceFile("f/test.main.variable.json", "main"),
|
|
).toEqual(true);
|
|
expect(
|
|
isCurrentWorkspaceFile("f/test.dev.variable.json", "main"),
|
|
).toEqual(false);
|
|
expect(
|
|
isCurrentWorkspaceFile("settings.prod.json", "prod"),
|
|
).toEqual(true);
|
|
});
|
|
|
|
test("isSpecificItem: matches .json paths against .yaml-only patterns", () => {
|
|
// Patterns from wmill.yaml are conventionally written with .yaml; the
|
|
// helper should normalize a local .json file before matching so users
|
|
// configured for opts.json still get specificItems coverage.
|
|
const config: SpecificItemsConfig = {
|
|
variables: ["f/*.variable.yaml"],
|
|
resources: ["u/admin/db.resource.yaml"],
|
|
};
|
|
expect(isSpecificItem("f/test.variable.json", config)).toEqual(true);
|
|
expect(isSpecificItem("u/admin/db.resource.json", config)).toEqual(true);
|
|
expect(isSpecificItem("u/admin/other.resource.json", config)).toEqual(false);
|
|
});
|
|
|
|
test("isItemTypeConfigured: works for .json folder.meta and settings", () => {
|
|
const config: SpecificItemsConfig = {
|
|
folders: ["f/env_*"],
|
|
settings: true,
|
|
};
|
|
expect(
|
|
isItemTypeConfigured("f/env_staging/folder.meta.json", config),
|
|
).toEqual(true);
|
|
expect(isItemTypeConfigured("settings.json", config)).toEqual(true);
|
|
});
|