mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-28 01:03:50 +08:00
fix: 按评审补齐 mock 校验、Worker 上限保护与测试(Codex gpt-6-sol)
This commit is contained in:
1 parent
a1dc362bae
commit
2e6ceddf95
8 files changed
+125
-21
No files matched your search
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user