diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7935cf9..961eed2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,8 +37,13 @@ jobs: - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: "10.0.x" - - run: dotnet test desktop/CodexProviderSync.Core.Tests/CodexProviderSync.Core.Tests.csproj - - run: dotnet test desktop/CodexProviderSync.App.Tests/CodexProviderSync.App.Tests.csproj + - run: dotnet build CodexProviderSync.sln --configuration Release + - run: dotnet test desktop/CodexProviderSync.Core.Tests/CodexProviderSync.Core.Tests.csproj --configuration Release --no-build + - run: dotnet test desktop/CodexProviderSync.Application.Tests/CodexProviderSync.Application.Tests.csproj --configuration Release --no-build + - run: dotnet test desktop/CodexProviderSync.Automation.Tests/CodexProviderSync.Automation.Tests.csproj --configuration Release --no-build + - run: dotnet test desktop/CodexProviderSync.App.Tests/CodexProviderSync.App.Tests.csproj --configuration Release --no-build + - name: Run GUI E2E contract tests (not headful) + run: dotnet test desktop/CodexProviderSync.GuiE2E.Tests/CodexProviderSync.GuiE2E.Tests.csproj --configuration Release --no-build desktop-macos: runs-on: macos-latest @@ -48,8 +53,18 @@ jobs: with: dotnet-version: "10.0.x" - run: dotnet test desktop/CodexProviderSync.Core.Tests/CodexProviderSync.Core.Tests.csproj + - run: dotnet test desktop/CodexProviderSync.Application.Tests/CodexProviderSync.Application.Tests.csproj - run: dotnet build desktop/CodexProviderSync.Mac/CodexProviderSync.Mac.csproj --configuration Release + desktop-linux-lock: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: "10.0.x" + - run: dotnet test desktop/CodexProviderSync.Core.Tests/CodexProviderSync.Core.Tests.csproj --filter FullyQualifiedName~LockServiceTests + ci-gate: name: ci-gate if: ${{ always() }} @@ -57,6 +72,7 @@ jobs: - test - desktop-test - desktop-macos + - desktop-linux-lock runs-on: ubuntu-latest steps: - name: Verify required jobs @@ -64,10 +80,12 @@ jobs: NODE_TEST_RESULT: ${{ needs.test.result }} DESKTOP_TEST_RESULT: ${{ needs.desktop-test.result }} MACOS_TEST_RESULT: ${{ needs.desktop-macos.result }} + LINUX_LOCK_RESULT: ${{ needs.desktop-linux-lock.result }} run: | if [ "$NODE_TEST_RESULT" != "success" ] || [ "$DESKTOP_TEST_RESULT" != "success" ] || - [ "$MACOS_TEST_RESULT" != "success" ]; then + [ "$MACOS_TEST_RESULT" != "success" ] || + [ "$LINUX_LOCK_RESULT" != "success" ]; then echo "One or more required CI jobs did not succeed." exit 1 fi diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b22669d..5f65d2d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -44,10 +44,15 @@ jobs: - run: npm ci - run: npm test - - run: dotnet test desktop/CodexProviderSync.Core.Tests/CodexProviderSync.Core.Tests.csproj - - run: dotnet test desktop/CodexProviderSync.App.Tests/CodexProviderSync.App.Tests.csproj - - - name: Build Windows GUI + - run: dotnet build CodexProviderSync.sln --configuration Release + - run: dotnet test desktop/CodexProviderSync.Core.Tests/CodexProviderSync.Core.Tests.csproj --configuration Release --no-build + - run: dotnet test desktop/CodexProviderSync.Application.Tests/CodexProviderSync.Application.Tests.csproj --configuration Release --no-build + - run: dotnet test desktop/CodexProviderSync.Automation.Tests/CodexProviderSync.Automation.Tests.csproj --configuration Release --no-build + - run: dotnet test desktop/CodexProviderSync.App.Tests/CodexProviderSync.App.Tests.csproj --configuration Release --no-build + - name: Run GUI E2E contract tests (not headful) + run: dotnet test desktop/CodexProviderSync.GuiE2E.Tests/CodexProviderSync.GuiE2E.Tests.csproj --configuration Release --no-build + + - name: Build Windows GUI and Business Automation API shell: pwsh run: ./scripts/publish-gui.ps1 -Output artifacts/win-x64 diff --git a/AGENTS.md b/AGENTS.md index 566263f..870da3a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,22 @@ The tool works by updating both: - rollout metadata under `~/.codex/sessions` and `~/.codex/archived_sessions` - SQLite thread metadata in the resolved Codex state database +## Architecture Direction + +`docs/AUTOMATION_DESIGN_NOTES.md` records the experimental 0.x direction. +It is not a public compatibility contract. No Automation executable, stable +JSONL protocol, or UI probe is currently shipped. + +For Windows GUI work: + +- move UI-independent state, validation, and Core request construction into the + Application/controller layer incrementally +- keep Core authoritative for config, rollout, SQLite, backup, restore, and + storage-safety behavior +- keep WinForms handlers focused on presentation and platform interaction +- preserve observable behavior and add controller tests for each migrated slice +- prefer controller tests over adding new reflection-based MainForm business tests + Resolve SQLite Home in this order: explicit CLI/GUI override, root `sqlite_home` in `config.toml`, `CODEX_SQLITE_HOME`, then `/sqlite`. Only the default layout may fall back to `/state_5.sqlite`. Never fall back when an explicit/config/environment SQLite Home is missing. On Windows, `\\wsl.localhost\...` and `\\wsl$\...` SQLite Homes are diagnostic-only. SQLite operations for these paths run inside WSL and use the corresponding Linux path. diff --git a/CodexProviderSync.sln b/CodexProviderSync.sln index 800e03d..a06a944 100644 --- a/CodexProviderSync.sln +++ b/CodexProviderSync.sln @@ -11,8 +11,26 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodexProviderSync.Mac", "de EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodexProviderSync.Core", "desktop\CodexProviderSync.Core\CodexProviderSync.Core.csproj", "{0BAD4EAB-F18E-405C-AA1F-0D08B03B599B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodexProviderSync.Application", "desktop\CodexProviderSync.Application\CodexProviderSync.Application.csproj", "{A2A9E0B8-D2F3-4E21-AEF0-6DDE4688D80A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodexProviderSync.Automation", "desktop\CodexProviderSync.Automation\CodexProviderSync.Automation.csproj", "{D4E17AB1-7F5C-48A8-A47E-8BE5F37BD761}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodexProviderSync.GuiE2E", "desktop\CodexProviderSync.GuiE2E\CodexProviderSync.GuiE2E.csproj", "{B8C38C3E-5C80-4F69-B4AB-55772F61A138}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodexProviderSync.App.Tests", "desktop\CodexProviderSync.App.Tests\CodexProviderSync.App.Tests.csproj", "{C2B35A41-4A58-49EC-A8D1-65F8AA6D9914}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodexProviderSync.Application.Tests", "desktop\CodexProviderSync.Application.Tests\CodexProviderSync.Application.Tests.csproj", "{F4F86035-7CC3-4D8E-BA71-1C87E2A7E2E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodexProviderSync.Automation.Tests", "desktop\CodexProviderSync.Automation.Tests\CodexProviderSync.Automation.Tests.csproj", "{E650246E-C6E1-4E34-9CF7-9890CE46B65C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodexProviderSync.GuiE2E.Tests", "desktop\CodexProviderSync.GuiE2E.Tests\CodexProviderSync.GuiE2E.Tests.csproj", "{9E3EA4B7-BD0E-48CD-B74A-C4E4F1F44589}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodexProviderSync.PlanClaimHost", "desktop\CodexProviderSync.Application.Tests\ClaimHost\CodexProviderSync.PlanClaimHost.csproj", "{577DF582-39B9-4306-8F51-DB65DFD375E0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodexProviderSync.CrashHost", "desktop\CodexProviderSync.Core.Tests\CrashHost\CodexProviderSync.CrashHost.csproj", "{1DD4AD0F-F74F-452B-8633-96326FE0769E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodexProviderSync.Core.Tests", "desktop\CodexProviderSync.Core.Tests\CodexProviderSync.Core.Tests.csproj", "{3D212656-31F9-4591-A19C-B7F77EE09AA3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,6 +77,42 @@ Global {0BAD4EAB-F18E-405C-AA1F-0D08B03B599B}.Release|x64.Build.0 = Release|Any CPU {0BAD4EAB-F18E-405C-AA1F-0D08B03B599B}.Release|x86.ActiveCfg = Release|Any CPU {0BAD4EAB-F18E-405C-AA1F-0D08B03B599B}.Release|x86.Build.0 = Release|Any CPU + {A2A9E0B8-D2F3-4E21-AEF0-6DDE4688D80A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2A9E0B8-D2F3-4E21-AEF0-6DDE4688D80A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2A9E0B8-D2F3-4E21-AEF0-6DDE4688D80A}.Debug|x64.ActiveCfg = Debug|Any CPU + {A2A9E0B8-D2F3-4E21-AEF0-6DDE4688D80A}.Debug|x64.Build.0 = Debug|Any CPU + {A2A9E0B8-D2F3-4E21-AEF0-6DDE4688D80A}.Debug|x86.ActiveCfg = Debug|Any CPU + {A2A9E0B8-D2F3-4E21-AEF0-6DDE4688D80A}.Debug|x86.Build.0 = Debug|Any CPU + {A2A9E0B8-D2F3-4E21-AEF0-6DDE4688D80A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2A9E0B8-D2F3-4E21-AEF0-6DDE4688D80A}.Release|Any CPU.Build.0 = Release|Any CPU + {A2A9E0B8-D2F3-4E21-AEF0-6DDE4688D80A}.Release|x64.ActiveCfg = Release|Any CPU + {A2A9E0B8-D2F3-4E21-AEF0-6DDE4688D80A}.Release|x64.Build.0 = Release|Any CPU + {A2A9E0B8-D2F3-4E21-AEF0-6DDE4688D80A}.Release|x86.ActiveCfg = Release|Any CPU + {A2A9E0B8-D2F3-4E21-AEF0-6DDE4688D80A}.Release|x86.Build.0 = Release|Any CPU + {D4E17AB1-7F5C-48A8-A47E-8BE5F37BD761}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4E17AB1-7F5C-48A8-A47E-8BE5F37BD761}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4E17AB1-7F5C-48A8-A47E-8BE5F37BD761}.Debug|x64.ActiveCfg = Debug|Any CPU + {D4E17AB1-7F5C-48A8-A47E-8BE5F37BD761}.Debug|x64.Build.0 = Debug|Any CPU + {D4E17AB1-7F5C-48A8-A47E-8BE5F37BD761}.Debug|x86.ActiveCfg = Debug|Any CPU + {D4E17AB1-7F5C-48A8-A47E-8BE5F37BD761}.Debug|x86.Build.0 = Debug|Any CPU + {D4E17AB1-7F5C-48A8-A47E-8BE5F37BD761}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4E17AB1-7F5C-48A8-A47E-8BE5F37BD761}.Release|Any CPU.Build.0 = Release|Any CPU + {D4E17AB1-7F5C-48A8-A47E-8BE5F37BD761}.Release|x64.ActiveCfg = Release|Any CPU + {D4E17AB1-7F5C-48A8-A47E-8BE5F37BD761}.Release|x64.Build.0 = Release|Any CPU + {D4E17AB1-7F5C-48A8-A47E-8BE5F37BD761}.Release|x86.ActiveCfg = Release|Any CPU + {D4E17AB1-7F5C-48A8-A47E-8BE5F37BD761}.Release|x86.Build.0 = Release|Any CPU + {B8C38C3E-5C80-4F69-B4AB-55772F61A138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8C38C3E-5C80-4F69-B4AB-55772F61A138}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8C38C3E-5C80-4F69-B4AB-55772F61A138}.Debug|x64.ActiveCfg = Debug|Any CPU + {B8C38C3E-5C80-4F69-B4AB-55772F61A138}.Debug|x64.Build.0 = Debug|Any CPU + {B8C38C3E-5C80-4F69-B4AB-55772F61A138}.Debug|x86.ActiveCfg = Debug|Any CPU + {B8C38C3E-5C80-4F69-B4AB-55772F61A138}.Debug|x86.Build.0 = Debug|Any CPU + {B8C38C3E-5C80-4F69-B4AB-55772F61A138}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8C38C3E-5C80-4F69-B4AB-55772F61A138}.Release|Any CPU.Build.0 = Release|Any CPU + {B8C38C3E-5C80-4F69-B4AB-55772F61A138}.Release|x64.ActiveCfg = Release|Any CPU + {B8C38C3E-5C80-4F69-B4AB-55772F61A138}.Release|x64.Build.0 = Release|Any CPU + {B8C38C3E-5C80-4F69-B4AB-55772F61A138}.Release|x86.ActiveCfg = Release|Any CPU + {B8C38C3E-5C80-4F69-B4AB-55772F61A138}.Release|x86.Build.0 = Release|Any CPU {C2B35A41-4A58-49EC-A8D1-65F8AA6D9914}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C2B35A41-4A58-49EC-A8D1-65F8AA6D9914}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2B35A41-4A58-49EC-A8D1-65F8AA6D9914}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -71,6 +125,78 @@ Global {C2B35A41-4A58-49EC-A8D1-65F8AA6D9914}.Release|x64.Build.0 = Release|Any CPU {C2B35A41-4A58-49EC-A8D1-65F8AA6D9914}.Release|x86.ActiveCfg = Release|Any CPU {C2B35A41-4A58-49EC-A8D1-65F8AA6D9914}.Release|x86.Build.0 = Release|Any CPU + {F4F86035-7CC3-4D8E-BA71-1C87E2A7E2E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4F86035-7CC3-4D8E-BA71-1C87E2A7E2E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4F86035-7CC3-4D8E-BA71-1C87E2A7E2E9}.Debug|x64.ActiveCfg = Debug|Any CPU + {F4F86035-7CC3-4D8E-BA71-1C87E2A7E2E9}.Debug|x64.Build.0 = Debug|Any CPU + {F4F86035-7CC3-4D8E-BA71-1C87E2A7E2E9}.Debug|x86.ActiveCfg = Debug|Any CPU + {F4F86035-7CC3-4D8E-BA71-1C87E2A7E2E9}.Debug|x86.Build.0 = Debug|Any CPU + {F4F86035-7CC3-4D8E-BA71-1C87E2A7E2E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F4F86035-7CC3-4D8E-BA71-1C87E2A7E2E9}.Release|Any CPU.Build.0 = Release|Any CPU + {F4F86035-7CC3-4D8E-BA71-1C87E2A7E2E9}.Release|x64.ActiveCfg = Release|Any CPU + {F4F86035-7CC3-4D8E-BA71-1C87E2A7E2E9}.Release|x64.Build.0 = Release|Any CPU + {F4F86035-7CC3-4D8E-BA71-1C87E2A7E2E9}.Release|x86.ActiveCfg = Release|Any CPU + {F4F86035-7CC3-4D8E-BA71-1C87E2A7E2E9}.Release|x86.Build.0 = Release|Any CPU + {E650246E-C6E1-4E34-9CF7-9890CE46B65C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E650246E-C6E1-4E34-9CF7-9890CE46B65C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E650246E-C6E1-4E34-9CF7-9890CE46B65C}.Debug|x64.ActiveCfg = Debug|Any CPU + {E650246E-C6E1-4E34-9CF7-9890CE46B65C}.Debug|x64.Build.0 = Debug|Any CPU + {E650246E-C6E1-4E34-9CF7-9890CE46B65C}.Debug|x86.ActiveCfg = Debug|Any CPU + {E650246E-C6E1-4E34-9CF7-9890CE46B65C}.Debug|x86.Build.0 = Debug|Any CPU + {E650246E-C6E1-4E34-9CF7-9890CE46B65C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E650246E-C6E1-4E34-9CF7-9890CE46B65C}.Release|Any CPU.Build.0 = Release|Any CPU + {E650246E-C6E1-4E34-9CF7-9890CE46B65C}.Release|x64.ActiveCfg = Release|Any CPU + {E650246E-C6E1-4E34-9CF7-9890CE46B65C}.Release|x64.Build.0 = Release|Any CPU + {E650246E-C6E1-4E34-9CF7-9890CE46B65C}.Release|x86.ActiveCfg = Release|Any CPU + {E650246E-C6E1-4E34-9CF7-9890CE46B65C}.Release|x86.Build.0 = Release|Any CPU + {9E3EA4B7-BD0E-48CD-B74A-C4E4F1F44589}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E3EA4B7-BD0E-48CD-B74A-C4E4F1F44589}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E3EA4B7-BD0E-48CD-B74A-C4E4F1F44589}.Debug|x64.ActiveCfg = Debug|Any CPU + {9E3EA4B7-BD0E-48CD-B74A-C4E4F1F44589}.Debug|x64.Build.0 = Debug|Any CPU + {9E3EA4B7-BD0E-48CD-B74A-C4E4F1F44589}.Debug|x86.ActiveCfg = Debug|Any CPU + {9E3EA4B7-BD0E-48CD-B74A-C4E4F1F44589}.Debug|x86.Build.0 = Debug|Any CPU + {9E3EA4B7-BD0E-48CD-B74A-C4E4F1F44589}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E3EA4B7-BD0E-48CD-B74A-C4E4F1F44589}.Release|Any CPU.Build.0 = Release|Any CPU + {9E3EA4B7-BD0E-48CD-B74A-C4E4F1F44589}.Release|x64.ActiveCfg = Release|Any CPU + {9E3EA4B7-BD0E-48CD-B74A-C4E4F1F44589}.Release|x64.Build.0 = Release|Any CPU + {9E3EA4B7-BD0E-48CD-B74A-C4E4F1F44589}.Release|x86.ActiveCfg = Release|Any CPU + {9E3EA4B7-BD0E-48CD-B74A-C4E4F1F44589}.Release|x86.Build.0 = Release|Any CPU + {577DF582-39B9-4306-8F51-DB65DFD375E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {577DF582-39B9-4306-8F51-DB65DFD375E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {577DF582-39B9-4306-8F51-DB65DFD375E0}.Debug|x64.ActiveCfg = Debug|Any CPU + {577DF582-39B9-4306-8F51-DB65DFD375E0}.Debug|x64.Build.0 = Debug|Any CPU + {577DF582-39B9-4306-8F51-DB65DFD375E0}.Debug|x86.ActiveCfg = Debug|Any CPU + {577DF582-39B9-4306-8F51-DB65DFD375E0}.Debug|x86.Build.0 = Debug|Any CPU + {577DF582-39B9-4306-8F51-DB65DFD375E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {577DF582-39B9-4306-8F51-DB65DFD375E0}.Release|Any CPU.Build.0 = Release|Any CPU + {577DF582-39B9-4306-8F51-DB65DFD375E0}.Release|x64.ActiveCfg = Release|Any CPU + {577DF582-39B9-4306-8F51-DB65DFD375E0}.Release|x64.Build.0 = Release|Any CPU + {577DF582-39B9-4306-8F51-DB65DFD375E0}.Release|x86.ActiveCfg = Release|Any CPU + {577DF582-39B9-4306-8F51-DB65DFD375E0}.Release|x86.Build.0 = Release|Any CPU + {1DD4AD0F-F74F-452B-8633-96326FE0769E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1DD4AD0F-F74F-452B-8633-96326FE0769E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1DD4AD0F-F74F-452B-8633-96326FE0769E}.Debug|x64.ActiveCfg = Debug|Any CPU + {1DD4AD0F-F74F-452B-8633-96326FE0769E}.Debug|x64.Build.0 = Debug|Any CPU + {1DD4AD0F-F74F-452B-8633-96326FE0769E}.Debug|x86.ActiveCfg = Debug|Any CPU + {1DD4AD0F-F74F-452B-8633-96326FE0769E}.Debug|x86.Build.0 = Debug|Any CPU + {1DD4AD0F-F74F-452B-8633-96326FE0769E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1DD4AD0F-F74F-452B-8633-96326FE0769E}.Release|Any CPU.Build.0 = Release|Any CPU + {1DD4AD0F-F74F-452B-8633-96326FE0769E}.Release|x64.ActiveCfg = Release|Any CPU + {1DD4AD0F-F74F-452B-8633-96326FE0769E}.Release|x64.Build.0 = Release|Any CPU + {1DD4AD0F-F74F-452B-8633-96326FE0769E}.Release|x86.ActiveCfg = Release|Any CPU + {1DD4AD0F-F74F-452B-8633-96326FE0769E}.Release|x86.Build.0 = Release|Any CPU + {3D212656-31F9-4591-A19C-B7F77EE09AA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D212656-31F9-4591-A19C-B7F77EE09AA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D212656-31F9-4591-A19C-B7F77EE09AA3}.Debug|x64.ActiveCfg = Debug|Any CPU + {3D212656-31F9-4591-A19C-B7F77EE09AA3}.Debug|x64.Build.0 = Debug|Any CPU + {3D212656-31F9-4591-A19C-B7F77EE09AA3}.Debug|x86.ActiveCfg = Debug|Any CPU + {3D212656-31F9-4591-A19C-B7F77EE09AA3}.Debug|x86.Build.0 = Debug|Any CPU + {3D212656-31F9-4591-A19C-B7F77EE09AA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D212656-31F9-4591-A19C-B7F77EE09AA3}.Release|Any CPU.Build.0 = Release|Any CPU + {3D212656-31F9-4591-A19C-B7F77EE09AA3}.Release|x64.ActiveCfg = Release|Any CPU + {3D212656-31F9-4591-A19C-B7F77EE09AA3}.Release|x64.Build.0 = Release|Any CPU + {3D212656-31F9-4591-A19C-B7F77EE09AA3}.Release|x86.ActiveCfg = Release|Any CPU + {3D212656-31F9-4591-A19C-B7F77EE09AA3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -79,6 +205,15 @@ Global {046C07E6-8117-4F19-B2CF-683D62D944C9} = {7A1224A0-F22D-34FF-DE0A-A058FAFFD90C} {2F00CBA5-C5E9-42BC-9F25-01641E0B8901} = {7A1224A0-F22D-34FF-DE0A-A058FAFFD90C} {0BAD4EAB-F18E-405C-AA1F-0D08B03B599B} = {7A1224A0-F22D-34FF-DE0A-A058FAFFD90C} + {A2A9E0B8-D2F3-4E21-AEF0-6DDE4688D80A} = {7A1224A0-F22D-34FF-DE0A-A058FAFFD90C} + {D4E17AB1-7F5C-48A8-A47E-8BE5F37BD761} = {7A1224A0-F22D-34FF-DE0A-A058FAFFD90C} + {B8C38C3E-5C80-4F69-B4AB-55772F61A138} = {7A1224A0-F22D-34FF-DE0A-A058FAFFD90C} {C2B35A41-4A58-49EC-A8D1-65F8AA6D9914} = {7A1224A0-F22D-34FF-DE0A-A058FAFFD90C} + {F4F86035-7CC3-4D8E-BA71-1C87E2A7E2E9} = {7A1224A0-F22D-34FF-DE0A-A058FAFFD90C} + {E650246E-C6E1-4E34-9CF7-9890CE46B65C} = {7A1224A0-F22D-34FF-DE0A-A058FAFFD90C} + {9E3EA4B7-BD0E-48CD-B74A-C4E4F1F44589} = {7A1224A0-F22D-34FF-DE0A-A058FAFFD90C} + {577DF582-39B9-4306-8F51-DB65DFD375E0} = {7A1224A0-F22D-34FF-DE0A-A058FAFFD90C} + {1DD4AD0F-F74F-452B-8633-96326FE0769E} = {7A1224A0-F22D-34FF-DE0A-A058FAFFD90C} + {3D212656-31F9-4591-A19C-B7F77EE09AA3} = {7A1224A0-F22D-34FF-DE0A-A058FAFFD90C} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 2e1cdce..17db0e5 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,34 @@ codex-provider sync --codex-home /mnt/c/Users/you/.codex --sqlite-home /home/you `status` 会显示 effective SQLite Home 和来源。显式路径缺少 `state_5.sqlite` 时,状态查询只报告诊断,`sync`、`switch` 和数据库恢复不会偷偷回退到其它位置。默认布局中的数据库被删除时,`restore` 可以根据备份 metadata 在原默认位置重建数据库。 +### Business Automation API(v0.4 实验性) + +v0.4 Windows Release 构建同时包含 `CodexProviderSync.Automation.exe` 和 `automation-protocol-v0.4.schema.json`。这个一次性进程接口与 Windows GUI 共用同一套 Application 用例;每次调用只在 stdout 输出一份协议 `0.4` JSON,诊断信息写入 stderr。 + +| 命令 | 用途 | +| --- | --- | +| `describe` | 描述协议能力和安全要求 | +| `status` | 读取状态和诊断 | +| `plan --operation sync\|switch\|restore\|prune` | 为指定写操作创建计划 | +| `sync` | 规划或显式执行同步 | +| `switch` | 规划或显式执行 Provider/model 切换与同步 | +| `restore` | 规划或显式执行备份恢复 | +| `prune` | 规划或显式清理托管备份 | + +所有写命令默认都是 dry-run,只返回计划,不会修改目标。实际写入必须同时提供 `--apply`、只包含 `plan` 响应中 `data` 对象的计划文件,以及该对象的精确小写 SHA-256 `digest`: + +```powershell +.\CodexProviderSync.Automation.exe describe +.\CodexProviderSync.Automation.exe status --codex-home C:\isolated\.codex +.\CodexProviderSync.Automation.exe sync --codex-home C:\isolated\.codex --provider openai +$planResponse = .\CodexProviderSync.Automation.exe plan --operation sync --codex-home C:\isolated\.codex --provider openai | ConvertFrom-Json +$planResponse.data | ConvertTo-Json -Depth 100 -Compress | Set-Content -LiteralPath C:\isolated\sync-plan.json -Encoding utf8NoBOM +$planDigest = $planResponse.data.digest +.\CodexProviderSync.Automation.exe sync --codex-home C:\isolated\.codex --provider openai --apply --plan C:\isolated\sync-plan.json --plan-digest $planDigest +``` + +计划有有效期、绑定规范化输入和目标状态,并由持久化 ledger 保证只能使用一次;默认 ledger 位于 `\tmp\provider-sync-automation-ledger`。所有路径参数必须是绝对路径,不能穿过符号链接或 reparse point,Automation 也拒绝直接指向或访问 `auth.json`。协议仍处于 pre-1.0 实验阶段,`0.4` 之外不承诺兼容。 + ## 安全与限制 每次 `sync` / `switch` 前都会备份到: @@ -111,6 +139,8 @@ codex-provider sync --codex-home /mnt/c/Users/you/.codex --sqlite-home /home/you - [Windows GUI 说明](docs/README_GUI_ZH.md) - macOS GUI 说明:[中文](docs/README_MAC_GUI_ZH.md) · [English](docs/README_MAC_GUI_EN.md) +- [v0.4.0 Release Notes(Draft)](docs/RELEASE_NOTES_V0.4.0.md) +- [v0.4 Automation 执行计划](docs/V0.4_AUTOMATION_PLAN.md) - [English documentation](docs/README_EN.md) - [AI / Agent 操作指南](AGENTS.md) - [贡献指南](CONTRIBUTING.md) @@ -124,10 +154,11 @@ npm test dotnet test desktop/CodexProviderSync.Core.Tests/CodexProviderSync.Core.Tests.csproj ./scripts/test-wsl-unc-safety.sh pwsh ./scripts/publish-gui.ps1 +pwsh ./scripts/run-windows-gui-e2e.ps1 ./scripts/publish-gui-macos.sh ``` -`test-wsl-unc-safety.sh` 需要从 WSL 运行,并调用 Windows `dotnet.exe` 验证真实 WSL ext4 SQLite 的安全阻断。 +`test-wsl-unc-safety.sh` 需要从 WSL 运行,并调用 Windows `dotnet.exe` 验证真实 WSL ext4 SQLite 的安全阻断。`run-windows-gui-e2e.ps1` 必须在可见、可交互的 Windows 桌面中运行。v0.4 的实现提交 `7545b5d` 已通过该 gate:40/40 manifest 入口覆盖、53/53 必需场景通过、0 error、0 blocker,且发布 EXE 哈希、真实控件事件、原生对话框、文件/SQLite 差异、重启持久化和 GUI → Application trace 均由证据门禁核验。后续若修改相关实现必须重新运行;隐藏、跳过或直接调用 Application 不能替代真实 GUI PASS。 ## License diff --git a/desktop/CodexProviderSync.App.Tests/ApplicationOperationTrackingTests.cs b/desktop/CodexProviderSync.App.Tests/ApplicationOperationTrackingTests.cs new file mode 100644 index 0000000..1ff5c87 --- /dev/null +++ b/desktop/CodexProviderSync.App.Tests/ApplicationOperationTrackingTests.cs @@ -0,0 +1,339 @@ +using CodexProviderSync.Application; +using CodexProviderSync.App.Automation; +using CodexProviderSync.Core; +using Microsoft.Data.Sqlite; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Windows.Forms; + +namespace CodexProviderSync.App.Tests; + +public sealed class ApplicationOperationTrackingTests +{ + [Theory] + [InlineData(GuiAutomationCatalog.Ids.RefreshStatus)] + [InlineData(GuiAutomationCatalog.Ids.Execute)] + [InlineData(GuiAutomationCatalog.Ids.Restore)] + [InlineData(GuiAutomationCatalog.Ids.PruneBackups)] + public void EveryBusinessButton_RequiresAnApplicationTraceWindow(string automationId) + { + Assert.True(MainForm.IsApplicationBoundAutomationId(automationId)); + } + + [Theory] + [InlineData(GuiAutomationCatalog.Ids.AddManualProvider)] + [InlineData(GuiAutomationCatalog.Ids.OpenBackupDirectory)] + [InlineData(GuiAutomationCatalog.Ids.CheckUpdates)] + public void UiAndSettingsButtons_AreExplicitlyNotApplicationBound(string automationId) + { + Assert.False(MainForm.IsApplicationBoundAutomationId(automationId)); + } + + [Fact] + public void DryRunReadyToApply_IsNeverAcceptedAsGuiApplySuccess() + { + DateTimeOffset now = DateTimeOffset.UtcNow; + ApplicationOutcome> dryRun = new( + "dry-run-operation", + ApplicationOperationKind.Sync, + ApplicationOperationLifecycle.ReadyToApply, + now, + now, + new ApplicationWriteResult(null!, Applied: false, Result: null), + [], + [], + []); + + Assert.False(GuiApplicationOutcomePolicy.IsAppliedSuccess(dryRun)); + } + + [Fact] + public void PlanStale_IsTheOnlyBoundedGuiReplanSignal() + { + DateTimeOffset now = DateTimeOffset.UtcNow; + ApplicationOutcome> stale = new( + "stale-operation", + ApplicationOperationKind.Sync, + ApplicationOperationLifecycle.Rejected, + now, + now, + null, + [], + [new ApplicationError("plan_stale", "changed")], + []); + ApplicationOutcome> busy = stale with + { + Errors = [new ApplicationError("target_busy", "busy")] + }; + + Assert.True(GuiApplicationOutcomePolicy.IsPlanStale(stale)); + Assert.False(GuiApplicationOutcomePolicy.IsPlanStale(busy)); + } + + [Fact] + public async Task AsyncHandler_RetainsGuiCorrelation_AfterDispatcherScopeReturns() + { + ApplicationOperationTraceHub hub = new(); + DeferredStatusService inner = new(); + TrackedApplicationService tracked = new(inner, hub); + Task handler; + ApplicationInvocationWindow window; + + using (ApplicationOperationTraceHub.ApplicationInvocationScope scope = + hub.BeginInvocation("request-1", GuiAutomationCatalog.Ids.RefreshStatus)) + { + window = scope.Window; + handler = RunHandlerAsync(); + } + + inner.Complete(CreateStatusOutcome("status-real-1")); + await handler; + IReadOnlyList operations = + await window.WaitAsync(CancellationToken.None); + + ApplicationOperationTraceRecord operation = Assert.Single(operations); + Assert.Equal("status-real-1", operation.OperationId); + Assert.Equal(ApplicationOperationKind.Status, operation.Operation); + Assert.Equal(ApplicationOperationLifecycle.Succeeded, operation.Lifecycle); + + async Task RunHandlerAsync() + { + await tracked.GetStatusAsync(new ApplicationStatusRequest("C:\\fixture")); + hub.CompleteCurrentInvocation(); + } + } + + [Fact] + public void TraceSink_WritesRealApplicationIdentityLifecycleAndCausalRequest() + { + string root = Path.Combine( + Path.GetTempPath(), + $"codex-provider-gui-trace-{Guid.NewGuid():N}"); + string path = Path.Combine(root, "trace.jsonl"); + try + { + DateTimeOffset now = DateTimeOffset.UtcNow; + GuiAutomationTraceSink sink = new(path); + sink.Append( + "request-apply", + "ui.invoke", + GuiAutomationCatalog.Ids.Execute, + "Click", + eventObserved: true, + [new ApplicationOperationTraceRecord( + "apply-operation-1", + ApplicationOperationKind.Sync, + ApplicationOperationLifecycle.Succeeded, + now, + now, + [])]); + + using JsonDocument document = JsonDocument.Parse(File.ReadAllText(path)); + JsonElement rootElement = document.RootElement; + Assert.Equal(2, rootElement.GetProperty("schemaVersion").GetInt32()); + Assert.Equal("request-apply", rootElement.GetProperty("requestId").GetString()); + Assert.True(rootElement.GetProperty("applicationOperationLinked").GetBoolean()); + Assert.Equal("apply-operation-1", rootElement.GetProperty("applicationOperationId").GetString()); + Assert.Equal("Sync", rootElement.GetProperty("applicationOperation").GetString()); + Assert.Equal("Succeeded", rootElement.GetProperty("applicationLifecycle").GetString()); + Assert.Equal(1, rootElement.GetProperty("applicationOperations").GetArrayLength()); + } + finally + { + if (Directory.Exists(root)) + { + Directory.Delete(root, recursive: true); + } + } + } + + [Fact] + public async Task RefreshButton_RealWinFormsEvent_UsesProductionApplicationAndCoreFixture() + { + string root = Path.Combine( + Path.GetTempPath(), + $"codex-provider-gui-core-event-{Guid.NewGuid():N}"); + Directory.CreateDirectory(root); + IAppPathProvider paths = new IsolatedAppPathProvider(root); + Directory.CreateDirectory(paths.DefaultCodexHome); + Directory.CreateDirectory(paths.RequiredSqliteHomeOverride!); + await File.WriteAllTextAsync( + Path.Combine(paths.DefaultCodexHome, "config.toml"), + "model_provider = \"openai\"\n"); + await CreateStateDatabaseAsync(Path.Combine( + paths.RequiredSqliteHomeOverride!, + "state_5.sqlite")); + SettingsService settings = new(paths.SettingsPath); + AutomationIsolation.PrepareSettings(settings, paths); + + TaskCompletionSource completion = + new(TaskCreationOptions.RunContinuationsAsynchronously); + Thread ui = new(() => + { + try + { + using MainForm form = new( + new ExecutionLogService(paths.LogDirectory), + settings, + paths: paths, + platformBoundary: new IsolatedAppPlatformBoundary(paths)); + GuiAutomationDispatcher dispatcher = new( + form, + new GuiAutomationTraceSink(paths.AutomationTracePath!)); + form.Shown += async (_, _) => + { + try + { + Button refresh = form.Controls + .Find(GuiAutomationCatalog.Ids.RefreshStatus, true) + .OfType