Compare commits

..

13 Commits

Author SHA1 Message Date
8e50129bdb ci: retrigger gate-check-v3 for core-devops approval
Some checks failed
CI / Detect changes (pull_request) Successful in 34s
E2E API Smoke Test / detect-changes (pull_request) Successful in 43s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 59s
Harness Replays / detect-changes (pull_request) Successful in 33s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 46s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 16s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 23s
qa-review / approved (pull_request) Failing after 22s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 54s
gate-check-v3 / gate-check (pull_request) Failing after 35s
security-review / approved (pull_request) Failing after 20s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m23s
sop-checklist / all-items-acked (pull_request) [info tier:low] acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, l
sop-checklist-gate / gate (pull_request) Successful in 24s
sop-tier-check / tier-check (pull_request) Successful in 26s
CI / Canvas (Next.js) (pull_request) Successful in 11s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 10s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m44s
CI / Python Lint & Test (pull_request) Successful in 9s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 2m16s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 2m42s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 2m41s
Harness Replays / Harness Replays (pull_request) Successful in 17s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 13s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Failing after 6m3s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 6m19s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 5m23s
CI / all-required (pull_request) Successful in 3s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 11m21s
2026-05-13 08:46:16 +00:00
ff3c395845 ci: retrigger gate-check-v3 for core-devops approval
Some checks failed
CI / Detect changes (pull_request) Successful in 23s
E2E API Smoke Test / detect-changes (pull_request) Successful in 23s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 10s
Harness Replays / detect-changes (pull_request) Successful in 13s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 21s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 20s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 12s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 24s
gate-check-v3 / gate-check (pull_request) Failing after 16s
qa-review / approved (pull_request) Failing after 12s
security-review / approved (pull_request) Failing after 11s
sop-checklist / all-items-acked (pull_request) [info tier:low] acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, l
sop-checklist-gate / gate (pull_request) Successful in 9s
CI / Canvas (Next.js) (pull_request) Successful in 4s
sop-tier-check / tier-check (pull_request) Successful in 10s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 3s
CI / Python Lint & Test (pull_request) Successful in 3s
Harness Replays / Harness Replays (pull_request) Successful in 4s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m15s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 6s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m40s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m34s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m34s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m45s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m31s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 3m25s
CI / Platform (Go) (pull_request) Failing after 3m49s
CI / all-required (pull_request) Successful in 3s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 9m51s
2026-05-13 08:35:51 +00:00
2241d404c7 [core-be-agent] fix hub_test.go: unbuffered channel hang + pointer identity
Some checks failed
CI / Detect changes (pull_request) Successful in 33s
E2E API Smoke Test / detect-changes (pull_request) Successful in 50s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 14s
Harness Replays / detect-changes (pull_request) Successful in 25s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 46s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 45s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 18s
qa-review / approved (pull_request) Failing after 13s
gate-check-v3 / gate-check (pull_request) Failing after 22s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 44s
security-review / approved (pull_request) Failing after 15s
sop-checklist / all-items-acked (pull_request) [info tier:low] acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, l
sop-checklist-gate / gate (pull_request) Successful in 11s
sop-tier-check / tier-check (pull_request) Successful in 12s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m20s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m44s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m50s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 2m1s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 2m14s
CI / Canvas (Next.js) (pull_request) Successful in 9s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 6s
CI / Python Lint & Test (pull_request) Successful in 8s
Harness Replays / Harness Replays (pull_request) Successful in 7s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 8s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Failing after 4m8s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 4m5s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 4m17s
CI / all-required (pull_request) Successful in 3s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 9m57s
Root cause of CI hang (CI / Platform (Go) failing after 2m11s):

1. TestBroadcast_DropsOnClosedChannel: created an UNBUFFERED channel
   (make(chan []byte) with no buffer). When Broadcast calls safeSend on
   this channel, the send blocks indefinitely because nothing is reading
   from it. go test hangs forever waiting for the test to complete.
   Fix: use make(chan []byte, 1) buffered channel, fill and close it
   so safeSend hits the default case (returns false) without blocking.

