Skip to content

Commit 78467e5

Browse files
authored
Merge pull request #1 from webfuse-com/sync/2026-04-28
sync: 2026-04-28 — absorb 54 upstream commits
2 parents e45f39d + e4457a8 commit 78467e5

128 files changed

Lines changed: 7298 additions & 821 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/argus-ci.yml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,4 @@ jobs:
5454
5555
- name: Run unit tests
5656
working-directory: backend
57-
# test_artifacts_router.py expects lxml >= 6.1.0 behavior (xhtml is
58-
# classified as active content). Our argus branch is pinned to
59-
# upstream 898f4e8a, where uv.lock still has lxml 6.0.2; the bump
60-
# to 6.1.0 lives in upstream commit 1ca2621 which we'll absorb on
61-
# the first weekly sync. Drop --ignore once that merge lands.
62-
run: PYTHONPATH=. uv run pytest tests/ -v --ignore=tests/test_artifacts_router.py
57+
run: make test

.github/workflows/e2e-tests.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: E2E Tests
2+
3+
on:
4+
push:
5+
branches: [ 'main' ]
6+
paths:
7+
- 'frontend/**'
8+
- '.github/workflows/e2e-tests.yml'
9+
pull_request:
10+
types: [opened, synchronize, reopened, ready_for_review]
11+
paths:
12+
- 'frontend/**'
13+
- '.github/workflows/e2e-tests.yml'
14+
15+
concurrency:
16+
group: e2e-tests-${{ github.event.pull_request.number || github.ref }}
17+
cancel-in-progress: true
18+
19+
permissions:
20+
contents: read
21+
22+
jobs:
23+
e2e-tests:
24+
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
25+
runs-on: ubuntu-latest
26+
timeout-minutes: 15
27+
28+
steps:
29+
- name: Checkout
30+
uses: actions/checkout@v6
31+
32+
- name: Setup Node.js
33+
uses: actions/setup-node@v4
34+
with:
35+
node-version: '22'
36+
37+
- name: Enable Corepack
38+
run: corepack enable
39+
40+
- name: Use pinned pnpm version
41+
run: corepack prepare pnpm@10.26.2 --activate
42+
43+
- name: Install frontend dependencies
44+
working-directory: frontend
45+
run: pnpm install --frozen-lockfile
46+
47+
- name: Install Playwright Chromium
48+
working-directory: frontend
49+
run: npx playwright install chromium --with-deps
50+
51+
- name: Run E2E tests
52+
working-directory: frontend
53+
run: pnpm exec playwright test
54+
env:
55+
SKIP_ENV_VALIDATION: '1'
56+
57+
- name: Upload Playwright report
58+
uses: actions/upload-artifact@v4
59+
if: ${{ !cancelled() }}
60+
with:
61+
name: playwright-report
62+
path: frontend/playwright-report/
63+
retention-days: 7

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ coverage/
4040
skills/custom/*
4141
logs/
4242
log/
43+
debug.log
4344

4445
# Local git hooks (keep only on this machine, do not push)
4546
.githooks/
@@ -55,5 +56,7 @@ web/
5556
backend/Dockerfile.langgraph
5657
config.yaml.bak
5758
.playwright-mcp
59+
/frontend/test-results/
60+
/frontend/playwright-report/
5861
.gstack/
5962
.worktrees

.pre-commit-config.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
repos:
2+
# Backend: ruff lint + format via uv (uses the same ruff version as backend deps)
3+
- repo: local
4+
hooks:
5+
- id: ruff
6+
name: ruff lint
7+
entry: bash -c 'cd backend && uv run ruff check --fix "${@/#backend\//}"' --
8+
language: system
9+
types_or: [python]
10+
files: ^backend/
11+
- id: ruff-format
12+
name: ruff format
13+
entry: bash -c 'cd backend && uv run ruff format "${@/#backend\//}"' --
14+
language: system
15+
types_or: [python]
16+
files: ^backend/
17+
18+
# Frontend: eslint + prettier (must run from frontend/ for node_modules resolution)
19+
- repo: local
20+
hooks:
21+
- id: frontend-eslint
22+
name: eslint (frontend)
23+
entry: bash -c 'cd frontend && npx eslint --fix "${@/#frontend\//}"' --
24+
language: system
25+
types_or: [javascript, tsx, ts]
26+
files: ^frontend/
27+
28+
- id: frontend-prettier
29+
name: prettier (frontend)
30+
entry: bash -c 'cd frontend && npx prettier --write "${@/#frontend\//}"' --
31+
language: system
32+
files: ^frontend/
33+
types_or: [javascript, tsx, ts, json, css]

CONTRIBUTING.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ Required tools:
166166

167167
1. **Configure the application** (same as Docker setup above)
168168

169-
2. **Install dependencies**:
169+
2. **Install dependencies** (this also sets up pre-commit hooks):
170170
```bash
171171
make install
172172
```
@@ -300,9 +300,13 @@ Nginx (port 2026) ← Unified entry point
300300
cd backend
301301
make test
302302

303-
# Frontend tests
303+
# Frontend unit tests
304304
cd frontend
305305
make test
306+
307+
# Frontend E2E tests (requires Chromium; builds and auto-starts the Next.js production server)
308+
cd frontend
309+
make test-e2e
306310
```
307311

308312
### PR Regression Checks
@@ -311,6 +315,7 @@ Every pull request triggers the following CI workflows:
311315

312316
- **Backend unit tests**[.github/workflows/backend-unit-tests.yml](.github/workflows/backend-unit-tests.yml)
313317
- **Frontend unit tests**[.github/workflows/frontend-unit-tests.yml](.github/workflows/frontend-unit-tests.yml)
318+
- **Frontend E2E tests**[.github/workflows/e2e-tests.yml](.github/workflows/e2e-tests.yml) (triggered only when `frontend/` files change)
314319

315320
## Code Style
316321

Makefile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ help:
2323
@echo " make config - Generate local config files (aborts if config already exists)"
2424
@echo " make config-upgrade - Merge new fields from config.example.yaml into config.yaml"
2525
@echo " make check - Check if all required tools are installed"
26-
@echo " make install - Install all dependencies (frontend + backend)"
26+
@echo " make install - Install all dependencies (frontend + backend + pre-commit hooks)"
2727
@echo " make setup-sandbox - Pre-pull sandbox container image (recommended)"
2828
@echo " make dev - Start all services in development mode (with hot-reloading)"
2929
@echo " make dev-pro - Start in dev + Gateway mode (experimental, no LangGraph server)"
@@ -73,6 +73,8 @@ install:
7373
@cd backend && uv sync
7474
@echo "Installing frontend dependencies..."
7575
@cd frontend && pnpm install
76+
@echo "Installing pre-commit hooks..."
77+
@$(BACKEND_UV_RUN) --with pre-commit pre-commit install
7678
@echo "✓ All dependencies installed"
7779
@echo ""
7880
@echo "=========================================="
@@ -99,7 +101,7 @@ setup-sandbox:
99101
echo ""; \
100102
if command -v container >/dev/null 2>&1 && [ "$$(uname)" = "Darwin" ]; then \
101103
echo "Detected Apple Container on macOS, pulling image..."; \
102-
container pull "$$IMAGE" || echo "⚠ Apple Container pull failed, will try Docker"; \
104+
container image pull "$$IMAGE" || echo "⚠ Apple Container pull failed, will try Docker"; \
103105
fi; \
104106
if command -v docker >/dev/null 2>&1; then \
105107
echo "Pulling image using Docker..."; \

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ On Windows, run the local development flow from Git Bash. Native `cmd.exe` and P
264264

265265
2. **Install dependencies**:
266266
```bash
267-
make install # Install backend + frontend dependencies
267+
make install # Install backend + frontend dependencies + pre-commit hooks
268268
```
269269

270270
3. **(Optional) Pre-pull sandbox image**:

backend/app/channels/service.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@
2323
"wecom": "app.channels.wecom:WeComChannel",
2424
}
2525

26+
# Keys that indicate a user has configured credentials for a channel.
27+
_CHANNEL_CREDENTIAL_KEYS: dict[str, list[str]] = {
28+
"discord": ["bot_token"],
29+
"feishu": ["app_id", "app_secret"],
30+
"slack": ["bot_token", "app_token"],
31+
"telegram": ["bot_token"],
32+
"wecom": ["bot_id", "bot_secret"],
33+
"wechat": ["bot_token"],
34+
}
35+
2636
_CHANNELS_LANGGRAPH_URL_ENV = "DEER_FLOW_CHANNELS_LANGGRAPH_URL"
2737
_CHANNELS_GATEWAY_URL_ENV = "DEER_FLOW_CHANNELS_GATEWAY_URL"
2838

@@ -88,7 +98,16 @@ async def start(self) -> None:
8898
if not isinstance(channel_config, dict):
8999
continue
90100
if not channel_config.get("enabled", False):
91-
logger.info("Channel %s is disabled, skipping", name)
101+
cred_keys = _CHANNEL_CREDENTIAL_KEYS.get(name, [])
102+
has_creds = any(not isinstance(channel_config.get(k), bool) and channel_config.get(k) is not None and str(channel_config[k]).strip() for k in cred_keys)
103+
if has_creds:
104+
logger.warning(
105+
"Channel '%s' has credentials configured but is disabled. Set enabled: true under channels.%s in config.yaml to activate it.",
106+
name,
107+
name,
108+
)
109+
else:
110+
logger.info("Channel %s is disabled, skipping", name)
92111
continue
93112

94113
await self._start_channel(name, channel_config)

backend/app/channels/slack.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,39 @@
1616
_slack_md_converter = SlackMarkdownConverter()
1717

1818

19+
def _normalize_allowed_users(allowed_users: Any) -> set[str]:
20+
if allowed_users is None:
21+
return set()
22+
if isinstance(allowed_users, str):
23+
values = [allowed_users]
24+
elif isinstance(allowed_users, list | tuple | set):
25+
values = allowed_users
26+
else:
27+
logger.warning(
28+
"Slack allowed_users should be a list of Slack user IDs or a single Slack user ID string; treating %s as one string value",
29+
type(allowed_users).__name__,
30+
)
31+
values = [allowed_users]
32+
return {str(user_id) for user_id in values if str(user_id)}
33+
34+
1935
class SlackChannel(Channel):
2036
"""Slack IM channel using Socket Mode (WebSocket, no public IP).
2137
2238
Configuration keys (in ``config.yaml`` under ``channels.slack``):
2339
- ``bot_token``: Slack Bot User OAuth Token (xoxb-...).
2440
- ``app_token``: Slack App-Level Token (xapp-...) for Socket Mode.
25-
- ``allowed_users``: (optional) List of allowed Slack user IDs. Empty = allow all.
41+
- ``allowed_users``: (optional) List of allowed Slack user IDs, or a
42+
single Slack user ID string as shorthand. Empty = allow all. Other
43+
scalar values are treated as a single string with a warning.
2644
"""
2745

2846
def __init__(self, bus: MessageBus, config: dict[str, Any]) -> None:
2947
super().__init__(name="slack", bus=bus, config=config)
3048
self._socket_client = None
3149
self._web_client = None
3250
self._loop: asyncio.AbstractEventLoop | None = None
33-
self._allowed_users: set[str] = {str(user_id) for user_id in config.get("allowed_users", [])}
51+
self._allowed_users = _normalize_allowed_users(config.get("allowed_users", []))
3452

3553
async def start(self) -> None:
3654
if self._running:

backend/app/gateway/app.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import logging
23
from collections.abc import AsyncGenerator
34
from contextlib import asynccontextmanager
@@ -32,6 +33,11 @@
3233

3334
logger = logging.getLogger(__name__)
3435

36+
# Upper bound (seconds) each lifespan shutdown hook is allowed to run.
37+
# Bounds worker exit time so uvicorn's reload supervisor does not keep
38+
# firing signals into a worker that is stuck waiting for shutdown cleanup.
39+
_SHUTDOWN_HOOK_TIMEOUT_SECONDS = 5.0
40+
3541

3642
@asynccontextmanager
3743
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
@@ -63,11 +69,19 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
6369

6470
yield
6571

66-
# Stop channel service on shutdown
72+
# Stop channel service on shutdown (bounded to prevent worker hang)
6773
try:
6874
from app.channels.service import stop_channel_service
6975

70-
await stop_channel_service()
76+
await asyncio.wait_for(
77+
stop_channel_service(),
78+
timeout=_SHUTDOWN_HOOK_TIMEOUT_SECONDS,
79+
)
80+
except TimeoutError:
81+
logger.warning(
82+
"Channel service shutdown exceeded %.1fs; proceeding with worker exit.",
83+
_SHUTDOWN_HOOK_TIMEOUT_SECONDS,
84+
)
7185
except Exception:
7286
logger.exception("Failed to stop channel service")
7387

0 commit comments

Comments
 (0)