FEAT: Implement Micro-Loader with Topological Sort and Jailing
This commit is contained in:
@@ -6,29 +6,69 @@
|
||||
(def-suite boot-suite :description "Verification of the Micro-Loader.")
|
||||
(in-suite boot-suite)
|
||||
|
||||
(test test-skill-catalog-tracking
|
||||
"Verify that skills are added to the catalog with correct status."
|
||||
(clrhash org-agent::*skill-catalog*)
|
||||
;; We need a temporary skill file to test loading
|
||||
(let ((tmp-skill "/tmp/org-skill-test-catalog.org"))
|
||||
(with-open-file (out tmp-skill :direction :output :if-exists :supersede)
|
||||
(format out "#+TITLE: Test Skill~%#+begin_src lisp~%(defun test-catalog-fn () t)~%#+end_src"))
|
||||
|
||||
(org-agent:load-skill-from-org tmp-skill)
|
||||
(let ((entry (gethash "org-skill-test-catalog" org-agent::*skill-catalog*)))
|
||||
(is (not (null entry)))
|
||||
(is (eq :ready (org-agent::skill-entry-status entry))))
|
||||
(uiop:delete-file-if-exists tmp-skill)))
|
||||
(test test-parse-skill-metadata
|
||||
"Verify extraction of ID and DEPENDS_ON from Org headers."
|
||||
(let ((tmp-file "/tmp/org-skill-test-metadata.org"))
|
||||
(with-open-file (out tmp-file :direction :output :if-exists :supersede)
|
||||
(format out ":PROPERTIES:~%:ID: test-id~%:END:~%#+DEPENDS_ON: dep1 dep2~%"))
|
||||
(unwind-protect
|
||||
(multiple-value-bind (id deps) (org-agent::parse-skill-metadata tmp-file)
|
||||
(is (equal "test-id" id))
|
||||
(is (member "dep1" deps :test #'string=))
|
||||
(is (member "dep2" deps :test #'string=)))
|
||||
(uiop:delete-file-if-exists tmp-file))))
|
||||
|
||||
(test test-syntax-preflight-blocking
|
||||
"Verify that malformed Lisp prevents skill from loading."
|
||||
(clrhash org-agent::*skill-catalog*)
|
||||
(let ((bad-skill "/tmp/org-skill-bad-syntax.org"))
|
||||
(with-open-file (out bad-skill :direction :output :if-exists :supersede)
|
||||
(format out "#+TITLE: Bad Skill~%#+begin_src lisp~%(defun unclosed (x~%#+end_src"))
|
||||
(test test-topological-sort-basic
|
||||
"Verify that skills are ordered by dependency."
|
||||
(let ((tmp-dir "/tmp/org-agent-boot-test/"))
|
||||
(uiop:ensure-all-directories-exist (list tmp-dir))
|
||||
;; A depends on B
|
||||
(with-open-file (out (merge-pathnames "org-skill-a.org" tmp-dir) :direction :output :if-exists :supersede)
|
||||
(format out "#+DEPENDS_ON: id:skill-b-id~%"))
|
||||
(with-open-file (out (merge-pathnames "org-skill-b.org" tmp-dir) :direction :output :if-exists :supersede)
|
||||
(format out ":PROPERTIES:~%:ID: skill-b-id~%:END:~%"))
|
||||
;; Add executive soul (required)
|
||||
(with-open-file (out (merge-pathnames "org-skill-agent.org" tmp-dir) :direction :output :if-exists :supersede)
|
||||
(format out "#+TITLE: Agent~%"))
|
||||
|
||||
(org-agent:load-skill-from-org bad-skill)
|
||||
(let ((entry (gethash "org-skill-bad-syntax" org-agent::*skill-catalog*)))
|
||||
(is (eq :failed (org-agent::skill-entry-status entry)))
|
||||
(is (search "Syntax Error" (org-agent::skill-entry-error-log entry))))
|
||||
(uiop:delete-file-if-exists bad-skill)))
|
||||
(unwind-protect
|
||||
(let ((sorted (org-agent::topological-sort-skills tmp-dir)))
|
||||
;; B must appear before A
|
||||
(let ((pos-a (position "org-skill-a" sorted :key #'pathname-name :test #'string-equal))
|
||||
(pos-b (position "org-skill-b" sorted :key #'pathname-name :test #'string-equal)))
|
||||
(is (not (null pos-a)))
|
||||
(is (not (null pos-b)))
|
||||
(is (< pos-b pos-a))))
|
||||
(uiop:delete-directory-tree (uiop:ensure-directory-pathname tmp-dir) :validate t))))
|
||||
|
||||
(test test-topological-sort-circular
|
||||
"Verify that circular dependencies raise an error."
|
||||
(let ((tmp-dir "/tmp/org-agent-boot-test-circ/"))
|
||||
(uiop:ensure-all-directories-exist (list tmp-dir))
|
||||
;; A depends on B, B depends on A
|
||||
(with-open-file (out (merge-pathnames "org-skill-a.org" tmp-dir) :direction :output :if-exists :supersede)
|
||||
(format out "#+DEPENDS_ON: org-skill-b~%"))
|
||||
(with-open-file (out (merge-pathnames "org-skill-b.org" tmp-dir) :direction :output :if-exists :supersede)
|
||||
(format out "#+DEPENDS_ON: org-skill-a~%"))
|
||||
|
||||
(unwind-protect
|
||||
(signals error (org-agent::topological-sort-skills tmp-dir))
|
||||
(uiop:delete-directory-tree (uiop:ensure-directory-pathname tmp-dir) :validate t))))
|
||||
|
||||
(test test-skill-jailing
|
||||
"Verify that skills are loaded into their own packages."
|
||||
(let ((tmp-skill "/tmp/org-skill-jail-test.org"))
|
||||
(with-open-file (out tmp-skill :direction :output :if-exists :supersede)
|
||||
(format out "#+begin_src lisp~%(defvar *jailed-var* 42)~%#+end_src"))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(org-agent::load-skill-from-org tmp-skill)
|
||||
(let ((pkg (find-package :ORG-AGENT.SKILLS.ORG-SKILL-JAIL-TEST)))
|
||||
(is (not (null pkg)))
|
||||
(is (= 42 (symbol-value (find-symbol "*JAILED-VAR*" pkg))))))
|
||||
(uiop:delete-file-if-exists tmp-skill))))
|
||||
|
||||
(test test-syntax-validation
|
||||
"Verify that malformed Lisp is caught by the pre-flight check."
|
||||
(is (nth-value 0 (org-agent::validate-lisp-syntax "(defun x () t)")))
|
||||
(is (not (nth-value 0 (org-agent::validate-lisp-syntax "(defun x (")))))
|
||||
|
||||
Reference in New Issue
Block a user