Compare commits

..

4 Commits

Author SHA1 Message Date
Molecule AI Dev Engineer A (Kimi) 323c080743 test(handlers): add org_scope coverage — orgRootID + sameOrg at 0% (#1953)
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 11s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 7s
Check migration collisions / Migration version collision check (pull_request) Successful in 20s
CI / Detect changes (pull_request) Successful in 19s
CI / Python Lint & Test (pull_request) Successful in 16s
E2E API Smoke Test / detect-changes (pull_request) Successful in 13s
E2E Chat / detect-changes (pull_request) Successful in 14s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Has been skipped
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 12s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 37s
security-review / approved (pull_request) Failing after 11s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Successful in 1m12s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 10s
Harness Replays / detect-changes (pull_request) Successful in 5s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 4s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 4s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m40s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 4s
lint-mask-pr-atomicity / lint-mask-pr-atomicity (pull_request) Successful in 1m17s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m11s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m24s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 4s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Successful in 4m28s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m21s
review-check-tests / review-check.sh regression tests (pull_request) Successful in 8s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m13s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 8s
gate-check-v3 / gate-check (pull_request) Successful in 12s
qa-review / approved (pull_request) Failing after 8s
verify-providers-gen / Regenerate providers artifact and fail on drift (pull_request) Successful in 32s
sop-checklist / review-refire (pull_request) Has been skipped
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-tier-check / tier-check (pull_request) Successful in 8s
sop-checklist / all-items-acked (pull_request) Successful in 9s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m21s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 1m11s
CI / Canvas (Next.js) (pull_request) Successful in 3s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 8s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 2m42s
E2E Chat / E2E Chat (pull_request) Successful in 5s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 3s
Harness Replays / Harness Replays (pull_request) Successful in 3s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2m10s
CI / Platform (Go) (pull_request) Successful in 5m28s
CI / all-required (pull_request) Successful in 36m6s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
audit-force-merge / audit (pull_request_target) Has been skipped
Adds 10 sqlmock-backed tests covering the cross-tenant isolation
helpers introduced in #1953.

Covered:
- orgRootID: happy path (child→root), workspace-is-root, no rows,
  DB error, empty root string
- sameOrg: identical IDs (short-circuit), same org root,
  different org roots, orgRootID fails, orgRootID not found

Closes #1953 follow-up (test debt)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 04:59:11 +00:00
Molecule AI Dev Engineer A (Kimi) 952949ee95 test(handlers): add PatchAbilities coverage — workspace_abilities.go at 0%
Adds 11 sqlmock-backed tests covering the PATCH /workspaces/:id/abilities
handler (PatchAbilities):

- Invalid workspace ID → 400
- Invalid JSON body → 400
- Empty body (no fields) → 400
- Workspace not found → 404
- Existence query error → 404 (fail-closed)
- Patch broadcast_enabled only → 200
- Patch talk_to_user_enabled only → 200
- Patch both fields → 200
- DB error on broadcast update → 500
- DB error on talk_to_user update → 500
- DB error on broadcast when both supplied → 500 (partial update not committed)

Closes #1312

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 04:59:11 +00:00
Molecule AI Dev Engineer A (Kimi) 049df05ca5 ci(workflows): flip cancel-in-progress false→true on 16 workflows (#1357)
Gitea 1.22.6 does not honor cancel-in-progress: false for scheduled/push
events — queued runs accumulate as stale scheduled tasks instead of
waiting, saturating the runner pool (#1357). Flipping to true lets
obsolete in-flight runs cancel correctly, freeing slots.

Safe-flip set (PM + Eng B reviewed, 16 workflows):
- ci-required-drift, staging-smoke, e2e-staging-sanity
- sweep-cf-orphans, sweep-aws-secrets, sweep-cf-tunnels, sweep-stale-e2e-orgs
- e2e-chat, e2e-legacy-advisory, e2e-peer-visibility, e2e-staging-canvas
- continuous-synth-e2e, railway-pin-audit
- handlers-postgres-integration, harness-replays, e2e-api

Excluded (protected — half-rolled fleet / auto-promote / merge ordering):
- e2e-staging-external, e2e-staging-saas, gitea-merge-queue
- redeploy-tenants-on-staging, redeploy-tenants-on-main
- main-red-watchdog, publish-workspace-server-image, status-reaper
- gate-check-v3

Fixes #1357

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 04:59:11 +00:00
Molecule AI Dev Engineer A (Kimi) 0078f702ee ci(workflows): renew continue-on-error tracker mc#774 → mc#1982
mc#774 reached its 14-day renewal cap, causing lint-continue-on-error-tracking
to fail across all workflow PRs and making main red (#1975). Renew the forced-
renewal tracker by creating mc#1982 and updating all 37 job-level mask comments.

Affected: 34 workflow files with continue-on-error: true directives.
Next renewal due: 2026-06-11.

Fixes #1975
Refs: mc#774, mc#1982, feedback_chained_defects_in_never_tested_workflows

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 04:59:11 +00:00
3 changed files with 3 additions and 170 deletions
@@ -2,8 +2,6 @@ package handlers
import (
"context"
"database/sql"
"errors"
"testing"
"github.com/DATA-DOG/go-sqlmock"
@@ -113,125 +111,3 @@ func TestExtractExpiresInSeconds(t *testing.T) {
})
}
}
// TestQueueStatusByID_HappyPath verifies the full projection including optional
// nullable fields and response_body surfacing when status == completed.
func TestQueueStatusByID_HappyPath(t *testing.T) {
mock := setupTestDB(t)
queueID := "queue-789"
mock.ExpectQuery(`SELECT\s+q\.id,\s+q\.workspace_id,\s+q\.status,\s+q\.priority,\s+q\.attempts,\s+q\.last_error,\s+q\.enqueued_at::text,\s+q\.dispatched_at::text,\s+q\.completed_at::text,\s+q\.expires_at::text,\s+al\.response_body::text\s+FROM a2a_queue q\s+LEFT JOIN activity_logs al`).
WithArgs(queueID).
WillReturnRows(sqlmock.NewRows([]string{
"id", "workspace_id", "status", "priority", "attempts",
"last_error", "enqueued_at", "dispatched_at", "completed_at", "expires_at",
"response_body",
}).AddRow(
queueID, "ws-target", "completed", 50, 2,
"previous error", "2026-05-28T10:00:00Z", "2026-05-28T10:01:00Z", "2026-05-28T10:02:00Z", "2026-05-28T11:00:00Z",
[]byte(`{"result":"ok"}`),
))
qs, err := QueueStatusByID(context.Background(), queueID)
if err != nil {
t.Fatalf("QueueStatusByID returned error: %v", err)
}
if qs.ID != queueID {
t.Errorf("ID = %q, want %q", qs.ID, queueID)
}
if qs.Status != "completed" {
t.Errorf("Status = %q, want completed", qs.Status)
}
if qs.LastError == nil || *qs.LastError != "previous error" {
t.Errorf("LastError = %v, want 'previous error'", qs.LastError)
}
if qs.DispatchedAt == nil || *qs.DispatchedAt != "2026-05-28T10:01:00Z" {
t.Errorf("DispatchedAt = %v", qs.DispatchedAt)
}
if qs.CompletedAt == nil || *qs.CompletedAt != "2026-05-28T10:02:00Z" {
t.Errorf("CompletedAt = %v", qs.CompletedAt)
}
if qs.ExpiresAt == nil || *qs.ExpiresAt != "2026-05-28T11:00:00Z" {
t.Errorf("ExpiresAt = %v", qs.ExpiresAt)
}
if string(qs.ResponseBody) != `{"result":"ok"}` {
t.Errorf("ResponseBody = %q", qs.ResponseBody)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet expectations: %v", err)
}
}
// TestQueueStatusByID_NoRows returns sql.ErrNoRows when the queue id does not exist.
func TestQueueStatusByID_NoRows(t *testing.T) {
mock := setupTestDB(t)
queueID := "queue-missing"
mock.ExpectQuery(`SELECT\s+q\.id,\s+q\.workspace_id,\s+q\.status,\s+q\.priority,\s+q\.attempts,\s+q\.last_error,\s+q\.enqueued_at::text,\s+q\.dispatched_at::text,\s+q\.completed_at::text,\s+q\.expires_at::text,\s+al\.response_body::text\s+FROM a2a_queue q\s+LEFT JOIN activity_logs`).
WithArgs(queueID).
WillReturnRows(sqlmock.NewRows([]string{
"id", "workspace_id", "status", "priority", "attempts",
"last_error", "enqueued_at", "dispatched_at", "completed_at", "expires_at",
"response_body",
}))
_, err := QueueStatusByID(context.Background(), queueID)
if !errors.Is(err, sql.ErrNoRows) {
t.Fatalf("expected sql.ErrNoRows, got %v", err)
}
}
// TestQueueStatusByID_NullOptionals confirms that NULL dispatched_at / completed_at /
// expires_at / last_error are projected as nil pointers, and response_body is NOT
// included when status != completed.
func TestQueueStatusByID_NullOptionals(t *testing.T) {
mock := setupTestDB(t)
queueID := "queue-nulls"
mock.ExpectQuery(`SELECT\s+q\.id,\s+q\.workspace_id,\s+q\.status,\s+q\.priority,\s+q\.attempts,\s+q\.last_error,\s+q\.enqueued_at::text,\s+q\.dispatched_at::text,\s+q\.completed_at::text,\s+q\.expires_at::text,\s+al\.response_body::text\s+FROM a2a_queue q\s+LEFT JOIN activity_logs`).
WithArgs(queueID).
WillReturnRows(sqlmock.NewRows([]string{
"id", "workspace_id", "status", "priority", "attempts",
"last_error", "enqueued_at", "dispatched_at", "completed_at", "expires_at",
"response_body",
}).AddRow(
queueID, "ws-target", "queued", 50, 0,
nil, "2026-05-28T10:00:00Z", nil, nil, nil,
nil,
))
qs, err := QueueStatusByID(context.Background(), queueID)
if err != nil {
t.Fatalf("QueueStatusByID returned error: %v", err)
}
if qs.LastError != nil {
t.Errorf("LastError = %v, want nil", qs.LastError)
}
if qs.DispatchedAt != nil {
t.Errorf("DispatchedAt = %v, want nil", qs.DispatchedAt)
}
if qs.CompletedAt != nil {
t.Errorf("CompletedAt = %v, want nil", qs.CompletedAt)
}
if qs.ExpiresAt != nil {
t.Errorf("ExpiresAt = %v, want nil", qs.ExpiresAt)
}
if qs.ResponseBody != nil {
t.Errorf("ResponseBody = %q, want nil for non-completed status", qs.ResponseBody)
}
}
// TestQueueStatusByID_DBError surfaces the underlying error on unexpected failure.
func TestQueueStatusByID_DBError(t *testing.T) {
mock := setupTestDB(t)
queueID := "queue-dberr"
mock.ExpectQuery(`SELECT\s+q\.id,\s+q\.workspace_id,\s+q\.status,\s+q\.priority,\s+q\.attempts,\s+q\.last_error,\s+q\.enqueued_at::text,\s+q\.dispatched_at::text,\s+q\.completed_at::text,\s+q\.expires_at::text,\s+al\.response_body::text\s+FROM a2a_queue q\s+LEFT JOIN activity_logs`).
WithArgs(queueID).
WillReturnError(errors.New("disk full"))
_, err := QueueStatusByID(context.Background(), queueID)
if err == nil || errors.Is(err, sql.ErrNoRows) {
t.Fatalf("expected DB error, got %v", err)
}
}
@@ -12,7 +12,6 @@ package handlers
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"net/http/httptest"
@@ -521,40 +520,3 @@ func TestDrainQueueForWorkspace_ClaimGuarding_SecondDrainGetsEmpty(t *testing.T)
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
// ──────────────────────────────────────────────────────────────────────────────
// QueueDepth
// ──────────────────────────────────────────────────────────────────────────────
func TestQueueDepth_HappyPath(t *testing.T) {
mock := setupTestDBForQueueTests(t)
wsID := "ws-depth-1"
mock.ExpectQuery("SELECT COUNT(*) FROM a2a_queue WHERE workspace_id = $1 AND status = 'queued'").
WithArgs(wsID).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(7))
if got := QueueDepth(context.Background(), wsID); got != 7 {
t.Errorf("QueueDepth = %d, want 7", got)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet expectations: %v", err)
}
}
func TestQueueDepth_QueryError(t *testing.T) {
mock := setupTestDBForQueueTests(t)
wsID := "ws-depth-2"
mock.ExpectQuery("SELECT COUNT(*) FROM a2a_queue WHERE workspace_id = $1 AND status = 'queued'").
WithArgs(wsID).
WillReturnError(errors.New("conn lost"))
// Must return 0 (fail-open informational) rather than panic or propagate.
if got := QueueDepth(context.Background(), wsID); got != 0 {
t.Errorf("QueueDepth on error = %d, want 0", got)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet expectations: %v", err)
}
}
@@ -82,23 +82,18 @@ func (h *BroadcastHandler) Broadcast(c *gin.Context) {
// Find the sender's org root by walking the parent_id chain.
// Workspaces with parent_id = NULL are org roots; every other workspace
// belongs to the org identified by its topmost ancestor.
//
// NOTE: this uses the corrected CTE from org_scope.go (#1954). The old
// shape carried `id AS root_id` from the recursive seed, which caused a
// non-root sender to resolve to itself rather than its org root, making
// broadcasts under-deliver (miss the rest of the org). See #1959.
var orgRootID string
err = db.DB.QueryRowContext(ctx, `
WITH RECURSIVE org_chain AS (
SELECT id, parent_id
SELECT id, parent_id, id AS root_id
FROM workspaces
WHERE id = $1
UNION ALL
SELECT w.id, w.parent_id
SELECT w.id, w.parent_id, c.root_id
FROM workspaces w
JOIN org_chain c ON w.id = c.parent_id
)
SELECT id AS root_id FROM org_chain WHERE parent_id IS NULL LIMIT 1
SELECT root_id FROM org_chain WHERE parent_id IS NULL LIMIT 1
`, senderID).Scan(&orgRootID)
if err != nil {
log.Printf("Broadcast: org root lookup for %s: %v", senderID, err)