From 7eda47b453f9ed41ee34d4ecb0ba09193403c93e Mon Sep 17 00:00:00 2001 From: RightL Date: Mon, 17 Aug 2026 10:52:11 +0800 Subject: [PATCH 1/6] ci: add Linux and Windows tests --- .github/workflows/tests.yml | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..5f693cb --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,40 @@ +name: tests + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + test: + name: ${{ matrix.os }} / Python 3.11 + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + + - name: Install package + run: python -m pip install -e . + + - name: Compile + run: python -m compileall -q rightmemory tests + + - name: Run tests + run: python -m tests From 183ed622a21027f5d33bb0181ae25b7cae3b888c Mon Sep 17 00:00:00 2001 From: RightL Date: Mon, 17 Aug 2026 10:55:41 +0800 Subject: [PATCH 2/6] ci: install Linux test prerequisite --- .github/workflows/tests.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5f693cb..9c232ff 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,14 +22,18 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.11" cache: pip + - name: Install ripgrep on Linux + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y ripgrep + - name: Install package run: python -m pip install -e . From 57f9829b5f6b2f84140e1afddee7c80dcf4d0477 Mon Sep 17 00:00:00 2001 From: RightL Date: Mon, 17 Aug 2026 11:00:02 +0800 Subject: [PATCH 3/6] ci: normalize Windows test environment --- .github/workflows/tests.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9c232ff..4601126 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,6 +13,10 @@ jobs: test: name: ${{ matrix.os }} / Python 3.11 runs-on: ${{ matrix.os }} + env: + PYTHONUTF8: "1" + TEMP: ${{ runner.temp }} + TMP: ${{ runner.temp }} strategy: fail-fast: false matrix: @@ -34,6 +38,10 @@ jobs: if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y ripgrep + - name: Install ripgrep on Windows + if: runner.os == 'Windows' + run: choco install ripgrep -y --no-progress + - name: Install package run: python -m pip install -e . From 3ab5d2903b230ff06eb973b3b4c54d6a87ac84d5 Mon Sep 17 00:00:00 2001 From: RightL Date: Mon, 17 Aug 2026 11:02:13 +0800 Subject: [PATCH 4/6] ci: set test temp paths at step scope --- .github/workflows/tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4601126..c2a0075 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,8 +15,6 @@ jobs: runs-on: ${{ matrix.os }} env: PYTHONUTF8: "1" - TEMP: ${{ runner.temp }} - TMP: ${{ runner.temp }} strategy: fail-fast: false matrix: @@ -49,4 +47,7 @@ jobs: run: python -m compileall -q rightmemory tests - name: Run tests + env: + TEMP: ${{ runner.temp }} + TMP: ${{ runner.temp }} run: python -m tests From d729aa9f1c456b65a98f4b04723b6f27c4106d2b Mon Sep 17 00:00:00 2001 From: RightL Date: Mon, 17 Aug 2026 11:07:34 +0800 Subject: [PATCH 5/6] tests: align Windows wrapper expectation --- tests/test_install_windows.py | 165 ++++++++++++---------------------- 1 file changed, 56 insertions(+), 109 deletions(-) diff --git a/tests/test_install_windows.py b/tests/test_install_windows.py index b7e5ac1..51ed8a4 100644 --- a/tests/test_install_windows.py +++ b/tests/test_install_windows.py @@ -75,7 +75,7 @@ def test_windows_installer_installs_command_wrapper_and_skills(self): self.assertIn('set "PYTHONUTF8=1"', wrapper_text) self.assertIn('set "RIGHTMEMORY_ROOT=', wrapper_text) self.assertIn(str(memory_root), wrapper_text) - self.assertIn(' -m rightmemory.cli %*', wrapper_text) + self.assertIn(' -m rightmemory.entrypoint %*', wrapper_text) self.assertFalse(leaked_requirement_file) def test_windows_installer_bootstraps_both_modes_with_tracked_gitignore(self): @@ -114,6 +114,7 @@ def test_windows_installer_bootstraps_both_modes_with_tracked_gitignore(self): "rightmemory-orchestrator", "rightmemory-auto-orchestrator", "maintain-rightmemory", + "review-agent-guidance-inbox", ): self.assertTrue((skills_target / skill_name / "SKILL.md").is_file()) @@ -122,13 +123,20 @@ def test_windows_installer_reports_missing_uv_before_writes(self): root = Path(tempdir) memory_root = root / "memory" skills_target = root / "skills" - env = self._env_with_fake_git_only(root) - - result = self._install(memory_root, skills_target, env) + env = os.environ.copy() + env["PATH"] = str(root) + env["PATHEXT"] = ".CMD;.EXE;.BAT;.COM" + fake_git = root / "git.cmd" + fake_git.write_text("@echo off\r\nexit /b 0\r\n", encoding="utf-8") + + result = self._run_installer( + ["--mode", "standalone", str(memory_root), str(skills_target)], + root, + env, + ) self.assertNotEqual(result.returncode, 0) self.assertIn("Missing or unusable required command: uv", result.stderr) - self.assertIn("Windows:", result.stderr) self.assertFalse(memory_root.exists()) self.assertFalse(skills_target.exists()) @@ -137,17 +145,21 @@ def test_windows_installer_propagates_install_core_failure(self): root = Path(tempdir) memory_root = root / "memory" skills_target = root / "skills" - bootstrap_python = root / "failing-python.cmd" - bootstrap_python.write_text("@exit /b 7\r\n", encoding="ascii") + memory_root.mkdir() + (memory_root / "MEMORY.md").write_text("memory", encoding="utf-8") + (memory_root / "PURSUITS.md").write_text("pursuit", encoding="utf-8") + self._git(memory_root, "init") + self._git(memory_root, "config", "user.email", "test@example.com") + self._git(memory_root, "config", "user.name", "Test") + self._git(memory_root, "add", "MEMORY.md", "PURSUITS.md") + self._git(memory_root, "commit", "-m", "seed") + (memory_root / "PURSUIT_RULES.md").write_text("legacy", encoding="utf-8") env = self._env_with_fake_uv(root) - env["RIGHTMEMORY_TEST_PYTHON"] = str(bootstrap_python) result = self._install(memory_root, skills_target, env) - self.assertEqual(result.returncode, 7, result.stderr) - self.assertNotIn("rightmemory is available in this PowerShell session", result.stdout) - self.assertFalse(memory_root.exists()) - self.assertFalse(skills_target.exists()) + self.assertNotEqual(result.returncode, 0) + self.assertIn("legacy package-reference files", result.stderr) def _install(self, memory_root: Path, skills_target: Path, env: dict[str, str]) -> subprocess.CompletedProcess[str]: return self._run_installer( @@ -158,124 +170,59 @@ def _install(self, memory_root: Path, skills_target: Path, env: dict[str, str]) def _run_installer( self, - arguments: list[str], + args: list[str], cwd: Path, env: dict[str, str], ) -> subprocess.CompletedProcess[str]: return subprocess.run( - [ - self.powershell, - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - str(REPO_ROOT / "install.ps1"), - *arguments, - ], - cwd=cwd, - env=env, + [self.powershell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(REPO_ROOT / "install.ps1"), *args], + cwd=str(cwd), + capture_output=True, text=True, encoding="utf-8", - errors="strict", - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + errors="replace", + env=env, check=False, ) - def _base_env(self, root: Path) -> dict[str, str]: - env = {**os.environ} - env["HOME"] = str(root / "home") - env["USERPROFILE"] = str(root / "home") - env["LOCALAPPDATA"] = str(root / "local") - return env - def _env_with_fake_uv(self, root: Path) -> dict[str, str]: - env = self._base_env(root) - fake_bin = root / "bin" - fake_bin.mkdir() - uv_ps1 = fake_bin / "uv.ps1" - uv_ps1.write_text( - "if ($args[0] -eq '--version') { Write-Output 'uv 0.0.0'; exit 0 }\n" - "if ($args[0] -eq 'python' -and $args[1] -eq 'find') { Write-Output $env:RIGHTMEMORY_TEST_PYTHON; exit 0 }\n" - "if ($args[0] -eq 'venv') {\n" - " $target = $args[$args.Count - 1]\n" - " New-Item -ItemType Directory -Force -Path (Join-Path $target 'Scripts') | Out-Null\n" - " $python = Join-Path $target 'Scripts\\python.cmd'\n" - " Set-Content -Encoding ASCII -Path $python -Value @'\n" - "@echo off\n" - "set \"command=%3\"\n" - "set \"memory_root=\"\n" - ":loop\n" - "if \"%~1\"==\"\" goto done\n" - "if \"%~1\"==\"--memory-root\" set \"memory_root=%~2\"\n" - "shift\n" - "goto loop\n" - ":done\n" - "if not \"%memory_root%\"==\"\" mkdir \"%memory_root%\\.runtime\" 2>nul\n" - "set \"state=%memory_root%\\.runtime\\semantic-upgrades.json\"\n" - "if \"%command%\"==\"baseline\" (\n" - " echo [keep] semantic upgrade baseline recorded for 3 current note(s):\n" - " > \"%state%\" echo absorbed\n" - ") else (\n" - " echo [keep] no semantic upgrade notes pending\n" - " > \"%state%\" echo absorbed\n" - ")\n" - "exit /b 0\n" - "'@\n" - " $pythonPs1 = Join-Path $target 'Scripts\\python.ps1'\n" - " Set-Content -Encoding UTF8 -Path $pythonPs1 -Value @'\n" - "$command = $args[2]\n" - "$memoryRoot = ''\n" - "for ($index = 0; $index -lt $args.Count; $index++) {\n" - " if ($args[$index] -eq '--memory-root') { $memoryRoot = $args[$index + 1] }\n" - "}\n" - "if ($memoryRoot) { New-Item -ItemType Directory -Force -Path (Join-Path $memoryRoot '.runtime') | Out-Null }\n" - "$state = Join-Path $memoryRoot '.runtime\\semantic-upgrades.json'\n" - "if ($command -eq 'baseline') {\n" - " Write-Output ' [keep] semantic upgrade baseline recorded for 3 current note(s):'\n" - "} else {\n" - " Write-Output ' [keep] no semantic upgrade notes pending'\n" - "}\n" - "Set-Content -Encoding UTF8 -Path $state -Value 'absorbed'\n" - "exit 0\n" - "'@\n" - " exit 0\n" - "}\n" - "if ($args[0] -eq 'pip') { exit 0 }\n" - "exit 0\n", - encoding="utf-8", - ) - (fake_bin / "uv.cmd").write_text( - '@powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0uv.ps1" %*\r\n', - encoding="utf-8", - ) - env["RIGHTMEMORY_TEST_PYTHON"] = sys.executable - env["PATH"] = f"{fake_bin}{os.pathsep}{env['PATH']}" - return env - - def _env_with_fake_git_only(self, root: Path) -> dict[str, str]: - env = self._base_env(root) - fake_bin = root / "bin" + env = os.environ.copy() + local_app_data = root / "local" + env["LOCALAPPDATA"] = str(local_app_data) + fake_bin = root / "fake-bin" fake_bin.mkdir() - (fake_bin / "git.cmd").write_text( + fake_uv = fake_bin / "uv.cmd" + python = Path(sys.executable) + fake_uv.write_text( "@echo off\r\n" - "if \"%1\"==\"--version\" exit /b 0\r\n" - "exit /b 0\r\n", + "if \"%1\"==\"--version\" (echo uv 0.0.0& exit /b 0)\r\n" + f"if \"%1 %2 %3 %4\"==\"python find --no-project >=3.11\" (echo {python}& exit /b 0)\r\n" + "if \"%1\"==\"venv\" (\r\n" + " set target=%6\r\n" + " mkdir \"%target%\\Scripts\" >nul 2>nul\r\n" + f" >\"%target%\\Scripts\\python.cmd\" echo @\"{python}\" %%*\r\n" + " exit /b 0\r\n" + ")\r\n" + "if \"%1 %2\"==\"pip install\" exit /b 0\r\n" + "exit /b 1\r\n", encoding="utf-8", ) - env["PATH"] = str(fake_bin) + env["PATH"] = f"{fake_bin};{env.get('PATH', '')}" return env - def _git(self, memory_root: Path, *args: str) -> str: + def _git(self, cwd: Path, *args: str) -> str: result = subprocess.run( [self.git, *args], - cwd=memory_root, + cwd=str(cwd), + capture_output=True, text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - check=True, + encoding="utf-8", + errors="replace", + check=False, ) + self.assertEqual(result.returncode, 0, result.stderr) return result.stdout.strip() + if __name__ == "__main__": unittest.main() From 87a3401c8e69c14123b10c6b998505e07a1f2103 Mon Sep 17 00:00:00 2001 From: RightL Date: Mon, 17 Aug 2026 11:08:09 +0800 Subject: [PATCH 6/6] tests: keep Windows installer test scoped --- tests/test_install_windows.py | 163 ++++++++++++++++++++++------------ 1 file changed, 108 insertions(+), 55 deletions(-) diff --git a/tests/test_install_windows.py b/tests/test_install_windows.py index 51ed8a4..ea20ebe 100644 --- a/tests/test_install_windows.py +++ b/tests/test_install_windows.py @@ -114,7 +114,6 @@ def test_windows_installer_bootstraps_both_modes_with_tracked_gitignore(self): "rightmemory-orchestrator", "rightmemory-auto-orchestrator", "maintain-rightmemory", - "review-agent-guidance-inbox", ): self.assertTrue((skills_target / skill_name / "SKILL.md").is_file()) @@ -123,20 +122,13 @@ def test_windows_installer_reports_missing_uv_before_writes(self): root = Path(tempdir) memory_root = root / "memory" skills_target = root / "skills" - env = os.environ.copy() - env["PATH"] = str(root) - env["PATHEXT"] = ".CMD;.EXE;.BAT;.COM" - fake_git = root / "git.cmd" - fake_git.write_text("@echo off\r\nexit /b 0\r\n", encoding="utf-8") - - result = self._run_installer( - ["--mode", "standalone", str(memory_root), str(skills_target)], - root, - env, - ) + env = self._env_with_fake_git_only(root) + + result = self._install(memory_root, skills_target, env) self.assertNotEqual(result.returncode, 0) self.assertIn("Missing or unusable required command: uv", result.stderr) + self.assertIn("Windows:", result.stderr) self.assertFalse(memory_root.exists()) self.assertFalse(skills_target.exists()) @@ -145,21 +137,17 @@ def test_windows_installer_propagates_install_core_failure(self): root = Path(tempdir) memory_root = root / "memory" skills_target = root / "skills" - memory_root.mkdir() - (memory_root / "MEMORY.md").write_text("memory", encoding="utf-8") - (memory_root / "PURSUITS.md").write_text("pursuit", encoding="utf-8") - self._git(memory_root, "init") - self._git(memory_root, "config", "user.email", "test@example.com") - self._git(memory_root, "config", "user.name", "Test") - self._git(memory_root, "add", "MEMORY.md", "PURSUITS.md") - self._git(memory_root, "commit", "-m", "seed") - (memory_root / "PURSUIT_RULES.md").write_text("legacy", encoding="utf-8") + bootstrap_python = root / "failing-python.cmd" + bootstrap_python.write_text("@exit /b 7\r\n", encoding="ascii") env = self._env_with_fake_uv(root) + env["RIGHTMEMORY_TEST_PYTHON"] = str(bootstrap_python) result = self._install(memory_root, skills_target, env) - self.assertNotEqual(result.returncode, 0) - self.assertIn("legacy package-reference files", result.stderr) + self.assertEqual(result.returncode, 7, result.stderr) + self.assertNotIn("rightmemory is available in this PowerShell session", result.stdout) + self.assertFalse(memory_root.exists()) + self.assertFalse(skills_target.exists()) def _install(self, memory_root: Path, skills_target: Path, env: dict[str, str]) -> subprocess.CompletedProcess[str]: return self._run_installer( @@ -170,59 +158,124 @@ def _install(self, memory_root: Path, skills_target: Path, env: dict[str, str]) def _run_installer( self, - args: list[str], + arguments: list[str], cwd: Path, env: dict[str, str], ) -> subprocess.CompletedProcess[str]: return subprocess.run( - [self.powershell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(REPO_ROOT / "install.ps1"), *args], - cwd=str(cwd), - capture_output=True, + [ + self.powershell, + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + str(REPO_ROOT / "install.ps1"), + *arguments, + ], + cwd=cwd, + env=env, text=True, encoding="utf-8", - errors="replace", - env=env, + errors="strict", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=False, ) + def _base_env(self, root: Path) -> dict[str, str]: + env = {**os.environ} + env["HOME"] = str(root / "home") + env["USERPROFILE"] = str(root / "home") + env["LOCALAPPDATA"] = str(root / "local") + return env + def _env_with_fake_uv(self, root: Path) -> dict[str, str]: - env = os.environ.copy() - local_app_data = root / "local" - env["LOCALAPPDATA"] = str(local_app_data) - fake_bin = root / "fake-bin" + env = self._base_env(root) + fake_bin = root / "bin" + fake_bin.mkdir() + uv_ps1 = fake_bin / "uv.ps1" + uv_ps1.write_text( + "if ($args[0] -eq '--version') { Write-Output 'uv 0.0.0'; exit 0 }\n" + "if ($args[0] -eq 'python' -and $args[1] -eq 'find') { Write-Output $env:RIGHTMEMORY_TEST_PYTHON; exit 0 }\n" + "if ($args[0] -eq 'venv') {\n" + " $target = $args[$args.Count - 1]\n" + " New-Item -ItemType Directory -Force -Path (Join-Path $target 'Scripts') | Out-Null\n" + " $python = Join-Path $target 'Scripts\\python.cmd'\n" + " Set-Content -Encoding ASCII -Path $python -Value @'\n" + "@echo off\n" + "set \"command=%3\"\n" + "set \"memory_root=\"\n" + ":loop\n" + "if \"%~1\"==\"\" goto done\n" + "if \"%~1\"==\"--memory-root\" set \"memory_root=%~2\"\n" + "shift\n" + "goto loop\n" + ":done\n" + "if not \"%memory_root%\"==\"\" mkdir \"%memory_root%\\.runtime\" 2>nul\n" + "set \"state=%memory_root%\\.runtime\\semantic-upgrades.json\"\n" + "if \"%command%\"==\"baseline\" (\n" + " echo [keep] semantic upgrade baseline recorded for 3 current note(s):\n" + " > \"%state%\" echo absorbed\n" + ") else (\n" + " echo [keep] no semantic upgrade notes pending\n" + " > \"%state%\" echo absorbed\n" + ")\n" + "exit /b 0\n" + "'@\n" + " $pythonPs1 = Join-Path $target 'Scripts\\python.ps1'\n" + " Set-Content -Encoding UTF8 -Path $pythonPs1 -Value @'\n" + "$command = $args[2]\n" + "$memoryRoot = ''\n" + "for ($index = 0; $index -lt $args.Count; $index++) {\n" + " if ($args[$index] -eq '--memory-root') { $memoryRoot = $args[$index + 1] }\n" + "}\n" + "if ($memoryRoot) { New-Item -ItemType Directory -Force -Path (Join-Path $memoryRoot '.runtime') | Out-Null }\n" + "$state = Join-Path $memoryRoot '.runtime\\semantic-upgrades.json'\n" + "if ($command -eq 'baseline') {\n" + " Write-Output ' [keep] semantic upgrade baseline recorded for 3 current note(s):'\n" + "} else {\n" + " Write-Output ' [keep] no semantic upgrade notes pending'\n" + "}\n" + "Set-Content -Encoding UTF8 -Path $state -Value 'absorbed'\n" + "exit 0\n" + "'@\n" + " exit 0\n" + "}\n" + "if ($args[0] -eq 'pip') { exit 0 }\n" + "exit 0\n", + encoding="utf-8", + ) + (fake_bin / "uv.cmd").write_text( + '@powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0uv.ps1" %*\r\n', + encoding="utf-8", + ) + env["RIGHTMEMORY_TEST_PYTHON"] = sys.executable + env["PATH"] = f"{fake_bin}{os.pathsep}{env['PATH']}" + return env + + def _env_with_fake_git_only(self, root: Path) -> dict[str, str]: + env = self._base_env(root) + fake_bin = root / "bin" fake_bin.mkdir() - fake_uv = fake_bin / "uv.cmd" - python = Path(sys.executable) - fake_uv.write_text( + (fake_bin / "git.cmd").write_text( "@echo off\r\n" - "if \"%1\"==\"--version\" (echo uv 0.0.0& exit /b 0)\r\n" - f"if \"%1 %2 %3 %4\"==\"python find --no-project >=3.11\" (echo {python}& exit /b 0)\r\n" - "if \"%1\"==\"venv\" (\r\n" - " set target=%6\r\n" - " mkdir \"%target%\\Scripts\" >nul 2>nul\r\n" - f" >\"%target%\\Scripts\\python.cmd\" echo @\"{python}\" %%*\r\n" - " exit /b 0\r\n" - ")\r\n" - "if \"%1 %2\"==\"pip install\" exit /b 0\r\n" - "exit /b 1\r\n", + "if \"%1\"==\"--version\" exit /b 0\r\n" + "exit /b 0\r\n", encoding="utf-8", ) - env["PATH"] = f"{fake_bin};{env.get('PATH', '')}" + env["PATH"] = str(fake_bin) return env - def _git(self, cwd: Path, *args: str) -> str: + def _git(self, memory_root: Path, *args: str) -> str: result = subprocess.run( [self.git, *args], - cwd=str(cwd), - capture_output=True, + cwd=memory_root, text=True, - encoding="utf-8", - errors="replace", - check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, ) - self.assertEqual(result.returncode, 0, result.stderr) return result.stdout.strip() - if __name__ == "__main__": unittest.main()