Files
windmill/cli/test/pipeline_upload_unit.test.ts
Ruben Fiszel d65f58c388 fix: pipeline dogfooding fixes — SCD2 data-test scope, --partition, s3object upload binding (#9875)
* fix: scope SCD2 built-in data tests to current rows

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: add --partition to pipeline run and fix duckdb s3object upload binding

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: note filesystem storage type is dev-only in storage settings

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: use ISO week for weekly partition default in pipeline run

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:04:54 +02:00

152 lines
5.5 KiB
TypeScript

import { expect, test } from "bun:test";
import {
devUploadKey,
parseArgBinding,
parseS3Uri,
parseUploadBinding,
s3Arg,
s3ObjectParams,
} from "../src/commands/pipeline/pipelineUpload.ts";
test("parseUploadBinding: bare script + local source infers the param later", () => {
expect(parseUploadBinding("ingest=./fixtures/events.csv")).toEqual({
scriptTok: "ingest",
source: "./fixtures/events.csv",
});
});
test("parseUploadBinding: explicit :param on the left", () => {
expect(parseUploadBinding("f/analytics/ingest:file=./events.csv")).toEqual({
scriptTok: "f/analytics/ingest",
param: "file",
source: "./events.csv",
});
});
test("parseUploadBinding: s3:// source — its `:` is right of the first `=`, so it stays intact", () => {
expect(parseUploadBinding("ingest=s3://bucket/2026/events.csv")).toEqual({
scriptTok: "ingest",
source: "s3://bucket/2026/events.csv",
});
expect(parseUploadBinding("ingest:file=s3://bucket/k")).toEqual({
scriptTok: "ingest",
param: "file",
source: "s3://bucket/k",
});
});
test("parseUploadBinding: malformed specs throw", () => {
expect(() => parseUploadBinding("ingest")).toThrow(); // no `=`
expect(() => parseUploadBinding("=./x.csv")).toThrow(); // no script
expect(() => parseUploadBinding("ingest=")).toThrow(); // no source
expect(() => parseUploadBinding("ingest:=./x.csv")).toThrow(); // empty param
expect(() => parseUploadBinding(":file=./x.csv")).toThrow(); // empty script
});
test("parseArgBinding: JSON values parse, non-JSON falls back to the raw string", () => {
// a bare date is not valid JSON → string
expect(parseArgBinding("daily_report:partition=2026-07-02")).toEqual({
scriptTok: "daily_report",
param: "partition",
value: "2026-07-02",
});
// JSON scalars/objects keep their type
expect(parseArgBinding("stats:limit=10")).toEqual({
scriptTok: "stats",
param: "limit",
value: 10,
});
expect(parseArgBinding("stats:enabled=true")).toEqual({
scriptTok: "stats",
param: "enabled",
value: true,
});
expect(parseArgBinding('f/pd/stats:opts={"a":1}')).toEqual({
scriptTok: "f/pd/stats",
param: "opts",
value: { a: 1 },
});
// quoting forces a string even for number-looking values
expect(parseArgBinding('stats:code="42"')).toEqual({
scriptTok: "stats",
param: "code",
value: "42",
});
// split on the FIRST `=` — values may contain `=`
expect(parseArgBinding("stats:expr=a=b")).toEqual({
scriptTok: "stats",
param: "expr",
value: "a=b",
});
});
test("parseArgBinding: :param is required and malformed specs throw", () => {
expect(() => parseArgBinding("stats=10")).toThrow(); // no :param
expect(() => parseArgBinding("stats:limit")).toThrow(); // no `=`
expect(() => parseArgBinding(":limit=10")).toThrow(); // empty script
expect(() => parseArgBinding("stats:=10")).toThrow(); // empty param
});
test("s3ObjectParams: only resource-s3_object properties, in declaration order", () => {
const schema = {
properties: {
note: { type: "string" },
file: { type: "object", format: "resource-s3_object" },
db: { type: "object", format: "resource-postgresql" },
backup: { type: "object", format: "resource-s3_object" },
},
};
expect(s3ObjectParams(schema)).toEqual(["file", "backup"]);
expect(s3ObjectParams({})).toEqual([]);
expect(s3ObjectParams(undefined)).toEqual([]);
});
test("s3ObjectParams: matches SQL-dialect resource-S3Object casing too", () => {
// TS/python parsers emit `resource-s3_object`; the SQL dialects (duckdb
// `-- $file (s3object)`) emit `resource-S3Object` — both must bind.
const schema = {
properties: {
file: { type: "object", format: "resource-S3Object" },
other: { type: "object", format: "resource-s3" },
},
};
expect(s3ObjectParams(schema)).toEqual(["file"]);
});
test("devUploadKey: key scoped by script path + param so basenames don't clobber", () => {
expect(devUploadKey("f/analytics/ingest", "file", "./fixtures/events.csv")).toBe(
"wmilldev/pipeline/f/analytics/ingest/file/events.csv",
);
// same basename, different script/param → distinct keys
expect(devUploadKey("f/analytics/other", "file", "/abs/events.csv")).toBe(
"wmilldev/pipeline/f/analytics/other/file/events.csv",
);
expect(devUploadKey("f/analytics/ingest", "backup", "/abs/events.csv")).toBe(
"wmilldev/pipeline/f/analytics/ingest/backup/events.csv",
);
});
test("s3Arg: shapes the S3Object run-arg", () => {
expect(s3Arg("file", { s3: "wmilldev/pipeline/analytics/events.csv" })).toEqual({
file: { s3: "wmilldev/pipeline/analytics/events.csv" },
});
expect(s3Arg("file", { s3: "k.csv", storage: "secondary" })).toEqual({
file: { s3: "k.csv", storage: "secondary" },
});
});
test("parseS3Uri: canonical `s3://<storage>/<key>` (authority is the named storage)", () => {
// named storage — the authority is the storage, the rest is the key
expect(parseS3Uri("s3://secondary/k.csv")).toEqual({ s3: "k.csv", storage: "secondary" });
// empty authority (`s3:///key`) ⇒ default storage, incl. nested keys
expect(parseS3Uri("s3:///events.csv")).toEqual({ s3: "events.csv" });
expect(parseS3Uri("s3:///raw/2026/events.csv")).toEqual({ s3: "raw/2026/events.csv" });
// named storage with a nested key
expect(parseS3Uri("s3://secondary/raw/2026/events.csv")).toEqual({
s3: "raw/2026/events.csv",
storage: "secondary",
});
// no `/` after the scheme → no authority; whole rest is the default-storage key
expect(parseS3Uri("s3://events.csv")).toEqual({ s3: "events.csv" });
});