fix(engine): address review checkout routing feedback
This commit is contained in:
@@ -1,11 +1,6 @@
|
||||
/*
|
||||
Explicit external review checkout metadata contract tests.
|
||||
|
||||
Resolves review checkout cwd from task metadata with fail-closed defaults:
|
||||
- absent/blank/relative/non-git metadata → fallback (task worktree)
|
||||
- valid explicit absolute git checkout → resolved realpath
|
||||
- sourceMetadata.externalReviewCheckout is the canonical external field
|
||||
- invalid higher-priority metadata → fallback, not silent lower-priority widening
|
||||
FNXC:ReviewRouting 2026-07-01-16:36:
|
||||
External review checkout contract tests pin fail-closed resolution: absent, blank, relative, or non-git metadata resolves to the task worktree fallback; valid explicit absolute git checkouts resolve to their realpath; sourceMetadata.externalReviewCheckout is the canonical external field; invalid higher-priority metadata must not silently widen to lower-priority metadata.
|
||||
*/
|
||||
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import { WorkflowGraphTaskRunner, type WorkflowGraphTaskRunResult } from "./workflow-graph-task-runner.js";
|
||||
import { ensureWorkflowCompletionSummary } from "./workflow-completion-summary.js";
|
||||
import { createCodeNodeRunner } from "./code-node-runner.js";
|
||||
import { resolveReviewCheckoutCwd } from "./review-checkout.js";
|
||||
import { getTaskReviewCheckoutPath, resolveReviewCheckoutCwd } from "./review-checkout.js";
|
||||
import { getActiveNotificationService } from "./notifier.js";
|
||||
import type { ParseStepsHandlerDeps, CodeNodeRunner } from "./workflow-node-handlers.js";
|
||||
import type { WorkflowBranchPersistence, WorkflowBranchRunState } from "./workflow-graph-branches.js";
|
||||
@@ -1736,6 +1736,26 @@ export class TaskExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:ReviewRouting 2026-07-01-16:36:
|
||||
* Review routing must expose whether the reviewer is using an explicit external checkout or the task worktree, but the invalid-sourceMetadata warning is only valid when sourceMetadata supplied the selected candidate. Higher-priority metadata can fail closed before sourceMetadata is considered, so centralize the logging to keep both review seams consistent and avoid false invalid-path warnings.
|
||||
*/
|
||||
private logReviewCheckoutRouting(taskId: string, task: unknown, reviewCwd: string, worktreePath: string): void {
|
||||
if (reviewCwd !== worktreePath) {
|
||||
reviewerLog.log(`${taskId}: review routed to external checkout ${reviewCwd} (task worktree: ${worktreePath})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedCandidate = getTaskReviewCheckoutPath(task);
|
||||
const sourceMetadata = task && typeof task === "object" ? (task as Record<string, unknown>).sourceMetadata : undefined;
|
||||
const sourceRecord = sourceMetadata && typeof sourceMetadata === "object" ? sourceMetadata as Record<string, unknown> : undefined;
|
||||
const sourceExternalReviewCheckout = sourceRecord?.externalReviewCheckout;
|
||||
const sourceExternalReviewCheckoutPath = typeof sourceExternalReviewCheckout === "string" ? sourceExternalReviewCheckout.trim() : undefined;
|
||||
if (sourceExternalReviewCheckoutPath && selectedCandidate === sourceExternalReviewCheckoutPath) {
|
||||
reviewerLog.warn(`${taskId}: external review checkout metadata present (${sourceExternalReviewCheckoutPath}) but invalid — reviewing task worktree ${worktreePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
private markCompletionFinalized(taskId: string): void {
|
||||
this.markPausedAborted(taskId, "completion-finalize", "completion-finalize");
|
||||
this.completionFinalizedTaskIds.add(taskId);
|
||||
@@ -6363,18 +6383,7 @@ export class TaskExecutor {
|
||||
// Worktree isolation (KTD-11): review the instance's OWN worktree when set.
|
||||
const worktreePath = active.worktreePath || detail.worktree || this.rootDir;
|
||||
const reviewCwd = resolveReviewCheckoutCwd(detail, worktreePath);
|
||||
// Make the actual review target visible: explicit
|
||||
// sourceMetadata.externalReviewCheckout routes review to an external
|
||||
// checkout; without valid metadata, review defaults to the task worktree.
|
||||
if (reviewCwd !== worktreePath) {
|
||||
reviewerLog.log(`${seamTask.id}: review routed to external checkout ${reviewCwd} (task worktree: ${worktreePath})`);
|
||||
} else {
|
||||
const sm = detail.sourceMetadata as Record<string, unknown> | undefined;
|
||||
const hasExternalMeta = sm && typeof sm.externalReviewCheckout === "string" && sm.externalReviewCheckout.trim();
|
||||
if (hasExternalMeta) {
|
||||
reviewerLog.warn(`${seamTask.id}: external review checkout metadata present (${sm!.externalReviewCheckout}) but invalid — reviewing task worktree ${worktreePath}`);
|
||||
}
|
||||
}
|
||||
this.logReviewCheckoutRouting(seamTask.id, detail, reviewCwd, worktreePath);
|
||||
const stepName = detail.steps[stepIndex]?.name ?? `Step ${stepIndex}`;
|
||||
const promptContent = detail.prompt ?? "";
|
||||
const userComments = selectUserCommentsForAgentContext(detail, { limit: null });
|
||||
@@ -12991,18 +13000,7 @@ export class TaskExecutor {
|
||||
const userComments = selectUserCommentsForAgentContext(latestDetailForReview, { limit: null });
|
||||
const settings = await mergeEffectiveSettings(store, latestDetailForReview, await store.getSettings());
|
||||
const reviewCwd = resolveReviewCheckoutCwd(latestDetailForReview, worktreePath);
|
||||
// Make the actual review target visible: explicit
|
||||
// sourceMetadata.externalReviewCheckout routes review to an external
|
||||
// checkout; without valid metadata, review defaults to the task worktree.
|
||||
if (reviewCwd !== worktreePath) {
|
||||
reviewerLog.log(`${taskId}: review routed to external checkout ${reviewCwd} (task worktree: ${worktreePath})`);
|
||||
} else {
|
||||
const sm = latestDetailForReview.sourceMetadata as Record<string, unknown> | undefined;
|
||||
const hasExternalMeta = sm && typeof sm.externalReviewCheckout === "string" && sm.externalReviewCheckout.trim();
|
||||
if (hasExternalMeta) {
|
||||
reviewerLog.warn(`${taskId}: external review checkout metadata present (${sm!.externalReviewCheckout}) but invalid — reviewing task worktree ${worktreePath}`);
|
||||
}
|
||||
}
|
||||
this.logReviewCheckoutRouting(taskId, latestDetailForReview, reviewCwd, worktreePath);
|
||||
// Run the reviewer via semaphore.runNested so its slot accounting
|
||||
// is honest: activeCount transiently bumps to reflect the second
|
||||
// agent session, but the reviewer doesn't enter the wait queue
|
||||
|
||||
@@ -5,13 +5,10 @@ import { isAbsolute } from "node:path";
|
||||
function readMetadataPath(value: unknown): string | undefined {
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
const record = value as Record<string, unknown>;
|
||||
// External review routing must be explicit metadata, not inferred from prompt
|
||||
// text or task descriptions. The resolver only accepts known metadata fields,
|
||||
// treats blank/non-string values as absent, and later validates that the chosen
|
||||
// path is an absolute git checkout. Source priority is fixed
|
||||
// (customFields > branchContext > sourceMetadata > root); an invalid higher
|
||||
// priority candidate fails closed to the task worktree rather than silently
|
||||
// falling through to a lower-priority path.
|
||||
/*
|
||||
FNXC:ReviewRouting 2026-07-01-16:36:
|
||||
External review routing must be explicit metadata, not inferred from prompt text or task descriptions. The resolver only accepts known metadata fields, treats blank/non-string values as absent, and later validates that the chosen path is an absolute git checkout. Source priority is fixed (customFields > branchContext > sourceMetadata > root); an invalid higher-priority candidate fails closed to the task worktree rather than silently falling through to a lower-priority path.
|
||||
*/
|
||||
for (const direct of [record.reviewCheckoutPath, record.externalReviewCheckoutPath, record.externalReviewCheckout]) {
|
||||
if (typeof direct === "string" && direct.trim()) return direct.trim();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user