feat: 完善两阶段 Agent 自进化 Pipeline、Skills 与 Benchmark (#129)
This commit is contained in:
@@ -1,47 +1,32 @@
|
||||
/**
|
||||
* Unit tests for benchmark-metrics.ts: metric switching on the evaluation-center
|
||||
* chart (score / cost / duration) — value extraction (missing -> null), gap
|
||||
* segmentation (skipped points: connect within a segment, break between
|
||||
* segments, a lone point still forms its own segment), and the y-axis max.
|
||||
* Unit tests for the Evaluation center's Score-only chart helpers: Score extraction,
|
||||
* gap segmentation across runtime series, y-axis max, and runtime grouping.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
lineSegments,
|
||||
metricMax,
|
||||
metricValues,
|
||||
modelSeries,
|
||||
scoreValues,
|
||||
seriesValues,
|
||||
} from "../src/features/benchmark/benchmark-metrics";
|
||||
|
||||
const evaluations = [
|
||||
{ score: 6, cost: 0.12, durationMs: 90_000 },
|
||||
{ score: 7.5 }, // legacy record: no cost / durationMs
|
||||
{ score: 8.5, cost: 0.2, durationMs: 60_000 },
|
||||
];
|
||||
const evaluations = [{ score: 60 }, { score: 75.25 }, { score: 85.5 }];
|
||||
|
||||
describe("metricValues (value extraction by metric, missing → null)", () => {
|
||||
it("score is always present", () => {
|
||||
expect(metricValues(evaluations, "score")).toEqual([6, 7.5, 8.5]);
|
||||
});
|
||||
|
||||
it("missing cost / duration yields null (skipped point)", () => {
|
||||
expect(metricValues(evaluations, "cost")).toEqual([0.12, null, 0.2]);
|
||||
expect(metricValues(evaluations, "duration")).toEqual([90_000, null, 60_000]);
|
||||
});
|
||||
|
||||
it("non-finite values (NaN / Infinity) are treated as missing", () => {
|
||||
expect(metricValues([{ score: 1, cost: Number.NaN }], "cost")).toEqual([null]);
|
||||
expect(metricValues([{ score: 1, durationMs: Infinity }], "duration")).toEqual([null]);
|
||||
describe("scoreValues", () => {
|
||||
it("extracts stored Scores and treats non-finite malformed input as a gap", () => {
|
||||
expect(scoreValues(evaluations)).toEqual([60, 75.25, 85.5]);
|
||||
expect(scoreValues([{ score: Number.NaN }, { score: Infinity }])).toEqual([null, null]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lineSegments (gap segmentation)", () => {
|
||||
it("no gaps: one segment with everything (consecutive indexes)", () => {
|
||||
expect(lineSegments([6, 7.5, 8.5])).toEqual([
|
||||
expect(lineSegments([60, 75.25, 85.5])).toEqual([
|
||||
[
|
||||
{ index: 0, value: 6 },
|
||||
{ index: 1, value: 7.5 },
|
||||
{ index: 2, value: 8.5 },
|
||||
{ index: 0, value: 60 },
|
||||
{ index: 1, value: 75.25 },
|
||||
{ index: 2, value: 85.5 },
|
||||
],
|
||||
]);
|
||||
});
|
||||
@@ -77,42 +62,78 @@ describe("metricMax (y-axis upper bound)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("modelSeries / seriesValues (curves split into series by model)", () => {
|
||||
describe("modelSeries / seriesValues (curves split by model ID and thinking level)", () => {
|
||||
const mixed = [
|
||||
{ score: 6, provider: "deepseek", modelId: "deepseek-v4-flash" },
|
||||
{ score: 7 }, // legacy record: no model tagged -> trailing gray series
|
||||
{ score: 7.5, provider: "deepseek", modelId: "deepseek-v4-pro" },
|
||||
{ score: 8.5, provider: "deepseek", modelId: "deepseek-v4-pro" },
|
||||
{
|
||||
score: 6,
|
||||
provider: "deepseek",
|
||||
modelId: "deepseek-v4-flash",
|
||||
thinkingLevel: "medium",
|
||||
},
|
||||
{ score: 7 }, // Defensive untagged input -> trailing gray series.
|
||||
{
|
||||
score: 7.5,
|
||||
provider: "deepseek",
|
||||
modelId: "deepseek-v4-pro",
|
||||
thinkingLevel: "xhigh",
|
||||
},
|
||||
{
|
||||
score: 8.5,
|
||||
provider: "deepseek",
|
||||
modelId: "deepseek-v4-pro",
|
||||
thinkingLevel: "xhigh",
|
||||
},
|
||||
];
|
||||
|
||||
it("groups by (provider, modelId) in first-appearance order; untagged records go to a trailing unnamed series", () => {
|
||||
it("groups by (modelId, thinkingLevel) in first-appearance order; untagged records go to a trailing unnamed series", () => {
|
||||
const series = modelSeries(mixed);
|
||||
expect(series.map((s) => s.modelId)).toEqual([
|
||||
"deepseek-v4-flash",
|
||||
"deepseek-v4-pro",
|
||||
undefined,
|
||||
]);
|
||||
expect(series.map((s) => s.thinkingLevel)).toEqual(["medium", "xhigh", undefined]);
|
||||
expect(series.map((s) => s.indices)).toEqual([[0], [2, 3], [1]]);
|
||||
expect(series[2]!.key).toBe("");
|
||||
});
|
||||
|
||||
it("the same modelId across providers forms separate series (paired grouping, no concatenation semantics)", () => {
|
||||
const dup = [
|
||||
{ score: 1, provider: "moonshot", modelId: "kimi-k2.6" },
|
||||
{ score: 2, provider: "siliconflow", modelId: "kimi-k2.6" },
|
||||
it("the same model ID and thinking level across providers stays in one series", () => {
|
||||
const sameRuntime = [
|
||||
{
|
||||
score: 1,
|
||||
provider: "moonshot",
|
||||
modelId: "kimi-k2.6",
|
||||
thinkingLevel: "medium",
|
||||
},
|
||||
{
|
||||
score: 2,
|
||||
provider: "siliconflow",
|
||||
modelId: "kimi-k2.6",
|
||||
thinkingLevel: "medium",
|
||||
},
|
||||
];
|
||||
const series = modelSeries(dup);
|
||||
const series = modelSeries(sameRuntime);
|
||||
expect(series).toHaveLength(1);
|
||||
expect(series[0]!.indices).toEqual([0, 1]);
|
||||
});
|
||||
|
||||
it("the same model ID at different thinking levels forms separate series", () => {
|
||||
const levels = [
|
||||
{ score: 1, modelId: "deepseek-v4-pro", thinkingLevel: "medium" },
|
||||
{ score: 2, modelId: "deepseek-v4-pro", thinkingLevel: "xhigh" },
|
||||
];
|
||||
const series = modelSeries(levels);
|
||||
expect(series).toHaveLength(2);
|
||||
expect(series.map((s) => s.provider)).toEqual(["moonshot", "siliconflow"]);
|
||||
expect(series.map((s) => s.thinkingLevel)).toEqual(["medium", "xhigh"]);
|
||||
});
|
||||
|
||||
it("seriesValues: indexes outside the series are null (skipped points), keeping the global time axis", () => {
|
||||
const series = modelSeries(mixed);
|
||||
expect(seriesValues(mixed, series[1]!, "score")).toEqual([null, null, 7.5, 8.5]);
|
||||
expect(seriesValues(mixed, series[2]!, "score")).toEqual([null, 7, null, null]);
|
||||
expect(seriesValues(mixed, series[1]!)).toEqual([null, null, 7.5, 8.5]);
|
||||
expect(seriesValues(mixed, series[2]!)).toEqual([null, 7, null, null]);
|
||||
});
|
||||
|
||||
it("all untagged: just one unnamed series (legacy data still draws as a single series)", () => {
|
||||
it("all untagged defensive input forms one unnamed series", () => {
|
||||
const series = modelSeries([{}, {}]);
|
||||
expect(series).toHaveLength(1);
|
||||
expect(series[0]!.key).toBe("");
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { EXAMPLE_TASKS } from "../src/features/chat/example-tasks";
|
||||
import { buildSkillsMessage } from "../src/features/chat/skill-use";
|
||||
import { en } from "../src/lib/strings-en";
|
||||
import { zh } from "../src/lib/strings";
|
||||
|
||||
describe("draft example tasks", () => {
|
||||
it.each(["agentBenchmarkBuild", "agentOptimization"] as const)(
|
||||
"submits the %s prompt without an implicit Skill block",
|
||||
(id) => {
|
||||
const task = EXAMPLE_TASKS.find((candidate) => candidate.id === id);
|
||||
expect(task).toBeDefined();
|
||||
expect(task?.skills).toEqual([]);
|
||||
expect(buildSkillsMessage([...(task?.skills ?? [])], zh.chat.exampleTasks[id].prompt)).toBe(
|
||||
zh.chat.exampleTasks[id].prompt,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
locale: "zh",
|
||||
buildPrompt: zh.chat.exampleTasks.agentBenchmarkBuild.prompt,
|
||||
optimizationPrompt: zh.chat.exampleTasks.agentOptimization.prompt,
|
||||
buildMarkers: [
|
||||
"依次使用 `agent-creation` 和 `benchmark-design`",
|
||||
"id:`finite_choice_agent`",
|
||||
"installed_skills:`[]`",
|
||||
"id:`contextual-choice-adaptation`",
|
||||
"desired_baseline_score:`<75`",
|
||||
"pilot_iteration_limit:`5`",
|
||||
"足球投注决策",
|
||||
"售后政策与工单事实",
|
||||
"投资策略、历史市场与当前指标",
|
||||
],
|
||||
buildForbiddenMarkers: ["thinking_level", "provider", "model_id"],
|
||||
optimizationMarkers: [
|
||||
"使用 `agent-optimization`",
|
||||
"test_agent_id:`finite_choice_agent`",
|
||||
"benchmark_id:`contextual-choice-adaptation`",
|
||||
"提高信息不完整、规则冲突和有限选项决策中的稳定性",
|
||||
"desired_score:`>=95`",
|
||||
"candidate_round_limit:`5`",
|
||||
],
|
||||
},
|
||||
{
|
||||
locale: "en",
|
||||
buildPrompt: en.chat.exampleTasks.agentBenchmarkBuild.prompt,
|
||||
optimizationPrompt: en.chat.exampleTasks.agentOptimization.prompt,
|
||||
buildMarkers: [
|
||||
"Use `agent-creation` followed by `benchmark-design`",
|
||||
"id: `finite_choice_agent`",
|
||||
"installed_skills: `[]`",
|
||||
"id: `contextual-choice-adaptation`",
|
||||
"desired_baseline_score: `<75`",
|
||||
"pilot_iteration_limit: `5`",
|
||||
"football betting decisions",
|
||||
"policy and ticket facts",
|
||||
"strategy, historical markets, and current indicators",
|
||||
],
|
||||
buildForbiddenMarkers: ["thinking_level", "provider", "model_id"],
|
||||
optimizationMarkers: [
|
||||
"Use `agent-optimization`",
|
||||
"test_agent_id: `finite_choice_agent`",
|
||||
"benchmark_id: `contextual-choice-adaptation`",
|
||||
"improve stability under incomplete information, conflicting rules, and finite choices",
|
||||
"desired_score: `>=95`",
|
||||
"candidate_round_limit: `5`",
|
||||
],
|
||||
},
|
||||
])(
|
||||
"$locale preserves the two-session agent evolution contract",
|
||||
({
|
||||
buildPrompt,
|
||||
optimizationPrompt,
|
||||
buildMarkers,
|
||||
buildForbiddenMarkers,
|
||||
optimizationMarkers,
|
||||
}) => {
|
||||
const normalizedBuild = buildPrompt.replace(/\s+/g, " ");
|
||||
const normalizedOptimization = optimizationPrompt.replace(/\s+/g, " ");
|
||||
|
||||
for (const marker of buildMarkers) {
|
||||
expect(normalizedBuild).toContain(marker);
|
||||
}
|
||||
for (const marker of buildForbiddenMarkers) {
|
||||
expect(normalizedBuild).not.toContain(marker);
|
||||
}
|
||||
for (const marker of optimizationMarkers) {
|
||||
expect(normalizedOptimization).toContain(marker);
|
||||
}
|
||||
|
||||
expect(normalizedBuild).not.toContain("penguin run");
|
||||
expect(normalizedOptimization).not.toContain("penguin run");
|
||||
expect(normalizedBuild).not.toContain("run_subagent");
|
||||
expect(normalizedOptimization).not.toContain("run_subagent");
|
||||
expect(normalizedBuild).not.toContain("Scoreboard");
|
||||
expect(normalizedOptimization).not.toContain("Scoreboard");
|
||||
expect(normalizedBuild.length).toBeLessThan(1600);
|
||||
expect(normalizedOptimization.length).toBeLessThan(600);
|
||||
expect(normalizedBuild).not.toContain("Phase 1");
|
||||
expect(normalizedOptimization).not.toContain("Phase 3");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -12,12 +12,21 @@ import {
|
||||
formatPercent,
|
||||
formatRelativeDate,
|
||||
formatRelativeDays,
|
||||
formatScore,
|
||||
formatTps,
|
||||
humanizeDuration,
|
||||
humanizeTokens,
|
||||
signedDelta,
|
||||
} from "../src/lib/format";
|
||||
|
||||
describe("formatScore", () => {
|
||||
it("keeps up to the stored two decimal places", () => {
|
||||
expect(formatScore(72)).toBe("72");
|
||||
expect(formatScore(72.5)).toBe("72.5");
|
||||
expect(formatScore(72.35)).toBe("72.35");
|
||||
});
|
||||
});
|
||||
|
||||
describe("humanizeTokens", () => {
|
||||
it("below 1000 unchanged", () => {
|
||||
expect(humanizeTokens(0)).toBe("0");
|
||||
|
||||
Reference in New Issue
Block a user