functualize-tasks-local 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.egg-info/
7
+ *.egg
8
+ dist/
9
+ build/
10
+ *.whl
11
+
12
+ # Agents
13
+ .spec/archive/
14
+ .spec/features/
15
+ .spec/scrutiny-reports/
16
+ .spec/.agentic-coding
17
+ .spec/STATE.md
18
+ .spec/PROJECT.md
19
+ .spec/REQUIREMENTS.md
20
+ .spec/ROADMAP.md
21
+ .opencode/
22
+
23
+
24
+ # Virtual environments
25
+ .venv/
26
+ venv/
27
+ ENV/
28
+
29
+ # Testing
30
+ .coverage
31
+ .pytest_cache/
32
+ htmlcov/
33
+ .hypothesis/
34
+ snapshot_report.html
35
+ _*_result*.txt
36
+ _debug.txt
37
+ _tui_debug.txt
38
+ _tui_eval_debug.txt
39
+
40
+ # IDE
41
+ .idea/
42
+ *.swp
43
+ *.swo
44
+ *~
45
+ *.code-workspace
46
+
47
+ # Coding-agent tooling state (guards — these dirs are not part of the repo)
48
+ .kiro/
49
+ .moai/
50
+
51
+ # OS
52
+ .DS_Store
53
+ Thumbs.db
54
+
55
+ # Environment / secrets
56
+ .env
57
+ .env.*
58
+ !.env.example
59
+
60
+ # Agent scratch space (test output, temp scripts)
61
+ tmp/
62
+
63
+ # Local-only files (not for the repo)
64
+ *.local.md
65
+ *.local.*
66
+
67
+ # Personal notes
68
+ HUMAN_NOTE.md
69
+
70
+ # Distribution
71
+ dist/
72
+
73
+ # Documentation site build output
74
+ site/
75
+
76
+ # uv
77
+ .python-version
78
+ .functualize/cache.json
79
+ .functualize_cache.json
80
+ .todos/
81
+ .sidecar/
82
+ .sidecar-agent
83
+ .sidecar-task
84
+ .sidecar-pr
85
+ .sidecar-start.sh
86
+ .sidecar-base
87
+ .td-root
88
+ .functualize/
89
+ .import_linter_cache/
90
+ .mypy_cache/
91
+ .pytest_cache/
92
+ .ruff_cache/
93
+
94
+ # OmO / OpenCode agent run-continuation scratch state
95
+ .omo/
96
+ .mcp.json
97
+ .agentsroom/handoff-transcript-*.txt
98
+ .agentsroom/handoff-summary-*.md
99
+
100
+ # Internal pre-release audit reports (contain session IDs / local infra notes)
101
+ .release/
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: functualize-tasks-local
3
+ Version: 0.1.0
4
+ Summary: Local state-backed task storage plugin for functualize
5
+ Author-email: Mohammad Hakim Adiprasetya <viltohmyst@gmail.com>
6
+ License-Expression: MIT
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: functualize-state<1.0.0,>=0.1.0
15
+ Requires-Dist: functualize-tasks<1.0.0,>=0.1.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
18
+ Requires-Dist: pytest>=7.4.0; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # functualize-tasks-local
22
+
23
+ > **Status: Published** — Independently installable from PyPI.
24
+
25
+ Local state-backed task storage plugin for functualize. Provides a `TaskProvider`
26
+ implementation that persists tasks as JSON blobs in the active `StateBackend`,
27
+ using keys prefixed with `tasks:`. Zero external dependencies beyond the
28
+ functualize workspace packages — if you have a state backend registered, this
29
+ plugin gives you a fully functional task queue with no additional infrastructure.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install functualize-tasks-local
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```python
40
+ from functualize_state import InMemoryState
41
+ from functualize_tasks import TaskStatus
42
+ from functualize_tasks_local import LocalTaskProvider
43
+
44
+ # Create a provider backed by an in-memory state store
45
+ backend = InMemoryState()
46
+ provider = LocalTaskProvider(backend=backend)
47
+
48
+ # Add a task and retrieve it
49
+ task_id = provider.add("Deploy staging server")
50
+ tasks = provider.list(status=TaskStatus.PENDING)
51
+ print(tasks[0].title) # "Deploy staging server"
52
+
53
+ # Update status and clean up
54
+ provider.update(task_id, status=TaskStatus.DONE)
55
+ provider.delete(task_id)
56
+ ```
57
+
58
+ ## Features
59
+
60
+ - **State-backed persistence** — delegates all storage to the active `StateBackend`, so tasks survive restarts when using a durable backend like SQLite
61
+ - **Automatic plugin registration** — registers via the `functualize.tasks_providers` entry point with name `"local"`, no manual wiring required
62
+ - **Full CRUD operations** — create, list, update, delete, and link tasks with filtering by status or title substring
63
+ - **Zero external dependencies** — only depends on `functualize-tasks` and `functualize-state` from the workspace
64
+ - **JSON serialization** — each task stored as a compact JSON blob under `tasks:{task_id}`, easily inspectable for debugging
65
+
66
+ ## API Reference
67
+
68
+ Public classes exported by this plugin:
69
+
70
+ - `LocalTaskProvider` — `TaskProvider` implementation that stores tasks in a `StateBackend` with `tasks:` key prefix. Methods: `add()`, `list()`, `update()`, `delete()`, `link()`
71
+ - `LocalTasksPlugin` — Plugin entry point that resolves the active `StateBackend` from DI at boot and registers a `LocalTaskProvider` as the `TaskProvider` implementation
72
+
73
+ ## Development
74
+
75
+ Run plugin tests:
76
+
77
+ ```bash
78
+ uv run pytest plugins/functualize-tasks-local/tests/ -v
79
+ ```
@@ -0,0 +1,59 @@
1
+ # functualize-tasks-local
2
+
3
+ > **Status: Published** — Independently installable from PyPI.
4
+
5
+ Local state-backed task storage plugin for functualize. Provides a `TaskProvider`
6
+ implementation that persists tasks as JSON blobs in the active `StateBackend`,
7
+ using keys prefixed with `tasks:`. Zero external dependencies beyond the
8
+ functualize workspace packages — if you have a state backend registered, this
9
+ plugin gives you a fully functional task queue with no additional infrastructure.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install functualize-tasks-local
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```python
20
+ from functualize_state import InMemoryState
21
+ from functualize_tasks import TaskStatus
22
+ from functualize_tasks_local import LocalTaskProvider
23
+
24
+ # Create a provider backed by an in-memory state store
25
+ backend = InMemoryState()
26
+ provider = LocalTaskProvider(backend=backend)
27
+
28
+ # Add a task and retrieve it
29
+ task_id = provider.add("Deploy staging server")
30
+ tasks = provider.list(status=TaskStatus.PENDING)
31
+ print(tasks[0].title) # "Deploy staging server"
32
+
33
+ # Update status and clean up
34
+ provider.update(task_id, status=TaskStatus.DONE)
35
+ provider.delete(task_id)
36
+ ```
37
+
38
+ ## Features
39
+
40
+ - **State-backed persistence** — delegates all storage to the active `StateBackend`, so tasks survive restarts when using a durable backend like SQLite
41
+ - **Automatic plugin registration** — registers via the `functualize.tasks_providers` entry point with name `"local"`, no manual wiring required
42
+ - **Full CRUD operations** — create, list, update, delete, and link tasks with filtering by status or title substring
43
+ - **Zero external dependencies** — only depends on `functualize-tasks` and `functualize-state` from the workspace
44
+ - **JSON serialization** — each task stored as a compact JSON blob under `tasks:{task_id}`, easily inspectable for debugging
45
+
46
+ ## API Reference
47
+
48
+ Public classes exported by this plugin:
49
+
50
+ - `LocalTaskProvider` — `TaskProvider` implementation that stores tasks in a `StateBackend` with `tasks:` key prefix. Methods: `add()`, `list()`, `update()`, `delete()`, `link()`
51
+ - `LocalTasksPlugin` — Plugin entry point that resolves the active `StateBackend` from DI at boot and registers a `LocalTaskProvider` as the `TaskProvider` implementation
52
+
53
+ ## Development
54
+
55
+ Run plugin tests:
56
+
57
+ ```bash
58
+ uv run pytest plugins/functualize-tasks-local/tests/ -v
59
+ ```
@@ -0,0 +1,11 @@
1
+ # functualize-tasks-local Examples
2
+
3
+ The local task provider: stores tasks in any `StateBackend`, so task lists persist wherever your state does.
4
+
5
+ | Directory | Demonstrates |
6
+ |-----------|--------------|
7
+ | [`todo_local/`](todo_local/) | `LocalTaskProvider` wired to a state backend, driven through the `Tasks` capability |
8
+
9
+ ```bash
10
+ uv run pytest plugins/functualize-tasks-local/examples/ -v
11
+ ```
@@ -0,0 +1,13 @@
1
+ """Tests for the todo_local example."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from unittest.mock import MagicMock
6
+
7
+ sys.path.insert(0, str(Path(__file__).parent))
8
+
9
+ from todo_local import checklist
10
+
11
+
12
+ def test_one_item_remains_open():
13
+ assert checklist(MagicMock()) == 1
@@ -0,0 +1,27 @@
1
+ """Tasks stored in a state backend via LocalTaskProvider.
2
+
3
+ Pairs the tasks domain with the state domain: tasks live wherever your
4
+ StateBackend does (in-memory here; SQLite in production). In a real app
5
+ `LocalTasksPlugin` wires this automatically at boot.
6
+ """
7
+
8
+ from functualize_state.testing import InMemoryState
9
+ from functualize_tasks import Tasks, TaskStatus
10
+ from functualize_tasks_local import LocalTaskProvider
11
+
12
+ from functualize.job import RunContext
13
+
14
+
15
+ def checklist(rc: RunContext) -> int:
16
+ """Create a checklist and report how many items remain open."""
17
+ backend = InMemoryState()
18
+ tasks = Tasks(_provider=LocalTaskProvider(backend))
19
+
20
+ first = tasks.add("Write the report")
21
+ tasks.add("Review the report")
22
+
23
+ tasks.update(first, status=TaskStatus.DONE)
24
+
25
+ open_items = [t for t in tasks.list() if t.status is not TaskStatus.DONE]
26
+ rc.log(f"{len(open_items)} item(s) still open")
27
+ return len(open_items)
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "functualize-tasks-local"
3
+ version = "0.1.0"
4
+ description = "Local state-backed task storage plugin for functualize"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "Mohammad Hakim Adiprasetya", email = "viltohmyst@gmail.com" }
9
+ ]
10
+ requires-python = ">=3.11"
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.11",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ "Typing :: Typed",
18
+ ]
19
+ dependencies = [
20
+ "functualize-tasks>=0.1.0,<1.0.0",
21
+ "functualize-state>=0.1.0,<1.0.0",
22
+ ]
23
+
24
+ [project.entry-points."functualize.tasks_providers"]
25
+ local = "functualize_tasks_local:LocalTasksPlugin"
26
+
27
+ [project.optional-dependencies]
28
+ dev = [
29
+ "pytest>=7.4.0",
30
+ "pytest-cov>=4.1.0",
31
+ ]
32
+
33
+ [build-system]
34
+ requires = ["hatchling"]
35
+ build-backend = "hatchling.build"
36
+
37
+ [tool.uv.sources]
38
+ functualize-tasks = { workspace = true }
39
+ functualize-state = { workspace = true }
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/functualize_tasks_local"]
@@ -0,0 +1,13 @@
1
+ """Functualize Tasks Local Plugin — state-backed task storage.
2
+
3
+ Provides a TaskProvider implementation that stores tasks in the active
4
+ StateBackend using keys prefixed with ``tasks:``. Zero external dependencies.
5
+ """
6
+
7
+ from functualize_tasks_local._plugin import LocalTasksPlugin
8
+ from functualize_tasks_local._provider import LocalTaskProvider
9
+
10
+ __all__ = [
11
+ "LocalTasksPlugin",
12
+ "LocalTaskProvider",
13
+ ]
@@ -0,0 +1,79 @@
1
+ """Local Tasks Plugin — DI registration.
2
+
3
+ Registers LocalTaskProvider as TaskProvider with the DI registry via
4
+ app.provide(). Uses the active StateBackend for task storage with
5
+ keys prefixed ``tasks:``.
6
+
7
+ Registered via entry point ``functualize.tasks_providers`` with name "local".
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import Any
14
+
15
+ from functualize_state import StateBackend
16
+ from functualize_tasks import TaskProvider
17
+
18
+ from functualize_tasks_local._provider import LocalTaskProvider
19
+
20
+ __all__ = ["LocalTasksPlugin"]
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class LocalTasksPlugin:
26
+ """Plugin that registers a local StateBackend-backed TaskProvider.
27
+
28
+ At boot time (APP_READY), resolves the active StateBackend from the DI
29
+ registry, creates a LocalTaskProvider wrapping it, and registers the
30
+ provider as the TaskProvider implementation via app.provide().
31
+
32
+ Implements the plugin callable protocol expected by functualize's plugin
33
+ discovery system.
34
+ """
35
+
36
+ name: str = "tasks-local"
37
+ version: str = "0.1.0"
38
+ description: str = "Local state-backed TaskProvider using tasks: prefix"
39
+
40
+ def __init__(self) -> None:
41
+ self._provider: LocalTaskProvider | None = None
42
+
43
+ @property
44
+ def provider(self) -> LocalTaskProvider | None:
45
+ """The LocalTaskProvider instance (available after APP_READY)."""
46
+ return self._provider
47
+
48
+ def __call__(self, app: Any) -> None:
49
+ """Register the plugin with the application instance.
50
+
51
+ Hooks into APP_READY for initialization and DI registration.
52
+ """
53
+ from functualize._events.hooks import HookEvent
54
+
55
+ app.hook_registry.register_global(HookEvent.APP_READY, self._on_app_ready)
56
+
57
+ def _on_app_ready(self, app: Any) -> None:
58
+ """Initialize LocalTaskProvider and register with DI registry.
59
+
60
+ Resolves the StateBackend from the DI registry and creates a
61
+ LocalTaskProvider backed by it. Registers the provider as
62
+ TaskProvider via app.provide().
63
+ """
64
+ try:
65
+ # Resolve the active StateBackend from DI
66
+ backend = app.resolve(StateBackend)
67
+
68
+ # Create local provider backed by the state backend
69
+ self._provider = LocalTaskProvider(backend=backend)
70
+
71
+ # Register as TaskProvider
72
+ app.provide(TaskProvider, self._provider)
73
+
74
+ logger.debug(
75
+ "LocalTasksPlugin: Registered TaskProvider (state-backed, "
76
+ "prefix='tasks:')"
77
+ )
78
+ except Exception as e:
79
+ logger.error("LocalTasksPlugin: Failed to initialize: %s", e)
@@ -0,0 +1,162 @@
1
+ """Local TaskProvider implementation backed by StateBackend.
2
+
3
+ Stores tasks as JSON in the active StateBackend using keys prefixed with
4
+ ``tasks:``. Each task is stored under ``tasks:{task_id}`` as a JSON-encoded
5
+ dict containing all TaskItem fields.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import time
12
+ import uuid
13
+ from typing import TYPE_CHECKING
14
+
15
+ from functualize_tasks import TaskItem, TaskLink, TaskNotFoundError, TaskStatus
16
+
17
+ if TYPE_CHECKING:
18
+ from functualize_state import StateBackend
19
+
20
+
21
+ class LocalTaskProvider:
22
+ """TaskProvider implementation using StateBackend with ``tasks:`` prefix.
23
+
24
+ Each task is stored as a JSON blob under the key ``tasks:{task_id}``.
25
+ Listing operations scan all keys with the ``tasks:`` prefix and
26
+ deserialize them for filtering.
27
+ """
28
+
29
+ PREFIX = "tasks:"
30
+
31
+ def __init__(self, backend: StateBackend) -> None:
32
+ self._backend = backend
33
+
34
+ def _task_key(self, task_id: str) -> str:
35
+ """Return the full state key for a task ID."""
36
+ return f"{self.PREFIX}{task_id}"
37
+
38
+ def _serialize_task(self, task: TaskItem) -> str:
39
+ """Serialize a TaskItem to JSON string."""
40
+ data: dict = {
41
+ "id": task.id,
42
+ "title": task.title,
43
+ "status": task.status.value,
44
+ "linked_to": None,
45
+ "notes": task.notes,
46
+ "creator": task.creator,
47
+ "created_at": task.created_at,
48
+ }
49
+ if task.linked_to is not None:
50
+ data["linked_to"] = {
51
+ "kind": task.linked_to.kind,
52
+ "target": task.linked_to.target,
53
+ }
54
+ return json.dumps(data)
55
+
56
+ def _deserialize_task(self, raw: str) -> TaskItem:
57
+ """Deserialize a JSON string to a TaskItem."""
58
+ data = json.loads(raw)
59
+ linked_to = None
60
+ if data.get("linked_to") is not None:
61
+ linked_to = TaskLink(
62
+ kind=data["linked_to"]["kind"],
63
+ target=data["linked_to"]["target"],
64
+ )
65
+ return TaskItem(
66
+ id=data["id"],
67
+ title=data["title"],
68
+ status=TaskStatus(data["status"]),
69
+ linked_to=linked_to,
70
+ notes=data.get("notes"),
71
+ creator=data.get("creator"),
72
+ created_at=data.get("created_at"),
73
+ )
74
+
75
+ def _get_task(self, task_id: str) -> TaskItem:
76
+ """Retrieve a task by ID, raising TaskNotFoundError if it doesn't exist."""
77
+ raw = self._backend.get(self._task_key(task_id))
78
+ if raw is None:
79
+ raise TaskNotFoundError(f"Task '{task_id}' not found")
80
+ return self._deserialize_task(raw)
81
+
82
+ def add(self, title: str, linked_to: TaskLink | None = None) -> str:
83
+ """Create a new task and return its generated unique ID."""
84
+ task_id = uuid.uuid4().hex[:12]
85
+ task = TaskItem(
86
+ id=task_id,
87
+ title=title,
88
+ status=TaskStatus.PENDING,
89
+ linked_to=linked_to,
90
+ notes=None,
91
+ creator=None,
92
+ created_at=time.time(),
93
+ )
94
+ self._backend.set(self._task_key(task_id), self._serialize_task(task))
95
+ return task_id
96
+
97
+ def list(
98
+ self, status: TaskStatus | None = None, filter: str | None = None
99
+ ) -> list[TaskItem]:
100
+ """List tasks, optionally filtered by status or title substring."""
101
+ keys = self._backend.keys(self.PREFIX)
102
+ tasks: list[TaskItem] = []
103
+ for key in keys:
104
+ raw = self._backend.get(key)
105
+ if raw is None:
106
+ continue
107
+ task = self._deserialize_task(raw)
108
+ if status is not None and task.status != status:
109
+ continue
110
+ if filter is not None and filter not in task.title:
111
+ continue
112
+ tasks.append(task)
113
+ return tasks
114
+
115
+ def update(
116
+ self, task_id: str, status: TaskStatus | None = None, notes: str | None = None
117
+ ) -> None:
118
+ """Update a task's status and/or notes.
119
+
120
+ Raises:
121
+ TaskNotFoundError: If the task_id does not exist.
122
+ """
123
+ task = self._get_task(task_id)
124
+ # Build updated task (TaskItem is frozen, so we reconstruct)
125
+ updated = TaskItem(
126
+ id=task.id,
127
+ title=task.title,
128
+ status=status if status is not None else task.status,
129
+ linked_to=task.linked_to,
130
+ notes=notes if notes is not None else task.notes,
131
+ creator=task.creator,
132
+ created_at=task.created_at,
133
+ )
134
+ self._backend.set(self._task_key(task_id), self._serialize_task(updated))
135
+
136
+ def delete(self, task_id: str) -> None:
137
+ """Delete a task by its ID.
138
+
139
+ Raises:
140
+ TaskNotFoundError: If the task_id does not exist.
141
+ """
142
+ # Verify existence first
143
+ self._get_task(task_id)
144
+ self._backend.delete(self._task_key(task_id))
145
+
146
+ def link(self, task_id: str, linked_to: TaskLink) -> None:
147
+ """Associate a task with a job, workflow step, or job phase.
148
+
149
+ Raises:
150
+ TaskNotFoundError: If the task_id does not exist.
151
+ """
152
+ task = self._get_task(task_id)
153
+ updated = TaskItem(
154
+ id=task.id,
155
+ title=task.title,
156
+ status=task.status,
157
+ linked_to=linked_to,
158
+ notes=task.notes,
159
+ creator=task.creator,
160
+ created_at=task.created_at,
161
+ )
162
+ self._backend.set(self._task_key(task_id), self._serialize_task(updated))
File without changes
@@ -0,0 +1 @@
1
+ """Shared fixtures for functualize-tasks-local plugin tests."""
@@ -0,0 +1,97 @@
1
+ """Functional tests for LocalTaskProvider.
2
+
3
+ Tests local store/retrieve tasks, list filtering, and state persistence
4
+ via InMemoryState fixture.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import pytest
10
+ from functualize_state import InMemoryState
11
+ from functualize_tasks import TaskLink, TaskNotFoundError, TaskStatus
12
+ from functualize_tasks_local import LocalTaskProvider
13
+
14
+
15
+ @pytest.fixture
16
+ def backend() -> InMemoryState:
17
+ """Provide a fresh InMemoryState backend for each test."""
18
+ return InMemoryState()
19
+
20
+
21
+ @pytest.fixture
22
+ def provider(backend: InMemoryState) -> LocalTaskProvider:
23
+ """Provide a LocalTaskProvider backed by InMemoryState."""
24
+ return LocalTaskProvider(backend=backend)
25
+
26
+
27
+ class TestStoreAndRetrieve:
28
+ """Tests for adding and listing tasks."""
29
+
30
+ def test_add_task_returns_id_and_persists(
31
+ self, provider: LocalTaskProvider
32
+ ) -> None:
33
+ """Adding a task returns a unique ID and is retrievable via list."""
34
+ task_id = provider.add("Deploy service")
35
+
36
+ assert task_id is not None
37
+ assert len(task_id) == 12 # uuid4 hex[:12]
38
+
39
+ tasks = provider.list()
40
+ assert len(tasks) == 1
41
+ assert tasks[0].id == task_id
42
+ assert tasks[0].title == "Deploy service"
43
+ assert tasks[0].status == TaskStatus.PENDING
44
+
45
+ def test_add_task_with_link(self, provider: LocalTaskProvider) -> None:
46
+ """Adding a task with a TaskLink persists the link correctly."""
47
+ link = TaskLink(kind="job", target="deploy-job")
48
+ provider.add("Linked task", linked_to=link)
49
+
50
+ tasks = provider.list()
51
+ assert len(tasks) == 1
52
+ assert tasks[0].linked_to is not None
53
+ assert tasks[0].linked_to.kind == "job"
54
+ assert tasks[0].linked_to.target == "deploy-job"
55
+
56
+
57
+ class TestListFiltering:
58
+ """Tests for filtering tasks by status and title substring."""
59
+
60
+ def test_filter_by_status(self, provider: LocalTaskProvider) -> None:
61
+ """Listing with a status filter returns only matching tasks."""
62
+ id1 = provider.add("Task A")
63
+ id2 = provider.add("Task B")
64
+ provider.update(id1, status=TaskStatus.DONE)
65
+
66
+ pending = provider.list(status=TaskStatus.PENDING)
67
+ done = provider.list(status=TaskStatus.DONE)
68
+
69
+ assert len(pending) == 1
70
+ assert pending[0].id == id2
71
+ assert len(done) == 1
72
+ assert done[0].id == id1
73
+
74
+ def test_filter_by_title_substring(self, provider: LocalTaskProvider) -> None:
75
+ """Listing with a filter string returns only tasks whose title contains it."""
76
+ provider.add("Deploy service")
77
+ provider.add("Run migrations")
78
+ provider.add("Deploy database")
79
+
80
+ results = provider.list(filter="Deploy")
81
+ assert len(results) == 2
82
+ titles = {t.title for t in results}
83
+ assert titles == {"Deploy service", "Deploy database"}
84
+
85
+
86
+ class TestErrorHandling:
87
+ """Tests for error cases and proper exception raising."""
88
+
89
+ def test_update_nonexistent_task_raises(self, provider: LocalTaskProvider) -> None:
90
+ """Updating a task that doesn't exist raises TaskNotFoundError."""
91
+ with pytest.raises(TaskNotFoundError):
92
+ provider.update("nonexistent-id", status=TaskStatus.DONE)
93
+
94
+ def test_delete_nonexistent_task_raises(self, provider: LocalTaskProvider) -> None:
95
+ """Deleting a task that doesn't exist raises TaskNotFoundError."""
96
+ with pytest.raises(TaskNotFoundError):
97
+ provider.delete("nonexistent-id")
@@ -0,0 +1,15 @@
1
+ """Unit tests for functualize-tasks-local plugin.
2
+
3
+ Tests the local task runner implementation.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+
9
+ class TestImports:
10
+ """Verify the plugin is importable."""
11
+
12
+ def test_import_package(self):
13
+ import functualize_tasks_local
14
+
15
+ assert dir(functualize_tasks_local)