chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { formatDateTimeISO, formatUtcOffset } from "~/components/primitives/DateTime";
|
||||
|
||||
describe("formatDateTimeISO", () => {
|
||||
it("should format UTC dates with Z suffix", () => {
|
||||
const date = new Date("2025-04-29T14:01:19.000Z");
|
||||
const result = formatDateTimeISO(date, "UTC");
|
||||
expect(result).toBe("2025-04-29T14:01:19.000Z");
|
||||
});
|
||||
|
||||
describe("British Time (Europe/London)", () => {
|
||||
it("should format with +01:00 during BST (summer)", () => {
|
||||
// BST - British Summer Time (last Sunday in March to last Sunday in October)
|
||||
const summerDate = new Date("2025-07-15T14:01:19.000Z");
|
||||
const result = formatDateTimeISO(summerDate, "Europe/London");
|
||||
expect(result).toBe("2025-07-15T15:01:19.000+01:00");
|
||||
});
|
||||
|
||||
it("should format with +00:00 during GMT (winter)", () => {
|
||||
// GMT - Greenwich Mean Time (winter)
|
||||
const winterDate = new Date("2025-01-15T14:01:19.000Z");
|
||||
const result = formatDateTimeISO(winterDate, "Europe/London");
|
||||
expect(result).toBe("2025-01-15T14:01:19.000+00:00");
|
||||
});
|
||||
});
|
||||
|
||||
describe("US Pacific Time (America/Los_Angeles)", () => {
|
||||
it("should format with -07:00 during PDT (summer)", () => {
|
||||
// PDT - Pacific Daylight Time (second Sunday in March to first Sunday in November)
|
||||
const summerDate = new Date("2025-07-15T14:01:19.000Z");
|
||||
const result = formatDateTimeISO(summerDate, "America/Los_Angeles");
|
||||
expect(result).toBe("2025-07-15T07:01:19.000-07:00");
|
||||
});
|
||||
|
||||
it("should format with -08:00 during PST (winter)", () => {
|
||||
// PST - Pacific Standard Time (winter)
|
||||
const winterDate = new Date("2025-01-15T14:01:19.000Z");
|
||||
const result = formatDateTimeISO(winterDate, "America/Los_Angeles");
|
||||
expect(result).toBe("2025-01-15T06:01:19.000-08:00");
|
||||
});
|
||||
});
|
||||
|
||||
it("should preserve milliseconds", () => {
|
||||
const date = new Date("2025-04-29T14:01:19.123Z");
|
||||
const result = formatDateTimeISO(date, "UTC");
|
||||
expect(result).toBe("2025-04-29T14:01:19.123Z");
|
||||
});
|
||||
|
||||
it("should preserve milliseconds, not UTC", () => {
|
||||
const date = new Date("2025-04-29T14:01:19.123Z");
|
||||
const result = formatDateTimeISO(date, "Europe/London");
|
||||
expect(result).toBe("2025-04-29T15:01:19.123+01:00");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatUtcOffset", () => {
|
||||
const date = new Date("2026-06-30T13:16:26.000Z");
|
||||
|
||||
it("returns an empty string for UTC", () => {
|
||||
expect(formatUtcOffset(date, "UTC")).toBe("");
|
||||
});
|
||||
|
||||
it("returns an empty offset for UTC-equivalent zones", () => {
|
||||
expect(formatUtcOffset(date, "Atlantic/Reykjavik")).toBe("(UTC +0)");
|
||||
});
|
||||
|
||||
// The reported bug: the offset label must reflect the displayed timezone, not the
|
||||
// viewer's machine. A viewer on a UTC machine looking at a UTC+3 zone must see +3.
|
||||
it("reflects the timezone being displayed, not the viewer's machine", () => {
|
||||
expect(formatUtcOffset(date, "Europe/Moscow")).toBe("(UTC +3)");
|
||||
});
|
||||
|
||||
it("formats half-hour offsets", () => {
|
||||
expect(formatUtcOffset(date, "Asia/Kolkata")).toBe("(UTC +5:30)");
|
||||
});
|
||||
|
||||
it("formats negative offsets", () => {
|
||||
expect(formatUtcOffset(date, "America/Los_Angeles")).toBe("(UTC -7)");
|
||||
});
|
||||
|
||||
// The offset is derived from the given instant, so it stays correct across DST
|
||||
// boundaries regardless of what season the viewer is currently in.
|
||||
describe("is DST-aware for the given instant", () => {
|
||||
it("uses +0 for a London winter date", () => {
|
||||
expect(formatUtcOffset(new Date("2026-01-15T12:00:00.000Z"), "Europe/London")).toBe(
|
||||
"(UTC +0)"
|
||||
);
|
||||
});
|
||||
|
||||
it("uses +1 for a London summer date", () => {
|
||||
expect(formatUtcOffset(new Date("2026-07-15T12:00:00.000Z"), "Europe/London")).toBe(
|
||||
"(UTC +1)"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,327 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { createTSQLCompletion } from "~/components/code/tsql/tsqlCompletion";
|
||||
import type { TableSchema } from "@internal/tsql";
|
||||
|
||||
// Helper to create a mock completion context
|
||||
function createMockContext(doc: string, pos: number, explicit = false) {
|
||||
return {
|
||||
state: {
|
||||
doc: {
|
||||
toString: () => doc,
|
||||
sliceString: (from: number, to: number) => doc.slice(from, to),
|
||||
},
|
||||
},
|
||||
pos,
|
||||
explicit,
|
||||
matchBefore: (regex: RegExp) => {
|
||||
const beforePos = doc.slice(0, pos);
|
||||
const match = beforePos.match(new RegExp(regex.source + "$"));
|
||||
if (match) {
|
||||
return {
|
||||
from: pos - match[0].length,
|
||||
to: pos,
|
||||
text: match[0],
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
||||
// Test schema with enum columns
|
||||
const testSchema: TableSchema[] = [
|
||||
{
|
||||
name: "runs",
|
||||
clickhouseName: "trigger_dev.task_runs_v2",
|
||||
tenantColumns: {
|
||||
organizationId: "organization_id",
|
||||
projectId: "project_id",
|
||||
environmentId: "environment_id",
|
||||
},
|
||||
description: "Task runs table",
|
||||
columns: {
|
||||
id: { name: "id", type: "String", description: "Run ID" },
|
||||
status: {
|
||||
name: "status",
|
||||
type: "LowCardinality(String)",
|
||||
description: "Run status",
|
||||
allowedValues: ["Completed", "Failed", "Queued", "Executing"],
|
||||
},
|
||||
machine: {
|
||||
name: "machine",
|
||||
type: "LowCardinality(String)",
|
||||
description: "Machine preset",
|
||||
allowedValues: ["micro", "small-1x", "small-2x", "medium-1x"],
|
||||
},
|
||||
environment_type: {
|
||||
name: "environment_type",
|
||||
type: "LowCardinality(String)",
|
||||
description: "Environment type",
|
||||
allowedValues: ["PRODUCTION", "STAGING", "DEVELOPMENT", "PREVIEW"],
|
||||
},
|
||||
created_at: { name: "created_at", type: "DateTime64", description: "Creation time" },
|
||||
organization_id: { name: "organization_id", type: "String" },
|
||||
project_id: { name: "project_id", type: "String" },
|
||||
environment_id: { name: "environment_id", type: "String" },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "logs",
|
||||
clickhouseName: "trigger_dev.task_events_v2",
|
||||
tenantColumns: {
|
||||
organizationId: "organization_id",
|
||||
projectId: "project_id",
|
||||
environmentId: "environment_id",
|
||||
},
|
||||
description: "Task logs table",
|
||||
columns: {
|
||||
id: { name: "id", type: "String" },
|
||||
run_id: { name: "run_id", type: "String" },
|
||||
message: { name: "message", type: "String" },
|
||||
level: { name: "level", type: "String" },
|
||||
timestamp: { name: "timestamp", type: "DateTime64" },
|
||||
organization_id: { name: "organization_id", type: "String" },
|
||||
project_id: { name: "project_id", type: "String" },
|
||||
environment_id: { name: "environment_id", type: "String" },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe("createTSQLCompletion", () => {
|
||||
const completionSource = createTSQLCompletion(testSchema);
|
||||
|
||||
it("should return null for empty input without explicit trigger", () => {
|
||||
const context = createMockContext("", 0, false);
|
||||
const result = completionSource(context);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return completions when explicitly triggered", () => {
|
||||
const context = createMockContext("", 0, true);
|
||||
const result = completionSource(context);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.options.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should include tables in completions", () => {
|
||||
// When typing after FROM, tables should be available
|
||||
const doc = "SELECT * FROM r";
|
||||
const context = createMockContext(doc, doc.length, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const tableLabels = result?.options.map((o) => o.label);
|
||||
// Tables should always be available in completions
|
||||
expect(tableLabels).toContain("runs");
|
||||
expect(tableLabels).toContain("logs");
|
||||
});
|
||||
|
||||
it("should suggest columns after SELECT keyword", () => {
|
||||
const doc = "SELECT FROM runs";
|
||||
// Position cursor right after SELECT
|
||||
const pos = 7;
|
||||
const context = createMockContext(doc, pos, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
// Should include functions
|
||||
const labels = result?.options.map((o) => o.label) || [];
|
||||
expect(labels.some((l) => l === "count")).toBe(true);
|
||||
expect(labels.some((l) => l === "sum")).toBe(true);
|
||||
});
|
||||
|
||||
it("should suggest columns with table prefix for qualified references", () => {
|
||||
const doc = "SELECT runs. FROM runs";
|
||||
// Position cursor right after "runs."
|
||||
const pos = 12;
|
||||
const context = createMockContext(doc, pos, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const columnLabels = result?.options.map((o) => o.label);
|
||||
expect(columnLabels).toContain("id");
|
||||
expect(columnLabels).toContain("status");
|
||||
expect(columnLabels).toContain("created_at");
|
||||
});
|
||||
|
||||
it("should include SQL keywords in general context", () => {
|
||||
const doc = "S";
|
||||
const context = createMockContext(doc, doc.length, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const labels = result?.options.map((o) => o.label);
|
||||
expect(labels).toContain("SELECT");
|
||||
});
|
||||
|
||||
it("should include aggregate functions", () => {
|
||||
const doc = "SELECT ";
|
||||
const context = createMockContext(doc, doc.length, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const labels = result?.options.map((o) => o.label);
|
||||
expect(labels).toContain("count");
|
||||
expect(labels).toContain("sum");
|
||||
expect(labels).toContain("avg");
|
||||
expect(labels).toContain("min");
|
||||
expect(labels).toContain("max");
|
||||
});
|
||||
|
||||
it("should handle WHERE clause context", () => {
|
||||
const doc = "SELECT * FROM runs WHERE ";
|
||||
const context = createMockContext(doc, doc.length, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
// Should suggest columns
|
||||
const labels = result?.options.map((o) => o.label) || [];
|
||||
expect(labels).toContain("status");
|
||||
|
||||
// Should include conditional keywords
|
||||
expect(labels).toContain("AND");
|
||||
expect(labels).toContain("OR");
|
||||
});
|
||||
|
||||
describe("enum value completions", () => {
|
||||
it("should suggest enum values after = operator", () => {
|
||||
const doc = "SELECT * FROM runs WHERE status = ";
|
||||
const context = createMockContext(doc, doc.length, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const labels = result?.options.map((o) => o.label) || [];
|
||||
expect(labels).toContain("'Completed'");
|
||||
expect(labels).toContain("'Failed'");
|
||||
expect(labels).toContain("'Queued'");
|
||||
expect(labels).toContain("'Executing'");
|
||||
});
|
||||
|
||||
it("should suggest enum values after = with opening quote", () => {
|
||||
const doc = "SELECT * FROM runs WHERE status = '";
|
||||
const context = createMockContext(doc, doc.length, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const labels = result?.options.map((o) => o.label) || [];
|
||||
expect(labels).toContain("'Completed'");
|
||||
expect(labels).toContain("'Failed'");
|
||||
});
|
||||
|
||||
it("should suggest enum values with partial input", () => {
|
||||
const doc = "SELECT * FROM runs WHERE status = 'Comp";
|
||||
const context = createMockContext(doc, doc.length, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const labels = result?.options.map((o) => o.label) || [];
|
||||
expect(labels).toContain("'Completed'");
|
||||
});
|
||||
|
||||
it("should suggest enum values for machine column", () => {
|
||||
const doc = "SELECT * FROM runs WHERE machine = ";
|
||||
const context = createMockContext(doc, doc.length, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const labels = result?.options.map((o) => o.label) || [];
|
||||
expect(labels).toContain("'micro'");
|
||||
expect(labels).toContain("'small-1x'");
|
||||
expect(labels).toContain("'medium-1x'");
|
||||
});
|
||||
|
||||
it("should suggest enum values for environment_type column", () => {
|
||||
const doc = "SELECT * FROM runs WHERE environment_type = ";
|
||||
const context = createMockContext(doc, doc.length, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const labels = result?.options.map((o) => o.label) || [];
|
||||
expect(labels).toContain("'PRODUCTION'");
|
||||
expect(labels).toContain("'STAGING'");
|
||||
expect(labels).toContain("'DEVELOPMENT'");
|
||||
});
|
||||
|
||||
it("should suggest enum values after != operator", () => {
|
||||
const doc = "SELECT * FROM runs WHERE status != ";
|
||||
const context = createMockContext(doc, doc.length, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const labels = result?.options.map((o) => o.label) || [];
|
||||
expect(labels).toContain("'Completed'");
|
||||
expect(labels).toContain("'Failed'");
|
||||
});
|
||||
|
||||
it("should suggest enum values after IN (", () => {
|
||||
const doc = "SELECT * FROM runs WHERE status IN (";
|
||||
const context = createMockContext(doc, doc.length, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const labels = result?.options.map((o) => o.label) || [];
|
||||
expect(labels).toContain("'Completed'");
|
||||
expect(labels).toContain("'Failed'");
|
||||
});
|
||||
|
||||
it("should suggest enum values after comma in IN clause", () => {
|
||||
const doc = "SELECT * FROM runs WHERE status IN ('Completed', ";
|
||||
const context = createMockContext(doc, doc.length, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const labels = result?.options.map((o) => o.label) || [];
|
||||
expect(labels).toContain("'Failed'");
|
||||
expect(labels).toContain("'Queued'");
|
||||
});
|
||||
|
||||
it("should include closing quote in replacement range when auto-paired", () => {
|
||||
// Simulate cursor between auto-paired quotes: status = '|'
|
||||
const doc = "SELECT * FROM runs WHERE status = ''";
|
||||
const pos = doc.length - 1; // Cursor is between the quotes
|
||||
const context = createMockContext(doc, pos, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
// The 'to' should extend past the cursor to include the closing quote
|
||||
expect(result?.to).toBe(pos + 1);
|
||||
});
|
||||
|
||||
it("should not extend 'to' when no closing quote present", () => {
|
||||
const doc = "SELECT * FROM runs WHERE status = '";
|
||||
const context = createMockContext(doc, doc.length, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
// 'to' should be undefined (or not set) since there's no closing quote
|
||||
expect(result?.to).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should not suggest enum values for columns without allowedValues", () => {
|
||||
const doc = "SELECT * FROM runs WHERE id = ";
|
||||
const context = createMockContext(doc, doc.length, true);
|
||||
const result = completionSource(context);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
// Should return empty options for value context with non-enum column
|
||||
const labels = result?.options.map((o) => o.label) || [];
|
||||
expect(labels).not.toContain("'Completed'");
|
||||
expect(labels.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { isValidTSQLQuery, getTSQLError } from "~/components/code/tsql/tsqlLinter";
|
||||
|
||||
describe("tsqlLinter", () => {
|
||||
describe("isValidTSQLQuery", () => {
|
||||
it("should return true for empty queries", () => {
|
||||
expect(isValidTSQLQuery("")).toBe(true);
|
||||
expect(isValidTSQLQuery(" ")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for valid SELECT queries", () => {
|
||||
expect(isValidTSQLQuery("SELECT * FROM users")).toBe(true);
|
||||
expect(isValidTSQLQuery("SELECT id, name FROM users WHERE status = 'active'")).toBe(true);
|
||||
expect(isValidTSQLQuery("SELECT count(*) FROM users GROUP BY status")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for queries with ORDER BY", () => {
|
||||
expect(isValidTSQLQuery("SELECT * FROM users ORDER BY created_at DESC")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for queries with LIMIT", () => {
|
||||
expect(isValidTSQLQuery("SELECT * FROM users LIMIT 10")).toBe(true);
|
||||
expect(isValidTSQLQuery("SELECT * FROM users LIMIT 10 OFFSET 20")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for queries with JOINs", () => {
|
||||
expect(isValidTSQLQuery("SELECT * FROM users JOIN orders ON users.id = orders.user_id")).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
isValidTSQLQuery("SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for invalid syntax", () => {
|
||||
expect(isValidTSQLQuery("SELEC * FROM users")).toBe(false);
|
||||
expect(isValidTSQLQuery("SELECT * FORM users")).toBe(false);
|
||||
expect(isValidTSQLQuery("SELECT FROM users")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for incomplete queries", () => {
|
||||
expect(isValidTSQLQuery("SELECT * FROM")).toBe(false);
|
||||
expect(isValidTSQLQuery("SELECT")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTSQLError", () => {
|
||||
it("should return null for empty queries", () => {
|
||||
expect(getTSQLError("")).toBeNull();
|
||||
expect(getTSQLError(" ")).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for valid queries", () => {
|
||||
expect(getTSQLError("SELECT * FROM users")).toBeNull();
|
||||
expect(getTSQLError("SELECT id, name FROM users WHERE id = 1")).toBeNull();
|
||||
});
|
||||
|
||||
it("should return error message for invalid queries", () => {
|
||||
const error = getTSQLError("SELEC * FROM users");
|
||||
expect(error).not.toBeNull();
|
||||
expect(typeof error).toBe("string");
|
||||
});
|
||||
|
||||
it("should include position information in error", () => {
|
||||
const error = getTSQLError("SELECT * FORM users");
|
||||
expect(error).not.toBeNull();
|
||||
// Error message should contain line/column info
|
||||
expect(error).toContain("line");
|
||||
});
|
||||
|
||||
it("should handle unclosed string literals", () => {
|
||||
const error = getTSQLError("SELECT * FROM users WHERE name = 'test");
|
||||
expect(error).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { splitTag } from "~/components/runs/v3/RunTag";
|
||||
|
||||
describe("splitTag", () => {
|
||||
it("should return the original string when no separator is found", () => {
|
||||
expect(splitTag("simpletag")).toBe("simpletag");
|
||||
expect(splitTag("tag-with-dashes")).toBe("tag-with-dashes");
|
||||
expect(splitTag("tag.with.dots")).toBe("tag.with.dots");
|
||||
});
|
||||
|
||||
it("should return the original string when key is longer than 12 characters", () => {
|
||||
expect(splitTag("verylongcategory:prod")).toBe("verylongcategory:prod");
|
||||
expect(splitTag("verylongcategory_prod")).toBe("verylongcategory_prod");
|
||||
});
|
||||
|
||||
it("should split tag with underscore separator", () => {
|
||||
expect(splitTag("env_prod")).toEqual({ key: "env", value: "prod" });
|
||||
expect(splitTag("category_batch")).toEqual({ key: "category", value: "batch" });
|
||||
});
|
||||
|
||||
it("should split tag with colon separator", () => {
|
||||
expect(splitTag("env:prod")).toEqual({ key: "env", value: "prod" });
|
||||
expect(splitTag("category:batch")).toEqual({ key: "category", value: "batch" });
|
||||
expect(splitTag("customer:test_customer")).toEqual({ key: "customer", value: "test_customer" });
|
||||
});
|
||||
|
||||
it("should handle mixed delimiters", () => {
|
||||
expect(splitTag("category:batch_job")).toEqual({ key: "category", value: "batch_job" });
|
||||
expect(splitTag("status_error:500")).toEqual({ key: "status", value: "error:500" });
|
||||
});
|
||||
|
||||
it("should preserve common ID formats", () => {
|
||||
expect(splitTag("job_123_456")).toBe("job_123_456");
|
||||
expect(splitTag("run:123:456")).toBe("run:123:456");
|
||||
expect(splitTag("task123_job_456")).toBe("task123_job_456");
|
||||
});
|
||||
|
||||
it("should return original string when multiple separators are found", () => {
|
||||
expect(splitTag("env:prod:test")).toBe("env:prod:test");
|
||||
expect(splitTag("env_prod_test")).toBe("env_prod_test");
|
||||
});
|
||||
|
||||
it("should handle edge case with exactly 12 character key", () => {
|
||||
expect(splitTag("abcdefghijkl:value")).toEqual({ key: "abcdefghijkl", value: "value" });
|
||||
expect(splitTag("exactlytwelv_chars")).toEqual({ key: "exactlytwelv", value: "chars" });
|
||||
});
|
||||
|
||||
it("should handle empty values", () => {
|
||||
expect(splitTag("empty:")).toEqual({ key: "empty", value: "" });
|
||||
expect(splitTag("nothing_")).toEqual({ key: "nothing", value: "" });
|
||||
});
|
||||
|
||||
it("should handle special characters in values", () => {
|
||||
expect(splitTag("region:us-west-2")).toEqual({ key: "region", value: "us-west-2" });
|
||||
expect(splitTag("query:SELECT * FROM users")).toEqual({
|
||||
key: "query",
|
||||
value: "SELECT * FROM users",
|
||||
});
|
||||
expect(splitTag("path:/api/v1/users")).toEqual({ key: "path", value: "/api/v1/users" });
|
||||
});
|
||||
|
||||
it("should handle values containing numbers and special formats", () => {
|
||||
expect(splitTag("uuid:123e4567-e89b-12d3-a456-426614174000")).toEqual({
|
||||
key: "uuid",
|
||||
value: "123e4567-e89b-12d3-a456-426614174000",
|
||||
});
|
||||
expect(splitTag("ip_192.168.1.1")).toEqual({ key: "ip", value: "192.168.1.1" });
|
||||
expect(splitTag("date:2023-04-01T12:00:00Z")).toEqual({
|
||||
key: "date",
|
||||
value: "2023-04-01T12:00:00Z",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle keys with numbers", () => {
|
||||
expect(splitTag("env2:staging")).toEqual({ key: "env2", value: "staging" });
|
||||
expect(splitTag("v1_endpoint")).toEqual({ key: "v1", value: "endpoint" });
|
||||
});
|
||||
|
||||
it("should handle particularly complex mixed cases", () => {
|
||||
expect(splitTag("env:prod_us-west-2_replica")).toEqual({
|
||||
key: "env",
|
||||
value: "prod_us-west-2_replica",
|
||||
});
|
||||
expect(splitTag("status_error:connection:timeout")).toEqual({
|
||||
key: "status",
|
||||
value: "error:connection:timeout",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { toSafeUrl } from "~/components/runs/v3/agent/AgentMessageView";
|
||||
|
||||
describe("toSafeUrl", () => {
|
||||
it("allows http(s) and blob URLs", () => {
|
||||
expect(toSafeUrl("https://example.com/x")).toBe("https://example.com/x");
|
||||
expect(toSafeUrl("http://example.com/x")).toBe("http://example.com/x");
|
||||
expect(toSafeUrl("blob:https://example.com/uuid")).toBe("blob:https://example.com/uuid");
|
||||
});
|
||||
|
||||
it("rejects javascript: and other dangerous schemes", () => {
|
||||
expect(toSafeUrl("javascript:alert(1)")).toBeNull();
|
||||
expect(toSafeUrl("JavaScript:alert(1)")).toBeNull();
|
||||
expect(toSafeUrl("vbscript:msgbox(1)")).toBeNull();
|
||||
expect(toSafeUrl("file:///etc/passwd")).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects data: URLs unless inline images are explicitly allowed", () => {
|
||||
const dataImage = "data:image/png;base64,iVBORw0KGgo=";
|
||||
expect(toSafeUrl(dataImage)).toBeNull();
|
||||
expect(toSafeUrl(dataImage, true)).toBe(dataImage);
|
||||
// Only image data is allowed, even in image context — never data:text/html.
|
||||
expect(toSafeUrl("data:text/html,<script>alert(1)</script>", true)).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects relative URLs and non-string/malformed input", () => {
|
||||
expect(toSafeUrl("/relative/path")).toBeNull();
|
||||
expect(toSafeUrl("not a url")).toBeNull();
|
||||
expect(toSafeUrl(undefined)).toBeNull();
|
||||
expect(toSafeUrl(null)).toBeNull();
|
||||
expect(toSafeUrl(42)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user