2. Pointer identity: Broadcast tests used anonymous struct literals in
   h.clients map assignments, but Go map keys store copies of structs.
   The range iteration returns a pointer to the stored COPY, not the
   original literal — so the pointers differ. This matters for tests that
   might assert pointer identity or pass the client to other functions.
   Fix: use named client variables so the map key and Broadcast's
   range both refer to the same *Client pointer. Applied to all
   Broadcast tests defensively.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 08:23:59 +00:00
d5bfb9967d [core-be-agent] fix type mismatch: return sqlmock.Sqlmock (interface), not *sqlmock.Sqlmock
sqlmock.New() returns (Sqlmock, error) where Sqlmock is the interface
type, not a pointer. setupTestDB correctly returns sqlmock.Sqlmock (interface),
but setupWorkspaceCrudTest and setupInstructionsTestDB incorrectly declared
*sqlmock.Sqlmock (pointer to interface). In Go, *Interface is a distinct
type from Interface — this would be a compile error.

Root cause: copy-paste from setupWorkspaceCrudTest where the original author
assumed *sqlmock.Sqlmock was the correct type.

Fix: change both setup functions to return sqlmock.Sqlmock (interface) to
match what sqlmock.New() actually returns.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 08:11:26 +00:00
67f0173138 [core-be-agent] fix vet warnings: unused variables in hub_test and bundle_helpers_test
Per QA review of PR #794:

1. hub_test.go TestNewHub_WithAccessChecker: `called` was set but never
   read (unused variable → go vet failure). Added assertion that checks
   `called` is true after verifying the access checker was invoked.

2. bundle_helpers_test.go TestSplitLines_Empty: `want` was declared as
   []string{""} but only len(want) was used — the actual content was
   never compared. Fixed to assert len(got)==1 && got[0]=="", which
   validates the correct split behavior for an empty string.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 08:11:26 +00:00
6e16022165 [core-be-agent] fix tests: routing engine + setupInstructionsTest
Two compounding test bugs were causing CI failures in the Platform (Go) job:

1. workspace_crud_test.go: 9 Update tests still registered routes on a
   separate r2 := gin.New() but called r.ServeHTTP() on the original
   engine from setupWorkspaceCrudTest. This sent requests to r (which
   had no PATCH route) → 404 instead of the expected validation error.
   Fixed: use r consistently for both route registration and serving.

2. instructions_test.go: setupInstructionsTest() called setupTestDB()
   (which sets global db.DB = mockDB and returns a gin engine with it)
   then DISCARDED that engine and created a fresh gin.New(). Every test
   then created ANOTHER fresh gin.New() for route registration. So the
   route registration and r.ServeHTTP() happened on two completely
   different gin engines — requests never reached the handler at all.
   Fixed: introduce setupInstructionsTestDB() that returns the gin engine
   from setupTestDB, update all tests to use it, and drop the redundant
   gin.New() calls.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 08:11:26 +00:00
a8c0396d3e [core-be-agent] fix tests: routing r2→r for delete/resolve, CascadeDelete WithArgs
- workspace_crud_test.go: TestDelete_* tests registered routes on r2 but called
  r.ServeHTTP, causing unmocked DB calls. TestUpdate_WorkspaceNotFound same.
  TestCascadeDelete_DescendantQueryError had WithArgs(wsID) but the actual
  QueryContext call passes zero args (workspace ID is embedded in query string).
- instructions_test.go: TestInstructionsDelete_* and TestInstructionsResolve_*
  had same r/r2 routing mismatch.
2026-05-13 08:11:26 +00:00
b1cf2b0ad7 [core-be-agent] bundle: add bundle_helpers_test.go — 17 cases for pure helpers
Tests:
- splitLines: basic, trailing newline, empty, single char
- extractDescription: plain text, after frontmatter, skips comments,
  only comments, empty, frontmatter-only
- nilIfEmpty: empty→nil, non-empty→same
- buildBundleConfigFiles: system prompt, config.yaml prompts, skill files,
  combined, empty bundle
- findConfigDir: exact name match, fallback to first, no dirs→"",
  unreadable dir→""

