Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 84 additions & 3 deletions .github/workflows/ci-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,18 @@ jobs:
# skipped via `if:` reports conclusion `skipped`, which passes. Two scopes:
# `relevant` gates unit/integration; `e2e_relevant` gates e2e and skips only
# docs — the e2e image build is the sole CI check that compiles the frontend,
# and docker/** changes must be e2e-tested.
# and docker/** changes must be e2e-tested. `frontend` gates the JS tiers,
# which are the mirror image of `relevant`: only frontend/** matters to them.
changes:
runs-on: ubuntu-latest
outputs:
# Filtering is a PR-time saving only. On workflow_dispatch there is no
# base to diff, so paths-filter resolves against the default branch and a
# manual run on main diffs main against itself — every filter false, every
# tier skipped, gate green with nothing run. Force both true off-PR.
# tier skipped, gate green with nothing run. Force all true off-PR.
relevant: ${{ github.event_name != 'pull_request' || steps.filter.outputs.relevant == 'true' }}
e2e_relevant: ${{ github.event_name != 'pull_request' || steps.filter.outputs.e2e_relevant == 'true' }}
frontend: ${{ github.event_name != 'pull_request' || steps.filter.outputs.frontend == 'true' }}
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
Expand All @@ -54,6 +56,8 @@ jobs:
- "!docker/**"
- "!frontend/**"
- "!docs/**"
frontend:
- "frontend/**"
e2e_relevant:
- "!docs/**"

Expand Down Expand Up @@ -124,6 +128,78 @@ jobs:
if-no-files-found: ignore
retention-days: 14

# Frontend tiers. Runs alongside the python tiers, not after them: nothing is
# shared, and the whole job is minutes against e2e's tens of minutes.
#
# Two vitest projects, one job:
# unit — happy-dom. Fast, but every rect measures 0, so it cannot see
# layout at all.
# layout — real Chromium. Asserts the geometric contracts of list rows
# (columns line up, nothing spills onto the action icons, nothing
# overflows) at the widths the CSS claims to support. These are
# the regressions that reach staging because a CSS diff looks
# innocent in review.
frontend:
needs: changes
if: needs.changes.outputs.frontend == 'true' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: frontend
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Set up bun
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
with:
bun-version: "1.3.11"

- name: Install dependencies
# --ignore-scripts matches the frontend image build. Playwright's
# postinstall browser download is one of the scripts it skips; the
# dedicated step below does that explicitly and cacheably instead.
run: bun install --frozen-lockfile --ignore-scripts

# Keyed on the lockfile: the browser build is pinned by the playwright
# version in it, so a bump invalidates the cache and re-downloads.
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v5
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('frontend/bun.lock') }}
restore-keys: |
${{ runner.os }}-playwright-

- name: Install Chromium
# The locally installed binary, not `bunx playwright`: that would fetch
# whatever version the registry serves, while this is the one pinned in
# bun.lock and matched to @vitest/browser.
# --with-deps installs the shared libraries headless Chromium needs;
# they live outside the cached browser dir, so this runs either way.
run: ./node_modules/.bin/playwright install chromium --with-deps

- name: Run unit tests
run: bun run test:unit

- name: Run layout tests
run: bun run test:layout

- name: Upload layout failure screenshots
# Written by vitest's browser mode on failure — a red layout assertion
# is far quicker to read as a picture than as coordinates.
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: frontend-layout-screenshots
path: frontend/src/**/__screenshots__/**
if-no-files-found: ignore
retention-days: 14

# Builds images then runs e2e against the full compose platform. Gated by
# `e2e_relevant`, which unlike `relevant` keeps docker/** and frontend/** in
# scope — both are built here — and skips only docs-only changes.
Expand Down Expand Up @@ -236,7 +312,10 @@ jobs:
retention-days: 14

report:
needs: [changes, test, e2e]
# `frontend` is in `needs` for the gate below only: it emits no junit, so
# it contributes nothing to the combined report or the baseline. Without it
# a red layout assertion would leave the branch-protection check green.
needs: [changes, test, e2e, frontend]
# `always()` so a red tier still posts a report. Any tier may skip on
# irrelevant paths; the fail step below distinguishes a legitimate skip
# from a real tier failure.
Expand Down Expand Up @@ -336,7 +415,9 @@ jobs:
changes_result="${{ needs.changes.result }}"
test_result="${{ needs.test.result }}"
e2e_result="${{ needs.e2e.result }}"
frontend_result="${{ needs.frontend.result }}"
# A broken filter job skips both tiers, which would read as pass.
[ "$changes_result" = "success" ] || exit 1
[ "$test_result" = "success" ] || [ "$test_result" = "skipped" ] || exit 1
[ "$e2e_result" = "success" ] || [ "$e2e_result" = "skipped" ] || exit 1
[ "$frontend_result" = "success" ] || [ "$frontend_result" = "skipped" ] || exit 1
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -717,3 +717,6 @@ AGENTS.md
# Unstract test rig
reports/
.test-selection

# Vitest browser-mode failure screenshots (written on a red layout assertion)
frontend/src/**/__screenshots__/
41 changes: 41 additions & 0 deletions backend/utils/tests/test_user_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Regression tests for UserContext.get_organization.

