Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docker/agent/daemon/repos.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ vi.mock("node:fs", async (orig) => ({
writeFileSync: vi.fn(),
mkdirSync: vi.fn(),
rmSync: vi.fn(),
// The orphan sweep in reconcileRepos reads the workspace root via readdirSync
// (+ statSync for mtimes). Without mocking these, the sweep enumerates the REAL
// filesystem — so any sibling dir in /workspace (e.g. other checked-out repos in
// a CI/dev container) is treated as an orphan and rm-rf'd, firing spurious rmSync
// calls that break the reconcileRepos rmSync assertions below. Default to an empty
// workspace so these integration tests only exercise the adopt/clone decisions;
// the sweep itself is covered separately by the pure sweepOrphanWorkspaceDirs tests.
readdirSync: vi.fn(() => [] as unknown as ReturnType<typeof import("node:fs").readdirSync>),
statSync: vi.fn(() => ({ mtimeMs: 0 }) as unknown as ReturnType<typeof import("node:fs").statSync>),
}));

// Isolate reconcileRepos from the (heavy, fs/exec-driven) E2 setup path — the
Expand Down
2 changes: 1 addition & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ enum SyncDirection {
enum GoalPriorityEntityType {
task
deliverable
checkpoint
feedback
phase
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ function CheckpointCard({
onDelete={onDelete ? () => onDelete(item) : undefined}
/>
<span onClick={(e) => e.stopPropagation()}>
<GoalPriorityBadge projectId={projectId} entityType="checkpoint" entityId={item.id} />
<GoalPriorityBadge projectId={projectId} entityType="feedback" entityId={item.id} />
</span>
<button
type="button"
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/v1/projects/[projectId]/feedbacks/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ export async function PATCH(req: NextRequest, { params }: Params) {
await enqueue(
"documents",
SCORE_GOAL_PRIORITY_JOB,
{ projectId, entityType: "checkpoint", entityId: id },
{ idempotencyKey: `goal-score:checkpoint:${id}:update-${window}`, delay: 10_000 }
{ projectId, entityType: "feedback", entityId: id },
{ idempotencyKey: `goal-score:feedback:${id}:update-${window}`, delay: 10_000 }
);
}

Expand Down
4 changes: 2 additions & 2 deletions src/app/api/v1/projects/[projectId]/feedbacks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ export async function POST(
await enqueue(
"documents",
SCORE_GOAL_PRIORITY_JOB,
{ projectId, entityType: "checkpoint", entityId: feedback.id },
{ idempotencyKey: `goal-score:checkpoint:${feedback.id}:created` }
{ projectId, entityType: "feedback", entityId: feedback.id },
{ idempotencyKey: `goal-score:feedback:${feedback.id}:created` }
);

return auditedResponse(feedback, { status: 201 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { enqueue } from "@/lib/queue/enqueue";
import { SCORE_GOAL_PRIORITY_JOB } from "@/worker/jobs/score-goal-priority";
import type { GoalPriorityEntityType } from "@prisma/client";

const VALID_ENTITY_TYPES: GoalPriorityEntityType[] = ["task", "deliverable", "checkpoint", "phase"];
const VALID_ENTITY_TYPES: GoalPriorityEntityType[] = ["task", "deliverable", "feedback", "phase"];

const overrideSchema = z.object({
score: z.number().min(0).max(100),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { db } from "@/lib/db";
import { authorizeProject } from "@/lib/auth/with-project-auth";
import type { GoalPriorityEntityType } from "@prisma/client";

const VALID_ENTITY_TYPES: GoalPriorityEntityType[] = ["task", "deliverable", "checkpoint", "phase"];
const VALID_ENTITY_TYPES: GoalPriorityEntityType[] = ["task", "deliverable", "feedback", "phase"];

export async function GET(
_req: NextRequest,
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/v1/projects/[projectId]/goal-scores/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { db } from "@/lib/db";
import { authorizeProject } from "@/lib/auth/with-project-auth";
import type { GoalPriorityEntityType } from "@prisma/client";

const VALID_ENTITY_TYPES: GoalPriorityEntityType[] = ["task", "deliverable", "checkpoint", "phase"];
const VALID_ENTITY_TYPES: GoalPriorityEntityType[] = ["task", "deliverable", "feedback", "phase"];

export async function GET(
req: NextRequest,
Expand Down
2 changes: 1 addition & 1 deletion src/components/goals/goal-priority-badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function scoreColor(score: number): string {

interface GoalPriorityBadgeProps {
projectId: string;
entityType: "task" | "deliverable" | "checkpoint" | "phase";
entityType: "task" | "deliverable" | "feedback" | "phase";
entityId: string;
className?: string;
}
Expand Down
8 changes: 7 additions & 1 deletion src/components/phases/item-detail-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,13 @@ function ItemDetailPanelInner({ projectId, orgId, item, currentUserId, onClose,
<span onClick={(e) => e.stopPropagation()}>
<GoalPriorityBadge
projectId={projectId}
entityType={item.type as "task" | "deliverable" | "checkpoint" | "phase"}
entityType={
(item.type === "checkpoint" ? "feedback" : item.type) as
| "task"
| "deliverable"
| "feedback"
| "phase"
}
entityId={item.id}
/>
</span>
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/use-goal-score.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,11 @@ describe("useGoalScore", () => {
expect(result.current.score?.effectiveScore).toBe(70);
});

it("works for 'checkpoint' entityType", async () => {
it("works for 'feedback' entityType", async () => {
mockFetchScore({ data: MOCK_SCORE });
renderHook(() => useGoalScore("proj-2", "checkpoint", "chk-1"));
renderHook(() => useGoalScore("proj-2", "feedback", "chk-1"));
await waitFor(() => expect(global.fetch).toHaveBeenCalledWith(
"/api/v1/projects/proj-2/goal-scores/checkpoint/chk-1"
"/api/v1/projects/proj-2/goal-scores/feedback/chk-1"
));
});
});
2 changes: 1 addition & 1 deletion src/hooks/use-goal-score.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export type GoalScore = {

export function useGoalScore(
projectId: string,
entityType: "task" | "deliverable" | "checkpoint" | "phase",
entityType: "task" | "deliverable" | "feedback" | "phase",
entityId: string
) {
const [score, setScore] = useState<GoalScore | null>(null);
Expand Down
4 changes: 2 additions & 2 deletions src/lib/goals/trigger-rescore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { enqueue } from "@/lib/queue/enqueue";
import { SCORE_GOAL_PRIORITY_JOB } from "@/worker/jobs/score-goal-priority";
import type { GoalPriorityEntityType } from "@prisma/client";

const ENTITY_TYPES: GoalPriorityEntityType[] = ["task", "deliverable", "checkpoint", "phase"];
const ENTITY_TYPES: GoalPriorityEntityType[] = ["task", "deliverable", "feedback", "phase"];
const RESCORE_LIMIT = 50;

export async function triggerProjectRescore(projectId: string): Promise<void> {
Expand All @@ -18,7 +18,7 @@ export async function triggerProjectRescore(projectId: string): Promise<void> {
entities = await db.task.findMany({ where: { projectId }, select: { id: true } });
} else if (entityType === "deliverable") {
entities = await db.deliverable.findMany({ where: { projectId }, select: { id: true } });
} else if (entityType === "checkpoint") {
} else if (entityType === "feedback") {
const phases = await db.phase.findMany({ where: { projectId }, select: { id: true } });
const phaseIds = phases.map((p) => p.id);
entities = await db.checkpoint.findMany({
Expand Down
12 changes: 9 additions & 3 deletions src/worker/jobs/score-goal-priority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ const MODEL = process.env.ANTHROPIC_GOAL_SCORE_MODEL ?? "claude-sonnet-4-6";
export async function processScoreGoalPriority(
job: Job
): Promise<{ entityId: string; score: number }> {
const { projectId, entityType, entityId } = job.data as ScoreGoalPriorityPayload;
const { projectId, entityId } = job.data as ScoreGoalPriorityPayload;
// Legacy tolerance: the GoalPriorityEntityType enum member 'checkpoint' was
// renamed to 'feedback' to match the deployed DB enum. Residual BullMQ jobs
// queued before the rename still carry entityType='checkpoint'; normalize
// them here so any Prisma call uses the valid enum value and drains cleanly.
const rawEntityType = (job.data as ScoreGoalPriorityPayload).entityType as string;
const entityType = (rawEntityType === "checkpoint" ? "feedback" : rawEntityType) as GoalPriorityEntityType;

log.info({ event: "score_goal_priority_start", projectId, entityType, entityId });

Expand Down Expand Up @@ -154,7 +160,7 @@ async function fetchEntity(
});
return d ? { title: d.name, description: d.description, dueDate: d.dueDate, phase: d.phase } : null;
}
case "checkpoint": {
case "feedback": {
const c = await db.checkpoint.findUnique({
where: { id },
select: { name: true, description: true, dueDate: true, phase: { select: { name: true, description: true } } },
Expand Down Expand Up @@ -204,7 +210,7 @@ async function resolveEntityTitle(type: GoalPriorityEntityType, id: string): Pro
const d = await db.deliverable.findUnique({ where: { id }, select: { name: true } });
return d?.name ?? null;
}
case "checkpoint": {
case "feedback": {
const c = await db.checkpoint.findUnique({ where: { id }, select: { name: true } });
return c?.name ?? null;
}
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/api/feedbacks-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ describe("POST /api/v1/projects/[projectId]/feedbacks", () => {
expect(mockEnqueue).toHaveBeenCalledWith(
"documents",
"score-goal-priority",
expect.objectContaining({ entityType: "checkpoint" }),
expect.objectContaining({ entityType: "feedback" }),
expect.any(Object)
);
});
Expand Down
35 changes: 33 additions & 2 deletions tests/unit/worker/jobs/score-goal-priority.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ describe("processScoreGoalPriority", () => {
expect(mocks.deliverableFindUnique).toHaveBeenCalled();
});

it("fetches checkpoint entity", async () => {
it("fetches feedback entity", async () => {
mocks.fetchGoalContext.mockResolvedValue([{ id: "g1", title: "G", description: null, successCriteria: null, keyResults: [], targetDate: null, weight: 1, isActive: true, order: 0 }]);
mocks.checkpointFindUnique.mockResolvedValue({
name: "Checkpoint B",
Expand All @@ -243,8 +243,39 @@ describe("processScoreGoalPriority", () => {
mocks.callStructured.mockResolvedValue({ data: { score: 40, rationale: "OK", factors: {} }, usage: {} });
mocks.projectFindUnique.mockResolvedValue({ organizationId: "org-1" });

const result = await processScoreGoalPriority(makeJob({ ...PAYLOAD, entityType: "checkpoint", entityId: "chk-1" }));
const result = await processScoreGoalPriority(makeJob({ ...PAYLOAD, entityType: "feedback", entityId: "chk-1" }));
expect(result.entityId).toBe("chk-1");
expect(mocks.checkpointFindUnique).toHaveBeenCalled();
});

it("normalizes a legacy 'checkpoint' payload to 'feedback' and succeeds", async () => {
// Residual BullMQ jobs queued before the enum rename still carry
// entityType='checkpoint'; the job must normalize to 'feedback' so it
// drains without re-raising the Postgres enum error.
mocks.fetchGoalContext.mockResolvedValue([{ id: "g1", title: "G", description: null, successCriteria: null, keyResults: [], targetDate: null, weight: 1, isActive: true, order: 0 }]);
mocks.checkpointFindUnique.mockResolvedValue({
name: "Legacy Checkpoint",
description: null,
dueDate: null,
phase: null,
});
mocks.goalPriorityScoreFindUnique.mockResolvedValue(null);
mocks.goalPriorityScoreFindMany.mockResolvedValue([]);
mocks.callStructured.mockResolvedValue({ data: { score: 42, rationale: "OK", factors: {} }, usage: {} });
mocks.projectFindUnique.mockResolvedValue({ organizationId: "org-1" });

const result = await processScoreGoalPriority(
makeJob({ ...PAYLOAD, entityType: "checkpoint", entityId: "chk-legacy" })
);
expect(result.entityId).toBe("chk-legacy");
expect(mocks.checkpointFindUnique).toHaveBeenCalled();
// Persisted with the normalized enum value, never the raw 'checkpoint'.
expect(mocks.goalPriorityScoreUpsert).toHaveBeenCalledWith(
expect.objectContaining({
where: { entityType_entityId: { entityType: "feedback", entityId: "chk-legacy" } },
create: expect.objectContaining({ entityType: "feedback" }),
})
);
});

it("fetches phase entity", async () => {
Expand Down
Loading