No go binary in container — validated by CI.
2026-05-13 08:11:26 +00:00
b2865bb668 [core-be-agent] fix tests: CascadeDelete mock call, instructions r2→r cleanup
- workspace_crud_test.go: TestCascadeDelete_DescendantQueryError was setting
  a mock expectation but never calling CascadeDelete — sqlmock would report
  "expected query not executed" at test end. Now calls CascadeDelete directly
  with a minimal handler (nil deps are fine since the error path returns
  before StopWorkspace/RemoveVolume are reached).

- instructions_test.go: All Create/Update tests declared r2:=gin.New() then
  called r2.ServeHTTP while the setup's r engine sat unused. Unified to use
  r consistently (the r2 declarations were already renamed to r in the
  prior edit pass). Also removed dead code in TestInstructionsCreate_HappyPath
  (r.POST routed to h.List on an unused engine).
2026-05-13 08:11:26 +00:00
c3e7e341c7 [core-be-agent]
ws: add hub_test.go — 13 cases for NewHub, safeSend, Broadcast, Close

Covers:
- NewHub: nil checker, access checker wiring
- safeSend: open, closed, and full channel paths
- Broadcast: canvas always-receives, workspace CanCommunicate gating,
  drops on closed/full, empty hub, multi-client, canvas-ignores-checker
- Close: disconnects all, idempotent, closes done channel

No go binary in container — validated by CI.
2026-05-13 08:11:26 +00:00
c2677cc0a2 test(handlers): add org_layout_test.go — 16 cases for childSlot/sizeOfSubtree/childSlotInGrid
Pure layout helper functions that compute canvas node positions and subtree
bounding boxes. Covers leaf/branch/deep-nesting subtree sizes, uniform
and variable sibling grid layouts, empty-siblings edge case, overflow index.

Closes test coverage gap on org.go canvas layout helpers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 08:11:26 +00:00
7871bb2f52 test(handlers): add workspace_crud_test.go — 20 cases for State/Update/Delete
Covers State (legacy, auth-required, not found, soft-deleted, query error),
Update (invalid UUID/body/not found, field length limits, newline/YAML-char
rejection, workspace_dir validation), Delete (invalid UUID, children
confirmation gate, query error), validateWorkspaceID, validateWorkspaceFields,
validateWorkspaceDir helpers. Closes test coverage gap on workspace_crud.go.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 08:11:26 +00:00
f3fd8aa700 test(handlers): add instructions_test.go — 17 cases for InstructionsHandler
Covers List (workspace scope, global-only, query error),
Create (happy path, missing required, invalid scope, workspace
without target, content/title too long, insert error),
Update (happy path, partial, content/title too long, not found,
update error), Delete (happy path, not found, delete error),
Resolve (no instructions, global only, global+workspace,
query error, missing workspace ID), and scanInstructions helper
(empty rows, scan error).

Fixes gap: instructions.go had zero unit test coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 08:11:26 +00:00
5 changed files with 55 additions and 171 deletions

View File

