getObjects.test.ts•42.8 kB
import { join } from "path";
import { getObjects } from "../getObjects";
import {
TrellisObjectType,
TrellisObjectStatus,
TrellisObjectPriority,
} from "../../../models";
describe("getObjects", () => {
const testRoot = join(__dirname, "schema1_0");
describe("basic functionality", () => {
it("should return all open objects from the test data", async () => {
const objects = await getObjects(testRoot);
// Should find 17 objects (excludes 3 closed tasks by default)
expect(objects).toHaveLength(17);
// All returned objects should be valid TrellisObject instances
objects.forEach((obj) => {
expect(obj).toHaveProperty("id");
expect(obj).toHaveProperty("type");
expect(obj).toHaveProperty("title");
expect(obj).toHaveProperty("status");
expect(obj).toHaveProperty("priority");
expect(obj).toHaveProperty("schema");
expect(obj.schema).toBe("v1.0");
});
});
it("should include objects of all types", async () => {
const objects = await getObjects(testRoot);
const projectObjects = objects.filter(
(obj) => obj.type === TrellisObjectType.PROJECT,
);
const epicObjects = objects.filter(
(obj) => obj.type === TrellisObjectType.EPIC,
);
const featureObjects = objects.filter(
(obj) => obj.type === TrellisObjectType.FEATURE,
);
const taskObjects = objects.filter(
(obj) => obj.type === TrellisObjectType.TASK,
);
expect(projectObjects.length).toBeGreaterThan(0);
expect(epicObjects.length).toBeGreaterThan(0);
expect(featureObjects.length).toBeGreaterThan(0);
expect(taskObjects.length).toBeGreaterThan(0);
// Verify specific objects exist
expect(
projectObjects.some((obj) => obj.id === "P-ecommerce-platform"),
).toBe(true);
expect(projectObjects.some((obj) => obj.id === "P-mobile-app")).toBe(
true,
);
expect(epicObjects.some((obj) => obj.id === "E-user-management")).toBe(
true,
);
expect(
featureObjects.some((obj) => obj.id === "F-user-authentication"),
).toBe(true);
expect(taskObjects.some((obj) => obj.id === "T-setup-database")).toBe(
true,
);
});
it("should include both hierarchical and standalone objects", async () => {
const objects = await getObjects(testRoot);
// Standalone features (not within projects)
const standaloneFeatures = objects.filter(
(obj) =>
obj.type === TrellisObjectType.FEATURE &&
(obj.id === "F-user-authentication" ||
obj.id === "F-api-documentation"),
);
expect(standaloneFeatures.length).toBe(2);
// Standalone tasks (not within projects) - only open ones are included by default
const standaloneTasks = objects.filter(
(obj) =>
obj.type === TrellisObjectType.TASK &&
["T-setup-database", "T-implement-logging"].includes(obj.id),
);
expect(standaloneTasks.length).toBe(2);
});
});
describe("includeClosed parameter", () => {
it("should exclude closed objects by default", async () => {
const objects = await getObjects(testRoot);
// Should not include any DONE or WONT_DO objects
const closedObjects = objects.filter(
(obj) =>
obj.status === TrellisObjectStatus.DONE ||
obj.status === TrellisObjectStatus.WONT_DO,
);
expect(closedObjects.length).toBe(0);
// Should only include open objects (not DONE or WONT_DO)
objects.forEach((obj) => {
expect(obj.status).not.toBe(TrellisObjectStatus.DONE);
expect(obj.status).not.toBe(TrellisObjectStatus.WONT_DO);
});
});
it("should exclude closed objects when includeClosed is false", async () => {
const objects = await getObjects(testRoot, false);
// Should not include any DONE or WONT_DO objects
const closedObjects = objects.filter(
(obj) =>
obj.status === TrellisObjectStatus.DONE ||
obj.status === TrellisObjectStatus.WONT_DO,
);
expect(closedObjects).toHaveLength(0);
// Should still include open objects
const openObjects = objects.filter(
(obj) =>
obj.status !== TrellisObjectStatus.DONE &&
obj.status !== TrellisObjectStatus.WONT_DO,
);
expect(openObjects.length).toBeGreaterThan(0);
expect(openObjects.length).toBe(objects.length);
});
it("should include closed objects when includeClosed is explicitly true", async () => {
const objects = await getObjects(testRoot, true);
// Should include DONE objects
const doneObjects = objects.filter(
(obj) => obj.status === TrellisObjectStatus.DONE,
);
expect(doneObjects.length).toBeGreaterThan(0);
// Should also include open objects
const openObjects = objects.filter(
(obj) =>
obj.status !== TrellisObjectStatus.DONE &&
obj.status !== TrellisObjectStatus.WONT_DO,
);
expect(openObjects.length).toBeGreaterThan(0);
});
it("should have more objects when includeClosed is true", async () => {
const defaultObjects = await getObjects(testRoot); // excludes closed by default
const allObjects = await getObjects(testRoot, true); // includes closed
expect(allObjects.length).toBeGreaterThan(defaultObjects.length);
// The difference should be exactly the number of closed objects
const closedObjects = allObjects.filter(
(obj) =>
obj.status === TrellisObjectStatus.DONE ||
obj.status === TrellisObjectStatus.WONT_DO,
);
expect(allObjects.length - defaultObjects.length).toBe(
closedObjects.length,
);
expect(closedObjects.length).toBeGreaterThan(0); // Should have some closed objects
});
it("should filter both DONE and WONT_DO objects when includeClosed is false", async () => {
// First get all objects to see what we have
const allObjects = await getObjects(testRoot, true);
const filteredObjects = await getObjects(testRoot, false);
// Count DONE and WONT_DO objects in all objects
const doneObjects = allObjects.filter(
(obj) => obj.status === TrellisObjectStatus.DONE,
);
const wontDoObjects = allObjects.filter(
(obj) => obj.status === TrellisObjectStatus.WONT_DO,
);
// Filtered objects should not contain any DONE or WONT_DO objects
const filteredDoneObjects = filteredObjects.filter(
(obj) => obj.status === TrellisObjectStatus.DONE,
);
const filteredWontDoObjects = filteredObjects.filter(
(obj) => obj.status === TrellisObjectStatus.WONT_DO,
);
expect(filteredDoneObjects.length).toBe(0);
expect(filteredWontDoObjects.length).toBe(0);
// The difference should be the sum of DONE and WONT_DO objects
const expectedDifference = doneObjects.length + wontDoObjects.length;
expect(allObjects.length - filteredObjects.length).toBe(
expectedDifference,
);
});
});
describe("scope parameter", () => {
it("should filter objects by project scope", async () => {
const objects = await getObjects(testRoot, true, "P-ecommerce-platform");
// Should only include objects within the P-ecommerce-platform project
expect(objects.some((obj) => obj.id === "P-ecommerce-platform")).toBe(
true,
);
expect(objects.some((obj) => obj.id === "E-user-management")).toBe(true);
expect(objects.some((obj) => obj.id === "E-product-catalog")).toBe(true);
expect(objects.some((obj) => obj.id === "F-user-registration")).toBe(
true,
);
expect(objects.some((obj) => obj.id === "T-setup-user-schema")).toBe(
true,
);
expect(
objects.some((obj) => obj.id === "T-create-registration-form"),
).toBe(true);
// Should NOT include objects from other projects
expect(objects.some((obj) => obj.id === "P-mobile-app")).toBe(false);
expect(objects.some((obj) => obj.id === "E-offline-sync")).toBe(false);
// Should NOT include standalone features or tasks
expect(objects.some((obj) => obj.id === "F-user-authentication")).toBe(
false,
);
expect(objects.some((obj) => obj.id === "T-setup-database")).toBe(false);
});
it("should filter objects by epic scope", async () => {
const objects = await getObjects(testRoot, true, "E-user-management");
// Should only include objects within the E-user-management epic
expect(objects.some((obj) => obj.id === "E-user-management")).toBe(true);
expect(objects.some((obj) => obj.id === "F-user-registration")).toBe(
true,
);
expect(objects.some((obj) => obj.id === "T-setup-user-schema")).toBe(
true,
);
expect(
objects.some((obj) => obj.id === "T-create-registration-form"),
).toBe(true);
// Should NOT include objects from other epics
expect(objects.some((obj) => obj.id === "E-product-catalog")).toBe(false);
expect(objects.some((obj) => obj.id === "F-product-search")).toBe(false);
});
it("should filter objects by feature scope", async () => {
const objects = await getObjects(testRoot, true, "F-user-registration");
// Should only include objects within the F-user-registration feature
expect(objects.some((obj) => obj.id === "F-user-registration")).toBe(
true,
);
expect(objects.some((obj) => obj.id === "T-setup-user-schema")).toBe(
true,
);
expect(
objects.some((obj) => obj.id === "T-create-registration-form"),
).toBe(true);
// Should NOT include objects from other features
expect(objects.some((obj) => obj.id === "F-product-search")).toBe(false);
expect(
objects.some((obj) => obj.id === "T-implement-elasticsearch"),
).toBe(false);
});
it("should work with standalone features", async () => {
const objects = await getObjects(testRoot, true, "F-user-authentication");
// Should only include objects within the standalone F-user-authentication feature
expect(objects.some((obj) => obj.id === "F-user-authentication")).toBe(
true,
);
expect(objects.some((obj) => obj.id === "T-implement-login")).toBe(true);
expect(objects.some((obj) => obj.id === "T-setup-auth-models")).toBe(
true,
);
// Should NOT include objects from project hierarchies
expect(objects.some((obj) => obj.id === "P-ecommerce-platform")).toBe(
false,
);
expect(objects.some((obj) => obj.id === "F-user-registration")).toBe(
false,
);
});
it("should return empty array for non-existent scope", async () => {
const objects = await getObjects(testRoot, true, "P-non-existent");
expect(objects).toEqual([]);
});
});
describe("type parameter", () => {
it("should filter objects by PROJECT type", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
TrellisObjectType.PROJECT,
);
expect(objects.length).toBeGreaterThan(0);
objects.forEach((obj) => {
expect(obj.type).toBe(TrellisObjectType.PROJECT);
});
expect(objects.some((obj) => obj.id === "P-ecommerce-platform")).toBe(
true,
);
expect(objects.some((obj) => obj.id === "P-mobile-app")).toBe(true);
});
it("should filter objects by EPIC type", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
TrellisObjectType.EPIC,
);
expect(objects.length).toBeGreaterThan(0);
objects.forEach((obj) => {
expect(obj.type).toBe(TrellisObjectType.EPIC);
});
expect(objects.some((obj) => obj.id === "E-user-management")).toBe(true);
expect(objects.some((obj) => obj.id === "E-product-catalog")).toBe(true);
});
it("should filter objects by FEATURE type", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
TrellisObjectType.FEATURE,
);
expect(objects.length).toBeGreaterThan(0);
objects.forEach((obj) => {
expect(obj.type).toBe(TrellisObjectType.FEATURE);
});
expect(objects.some((obj) => obj.id === "F-user-authentication")).toBe(
true,
);
expect(objects.some((obj) => obj.id === "F-user-registration")).toBe(
true,
);
});
it("should filter objects by TASK type", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
TrellisObjectType.TASK,
);
expect(objects.length).toBeGreaterThan(0);
objects.forEach((obj) => {
expect(obj.type).toBe(TrellisObjectType.TASK);
});
expect(objects.some((obj) => obj.id === "T-setup-database")).toBe(true);
expect(objects.some((obj) => obj.id === "T-implement-login")).toBe(true);
});
});
describe("status parameter", () => {
it("should filter objects by OPEN status", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
undefined,
TrellisObjectStatus.OPEN,
);
expect(objects.length).toBeGreaterThan(0);
objects.forEach((obj) => {
expect(obj.status).toBe(TrellisObjectStatus.OPEN);
});
// Should include some open objects
expect(objects.some((obj) => obj.id === "T-setup-database")).toBe(true);
expect(objects.some((obj) => obj.id === "T-implement-login")).toBe(true);
});
it("should filter objects by IN_PROGRESS status", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
undefined,
TrellisObjectStatus.IN_PROGRESS,
);
objects.forEach((obj) => {
expect(obj.status).toBe(TrellisObjectStatus.IN_PROGRESS);
});
// Should include any in-progress objects if they exist
const inProgressObjects = objects.filter(
(obj) => obj.status === TrellisObjectStatus.IN_PROGRESS,
);
expect(inProgressObjects.length).toBe(objects.length);
});
it("should filter objects by DONE status", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
undefined,
TrellisObjectStatus.DONE,
);
objects.forEach((obj) => {
expect(obj.status).toBe(TrellisObjectStatus.DONE);
});
// Should include closed objects
expect(objects.some((obj) => obj.id === "T-project-initialization")).toBe(
true,
);
expect(objects.some((obj) => obj.id === "T-setup-user-schema")).toBe(
true,
);
});
it("should return empty array when no objects match status", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
undefined,
TrellisObjectStatus.WONT_DO,
);
expect(objects).toEqual([]);
});
});
describe("priority parameter", () => {
it("should filter objects by HIGH priority", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
TrellisObjectPriority.HIGH,
);
objects.forEach((obj) => {
expect(obj.priority).toBe(TrellisObjectPriority.HIGH);
});
// Verify we have some high priority objects
if (objects.length > 0) {
expect(objects.length).toBeGreaterThan(0);
}
});
it("should filter objects by MEDIUM priority", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
TrellisObjectPriority.MEDIUM,
);
objects.forEach((obj) => {
expect(obj.priority).toBe(TrellisObjectPriority.MEDIUM);
});
});
it("should filter objects by LOW priority", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
TrellisObjectPriority.LOW,
);
objects.forEach((obj) => {
expect(obj.priority).toBe(TrellisObjectPriority.LOW);
});
});
it("should return all objects when no priority filter is specified", async () => {
const allObjects = await getObjects(testRoot, true);
const highPriorityObjects = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
TrellisObjectPriority.HIGH,
);
const mediumPriorityObjects = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
TrellisObjectPriority.MEDIUM,
);
const lowPriorityObjects = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
TrellisObjectPriority.LOW,
);
// All filtered objects combined should equal all objects
const filteredTotal =
highPriorityObjects.length +
mediumPriorityObjects.length +
lowPriorityObjects.length;
expect(filteredTotal).toBe(allObjects.length);
});
});
describe("combined parameters", () => {
it("should work with includeClosed=false and scope", async () => {
const objects = await getObjects(testRoot, false, "E-user-management");
// Should only include objects within the E-user-management epic
// and exclude closed objects
objects.forEach((obj) => {
expect(obj.status).not.toBe(TrellisObjectStatus.DONE);
});
expect(
objects.some((obj) => obj.id === "T-create-registration-form"),
).toBe(true);
expect(objects.some((obj) => obj.id === "T-setup-user-schema")).toBe(
false,
); // This is closed
});
it("should work with scope and type filters", async () => {
const objects = await getObjects(
testRoot,
true,
"P-ecommerce-platform",
TrellisObjectType.TASK,
);
// Should only include tasks within the P-ecommerce-platform project
objects.forEach((obj) => {
expect(obj.type).toBe(TrellisObjectType.TASK);
});
expect(objects.some((obj) => obj.id === "T-setup-user-schema")).toBe(
true,
);
expect(
objects.some((obj) => obj.id === "T-create-registration-form"),
).toBe(true);
// Should not include tasks from other projects or standalone tasks
expect(objects.some((obj) => obj.id === "T-setup-database")).toBe(false);
expect(objects.some((obj) => obj.id === "T-implement-cache-layer")).toBe(
false,
);
});
it("should work with status and priority filters", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
undefined,
TrellisObjectStatus.OPEN,
TrellisObjectPriority.HIGH,
);
// Should only include open, high priority objects
objects.forEach((obj) => {
expect(obj.status).toBe(TrellisObjectStatus.OPEN);
expect(obj.priority).toBe(TrellisObjectPriority.HIGH);
});
});
it("should work with scope, type, and status filters", async () => {
const objects = await getObjects(
testRoot,
true,
"P-ecommerce-platform",
TrellisObjectType.TASK,
TrellisObjectStatus.OPEN,
);
// Should only include open tasks within the P-ecommerce-platform project
objects.forEach((obj) => {
expect(obj.type).toBe(TrellisObjectType.TASK);
expect(obj.status).toBe(TrellisObjectStatus.OPEN);
});
// Should not include tasks from other projects or closed tasks
expect(objects.some((obj) => obj.id === "T-setup-database")).toBe(false);
expect(objects.some((obj) => obj.id === "T-setup-user-schema")).toBe(
false,
); // This is closed
});
it("should work with all parameters combined", async () => {
const objects = await getObjects(
testRoot,
false,
"E-user-management",
TrellisObjectType.TASK,
TrellisObjectStatus.OPEN,
TrellisObjectPriority.MEDIUM,
);
// Should only include open, medium priority tasks within the E-user-management epic
objects.forEach((obj) => {
expect(obj.type).toBe(TrellisObjectType.TASK);
expect(obj.status).toBe(TrellisObjectStatus.OPEN);
expect(obj.priority).toBe(TrellisObjectPriority.MEDIUM);
});
});
it("should work with original parameters (backward compatibility)", async () => {
const objects = await getObjects(
testRoot,
false,
"E-user-management",
TrellisObjectType.TASK,
);
// Should only include open tasks within the E-user-management epic
objects.forEach((obj) => {
expect(obj.type).toBe(TrellisObjectType.TASK);
expect(obj.status).not.toBe(TrellisObjectStatus.DONE);
});
expect(
objects.some((obj) => obj.id === "T-create-registration-form"),
).toBe(true);
expect(objects.some((obj) => obj.id === "T-setup-user-schema")).toBe(
false,
); // This is closed
});
});
describe("error handling", () => {
it("should handle non-existent directory gracefully", async () => {
const objects = await getObjects("/non/existent/path");
expect(objects).toEqual([]);
});
it("should skip invalid markdown files and continue processing", async () => {
// This test relies on the fact that getObjects catches errors from
// getObjectByFilePath and continues processing other files
const objects = await getObjects(testRoot);
// Should still return valid objects even if some files are invalid
expect(objects.length).toBeGreaterThan(0);
// All returned objects should be valid
objects.forEach((obj) => {
expect(obj).toHaveProperty("id");
expect(obj).toHaveProperty("type");
expect(obj).toHaveProperty("title");
});
});
it("should handle empty directory gracefully", async () => {
// Create a path that exists but has no markdown files
const emptyDir = join(__dirname, "schema1_0", ".trellis", "empty");
const objects = await getObjects(emptyDir);
expect(objects).toEqual([]);
});
});
describe("console warning behavior", () => {
it("should log warnings for files that cannot be deserialized", async () => {
// Mock console.warn to capture warnings
const originalWarn = console.warn;
const mockWarn = jest.fn();
console.warn = mockWarn;
try {
// This should process normally but might encounter some files that can't be deserialized
await getObjects(testRoot);
// If there were any deserialization errors, they should have been logged
// Note: In our test data, all files should be valid, so this mainly tests the mechanism
} finally {
console.warn = originalWarn;
}
});
});
describe("multiple value filtering", () => {
describe("multiple type values", () => {
it("should filter objects by multiple types", async () => {
const objects = await getObjects(testRoot, true, undefined, [
TrellisObjectType.FEATURE,
TrellisObjectType.TASK,
]);
expect(objects.length).toBeGreaterThan(0);
objects.forEach((obj) => {
expect([TrellisObjectType.FEATURE, TrellisObjectType.TASK]).toContain(
obj.type,
);
});
// Should include both features and tasks
const features = objects.filter(
(obj) => obj.type === TrellisObjectType.FEATURE,
);
const tasks = objects.filter(
(obj) => obj.type === TrellisObjectType.TASK,
);
expect(features.length).toBeGreaterThan(0);
expect(tasks.length).toBeGreaterThan(0);
// Should not include projects or epics
const projects = objects.filter(
(obj) => obj.type === TrellisObjectType.PROJECT,
);
const epics = objects.filter(
(obj) => obj.type === TrellisObjectType.EPIC,
);
expect(projects.length).toBe(0);
expect(epics.length).toBe(0);
});
it("should filter objects by multiple types (PROJECT and EPIC)", async () => {
const objects = await getObjects(testRoot, true, undefined, [
TrellisObjectType.PROJECT,
TrellisObjectType.EPIC,
]);
expect(objects.length).toBeGreaterThan(0);
objects.forEach((obj) => {
expect([TrellisObjectType.PROJECT, TrellisObjectType.EPIC]).toContain(
obj.type,
);
});
// Should include both projects and epics
const projects = objects.filter(
(obj) => obj.type === TrellisObjectType.PROJECT,
);
const epics = objects.filter(
(obj) => obj.type === TrellisObjectType.EPIC,
);
expect(projects.length).toBeGreaterThan(0);
expect(epics.length).toBeGreaterThan(0);
// Should not include features or tasks
const features = objects.filter(
(obj) => obj.type === TrellisObjectType.FEATURE,
);
const tasks = objects.filter(
(obj) => obj.type === TrellisObjectType.TASK,
);
expect(features.length).toBe(0);
expect(tasks.length).toBe(0);
});
it("should work with single element array (equivalent to single value)", async () => {
const singleArrayObjects = await getObjects(testRoot, true, undefined, [
TrellisObjectType.FEATURE,
]);
const singleValueObjects = await getObjects(
testRoot,
true,
undefined,
TrellisObjectType.FEATURE,
);
expect(singleArrayObjects).toEqual(singleValueObjects);
});
it("should return all types when empty type array is provided", async () => {
const emptyArrayObjects = await getObjects(
testRoot,
true,
undefined,
[],
);
const allObjects = await getObjects(
testRoot,
true,
undefined,
undefined,
);
expect(emptyArrayObjects).toEqual(allObjects);
});
});
describe("multiple status values", () => {
it("should filter objects by multiple statuses", async () => {
const objects = await getObjects(testRoot, true, undefined, undefined, [
TrellisObjectStatus.OPEN,
TrellisObjectStatus.IN_PROGRESS,
]);
expect(objects.length).toBeGreaterThan(0);
objects.forEach((obj) => {
expect([
TrellisObjectStatus.OPEN,
TrellisObjectStatus.IN_PROGRESS,
]).toContain(obj.status);
});
// Should not include done objects
const doneObjects = objects.filter(
(obj) => obj.status === TrellisObjectStatus.DONE,
);
expect(doneObjects.length).toBe(0);
});
it("should filter objects by multiple statuses including DONE", async () => {
const objects = await getObjects(testRoot, true, undefined, undefined, [
TrellisObjectStatus.OPEN,
TrellisObjectStatus.DONE,
]);
expect(objects.length).toBeGreaterThan(0);
objects.forEach((obj) => {
expect([
TrellisObjectStatus.OPEN,
TrellisObjectStatus.DONE,
]).toContain(obj.status);
});
// Should include both open and done objects
const openObjects = objects.filter(
(obj) => obj.status === TrellisObjectStatus.OPEN,
);
const doneObjects = objects.filter(
(obj) => obj.status === TrellisObjectStatus.DONE,
);
expect(openObjects.length).toBeGreaterThan(0);
expect(doneObjects.length).toBeGreaterThan(0);
// Should not include in-progress objects
const inProgressObjects = objects.filter(
(obj) => obj.status === TrellisObjectStatus.IN_PROGRESS,
);
expect(inProgressObjects.length).toBe(0);
});
it("should work with single element status array", async () => {
const singleArrayObjects = await getObjects(
testRoot,
true,
undefined,
undefined,
[TrellisObjectStatus.OPEN],
);
const singleValueObjects = await getObjects(
testRoot,
true,
undefined,
undefined,
TrellisObjectStatus.OPEN,
);
expect(singleArrayObjects).toEqual(singleValueObjects);
});
it("should return all statuses when empty status array is provided", async () => {
const emptyArrayObjects = await getObjects(
testRoot,
true,
undefined,
undefined,
[],
);
const allObjects = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
);
expect(emptyArrayObjects).toEqual(allObjects);
});
});
describe("multiple priority values", () => {
it("should filter objects by multiple priorities", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
[TrellisObjectPriority.HIGH, TrellisObjectPriority.MEDIUM],
);
expect(objects.length).toBeGreaterThan(0);
objects.forEach((obj) => {
expect([
TrellisObjectPriority.HIGH,
TrellisObjectPriority.MEDIUM,
]).toContain(obj.priority);
});
// Should not include low priority objects
const lowPriorityObjects = objects.filter(
(obj) => obj.priority === TrellisObjectPriority.LOW,
);
expect(lowPriorityObjects.length).toBe(0);
});
it("should filter objects by all three priorities", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
[
TrellisObjectPriority.HIGH,
TrellisObjectPriority.MEDIUM,
TrellisObjectPriority.LOW,
],
);
const allObjects = await getObjects(testRoot, true);
// Should return the same as no filter
expect(objects.length).toBe(allObjects.length);
expect(objects.sort((a, b) => a.id.localeCompare(b.id))).toEqual(
allObjects.sort((a, b) => a.id.localeCompare(b.id)),
);
});
it("should work with single element priority array", async () => {
const singleArrayObjects = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
[TrellisObjectPriority.HIGH],
);
const singleValueObjects = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
TrellisObjectPriority.HIGH,
);
expect(singleArrayObjects).toEqual(singleValueObjects);
});
it("should return all priorities when empty priority array is provided", async () => {
const emptyArrayObjects = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
[],
);
const allObjects = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
undefined,
);
expect(emptyArrayObjects).toEqual(allObjects);
});
});
describe("mixed single and multiple filters", () => {
it("should work with single type and multiple statuses", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
TrellisObjectType.TASK,
[TrellisObjectStatus.OPEN, TrellisObjectStatus.DONE],
);
objects.forEach((obj) => {
expect(obj.type).toBe(TrellisObjectType.TASK);
expect([
TrellisObjectStatus.OPEN,
TrellisObjectStatus.DONE,
]).toContain(obj.status);
});
});
it("should work with multiple types and single status", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
[TrellisObjectType.FEATURE, TrellisObjectType.TASK],
TrellisObjectStatus.OPEN,
);
objects.forEach((obj) => {
expect([TrellisObjectType.FEATURE, TrellisObjectType.TASK]).toContain(
obj.type,
);
expect(obj.status).toBe(TrellisObjectStatus.OPEN);
});
});
it("should work with single type and multiple priorities", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
TrellisObjectType.TASK,
undefined,
[TrellisObjectPriority.HIGH, TrellisObjectPriority.MEDIUM],
);
objects.forEach((obj) => {
expect(obj.type).toBe(TrellisObjectType.TASK);
expect([
TrellisObjectPriority.HIGH,
TrellisObjectPriority.MEDIUM,
]).toContain(obj.priority);
});
});
});
describe("combined multiple filters", () => {
it("should work with multiple types, statuses, and priorities", async () => {
const objects = await getObjects(
testRoot,
true,
undefined,
[TrellisObjectType.FEATURE, TrellisObjectType.TASK],
[TrellisObjectStatus.OPEN, TrellisObjectStatus.IN_PROGRESS],
[TrellisObjectPriority.HIGH, TrellisObjectPriority.MEDIUM],
);
objects.forEach((obj) => {
expect([TrellisObjectType.FEATURE, TrellisObjectType.TASK]).toContain(
obj.type,
);
expect([
TrellisObjectStatus.OPEN,
TrellisObjectStatus.IN_PROGRESS,
]).toContain(obj.status);
expect([
TrellisObjectPriority.HIGH,
TrellisObjectPriority.MEDIUM,
]).toContain(obj.priority);
});
});
it("should work with multiple filters and scope", async () => {
const objects = await getObjects(
testRoot,
true,
"P-ecommerce-platform",
[TrellisObjectType.FEATURE, TrellisObjectType.TASK],
[TrellisObjectStatus.OPEN, TrellisObjectStatus.DONE],
);
// All returned objects should match the filters
objects.forEach((obj) => {
expect([TrellisObjectType.FEATURE, TrellisObjectType.TASK]).toContain(
obj.type,
);
expect([
TrellisObjectStatus.OPEN,
TrellisObjectStatus.DONE,
]).toContain(obj.status);
});
// Get all objects from the scope to verify filtering is working correctly
const allScopeObjects = await getObjects(
testRoot,
true,
"P-ecommerce-platform",
);
// Objects should be a subset of all scope objects
expect(objects.length).toBeLessThanOrEqual(allScopeObjects.length);
// All returned objects should be in the scope
objects.forEach((obj) => {
expect(
allScopeObjects.some((scopeObj) => scopeObj.id === obj.id),
).toBe(true);
});
// Should not include objects from other scopes (standalone objects)
const standaloneObjects = await getObjects(
testRoot,
true,
"F-user-authentication", // A standalone feature scope
[TrellisObjectType.FEATURE, TrellisObjectType.TASK],
);
// No overlap between scoped and standalone objects
objects.forEach((obj) => {
expect(
standaloneObjects.some(
(standaloneObj) => standaloneObj.id === obj.id,
),
).toBe(false);
});
});
it("should work with multiple filters and includeClosed=false", async () => {
const objects = await getObjects(
testRoot,
false,
undefined,
[TrellisObjectType.FEATURE, TrellisObjectType.TASK],
[TrellisObjectStatus.OPEN, TrellisObjectStatus.IN_PROGRESS],
);
objects.forEach((obj) => {
expect([TrellisObjectType.FEATURE, TrellisObjectType.TASK]).toContain(
obj.type,
);
expect([
TrellisObjectStatus.OPEN,
TrellisObjectStatus.IN_PROGRESS,
]).toContain(obj.status);
// All should be open (not closed)
expect(obj.status).not.toBe(TrellisObjectStatus.DONE);
expect(obj.status).not.toBe(TrellisObjectStatus.WONT_DO);
});
});
});
describe("backward compatibility", () => {
it("should maintain existing behavior for single values", async () => {
// Test that all existing single-value calls still work exactly the same
const singleTypeObjects = await getObjects(
testRoot,
true,
undefined,
TrellisObjectType.TASK,
);
const singleStatusObjects = await getObjects(
testRoot,
true,
undefined,
undefined,
TrellisObjectStatus.OPEN,
);
const singlePriorityObjects = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
TrellisObjectPriority.HIGH,
);
// Verify single value filtering still works correctly
singleTypeObjects.forEach((obj) => {
expect(obj.type).toBe(TrellisObjectType.TASK);
});
singleStatusObjects.forEach((obj) => {
expect(obj.status).toBe(TrellisObjectStatus.OPEN);
});
singlePriorityObjects.forEach((obj) => {
expect(obj.priority).toBe(TrellisObjectPriority.HIGH);
});
});
it("should return same results for single value vs single-element array", async () => {
// Compare single value with equivalent single-element array for all filter types
const singleType = await getObjects(
testRoot,
true,
undefined,
TrellisObjectType.FEATURE,
);
const arrayType = await getObjects(testRoot, true, undefined, [
TrellisObjectType.FEATURE,
]);
const singleStatus = await getObjects(
testRoot,
true,
undefined,
undefined,
TrellisObjectStatus.OPEN,
);
const arrayStatus = await getObjects(
testRoot,
true,
undefined,
undefined,
[TrellisObjectStatus.OPEN],
);
const singlePriority = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
TrellisObjectPriority.MEDIUM,
);
const arrayPriority = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
[TrellisObjectPriority.MEDIUM],
);
expect(singleType).toEqual(arrayType);
expect(singleStatus).toEqual(arrayStatus);
expect(singlePriority).toEqual(arrayPriority);
});
});
describe("edge cases", () => {
it("should handle empty arrays as no filter applied", async () => {
const emptyArrayObjects = await getObjects(
testRoot,
true,
undefined,
[],
[],
[],
);
const noFilterObjects = await getObjects(
testRoot,
true,
undefined,
undefined,
undefined,
undefined,
);
expect(emptyArrayObjects).toEqual(noFilterObjects);
});
it("should return empty result when multiple filters have no intersection", async () => {
// Try to find tasks that are both DONE and OPEN (impossible)
const objects = await getObjects(
testRoot,
true,
undefined,
TrellisObjectType.TASK,
[TrellisObjectStatus.DONE, TrellisObjectStatus.OPEN],
TrellisObjectPriority.LOW, // Assuming no low priority tasks exist with these statuses
);
// The result might be empty or might have some objects depending on test data
// The important thing is that it doesn't throw an error and returns a valid array
expect(Array.isArray(objects)).toBe(true);
// If there are results, they should match all the filters
objects.forEach((obj) => {
expect(obj.type).toBe(TrellisObjectType.TASK);
expect([
TrellisObjectStatus.DONE,
TrellisObjectStatus.OPEN,
]).toContain(obj.status);
expect(obj.priority).toBe(TrellisObjectPriority.LOW);
});
});
it("should handle large filter arrays efficiently", async () => {
// Test with all possible enum values to ensure performance is acceptable
const allTypes = [
TrellisObjectType.PROJECT,
TrellisObjectType.EPIC,
TrellisObjectType.FEATURE,
TrellisObjectType.TASK,
];
const allStatuses = [
TrellisObjectStatus.DRAFT,
TrellisObjectStatus.OPEN,
TrellisObjectStatus.IN_PROGRESS,
TrellisObjectStatus.DONE,
TrellisObjectStatus.WONT_DO,
];
const allPriorities = [
TrellisObjectPriority.HIGH,
TrellisObjectPriority.MEDIUM,
TrellisObjectPriority.LOW,
];
const startTime = Date.now();
const objects = await getObjects(
testRoot,
true,
undefined,
allTypes,
allStatuses,
allPriorities,
);
const endTime = Date.now();
// Should return all objects (same as no filter)
const allObjects = await getObjects(testRoot, true);
expect(objects.length).toBe(allObjects.length);
// Performance test - should complete in reasonable time (under 1 second)
expect(endTime - startTime).toBeLessThan(1000);
});
});
});
});