240 lines
8.9 KiB
TypeScript
240 lines
8.9 KiB
TypeScript
import { describe, test, expect } from "vitest";
|
|
import type { SpanEvent } from "@trigger.dev/core/v3";
|
|
import { createTimelineSpanEventsFromSpanEvents } from "../app/utils/timelineSpanEvents";
|
|
import { millisecondsToNanoseconds } from "@trigger.dev/core/v3/utils/durations";
|
|
|
|
describe("createTimelineSpanEventsFromSpanEvents", () => {
|
|
const sampleSpanEvents: SpanEvent[] = [
|
|
{
|
|
name: "trigger.dev/start",
|
|
time: new Date("2025-04-04T08:39:27.046Z"),
|
|
properties: { event: "fork", duration: 127 },
|
|
},
|
|
{
|
|
name: "trigger.dev/start",
|
|
time: new Date("2025-04-04T08:39:26.985Z"),
|
|
properties: { event: "create_attempt", duration: 56 },
|
|
},
|
|
{
|
|
name: "trigger.dev/start",
|
|
time: new Date("2025-04-04T08:39:26.980Z"),
|
|
properties: { event: "dequeue", duration: 0 },
|
|
},
|
|
{
|
|
name: "trigger.dev/start",
|
|
time: new Date("2025-04-04T08:39:27.224Z"),
|
|
properties: {
|
|
file: "src/trigger/chat.ts",
|
|
event: "import",
|
|
duration: 67,
|
|
entryPoint: "/project/.trigger/tmp/build-AL7zTl/src/trigger/chat.mjs",
|
|
},
|
|
},
|
|
];
|
|
|
|
// Sample events without fork event
|
|
const eventsWithoutFork: SpanEvent[] = [
|
|
{
|
|
name: "trigger.dev/start",
|
|
time: new Date("2025-04-04T08:39:26.980Z"),
|
|
properties: { event: "dequeue", duration: 0 },
|
|
},
|
|
{
|
|
name: "trigger.dev/start",
|
|
time: new Date("2025-04-04T08:39:26.985Z"),
|
|
properties: { event: "create_attempt", duration: 56 },
|
|
},
|
|
{
|
|
name: "trigger.dev/start",
|
|
time: new Date("2025-04-04T08:39:27.224Z"),
|
|
properties: {
|
|
file: "src/trigger/chat.ts",
|
|
event: "import",
|
|
duration: 67,
|
|
entryPoint: "/project/.trigger/tmp/build-AL7zTl/src/trigger/chat.mjs",
|
|
},
|
|
},
|
|
];
|
|
|
|
test("should filter non-admin events when isAdmin is false", () => {
|
|
const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, false);
|
|
|
|
// Only dequeue and fork events should be visible for non-admins
|
|
expect(result.length).toBe(3);
|
|
expect(result.some((event) => event.name === "Dequeued")).toBe(true);
|
|
expect(result.some((event) => event.name === "Launched")).toBe(true);
|
|
expect(result.some((event) => event.name === "Attempt created")).toBe(false);
|
|
expect(result.some((event) => event.name.includes("Importing"))).toBe(true);
|
|
});
|
|
|
|
test("should include all events when isAdmin is true", () => {
|
|
const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true);
|
|
|
|
expect(result.length).toBe(4);
|
|
expect(result.some((event) => event.name === "Dequeued")).toBe(true);
|
|
expect(result.some((event) => event.name === "Launched")).toBe(true);
|
|
expect(result.some((event) => event.name === "Attempt created")).toBe(true);
|
|
expect(result.some((event) => event.name === "Importing task file")).toBe(true);
|
|
});
|
|
|
|
test("should sort events by timestamp", () => {
|
|
const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true);
|
|
|
|
// Events should be sorted by time (offset)
|
|
expect(result[0].name).toBe("Dequeued");
|
|
expect(result[1].name).toBe("Attempt created");
|
|
expect(result[2].name).toBe("Launched");
|
|
expect(result[3].name).toBe("Importing task file");
|
|
});
|
|
|
|
test("should calculate offsets correctly from the first event", () => {
|
|
const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true);
|
|
|
|
// First event (dequeue) should have offset 0
|
|
const firstEventTime = new Date("2025-04-04T08:39:26.980Z").getTime();
|
|
|
|
expect(result[0].offset).toBe(0);
|
|
expect(result[1].offset).toBe(
|
|
millisecondsToNanoseconds(new Date("2025-04-04T08:39:26.985Z").getTime() - firstEventTime)
|
|
);
|
|
expect(result[2].offset).toBe(
|
|
millisecondsToNanoseconds(new Date("2025-04-04T08:39:27.046Z").getTime() - firstEventTime)
|
|
);
|
|
expect(result[3].offset).toBe(
|
|
millisecondsToNanoseconds(new Date("2025-04-04T08:39:27.224Z").getTime() - firstEventTime)
|
|
);
|
|
});
|
|
|
|
test("should use the provided relativeStartTime when specified", () => {
|
|
const customStartTime = new Date("2025-04-04T08:39:26.900Z").getTime();
|
|
const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true, customStartTime);
|
|
|
|
// Offsets should be calculated relative to customStartTime
|
|
expect(result[0].offset).toBe(
|
|
millisecondsToNanoseconds(new Date("2025-04-04T08:39:26.980Z").getTime() - customStartTime)
|
|
);
|
|
expect(result[1].offset).toBe(
|
|
millisecondsToNanoseconds(new Date("2025-04-04T08:39:26.985Z").getTime() - customStartTime)
|
|
);
|
|
expect(result[2].offset).toBe(
|
|
millisecondsToNanoseconds(new Date("2025-04-04T08:39:27.046Z").getTime() - customStartTime)
|
|
);
|
|
expect(result[3].offset).toBe(
|
|
millisecondsToNanoseconds(new Date("2025-04-04T08:39:27.224Z").getTime() - customStartTime)
|
|
);
|
|
});
|
|
|
|
test("should handle empty span events array", () => {
|
|
const result = createTimelineSpanEventsFromSpanEvents([], true);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
test("should handle undefined span events", () => {
|
|
const result = createTimelineSpanEventsFromSpanEvents(
|
|
undefined as unknown as SpanEvent[],
|
|
true
|
|
);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
test("should handle non-matching span events", () => {
|
|
const nonMatchingEvents: SpanEvent[] = [
|
|
{
|
|
name: "non-trigger.dev/event",
|
|
time: new Date("2025-04-04T08:39:27.046Z"),
|
|
properties: { event: "something", duration: 127 },
|
|
},
|
|
];
|
|
|
|
const result = createTimelineSpanEventsFromSpanEvents(nonMatchingEvents, true);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
test("should set marker variant correctly", () => {
|
|
const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true);
|
|
|
|
// First event should have start-cap marker
|
|
expect(result[0].markerVariant).toBe("start-cap");
|
|
|
|
// Other events should have dot-hollow marker
|
|
for (let i = 1; i < result.length; i++) {
|
|
expect(result[i].markerVariant).toBe("dot-hollow");
|
|
}
|
|
});
|
|
|
|
test("should include helpText for known events", () => {
|
|
const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true);
|
|
|
|
expect(result.find((e) => e.name === "Dequeued")?.helpText).toBe(
|
|
"The run was dequeued from the queue"
|
|
);
|
|
expect(result.find((e) => e.name === "Launched")?.helpText).toBe(
|
|
"The process was created to run the task"
|
|
);
|
|
expect(result.find((e) => e.name === "Attempt created")?.helpText).toBe(
|
|
"An attempt was created for the run"
|
|
);
|
|
expect(result.find((e) => e.name === "Importing task file")?.helpText).toBe(
|
|
"A task file was imported"
|
|
);
|
|
});
|
|
|
|
test("should not attach raw span event properties to timeline events", () => {
|
|
const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true);
|
|
|
|
expect(result.length).toBeGreaterThan(0);
|
|
for (const event of result) {
|
|
expect(event).not.toHaveProperty("properties");
|
|
}
|
|
});
|
|
|
|
test("should preserve duration from span events", () => {
|
|
const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, true);
|
|
|
|
expect(result.find((e) => e.name === "Dequeued")?.duration).toBe(0);
|
|
expect(result.find((e) => e.name === "Launched")?.duration).toBe(127);
|
|
expect(result.find((e) => e.name === "Attempt created")?.duration).toBe(56);
|
|
expect(result.find((e) => e.name === "Importing task file")?.duration).toBe(67);
|
|
});
|
|
|
|
test("should use fallback name for import event without file property", () => {
|
|
const eventsWithoutFile: SpanEvent[] = [
|
|
{
|
|
name: "trigger.dev/start",
|
|
time: new Date("2025-04-04T08:39:27.224Z"),
|
|
properties: {
|
|
event: "import",
|
|
duration: 67,
|
|
},
|
|
},
|
|
];
|
|
|
|
const result = createTimelineSpanEventsFromSpanEvents(eventsWithoutFile, true);
|
|
|
|
expect(result.length).toBe(1);
|
|
expect(result[0].name).toBe("Importing task file");
|
|
});
|
|
|
|
test("should include import events for non-admin when no fork event exists", () => {
|
|
const result = createTimelineSpanEventsFromSpanEvents(eventsWithoutFork, false);
|
|
|
|
// Without fork event, import should also be visible for non-admins
|
|
expect(result.length).toBe(2);
|
|
expect(result.some((event) => event.name === "Dequeued")).toBe(true);
|
|
expect(result.some((event) => event.name === "Importing task file")).toBe(true);
|
|
|
|
// create_attempt should still be admin-only
|
|
expect(result.some((event) => event.name === "Attempt created")).toBe(false);
|
|
});
|
|
|
|
test.skip("should filter import events for non-admin when fork event exists", () => {
|
|
const result = createTimelineSpanEventsFromSpanEvents(sampleSpanEvents, false);
|
|
|
|
// With fork event, import should be hidden for non-admins
|
|
expect(result.length).toBe(2);
|
|
expect(result.some((event) => event.name === "Dequeued")).toBe(true);
|
|
expect(result.some((event) => event.name === "Launched")).toBe(true);
|
|
expect(result.some((event) => event.name.includes("Importing"))).toBe(false);
|
|
});
|
|
});
|