diff --git a/enkan-repl.el b/enkan-repl.el index 42f1cda..0517876 100644 --- a/enkan-repl.el +++ b/enkan-repl.el @@ -411,6 +411,16 @@ ALIASES may be a list of strings or an alist of (alias . project-name)." (or (cdr (assoc alias project-aliases)) alias)) +(defun enkan-repl--merge-target-directories (primary fallback) + "Return target directories from PRIMARY merged with FALLBACK. +Entries in PRIMARY win when both lists contain the same alias. This keeps +workspace-local imported paths authoritative while preserving the user's wider +target directory registry across workspace loads." + (append primary + (cl-remove-if (lambda (entry) + (assoc (car entry) primary)) + fallback))) + (defun enkan-repl--projects-with-current-aliases (projects current-project project-aliases) "Return PROJECTS augmented with CURRENT-PROJECT's PROJECT-ALIASES. Merge the current workspace's aliases into CURRENT-PROJECT's project @@ -460,7 +470,9 @@ This function restores workspace state from the given plist." (setq enkan-repl-project-aliases (plist-get state :project-aliases))) (when (plist-member state :target-directories) (setq enkan-repl-target-directories - (plist-get state :target-directories)))) + (enkan-repl--merge-target-directories + (plist-get state :target-directories) + enkan-repl-target-directories)))) (defun enkan-repl--save-workspace-state (&optional workspace-id) "Save current globals into `enkan-repl--workspaces' under WORKSPACE-ID. @@ -798,23 +810,29 @@ This ensures that directory lookups work correctly after workspace switch." ;; Find project directories from files in standard locations (let ((dirs '())) ;; Check standard project locations - (dolist (alias enkan-repl-project-aliases) + (dolist (alias (enkan-repl--project-alias-names enkan-repl-project-aliases)) ;; Try to find project directory from standard locations - (let* ((home-dir (expand-file-name "~")) + (let* ((project-name + (enkan-repl--project-name-for-alias + alias enkan-repl-project-aliases)) + (home-dir (expand-file-name "~")) (common-paths (list - (expand-file-name (format "dev/self/%s" enkan-repl--current-project) home-dir) - (expand-file-name (format "dev/%s" enkan-repl--current-project) home-dir) - (expand-file-name (format "Documents/%s" enkan-repl--current-project) home-dir) - (expand-file-name (format "projects/%s" enkan-repl--current-project) home-dir) - (expand-file-name enkan-repl--current-project default-directory)))) + (expand-file-name (format "dev/self/%s" project-name) home-dir) + (expand-file-name (format "dev/%s" project-name) home-dir) + (expand-file-name (format "Documents/%s" project-name) home-dir) + (expand-file-name (format "projects/%s" project-name) home-dir) + (expand-file-name project-name default-directory)))) ;; Find first existing directory (dolist (path common-paths) (when (and (not (assoc alias dirs)) (file-directory-p path)) - (push (cons alias (cons enkan-repl--current-project path)) dirs))))) + (push (cons alias (cons project-name path)) dirs))))) ;; Update enkan-repl-target-directories if we found directories (when dirs - (setq enkan-repl-target-directories dirs))))) + (setq enkan-repl-target-directories + (enkan-repl--merge-target-directories + (nreverse dirs) + enkan-repl-target-directories)))))) (defun enkan-repl--initialize-default-workspace () "Initialize default workspace '01' with first available project. @@ -1509,7 +1527,8 @@ COUNTER: session counter" (defun enkan-repl--setup-start-sessions (alias-list buffer-name) "Start terminal sessions for each alias in ALIAS-LIST and log to BUFFER-NAME. -Includes error handling for individual session failures." +Includes error handling for individual session failures. +Returns a plist with `:success-count' and `:failure-count'." (with-current-buffer buffer-name (princ "šŸš€ Starting terminal sessions:\n")) (let ((session-number 1) @@ -1533,7 +1552,9 @@ Includes error handling for individual session failures." (setq failure-count (1+ failure-count)))) (setq session-number (1+ session-number))) (with-current-buffer buffer-name - (princ (format "\nšŸ“Š Session start summary: %d success, %d failed\n\n" success-count failure-count))))) + (princ (format "\nšŸ“Š Session start summary: %d success, %d failed\n\n" success-count failure-count))) + (list :success-count success-count + :failure-count failure-count))) (defun enkan-repl--setup-project-session (alias) "Setup project session for given ALIAS. @@ -1600,7 +1621,12 @@ Category: Session Controller" (error "Project '%s' not found" project-name)) (enkan-repl--setup-set-project-aliases project-name alias-list buffer-name) ;; Start sessions - (enkan-repl--setup-start-sessions alias-list buffer-name)) + (let ((start-result + (enkan-repl--setup-start-sessions alias-list buffer-name))) + (when (> (plist-get start-result :failure-count) 0) + (error "Failed to start %d of %d terminal session(s)" + (plist-get start-result :failure-count) + (length alias-list))))) ;; Set final project configuration (enkan-repl--ws-set-current-project project-name) (princ (format "\nāœ… Setup completed for project: %s\n" project-name)) diff --git a/test/enkan-repl-workspace-create-test.el b/test/enkan-repl-workspace-create-test.el index b8506f4..cb79aa0 100644 --- a/test/enkan-repl-workspace-create-test.el +++ b/test/enkan-repl-workspace-create-test.el @@ -118,6 +118,24 @@ (should (string= (plist-get ws-state :current-project) "MyProject")) (should (equal (plist-get ws-state :project-aliases) '("alias1")))))) +(ert-deftest test-enkan-repl--setup-start-sessions-reports-failures () + "Session setup should report failures instead of looking successful." + (let ((buffer-name "*enkan-repl-test-setup-start*") + (enkan-repl-target-directories nil)) + (unwind-protect + (with-current-buffer (get-buffer-create buffer-name) + (erase-buffer) + (let* ((standard-output (current-buffer)) + (result (enkan-repl--setup-start-sessions + '("junk") + buffer-name))) + (should (= 0 (plist-get result :success-count))) + (should (= 1 (plist-get result :failure-count))) + (should (string-match-p "Project alias .junk. not found" + (buffer-string))))) + (when (get-buffer buffer-name) + (kill-buffer buffer-name))))) + (ert-deftest test-enkan-repl--setup-window-terminal-buffer-pure-workspace-context () "Test that window eat buffer name includes correct workspace ID." ;; Load window-layouts if exists @@ -149,4 +167,4 @@ (should (string-match-p "\\*ws:02 " (cdr result)))))))) (provide 'enkan-repl-workspace-create-test) -;;; enkan-repl-workspace-create-test.el ends here \ No newline at end of file +;;; enkan-repl-workspace-create-test.el ends here diff --git a/test/enkan-repl-workspace-delete-deletion-test.el b/test/enkan-repl-workspace-delete-deletion-test.el index 5e2b6c2..7ea6956 100644 --- a/test/enkan-repl-workspace-delete-deletion-test.el +++ b/test/enkan-repl-workspace-delete-deletion-test.el @@ -44,5 +44,39 @@ (should (equal 1 enkan-repl--session-counter)) (should (equal "other" enkan-repl--current-project))))) +(ert-deftest test-workspace-delete-preserves-target-registry-for-new-setup () + "Deleting a workspace must not make unrelated project aliases unusable." + (let ((enkan-repl--workspaces + '(("01" . (:current-project "junk" + :session-list ((1 . "junk")) + :session-counter 1 + :project-aliases (("junk" . "junk")) + :target-directories + (("junk" . ("junk" . "/repo/junk"))))) + ("02" . (:current-project "er" + :session-list ((1 . "enkan-repl")) + :session-counter 1 + :project-aliases (("er" . "enkan-repl")) + :target-directories + (("er" . ("enkan-repl" . "/repo/enkan-repl"))))))) + (enkan-repl--current-workspace "01") + (enkan-repl-session-list '((1 . "junk"))) + (enkan-repl--session-counter 1) + (enkan-repl--current-project "junk") + (enkan-repl-project-aliases '(("junk" . "junk"))) + (enkan-repl-target-directories + '(("junk" . ("junk" . "/repo/junk")) + ("er" . ("enkan-repl" . "/repo/enkan-repl"))))) + (cl-letf (((symbol-function 'enkan-repl--stop-workspace-terminals) + (lambda (_workspace-id) + (list :buffers-killed 0 :tmux-killed nil)))) + (enkan-repl--delete-workspace-completely "01") + (should (equal "02" enkan-repl--current-workspace)) + (should (equal '(("er" . ("enkan-repl" . "/repo/enkan-repl")) + ("junk" . ("junk" . "/repo/junk"))) + enkan-repl-target-directories)) + (should (equal '("junk" . "/repo/junk") + (enkan-repl--setup-project-session "junk")))))) + (provide 'enkan-repl-workspace-delete-deletion-test) ;;; enkan-repl-workspace-delete-deletion-test.el ends here diff --git a/test/enkan-repl-workspace-state-test.el b/test/enkan-repl-workspace-state-test.el index 43d8a2b..0e3921d 100644 --- a/test/enkan-repl-workspace-state-test.el +++ b/test/enkan-repl-workspace-state-test.el @@ -73,6 +73,69 @@ (should (equal enkan-repl--session-counter 2)) (should (equal enkan-repl-project-aliases '(("p3" . "proj3") ("p4" . "proj4"))))))) +(ert-deftest test-enkan-repl--load-workspace-state-preserves-target-registry () + "Loading a workspace should not discard unrelated configured targets." + (let ((enkan-repl--workspaces + '(("01" . (:current-project "er" + :session-list ((1 . "enkan-repl")) + :session-counter 1 + :project-aliases (("er" . "enkan-repl")) + :target-directories + (("er" . ("enkan-repl" . "/repo/enkan-repl"))))))) + (enkan-repl--current-workspace "01") + (enkan-repl--current-project nil) + (enkan-repl-session-list nil) + (enkan-repl--session-counter 0) + (enkan-repl-project-aliases nil) + (enkan-repl-target-directories + '(("er" . ("enkan-repl" . "/repo/enkan-repl")) + ("junk" . ("junk" . "/repo/junk"))))) + (enkan-repl--load-workspace-state "01") + (should (equal '(("er" . ("enkan-repl" . "/repo/enkan-repl")) + ("junk" . ("junk" . "/repo/junk"))) + enkan-repl-target-directories)) + (should (equal '("junk" . "/repo/junk") + (enkan-repl--setup-project-session "junk"))))) + +(ert-deftest test-enkan-repl--load-legacy-state-merges-discovered-targets () + "Loading old states without target dirs should preserve the target registry." + (let* ((project-name "enkan-repl-legacy-project") + (temp-root (file-name-as-directory + (make-temp-file "enkan-repl-legacy-state-" t))) + (project-dir (expand-file-name project-name temp-root)) + (default-directory temp-root) + (enkan-repl--workspaces + `(("01" . (:current-project ,project-name + :session-list ((1 . ,project-name)) + :session-counter 1 + :project-aliases (("legacy" . ,project-name)))))) + (enkan-repl--current-workspace "01") + (enkan-repl--current-project nil) + (enkan-repl-session-list nil) + (enkan-repl--session-counter 0) + (enkan-repl-project-aliases nil) + (enkan-repl-target-directories + '(("junk" . ("junk" . "/repo/junk"))))) + (unwind-protect + (progn + (make-directory project-dir) + (enkan-repl--load-workspace-state "01") + (should (equal `(("legacy" . (,project-name . ,project-dir)) + ("junk" . ("junk" . "/repo/junk"))) + enkan-repl-target-directories)) + (should (equal '("junk" . "/repo/junk") + (enkan-repl--setup-project-session "junk")))) + (delete-directory temp-root t)))) + +(ert-deftest test-enkan-repl--merge-target-directories-primary-wins () + "Workspace target directories should win over preserved registry entries." + (should (equal '(("er" . ("enkan-repl" . "/state/enkan-repl")) + ("junk" . ("junk" . "/repo/junk"))) + (enkan-repl--merge-target-directories + '(("er" . ("enkan-repl" . "/state/enkan-repl"))) + '(("er" . ("enkan-repl" . "/config/enkan-repl")) + ("junk" . ("junk" . "/repo/junk"))))))) + (ert-deftest test-enkan-repl--save-workspace-state () "Test saving workspace state." (let ((enkan-repl--workspaces nil)