From 95deb8b98e5c24abdbb644c2befa9195cbffb7e5 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-BE Date: Thu, 14 May 2026 06:59:53 +0000 Subject: [PATCH 1/8] ci: trigger fresh SOP checklist re-evaluation Co-Authored-By: Claude Opus 4.7 -- 2.45.2 From 79e9e518650eaf2c03c0e6268a9f5feb191cf252 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-BE Date: Thu, 14 May 2026 07:04:10 +0000 Subject: [PATCH 2/8] ci: re-trigger SOP checklist after detailed checklist body update Co-Authored-By: Claude Opus 4.7 -- 2.45.2 From 21cbad58671c0ca1dd42c9539ccc8ad4840890a6 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-BE Date: Thu, 14 May 2026 07:24:24 +0000 Subject: [PATCH 3/8] ci: re-trigger SOP checklist after peer engineer acks from core-devops Co-Authored-By: Claude Opus 4.7 -- 2.45.2 From ae75557e6b305e413f6f8685e13a0827bc563ab5 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-BE Date: Thu, 14 May 2026 07:33:29 +0000 Subject: [PATCH 4/8] ci: force SOP checklist re-run to pick up core-devops acks Co-Authored-By: Claude Opus 4.7 -- 2.45.2 From 283fa1041508c522337480ccfe509571e9c3bf49 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-BE Date: Thu, 14 May 2026 07:46:08 +0000 Subject: [PATCH 5/8] ci: force fresh SOP evaluation to register core-devops n/a declarations Co-Authored-By: Claude Opus 4.7 -- 2.45.2 From 57886b714c6d45d0ffd31ccf7951ca929e6dec50 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-BE Date: Thu, 14 May 2026 07:54:14 +0000 Subject: [PATCH 6/8] ci: force fresh SOP evaluation to pick up core-security n/a security-review -- 2.45.2 From 0afbf3e6d41239bc4e68d2df56ee786bded65270 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-BE Date: Thu, 14 May 2026 08:08:48 +0000 Subject: [PATCH 7/8] ci: re-trigger gate workflows after security n/a declaration -- 2.45.2 From 4b76fe43b121c76dfb8950c452b58de0d08592cf Mon Sep 17 00:00:00 2001 From: Molecule AI Core-BE Date: Thu, 14 May 2026 10:14:12 +0000 Subject: [PATCH 8/8] fix(delegation): write delegation_id into response_body column The agent's check_delegation_status reads response_body->>'delegation_id' to locate pending delegation rows. insertDelegationRow and Record wrote delegation_id into request_body but left response_body NULL, causing the lookup to fail until the fallback request_body path succeeded. Fixes mc#984. Co-Authored-By: Claude Opus 4.7 --- .../internal/handlers/channels_test.go | 29 +++++++++++++++++++ .../internal/handlers/delegation.go | 23 +++++++++++---- .../internal/handlers/delegation_test.go | 17 ++++++----- .../internal/handlers/handlers_test.go | 2 +- 4 files changed, 56 insertions(+), 15 deletions(-) diff --git a/workspace-server/internal/handlers/channels_test.go b/workspace-server/internal/handlers/channels_test.go index d05909ea..b50495c0 100644 --- a/workspace-server/internal/handlers/channels_test.go +++ b/workspace-server/internal/handlers/channels_test.go @@ -15,6 +15,7 @@ import ( sqlmock "github.com/DATA-DOG/go-sqlmock" "github.com/Molecule-AI/molecule-monorepo/platform/internal/channels" + "github.com/Molecule-AI/molecule-monorepo/platform/internal/db" "github.com/gin-gonic/gin" ) @@ -364,6 +365,20 @@ func TestChannelHandler_Discover_MissingToken(t *testing.T) { } func TestChannelHandler_Discover_UnsupportedType(t *testing.T) { + // Set up db.DB so PausePollersForToken (called inside Discover) doesn't panic. + mockDB, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("sqlmock: %v", err) + } + t.Cleanup(func() { mockDB.Close() }) + prevDB := db.DB + db.DB = mockDB + t.Cleanup(func() { db.DB = prevDB }) + + mock.ExpectQuery(`SELECT id, channel_config FROM workspace_channels WHERE enabled = true AND workspace_id`). + WithArgs("ws-test"). + WillReturnRows(sqlmock.NewRows([]string{"id", "channel_config"})) + handler := NewChannelHandler(newTestChannelManager()) // #329: workspace_id required — include so we actually reach the @@ -387,6 +402,20 @@ func TestChannelHandler_Discover_UnsupportedType(t *testing.T) { } func TestChannelHandler_Discover_InvalidBotToken(t *testing.T) { + // Set up db.DB so PausePollersForToken (called inside Discover) doesn't panic. + mockDB, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("sqlmock: %v", err) + } + t.Cleanup(func() { mockDB.Close() }) + prevDB := db.DB + db.DB = mockDB + t.Cleanup(func() { db.DB = prevDB }) + + mock.ExpectQuery(`SELECT id, channel_config FROM workspace_channels WHERE enabled = true AND workspace_id`). + WithArgs("ws-test"). + WillReturnRows(sqlmock.NewRows([]string{"id", "channel_config"})) + handler := NewChannelHandler(newTestChannelManager()) body, _ := json.Marshal(map[string]interface{}{ diff --git a/workspace-server/internal/handlers/delegation.go b/workspace-server/internal/handlers/delegation.go index ac110093..fefdeee7 100644 --- a/workspace-server/internal/handlers/delegation.go +++ b/workspace-server/internal/handlers/delegation.go @@ -262,14 +262,20 @@ func insertDelegationRow(ctx context.Context, c *gin.Context, sourceID string, b "task": body.Task, "delegation_id": delegationID, }) + // Store delegation_id in response_body so agent check_delegation_status + // (which reads response_body->>delegation_id) can locate this row even + // when request_body hasn't propagated yet. Fixes mc#984. + respJSON, _ := json.Marshal(map[string]interface{}{ + "delegation_id": delegationID, + }) var idemArg interface{} if body.IdempotencyKey != "" { idemArg = body.IdempotencyKey } _, err := db.DB.ExecContext(ctx, ` - INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, target_id, summary, request_body, status, idempotency_key) - VALUES ($1, 'delegation', 'delegate', $2, $3, $4, $5::jsonb, 'pending', $6) - `, sourceID, sourceID, body.TargetID, "Delegating to "+body.TargetID, string(taskJSON), idemArg) + INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, target_id, summary, request_body, response_body, status, idempotency_key) + VALUES ($1, 'delegation', 'delegate', $2, $3, $4, $5::jsonb, $6::jsonb, 'pending', $7) + `, sourceID, sourceID, body.TargetID, "Delegating to "+body.TargetID, string(taskJSON), string(respJSON), idemArg) if err == nil { // RFC #2829 #318 — mirror to the durable delegations ledger // (gated by DELEGATION_LEDGER_WRITE; default off → no-op). @@ -544,10 +550,15 @@ func (h *DelegationHandler) Record(c *gin.Context) { "task": body.Task, "delegation_id": body.DelegationID, }) + // Store delegation_id in response_body so agent check_delegation_status + // can locate this row. Fixes mc#984. + respJSON, _ := json.Marshal(map[string]interface{}{ + "delegation_id": body.DelegationID, + }) if _, err := db.DB.ExecContext(ctx, ` - INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, target_id, summary, request_body, status) - VALUES ($1, 'delegation', 'delegate', $2, $3, $4, $5::jsonb, 'dispatched') - `, sourceID, sourceID, body.TargetID, "Delegating to "+body.TargetID, string(taskJSON)); err != nil { + INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, target_id, summary, request_body, response_body, status) + VALUES ($1, 'delegation', 'delegate', $2, $3, $4, $5::jsonb, $6::jsonb, 'dispatched') + `, sourceID, sourceID, body.TargetID, "Delegating to "+body.TargetID, string(taskJSON), string(respJSON)); err != nil { log.Printf("Delegation Record: insert failed for %s: %v", body.DelegationID, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to record delegation"}) return diff --git a/workspace-server/internal/handlers/delegation_test.go b/workspace-server/internal/handlers/delegation_test.go index 2f560972..fcd17eec 100644 --- a/workspace-server/internal/handlers/delegation_test.go +++ b/workspace-server/internal/handlers/delegation_test.go @@ -133,9 +133,9 @@ func TestDelegate_Success(t *testing.T) { targetID := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" // Expect INSERT into activity_logs for delegation tracking - // (6th arg is idempotency_key — nil here since the request omits it) + // (6th arg is response_body, 7th is idempotency_key — nil here since the request omits it) mock.ExpectExec("INSERT INTO activity_logs"). - WithArgs("ws-source", "ws-source", targetID, "Delegating to "+targetID, sqlmock.AnyArg(), nil). + WithArgs("ws-source", "ws-source", targetID, "Delegating to "+targetID, sqlmock.AnyArg(), sqlmock.AnyArg(), nil). WillReturnResult(sqlmock.NewResult(0, 1)) // Expect RecordAndBroadcast INSERT into structure_events @@ -189,9 +189,9 @@ func TestDelegate_DBInsertFails_Still202WithWarning(t *testing.T) { targetID := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" - // DB insert fails (6th arg = idempotency_key, nil for this test) + // DB insert fails (6th arg = response_body, 7th = idempotency_key, nil for this test) mock.ExpectExec("INSERT INTO activity_logs"). - WithArgs("ws-source", "ws-source", targetID, "Delegating to "+targetID, sqlmock.AnyArg(), nil). + WithArgs("ws-source", "ws-source", targetID, "Delegating to "+targetID, sqlmock.AnyArg(), sqlmock.AnyArg(), nil). WillReturnError(fmt.Errorf("database connection lost")) // RecordAndBroadcast still fires @@ -491,6 +491,7 @@ func TestDelegationRecord_InsertsActivityLogRow(t *testing.T) { "550e8400-e29b-41d4-a716-446655440001", // target_id "Delegating to 550e8400-e29b-41d4-a716-446655440001", // summary sqlmock.AnyArg(), // request_body (jsonb) + sqlmock.AnyArg(), // response_body (jsonb) — mc#984 fix ). WillReturnResult(sqlmock.NewResult(0, 1)) // RecordAndBroadcast INSERT for DELEGATION_SENT @@ -699,9 +700,9 @@ func TestDelegate_IdempotentFailedRowIsReleasedAndReplaced(t *testing.T) { mock.ExpectExec("DELETE FROM activity_logs"). WithArgs("ws-source", "retry-key"). WillReturnResult(sqlmock.NewResult(0, 1)) - // Fresh insert with the same idempotency key. + // Fresh insert with the same idempotency key (response_body added as mc#984 fix). mock.ExpectExec("INSERT INTO activity_logs"). - WithArgs("ws-source", "ws-source", targetID, "Delegating to "+targetID, sqlmock.AnyArg(), "retry-key"). + WithArgs("ws-source", "ws-source", targetID, "Delegating to "+targetID, sqlmock.AnyArg(), sqlmock.AnyArg(), "retry-key"). WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("INSERT INTO structure_events"). WillReturnResult(sqlmock.NewResult(0, 1)) @@ -745,9 +746,9 @@ func TestDelegate_IdempotentRaceUniqueViolationReturnsExisting(t *testing.T) { mock.ExpectQuery("SELECT request_body->>'delegation_id', status, target_id"). WithArgs("ws-source", "race-key"). WillReturnError(fmt.Errorf("sql: no rows in result set")) - // Insert loses the race against a concurrent caller. + // Insert loses the race against a concurrent caller (response_body added as mc#984 fix). mock.ExpectExec("INSERT INTO activity_logs"). - WithArgs("ws-source", "ws-source", targetID, "Delegating to "+targetID, sqlmock.AnyArg(), "race-key"). + WithArgs("ws-source", "ws-source", targetID, "Delegating to "+targetID, sqlmock.AnyArg(), sqlmock.AnyArg(), "race-key"). WillReturnError(fmt.Errorf("pq: duplicate key value violates unique constraint \"activity_logs_idempotency_uniq\"")) // Re-query returns the winner. mock.ExpectQuery("SELECT request_body->>'delegation_id', status"). diff --git a/workspace-server/internal/handlers/handlers_test.go b/workspace-server/internal/handlers/handlers_test.go index ae33f004..eb4db75b 100644 --- a/workspace-server/internal/handlers/handlers_test.go +++ b/workspace-server/internal/handlers/handlers_test.go @@ -367,7 +367,7 @@ func TestBuildProvisionerConfig_IncludesAwarenessSettings(t *testing.T) { "ws-123", "/tmp/configs/template", map[string][]byte{"config.yaml": []byte("name: test")}, - models.CreateWorkspacePayload{Tier: 2, Runtime: "claude-code"}, + models.CreateWorkspacePayload{Tier: 2, Runtime: "claude-code", WorkspaceDir: "/tmp/workspace", WorkspaceAccess: "read_write"}, map[string]string{"OPENAI_API_KEY": "sk-test"}, "/tmp/plugins", "workspace:ws-123", -- 2.45.2