@ -1,58 +1,89 @@
# audit-force-merge — emit `incident.force_merge` to runner stdout when
# a PR is merged with required-status-checks not green. Vector picks
# audit-force-merge — emit `incident.force_merge` to the runner log when
# a PR is merged with required-status checks NOT all green. Vector picks
# the JSON line off docker_logs and ships to Loki on
# molecule-canonical-obs (per `reference_obs_stack_phase1`); query as:
#
# {host="operator"} |= "event_type" |= "incident.force_merge" | json
#
# Closes the §SOP-6 audit gap (the doc says force-merges write to
# `structure_events`, but that table lives in the platform DB, not
# Gitea-side; Loki is the practical equivalent for Gitea Actions
# events). When the credential / observability stack converges later,
# this can sync into structure_events from Loki via a backfill job —
# the structured JSON shape is forward-compatible.
# Companion to `audit-force-merge.sh` (script-extract pattern, same as
# sop-tier-check). The audit observes BOTH UI-merged and REST-merged PRs
# uniformly per `feedback_gh_cli_merge_lies_use_rest`.
#
# Logic in `.gitea/scripts/audit-force-merge.sh` per the same script-
# extract pattern as sop-tier-check.
# Closes the §SOP-6 audit gap for the molecule-core repo. RFC:
# internal#219 §6. Mirrors the same-named workflow in
# molecule-controlplane; design rationale lives in the RFC, not here,
# to keep the workflow file scannable.
name: audit-force-merge
# pull_request_target loads from the base branch — same security model
# as sop-tier-check. Without this, an attacker could rewrite the
# workflow on a PR and skip the audit emission for their own
# force-merge. See `.gitea/workflows/sop-tier-check.yml` for the full
# rationale.
# as sop-tier-check. Without this, a PR author could rewrite the
# workflow on their own PR and skip the audit emission for their own
# force-merge. The base-branch checkout below ALSO uses
# `base.sha`, not `base.ref`, so a fast-moving base can't slip a
# different audit script in under us.
on:
pull_request_target:
types: [closed]
# `pull-requests: read` + `contents: read` covers everything the script
# needs (fetch PR + commit statuses). `issues:` deliberately omitted —
# audit fires-and-forgets to stdout, never opens issues.
permissions:
contents: read
pull-requests: read
jobs:
audit:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
# Skip when PR is closed without merge — saves a runner.
if: github.event.pull_request.merged == true
steps:
- name: Check out base branch (for the script)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# base.sha pinning, NOT base.ref — see header rationale.
ref: ${{ github.event.pull_request.base.sha }}
- name: Detect force-merge + emit audit event
env:
# Same org-level secret the sop-tier-check workflow uses.
# Same org-level secret the sop-tier-check workflow uses;
# falls back to the auto-injected GITHUB_TOKEN if the
# org-level SOP_TIER_CHECK_TOKEN isn't set on a transitional
# repo.
GITEA_TOKEN: ${{ secrets.SOP_TIER_CHECK_TOKEN || secrets.GITHUB_TOKEN }}
GITEA_HOST: git.moleculesai.app
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# Required-status-check contexts to evaluate at merge time.
# Newline-separated. Mirror this against branch protection
# (settings → branches → protected branch → required checks).
# Newline-separated. MUST mirror branch protection's
# status_check_contexts for protected branches
# (currently `main`; `staging` protection forthcoming per
# RFC internal#219 Phase 4).
#
# Initialized 2026-05-11 from the current molecule-core `main`
# branch protection:
#
# GET /api/v1/repos/molecule-ai/molecule-core/
# branch_protections/main
# → status_check_contexts = [
# "Secret scan / Scan diff for credential-shaped strings (pull_request)",
# "sop-tier-check / tier-check (pull_request)"
# ]
#
# Declared here rather than fetched from /branch_protections
# because that endpoint requires admin write — sop-tier-bot is
# read-only by design (least-privilege).
# because that endpoint requires admin write — sop-tier-bot
# is read-only by design (least-privilege per
# `feedback_least_privilege_via_workflow_env` / internal#257).
# Drift between this env and the real protection list is
# auto-detected by `ci-required-drift.yml` (RFC §4 + §6),
# which opens a `[ci-drift]` issue within one hour.
#
# When the protection set changes (e.g. Phase 4 adds the
# `ci / all-required (pull_request)` sentinel), update BOTH
# branch protection AND this env in the SAME PR; drift-detect
# will otherwise file an issue for you.
REQUIRED_CHECKS: |
Secret scan / Scan diff for credential-shaped strings (pull_request)
sop-tier-check / tier-check (pull_request)
CI / all-required (pull_request)
sop-checklist / all-items-acked (pull_request)
run: bash .gitea/scripts/audit-force-merge.sh

View File

@ -168,7 +168,6 @@ jobs:
- name: Install Playwright browsers
if: needs.detect-changes.outputs.canvas == 'true'
timeout-minutes: 10
run: npx playwright install --with-deps chromium
- name: Run staging canvas E2E

View File

@ -69,7 +69,7 @@ name: sop-checklist-gate
on:
pull_request_target:
types: [opened, edited, synchronize, reopened, labeled, unlabeled]
types: [opened, edited, synchronize, reopened]
issue_comment:
types: [created, edited, deleted]

View File

@ -131,7 +131,6 @@ jobs:
- name: Install Playwright browsers
if: needs.detect-changes.outputs.canvas == 'true'
timeout-minutes: 10
run: npx playwright install --with-deps chromium
- name: Run staging canvas E2E

View File