Pins the no-org short-circuit: the lookup must return None without touching the
DB, so it stays evaluable on a DB-less/unmigrated setup.
"""

from __future__ import annotations

from unittest.mock import patch

from utils.user_context import UserContext


class TestGetOrganizationNoContext:
def test_returns_none_without_hitting_db(self):
with (
patch("utils.user_context.StateStore.get", return_value=None),
patch("utils.user_context.Organization.objects.get") as mock_get,
):
assert UserContext.get_organization() is None
mock_get.assert_not_called()

def test_empty_identifier_short_circuits(self):
with (
patch("utils.user_context.StateStore.get", return_value=""),
patch("utils.user_context.Organization.objects.get") as mock_get,
):
assert UserContext.get_organization() is None
mock_get.assert_not_called()

def test_identifier_present_looks_up_organization(self):
"""Pins the complement, so inverting the guard can't pass unnoticed."""
sentinel = object()
with (
patch("utils.user_context.StateStore.get", return_value="org-123"),
patch(
"utils.user_context.Organization.objects.get", return_value=sentinel
) as mock_get,
):
assert UserContext.get_organization() is sentinel
mock_get.assert_called_once_with(organization_id="org-123")
3 changes: 3 additions & 0 deletions backend/utils/user_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ def set_organization_identifier(organization_identifier: str) -> None:
@staticmethod
def get_organization() -> Organization | None:
organization_id = StateStore.get(Account.ORGANIZATION_ID)
# Skip the query so this stays evaluable on a DB-less/unmigrated setup.
if not organization_id:
return None
try:
organization: Organization = Organization.objects.get(
organization_id=organization_id
Expand Down
15 changes: 0 additions & 15 deletions docker/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ services:
- ./workflow_data:/data
- ${TOOL_REGISTRY_CONFIG_SRC_PATH}:/data/tool_registry_config
environment:
- ENVIRONMENT=development
- APPLICATION_NAME=unstract-backend
labels:
- traefik.enable=true
Expand All @@ -60,7 +59,6 @@ services:
- rabbitmq
- db
environment:
- ENVIRONMENT=development
- APPLICATION_NAME=unstract-worker-metrics

labels:
Expand All @@ -83,7 +81,6 @@ services:
- redis
- rabbitmq
environment:
- ENVIRONMENT=development
- APPLICATION_NAME=unstract-worker-ide-callback
- WORKER_TYPE=ide_callback
- WORKER_NAME=ide-callback-worker
Expand All @@ -107,7 +104,6 @@ services:
ports:
- "5555:5555"
environment:
- ENVIRONMENT=development
- APPLICATION_NAME=unstract-celery-flower
volumes:
- unstract_data:/data
Expand All @@ -129,7 +125,6 @@ services:
- db
- rabbitmq
environment:
- ENVIRONMENT=development
- APPLICATION_NAME=unstract-celery-beat

# Frontend React app
Expand All @@ -143,7 +138,6 @@ services:
- backend
- reverse-proxy
environment:
- ENVIRONMENT=development
# Running platform version, surfaced on the profile page. Reuses the same
# ${VERSION} that tags the image, so it always matches what's deployed
# (and updates on RC->stable promotion, which only retags — doesn't rebuild).
Expand Down Expand Up @@ -219,7 +213,6 @@ services:
- redis
- rabbitmq
environment:
- ENVIRONMENT=development
- APPLICATION_NAME=unstract-worker-api-deployment-v2
- WORKER_TYPE=api_deployment
- CELERY_QUEUES_API_DEPLOYMENT=${CELERY_QUEUES_API_DEPLOYMENT:-celery_api_deployments}
Expand Down Expand Up @@ -252,7 +245,6 @@ services:
- redis
- rabbitmq
environment:
- ENVIRONMENT=development
- APPLICATION_NAME=unstract-worker-callback-v2
- WORKER_TYPE=callback
- WORKER_NAME=callback-worker-v2
Expand Down Expand Up @@ -294,7 +286,6 @@ services:
- redis
- rabbitmq
environment:
- ENVIRONMENT=development
- APPLICATION_NAME=unstract-worker-file-processing-v2
- WORKER_TYPE=file_processing
- WORKER_MODE=oss
Expand Down Expand Up @@ -332,7 +323,6 @@ services:
- redis
- rabbitmq
environment:
- ENVIRONMENT=development
- APPLICATION_NAME=unstract-worker-general-v2
- WORKER_TYPE=general
- WORKER_NAME=general-worker-v2
Expand Down Expand Up @@ -360,7 +350,6 @@ services:
- redis
- rabbitmq
environment:
- ENVIRONMENT=development
- APPLICATION_NAME=unstract-worker-notification-v2
- WORKER_TYPE=notification
- WORKER_NAME=notification-worker-v2
Expand Down Expand Up @@ -409,7 +398,6 @@ services:
- redis
- rabbitmq
environment:
- ENVIRONMENT=development
- APPLICATION_NAME=unstract-worker-log-consumer-v2
- WORKER_TYPE=log_consumer
- WORKER_NAME=log-consumer-worker-v2
Expand Down Expand Up @@ -458,7 +446,6 @@ services:
- redis
- rabbitmq
environment:
- ENVIRONMENT=development
- APPLICATION_NAME=unstract-worker-log-history-scheduler-v2
# Scheduler interval in seconds
- LOG_HISTORY_CONSUMER_INTERVAL=${LOG_HISTORY_CONSUMER_INTERVAL:-5}
Expand All @@ -483,7 +470,6 @@ services:
- redis
- rabbitmq
environment:
- ENVIRONMENT=development
- APPLICATION_NAME=unstract-worker-scheduler-v2
- WORKER_TYPE=scheduler
- WORKER_NAME=scheduler-worker-v2
Expand Down Expand Up @@ -529,7 +515,6 @@ services:
- rabbitmq
- platform-service
environment:
- ENVIRONMENT=development
- APPLICATION_NAME=unstract-worker-executor-v2
- WORKER_TYPE=executor
- WORKER_NAME=executor-worker-v2
Expand Down
Loading
Loading