worklog-opsdevnz 0.1.3__tar.gz → 0.2.1__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.
- {worklog_opsdevnz-0.1.3/src/worklog_opsdevnz.egg-info → worklog_opsdevnz-0.2.1}/PKG-INFO +6 -1
- {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/README.md +4 -0
- {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/pyproject.toml +7 -1
- {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/src/worklog_opsdevnz/cli.py +19 -3
- worklog_opsdevnz-0.2.1/src/worklog_opsdevnz/paths.py +73 -0
- {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/src/worklog_opsdevnz/template.py +37 -10
- {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1/src/worklog_opsdevnz.egg-info}/PKG-INFO +6 -1
- {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/src/worklog_opsdevnz.egg-info/requires.txt +1 -0
- worklog_opsdevnz-0.2.1/tests/test_cli.py +137 -0
- worklog_opsdevnz-0.2.1/tests/test_paths.py +79 -0
- worklog_opsdevnz-0.2.1/tests/test_template.py +285 -0
- worklog_opsdevnz-0.1.3/src/worklog_opsdevnz/paths.py +0 -28
- worklog_opsdevnz-0.1.3/tests/test_cli.py +0 -71
- worklog_opsdevnz-0.1.3/tests/test_paths.py +0 -29
- worklog_opsdevnz-0.1.3/tests/test_template.py +0 -99
- {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/LICENSE +0 -0
- {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/setup.cfg +0 -0
- {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/src/worklog_opsdevnz/__init__.py +0 -0
- {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/src/worklog_opsdevnz/config.py +0 -0
- {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/src/worklog_opsdevnz.egg-info/SOURCES.txt +0 -0
- {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/src/worklog_opsdevnz.egg-info/dependency_links.txt +0 -0
- {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/src/worklog_opsdevnz.egg-info/entry_points.txt +0 -0
- {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/src/worklog_opsdevnz.egg-info/top_level.txt +0 -0
- {worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/tests/test_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: worklog-opsdevnz
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Configurable worklog management CLI for dated development logs
|
|
5
5
|
Author-email: "OpsDev.nz Collective" <john@opsdev.nz>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -29,6 +29,7 @@ Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
|
29
29
|
Requires-Dist: pytest-mock>=3.10; extra == "dev"
|
|
30
30
|
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
31
31
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: zensical>=0.0.44; extra == "dev"
|
|
32
33
|
Dynamic: license-file
|
|
33
34
|
|
|
34
35
|
# worklog-opsdevnz
|
|
@@ -49,6 +50,9 @@ worklog-opsdevnz --version
|
|
|
49
50
|
|
|
50
51
|
# Override the editor for this run
|
|
51
52
|
worklog-opsdevnz --editor nvim
|
|
53
|
+
|
|
54
|
+
# Open the most recent previous entry
|
|
55
|
+
worklog-opsdevnz -p
|
|
52
56
|
```
|
|
53
57
|
|
|
54
58
|
## Configuration
|
|
@@ -84,6 +88,7 @@ title = "Next"
|
|
|
84
88
|
- Configurable section headers per project
|
|
85
89
|
- Custom body templates with `{{DATE}}` and `{{TITLE}}` placeholders
|
|
86
90
|
- Editor integration: `-e` flag → config → `$VISUAL` → `$EDITOR`
|
|
91
|
+
- `-p` / `--previous` flag to open the most recent past entry
|
|
87
92
|
- `--version` flag for installed version
|
|
88
93
|
|
|
89
94
|
## Requirements
|
|
@@ -16,6 +16,9 @@ worklog-opsdevnz --version
|
|
|
16
16
|
|
|
17
17
|
# Override the editor for this run
|
|
18
18
|
worklog-opsdevnz --editor nvim
|
|
19
|
+
|
|
20
|
+
# Open the most recent previous entry
|
|
21
|
+
worklog-opsdevnz -p
|
|
19
22
|
```
|
|
20
23
|
|
|
21
24
|
## Configuration
|
|
@@ -51,6 +54,7 @@ title = "Next"
|
|
|
51
54
|
- Configurable section headers per project
|
|
52
55
|
- Custom body templates with `{{DATE}}` and `{{TITLE}}` placeholders
|
|
53
56
|
- Editor integration: `-e` flag → config → `$VISUAL` → `$EDITOR`
|
|
57
|
+
- `-p` / `--previous` flag to open the most recent past entry
|
|
54
58
|
- `--version` flag for installed version
|
|
55
59
|
|
|
56
60
|
## Requirements
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "worklog-opsdevnz"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Configurable worklog management CLI for dated development logs"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
@@ -37,6 +37,7 @@ dev = [
|
|
|
37
37
|
"pytest-mock>=3.10",
|
|
38
38
|
"ruff>=0.4",
|
|
39
39
|
"mypy>=1.0",
|
|
40
|
+
"zensical>=0.0.44",
|
|
40
41
|
]
|
|
41
42
|
|
|
42
43
|
[project.urls]
|
|
@@ -64,6 +65,11 @@ addopts = "-v --cov=src/worklog_opsdevnz --cov-report=term-missing"
|
|
|
64
65
|
line-length = 88
|
|
65
66
|
target-version = "py312"
|
|
66
67
|
|
|
68
|
+
# Explicit rule set (ruff's historical defaults) so CI is stable across
|
|
69
|
+
# ruff upgrades — DTZ/I/etc. are intentionally not enabled: local-date
|
|
70
|
+
# handling is this tool's core domain, not an oversight.
|
|
71
|
+
lint.select = ["E4", "E7", "E9", "F"]
|
|
72
|
+
|
|
67
73
|
[tool.mypy]
|
|
68
74
|
python_version = "3.12"
|
|
69
75
|
warn_return_any = true
|
|
@@ -9,7 +9,7 @@ import click
|
|
|
9
9
|
|
|
10
10
|
from worklog_opsdevnz import __version__
|
|
11
11
|
from worklog_opsdevnz.config import get_config
|
|
12
|
-
from worklog_opsdevnz.paths import resolve_path
|
|
12
|
+
from worklog_opsdevnz.paths import find_previous, resolve_path
|
|
13
13
|
from worklog_opsdevnz.template import generate_content
|
|
14
14
|
|
|
15
15
|
|
|
@@ -21,11 +21,27 @@ from worklog_opsdevnz.template import generate_content
|
|
|
21
21
|
default=None,
|
|
22
22
|
help="Override the editor command (overrides config, $VISUAL, $EDITOR).",
|
|
23
23
|
)
|
|
24
|
-
|
|
25
|
-
""
|
|
24
|
+
@click.option(
|
|
25
|
+
"-p",
|
|
26
|
+
"--previous",
|
|
27
|
+
is_flag=True,
|
|
28
|
+
help="Open the most recent worklog entry before today (never creates).",
|
|
29
|
+
)
|
|
30
|
+
def main(editor: str | None, previous: bool) -> None:
|
|
31
|
+
"""Create or open today's worklog entry, or the previous one with -p."""
|
|
26
32
|
entry_date = date.today()
|
|
27
33
|
|
|
28
34
|
config = get_config()
|
|
35
|
+
|
|
36
|
+
if previous:
|
|
37
|
+
target = find_previous(config, entry_date)
|
|
38
|
+
if target is None:
|
|
39
|
+
click.echo("No previous worklog entry found.", err=True)
|
|
40
|
+
return
|
|
41
|
+
print(f"Opening previous worklog: {target}")
|
|
42
|
+
_open_editor(target, editor, config.get("editor"))
|
|
43
|
+
return
|
|
44
|
+
|
|
29
45
|
target = resolve_path(config, entry_date)
|
|
30
46
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
31
47
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Path resolution for worklog files."""
|
|
2
|
+
|
|
3
|
+
from datetime import date, datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def resolve_path(
|
|
9
|
+
config: dict[str, Any],
|
|
10
|
+
entry_date: date,
|
|
11
|
+
) -> Path:
|
|
12
|
+
"""Compute the target file path based on config structure."""
|
|
13
|
+
base = Path(config.get("worklog_dir", "docs/worklog"))
|
|
14
|
+
structure = config.get("structure", "year")
|
|
15
|
+
suffix = config.get("suffix", "worklog")
|
|
16
|
+
|
|
17
|
+
if structure == "flat":
|
|
18
|
+
filename = f"{entry_date.isoformat()}-{suffix}.md"
|
|
19
|
+
return base / filename
|
|
20
|
+
elif structure == "year-month":
|
|
21
|
+
year = entry_date.strftime("%Y")
|
|
22
|
+
month = entry_date.strftime("%m")
|
|
23
|
+
filename = f"{entry_date.strftime('%d-%m-%Y')}-{suffix}.md"
|
|
24
|
+
return base / year / month / filename
|
|
25
|
+
else: # "year" (default)
|
|
26
|
+
year = entry_date.strftime("%Y")
|
|
27
|
+
filename = f"{entry_date.strftime('%d-%m-%Y')}-{suffix}.md"
|
|
28
|
+
return base / year / filename
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_DATE_FORMATS = ("%Y-%m-%d", "%d-%m-%Y")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _parse_entry_date(filename: str, suffix: str) -> date | None:
|
|
35
|
+
"""Extract the entry date from a worklog filename, or None if not parseable."""
|
|
36
|
+
stem = filename.removesuffix(".md")
|
|
37
|
+
prefix = stem.removesuffix(f"-{suffix}")
|
|
38
|
+
for fmt in _DATE_FORMATS:
|
|
39
|
+
try:
|
|
40
|
+
return datetime.strptime(prefix, fmt).date()
|
|
41
|
+
except ValueError:
|
|
42
|
+
continue
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def find_previous(config: dict[str, Any], today: date) -> Path | None:
|
|
47
|
+
"""Locate the most recent worklog entry strictly before *today*.
|
|
48
|
+
|
|
49
|
+
Scans the directory tree implied by the configured structure mode
|
|
50
|
+
(flat, year, or year-month) for files matching ``*-{suffix}.md``,
|
|
51
|
+
parses their dates, and returns the newest one older than today.
|
|
52
|
+
Returns None when no previous entry exists. Never creates anything.
|
|
53
|
+
"""
|
|
54
|
+
base = Path(config.get("worklog_dir", "docs/worklog"))
|
|
55
|
+
suffix = config.get("suffix", "worklog")
|
|
56
|
+
structure = config.get("structure", "year")
|
|
57
|
+
|
|
58
|
+
pattern = f"*-{suffix}.md"
|
|
59
|
+
if structure == "flat":
|
|
60
|
+
candidates = base.glob(pattern)
|
|
61
|
+
elif structure == "year-month":
|
|
62
|
+
candidates = base.glob(f"*/*/{pattern}")
|
|
63
|
+
else: # "year" (default)
|
|
64
|
+
candidates = base.glob(f"*/{pattern}")
|
|
65
|
+
|
|
66
|
+
best: tuple[date, Path] | None = None
|
|
67
|
+
for path in candidates:
|
|
68
|
+
entry_date = _parse_entry_date(path.name, suffix)
|
|
69
|
+
if entry_date is None or entry_date >= today:
|
|
70
|
+
continue
|
|
71
|
+
if best is None or entry_date > best[0]:
|
|
72
|
+
best = (entry_date, path)
|
|
73
|
+
return best[1] if best else None
|
|
@@ -36,13 +36,39 @@ def generate_body(config: dict[str, Any]) -> str:
|
|
|
36
36
|
return "\n".join(lines)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def
|
|
40
|
-
"""Render
|
|
39
|
+
def _render_tags(tags: list[str]) -> str:
|
|
40
|
+
"""Render tags as a block-style YAML list substitution.
|
|
41
|
+
|
|
42
|
+
Empty list → '[]'. Populated list → leading newline followed by
|
|
43
|
+
each tag on its own indented line with '-' prefix.
|
|
44
|
+
"""
|
|
45
|
+
if not tags:
|
|
46
|
+
return "[]"
|
|
47
|
+
return "\n" + "\n".join(f" - {t}" for t in tags)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def render_template(
|
|
51
|
+
template_path: str,
|
|
52
|
+
iso_date: str,
|
|
53
|
+
config: dict[str, Any],
|
|
54
|
+
) -> str:
|
|
55
|
+
"""Render a custom Markdown template with placeholder substitution.
|
|
56
|
+
|
|
57
|
+
Supports {{DATE}}, {{TITLE}}, {{AUTHOR}}, and {{TAGS}} placeholders.
|
|
58
|
+
All placeholders are case-sensitive.
|
|
59
|
+
"""
|
|
41
60
|
title = f"Work Log - {iso_date}"
|
|
61
|
+
tags = _render_tags(config.get("default_tags", []))
|
|
62
|
+
author = config.get("author", "unknown")
|
|
42
63
|
with open(template_path) as f:
|
|
43
64
|
content = f.read()
|
|
44
65
|
content = content.replace("{{DATE}}", iso_date)
|
|
45
66
|
content = content.replace("{{TITLE}}", title)
|
|
67
|
+
content = content.replace("{{AUTHOR}}", author)
|
|
68
|
+
content = content.replace("{{TAGS}}", tags)
|
|
69
|
+
# Clean trailing whitespace (avoids artifacts when placeholder
|
|
70
|
+
# substitution leaves space before a newline, e.g. 'tags: \n')
|
|
71
|
+
content = "\n".join(line.rstrip() for line in content.split("\n"))
|
|
46
72
|
return content
|
|
47
73
|
|
|
48
74
|
|
|
@@ -52,16 +78,17 @@ def generate_content(
|
|
|
52
78
|
) -> str:
|
|
53
79
|
"""Generate full worklog content.
|
|
54
80
|
|
|
55
|
-
If a 'template' field is set in config,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"""
|
|
59
|
-
frontmatter = generate_frontmatter(config, iso_date)
|
|
81
|
+
If a 'template' field is set in config, the template defines the
|
|
82
|
+
complete entry (including YAML frontmatter). Placeholders are
|
|
83
|
+
substituted but nothing else is prepended.
|
|
60
84
|
|
|
85
|
+
Otherwise uses the built-in default: config-driven frontmatter and
|
|
86
|
+
sections-based body.
|
|
87
|
+
"""
|
|
61
88
|
template_path = config.get("template")
|
|
62
89
|
if template_path:
|
|
63
|
-
|
|
64
|
-
else:
|
|
65
|
-
body = generate_body(config)
|
|
90
|
+
return render_template(template_path, iso_date, config)
|
|
66
91
|
|
|
92
|
+
frontmatter = generate_frontmatter(config, iso_date)
|
|
93
|
+
body = generate_body(config)
|
|
67
94
|
return f"{frontmatter}\n{body}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: worklog-opsdevnz
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Configurable worklog management CLI for dated development logs
|
|
5
5
|
Author-email: "OpsDev.nz Collective" <john@opsdev.nz>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -29,6 +29,7 @@ Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
|
29
29
|
Requires-Dist: pytest-mock>=3.10; extra == "dev"
|
|
30
30
|
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
31
31
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: zensical>=0.0.44; extra == "dev"
|
|
32
33
|
Dynamic: license-file
|
|
33
34
|
|
|
34
35
|
# worklog-opsdevnz
|
|
@@ -49,6 +50,9 @@ worklog-opsdevnz --version
|
|
|
49
50
|
|
|
50
51
|
# Override the editor for this run
|
|
51
52
|
worklog-opsdevnz --editor nvim
|
|
53
|
+
|
|
54
|
+
# Open the most recent previous entry
|
|
55
|
+
worklog-opsdevnz -p
|
|
52
56
|
```
|
|
53
57
|
|
|
54
58
|
## Configuration
|
|
@@ -84,6 +88,7 @@ title = "Next"
|
|
|
84
88
|
- Configurable section headers per project
|
|
85
89
|
- Custom body templates with `{{DATE}}` and `{{TITLE}}` placeholders
|
|
86
90
|
- Editor integration: `-e` flag → config → `$VISUAL` → `$EDITOR`
|
|
91
|
+
- `-p` / `--previous` flag to open the most recent past entry
|
|
87
92
|
- `--version` flag for installed version
|
|
88
93
|
|
|
89
94
|
## Requirements
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Tests for CLI entry point."""
|
|
2
|
+
|
|
3
|
+
from datetime import date, timedelta
|
|
4
|
+
|
|
5
|
+
from click.testing import CliRunner
|
|
6
|
+
|
|
7
|
+
from worklog_opsdevnz.cli import main
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_main_creates_file(tmp_path, monkeypatch):
|
|
11
|
+
"""Running with no args creates today's worklog in tmp_path."""
|
|
12
|
+
monkeypatch.chdir(tmp_path)
|
|
13
|
+
runner = CliRunner()
|
|
14
|
+
result = runner.invoke(main, [])
|
|
15
|
+
assert result.exit_code == 0
|
|
16
|
+
assert "Created" in result.output
|
|
17
|
+
expected_leaf = (
|
|
18
|
+
f"docs/worklog/{date.today().year}/{date.today():%d-%m-%Y}-worklog.md"
|
|
19
|
+
)
|
|
20
|
+
assert expected_leaf in result.output
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_main_opens_existing(tmp_path, monkeypatch):
|
|
24
|
+
"""Running again opens the existing file."""
|
|
25
|
+
monkeypatch.chdir(tmp_path)
|
|
26
|
+
runner = CliRunner()
|
|
27
|
+
runner.invoke(main, [])
|
|
28
|
+
result = runner.invoke(main, [])
|
|
29
|
+
assert result.exit_code == 0
|
|
30
|
+
assert "Opening existing" in result.output
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_main_help():
|
|
34
|
+
"""--help works."""
|
|
35
|
+
runner = CliRunner()
|
|
36
|
+
result = runner.invoke(main, ["--help"])
|
|
37
|
+
assert result.exit_code == 0
|
|
38
|
+
assert "--editor" in result.output
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_main_version():
|
|
42
|
+
"""--version prints the installed version."""
|
|
43
|
+
runner = CliRunner()
|
|
44
|
+
result = runner.invoke(main, ["--version"])
|
|
45
|
+
assert result.exit_code == 0
|
|
46
|
+
assert "worklog-opsdevnz, version" in result.output
|
|
47
|
+
# Extract and verify it's a non-empty version string
|
|
48
|
+
version_str = result.output.strip().split(", version ")[-1]
|
|
49
|
+
assert version_str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_main_editor_from_config(tmp_path, monkeypatch):
|
|
53
|
+
"""Editor in worklog.toml is resolved before $VISUAL/$EDITOR."""
|
|
54
|
+
monkeypatch.chdir(tmp_path)
|
|
55
|
+
(tmp_path / "worklog.toml").write_text('editor = "fake-editor-test"\n')
|
|
56
|
+
runner = CliRunner()
|
|
57
|
+
result = runner.invoke(main, [])
|
|
58
|
+
assert result.exit_code == 0
|
|
59
|
+
# config-based editor picked up but not found in PATH
|
|
60
|
+
assert "Editor 'fake-editor-test' not found" in result.output
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_main_editor_cli_overrides_config(tmp_path, monkeypatch):
|
|
64
|
+
"""-e flag overrides the editor set in worklog.toml."""
|
|
65
|
+
monkeypatch.chdir(tmp_path)
|
|
66
|
+
(tmp_path / "worklog.toml").write_text('editor = "fake-editor-test"\n')
|
|
67
|
+
runner = CliRunner()
|
|
68
|
+
result = runner.invoke(main, ["-e", "override-editor-test"])
|
|
69
|
+
assert result.exit_code == 0
|
|
70
|
+
# CLI override takes priority over config
|
|
71
|
+
assert "Editor 'override-editor-test' not found" in result.output
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_previous_no_entries(tmp_path, monkeypatch):
|
|
75
|
+
"""-p with no prior entries: message to stderr, exit 0, creates nothing."""
|
|
76
|
+
monkeypatch.chdir(tmp_path)
|
|
77
|
+
runner = CliRunner()
|
|
78
|
+
result = runner.invoke(main, ["-p"])
|
|
79
|
+
assert result.exit_code == 0
|
|
80
|
+
assert "No previous worklog entry found." in result.stderr
|
|
81
|
+
assert not (tmp_path / "docs").exists()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_previous_opens_most_recent_before_today(tmp_path, monkeypatch):
|
|
85
|
+
"""-p opens the newest prior entry, not today's, and creates no file."""
|
|
86
|
+
monkeypatch.chdir(tmp_path)
|
|
87
|
+
today = date.today()
|
|
88
|
+
year_dir = tmp_path / "docs" / "worklog" / str(today.year)
|
|
89
|
+
year_dir.mkdir(parents=True)
|
|
90
|
+
older = (today - timedelta(days=3)).strftime("%d-%m-%Y")
|
|
91
|
+
recent = (today - timedelta(days=1)).strftime("%d-%m-%Y")
|
|
92
|
+
(year_dir / f"{older}-worklog.md").write_text("older")
|
|
93
|
+
(year_dir / f"{recent}-worklog.md").write_text("recent")
|
|
94
|
+
today_file = year_dir / f"{today:%d-%m-%Y}-worklog.md"
|
|
95
|
+
today_file.write_text("today")
|
|
96
|
+
|
|
97
|
+
runner = CliRunner()
|
|
98
|
+
result = runner.invoke(main, ["--previous"])
|
|
99
|
+
assert result.exit_code == 0
|
|
100
|
+
assert recent in result.output
|
|
101
|
+
assert today.strftime("%d-%m-%Y") not in result.output
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_previous_does_not_create_today(tmp_path, monkeypatch):
|
|
105
|
+
"""-p must not create today's entry even though it does not exist."""
|
|
106
|
+
monkeypatch.chdir(tmp_path)
|
|
107
|
+
today = date.today()
|
|
108
|
+
year_dir = tmp_path / "docs" / "worklog" / str(today.year)
|
|
109
|
+
year_dir.mkdir(parents=True)
|
|
110
|
+
(year_dir / f"{(today - timedelta(days=2)):%d-%m-%Y}-worklog.md").write_text("x")
|
|
111
|
+
|
|
112
|
+
runner = CliRunner()
|
|
113
|
+
runner.invoke(main, ["-p"])
|
|
114
|
+
assert not (year_dir / f"{today:%d-%m-%Y}-worklog.md").exists()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_previous_editor_override(tmp_path, monkeypatch):
|
|
118
|
+
"""-p combined with -e applies the editor override to the previous entry."""
|
|
119
|
+
monkeypatch.chdir(tmp_path)
|
|
120
|
+
today = date.today()
|
|
121
|
+
year_dir = tmp_path / "docs" / "worklog" / str(today.year)
|
|
122
|
+
year_dir.mkdir(parents=True)
|
|
123
|
+
recent = (today - timedelta(days=1)).strftime("%d-%m-%Y")
|
|
124
|
+
(year_dir / f"{recent}-worklog.md").write_text("recent")
|
|
125
|
+
|
|
126
|
+
runner = CliRunner()
|
|
127
|
+
result = runner.invoke(main, ["-p", "-e", "override-editor-test"])
|
|
128
|
+
assert result.exit_code == 0
|
|
129
|
+
assert "Editor 'override-editor-test' not found" in result.output
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_previous_help():
|
|
133
|
+
"""--help mentions the --previous flag."""
|
|
134
|
+
runner = CliRunner()
|
|
135
|
+
result = runner.invoke(main, ["--help"])
|
|
136
|
+
assert result.exit_code == 0
|
|
137
|
+
assert "--previous" in result.output
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Tests for path resolution."""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
|
|
5
|
+
from worklog_opsdevnz.paths import find_previous, resolve_path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_resolve_path_flat():
|
|
9
|
+
config = {"worklog_dir": "docs/worklog", "structure": "flat", "suffix": "worklog"}
|
|
10
|
+
path = resolve_path(config, date(2026, 5, 23))
|
|
11
|
+
assert str(path) == "docs/worklog/2026-05-23-worklog.md"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_resolve_path_year():
|
|
15
|
+
config = {"worklog_dir": "logs", "structure": "year", "suffix": "log"}
|
|
16
|
+
path = resolve_path(config, date(2026, 5, 23))
|
|
17
|
+
assert str(path) == "logs/2026/23-05-2026-log.md"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_resolve_path_year_month():
|
|
21
|
+
config = {"worklog_dir": "logs", "structure": "year-month", "suffix": "ops"}
|
|
22
|
+
path = resolve_path(config, date(2026, 5, 23))
|
|
23
|
+
assert str(path) == "logs/2026/05/23-05-2026-ops.md"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_resolve_path_custom_suffix():
|
|
27
|
+
config = {"structure": "year", "suffix": "retro"}
|
|
28
|
+
path = resolve_path(config, date(2026, 1, 1))
|
|
29
|
+
assert str(path).endswith("-retro.md")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_find_previous_year(tmp_path):
|
|
33
|
+
base = tmp_path / "logs" / "2026"
|
|
34
|
+
base.mkdir(parents=True)
|
|
35
|
+
(base / "28-08-2026-worklog.md").write_text("old")
|
|
36
|
+
(base / "01-09-2026-worklog.md").write_text("prev")
|
|
37
|
+
(base / "03-09-2026-worklog.md").write_text("today")
|
|
38
|
+
config = {"worklog_dir": str(tmp_path / "logs"), "structure": "year", "suffix": "worklog"}
|
|
39
|
+
result = find_previous(config, date(2026, 9, 3))
|
|
40
|
+
assert result is not None
|
|
41
|
+
assert result.name == "01-09-2026-worklog.md"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_find_previous_year_month(tmp_path):
|
|
45
|
+
base = tmp_path / "docs" / "worklogs"
|
|
46
|
+
(base / "2026" / "08").mkdir(parents=True)
|
|
47
|
+
(base / "2026" / "09").mkdir(parents=True)
|
|
48
|
+
(base / "2026" / "08" / "27-08-2026-worklog.md").write_text("aug")
|
|
49
|
+
(base / "2026" / "09" / "01-09-2026-worklog.md").write_text("sep")
|
|
50
|
+
config = {"worklog_dir": str(base), "structure": "year-month", "suffix": "worklog"}
|
|
51
|
+
result = find_previous(config, date(2026, 9, 2))
|
|
52
|
+
assert result is not None
|
|
53
|
+
assert result.name == "01-09-2026-worklog.md"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_find_previous_flat_iso_names(tmp_path):
|
|
57
|
+
base = tmp_path / "wl"
|
|
58
|
+
base.mkdir()
|
|
59
|
+
(base / "2026-08-30-worklog.md").write_text("a")
|
|
60
|
+
(base / "2026-09-01-worklog.md").write_text("b")
|
|
61
|
+
config = {"worklog_dir": str(base), "structure": "flat", "suffix": "worklog"}
|
|
62
|
+
result = find_previous(config, date(2026, 9, 2))
|
|
63
|
+
assert result is not None
|
|
64
|
+
assert result.name == "2026-09-01-worklog.md"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_find_previous_ignores_today_unrelated_and_wrong_suffix(tmp_path):
|
|
68
|
+
base = tmp_path / "logs" / "2026"
|
|
69
|
+
base.mkdir(parents=True)
|
|
70
|
+
(base / "03-09-2026-worklog.md").write_text("today")
|
|
71
|
+
(base / "README.md").write_text("not an entry")
|
|
72
|
+
(base / "01-09-2026-journal.md").write_text("wrong suffix")
|
|
73
|
+
config = {"worklog_dir": str(tmp_path / "logs"), "structure": "year", "suffix": "worklog"}
|
|
74
|
+
assert find_previous(config, date(2026, 9, 3)) is None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_find_previous_missing_dir(tmp_path):
|
|
78
|
+
config = {"worklog_dir": str(tmp_path / "nothing"), "structure": "year", "suffix": "worklog"}
|
|
79
|
+
assert find_previous(config, date(2026, 9, 3)) is None
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""Tests for frontmatter and body generation."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from worklog_opsdevnz.template import (
|
|
6
|
+
generate_frontmatter,
|
|
7
|
+
generate_body,
|
|
8
|
+
generate_content,
|
|
9
|
+
render_template,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_generate_frontmatter():
|
|
14
|
+
config = {
|
|
15
|
+
"author": "Test Author",
|
|
16
|
+
"default_tags": ["internal", "test"],
|
|
17
|
+
}
|
|
18
|
+
fm = generate_frontmatter(config, "2026-05-23")
|
|
19
|
+
assert "title: " in fm
|
|
20
|
+
assert "Work Log - 2026-05-23" in fm
|
|
21
|
+
assert "date: 2026-05-23" in fm
|
|
22
|
+
assert "author: Test Author" in fm
|
|
23
|
+
assert " - internal" in fm
|
|
24
|
+
assert " - test" in fm
|
|
25
|
+
assert "draft: false" in fm
|
|
26
|
+
assert fm.startswith("---")
|
|
27
|
+
assert fm.strip().endswith("---")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_generate_body_with_sections():
|
|
31
|
+
config = {
|
|
32
|
+
"sections": [
|
|
33
|
+
{"title": "Focus", "content": ""},
|
|
34
|
+
{"title": "Notes", "content": "some note"},
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
body = generate_body(config)
|
|
38
|
+
assert "## Focus" in body
|
|
39
|
+
assert "## Notes" in body
|
|
40
|
+
assert "some note" in body
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_generate_content_full():
|
|
44
|
+
config = {
|
|
45
|
+
"author": "opsdev",
|
|
46
|
+
"default_tags": ["log"],
|
|
47
|
+
"sections": [{"title": "Today", "content": ""}],
|
|
48
|
+
}
|
|
49
|
+
content = generate_content(config, "2026-05-23")
|
|
50
|
+
assert content.startswith("---")
|
|
51
|
+
assert "Work Log - 2026-05-23" in content
|
|
52
|
+
assert "## Today" in content
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_render_template_with_placeholders(tmp_path):
|
|
56
|
+
template = tmp_path / "my-template.md"
|
|
57
|
+
template.write_text(
|
|
58
|
+
"---\n"
|
|
59
|
+
'title: "{{TITLE}}"\n'
|
|
60
|
+
"date: {{DATE}}\n"
|
|
61
|
+
"author: {{AUTHOR}}\n"
|
|
62
|
+
"tags: {{TAGS}}\n"
|
|
63
|
+
"draft: false\n"
|
|
64
|
+
"---\n\n"
|
|
65
|
+
"# {{TITLE}}\n\n"
|
|
66
|
+
"Date: {{DATE}}\n\n"
|
|
67
|
+
"Free-form notes."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
config = {"author": "opsdev", "default_tags": ["dev", "log"]}
|
|
71
|
+
result = render_template(str(template), "2026-05-26", config)
|
|
72
|
+
|
|
73
|
+
# Placeholders replaced
|
|
74
|
+
assert "{{DATE}}" not in result
|
|
75
|
+
assert "{{TITLE}}" not in result
|
|
76
|
+
assert "{{AUTHOR}}" not in result
|
|
77
|
+
assert "{{TAGS}}" not in result
|
|
78
|
+
|
|
79
|
+
# Content verified
|
|
80
|
+
assert "# Work Log - 2026-05-26" in result
|
|
81
|
+
assert "Date: 2026-05-26" in result
|
|
82
|
+
assert "Free-form notes." in result
|
|
83
|
+
assert 'title: "Work Log - 2026-05-26"' in result
|
|
84
|
+
assert "date: 2026-05-26" in result
|
|
85
|
+
assert "author: opsdev" in result
|
|
86
|
+
assert "draft: false" in result
|
|
87
|
+
|
|
88
|
+
# Tags rendered as block-style YAML
|
|
89
|
+
assert " - dev" in result
|
|
90
|
+
assert " - log" in result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_render_template_file_not_found():
|
|
94
|
+
with pytest.raises(FileNotFoundError):
|
|
95
|
+
render_template("/nonexistent/template.md", "2026-05-26", {})
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_generate_content_with_template(tmp_path):
|
|
99
|
+
"""When template is set, it defines the complete entry.
|
|
100
|
+
|
|
101
|
+
No auto-generated frontmatter is prepended.
|
|
102
|
+
"""
|
|
103
|
+
template = tmp_path / "my-template.md"
|
|
104
|
+
template.write_text(
|
|
105
|
+
"---\n"
|
|
106
|
+
'title: "{{TITLE}}"\n'
|
|
107
|
+
"date: {{DATE}}\n"
|
|
108
|
+
"author: {{AUTHOR}}\n"
|
|
109
|
+
"tags: {{TAGS}}\n"
|
|
110
|
+
"draft: false\n"
|
|
111
|
+
"---\n\n"
|
|
112
|
+
"# {{TITLE}}\n\n"
|
|
113
|
+
"Date: {{DATE}}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
config = {
|
|
117
|
+
"author": "opsdev",
|
|
118
|
+
"default_tags": ["log"],
|
|
119
|
+
"template": str(template),
|
|
120
|
+
}
|
|
121
|
+
content = generate_content(config, "2026-05-26")
|
|
122
|
+
|
|
123
|
+
# Template defines the full entry — its frontmatter is present
|
|
124
|
+
assert content.startswith("---")
|
|
125
|
+
assert 'title: "Work Log - 2026-05-26"' in content
|
|
126
|
+
assert "date: 2026-05-26" in content
|
|
127
|
+
assert "author: opsdev" in content
|
|
128
|
+
assert " - log" in content
|
|
129
|
+
assert "draft: false" in content
|
|
130
|
+
|
|
131
|
+
# Body comes from template
|
|
132
|
+
assert "# Work Log - 2026-05-26" in content
|
|
133
|
+
assert "Date: 2026-05-26" in content
|
|
134
|
+
|
|
135
|
+
# No duplicate frontmatter — only one '---' block
|
|
136
|
+
assert content.count("---") == 2 # opening + closing
|
|
137
|
+
|
|
138
|
+
# Sections are NOT present (template controls everything)
|
|
139
|
+
assert "## Focus for Today" not in content
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_generate_content_without_template():
|
|
143
|
+
"""No template field → built-in sections body, unchanged behaviour."""
|
|
144
|
+
config = {
|
|
145
|
+
"author": "opsdev",
|
|
146
|
+
"default_tags": ["log"],
|
|
147
|
+
"sections": [{"title": "Today", "content": ""}],
|
|
148
|
+
}
|
|
149
|
+
content = generate_content(config, "2026-05-26")
|
|
150
|
+
assert "## Today" in content
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ── {{TAGS}} rendering ────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_render_template_tags_empty(tmp_path):
|
|
157
|
+
"""Empty default_tags → {{TAGS}} substitutes as '[]'."""
|
|
158
|
+
template = tmp_path / "t.md"
|
|
159
|
+
template.write_text("tags: {{TAGS}}\n")
|
|
160
|
+
|
|
161
|
+
result = render_template(str(template), "2026-06-01", {"default_tags": []})
|
|
162
|
+
assert "tags: []" in result
|
|
163
|
+
assert "{{TAGS}}" not in result
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_render_template_tags_populated(tmp_path):
|
|
167
|
+
"""Populated default_tags → {{TAGS}} renders as block-style YAML list."""
|
|
168
|
+
template = tmp_path / "t.md"
|
|
169
|
+
template.write_text("tags: {{TAGS}}\n")
|
|
170
|
+
|
|
171
|
+
config = {"default_tags": ["dev", "log", "ops"]}
|
|
172
|
+
result = render_template(str(template), "2026-06-01", config)
|
|
173
|
+
|
|
174
|
+
# Block-style: leading newline + indented items, no trailing whitespace
|
|
175
|
+
assert "tags:\n - dev\n - log\n - ops" in result
|
|
176
|
+
assert "{{TAGS}}" not in result
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_render_template_tags_not_configured(tmp_path):
|
|
180
|
+
"""No default_tags in config → {{TAGS}} substitutes as '[]'."""
|
|
181
|
+
template = tmp_path / "t.md"
|
|
182
|
+
template.write_text("tags: {{TAGS}}\n")
|
|
183
|
+
|
|
184
|
+
result = render_template(str(template), "2026-06-01", {})
|
|
185
|
+
assert "tags: []" in result
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ── {{AUTHOR}} rendering ──────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_render_template_author_configured(tmp_path):
|
|
192
|
+
"""{{AUTHOR}} pulls the author value from config."""
|
|
193
|
+
template = tmp_path / "t.md"
|
|
194
|
+
template.write_text("author: {{AUTHOR}}\n")
|
|
195
|
+
|
|
196
|
+
result = render_template(str(template), "2026-06-01", {"author": "opsdev"})
|
|
197
|
+
assert "author: opsdev" in result
|
|
198
|
+
assert "{{AUTHOR}}" not in result
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_render_template_author_fallback(tmp_path):
|
|
202
|
+
"""{{AUTHOR}} falls back to 'unknown' when not in config."""
|
|
203
|
+
template = tmp_path / "t.md"
|
|
204
|
+
template.write_text("author: {{AUTHOR}}\n")
|
|
205
|
+
|
|
206
|
+
result = render_template(str(template), "2026-06-01", {})
|
|
207
|
+
assert "author: unknown" in result
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ── Full-entry template edge cases ────────────────────────────────
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_generate_content_template_defines_full_entry(tmp_path):
|
|
214
|
+
"""Template with custom YAML fields — the tool only substitutes placeholders."""
|
|
215
|
+
template = tmp_path / "custom.md"
|
|
216
|
+
template.write_text(
|
|
217
|
+
"---\n"
|
|
218
|
+
'title: "{{TITLE}}"\n'
|
|
219
|
+
"date: {{DATE}}\n"
|
|
220
|
+
"author: {{AUTHOR}}\n"
|
|
221
|
+
"tags: {{TAGS}}\n"
|
|
222
|
+
"mood: creative\n"
|
|
223
|
+
"project: outcome-engineering\n"
|
|
224
|
+
"draft: false\n"
|
|
225
|
+
"---\n\n"
|
|
226
|
+
"# {{TITLE}}\n\n"
|
|
227
|
+
"Custom body content."
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
config = {
|
|
231
|
+
"author": "opsdev",
|
|
232
|
+
"default_tags": ["dev"],
|
|
233
|
+
"template": str(template),
|
|
234
|
+
}
|
|
235
|
+
content = generate_content(config, "2026-06-05")
|
|
236
|
+
|
|
237
|
+
# Custom fields preserved verbatim
|
|
238
|
+
assert "mood: creative" in content
|
|
239
|
+
assert "project: outcome-engineering" in content
|
|
240
|
+
|
|
241
|
+
# Placeholders substituted
|
|
242
|
+
assert 'title: "Work Log - 2026-06-05"' in content
|
|
243
|
+
assert "date: 2026-06-05" in content
|
|
244
|
+
assert "author: opsdev" in content
|
|
245
|
+
assert " - dev" in content
|
|
246
|
+
|
|
247
|
+
# Body preserved
|
|
248
|
+
assert "Custom body content." in content
|
|
249
|
+
|
|
250
|
+
# No auto-generated frontmatter prepended
|
|
251
|
+
assert content.count("---") == 2 # only the template's opening/closing
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_generate_content_template_no_frontmatter(tmp_path):
|
|
255
|
+
"""Template without frontmatter — entry has no frontmatter at all."""
|
|
256
|
+
template = tmp_path / "body-only.md"
|
|
257
|
+
template.write_text("# {{TITLE}}\n\nJust body content.")
|
|
258
|
+
|
|
259
|
+
config = {"template": str(template)}
|
|
260
|
+
content = generate_content(config, "2026-06-05")
|
|
261
|
+
|
|
262
|
+
# No frontmatter markers, just the rendered template
|
|
263
|
+
assert "---" not in content
|
|
264
|
+
assert "# Work Log - 2026-06-05" in content
|
|
265
|
+
assert "Just body content." in content
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_render_template_case_sensitive(tmp_path):
|
|
269
|
+
"""Lowercase placeholders are NOT substituted."""
|
|
270
|
+
template = tmp_path / "t.md"
|
|
271
|
+
template.write_text(
|
|
272
|
+
"title: {{title}}\n"
|
|
273
|
+
"date: {{date}}\n"
|
|
274
|
+
"author: {{author}}\n"
|
|
275
|
+
"tags: {{tags}}\n"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
config = {"author": "opsdev", "default_tags": ["dev"]}
|
|
279
|
+
result = render_template(str(template), "2026-06-05", config)
|
|
280
|
+
|
|
281
|
+
# Lowercase placeholders left untouched
|
|
282
|
+
assert "{{title}}" in result
|
|
283
|
+
assert "{{date}}" in result
|
|
284
|
+
assert "{{author}}" in result
|
|
285
|
+
assert "{{tags}}" in result
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"""Path resolution for worklog files."""
|
|
2
|
-
|
|
3
|
-
from datetime import date
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def resolve_path(
|
|
9
|
-
config: dict[str, Any],
|
|
10
|
-
entry_date: date,
|
|
11
|
-
) -> Path:
|
|
12
|
-
"""Compute the target file path based on config structure."""
|
|
13
|
-
base = Path(config.get("worklog_dir", "docs/worklog"))
|
|
14
|
-
structure = config.get("structure", "year")
|
|
15
|
-
suffix = config.get("suffix", "worklog")
|
|
16
|
-
|
|
17
|
-
if structure == "flat":
|
|
18
|
-
filename = f"{entry_date.isoformat()}-{suffix}.md"
|
|
19
|
-
return base / filename
|
|
20
|
-
elif structure == "year-month":
|
|
21
|
-
year = entry_date.strftime("%Y")
|
|
22
|
-
month = entry_date.strftime("%m")
|
|
23
|
-
filename = f"{entry_date.strftime('%d-%m-%Y')}-{suffix}.md"
|
|
24
|
-
return base / year / month / filename
|
|
25
|
-
else: # "year" (default)
|
|
26
|
-
year = entry_date.strftime("%Y")
|
|
27
|
-
filename = f"{entry_date.strftime('%d-%m-%Y')}-{suffix}.md"
|
|
28
|
-
return base / year / filename
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"""Tests for CLI entry point."""
|
|
2
|
-
|
|
3
|
-
from datetime import date
|
|
4
|
-
|
|
5
|
-
from click.testing import CliRunner
|
|
6
|
-
|
|
7
|
-
from worklog_opsdevnz.cli import main
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_main_creates_file(tmp_path, monkeypatch):
|
|
11
|
-
"""Running with no args creates today's worklog in tmp_path."""
|
|
12
|
-
monkeypatch.chdir(tmp_path)
|
|
13
|
-
runner = CliRunner()
|
|
14
|
-
result = runner.invoke(main, [])
|
|
15
|
-
assert result.exit_code == 0
|
|
16
|
-
assert "Created" in result.output
|
|
17
|
-
expected_leaf = (
|
|
18
|
-
f"docs/worklog/{date.today().year}/{date.today():%d-%m-%Y}-worklog.md"
|
|
19
|
-
)
|
|
20
|
-
assert expected_leaf in result.output
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_main_opens_existing(tmp_path, monkeypatch):
|
|
24
|
-
"""Running again opens the existing file."""
|
|
25
|
-
monkeypatch.chdir(tmp_path)
|
|
26
|
-
runner = CliRunner()
|
|
27
|
-
runner.invoke(main, [])
|
|
28
|
-
result = runner.invoke(main, [])
|
|
29
|
-
assert result.exit_code == 0
|
|
30
|
-
assert "Opening existing" in result.output
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_main_help():
|
|
34
|
-
"""--help works."""
|
|
35
|
-
runner = CliRunner()
|
|
36
|
-
result = runner.invoke(main, ["--help"])
|
|
37
|
-
assert result.exit_code == 0
|
|
38
|
-
assert "--editor" in result.output
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def test_main_version():
|
|
42
|
-
"""--version prints the installed version."""
|
|
43
|
-
runner = CliRunner()
|
|
44
|
-
result = runner.invoke(main, ["--version"])
|
|
45
|
-
assert result.exit_code == 0
|
|
46
|
-
assert "worklog-opsdevnz, version" in result.output
|
|
47
|
-
# Extract and verify it's a non-empty version string
|
|
48
|
-
version_str = result.output.strip().split(", version ")[-1]
|
|
49
|
-
assert version_str
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def test_main_editor_from_config(tmp_path, monkeypatch):
|
|
53
|
-
"""Editor in worklog.toml is resolved before $VISUAL/$EDITOR."""
|
|
54
|
-
monkeypatch.chdir(tmp_path)
|
|
55
|
-
(tmp_path / "worklog.toml").write_text('editor = "fake-editor-test"\n')
|
|
56
|
-
runner = CliRunner()
|
|
57
|
-
result = runner.invoke(main, [])
|
|
58
|
-
assert result.exit_code == 0
|
|
59
|
-
# config-based editor picked up but not found in PATH
|
|
60
|
-
assert "Editor 'fake-editor-test' not found" in result.output
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def test_main_editor_cli_overrides_config(tmp_path, monkeypatch):
|
|
64
|
-
"""-e flag overrides the editor set in worklog.toml."""
|
|
65
|
-
monkeypatch.chdir(tmp_path)
|
|
66
|
-
(tmp_path / "worklog.toml").write_text('editor = "fake-editor-test"\n')
|
|
67
|
-
runner = CliRunner()
|
|
68
|
-
result = runner.invoke(main, ["-e", "override-editor-test"])
|
|
69
|
-
assert result.exit_code == 0
|
|
70
|
-
# CLI override takes priority over config
|
|
71
|
-
assert "Editor 'override-editor-test' not found" in result.output
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
"""Tests for path resolution."""
|
|
2
|
-
|
|
3
|
-
from datetime import date
|
|
4
|
-
|
|
5
|
-
from worklog_opsdevnz.paths import resolve_path
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_resolve_path_flat():
|
|
9
|
-
config = {"worklog_dir": "docs/worklog", "structure": "flat", "suffix": "worklog"}
|
|
10
|
-
path = resolve_path(config, date(2026, 5, 23))
|
|
11
|
-
assert str(path) == "docs/worklog/2026-05-23-worklog.md"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_resolve_path_year():
|
|
15
|
-
config = {"worklog_dir": "logs", "structure": "year", "suffix": "log"}
|
|
16
|
-
path = resolve_path(config, date(2026, 5, 23))
|
|
17
|
-
assert str(path) == "logs/2026/23-05-2026-log.md"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_resolve_path_year_month():
|
|
21
|
-
config = {"worklog_dir": "logs", "structure": "year-month", "suffix": "ops"}
|
|
22
|
-
path = resolve_path(config, date(2026, 5, 23))
|
|
23
|
-
assert str(path) == "logs/2026/05/23-05-2026-ops.md"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_resolve_path_custom_suffix():
|
|
27
|
-
config = {"structure": "year", "suffix": "retro"}
|
|
28
|
-
path = resolve_path(config, date(2026, 1, 1))
|
|
29
|
-
assert str(path).endswith("-retro.md")
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"""Tests for frontmatter and body generation."""
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
|
-
from worklog_opsdevnz.template import (
|
|
6
|
-
generate_frontmatter,
|
|
7
|
-
generate_body,
|
|
8
|
-
generate_content,
|
|
9
|
-
render_template,
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def test_generate_frontmatter():
|
|
14
|
-
config = {
|
|
15
|
-
"author": "Test Author",
|
|
16
|
-
"default_tags": ["internal", "test"],
|
|
17
|
-
}
|
|
18
|
-
fm = generate_frontmatter(config, "2026-05-23")
|
|
19
|
-
assert "title: " in fm
|
|
20
|
-
assert "Work Log - 2026-05-23" in fm
|
|
21
|
-
assert "date: 2026-05-23" in fm
|
|
22
|
-
assert "author: Test Author" in fm
|
|
23
|
-
assert " - internal" in fm
|
|
24
|
-
assert " - test" in fm
|
|
25
|
-
assert "draft: false" in fm
|
|
26
|
-
assert fm.startswith("---")
|
|
27
|
-
assert fm.strip().endswith("---")
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def test_generate_body_with_sections():
|
|
31
|
-
config = {
|
|
32
|
-
"sections": [
|
|
33
|
-
{"title": "Focus", "content": ""},
|
|
34
|
-
{"title": "Notes", "content": "some note"},
|
|
35
|
-
]
|
|
36
|
-
}
|
|
37
|
-
body = generate_body(config)
|
|
38
|
-
assert "## Focus" in body
|
|
39
|
-
assert "## Notes" in body
|
|
40
|
-
assert "some note" in body
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_generate_content_full():
|
|
44
|
-
config = {
|
|
45
|
-
"author": "opsdev",
|
|
46
|
-
"default_tags": ["log"],
|
|
47
|
-
"sections": [{"title": "Today", "content": ""}],
|
|
48
|
-
}
|
|
49
|
-
content = generate_content(config, "2026-05-23")
|
|
50
|
-
assert content.startswith("---")
|
|
51
|
-
assert "Work Log - 2026-05-23" in content
|
|
52
|
-
assert "## Today" in content
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_render_template_with_placeholders(tmp_path):
|
|
56
|
-
template = tmp_path / "my-template.md"
|
|
57
|
-
template.write_text("# {{TITLE}}\n\nDate: {{DATE}}\n\nFree-form notes.")
|
|
58
|
-
|
|
59
|
-
result = render_template(str(template), "2026-05-26")
|
|
60
|
-
assert "# Work Log - 2026-05-26" in result
|
|
61
|
-
assert "Date: 2026-05-26" in result
|
|
62
|
-
assert "Free-form notes." in result
|
|
63
|
-
assert "{{DATE}}" not in result
|
|
64
|
-
assert "{{TITLE}}" not in result
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def test_render_template_file_not_found():
|
|
68
|
-
with pytest.raises(FileNotFoundError):
|
|
69
|
-
render_template("/nonexistent/template.md", "2026-05-26")
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def test_generate_content_with_template(tmp_path):
|
|
73
|
-
template = tmp_path / "my-template.md"
|
|
74
|
-
template.write_text("# {{TITLE}}\n\nDate: {{DATE}}")
|
|
75
|
-
|
|
76
|
-
config = {
|
|
77
|
-
"author": "opsdev",
|
|
78
|
-
"default_tags": ["log"],
|
|
79
|
-
"template": str(template),
|
|
80
|
-
}
|
|
81
|
-
content = generate_content(config, "2026-05-26")
|
|
82
|
-
assert content.startswith("---")
|
|
83
|
-
assert "Work Log - 2026-05-26" in content
|
|
84
|
-
assert "author: opsdev" in content
|
|
85
|
-
assert "# Work Log - 2026-05-26" in content
|
|
86
|
-
assert "Date: 2026-05-26" in content
|
|
87
|
-
# Frontmatter generated, body from template — not sections
|
|
88
|
-
assert "## " not in content.split("---\n")[2] if "---\n" in content else True
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def test_generate_content_without_template():
|
|
92
|
-
"""No template field → built-in sections body, unchanged behaviour."""
|
|
93
|
-
config = {
|
|
94
|
-
"author": "opsdev",
|
|
95
|
-
"default_tags": ["log"],
|
|
96
|
-
"sections": [{"title": "Today", "content": ""}],
|
|
97
|
-
}
|
|
98
|
-
content = generate_content(config, "2026-05-26")
|
|
99
|
-
assert "## Today" in content
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/src/worklog_opsdevnz.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/src/worklog_opsdevnz.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{worklog_opsdevnz-0.1.3 → worklog_opsdevnz-0.2.1}/src/worklog_opsdevnz.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|