Merge pull request 'test: live review smoke' (#1) from test/live-review-smoke into main
This commit is contained in:
@@ -93,3 +93,5 @@ path instead of the host-port URL.)
|
|||||||
```
|
```
|
||||||
npm test # node --test: JSON extractor, verdict derivation, trailer parser
|
npm test # node --test: JSON extractor, verdict derivation, trailer parser
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- live-test marker 20260818 -->
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const DEFAULTS = {
|
|||||||
webhookSecret: '',
|
webhookSecret: '',
|
||||||
routerUrl: 'http://localhost:5497',
|
routerUrl: 'http://localhost:5497',
|
||||||
model: 'anthropic/claude-sonnet-4-6',
|
model: 'anthropic/claude-sonnet-4-6',
|
||||||
|
agentId: 'code-reviewer',
|
||||||
|
fallbackAgentId: 'architect',
|
||||||
tenantId: 'nirlab',
|
tenantId: 'nirlab',
|
||||||
publicBaseUrl: 'http://localhost:5498',
|
publicBaseUrl: 'http://localhost:5498',
|
||||||
dbPath: join(CONFIG_DIR, 'reviews.db'),
|
dbPath: join(CONFIG_DIR, 'reviews.db'),
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ export class GiteaClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPrCommits(owner, repo, index) {
|
||||||
|
return this.req('GET', `/api/v1/repos/${owner}/${repo}/pulls/${index}/commits?limit=50`);
|
||||||
|
}
|
||||||
|
|
||||||
getCommit(owner, repo, sha) {
|
getCommit(owner, repo, sha) {
|
||||||
return this.req('GET', `/api/v1/repos/${owner}/${repo}/git/commits/${sha}?stat=false&verification=false&files=false`);
|
return this.req('GET', `/api/v1/repos/${owner}/${repo}/git/commits/${sha}?stat=false&verification=false&files=false`);
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-7
@@ -19,6 +19,18 @@ export async function runReview({ cfg, gitea, db, job, withGlobal, log }) {
|
|||||||
const dashboardUrl = `${cfg.publicBaseUrl}/review/${owner}/${repo}`;
|
const dashboardUrl = `${cfg.publicBaseUrl}/review/${owner}/${repo}`;
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
|
|
||||||
|
// For PR events the webhook only carries title/body; pull the PR's commits
|
||||||
|
// so Co-Authored-By trailer attribution sees real commit messages.
|
||||||
|
if (job.prIndex) {
|
||||||
|
const commits = await safe(log, 'pr-commits', () => gitea.getPrCommits(owner, repo, job.prIndex));
|
||||||
|
if (Array.isArray(commits)) {
|
||||||
|
job.commitMessages = job.commitMessages.concat(
|
||||||
|
commits.map(c => c?.commit?.message || '').filter(Boolean));
|
||||||
|
const names = commits.map(c => c?.commit?.author?.name || c?.author?.login).filter(Boolean);
|
||||||
|
job.authors = [...new Set(job.authors.concat(names))];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 0. pending status (best-effort)
|
// 0. pending status (best-effort)
|
||||||
await safe(log, 'pending-status', () =>
|
await safe(log, 'pending-status', () =>
|
||||||
gitea.postStatus(owner, repo, sha, {
|
gitea.postStatus(owner, repo, sha, {
|
||||||
@@ -55,18 +67,26 @@ export async function runReview({ cfg, gitea, db, job, withGlobal, log }) {
|
|||||||
const messages = buildReviewPrompt({
|
const messages = buildReviewPrompt({
|
||||||
repo: fullRepo, ref: job.ref, sha, prTitle: job.prTitle, conventions, diff, truncated
|
repo: fullRepo, ref: job.ref, sha, prTitle: job.prTitle, conventions, diff, truncated
|
||||||
});
|
});
|
||||||
let review = null, llmError = null;
|
let review = null, llmError = null, servedModel = null;
|
||||||
await withGlobal(async () => {
|
await withGlobal(async () => {
|
||||||
for (let attempt = 1; attempt <= 2; attempt++) {
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||||
try {
|
try {
|
||||||
const content = await chat({
|
// Attempt 1 uses the review agent. Attempt 2 falls back to the
|
||||||
|
// c-suite agent (bypasses the router's domain preflight), covering
|
||||||
|
// classifier false-positives on diff content: agent_mismatch 400s
|
||||||
|
// and tool fast-path hijacks that return non-JSON.
|
||||||
|
const out = await chat({
|
||||||
routerUrl: cfg.routerUrl, model: cfg.model, messages,
|
routerUrl: cfg.routerUrl, model: cfg.model, messages,
|
||||||
agentId: 'code-reviewer',
|
agentId: attempt === 1 ? cfg.agentId : cfg.fallbackAgentId,
|
||||||
sessionId: `review-${owner}-${repo}-${sha.slice(0, 12)}`,
|
sessionId: `review-${owner}-${repo}-${sha.slice(0, 12)}-a${attempt}`,
|
||||||
tenantId: cfg.tenantId, timeoutMs: cfg.routerTimeoutMs
|
tenantId: cfg.tenantId, timeoutMs: cfg.routerTimeoutMs
|
||||||
});
|
});
|
||||||
const raw = extractJson(content);
|
servedModel = out.servedModel;
|
||||||
if (!raw) throw new Error(`no JSON object in model output (attempt ${attempt})`);
|
const raw = extractJson(out.content);
|
||||||
|
if (!raw) {
|
||||||
|
log(`[${fullRepo}@${sha.slice(0, 8)}] unparseable model output (served=${servedModel}): ${JSON.stringify(out.content.slice(0, 400))}`);
|
||||||
|
throw new Error(`no JSON object in model output (attempt ${attempt})`);
|
||||||
|
}
|
||||||
review = normalizeReview(raw);
|
review = normalizeReview(raw);
|
||||||
if (!review) throw new Error('normalization failed');
|
if (!review) throw new Error('normalization failed');
|
||||||
llmError = null;
|
llmError = null;
|
||||||
@@ -111,7 +131,7 @@ export async function runReview({ cfg, gitea, db, job, withGlobal, log }) {
|
|||||||
axes: review.axes, overall: review.overall, grade: review.grade,
|
axes: review.axes, overall: review.overall, grade: review.grade,
|
||||||
verdict: review.verdict, findings: review.findings,
|
verdict: review.verdict, findings: review.findings,
|
||||||
statusState: state, error: null, diffBytes, truncated,
|
statusState: state, error: null, diffBytes, truncated,
|
||||||
model: cfg.model, durationMs: Date.now() - t0
|
model: servedModel || cfg.model, durationMs: Date.now() - t0
|
||||||
});
|
});
|
||||||
log(`[${fullRepo}@${sha.slice(0, 8)}] reviewed: ${review.verdict} grade=${review.grade} overall=${review.overall} findings=${review.findings.length} in ${Date.now() - t0}ms`);
|
log(`[${fullRepo}@${sha.slice(0, 8)}] reviewed: ${review.verdict} grade=${review.grade} overall=${review.overall} findings=${review.findings.length} in ${Date.now() - t0}ms`);
|
||||||
return { ok: true, verdict: review.verdict };
|
return { ok: true, verdict: review.verdict };
|
||||||
|
|||||||
+15
-3
@@ -6,7 +6,14 @@ export async function chat({ routerUrl, model, messages, agentId, sessionId, ten
|
|||||||
const res = await fetch(`${routerUrl.replace(/\/$/, '')}/v1/chat`, {
|
const res = await fetch(`${routerUrl.replace(/\/$/, '')}/v1/chat`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ model, messages, agentId, sessionId, tenantId, stream: false }),
|
// tools:false + raw:true — reviews need pure model reasoning. raw:true is
|
||||||
|
// the router's sanctioned _bypassToolRouting flag: without it, keyword
|
||||||
|
// fast-paths (current-info briefing, store-resolver, retail dispatch)
|
||||||
|
// hijack prompts whose DIFF CONTENT mentions github/sales/store-ish terms
|
||||||
|
// and return non-review text. The domain preflight (agent_mismatch 400)
|
||||||
|
// still applies per agent tier; the pipeline's attempt-2 fallback agent
|
||||||
|
// covers that.
|
||||||
|
body: JSON.stringify({ model, messages, agentId, sessionId, tenantId, stream: false, tools: false, raw: true }),
|
||||||
signal: AbortSignal.timeout(timeoutMs)
|
signal: AbortSignal.timeout(timeoutMs)
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -14,9 +21,14 @@ export async function chat({ routerUrl, model, messages, agentId, sessionId, ten
|
|||||||
throw new Error(`router -> ${res.status}: ${text.slice(0, 300)}`);
|
throw new Error(`router -> ${res.status}: ${text.slice(0, 300)}`);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const content = data?.message?.content ?? data?.content;
|
// Response shape varies by upstream: prefer .message.content, then
|
||||||
|
// OpenAI-style choices, then top-level .content (which can carry an
|
||||||
|
// upstream-down apology — hence last).
|
||||||
|
const content = data?.message?.content
|
||||||
|
?? data?.choices?.[0]?.message?.content
|
||||||
|
?? data?.content;
|
||||||
if (typeof content !== 'string' || !content.trim()) {
|
if (typeof content !== 'string' || !content.trim()) {
|
||||||
throw new Error('router returned empty content');
|
throw new Error('router returned empty content');
|
||||||
}
|
}
|
||||||
return content;
|
return { content, servedModel: data?._shre?.model ?? null };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user