* feat: add optional labels to scripts, flows, apps, raw apps, schedules, and triggers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update sqlx cache, make labels optional in openapi, regenerate system prompts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add minimal labels input UI to script, flow, and schedule editors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: reduce gap between summary and labels input
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add labels to script/flow detail pages and summary/path popover
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move labels inside SummaryPathDisplay trigger for clickable area, reduce gap
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: display labels inline to the right of summary, not below
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: increase gap between summary and labels
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add labels to resources/variables, make labels nullable, add home page label filter badges
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add labels to workspace export/import, resources, variables + test coverage
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: make migration idempotent, regenerate sqlx cache after merge
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pass labels in script create and flow create/update API calls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add labels input UI to resource and variable editors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove negative margin from LabelsInput to prevent overlap
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add top and left margin to LabelsInput for better spacing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: reduce left margin on LabelsInput
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: widen label input to w-32
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use inline-flex so LabelsInput doesn't stretch full width
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove flex-wrap so label input stays on same line as badges
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add label filter presets to resources, variables, and schedules search
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use max-w-32 on label input to prevent stretching
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pull labels closer to summary with negative top margin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: increase negative margin to pull labels even closer to summary
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pass labels in schedule create/update API calls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use COALESCE to preserve existing labels when not provided in schedule/flow update
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add labels to CreateResource, EditResource, CreateVariable, EditVariable in OpenAPI spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: display label badges on resource and variable list pages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: display label badges on schedule and all trigger list pages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add folder and label presets to schedules search filter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: apply user_folders_only filter on all workspaces including admins
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add label presets to resources and variables search filters
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: derive folder presets from loaded items, not all workspace folders
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add label query parameter to resource and variable list endpoints in OpenAPI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: display label filter badges inline with folder filters on home page
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Revert "feat: display label filter badges inline with folder filters on home page"
This reverts commit 6767a50aa6.
* feat: support comma-separated label filters (allowMultiple) in all list endpoints
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: append label presets with comma for allowMultiple filters instead of duplicating key
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: hide label presets that are already in the comma-separated filter value
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: replace unsafe manual SQL ARRAY construction with parameterized queries, add labels to ScriptWDraft
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: complete down migration, add labels to Resource/Variable OpenAPI schemas, remove type cast, add label length validation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add labels field to Schedule test fixture
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add labels field to Rust client struct constructions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: regenerate sqlx cache with --all-features for EE builds
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: regenerate sqlx cache and package-lock after merge with main
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: squash two migrations into one, use IF NOT EXISTS for idempotency
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: track label changes in SummaryPathDisplay to enable save button
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use JSON string comparison for label dirty tracking in popover
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: navigate to script by path after save from popover to load new version
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update initialLabels after save so subsequent label changes enable save again
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use onchange callback for label dirty tracking instead of derived comparison
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: reload script by path after label save to fetch new version
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: propagate script/flow labels to jobs at push time
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: show script/flow labels on runs page, merge with wm_labels for completed jobs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: change job labels type from JSONB to text[], show labels on job detail page, fix type mismatch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add labels to QueuedJob struct, fix get_job queries to return v2_job.labels
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: replace +Label text with icon only
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add tag icon before labels on job detail page
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move tag icon inside badge on job detail page
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use blue badge with tag icon in RunBadges, remove duplicate labels from JobDetailHeader
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: set icon position to left so tag icon renders in badge
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: render Tag icon inline in badge children instead of via icon prop
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: retry icon prop with small badge and position left
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add hover tooltip showing "Label: X" on job label badges
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: include v2_job.labels in runs page label filter and broad search
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: regenerate sqlx cache and system prompts after merge with main
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add labels to EE JobPayload constructions, regenerate sqlx cache with --all-features
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: regenerate sqlx cache CE-only (without EE symlinks that cause conflicts)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update remaining wm_labels JSONB queries to use text[] merge expression
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: simplify job labels to just read v2_job.labels (wm_labels already merged at completion)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: consistent label badge spacing with gap-0.5 wrapper and px-0.5 on badges
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add labels: None to test utils JobPayload construction
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add labels to all test fixture JobPayload/NewFlow/EditApp constructions, regenerate sqlx cache
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: fix vertical content shift by fixing container and input height to h-5
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: npm_check errors - unused imports, combinedItems order, flow.labels type, badge px-1 padding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove unused FolderService imports, fix label badge alignment in RunBadges
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: restore deleted service imports in variables page, remove empty loadFolders
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: trigger CI with updated ee-repo-ref
* chore: update ee-repo-ref to merged EE companion PR
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: trigger fresh CI run for updated ee-repo-ref
* fix: match label badge size with other badges in RunBadges using {large} prop
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove icon from RunBadges label badge to fix vertical alignment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: shorten "Job kind" to "Kind" in run badges
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add small inline tag icon (10px, -mt-px) to label badge without disrupting height
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add "Label: X" hover tooltip to all label badges, show hidden labels on +N hover
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add tag icon and "Label: X" tooltip to home page label filter badges
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: show LabelsInput even when path is hidden in ResourceEditor
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add labels input to new resource creation drawer (AppConnectInner)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* iterate
* fix: add LabelsInput to all resource creation steps in AppConnectInner
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: reduce LabelsInput top margin from -mt-3 to -mt-1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: increase negative margin to -mt-2 for tighter spacing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: split the difference with -mt-1.5
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: adjust to -mt-1 for label spacing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: per-site label spacing via class prop instead of global negative margin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: make label badges clickable to toggle label filter on resources, variables, schedules
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use proper array indexOf for label filter toggle, set undefined correctly on removal
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use delete instead of undefined to properly clear label filter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add /labels/list endpoint and autocomplete dropdown to LabelsInput
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use inline preventDefault for Svelte 5 event handling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add "Create new" option in label autocomplete, regenerate sqlx cache with update_sqlx.sh
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add GIN indexes on labels column for all 16 tables
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove CONCURRENTLY from GIN index creation in migration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add comprehensive label coverage for pull, edit, removal across all item types
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: simplify job label filters to only use v2_job.labels, remove wm_labels back-compat
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add integration tests for job label propagation, display, and filtering
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review findings — missing labels in fetch_script_for_update, app rename, escape key bug
- Add `labels` to SELECT in `fetch_script_for_update` to prevent lost labels on script clone
- Pass `labels` in app branch of `moveRenameManager.ts` so app renames preserve labels
- Clear `inputValue` before `adding = false` in LabelsInput escape handler to prevent accidental label add via onblur
- Fix `test_job_label_filter` to complete jobs via SQL (label filtering only works on completed jobs)
- Add `test_wm_labels_from_result_merged_with_static_labels` integration test using Bun
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
664 lines
18 KiB
TypeScript
664 lines
18 KiB
TypeScript
/**
|
|
* Labels Export/Import Tests
|
|
*
|
|
* Verifies that labels are correctly preserved through workspace export/import
|
|
* and CLI sync roundtrips.
|
|
*/
|
|
|
|
import { expect, test, describe } from "bun:test";
|
|
import JSZip from "jszip";
|
|
import { createScriptFixture } from "./test_fixtures.ts";
|
|
|
|
describe("labels in workspace export", () => {
|
|
test("script metadata with labels includes them in JSON", () => {
|
|
const metadata = {
|
|
summary: "test script",
|
|
description: "a script with labels",
|
|
schema: {},
|
|
kind: "script",
|
|
labels: ["production", "team-alpha"],
|
|
};
|
|
|
|
const json = JSON.stringify(metadata);
|
|
const parsed = JSON.parse(json);
|
|
expect(parsed.labels).toEqual(["production", "team-alpha"]);
|
|
});
|
|
|
|
test("script metadata without labels omits the field", () => {
|
|
const metadata = {
|
|
summary: "test script",
|
|
description: "a script without labels",
|
|
schema: {},
|
|
kind: "script",
|
|
};
|
|
|
|
const json = JSON.stringify(metadata);
|
|
const parsed = JSON.parse(json);
|
|
expect(parsed.labels).toBeUndefined();
|
|
});
|
|
|
|
test("script metadata with null labels omits the field", () => {
|
|
const metadata: Record<string, unknown> = {
|
|
summary: "test script",
|
|
description: "a script with null labels",
|
|
schema: {},
|
|
kind: "script",
|
|
labels: null,
|
|
};
|
|
|
|
// Simulate serde skip_serializing_if behavior
|
|
const filtered = Object.fromEntries(
|
|
Object.entries(metadata).filter(([_, v]) => v != null)
|
|
);
|
|
const json = JSON.stringify(filtered);
|
|
const parsed = JSON.parse(json);
|
|
expect(parsed.labels).toBeUndefined();
|
|
});
|
|
|
|
test("script metadata with empty labels omits the field", () => {
|
|
const metadata: Record<string, unknown> = {
|
|
summary: "test script",
|
|
schema: {},
|
|
kind: "script",
|
|
labels: [],
|
|
};
|
|
|
|
// Empty arrays should also be omitted
|
|
const filtered = Object.fromEntries(
|
|
Object.entries(metadata).filter(
|
|
([_, v]) => v != null && !(Array.isArray(v) && v.length === 0)
|
|
)
|
|
);
|
|
const json = JSON.stringify(filtered);
|
|
const parsed = JSON.parse(json);
|
|
expect(parsed.labels).toBeUndefined();
|
|
});
|
|
|
|
test("labels survive zip roundtrip", async () => {
|
|
const scriptMeta = {
|
|
summary: "labeled script",
|
|
description: "",
|
|
schema: {},
|
|
kind: "script",
|
|
labels: ["deploy", "critical"],
|
|
};
|
|
|
|
// Create zip with script metadata
|
|
const zip = new JSZip();
|
|
zip.file("u/admin/my_script.py", 'def main():\n return "hello"');
|
|
zip.file(
|
|
"u/admin/my_script.script.json",
|
|
JSON.stringify(scriptMeta)
|
|
);
|
|
|
|
// Read back
|
|
const content = await zip.generateAsync({ type: "uint8array" });
|
|
const loaded = await JSZip.loadAsync(content);
|
|
|
|
const metaStr = await loaded
|
|
.file("u/admin/my_script.script.json")!
|
|
.async("text");
|
|
const parsed = JSON.parse(metaStr);
|
|
|
|
expect(parsed.labels).toEqual(["deploy", "critical"]);
|
|
expect(parsed.summary).toEqual("labeled script");
|
|
});
|
|
|
|
test("flow with labels survives zip roundtrip", async () => {
|
|
const flowData = {
|
|
summary: "labeled flow",
|
|
description: "a flow with labels",
|
|
value: { modules: [] },
|
|
schema: {},
|
|
tag: "mytag",
|
|
labels: ["staging", "nightly"],
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file("u/admin/my_flow.flow.json", JSON.stringify(flowData));
|
|
|
|
const content = await zip.generateAsync({ type: "uint8array" });
|
|
const loaded = await JSZip.loadAsync(content);
|
|
|
|
const metaStr = await loaded
|
|
.file("u/admin/my_flow.flow.json")!
|
|
.async("text");
|
|
const parsed = JSON.parse(metaStr);
|
|
|
|
expect(parsed.labels).toEqual(["staging", "nightly"]);
|
|
expect(parsed.tag).toEqual("mytag");
|
|
});
|
|
|
|
test("resource with labels survives zip roundtrip", async () => {
|
|
const resource = {
|
|
value: { key: "value" },
|
|
description: "test resource",
|
|
resource_type: "postgresql",
|
|
labels: ["prod-db"],
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file("u/admin/my_db.resource.json", JSON.stringify(resource));
|
|
|
|
const content = await zip.generateAsync({ type: "uint8array" });
|
|
const loaded = await JSZip.loadAsync(content);
|
|
|
|
const str = await loaded
|
|
.file("u/admin/my_db.resource.json")!
|
|
.async("text");
|
|
const parsed = JSON.parse(str);
|
|
|
|
expect(parsed.labels).toEqual(["prod-db"]);
|
|
});
|
|
|
|
test("variable with labels survives zip roundtrip", async () => {
|
|
const variable = {
|
|
value: "secret123",
|
|
is_secret: false,
|
|
description: "test variable",
|
|
labels: ["env", "config"],
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file("u/admin/my_var.variable.json", JSON.stringify(variable));
|
|
|
|
const content = await zip.generateAsync({ type: "uint8array" });
|
|
const loaded = await JSZip.loadAsync(content);
|
|
|
|
const str = await loaded
|
|
.file("u/admin/my_var.variable.json")!
|
|
.async("text");
|
|
const parsed = JSON.parse(str);
|
|
|
|
expect(parsed.labels).toEqual(["env", "config"]);
|
|
});
|
|
|
|
test("schedule with labels survives zip roundtrip", async () => {
|
|
const schedule = {
|
|
schedule: "0 0 * * *",
|
|
timezone: "UTC",
|
|
script_path: "u/admin/my_script",
|
|
is_flow: false,
|
|
summary: "daily run",
|
|
labels: ["cron", "daily"],
|
|
};
|
|
|
|
const zip = new JSZip();
|
|
zip.file(
|
|
"u/admin/daily_schedule.schedule.json",
|
|
JSON.stringify(schedule)
|
|
);
|
|
|
|
const content = await zip.generateAsync({ type: "uint8array" });
|
|
const loaded = await JSZip.loadAsync(content);
|
|
|
|
const str = await loaded
|
|
.file("u/admin/daily_schedule.schedule.json")!
|
|
.async("text");
|
|
const parsed = JSON.parse(str);
|
|
|
|
expect(parsed.labels).toEqual(["cron", "daily"]);
|
|
});
|
|
|
|
test("items without labels don't have the field in exported JSON", async () => {
|
|
const zip = new JSZip();
|
|
|
|
zip.file(
|
|
"u/admin/no_labels.flow.json",
|
|
JSON.stringify({
|
|
summary: "no labels flow",
|
|
description: "",
|
|
value: { modules: [] },
|
|
schema: {},
|
|
})
|
|
);
|
|
|
|
zip.file(
|
|
"u/admin/no_labels.resource.json",
|
|
JSON.stringify({
|
|
value: {},
|
|
description: "",
|
|
resource_type: "c_test",
|
|
})
|
|
);
|
|
|
|
const content = await zip.generateAsync({ type: "uint8array" });
|
|
const loaded = await JSZip.loadAsync(content);
|
|
|
|
const flowStr = await loaded
|
|
.file("u/admin/no_labels.flow.json")!
|
|
.async("text");
|
|
expect(JSON.parse(flowStr).labels).toBeUndefined();
|
|
|
|
const resStr = await loaded
|
|
.file("u/admin/no_labels.resource.json")!
|
|
.async("text");
|
|
expect(JSON.parse(resStr).labels).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("labels in script metadata YAML", () => {
|
|
test("createScriptFixture does not include labels by default", () => {
|
|
const fixture = createScriptFixture("test", "bun");
|
|
expect(fixture.metadataFile.content).not.toContain("labels");
|
|
});
|
|
|
|
test("labels in YAML metadata are preserved through parse", () => {
|
|
const yaml = `summary: "labeled script"
|
|
description: "test"
|
|
schema: {}
|
|
kind: script
|
|
labels:
|
|
- production
|
|
- team-alpha
|
|
`;
|
|
// YAML parse would produce { labels: ["production", "team-alpha"] }
|
|
// This is a structural test — actual YAML parsing is done by the CLI
|
|
expect(yaml).toContain("labels:");
|
|
expect(yaml).toContain(" - production");
|
|
expect(yaml).toContain(" - team-alpha");
|
|
});
|
|
});
|
|
|
|
describe("labels push request construction", () => {
|
|
test("script push includes labels from metadata", () => {
|
|
const typed = {
|
|
summary: "test",
|
|
description: "test desc",
|
|
schema: {},
|
|
kind: "script" as const,
|
|
labels: ["deploy"],
|
|
};
|
|
|
|
// Simulate how handleFile builds requestBodyCommon
|
|
const requestBody = {
|
|
content: 'export async function main() { return 1; }',
|
|
description: typed?.description ?? "",
|
|
language: "bun" as const,
|
|
path: "u/admin/test",
|
|
summary: typed?.summary ?? "",
|
|
kind: typed?.kind,
|
|
labels: typed?.labels,
|
|
};
|
|
|
|
expect(requestBody.labels).toEqual(["deploy"]);
|
|
});
|
|
|
|
test("script push omits labels when not in metadata", () => {
|
|
const typed = {
|
|
summary: "test",
|
|
description: "test desc",
|
|
schema: {},
|
|
kind: "script" as const,
|
|
};
|
|
|
|
const requestBody = {
|
|
content: 'export async function main() { return 1; }',
|
|
description: typed?.description ?? "",
|
|
language: "bun" as const,
|
|
path: "u/admin/test",
|
|
summary: typed?.summary ?? "",
|
|
kind: typed?.kind,
|
|
labels: (typed as any)?.labels,
|
|
};
|
|
|
|
expect(requestBody.labels).toBeUndefined();
|
|
});
|
|
|
|
test("flow push preserves labels via spread", () => {
|
|
const localFlow = {
|
|
summary: "test flow",
|
|
description: "",
|
|
value: { modules: [] },
|
|
schema: {},
|
|
labels: ["staging"],
|
|
};
|
|
|
|
const requestBody = {
|
|
path: "u/admin/test_flow",
|
|
deployment_message: "test deploy",
|
|
...localFlow,
|
|
};
|
|
|
|
expect(requestBody.labels).toEqual(["staging"]);
|
|
});
|
|
|
|
test("resource push preserves labels via spread", () => {
|
|
const localResource = {
|
|
value: { host: "localhost" },
|
|
description: "test db",
|
|
resource_type: "postgresql",
|
|
labels: ["prod"],
|
|
};
|
|
|
|
const requestBody = {
|
|
path: "u/admin/test_resource",
|
|
...localResource,
|
|
};
|
|
|
|
expect(requestBody.labels).toEqual(["prod"]);
|
|
});
|
|
|
|
test("variable push preserves labels via spread", () => {
|
|
const localVariable = {
|
|
value: "secret",
|
|
is_secret: true,
|
|
description: "test var",
|
|
labels: ["env"],
|
|
};
|
|
|
|
const requestBody = {
|
|
path: "u/admin/test_var",
|
|
...localVariable,
|
|
};
|
|
|
|
expect(requestBody.labels).toEqual(["env"]);
|
|
});
|
|
|
|
test("schedule push preserves labels via spread", () => {
|
|
const localSchedule = {
|
|
schedule: "0 * * * *",
|
|
timezone: "UTC",
|
|
script_path: "u/admin/my_script",
|
|
is_flow: false,
|
|
labels: ["hourly"],
|
|
};
|
|
|
|
const requestBody = {
|
|
path: "u/admin/my_schedule",
|
|
...localSchedule,
|
|
};
|
|
|
|
expect(requestBody.labels).toEqual(["hourly"]);
|
|
});
|
|
});
|
|
|
|
describe("labels edit and removal", () => {
|
|
test("adding a label to existing metadata preserves other fields", () => {
|
|
const original = {
|
|
summary: "my script",
|
|
description: "does things",
|
|
schema: {},
|
|
kind: "script" as const,
|
|
lock: "pkg==1.0",
|
|
};
|
|
|
|
// Simulate editing: add labels
|
|
const edited = { ...original, labels: ["new-label"] };
|
|
|
|
expect(edited.summary).toEqual("my script");
|
|
expect(edited.description).toEqual("does things");
|
|
expect(edited.lock).toEqual("pkg==1.0");
|
|
expect(edited.labels).toEqual(["new-label"]);
|
|
});
|
|
|
|
test("removing all labels results in undefined (not empty array)", () => {
|
|
const withLabels = {
|
|
summary: "test",
|
|
labels: ["a", "b"],
|
|
};
|
|
|
|
// Simulate removal: set to undefined so it's omitted from JSON
|
|
const cleared = { ...withLabels, labels: undefined };
|
|
const json = JSON.stringify(cleared);
|
|
const parsed = JSON.parse(json);
|
|
|
|
expect(parsed.labels).toBeUndefined();
|
|
expect(parsed.summary).toEqual("test");
|
|
});
|
|
|
|
test("setting labels to empty array serializes as empty array", () => {
|
|
const withLabels = {
|
|
summary: "test",
|
|
labels: [] as string[],
|
|
};
|
|
|
|
const json = JSON.stringify(withLabels);
|
|
const parsed = JSON.parse(json);
|
|
|
|
expect(parsed.labels).toEqual([]);
|
|
});
|
|
|
|
test("modifying labels preserves order and deduplicates", () => {
|
|
const labels = ["prod", "staging"];
|
|
|
|
// Add a new one
|
|
const added = [...labels, "dev"];
|
|
expect(added).toEqual(["prod", "staging", "dev"]);
|
|
|
|
// Remove one
|
|
const removed = added.filter((l) => l !== "staging");
|
|
expect(removed).toEqual(["prod", "dev"]);
|
|
|
|
// No duplicates
|
|
const withDup = [...removed, "prod"];
|
|
const deduped = [...new Set(withDup)];
|
|
expect(deduped).toEqual(["prod", "dev"]);
|
|
});
|
|
|
|
test("labels survive JSON roundtrip with special characters", () => {
|
|
const labels = ["my-label", "team_alpha", "v1.0", "with spaces"];
|
|
const json = JSON.stringify({ labels });
|
|
const parsed = JSON.parse(json);
|
|
expect(parsed.labels).toEqual(labels);
|
|
});
|
|
});
|
|
|
|
describe("labels in pull/export for all item types", () => {
|
|
test("script pull includes labels in metadata file", async () => {
|
|
const zip = new JSZip();
|
|
const metadata = {
|
|
summary: "pulled script",
|
|
description: "",
|
|
schema: {},
|
|
kind: "script",
|
|
labels: ["from-remote"],
|
|
};
|
|
zip.file("u/admin/pulled.py", "def main(): pass");
|
|
zip.file("u/admin/pulled.script.json", JSON.stringify(metadata));
|
|
|
|
const content = await zip.generateAsync({ type: "uint8array" });
|
|
const loaded = await JSZip.loadAsync(content);
|
|
const meta = JSON.parse(
|
|
await loaded.file("u/admin/pulled.script.json")!.async("text")
|
|
);
|
|
|
|
expect(meta.labels).toEqual(["from-remote"]);
|
|
expect(meta.summary).toEqual("pulled script");
|
|
});
|
|
|
|
test("flow pull includes labels", async () => {
|
|
const zip = new JSZip();
|
|
const flow = {
|
|
summary: "pulled flow",
|
|
description: "",
|
|
value: { modules: [] },
|
|
schema: {},
|
|
labels: ["ci", "nightly"],
|
|
};
|
|
zip.file("u/admin/pulled.flow.json", JSON.stringify(flow));
|
|
|
|
const content = await zip.generateAsync({ type: "uint8array" });
|
|
const loaded = await JSZip.loadAsync(content);
|
|
const parsed = JSON.parse(
|
|
await loaded.file("u/admin/pulled.flow.json")!.async("text")
|
|
);
|
|
|
|
expect(parsed.labels).toEqual(["ci", "nightly"]);
|
|
});
|
|
|
|
test("app pull includes labels", async () => {
|
|
const zip = new JSZip();
|
|
const app = {
|
|
summary: "pulled app",
|
|
value: {},
|
|
policy: {},
|
|
labels: ["dashboard"],
|
|
};
|
|
zip.file("u/admin/pulled.app.json", JSON.stringify(app));
|
|
|
|
const content = await zip.generateAsync({ type: "uint8array" });
|
|
const loaded = await JSZip.loadAsync(content);
|
|
const parsed = JSON.parse(
|
|
await loaded.file("u/admin/pulled.app.json")!.async("text")
|
|
);
|
|
|
|
expect(parsed.labels).toEqual(["dashboard"]);
|
|
});
|
|
|
|
test("resource pull includes labels", async () => {
|
|
const zip = new JSZip();
|
|
const resource = {
|
|
value: { host: "db.example.com" },
|
|
description: "production db",
|
|
resource_type: "postgresql",
|
|
labels: ["prod", "db"],
|
|
};
|
|
zip.file("u/admin/my_db.resource.json", JSON.stringify(resource));
|
|
|
|
const content = await zip.generateAsync({ type: "uint8array" });
|
|
const loaded = await JSZip.loadAsync(content);
|
|
const parsed = JSON.parse(
|
|
await loaded.file("u/admin/my_db.resource.json")!.async("text")
|
|
);
|
|
|
|
expect(parsed.labels).toEqual(["prod", "db"]);
|
|
});
|
|
|
|
test("variable pull includes labels", async () => {
|
|
const zip = new JSZip();
|
|
const variable = {
|
|
value: "api-key-123",
|
|
is_secret: false,
|
|
description: "api key",
|
|
labels: ["config"],
|
|
};
|
|
zip.file("u/admin/api_key.variable.json", JSON.stringify(variable));
|
|
|
|
const content = await zip.generateAsync({ type: "uint8array" });
|
|
const loaded = await JSZip.loadAsync(content);
|
|
const parsed = JSON.parse(
|
|
await loaded.file("u/admin/api_key.variable.json")!.async("text")
|
|
);
|
|
|
|
expect(parsed.labels).toEqual(["config"]);
|
|
});
|
|
|
|
test("schedule pull includes labels", async () => {
|
|
const zip = new JSZip();
|
|
const schedule = {
|
|
schedule: "0 0 * * *",
|
|
timezone: "UTC",
|
|
script_path: "u/admin/daily",
|
|
is_flow: false,
|
|
summary: "daily cleanup",
|
|
labels: ["cron", "maintenance"],
|
|
};
|
|
zip.file(
|
|
"u/admin/daily_cleanup.schedule.json",
|
|
JSON.stringify(schedule)
|
|
);
|
|
|
|
const content = await zip.generateAsync({ type: "uint8array" });
|
|
const loaded = await JSZip.loadAsync(content);
|
|
const parsed = JSON.parse(
|
|
await loaded
|
|
.file("u/admin/daily_cleanup.schedule.json")!
|
|
.async("text")
|
|
);
|
|
|
|
expect(parsed.labels).toEqual(["cron", "maintenance"]);
|
|
});
|
|
|
|
test("pull without labels doesn't include labels field", async () => {
|
|
const zip = new JSZip();
|
|
zip.file(
|
|
"u/admin/no_labels.script.json",
|
|
JSON.stringify({ summary: "no labels", schema: {}, kind: "script" })
|
|
);
|
|
zip.file(
|
|
"u/admin/no_labels.flow.json",
|
|
JSON.stringify({ summary: "no labels", value: { modules: [] } })
|
|
);
|
|
zip.file(
|
|
"u/admin/no_labels.resource.json",
|
|
JSON.stringify({ value: {}, resource_type: "c_test" })
|
|
);
|
|
zip.file(
|
|
"u/admin/no_labels.variable.json",
|
|
JSON.stringify({ value: "x", is_secret: false, description: "" })
|
|
);
|
|
zip.file(
|
|
"u/admin/no_labels.schedule.json",
|
|
JSON.stringify({
|
|
schedule: "0 0 * * *",
|
|
timezone: "UTC",
|
|
script_path: "u/admin/x",
|
|
is_flow: false,
|
|
})
|
|
);
|
|
|
|
const content = await zip.generateAsync({ type: "uint8array" });
|
|
const loaded = await JSZip.loadAsync(content);
|
|
|
|
for (const name of [
|
|
"u/admin/no_labels.script.json",
|
|
"u/admin/no_labels.flow.json",
|
|
"u/admin/no_labels.resource.json",
|
|
"u/admin/no_labels.variable.json",
|
|
"u/admin/no_labels.schedule.json",
|
|
]) {
|
|
const parsed = JSON.parse(await loaded.file(name)!.async("text"));
|
|
expect(parsed.labels).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
test("editing labels on pulled item and re-pushing preserves changes", () => {
|
|
// Simulate: pull -> edit labels -> push
|
|
const pulled = {
|
|
summary: "my script",
|
|
schema: {},
|
|
kind: "script" as const,
|
|
labels: ["old-label"],
|
|
};
|
|
|
|
// User edits labels
|
|
const edited = { ...pulled, labels: ["new-label", "another"] };
|
|
|
|
// Push constructs request body
|
|
const requestBody = {
|
|
content: "def main(): pass",
|
|
path: "u/admin/my_script",
|
|
summary: edited.summary,
|
|
kind: edited.kind,
|
|
labels: edited.labels,
|
|
};
|
|
|
|
expect(requestBody.labels).toEqual(["new-label", "another"]);
|
|
expect(requestBody.labels).not.toContain("old-label");
|
|
});
|
|
|
|
test("removing all labels on pulled item clears them on push", () => {
|
|
const pulled = {
|
|
summary: "labeled script",
|
|
schema: {},
|
|
kind: "script" as const,
|
|
labels: ["to-remove"],
|
|
};
|
|
|
|
// User removes all labels
|
|
const edited = { ...pulled };
|
|
delete (edited as any).labels;
|
|
|
|
const requestBody = {
|
|
content: "def main(): pass",
|
|
path: "u/admin/my_script",
|
|
summary: edited.summary,
|
|
kind: edited.kind,
|
|
labels: (edited as any).labels,
|
|
};
|
|
|
|
expect(requestBody.labels).toBeUndefined();
|
|
});
|
|
});
|