From 7c1ac38b9fb2da28b5e4102d33ed6967bb198aca Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Thu, 26 Feb 2026 12:20:21 -0500 Subject: [PATCH 1/3] Add agents.md --- .claude/.gitignore | 4 ++ .claude/CLAUDE.md | 1 + .claude/settings.json | 34 ++++++++++++ AGENTS.md | 126 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 .claude/.gitignore create mode 100644 .claude/CLAUDE.md create mode 100644 .claude/settings.json create mode 100644 AGENTS.md diff --git a/.claude/.gitignore b/.claude/.gitignore new file mode 100644 index 0000000..3a2f7f6 --- /dev/null +++ b/.claude/.gitignore @@ -0,0 +1,4 @@ +CLAUDE.local.md +settings.local.json +worktrees/ +plans/ diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..dba71e9 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1 @@ +@../AGENTS.md diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..383468b --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,34 @@ +{ + "permissions": { + "allow": [ + "Bash(ruff check:*)", + "Bash(ruff format:*)", + "Bash(pytest:*)", + "Bash(pip install:*)", + "Bash(python3 app.py:*)", + "Bash(slack run:*)", + "Bash(gh issue view:*)", + "Bash(gh label list:*)", + "Bash(gh pr checks:*)", + "Bash(gh pr diff:*)", + "Bash(gh pr list:*)", + "Bash(gh pr status:*)", + "Bash(gh pr update-branch:*)", + "Bash(gh pr view:*)", + "Bash(gh search code:*)", + "Bash(git diff:*)", + "Bash(git grep:*)", + "Bash(git log:*)", + "Bash(git show:*)", + "Bash(git status:*)", + "Bash(grep:*)", + "Bash(ls:*)", + "Bash(tree:*)", + "WebFetch(domain:github.com)", + "WebFetch(domain:raw.githubusercontent.com)", + "WebFetch(domain:docs.slack.dev)", + "WebFetch(domain:api.slack.com)", + "Bash(gh api:*)" + ] + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9d32877 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,126 @@ +# AGENTS.md — bolt-python-search-template + +## Project Overview + +This is a Slack sample app demonstrating **Enterprise Search** using [Bolt for Python](https://docs.slack.dev/bolt-python). It serves as a template for building search integrations that appear natively in Slack's search experience. + +- **Repo:** [slack-samples/bolt-python-search-template](https://github.com/slack-samples/bolt-python-search-template) +- **README:** [README.md](./README.md) — setup instructions, environment variables, and project structure overview +- **Docs:** [Enterprise Search](https://docs.slack.dev/surfaces/search) +- **Framework:** Bolt for Python (`slack-bolt`) +- **Runtime:** Python 3.11+, Socket Mode + +## Architecture + +The app registers two **custom functions** (`search` and `filters`) and one **event listener** (`entity_details_requested`) with Slack. These are declared in `manifest.json` and wired up via the listener registration pattern. + +### Listener Registration Pattern + +`app.py` creates the Bolt `App` and calls `register_listeners(app)` from `listeners/__init__.py`. This delegates to sub-modules: + +- `listeners/functions/__init__.py` — registers `search` and `filters` function callbacks via `app.function()` +- `listeners/events/__init__.py` — registers the `entity_details_requested` event callback via `app.event()` + +### Data Flow + +1. **Filters request** → `listeners/functions/filters.py` returns predefined filter definitions (languages, templates, samples) from `listeners/filters.py` +2. **Search request** → `listeners/functions/search.py` calls `fetch_sample_data()` from `listeners/sample_data_service.py`, which hits the `developer.sampleData.get` Slack API, then maps results to `SearchResult` typed dicts +3. **Entity details** → `listeners/events/entity_details_requested.py` fetches sample data and presents entity details via `entity.presentDetails` API + +## Key Files & Directories + +``` +app.py # Entry point — creates App, registers listeners, starts SocketModeHandler +manifest.json # Slack app manifest — defines functions, events, scopes, features +pyproject.toml # Build config, dependencies, ruff + pytest settings +listeners/ + __init__.py # register_listeners(app) — delegates to functions/ and events/ + filters.py # Filter/FilterOptions dataclasses and predefined filter constants + sample_data_service.py # fetch_sample_data() — calls developer.sampleData.get API + functions/ + __init__.py # Registers search and filters function callbacks + search.py # search_step_callback — handles search function execution + filters.py # filters_step_callback — returns available search filters + events/ + __init__.py # Registers entity_details_requested event callback + entity_details_requested.py # Handles rich preview entity detail requests +tests/ # Mirrors listeners/ structure + listeners/ + test_sample_data_service.py + functions/ + test_search.py + test_filters.py + events/ + test_entity_details_requested.py +.github/workflows/ + lint.yml # Ruff linting on push/PR + tests.yml # pytest on Python 3.11, 3.12, 3.13 + dependencies.yml # Dependabot auto-merge +``` + +## Development Commands + +```bash +# Install dependencies (editable mode) +pip install -e . + +# Run the app (Socket Mode) +python3 app.py +# Or via Slack CLI +slack run + +# Lint +ruff check + +# Format +ruff format +ruff check --fix + +# Run tests +pytest . +``` + +## Code Style & Tooling + +- **Linter/Formatter:** Ruff (configured in `pyproject.toml`) + - Line length: 125 + - Rules: `E` (pycodestyle errors), `W` (pycodestyle warnings), `F` (Pyflakes), `I` (isort) +- **Testing:** pytest (test paths: `tests/`, logs to `logs/pytest.log`) +- **Python:** 3.11+ required + +## CI Pipeline + +| Workflow | File | What it does | +|---|---|---| +| Lint | `.github/workflows/lint.yml` | Runs `ruff check` on Python 3.13 | +| Tests | `.github/workflows/tests.yml` | Runs `pytest .` on Python 3.11, 3.12, 3.13 | +| Dependencies | `.github/workflows/dependencies.yml` | Dependabot auto-merge for patch/minor updates | + +## Test Patterns + +- Tests use `pytest` with class-based organization (e.g., `TestSearch`) +- `setup_method` creates `MagicMock` instances for Bolt primitives (`Ack`, `Complete`, `Fail`, `WebClient`, `Logger`) +- External calls (e.g., `fetch_sample_data`) are patched with `@patch` decorators +- Tests verify: correct arguments passed to service calls, expected outputs, error handling paths, and that `ack()` is always called + +## Common Contribution Workflows + +### Adding a new filter + +1. Define the filter constant in `listeners/filters.py` using the `Filter` dataclass +2. Add it to the `complete(outputs=...)` list in `listeners/functions/filters.py` +3. If it affects search, handle it in `listeners/sample_data_service.py`'s filter processing +4. Add tests in `tests/listeners/functions/test_filters.py` + +### Modifying search results + +1. Update `SearchResult` TypedDict in `listeners/functions/search.py` if adding fields +2. Update the result mapping in `search_step_callback` +3. Update tests in `tests/listeners/functions/test_search.py` + +### Fixing a bug + +1. Run `pytest .` to confirm current test state +2. Write a failing test that reproduces the bug +3. Fix the bug +4. Run `ruff check` and `pytest .` to verify From 2a1621882c76d62b3b70c87f11ea7ef72f435974 Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Thu, 26 Feb 2026 14:29:00 -0500 Subject: [PATCH 2/3] fix: simplifications and improvements --- .claude/settings.json | 21 ++++---- AGENTS.md | 25 +++++----- listeners/events/entity_details_requested.py | 4 +- listeners/filters.py | 52 +++++--------------- listeners/functions/__init__.py | 4 +- listeners/functions/filters.py | 26 +++------- listeners/functions/search.py | 49 +++--------------- listeners/sample_data_service.py | 16 +++--- tests/listeners/functions/test_filters.py | 5 +- tests/listeners/functions/test_search.py | 13 ++--- tests/listeners/test_sample_data_service.py | 19 ++++--- 11 files changed, 81 insertions(+), 153 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 383468b..af929c7 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,12 +1,8 @@ { "permissions": { "allow": [ - "Bash(ruff check:*)", - "Bash(ruff format:*)", - "Bash(pytest:*)", - "Bash(pip install:*)", - "Bash(python3 app.py:*)", - "Bash(slack run:*)", + "Bash(echo $VIRTUAL_ENV)", + "Bash(gh api:*)", "Bash(gh issue view:*)", "Bash(gh label list:*)", "Bash(gh pr checks:*)", @@ -16,19 +12,24 @@ "Bash(gh pr update-branch:*)", "Bash(gh pr view:*)", "Bash(gh search code:*)", + "Bash(git branch:*)", "Bash(git diff:*)", + "Bash(git fetch:*)", "Bash(git grep:*)", "Bash(git log:*)", "Bash(git show:*)", "Bash(git status:*)", "Bash(grep:*)", "Bash(ls:*)", + "Bash(pip install:*)", + "Bash(pytest:*)", + "Bash(ruff check:*)", + "Bash(ruff format:*)", "Bash(tree:*)", - "WebFetch(domain:github.com)", - "WebFetch(domain:raw.githubusercontent.com)", - "WebFetch(domain:docs.slack.dev)", "WebFetch(domain:api.slack.com)", - "Bash(gh api:*)" + "WebFetch(domain:docs.slack.dev)", + "WebFetch(domain:github.com)", + "WebFetch(domain:raw.githubusercontent.com)" ] } } diff --git a/AGENTS.md b/AGENTS.md index 9d32877..dc57eca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ This is a Slack sample app demonstrating **Enterprise Search** using [Bolt for P - **README:** [README.md](./README.md) — setup instructions, environment variables, and project structure overview - **Docs:** [Enterprise Search](https://docs.slack.dev/surfaces/search) - **Framework:** Bolt for Python (`slack-bolt`) -- **Runtime:** Python 3.11+, Socket Mode +- **Runtime:** Python (see `requires-python` in `pyproject.toml`), Socket Mode ## Architecture @@ -24,7 +24,7 @@ The app registers two **custom functions** (`search` and `filters`) and one **ev ### Data Flow 1. **Filters request** → `listeners/functions/filters.py` returns predefined filter definitions (languages, templates, samples) from `listeners/filters.py` -2. **Search request** → `listeners/functions/search.py` calls `fetch_sample_data()` from `listeners/sample_data_service.py`, which hits the `developer.sampleData.get` Slack API, then maps results to `SearchResult` typed dicts +2. **Search request** → `listeners/functions/search.py` calls `fetch_sample_data()` from `listeners/sample_data_service.py`, which hits the `developer.sampleData.get` Slack API, then passes samples through as search results 3. **Entity details** → `listeners/events/entity_details_requested.py` fetches sample data and presents entity details via `entity.presentDetails` API ## Key Files & Directories @@ -35,7 +35,7 @@ manifest.json # Slack app manifest — defines functions, pyproject.toml # Build config, dependencies, ruff + pytest settings listeners/ __init__.py # register_listeners(app) — delegates to functions/ and events/ - filters.py # Filter/FilterOptions dataclasses and predefined filter constants + filters.py # Predefined filter constants (plain dicts) sample_data_service.py # fetch_sample_data() — calls developer.sampleData.get API functions/ __init__.py # Registers search and filters function callbacks @@ -60,6 +60,10 @@ tests/ # Mirrors listeners/ structure ## Development Commands +> **IMPORTANT: A Python virtual environment MUST be activated before running ANY command in this project.** +> Before executing any command below, first verify the virtual environment is active by running `echo $VIRTUAL_ENV` — it MUST output a path. +> If it outputs nothing, do NOT proceed. Ask the user to activate a virtual environment first. + ```bash # Install dependencies (editable mode) pip install -e . @@ -82,11 +86,9 @@ pytest . ## Code Style & Tooling -- **Linter/Formatter:** Ruff (configured in `pyproject.toml`) - - Line length: 125 - - Rules: `E` (pycodestyle errors), `W` (pycodestyle warnings), `F` (Pyflakes), `I` (isort) -- **Testing:** pytest (test paths: `tests/`, logs to `logs/pytest.log`) -- **Python:** 3.11+ required +- **Linter/Formatter:** Ruff (see `[tool.ruff]` in `pyproject.toml` for line length, lint rules, and other settings) +- **Testing:** pytest (see `[tool.pytest.ini_options]` in `pyproject.toml` for test paths and logging config) +- **Python:** see `requires-python` in `pyproject.toml` ## CI Pipeline @@ -107,16 +109,15 @@ pytest . ### Adding a new filter -1. Define the filter constant in `listeners/filters.py` using the `Filter` dataclass +1. Define the filter constant in `listeners/filters.py` as a plain dict 2. Add it to the `complete(outputs=...)` list in `listeners/functions/filters.py` 3. If it affects search, handle it in `listeners/sample_data_service.py`'s filter processing 4. Add tests in `tests/listeners/functions/test_filters.py` ### Modifying search results -1. Update `SearchResult` TypedDict in `listeners/functions/search.py` if adding fields -2. Update the result mapping in `search_step_callback` -3. Update tests in `tests/listeners/functions/test_search.py` +1. Update `search_step_callback` in `listeners/functions/search.py` +2. Update tests in `tests/listeners/functions/test_search.py` ### Fixing a bug diff --git a/listeners/events/entity_details_requested.py b/listeners/events/entity_details_requested.py index 7147d5c..ab19ba8 100644 --- a/listeners/events/entity_details_requested.py +++ b/listeners/events/entity_details_requested.py @@ -61,9 +61,9 @@ def entity_details_requested_callback(event: dict, client: WebClient, logger: lo json=payload, ) except SlackResponseError as e: - logger.error(f"Failed to fetch or parse sample data. Error details: {str(e)}", exc_info=e) + logger.error(f"Failed to fetch or parse sample data. Error details: {e}", exc_info=e) except Exception as e: logger.error( - f"An unexpected error occurred handling entity_details_requested event: {type(e).__name__} - {str(e)}", + f"An unexpected error occurred handling entity_details_requested event: {type(e).__name__} - {e}", exc_info=e, ) diff --git a/listeners/filters.py b/listeners/filters.py index ae6c186..7dac041 100644 --- a/listeners/filters.py +++ b/listeners/filters.py @@ -1,42 +1,16 @@ -from dataclasses import dataclass -from enum import Enum -from typing import List, Optional - - -class FilterType(Enum): - MULTI_SELECT = "multi_select" - TOGGLE = "toggle" - - -@dataclass -class FilterOptions: - name: str - value: str - - -@dataclass -class Filter: - name: str - display_name: str - type: FilterType - display_name_plural: Optional[str] = None - options: Optional[List[FilterOptions]] = None - - -LANGUAGES_FILTER = Filter( - name="languages", - display_name="Language", - display_name_plural="Languages", - type=FilterType.MULTI_SELECT.value, - options=[ - FilterOptions(name="Python", value="python"), - FilterOptions(name="Java", value="java"), - FilterOptions(name="JavaScript", value="javascript"), - FilterOptions(name="TypeScript", value="typescript"), +LANGUAGES_FILTER = { + "name": "languages", + "display_name": "Language", + "display_name_plural": "Languages", + "type": "multi_select", + "options": [ + {"name": "Python", "value": "python"}, + {"name": "Java", "value": "java"}, + {"name": "JavaScript", "value": "javascript"}, + {"name": "TypeScript", "value": "typescript"}, ], -) - -TEMPLATES_FILTER = Filter(name="template", display_name="Templates", type=FilterType.TOGGLE.value) +} +TEMPLATES_FILTER = {"name": "template", "display_name": "Templates", "type": "toggle"} -SAMPLES_FILTER = Filter(name="sample", display_name="Samples", type=FilterType.TOGGLE.value) +SAMPLES_FILTER = {"name": "sample", "display_name": "Samples", "type": "toggle"} diff --git a/listeners/functions/__init__.py b/listeners/functions/__init__.py index 31aff27..8abbcbc 100644 --- a/listeners/functions/__init__.py +++ b/listeners/functions/__init__.py @@ -5,5 +5,5 @@ def register(app: App): - app.function("search", auto_acknowledge=False)(search_step_callback) - app.function("filters", auto_acknowledge=False)(filters_step_callback) + app.function("search", auto_acknowledge=False, ack_timeout=10)(search_step_callback) + app.function("filters", auto_acknowledge=False, ack_timeout=10)(filters_step_callback) diff --git a/listeners/functions/filters.py b/listeners/functions/filters.py index 1904370..13b4739 100644 --- a/listeners/functions/filters.py +++ b/listeners/functions/filters.py @@ -1,38 +1,24 @@ import logging -from dataclasses import asdict -from typing import Dict from slack_bolt import Ack, Complete, Fail from listeners.filters import LANGUAGES_FILTER, SAMPLES_FILTER, TEMPLATES_FILTER -FILTER_PROCESSING_ERROR_MSG = ( - "We encountered an issue processing filter results. Please try again or contact the app owner if the problem persists." -) - -def filter_none(items: Dict): - return {k: v for k, v in items if v is not None} - def filters_step_callback(ack: Ack, inputs: dict, fail: Fail, complete: Complete, logger: logging.Logger): try: user_context = inputs.get("user_context", {}) logger.debug(f"User {user_context.get('id')} executing filter request") - complete( - outputs={ - "filters": [ - asdict(LANGUAGES_FILTER, dict_factory=filter_none), - asdict(TEMPLATES_FILTER, dict_factory=filter_none), - asdict(SAMPLES_FILTER, dict_factory=filter_none), - ] - } - ) + complete(outputs={"filters": [LANGUAGES_FILTER, TEMPLATES_FILTER, SAMPLES_FILTER]}) except Exception as e: logger.error( - f"Unexpected error occurred while processing filter request: {type(e).__name__} - {str(e)}", + f"Unexpected error occurred while processing filter request: {type(e).__name__} - {e}", exc_info=e, ) - fail(error=FILTER_PROCESSING_ERROR_MSG) + fail( + error="We encountered an issue processing filter results. " + "Please try again or contact the app owner if the problem persists." + ) finally: ack() diff --git a/listeners/functions/search.py b/listeners/functions/search.py index 0e3883d..db60ac7 100644 --- a/listeners/functions/search.py +++ b/listeners/functions/search.py @@ -1,30 +1,10 @@ import logging -from typing import List, NotRequired, Optional, TypedDict from slack_bolt import Ack, Complete, Fail from slack_sdk import WebClient from listeners.sample_data_service import SlackResponseError, fetch_sample_data -SEARCH_PROCESSING_ERROR_MSG = ( - "We encountered an issue processing your search results. " - "Please try again or contact the app owner if the problem persists." -) - - -class EntityReference(TypedDict): - id: str - type: Optional[str] - - -class SearchResult(TypedDict): - title: str - description: str - link: str - date_updated: str - external_ref: EntityReference - content: NotRequired[str] - def search_step_callback( ack: Ack, @@ -42,27 +22,14 @@ def search_step_callback( samples = response.get("samples", []) - results: List[SearchResult] = [ - { - "title": sample["title"], - "description": sample["description"], - "link": sample["link"], - "date_updated": sample["date_updated"], - "external_ref": sample["external_ref"], - **({"content": sample["content"]} if "content" in sample else {}), - } - for sample in samples - ] - - complete(outputs={"search_results": results}) + complete(outputs={"search_results": samples}) + except SlackResponseError as e: + logger.error(f"Failed to fetch or parse sample data. Error details: {e}", exc_info=e) + fail( + error="We encountered an issue processing your search results. " + "Please try again or contact the app owner if the problem persists." + ) except Exception as e: - if isinstance(e, SlackResponseError): - logger.error(f"Failed to fetch or parse sample data. Error details: {str(e)}", exc_info=e) - fail(error=SEARCH_PROCESSING_ERROR_MSG) - else: - logger.error( - f"Unexpected error occurred while processing search request: {type(e).__name__} - {str(e)}", - exc_info=e, - ) + logger.error(f"Unexpected error processing search request: {type(e).__name__} - {e}", exc_info=e) finally: ack() diff --git a/listeners/sample_data_service.py b/listeners/sample_data_service.py index b61bdb3..d1d7c5a 100644 --- a/listeners/sample_data_service.py +++ b/listeners/sample_data_service.py @@ -8,9 +8,7 @@ class SlackResponseError(Exception): - def __init__(self, message: str): - super().__init__(message) - self.name = "SlackResponseError" + pass def fetch_sample_data(client: WebClient, query: str = None, filters: dict = None, logger: logging.Logger = None): @@ -19,18 +17,18 @@ def fetch_sample_data(client: WebClient, query: str = None, filters: dict = None if filters: selected_filters = {} - languages = filters.get(LANGUAGES_FILTER.name, []) - templates = filters.get(TEMPLATES_FILTER.name, False) - samples = filters.get(SAMPLES_FILTER.name, False) + languages = filters.get(LANGUAGES_FILTER["name"], []) + templates = filters.get(TEMPLATES_FILTER["name"], False) + samples = filters.get(SAMPLES_FILTER["name"], False) if languages: - selected_filters[LANGUAGES_FILTER.name] = languages + selected_filters[LANGUAGES_FILTER["name"]] = languages if templates ^ samples: if templates: - selected_filters["type"] = TEMPLATES_FILTER.name + selected_filters["type"] = TEMPLATES_FILTER["name"] elif samples: - selected_filters["type"] = SAMPLES_FILTER.name + selected_filters["type"] = SAMPLES_FILTER["name"] if selected_filters: params["filters"] = selected_filters diff --git a/tests/listeners/functions/test_filters.py b/tests/listeners/functions/test_filters.py index f55b6b5..c6a3687 100644 --- a/tests/listeners/functions/test_filters.py +++ b/tests/listeners/functions/test_filters.py @@ -2,7 +2,7 @@ from slack_bolt import Ack, Complete, Fail -from listeners.functions.filters import FILTER_PROCESSING_ERROR_MSG, filters_step_callback +from listeners.functions.filters import filters_step_callback class TestFilters: @@ -84,7 +84,4 @@ def test_filters_step_callback_unexpected_exception(self): ) self.mock_fail.assert_called_once() - call_args = self.mock_fail.call_args - assert call_args.kwargs["error"] == FILTER_PROCESSING_ERROR_MSG - self.mock_ack.assert_called_once() diff --git a/tests/listeners/functions/test_search.py b/tests/listeners/functions/test_search.py index 97b39c9..fbef560 100644 --- a/tests/listeners/functions/test_search.py +++ b/tests/listeners/functions/test_search.py @@ -4,7 +4,7 @@ from slack_sdk import WebClient from listeners.filters import LANGUAGES_FILTER, SAMPLES_FILTER, TEMPLATES_FILTER -from listeners.functions.search import SEARCH_PROCESSING_ERROR_MSG, search_step_callback +from listeners.functions.search import search_step_callback from listeners.sample_data_service import SlackResponseError @@ -41,7 +41,7 @@ def setup_method(self): def test_search_step_callback_success(self, mock_fetch_sample_data): mock_fetch_sample_data.return_value = self.mock_sample_data - filters = {LANGUAGES_FILTER.name: ["python"]} + filters = {LANGUAGES_FILTER["name"]: ["python"]} inputs = {"query": "test query", "filters": filters} @@ -74,7 +74,11 @@ def test_search_step_callback_success(self, mock_fetch_sample_data): def test_search_step_callback_multiple_filter_types(self, mock_fetch_sample_data): mock_fetch_sample_data.return_value = self.mock_sample_data - filters = {TEMPLATES_FILTER.name: True, SAMPLES_FILTER.name: True, LANGUAGES_FILTER.name: ["python", "javascript"]} + filters = { + TEMPLATES_FILTER["name"]: True, + SAMPLES_FILTER["name"]: True, + LANGUAGES_FILTER["name"]: ["python", "javascript"], + } inputs = {"query": "test query", "filters": filters} @@ -135,9 +139,6 @@ def test_search_step_callback_slack_response_error(self, mock_fetch_sample_data) ) self.mock_fail.assert_called_once() - call_args = self.mock_fail.call_args - assert call_args.kwargs["error"] == SEARCH_PROCESSING_ERROR_MSG - self.mock_complete.assert_not_called() self.mock_ack.assert_called_once() diff --git a/tests/listeners/test_sample_data_service.py b/tests/listeners/test_sample_data_service.py index 06d543e..a69e40e 100644 --- a/tests/listeners/test_sample_data_service.py +++ b/tests/listeners/test_sample_data_service.py @@ -48,7 +48,7 @@ def test_fetch_sample_data_with_query(self): assert result == self.mock_response def test_fetch_sample_data_with_languages_filter(self): - filters = {LANGUAGES_FILTER.name: ["python", "javascript"]} + filters = {LANGUAGES_FILTER["name"]: ["python", "javascript"]} result = fetch_sample_data(client=self.mock_client, query="test query", filters=filters, logger=self.mock_logger) @@ -57,41 +57,44 @@ def test_fetch_sample_data_with_languages_filter(self): assert result == self.mock_response def test_fetch_sample_data_with_templates_filter(self): - filters = {TEMPLATES_FILTER.name: True} + filters = {TEMPLATES_FILTER["name"]: True} result = fetch_sample_data(client=self.mock_client, query="test query", filters=filters, logger=self.mock_logger) self.mock_client.api_call.assert_called_once_with( - API_METHOD, params={"query": "test query", "filters": {"type": TEMPLATES_FILTER.name}} + API_METHOD, params={"query": "test query", "filters": {"type": TEMPLATES_FILTER["name"]}} ) assert result == self.mock_response def test_fetch_sample_data_with_samples_filter(self): - filters = {SAMPLES_FILTER.name: True} + filters = {SAMPLES_FILTER["name"]: True} result = fetch_sample_data(client=self.mock_client, query="test query", filters=filters, logger=self.mock_logger) self.mock_client.api_call.assert_called_once_with( - API_METHOD, params={"query": "test query", "filters": {"type": SAMPLES_FILTER.name}} + API_METHOD, params={"query": "test query", "filters": {"type": SAMPLES_FILTER["name"]}} ) assert result == self.mock_response def test_fetch_sample_data_with_combined_filters(self): - filters = {LANGUAGES_FILTER.name: ["python"], TEMPLATES_FILTER.name: True} + filters = {LANGUAGES_FILTER["name"]: ["python"], TEMPLATES_FILTER["name"]: True} result = fetch_sample_data(client=self.mock_client, query="test query", filters=filters, logger=self.mock_logger) self.mock_client.api_call.assert_called_once_with( API_METHOD, - params={"query": "test query", "filters": {LANGUAGES_FILTER.name: ["python"], "type": TEMPLATES_FILTER.name}}, + params={ + "query": "test query", + "filters": {LANGUAGES_FILTER["name"]: ["python"], "type": TEMPLATES_FILTER["name"]}, + }, ) assert result == self.mock_response def test_fetch_sample_data_with_both_template_and_sample(self): - filters = {TEMPLATES_FILTER.name: True, SAMPLES_FILTER.name: True} + filters = {TEMPLATES_FILTER["name"]: True, SAMPLES_FILTER["name"]: True} result = fetch_sample_data(client=self.mock_client, query="test query", filters=filters, logger=self.mock_logger) From ed59341843bbc73336e38f5558d96a962bbc9edc Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Thu, 26 Feb 2026 14:34:42 -0500 Subject: [PATCH 3/3] Remove AGENTS.md and .claude/ from tracking --- .claude/.gitignore | 4 -- .claude/CLAUDE.md | 1 - .claude/settings.json | 35 ------------ .gitignore | 2 + AGENTS.md | 127 ------------------------------------------ 5 files changed, 2 insertions(+), 167 deletions(-) delete mode 100644 .claude/.gitignore delete mode 100644 .claude/CLAUDE.md delete mode 100644 .claude/settings.json delete mode 100644 AGENTS.md diff --git a/.claude/.gitignore b/.claude/.gitignore deleted file mode 100644 index 3a2f7f6..0000000 --- a/.claude/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -CLAUDE.local.md -settings.local.json -worktrees/ -plans/ diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md deleted file mode 100644 index dba71e9..0000000 --- a/.claude/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -@../AGENTS.md diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index af929c7..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(echo $VIRTUAL_ENV)", - "Bash(gh api:*)", - "Bash(gh issue view:*)", - "Bash(gh label list:*)", - "Bash(gh pr checks:*)", - "Bash(gh pr diff:*)", - "Bash(gh pr list:*)", - "Bash(gh pr status:*)", - "Bash(gh pr update-branch:*)", - "Bash(gh pr view:*)", - "Bash(gh search code:*)", - "Bash(git branch:*)", - "Bash(git diff:*)", - "Bash(git fetch:*)", - "Bash(git grep:*)", - "Bash(git log:*)", - "Bash(git show:*)", - "Bash(git status:*)", - "Bash(grep:*)", - "Bash(ls:*)", - "Bash(pip install:*)", - "Bash(pytest:*)", - "Bash(ruff check:*)", - "Bash(ruff format:*)", - "Bash(tree:*)", - "WebFetch(domain:api.slack.com)", - "WebFetch(domain:docs.slack.dev)", - "WebFetch(domain:github.com)", - "WebFetch(domain:raw.githubusercontent.com)" - ] - } -} diff --git a/.gitignore b/.gitignore index 3ad9e1b..1e111bb 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ logs/ .pytype/ .vscode .cursor +.claude/ +AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index dc57eca..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,127 +0,0 @@ -# AGENTS.md — bolt-python-search-template - -## Project Overview - -This is a Slack sample app demonstrating **Enterprise Search** using [Bolt for Python](https://docs.slack.dev/bolt-python). It serves as a template for building search integrations that appear natively in Slack's search experience. - -- **Repo:** [slack-samples/bolt-python-search-template](https://github.com/slack-samples/bolt-python-search-template) -- **README:** [README.md](./README.md) — setup instructions, environment variables, and project structure overview -- **Docs:** [Enterprise Search](https://docs.slack.dev/surfaces/search) -- **Framework:** Bolt for Python (`slack-bolt`) -- **Runtime:** Python (see `requires-python` in `pyproject.toml`), Socket Mode - -## Architecture - -The app registers two **custom functions** (`search` and `filters`) and one **event listener** (`entity_details_requested`) with Slack. These are declared in `manifest.json` and wired up via the listener registration pattern. - -### Listener Registration Pattern - -`app.py` creates the Bolt `App` and calls `register_listeners(app)` from `listeners/__init__.py`. This delegates to sub-modules: - -- `listeners/functions/__init__.py` — registers `search` and `filters` function callbacks via `app.function()` -- `listeners/events/__init__.py` — registers the `entity_details_requested` event callback via `app.event()` - -### Data Flow - -1. **Filters request** → `listeners/functions/filters.py` returns predefined filter definitions (languages, templates, samples) from `listeners/filters.py` -2. **Search request** → `listeners/functions/search.py` calls `fetch_sample_data()` from `listeners/sample_data_service.py`, which hits the `developer.sampleData.get` Slack API, then passes samples through as search results -3. **Entity details** → `listeners/events/entity_details_requested.py` fetches sample data and presents entity details via `entity.presentDetails` API - -## Key Files & Directories - -``` -app.py # Entry point — creates App, registers listeners, starts SocketModeHandler -manifest.json # Slack app manifest — defines functions, events, scopes, features -pyproject.toml # Build config, dependencies, ruff + pytest settings -listeners/ - __init__.py # register_listeners(app) — delegates to functions/ and events/ - filters.py # Predefined filter constants (plain dicts) - sample_data_service.py # fetch_sample_data() — calls developer.sampleData.get API - functions/ - __init__.py # Registers search and filters function callbacks - search.py # search_step_callback — handles search function execution - filters.py # filters_step_callback — returns available search filters - events/ - __init__.py # Registers entity_details_requested event callback - entity_details_requested.py # Handles rich preview entity detail requests -tests/ # Mirrors listeners/ structure - listeners/ - test_sample_data_service.py - functions/ - test_search.py - test_filters.py - events/ - test_entity_details_requested.py -.github/workflows/ - lint.yml # Ruff linting on push/PR - tests.yml # pytest on Python 3.11, 3.12, 3.13 - dependencies.yml # Dependabot auto-merge -``` - -## Development Commands - -> **IMPORTANT: A Python virtual environment MUST be activated before running ANY command in this project.** -> Before executing any command below, first verify the virtual environment is active by running `echo $VIRTUAL_ENV` — it MUST output a path. -> If it outputs nothing, do NOT proceed. Ask the user to activate a virtual environment first. - -```bash -# Install dependencies (editable mode) -pip install -e . - -# Run the app (Socket Mode) -python3 app.py -# Or via Slack CLI -slack run - -# Lint -ruff check - -# Format -ruff format -ruff check --fix - -# Run tests -pytest . -``` - -## Code Style & Tooling - -- **Linter/Formatter:** Ruff (see `[tool.ruff]` in `pyproject.toml` for line length, lint rules, and other settings) -- **Testing:** pytest (see `[tool.pytest.ini_options]` in `pyproject.toml` for test paths and logging config) -- **Python:** see `requires-python` in `pyproject.toml` - -## CI Pipeline - -| Workflow | File | What it does | -|---|---|---| -| Lint | `.github/workflows/lint.yml` | Runs `ruff check` on Python 3.13 | -| Tests | `.github/workflows/tests.yml` | Runs `pytest .` on Python 3.11, 3.12, 3.13 | -| Dependencies | `.github/workflows/dependencies.yml` | Dependabot auto-merge for patch/minor updates | - -## Test Patterns - -- Tests use `pytest` with class-based organization (e.g., `TestSearch`) -- `setup_method` creates `MagicMock` instances for Bolt primitives (`Ack`, `Complete`, `Fail`, `WebClient`, `Logger`) -- External calls (e.g., `fetch_sample_data`) are patched with `@patch` decorators -- Tests verify: correct arguments passed to service calls, expected outputs, error handling paths, and that `ack()` is always called - -## Common Contribution Workflows - -### Adding a new filter - -1. Define the filter constant in `listeners/filters.py` as a plain dict -2. Add it to the `complete(outputs=...)` list in `listeners/functions/filters.py` -3. If it affects search, handle it in `listeners/sample_data_service.py`'s filter processing -4. Add tests in `tests/listeners/functions/test_filters.py` - -### Modifying search results - -1. Update `search_step_callback` in `listeners/functions/search.py` -2. Update tests in `tests/listeners/functions/test_search.py` - -### Fixing a bug - -1. Run `pytest .` to confirm current test state -2. Write a failing test that reproduces the bug -3. Fix the bug -4. Run `ruff check` and `pytest .` to verify