Skip to Content
文档本地 E2E

Local E2E Workflow

The formal local end-to-end loop is now repository-owned:

  • a sanitized Obsidian vault fixture: tests/fixtures/obsidian_vault_minimal
  • a real disposable NocoDB service: tests/fixtures/nocodb_local
  • one formal suite: tests/test_e2e_obsidian_nocodb.py

This is the standard way to validate the business loop:

Obsidian Thought -> ingest -> reactive action -> Bookmark -> writeback -> NocoDB

Preconditions

  • Docker-compatible runtime available locally OrbStack headless was validated as one working option.
  • uv installed

One-command entry point

Use the repository script:

scripts/run-local-e2e.sh

This does four things in order:

  1. docker compose up -d for the local NocoDB service
  2. bootstraps the 自媒体作品快照 / 作品快照 schema and seed rows
  3. loads the generated env file
  4. runs the formal e2e suite

Useful variants

Run pytest in quiet mode:

scripts/run-local-e2e.sh -- -q

Stop the local NocoDB service after the run:

scripts/run-local-e2e.sh --down

Rebuild from a clean local state:

scripts/run-local-e2e.sh --fresh -- -q

Filter to one test:

scripts/run-local-e2e.sh -- -k writeback -q

Generated state

Bootstrap writes:

tests/fixtures/nocodb_local/.env.generated

That file is the source of truth for:

  • RUNEX_E2E_NOCO_URL
  • RUNEX_E2E_NOCO_TOKEN
  • RUNEX_E2E_NOCO_BASE_ID
  • RUNEX_E2E_NOCO_TABLE_ID

Runtime NocoDB data lives under:

tests/fixtures/nocodb_local/.runtime/

Both paths are git-ignored.

Clean rebuild mode

Use --fresh when you want to throw away the local NocoDB runtime state and bootstrap again from scratch:

scripts/run-local-e2e.sh --fresh -- -q

This does three extra things before the normal workflow:

  1. docker compose down
  2. removes tests/fixtures/nocodb_local/.runtime/
  3. removes tests/fixtures/nocodb_local/.env.generated

Use it after changing bootstrap logic, table shape, or seed assumptions.

What is actually proven

The formal suite covers two assertions:

  1. the local NocoDB seed rows are readable through the real media-snapshot source adapter
  2. a real Obsidian Thought fixture containing a bilibili link ingests, creates a Bookmark, and writes that link into the local NocoDB table

This is intentionally stronger than a stub-only test and intentionally safer than using production NocoDB.

Maintenance rule

When a fixture note, NocoDB column mapping, or ontology rule changes, rerun:

scripts/run-local-e2e.sh -- -q

Do not treat the local fixture as throwaway data. It is part of the repository contract.


Proactive reflex arc (v0.2 Phase 5)

The second repository-owned loop is the proactive reflex arc — the milestone’s complete user-visible reflex: a Thought becomes a reviewed, approved, real Task, with every hop visible in graph state and event history.

Obsidian Thought → ingest → reactive request_extraction (Thought machine: new → extraction_requested) → ExtractionScheduler signals a WorkItem (durable, off the bus) → WorkerLoop runs the extraction Agent handler (slow I/O, off the bus) → Task Proposal written back (pending_review) with provenance + link to the Thought → StoreWatcher projects the Proposal into Obsidian for review → human sets `审批结果: approved` in Frontmatter → ingest → reactive approve_proposal (Proposal machine → approved) → ProposalApplier materializes exactly one real Task (idempotent by natural key) → StoreWatcher projects the Task back into Obsidian

Formal suite: tests/test_reflex_arc_e2e.py. It runs from a clean RUNEX_HOME, injects a worker crash during the work stage (expired lease → recovery → exactly-once), and asserts no echo loop, full provenance, and that rejecting creates no Task.

Run it

uv run python -m pytest tests/test_reflex_arc_e2e.py -q

Drive it live against a vault

The long-lived host composes all stages (ingest → signal → recover → work → materialize → project) in one supervised loop:

runex ontology load <skill>/ontology/core/memory.scm runex ontology load <skill>/ontology/core/conflict.scm runex ontology load <skill>/ontology/core/workitem.scm runex ontology load <skill>/ontology/core/proposal.scm runex ontology listen --vault /path/to/vault --poll 2

To review pending proposals from the terminal (the operator/debug fallback — the canonical review surface is the Obsidian Frontmatter):

runex ontology proposals # list, with state + risk + summary runex ontology proposal-approve <提议> --by me --reason "..." runex ontology proposal-reject <提议> --by me

