Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,80 @@ describe('createFlowConfig', () => {
});
});

describe('V2 data with undefined reviewers', () => {
it('does not crash when reviewers is undefined and names come from iterations[].perReviewer', () => {
const iteration = Object.assign(
{ reviewers: ['orchestrated-reviewer', 'aspect-test-reviewer'] },
{
perReviewer: {
'orchestrated-reviewer': { status: 'failed', criticality: 'medium' },
'aspect-test-reviewer': { status: 'completed', criticality: 'medium' },
},
},
);
const parallelReview: ParallelReviewPhase = {
aggregatedCriticality: 'low',
reviewRoundsUsed: 1,
coderFixCycleRan: false,
selectiveReReview: undefined,
iterations: [iteration],
};

const status = createMockRunStatus({
status: 'completed',
completedAt: '2026-01-01T01:00:00Z',
phases: { ...emptyPhases(), parallelReview },
});

const { nodes, edges } = createFlowConfig(status);
const reviewerNodes = nodes.filter((n) => n.id.startsWith('reviewer-'));

expect(reviewerNodes.length).toBe(2);
expect(reviewerNodes[0]?.data.status).toBe('idle');
expect(reviewerNodes[1]?.data.status).toBe('idle');

// Dispatch + return edges for each reviewer
const dispatchEdges = edges.filter((e) => e.id.startsWith('dispatch-reviewer-'));
const returnEdges = edges.filter((e) => e.id.startsWith('return-reviewer-'));
expect(dispatchEdges.length).toBe(2);
expect(returnEdges.length).toBe(2);
});

it('does not crash when reviewers is undefined and names come from reviewerDetails', () => {
const base: ParallelReviewPhase = {
aggregatedCriticality: 'low',
reviewRoundsUsed: 1,
coderFixCycleRan: false,
selectiveReReview: undefined,
};
const parallelReview = Object.assign(base, {
reviewerDetails: {
'code-reviewer': { status: 'completed', criticality: 'none' },
'test-reviewer': { status: 'completed', criticality: 'low' },
},
});

const status = createMockRunStatus({
status: 'completed',
completedAt: '2026-01-01T01:00:00Z',
phases: { ...emptyPhases(), parallelReview },
});

const { nodes, edges } = createFlowConfig(status);
const reviewerNodes = nodes.filter((n) => n.id.startsWith('reviewer-'));

expect(reviewerNodes.length).toBe(2);
expect(reviewerNodes[0]?.data.status).toBe('idle');
expect(reviewerNodes[1]?.data.status).toBe('idle');

// Dispatch + return edges for each reviewer
const dispatchEdges = edges.filter((e) => e.id.startsWith('dispatch-reviewer-'));
const returnEdges = edges.filter((e) => e.id.startsWith('return-reviewer-'));
expect(dispatchEdges.length).toBe(2);
expect(returnEdges.length).toBe(2);
});
});

