mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-19 22:17:42 +02:00
A node claiming something ran must cite a tool_call_id the host issued in the trajectory. A citation absent from the trajectory is invented, so the node is dropped along with its edges; a missing citation counts as invented only when every real ID survived budget fitting, so a trimmed payload cannot silently empty a graph. Judging is skipped where the evidence cannot be enumerated: the process-detail supplement dumps raw event JSON and the messages-table fallback carries no call IDs. Verified citations land in node metadata rather than tool_execution_id, which is foreign-keyed to tool_executions. Also close two smaller holes: fact text can no longer carry the host index delimiters, which would close the block early and spill the remaining index into the system prompt; and the console fact endpoints reject the reserved note/context-* prefix, matching the MCP tool layer.
107 lines
3.8 KiB
Go
107 lines
3.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"cyberstrike-ai/internal/config"
|
|
"cyberstrike-ai/internal/database"
|
|
"github.com/gin-gonic/gin"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func TestProjectBlackboardInitialInjectionUsesCurrentConversation(t *testing.T) {
|
|
db, err := database.NewDB(filepath.Join(t.TempDir(), "injection.db"), zap.NewNop())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
p, err := db.CreateProject(&database.Project{Name: "same project"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a, err := db.CreateConversation("a", database.ConversationCreateMeta{ProjectID: p.ID})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := db.CreateConversation("b", database.ConversationCreateMeta{ProjectID: p.ID})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, f := range []*database.ProjectFact{
|
|
{ProjectID: p.ID, FactKey: "note/context-a", SourceConversationID: a.ID, Summary: "own continuation"},
|
|
{ProjectID: p.ID, FactKey: "note/context-b", SourceConversationID: b.ID, Summary: "other continuation"},
|
|
{ProjectID: p.ID, FactKey: "finding/shared", Category: "finding", Summary: "shared result", Confidence: "confirmed"},
|
|
} {
|
|
if _, err := db.UpsertProjectFact(f); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
h := &AgentHandler{db: db, config: &config.Config{Project: config.ProjectConfig{Enabled: true}}, logger: zap.NewNop()}
|
|
block := h.projectBlackboardBlock(a.ID)
|
|
if !strings.Contains(block, "note/context-a") || !strings.Contains(block, "finding/shared") || strings.Contains(block, "note/context-b") {
|
|
t.Fatalf("initial prompt mixed sessions: %s", block)
|
|
}
|
|
}
|
|
|
|
func TestConsoleCannotCreateOrRewriteReservedCheckpointKeys(t *testing.T) {
|
|
db, err := database.NewDB(filepath.Join(t.TempDir(), "reserved.db"), zap.NewNop())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
p, err := db.CreateProject(&database.Project{Name: "reserved"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
checkpoint, err := db.UpsertProjectFact(&database.ProjectFact{
|
|
ProjectID: p.ID, FactKey: "note/context-victim", SourceConversationID: "victim",
|
|
Summary: "original progress", Body: "ORIGINAL_BODY",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ordinary, err := db.UpsertProjectFact(&database.ProjectFact{ProjectID: p.ID, FactKey: "finding/ordinary", Summary: "shared"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := NewProjectHandler(db, zap.NewNop())
|
|
request := func(method, factID, payload string) (*httptest.ResponseRecorder, *gin.Context) {
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Params = gin.Params{{Key: "id", Value: p.ID}, {Key: "factId", Value: factID}}
|
|
c.Request = httptest.NewRequest(method, "/", strings.NewReader(payload))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
return w, c
|
|
}
|
|
|
|
w, c := request(http.MethodPost, "", `{"fact_key":"note/context-forged","summary":"forged current work"}`)
|
|
h.CreateFact(c)
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("console created a reserved key: %d %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
w, c = request(http.MethodPut, ordinary.ID, `{"fact_key":"note/context-forged"}`)
|
|
h.UpdateFact(c)
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("console renamed a fact into the reserved prefix: %d %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
w, c = request(http.MethodPut, checkpoint.ID, `{"summary":"rewritten","body":"REWRITTEN"}`)
|
|
h.UpdateFact(c)
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("console rewrote another conversation's checkpoint: %d %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
stored, err := db.GetProjectFact(checkpoint.ID)
|
|
if err != nil || stored.Summary != "original progress" || stored.Body != "ORIGINAL_BODY" || stored.SourceConversationID != "victim" {
|
|
t.Fatalf("rejected requests still modified the checkpoint: %+v %v", stored, err)
|
|
}
|
|
if renamed, err := db.GetProjectFact(ordinary.ID); err != nil || renamed.FactKey != "finding/ordinary" {
|
|
t.Fatalf("rejected rename still applied: %+v %v", renamed, err)
|
|
}
|
|
}
|