How the layers hold the boundary (ADR-0010)

  • Decision/policy is ontology data. The Thought and Proposal machines, their guards, and field ownership live in .scm. Approval is a later graph write (a field a human/CLI/Obsidian sets), never a waiting thread.
  • Slow work is durable and off the bus. Extraction runs in a handler behind a WorkItem with lease/retry/recovery — a crash mid-work is reclaimed and finished exactly once.
  • Scheduling and materialization are thin peripherals. ExtractionScheduler turns extraction-requested Thoughts into WorkItems via the one blessed signal_workitem; ProposalApplier upserts the approved command batch by stable natural key. Both are idempotent, so duplicate delivery / restart / replay never double-applies an effect.

Troubleshooting

  • A proposal never appears in the vault. A machine-born node is projected only once it has a 来源路径. The Agent/applier stamp it under the vault when given a projection base (--vault); without a base, the arc still runs graph-only (no Obsidian files).
  • An approval edit seems ignored. The supertag is resolved from the type: Frontmatter key; keep it. Set 审批结果 to exactly approved / rejected / revision / cancelled. The decision only fires from pending_review / revision_requested.
  • A Task is materialized twice. It should not be: materialization upserts by natural key and stamps 已实现. If you see duplicates, check that the proposed command’s natural_key matches the target supertag’s schema.
  • 物化错误 is set on a Proposal. The command batch failed re-validation against the current manifest (stale context). Fix the schema drift; the applier retries the still-approved proposal on the next tick.

Safe natural-language rule evolution (v0.2 Phase 6)

The third repository-owned loop lets a human change behavior in words without giving an Agent direct production authority. A BusinessRule flows:

draft (人写自然语言 源稿) → compile (Compiler Agent → .scm 编译产物, off the bus) draft → compiled → verify (RuleVerifier: parse + static check vs manifest + primitive catalog) compiled → verified | failed → approve (人写 审批结果: approved in Frontmatter/CLI) verified → approved → deploy (RuleDeployer loads the .scm into production) approved → deployed → rollback (reload the preserved prior snapshot) deployed → rolled_back

Formal suite: tests/test_rule_compiler.py, including the dogfood rule “remind on a blocked Task” — which actually changes production behavior only after it is approved and deployed.

The vocabulary a rule may use (RULE-01)

runex ontology primitives # full catalog runex ontology primitives --kind effect # only write primitives runex ontology primitives --json # machine-readable (for a compiler)

Each entry carries kind (control/builtin/read/effect), arity, whether it is guard-safe, a summary, and a minimal example. The verifier rejects any symbol outside this catalog as an unknown_primitive diagnostic.

Review & deploy (RULE-06)

The canonical approval surface is the Obsidian Frontmatter (审批结果: approved). The operator/debug fallback:

runex ontology rules # list with state + version + diag flag runex ontology rule-approve <规则> --by me

What the verifier catches (RULE-04/05)

Each is written back as a structured 诊断 entry and fails the rule closed (→ failed), so it never reaches production:

  • parse_error — the draft is not valid .scm
  • unknown_field — a (field "X") / (set-field "X" …) naming a field the target supertag does not declare (the canonical “fails visibly” case)
  • unknown_primitive — a symbol outside the primitive catalog
  • unknown_machine — an action on a machine that does not exist
  • blocking_kernel — a blocking kernel called directly in an effect (would freeze the bus; use Signal-Then-Work)

How the layers hold the boundary (ADR-0010)

  • Lifecycle/policy is ontology data (businessrule.scm). Approval is a later graph write, never a waiting thread.
  • Compilation is slow Agent work, off the bus — it only produces a draft; it has no deploy authority (RULE-03).
  • Verify and deploy are thin peripherals. The verifier is pure static analysis against the live manifest + primitive catalog; the deployer is the only component that loads into production, and only from approved.

Troubleshooting

  • A rule sits in compiled. The verifier stage hasn’t run, or it failed — check runex ontology rules and the rule’s 诊断.
  • A rule is failed with unknown_field. The compiled .scm references a field the supertag doesn’t have. Fix the source/compiler; re-author to retry.
  • An approved rule didn’t deploy. Deploy loads the .scm; a load error fails the rule closed with a deploy_error diagnostic. Inspect 诊断.
  • Rollback didn’t remove a newly-added action. v0.2 rollback is a best-effort override-restore of the prior snapshot; fully retracting actions a draft added needs loader unload support (a later phase). The prior version is preserved in 回滚目标 either way.
Last updated on