refactor(daily): replace slug with session_id for daily note identification (#266)
Some checks are pending
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.10 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run

- Rename slug parameter to session_id across daily note operations
- Update validation function from validate_slug to validate_session_id
- Change data structure keys from slug to session_id in note objects
- Modify file paths to use session_id instead of slug in daily folder
- Update documentation and comments to reflect session_id terminology
- Adjust test cases to use session_id parameter instead of slug
- Change default frontmatter to include empty description field
- Update configuration files to use session_id parameter name
- Modify scan_notes function to return session_id instead of slug
This commit is contained in:
jinliyl 2026-05-29 16:00:51 +08:00 committed by GitHub
parent ef22bfb071
commit 9ee2f0f7ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 99 additions and 571 deletions

View file

@ -109,19 +109,19 @@ jobs:
daily_create:
backend: base
description: "Provision a note slug under a daily folder: daily/<date>/<slug>.md"
description: "Provision a session note under a daily folder: daily/<date>/<session_id>.md"
parameters:
type: object
properties:
slug:
session_id:
type: string
description: "the file stem: event name or topic name"
description: "the session identifier (also the file stem)"
date:
type: string
description: "YYYY-MM-DD; empty = today"
default: ""
required:
- slug
- session_id
steps:
- backend: daily_create_step

View file

@ -1,474 +0,0 @@
service:
backend: http
daily_dir: daily
digest_dir: digest
resource_dir: ""
jobs:
version:
backend: base
description: "return reme package version"
parameters:
type: object
properties: {}
steps:
- backend: version_step
health_check:
backend: base
description: "return a concise health-check snapshot of reme components"
parameters:
type: object
properties: {}
steps:
- backend: health_check_step
help:
backend: base
description: "list all registered jobs with their metadata"
parameters:
type: object
properties: {}
steps:
- backend: help_step
traverse:
backend: base
description: "Walk the wikilink graph from a path."
parameters:
type: object
properties:
path:
type: string
description: "path"
depth:
type: integer
description: "hop limit"
default: 1
direction:
type: string
enum:
- forward
- backward
- both
default: both
required:
- path
steps:
- backend: traverse_step
reindex:
backend: base
description: "wipe the file store and rebuild it from the watcher's tracked files"
parameters:
type: object
properties: {}
steps:
- backend: clear_and_scan_step
- backend: update_index_step
persist: true
index_changes:
backend: base
description: "apply a batch of file changes (added/modified/deleted) into file_store"
parameters:
type: object
properties:
changes:
type: array
description: "list of change items"
items:
type: object
properties:
change:
type: string
enum: [added, modified, deleted]
description: "type of file change"
path:
type: string
description: "absolute file path"
required:
- change
- path
required:
- changes
steps:
- backend: index_changes_step
# ════════════════════════════════════════════════════════════════════
# ATOMIC TOOLS — same surface plugins/reme-{service,expert} expose
# ════════════════════════════════════════════════════════════════════
# ── Retrieve ───────────────────────────────────────────────────────
search:
backend: base
description: "Hybrid vault search (vector + BM25, RRF-fused)."
parameters:
type: object
properties:
query:
type: string
description: "search query"
limit:
type: integer
description: "max results"
default: 5
min_score:
type: number
description: "min fused score"
default: 0.0
required:
- query
steps:
- backend: search_step
vector_weight: 0.7
candidate_multiplier: 3.0
expand_links: true
max_links_per_direction: 10
# ── Read Operations ───────────────────────────────────────────────────────────
list:
backend: base
description: "List files under a vault path."
parameters:
type: object
properties:
path:
type: string
description: "vault-relative dir; empty = root"
default: ""
recursive:
type: boolean
description: "recurse"
default: false
limit:
type: integer
description: "max results"
default: 100
steps:
- backend: list_step
read:
backend: base
description: "Read a markdown file under the vault."
parameters:
type: object
properties:
path:
type: string
description: "vault-relative path; markdown only"
start_line:
type: integer
description: "first line (1-based, inclusive)"
end_line:
type: integer
description: "last line (1-based, inclusive)"
required:
- path
steps:
- backend: read_step
stat:
backend: base
description: "Stat a vault file (size, mtime, exists, is_dir, is_file)."
parameters:
type: object
properties:
path:
type: string
description: "vault-relative path"
required:
- path
steps:
- backend: stat_step
frontmatter_read:
backend: base
description: "Read a file's YAML frontmatter as a dict."
parameters:
type: object
properties:
path:
type: string
description: "vault-relative path"
required:
- path
steps:
- backend: frontmatter_read_step
# ── Write Operations──────────────────────────────────────────────────────────
write:
backend: base
description: "Write a markdown file (create or overwrite) with name/description frontmatter."
parameters:
type: object
properties:
path:
type: string
description: "vault-relative path; markdown only"
name:
type: string
description: "frontmatter name"
description:
type: string
description: "frontmatter description"
content:
type: string
description: "body"
required:
- path
- name
- description
- content
steps:
- backend: write_step
edit:
backend: base
description: "Find-and-replace in a markdown file (all occurrences)."
parameters:
type: object
properties:
path:
type: string
description: "vault-relative path"
old:
type: string
description: "text to find"
new:
type: string
description: "replacement"
default: ""
required:
- path
- old
- new
steps:
- backend: edit_step
append:
backend: base
description: "Append content to a markdown file."
parameters:
type: object
properties:
path:
type: string
description: "vault-relative path"
content:
type: string
description: "content to append"
required:
- path
- content
steps:
- backend: append_step
frontmatter_update:
backend: base
description: "Merge keys into a file's YAML frontmatter."
parameters:
type: object
properties:
path:
type: string
description: "vault-relative path"
metadata:
type: object
description: "keys to merge"
additionalProperties: true
required:
- path
- metadata
steps:
- backend: frontmatter_update_step
frontmatter_delete:
backend: base
description: "Drop keys from a file's YAML frontmatter."
parameters:
type: object
properties:
path:
type: string
description: "vault-relative path"
keys:
type: array
description: "keys to remove"
items:
type: string
required:
- path
- keys
steps:
- backend: frontmatter_delete_step
delete:
backend: base
description: "Delete a vault file or folder; returns surviving inbound wikilinks."
parameters:
type: object
properties:
path:
type: string
description: "vault-relative path"
required:
- path
steps:
- backend: delete_step
upload_resource:
backend: base
description: "Ingest an external-channel asset into resource/<today>/ with provenance."
parameters:
type: object
properties:
path:
type: string
description: "host source path"
channel:
type: string
description: "channel id (wechat / email / browser / api / ...)"
description:
type: string
description: "what the asset is and how to interpret it"
metadata:
type: object
description: "extra provenance keys (e.g. source)"
default: {}
required:
- path
- channel
- description
steps:
- backend: upload_resource_step
download:
backend: base
description: "Copy a vault file out to the host filesystem."
parameters:
type: object
properties:
src_path:
type: string
description: "vault-relative source"
dst_path:
type: string
description: "host absolute dest; empty = temp file"
default: ""
overwrite:
type: boolean
description: "overwrite if dst exists"
default: false
required:
- src_path
steps:
- backend: download_step
# ── Daily Operations (slug provisioning + day-index rollup) ──────────
daily_create:
backend: base
description: "Provision daily/<date>/<slug>.md (empty body, frontmatter {name: slug}); idempotent; refreshes the day index."
parameters:
type: object
properties:
slug:
type: string
description: "note slug"
date:
type: string
description: "ISO date; empty = today"
default: ""
required:
- slug
steps:
- backend: daily_create_step
daily_list:
backend: base
description: "List notes under a single day."
parameters:
type: object
properties:
date:
type: string
description: "ISO date; empty = today"
default: ""
steps:
- backend: daily_list_step
daily_reindex:
backend: base
description: "Rebuild the day-index page daily/<date>.md."
parameters:
type: object
properties:
date:
type: string
description: "ISO date; empty = today"
default: ""
steps:
- backend: daily_reindex_step
watch_file:
backend: background
watch_paths:
- MEMORY.md
- memory
suffix_filters:
- md
steps:
- backend: scan_changes_step
- backend: update_index_step
persist: true
- backend: watch_changes_step
dispatch_step: update_index_step
components:
tokenizer:
default:
backend: regex
embedding_model:
default:
backend: ${EMBEDDING_BACKEND:-openai}
api_key: ${EMBEDDING_API_KEY}
base_url: ${EMBEDDING_BASE_URL:-https://api.openai.com/v1}
model_name: ${EMBEDDING_MODEL_NAME:-text-embedding-v4}
dimensions: 1024
file_graph:
default:
backend: local
file_parser:
linked:
backend: linked
supported_extensions:
- md
chunked:
backend: chunked
supported_extensions:
- txt
- html
- json
- yaml
- py
default:
backend: default
keyword_index:
default:
backend: bm25
tokenizer: default
file_store:
default:
backend: local
store_name: local
embedding_model: default
keyword_index: default
file_graph: default

View file

@ -4,10 +4,10 @@ Two related concerns, both private to the ``crud`` package:
1. **Generic file IO** path gating, encoding-aware read/write, output
truncation (used by every CRUD step that touches the filesystem).
2. **Daily-note helpers** slug validation + ``daily/<date>.md`` index
rebuild (used by the ``daily_*`` steps). The day index is a derived
rollup page auto-managed in marker-delimited sections; user-edited
manual sections are preserved verbatim across refreshes.
2. **Daily-note helpers** session_id validation + ``daily/<date>.md``
index rebuild (used by the ``daily_*`` steps). The day index is a
derived rollup page auto-managed in marker-delimited sections;
user-edited manual sections are preserved verbatim across refreshes.
"""
import asyncio
@ -140,7 +140,7 @@ def validate_filename_component(name: str, *, kind: str = "filename") -> str | N
for callers that validate per component)
``kind`` is the human-readable label inserted into error messages
(e.g. ``"slug"``, ``"path component"``).
(e.g. ``"session_id"``, ``"path component"``).
"""
if not name:
return f"{kind} is required"
@ -382,16 +382,16 @@ def truncate_text_output(
# ---------------------------------------------------------------------------
# Daily-note helpers: slug validation + day-index rebuild
# Daily-note helpers: session_id validation + day-index rebuild
# ---------------------------------------------------------------------------
# Slug validation
# ---------------
# Session ID validation
# ---------------------
def validate_slug(slug: str) -> str | None:
"""Validate a daily-note slug. Thin wrapper over :func:`validate_filename_component`."""
return validate_filename_component(slug, kind="slug")
def validate_session_id(session_id: str) -> str | None:
"""Validate a daily-note session_id. Thin wrapper over :func:`validate_filename_component`."""
return validate_filename_component(session_id, kind="session_id")
# Day-index rebuild
@ -447,26 +447,26 @@ def scan_notes(vault_dir: Path, date: str, daily_dir: str) -> list[dict]:
Returns one dict per note::
{"slug": str, "path": str, "metadata": dict}
{"session_id": str, "path": str, "metadata": dict}
``metadata`` is the raw frontmatter dict (insertion-ordered);
consumers decide which keys to surface. Each ``.md`` directly
under the day folder is a note; the file's stem is the slug.
under the day folder is a note; the file's stem is the session_id.
"""
date_dir = vault_dir / daily_dir / date
if not date_dir.is_dir():
return []
out: list[dict] = []
for md_path in sorted(p for p in date_dir.iterdir() if p.is_file() and p.suffix == ".md"):
slug = md_path.stem
session_id = md_path.stem
try:
post = frontmatter.loads(md_path.read_text(encoding="utf-8"))
except Exception: # pylint: disable=broad-except
continue
out.append(
{
"slug": slug,
"path": f"{daily_dir}/{date}/{slug}.md",
"session_id": session_id,
"path": f"{daily_dir}/{date}/{session_id}.md",
"metadata": dict(post.metadata or {}),
},
)
@ -492,7 +492,7 @@ async def refresh_day_index(file_store, date: str, daily_dir: str) -> dict:
index_abs = vault_dir / index_rel
notes = scan_notes(vault_dir, date, daily_dir)
notes_payload = [{"path": n["path"], "slug": n["slug"], "metadata": n["metadata"]} for n in notes]
notes_payload = [{"path": n["path"], "session_id": n["session_id"], "metadata": n["metadata"]} for n in notes]
if not notes and not index_abs.is_file():
return {

View file

@ -1,9 +1,9 @@
"""``daily_create`` — provision a note slug under a daily folder: ``daily/<date>/<slug>.md``.
"""``daily_create`` — provision a session note under a daily folder: ``daily/<date>/<session_id>.md``.
Minimal slug provisioner. Validates the slug, mkdirs the day folder,
writes an empty-body note with frontmatter ``{name: slug}`` if (and
only if) the file does not already exist, refreshes the day index,
and returns the vault-relative path.
Validates the session_id, mkdirs the day folder, writes an empty-body
note with frontmatter ``{name: session_id}`` if (and only if) the file
does not already exist, refreshes the day index, and returns the
vault-relative path.
Idempotent: when the note already exists this is a no-op write (the
day index still refreshes siblings may have changed; cheap
@ -12,12 +12,12 @@ self-healing). The caller fills the body via ``file_write`` /
deliberately does not accept a body.
Inputs:
slug (required, validated) the note's name (also the file stem)
date (optional, ``YYYY-MM-DD``; empty = today)
session_id (required, validated) the note's session identifier (also the file stem)
date (optional, ``YYYY-MM-DD``; empty = today)
Outputs:
answer = one-line human-readable status
metadata = {date, slug, path, created, index?}
metadata = {date, session_id, path, created, index?}
"""
from datetime import date as _date
@ -25,14 +25,14 @@ from pathlib import Path
import frontmatter
from ._file_io import refresh_day_index, validate_slug, write_file_safe
from ._file_io import refresh_day_index, validate_session_id, write_file_safe
from ..base_step import BaseStep
from ...components import R
@R.register("daily_create_step")
class DailyCreateStep(BaseStep):
"""Provision ``daily/<date>/<slug>.md`` (idempotent); refresh day index."""
"""Provision ``daily/<date>/<session_id>.md`` (idempotent); refresh day index."""
def _fail(self, message: str, **meta) -> None:
"""Mark response failed; copy ``meta`` into ``response.metadata``."""
@ -43,24 +43,24 @@ class DailyCreateStep(BaseStep):
self.context.response.metadata.update(meta)
def _collect_params(self) -> tuple[str, str, str]:
"""Read ``slug`` + ``date`` from context; default ``date`` today, ``daily_dir`` from app config."""
"""Read ``session_id`` + ``date`` from context; default ``date`` today, ``daily_dir`` from app config."""
assert self.context is not None
slug = self.context.get("slug", "")
session_id = self.context.get("session_id", "")
day = self.context.get("date", "") or _date.today().strftime("%Y-%m-%d")
daily_dir = self.app_context.app_config.daily_dir if self.app_context is not None else "daily"
return slug, day, daily_dir
return session_id, day, daily_dir
@staticmethod
def _empty_note_text(slug: str) -> str:
"""Serialize an empty-body markdown note with frontmatter ``{name: slug}``; trailing newline guaranteed."""
text = frontmatter.dumps(frontmatter.Post("", name=slug))
def _empty_note_text(session_id: str) -> str:
"""Serialize an empty-body markdown note with frontmatter ``{name, description}``; trailing newline."""
text = frontmatter.dumps(frontmatter.Post("", name=session_id, description=""))
return text if text.endswith("\n") else text + "\n"
async def _create_if_missing(self, path_abs: Path, slug: str) -> bool:
async def _create_if_missing(self, path_abs: Path, session_id: str) -> bool:
"""Write the empty note only when the file is absent. Returns ``True`` iff a new file was created."""
if path_abs.is_file():
return False
await write_file_safe(path_abs, self._empty_note_text(slug), encoding="utf-8")
await write_file_safe(path_abs, self._empty_note_text(session_id), encoding="utf-8")
return True
def _set_success(self, payload: dict, created: bool) -> None:
@ -71,24 +71,27 @@ class DailyCreateStep(BaseStep):
self.context.response.metadata.update(payload)
async def execute(self):
"""Validate the slug, provision the note file, refresh the day index, stamp the response."""
"""Validate the session_id, provision the note file, refresh the day index, stamp the response."""
assert self.context is not None
slug, day, daily_dir = self._collect_params()
session_id, day, daily_dir = self._collect_params()
err = validate_slug(slug)
err = validate_session_id(session_id)
if err:
self._fail(err)
return None
path_rel = f"{daily_dir}/{day}/{slug}.md"
path_rel = f"{daily_dir}/{day}/{session_id}.md"
path_abs = (self.vault_path / path_rel).resolve()
try:
created = await self._create_if_missing(path_abs, slug)
created = await self._create_if_missing(path_abs, session_id)
except Exception as e: # pylint: disable=broad-except
self._fail(f"create failed: {e}", date=day, slug=slug, path=path_rel)
self._fail(f"create failed: {e}", date=day, session_id=session_id, path=path_rel)
return None
index = await refresh_day_index(self.file_store, day, daily_dir)
self._set_success({"date": day, "slug": slug, "path": path_rel, "created": created, "index": index}, created)
self._set_success(
{"date": day, "session_id": session_id, "path": path_rel, "created": created, "index": index},
created,
)
self.logger.info(f"[{self.name}] {'created' if created else 'reused'} path={path_rel}")
return self.context.response

View file

@ -1,8 +1,8 @@
"""``daily_list`` — list the notes under a single day (pure read, no side effects).
Returns one row per ``daily/<date>/<slug>.md`` note file with its
vault-relative ``path``, ``slug``, and the raw ``metadata`` dict
(full frontmatter). Sorted by slug for stable output.
Returns one row per ``daily/<date>/<session_id>.md`` note file with its
vault-relative ``path``, ``session_id``, and the raw ``metadata`` dict
(full frontmatter). Sorted by session_id for stable output.
**Does NOT refresh** ``daily/<date>.md`` call ``daily_reindex``
explicitly when the index page needs to be rebuilt. Decoupling
@ -35,7 +35,7 @@ class DailyListStep(BaseStep):
@staticmethod
def _project(note: dict) -> dict:
"""Keep only the user-facing keys (drop internal scan_notes fields, if any)."""
return {"path": note["path"], "slug": note["slug"], "metadata": note["metadata"]}
return {"path": note["path"], "session_id": note["session_id"], "metadata": note["metadata"]}
@staticmethod
def _format_note_line(note: dict) -> str:

View file

@ -1,8 +1,8 @@
"""``daily_reindex_step`` — rebuild ``daily/<date>.md`` from its notes.
"""``daily_reindex_step`` — rebuild ``daily/<date>.md`` from its session notes.
The day index ``daily/<date>.md`` is a derived artifact whose job is to
list and describe every note file under ``daily/<date>/``. It is auto-
refreshed by ``daily_create``. Generic ops like ``file_write`` /
list and describe every session note file under ``daily/<date>/``. It is
auto-refreshed by ``daily_create``. Generic ops like ``file_write`` /
``file_append`` / ``frontmatter_update`` leave it stale this step
is the standalone writer to call after batch flows (historical
backfill, drift recovery, end-of-batch consolidation, or a

View file

@ -5,10 +5,10 @@ provision / listing / index-rebuild operations. Body authoring and
frontmatter mutations are generic CRUD (covered in test_crud_steps
and test_property_steps).
A daily note is the single file ``daily/<YYYY-MM-DD>/<slug>.md`` (no
folder, no sibling materials). ``daily_create`` is a minimal slug
provisioner: it validates the slug, writes an empty-body note with
default ``{name: slug}`` frontmatter when the file is absent, and
A daily note is the single file ``daily/<YYYY-MM-DD>/<session_id>.md``
(no folder, no sibling materials). ``daily_create`` validates the
session_id, writes an empty-body note with default
``{name: session_id}`` frontmatter when the file is absent, and
refreshes the day index. When the file already exists it is a no-op
write (``created=False``) the body is filled in afterwards via
``file_write`` / ``file_edit`` / ``frontmatter_update`` or a native
@ -69,17 +69,17 @@ def _today() -> str:
async def _make_store_with_dailies(entries: list[tuple[str, str, str]]) -> LocalFileStore:
"""Seed the vault with daily notes.
entries: list of (date, slug, body). Each tuple creates
``daily/<date>/<slug>.md`` with a minimal ``name``-only
entries: list of (date, session_id, body). Each tuple creates
``daily/<date>/<session_id>.md`` with a minimal ``name``-only
frontmatter no opinionated status / lifecycle axes.
"""
store = LocalFileStore(name="t", embedding_model="")
await store.start()
for day, slug, body in entries:
for day, session_id, body in entries:
day_dir = Path.cwd() / "daily" / day
day_dir.mkdir(parents=True, exist_ok=True)
text = f"---\nname: {slug}\n---\n{body}\n"
(day_dir / f"{slug}.md").write_text(text, encoding="utf-8")
text = f"---\nname: {session_id}\n---\n{body}\n"
(day_dir / f"{session_id}.md").write_text(text, encoding="utf-8")
return store
@ -87,15 +87,15 @@ def _metadata(step) -> dict:
return step.context.response.metadata
async def _seed_note(date: str, slug: str, name: str = "", description: str = "") -> None:
"""Write ``daily/<date>/<slug>.md`` with optional frontmatter."""
async def _seed_note(date: str, session_id: str, name: str = "", description: str = "") -> None:
"""Write ``daily/<date>/<session_id>.md`` with optional frontmatter."""
day_dir = Path.cwd() / "daily" / date
day_dir.mkdir(parents=True, exist_ok=True)
fm_lines = [f"name: {name or slug}"]
fm_lines = [f"name: {name or session_id}"]
if description:
fm_lines.append(f"description: {description}")
text = "---\n" + "\n".join(fm_lines) + "\n---\nbody\n"
(day_dir / f"{slug}.md").write_text(text, encoding="utf-8")
(day_dir / f"{session_id}.md").write_text(text, encoding="utf-8")
# -- daily_list_step ----------------------------------------------------------
@ -151,8 +151,8 @@ def test_daily_list_filters_by_date():
asyncio.run(run())
def test_daily_list_returns_path_slug_metadata():
"""Each note row exposes path / slug / metadata (full frontmatter dict)."""
def test_daily_list_returns_path_session_id_metadata():
"""Each note row exposes path / session_id / metadata (full frontmatter dict)."""
async def run():
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
@ -173,7 +173,7 @@ def test_daily_list_returns_path_slug_metadata():
assert "Alpha Project" in answer
assert "JWT auth migration" in answer
await store.close()
print("✓ test_daily_list_returns_path_slug_metadata passed")
print("✓ test_daily_list_returns_path_session_id_metadata passed")
asyncio.run(run())
@ -272,19 +272,19 @@ def test_daily_list_response_shape():
def test_daily_create_provisions_note_and_refreshes_index():
"""Fresh slug ⇒ empty-body note with ``{name: slug}`` + day index refreshed."""
"""Fresh session_id ⇒ empty-body note with ``{name: session_id}`` + day index refreshed."""
async def run():
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
store = await _make_store_with_dailies([])
step = daily_create_step.DailyCreateStep(file_store=store)
await step(slug="kickoff", date="2026-05-18")
await step(session_id="kickoff", date="2026-05-18")
payload = _metadata(step)
assert step.context.response.success is True
assert payload["created"] is True
assert payload["date"] == "2026-05-18"
assert payload["slug"] == "kickoff"
assert payload["session_id"] == "kickoff"
assert payload["path"] == "daily/2026-05-18/kickoff.md"
note = Path(tmp) / "daily" / "2026-05-18" / "kickoff.md"
@ -314,7 +314,7 @@ def test_daily_create_is_idempotent_on_existing():
before = file_path.read_text(encoding="utf-8")
step = daily_create_step.DailyCreateStep(file_store=store)
await step(slug="ongoing", date="2026-05-18")
await step(session_id="ongoing", date="2026-05-18")
payload = _metadata(step)
assert step.context.response.success is True
@ -335,7 +335,7 @@ def test_daily_create_default_date_is_today():
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
store = await _make_store_with_dailies([])
step = daily_create_step.DailyCreateStep(file_store=store)
await step(slug="today-task")
await step(session_id="today-task")
payload = _metadata(step)
assert payload["date"] == _today()
assert payload["path"] == f"daily/{_today()}/today-task.md"
@ -346,55 +346,54 @@ def test_daily_create_default_date_is_today():
asyncio.run(run())
def test_daily_create_default_frontmatter_uses_slug_as_name():
"""The provisioned note's frontmatter is ``{name: slug}`` (no body)."""
def test_daily_create_default_frontmatter_uses_session_id_as_name():
"""The provisioned note's frontmatter is ``{name: session_id, description: ''}`` (no body)."""
async def run():
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
store = await _make_store_with_dailies([])
step = daily_create_step.DailyCreateStep(file_store=store)
await step(slug="auth-refactor", date="2026-05-18")
await step(session_id="auth-refactor", date="2026-05-18")
note = Path(tmp) / "daily" / "2026-05-18" / "auth-refactor.md"
text = note.read_text(encoding="utf-8")
assert "name: auth-refactor" in text
# No description in default frontmatter.
assert "description:" not in text
assert "description:" in text
await store.close()
print("✓ test_daily_create_default_frontmatter_uses_slug_as_name passed")
print("✓ test_daily_create_default_frontmatter_uses_session_id_as_name passed")
asyncio.run(run())
def test_daily_create_rejects_invalid_slug():
"""Slug validation runs before any IO; no day folder is created on reject."""
def test_daily_create_rejects_invalid_session_id():
"""session_id validation runs before any IO; no day folder is created on reject."""
async def run():
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
store = await _make_store_with_dailies([])
step = daily_create_step.DailyCreateStep(file_store=store)
for bad in ("foo/bar", "foo:bar", "CON", "lpt9", "foo.", " bar"):
await step(slug=bad, date="2026-05-18")
await step(session_id=bad, date="2026-05-18")
assert step.context.response.success is False, f"expected reject for {bad!r}"
assert not (Path(tmp) / "daily" / "2026-05-18").exists()
await store.close()
print("✓ test_daily_create_rejects_invalid_slug passed")
print("✓ test_daily_create_rejects_invalid_session_id passed")
asyncio.run(run())
def test_daily_create_rejects_empty_slug():
"""Empty / missing slug is rejected with a clear message."""
def test_daily_create_rejects_empty_session_id():
"""Empty / missing session_id is rejected with a clear message."""
async def run():
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
store = await _make_store_with_dailies([])
step = daily_create_step.DailyCreateStep(file_store=store)
await step(slug="", date="2026-05-18")
await step(session_id="", date="2026-05-18")
assert step.context.response.success is False
assert "slug" in (step.context.response.answer or "").lower()
assert "session_id" in (step.context.response.answer or "").lower()
await store.close()
print("✓ test_daily_create_rejects_empty_slug passed")
print("✓ test_daily_create_rejects_empty_session_id passed")
asyncio.run(run())
@ -407,14 +406,14 @@ def test_daily_create_then_skip_round_trip():
store = await _make_store_with_dailies([])
step = daily_create_step.DailyCreateStep(file_store=store)
await step(slug="probe", date="2026-05-18")
await step(session_id="probe", date="2026-05-18")
first = _metadata(step)
assert first["created"] is True
note = Path(tmp) / "daily" / "2026-05-18" / "probe.md"
before = note.read_text(encoding="utf-8")
await step(slug="probe", date="2026-05-18")
await step(session_id="probe", date="2026-05-18")
second = _metadata(step)
assert second["created"] is False
assert note.read_text(encoding="utf-8") == before
@ -465,8 +464,8 @@ def test_day_index_includes_note_descriptions():
("beta", "beta", "调研增值税新政对 SaaS 的影响"),
("gamma", "Gamma", ""),
]
for slug, name, description in cases:
await _seed_note("2026-05-18", slug, name=name, description=description)
for sid, name, description in cases:
await _seed_note("2026-05-18", sid, name=name, description=description)
await daily_reindex_step.DailyReindexStep(file_store=store)(date="2026-05-18")
text = _day_index_text(tmp, "2026-05-18")
@ -601,7 +600,7 @@ if __name__ == "__main__":
print("\n=== Daily step tests ===")
test_daily_list_default_date_is_today()
test_daily_list_filters_by_date()
test_daily_list_returns_path_slug_metadata()
test_daily_list_returns_path_session_id_metadata()
test_daily_list_ignores_subdirectories()
test_daily_list_empty_when_no_daily_dir()
test_daily_list_does_not_refresh_index()
@ -609,9 +608,9 @@ if __name__ == "__main__":
test_daily_create_provisions_note_and_refreshes_index()
test_daily_create_is_idempotent_on_existing()
test_daily_create_default_date_is_today()
test_daily_create_default_frontmatter_uses_slug_as_name()
test_daily_create_rejects_invalid_slug()
test_daily_create_rejects_empty_slug()
test_daily_create_default_frontmatter_uses_session_id_as_name()
test_daily_create_rejects_invalid_session_id()
test_daily_create_rejects_empty_session_id()
test_daily_create_then_skip_round_trip()
test_day_index_lists_each_note()
test_day_index_includes_note_descriptions()