feat(FN-676): fix mission route typing and add regression coverage

- Tighten mission route status validation with generic typing to preserve exact status unions
- Expand mission API end-to-end coverage for feature patching, invalid statuses, and task unlink support in the mock store
- Point dashboard Vitest core alias at the package index so mission route tests resolve the full mission store API
- Add a changeset for the sticky New Task modal action button position fix
This commit is contained in:
gsxdsm
2026-04-01 12:44:48 -07:00
parent 91b3bbf564
commit 3279da3db8
3 changed files with 79 additions and 13 deletions

View File

@@ -175,6 +175,19 @@ function createMockMissionStore() {
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");
@@ -183,6 +196,14 @@ function createMockMissionStore() {
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(),
@@ -391,7 +412,56 @@ describe("Mission API", () => {
});
});
describe("Feature linking", () => {
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" });