diff --git a/Autotests/mock/test_workflow_plugin_mock.py b/Autotests/mock/test_workflow_plugin_mock.py index ba057510..dddfb419 100644 --- a/Autotests/mock/test_workflow_plugin_mock.py +++ b/Autotests/mock/test_workflow_plugin_mock.py @@ -32,7 +32,7 @@ DEMO_MESSAGE = "This is a test workflow demonstration" RESEARCH_WORKFLOW = "research-workflow" -RESEARCH_DIR = "/PeTTa/repos/Omega/memory/workflow_space/research" +RESEARCH_DIR = "/PeTTa/repos/Omega/memory/workflow/space/research" RESEARCH_NAME = "qa-research-autotest" diff --git a/config/config.yaml b/config/config.yaml index 8cbf65da..f276fba3 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -157,13 +157,17 @@ asione_model: "asi1-ultra" # Plugins # Workflow -# Path to the directory which contains available workflows. Default value is -# /plugins/workflow/instructions. See plugins/workflow/README.md -# for details. -#pluginWorkflowInstructionsDir: ./plugins/workflow/instructions +# Path to the directory which contains built-in workflows shipped with Omega. +# Default value is /plugins/workflow/instructions. See +# plugins/workflow/README.md for details. +pluginWorkflowInstructionsDir: ./repos/Omega/plugins/workflow/instructions +# Path to the directory which contains custom workflows. A custom workflow +# overrides the built-in one with the same name. Default value is +# /workflow/skills +pluginWorkflowCustomInstructionsDir: ./repos/Omega/memory/workflow/skills # Path to the directory to keep workflow working files when workflow is active. -# Default value is /workflow_space -#pluginWorkflowMemoryDir: ./memory/workflow_space +# Default value is /workflow/space +pluginWorkflowMemoryDir: ./repos/Omega/memory/workflow/space # OpenClaw openClawEnabled: disabled diff --git a/docs/reference-channels.md b/docs/reference-channels.md index 03513ef1..affd15ae 100644 --- a/docs/reference-channels.md +++ b/docs/reference-channels.md @@ -79,7 +79,7 @@ Minimal JSON chat adapter over a WebSocket connection. Selected with `commchanne - `stop_websocket()` — stop the listener thread and close the socket. - Requires the `websockets` Python package. - When `WS_TOKEN` is set it is sent as an `Authorization: Bearer ` header. Unlike the IRC/Telegram/Slack adapters there is no one-time `auth ` gate — trust is established by the endpoint URL and bearer token. -- Supports immediate `/memory-export history|ltm|both` commands when memory export is enabled and `WS_TOKEN` is configured. The export handler uses a SHA-256-derived connection principal and never exposes the bearer token. Protect the endpoint with `wss://` and server-side access controls because WebSocket does not have the per-user ownership gate used by the other channels. +- Supports immediate `/memory-export history|ltm|skills|all` commands when memory export is enabled and `WS_TOKEN` is configured. The export handler uses a SHA-256-derived connection principal and never exposes the bearer token. Protect the endpoint with `wss://` and server-side access controls because WebSocket does not have the per-user ownership gate used by the other channels. - Reconnects automatically with exponential backoff (1s → 30s, ±20% jitter) and is safe to start once at process startup. ### Frame protocol diff --git a/docs/reference-memory-portability.md b/docs/reference-memory-portability.md index 097e81ce..b95d3f88 100644 --- a/docs/reference-memory-portability.md +++ b/docs/reference-memory-portability.md @@ -47,7 +47,8 @@ token so the credential itself is never used as an identifier: ```text /memory-export history /memory-export ltm -/memory-export both +/memory-export skills +/memory-export all ``` Completion is delivered through the active channel and includes the filename, @@ -75,10 +76,15 @@ manifest.json history/history.metta vector/collections.json vector/records.jsonl +skills/... +space/... ``` History is the conversation trace. LTM is logical user-memory records from -ChromaDB. Prompts, credentials, logs, skills, and other operational state are +ChromaDB. Skills are custom workflows from `pluginWorkflowCustomInstructionsDir` +together with workflow working files from `pluginWorkflowMemoryDir`; only +regular files are exported, symbolic links and empty directories are skipped. +Built-in workflows, prompts, credentials, logs, and other operational state are not exported. SHA-256 detects corruption, not archive authorship. ## Import @@ -106,9 +112,12 @@ scripts/omega start -d "$image" -p OpenAI -t telegram \ ``` `overwrite` replaces the selected components after validation and rollback -preparation. `append` preserves existing history and adds imported LTM records -under new IDs. Select a single component with `--only-history` or -`--only-vector`. Without either option, both components are imported. +preparation. For skills it replaces both the custom skills and the workflow +space directories, even when the archive contains no skills. `append` preserves +existing history and adds imported LTM records under new IDs, and copies imported +skills and space files over the existing ones with the same names. Select a +single component with `--only-history`, `--only-vector`, or `--only-skills`. +Without these options, all components present in the archive are imported. The importer validates archive paths, checksums, manifest metadata, record counts, and embedding compatibility before changing live memory. It runs before diff --git a/entrypoint.sh b/entrypoint.sh index aea098f1..1ae0effb 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -66,6 +66,7 @@ elif operation == "import": mode=os.environ.get("MEMORY_IMPORT_MODE", "overwrite"), include_history=os.environ.get("MEMORY_IMPORT_NO_HISTORY") != "1", include_vectors=os.environ.get("MEMORY_IMPORT_NO_VECTOR") != "1", + include_skills=os.environ.get("MEMORY_IMPORT_NO_SKILLS") != "1", ) else: raise ValueError(f"Unsupported memory portability operation: {operation!r}")' diff --git a/plugins/workflow/README.md b/plugins/workflow/README.md index dd555817..76a46136 100644 --- a/plugins/workflow/README.md +++ b/plugins/workflow/README.md @@ -60,7 +60,19 @@ For example: Start an agent and ask it to use the workflow. For the test workflow above send: `Demonstrate workflow plugin` -New workflows put into `pluginWorkflowInstructionsDir` while the agent is +## Built-in and custom workflows + +Workflows are looked up in two directories: +- built-in workflows shipped with Omega are kept in + `pluginWorkflowInstructionsDir` and are updated together with Omega; +- custom workflows are kept in `pluginWorkflowCustomInstructionsDir` which is + placed in the memory directory by default, so they survive container + restarts and image updates. The directory is created on start if missing. + +A custom workflow overrides the built-in one with the same name. To change a +built-in workflow copy it into the custom directory and edit the copy. + +New workflows put into `pluginWorkflowCustomInstructionsDir` while the agent is running become available after the agent calls the `workflow-refresh-list` skill. Ask the agent to refresh the list of workflows, or just ask it to load the new workflow by name. @@ -69,11 +81,14 @@ the new workflow by name. Wofkflow plugin Omega configuration parameters: - `pluginWorkflowInstructionsDir` - path to the directory which contains - available workflows. Default value is `/plugins/workflow/instructions` + built-in workflows. Default value is `/plugins/workflow/instructions` +- `pluginWorkflowCustomInstructionsDir` - path to the directory which contains + custom workflows. Default value is `/workflow/skills` - `pluginWorkflowMemoryDir` - path to the directory to keep workflow working - files when workflow is active. Default value is `/memory/workflow_space` + files when workflow is active. Default value is `/workflow/space` + +Custom and working directories are created on start together with missing +parent directories. One can set this parameters passing them as a command line arguments. For example: diff --git a/plugins/workflow/workflow.metta b/plugins/workflow/workflow.metta index 3058c12d..c78336c5 100644 --- a/plugins/workflow/workflow.metta +++ b/plugins/workflow/workflow.metta @@ -1,6 +1,7 @@ !(import! &self (library lib_spaces)) (= (pluginWorkflowInstructionsDir) (empty)) +(= (pluginWorkflowCustomInstructionsDir) (empty)) (= (pluginWorkflowMemoryDir) (empty)) (= (loadOmegaPlugin) @@ -9,10 +10,15 @@ (configure pluginWorkflowInstructionsDir (joinPath ((projectRootDirectory) "plugins" "workflow" "instructions"))) (if (not (exists-directory (pluginWorkflowInstructionsDir))) (Error (_loadOmegaPlugin) (strings-concat ("Instructions directory: " (pluginWorkflowInstructionsDir) " doesn't exist"))) - (progn - (configure pluginWorkflowMemoryDir (joinPath ((memoryDirectory) workflow_space))) + (progn + (configure pluginWorkflowCustomInstructionsDir (joinPath ((memoryDirectory) "workflow" "skills"))) + (let $customrc (make-directory-path (pluginWorkflowCustomInstructionsDir)) ()) + (if (not (exists-directory (pluginWorkflowCustomInstructionsDir))) + (log ERROR "workflow-plugin" (strings-concat ("Could not create custom instructions directory: " (pluginWorkflowCustomInstructionsDir)", only built-in workflows are available"))) + ()) + (configure pluginWorkflowMemoryDir (joinPath ((memoryDirectory) "workflow" "space"))) (log INFO "workflow-plugin" (strings-concat ("Creating working directory: " (pluginWorkflowMemoryDir)))) - (let $rc (make-directory (pluginWorkflowMemoryDir)) ()) + (let $rc (make-directory-path (pluginWorkflowMemoryDir)) ()) (if (exists-directory (pluginWorkflowMemoryDir)) (progn (log INFO "workflow-plugin" "Adding plugin's skills") @@ -45,12 +51,19 @@ ; False)) (= (instructionsDir) (get-existing-dir (pluginWorkflowInstructionsDir))) + +;; custom workflow overrides built-in one with the same name +(= (workflow-dir $name) + (if (exists-file (joinPath ((pluginWorkflowCustomInstructionsDir) $name "SKILL.md"))) + (pluginWorkflowCustomInstructionsDir) + (instructionsDir))) + (= (workflow-load-instructions $name) (let $active (get-state &workflow_active_name) (if (== $active "") (if (not (valid-file-or-folder-name $name)) (Error (quote (workflow-load-instructions $name)) (strings-concat ("ERROR: invalid workflow name: " $name))) - (let $path (joinPath ((instructionsDir) $name "SKILL.md")) + (let $path (joinPath ((workflow-dir $name) $name "SKILL.md")) (if (not (exists-file $path)) (Error (quote (workflow-load-instructions $name)) (strings-concat ("ERROR: skill file not found: " $path ". Try to call workflow-refresh-list"))) (let $content (read-file $path) @@ -83,7 +96,7 @@ ; to load skills descriptions which are defined only for current workflow (= (add-workflow-skills $name) - (let $path (joinPath ((instructionsDir) $name "skill")) + (let $path (joinPath ((workflow-dir $name) $name "skill")) (progn (import! &workflow_skills $path) (collapse (match &workflow_skills (skill $func $desc $args) (add-skill $func $desc $args))) @@ -102,11 +115,29 @@ (add-prompt-extension workflow_active_instructions $content))) ;;========================== Functions to load workflow description ======================= -;; returns workflow name and its description +;; returns workflow name and its description, custom workflows override built-in ones with the same name (= (workflow-list-available) (let* ( ($base (instructionsDir)) - ($dirs (directory-dirs $base))) - (collect-description $dirs $base))) + ($builtin (collect-description (filter-shadowed (directory-dirs $base)) $base ())) + ($custom (pluginWorkflowCustomInstructionsDir))) + (if (exists-directory $custom) + (collect-description (directory-dirs $custom) $custom $builtin) + $builtin))) + +;; drops built-in workflows overridden by custom ones with the same name +(= (filter-shadowed ()) ()) +(= (filter-shadowed (cons $first $tail)) + (let $rest (filter-shadowed $tail) + (if (exists-file (joinPath ((pluginWorkflowCustomInstructionsDir) $first "SKILL.md"))) + $rest + (cons-atom $first $rest)))) + +;; re-reads available workflows into the prompt +(= (workflow-refresh-list) + (let $text (join (newline) (cons-atom "Available workflows: " (workflow-list-available))) + (progn + (remove-prompt-extension workflows-available) + (add-prompt-extension workflows-available $text)))) ;; re-reads available workflows into the prompt (= (workflow-refresh-list) @@ -116,10 +147,10 @@ (add-prompt-extension workflows-available $text)))) ;; collects workflow description from description.txt -(= (collect-description () $base) ()) -(= (collect-description (cons $first $tail) $base) +(= (collect-description () $base $acc) $acc) +(= (collect-description (cons $first $tail) $base $acc) (let* (($path (joinPath ($base $first "description.txt"))) - ($rest (collect-description $tail $base))) + ($rest (collect-description $tail $base $acc))) (if (exists-file $path) (cons-atom (strings-concat ($first ": " (read-file $path))) $rest) $rest))) diff --git a/requirements.txt b/requirements.txt index 49dd2c7e..b06c9448 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ chromadb==1.5.9 openai==2.38.0 transformers==5.8.0 sentence-transformers==5.5.1 -import-kb==0.2.5 +import-kb==0.2.6 py-landlock==0.1.1 pyyaml==6.0.3 ddgs==9.14.4 diff --git a/scripts/omega b/scripts/omega index 7d66dbf5..6e3e2b73 100755 --- a/scripts/omega +++ b/scripts/omega @@ -673,6 +673,7 @@ help() { echo -e "\t--memory-mode overwrite|append import mode (default: overwrite)" echo -e "\t--only-history restore history only" echo -e "\t--only-vector restore long-term memory only" + echo -e "\t--only-skills restore custom skills and workflow space only" } require_option_value() { @@ -704,6 +705,7 @@ options() { memory_import_mode="overwrite" memory_import_only_history=0 memory_import_only_vector=0 + memory_import_only_skills=0 memory_export_enabled=0 if [[ "$#" -eq 0 ]]; then @@ -767,6 +769,10 @@ options() { memory_import_only_vector=1 shift ;; + --only-skills) + memory_import_only_skills=1 + shift + ;; --version|-v) version; exit 0;; --help|-h) help; exit 0;; *) @@ -854,13 +860,12 @@ options() { validate_memory_archive_name "${memory_import_file}" "--memory-import" || return 1 fi - if [[ "${memory_import_only_history}" == "1" && "${memory_import_only_vector}" == "1" ]]; then - echo "--only-history and --only-vector cannot be combined" >&2 + if (( memory_import_only_history + memory_import_only_vector + memory_import_only_skills > 1 )); then + echo "--only-history, --only-vector and --only-skills cannot be combined" >&2 return 1 fi - if [[ -z "${memory_import_file}" && - ( "${memory_import_only_history}" == "1" || - "${memory_import_only_vector}" == "1" ) ]]; then + if [[ -z "${memory_import_file}" ]] && + (( memory_import_only_history + memory_import_only_vector + memory_import_only_skills > 0 )); then echo "Import component flags require --memory-import" >&2 return 1 fi @@ -1140,10 +1145,13 @@ if sys.argv[1]: -e "MEMORY_IMPORT_MODE=${memory_import_mode}" ) if [[ "${memory_import_only_history}" == "1" ]]; then - memory_import_env+=(-e MEMORY_IMPORT_NO_VECTOR=1) + memory_import_env+=(-e MEMORY_IMPORT_NO_VECTOR=1 -e MEMORY_IMPORT_NO_SKILLS=1) fi if [[ "${memory_import_only_vector}" == "1" ]]; then - memory_import_env+=(-e MEMORY_IMPORT_NO_HISTORY=1) + memory_import_env+=(-e MEMORY_IMPORT_NO_HISTORY=1 -e MEMORY_IMPORT_NO_SKILLS=1) + fi + if [[ "${memory_import_only_skills}" == "1" ]]; then + memory_import_env+=(-e MEMORY_IMPORT_NO_HISTORY=1 -e MEMORY_IMPORT_NO_VECTOR=1) fi fi diff --git a/src/memory_export.py b/src/memory_export.py index 65177875..5e90507d 100644 --- a/src/memory_export.py +++ b/src/memory_export.py @@ -34,6 +34,10 @@ def _resolve_chroma_path() -> Path: return Path(str(configured)).expanduser().resolve() +def _resolve_config_path(key: str) -> Path: + return Path(str(config_get_by_key(key))).expanduser().resolve() + + def _cloud_embedder(provider, model): def embed(texts): from import_knowledge.import_knowledge import embed_batch, init_embeddings @@ -67,6 +71,8 @@ def create_memory_store(): memory_dir=_resolve_memory_dir(), chroma_path=_resolve_chroma_path(), collection_name="memories", + skills_dir=_resolve_config_path("pluginWorkflowCustomInstructionsDir"), + space_dir=_resolve_config_path("pluginWorkflowMemoryDir"), **_embedding_options(), ) @@ -89,7 +95,7 @@ def _get_transfer(): return _transfer -_VALID_COMPONENTS = ("history", "ltm", "both") +_VALID_COMPONENTS = ("history", "ltm", "skills", "all") def is_export_enabled() -> bool: @@ -135,7 +141,7 @@ def handle_export_command( return ( "Unknown /memory-export command. " - "Use: /memory-export history|ltm|both" + "Use: /memory-export history|ltm|skills|all" ) diff --git a/src/utils.metta b/src/utils.metta index 3aa2e0f6..714b3e58 100644 --- a/src/utils.metta +++ b/src/utils.metta @@ -8,6 +8,7 @@ !(import_prolog_function sread) !(import_prolog_function string_concat) !(import_prolog_function make_directory) +!(import_prolog_function make_directory_path) (= (sleep $t) (progn (translatePredicate (sleep $t)) @@ -29,6 +30,10 @@ (let $rc (catch (((translatePredicate (make_directory $path))))) (return-on-error $rc True))) +(= (make-directory-path $path) + (let $rc (catch (((translatePredicate (make_directory_path $path))))) + (return-on-error $rc True))) + (= (string-replace $str $a $b) (atomic_list_concat (split_string $str $a "") $b)) diff --git a/tests/test_memory_export.py b/tests/test_memory_export.py index d80bde44..7ec75693 100644 --- a/tests/test_memory_export.py +++ b/tests/test_memory_export.py @@ -40,15 +40,15 @@ def test_export_command_requires_policy(handler): ) assert "Memory export complete" in handler.handle_export_command( - "/memory-export both", "authenticated-user" + "/memory-export all", "authenticated-user" ) - assert exported == ["both"] + assert exported == ["all"] handler.is_export_enabled = lambda: False assert handler.handle_export_command( - "/memory-export both", "authenticated-user" + "/memory-export all", "authenticated-user" ) is None - assert exported == ["both"] + assert exported == ["all"] def test_export_requires_authenticated_user(handler): @@ -56,13 +56,13 @@ def test_export_requires_authenticated_user(handler): "an unauthenticated command must not start an export" ) - assert handler.handle_export_command("/memory-export both") == ( + assert handler.handle_export_command("/memory-export all") == ( "Memory export denied: an authenticated user is required." ) def test_module_import_does_not_require_memory_portability(handler): assert "memory_portability" not in sys.modules - assert handler.is_export_command("/memory-export both") + assert handler.is_export_command("/memory-export all") def test_entrypoint_configures_memory_transfer_directory(): @@ -70,7 +70,7 @@ def test_entrypoint_configures_memory_transfer_directory(): assert 'transfer_dir="/memory-transfer"' in entrypoint -@pytest.mark.parametrize("component", ["history", "ltm", "both"]) +@pytest.mark.parametrize("component", ["history", "ltm", "skills", "all"]) def test_export_runs_immediately(handler, component): exported = [] handler._get_transfer = lambda: types.SimpleNamespace( @@ -100,7 +100,7 @@ def test_confirmation_command_is_no_longer_supported(handler): assert reply == ( "Unknown /memory-export command. " - "Use: /memory-export history|ltm|both" + "Use: /memory-export history|ltm|skills|all" ) def test_transfer_uses_effective_runtime_embedding_provider(handler, monkeypatch): @@ -183,12 +183,17 @@ def __init__(self, **kwargs): memory_dir = tmp_path / "custom-memory" chroma_path = tmp_path / "custom-chroma" + settings = { + "embeddingprovider": "Local", + "pluginWorkflowCustomInstructionsDir": str(tmp_path / "custom-skills"), + "pluginWorkflowMemoryDir": str(tmp_path / "custom-space"), + } monkeypatch.setattr(handler, "_resolve_memory_dir", lambda: memory_dir) monkeypatch.setattr(handler, "_resolve_chroma_path", lambda: chroma_path) monkeypatch.setattr( handler, "config_get_by_key", - lambda key, default=None: "Local" if key == "embeddingprovider" else default, + lambda key, default=None: settings.get(key, default), ) store = handler.create_memory_store() @@ -199,6 +204,8 @@ def __init__(self, **kwargs): "memory_dir": memory_dir, "chroma_path": chroma_path, "collection_name": "memories", + "skills_dir": tmp_path / "custom-skills", + "space_dir": tmp_path / "custom-space", } ] @@ -239,7 +246,12 @@ def configure(monkeypatch, handler, tmp_path, provider, config_model="", env_provider="Local", env_model="env-only-model"): monkeypatch.setenv("EMBEDDING_PROVIDER", env_provider) monkeypatch.setenv("EMBEDDING_MODEL", env_model) - settings = {"embeddingprovider": provider, "embeddingModel": config_model} + settings = { + "embeddingprovider": provider, + "embeddingModel": config_model, + "pluginWorkflowCustomInstructionsDir": str(tmp_path / "skills"), + "pluginWorkflowMemoryDir": str(tmp_path / "space"), + } monkeypatch.setattr( handler, "config_get_by_key", @@ -331,6 +343,8 @@ def test_memory_store_ignores_embedding_environment_variables( "memory_dir": tmp_path / "memory", "chroma_path": tmp_path / "chroma", "collection_name": "memories", + "skills_dir": tmp_path / "skills", + "space_dir": tmp_path / "space", } ] @@ -340,6 +354,9 @@ def test_entrypoint_passes_container_arguments_to_memory_portability(): assert "init_config(sys.argv[1:])" in entrypoint assert "init_config([])" not in entrypoint + assert ( + 'include_skills=os.environ.get("MEMORY_IMPORT_NO_SKILLS") != "1"' in entrypoint + ) assert entrypoint.count( '''"${agent_su[@]}" -s /bin/sh -c 'exec python3 -c "$MEMORY_PORTABILITY_PYTHON" "$@"' sh "$@"''' ) == 2 @@ -388,6 +405,8 @@ def test_storage_paths_are_resolved_from_omega_config( configured = { "memoryDirectory": str(tmp_path / "configured-memory"), "chromaDbPath": str(tmp_path / "configured-chroma"), + "pluginWorkflowCustomInstructionsDir": str(tmp_path / "configured-skills"), + "pluginWorkflowMemoryDir": str(tmp_path / "configured-space"), } monkeypatch.delenv("CHROMA_DB_PATH", raising=False) monkeypatch.setattr( @@ -398,6 +417,12 @@ def test_storage_paths_are_resolved_from_omega_config( assert handler._resolve_memory_dir() == tmp_path / "configured-memory" assert handler._resolve_chroma_path() == tmp_path / "configured-chroma" + assert handler._resolve_config_path( + "pluginWorkflowCustomInstructionsDir" + ) == tmp_path / "configured-skills" + assert handler._resolve_config_path( + "pluginWorkflowMemoryDir" + ) == tmp_path / "configured-space" def test_websocket_defers_memory_export_to_core_dispatch(monkeypatch): @@ -423,9 +448,9 @@ def test_websocket_defers_memory_export_to_core_dispatch(monkeypatch): module.send_message = replies.append module._handle_frame(json.dumps({ - "type": "user_message", "seq": 1, "text": "/memory-export both" + "type": "user_message", "seq": 1, "text": "/memory-export all" })) - assert received == [(1, "/memory-export both")] + assert received == [(1, "/memory-export all")] assert replies == [] @@ -440,7 +465,7 @@ def test_commchannel_receive_dispatches_control_commands(monkeypatch): principals: list[str] = [] control = types.ModuleType("src.memory_export") - control.is_export_command = lambda text: text == "/memory-export both" + control.is_export_command = lambda text: text == "/memory-export all" control.handle_export_command = lambda text, principal: ( principals.append(principal) or "Memory export complete" ) @@ -451,7 +476,7 @@ def test_commchannel_receive_dispatches_control_commands(monkeypatch): replies: list[str] = [] channels._commchannel = types.SimpleNamespace( - receive=lambda: "alice: /memory-export both | alice: hello", + receive=lambda: "alice: /memory-export all | alice: hello", send=replies.append, ) channels._commchannel_id = "telegram" @@ -470,7 +495,7 @@ def test_commchannel_receive_denies_export_without_authenticated_user(monkeypatc monkeypatch.setitem(sys.modules, "auth", auth) control = types.ModuleType("src.memory_export") - control.is_export_command = lambda text: text == "/memory-export both" + control.is_export_command = lambda text: text == "/memory-export all" control.handle_export_command = lambda text, principal: ( "Memory export denied: an authenticated user is required." if principal is None @@ -483,7 +508,7 @@ def test_commchannel_receive_denies_export_without_authenticated_user(monkeypatc replies: list[str] = [] channels._commchannel = types.SimpleNamespace( - receive=lambda: "alice: /memory-export both", + receive=lambda: "alice: /memory-export all", send=replies.append, ) channels._commchannel_id = "telegram" @@ -503,7 +528,7 @@ def test_commchannel_receive_dispatches_websocket_export(monkeypatch): commands: list[str] = [] principals: list[str] = [] control = types.ModuleType("src.memory_export") - control.is_export_command = lambda text: text == "/memory-export both" + control.is_export_command = lambda text: text == "/memory-export all" control.handle_export_command = lambda text, principal: ( commands.append(text) or principals.append(principal) @@ -516,13 +541,13 @@ def test_commchannel_receive_dispatches_websocket_export(monkeypatch): replies: list[str] = [] channels._commchannel = types.SimpleNamespace( - receive=lambda: "/memory-export both", + receive=lambda: "/memory-export all", send=replies.append, ) channels._commchannel_id = "websocket" assert channels.commChannelReceive() == "" - assert commands == ["/memory-export both"] + assert commands == ["/memory-export all"] assert principals == [ f"websocket:{hashlib.sha256(websocket_token.encode('utf-8')).hexdigest()}" ] @@ -544,7 +569,7 @@ def test_websocket_export_requires_bearer_token(monkeypatch): def test_commchannel_receive_does_not_consume_command_mentions(monkeypatch): control = types.ModuleType("src.memory_export") - control.is_export_command = lambda text: text == "/memory-export both" + control.is_export_command = lambda text: text == "/memory-export all" control.handle_export_command = lambda *_: pytest.fail( "a command mentioned in normal text must not execute" ) @@ -553,7 +578,7 @@ def test_commchannel_receive_does_not_consume_command_mentions(monkeypatch): monkeypatch.delitem(sys.modules, "channels", raising=False) channels = importlib.import_module("channels") - message = "alice: please use /memory-export both" + message = "alice: please use /memory-export all" channels._commchannel = types.SimpleNamespace( receive=lambda: message, send=lambda *_: pytest.fail("normal messages must not generate replies"), diff --git a/tests/test_omegaclaw_launcher.py b/tests/test_omegaclaw_launcher.py index a5aab9d1..98953903 100644 --- a/tests/test_omegaclaw_launcher.py +++ b/tests/test_omegaclaw_launcher.py @@ -453,8 +453,21 @@ def run(command, *arguments): @pytest.mark.parametrize( ("option", "included_environment", "excluded_environment"), [ - ("--only-history", "MEMORY_IMPORT_NO_VECTOR=1", "MEMORY_IMPORT_NO_HISTORY=1"), - ("--only-vector", "MEMORY_IMPORT_NO_HISTORY=1", "MEMORY_IMPORT_NO_VECTOR=1"), + ( + "--only-history", + ("MEMORY_IMPORT_NO_VECTOR=1", "MEMORY_IMPORT_NO_SKILLS=1"), + "MEMORY_IMPORT_NO_HISTORY=1", + ), + ( + "--only-vector", + ("MEMORY_IMPORT_NO_HISTORY=1", "MEMORY_IMPORT_NO_SKILLS=1"), + "MEMORY_IMPORT_NO_VECTOR=1", + ), + ( + "--only-skills", + ("MEMORY_IMPORT_NO_HISTORY=1", "MEMORY_IMPORT_NO_VECTOR=1"), + "MEMORY_IMPORT_NO_SKILLS=1", + ), ], ) def test_only_component_options_select_one_import_component( @@ -466,15 +479,27 @@ def test_only_component_options_select_one_import_component( result = _run_launcher(tmp_path, option) assert result.returncode == 0, result.stderr - assert included_environment in result.stdout + for environment in included_environment: + assert environment in result.stdout assert excluded_environment not in result.stdout -def test_only_component_options_are_mutually_exclusive(tmp_path): - result = _run_launcher(tmp_path, "--only-history", "--only-vector") +@pytest.mark.parametrize( + "options", + [ + ("--only-history", "--only-vector"), + ("--only-history", "--only-skills"), + ("--only-vector", "--only-skills"), + ], +) +def test_only_component_options_are_mutually_exclusive(tmp_path, options): + result = _run_launcher(tmp_path, *options) assert result.returncode != 0 - assert "--only-history and --only-vector cannot be combined" in result.stderr + assert ( + "--only-history, --only-vector and --only-skills cannot be combined" + in result.stderr + ) @pytest.mark.parametrize("command", ["start", "stop", "clean", "memory-download", "memory-delete"])