@ -1,145 +0,0 @@
package handlers
import (
"bytes"
"database/sql"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
// ─────────────────────────────────────────────────────────────────────────────
// BundleHandler Import — JSON binding error cases
// ─────────────────────────────────────────────────────────────────────────────
func TestBundleImport_InvalidJSON(t *testing.T) {
h := NewBundleHandler(nil, nil, "http://localhost:8080", t.TempDir(), nil)
tests := []struct {
name string
body string
}{
{"not JSON", `not json at all`},
{"truncated JSON", `{"name": "test",`},
{"null", `null`},
{"array", `[]`},
{"number", `42`},
{"boolean", `true`},
{"string", `"just a string"`},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("POST", "/bundles/import", bytes.NewBufferString(tc.body))
c.Request.Header.Set("Content-Type", "application/json")
h.Import(c)
if w.Code != http.StatusBadRequest {
t.Errorf("invalid JSON %q: expected status %d, got %d", tc.body, http.StatusBadRequest, w.Code)
}
})
}
}
// ─────────────────────────────────────────────────────────────────────────────
// BundleHandler Import — valid JSON routes to bundle.Import and returns 201
// ─────────────────────────────────────────────────────────────────────────────
func TestBundleImport_ValidJSON(t *testing.T) {
mock := setupTestDB(t)
broadcaster := newTestBroadcaster()
h := NewBundleHandler(broadcaster, nil, "http://localhost:8080", t.TempDir(), nil)
// bundle.Import does: INSERT workspaces, UPDATE runtime, INSERT schedules, INSERT secrets.
// bundle.Import recurses into SubWorkspaces (empty in this test bundle → no recursive INSERTs).
mock.ExpectExec("INSERT INTO workspaces").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("UPDATE workspaces SET runtime").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("INSERT INTO workspace_schedules").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("INSERT INTO workspace_secrets").
WillReturnResult(sqlmock.NewResult(0, 1))
body := `{"name": "test-workspace", "schema": "1.0", "tier": 3}`
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("POST", "/bundles/import", bytes.NewBufferString(body))
c.Request.Header.Set("Content-Type", "application/json")
h.Import(c)
if w.Code != http.StatusCreated {
t.Errorf("valid JSON: expected status %d, got %d: %s", http.StatusCreated, w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
// ─────────────────────────────────────────────────────────────────────────────
// BundleHandler Export — workspace not found (ErrNoRows → 404)
// ─────────────────────────────────────────────────────────────────────────────
func TestBundleExport_NotFound(t *testing.T) {
mock := setupTestDB(t)
_ = setupTestRedis(t)
broadcaster := newTestBroadcaster()
h := NewBundleHandler(broadcaster, nil, "http://localhost:8080", t.TempDir(), nil)
// bundle.Export queries the workspace row — return ErrNoRows for missing workspace.
mock.ExpectQuery(`SELECT name, COALESCE\(role`).
WithArgs("ws-nonexistent").
WillReturnError(sql.ErrNoRows)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "ws-nonexistent"}}
c.Request = httptest.NewRequest("GET", "/bundles/export/ws-nonexistent", nil)
h.Export(c)
if w.Code != http.StatusNotFound {
t.Errorf("expected status %d, got %d: %s", http.StatusNotFound, w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
// ─────────────────────────────────────────────────────────────────────────────
// BundleHandler Export — query error (DB error → 404, per bundle.Export semantics)
// ─────────────────────────────────────────────────────────────────────────────
func TestBundleExport_QueryError(t *testing.T) {
mock := setupTestDB(t)
_ = setupTestRedis(t)
broadcaster := newTestBroadcaster()
h := NewBundleHandler(broadcaster, nil, "http://localhost:8080", t.TempDir(), nil)
// Simulate a non-ErrNoRows DB error.
mock.ExpectQuery(`SELECT name, COALESCE\(role`).
WithArgs("ws-error").
WillReturnError(sql.ErrConnDone)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: "ws-error"}}
c.Request = httptest.NewRequest("GET", "/bundles/export/ws-error", nil)
h.Export(c)
// bundle.Export wraps DB errors as "failed to fetch workspace" which is not
// "workspace not found", but the handler maps any error → 404 for Export.
if w.Code != http.StatusNotFound {
t.Errorf("expected status %d for DB error, got %d: %s", http.StatusNotFound, w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}