describe('reviewer dimming', () => {
it('dims reviewers not in selectiveReReview.reviewersDispatched when ran is true', () => {
const status = createMockRunStatus({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ function buildReviewerNodes(phases: Phases): Node<FlowNodeData>[] {
const reReviewSet = isReReviewActive ? new Set(selectiveReReview.reviewersDispatched) : undefined;

for (const [i, name] of reviewerNames.entries()) {
const reviewerInfo = phases.parallelReview.reviewers[name];
const reviewerInfo = phases.parallelReview.reviewers?.[name];
const isDimmed = reReviewSet !== undefined && !reReviewSet.has(name);
nodes.push({
id: `reviewer-${name}`,
Expand All @@ -283,7 +283,7 @@ function buildReviewerNodes(phases: Phases): Node<FlowNodeData>[] {
}

function resolveReviewerStatus(parallelReview: ParallelReviewPhase, reviewerName: string): FlowNodeData['status'] {
const reviewerInfo = parallelReview.reviewers[reviewerName];
const reviewerInfo = parallelReview.reviewers?.[reviewerName];
if (reviewerInfo === undefined) return 'idle';
if (reviewerInfo.status === 'completed') return 'completed';
if (reviewerInfo.status === 'failed') return 'failed';
Expand Down Expand Up @@ -521,7 +521,7 @@ function buildReviewerEdges(phases: Phases): Array<Edge<DispatchEdgeData> | Edge
const offsetMap = new Map<string, number>();

for (const name of reviewerNames) {
const reviewerInfo = phases.parallelReview.reviewers[name];
const reviewerInfo = phases.parallelReview.reviewers?.[name];
const isCompleted = reviewerInfo?.status === 'completed';
const edgeStatus: DispatchEdgeData['status'] = isCompleted ? 'completed' : 'pending';

Expand Down Expand Up @@ -569,7 +569,7 @@ function buildReviewerEdges(phases: Phases): Array<Edge<DispatchEdgeData> | Edge
const selectiveReReview = phases.parallelReview.selectiveReReview;
if (selectiveReReview !== undefined && selectiveReReview.ran) {
for (const name of selectiveReReview.reviewersDispatched) {
const reviewerInfo = phases.parallelReview.reviewers[name];
const reviewerInfo = phases.parallelReview.reviewers?.[name];
const reReviewCompleted = reviewerInfo?.reReviewCriticality !== undefined;
const reReviewStatus: DispatchEdgeData['status'] = reReviewCompleted ? 'completed' : 'pending';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ const PHASE_STATUS_ACCESSORS: Record<PhaseName, PhaseStatusAccessor> = {
if (phases.parallelReview.status !== undefined) {
return phases.parallelReview.status;
}
const hasRunningReviewer = Object.values(phases.parallelReview.reviewers).some((r) => r.status === undefined);
const hasRunningReviewer = Object.values(phases.parallelReview.reviewers ?? {}).some(
(r) => r.status === undefined,
);
return hasRunningReviewer ? 'in_progress' : 'completed';
}
return phases.review?.status;
Expand Down
10 changes: 5 additions & 5 deletions packages/factory/src/shared/__tests__/event-folder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,12 @@ describe('foldEvents', () => {

const result = foldEvents(header, events);

expect(result.phases.parallelReview?.reviewers['code-reviewer']).toMatchObject({
expect(result.phases.parallelReview?.reviewers?.['code-reviewer']).toMatchObject({
ran: true,
status: undefined,
criticality: undefined,
});
expect(result.phases.parallelReview?.reviewers['test-reviewer']).toMatchObject({
expect(result.phases.parallelReview?.reviewers?.['test-reviewer']).toMatchObject({
ran: true,
});
});
Expand All @@ -132,7 +132,7 @@ describe('foldEvents', () => {

const result = foldEvents(header, events);

expect(result.phases.parallelReview?.reviewers['code-reviewer']).toMatchObject({
expect(result.phases.parallelReview?.reviewers?.['code-reviewer']).toMatchObject({
status: 'completed',
criticality: 'low',
});
Expand Down Expand Up @@ -185,8 +185,8 @@ describe('foldEvents', () => {

const result = foldEvents(header, events);

expect(result.phases.parallelReview?.reviewers['code-reviewer']?.reReviewCriticality).toBe('none');
expect(result.phases.parallelReview?.reviewers['test-reviewer']?.reReviewCriticality).toBe('low');
expect(result.phases.parallelReview?.reviewers?.['code-reviewer']?.reReviewCriticality).toBe('none');
expect(result.phases.parallelReview?.reviewers?.['test-reviewer']?.reReviewCriticality).toBe('low');
});

it('appends artifacts on artifact_written', () => {
Expand Down
18 changes: 9 additions & 9 deletions packages/run-core/src/__tests__/event-folder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,12 @@ describe('foldEvents', () => {

const result = foldEvents(header, events);

expect(result.phases.parallelReview?.reviewers['code-reviewer']).toMatchObject({
expect(result.phases.parallelReview?.reviewers?.['code-reviewer']).toMatchObject({
ran: true,
status: undefined,
criticality: undefined,
});
expect(result.phases.parallelReview?.reviewers['test-reviewer']).toMatchObject({
expect(result.phases.parallelReview?.reviewers?.['test-reviewer']).toMatchObject({
ran: true,
});
});
Expand All @@ -132,7 +132,7 @@ describe('foldEvents', () => {

const result = foldEvents(header, events);

expect(result.phases.parallelReview?.reviewers['code-reviewer']).toMatchObject({
expect(result.phases.parallelReview?.reviewers?.['code-reviewer']).toMatchObject({
status: 'completed',
criticality: 'low',
});
Expand Down Expand Up @@ -185,8 +185,8 @@ describe('foldEvents', () => {

const result = foldEvents(header, events);

expect(result.phases.parallelReview?.reviewers['code-reviewer']?.reReviewCriticality).toBe('none');
expect(result.phases.parallelReview?.reviewers['test-reviewer']?.reReviewCriticality).toBe('low');
expect(result.phases.parallelReview?.reviewers?.['code-reviewer']?.reReviewCriticality).toBe('none');
expect(result.phases.parallelReview?.reviewers?.['test-reviewer']?.reReviewCriticality).toBe('low');
});

it('appends artifacts on artifact_written', () => {
Expand Down Expand Up @@ -644,9 +644,9 @@ describe('foldEvents', () => {
const result = foldEvents(header, events);

// Known reviewer is updated
expect(result.phases.parallelReview?.reviewers['code-reviewer']?.reReviewCriticality).toBe('low');
expect(result.phases.parallelReview?.reviewers?.['code-reviewer']?.reReviewCriticality).toBe('low');
// Unknown reviewer is not added to the reviewers map
expect(result.phases.parallelReview?.reviewers['unknown-reviewer']).toBeUndefined();
expect(result.phases.parallelReview?.reviewers?.['unknown-reviewer']).toBeUndefined();
});

// -- Full event sequence integration test (F1) ----------------------------------------
Expand Down Expand Up @@ -834,13 +834,13 @@ describe('foldEvents', () => {
expect(result.phases.parallelReview?.aggregatedCriticality).toBe('none');
expect(result.phases.parallelReview?.reviewRoundsUsed).toBe(2);
expect(result.phases.parallelReview?.coderFixCycleRan).toBe(true);
expect(result.phases.parallelReview?.reviewers['code-reviewer']).toMatchObject({
expect(result.phases.parallelReview?.reviewers?.['code-reviewer']).toMatchObject({
ran: true,
status: 'completed',
criticality: 'low',
reReviewCriticality: 'none',
});
expect(result.phases.parallelReview?.reviewers['test-reviewer']).toMatchObject({
expect(result.phases.parallelReview?.reviewers?.['test-reviewer']).toMatchObject({
ran: true,
status: 'completed',
criticality: 'medium',
Expand Down
5 changes: 3 additions & 2 deletions packages/run-core/src/event-folder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ function applyReviewerDispatched(
review: ParallelReviewPhase,
event: Extract<ReviewEvent, { event: 'reviewer_dispatched' }>,
): void {
review.reviewers ??= {};
review.reviewers[event.reviewer] = {
ran: true,
status: undefined,
Expand All @@ -186,7 +187,7 @@ function applyReviewerCompleted(
review: ParallelReviewPhase,
event: Extract<ReviewEvent, { event: 'reviewer_completed' }>,
): void {
const entry = review.reviewers[event.reviewer];
const entry = review.reviewers?.[event.reviewer];
if (entry) {
entry.status = event.status;
entry.criticality = event.criticality;
Expand Down Expand Up @@ -216,7 +217,7 @@ function applyReReviewCompleted(
event: Extract<ReviewEvent, { event: 're_review_completed' }>,
): void {
for (const [reviewer, crit] of Object.entries(event.criticalities)) {
const entry = review.reviewers[reviewer];
const entry = review.reviewers?.[reviewer];
if (entry) {
entry.reReviewCriticality = crit;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/run-core/src/types/canonical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export interface ReviewIteration {
export interface ParallelReviewPhase {
aggregatedCriticality: Criticality | undefined;
reviewRoundsUsed: number;
reviewers: Record<string, ReviewerInfo>;
reviewers?: Record<string, ReviewerInfo>;
coderFixCycleRan: boolean;
selectiveReReview: SelectiveReReview | undefined;
status?: PhaseStatus;
Expand Down