fix: 按评审补齐 mock 校验、Worker 上限保护与测试(Codex gpt-6-sol)

This commit is contained in:
RuoJi6 committed 2026-10-24 22:08:35 +08:00
1 parent a1dc362bae
commit 2e6ceddf95
8 files changed
+125 -21

No files matched your search

+4 -2
View File
@@ -132,7 +132,9 @@ func TestTaskArchiveDatabaseRoundTrip(t *testing.T) {
if err := d.EnsureLLMUsageTable(); err != nil { if err := d.EnsureLLMUsageTable(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
task, err := d.CreateTaskWithOptions("archive database roundtrip", "restore exact graph", TaskCreateOptions{Name: "cold task"}) task, err := d.CreateTaskWithOptions("archive database roundtrip", "restore exact graph", TaskCreateOptions{
Name: "cold task", WorkerCount: 7,
})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -297,7 +299,7 @@ VALUES($1,$2,0,'quota_exhausted','balance exhausted',$3,$4,$3)`, task.ID, llmPro
if err != nil || live == nil { if err != nil || live == nil {
t.Fatalf("restored task = %+v, %v", live, err) t.Fatalf("restored task = %+v, %v", live, err)
} }
if live.Name != "cold task" || live.Remark != remark || !live.Paused { if live.Name != "cold task" || live.Remark != remark || live.WorkerCount != 7 || !live.Paused {
t.Fatalf("restored task metadata mismatch: %+v", live) t.Fatalf("restored task metadata mismatch: %+v", live)
} }
if err := d.QueryRow(`SELECT count(*) FROM exploration_nodes WHERE exploration_id=$1`, task.ExplorationID).Scan(&nodes); err != nil { if err := d.QueryRow(`SELECT count(*) FROM exploration_nodes WHERE exploration_id=$1`, task.ExplorationID).Scan(&nodes); err != nil {
+74
View File
@@ -1,8 +1,10 @@
package db package db
import ( import (
"fmt"
"strings" "strings"
"testing" "testing"
"time"
) )
func TestTaskAndTemplateRemarkLimitIsShared(t *testing.T) { func TestTaskAndTemplateRemarkLimitIsShared(t *testing.T) {
@@ -26,3 +28,75 @@ func TestWorkerCountNormalizationAndLimit(t *testing.T) {
t.Fatal("oversized worker count accepted") t.Fatal("oversized worker count accepted")
} }
} }
func TestTaskOptionsPersistAndValidateLimits(t *testing.T) {
d, err := Open(testDSN(t))
if err != nil {
t.Skipf("postgres unavailable (%v) — skipping", err)
}
defer d.Close()
if _, err := d.CreateTaskWithOptions("oversized remark", "goal", TaskCreateOptions{
Remark: strings.Repeat("界", MaxTaskRemarkRunes+1),
}); err == nil {
t.Fatal("2001-rune task remark accepted")
}
if _, err := d.CreateTaskWithOptions("oversized workers", "goal", TaskCreateOptions{
WorkerCount: MaxTaskWorkerCount + 1,
}); err == nil {
t.Fatal("51-worker task accepted")
}
remark := strings.Repeat("界", MaxTaskRemarkRunes)
task, err := d.CreateTaskWithOptions("task options boundary", "goal", TaskCreateOptions{
Remark: remark, WorkerCount: MaxTaskWorkerCount,
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = d.DeleteTask(task.ID) })
gotTask, err := d.GetTask(task.ID)
if err != nil {
t.Fatal(err)
}
if gotTask == nil || gotTask.Remark != remark || gotTask.WorkerCount != MaxTaskWorkerCount {
t.Fatalf("task options round trip = %+v", gotTask)
}
name := fmt.Sprintf("options template %d", time.Now().UnixNano())
templateRemark := "模板备注 📝"
template, err := d.CreateTaskTemplate(TaskTemplateInput{
Name: name, Description: "template description", Goal: "template goal",
Remark: templateRemark, WorkerCount: MaxTaskWorkerCount,
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _, _ = d.DeleteTaskTemplate(template.ID) })
gotTemplate, err := d.GetTaskTemplate(template.ID)
if err != nil {
t.Fatal(err)
}
if gotTemplate == nil || gotTemplate.Remark != templateRemark || gotTemplate.WorkerCount != MaxTaskWorkerCount {
t.Fatalf("created template options round trip = %+v", gotTemplate)
}
updatedRemark := strings.Repeat("改", MaxTaskRemarkRunes)
updatedWorkers := MaxTaskWorkerCount - 1
updated, err := d.PatchTaskTemplate(template.ID, TaskTemplatePatch{
Remark: &updatedRemark, WorkerCount: &updatedWorkers,
})
if err != nil {
t.Fatal(err)
}
if updated.Remark != updatedRemark || updated.WorkerCount != updatedWorkers {
t.Fatalf("updated template options = %+v", updated)
}
gotTemplate, err = d.GetTaskTemplate(template.ID)
if err != nil {
t.Fatal(err)
}
if gotTemplate == nil || gotTemplate.Remark != updatedRemark || gotTemplate.WorkerCount != updatedWorkers {
t.Fatalf("updated template options round trip = %+v", gotTemplate)
}
}
+6 -8
View File
@@ -119,11 +119,10 @@ func normalizeTaskTemplateInput(in TaskTemplateInput) (TaskTemplateInput, string
return in, "", fmt.Errorf("%w: goal exceeds %d characters", ErrTaskTemplateInvalid, MaxTaskTemplateTextRunes) return in, "", fmt.Errorf("%w: goal exceeds %d characters", ErrTaskTemplateInvalid, MaxTaskTemplateTextRunes)
} }
in.Remark = strings.TrimSpace(in.Remark) in.Remark = strings.TrimSpace(in.Remark)
// Align with task remarks (server/remarks.go maxRemarkRunes=2000) so applying a // Keep template remarks within the same shared limit as task remarks so
// template cannot produce a create-task payload that the task API rejects. // applying a template cannot produce a rejected create-task payload.
const maxTemplateRemarkRunes = MaxTaskRemarkRunes if utf8.RuneCountInString(in.Remark) > MaxTaskRemarkRunes {
if utf8.RuneCountInString(in.Remark) > maxTemplateRemarkRunes { return in, "", fmt.Errorf("%w: remark exceeds %d characters", ErrTaskTemplateInvalid, MaxTaskRemarkRunes)
return in, "", fmt.Errorf("%w: remark exceeds %d characters", ErrTaskTemplateInvalid, maxTemplateRemarkRunes)
} }
in.WorkerCount = normalizeWorkerCount(in.WorkerCount) in.WorkerCount = normalizeWorkerCount(in.WorkerCount)
if err := validateWorkerCount(in.WorkerCount); err != nil { if err := validateWorkerCount(in.WorkerCount); err != nil {
@@ -171,9 +170,8 @@ func normalizeTaskTemplatePatch(patch TaskTemplatePatch) (TaskTemplatePatch, *st
} }
if patch.Remark != nil { if patch.Remark != nil {
remark := strings.TrimSpace(*patch.Remark) remark := strings.TrimSpace(*patch.Remark)
const maxTemplateRemarkRunes = MaxTaskRemarkRunes if utf8.RuneCountInString(remark) > MaxTaskRemarkRunes {
if utf8.RuneCountInString(remark) > maxTemplateRemarkRunes { return patch, nil, fmt.Errorf("%w: remark exceeds %d characters", ErrTaskTemplateInvalid, MaxTaskRemarkRunes)
return patch, nil, fmt.Errorf("%w: remark exceeds %d characters", ErrTaskTemplateInvalid, maxTemplateRemarkRunes)
} }
patch.Remark = &remark patch.Remark = &remark
} }
+3 -2
View File
@@ -143,8 +143,9 @@ const MaxTaskWorkerCount = 50
// MaxTaskRemarkRunes is the shared limit for task and template remarks. // MaxTaskRemarkRunes is the shared limit for task and template remarks.
const MaxTaskRemarkRunes = 2000 const MaxTaskRemarkRunes = 2000
// normalizeWorkerCount clamps a per-task worker override into [0, MaxTaskWorkerCount]. // normalizeWorkerCount converts negative per-task worker overrides to 0.
// 0 means "use the global settings.workers default". // The upper bound is enforced separately by validateWorkerCount; 0 means
// "use the global settings.workers default".
func normalizeWorkerCount(n int) int { func normalizeWorkerCount(n int) int {
if n < 0 { if n < 0 {
return 0 return 0
+5
View File
@@ -778,6 +778,11 @@ func (e *Engine) Run(ctx context.Context, t *Task) {
func workerCountForTask(t *Task, global int) int { func workerCountForTask(t *Task, global int) int {
if t != nil && t.WorkerCount > 0 { if t != nil && t.WorkerCount > 0 {
// Direct database edits can bypass validation; cap those values so the
// engine never starts more than the supported per-task worker count.
if t.WorkerCount > db.MaxTaskWorkerCount {
return db.MaxTaskWorkerCount
}
return t.WorkerCount return t.WorkerCount
} }
return global return global
+8 -1
View File
@@ -1,6 +1,10 @@
package server package server
import "testing" import (
"testing"
"github.com/Autumn-27/artex/db"
)
func TestWorkerCountForTaskUsesOverrideOrGlobalDefault(t *testing.T) { func TestWorkerCountForTaskUsesOverrideOrGlobalDefault(t *testing.T) {
if got := workerCountForTask(&Task{WorkerCount: 5}, 3); got != 5 { if got := workerCountForTask(&Task{WorkerCount: 5}, 3); got != 5 {
@@ -12,4 +16,7 @@ func TestWorkerCountForTaskUsesOverrideOrGlobalDefault(t *testing.T) {
if got := workerCountForTask(&Task{WorkerCount: -1}, 3); got != 3 { if got := workerCountForTask(&Task{WorkerCount: -1}, 3); got != 3 {
t.Fatalf("negative override = %d, want global 3", got) t.Fatalf("negative override = %d, want global 3", got)
} }
if got := workerCountForTask(&Task{WorkerCount: db.MaxTaskWorkerCount + 1}, 3); got != db.MaxTaskWorkerCount {
t.Fatalf("oversized override = %d, want capped %d", got, db.MaxTaskWorkerCount)
}
} }
@@ -217,7 +217,7 @@ function TaskTemplateManager({
<SheetContent className="grid h-full w-full! max-w-none! grid-rows-[auto_minmax(0,1fr)_auto] gap-0 overflow-hidden p-0 sm:w-[48rem]! sm:max-w-[48rem]!"> <SheetContent className="grid h-full w-full! max-w-none! grid-rows-[auto_minmax(0,1fr)_auto] gap-0 overflow-hidden p-0 sm:w-[48rem]! sm:max-w-[48rem]!">
<SheetHeader className="border-b px-6 py-5"> <SheetHeader className="border-b px-6 py-5">
<SheetTitle>任务模板管理</SheetTitle> <SheetTitle>任务模板管理</SheetTitle>
<SheetDescription>模板保存描述、目标、分类与任务级拦截/允许规则;修改不会影响已经创建的任务。</SheetDescription> <SheetDescription>模板保存描述、目标、备注、Worker 数量、分类与任务级拦截/允许规则;修改不会影响已经创建的任务。</SheetDescription>
</SheetHeader> </SheetHeader>
<div className="grid min-h-0 overflow-y-auto lg:grid-cols-[15rem_minmax(0,1fr)] lg:overflow-hidden"> <div className="grid min-h-0 overflow-y-auto lg:grid-cols-[15rem_minmax(0,1fr)] lg:overflow-hidden">
<div className="flex min-h-0 flex-col border-b p-3 lg:border-r lg:border-b-0"> <div className="flex min-h-0 flex-col border-b p-3 lg:border-r lg:border-b-0">
+24 -7
View File
@@ -33,7 +33,7 @@ import type {
TrafficEvidenceRole, TrafficEvidenceRole,
TrafficEvidenceSnapshot, TrafficEvidenceSnapshot,
} from "../types"; } from "../types";
import { MAX_TASK_WORKER_COUNT } from "../types"; import { MAX_TASK_REMARK_RUNES, MAX_TASK_WORKER_COUNT } from "../types";
import * as D from "./data"; import * as D from "./data";
const delay = (ms = 120) => new Promise((r) => setTimeout(r, ms)); const delay = (ms = 120) => new Promise((r) => setTimeout(r, ms));
@@ -999,6 +999,21 @@ function parseBody(body?: BodyInit | null): Record<string, unknown> {
} }
} }
function mockRemark(value: unknown): string {
const remark = typeof value === "string" ? value.trim() : "";
if (Array.from(remark).length > MAX_TASK_REMARK_RUNES) {
throw new Error(`备注最多 ${MAX_TASK_REMARK_RUNES} 个字符`);
}
return remark;
}
function mockWorkerCount(value: unknown): number {
if (typeof value !== "number") return 0;
if (value < 0) throw new Error("Worker 运行数量不能为负数");
if (value > MAX_TASK_WORKER_COUNT) throw new Error(`Worker 运行数量最多 ${MAX_TASK_WORKER_COUNT}`);
return Math.floor(value);
}
export async function mockHandle<T>(method: string, rawPath: string, body?: BodyInit | null): Promise<T> { export async function mockHandle<T>(method: string, rawPath: string, body?: BodyInit | null): Promise<T> {
advanceMockRetests(); advanceMockRetests();
await delay(); await delay();
@@ -1183,12 +1198,12 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const created: Task = { const created: Task = {
id, id,
name: String(b.name ?? ""), name: String(b.name ?? ""),
remark: typeof b.remark === "string" ? b.remark.trim() : "", remark: mockRemark(b.remark),
category_id: category?.id, category_id: category?.id,
category_name: category?.name, category_name: category?.name,
description: String(b.description ?? "新任务"), description: String(b.description ?? "新任务"),
goal: String(b.goal ?? ""), goal: String(b.goal ?? ""),
worker_count: typeof b.worker_count === "number" ? Math.min(MAX_TASK_WORKER_COUNT, Math.max(0, Math.floor(b.worker_count))) : 0, worker_count: mockWorkerCount(b.worker_count),
status: "created", status: "created",
created_at: now.toISOString(), created_at: now.toISOString(),
created_unix: Math.floor(now.getTime() / 1000), created_unix: Math.floor(now.getTime() / 1000),
@@ -1318,10 +1333,10 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
name, name,
description, description,
goal, goal,
remark: typeof b.remark === "string" ? b.remark.trim() : "", remark: mockRemark(b.remark),
category_id: typeof b.category_id === "number" ? b.category_id : null, category_id: typeof b.category_id === "number" ? b.category_id : null,
intercept_rules: Array.isArray(b.intercept_rules) ? b.intercept_rules : [], intercept_rules: Array.isArray(b.intercept_rules) ? b.intercept_rules : [],
worker_count: typeof b.worker_count === "number" ? Math.min(MAX_TASK_WORKER_COUNT, Math.max(0, Math.floor(b.worker_count))) : 0, worker_count: mockWorkerCount(b.worker_count),
created_at: now, created_at: now,
updated_at: now, updated_at: now,
}; };
@@ -1342,13 +1357,15 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
) { ) {
throw new Error("模板名称已存在"); throw new Error("模板名称已存在");
} }
const remark = typeof b.remark === "string" ? mockRemark(b.remark) : template.remark;
const workerCount = typeof b.worker_count === "number" ? mockWorkerCount(b.worker_count) : template.worker_count;
template.name = name; template.name = name;
template.description = description; template.description = description;
template.goal = goal; template.goal = goal;
if (typeof b.remark === "string") template.remark = b.remark.trim(); template.remark = remark;
if ("category_id" in b) template.category_id = typeof b.category_id === "number" ? b.category_id : null; if ("category_id" in b) template.category_id = typeof b.category_id === "number" ? b.category_id : null;
if (Array.isArray(b.intercept_rules)) template.intercept_rules = b.intercept_rules; if (Array.isArray(b.intercept_rules)) template.intercept_rules = b.intercept_rules;
if (typeof b.worker_count === "number") template.worker_count = Math.min(MAX_TASK_WORKER_COUNT, Math.max(0, Math.floor(b.worker_count))); template.worker_count = workerCount;
template.updated_at = new Date().toISOString(); template.updated_at = new Date().toISOString();
mockTaskTemplates.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || b.id - a.id); mockTaskTemplates.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || b.id - a.id);
return template; return template;