修复
1、修复流量包里的复制按钮不工作问题 2、修复新建任务关联资产不同步到任务测试范围问题
This commit is contained in:
1 parent
3675bd815c
commit
cf0989c67d
10 files changed
+297
-32
No files matched your search
@@ -177,9 +177,13 @@ func TestCompanyScopeInvalid(t *testing.T) {
|
||||
}
|
||||
defer cleanupCompany(d, id)
|
||||
|
||||
// TLD-only domains and overly broad CIDRs should be rejected
|
||||
lines := []string{"com", "1.2.3.4/8"} // rejected
|
||||
added, _, invalid, _ := cs.AddScope(id, lines, "test")
|
||||
// Explicitly typed TLD-only domains and overly broad CIDRs should be
|
||||
// rejected. Untyped plain text is intentionally classified as a keyword.
|
||||
inputs := []ScopeInput{
|
||||
{Kind: "domain", Value: "com"},
|
||||
{Kind: "cidr", Value: "1.2.3.4/8"},
|
||||
}
|
||||
added, _, invalid, _ := cs.AddScopeInputs(id, inputs, "test")
|
||||
if added != 0 {
|
||||
t.Errorf("want 0 added for invalid lines, got %d", added)
|
||||
}
|
||||
|
||||
+35
-2
@@ -89,10 +89,12 @@ const MinPlanHeartbeatSeconds = 600
|
||||
const MaxTaskSourceCount = 8
|
||||
|
||||
// MaxTaskCompanyCount bounds the number of company asset scopes attached to a
|
||||
// task. Company scopes are prompt context, so an unbounded list would inflate
|
||||
// every planner and worker call.
|
||||
// task. Company scopes are prompt context, and their currently attributed
|
||||
// assets are snapshotted into the task at creation time.
|
||||
const MaxTaskCompanyCount = 32
|
||||
|
||||
const taskCompanyAssetSource = "company"
|
||||
|
||||
var (
|
||||
ErrTaskCompanyIDsInvalid = errors.New("invalid task company ids")
|
||||
ErrTaskCompanyNotFound = errors.New("task company not found")
|
||||
@@ -248,6 +250,11 @@ func insertTaskCompanies(tx *sql.Tx, taskID int64, companyIDs []int64) error {
|
||||
if len(companyIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Company scope edits rebuild assets.company_id. Serialize the creation-time
|
||||
// snapshot with those edits so the task sees one committed attribution state.
|
||||
if err := lockCompanyScopeMutation(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
var inserted int
|
||||
err := tx.QueryRow(`
|
||||
WITH requested(company_id, position) AS (
|
||||
@@ -268,6 +275,32 @@ SELECT count(*) FROM inserted`, taskID, companyIDs).Scan(&inserted)
|
||||
if inserted != len(companyIDs) {
|
||||
return fmt.Errorf("%w: one or more companies do not exist", ErrTaskCompanyNotFound)
|
||||
}
|
||||
|
||||
// Updating task_ids fires sync_task_asset_links, which first creates generic
|
||||
// source rows. The provenance upsert must therefore run afterwards so the
|
||||
// company name and creation-time reason remain visible to operators.
|
||||
if _, err := tx.Exec(`
|
||||
UPDATE assets
|
||||
SET task_ids=CASE
|
||||
WHEN $1=ANY(task_ids) THEN task_ids
|
||||
ELSE array_append(task_ids, $1)
|
||||
END
|
||||
WHERE company_id=ANY($2::bigint[])`, taskID, companyIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(`
|
||||
INSERT INTO task_asset_links(task_id, asset_id, source, source_summary)
|
||||
SELECT $1, asset.id, $3, '任务创建时关联企业:' || company.name
|
||||
FROM assets asset
|
||||
JOIN companies company ON company.id=asset.company_id
|
||||
WHERE asset.company_id=ANY($2::bigint[])
|
||||
AND $1=ANY(asset.task_ids)
|
||||
ON CONFLICT (task_id, asset_id) DO UPDATE
|
||||
SET source=EXCLUDED.source,
|
||||
source_summary=EXCLUDED.source_summary,
|
||||
source_node_id=NULL`, taskID, companyIDs, taskCompanyAssetSource); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+78
-1
@@ -509,10 +509,36 @@ func TestCreateTaskWithCompanyScopes(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
emptyCompany, _, err := d.Companies().UpsertCompany(fmt.Sprintf("Task Empty Company %d", suffix), "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = d.Exec(`DELETE FROM companies WHERE id IN ($1,$2)`, companyA, companyB)
|
||||
_, _ = d.Exec(`DELETE FROM companies WHERE id IN ($1,$2,$3)`, companyA, companyB, emptyCompany)
|
||||
})
|
||||
|
||||
existingTask, err := d.CreateTask("existing company asset owner", "goal", nil, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = d.DeleteTask(existingTask.ID) })
|
||||
var companyAssetA, companyAssetB int64
|
||||
domainA := fmt.Sprintf("task-company-a-%d.example.test", suffix)
|
||||
domainB := fmt.Sprintf("task-company-b-%d.example.test", suffix)
|
||||
if err := d.QueryRow(`
|
||||
INSERT INTO assets(type, domain, root_domain, company_id, company_source, task_ids)
|
||||
VALUES ('root_domain',$1,$1,$2,'explicit',ARRAY[$3]::bigint[])
|
||||
RETURNING id`, domainA, companyA, existingTask.ID).Scan(&companyAssetA); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := d.QueryRow(`
|
||||
INSERT INTO assets(type, domain, root_domain, company_id, company_source)
|
||||
VALUES ('root_domain',$1,$1,$2,'explicit')
|
||||
RETURNING id`, domainB, companyB).Scan(&companyAssetB); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _, _ = d.Assets().DeleteByIDs([]int64{companyAssetA, companyAssetB}) })
|
||||
|
||||
task, err := d.CreateTaskWithOptions("company-scoped task", "use company scope", TaskCreateOptions{
|
||||
CompanyIDs: []int64{companyA, companyB},
|
||||
})
|
||||
@@ -537,6 +563,57 @@ func TestCreateTaskWithCompanyScopes(t *testing.T) {
|
||||
if scopeCount != 2 {
|
||||
t.Fatalf("company task scope rows=%d want 2", scopeCount)
|
||||
}
|
||||
assets, err := d.Assets().QueryByTask(task.ID, "root_domain", 10, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(assets) != 2 {
|
||||
t.Fatalf("company task assets=%d want 2: %+v", len(assets), assets)
|
||||
}
|
||||
assetByID := make(map[int64]*Asset, len(assets))
|
||||
for _, asset := range assets {
|
||||
assetByID[asset.ID] = asset
|
||||
}
|
||||
for assetID, companyName := range map[int64]string{
|
||||
companyAssetA: fmt.Sprintf("Task Company A %d", suffix),
|
||||
companyAssetB: fmt.Sprintf("Task Company B %d", suffix),
|
||||
} {
|
||||
asset := assetByID[assetID]
|
||||
if asset == nil {
|
||||
t.Errorf("company asset %d missing from task", assetID)
|
||||
continue
|
||||
}
|
||||
if asset.TaskSource != taskCompanyAssetSource || asset.TaskSourceSummary != "任务创建时关联企业:"+companyName {
|
||||
t.Errorf("asset %d provenance=%q/%q", assetID, asset.TaskSource, asset.TaskSourceSummary)
|
||||
}
|
||||
}
|
||||
var existingAssociation bool
|
||||
if err := d.QueryRow(`SELECT $1=ANY(task_ids) FROM assets WHERE id=$2`, existingTask.ID, companyAssetA).Scan(&existingAssociation); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !existingAssociation {
|
||||
t.Fatal("company snapshot removed the asset's existing task association")
|
||||
}
|
||||
var existingSource string
|
||||
if err := d.QueryRow(`SELECT source FROM task_asset_links WHERE task_id=$1 AND asset_id=$2`, existingTask.ID, companyAssetA).Scan(&existingSource); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if existingSource == taskCompanyAssetSource {
|
||||
t.Fatalf("company snapshot overwrote another task's provenance: %q", existingSource)
|
||||
}
|
||||
|
||||
emptyTask, err := d.CreateTaskWithOptions("empty company scope", "no current assets", TaskCreateOptions{
|
||||
CompanyIDs: []int64{emptyCompany},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if assets, err := d.Assets().QueryByTask(emptyTask.ID, "", 10, 0); err != nil || len(assets) != 0 {
|
||||
t.Fatalf("empty company task assets=%+v err=%v", assets, err)
|
||||
}
|
||||
if err := d.DeleteTask(emptyTask.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
duplicateTask, err := d.CreateTaskWithOptions("duplicate company scope", "deduplicate", TaskCreateOptions{
|
||||
CompanyIDs: []int64{companyA, companyA, companyB, companyA},
|
||||
|
||||
Reference in new issue
Block a user