Add project templates and clean up a bit

This commit is contained in:
K. Hodges 2026-05-17 17:32:09 -07:00
parent 5e5cd184b9
commit b0f8d59707
41 changed files with 862 additions and 17 deletions

View File

@ -116,7 +116,14 @@ nightshift run --task TASK-001
For the first real-model tutorial target: For the first real-model tutorial target:
```bash ```bash
nightshift init --template imageboard --root nightshift-imageboard nightshift init --template tutorial-imageboard --root nightshift-imageboard
```
Other built-in real-model templates:
```bash
nightshift init --template real-simple --root bookmarks-demo
nightshift init --template real-long-running --root incident-service
``` ```
Open the read-only artifact dashboard: Open the read-only artifact dashboard:

View File

@ -9,7 +9,7 @@ The target is a compact 4chan-style imageboard: boards, threads, replies, images
Run this from a disposable parent directory: Run this from a disposable parent directory:
```bash ```bash
nightshift init --template imageboard --root nightshift-imageboard nightshift init --template tutorial-imageboard --root nightshift-imageboard
cd nightshift-imageboard cd nightshift-imageboard
``` ```

View File

@ -8,7 +8,7 @@ import sys
from .config import validate_config from .config import validate_config
from .errors import NightShiftError from .errors import NightShiftError
from .init import init_project from .init import available_templates, init_project
from .pipeline import PipelineRunner from .pipeline import PipelineRunner
from .runlog import RunLogger from .runlog import RunLogger
from .status import build_status, format_status from .status import build_status, format_status
@ -33,7 +33,7 @@ def build_parser() -> argparse.ArgumentParser:
init_parser.add_argument( init_parser.add_argument(
"--template", "--template",
default="basic", default="basic",
choices=("basic", "imageboard"), choices=available_templates(),
help="Starter template to create.", help="Starter template to create.",
) )
init_parser.add_argument("--force", action="store_true", help="Overwrite existing starter files.") init_parser.add_argument("--force", action="store_true", help="Overwrite existing starter files.")

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
import shutil
from .errors import InitError from .errors import InitError
from . import templates from . import templates
@ -32,9 +33,18 @@ IMAGEBOARD_FILES = {
PROJECT_TEMPLATES = { PROJECT_TEMPLATES = {
"basic": STARTER_FILES, "basic": STARTER_FILES,
"imageboard": IMAGEBOARD_FILES, "tutorial-imageboard": IMAGEBOARD_FILES,
} }
TEMPLATE_ROOT = Path(__file__).resolve().parent / "project_templates"
def available_templates() -> tuple[str, ...]:
names = set(PROJECT_TEMPLATES)
if TEMPLATE_ROOT.exists():
names.update(path.name for path in TEMPLATE_ROOT.iterdir() if path.is_dir())
return tuple(sorted(names))
def init_project(root: Path, force: bool = False, template: str = "basic") -> list[Path]: def init_project(root: Path, force: bool = False, template: str = "basic") -> list[Path]:
"""Create starter NightShift files under root. """Create starter NightShift files under root.
@ -43,10 +53,11 @@ def init_project(root: Path, force: bool = False, template: str = "basic") -> li
""" """
root = root.resolve() root = root.resolve()
if template not in PROJECT_TEMPLATES: if template not in available_templates():
known = ", ".join(sorted(PROJECT_TEMPLATES)) known = ", ".join(available_templates())
raise InitError(f"Unknown template '{template}'. Available templates: {known}") raise InitError(f"Unknown template '{template}'. Available templates: {known}")
files = PROJECT_TEMPLATES[template] template_dir = TEMPLATE_ROOT / template
files = _template_files(template, template_dir)
targets = [root / relative for relative in files] targets = [root / relative for relative in files]
existing = [path for path in targets if path.exists()] existing = [path for path in targets if path.exists()]
if existing and not force: if existing and not force:
@ -60,7 +71,21 @@ def init_project(root: Path, force: bool = False, template: str = "basic") -> li
for relative, content in files.items(): for relative, content in files.items():
path = root / relative path = root / relative
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
if content is None:
source = template_dir / relative
shutil.copyfile(source, path)
else:
path.write_text(content, encoding="utf-8") path.write_text(content, encoding="utf-8")
written.append(path) written.append(path)
return written return written
def _template_files(template: str, template_dir: Path) -> dict[str, str | None]:
if template_dir.exists():
return {
path.relative_to(template_dir).as_posix(): None
for path in sorted(template_dir.rglob("*"))
if path.is_file()
}
return PROJECT_TEMPLATES[template]

View File

@ -0,0 +1,11 @@
# Implementer
You are the implementation agent for NightShift.
Implement the approved plan inside the scoped project directory.
Rules:
- Make the smallest correct change.
- Do not edit files outside scope.
- Preserve existing style.
- Write useful implementation notes.

View File

@ -0,0 +1,13 @@
# Planner
You are the planning agent for NightShift.
Create a conservative implementation plan for one coding task.
Rules:
- Do not write code.
- Identify relevant files.
- Preserve existing behavior.
- Prefer small changes.
- Include test strategy.
- Include risks.

View File

@ -0,0 +1,12 @@
# Reviewer
You are the review agent for NightShift.
Decide whether the current task should pass, retry implementation, retry planning, or fail.
Output exactly:
status: pass | fail | retry | escalate
reason: <short explanation>
next_stage: <optional stage id>
context_update: <compact useful note>

View File

@ -0,0 +1,67 @@
project:
name: example-project
root: .
task_file: tasks.md
artifact_dir: .nightshift
safety:
require_clean_worktree: false
scoped_paths:
- .
allowed_commands:
- python -m unittest
forbidden_commands:
- rm -rf
- git push
- curl | bash
agents:
planner:
backend: command
command: echo
system_prompt: agents/planner.md
implementer:
backend: command
command: echo
system_prompt: agents/implementer.md
reviewer:
backend: command
command: echo
system_prompt: agents/reviewer.md
pipeline:
max_task_retries: 3
stages:
- id: plan
type: agent
agent: planner
output: plan.md
- id: review_plan
type: agent_review
agent: reviewer
on_fail: plan
output: plan-review.md
- id: implement
type: agent
agent: implementer
output: implementation-log.md
- id: test
type: command
commands:
- python -m unittest
output: test-output.txt
- id: review
type: agent_review
agent: reviewer
on_fail: implement
output: review.md
- id: summarize
type: summarize
output: final-notes.md

View File

@ -0,0 +1,10 @@
# Tasks
- [ ] TASK-001: Add your first NightShift task
Description:
Describe the coding task NightShift should work on.
Acceptance Criteria:
- The expected behavior is clear
- The task can be reviewed from generated artifacts

View File

@ -0,0 +1,7 @@
You are the architecture agent for NightShift.
Use the plan, task, and context to produce a short implementation design.
Focus on module boundaries, data model, route responsibilities, and test seams.
Do not write code.
Do not request broad unrelated context.

View File

@ -0,0 +1,12 @@
You are the junior implementation agent for NightShift.
Implement the task from the plan and architecture notes.
Output only complete file content blocks:
```file:relative/path.py
<complete file content>
```
Keep the implementation simple and testable.
Include tests.
Do not include explanations outside file blocks.

View File

@ -0,0 +1,21 @@
You are the planning agent for NightShift.
Create a concise, realistic implementation plan for the current task.
Request repository context when needed.
Use lookup requests exactly:
lookup_requests:
- tool: read_file
path: relative/path.py
- tool: grep
path: .
pattern: search_regex
After context is available, write:
- implementation steps
- files to create or edit
- test strategy
- risks and sequencing notes
Do not write code.

View File

@ -0,0 +1,18 @@
You are the review agent for NightShift.
Review the task, plan, architecture notes, patch artifacts, test output, and final repository state.
Output exactly:
status: pass | fail | retry | escalate
reason: <short explanation>
next_stage: <optional stage id>
context_update: <compact useful note>
For the junior review:
- If the implementation satisfies the task, output `status: pass` and `next_stage: summarize`.
- If the implementation is close but flawed, output `status: retry` and `next_stage: implement_senior`.
For the senior review:
- Use retry only for fixable senior issues.
- Use pass only when acceptance criteria are satisfied.

View File

@ -0,0 +1,13 @@
You are the senior implementation agent for NightShift.
You receive the previous junior attempt, validation errors, test output, review notes, and repository context.
Produce a corrected implementation for the same task.
Output only complete file content blocks:
```file:relative/path.py
<complete file content>
```
Prioritize correctness, coherent design, and passing tests.
Preserve useful work from the junior attempt when it is sound.
Do not include explanations outside file blocks.

View File

@ -0,0 +1,40 @@
# Tasks
- [ ] TASK-001: Incident intake service foundation
Description:
Create a Flask service for incident intake and triage. Implement SQLite schema, app factory, incident model helpers, routes for creating/listing/detailing incidents, and tests. This is intentionally larger than the simple template and should exercise planning, architecture notes, implementation, tests, and review.
Acceptance Criteria:
- Provides app factory and package layout under `src/`
- Defines SQLite schema for incidents, status, severity, and audit events
- Implements create, list, and detail routes
- Records audit events when incidents are created or status changes
- Includes pytest tests with temporary database setup
- [ ] TASK-002: Assignment and status workflow
Dependencies:
- TASK-001
Description:
Add assignee tracking, status transitions, validation rules, and audit history views.
Acceptance Criteria:
- Supports assigning incidents to owners
- Validates allowed status transitions
- Stores audit events for assignment and status changes
- Includes route and model tests
- [ ] TASK-003: Search and reporting
Dependencies:
- TASK-002
Description:
Add filtering by severity/status/assignee and a lightweight CSV export for open incidents.
Acceptance Criteria:
- Supports filtered incident list queries
- Exports open incidents as CSV
- Includes tests for filter combinations and export content

View File

@ -0,0 +1,29 @@
# Real Long-Running Template: Junior/Senior Pipeline
Use this template for a realistic, longer NightShift run where several agents cooperate on a non-trivial service task.
```bash
nightshift init --template real-long-running --root incident-service
cd incident-service
python -m pip install flask pytest
nightshift run --task TASK-001
```
Use case: build and evolve a small incident intake and triage service with Flask, SQLite, tests, and audit history.
Agents:
- Planner: `qwen2.5-coder:14b`
- Architect: `qwen2.5-coder:14b`
- Junior implementer: `qwen2.5-coder:14b`
- Senior implementer: `qwen3-coder:30b`
- Reviewer: `qwen2.5-coder:14b`
Junior/senior routing:
- The junior implementer tries first.
- Patch validation, patch apply, or tests can route directly to `implement_senior`.
- The junior reviewer prompt asks the reviewer to jump to `summarize` when the junior passes, or to `implement_senior` when the junior needs escalation.
- The senior path then writes its own patch artifacts, applies, tests, and reviews.
This template intentionally has a longer pipeline and a bigger blast radius. Use it after the simple template works on your machine.

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,151 @@
project:
name: service-maintenance
root: .
task_file: .nightshift/tasks.md
artifact_dir: .nightshift
safety:
require_clean_worktree: false
scoped_paths:
- src
- tests
- docs
- pyproject.toml
- README.md
allowed_commands:
- python -m pytest -q
forbidden_commands:
- rm -rf
- git push
- curl | bash
experiment:
label: long-running-junior-senior
prompt_variant: qwen25-junior-qwen3-senior-v1
agents:
planner:
backend: ollama
model: qwen2.5-coder:14b
temperature: 0.2
system_prompt: .nightshift/agents/planner.md
architect:
backend: ollama
model: qwen2.5-coder:14b
temperature: 0.2
system_prompt: .nightshift/agents/architect.md
junior:
backend: ollama
model: qwen2.5-coder:14b
temperature: 0.1
system_prompt: .nightshift/agents/junior.md
senior:
backend: ollama
model: qwen3-coder:30b
temperature: 0.1
system_prompt: .nightshift/agents/senior.md
reviewer:
backend: ollama
model: qwen2.5-coder:14b
temperature: 0.1
system_prompt: .nightshift/agents/reviewer.md
pipeline:
max_task_retries: 4
continue_on_task_failure: false
stages:
- id: plan
type: agent
agent: planner
output: plan.md
- id: architecture
type: agent
agent: architect
output: architecture.md
- id: context
type: repo_context
output: context-pack.md
- id: implement_junior
type: file_writer
agent: junior
output: proposed.patch
- id: normalize_junior
type: patch_normalizer
output: normalized.patch
- id: validate_junior
type: patch_validator
output: patch-validation.md
max_files: 16
max_lines: 1400
on_fail: implement_senior
- id: apply_junior
type: patch_apply
mode: apply
output: patch-apply-output.txt
on_fail: implement_senior
- id: test_junior
type: command
commands:
- python -m pytest -q
output: test-output.txt
shell: true
timeout_seconds: 45
on_fail: implement_senior
- id: review_junior
type: agent_review
agent: reviewer
output: review.md
on_fail: implement_senior
- id: implement_senior
type: file_writer
agent: senior
output: senior-proposed.patch
- id: normalize_senior
type: patch_normalizer
output: senior-normalized.patch
- id: validate_senior
type: patch_validator
output: senior-patch-validation.md
max_files: 20
max_lines: 1800
on_fail: implement_senior
- id: apply_senior
type: patch_apply
mode: apply
output: senior-patch-apply-output.txt
on_fail: implement_senior
- id: test_senior
type: command
commands:
- python -m pytest -q
output: senior-test-output.txt
shell: true
timeout_seconds: 60
on_fail: implement_senior
- id: review_senior
type: agent_review
agent: reviewer
output: senior-review.md
on_fail: implement_senior
- id: summarize
type: summarize
output: final-notes.md

View File

@ -0,0 +1,12 @@
You are the implementation agent for NightShift.
Output only complete file content blocks.
Use one fenced block per changed file:
```file:relative/path.py
<complete file content>
```
Keep the change scoped to the current task.
Prefer straightforward Flask and sqlite3 code.
Include pytest tests when needed.
Do not include explanations outside file blocks.

View File

@ -0,0 +1,19 @@
You are the planning agent for NightShift.
Create a concise implementation plan for the current task.
If repository context is needed, output lookup requests exactly:
lookup_requests:
- tool: read_file
path: relative/path.py
- tool: grep
path: .
pattern: search_regex
After context is available, write:
- files to create or edit
- tests to add
- risks
Do not write code.

View File

@ -0,0 +1,13 @@
You are the review agent for NightShift.
Review the task, plan, patch artifacts, test output, and final repository state.
Output exactly:
status: pass | fail | retry | escalate
reason: <short explanation>
next_stage: <optional stage id>
context_update: <compact useful note>
Use retry for fixable implementation or test failures.
Use pass only when acceptance criteria are satisfied.

View File

@ -0,0 +1,41 @@
# Tasks
- [ ] TASK-001: Bookmark API foundation
Description:
Create a small Flask bookmark API with SQLite persistence. Implement app factory, schema initialization, bookmark model helpers, and routes to create, list, update, and delete bookmarks. Keep code under `src/` and tests under `tests/`.
Acceptance Criteria:
- Provides an app factory under `src/`
- Initializes a SQLite schema for bookmarks
- Supports create, list, update, and delete routes
- Validates required URL/title fields
- Includes pytest tests using a temporary database
- [ ] TASK-002: Tags and filtering
Dependencies:
- TASK-001
Description:
Add tags to bookmarks and support filtering bookmarks by tag.
Acceptance Criteria:
- Stores tags in SQLite
- Supports assigning multiple tags to a bookmark
- Supports filtering list output by tag
- Includes model and route tests
- [ ] TASK-003: Import/export
Dependencies:
- TASK-002
Description:
Add JSON import and export endpoints for bookmark backups.
Acceptance Criteria:
- Exports all bookmarks and tags as JSON
- Imports valid JSON backup payloads
- Rejects malformed imports without corrupting existing data
- Includes tests for export and import behavior

View File

@ -0,0 +1,14 @@
# Real Simple Template: Bookmark API
Use this template for a short, practical model-backed NightShift run.
```bash
nightshift init --template real-simple --root bookmarks-demo
cd bookmarks-demo
python -m pip install flask pytest
nightshift run --task TASK-001
```
The model is `qwen2.5-coder:14b` for planning, implementation, and review. The target is intentionally modest: a Flask + SQLite bookmark API with pytest coverage.
NightShift files live in `.nightshift/`. Application code should be created under `src/`, and tests under `tests/`.

View File

@ -0,0 +1,96 @@
project:
name: simple-bookmarks
root: .
task_file: .nightshift/tasks.md
artifact_dir: .nightshift
safety:
require_clean_worktree: false
scoped_paths:
- src
- tests
- pyproject.toml
- README.md
allowed_commands:
- python -m pytest -q
forbidden_commands:
- rm -rf
- git push
- curl | bash
experiment:
label: real-simple-qwen25
prompt_variant: qwen2.5-coder-14b-file-writer-v1
agents:
planner:
backend: ollama
model: qwen2.5-coder:14b
temperature: 0.2
system_prompt: .nightshift/agents/planner.md
implementer:
backend: ollama
model: qwen2.5-coder:14b
temperature: 0.1
system_prompt: .nightshift/agents/implementer.md
reviewer:
backend: ollama
model: qwen2.5-coder:14b
temperature: 0.1
system_prompt: .nightshift/agents/reviewer.md
pipeline:
max_task_retries: 2
continue_on_task_failure: false
stages:
- id: plan
type: agent
agent: planner
output: plan.md
- id: context
type: repo_context
output: context-pack.md
- id: implement
type: file_writer
agent: implementer
output: proposed.patch
- id: normalize
type: patch_normalizer
output: normalized.patch
- id: validate_patch
type: patch_validator
output: patch-validation.md
max_files: 8
max_lines: 700
on_fail: implement
- id: apply_patch
type: patch_apply
mode: apply
output: patch-apply-output.txt
on_fail: implement
- id: test
type: command
commands:
- python -m pytest -q
output: test-output.txt
shell: true
timeout_seconds: 20
on_fail: implement
- id: review
type: agent_review
agent: reviewer
output: review.md
on_fail: implement
- id: summarize
type: summarize
output: final-notes.md

View File

@ -0,0 +1 @@

View File

@ -1,13 +1,11 @@
You are the implementation agent for NightShift. You are the implementation agent for NightShift.
Output only complete file content blocks. Output only complete file content blocks.
Use one fenced block per changed file: Use one fenced block per file with this exact opening form:
```file:relative/path.py ```file:relative/path.py
<complete file content> <complete file content>
``` ```
Do not include explanations before or after the file blocks.
Do not include explanations before or after the patch.
Include tests when needed. Include tests when needed.
Keep the change as small as possible. Keep the change as small as possible.
Only edit files needed for the task. Only edit files needed for the task.

View File

@ -0,0 +1,72 @@
# Tasks
- [ ] TASK-001: Board and thread foundation
Description:
Create the initial Flask imageboard application. Implement the board and thread data model, SQLite schema, model helpers, `/board/<name>` and `/thread/<id>` routes, and tests. Keep source code under `src/`, tests under `tests/`, HTML templates under `templates/`, and static files under `static/`.
Acceptance Criteria:
- Defines SQLite tables for boards, threads, and replies
- Provides database initialization and model helper functions
- Implements `/board/<name>` route showing threads for that board
- Implements `/thread/<id>` route showing the thread and replies
- Includes route and model tests using a temporary database
- [ ] TASK-002: Image upload and thumbnails
Dependencies:
- TASK-001
Description:
Add image attachment support for new threads and replies. Store uploaded image metadata in SQLite, save uploaded files under `static/uploads`, and generate thumbnails under `static/thumbs`.
Acceptance Criteria:
- Accepts image uploads for threads and replies
- Stores image filename, thumbnail filename, MIME type, and size
- Generates thumbnails with Pillow
- Rejects unsupported or oversized files
- Includes upload and thumbnail tests
- [ ] TASK-003: Bump ordering and reply counts
Dependencies:
- TASK-002
Description:
Sort board threads by most recent bump. Creating a reply updates the thread bump timestamp and increments reply counters.
Acceptance Criteria:
- Board pages sort threads by latest bump time
- Replies increment thread reply count
- Reply creation updates bump timestamp
- Tests cover ordering and counters
- [ ] TASK-004: Tripcodes and session cookies
Dependencies:
- TASK-003
Description:
Add anonymous names, optional tripcodes, and a session cookie for lightweight poster identity.
Acceptance Criteria:
- Supports optional name and tripcode input
- Stores tripcode hashes without storing raw tripcode secrets
- Sets and reuses a poster session cookie
- Displays stable poster identity on posts
- Includes tripcode and session tests
- [ ] TASK-005: Moderation and report queue
Dependencies:
- TASK-004
Description:
Add post reporting and a simple moderation queue. Moderators can view reports, dismiss reports, and hide reported posts.
Acceptance Criteria:
- Users can report threads and replies
- Reports are stored with reason and timestamp
- Moderation queue lists open reports
- Moderation actions can dismiss reports or hide posts
- Includes moderation and report queue tests

View File

@ -0,0 +1,27 @@
# NightShift Imageboard Target
This project was created with:
```bash
nightshift init --template tutorial-imageboard
```
NightShift control files live in `.nightshift/`. Target application code should live under `src/`, tests under `tests/`, templates under `templates/`, and uploaded/generated static files under `static/`.
Install target dependencies:
```bash
python -m pip install flask pillow pytest
```
Validate the project:
```bash
nightshift validate
```
Run the first task:
```bash
nightshift run --task TASK-001
```

View File

@ -0,0 +1,98 @@
project:
name: imageboard
root: .
task_file: .nightshift/tasks.md
artifact_dir: .nightshift
safety:
require_clean_worktree: false
scoped_paths:
- src
- tests
- templates
- static
- schema.sql
- pyproject.toml
allowed_commands:
- python -m pytest -q
forbidden_commands:
- rm -rf
- git push
- curl | bash
experiment:
label: imageboard-real-model
prompt_variant: ollama-qwen25-coder-14b-v1
agents:
planner:
backend: ollama
model: qwen2.5-coder:14b
temperature: 0.2
system_prompt: .nightshift/agents/planner.md
implementer:
backend: ollama
model: qwen2.5-coder:14b
temperature: 0.1
system_prompt: .nightshift/agents/implementer.md
reviewer:
backend: ollama
model: qwen2.5-coder:14b
temperature: 0.1
system_prompt: .nightshift/agents/reviewer.md
pipeline:
max_task_retries: 3
continue_on_task_failure: false
stages:
- id: plan
type: agent
agent: planner
output: plan.md
- id: context
type: repo_context
output: context-pack.md
- id: implement
type: file_writer
agent: implementer
output: proposed.patch
- id: normalize
type: patch_normalizer
output: normalized.patch
- id: validate_patch
type: patch_validator
output: patch-validation.md
max_files: 10
max_lines: 900
on_fail: implement
- id: apply_patch
type: patch_apply
mode: apply
output: patch-apply-output.txt
on_fail: implement
- id: test
type: command
commands:
- python -m pytest -q
output: test-output.txt
shell: true
timeout_seconds: 20
on_fail: implement
- id: review
type: agent_review
agent: reviewer
on_fail: implement
output: review.md
- id: summarize
type: summarize
output: final-notes.md

View File

@ -18,3 +18,6 @@ nightshift = "nightshift.cli:main"
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
include = ["nightshift*"] include = ["nightshift*"]
[tool.setuptools.package-data]
nightshift = ["project_templates/**/*"]

View File

@ -3,7 +3,7 @@ import tempfile
import unittest import unittest
from nightshift.errors import InitError from nightshift.errors import InitError
from nightshift.init import init_project from nightshift.init import available_templates, init_project
class InitProjectTests(unittest.TestCase): class InitProjectTests(unittest.TestCase):
@ -42,7 +42,7 @@ class InitProjectTests(unittest.TestCase):
with tempfile.TemporaryDirectory() as directory: with tempfile.TemporaryDirectory() as directory:
root = Path(directory) root = Path(directory)
written = init_project(root, template="imageboard") written = init_project(root, template="tutorial-imageboard")
self.assertIn(root / "nightshift.yaml", written) self.assertIn(root / "nightshift.yaml", written)
self.assertTrue((root / ".nightshift" / "tasks.md").exists()) self.assertTrue((root / ".nightshift" / "tasks.md").exists())
@ -54,6 +54,12 @@ class InitProjectTests(unittest.TestCase):
(root / "nightshift.yaml").read_text(encoding="utf-8"), (root / "nightshift.yaml").read_text(encoding="utf-8"),
) )
def test_available_templates_includes_filesystem_templates(self) -> None:
self.assertIn("basic", available_templates())
self.assertIn("real-long-running", available_templates())
self.assertIn("real-simple", available_templates())
self.assertIn("tutorial-imageboard", available_templates())
def test_init_rejects_unknown_template(self) -> None: def test_init_rejects_unknown_template(self) -> None:
with tempfile.TemporaryDirectory() as directory: with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(InitError, "Unknown template"): with self.assertRaisesRegex(InitError, "Unknown template"):