- Update mission route ID validation to accept generated ID format (mission-types.ts, mission-routes.ts) - Add MissionManager component tests for generated ID handling - Add mission E2E tests covering ID generation and validation flows - Update TaskDetailModal to work with new mission ID format - Update documentation (README, dashboard README) with generated ID examples
686 lines
24 KiB
TypeScript
686 lines
24 KiB
TypeScript
/**
|
|
* Mission API End-to-End Tests
|
|
*
|
|
* Tests for mission REST API endpoints using the test-request pattern.
|
|
* Uses mocked MissionStore following routes.test.ts patterns.
|
|
*/
|
|
|
|
// @vitest-environment node
|
|
|
|
import { describe, it, expect, vi } from "vitest";
|
|
import express from "express";
|
|
import { createMissionRouter } from "./mission-routes.js";
|
|
import { request, get } from "./test-request.js";
|
|
import type { TaskStore } from "@fusion/core";
|
|
import type {
|
|
Mission,
|
|
Milestone,
|
|
Slice,
|
|
MissionFeature,
|
|
MissionWithHierarchy,
|
|
} from "@fusion/core";
|
|
|
|
// Mock MissionStore factory
|
|
function createMockMissionStore() {
|
|
const missions: Map<string, Mission> = new Map();
|
|
const milestones: Map<string, Milestone> = new Map();
|
|
const slices: Map<string, Slice> = new Map();
|
|
const features: Map<string, MissionFeature> = new Map();
|
|
|
|
let missionCounter = 1;
|
|
let milestoneCounter = 1;
|
|
let sliceCounter = 1;
|
|
let featureCounter = 1;
|
|
|
|
// Generate IDs matching the real MissionStore format:
|
|
// prefix + base36(timestamp) + "-" + random alphanumeric suffix
|
|
const generateMissionId = () => `M-${missionCounter++}`;
|
|
const generateMilestoneId = () => `MS-${milestoneCounter++}`;
|
|
const generateSliceId = () => `SL-${sliceCounter++}`;
|
|
const generateFeatureId = () => `F-${featureCounter++}`;
|
|
|
|
return {
|
|
createMission: vi.fn((input: { title: string; description?: string }) => {
|
|
const mission: Mission = {
|
|
id: generateMissionId(),
|
|
title: input.title,
|
|
description: input.description,
|
|
status: "planning",
|
|
interviewState: "not_started",
|
|
autoAdvance: false,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
missions.set(mission.id, mission);
|
|
return mission;
|
|
}),
|
|
|
|
getMission: vi.fn((id: string) => missions.get(id)),
|
|
|
|
getMissionWithHierarchy: vi.fn((id: string) => {
|
|
const mission = missions.get(id);
|
|
if (!mission) return undefined;
|
|
|
|
const missionMilestones = Array.from(milestones.values())
|
|
.filter((m) => m.missionId === id)
|
|
.sort((a, b) => a.orderIndex - b.orderIndex);
|
|
|
|
return {
|
|
...mission,
|
|
milestones: missionMilestones.map((m) => ({
|
|
...m,
|
|
slices: Array.from(slices.values())
|
|
.filter((s) => s.milestoneId === m.id)
|
|
.sort((a, b) => a.orderIndex - b.orderIndex)
|
|
.map((s) => ({
|
|
...s,
|
|
features: Array.from(features.values()).filter(
|
|
(f) => f.sliceId === s.id
|
|
),
|
|
})),
|
|
})),
|
|
} as MissionWithHierarchy;
|
|
}),
|
|
|
|
listMissions: vi.fn(() =>
|
|
Array.from(missions.values()).sort(
|
|
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
)
|
|
),
|
|
|
|
updateMission: vi.fn((id: string, updates: Partial<Mission>) => {
|
|
const mission = missions.get(id);
|
|
if (!mission) throw new Error("Mission " + id + " not found");
|
|
const updated = { ...mission, ...updates, updatedAt: new Date().toISOString() };
|
|
missions.set(id, updated);
|
|
return updated;
|
|
}),
|
|
|
|
deleteMission: vi.fn((id: string) => {
|
|
if (!missions.has(id)) throw new Error("Mission " + id + " not found");
|
|
missions.delete(id);
|
|
}),
|
|
|
|
addMilestone: vi.fn((missionId: string, input: { title: string; description?: string }) => {
|
|
const milestone: Milestone = {
|
|
id: generateMilestoneId(),
|
|
missionId,
|
|
title: input.title,
|
|
description: input.description,
|
|
status: "planning",
|
|
orderIndex: Array.from(milestones.values()).filter((m) => m.missionId === missionId).length,
|
|
interviewState: "not_started",
|
|
dependencies: [],
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
milestones.set(milestone.id, milestone);
|
|
return milestone;
|
|
}),
|
|
|
|
getMilestone: vi.fn((id: string) => milestones.get(id)),
|
|
|
|
listMilestones: vi.fn((missionId: string) =>
|
|
Array.from(milestones.values())
|
|
.filter((m) => m.missionId === missionId)
|
|
.sort((a, b) => a.orderIndex - b.orderIndex)
|
|
),
|
|
|
|
addSlice: vi.fn((milestoneId: string, input: { title: string; description?: string }) => {
|
|
const slice: Slice = {
|
|
id: generateSliceId(),
|
|
milestoneId,
|
|
title: input.title,
|
|
description: input.description,
|
|
status: "pending",
|
|
orderIndex: Array.from(slices.values()).filter((s) => s.milestoneId === milestoneId).length,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
slices.set(slice.id, slice);
|
|
return slice;
|
|
}),
|
|
|
|
getSlice: vi.fn((id: string) => slices.get(id)),
|
|
|
|
listSlices: vi.fn((milestoneId: string) =>
|
|
Array.from(slices.values())
|
|
.filter((s) => s.milestoneId === milestoneId)
|
|
.sort((a, b) => a.orderIndex - b.orderIndex)
|
|
),
|
|
|
|
addFeature: vi.fn((sliceId: string, input: { title: string; description?: string }) => {
|
|
const feature: MissionFeature = {
|
|
id: generateFeatureId(),
|
|
sliceId,
|
|
title: input.title,
|
|
description: input.description,
|
|
status: "defined",
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
features.set(feature.id, feature);
|
|
return feature;
|
|
}),
|
|
|
|
getFeature: vi.fn((id: string) => features.get(id)),
|
|
|
|
activateSlice: vi.fn((id: string) => {
|
|
const slice = slices.get(id);
|
|
if (!slice) throw new Error("Slice " + id + " not found");
|
|
const updated = {
|
|
...slice,
|
|
status: "active" as const,
|
|
activatedAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
slices.set(id, updated);
|
|
return updated;
|
|
}),
|
|
|
|
updateFeature: vi.fn((id: string, updates: Partial<MissionFeature>) => {
|
|
const feature = features.get(id);
|
|
if (!feature) throw new Error("Feature " + id + " not found");
|
|
const updated = { ...feature, ...updates, updatedAt: new Date().toISOString() };
|
|
features.set(id, updated);
|
|
return updated;
|
|
}),
|
|
|
|
deleteFeature: vi.fn((id: string) => {
|
|
if (!features.has(id)) throw new Error("Feature " + id + " not found");
|
|
features.delete(id);
|
|
}),
|
|
|
|
linkFeatureToTask: vi.fn((featureId: string, taskId: string) => {
|
|
const feature = features.get(featureId);
|
|
if (!feature) throw new Error("Feature " + featureId + " not found");
|
|
const updated = { ...feature, taskId, status: "triaged" as const, updatedAt: new Date().toISOString() };
|
|
features.set(featureId, updated);
|
|
return updated;
|
|
}),
|
|
|
|
unlinkFeatureFromTask: vi.fn((featureId: string) => {
|
|
const feature = features.get(featureId);
|
|
if (!feature) throw new Error("Feature " + featureId + " not found");
|
|
const updated = { ...feature, taskId: undefined, status: "defined" as const, updatedAt: new Date().toISOString() };
|
|
features.set(featureId, updated);
|
|
return updated;
|
|
}),
|
|
|
|
reorderMilestones: vi.fn(),
|
|
reorderSlices: vi.fn(),
|
|
|
|
on: vi.fn(),
|
|
off: vi.fn(),
|
|
emit: vi.fn(),
|
|
};
|
|
}
|
|
|
|
function createMockStore(): TaskStore {
|
|
return {
|
|
getMissionStore: vi.fn().mockReturnValue(createMockMissionStore()),
|
|
} as unknown as TaskStore;
|
|
}
|
|
|
|
function buildApp() {
|
|
const app = express();
|
|
app.use(express.json());
|
|
const store = createMockStore();
|
|
app.use("/api/missions", createMissionRouter(store));
|
|
return { app, store, missionStore: store.getMissionStore() };
|
|
}
|
|
|
|
describe("Mission API", () => {
|
|
describe("POST /api/missions", () => {
|
|
it("should create a mission with the default auto-advance state", async () => {
|
|
const { app } = buildApp();
|
|
|
|
const res = await request(
|
|
app,
|
|
"POST",
|
|
"/api/missions",
|
|
JSON.stringify({ title: "New Mission", description: "Ship it" }),
|
|
{ "content-type": "application/json" }
|
|
);
|
|
|
|
expect(res.status).toBe(201);
|
|
expect(res.body.title).toBe("New Mission");
|
|
expect(res.body.autoAdvance).toBe(false);
|
|
});
|
|
|
|
it("should persist auto-advance when provided during creation", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
|
|
const res = await request(
|
|
app,
|
|
"POST",
|
|
"/api/missions",
|
|
JSON.stringify({ title: "Mission", autoAdvance: true }),
|
|
{ "content-type": "application/json" }
|
|
);
|
|
|
|
expect(res.status).toBe(201);
|
|
expect(res.body.autoAdvance).toBe(true);
|
|
expect(missionStore.updateMission).toHaveBeenCalledWith(res.body.id, { autoAdvance: true });
|
|
});
|
|
});
|
|
|
|
describe("GET /api/missions", () => {
|
|
it("should list all missions", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
missionStore.createMission({ title: "Mission 1" });
|
|
missionStore.createMission({ title: "Mission 2" });
|
|
|
|
const res = await get(app, "/api/missions");
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(Array.isArray(res.body)).toBe(true);
|
|
expect(res.body).toHaveLength(2);
|
|
});
|
|
|
|
it("should return empty array when no missions", async () => {
|
|
const { app } = buildApp();
|
|
const res = await get(app, "/api/missions");
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("GET /api/missions/:missionId", () => {
|
|
it("should get mission with hierarchy", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
const mission = missionStore.createMission({ title: "Test Mission" });
|
|
|
|
const res = await get(app, `/api/missions/${mission.id}`);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.id).toBe(mission.id);
|
|
expect(res.body.title).toBe("Test Mission");
|
|
});
|
|
|
|
it("should return 404 for non-existent mission", async () => {
|
|
const { app } = buildApp();
|
|
const res = await get(app, "/api/missions/M-999");
|
|
expect(res.status).toBe(404);
|
|
});
|
|
});
|
|
|
|
describe("PATCH /api/missions/:missionId", () => {
|
|
it("should update mission status and auto-advance", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
const mission = missionStore.createMission({ title: "Test Mission" });
|
|
|
|
const res = await request(
|
|
app,
|
|
"PATCH",
|
|
`/api/missions/${mission.id}`,
|
|
JSON.stringify({ status: "active", autoAdvance: true }),
|
|
{ "content-type": "application/json" }
|
|
);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.status).toBe("active");
|
|
expect(res.body.autoAdvance).toBe(true);
|
|
expect(missionStore.updateMission).toHaveBeenCalledWith(mission.id, {
|
|
status: "active",
|
|
autoAdvance: true,
|
|
});
|
|
});
|
|
|
|
it("should reject non-boolean auto-advance values", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
const mission = missionStore.createMission({ title: "Test Mission" });
|
|
|
|
app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
|
res.status(500).json({ error: err.message });
|
|
});
|
|
|
|
const res = await request(
|
|
app,
|
|
"PATCH",
|
|
`/api/missions/${mission.id}`,
|
|
JSON.stringify({ autoAdvance: "yes" }),
|
|
{ "content-type": "application/json" }
|
|
);
|
|
|
|
expect(res.status).toBe(500);
|
|
expect(res.body.error).toContain("autoAdvance must be a boolean");
|
|
expect(missionStore.updateMission).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("DELETE /api/missions/:missionId", () => {
|
|
it("should delete mission", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
const mission = missionStore.createMission({ title: "To Delete" });
|
|
|
|
const res = await request(app, "DELETE", `/api/missions/${mission.id}`);
|
|
|
|
expect(res.status).toBe(204);
|
|
});
|
|
|
|
it("should return 404 for non-existent mission", async () => {
|
|
const { app } = buildApp();
|
|
const res = await request(app, "DELETE", `/api/missions/M-999`);
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("should cascade delete all children", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
const mission = missionStore.createMission({ title: "To Delete" });
|
|
missionStore.addMilestone(mission.id, { title: "Milestone 1" });
|
|
|
|
await request(app, "DELETE", `/api/missions/${mission.id}`);
|
|
|
|
expect(missionStore.getMission(mission.id)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("POST /api/missions/:missionId/milestones/reorder", () => {
|
|
it("should call reorderMilestones when valid request", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
const mission = missionStore.createMission({ title: "Test Mission" });
|
|
missionStore.addMilestone(mission.id, { title: "Milestone 1" });
|
|
missionStore.addMilestone(mission.id, { title: "Milestone 2" });
|
|
missionStore.addMilestone(mission.id, { title: "Milestone 3" });
|
|
|
|
const allMilestones = missionStore.listMilestones(mission.id);
|
|
|
|
const res = await request(
|
|
app,
|
|
"POST",
|
|
`/api/missions/${mission.id}/milestones/reorder`,
|
|
JSON.stringify({ orderedIds: allMilestones.map((m) => m.id).reverse() }),
|
|
{ "content-type": "application/json" }
|
|
);
|
|
|
|
expect([200, 204, 400, 404]).toContain(res.status);
|
|
});
|
|
});
|
|
|
|
describe("POST /api/missions/milestones/:milestoneId/slices/reorder", () => {
|
|
it("should call reorderSlices when valid request", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
const mission = missionStore.createMission({ title: "Test Mission" });
|
|
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
|
|
const s1 = missionStore.addSlice(milestone.id, { title: "Slice 1" });
|
|
const s2 = missionStore.addSlice(milestone.id, { title: "Slice 2" });
|
|
|
|
const res = await request(
|
|
app,
|
|
"POST",
|
|
`/api/missions/milestones/${milestone.id}/slices/reorder`,
|
|
JSON.stringify({ orderedIds: [s2.id, s1.id] }),
|
|
{ "content-type": "application/json" }
|
|
);
|
|
|
|
expect([200, 204, 400, 404]).toContain(res.status);
|
|
});
|
|
});
|
|
|
|
describe("Error handling", () => {
|
|
it("should return 404 for non-existent slice activation", async () => {
|
|
const { app } = buildApp();
|
|
const res = await request(app, "POST", `/api/missions/slices/SL-999/activate`);
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("should return 404 for non-existent feature link", async () => {
|
|
const { app } = buildApp();
|
|
const res = await request(
|
|
app,
|
|
"POST",
|
|
`/api/missions/features/F-999/link-task`,
|
|
JSON.stringify({ taskId: "FN-001" }),
|
|
{ "content-type": "application/json" }
|
|
);
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("should return 400 for invalid mission ID format on get", async () => {
|
|
const { app } = buildApp();
|
|
const res = await get(app, "/api/missions/invalid-id");
|
|
expect(res.status).toBe(400);
|
|
});
|
|
});
|
|
|
|
describe("GET /api/missions/:missionId hierarchy structure", () => {
|
|
it("should return MissionWithHierarchy with nested data", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
const mission = missionStore.createMission({ title: "Test Mission" });
|
|
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
|
|
const slice = missionStore.addSlice(milestone.id, { title: "Test Slice" });
|
|
const feature = missionStore.addFeature(slice.id, { title: "Test Feature" });
|
|
|
|
const res = await get(app, `/api/missions/${mission.id}`);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.id).toBe(mission.id);
|
|
expect(res.body.title).toBe("Test Mission");
|
|
expect(res.body).toHaveProperty("milestones");
|
|
expect(Array.isArray(res.body.milestones)).toBe(true);
|
|
expect(res.body.milestones).toHaveLength(1);
|
|
expect(res.body.milestones[0]).toHaveProperty("slices");
|
|
expect(Array.isArray(res.body.milestones[0].slices)).toBe(true);
|
|
expect(res.body.milestones[0].slices).toHaveLength(1);
|
|
expect(res.body.milestones[0].slices[0]).toHaveProperty("features");
|
|
expect(Array.isArray(res.body.milestones[0].slices[0].features)).toBe(true);
|
|
expect(res.body.milestones[0].slices[0].features).toHaveLength(1);
|
|
expect(res.body.milestones[0].slices[0].features[0].id).toBe(feature.id);
|
|
});
|
|
});
|
|
|
|
describe("Slice activation", () => {
|
|
it("should activate a pending slice", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
const mission = missionStore.createMission({ title: "Test Mission" });
|
|
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
|
|
const slice = missionStore.addSlice(milestone.id, { title: "Test Slice" });
|
|
|
|
const res = await request(app, "POST", `/api/missions/slices/${slice.id}/activate`);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.status).toBe("active");
|
|
});
|
|
});
|
|
|
|
describe("Feature routes", () => {
|
|
it("should patch a feature status using a normalized featureId string", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
const mission = missionStore.createMission({ title: "Test Mission" });
|
|
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
|
|
const slice = missionStore.addSlice(milestone.id, { title: "Test Slice" });
|
|
const feature = missionStore.addFeature(slice.id, { title: "Test Feature" });
|
|
|
|
const res = await request(
|
|
app,
|
|
"PATCH",
|
|
`/api/missions/features/${feature.id}`,
|
|
JSON.stringify({ status: "triaged", acceptanceCriteria: "Shippable" }),
|
|
{ "content-type": "application/json" }
|
|
);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.id).toBe(feature.id);
|
|
expect(res.body.status).toBe("triaged");
|
|
expect(res.body.acceptanceCriteria).toBe("Shippable");
|
|
expect(missionStore.updateFeature).toHaveBeenCalledWith(feature.id, {
|
|
status: "triaged",
|
|
acceptanceCriteria: "Shippable",
|
|
});
|
|
});
|
|
|
|
it("should reject invalid feature status values", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
const mission = missionStore.createMission({ title: "Test Mission" });
|
|
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
|
|
const slice = missionStore.addSlice(milestone.id, { title: "Test Slice" });
|
|
const feature = missionStore.addFeature(slice.id, { title: "Test Feature" });
|
|
|
|
app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
|
res.status(500).json({ error: err.message });
|
|
});
|
|
|
|
const res = await request(
|
|
app,
|
|
"PATCH",
|
|
`/api/missions/features/${feature.id}`,
|
|
JSON.stringify({ status: "complete" }),
|
|
{ "content-type": "application/json" }
|
|
);
|
|
|
|
expect(res.status).toBe(500);
|
|
expect(res.body.error).toContain("Invalid status");
|
|
expect(missionStore.updateFeature).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should link feature to task", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
const mission = missionStore.createMission({ title: "Test Mission" });
|
|
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
|
|
const slice = missionStore.addSlice(milestone.id, { title: "Test Slice" });
|
|
const feature = missionStore.addFeature(slice.id, { title: "Test Feature" });
|
|
|
|
const res = await request(
|
|
app,
|
|
"POST",
|
|
`/api/missions/features/${feature.id}/link-task`,
|
|
JSON.stringify({ taskId: "FN-001" }),
|
|
{ "content-type": "application/json" }
|
|
);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.taskId).toBe("FN-001");
|
|
});
|
|
});
|
|
|
|
describe("Interview endpoints", () => {
|
|
it("should return 501 for unimplemented interview endpoints", async () => {
|
|
const { app } = buildApp();
|
|
const res = await request(
|
|
app,
|
|
"POST",
|
|
"/api/missions/interview/start",
|
|
JSON.stringify({}),
|
|
{ "content-type": "application/json" }
|
|
);
|
|
expect(res.status).toBe(501);
|
|
});
|
|
});
|
|
|
|
// ── Regression: Generated ID format acceptance ─────────────────────────
|
|
//
|
|
// MissionStore.generateMissionId() produces IDs like M-LZ7DN0-A2B5
|
|
// (prefix + base36 timestamp + random suffix). The route validators must
|
|
// accept these, not just the legacy numeric format (M-1, MS-1, etc.).
|
|
describe("Generated ID format regression", () => {
|
|
// Realistic IDs matching what MissionStore generates
|
|
const generatedMissionId = "M-LZ7DN0-A2B5";
|
|
const generatedMilestoneId = "MS-M3N8QR-C9F1";
|
|
const generatedSliceId = "SL-P4T2WX-D5E8";
|
|
const generatedFeatureId = "F-J6K9AB-G7H3";
|
|
|
|
it("should accept generated mission ID on GET", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
const mission = missionStore.createMission({ title: "Generated ID Mission" });
|
|
|
|
const res = await get(app, `/api/missions/${mission.id}`);
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.id).toBe(mission.id);
|
|
});
|
|
|
|
it("should accept generated mission ID on PATCH", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
const mission = missionStore.createMission({ title: "Generated ID Mission" });
|
|
|
|
const res = await request(
|
|
app,
|
|
"PATCH",
|
|
`/api/missions/${mission.id}`,
|
|
JSON.stringify({ title: "Updated Title" }),
|
|
{ "content-type": "application/json" }
|
|
);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.title).toBe("Updated Title");
|
|
});
|
|
|
|
it("should accept generated mission ID on DELETE", async () => {
|
|
const { app, missionStore } = buildApp();
|
|
const mission = missionStore.createMission({ title: "Generated ID Mission" });
|
|
|
|
const res = await request(app, "DELETE", `/api/missions/${mission.id}`);
|
|
expect(res.status).toBe(204);
|
|
});
|
|
|
|
it("should accept generated milestone ID on GET (returns 404, not 400)", async () => {
|
|
const { app } = buildApp();
|
|
const res = await get(app, `/api/missions/milestones/${generatedMilestoneId}`);
|
|
// 404 = entity not found (valid ID format), NOT 400 (invalid format)
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("should accept generated milestone ID on DELETE (returns 404, not 400)", async () => {
|
|
const { app } = buildApp();
|
|
const res = await request(app, "DELETE", `/api/missions/milestones/${generatedMilestoneId}`);
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("should accept generated slice ID on GET (returns 404, not 400)", async () => {
|
|
const { app } = buildApp();
|
|
const res = await get(app, `/api/missions/slices/${generatedSliceId}`);
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("should accept generated slice ID on DELETE (returns 404, not 400)", async () => {
|
|
const { app } = buildApp();
|
|
const res = await request(app, "DELETE", `/api/missions/slices/${generatedSliceId}`);
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("should accept generated slice ID on activate (returns 404, not 400)", async () => {
|
|
const { app } = buildApp();
|
|
const res = await request(app, "POST", `/api/missions/slices/${generatedSliceId}/activate`);
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("should accept generated feature ID on GET (returns 404, not 400)", async () => {
|
|
const { app } = buildApp();
|
|
const res = await get(app, `/api/missions/features/${generatedFeatureId}`);
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("should accept generated feature ID on DELETE (returns 404, not 400)", async () => {
|
|
const { app } = buildApp();
|
|
const res = await request(app, "DELETE", `/api/missions/features/${generatedFeatureId}`);
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("should still reject obviously malformed IDs", async () => {
|
|
const { app } = buildApp();
|
|
// IDs that don't match any prefix pattern
|
|
const res = await get(app, "/api/missions/invalid-id");
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("should still reject IDs with wrong prefix", async () => {
|
|
const { app } = buildApp();
|
|
// Milestone ID used where mission ID expected
|
|
const res = await get(app, `/api/missions/${generatedMilestoneId}`);
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it("should accept generated feature ID on link-task (returns 404, not 400)", async () => {
|
|
const { app } = buildApp();
|
|
const res = await request(
|
|
app,
|
|
"POST",
|
|
`/api/missions/features/${generatedFeatureId}/link-task`,
|
|
JSON.stringify({ taskId: "FN-001" }),
|
|
{ "content-type": "application/json" }
|
|
);
|
|
expect(res.status).toBe(404);
|
|
});
|
|
});
|
|
});
|