docs: add Contract sections + tag tests to contract items (Tier 2 — 10 files)
Some checks failed
Deploy (Gitea) / deploy (push) Failing after 2s
Some checks failed
Deploy (Gitea) / deploy (push) Failing after 2s
This commit is contained in:
@@ -29,6 +29,16 @@ The length prefix solves all three problems. The reader reads exactly 6 characte
|
||||
|
||||
The 6-character hex length supports messages up to ~16MB (0xFFFFFF bytes). This is sufficient for any single message the agent would produce. Larger payloads should be split across multiple messages.
|
||||
|
||||
** Contract
|
||||
|
||||
1. (frame-message msg): serializes a plist message to a length-prefixed
|
||||
string. The first 6 characters are the hex-encoded payload length.
|
||||
2. (read-framed-message stream): reads a framed message from a stream,
|
||||
returning the deserialized plist. Consumes exactly the length-prefixed
|
||||
bytes.
|
||||
3. Round-trip invariant: ~(read-framed-message (make-string-input-stream
|
||||
(frame-message msg)))~ equals ~msg~.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
@@ -261,19 +271,20 @@ Verifies that the framing protocol correctly serializes and deserializes message
|
||||
(in-suite communication-protocol-suite)
|
||||
|
||||
(test test-framing
|
||||
"Contract 1: frame-message produces correct hex length prefix."
|
||||
(let* ((msg '(:type :EVENT :payload (:action :handshake)))
|
||||
(framed (frame-message msg)))
|
||||
(is (string= "00002C" (string-upcase (subseq framed 0 6))))))
|
||||
|
||||
(test test-framing-round-trip
|
||||
"A message should survive frame → read-frame without loss."
|
||||
"Contract 3: frame → read-frame preserves message identity."
|
||||
(let* ((msg '(:type :EVENT :payload (:action :handshake :version "1.0") :meta (:source :tui)))
|
||||
(framed (frame-message msg))
|
||||
(unframed (read-framed-message (make-string-input-stream framed))))
|
||||
(is (equal msg unframed))))
|
||||
|
||||
(test test-framing-empty-message
|
||||
"An empty or simple message should still frame correctly."
|
||||
"Contract 1: simple messages frame with valid hex length."
|
||||
(let* ((msg '(:type :ping))
|
||||
(framed (frame-message msg)))
|
||||
(is (> (length framed) 5))
|
||||
|
||||
@@ -24,6 +24,15 @@ A naive implementation that serializes every ~org-object~ to text would produce
|
||||
|
||||
The semantic threshold is configurable via ~CONTEXT_SEMANTIC_THRESHOLD~ env var (default 0.75). Lower values include more peripherally related content; higher values restrict to tightly related content.
|
||||
|
||||
** Contract
|
||||
|
||||
1. (context-awareness-assemble &optional signal): produces a skeletal
|
||||
outline of current Memory for the LLM. If ~:foveal-focus~ is set,
|
||||
the foveal node gets inline rendering; peripheral nodes get title-only.
|
||||
Privacy-filtered objects are excluded.
|
||||
2. (context-assemble-global-awareness): zero-arg wrapper — calls
|
||||
~context-awareness-assemble~ without foveal focus.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
@@ -300,6 +309,7 @@ Verifies that the Foveal-Peripheral rendering correctly distinguishes between fo
|
||||
(in-suite vision-suite)
|
||||
|
||||
(test test-foveal-rendering
|
||||
"Contract 1: foveal content inline, peripheral content title-only."
|
||||
(clrhash passepartout::*memory-store*)
|
||||
(let* ((ast '(:type :HEADLINE :properties (:ID "proj-root" :TITLE "Project" :TAGS ("project"))
|
||||
:contents ((:type :HEADLINE :properties (:ID "node-foveal" :TITLE "Foveal Node")
|
||||
@@ -313,6 +323,7 @@ Verifies that the Foveal-Peripheral rendering correctly distinguishes between fo
|
||||
(is (not (search "PERIPHERAL CONTENT" output))))))
|
||||
|
||||
(test test-awareness-budget
|
||||
"Contract 1: all active projects appear in awareness output."
|
||||
(clrhash passepartout::*memory-store*)
|
||||
(ingest-ast '(:type :HEADLINE :properties (:ID "p1" :TITLE "Project 1" :TAGS ("project")) :contents nil))
|
||||
(ingest-ast '(:type :HEADLINE :properties (:ID "p2" :TITLE "Project 2" :TAGS ("project")) :contents nil))
|
||||
@@ -321,14 +332,14 @@ Verifies that the Foveal-Peripheral rendering correctly distinguishes between fo
|
||||
(is (search "Project 2" output))))
|
||||
|
||||
(test test-context-empty-memory
|
||||
"An empty memory should produce a clean awareness output without errors."
|
||||
"Contract 1: empty memory produces clean output without error."
|
||||
(clrhash passepartout::*memory-store*)
|
||||
(let ((output (context-awareness-assemble)))
|
||||
(is (stringp output))
|
||||
(is (search "MEMEX" output :test #'char-equal))))
|
||||
|
||||
(test test-context-no-foveal-focus
|
||||
"Without a foveal focus, all content should be peripheral (no inline content)."
|
||||
"Contract 2: without foveal focus, no inline content appears."
|
||||
(clrhash passepartout::*memory-store*)
|
||||
(let* ((ast '(:type :HEADLINE :properties (:ID "root" :TITLE "Root" :TAGS ("project"))
|
||||
:contents ((:type :HEADLINE :properties (:ID "child" :TITLE "Child Node")
|
||||
|
||||
@@ -22,6 +22,16 @@ The Reason stage already ran every proposed action through the deterministic eng
|
||||
|
||||
Because a skill's deterministic gate runs during Reason, but between Reason and Act, the action might have been transformed by the pipeline (metadata added, format normalized). The last-mile verification catches any transformation that might have introduced an unsafe property. It's the same philosophy as "trust but verify" — the second check is cheap and catches a class of bugs that would otherwise be silent data corruption.
|
||||
|
||||
** Contract
|
||||
|
||||
1. (loop-gate-act signal): the final pipeline stage. Handles HITL
|
||||
~:approval-required~ (suspends action), runs last-mile
|
||||
~cognitive-verify~ on approved actions, dispatches via
|
||||
~action-dispatch~, sets ~:status :acted~, returns feedback.
|
||||
2. (act-gate signal): thin alias for ~loop-gate-act~.
|
||||
3. (action-dispatch approved signal): routes approved actions to
|
||||
registered actuators by ~:target~ keyword.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
@@ -289,6 +299,7 @@ Verifies that the act gate correctly processes an approved action and sets the s
|
||||
(in-suite pipeline-act-suite)
|
||||
|
||||
(test test-loop-gate-act-basic
|
||||
"Contract 1: approved action reaches :acted status via loop-gate-act."
|
||||
(clrhash passepartout::*skill-registry*)
|
||||
(let* ((signal (list :type :EVENT :status nil :depth 0 :approved-action '(:target :cli :payload (:text "Hello"))))
|
||||
(result (loop-gate-act signal)))
|
||||
@@ -296,14 +307,14 @@ Verifies that the act gate correctly processes an approved action and sets the s
|
||||
(is (null result))))
|
||||
|
||||
(test test-loop-gate-act-no-approved-action
|
||||
"When no approved-action is set, the signal should still reach :acted status."
|
||||
"Contract 1: signal with no approved-action still reaches :acted status."
|
||||
(clrhash passepartout::*skill-registry*)
|
||||
(let* ((signal (list :type :EVENT :status nil :depth 0)))
|
||||
(loop-gate-act signal)
|
||||
(is (eq :acted (getf signal :status)))))
|
||||
|
||||
(test test-loop-gate-act-last-mile-reject
|
||||
"When the last-mile cognitive-verify rejects with :LOG, status should not reach :acted."
|
||||
"Contract 1: last-mile cognitive-verify rejection blocks approved-action."
|
||||
(clrhash passepartout::*skill-registry*)
|
||||
(passepartout::defskill :mock-blocker
|
||||
:priority 50
|
||||
@@ -318,7 +329,7 @@ Verifies that the act gate correctly processes an approved action and sets the s
|
||||
(is (null (getf signal :approved-action)))))
|
||||
|
||||
(test test-loop-gate-act-preserves-meta
|
||||
"Signal metadata should not be mutated by the act gate."
|
||||
"Contract 1: signal metadata is not mutated by loop-gate-act."
|
||||
(clrhash passepartout::*skill-registry*)
|
||||
(let* ((meta '(:source :tui :session "s1"))
|
||||
(signal (list :type :EVENT :status nil :depth 0 :meta meta
|
||||
|
||||
@@ -27,6 +27,14 @@ The `*loop-async-sensors*` list defines which sensor types are processed in dedi
|
||||
|
||||
The depth limit prevents runaway recursive loops. A signal that generates another signal that generates another signal can infinite-loop. If depth exceeds a threshold (10), the signal is silently dropped rather than processed. This is the metabolic loop's circuit breaker.
|
||||
|
||||
** Contract
|
||||
|
||||
1. (loop-gate-perceive signal): normalizes sensory input. Routes by
|
||||
sensor type (~:buffer-update~, ~:point-update~, ~:interrupt~,
|
||||
~:approval-required~) and signal type (~:EVENT~, ~:RESPONSE~).
|
||||
Sets ~:status :perceived~ on completion. Returns the signal.
|
||||
2. (perceive-gate signal): thin alias for ~loop-gate-perceive~.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
@@ -253,6 +261,7 @@ Verifies that the perceive gate correctly ingests AST nodes into memory and that
|
||||
(in-suite pipeline-perceive-suite)
|
||||
|
||||
(test test-loop-gate-perceive
|
||||
"Contract 1: :buffer-update ingests AST and sets :perceived status."
|
||||
(clrhash passepartout::*memory-store*)
|
||||
(let* ((signal (list :type :EVENT :payload (list :sensor :buffer-update :ast (list :type :HEADLINE :properties (list :ID "test-node" :TITLE "Test") :contents nil))))
|
||||
(result (loop-gate-perceive signal)))
|
||||
@@ -260,24 +269,25 @@ Verifies that the perceive gate correctly ingests AST nodes into memory and that
|
||||
(is (not (null (gethash "test-node" passepartout::*memory-store*))))))
|
||||
|
||||
(test test-depth-limiting
|
||||
"Edge: depth 11 signals are rejected by the pipeline."
|
||||
(let ((runaway-signal (list :type :EVENT :depth 11 :payload (list :sensor :heartbeat))))
|
||||
(is (null (process-signal runaway-signal)))))
|
||||
|
||||
(test test-loop-gate-perceive-unknown-sensor
|
||||
"Unknown sensors should pass through and still reach :perceived status."
|
||||
"Contract 1: unknown sensors pass through and reach :perceived."
|
||||
(let* ((signal (list :type :EVENT :depth 0 :payload (list :sensor :custom-metric)))
|
||||
(result (loop-gate-perceive signal)))
|
||||
(is (eq :perceived (getf result :status)))))
|
||||
|
||||
(test test-loop-gate-perceive-no-ast
|
||||
"A :buffer-update with no AST should not crash and reach :perceived."
|
||||
"Contract 1: :buffer-update without AST doesn't crash, reaches :perceived."
|
||||
(clrhash passepartout::*memory-store*)
|
||||
(let* ((signal (list :type :EVENT :depth 0 :payload (list :sensor :buffer-update)))
|
||||
(result (loop-gate-perceive signal)))
|
||||
(is (eq :perceived (getf result :status)))))
|
||||
|
||||
(test test-depth-limiting-normal
|
||||
"Signals at depth 10 or below should not be rejected by the depth guard."
|
||||
"Contract 1: signals at normal depth pass through without rejection."
|
||||
(let ((normal-signal (list :type :EVENT :depth 5 :payload (list :sensor :heartbeat))))
|
||||
(is (not (eq :rejected (getf normal-signal :status)))
|
||||
"Signal at normal depth should not be rejected")))
|
||||
|
||||
@@ -36,6 +36,22 @@ A plist is simultaneously:
|
||||
|
||||
This is not a cosmetic choice. It means the reasoning pipeline can generate, modify, and execute its own communication protocol without external parsing libraries. There is no JSON encoder, no schema validator, no serialization layer between the two engines. They speak the same language because they *are* the same language.
|
||||
|
||||
** Contract
|
||||
|
||||
1. (cognitive-verify proposed-action context): runs all registered
|
||||
deterministic gates sorted by priority. Returns a rejection plist
|
||||
(~:LOG~ or ~:EVENT~) if any gate blocks the action, an
|
||||
~:approval-required~ event if a gate requires HITL, or the action
|
||||
(potentially modified) if it passes.
|
||||
2. (loop-gate-reason signal): the full reason pipeline — only processes
|
||||
~:user-input~ and ~:chat-message~ sensors. Runs ~think~ to generate
|
||||
a candidate, then ~cognitive-verify~ to gate it. Retries up to 3
|
||||
times on rejection. Sets ~:status :reasoned~ on completion.
|
||||
3. (reason-gate signal): thin alias for ~loop-gate-reason~.
|
||||
4. (backend-cascade-call prompt): iterates ~*provider-cascade*~ calling
|
||||
each backend's handler until one succeeds. Returns the LLM content
|
||||
string, or a ~:LOG~ failure if all backends are exhausted.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
@@ -360,6 +376,7 @@ Verifies that the deterministic engine correctly rejects unsafe actions (like ~r
|
||||
(in-suite pipeline-reason-suite)
|
||||
|
||||
(test test-decide-gate-safety
|
||||
"Contract 1: cognitive-verify blocks unsafe actions with :LOG rejection."
|
||||
(clrhash passepartout::*skill-registry*)
|
||||
(passepartout::defskill :mock-safety
|
||||
:priority 50
|
||||
@@ -375,7 +392,7 @@ Verifies that the deterministic engine correctly rejects unsafe actions (like ~r
|
||||
(is (eq :LOG (getf result :type)))))
|
||||
|
||||
(test test-cognitive-verify-pass-through
|
||||
"Safe actions should pass through cognitive-verify unchanged."
|
||||
"Contract 1: safe actions pass through cognitive-verify unchanged."
|
||||
(clrhash passepartout::*skill-registry*)
|
||||
(passepartout::defskill :mock-passthrough
|
||||
:priority 50
|
||||
@@ -389,7 +406,7 @@ Verifies that the deterministic engine correctly rejects unsafe actions (like ~r
|
||||
(is (equal candidate result))))
|
||||
|
||||
(test test-cognitive-verify-empty-registry
|
||||
"When no gates are registered, the action passes through unchanged."
|
||||
"Contract 1: with no gates registered, action passes through unchanged."
|
||||
(clrhash passepartout::*skill-registry*)
|
||||
(let* ((candidate '(:type :REQUEST :payload (:action :shell :cmd "ls")))
|
||||
(signal '(:type :EVENT :payload (:sensor :user-input)))
|
||||
@@ -397,7 +414,7 @@ Verifies that the deterministic engine correctly rejects unsafe actions (like ~r
|
||||
(is (equal candidate result))))
|
||||
|
||||
(test test-cognitive-verify-approval-required
|
||||
"A gate returning :level :approval-required should produce an approval event."
|
||||
"Contract 1: gate returning :approval-required produces an approval event."
|
||||
(clrhash passepartout::*skill-registry*)
|
||||
(passepartout::defskill :mock-approval
|
||||
:priority 50
|
||||
|
||||
@@ -33,6 +33,15 @@ The three-tier error recovery model:
|
||||
2. **Critical errors** (undefined functions, malformed data) — require memory rollback to the last snapshot
|
||||
3. **Recursive loops** (signals generating more signals indefinitely) — depth limit enforcement
|
||||
|
||||
** Contract
|
||||
|
||||
1. (loop-process signal): the full pipeline loop — Perceive → Reason
|
||||
→ Act. Enforces depth limit (10). Catches errors with rollback and
|
||||
~:loop-error~ re-injection on non-terminal errors below depth 2.
|
||||
2. (process-signal signal): thin alias for ~loop-process~.
|
||||
3. (diagnostics-startup-run): runs health check on startup, sets
|
||||
~*system-health*~ to ~:healthy~, ~:degraded~, or ~:unhealthy~.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
@@ -311,7 +320,7 @@ Verifies that the immune system (error handling) correctly catches and reports e
|
||||
(in-suite immune-suite)
|
||||
|
||||
(test loop-error-injection
|
||||
"Verify that a crash in think/decide triggers a :loop-error stimulus."
|
||||
"Contract 1: a crash in think/decide triggers :loop-error stimulus."
|
||||
(clrhash passepartout::*skill-registry*)
|
||||
(passepartout:defskill :evil-skill
|
||||
:priority 100
|
||||
@@ -323,7 +332,7 @@ Verifies that the immune system (error handling) correctly catches and reports e
|
||||
(is (not (null (find-if (lambda (line) (search "CRITICAL BRAIN FAILURE" line)) logs))))))
|
||||
|
||||
(test test-process-signal-normal-path
|
||||
"A valid signal should pass through the pipeline without error."
|
||||
"Contract 1: a valid signal passes through the pipeline without crash."
|
||||
(clrhash passepartout::*skill-registry*)
|
||||
(handler-case
|
||||
(let ((signal (list :type :EVENT :depth 0 :payload (list :sensor :heartbeat))))
|
||||
@@ -333,7 +342,7 @@ Verifies that the immune system (error handling) correctly catches and reports e
|
||||
(fail "Pipeline crashed on normal signal: ~a" c))))
|
||||
|
||||
(test test-loop-process-returns-nil-on-deep
|
||||
"Processing a signal at depth > 10 should return nil."
|
||||
"Contract 1: depth > 10 returns nil from loop-process."
|
||||
(let ((result (loop-process '(:type :EVENT :depth 11 :payload (:sensor :heartbeat)))))
|
||||
(is (null result))))
|
||||
#+end_src
|
||||
|
||||
@@ -34,6 +34,17 @@ Git tracks changes to files. Passepartout tracks changes to live memory state. T
|
||||
|
||||
The tradeoff is memory usage: each snapshot is a deep copy of every object in active memory. 20 snapshots means 20x the active memory size. For a typical knowledge base of 10,000 objects, this is manageable (~100MB for 20 snapshots).
|
||||
|
||||
** Contract
|
||||
|
||||
1. (ingest-ast ast &key scope): stores AST nodes in ~*memory-store*~.
|
||||
Detaches children, gives each an ID, computes Merkle hash. Returns the
|
||||
root ID string.
|
||||
2. (memory-object-hash object): returns the SHA-256 Merkle hash of the
|
||||
object's content. Hash is deterministic — same content → same hash.
|
||||
3. (memory-object-get id): retrieves a stored object by ID, or nil.
|
||||
4. (snapshot-memory): deep-copies ~*memory-store*~ to ~*memory-snapshots*~.
|
||||
5. (rollback-memory snap-index): restores ~*memory-store*~ from a snapshot.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
@@ -358,6 +369,7 @@ Verifies that the Merkle hash is deterministic and consistent across independent
|
||||
(in-suite memory-suite)
|
||||
|
||||
(test merkle-hash-consistency
|
||||
"Contract 2: identical ASTs produce identical Merkle hashes."
|
||||
(let* ((ast1 '(:type :HEADLINE :properties (:ID "test-1" :TITLE "Node 1") :contents nil)))
|
||||
(clrhash passepartout::*memory-store*)
|
||||
(let ((id1 (ingest-ast ast1)))
|
||||
@@ -367,7 +379,7 @@ Verifies that the Merkle hash is deterministic and consistent across independent
|
||||
(is (equal hash1 (memory-object-hash (memory-object-get id2)))))))))
|
||||
|
||||
(test merkle-hash-different
|
||||
"Different ASTs should produce different hashes."
|
||||
"Contract 2: distinct ASTs produce different Merkle hashes."
|
||||
(clrhash passepartout::*memory-store*)
|
||||
(let* ((ast1 '(:type :HEADLINE :properties (:ID "a" :TITLE "Alpha") :contents nil))
|
||||
(ast2 '(:type :HEADLINE :properties (:ID "b" :TITLE "Beta") :contents nil))
|
||||
|
||||
@@ -25,6 +25,14 @@ After loading, the engine exports the skill's public symbols into the ~passepart
|
||||
|
||||
This is how the "thin org, fat skills" principle works in practice: the org provides the loading infrastructure; the skills provide all the intelligence.
|
||||
|
||||
** Contract
|
||||
|
||||
1. (lisp-syntax-validate code-string): returns T if the Lisp code is
|
||||
structurally valid, nil if reader errors are detected.
|
||||
2. (skill-topological-sort dir): reads org files in a directory, parses
|
||||
~#+DEPENDS_ON:~ declarations, returns files sorted such that
|
||||
dependencies come before dependents.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
@@ -425,6 +433,7 @@ Verifies that the topological sorter correctly orders skills by their ~#+DEPENDS
|
||||
(in-suite boot-suite)
|
||||
|
||||
(test test-topological-sort-basic
|
||||
"Contract 2: dependency ordering puts dependencies before dependents."
|
||||
(let ((tmp-dir "/tmp/passepartout-boot-test/"))
|
||||
(uiop:ensure-all-directories-exist (list tmp-dir))
|
||||
(with-open-file (out (merge-pathnames "org-skill-a.org" tmp-dir) :direction :output :if-exists :supersede)
|
||||
@@ -439,10 +448,10 @@ Verifies that the topological sorter correctly orders skills by their ~#+DEPENDS
|
||||
(uiop:delete-directory-tree (uiop:ensure-directory-pathname tmp-dir) :validate t))))
|
||||
|
||||
(test test-lisp-syntax-validate-valid
|
||||
"Valid Lisp code should pass syntax validation."
|
||||
"Contract 1: valid Lisp code passes syntax validation."
|
||||
(is (eq t (lisp-syntax-validate "(+ 1 2)"))))
|
||||
|
||||
(test test-lisp-syntax-validate-invalid
|
||||
"Unbalanced Lisp code should fail syntax validation."
|
||||
"Contract 1: unbalanced Lisp code fails syntax validation."
|
||||
(is (null (lisp-syntax-validate "(+ 1 2"))))
|
||||
#+end_src
|
||||
|
||||
@@ -14,10 +14,15 @@ The Doctor transforms opaque startup failures into actionable engineering report
|
||||
** Detection Invariant
|
||||
Binary detection must use shell probing (`which`) to account for varying `$PATH` inheritance between interactive and headless sessions.
|
||||
|
||||
* Phase B: Protocol (Success Criteria)
|
||||
- Dependency check passes when all required binaries are found
|
||||
- Environment check passes when XDG directories exist and are accessible
|
||||
- LLM check passes when at least one provider is configured or Ollama is running locally
|
||||
* Phase B: Contract
|
||||
|
||||
1. (diagnostics-dependencies-check): probes PATH for every binary in
|
||||
~*diagnostics-binaries*~. Returns T if all found, NIL if any missing.
|
||||
Side-effect: populates ~*doctor-missing-deps*~.
|
||||
2. (diagnostics-env-check): validates XDG directories exist. Returns T
|
||||
if all critical dirs present, NIL otherwise.
|
||||
3. (diagnostics-run-all &key auto-install): orchestrates 1-3. Returns
|
||||
a plist with ~:deps~, ~:env~, ~:llm~ keys. Respects ~:auto-install nil~.
|
||||
|
||||
* Phase C: Implementation (Build)
|
||||
|
||||
@@ -257,12 +262,12 @@ The doctor checks all supported LLM providers and detects local Ollama instances
|
||||
(in-suite diagnostics-suite)
|
||||
|
||||
(test test-diagnostics-dependency-fail
|
||||
"Verify that missing binaries are correctly identified as failures."
|
||||
"Contract 1: missing binaries cause diagnostics-dependencies-check to return nil."
|
||||
(let ((passepartout::*diagnostics-binaries* '("non-existent-binary-123")))
|
||||
(is (null (diagnostics-dependencies-check)))))
|
||||
|
||||
(test test-diagnostics-env-fail
|
||||
"Verify that an invalid MEMEX_DIR triggers a critical failure."
|
||||
"Contract 2: invalid MEMEX_DIR causes diagnostics-env-check to return nil."
|
||||
(let ((old-m (uiop:getenv "MEMEX_DIR"))
|
||||
(old-d (uiop:getenv "PASSEPARTOUT_DATA_DIR")))
|
||||
(unwind-protect
|
||||
@@ -273,7 +278,7 @@ The doctor checks all supported LLM providers and detects local Ollama instances
|
||||
(setf (uiop:getenv "PASSEPARTOUT_DATA_DIR") (or old-d "")))))
|
||||
|
||||
(test test-diagnostics-dependency-success
|
||||
"When all binaries exist, diagnostics should pass."
|
||||
"Contract 1: all binaries present returns T."
|
||||
(let ((passepartout::*diagnostics-binaries* '("ls")))
|
||||
(is (eq t (diagnostics-dependencies-check)))))
|
||||
#+end_src
|
||||
|
||||
@@ -13,6 +13,18 @@ Providers register themselves at boot. No API key? That provider doesn't registe
|
||||
|
||||
=*provider-cascade*= defaults to cloud-only (all providers except ~:local~ and ~:ollama~). If you want a local fallback, set ~LOCAL_BASE_URL~ in your env and add ~:local~ to the ~PROVIDER_CASCADE~ list.
|
||||
|
||||
** Contract
|
||||
|
||||
1. (provider-config provider): returns the configuration plist for a
|
||||
provider keyword, or nil if unregistered.
|
||||
2. (provider-available-p provider): returns T if the provider's API key
|
||||
or base URL is configured.
|
||||
3. (provider-openai-request prompt system-prompt &key model provider):
|
||||
executes an OpenAI-compatible /v1/chat/completions request. Returns
|
||||
~(:status :success :content ...)~ or ~(:status :error :message ...)~.
|
||||
4. (provider-cascade-initialize): reads ~PROVIDER_CASCADE~ from env and
|
||||
sets ~*provider-cascade*~.
|
||||
|
||||
* Implementation
|
||||
|
||||
** Provider registry
|
||||
@@ -177,12 +189,12 @@ If API-KEY is nil, reads from environment."
|
||||
(fiveam:in-suite llm-gateway-suite)
|
||||
|
||||
(fiveam:test test-provider-rejects-bad-keyword
|
||||
"Verify that provider-openai-request returns :error for an unregistered provider."
|
||||
"Edge: provider-openai-request returns :error for unregistered provider."
|
||||
(let ((result (provider-openai-request "hello" "test" :provider :not-a-real-provider)))
|
||||
(fiveam:is (eq (getf result :status) :error))))
|
||||
|
||||
(fiveam:test test-provider-config-registered
|
||||
"A registered provider should return its configuration plist."
|
||||
"Contract: provider-config returns configuration plist for registered provider."
|
||||
(let ((config (provider-config :openrouter)))
|
||||
(fiveam:is (listp config))
|
||||
(fiveam:is (getf config :base-url))))
|
||||
|
||||
Reference in New Issue
Block a user