mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-21 18:49:56 +01:00
Compare commits
56 Commits
shawn/AddT
...
docs/skill
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7aabac375 | ||
|
|
d37c37fa70 | ||
|
|
1789b23cd8 | ||
|
|
e4d1325c5c | ||
|
|
456fe207ca | ||
|
|
c32c5233ca | ||
|
|
2be4c4eb46 | ||
|
|
731532fdd8 | ||
|
|
bde2055f26 | ||
|
|
3336c134dd | ||
|
|
4c0926d7b7 | ||
|
|
d9a1c35132 | ||
|
|
0259e31d20 | ||
|
|
266908c62a | ||
|
|
6f87e947ff | ||
|
|
70d84fcb88 | ||
|
|
8d9de117b9 | ||
|
|
42a7213644 | ||
|
|
27ba536872 | ||
|
|
18efa0559c | ||
|
|
b3e7c9d227 | ||
|
|
49cc504d94 | ||
|
|
18c6d6b0f3 | ||
|
|
4d1f92199c | ||
|
|
dca532cf4b | ||
|
|
b5991642f8 | ||
|
|
84b39a9edc | ||
|
|
67d96b0a13 | ||
|
|
c5d4f992c1 | ||
|
|
11b406feee | ||
|
|
256af8f6e0 | ||
|
|
87c65f9eec | ||
|
|
971c7e9fba | ||
|
|
055c3011cc | ||
|
|
2f7fc91956 | ||
|
|
6d4f56cd83 | ||
|
|
4986915dae | ||
|
|
cc2dce8816 | ||
|
|
0de2af77ac | ||
|
|
4694e99477 | ||
|
|
64cabc8789 | ||
|
|
989e005500 | ||
|
|
5f124cec55 | ||
|
|
8ec530c65e | ||
|
|
f82afdf384 | ||
|
|
aa2ba0c325 | ||
|
|
f534e5b8e5 | ||
|
|
08715a6e46 | ||
|
|
d26d9f745a | ||
|
|
6661adbd5c | ||
|
|
5ecb97b4e0 | ||
|
|
13ce5db6b1 | ||
|
|
f0831742d6 | ||
|
|
ea43974287 | ||
|
|
0b3dc089ac | ||
|
|
4ba6fd2723 |
1
.claude/CLAUDE.md
Symbolic link
1
.claude/CLAUDE.md
Symbolic link
@@ -0,0 +1 @@
|
||||
../.github/copilot-instructions.md
|
||||
1
.claude/agents
Symbolic link
1
.claude/agents
Symbolic link
@@ -0,0 +1 @@
|
||||
../.github/agents
|
||||
1
.claude/commands
Symbolic link
1
.claude/commands
Symbolic link
@@ -0,0 +1 @@
|
||||
../.github/prompts
|
||||
1
.claude/rules
Symbolic link
1
.claude/rules
Symbolic link
@@ -0,0 +1 @@
|
||||
../.github/instructions
|
||||
1
.claude/skills
Symbolic link
1
.claude/skills
Symbolic link
@@ -0,0 +1 @@
|
||||
../.github/skills
|
||||
63
.github/actions/spell-check/allow/zoomit.txt
vendored
Normal file
63
.github/actions/spell-check/allow/zoomit.txt
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
acq
|
||||
APPLYTOSUBMENUS
|
||||
AUDCLNT
|
||||
bitmaps
|
||||
BUFFERFLAGS
|
||||
centiseconds
|
||||
Ctl
|
||||
CTLCOLOR
|
||||
CTLCOLORBTN
|
||||
CTLCOLORDLG
|
||||
CTLCOLOREDIT
|
||||
CTLCOLORLISTBOX
|
||||
CTrim
|
||||
DFCS
|
||||
dlg
|
||||
dlu
|
||||
DONTCARE
|
||||
DRAWITEM
|
||||
DRAWITEMSTRUCT
|
||||
DWLP
|
||||
EDITCONTROL
|
||||
ENABLEHOOK
|
||||
FDE
|
||||
GETCHANNELRECT
|
||||
GETCHECK
|
||||
GETTHUMBRECT
|
||||
GIFs
|
||||
HTBOTTOMRIGHT
|
||||
HTHEME
|
||||
KSDATAFORMAT
|
||||
LEFTNOWORDWRAP
|
||||
letterbox
|
||||
lld
|
||||
logfont
|
||||
lround
|
||||
MENUINFO
|
||||
mic
|
||||
MMRESULT
|
||||
OWNERDRAW
|
||||
PBGRA
|
||||
pfdc
|
||||
playhead
|
||||
pwfx
|
||||
quantums
|
||||
REFKNOWNFOLDERID
|
||||
reposted
|
||||
SCROLLSIZEGRIP
|
||||
SETDEFID
|
||||
SETRECT
|
||||
SHAREMODE
|
||||
SHAREVIOLATION
|
||||
STREAMFLAGS
|
||||
submix
|
||||
tci
|
||||
TEXTMETRIC
|
||||
tme
|
||||
TRACKMOUSEEVENT
|
||||
Unadvise
|
||||
WASAPI
|
||||
WAVEFORMATEX
|
||||
WAVEFORMATEXTENSIBLE
|
||||
wil
|
||||
WMU
|
||||
@@ -565,7 +565,7 @@ perl(?:\s+-[a-zA-Z]\w*)+
|
||||
regexp?\.MustCompile\((?:`[^`]*`|".*"|'.*')\)
|
||||
|
||||
# regex choice
|
||||
\(\?:[^)]+\|[^)]+\)
|
||||
# \(\?:[^)]+\|[^)]+\)
|
||||
|
||||
# proto
|
||||
^\s*(\w+)\s\g{-1} =
|
||||
|
||||
5
.github/actions/spell-check/excludes.txt
vendored
5
.github/actions/spell-check/excludes.txt
vendored
@@ -101,11 +101,16 @@
|
||||
^doc/devdocs/akaLinks\.md$
|
||||
^NOTICE\.md$
|
||||
^src/common/CalculatorEngineCommon/exprtk\.hpp$
|
||||
^src/common/UnitTests-CommonUtils/
|
||||
^src/common/ManagedCommon/ColorFormatHelper\.cs$
|
||||
^src/common/notifications/BackgroundActivatorDLL/cpp\.hint$
|
||||
^src/common/sysinternals/Eula/
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherComparisonTests.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherDiacriticsTests.cs$
|
||||
^src/modules/cmdpal/doc/initial-sdk-spec/list-elements-mock-002\.pdn$
|
||||
^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$
|
||||
^src/modules/cmdpal/Microsoft\.CmdPal\.UI/Settings/InternalPage\.SampleData\.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Core\.Common\.UnitTests/.*\.TestData\.cs$
|
||||
^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$
|
||||
^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/
|
||||
^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$
|
||||
|
||||
160
.github/actions/spell-check/expect.txt
vendored
160
.github/actions/spell-check/expect.txt
vendored
@@ -11,6 +11,7 @@ ACCESSDENIED
|
||||
ACCESSTOKEN
|
||||
acfs
|
||||
ACIE
|
||||
ACR
|
||||
AClient
|
||||
AColumn
|
||||
acrt
|
||||
@@ -22,7 +23,6 @@ ADate
|
||||
ADDSTRING
|
||||
ADDUNDORECORD
|
||||
ADifferent
|
||||
adjacents
|
||||
ADMINS
|
||||
adml
|
||||
admx
|
||||
@@ -45,6 +45,7 @@ ALLCHILDREN
|
||||
ALLINPUT
|
||||
Allman
|
||||
Allmodule
|
||||
ALLNOISE
|
||||
ALLOWUNDO
|
||||
ALLVIEW
|
||||
ALPHATYPE
|
||||
@@ -58,7 +59,6 @@ AOC
|
||||
aocfnapldcnfbofgmbbllojgocaelgdd
|
||||
AOklab
|
||||
aot
|
||||
APARTMENTTHREADED
|
||||
APeriod
|
||||
apicontract
|
||||
apidl
|
||||
@@ -96,6 +96,7 @@ asf
|
||||
Ashcraft
|
||||
AShortcut
|
||||
ASingle
|
||||
ASUS
|
||||
ASSOCCHANGED
|
||||
ASSOCF
|
||||
ASSOCSTR
|
||||
@@ -105,6 +106,7 @@ atl
|
||||
ATRIOX
|
||||
aumid
|
||||
authenticode
|
||||
AUO
|
||||
AUTOBUDDY
|
||||
AUTOCHECKBOX
|
||||
AUTOHIDE
|
||||
@@ -122,6 +124,10 @@ azureaiinference
|
||||
azureinference
|
||||
azureopenai
|
||||
backticks
|
||||
Backlight
|
||||
Badflags
|
||||
Badmode
|
||||
Badparam
|
||||
bbwe
|
||||
BCIE
|
||||
bck
|
||||
@@ -130,6 +136,7 @@ bezelled
|
||||
bhid
|
||||
BIF
|
||||
bigbar
|
||||
BIGGERSIZEOK
|
||||
bigobj
|
||||
binlog
|
||||
binres
|
||||
@@ -194,6 +201,7 @@ Carlseibert
|
||||
CAtl
|
||||
caub
|
||||
CBN
|
||||
Cds
|
||||
cch
|
||||
CCHDEVICENAME
|
||||
CCHFORMNAME
|
||||
@@ -213,20 +221,22 @@ checkmarks
|
||||
CHILDACTIVATE
|
||||
CHILDWINDOW
|
||||
CHOOSEFONT
|
||||
Chunghwa
|
||||
CIBUILD
|
||||
cidl
|
||||
CIELCh
|
||||
cim
|
||||
CImage
|
||||
cla
|
||||
claude
|
||||
CLASSDC
|
||||
classguid
|
||||
classmethod
|
||||
CLASSNOTAVAILABLE
|
||||
claude
|
||||
CLEARTYPE
|
||||
clickable
|
||||
clickonce
|
||||
CLIENTEDGE
|
||||
clientedge
|
||||
clientid
|
||||
clientside
|
||||
CLIPBOARDUPDATE
|
||||
@@ -238,6 +248,7 @@ CLSCTX
|
||||
clsids
|
||||
Clusion
|
||||
cmder
|
||||
CMN
|
||||
CMDNOTFOUNDMODULEINTERFACE
|
||||
cmdpal
|
||||
CMIC
|
||||
@@ -261,7 +272,6 @@ colorhistory
|
||||
colorhistorylimit
|
||||
COLORKEY
|
||||
colorref
|
||||
Convs
|
||||
comctl
|
||||
comdlg
|
||||
comexp
|
||||
@@ -282,6 +292,7 @@ CONTEXTHELP
|
||||
CONTEXTMENUHANDLER
|
||||
contractversion
|
||||
CONTROLPARENT
|
||||
Convs
|
||||
copiedcolorrepresentation
|
||||
coppied
|
||||
copyable
|
||||
@@ -292,6 +303,7 @@ Corpor
|
||||
cotaskmem
|
||||
COULDNOT
|
||||
countof
|
||||
Cowait
|
||||
covrun
|
||||
cpcontrols
|
||||
cph
|
||||
@@ -310,11 +322,14 @@ CRECT
|
||||
CRH
|
||||
critsec
|
||||
cropandlock
|
||||
crt
|
||||
CROPTOSQUARE
|
||||
Crossdevice
|
||||
csdevkit
|
||||
CSearch
|
||||
CSettings
|
||||
cso
|
||||
CSOT
|
||||
CSRW
|
||||
CStyle
|
||||
cswin
|
||||
@@ -348,18 +363,23 @@ datareader
|
||||
datatracker
|
||||
dataversion
|
||||
Dayof
|
||||
dbcc
|
||||
DBID
|
||||
DBLCLKS
|
||||
DBLEPSILON
|
||||
DBPROP
|
||||
DBPROPIDSET
|
||||
DBPROPSET
|
||||
DBT
|
||||
DCBA
|
||||
DCapabilities
|
||||
DCOM
|
||||
DComposition
|
||||
DCR
|
||||
ddc
|
||||
DDEIf
|
||||
Deact
|
||||
debouncer
|
||||
debugbreak
|
||||
decryptor
|
||||
Dedup
|
||||
@@ -371,13 +391,13 @@ DEFAULTICON
|
||||
defaultlib
|
||||
DEFAULTONLY
|
||||
DEFAULTSIZE
|
||||
DEFAULTTONEAREST
|
||||
Defaulttonearest
|
||||
defaulttonearest
|
||||
DEFAULTTONULL
|
||||
DEFAULTTOPRIMARY
|
||||
DEFERERASE
|
||||
DEFPUSHBUTTON
|
||||
deinitialization
|
||||
DELA
|
||||
DELETEDKEYIMAGE
|
||||
DELETESCANS
|
||||
DEMOTYPE
|
||||
@@ -394,31 +414,38 @@ DESKTOPVERTRES
|
||||
devblogs
|
||||
devdocs
|
||||
devenv
|
||||
DEVICEINTERFACE
|
||||
devicetype
|
||||
DEVINTERFACE
|
||||
devmgmt
|
||||
DEVMODE
|
||||
DEVMODEW
|
||||
DEVNODES
|
||||
devpal
|
||||
DEVTYP
|
||||
dfx
|
||||
DIALOGEX
|
||||
digicert
|
||||
diffs
|
||||
digicert
|
||||
DINORMAL
|
||||
DISABLEASACTIONKEY
|
||||
DISABLENOSCROLL
|
||||
diskmgmt
|
||||
DISPLAYCHANGE
|
||||
DISPLAYCONFIG
|
||||
displayconfig
|
||||
DISPLAYFLAGS
|
||||
DISPLAYFREQUENCY
|
||||
displayname
|
||||
DISPLAYORIENTATION
|
||||
diu
|
||||
divyan
|
||||
Dlg
|
||||
DLGFRAME
|
||||
DLGMODALFRAME
|
||||
dlgmodalframe
|
||||
dlib
|
||||
dllhost
|
||||
dllmain
|
||||
Dmdo
|
||||
DNLEN
|
||||
DONOTROUND
|
||||
DONTVALIDATEPATH
|
||||
@@ -428,6 +455,7 @@ downsampling
|
||||
downscale
|
||||
DPICHANGED
|
||||
DPIs
|
||||
DPMS
|
||||
DPSAPI
|
||||
DQTAT
|
||||
DQTYPE
|
||||
@@ -465,15 +493,19 @@ DWMWINDOWMAXIMIZEDCHANGE
|
||||
DWORDLONG
|
||||
dworigin
|
||||
dwrite
|
||||
Dxva
|
||||
dxgi
|
||||
eab
|
||||
EAccess
|
||||
easeofaccess
|
||||
ecount
|
||||
Edid
|
||||
edid
|
||||
EDITKEYBOARD
|
||||
EDITSHORTCUTS
|
||||
EDITTEXT
|
||||
EFile
|
||||
EInvalid
|
||||
eep
|
||||
eku
|
||||
emojis
|
||||
ENABLEDELAYEDEXPANSION
|
||||
@@ -483,14 +515,15 @@ ENABLETEMPLATE
|
||||
encodedlaunch
|
||||
encryptor
|
||||
ENDSESSION
|
||||
ENot
|
||||
ENSUREVISIBLE
|
||||
ENTERSIZEMOVE
|
||||
ENTRYW
|
||||
ENU
|
||||
environmentvariables
|
||||
EOAC
|
||||
EPO
|
||||
epu
|
||||
EProvider
|
||||
ERASEBKGND
|
||||
EREOF
|
||||
EResize
|
||||
@@ -544,7 +577,7 @@ fdx
|
||||
FErase
|
||||
fesf
|
||||
FFFF
|
||||
FInc
|
||||
FFh
|
||||
Figma
|
||||
FILEEXPLORER
|
||||
fileexploreraddons
|
||||
@@ -565,6 +598,7 @@ FILESYSPATH
|
||||
Filetime
|
||||
FILEVERSION
|
||||
FILTERMODE
|
||||
FInc
|
||||
findfast
|
||||
findmymouse
|
||||
FIXEDFILEINFO
|
||||
@@ -586,11 +620,13 @@ formatetc
|
||||
FORPARSING
|
||||
foundrylocal
|
||||
FRAMECHANGED
|
||||
Framechanged
|
||||
FRestore
|
||||
frm
|
||||
FROMTOUCH
|
||||
fsanitize
|
||||
fsmgmt
|
||||
ftps
|
||||
fuzzingtesting
|
||||
fxf
|
||||
FZE
|
||||
@@ -628,6 +664,7 @@ GMEM
|
||||
GNumber
|
||||
googleai
|
||||
googlegemini
|
||||
Gotchas
|
||||
gpedit
|
||||
gpo
|
||||
GPOCA
|
||||
@@ -645,6 +682,8 @@ gwl
|
||||
GWLP
|
||||
GWLSTYLE
|
||||
hangeul
|
||||
Hann
|
||||
Hantai
|
||||
Hanzi
|
||||
Hardlines
|
||||
hardlinks
|
||||
@@ -666,13 +705,14 @@ HCRYPTPROV
|
||||
hcursor
|
||||
hcwhite
|
||||
hdc
|
||||
HDEVNOTIFY
|
||||
hdr
|
||||
hdrop
|
||||
hdwwiz
|
||||
Helpline
|
||||
helptext
|
||||
HGFE
|
||||
hgdiobj
|
||||
HGFE
|
||||
hglobal
|
||||
hhk
|
||||
HHmmssfff
|
||||
@@ -702,6 +742,7 @@ HKPD
|
||||
HKU
|
||||
HMD
|
||||
hmenu
|
||||
HMON
|
||||
hmodule
|
||||
hmonitor
|
||||
homies
|
||||
@@ -719,6 +760,7 @@ hotkeys
|
||||
hotlight
|
||||
hotspot
|
||||
HPAINTBUFFER
|
||||
HPhysical
|
||||
HRAWINPUT
|
||||
hredraw
|
||||
hres
|
||||
@@ -729,6 +771,7 @@ hsb
|
||||
HSCROLL
|
||||
hsi
|
||||
HSpeed
|
||||
HSync
|
||||
HTCLIENT
|
||||
hthumbnail
|
||||
HTOUCHINPUT
|
||||
@@ -738,6 +781,7 @@ HVal
|
||||
HValue
|
||||
Hvci
|
||||
hwb
|
||||
HWP
|
||||
HWHEEL
|
||||
HWINEVENTHOOK
|
||||
hwnd
|
||||
@@ -748,9 +792,10 @@ HWNDPARENT
|
||||
HWNDPREV
|
||||
hyjiacan
|
||||
IAI
|
||||
icf
|
||||
ICONERROR
|
||||
ICONLOCATION
|
||||
icf
|
||||
ICONONLY
|
||||
IDCANCEL
|
||||
IDD
|
||||
idk
|
||||
@@ -794,6 +839,7 @@ INITTOLOGFONTSTRUCT
|
||||
INLINEPREFIX
|
||||
inlines
|
||||
Inno
|
||||
Innolux
|
||||
INPC
|
||||
inproc
|
||||
INPUTHARDWARE
|
||||
@@ -835,19 +881,21 @@ istep
|
||||
ith
|
||||
ITHUMBNAIL
|
||||
IUI
|
||||
IVO
|
||||
IUWP
|
||||
IWIC
|
||||
jeli
|
||||
jfif
|
||||
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
||||
jjw
|
||||
JOBOBJECT
|
||||
jobject
|
||||
JOBOBJECT
|
||||
jpe
|
||||
jpnime
|
||||
Jsons
|
||||
jsonval
|
||||
jxr
|
||||
Kantai
|
||||
keybd
|
||||
KEYBDDATA
|
||||
KEYBDINPUT
|
||||
@@ -869,6 +917,7 @@ KILLFOCUS
|
||||
killrunner
|
||||
kmph
|
||||
kvp
|
||||
KVM
|
||||
Kybd
|
||||
LARGEICON
|
||||
lastcodeanalysissucceeded
|
||||
@@ -884,12 +933,15 @@ Lclean
|
||||
Ldone
|
||||
Ldr
|
||||
LEFTALIGN
|
||||
leftclick
|
||||
LEFTSCROLLBAR
|
||||
LEFTTEXT
|
||||
leftclick
|
||||
LError
|
||||
LEVELID
|
||||
LExit
|
||||
Lenovo
|
||||
LGD
|
||||
LFU
|
||||
lhwnd
|
||||
LIBFUZZER
|
||||
LIBID
|
||||
@@ -929,9 +981,9 @@ LOWORD
|
||||
lparam
|
||||
LPBITMAPINFOHEADER
|
||||
LPCFHOOKPROC
|
||||
lpch
|
||||
LPCITEMIDLIST
|
||||
LPCLSID
|
||||
lpch
|
||||
lpcmi
|
||||
LPCMINVOKECOMMANDINFO
|
||||
LPCREATESTRUCT
|
||||
@@ -947,6 +999,7 @@ LPMONITORINFO
|
||||
LPOSVERSIONINFOEXW
|
||||
LPQUERY
|
||||
lprc
|
||||
LPrivate
|
||||
LPSAFEARRAY
|
||||
lpstr
|
||||
lpsz
|
||||
@@ -956,7 +1009,6 @@ lptpm
|
||||
LPTR
|
||||
LPTSTR
|
||||
lpv
|
||||
LPrivate
|
||||
LPW
|
||||
lpwcx
|
||||
lpwndpl
|
||||
@@ -994,19 +1046,21 @@ MAPTOSAMESHORTCUT
|
||||
MAPVK
|
||||
MARKDOWNPREVIEWHANDLERCPP
|
||||
MAXIMIZEBOX
|
||||
Maximizebox
|
||||
MAXSHORTCUTSIZE
|
||||
maxversiontested
|
||||
mber
|
||||
MBM
|
||||
MBR
|
||||
Mbuttondown
|
||||
mcp
|
||||
MDICHILD
|
||||
MDL
|
||||
mdtext
|
||||
mdtxt
|
||||
mdwn
|
||||
mccs
|
||||
meme
|
||||
mcp
|
||||
memicmp
|
||||
MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
@@ -1027,6 +1081,7 @@ mikeclayton
|
||||
mindaro
|
||||
Minimizable
|
||||
MINIMIZEBOX
|
||||
Minimizebox
|
||||
MINIMIZEEND
|
||||
MINIMIZESTART
|
||||
MINMAXINFO
|
||||
@@ -1042,8 +1097,8 @@ mmi
|
||||
mmsys
|
||||
mobileredirect
|
||||
mockapi
|
||||
modelcontextprotocol
|
||||
MODALFRAME
|
||||
modelcontextprotocol
|
||||
MODESPRUNED
|
||||
MONITORENUMPROC
|
||||
MONITORINFO
|
||||
@@ -1062,7 +1117,8 @@ mouseutils
|
||||
MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
MRM
|
||||
MRT
|
||||
Mrt
|
||||
mrt
|
||||
mru
|
||||
MSAL
|
||||
msc
|
||||
@@ -1087,9 +1143,10 @@ MSLLHOOKSTRUCT
|
||||
Mso
|
||||
msrc
|
||||
msstore
|
||||
mstsc
|
||||
mswhql
|
||||
msvcp
|
||||
MT
|
||||
mstsc
|
||||
MTND
|
||||
MULTIPLEUSE
|
||||
multizone
|
||||
@@ -1099,12 +1156,13 @@ muxxc
|
||||
muxxh
|
||||
MVPs
|
||||
mvvm
|
||||
myorg
|
||||
myrepo
|
||||
MVVMTK
|
||||
MWBEx
|
||||
MYICON
|
||||
myorg
|
||||
myrepo
|
||||
NAMECHANGE
|
||||
Nanjing
|
||||
namespaceanddescendants
|
||||
nao
|
||||
NCACTIVATE
|
||||
@@ -1173,6 +1231,7 @@ NOMCX
|
||||
NOMINMAX
|
||||
NOMIRRORBITMAP
|
||||
NOMOVE
|
||||
Nomove
|
||||
NONANTIALIASED
|
||||
nonclient
|
||||
NONCLIENTMETRICSW
|
||||
@@ -1194,6 +1253,7 @@ NORMALUSER
|
||||
NOSEARCH
|
||||
NOSENDCHANGING
|
||||
NOSIZE
|
||||
Nosize
|
||||
NOTHOUSANDS
|
||||
NOTICKS
|
||||
NOTIFICATIONSDLL
|
||||
@@ -1201,9 +1261,11 @@ NOTIFYICONDATA
|
||||
NOTIFYICONDATAW
|
||||
NOTIMPL
|
||||
NOTOPMOST
|
||||
Notopmost
|
||||
NOTRACK
|
||||
NOTSRCCOPY
|
||||
NOTSRCERASE
|
||||
Notupdated
|
||||
notwindows
|
||||
NOTXORPEN
|
||||
nowarn
|
||||
@@ -1244,11 +1306,10 @@ opencode
|
||||
OPENFILENAME
|
||||
openrdp
|
||||
opensource
|
||||
openxmlformats
|
||||
ollama
|
||||
onnx
|
||||
openurl
|
||||
openxmlformats
|
||||
OPTIMIZEFORINVOKE
|
||||
Optronics
|
||||
ORPHANEDDIALOGTITLE
|
||||
ORSCANS
|
||||
oss
|
||||
@@ -1284,6 +1345,7 @@ PATINVERT
|
||||
PATPAINT
|
||||
pbc
|
||||
pbi
|
||||
PBP
|
||||
PBlob
|
||||
pbrush
|
||||
pcb
|
||||
@@ -1298,6 +1360,7 @@ PDBs
|
||||
PDEVMODE
|
||||
pdisp
|
||||
PDLL
|
||||
pdmodels
|
||||
pdo
|
||||
pdto
|
||||
pdtobj
|
||||
@@ -1320,12 +1383,13 @@ pguid
|
||||
phbm
|
||||
phbmp
|
||||
phicon
|
||||
PHL
|
||||
Photoshop
|
||||
phwnd
|
||||
pici
|
||||
pidl
|
||||
PIDLIST
|
||||
PII
|
||||
pii
|
||||
pinfo
|
||||
pinvoke
|
||||
pipename
|
||||
@@ -1352,6 +1416,8 @@ Popups
|
||||
POPUPWINDOW
|
||||
POSITIONITEM
|
||||
POWERBROADCAST
|
||||
powerdisplay
|
||||
POWERDISPLAYMODULEINTERFACE
|
||||
POWERRENAMECONTEXTMENU
|
||||
powerrenameinput
|
||||
POWERRENAMETEST
|
||||
@@ -1406,6 +1472,7 @@ projectname
|
||||
PROPERTYKEY
|
||||
Propset
|
||||
PROPVARIANT
|
||||
prot
|
||||
PRTL
|
||||
prvpane
|
||||
psapi
|
||||
@@ -1433,12 +1500,16 @@ PTOKEN
|
||||
PToy
|
||||
ptstr
|
||||
pui
|
||||
pvct
|
||||
PWAs
|
||||
pwcs
|
||||
PWSTR
|
||||
pwsz
|
||||
pwtd
|
||||
Qdc
|
||||
QDC
|
||||
qdc
|
||||
QDS
|
||||
qit
|
||||
QITAB
|
||||
QITABENT
|
||||
@@ -1464,7 +1535,6 @@ rbhid
|
||||
Rbuttondown
|
||||
rclsid
|
||||
RCZOOMIT
|
||||
remotedesktop
|
||||
rdp
|
||||
RDW
|
||||
READMODE
|
||||
@@ -1483,7 +1553,9 @@ regfile
|
||||
REGISTERCLASSFAILED
|
||||
REGISTRYHEADER
|
||||
REGISTRYPREVIEWEXT
|
||||
registryroot
|
||||
regkey
|
||||
regroot
|
||||
regsvr
|
||||
REINSTALLMODE
|
||||
releaseblog
|
||||
@@ -1493,6 +1565,7 @@ remappings
|
||||
REMAPSUCCESSFUL
|
||||
REMAPUNSUCCESSFUL
|
||||
Remotable
|
||||
remotedesktop
|
||||
remoteip
|
||||
Removelnk
|
||||
renamable
|
||||
@@ -1526,8 +1599,8 @@ RIGHTSCROLLBAR
|
||||
riid
|
||||
RKey
|
||||
RNumber
|
||||
rop
|
||||
rollups
|
||||
rop
|
||||
ROUNDSMALL
|
||||
ROWSETEXT
|
||||
rpcrt
|
||||
@@ -1659,6 +1732,7 @@ sigdn
|
||||
Signedness
|
||||
SIGNINGSCENARIO
|
||||
signtool
|
||||
SIIGBF
|
||||
SINGLEKEY
|
||||
sipolicy
|
||||
SIZEBOX
|
||||
@@ -1711,6 +1785,7 @@ srw
|
||||
srwlock
|
||||
sse
|
||||
ssf
|
||||
Ssn
|
||||
sszzz
|
||||
STACKFRAME
|
||||
stackoverflow
|
||||
@@ -1722,6 +1797,7 @@ STARTUPINFOW
|
||||
startupscreen
|
||||
STATFLAG
|
||||
STATICEDGE
|
||||
Staticedge
|
||||
staticmethod
|
||||
STATSTG
|
||||
stdafx
|
||||
@@ -1758,6 +1834,7 @@ subkeys
|
||||
sublang
|
||||
SUBMODULEUPDATE
|
||||
subresource
|
||||
swp
|
||||
Superbar
|
||||
sut
|
||||
svchost
|
||||
@@ -1786,8 +1863,7 @@ SYSKEY
|
||||
syskeydown
|
||||
SYSKEYUP
|
||||
SYSLIB
|
||||
SYSMENU
|
||||
Sysmenu
|
||||
sysmenu
|
||||
systemai
|
||||
SYSTEMAPPS
|
||||
SYSTEMMODAL
|
||||
@@ -1822,12 +1898,15 @@ TEXTBOXNEWLINE
|
||||
textextractor
|
||||
TEXTINCLUDE
|
||||
tfopen
|
||||
tgamma
|
||||
tgz
|
||||
THEMECHANGED
|
||||
themeresources
|
||||
THH
|
||||
THICKFRAME
|
||||
Thickframe
|
||||
THISCOMPONENT
|
||||
Tianma
|
||||
throughs
|
||||
TILEDWINDOW
|
||||
TILLSON
|
||||
@@ -1891,9 +1970,9 @@ uitests
|
||||
UITo
|
||||
ULONGLONG
|
||||
Ultrawide
|
||||
ums
|
||||
UMax
|
||||
UMin
|
||||
ums
|
||||
uncompilable
|
||||
UNCPRIORITY
|
||||
UNDNAME
|
||||
@@ -1908,13 +1987,13 @@ UNLEN
|
||||
UNORM
|
||||
unremapped
|
||||
Unsubscribes
|
||||
unsubscribes
|
||||
unvirtualized
|
||||
unwide
|
||||
unzoom
|
||||
UOffset
|
||||
UOI
|
||||
UPDATENOW
|
||||
UPDATEREGISTRY
|
||||
updown
|
||||
UPGRADINGPRODUCTCODE
|
||||
upscaling
|
||||
@@ -1941,6 +2020,8 @@ vcamp
|
||||
vcenter
|
||||
vcgtq
|
||||
VCINSTALLDIR
|
||||
vcp
|
||||
vcpname
|
||||
Vcpkg
|
||||
VCRT
|
||||
vcruntime
|
||||
@@ -1953,6 +2034,8 @@ VERIFYCONTEXT
|
||||
VERSIONINFO
|
||||
VERTRES
|
||||
VERTSIZE
|
||||
VESA
|
||||
vesa
|
||||
VFT
|
||||
vget
|
||||
vgetq
|
||||
@@ -1984,6 +2067,7 @@ VSM
|
||||
vso
|
||||
vsonline
|
||||
VSpeed
|
||||
VSync
|
||||
vstemplate
|
||||
vstest
|
||||
VSTHRD
|
||||
@@ -2025,7 +2109,7 @@ winapi
|
||||
winappsdk
|
||||
windir
|
||||
WINDOWCREATED
|
||||
WINDOWEDGE
|
||||
windowedge
|
||||
WINDOWINFO
|
||||
WINDOWNAME
|
||||
WINDOWPLACEMENT
|
||||
@@ -2049,12 +2133,12 @@ WINL
|
||||
winlogon
|
||||
winmd
|
||||
winml
|
||||
WINNT
|
||||
winres
|
||||
winrt
|
||||
winsdk
|
||||
winsta
|
||||
WINTHRESHOLD
|
||||
WINNT
|
||||
WINVER
|
||||
winxamlmanager
|
||||
withinrafael
|
||||
@@ -2066,6 +2150,7 @@ WKSG
|
||||
Wlkr
|
||||
wmain
|
||||
Wman
|
||||
wmi
|
||||
WMI
|
||||
WMICIM
|
||||
wmimgmt
|
||||
@@ -2078,6 +2163,7 @@ WNDCLASSEX
|
||||
WNDCLASSEXW
|
||||
WNDCLASSW
|
||||
WNDPROC
|
||||
Wndproc
|
||||
wnode
|
||||
wom
|
||||
WORKSPACESEDITOR
|
||||
|
||||
13
.github/actions/spell-check/patterns.txt
vendored
13
.github/actions/spell-check/patterns.txt
vendored
@@ -274,5 +274,18 @@ St&yle
|
||||
# 0x6f677548 is user name but user folder causes a flag
|
||||
\bx6f677548\b
|
||||
|
||||
# Windows API constants and hardware interface terms
|
||||
\bCOINIT[_A-Z]*\b
|
||||
\bEOAC[_A-Z]*\b
|
||||
\b(?:RPC_C_AUTHN_)?WINNT\b
|
||||
\bUPDATEREGISTRY\b
|
||||
\b(?:CDS_)?UPDATEREGISTRY\b
|
||||
|
||||
# Display interface terms (HDMI, DVI, DisplayPort)
|
||||
\b(?:HDMI|DVI|DisplayPort)(?:-\d+)?\b
|
||||
|
||||
# 2D Region struct names
|
||||
\bDisplayConfig2?D?Region\b
|
||||
|
||||
# Microsoft Store URLs and product IDs
|
||||
ms-windows-store://\S+
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Generate an 80-character git commit title for the local diff'
|
||||
---
|
||||
|
||||
|
||||
1
.github/prompts/create-pr-summary.prompt.md
vendored
1
.github/prompts/create-pr-summary.prompt.md
vendored
@@ -1,6 +1,5 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Generate a PowerToys-ready pull request description from the local diff'
|
||||
---
|
||||
|
||||
|
||||
1
.github/prompts/fix-issue.prompt.md
vendored
1
.github/prompts/fix-issue.prompt.md
vendored
@@ -1,6 +1,5 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Execute the fix for a GitHub issue using the previously generated implementation plan'
|
||||
---
|
||||
|
||||
|
||||
70
.github/prompts/fix-pr-active-comments.prompt.md
vendored
Normal file
70
.github/prompts/fix-pr-active-comments.prompt.md
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
description: 'Fix active pull request comments with scoped changes'
|
||||
name: 'fix-pr-active-comments'
|
||||
agent: 'agent'
|
||||
argument-hint: 'PR number or active PR URL'
|
||||
---
|
||||
|
||||
# Fix Active PR Comments
|
||||
|
||||
## Mission
|
||||
Resolve active pull request comments by applying only simple fixes. For complex refactors, write a plan instead of changing code.
|
||||
|
||||
## Scope & Preconditions
|
||||
- You must have an active pull request context or a provided PR number.
|
||||
- Only implement simple changes. Do not implement large refactors.
|
||||
- If required context is missing, request it and stop.
|
||||
|
||||
## Inputs
|
||||
- Required: ${input:pr_number:PR number or URL}
|
||||
- Optional: ${input:comment_scope:files or areas to focus on}
|
||||
- Optional: ${input:fixing_guidelines:additional fixing guidelines from the user}
|
||||
|
||||
## Workflow
|
||||
1. Locate all active (unresolved) PR review comments for the given PR.
|
||||
2. For each comment, classify the change scope:
|
||||
- Simple change: limited edits, localized fix, low risk, no broad redesign.
|
||||
- Large refactor: multi-file redesign, architecture change, or risky behavior change.
|
||||
3. For each large refactor request:
|
||||
- Do not modify code.
|
||||
- Write a planning document to Generated Files/prReview/${input:pr_number}/fixPlan/.
|
||||
4. For each simple change request:
|
||||
- Implement the fix with minimal edits.
|
||||
- Run quick checks if needed.
|
||||
- Commit and push the change.
|
||||
5. For comments that seem invalid, unclear, or not applicable (even if simple):
|
||||
- Do not change code.
|
||||
- Add the item to a summary table in Generated Files/prReview/${input:pr_number}/fixPlan/overview.md.
|
||||
- Consult back to the end user in a friendly, polite tone.
|
||||
6. Respond to each comment that you fixed:
|
||||
- Reply in the active conversation.
|
||||
- Use a polite or friendly tone.
|
||||
- Keep the response under 200 words.
|
||||
- Resolve the comment after replying.
|
||||
|
||||
## Output Expectations
|
||||
- Simple fixes: code changes committed and pushed.
|
||||
- Large refactors: a plan file saved to Generated Files/prReview/${input:pr_number}/fixPlan/.
|
||||
- Invalid or unclear comments: captured in Generated Files/prReview/${input:pr_number}/fixPlan/overview.md.
|
||||
- Each fixed comment has a reply under 200 words and is resolved.
|
||||
|
||||
## Plan File Template
|
||||
Use this template for each large refactor item:
|
||||
|
||||
# Fix Plan: <short title>
|
||||
|
||||
## Context
|
||||
- Comment link:
|
||||
- Impacted areas:
|
||||
|
||||
## Overview Table Template
|
||||
Use this table in Generated Files/prReview/${input:pr_number}/fixPlan/overview.md:
|
||||
|
||||
| Comment link | Summary | Reason not applied | Suggested follow-up |
|
||||
| --- | --- | --- | --- |
|
||||
| | | | |
|
||||
|
||||
## Quality Assurance
|
||||
- Verify plan file path exists.
|
||||
- Ensure no code changes were made for large refactor items.
|
||||
- Confirm replies are under 200 words and comments are resolved.
|
||||
1
.github/prompts/fix-spelling.prompt.md
vendored
1
.github/prompts/fix-spelling.prompt.md
vendored
@@ -1,6 +1,5 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Resolve Code scanning / check-spelling comments on the active PR'
|
||||
---
|
||||
|
||||
|
||||
1
.github/prompts/review-issue.prompt.md
vendored
1
.github/prompts/review-issue.prompt.md
vendored
@@ -1,6 +1,5 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Review a GitHub issue, score it (0-100), and generate an implementation plan'
|
||||
---
|
||||
|
||||
|
||||
1
.github/prompts/review-pr.prompt.md
vendored
1
.github/prompts/review-pr.prompt.md
vendored
@@ -1,6 +1,5 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Perform a comprehensive PR review with per-step Markdown and machine-readable outputs'
|
||||
---
|
||||
|
||||
|
||||
220
.github/skills/issue-fix/SKILL.md
vendored
Normal file
220
.github/skills/issue-fix/SKILL.md
vendored
Normal file
@@ -0,0 +1,220 @@
|
||||
---
|
||||
name: issue-fix
|
||||
description: Automatically fix GitHub issues and create PRs. Use when asked to fix an issue, implement a feature from an issue, auto-fix an issue, apply implementation plan, create code changes for an issue, resolve a GitHub issue, or submit a PR for an issue. Creates isolated git worktree, applies AI-generated fixes, commits changes, and creates pull requests.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# Issue Fix Skill
|
||||
|
||||
Automatically fix GitHub issues by creating isolated worktrees, applying AI-generated code changes, and creating pull requests - the complete issue-to-PR workflow.
|
||||
|
||||
## Skill Contents
|
||||
|
||||
This skill is **self-contained** with all required resources:
|
||||
|
||||
```
|
||||
.github/skills/issue-fix/
|
||||
├── SKILL.md # This file
|
||||
├── LICENSE.txt # MIT License
|
||||
├── scripts/
|
||||
│ ├── Start-IssueAutoFix.ps1 # Main fix script (creates worktree, applies fix)
|
||||
│ ├── Start-IssueFixParallel.ps1 # Parallel runner (single terminal)
|
||||
│ ├── Get-WorktreeStatus.ps1 # Worktree status helper
|
||||
│ ├── Submit-IssueFix.ps1 # Commit and create PR
|
||||
│ └── IssueReviewLib.ps1 # Shared helpers
|
||||
└── references/
|
||||
├── fix-issue.prompt.md # AI prompt for fixing
|
||||
├── create-commit-title.prompt.md # AI prompt for commit messages
|
||||
├── create-pr-summary.prompt.md # AI prompt for PR descriptions
|
||||
└── mcp-config.json # MCP configuration
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
- **Worktrees**: Created at drive root level `Q:/PowerToys-xxxx/`
|
||||
- **PRs**: Created on GitHub linking to the original issue
|
||||
- **Signal file**: `Generated Files/issueFix/<issue>/.signal`
|
||||
|
||||
## Signal File
|
||||
|
||||
On completion, a `.signal` file is created for orchestrator coordination:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"issueNumber": 45363,
|
||||
"timestamp": "2026-02-04T10:05:23Z",
|
||||
"worktreePath": "Q:/PowerToys-ab12"
|
||||
}
|
||||
```
|
||||
|
||||
Status values: `success`, `failure`
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Fix a specific GitHub issue automatically
|
||||
- Implement a feature described in an issue
|
||||
- Apply an existing implementation plan
|
||||
- Create code changes and submit PR for an issue
|
||||
- Auto-fix high-confidence issues end-to-end
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- GitHub CLI (`gh`) installed and authenticated
|
||||
- Issue must be reviewed first (use `issue-review` skill)
|
||||
- PowerShell 7+ for running scripts
|
||||
- Copilot CLI or Claude CLI installed
|
||||
|
||||
## Required Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `{{IssueNumber}}` | GitHub issue number to fix | `44044` |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Ensure Issue is Reviewed
|
||||
|
||||
If not already reviewed, use the `issue-review` skill first.
|
||||
|
||||
### Step 2: Run Auto-Fix
|
||||
|
||||
```powershell
|
||||
# Create worktree and apply fix
|
||||
.github/skills/issue-fix/scripts/Start-IssueAutoFix.ps1 -IssueNumber {{IssueNumber}} -CLIType copilot -Force
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Create a new git worktree with branch `issue/{{IssueNumber}}`
|
||||
2. Copy the review files to the worktree
|
||||
3. Launch Copilot CLI to implement the fix
|
||||
4. Build and verify the changes
|
||||
|
||||
### Step 3: Submit PR
|
||||
|
||||
```powershell
|
||||
# Commit changes and create PR
|
||||
.github/skills/issue-fix/scripts/Submit-IssueFix.ps1 -IssueNumber {{IssueNumber}} -CLIType copilot -Force
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Generate AI commit message
|
||||
2. Commit all changes
|
||||
3. Push to origin
|
||||
4. Create PR with AI-generated description
|
||||
5. Link PR to issue with "Fixes #{{IssueNumber}}"
|
||||
|
||||
### One-Step Alternative
|
||||
|
||||
To fix AND submit in one command:
|
||||
|
||||
```powershell
|
||||
.github/skills/issue-fix/scripts/Start-IssueAutoFix.ps1 -IssueNumber {{IssueNumber}} -CLIType copilot -CreatePR -Force
|
||||
```
|
||||
|
||||
## CLI Options
|
||||
|
||||
### Start-IssueAutoFix.ps1
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `-IssueNumber` | Issue to fix | Required |
|
||||
| `-CLIType` | AI CLI: `copilot` or `claude` | `copilot` |
|
||||
| `-Model` | Copilot model (e.g., `gpt-5.2-codex`) | (optional) |
|
||||
| `-CreatePR` | Auto-create PR after fix | `false` |
|
||||
| `-SkipWorktree` | Fix in current repo (no worktree) | `false` |
|
||||
| `-Force` | Skip confirmation prompts | `false` |
|
||||
|
||||
### Submit-IssueFix.ps1
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `-IssueNumber` | Issue to submit | Required |
|
||||
| `-CLIType` | AI CLI: `copilot`, `claude`, `manual` | `copilot` |
|
||||
| `-Draft` | Create as draft PR | `false` |
|
||||
| `-SkipCommit` | Skip commit (changes already committed) | `false` |
|
||||
| `-Force` | Skip confirmation prompts | `false` |
|
||||
|
||||
## Batch Processing
|
||||
|
||||
Fix multiple issues:
|
||||
|
||||
```powershell
|
||||
# Fix multiple issues (creates worktrees, applies fixes)
|
||||
.github/skills/issue-fix/scripts/Start-IssueAutoFix.ps1 -IssueNumbers 44044, 32950 -CLIType copilot -Force
|
||||
|
||||
# Submit all fixed issues as PRs
|
||||
.github/skills/issue-fix/scripts/Submit-IssueFix.ps1 -CLIType copilot -Force
|
||||
```
|
||||
|
||||
## Parallel Execution (IMPORTANT)
|
||||
|
||||
**DO NOT** spawn separate terminals for each issue. Use the dedicated scripts:
|
||||
|
||||
```powershell
|
||||
# Run fixes in parallel (single terminal)
|
||||
.github/skills/issue-fix/scripts/Start-IssueFixParallel.ps1 -IssueNumbers 28726,13336,27507,3054,37800 -CLIType copilot -ThrottleLimit 5 -Force
|
||||
|
||||
# Check worktree status
|
||||
.github/skills/issue-fix/scripts/Get-WorktreeStatus.ps1
|
||||
```
|
||||
|
||||
This allows:
|
||||
- Tracking all jobs in one place
|
||||
- Waiting for completion with proper synchronization
|
||||
- Controlling parallelism with `-ThrottleLimit`
|
||||
- Combined output visibility
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| Worktree already exists | Use existing worktree or `git worktree remove <path>` |
|
||||
| No implementation plan | Use `issue-review` skill first |
|
||||
| Build failures | Check build logs, may need manual intervention |
|
||||
| PR already exists | Script will skip, check existing PR |
|
||||
| CLI not found | Install Copilot CLI |
|
||||
|
||||
## PR Creation Requirements (CRITICAL)
|
||||
|
||||
**NEVER create PRs with placeholder/stub code.** Every PR must have:
|
||||
|
||||
1. **Real implementation** - Actual working code that addresses the issue
|
||||
2. **Proper title** - Follow `create-commit-title.prompt.md` (Conventional Commits)
|
||||
3. **Full description** - Follow `create-pr-summary.prompt.md` based on actual diff
|
||||
|
||||
### PR Title Format (Conventional Commits)
|
||||
```
|
||||
feat(module): add feature description
|
||||
fix(module): fix bug description
|
||||
docs(module): update documentation
|
||||
```
|
||||
|
||||
### PR Description Must Include
|
||||
- Summary of changes (from actual diff)
|
||||
- `Fixes #IssueNumber` link
|
||||
- Checklist items marked appropriately
|
||||
- Validation steps performed
|
||||
|
||||
**Example of BAD PR (never do this):**
|
||||
```
|
||||
Title: fix: address issue #12345
|
||||
Body: Fixes #12345
|
||||
Code: class Fix12345 { public void Apply() { } } // EMPTY STUB!
|
||||
```
|
||||
|
||||
**Example of GOOD PR:**
|
||||
```
|
||||
Title: feat(peek): add symbolic link resolution for PDF/HTML files
|
||||
Body: ## Summary
|
||||
Adds SymlinkResolver helper to resolve symlinks...
|
||||
[Full description based on create-pr-summary.prompt.md]
|
||||
```
|
||||
|
||||
## Related Skills
|
||||
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `issue-review` | Review issues, generate implementation plans |
|
||||
| `pr-review` | Review the created PR |
|
||||
| `pr-fix` | Fix PR review comments |
|
||||
22
.github/skills/issue-fix/scripts/Get-WorktreeStatus.ps1
vendored
Normal file
22
.github/skills/issue-fix/scripts/Get-WorktreeStatus.ps1
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Show commit/uncommitted status for issue/* worktrees.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')
|
||||
Set-Location $repoRoot
|
||||
|
||||
git worktree list | Select-String "issue/" | ForEach-Object {
|
||||
$path = ($_ -split "\s+")[0]
|
||||
$branch = ($_ -split "\s+")[2] -replace "\[|\]",""
|
||||
$ahead = (git -C $path rev-list main..HEAD --count 2>$null)
|
||||
$uncommitted = (git -C $path status --porcelain 2>$null | Measure-Object).Count
|
||||
[pscustomobject]@{
|
||||
Branch = $branch
|
||||
CommitsAhead = $ahead
|
||||
Uncommitted = $uncommitted
|
||||
Path = $path
|
||||
}
|
||||
}
|
||||
562
.github/skills/issue-fix/scripts/Start-IssueAutoFix.ps1
vendored
Normal file
562
.github/skills/issue-fix/scripts/Start-IssueAutoFix.ps1
vendored
Normal file
@@ -0,0 +1,562 @@
|
||||
<#!
|
||||
.SYNOPSIS
|
||||
Auto-fix high-confidence issues using worktrees and AI CLI.
|
||||
|
||||
.DESCRIPTION
|
||||
Finds issues with high confidence scores from the review results, creates worktrees
|
||||
for each, copies the Generated Files, and kicks off the FixIssue agent to implement fixes.
|
||||
|
||||
.PARAMETER IssueNumber
|
||||
Specific issue number to fix. If not specified, finds high-confidence issues automatically.
|
||||
|
||||
.PARAMETER MinFeasibilityScore
|
||||
Minimum Technical Feasibility score (0-100). Default: 70.
|
||||
|
||||
.PARAMETER MinClarityScore
|
||||
Minimum Requirement Clarity score (0-100). Default: 60.
|
||||
|
||||
.PARAMETER MaxEffortDays
|
||||
Maximum effort estimate in days. Default: 2 (Small fixes).
|
||||
|
||||
.PARAMETER MaxParallel
|
||||
Maximum parallel fix jobs. Default: 5 (worktrees are resource-intensive).
|
||||
|
||||
.PARAMETER CLIType
|
||||
AI CLI to use: claude, gh-copilot, or vscode. Auto-detected if not specified.
|
||||
|
||||
.PARAMETER Model
|
||||
Copilot CLI model to use (e.g., gpt-5.2-codex).
|
||||
|
||||
.PARAMETER DryRun
|
||||
List issues without starting fixes.
|
||||
|
||||
.PARAMETER SkipWorktree
|
||||
Fix in the current repository instead of creating worktrees (useful for single issue).
|
||||
|
||||
.PARAMETER VSCodeProfile
|
||||
VS Code profile to use when opening worktrees. Default: Default.
|
||||
|
||||
.PARAMETER AutoCommit
|
||||
Automatically commit changes after successful fix.
|
||||
|
||||
.PARAMETER CreatePR
|
||||
Automatically create a pull request after successful fix.
|
||||
|
||||
.EXAMPLE
|
||||
# Fix a specific issue
|
||||
./Start-IssueAutoFix.ps1 -IssueNumber 12345
|
||||
|
||||
.EXAMPLE
|
||||
# Find and fix all high-confidence issues (dry run)
|
||||
./Start-IssueAutoFix.ps1 -DryRun
|
||||
|
||||
.EXAMPLE
|
||||
# Fix issues with very high confidence
|
||||
./Start-IssueAutoFix.ps1 -MinFeasibilityScore 80 -MinClarityScore 70 -MaxEffortDays 1
|
||||
|
||||
.EXAMPLE
|
||||
# Fix single issue in current repo (no worktree)
|
||||
./Start-IssueAutoFix.ps1 -IssueNumber 12345 -SkipWorktree
|
||||
|
||||
.NOTES
|
||||
Prerequisites:
|
||||
- Run Start-BulkIssueReview.ps1 first to generate review files
|
||||
- GitHub CLI (gh) authenticated
|
||||
- Claude Code CLI or VS Code with Copilot
|
||||
|
||||
Results:
|
||||
- Worktrees created at ../<RepoName>-<hash>/
|
||||
- Generated Files copied to each worktree
|
||||
- Fix agent invoked in each worktree
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int]$IssueNumber,
|
||||
|
||||
[int]$MinFeasibilityScore = 70,
|
||||
|
||||
[int]$MinClarityScore = 60,
|
||||
|
||||
[int]$MaxEffortDays = 2,
|
||||
|
||||
[int]$MaxParallel = 5,
|
||||
|
||||
[ValidateSet('claude', 'copilot', 'gh-copilot', 'vscode', 'auto')]
|
||||
[string]$CLIType = 'auto',
|
||||
|
||||
[string]$Model,
|
||||
|
||||
[switch]$DryRun,
|
||||
|
||||
[switch]$SkipWorktree,
|
||||
|
||||
[Alias('Profile')]
|
||||
[string]$VSCodeProfile = 'Default',
|
||||
|
||||
[switch]$AutoCommit,
|
||||
|
||||
[switch]$CreatePR,
|
||||
|
||||
[switch]$Force,
|
||||
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
# Load libraries
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$scriptDir/IssueReviewLib.ps1"
|
||||
|
||||
# Load worktree library from tools/build
|
||||
$repoRoot = Get-RepoRoot
|
||||
$worktreeLib = Join-Path $repoRoot 'tools/build/WorktreeLib.ps1'
|
||||
if (Test-Path $worktreeLib) {
|
||||
. $worktreeLib
|
||||
}
|
||||
|
||||
# Show help
|
||||
if ($Help) {
|
||||
Get-Help $MyInvocation.MyCommand.Path -Full
|
||||
return
|
||||
}
|
||||
|
||||
function Start-IssueFixInWorktree {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Analyze implementation plan and either take action or create worktree for fix.
|
||||
.DESCRIPTION
|
||||
First analyzes the implementation plan to determine if:
|
||||
- Issue is already resolved (close it)
|
||||
- Issue needs clarification (add comment)
|
||||
- Issue is a duplicate (close as duplicate)
|
||||
- Issue is ready to implement (create worktree and fix)
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$IssueNumber,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$SourceRepoRoot,
|
||||
[string]$CLIType = 'claude',
|
||||
[string]$Model,
|
||||
[string]$VSCodeProfile = 'Default',
|
||||
[switch]$SkipWorktree,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$issueReviewPath = Get-IssueReviewPath -RepoRoot $SourceRepoRoot -IssueNumber $IssueNumber
|
||||
$overviewPath = Join-Path $issueReviewPath 'overview.md'
|
||||
$implPlanPath = Join-Path $issueReviewPath 'implementation-plan.md'
|
||||
|
||||
# Verify review files exist
|
||||
if (-not (Test-Path $overviewPath)) {
|
||||
throw "No overview.md found for issue #$IssueNumber. Run Start-BulkIssueReview.ps1 first."
|
||||
}
|
||||
if (-not (Test-Path $implPlanPath)) {
|
||||
throw "No implementation-plan.md found for issue #$IssueNumber. Run Start-BulkIssueReview.ps1 first."
|
||||
}
|
||||
|
||||
# =====================================
|
||||
# STEP 1: Analyze the implementation plan
|
||||
# =====================================
|
||||
Info "Analyzing implementation plan for issue #$IssueNumber..."
|
||||
$planStatus = Get-ImplementationPlanStatus -ImplementationPlanPath $implPlanPath
|
||||
|
||||
# =====================================
|
||||
# STEP 2: Execute the recommended action
|
||||
# =====================================
|
||||
$actionResult = Invoke-ImplementationPlanAction -IssueNumber $IssueNumber -PlanStatus $planStatus -DryRun:$DryRun
|
||||
|
||||
# If we shouldn't proceed with fix, return early
|
||||
if (-not $actionResult.ShouldProceedWithFix) {
|
||||
return @{
|
||||
IssueNumber = $IssueNumber
|
||||
WorktreePath = $null
|
||||
Success = $actionResult.Success
|
||||
ActionTaken = $actionResult.ActionTaken
|
||||
SkippedCodeFix = $true
|
||||
}
|
||||
}
|
||||
|
||||
# =====================================
|
||||
# STEP 3: Proceed with code fix
|
||||
# =====================================
|
||||
|
||||
$workingDir = $SourceRepoRoot
|
||||
|
||||
if (-not $SkipWorktree) {
|
||||
# Use the simplified New-WorktreeFromIssue.cmd which only needs issue number
|
||||
$worktreeCmd = Join-Path $SourceRepoRoot 'tools/build/New-WorktreeFromIssue.cmd'
|
||||
|
||||
Info "Creating worktree for issue #$IssueNumber..."
|
||||
|
||||
# Call the cmd script with issue number and -NoVSCode for automation
|
||||
& cmd /c $worktreeCmd $IssueNumber -NoVSCode
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to create worktree for issue #$IssueNumber"
|
||||
}
|
||||
|
||||
# Find the created worktree
|
||||
$entries = Get-WorktreeEntries
|
||||
$worktreeEntry = $entries | Where-Object { $_.Branch -like "issue/$IssueNumber*" } | Select-Object -First 1
|
||||
|
||||
if (-not $worktreeEntry) {
|
||||
throw "Failed to find worktree for issue #$IssueNumber"
|
||||
}
|
||||
|
||||
$workingDir = $worktreeEntry.Path
|
||||
Info "Worktree created at: $workingDir"
|
||||
|
||||
# Copy Generated Files to worktree
|
||||
Info "Copying review files to worktree..."
|
||||
$destReviewPath = Copy-IssueReviewToWorktree -IssueNumber $IssueNumber -SourceRepoRoot $SourceRepoRoot -WorktreePath $workingDir
|
||||
Info "Review files copied to: $destReviewPath"
|
||||
|
||||
# Copy .github/skills folder to worktree (needed for MCP config)
|
||||
$sourceSkillsPath = Join-Path $SourceRepoRoot '.github/skills'
|
||||
$destSkillsPath = Join-Path $workingDir '.github/skills'
|
||||
if (Test-Path $sourceSkillsPath) {
|
||||
$destGithubPath = Join-Path $workingDir '.github'
|
||||
if (-not (Test-Path $destGithubPath)) {
|
||||
New-Item -ItemType Directory -Path $destGithubPath -Force | Out-Null
|
||||
}
|
||||
Copy-Item -Path $sourceSkillsPath -Destination $destGithubPath -Recurse -Force
|
||||
Info "Copied .github/skills to worktree"
|
||||
}
|
||||
}
|
||||
|
||||
# Build the prompt for the fix agent
|
||||
$prompt = @"
|
||||
You are the FixIssue agent. Fix GitHub issue #$IssueNumber.
|
||||
|
||||
The implementation plan is at: Generated Files/issueReview/$IssueNumber/implementation-plan.md
|
||||
The overview is at: Generated Files/issueReview/$IssueNumber/overview.md
|
||||
|
||||
Follow the implementation plan exactly. Build and verify after each change.
|
||||
"@
|
||||
|
||||
# Start the fix agent
|
||||
Info "Starting fix agent for issue #$IssueNumber in $workingDir..."
|
||||
|
||||
# MCP config for github-artifacts tools (relative to repo root)
|
||||
$mcpConfig = '@.github/skills/issue-fix/references/mcp-config.json'
|
||||
|
||||
switch ($CLIType) {
|
||||
'copilot' {
|
||||
# GitHub Copilot CLI (standalone copilot command)
|
||||
# -p: Non-interactive prompt mode (exits after completion)
|
||||
# --yolo: Enable all permissions for automated execution
|
||||
# -s: Silent mode - output only agent response
|
||||
# --additional-mcp-config: Load github-artifacts MCP for image/attachment analysis
|
||||
$copilotArgs = @(
|
||||
'--additional-mcp-config', $mcpConfig,
|
||||
'-p', $prompt,
|
||||
'--yolo',
|
||||
'-s'
|
||||
)
|
||||
if ($Model) {
|
||||
$copilotArgs += @('--model', $Model)
|
||||
}
|
||||
Info "Running: copilot $($copilotArgs -join ' ')"
|
||||
Push-Location $workingDir
|
||||
try {
|
||||
& copilot @copilotArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Warn "Copilot exited with code $LASTEXITCODE"
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
'claude' {
|
||||
$claudeArgs = @(
|
||||
'--print',
|
||||
'--dangerously-skip-permissions',
|
||||
'--prompt', $prompt
|
||||
)
|
||||
Start-Process -FilePath 'claude' -ArgumentList $claudeArgs -WorkingDirectory $workingDir -Wait -NoNewWindow
|
||||
}
|
||||
'gh-copilot' {
|
||||
# Use GitHub Copilot CLI via gh extension
|
||||
# gh copilot suggest requires interactive mode, so we open VS Code with the prompt
|
||||
Info "GitHub Copilot CLI detected. Opening VS Code with prompt..."
|
||||
|
||||
# Create a prompt file in the worktree for easy access
|
||||
$promptFile = Join-Path $workingDir "Generated Files/issueReview/$IssueNumber/fix-prompt.md"
|
||||
$promptContent = @"
|
||||
# Fix Issue #$IssueNumber
|
||||
|
||||
## Instructions
|
||||
|
||||
$prompt
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Read the implementation plan: ``Generated Files/issueReview/$IssueNumber/implementation-plan.md``
|
||||
2. Read the overview: ``Generated Files/issueReview/$IssueNumber/overview.md``
|
||||
3. Follow the plan step by step
|
||||
4. Build and test after each change
|
||||
"@
|
||||
Set-Content -Path $promptFile -Value $promptContent -Force
|
||||
|
||||
# Open VS Code with the worktree
|
||||
code --new-window $workingDir --profile $VSCodeProfile
|
||||
Info "VS Code opened at $workingDir"
|
||||
Info "Prompt file created at: $promptFile"
|
||||
Info "Use GitHub Copilot in VS Code to implement the fix."
|
||||
}
|
||||
'vscode' {
|
||||
# Open VS Code and let user manually trigger the fix
|
||||
code --new-window $workingDir --profile $VSCodeProfile
|
||||
Info "VS Code opened at $workingDir. Use Copilot to implement the fix."
|
||||
}
|
||||
default {
|
||||
Warn "CLI type '$CLIType' not fully supported for auto-fix. Opening VS Code..."
|
||||
code --new-window $workingDir --profile $VSCodeProfile
|
||||
}
|
||||
}
|
||||
|
||||
# Check if any changes were actually made
|
||||
$hasChanges = $false
|
||||
Push-Location $workingDir
|
||||
try {
|
||||
$uncommitted = git status --porcelain 2>$null
|
||||
$commitsAhead = git rev-list main..HEAD --count 2>$null
|
||||
if ($uncommitted -or ($commitsAhead -gt 0)) {
|
||||
$hasChanges = $true
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
return @{
|
||||
IssueNumber = $IssueNumber
|
||||
WorktreePath = $workingDir
|
||||
Success = $true
|
||||
ActionTaken = 'CodeFixAttempted'
|
||||
SkippedCodeFix = $false
|
||||
HasChanges = $hasChanges
|
||||
}
|
||||
}
|
||||
|
||||
#region Main Script
|
||||
try {
|
||||
Info "Repository root: $repoRoot"
|
||||
|
||||
# Detect or validate CLI
|
||||
if ($CLIType -eq 'auto') {
|
||||
$cli = Get-AvailableCLI
|
||||
if ($cli) {
|
||||
$CLIType = $cli.Type
|
||||
Info "Auto-detected CLI: $($cli.Name)"
|
||||
} else {
|
||||
$CLIType = 'vscode'
|
||||
Info "No CLI detected, will use VS Code"
|
||||
}
|
||||
}
|
||||
|
||||
# Find issues to fix
|
||||
$issuesToFix = @()
|
||||
|
||||
if ($IssueNumber) {
|
||||
# Single issue specified
|
||||
$reviewResult = Get-IssueReviewResult -IssueNumber $IssueNumber -RepoRoot $repoRoot
|
||||
if (-not $reviewResult.HasOverview -or -not $reviewResult.HasImplementationPlan) {
|
||||
throw "Issue #$IssueNumber does not have review files. Run Start-BulkIssueReview.ps1 first."
|
||||
}
|
||||
$issuesToFix += @{
|
||||
IssueNumber = $IssueNumber
|
||||
OverviewPath = $reviewResult.OverviewPath
|
||||
ImplementationPlanPath = $reviewResult.ImplementationPlanPath
|
||||
}
|
||||
} else {
|
||||
# Find high-confidence issues
|
||||
Info "`nSearching for high-confidence issues..."
|
||||
Info " Min Feasibility Score: $MinFeasibilityScore"
|
||||
Info " Min Clarity Score: $MinClarityScore"
|
||||
Info " Max Effort: $MaxEffortDays days"
|
||||
|
||||
$highConfidence = Get-HighConfidenceIssues `
|
||||
-RepoRoot $repoRoot `
|
||||
-MinFeasibilityScore $MinFeasibilityScore `
|
||||
-MinClarityScore $MinClarityScore `
|
||||
-MaxEffortDays $MaxEffortDays
|
||||
|
||||
if ($highConfidence.Count -eq 0) {
|
||||
Warn "No high-confidence issues found matching criteria."
|
||||
Info "Try lowering the score thresholds or increasing MaxEffortDays."
|
||||
return
|
||||
}
|
||||
|
||||
$issuesToFix = $highConfidence
|
||||
}
|
||||
|
||||
Info "`nIssues ready for auto-fix: $($issuesToFix.Count)"
|
||||
Info ("-" * 80)
|
||||
foreach ($issue in $issuesToFix) {
|
||||
$scores = ""
|
||||
if ($issue.FeasibilityScore) {
|
||||
$scores = " [Feasibility: $($issue.FeasibilityScore), Clarity: $($issue.ClarityScore), Effort: $($issue.EffortDays)d]"
|
||||
}
|
||||
Info ("#{0,-6}{1}" -f $issue.IssueNumber, $scores)
|
||||
}
|
||||
Info ("-" * 80)
|
||||
|
||||
# In DryRun mode, still analyze plans but don't take action
|
||||
if ($DryRun) {
|
||||
Info "`nAnalyzing implementation plans (dry run)..."
|
||||
foreach ($issue in $issuesToFix) {
|
||||
$implPlanPath = Join-Path (Get-IssueReviewPath -RepoRoot $repoRoot -IssueNumber $issue.IssueNumber) 'implementation-plan.md'
|
||||
if (Test-Path $implPlanPath) {
|
||||
$planStatus = Get-ImplementationPlanStatus -ImplementationPlanPath $implPlanPath
|
||||
$color = switch ($planStatus.Action) {
|
||||
'ImplementFix' { 'Green' }
|
||||
'CloseIssue' { 'Yellow' }
|
||||
'AddComment' { 'Cyan' }
|
||||
'LinkDuplicate' { 'Magenta' }
|
||||
default { 'Gray' }
|
||||
}
|
||||
Write-Host (" #{0,-6} [{1,-20}] -> {2}" -f $issue.IssueNumber, $planStatus.Status, $planStatus.Action) -ForegroundColor $color
|
||||
if ($planStatus.RelatedPR) {
|
||||
$prInfo = "PR #$($planStatus.RelatedPR)"
|
||||
if ($planStatus.ReleasedIn) {
|
||||
$prInfo += " (released in $($planStatus.ReleasedIn))"
|
||||
} elseif ($planStatus.Status -eq 'FixedButUnreleased') {
|
||||
$prInfo += " (merged, awaiting release)"
|
||||
}
|
||||
Write-Host " $prInfo" -ForegroundColor DarkGray
|
||||
}
|
||||
if ($planStatus.DuplicateOf) {
|
||||
Write-Host " Duplicate of #$($planStatus.DuplicateOf)" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
}
|
||||
Warn "`nDry run mode - no actions taken."
|
||||
return
|
||||
}
|
||||
|
||||
# Confirm before proceeding (skip if -Force)
|
||||
if (-not $Force) {
|
||||
$confirm = Read-Host "`nProceed with fixing $($issuesToFix.Count) issues? (y/N)"
|
||||
if ($confirm -notmatch '^[yY]') {
|
||||
Info "Cancelled."
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# Process issues
|
||||
$results = @{
|
||||
Succeeded = @()
|
||||
Failed = @()
|
||||
AlreadyResolved = @()
|
||||
AwaitingRelease = @()
|
||||
NeedsClarification = @()
|
||||
Duplicates = @()
|
||||
NoChanges = @()
|
||||
}
|
||||
|
||||
foreach ($issue in $issuesToFix) {
|
||||
try {
|
||||
Info "`n" + ("=" * 60)
|
||||
Info "PROCESSING ISSUE #$($issue.IssueNumber)"
|
||||
Info ("=" * 60)
|
||||
|
||||
$result = Start-IssueFixInWorktree `
|
||||
-IssueNumber $issue.IssueNumber `
|
||||
-SourceRepoRoot $repoRoot `
|
||||
-CLIType $CLIType `
|
||||
-Model $Model `
|
||||
-VSCodeProfile $VSCodeProfile `
|
||||
-SkipWorktree:$SkipWorktree `
|
||||
-DryRun:$DryRun
|
||||
|
||||
if ($result.SkippedCodeFix) {
|
||||
# Action was taken but no code fix (e.g., closed issue, added comment)
|
||||
switch -Wildcard ($result.ActionTaken) {
|
||||
'*Closing*' { $results.AlreadyResolved += $issue.IssueNumber }
|
||||
'*clarification*' { $results.NeedsClarification += $issue.IssueNumber }
|
||||
'*duplicate*' { $results.Duplicates += $issue.IssueNumber }
|
||||
'*merged*awaiting*' { $results.AwaitingRelease += $issue.IssueNumber }
|
||||
'*merged but not yet released*' { $results.AwaitingRelease += $issue.IssueNumber }
|
||||
default { $results.Succeeded += $issue.IssueNumber }
|
||||
}
|
||||
Success "✓ Issue #$($issue.IssueNumber) handled: $($result.ActionTaken)"
|
||||
}
|
||||
elseif ($result.HasChanges) {
|
||||
$results.Succeeded += $issue.IssueNumber
|
||||
Success "✓ Issue #$($issue.IssueNumber) fix completed with changes"
|
||||
}
|
||||
else {
|
||||
$results.NoChanges += $issue.IssueNumber
|
||||
Warn "⚠ Issue #$($issue.IssueNumber) fix ran but no code changes were made"
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Err "✗ Issue #$($issue.IssueNumber) failed: $($_.Exception.Message)"
|
||||
$results.Failed += $issue.IssueNumber
|
||||
}
|
||||
}
|
||||
|
||||
# Summary
|
||||
Info "`n" + ("=" * 80)
|
||||
Info "AUTO-FIX COMPLETE"
|
||||
Info ("=" * 80)
|
||||
Info "Total issues: $($issuesToFix.Count)"
|
||||
if ($results.Succeeded.Count -gt 0) {
|
||||
Success "Code fixes: $($results.Succeeded.Count)"
|
||||
}
|
||||
if ($results.AlreadyResolved.Count -gt 0) {
|
||||
Success "Already resolved: $($results.AlreadyResolved.Count) (issues closed)"
|
||||
}
|
||||
if ($results.AwaitingRelease.Count -gt 0) {
|
||||
Info "Awaiting release: $($results.AwaitingRelease.Count) (fix merged, pending release)"
|
||||
}
|
||||
if ($results.NeedsClarification.Count -gt 0) {
|
||||
Warn "Need clarification: $($results.NeedsClarification.Count) (comments added)"
|
||||
}
|
||||
if ($results.Duplicates.Count -gt 0) {
|
||||
Warn "Duplicates: $($results.Duplicates.Count) (issues closed)"
|
||||
}
|
||||
if ($results.NoChanges.Count -gt 0) {
|
||||
Warn "No changes made: $($results.NoChanges.Count)"
|
||||
}
|
||||
if ($results.Failed.Count -gt 0) {
|
||||
Err "Failed: $($results.Failed.Count)"
|
||||
Err "Failed issues: $($results.Failed -join ', ')"
|
||||
}
|
||||
Info ("=" * 80)
|
||||
|
||||
if (-not $SkipWorktree -and ($results.Succeeded.Count -gt 0 -or $results.NoChanges.Count -gt 0)) {
|
||||
Info "`nWorktrees created. Use 'git worktree list' to see all worktrees."
|
||||
Info "To clean up: Delete-Worktree.ps1 -Branch issue/<number>"
|
||||
}
|
||||
|
||||
# Write signal files for orchestrator
|
||||
$genFiles = Get-GeneratedFilesPath -RepoRoot $repoRoot
|
||||
foreach ($issueNum in $results.Succeeded) {
|
||||
$signalDir = Join-Path $genFiles "issueFix/$issueNum"
|
||||
if (-not (Test-Path $signalDir)) { New-Item -ItemType Directory -Path $signalDir -Force | Out-Null }
|
||||
@{
|
||||
status = "success"
|
||||
issueNumber = $issueNum
|
||||
timestamp = (Get-Date).ToString("o")
|
||||
worktreePath = (git worktree list --porcelain | Select-String "worktree.*issue.$issueNum" | ForEach-Object { $_.Line -replace 'worktree ', '' })
|
||||
} | ConvertTo-Json | Set-Content "$signalDir/.signal" -Force
|
||||
}
|
||||
foreach ($issueNum in $results.Failed) {
|
||||
$signalDir = Join-Path $genFiles "issueFix/$issueNum"
|
||||
if (-not (Test-Path $signalDir)) { New-Item -ItemType Directory -Path $signalDir -Force | Out-Null }
|
||||
@{
|
||||
status = "failure"
|
||||
issueNumber = $issueNum
|
||||
timestamp = (Get-Date).ToString("o")
|
||||
} | ConvertTo-Json | Set-Content "$signalDir/.signal" -Force
|
||||
}
|
||||
|
||||
return $results
|
||||
}
|
||||
catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
exit 1
|
||||
}
|
||||
#endregion
|
||||
73
.github/skills/issue-fix/scripts/Start-IssueFixParallel.ps1
vendored
Normal file
73
.github/skills/issue-fix/scripts/Start-IssueFixParallel.ps1
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Run issue-fix in parallel from a single terminal.
|
||||
|
||||
.PARAMETER IssueNumbers
|
||||
Issue numbers to fix.
|
||||
|
||||
.PARAMETER ThrottleLimit
|
||||
Maximum parallel tasks.
|
||||
|
||||
.PARAMETER CLIType
|
||||
AI CLI type (copilot/claude/gh-copilot/vscode/auto).
|
||||
|
||||
.PARAMETER Model
|
||||
Copilot CLI model to use (e.g., gpt-5.2-codex).
|
||||
|
||||
.PARAMETER Force
|
||||
Skip confirmation prompts in Start-IssueAutoFix.ps1.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int[]]$IssueNumbers,
|
||||
|
||||
[int]$ThrottleLimit = 5,
|
||||
|
||||
[ValidateSet('claude', 'copilot', 'gh-copilot', 'vscode', 'auto')]
|
||||
[string]$CLIType = 'copilot',
|
||||
|
||||
[string]$Model,
|
||||
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')
|
||||
$scriptPath = Join-Path $repoRoot '.github\skills\issue-fix\scripts\Start-IssueAutoFix.ps1'
|
||||
|
||||
$results = $IssueNumbers | ForEach-Object -Parallel {
|
||||
param($issue)
|
||||
|
||||
$repoRoot = $using:repoRoot
|
||||
$scriptPath = $using:scriptPath
|
||||
$cliType = $using:CLIType
|
||||
$model = $using:Model
|
||||
$force = $using:Force
|
||||
|
||||
Set-Location $repoRoot
|
||||
|
||||
$args = @('-IssueNumber', $issue, '-CLIType', $cliType)
|
||||
if ($model) {
|
||||
$args += @('-Model', $model)
|
||||
}
|
||||
if ($force) {
|
||||
$args += '-Force'
|
||||
}
|
||||
|
||||
try {
|
||||
& $scriptPath @args | Out-Default
|
||||
[pscustomobject]@{
|
||||
IssueNumber = $issue
|
||||
ExitCode = $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
catch {
|
||||
[pscustomobject]@{
|
||||
IssueNumber = $issue
|
||||
ExitCode = 1
|
||||
Error = $_.Exception.Message
|
||||
}
|
||||
}
|
||||
} -ThrottleLimit $ThrottleLimit
|
||||
|
||||
$results
|
||||
252
.github/skills/issue-to-pr-cycle/SKILL.md
vendored
Normal file
252
.github/skills/issue-to-pr-cycle/SKILL.md
vendored
Normal file
@@ -0,0 +1,252 @@
|
||||
---
|
||||
name: issue-to-pr-cycle
|
||||
description: End-to-end orchestration from issue analysis to PR creation and review. This skill is the ORCHESTRATION BRAIN that invokes other skills via CLI and performs VS Code MCP operations directly.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# Issue-to-PR Full Cycle Skill
|
||||
|
||||
**ORCHESTRATION BRAIN** - coordinates other skills and performs VS Code MCP operations.
|
||||
|
||||
## Skill Contents
|
||||
|
||||
```
|
||||
.github/skills/issue-to-pr-cycle/
|
||||
├── SKILL.md # This file (orchestration brain)
|
||||
├── LICENSE.txt # MIT License
|
||||
└── scripts/
|
||||
├── Get-CycleStatus.ps1 # Check status of issues/PRs
|
||||
├── IssueReviewLib.ps1 # Shared helpers
|
||||
└── Start-FullIssueCycle.ps1 # Legacy script (phases A-C)
|
||||
```
|
||||
|
||||
**Orchestrates these skills:**
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `issue-review` | Analyze issues, generate implementation plans |
|
||||
| `issue-fix` | Create worktrees, apply fixes, create PRs |
|
||||
| `pr-review` | Comprehensive PR review (13 steps) |
|
||||
| `pr-fix` | Fix review comments, resolve threads |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- GitHub CLI (`gh`) installed and authenticated
|
||||
- Copilot CLI or Claude CLI installed
|
||||
- PowerShell 7+
|
||||
- VS Code with MCP tools (for write operations)
|
||||
|
||||
## Required Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `{{IssueNumbers}}` | Issue numbers to process | `45363, 45364` |
|
||||
| (or) `{{PRNumbers}}` | PR numbers for review/fix loop | `45365, 45366` |
|
||||
|
||||
## How This Skill Works
|
||||
|
||||
The orchestrator:
|
||||
1. **Invokes skills via CLI** - kicks off `copilot` CLI (not `gh copilot`) to run each skill
|
||||
2. **Runs in parallel** - use PowerShell 7 `ForEach-Object -Parallel` in SINGLE terminal
|
||||
3. **Waits for signals** - polls for `.signal` files indicating completion
|
||||
4. **Performs VS Code MCP directly** - for operations that require write access (request reviewer, resolve threads)
|
||||
|
||||
## Quality Gates (CRITICAL)
|
||||
|
||||
**Every PR must pass these quality checks before creation:**
|
||||
|
||||
1. **Real Implementation** - NO placeholder/stub code
|
||||
- Files must contain actual working code
|
||||
- Empty classes like `class FixXXX { }` are FORBIDDEN
|
||||
|
||||
2. **Proper PR Title** - Follow Conventional Commits
|
||||
- Use `.github/prompts/create-commit-title.prompt.md`
|
||||
- Format: `feat(module): description` or `fix(module): description`
|
||||
- NEVER use generic titles like "fix: address issue #12345"
|
||||
|
||||
3. **Full PR Description** - Based on actual diff
|
||||
- Use `.github/prompts/create-pr-summary.prompt.md`
|
||||
- Run `git diff main...HEAD` to analyze changes
|
||||
- Fill PR template with real information
|
||||
|
||||
4. **Build Verification** - Code must compile
|
||||
- Run `tools/build/build.cmd` in worktree
|
||||
- Exit code 0 = success
|
||||
|
||||
### Checking Worktree Quality
|
||||
|
||||
```powershell
|
||||
# Check if worktree has real implementation (not stubs)
|
||||
$files = git diff main --name-only
|
||||
foreach ($file in $files) {
|
||||
if ($file -match "src/common/fixes/Fix\d+\.cs") {
|
||||
Write-Error "STUB FILE DETECTED: $file - Need real implementation"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Signal Files
|
||||
|
||||
Each skill produces a `.signal` file when complete:
|
||||
|
||||
| Skill | Signal Location | Status Values |
|
||||
|-------|-----------------|---------------|
|
||||
| `issue-review` | `Generated Files/issueReview/<issue>/.signal` | `success`, `failure` |
|
||||
| `issue-fix` | `Generated Files/issueFix/<issue>/.signal` | `success`, `failure` |
|
||||
| `pr-review` | `Generated Files/prReview/<pr>/.signal` | `success`, `failure` |
|
||||
| `pr-fix` | `Generated Files/prFix/<pr>/.signal` | `success`, `partial`, `failure` |
|
||||
|
||||
Signal format:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"issueNumber": 45363,
|
||||
"timestamp": "2026-02-04T10:05:23Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ORCHESTRATOR (this skill, VS Code agent) │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ issue-review│ │ issue-fix │ │ pr-review │ │ pr-fix │ │
|
||||
│ │ (CLI) │ │ (CLI) │ │ (CLI) │ │ (CLI) │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ▼ ▼ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Signal Files (Generated Files/*/.signal) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ VS Code MCP Operations (orchestrator executes directly): │
|
||||
│ - mcp_github_request_copilot_review │
|
||||
│ - gh api graphql (resolve threads) │
|
||||
│ - Post review comments │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase A: Issue Review
|
||||
|
||||
Use the orchestration script instead of inline commands:
|
||||
|
||||
```powershell
|
||||
.github/skills/issue-to-pr-cycle/scripts/Start-FullIssueCycle.ps1 -IssueNumbers 45363,45364
|
||||
```
|
||||
|
||||
### Phase B: Issue Fix
|
||||
|
||||
Use the parallel runner script:
|
||||
|
||||
```powershell
|
||||
.github/skills/issue-fix/scripts/Start-IssueFixParallel.ps1 -IssueNumbers 45363,45364 -CLIType copilot -ThrottleLimit 5 -Force
|
||||
```
|
||||
|
||||
### Phase C: PR Review
|
||||
|
||||
Use the pr-review script for each PR, or run the full cycle script to orchestrate:
|
||||
|
||||
```powershell
|
||||
.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 -PRNumber 45392
|
||||
```
|
||||
|
||||
### Phase D: Review/Fix Loop (VS Code Agent Orchestrated)
|
||||
|
||||
This phase requires the VS Code agent to:
|
||||
|
||||
**D1: Request Copilot review (VS Code MCP)**
|
||||
```
|
||||
mcp_github_request_copilot_review:
|
||||
owner: microsoft
|
||||
repo: PowerToys
|
||||
pullNumber: {{PRNumber}}
|
||||
```
|
||||
|
||||
**D2: Invoke pr-review skill (CLI, parallel)**
|
||||
```powershell
|
||||
gh copilot -p "Run skill pr-review for PR #{{PRNumber}}"
|
||||
# Wait for: Generated Files/prReview/{{PRNumber}}/.signal
|
||||
```
|
||||
|
||||
**D3: Check results**
|
||||
- Read `Generated Files/prReview/{{PRNumber}}/00-OVERVIEW.md`
|
||||
- Query unresolved threads via GraphQL
|
||||
|
||||
**D4: Post comments (VS Code MCP) - if medium+ severity**
|
||||
|
||||
**D5: Invoke pr-fix skill in WORKTREE (CLI)**
|
||||
```powershell
|
||||
# Find worktree for this PR's branch
|
||||
$branch = (gh pr view {{PRNumber}} --json headRefName -q .headRefName)
|
||||
$worktree = git worktree list --porcelain | Select-String "worktree.*$branch" | ...
|
||||
|
||||
# Run fix in worktree
|
||||
cd $worktreePath
|
||||
gh copilot -p "Run skill pr-fix for PR #{{PRNumber}}"
|
||||
# Wait for: Generated Files/prFix/{{PRNumber}}/.signal
|
||||
```
|
||||
|
||||
**D6: Resolve threads (VS Code MCP)**
|
||||
```powershell
|
||||
# Get thread IDs
|
||||
gh api graphql -f query='query { repository(owner:"microsoft",name:"PowerToys") {
|
||||
pullRequest(number:{{PRNumber}}) { reviewThreads(first:50) { nodes { id isResolved } } }
|
||||
} }'
|
||||
|
||||
# Resolve each (VS Code agent executes this)
|
||||
gh api graphql -f query='mutation { resolveReviewThread(input:{threadId:"{{ID}}"}) { thread { isResolved } } }'
|
||||
```
|
||||
|
||||
**D7: Loop**
|
||||
- If unresolved issues remain → go to D2
|
||||
- If all clear → done
|
||||
|
||||
## Timeout Handling
|
||||
|
||||
Default timeout: 10 minutes per skill invocation.
|
||||
|
||||
If no signal file appears within timeout:
|
||||
1. Check if the skill process is still running
|
||||
2. If hung, terminate and mark as `timeout`
|
||||
3. Log failure and continue with other items
|
||||
|
||||
## Parallel Execution (CRITICAL)
|
||||
|
||||
**DO NOT spawn separate terminals for each operation.** Use the dedicated scripts to run parallel work from a single terminal:
|
||||
|
||||
```powershell
|
||||
# Issue fixes in parallel
|
||||
.github/skills/issue-fix/scripts/Start-IssueFixParallel.ps1 -IssueNumbers 28726,13336,27507,3054,37800 -CLIType copilot -Model gpt-5.2-codex -ThrottleLimit 5 -Force
|
||||
|
||||
# PR fixes in parallel
|
||||
.github/skills/pr-fix/scripts/Start-PRFixParallel.ps1 -PRNumbers 45256,45257,45285,45286 -CLIType copilot -Model gpt-5.2-codex -ThrottleLimit 3 -Force
|
||||
```
|
||||
|
||||
## Worktree Mapping
|
||||
|
||||
The orchestrator must track which worktree belongs to which issue/PR:
|
||||
|
||||
```powershell
|
||||
# Get all worktrees
|
||||
$worktrees = git worktree list --porcelain | Select-String "worktree|branch" |
|
||||
ForEach-Object { $_.Line }
|
||||
|
||||
# Parse into mapping
|
||||
# Q:\PowerToys-ab12 → issue/44044
|
||||
# Q:\PowerToys-cd34 → issue/32950
|
||||
|
||||
# Find worktree for issue
|
||||
$issueNum = 45363
|
||||
$worktreeLine = git worktree list | Select-String "issue/$issueNum"
|
||||
$worktreePath = ($worktreeLine -split '\s+')[0]
|
||||
```
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Process multiple issues end-to-end
|
||||
- Automate the full issue → PR → review → fix cycle
|
||||
- Batch process high-confidence issues
|
||||
- Run continuous review/fix loops until clean
|
||||
349
.github/skills/issue-to-pr-cycle/scripts/Get-CycleStatus.ps1
vendored
Normal file
349
.github/skills/issue-to-pr-cycle/scripts/Get-CycleStatus.ps1
vendored
Normal file
@@ -0,0 +1,349 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get the current status of issues/PRs in the issue-to-PR cycle.
|
||||
|
||||
.DESCRIPTION
|
||||
Checks the status of:
|
||||
- Issue review completion (has overview.md + implementation-plan.md)
|
||||
- Issue fix completion (has worktree + commits)
|
||||
- PR creation status (has open PR)
|
||||
- PR review status (has review files)
|
||||
- PR active comments count
|
||||
|
||||
.PARAMETER IssueNumbers
|
||||
Array of issue numbers to check status for.
|
||||
|
||||
.PARAMETER PRNumbers
|
||||
Array of PR numbers to check status for.
|
||||
|
||||
.PARAMETER CheckAll
|
||||
Check all issues with review data and all open PRs with issue/* branches.
|
||||
|
||||
.EXAMPLE
|
||||
./Get-CycleStatus.ps1 -IssueNumbers 44044, 32950
|
||||
|
||||
.EXAMPLE
|
||||
./Get-CycleStatus.ps1 -PRNumbers 45234, 45235
|
||||
|
||||
.EXAMPLE
|
||||
./Get-CycleStatus.ps1 -CheckAll
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int[]]$IssueNumbers = @(),
|
||||
[int[]]$PRNumbers = @(),
|
||||
[switch]$CheckAll,
|
||||
[switch]$JsonOutput
|
||||
)
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. (Join-Path $scriptDir 'IssueReviewLib.ps1')
|
||||
|
||||
$repoRoot = Get-RepoRoot
|
||||
$genFiles = Get-GeneratedFilesPath -RepoRoot $repoRoot
|
||||
$worktreeLib = Join-Path $repoRoot 'tools/build/WorktreeLib.ps1'
|
||||
if (Test-Path $worktreeLib) {
|
||||
. $worktreeLib
|
||||
}
|
||||
|
||||
function Get-IssueStatus {
|
||||
param([int]$IssueNumber)
|
||||
|
||||
$status = @{
|
||||
IssueNumber = $IssueNumber
|
||||
HasReview = $false
|
||||
HasImplementationPlan = $false
|
||||
FeasibilityScore = 0
|
||||
ClarityScore = 0
|
||||
EffortDays = 0
|
||||
HasWorktree = $false
|
||||
WorktreePath = $null
|
||||
HasCommits = $false
|
||||
CommitCount = 0
|
||||
HasPR = $false
|
||||
PRNumber = 0
|
||||
PRState = $null
|
||||
PRUrl = $null
|
||||
ReviewSignalStatus = $null
|
||||
ReviewSignalTimestamp = $null
|
||||
FixSignalStatus = $null
|
||||
FixSignalTimestamp = $null
|
||||
}
|
||||
|
||||
# Check review status
|
||||
$reviewDir = Join-Path $genFiles "issueReview/$IssueNumber"
|
||||
$overviewPath = Join-Path $reviewDir 'overview.md'
|
||||
$implPlanPath = Join-Path $reviewDir 'implementation-plan.md'
|
||||
|
||||
if (Test-Path $overviewPath) {
|
||||
$status.HasReview = $true
|
||||
$overview = Get-Content $overviewPath -Raw
|
||||
|
||||
if ($overview -match 'Technical Feasibility[^\d]*(\d+)/100') {
|
||||
$status.FeasibilityScore = [int]$Matches[1]
|
||||
}
|
||||
if ($overview -match 'Requirement Clarity[^\d]*(\d+)/100') {
|
||||
$status.ClarityScore = [int]$Matches[1]
|
||||
}
|
||||
if ($overview -match 'Effort Estimate[^|]*\|\s*[\d.]+(?:-(\d+))?\s*days?') {
|
||||
$status.EffortDays = if ($Matches[1]) { [int]$Matches[1] } else { 1 }
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path $implPlanPath) {
|
||||
$status.HasImplementationPlan = $true
|
||||
}
|
||||
|
||||
# Check review signal
|
||||
$reviewSignalPath = Join-Path $reviewDir '.signal'
|
||||
if (Test-Path $reviewSignalPath) {
|
||||
try {
|
||||
$reviewSignal = Get-Content $reviewSignalPath -Raw | ConvertFrom-Json
|
||||
$status.ReviewSignalStatus = $reviewSignal.status
|
||||
$status.ReviewSignalTimestamp = $reviewSignal.timestamp
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
# Check worktree status
|
||||
$worktrees = Get-WorktreeEntries | Where-Object { $_.Branch -like "issue/$IssueNumber*" }
|
||||
if ($worktrees) {
|
||||
$status.HasWorktree = $true
|
||||
$status.WorktreePath = $worktrees[0].Path
|
||||
|
||||
# Check for commits
|
||||
Push-Location $status.WorktreePath
|
||||
try {
|
||||
$commits = git log --oneline "main..HEAD" 2>$null
|
||||
if ($commits) {
|
||||
$status.HasCommits = $true
|
||||
$status.CommitCount = @($commits).Count
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
# Check fix signal
|
||||
$fixSignalPath = Join-Path $genFiles "issueFix/$IssueNumber/.signal"
|
||||
if (Test-Path $fixSignalPath) {
|
||||
try {
|
||||
$fixSignal = Get-Content $fixSignalPath -Raw | ConvertFrom-Json
|
||||
$status.FixSignalStatus = $fixSignal.status
|
||||
$status.FixSignalTimestamp = $fixSignal.timestamp
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
# Check PR status
|
||||
$prs = gh pr list --head "issue/$IssueNumber" --state all --json number,url,state 2>$null | ConvertFrom-Json
|
||||
if (-not $prs -or $prs.Count -eq 0) {
|
||||
# Try searching by issue reference
|
||||
$prs = gh pr list --search "fixes #$IssueNumber OR closes #$IssueNumber" --state all --json number,url,state --limit 1 2>$null | ConvertFrom-Json
|
||||
}
|
||||
if ($prs -and $prs.Count -gt 0) {
|
||||
$status.HasPR = $true
|
||||
$status.PRNumber = $prs[0].number
|
||||
$status.PRState = $prs[0].state
|
||||
$status.PRUrl = $prs[0].url
|
||||
}
|
||||
|
||||
return $status
|
||||
}
|
||||
|
||||
function Get-PRStatus {
|
||||
param([int]$PRNumber)
|
||||
|
||||
$status = @{
|
||||
PRNumber = $PRNumber
|
||||
State = $null
|
||||
IssueNumber = 0
|
||||
Branch = $null
|
||||
HasReviewFiles = $false
|
||||
ReviewStepCount = 0
|
||||
HighSeverityCount = 0
|
||||
MediumSeverityCount = 0
|
||||
ActiveCommentCount = 0
|
||||
UnresolvedThreadCount = 0
|
||||
CopilotReviewRequested = $false
|
||||
ReviewSignalStatus = $null
|
||||
ReviewSignalTimestamp = $null
|
||||
FixSignalStatus = $null
|
||||
FixSignalTimestamp = $null
|
||||
}
|
||||
|
||||
# Get PR info
|
||||
$prInfo = gh pr view $PRNumber --json state,headRefName,number 2>$null | ConvertFrom-Json
|
||||
if (-not $prInfo) {
|
||||
return $status
|
||||
}
|
||||
|
||||
$status.State = $prInfo.state
|
||||
$status.Branch = $prInfo.headRefName
|
||||
|
||||
# Extract issue number from branch
|
||||
if ($status.Branch -match 'issue/(\d+)') {
|
||||
$status.IssueNumber = [int]$Matches[1]
|
||||
}
|
||||
|
||||
# Check review files
|
||||
$reviewDir = Join-Path $genFiles "prReview/$PRNumber"
|
||||
if (Test-Path $reviewDir) {
|
||||
$status.HasReviewFiles = $true
|
||||
$stepFiles = Get-ChildItem -Path $reviewDir -Filter "*.md" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -match '^\d{2}-' }
|
||||
$status.ReviewStepCount = $stepFiles.Count
|
||||
|
||||
# Count severity issues
|
||||
foreach ($stepFile in $stepFiles) {
|
||||
$content = Get-Content $stepFile.FullName -Raw -ErrorAction SilentlyContinue
|
||||
if ($content) {
|
||||
$status.HighSeverityCount += ([regex]::Matches($content, '\*\*Severity:\s*high\*\*', 'IgnoreCase')).Count
|
||||
$status.HighSeverityCount += ([regex]::Matches($content, '🔴\s*High', 'IgnoreCase')).Count
|
||||
$status.MediumSeverityCount += ([regex]::Matches($content, '\*\*Severity:\s*medium\*\*', 'IgnoreCase')).Count
|
||||
$status.MediumSeverityCount += ([regex]::Matches($content, '🟡\s*Medium', 'IgnoreCase')).Count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Check review signal
|
||||
$reviewSignalPath = Join-Path $reviewDir '.signal'
|
||||
if (Test-Path $reviewSignalPath) {
|
||||
try {
|
||||
$reviewSignal = Get-Content $reviewSignalPath -Raw | ConvertFrom-Json
|
||||
$status.ReviewSignalStatus = $reviewSignal.status
|
||||
$status.ReviewSignalTimestamp = $reviewSignal.timestamp
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
# Check fix signal
|
||||
$fixSignalPath = Join-Path $genFiles "prFix/$PRNumber/.signal"
|
||||
if (Test-Path $fixSignalPath) {
|
||||
try {
|
||||
$fixSignal = Get-Content $fixSignalPath -Raw | ConvertFrom-Json
|
||||
$status.FixSignalStatus = $fixSignal.status
|
||||
$status.FixSignalTimestamp = $fixSignal.timestamp
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
# Get active comments (not in reply to another comment)
|
||||
try {
|
||||
$commentCount = gh api "repos/microsoft/PowerToys/pulls/$PRNumber/comments" --jq '[.[] | select(.in_reply_to_id == null)] | length' 2>$null
|
||||
$status.ActiveCommentCount = [int]$commentCount
|
||||
}
|
||||
catch {
|
||||
$status.ActiveCommentCount = 0
|
||||
}
|
||||
|
||||
# Get unresolved thread count
|
||||
try {
|
||||
$threads = gh api graphql -f query="query { repository(owner: `"microsoft`", name: `"PowerToys`") { pullRequest(number: $PRNumber) { reviewThreads(first: 100) { nodes { isResolved } } } } }" --jq '.data.repository.pullRequest.reviewThreads.nodes | map(select(.isResolved == false)) | length' 2>$null
|
||||
$status.UnresolvedThreadCount = [int]$threads
|
||||
}
|
||||
catch {
|
||||
$status.UnresolvedThreadCount = 0
|
||||
}
|
||||
|
||||
# Check if Copilot review was requested
|
||||
try {
|
||||
$reviewers = gh pr view $PRNumber --json reviewRequests --jq '.reviewRequests[].login' 2>$null
|
||||
if ($reviewers -contains 'copilot' -or $reviewers -contains 'github-copilot') {
|
||||
$status.CopilotReviewRequested = $true
|
||||
}
|
||||
}
|
||||
catch {}
|
||||
|
||||
return $status
|
||||
}
|
||||
|
||||
# Main execution
|
||||
$results = @{
|
||||
Issues = @()
|
||||
PRs = @()
|
||||
Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||
}
|
||||
|
||||
# Gather issue numbers to check
|
||||
$issuesToCheck = @()
|
||||
$prsToCheck = @()
|
||||
|
||||
if ($CheckAll) {
|
||||
# Get all reviewed issues
|
||||
$reviewDir = Join-Path $genFiles 'issueReview'
|
||||
if (Test-Path $reviewDir) {
|
||||
$issuesToCheck = Get-ChildItem -Path $reviewDir -Directory |
|
||||
Where-Object { $_.Name -match '^\d+$' } |
|
||||
ForEach-Object { [int]$_.Name }
|
||||
}
|
||||
|
||||
# Get all open PRs with issue/* branches
|
||||
$openPRs = gh pr list --state open --json number,headRefName 2>$null | ConvertFrom-Json |
|
||||
Where-Object { $_.headRefName -like 'issue/*' }
|
||||
$prsToCheck = @($openPRs | ForEach-Object { $_.number })
|
||||
}
|
||||
else {
|
||||
$issuesToCheck = $IssueNumbers
|
||||
$prsToCheck = $PRNumbers
|
||||
}
|
||||
|
||||
# Get issue statuses
|
||||
foreach ($issueNum in $issuesToCheck) {
|
||||
$status = Get-IssueStatus -IssueNumber $issueNum
|
||||
$results.Issues += $status
|
||||
}
|
||||
|
||||
# Get PR statuses
|
||||
foreach ($prNum in $prsToCheck) {
|
||||
$status = Get-PRStatus -PRNumber $prNum
|
||||
$results.PRs += $status
|
||||
}
|
||||
|
||||
# Output
|
||||
if ($JsonOutput) {
|
||||
$results | ConvertTo-Json -Depth 5
|
||||
return $results
|
||||
}
|
||||
else {
|
||||
if ($results.Issues.Count -gt 0) {
|
||||
Write-Host "`n=== ISSUE STATUS ===" -ForegroundColor Cyan
|
||||
Write-Host ("-" * 100)
|
||||
Write-Host ("{0,-8} {1,-8} {2,-8} {3,-5} {4,-5} {5,-10} {6,-8} {7,-8} {8,-8} {9,-8}" -f "Issue", "Review", "Plan", "Feas", "Clar", "Worktree", "Commits", "PR", "RevSig", "FixSig")
|
||||
Write-Host ("-" * 100)
|
||||
foreach ($issue in $results.Issues | Sort-Object IssueNumber) {
|
||||
$reviewMark = if ($issue.HasReview) { "✓" } else { "-" }
|
||||
$planMark = if ($issue.HasImplementationPlan) { "✓" } else { "-" }
|
||||
$wtMark = if ($issue.HasWorktree) { "✓" } else { "-" }
|
||||
$commitMark = if ($issue.HasCommits) { $issue.CommitCount } else { "-" }
|
||||
$prMark = if ($issue.HasPR) { "#$($issue.PRNumber) ($($issue.PRState))" } else { "-" }
|
||||
$reviewSignalMark = if ($issue.ReviewSignalStatus) { $issue.ReviewSignalStatus } else { "-" }
|
||||
$fixSignalMark = if ($issue.FixSignalStatus) { $issue.FixSignalStatus } else { "-" }
|
||||
|
||||
Write-Host ("{0,-8} {1,-8} {2,-8} {3,-5} {4,-5} {5,-10} {6,-8} {7,-8} {8,-8} {9,-8}" -f
|
||||
"#$($issue.IssueNumber)", $reviewMark, $planMark, $issue.FeasibilityScore, $issue.ClarityScore, $wtMark, $commitMark, $prMark, $reviewSignalMark, $fixSignalMark)
|
||||
}
|
||||
}
|
||||
|
||||
if ($results.PRs.Count -gt 0) {
|
||||
Write-Host "`n=== PR STATUS ===" -ForegroundColor Cyan
|
||||
Write-Host ("-" * 120)
|
||||
Write-Host ("{0,-8} {1,-10} {2,-10} {3,-8} {4,-8} {5,-10} {6,-12} {7,-10} {8,-8} {9,-8}" -f "PR", "State", "Issue", "Reviews", "High", "Medium", "Comments", "Unresolved", "RevSig", "FixSig")
|
||||
Write-Host ("-" * 120)
|
||||
foreach ($pr in $results.PRs | Sort-Object PRNumber) {
|
||||
$reviewMark = if ($pr.HasReviewFiles) { "$($pr.ReviewStepCount) steps" } else { "-" }
|
||||
$issueMark = if ($pr.IssueNumber -gt 0) { "#$($pr.IssueNumber)" } else { "-" }
|
||||
$reviewSignalMark = if ($pr.ReviewSignalStatus) { $pr.ReviewSignalStatus } else { "-" }
|
||||
$fixSignalMark = if ($pr.FixSignalStatus) { $pr.FixSignalStatus } else { "-" }
|
||||
|
||||
Write-Host ("{0,-8} {1,-10} {2,-10} {3,-8} {4,-8} {5,-10} {6,-12} {7,-10} {8,-8} {9,-8}" -f
|
||||
"#$($pr.PRNumber)", $pr.State, $issueMark, $reviewMark, $pr.HighSeverityCount, $pr.MediumSeverityCount, $pr.ActiveCommentCount, $pr.UnresolvedThreadCount, $reviewSignalMark, $fixSignalMark)
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`nTimestamp: $($results.Timestamp)" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
return $results
|
||||
227
.github/skills/pr-fix/SKILL.md
vendored
Normal file
227
.github/skills/pr-fix/SKILL.md
vendored
Normal file
@@ -0,0 +1,227 @@
|
||||
---
|
||||
name: pr-fix
|
||||
description: Fix active PR review comments and resolve threads. Use when asked to fix PR comments, address review feedback, resolve review threads, implement PR fixes, or handle review iterations. Works with VS Code MCP tools to resolve GitHub threads after fixes are applied.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# PR Fix Skill
|
||||
|
||||
Fix active pull request review comments and resolve threads. This skill handles the **fix** part of the PR review cycle, separate from the review itself.
|
||||
|
||||
## ⚠️ Critical Architecture
|
||||
|
||||
This skill requires **both** CLI scripts AND VS Code MCP tools:
|
||||
|
||||
| Operation | Execution Method |
|
||||
|-----------|------------------|
|
||||
| Apply code fixes | Copilot/Claude CLI via script |
|
||||
| Resolve review threads | **VS Code Agent** via `gh api graphql` |
|
||||
| Check status | Script (read-only) |
|
||||
|
||||
**WHY**: Copilot CLI's MCP is **read-only**. Only VS Code can resolve threads.
|
||||
|
||||
## Skill Contents
|
||||
|
||||
```
|
||||
.github/skills/pr-fix/
|
||||
├── SKILL.md # This file
|
||||
├── LICENSE.txt # MIT License
|
||||
├── references/
|
||||
│ ├── fix-pr-comments.prompt.md # AI prompt for fixing comments
|
||||
│ └── mcp-config.json # MCP configuration
|
||||
└── scripts/
|
||||
├── Start-PRFix.ps1 # Main fix script
|
||||
├── Start-PRFixParallel.ps1 # Parallel runner (single terminal)
|
||||
├── Resolve-PRThreads.ps1 # Resolve threads helper
|
||||
├── Get-UnresolvedThreads.ps1 # Get threads needing resolution
|
||||
└── IssueReviewLib.ps1 # Shared helpers
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
- **Code changes**: Applied in the PR's worktree
|
||||
- **Signal file**: `Generated Files/prFix/<pr>/.signal`
|
||||
|
||||
## Signal File
|
||||
|
||||
On completion, a `.signal` file is created for orchestrator coordination:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"prNumber": 45365,
|
||||
"timestamp": "2026-02-04T10:05:23Z",
|
||||
"unresolvedBefore": 3,
|
||||
"unresolvedAfter": 0
|
||||
}
|
||||
```
|
||||
|
||||
Status values: `success`, `partial` (some threads remain), `failure`
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Fix active review comments on a PR
|
||||
- Address reviewer feedback
|
||||
- Resolve review threads after fixing
|
||||
- Run the fix portion of review/fix loop
|
||||
- Implement changes requested in PR reviews
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- GitHub CLI (`gh`) installed and authenticated
|
||||
- Copilot CLI or Claude CLI installed
|
||||
- PowerShell 7+
|
||||
- PR has active review comments to fix
|
||||
|
||||
## Required Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `{{PRNumber}}` | Pull request number to fix | `45286` |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Check Unresolved Threads
|
||||
|
||||
```powershell
|
||||
# See what needs to be fixed
|
||||
.github/skills/pr-fix/scripts/Get-UnresolvedThreads.ps1 -PRNumber {{PRNumber}}
|
||||
```
|
||||
|
||||
### Step 2: Run Fix (CLI Script)
|
||||
|
||||
```powershell
|
||||
# Apply AI-generated fixes to address comments
|
||||
.github/skills/pr-fix/scripts/Start-PRFix.ps1 -PRNumber {{PRNumber}} -CLIType copilot -Force
|
||||
```
|
||||
|
||||
### Step 3: Resolve Threads (VS Code Agent)
|
||||
|
||||
After fixes are pushed, **you (the VS Code agent) must resolve threads**:
|
||||
|
||||
```powershell
|
||||
# Get unresolved thread IDs
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
repository(owner: "microsoft", name: "PowerToys") {
|
||||
pullRequest(number: {{PRNumber}}) {
|
||||
reviewThreads(first: 50) {
|
||||
nodes { id isResolved path line }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)'
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Resolve each thread
|
||||
gh api graphql -f query='
|
||||
mutation {
|
||||
resolveReviewThread(input: {threadId: "{{threadId}}"}) {
|
||||
thread { isResolved }
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
### Step 4: Verify All Resolved
|
||||
|
||||
```powershell
|
||||
# Confirm no unresolved threads remain
|
||||
.github/skills/pr-fix/scripts/Get-UnresolvedThreads.ps1 -PRNumber {{PRNumber}}
|
||||
```
|
||||
|
||||
## CLI Options
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `-PRNumber` | PR number to fix | Required |
|
||||
| `-CLIType` | AI CLI: `copilot` or `claude` | `copilot` |
|
||||
| `-Model` | Copilot model (e.g., `gpt-5.2-codex`) | (optional) |
|
||||
| `-Force` | Skip confirmation prompts | `false` |
|
||||
| `-DryRun` | Show what would be done | `false` |
|
||||
|
||||
## Review/Fix Loop Integration
|
||||
|
||||
This skill is typically used with `pr-review` in a loop:
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ pr-review │ ← Generate review, post comments
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ pr-fix │ ← Fix comments, resolve threads
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Check status │ ← Any threads unresolved?
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────┴────┐
|
||||
│ YES │ NO
|
||||
▼ ▼
|
||||
(loop) ✓ Done
|
||||
```
|
||||
|
||||
## VS Code Agent Operations
|
||||
|
||||
These operations **must** be done by the VS Code agent (not scripts):
|
||||
|
||||
| Operation | Method |
|
||||
|-----------|--------|
|
||||
| Resolve thread | `gh api graphql` with `resolveReviewThread` mutation |
|
||||
| Unresolve thread | `gh api graphql` with `unresolveReviewThread` mutation |
|
||||
|
||||
### Batch Resolve All Threads
|
||||
|
||||
```powershell
|
||||
# Get all unresolved thread IDs and resolve them
|
||||
$threads = gh api graphql -f query='
|
||||
query {
|
||||
repository(owner: "microsoft", name: "PowerToys") {
|
||||
pullRequest(number: {{PRNumber}}) {
|
||||
reviewThreads(first: 100) {
|
||||
nodes { id isResolved }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .id'
|
||||
|
||||
foreach ($threadId in $threads) {
|
||||
gh api graphql -f query="mutation { resolveReviewThread(input: {threadId: `"$threadId`"}) { thread { isResolved } } }"
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| "Cannot resolve thread" | Use VS Code agent, not Copilot CLI |
|
||||
| Fix not applied | Check worktree is on correct branch |
|
||||
| Thread ID not found | Re-fetch threads, ID may have changed |
|
||||
| Fix pushed but thread unresolved | Must explicitly resolve via GraphQL |
|
||||
|
||||
## Batch Processing Multiple PRs (CRITICAL)
|
||||
|
||||
**DO NOT spawn separate terminals for each PR.** Use the dedicated scripts:
|
||||
|
||||
```powershell
|
||||
# Run fixes in parallel (single terminal)
|
||||
.github/skills/pr-fix/scripts/Start-PRFixParallel.ps1 -PRNumbers 45256,45257,45285,45286 -CLIType copilot -ThrottleLimit 3 -Force
|
||||
|
||||
# Resolve threads (VS Code agent)
|
||||
.github/skills/pr-fix/scripts/Resolve-PRThreads.ps1 -PRNumber 45256
|
||||
```
|
||||
|
||||
## Related Skills
|
||||
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `pr-review` | Review PR, generate findings, post comments |
|
||||
| `issue-fix` | Fix issues and create PRs |
|
||||
| `issue-to-pr-cycle` | Full orchestration |
|
||||
29
.github/skills/pr-fix/scripts/Resolve-PRThreads.ps1
vendored
Normal file
29
.github/skills/pr-fix/scripts/Resolve-PRThreads.ps1
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolve all unresolved review threads for a PR.
|
||||
|
||||
.PARAMETER PRNumber
|
||||
PR number to resolve.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$PRNumber
|
||||
)
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')
|
||||
Set-Location $repoRoot
|
||||
|
||||
$threads = gh api graphql -f query="query { repository(owner:\"microsoft\",name:\"PowerToys\") { pullRequest(number:$PRNumber) { reviewThreads(first:100) { nodes { id isResolved } } } } }" --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | .id'
|
||||
|
||||
foreach ($threadId in $threads) {
|
||||
gh api graphql -f query="mutation { resolveReviewThread(input:{threadId:\"$threadId\"}) { thread { isResolved } } }" | Out-Null
|
||||
}
|
||||
|
||||
$threadsAfter = gh api graphql -f query="query { repository(owner:\"microsoft\",name:\"PowerToys\") { pullRequest(number:$PRNumber) { reviewThreads(first:100) { nodes { id isResolved } } } } }" --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | .id'
|
||||
|
||||
if ($threadsAfter) {
|
||||
Write-Warning "Unresolved threads remain for PR #$PRNumber"
|
||||
} else {
|
||||
Write-Host "All threads resolved for PR #$PRNumber"
|
||||
}
|
||||
298
.github/skills/pr-fix/scripts/Start-PRFix.ps1
vendored
Normal file
298
.github/skills/pr-fix/scripts/Start-PRFix.ps1
vendored
Normal file
@@ -0,0 +1,298 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Fix active PR review comments using AI CLI.
|
||||
|
||||
.DESCRIPTION
|
||||
Kicks off Copilot/Claude CLI to address active review comments on a PR.
|
||||
Does NOT resolve threads - that must be done by VS Code agent via GraphQL.
|
||||
|
||||
.PARAMETER PRNumber
|
||||
PR number to fix.
|
||||
|
||||
.PARAMETER CLIType
|
||||
AI CLI to use: copilot or claude. Default: copilot.
|
||||
|
||||
.PARAMETER Model
|
||||
Copilot CLI model to use (e.g., gpt-5.2-codex).
|
||||
|
||||
.PARAMETER WorktreePath
|
||||
Path to the worktree containing the PR branch. Auto-detected if not specified.
|
||||
|
||||
.PARAMETER DryRun
|
||||
Show what would be done without executing.
|
||||
|
||||
.PARAMETER Force
|
||||
Skip confirmation prompts.
|
||||
|
||||
.EXAMPLE
|
||||
./Start-PRFix.ps1 -PRNumber 45286 -CLIType copilot -Force
|
||||
|
||||
.NOTES
|
||||
After this script completes, use VS Code agent to resolve threads via GraphQL.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$PRNumber,
|
||||
|
||||
[ValidateSet('copilot', 'claude')]
|
||||
[string]$CLIType = 'copilot',
|
||||
|
||||
[string]$Model,
|
||||
|
||||
[string]$WorktreePath,
|
||||
|
||||
[switch]$DryRun,
|
||||
|
||||
[switch]$Force,
|
||||
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. (Join-Path $scriptDir 'IssueReviewLib.ps1')
|
||||
|
||||
$repoRoot = Get-RepoRoot
|
||||
$worktreeLib = Join-Path $repoRoot 'tools/build/WorktreeLib.ps1'
|
||||
if (Test-Path $worktreeLib) {
|
||||
. $worktreeLib
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Get-Help $MyInvocation.MyCommand.Path -Full
|
||||
return
|
||||
}
|
||||
|
||||
function Get-PRBranch {
|
||||
param([int]$PRNumber)
|
||||
|
||||
$prInfo = gh pr view $PRNumber --json headRefName 2>$null | ConvertFrom-Json
|
||||
if ($prInfo) {
|
||||
return $prInfo.headRefName
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-WorktreeForPR {
|
||||
param([int]$PRNumber)
|
||||
|
||||
$branch = Get-PRBranch -PRNumber $PRNumber
|
||||
if (-not $branch) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$worktrees = Get-WorktreeEntries
|
||||
$wt = $worktrees | Where-Object { $_.Branch -eq $branch } | Select-Object -First 1
|
||||
|
||||
if ($wt) {
|
||||
return $wt.Path
|
||||
}
|
||||
|
||||
# If no dedicated worktree, check if we're on that branch in main repo
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
$currentBranch = git branch --show-current 2>$null
|
||||
if ($currentBranch -eq $branch) {
|
||||
return $repoRoot
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-ActiveComments {
|
||||
param([int]$PRNumber)
|
||||
|
||||
try {
|
||||
$comments = gh api "repos/microsoft/PowerToys/pulls/$PRNumber/comments" 2>$null | ConvertFrom-Json
|
||||
# Filter to root comments (not replies)
|
||||
$rootComments = $comments | Where-Object { $null -eq $_.in_reply_to_id }
|
||||
return $rootComments
|
||||
}
|
||||
catch {
|
||||
return @()
|
||||
}
|
||||
}
|
||||
|
||||
function Get-UnresolvedThreadCount {
|
||||
param([int]$PRNumber)
|
||||
|
||||
try {
|
||||
$result = gh api graphql -f query="query { repository(owner: `"microsoft`", name: `"PowerToys`") { pullRequest(number: $PRNumber) { reviewThreads(first: 100) { nodes { isResolved } } } } }" 2>$null | ConvertFrom-Json
|
||||
$threads = $result.data.repository.pullRequest.reviewThreads.nodes
|
||||
$unresolved = $threads | Where-Object { -not $_.isResolved }
|
||||
return @($unresolved).Count
|
||||
}
|
||||
catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
#region Main
|
||||
try {
|
||||
Info "=" * 60
|
||||
Info "PR FIX - PR #$PRNumber"
|
||||
Info "=" * 60
|
||||
|
||||
# Get PR info
|
||||
$prInfo = gh pr view $PRNumber --json state,headRefName,url 2>$null | ConvertFrom-Json
|
||||
if (-not $prInfo) {
|
||||
throw "PR #$PRNumber not found"
|
||||
}
|
||||
|
||||
if ($prInfo.state -ne 'OPEN') {
|
||||
Warn "PR #$PRNumber is $($prInfo.state), not OPEN"
|
||||
return
|
||||
}
|
||||
|
||||
Info "PR URL: $($prInfo.url)"
|
||||
Info "Branch: $($prInfo.headRefName)"
|
||||
Info "CLI: $CLIType"
|
||||
|
||||
# Find worktree
|
||||
if (-not $WorktreePath) {
|
||||
$WorktreePath = Find-WorktreeForPR -PRNumber $PRNumber
|
||||
}
|
||||
|
||||
if (-not $WorktreePath -or -not (Test-Path $WorktreePath)) {
|
||||
Warn "No worktree found for PR #$PRNumber"
|
||||
Warn "Using main repo root. Make sure the PR branch is checked out."
|
||||
$WorktreePath = $repoRoot
|
||||
}
|
||||
|
||||
Info "Working directory: $WorktreePath"
|
||||
|
||||
# Check for active comments
|
||||
$comments = Get-ActiveComments -PRNumber $PRNumber
|
||||
$unresolvedCount = Get-UnresolvedThreadCount -PRNumber $PRNumber
|
||||
|
||||
Info ""
|
||||
Info "Active review comments: $($comments.Count)"
|
||||
Info "Unresolved threads: $unresolvedCount"
|
||||
|
||||
if ($comments.Count -eq 0 -and $unresolvedCount -eq 0) {
|
||||
Success "No active comments or unresolved threads to fix!"
|
||||
return @{ PRNumber = $PRNumber; Status = 'NothingToFix' }
|
||||
}
|
||||
|
||||
if ($DryRun) {
|
||||
Info ""
|
||||
Warn "[DRY RUN] Would run AI CLI to fix comments"
|
||||
Info "Comments to address:"
|
||||
foreach ($c in $comments | Select-Object -First 5) {
|
||||
Info " - $($c.path):$($c.line) - $($c.body.Substring(0, [Math]::Min(80, $c.body.Length)))..."
|
||||
}
|
||||
return @{ PRNumber = $PRNumber; Status = 'DryRun' }
|
||||
}
|
||||
|
||||
# Confirm
|
||||
if (-not $Force) {
|
||||
$confirm = Read-Host "Fix $($comments.Count) comments on PR #$PRNumber? (y/N)"
|
||||
if ($confirm -notmatch '^[yY]') {
|
||||
Info "Cancelled."
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# Build prompt
|
||||
$prompt = @"
|
||||
You are fixing review comments on PR #$PRNumber.
|
||||
|
||||
Read the active review comments using GitHub tools and address each one:
|
||||
1. Fetch the PR review comments
|
||||
2. For each comment, understand what change is requested
|
||||
3. Make the code changes to address the feedback
|
||||
4. Build and verify your changes work
|
||||
|
||||
Focus on the reviewer's feedback and make targeted fixes.
|
||||
"@
|
||||
|
||||
# MCP config
|
||||
$mcpConfig = '@.github/skills/pr-fix/references/mcp-config.json'
|
||||
|
||||
Info ""
|
||||
Info "Starting AI fix..."
|
||||
|
||||
Push-Location $WorktreePath
|
||||
try {
|
||||
switch ($CLIType) {
|
||||
'copilot' {
|
||||
$copilotArgs = @('--additional-mcp-config', $mcpConfig, '-p', $prompt, '--yolo')
|
||||
if ($Model) {
|
||||
$copilotArgs += @('--model', $Model)
|
||||
}
|
||||
$output = & copilot @copilotArgs 2>&1
|
||||
# Log output
|
||||
$logPath = Join-Path $repoRoot "Generated Files/prReview/$PRNumber"
|
||||
if (-not (Test-Path $logPath)) {
|
||||
New-Item -ItemType Directory -Path $logPath -Force | Out-Null
|
||||
}
|
||||
$output | Out-File -FilePath (Join-Path $logPath "_fix.log") -Force
|
||||
}
|
||||
'claude' {
|
||||
$output = & claude --print --dangerously-skip-permissions --prompt $prompt 2>&1
|
||||
$logPath = Join-Path $repoRoot "Generated Files/prReview/$PRNumber"
|
||||
if (-not (Test-Path $logPath)) {
|
||||
New-Item -ItemType Directory -Path $logPath -Force | Out-Null
|
||||
}
|
||||
$output | Out-File -FilePath (Join-Path $logPath "_fix.log") -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
# Check results
|
||||
$newUnresolvedCount = Get-UnresolvedThreadCount -PRNumber $PRNumber
|
||||
|
||||
Info ""
|
||||
Info "Fix complete."
|
||||
Info "Unresolved threads before: $unresolvedCount"
|
||||
Info "Unresolved threads after: $newUnresolvedCount"
|
||||
|
||||
if ($newUnresolvedCount -gt 0) {
|
||||
Warn ""
|
||||
Warn "⚠️ $newUnresolvedCount threads still unresolved."
|
||||
Warn "Use VS Code agent to resolve them via GraphQL:"
|
||||
Warn " gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: \"THREAD_ID\"}) { thread { isResolved } } }'"
|
||||
}
|
||||
else {
|
||||
Success "✓ All threads resolved!"
|
||||
}
|
||||
|
||||
# Write signal file
|
||||
$signalDir = Join-Path $repoRoot "Generated Files/prFix/$PRNumber"
|
||||
if (-not (Test-Path $signalDir)) { New-Item -ItemType Directory -Path $signalDir -Force | Out-Null }
|
||||
@{
|
||||
status = if ($newUnresolvedCount -eq 0) { "success" } else { "partial" }
|
||||
prNumber = $PRNumber
|
||||
timestamp = (Get-Date).ToString("o")
|
||||
unresolvedBefore = $unresolvedCount
|
||||
unresolvedAfter = $newUnresolvedCount
|
||||
} | ConvertTo-Json | Set-Content "$signalDir/.signal" -Force
|
||||
|
||||
return @{
|
||||
PRNumber = $PRNumber
|
||||
Status = 'FixApplied'
|
||||
UnresolvedBefore = $unresolvedCount
|
||||
UnresolvedAfter = $newUnresolvedCount
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
|
||||
# Write failure signal
|
||||
$signalDir = Join-Path $repoRoot "Generated Files/prFix/$PRNumber"
|
||||
if (-not (Test-Path $signalDir)) { New-Item -ItemType Directory -Path $signalDir -Force | Out-Null }
|
||||
@{
|
||||
status = "failure"
|
||||
prNumber = $PRNumber
|
||||
timestamp = (Get-Date).ToString("o")
|
||||
error = $_.Exception.Message
|
||||
} | ConvertTo-Json | Set-Content "$signalDir/.signal" -Force
|
||||
|
||||
86
.github/skills/pr-fix/scripts/Start-PRFixParallel.ps1
vendored
Normal file
86
.github/skills/pr-fix/scripts/Start-PRFixParallel.ps1
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Run pr-fix in parallel from a single terminal.
|
||||
|
||||
.PARAMETER PRNumbers
|
||||
PR numbers to fix.
|
||||
|
||||
.PARAMETER ThrottleLimit
|
||||
Maximum parallel tasks.
|
||||
|
||||
.PARAMETER CLIType
|
||||
AI CLI type (copilot/claude).
|
||||
|
||||
.PARAMETER Model
|
||||
Copilot CLI model to use (e.g., gpt-5.2-codex).
|
||||
|
||||
.PARAMETER Force
|
||||
Skip confirmation prompts in Start-PRFix.ps1.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int[]]$PRNumbers,
|
||||
|
||||
[int]$ThrottleLimit = 3,
|
||||
|
||||
[ValidateSet('claude', 'copilot')]
|
||||
[string]$CLIType = 'copilot',
|
||||
|
||||
[string]$Model,
|
||||
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')
|
||||
$scriptPath = Join-Path $repoRoot '.github\skills\pr-fix\scripts\Start-PRFix.ps1'
|
||||
|
||||
$results = $PRNumbers | ForEach-Object -Parallel {
|
||||
param($pr)
|
||||
|
||||
$repoRoot = $using:repoRoot
|
||||
$scriptPath = $using:scriptPath
|
||||
$cliType = $using:CLIType
|
||||
$model = $using:Model
|
||||
$force = $using:Force
|
||||
|
||||
Set-Location $repoRoot
|
||||
|
||||
$branch = (gh pr view $pr --json headRefName -q .headRefName)
|
||||
$worktree = (git worktree list | Select-String $branch | ForEach-Object { ($_ -split '\s+')[0] })
|
||||
|
||||
if (-not $worktree) {
|
||||
return [pscustomobject]@{
|
||||
PRNumber = $pr
|
||||
ExitCode = 1
|
||||
Error = "Worktree not found for branch $branch"
|
||||
}
|
||||
}
|
||||
|
||||
Set-Location $worktree
|
||||
|
||||
$args = @('-PRNumber', $pr, '-CLIType', $cliType)
|
||||
if ($model) {
|
||||
$args += @('-Model', $model)
|
||||
}
|
||||
if ($force) {
|
||||
$args += '-Force'
|
||||
}
|
||||
|
||||
try {
|
||||
& $scriptPath @args | Out-Default
|
||||
[pscustomobject]@{
|
||||
PRNumber = $pr
|
||||
ExitCode = $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
catch {
|
||||
[pscustomobject]@{
|
||||
PRNumber = $pr
|
||||
ExitCode = 1
|
||||
Error = $_.Exception.Message
|
||||
}
|
||||
}
|
||||
} -ThrottleLimit $ThrottleLimit
|
||||
|
||||
$results
|
||||
292
.github/skills/pr-review/SKILL.md
vendored
Normal file
292
.github/skills/pr-review/SKILL.md
vendored
Normal file
@@ -0,0 +1,292 @@
|
||||
---
|
||||
name: pr-review
|
||||
description: Comprehensive pull request review with multi-step analysis and comment posting. Use when asked to review a PR, analyze pull request changes, check PR for issues, post review comments, validate PR quality, run code review on a PR, or audit pull request. Generates 13 review step files covering functionality, security, performance, accessibility, and more. For FIXING PR comments, use the pr-fix skill instead.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# PR Review Skill
|
||||
|
||||
Perform comprehensive pull request reviews with multi-step analysis covering functionality, security, performance, accessibility, localization, and more.
|
||||
|
||||
**Note**: This skill is for **reviewing** PRs only. To **fix** review comments, use the `pr-fix` skill.
|
||||
|
||||
## Critical Guidelines
|
||||
|
||||
### Load-on-Demand Architecture
|
||||
Step prompt files are loaded **only when that step is executed** to minimize context usage:
|
||||
- Read `references/review-pr.prompt.md` first for orchestration
|
||||
- Load each `references/0X-*.prompt.md` only when executing that step
|
||||
- Skip steps based on smart filtering (see review-pr.prompt.md)
|
||||
|
||||
### Mandatory External Reference Research
|
||||
**Each step prompt includes an `## External references (MUST research)` section.** Before completing any step, you **MUST**:
|
||||
|
||||
1. **Fetch the referenced URLs** using `fetch_webpage` or equivalent
|
||||
2. **Analyze PR changes against those authoritative sources**
|
||||
3. **Include a `## References consulted` section** in the output file listing:
|
||||
- Which guidelines were checked
|
||||
- Any violations found with specific IDs (e.g., WCAG 1.4.3, OWASP A03, CWE-79)
|
||||
|
||||
| Step | Key External References |
|
||||
|------|------------------------|
|
||||
| 04 Accessibility | WCAG 2.1, Windows Accessibility Guidelines |
|
||||
| 05 Security | OWASP Top 10, CWE Top 25, Microsoft SDL |
|
||||
| 06 Localization | .NET Localization, Microsoft Style Guide |
|
||||
| 07 Globalization | Unicode TR9 (BiDi), ICU Guidelines |
|
||||
| 09 SOLID Design | .NET Architecture Guidelines, Design Patterns |
|
||||
|
||||
**Failure to research external references is a review quality violation.**
|
||||
|
||||
## Skill Contents
|
||||
|
||||
This skill is **self-contained** with all required resources:
|
||||
|
||||
```
|
||||
.github/skills/pr-review/
|
||||
├── SKILL.md # This file
|
||||
├── LICENSE.txt # MIT License
|
||||
├── scripts/
|
||||
│ ├── Start-PRReviewWorkflow.ps1 # Main review script
|
||||
│ ├── Post-ReviewComments.ps1 # Post comments to GitHub
|
||||
│ ├── Get-GitHubPrFilePatch.ps1 # Fetch PR file diffs
|
||||
│ ├── Get-GitHubRawFile.ps1 # Download repo files
|
||||
│ ├── Get-PrIncrementalChanges.ps1 # Detect incremental changes
|
||||
│ └── Test-IncrementalReview.ps1 # Test incremental detection
|
||||
└── references/
|
||||
├── review-pr.prompt.md # Orchestration prompt (load first)
|
||||
├── 01-functionality.prompt.md # Step 01 detailed checks
|
||||
├── 02-compatibility.prompt.md # Step 02 detailed checks
|
||||
├── 03-performance.prompt.md # Step 03 detailed checks
|
||||
├── 04-accessibility.prompt.md # Step 04 detailed checks
|
||||
├── 05-security.prompt.md # Step 05 detailed checks
|
||||
├── 06-localization.prompt.md # Step 06 detailed checks
|
||||
├── 07-globalization.prompt.md # Step 07 detailed checks
|
||||
├── 08-extensibility.prompt.md # Step 08 detailed checks
|
||||
├── 09-solid-design.prompt.md # Step 09 detailed checks
|
||||
├── 10-repo-patterns.prompt.md # Step 10 detailed checks
|
||||
├── 11-docs-automation.prompt.md # Step 11 detailed checks
|
||||
├── 12-code-comments.prompt.md # Step 12 detailed checks
|
||||
└── 13-copilot-guidance.prompt.md # Step 13 (conditional)
|
||||
```
|
||||
|
||||
## Output Directory
|
||||
|
||||
All generated artifacts are placed under `Generated Files/prReview/<pr-number>/` at the repository root (gitignored).
|
||||
|
||||
```
|
||||
Generated Files/prReview/
|
||||
└── <pr-number>/
|
||||
├── 00-OVERVIEW.md # Summary with all findings
|
||||
├── 01-functionality.md # Functional correctness
|
||||
├── 02-compatibility.md # Breaking changes, versioning
|
||||
├── 03-performance.md # Performance implications
|
||||
├── 04-accessibility.md # A11y compliance
|
||||
├── 05-security.md # Security concerns
|
||||
├── 06-localization.md # L10n readiness
|
||||
├── 07-globalization.md # G11n considerations
|
||||
├── 08-extensibility.md # API/extension points
|
||||
├── 09-solid-design.md # SOLID principles
|
||||
├── 10-repo-patterns.md # PowerToys conventions
|
||||
├── 11-docs-automation.md # Documentation coverage
|
||||
├── 12-code-comments.md # Code comment quality
|
||||
├── 13-copilot-guidance.md # (if applicable)
|
||||
└── .signal # Completion signal for orchestrator
|
||||
```
|
||||
|
||||
## Signal File
|
||||
|
||||
On completion, a `.signal` file is created for orchestrator coordination:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"prNumber": 45365,
|
||||
"timestamp": "2026-02-04T10:05:23Z"
|
||||
}
|
||||
```
|
||||
|
||||
Status values: `success`, `failure`
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Review a specific pull request
|
||||
- Analyze PR changes for quality issues
|
||||
- Post review comments on a PR
|
||||
- Validate PR against PowerToys standards
|
||||
- Run comprehensive code review
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- GitHub CLI (`gh`) installed and authenticated
|
||||
- PowerShell 7+ for running scripts
|
||||
- GitHub MCP configured (for posting comments)
|
||||
|
||||
## Required Variables
|
||||
|
||||
⚠️ **For single PR review**, confirm `{{PRNumber}}` with the user. For batch modes, see "Batch Review Modes" below.
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `{{PRNumber}}` | Pull request number to review | `45234` |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Single PR Review
|
||||
|
||||
Execute the review workflow for a specific PR:
|
||||
|
||||
```powershell
|
||||
# From repo root
|
||||
.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 -PRNumbers {{PRNumber}} -CLIType copilot -SkipAssign -SkipFix -Force
|
||||
```
|
||||
|
||||
### Batch Review Modes
|
||||
|
||||
Review multiple PRs with a single command:
|
||||
|
||||
```powershell
|
||||
# Review ALL open non-draft PRs in the repository
|
||||
.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 -AllOpen -SkipAssign -SkipFix -Force
|
||||
|
||||
# Review only PRs assigned to me
|
||||
.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 -Assigned -SkipAssign -SkipFix -Force
|
||||
|
||||
# Review ALL open PRs, skip those already reviewed
|
||||
.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 -AllOpen -SkipExisting -SkipAssign -SkipFix -Force
|
||||
|
||||
# Limit batch size
|
||||
.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 -AllOpen -Limit 50 -SkipExisting -Force
|
||||
```
|
||||
|
||||
### Background Batch Review (Recommended for Large Batches)
|
||||
|
||||
For reviewing many PRs, generate a standalone batch script and run it in background:
|
||||
|
||||
```powershell
|
||||
# Step 1: Generate the batch script
|
||||
.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 -AllOpen -SkipExisting -GenerateBatchScript -Force
|
||||
|
||||
# Step 2: Run in background (minimized window)
|
||||
Start-Process pwsh -ArgumentList '-File', 'Generated Files/prReview/_batch-review.ps1' -WindowStyle Minimized
|
||||
|
||||
# Or run interactively to see progress
|
||||
pwsh -File "Generated Files/prReview/_batch-review.ps1"
|
||||
```
|
||||
|
||||
The batch script:
|
||||
- Processes PRs sequentially (more reliable than parallel)
|
||||
- Skips already-reviewed PRs automatically
|
||||
- Shows progress as `[N/Total] PR #XXXXX`
|
||||
- Logs copilot output to `_copilot.log` in each PR folder
|
||||
- Reports failed PRs at the end
|
||||
|
||||
### Step 2: Review Output
|
||||
|
||||
Check the generated files at `Generated Files/prReview/{{PRNumber}}/`
|
||||
|
||||
## CLI Options
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `-PRNumbers` | PR number(s) to review | From worktrees |
|
||||
| `-AllOpen` | Review ALL open non-draft PRs | `false` |
|
||||
| `-Assigned` | Review PRs assigned to current user | `false` |
|
||||
| `-Limit` | Max PRs to fetch for batch modes | `100` |
|
||||
| `-SkipExisting` | Skip PRs with completed reviews | `false` |
|
||||
| `-GenerateBatchScript` | Generate standalone script for background execution | `false` |
|
||||
| `-CLIType` | AI CLI to use: `copilot` or `claude` | `copilot` |
|
||||
| `-Model` | Copilot model (e.g., `gpt-5.2-codex`) | (optional) |
|
||||
| `-MinSeverity` | Min severity to post: `high`, `medium`, `low`, `info` | `medium` |
|
||||
| `-SkipAssign` | Skip assigning Copilot as reviewer | `false` |
|
||||
| `-SkipReview` | Skip the review step | `false` |
|
||||
| `-SkipFix` | Skip fix step (recommended - use `pr-fix` skill instead) | `false` |
|
||||
| `-MaxParallel` | Maximum parallel jobs | `3` |
|
||||
| `-Force` | Skip confirmation prompts | `false` |
|
||||
|
||||
**Note**: The `-SkipFix` option is kept for backward compatibility. For fixing PR comments, use the dedicated `pr-fix` skill which provides better control over the fix/resolve loop.
|
||||
|
||||
## AI Prompt References
|
||||
|
||||
### Orchestration (load first)
|
||||
- `references/review-pr.prompt.md` - Main orchestration with PR selection, iteration management, smart filtering
|
||||
|
||||
### Step Prompts (load on-demand per step)
|
||||
Each step prompt contains:
|
||||
- Detailed checklist of concerns (15-25 items)
|
||||
- PowerToys-specific checks
|
||||
- Severity guidelines
|
||||
- Output file template
|
||||
- **External references (MUST research)** section
|
||||
|
||||
| Step | Prompt File | External References |
|
||||
|------|-------------|---------------------|
|
||||
| 01 | `01-functionality.prompt.md` | C# Guidelines, .NET API Design |
|
||||
| 02 | `02-compatibility.prompt.md` | Windows Versions, .NET Breaking Changes |
|
||||
| 03 | `03-performance.prompt.md` | .NET Performance, Async Best Practices |
|
||||
| 04 | `04-accessibility.prompt.md` | **WCAG 2.1**, Windows Accessibility |
|
||||
| 05 | `05-security.prompt.md` | **OWASP Top 10**, **CWE Top 25**, SDL |
|
||||
| 06 | `06-localization.prompt.md` | .NET Localization, MS Style Guide |
|
||||
| 07 | `07-globalization.prompt.md` | Unicode BiDi, ICU, Date/Time Formatting |
|
||||
| 08 | `08-extensibility.prompt.md` | Plugin Architecture, SemVer |
|
||||
| 09 | `09-solid-design.prompt.md` | SOLID Principles, Clean Architecture |
|
||||
| 10 | `10-repo-patterns.prompt.md` | PowerToys docs (architecture, style, logging) |
|
||||
| 11 | `11-docs-automation.prompt.md` | MS Writing Style, XML Docs |
|
||||
| 12 | `12-code-comments.prompt.md` | XML Documentation, Comment Conventions |
|
||||
| 13 | `13-copilot-guidance.prompt.md` | Agent Skills Spec, Prompt Engineering |
|
||||
|
||||
### Fix Prompt
|
||||
- `references/fix-pr-active-comments.prompt.md` - Address active review comments
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| PR not found | Verify PR number: `gh pr view {{PRNumber}}` |
|
||||
| Review incomplete | Check `_copilot-review.log` for errors |
|
||||
| Comments not posted | Use VS Code MCP tools (Copilot CLI is read-only) |
|
||||
| Missing `## References consulted` | Re-run step with external reference research |
|
||||
| Cannot resolve comments | Use `gh api graphql` with resolveReviewThread mutation |
|
||||
|
||||
## ⚠️ VS Code Agent Operations
|
||||
|
||||
**Copilot CLI's MCP is read-only.** These operations require VS Code MCP tools:
|
||||
|
||||
| Operation | VS Code MCP Tool |
|
||||
|-----------|------------------|
|
||||
| Assign Copilot reviewer | `mcp_github_request_copilot_review` |
|
||||
| Post review comments | `mcp_github_pull_request_review_write` |
|
||||
| Add line-specific comments | `mcp_github_add_comment_to_pending_review` |
|
||||
| Resolve threads | `gh api graphql` with `resolveReviewThread` |
|
||||
|
||||
### Resolve Review Thread Example
|
||||
|
||||
```powershell
|
||||
# Get unresolved threads
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
repository(owner: "microsoft", name: "PowerToys") {
|
||||
pullRequest(number: {{PRNumber}}) {
|
||||
reviewThreads(first: 50) {
|
||||
nodes { id isResolved path line }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)'
|
||||
|
||||
# Resolve a specific thread
|
||||
gh api graphql -f query='
|
||||
mutation {
|
||||
resolveReviewThread(input: {threadId: "{{threadId}}"}) {
|
||||
thread { isResolved }
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
## Related Skills
|
||||
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `pr-fix` | Fix review comments after this skill identifies issues |
|
||||
| `issue-to-pr-cycle` | Full orchestration (review → fix loop) |
|
||||
807
.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1
vendored
Normal file
807
.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1
vendored
Normal file
@@ -0,0 +1,807 @@
|
||||
<#!
|
||||
.SYNOPSIS
|
||||
Review and fix PRs in parallel using GitHub Copilot and MCP.
|
||||
|
||||
.DESCRIPTION
|
||||
For each PR (from worktrees, specified, or fetched from repo), runs:
|
||||
1. Assigns GitHub Copilot as reviewer via GitHub MCP
|
||||
2. Runs review-pr.prompt.md to generate review and post comments
|
||||
3. Runs fix-pr-active-comments.prompt.md to fix issues
|
||||
|
||||
.PARAMETER PRNumbers
|
||||
Array of PR numbers to process. If not specified, finds PRs from issue worktrees.
|
||||
|
||||
.PARAMETER AllOpen
|
||||
Fetch and process ALL open non-draft PRs from the repository.
|
||||
|
||||
.PARAMETER Assigned
|
||||
Fetch and process PRs assigned to the current user.
|
||||
|
||||
.PARAMETER Limit
|
||||
Maximum number of PRs to fetch when using -AllOpen or -Assigned. Default: 100.
|
||||
|
||||
.PARAMETER SkipExisting
|
||||
Skip PRs that already have a completed review (00-OVERVIEW.md exists).
|
||||
|
||||
.PARAMETER SkipAssign
|
||||
Skip assigning Copilot as reviewer.
|
||||
|
||||
.PARAMETER SkipReview
|
||||
Skip the review step.
|
||||
|
||||
.PARAMETER SkipFix
|
||||
Skip the fix step.
|
||||
|
||||
.PARAMETER MinSeverity
|
||||
Minimum severity to post as PR comments: high, medium, low, info. Default: medium.
|
||||
|
||||
.PARAMETER MaxParallel
|
||||
Maximum parallel jobs. Default: 3.
|
||||
|
||||
.PARAMETER DryRun
|
||||
Show what would be done without executing.
|
||||
|
||||
.PARAMETER GenerateBatchScript
|
||||
Instead of running reviews, generate a standalone batch script that can be run
|
||||
in background. The script will be saved to Generated Files/prReview/_batch-review.ps1.
|
||||
|
||||
.PARAMETER CLIType
|
||||
AI CLI to use: copilot or claude. Default: copilot.
|
||||
|
||||
.PARAMETER Model
|
||||
Copilot CLI model to use (e.g., gpt-5.2-codex).
|
||||
|
||||
.EXAMPLE
|
||||
# Process all PRs from issue worktrees
|
||||
./Start-PRReviewWorkflow.ps1
|
||||
|
||||
.EXAMPLE
|
||||
# Process specific PRs
|
||||
./Start-PRReviewWorkflow.ps1 -PRNumbers 45234, 45235
|
||||
|
||||
.EXAMPLE
|
||||
# Review ALL open PRs in the repo
|
||||
./Start-PRReviewWorkflow.ps1 -AllOpen -SkipFix -SkipAssign
|
||||
|
||||
.EXAMPLE
|
||||
# Review PRs assigned to me, skip already reviewed
|
||||
./Start-PRReviewWorkflow.ps1 -Assigned -SkipExisting
|
||||
|
||||
.EXAMPLE
|
||||
# Generate a batch script for background execution
|
||||
./Start-PRReviewWorkflow.ps1 -AllOpen -SkipExisting -GenerateBatchScript
|
||||
|
||||
.EXAMPLE
|
||||
# Only review, don't fix
|
||||
./Start-PRReviewWorkflow.ps1 -SkipFix
|
||||
|
||||
.EXAMPLE
|
||||
# Dry run
|
||||
./Start-PRReviewWorkflow.ps1 -DryRun
|
||||
|
||||
.NOTES
|
||||
Prerequisites:
|
||||
- GitHub CLI (gh) authenticated
|
||||
- Copilot CLI installed
|
||||
- GitHub MCP configured for posting comments
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int[]]$PRNumbers,
|
||||
|
||||
[switch]$AllOpen,
|
||||
|
||||
[switch]$Assigned,
|
||||
|
||||
[int]$Limit = 100,
|
||||
|
||||
[switch]$SkipExisting,
|
||||
|
||||
[switch]$SkipAssign,
|
||||
|
||||
[switch]$SkipReview,
|
||||
|
||||
[switch]$SkipFix,
|
||||
|
||||
[ValidateSet('high', 'medium', 'low', 'info')]
|
||||
[string]$MinSeverity = 'medium',
|
||||
|
||||
[int]$MaxParallel = 3,
|
||||
|
||||
[switch]$DryRun,
|
||||
|
||||
[switch]$GenerateBatchScript,
|
||||
|
||||
[ValidateSet('copilot', 'claude')]
|
||||
[string]$CLIType = 'copilot',
|
||||
|
||||
[string]$Model,
|
||||
|
||||
[switch]$Force,
|
||||
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
# Load libraries
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$scriptDir/IssueReviewLib.ps1"
|
||||
|
||||
# Load worktree library
|
||||
$repoRoot = Get-RepoRoot
|
||||
$worktreeLib = Join-Path $repoRoot 'tools/build/WorktreeLib.ps1'
|
||||
if (Test-Path $worktreeLib) {
|
||||
. $worktreeLib
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Get-Help $MyInvocation.MyCommand.Path -Full
|
||||
return
|
||||
}
|
||||
|
||||
function Get-AllOpenPRs {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get all open non-draft PRs from the repository.
|
||||
#>
|
||||
param(
|
||||
[int]$Limit = 100
|
||||
)
|
||||
|
||||
Info "Fetching all open PRs (limit: $Limit)..."
|
||||
$prList = gh pr list --state open --json number,url,headRefName,isDraft --limit $Limit 2>$null | ConvertFrom-Json
|
||||
|
||||
if (-not $prList) {
|
||||
return @()
|
||||
}
|
||||
|
||||
# Filter out drafts
|
||||
$prs = @()
|
||||
foreach ($pr in $prList | Where-Object { -not $_.isDraft }) {
|
||||
$prs += @{
|
||||
PRNumber = $pr.number
|
||||
PRUrl = $pr.url
|
||||
Branch = $pr.headRefName
|
||||
WorktreePath = $repoRoot
|
||||
}
|
||||
}
|
||||
|
||||
Info "Found $($prs.Count) non-draft open PRs"
|
||||
return $prs
|
||||
}
|
||||
|
||||
function Get-AssignedPRs {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get PRs assigned to the current user.
|
||||
#>
|
||||
param(
|
||||
[int]$Limit = 100
|
||||
)
|
||||
|
||||
Info "Fetching PRs assigned to @me (limit: $Limit)..."
|
||||
$prList = gh pr list --assignee @me --state open --json number,url,headRefName,isDraft --limit $Limit 2>$null | ConvertFrom-Json
|
||||
|
||||
if (-not $prList) {
|
||||
return @()
|
||||
}
|
||||
|
||||
$prs = @()
|
||||
foreach ($pr in $prList | Where-Object { -not $_.isDraft }) {
|
||||
$prs += @{
|
||||
PRNumber = $pr.number
|
||||
PRUrl = $pr.url
|
||||
Branch = $pr.headRefName
|
||||
WorktreePath = $repoRoot
|
||||
}
|
||||
}
|
||||
|
||||
Info "Found $($prs.Count) assigned PRs"
|
||||
return $prs
|
||||
}
|
||||
|
||||
function Test-ReviewExists {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check if a PR review already exists (has 00-OVERVIEW.md).
|
||||
#>
|
||||
param(
|
||||
[int]$PRNumber
|
||||
)
|
||||
|
||||
$reviewPath = Join-Path $repoRoot "Generated Files/prReview/$PRNumber/00-OVERVIEW.md"
|
||||
return Test-Path $reviewPath
|
||||
}
|
||||
|
||||
function Get-PRsFromWorktrees {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get PR numbers from issue worktrees by checking for open PRs on each branch.
|
||||
#>
|
||||
$worktrees = Get-WorktreeEntries | Where-Object { $_.Branch -like 'issue/*' }
|
||||
$prs = @()
|
||||
|
||||
foreach ($wt in $worktrees) {
|
||||
$prInfo = gh pr list --head $wt.Branch --json number,url --state open 2>$null | ConvertFrom-Json
|
||||
if ($prInfo -and $prInfo.Count -gt 0) {
|
||||
$prs += @{
|
||||
PRNumber = $prInfo[0].number
|
||||
PRUrl = $prInfo[0].url
|
||||
Branch = $wt.Branch
|
||||
WorktreePath = $wt.Path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $prs
|
||||
}
|
||||
|
||||
function Invoke-AssignCopilotReviewer {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Assign GitHub Copilot as a reviewer to the PR using GitHub MCP.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$PRNumber,
|
||||
[string]$CLIType = 'copilot',
|
||||
[string]$Model,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
if ($DryRun) {
|
||||
Info " [DRY RUN] Would request Copilot review for PR #$PRNumber"
|
||||
return $true
|
||||
}
|
||||
|
||||
# Use a prompt that instructs Copilot to use GitHub MCP to assign Copilot as reviewer
|
||||
$prompt = @"
|
||||
Use the GitHub MCP to request a review from GitHub Copilot for PR #$PRNumber.
|
||||
|
||||
Steps:
|
||||
1. Use the GitHub MCP tool to add "Copilot" as a reviewer to pull request #$PRNumber in the microsoft/PowerToys repository
|
||||
2. This should add Copilot to the "Reviewers" section of the PR
|
||||
|
||||
If GitHub MCP is not available, report that and skip this step.
|
||||
"@
|
||||
|
||||
# MCP config for github-artifacts tools - use absolute path from main repo
|
||||
$mcpConfigPath = Join-Path $repoRoot '.github/skills/pr-review/references/mcp-config.json'
|
||||
$mcpConfig = "@$mcpConfigPath"
|
||||
|
||||
try {
|
||||
Info " Requesting Copilot review via GitHub MCP..."
|
||||
|
||||
switch ($CLIType) {
|
||||
'copilot' {
|
||||
$copilotArgs = @('--additional-mcp-config', $mcpConfig, '-p', $prompt, '--yolo', '-s')
|
||||
if ($Model) {
|
||||
$copilotArgs += @('--model', $Model)
|
||||
}
|
||||
& copilot @copilotArgs 2>&1 | Out-Null
|
||||
}
|
||||
'claude' {
|
||||
& claude --print --dangerously-skip-permissions --prompt $prompt 2>&1 | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
catch {
|
||||
Warn " Could not assign Copilot reviewer: $($_.Exception.Message)"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-PRReview {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Run review-pr.prompt.md using Copilot CLI.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$PRNumber,
|
||||
[string]$CLIType = 'copilot',
|
||||
[string]$Model,
|
||||
[string]$MinSeverity = 'medium',
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
# Simple prompt - let the prompt file define all the details
|
||||
$prompt = @"
|
||||
Follow exactly what at .github/prompts/review-pr.prompt.md to do with PR #$PRNumber.
|
||||
Post findings with severity >= $MinSeverity as PR review comments via GitHub MCP.
|
||||
"@
|
||||
|
||||
if ($DryRun) {
|
||||
Info " [DRY RUN] Would run PR review for #$PRNumber"
|
||||
return @{ Success = $true; ReviewPath = "Generated Files/prReview/$PRNumber" }
|
||||
}
|
||||
|
||||
$reviewPath = Join-Path $repoRoot "Generated Files/prReview/$PRNumber"
|
||||
|
||||
# Ensure the review directory exists
|
||||
if (-not (Test-Path $reviewPath)) {
|
||||
New-Item -ItemType Directory -Path $reviewPath -Force | Out-Null
|
||||
}
|
||||
|
||||
# MCP config for github-artifacts tools - use absolute path from main repo
|
||||
$mcpConfigPath = Join-Path $repoRoot '.github/skills/pr-review/references/mcp-config.json'
|
||||
$mcpConfig = "@$mcpConfigPath"
|
||||
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
switch ($CLIType) {
|
||||
'copilot' {
|
||||
Info " Running Copilot review (this may take several minutes)..."
|
||||
$copilotArgs = @('--additional-mcp-config', $mcpConfig, '-p', $prompt, '--yolo')
|
||||
if ($Model) {
|
||||
$copilotArgs += @('--model', $Model)
|
||||
}
|
||||
$output = & copilot @copilotArgs 2>&1
|
||||
# Log output for debugging
|
||||
$logFile = Join-Path $reviewPath "_copilot-review.log"
|
||||
$output | Out-File -FilePath $logFile -Force
|
||||
}
|
||||
'claude' {
|
||||
Info " Running Claude review (this may take several minutes)..."
|
||||
$output = & claude --print --dangerously-skip-permissions --prompt $prompt 2>&1
|
||||
$logFile = Join-Path $reviewPath "_claude-review.log"
|
||||
$output | Out-File -FilePath $logFile -Force
|
||||
}
|
||||
}
|
||||
|
||||
# Check if review files were created (at minimum, check for multiple step files)
|
||||
$overviewPath = Join-Path $reviewPath '00-OVERVIEW.md'
|
||||
$stepFiles = Get-ChildItem -Path $reviewPath -Filter "*.md" -ErrorAction SilentlyContinue
|
||||
$stepCount = ($stepFiles | Where-Object { $_.Name -match '^\d{2}-' }).Count
|
||||
|
||||
if ($stepCount -ge 5) {
|
||||
return @{ Success = $true; ReviewPath = $reviewPath; StepFilesCreated = $stepCount }
|
||||
} elseif (Test-Path $overviewPath) {
|
||||
Warn " Only overview created, step files may be incomplete ($stepCount step files)"
|
||||
return @{ Success = $true; ReviewPath = $reviewPath; StepFilesCreated = $stepCount; Partial = $true }
|
||||
} else {
|
||||
return @{ Success = $false; Error = "Review files not created (found $stepCount step files)" }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return @{ Success = $false; Error = $_.Exception.Message }
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-FixPRComments {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Run fix-pr-active-comments.prompt.md to fix issues.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$PRNumber,
|
||||
[string]$WorktreePath,
|
||||
[string]$CLIType = 'copilot',
|
||||
[string]$Model,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
# Simple prompt - let the prompt file define all the details
|
||||
$prompt = "Follow .github/prompts/fix-pr-active-comments.prompt.md for PR #$PRNumber."
|
||||
|
||||
if ($DryRun) {
|
||||
Info " [DRY RUN] Would fix PR comments for #$PRNumber"
|
||||
return @{ Success = $true }
|
||||
}
|
||||
|
||||
$workDir = if ($WorktreePath -and (Test-Path $WorktreePath)) { $WorktreePath } else { $repoRoot }
|
||||
|
||||
# MCP config for github-artifacts tools - use absolute path from main repo
|
||||
# This is needed because worktrees don't have .github folder
|
||||
$mcpConfigPath = Join-Path $repoRoot '.github/skills/pr-review/references/mcp-config.json'
|
||||
$mcpConfig = "@$mcpConfigPath"
|
||||
|
||||
Push-Location $workDir
|
||||
try {
|
||||
switch ($CLIType) {
|
||||
'copilot' {
|
||||
Info " Running Copilot to fix comments..."
|
||||
$copilotArgs = @('--additional-mcp-config', $mcpConfig, '-p', $prompt, '--yolo')
|
||||
if ($Model) {
|
||||
$copilotArgs += @('--model', $Model)
|
||||
}
|
||||
$output = & copilot @copilotArgs 2>&1
|
||||
# Log output for debugging
|
||||
$logPath = Join-Path $repoRoot "Generated Files/prReview/$PRNumber"
|
||||
if (-not (Test-Path $logPath)) {
|
||||
New-Item -ItemType Directory -Path $logPath -Force | Out-Null
|
||||
}
|
||||
$logFile = Join-Path $logPath "_copilot-fix.log"
|
||||
$output | Out-File -FilePath $logFile -Force
|
||||
}
|
||||
'claude' {
|
||||
Info " Running Claude to fix comments..."
|
||||
$output = & claude --print --dangerously-skip-permissions --prompt $prompt 2>&1
|
||||
$logPath = Join-Path $repoRoot "Generated Files/prReview/$PRNumber"
|
||||
if (-not (Test-Path $logPath)) {
|
||||
New-Item -ItemType Directory -Path $logPath -Force | Out-Null
|
||||
}
|
||||
$logFile = Join-Path $logPath "_claude-fix.log"
|
||||
$output | Out-File -FilePath $logFile -Force
|
||||
}
|
||||
}
|
||||
|
||||
return @{ Success = $true }
|
||||
}
|
||||
catch {
|
||||
return @{ Success = $false; Error = $_.Exception.Message }
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Start-PRWorkflowJob {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Process a single PR through the workflow.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$PRNumber,
|
||||
[string]$WorktreePath,
|
||||
[string]$CLIType = 'copilot',
|
||||
[string]$Model,
|
||||
[string]$MinSeverity = 'medium',
|
||||
[switch]$SkipAssign,
|
||||
[switch]$SkipReview,
|
||||
[switch]$SkipFix,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$result = @{
|
||||
PRNumber = $PRNumber
|
||||
AssignResult = $null
|
||||
ReviewResult = $null
|
||||
FixResult = $null
|
||||
Success = $true
|
||||
}
|
||||
|
||||
# Step 1: Assign Copilot as reviewer
|
||||
if (-not $SkipAssign) {
|
||||
Info " Step 1: Assigning Copilot reviewer..."
|
||||
$result.AssignResult = Invoke-AssignCopilotReviewer -PRNumber $PRNumber -CLIType $CLIType -Model $Model -DryRun:$DryRun
|
||||
if (-not $result.AssignResult) {
|
||||
Warn " Assignment step had issues (continuing...)"
|
||||
}
|
||||
} else {
|
||||
Info " Step 1: Skipped (assign)"
|
||||
}
|
||||
|
||||
# Step 2: Run PR review
|
||||
if (-not $SkipReview) {
|
||||
Info " Step 2: Running PR review..."
|
||||
$result.ReviewResult = Invoke-PRReview -PRNumber $PRNumber -CLIType $CLIType -Model $Model -MinSeverity $MinSeverity -DryRun:$DryRun
|
||||
if (-not $result.ReviewResult.Success) {
|
||||
Warn " Review step failed: $($result.ReviewResult.Error)"
|
||||
$result.Success = $false
|
||||
} else {
|
||||
$stepInfo = if ($result.ReviewResult.StepFilesCreated) { " ($($result.ReviewResult.StepFilesCreated) step files)" } else { "" }
|
||||
$partialInfo = if ($result.ReviewResult.Partial) { " [PARTIAL]" } else { "" }
|
||||
Success " Review completed: $($result.ReviewResult.ReviewPath)$stepInfo$partialInfo"
|
||||
}
|
||||
} else {
|
||||
Info " Step 2: Skipped (review)"
|
||||
}
|
||||
|
||||
# Step 3: Fix PR comments
|
||||
if (-not $SkipFix) {
|
||||
Info " Step 3: Fixing PR comments..."
|
||||
$result.FixResult = Invoke-FixPRComments -PRNumber $PRNumber -WorktreePath $WorktreePath -CLIType $CLIType -Model $Model -DryRun:$DryRun
|
||||
if (-not $result.FixResult.Success) {
|
||||
Warn " Fix step failed: $($result.FixResult.Error)"
|
||||
$result.Success = $false
|
||||
} else {
|
||||
Success " Fix step completed"
|
||||
}
|
||||
} else {
|
||||
Info " Step 3: Skipped (fix)"
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
#region Main Script
|
||||
try {
|
||||
Info "Repository root: $repoRoot"
|
||||
Info "CLI type: $CLIType"
|
||||
Info "Min severity for comments: $MinSeverity"
|
||||
Info "Max parallel: $MaxParallel"
|
||||
|
||||
# Determine PRs to process
|
||||
$prsToProcess = @()
|
||||
|
||||
if ($PRNumbers -and $PRNumbers.Count -gt 0) {
|
||||
# Use specified PR numbers
|
||||
foreach ($prNum in $PRNumbers) {
|
||||
$prInfo = gh pr view $prNum --json number,url,headRefName 2>$null | ConvertFrom-Json
|
||||
if ($prInfo) {
|
||||
# Try to find matching worktree
|
||||
$wt = Get-WorktreeEntries | Where-Object { $_.Branch -eq $prInfo.headRefName } | Select-Object -First 1
|
||||
$prsToProcess += @{
|
||||
PRNumber = $prInfo.number
|
||||
PRUrl = $prInfo.url
|
||||
Branch = $prInfo.headRefName
|
||||
WorktreePath = if ($wt) { $wt.Path } else { $repoRoot }
|
||||
}
|
||||
} else {
|
||||
Warn "PR #$prNum not found"
|
||||
}
|
||||
}
|
||||
} elseif ($AllOpen) {
|
||||
# Fetch all open PRs from repository
|
||||
$prsToProcess = Get-AllOpenPRs -Limit $Limit
|
||||
} elseif ($Assigned) {
|
||||
# Fetch PRs assigned to current user
|
||||
$prsToProcess = Get-AssignedPRs -Limit $Limit
|
||||
} else {
|
||||
# Get PRs from worktrees
|
||||
Info "`nFinding PRs from issue worktrees..."
|
||||
$prsToProcess = Get-PRsFromWorktrees
|
||||
}
|
||||
|
||||
# Filter out already reviewed PRs if requested
|
||||
if ($SkipExisting -and $prsToProcess.Count -gt 0) {
|
||||
$beforeCount = $prsToProcess.Count
|
||||
$prsToProcess = $prsToProcess | Where-Object { -not (Test-ReviewExists -PRNumber $_.PRNumber) }
|
||||
$skippedCount = $beforeCount - $prsToProcess.Count
|
||||
if ($skippedCount -gt 0) {
|
||||
Info "Skipped $skippedCount PRs with existing reviews"
|
||||
}
|
||||
}
|
||||
|
||||
if ($prsToProcess.Count -eq 0) {
|
||||
Warn "No PRs found to process."
|
||||
return
|
||||
}
|
||||
|
||||
# Display PRs
|
||||
Info "`nPRs to process:"
|
||||
Info ("-" * 80)
|
||||
foreach ($pr in $prsToProcess) {
|
||||
Info (" #{0,-6} {1}" -f $pr.PRNumber, $pr.PRUrl)
|
||||
}
|
||||
Info ("-" * 80)
|
||||
|
||||
# Generate batch script mode - creates a standalone script for background execution
|
||||
if ($GenerateBatchScript) {
|
||||
$batchPath = Join-Path $repoRoot "Generated Files/prReview/_batch-review.ps1"
|
||||
$prNumbers = $prsToProcess | ForEach-Object { $_.PRNumber }
|
||||
|
||||
$batchContent = @"
|
||||
# Auto-generated batch review script
|
||||
# Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
|
||||
# PRs to review: $($prNumbers.Count)
|
||||
#
|
||||
# Run this script in a PowerShell terminal to review all PRs sequentially.
|
||||
# Each review takes 2-5 minutes. Total estimated time: $([math]::Ceiling($prNumbers.Count * 3)) minutes.
|
||||
#
|
||||
# Usage: pwsh -File "$batchPath"
|
||||
|
||||
`$ErrorActionPreference = 'Continue'
|
||||
`$repoRoot = '$repoRoot'
|
||||
Set-Location `$repoRoot
|
||||
|
||||
`$prNumbers = @($($prNumbers -join ', '))
|
||||
`$total = `$prNumbers.Count
|
||||
`$completed = 0
|
||||
`$failed = @()
|
||||
|
||||
Write-Host "Starting batch review of `$total PRs" -ForegroundColor Cyan
|
||||
Write-Host "Estimated time: $([math]::Ceiling($prNumbers.Count * 3)) minutes" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
foreach (`$pr in `$prNumbers) {
|
||||
`$completed++
|
||||
`$reviewPath = Join-Path `$repoRoot "Generated Files/prReview/`$pr"
|
||||
|
||||
# Skip if already reviewed
|
||||
if (Test-Path (Join-Path `$reviewPath "00-OVERVIEW.md")) {
|
||||
Write-Host "[`$completed/`$total] PR #`$pr - Already reviewed, skipping" -ForegroundColor DarkGray
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host "[`$completed/`$total] PR #`$pr - Starting review..." -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
# Create output directory
|
||||
if (-not (Test-Path `$reviewPath)) {
|
||||
New-Item -ItemType Directory -Path `$reviewPath -Force | Out-Null
|
||||
}
|
||||
|
||||
# Run copilot review
|
||||
`$prompt = "Follow exactly what at .github/skills/pr-review/references/review-pr.prompt.md to do with PR #`$pr. Write output to Generated Files/prReview/`$pr/. Do not post comments to GitHub."
|
||||
|
||||
& copilot -p `$prompt --yolo 2>&1 | Out-File -FilePath (Join-Path `$reviewPath "_copilot.log") -Force
|
||||
|
||||
# Check if review completed
|
||||
if (Test-Path (Join-Path `$reviewPath "00-OVERVIEW.md")) {
|
||||
Write-Host "[`$completed/`$total] PR #`$pr - Review completed" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[`$completed/`$total] PR #`$pr - Review may be incomplete (no overview file)" -ForegroundColor Yellow
|
||||
`$failed += `$pr
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host "[`$completed/`$total] PR #`$pr - FAILED: `$(`$_.Exception.Message)" -ForegroundColor Red
|
||||
`$failed += `$pr
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "======================================" -ForegroundColor Cyan
|
||||
Write-Host "Batch review complete!" -ForegroundColor Cyan
|
||||
Write-Host "Total: `$total | Completed: `$(`$total - `$failed.Count) | Failed: `$(`$failed.Count)" -ForegroundColor Cyan
|
||||
if (`$failed.Count -gt 0) {
|
||||
Write-Host "Failed PRs: `$(`$failed -join ', ')" -ForegroundColor Red
|
||||
}
|
||||
Write-Host "======================================" -ForegroundColor Cyan
|
||||
"@
|
||||
|
||||
$batchContent | Out-File -FilePath $batchPath -Encoding UTF8 -Force
|
||||
|
||||
Success "`nBatch script generated: $batchPath"
|
||||
Info "PRs included: $($prNumbers.Count)"
|
||||
Info ""
|
||||
Info "To run the batch review in background:"
|
||||
Info " Start-Process pwsh -ArgumentList '-File',`"$batchPath`" -WindowStyle Minimized"
|
||||
Info ""
|
||||
Info "Or run interactively to see progress:"
|
||||
Info " pwsh -File `"$batchPath`""
|
||||
|
||||
return @{
|
||||
BatchScript = $batchPath
|
||||
PRCount = $prNumbers.Count
|
||||
PRNumbers = $prNumbers
|
||||
}
|
||||
}
|
||||
|
||||
if ($DryRun) {
|
||||
Warn "`nDry run mode - no changes will be made."
|
||||
}
|
||||
|
||||
# Confirm
|
||||
if (-not $Force -and -not $DryRun) {
|
||||
$stepsDesc = @()
|
||||
if (-not $SkipAssign) { $stepsDesc += "assign Copilot" }
|
||||
if (-not $SkipReview) { $stepsDesc += "review" }
|
||||
if (-not $SkipFix) { $stepsDesc += "fix comments" }
|
||||
|
||||
$confirm = Read-Host "`nProceed with $($prsToProcess.Count) PRs ($($stepsDesc -join ', '))? (y/N)"
|
||||
if ($confirm -notmatch '^[yY]') {
|
||||
Info "Cancelled."
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# Process PRs (using jobs for parallelization)
|
||||
$results = @{
|
||||
Success = @()
|
||||
Failed = @()
|
||||
}
|
||||
|
||||
if ($MaxParallel -gt 1 -and $prsToProcess.Count -gt 1) {
|
||||
# Parallel processing using PowerShell jobs
|
||||
Info "`nStarting parallel processing (max $MaxParallel concurrent)..."
|
||||
|
||||
$jobs = @()
|
||||
$prQueue = [System.Collections.Queue]::new($prsToProcess)
|
||||
|
||||
while ($prQueue.Count -gt 0 -or $jobs.Count -gt 0) {
|
||||
# Start new jobs up to MaxParallel
|
||||
while ($jobs.Count -lt $MaxParallel -and $prQueue.Count -gt 0) {
|
||||
$pr = $prQueue.Dequeue()
|
||||
|
||||
Info "`n" + ("=" * 60)
|
||||
Info "PROCESSING PR #$($pr.PRNumber)"
|
||||
Info ("=" * 60)
|
||||
|
||||
# For simplicity, process sequentially within each PR but PRs in parallel
|
||||
# Since copilot CLI might have issues with true parallel execution
|
||||
$jobResult = Start-PRWorkflowJob `
|
||||
-PRNumber $pr.PRNumber `
|
||||
-WorktreePath $pr.WorktreePath `
|
||||
-CLIType $CLIType `
|
||||
-Model $Model `
|
||||
-MinSeverity $MinSeverity `
|
||||
-SkipAssign:$SkipAssign `
|
||||
-SkipReview:$SkipReview `
|
||||
-SkipFix:$SkipFix `
|
||||
-DryRun:$DryRun
|
||||
|
||||
if ($jobResult.Success) {
|
||||
$results.Success += $jobResult
|
||||
Success "✓ PR #$($pr.PRNumber) workflow completed"
|
||||
} else {
|
||||
$results.Failed += $jobResult
|
||||
Err "✗ PR #$($pr.PRNumber) workflow had failures"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
# Sequential processing
|
||||
foreach ($pr in $prsToProcess) {
|
||||
Info "`n" + ("=" * 60)
|
||||
Info "PROCESSING PR #$($pr.PRNumber)"
|
||||
Info ("=" * 60)
|
||||
|
||||
$jobResult = Start-PRWorkflowJob `
|
||||
-PRNumber $pr.PRNumber `
|
||||
-WorktreePath $pr.WorktreePath `
|
||||
-CLIType $CLIType `
|
||||
-Model $Model `
|
||||
-MinSeverity $MinSeverity `
|
||||
-SkipAssign:$SkipAssign `
|
||||
-SkipReview:$SkipReview `
|
||||
-SkipFix:$SkipFix `
|
||||
-DryRun:$DryRun
|
||||
|
||||
if ($jobResult.Success) {
|
||||
$results.Success += $jobResult
|
||||
Success "✓ PR #$($pr.PRNumber) workflow completed"
|
||||
} else {
|
||||
$results.Failed += $jobResult
|
||||
Err "✗ PR #$($pr.PRNumber) workflow had failures"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Summary
|
||||
Info "`n" + ("=" * 80)
|
||||
Info "PR REVIEW WORKFLOW COMPLETE"
|
||||
Info ("=" * 80)
|
||||
Info "Total PRs: $($prsToProcess.Count)"
|
||||
|
||||
if ($results.Success.Count -gt 0) {
|
||||
Success "Succeeded: $($results.Success.Count)"
|
||||
foreach ($r in $results.Success) {
|
||||
Success " PR #$($r.PRNumber)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($results.Failed.Count -gt 0) {
|
||||
Err "Had issues: $($results.Failed.Count)"
|
||||
foreach ($r in $results.Failed) {
|
||||
Err " PR #$($r.PRNumber)"
|
||||
}
|
||||
}
|
||||
|
||||
Info "`nReview files location: Generated Files/prReview/<PR_NUMBER>/"
|
||||
Info ("=" * 80)
|
||||
|
||||
# Write signal files for orchestrator
|
||||
foreach ($r in $results.Success) {
|
||||
$signalPath = Join-Path $repoRoot "Generated Files/prReview/$($r.PRNumber)/.signal"
|
||||
@{
|
||||
status = "success"
|
||||
prNumber = $r.PRNumber
|
||||
timestamp = (Get-Date).ToString("o")
|
||||
} | ConvertTo-Json | Set-Content $signalPath -Force
|
||||
}
|
||||
foreach ($r in $results.Failed) {
|
||||
$signalPath = Join-Path $repoRoot "Generated Files/prReview/$($r.PRNumber)/.signal"
|
||||
@{
|
||||
status = "failure"
|
||||
prNumber = $r.PRNumber
|
||||
timestamp = (Get-Date).ToString("o")
|
||||
} | ConvertTo-Json | Set-Content $signalPath -Force
|
||||
}
|
||||
|
||||
return $results
|
||||
}
|
||||
catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
exit 1
|
||||
}
|
||||
#endregion
|
||||
@@ -210,6 +210,11 @@
|
||||
"PowerToys.PowerAccentModuleInterface.dll",
|
||||
"PowerToys.PowerAccentKeyboardService.dll",
|
||||
|
||||
"PowerToys.PowerDisplayModuleInterface.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerDisplay.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerDisplay.exe",
|
||||
"PowerDisplay.Lib.dll",
|
||||
|
||||
"WinUI3Apps\\PowerToys.PowerRenameExt.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerRename.exe",
|
||||
"WinUI3Apps\\PowerToys.PowerRenameContextMenu.dll",
|
||||
@@ -378,6 +383,8 @@
|
||||
"UnitsNet.dll",
|
||||
"UtfUnknown.dll",
|
||||
"Wpf.Ui.dll",
|
||||
"WmiLight.dll",
|
||||
"WmiLight.Native.dll",
|
||||
"Shmuelie.WinRTServer.dll",
|
||||
"ToolGood.Words.Pinyin.dll"
|
||||
],
|
||||
|
||||
@@ -35,7 +35,9 @@ stages:
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
demands: ImageOverride -equals SHINE-VS18-Preview
|
||||
${{ else }}:
|
||||
demands: ImageOverride -equals SHINE-VS18-Latest
|
||||
buildPlatforms:
|
||||
- ${{ parameters.platform }}
|
||||
buildConfigurations: [Release]
|
||||
|
||||
@@ -51,7 +51,9 @@ extends:
|
||||
pool:
|
||||
name: SHINE-INT-S
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
demands: ImageOverride -equals SHINE-VS18-Preview
|
||||
${{ else }}:
|
||||
demands: ImageOverride -equals SHINE-VS18-Latest
|
||||
os: windows
|
||||
sdl:
|
||||
tsa:
|
||||
@@ -74,7 +76,9 @@ extends:
|
||||
demands:
|
||||
# Our INT agents have a large disk mounted at P:\
|
||||
- ${{ if eq(parameters.useVSPreview, true) }}:
|
||||
- ImageOverride -equals SHINE-VS17-Preview
|
||||
- ImageOverride -equals SHINE-VS18-Latest-Preview
|
||||
- ${{ else }}:
|
||||
- ImageOverride -equals SHINE-VS18-Latest
|
||||
os: windows
|
||||
variables:
|
||||
IsPipeline: 1 # The installer uses this to detect whether it should pick up localizations
|
||||
@@ -87,6 +91,7 @@ extends:
|
||||
official: true
|
||||
codeSign: true
|
||||
runTests: false
|
||||
buildTests: false
|
||||
signingIdentity:
|
||||
serviceName: $(SigningServiceName)
|
||||
appId: $(SigningAppId)
|
||||
|
||||
@@ -253,11 +253,12 @@ jobs:
|
||||
displayName: Build PowerToys main project
|
||||
inputs:
|
||||
solution: 'PowerToys.slnx'
|
||||
vsVersion: 17.0
|
||||
vsVersion: 18.0
|
||||
msbuildArgs: >-
|
||||
-restore -graph
|
||||
/p:RestorePackagesConfig=true
|
||||
/p:CIBuild=true
|
||||
/p:BuildTests=${{ parameters.buildTests }}
|
||||
/bl:$(LogOutputDirectory)\build-0-main.binlog
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
$(MSBuildCacheParameters)
|
||||
@@ -276,7 +277,7 @@ jobs:
|
||||
condition: and(succeeded(), eq(variables['BuildPlatform'], 'arm64'))
|
||||
inputs:
|
||||
solution: PowerToys.slnx
|
||||
vsVersion: 17.0
|
||||
vsVersion: 18.0
|
||||
msbuildArgs: >-
|
||||
-restore
|
||||
/p:Configuration=$(BuildConfiguration)
|
||||
@@ -338,7 +339,7 @@ jobs:
|
||||
displayName: Build BugReportTool
|
||||
inputs:
|
||||
solution: '**/tools/BugReportTool/BugReportTool.sln'
|
||||
vsVersion: 17.0
|
||||
vsVersion: 18.0
|
||||
msbuildArgs: >-
|
||||
-restore -graph
|
||||
/p:RestorePackagesConfig=true
|
||||
@@ -359,7 +360,7 @@ jobs:
|
||||
displayName: Build StylesReportTool
|
||||
inputs:
|
||||
solution: '**/tools/StylesReportTool/StylesReportTool.sln'
|
||||
vsVersion: 17.0
|
||||
vsVersion: 18.0
|
||||
msbuildArgs: >-
|
||||
-restore -graph
|
||||
/p:RestorePackagesConfig=true
|
||||
@@ -381,7 +382,7 @@ jobs:
|
||||
displayName: Publish ${{ project }} for Packaging
|
||||
inputs:
|
||||
solution: ${{ project }}
|
||||
vsVersion: 17.0
|
||||
vsVersion: 18.0
|
||||
msbuildArgs: >-
|
||||
/target:Publish
|
||||
/graph
|
||||
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
displayName: Build UI Test Projects
|
||||
inputs:
|
||||
solution: '**/*UITest*.csproj'
|
||||
vsVersion: 17.0
|
||||
vsVersion: 18.0
|
||||
msbuildArgs: >-
|
||||
-restore
|
||||
-graph
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
displayName: 'Build UI Test Module: ${{ module }}'
|
||||
inputs:
|
||||
solution: '**/*${{ module }}*.csproj'
|
||||
vsVersion: 17.0
|
||||
vsVersion: 18.0
|
||||
msbuildArgs: >-
|
||||
-restore
|
||||
-graph
|
||||
|
||||
@@ -49,7 +49,9 @@ stages:
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
demands: ImageOverride -equals SHINE-VS18-Preview
|
||||
${{ else }}:
|
||||
demands: ImageOverride -equals SHINE-VS18-Latest
|
||||
buildPlatforms:
|
||||
- ${{ platform }}
|
||||
buildConfigurations: [Release]
|
||||
@@ -57,6 +59,7 @@ stages:
|
||||
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
|
||||
msBuildCacheIsReadOnly: ${{ parameters.msBuildCacheIsReadOnly }}
|
||||
runTests: ${{ parameters.runTests }}
|
||||
buildTests: true
|
||||
useVSPreview: ${{ parameters.useVSPreview }}
|
||||
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
|
||||
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
|
||||
@@ -76,7 +79,9 @@ stages:
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
demands: ImageOverride -equals SHINE-VS18-Preview
|
||||
${{ else }}:
|
||||
demands: ImageOverride -equals SHINE-VS18-Latest
|
||||
buildConfigurations: [Release]
|
||||
official: false
|
||||
codeSign: false
|
||||
|
||||
@@ -29,7 +29,9 @@ stages:
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
demands: ImageOverride -equals SHINE-VS18-Preview
|
||||
${{ else }}:
|
||||
demands: ImageOverride -equals SHINE-VS18-Latest
|
||||
buildPlatforms:
|
||||
- ${{ parameters.platform }}
|
||||
buildConfigurations: [Release]
|
||||
|
||||
@@ -36,7 +36,7 @@ steps:
|
||||
displayName: Build Shared Support DLLs
|
||||
inputs:
|
||||
solution: "**/installer/PowerToysSetup.slnx"
|
||||
vsVersion: 17.0
|
||||
vsVersion: 18.0
|
||||
msbuildArgs: >-
|
||||
/t:PowerToysSetupCustomActionsVNext;SilentFilesInUseBAFunction
|
||||
/p:RunBuildEvents=true;RestorePackagesConfig=true;CIBuild=true
|
||||
@@ -75,7 +75,7 @@ steps:
|
||||
displayName: 💻 Build VNext MSI
|
||||
inputs:
|
||||
solution: "**/installer/PowerToysSetup.slnx"
|
||||
vsVersion: 17.0
|
||||
vsVersion: 18.0
|
||||
msbuildArgs: >-
|
||||
-restore
|
||||
/t:PowerToysInstallerVNext
|
||||
@@ -92,7 +92,7 @@ steps:
|
||||
displayName: 👤 Build VNext MSI
|
||||
inputs:
|
||||
solution: "**/installer/PowerToysSetup.slnx"
|
||||
vsVersion: 17.0
|
||||
vsVersion: 18.0
|
||||
msbuildArgs: >-
|
||||
/t:PowerToysInstallerVNext
|
||||
/p:RunBuildEvents=false;PerUser=true;BuildProjectReferences=false;CIBuild=true
|
||||
@@ -143,7 +143,7 @@ steps:
|
||||
displayName: 💻 Build VNext Bootstrapper
|
||||
inputs:
|
||||
solution: "**/installer/PowerToysSetup.slnx"
|
||||
vsVersion: 17.0
|
||||
vsVersion: 18.0
|
||||
msbuildArgs: >-
|
||||
-restore
|
||||
/t:PowerToysBootstrapperVNext
|
||||
@@ -160,7 +160,7 @@ steps:
|
||||
displayName: 👤 Build VNext Bootstrapper
|
||||
inputs:
|
||||
solution: "**/installer/PowerToysSetup.slnx"
|
||||
vsVersion: 17.0
|
||||
vsVersion: 18.0
|
||||
msbuildArgs: >-
|
||||
/t:PowerToysBootstrapperVNext
|
||||
/p:PerUser=true;BuildProjectReferences=false;CIBuild=true
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
$VSInstances = ([xml](& 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -include packages -format xml))
|
||||
# Build common vswhere base arguments
|
||||
$vsWhereBaseArgs = @('-latest', '-requires', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64')
|
||||
if ($env:VCWhereExtraVersionTarget) {
|
||||
# Add version target if specified (e.g., '-version [18.0,19.0)' for VS2026)
|
||||
$vsWhereBaseArgs += $env:VCWhereExtraVersionTarget.Split(' ')
|
||||
}
|
||||
|
||||
$VSInstances = ([xml](& 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' @vsWhereBaseArgs -include packages -format xml))
|
||||
$VSPackages = $VSInstances.instances.instance.packages.package
|
||||
$LatestVCPackage = ($VSPackages | ? { $_.id -eq "Microsoft.VisualCpp.Tools.Core" })
|
||||
$LatestVCToolsVersion = $LatestVCPackage.version;
|
||||
|
||||
$VSRoot = (& 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property 'resolvedInstallationPath')
|
||||
$VSRoot = (& 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' @vsWhereBaseArgs -property 'resolvedInstallationPath')
|
||||
$VCToolsRoot = Join-Path $VSRoot "VC\Tools\MSVC"
|
||||
|
||||
# We have observed a few instances where the VC tools package version actually
|
||||
@@ -24,5 +31,12 @@ If ($Null -Eq (Get-Item $PackageVCToolPath -ErrorAction:Ignore)) {
|
||||
}
|
||||
|
||||
Write-Output "Latest VCToolsVersion: $LatestVCToolsVersion"
|
||||
Write-Output "Updating VCToolsVersion environment variable for job"
|
||||
Write-Output "##vso[task.setvariable variable=VCToolsVersion]$LatestVCToolsVersion"
|
||||
|
||||
# VS2026 (MSVC 14.50+) doesn't need explicit VCToolsVersion - let MSBuild auto-select
|
||||
$MajorMinorVersion = [Version]::Parse($LatestVCToolsVersion)
|
||||
If ($MajorMinorVersion.Major -eq 14 -and $MajorMinorVersion.Minor -ge 50) {
|
||||
Write-Output "VS2026 detected (MSVC 14.50+). Skipping VCToolsVersion override to allow MSBuild auto-selection."
|
||||
} Else {
|
||||
Write-Output "Updating VCToolsVersion environment variable for job"
|
||||
Write-Output "##vso[task.setvariable variable=VCToolsVersion]$LatestVCToolsVersion"
|
||||
}
|
||||
|
||||
@@ -90,9 +90,15 @@ if ($noticeMatch.Success) {
|
||||
$currentNoticePackageList = ""
|
||||
}
|
||||
|
||||
# Test-only packages that are allowed to be in NOTICE.md but not in the build
|
||||
# (e.g., when BuildTests=false, these packages won't appear in the NuGet list)
|
||||
$allowedExtraPackages = @(
|
||||
"- Moq"
|
||||
)
|
||||
|
||||
if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
{
|
||||
Write-Host -ForegroundColor Red "Notice.md does not match NuGet list."
|
||||
Write-Host -ForegroundColor Yellow "Notice.md does not exactly match NuGet list. Analyzing differences..."
|
||||
|
||||
# Show detailed differences
|
||||
$generatedPackages = $returnList -split "`r`n|`n" | Where-Object { $_.Trim() -ne "" } | Sort-Object
|
||||
@@ -105,7 +111,7 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
# Find packages in proj file list but not in NOTICE.md
|
||||
$missingFromNotice = $generatedPackages | Where-Object { $noticePackages -notcontains $_ }
|
||||
if ($missingFromNotice.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Red "MissingFromNotice:"
|
||||
Write-Host -ForegroundColor Red "MissingFromNotice (ERROR - these must be added to NOTICE.md):"
|
||||
foreach ($pkg in $missingFromNotice) {
|
||||
Write-Host -ForegroundColor Red " $pkg"
|
||||
}
|
||||
@@ -114,10 +120,23 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
|
||||
# Find packages in NOTICE.md but not in proj file list
|
||||
$extraInNotice = $noticePackages | Where-Object { $generatedPackages -notcontains $_ }
|
||||
if ($extraInNotice.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Yellow "ExtraInNotice:"
|
||||
foreach ($pkg in $extraInNotice) {
|
||||
Write-Host -ForegroundColor Yellow " $pkg"
|
||||
|
||||
# Filter out allowed extra packages (test-only dependencies)
|
||||
$unexpectedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -notcontains $_ }
|
||||
$allowedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -contains $_ }
|
||||
|
||||
if ($allowedExtra.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Green "ExtraInNotice (OK - allowed test-only packages):"
|
||||
foreach ($pkg in $allowedExtra) {
|
||||
Write-Host -ForegroundColor Green " $pkg"
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
if ($unexpectedExtra.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Red "ExtraInNotice (ERROR - unexpected packages in NOTICE.md):"
|
||||
foreach ($pkg in $unexpectedExtra) {
|
||||
Write-Host -ForegroundColor Red " $pkg"
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
@@ -127,10 +146,17 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
Write-Host " Proj file list has $($generatedPackages.Count) packages"
|
||||
Write-Host " NOTICE.md has $($noticePackages.Count) packages"
|
||||
Write-Host " MissingFromNotice: $($missingFromNotice.Count) packages"
|
||||
Write-Host " ExtraInNotice: $($extraInNotice.Count) packages"
|
||||
Write-Host " ExtraInNotice (allowed): $($allowedExtra.Count) packages"
|
||||
Write-Host " ExtraInNotice (unexpected): $($unexpectedExtra.Count) packages"
|
||||
Write-Host ""
|
||||
|
||||
exit 1
|
||||
# Fail if there are missing packages OR unexpected extra packages
|
||||
if ($missingFromNotice.Count -gt 0 -or $unexpectedExtra.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Red "FAILED: NOTICE.md mismatch detected."
|
||||
exit 1
|
||||
} else {
|
||||
Write-Host -ForegroundColor Green "PASSED: NOTICE.md matches (with allowed test-only packages)."
|
||||
}
|
||||
}
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -40,7 +40,7 @@ These instruction files are automatically applied when working in their respecti
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Visual Studio 2022 17.4+
|
||||
- Visual Studio 2022 17.4+ or Visual Studio 2026
|
||||
- Windows 10 1803+ (April 2018 Update or newer)
|
||||
- Initialize submodules once: `git submodule update --init --recursive`
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
<Project ToolsVersion="4.0"
|
||||
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<!-- Skip building C++ test projects when BuildTests=false -->
|
||||
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
|
||||
<UsePrecompiledHeaders>false</UsePrecompiledHeaders>
|
||||
<RunCodeAnalysis>false</RunCodeAnalysis>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Project configurations -->
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
@@ -51,7 +57,7 @@
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Use</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<DisableSpecificWarnings>4679;5271;%(DisableSpecificWarnings)</DisableSpecificWarnings>
|
||||
<DisableSpecificWarnings>4679;4706;4874;5271;%(DisableSpecificWarnings)</DisableSpecificWarnings>
|
||||
<DisableAnalyzeExternal >true</DisableAnalyzeExternal>
|
||||
<ExternalWarningLevel>TurnOffAllWarnings</ExternalWarningLevel>
|
||||
<ConformanceMode>false</ConformanceMode>
|
||||
@@ -110,6 +116,7 @@
|
||||
<!-- Props that are constant for both Debug and Release configurations -->
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<PlatformToolset Condition="'$(VisualStudioVersion)' == '18.0'">v145</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<DesktopCompatible>true</DesktopCompatible>
|
||||
<SpectreMitigation>Spectre</SpectreMitigation>
|
||||
|
||||
@@ -19,6 +19,39 @@
|
||||
<PlatformTarget>$(Platform)</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Completely skip building test projects when BuildTests=false (e.g., Release pipeline).
|
||||
This avoids InternalsVisibleTo/signing issues by not compiling test code at all.
|
||||
Match: projects ending in Test, Tests, UnitTests, UITests, FuzzTests, or in a folder named Tests.
|
||||
Also matches projects starting with UnitTests- (e.g., UnitTests-CommonLib).
|
||||
Also removes all PackageReference/ProjectReference to prevent NuGet restore and dependency builds.
|
||||
Note: Checking both 'false' and 'False' to handle YAML boolean serialization.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(BuildTests)' == 'false' or '$(BuildTests)' == 'False'">
|
||||
<_ProjectName>$(MSBuildProjectName)</_ProjectName>
|
||||
<!-- Match any project ending with "Test" or "Tests" (covers UnitTests, UITests, FuzzTests, etc.) -->
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Test'))">true</_IsSkippedTestProject>
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Tests'))">true</_IsSkippedTestProject>
|
||||
<!-- Match projects starting with UnitTests- or UITest- prefix -->
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UnitTests-'))">true</_IsSkippedTestProject>
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UITest-'))">true</_IsSkippedTestProject>
|
||||
<!-- Match projects in a Tests folder -->
|
||||
<_IsSkippedTestProject Condition="$(MSBuildProjectDirectory.Contains('\Tests\'))">true</_IsSkippedTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
|
||||
<EnableDefaultItems>false</EnableDefaultItems>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateGlobalUsings>false</GenerateGlobalUsings>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<!-- Disable all code analysis for skipped test projects -->
|
||||
<EnableNETAnalyzers>false</EnableNETAnalyzers>
|
||||
<RunAnalyzers>false</RunAnalyzers>
|
||||
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
|
||||
<Version>$(Version).0</Version>
|
||||
<RepositoryUrl>https://github.com/microsoft/PowerToys</RepositoryUrl>
|
||||
@@ -30,7 +63,9 @@
|
||||
<_PropertySheetDisplayName>PowerToys.Root.Props</_PropertySheetDisplayName>
|
||||
<ForceImportBeforeCppProps>$(MsbuildThisFileDirectory)\Cpp.Build.props</ForceImportBeforeCppProps>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
|
||||
|
||||
|
||||
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj' and '$(_IsSkippedTestProject)' != 'true'">
|
||||
<PackageReference Include="StyleCop.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -28,4 +28,41 @@
|
||||
<PropertyGroup Condition="'$(IgnoreExperimentalWarnings)' == 'true'">
|
||||
<NoWarn>$(NoWarn);CS8305;SA1500;CA1852</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
<!-- Skipped test projects when BuildTests=false: no-op build and remove references.
|
||||
This must be in targets (not props) so it runs AFTER the project file adds its items. -->
|
||||
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
|
||||
<BuildDependsOn />
|
||||
<CoreBuildDependsOn />
|
||||
<RebuildDependsOn />
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- For C# projects: remove all items -->
|
||||
<ItemGroup Condition="'$(_IsSkippedTestProject)' == 'true' and '$(MSBuildProjectExtension)' == '.csproj'">
|
||||
<PackageReference Remove="@(PackageReference)" />
|
||||
<ProjectReference Remove="@(ProjectReference)" />
|
||||
<Reference Remove="@(Reference)" />
|
||||
<Compile Remove="@(Compile)" />
|
||||
<Content Remove="@(Content)" />
|
||||
<EmbeddedResource Remove="@(EmbeddedResource)" />
|
||||
<None Remove="@(None)" />
|
||||
<Using Remove="@(Using)" />
|
||||
<GlobalUsing Remove="@(GlobalUsing)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For C++ projects (vcxproj): remove all compile/link items to prevent build -->
|
||||
<ItemGroup Condition="'$(_IsSkippedTestProject)' == 'true' and '$(MSBuildProjectExtension)' == '.vcxproj'">
|
||||
<ClCompile Remove="@(ClCompile)" />
|
||||
<ClInclude Remove="@(ClInclude)" />
|
||||
<Link Remove="@(Link)" />
|
||||
<Lib Remove="@(Lib)" />
|
||||
<ProjectReference Remove="@(ProjectReference)" />
|
||||
<None Remove="@(None)" />
|
||||
<ResourceCompile Remove="@(ResourceCompile)" />
|
||||
<Midl Remove="@(Midl)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Note: For C++ skipped test projects, build is effectively skipped by removing all compile items above.
|
||||
We don't define empty Build/Rebuild/Clean targets here because MSBuild Target definitions with Condition
|
||||
on the Target element still override the default targets even when condition is false. -->
|
||||
</Project>
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
|
||||
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.5.0" />
|
||||
<PackageVersion Include="Polly.Core" Version="8.6.5" />
|
||||
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
|
||||
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
||||
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
||||
@@ -104,6 +105,7 @@
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<!-- Package System.CodeDom added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Management but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="9.0.0" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.10" />
|
||||
@@ -133,6 +135,7 @@
|
||||
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||
<PackageVersion Include="WinUIEx" Version="2.8.0" />
|
||||
<PackageVersion Include="WmiLight" Version="6.14.0" />
|
||||
<PackageVersion Include="WPF-UI" Version="3.0.5" />
|
||||
<PackageVersion Include="WyHash" Version="1.0.5" />
|
||||
<PackageVersion Include="WixToolset.Heat" Version="5.0.2" />
|
||||
|
||||
32
NOTICE.md
32
NOTICE.md
@@ -10,6 +10,7 @@ This software incorporates material from third parties.
|
||||
- Installer/Runner
|
||||
- Measure tool
|
||||
- Peek
|
||||
- PowerDisplay
|
||||
- Registry Preview
|
||||
|
||||
## Utility: Color Picker
|
||||
@@ -1519,6 +1520,35 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
## Utility: PowerDisplay
|
||||
|
||||
### Twinkle Tray
|
||||
|
||||
PowerDisplay's DDC/CI implementation references techniques from Twinkle Tray.
|
||||
|
||||
**Source**: https://github.com/xanderfrangos/twinkle-tray
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright © 2020 Xander Frangos
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## NuGet Packages used by PowerToys
|
||||
|
||||
@@ -1557,6 +1587,7 @@ SOFTWARE.
|
||||
- NLog.Extensions.Logging
|
||||
- NLog.Schema
|
||||
- OpenAI
|
||||
- Polly.Core
|
||||
- ReverseMarkdown
|
||||
- ScipBe.Common.Office.OneNote
|
||||
- SharpCompress
|
||||
@@ -1569,5 +1600,6 @@ SOFTWARE.
|
||||
- UnitsNet
|
||||
- UTF.Unknown
|
||||
- WinUIEx
|
||||
- WmiLight
|
||||
- WPF-UI
|
||||
- WyHash
|
||||
@@ -55,6 +55,7 @@
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj" Id="1a066c63-64b3-45f8-92fe-664e1cce8077" />
|
||||
<Project Path="src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.vcxproj" Id="8b5cfb38-ccba-40a8-ad7a-89c57b070884" />
|
||||
<Project Path="src/common/updating/updating.vcxproj" Id="17da04df-e393-4397-9cf0-84dabe11032e" />
|
||||
<Project Path="src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
|
||||
</Folder>
|
||||
@@ -300,6 +301,10 @@
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/CommandPalette/Tests/">
|
||||
<Project Path="src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Microsoft.CmdPal.Core.Common.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/Microsoft.CmdPal.Ext.Apps.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
@@ -356,6 +361,10 @@
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/cmdpal/Tests/Microsoft.CommandPalette.Extensions.Toolkit.UnitTests/Microsoft.CommandPalette.Extensions.Toolkit.UnitTests.csproj" Id="2eca18b7-33b7-4829-88f1-439b20fd60f6">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/CommandPalette/UI/">
|
||||
<Project Path="src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj">
|
||||
@@ -676,6 +685,23 @@
|
||||
<Deploy />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/PowerDisplay/">
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib/PowerDisplay.Lib.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj" Id="d1234567-8901-2345-6789-abcdef012345" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/PowerDisplay/Tests/">
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/PowerDisplay.Lib.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/MeasureTool/">
|
||||
<Project Path="src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj" Id="54a93af7-60c7-4f6c-99d2-fbb1f75f853a">
|
||||
<BuildDependency Project="src/common/Display/Display.vcxproj" />
|
||||
|
||||
62
README.md
62
README.md
@@ -51,19 +51,19 @@ But to get started quickly, choose one of the installation methods below:
|
||||
Go to the <a href="https://aka.ms/installPowerToys">PowerToys GitHub releases</a>, click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
|
||||
<!-- items that need to be updated release to release -->
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.0/PowerToysUserSetup-0.97.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.0/PowerToysUserSetup-0.97.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.0/PowerToysSetup-0.97.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.0/PowerToysSetup-0.97.0-arm64.exe
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.98%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysUserSetup-0.97.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysUserSetup-0.97.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysSetup-0.97.1-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysSetup-0.97.1-arm64.exe
|
||||
|
||||
| Description | Filename |
|
||||
|----------------|----------|
|
||||
| Per user - x64 | [PowerToysUserSetup-0.97.0-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.97.0-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.97.0-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.97.0-arm64.exe][ptMachineArm64] |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.97.1-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.97.1-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.97.1-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.97.1-arm64.exe][ptMachineArm64] |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -103,18 +103,38 @@ There are <a href="https://learn.microsoft.com/windows/powertoys/install#communi
|
||||
</details>
|
||||
|
||||
## ✨ What's new
|
||||
**Version 0.97 (January 2026)**
|
||||
**Version 0.97.1 (January 2026)**
|
||||
|
||||
For an in-depth look at the latest changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
|
||||
This patch release fixes several important stability issues identified in v0.97.0 based on incoming reports. Check out the [v0.97.0](https://github.com/microsoft/PowerToys/releases/tag/v0.97.0) notes for the full list of changes.
|
||||
|
||||
**✨ Highlights**
|
||||
- **Command Palette**: Major expansion with PowerToys extension (Windows 11 only), Remote Desktop built-in extension, theme customization, drag-and-drop support, fallback ranking controls, sections/separators for pages, pinyin Chinese matching, and many UX refinements.
|
||||
- **Settings**: Quick Access flyout is now a standalone process for significantly faster startup, theme-adaptive tray icon, AOT serialization, and multiple UI/accessibility fixes
|
||||
- **CursorWrap (New!)**: New mouse utility that lets your cursor wrap around screen edges, making multi-monitor navigation faster and more seamless.
|
||||
- **Advanced Paste**: Image input for AI, color detection in clipboard history, Foundry Local improvements, Azure AI icons, and multiple bug fixes
|
||||
- **CLI Support Expanded**: FancyZones, Image Resizer, and File Locksmith can now be controlled from the command line for layout management, batch image resizing, and file lock inspection.
|
||||
- **LightSwitch**: Added support for automatically following Windows Night Light mode.
|
||||
- **Release Experience & Quality**: Refreshed "What’s new" dialog, plus many performance improvements, stability fixes, and refinements across PowerToys.
|
||||
**Highlights**
|
||||
|
||||
### Advanced Paste
|
||||
- #44862: Fixed Settings UI advanced paste page crash by using correct settings repository for null checking.
|
||||
|
||||
### Command Palette
|
||||
- #44886: Fixed personalization section not appearing by using latest MSIX for installation.
|
||||
- #44938: Fixed loading of icons from internet shortcuts. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- #45076: Fixed potential deadlock from lazy-loading AppListItem details. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
|
||||
### Cursor Wrap
|
||||
- #44936: Added improved multi-monitor support; Added laptop lid close detection for dynamic monitor topology updates. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- #44936: Added new settings dropdown to constrain wrapping to horizontal-only, vertical-only, or both directions. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
|
||||
### Peek
|
||||
- #44995: Fixed Space key triggering Peek during file rename, search, or address bar typing.
|
||||
|
||||
### PowerRename
|
||||
- #44944: Fixed regex `$` not working, preventing users from adding text at the end of filenames.
|
||||
|
||||
### Runner
|
||||
- #44931: Monochrome tray icon now adapts to Windows system theme instead of app theme.
|
||||
- #44982: Fixed right-click menu to dynamically update based on Quick Access enabled/disabled state.
|
||||
|
||||
### GPO / Enterprise
|
||||
- #45028: Added CursorWrap policy definition to ADMX templates. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
|
||||
For the full list of v0.97 changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
|
||||
|
||||
## Advanced Paste
|
||||
|
||||
@@ -289,7 +309,7 @@ For an in-depth look at the latest changes, visit the [Windows Command Line blog
|
||||
- Stabilized FancyZones UI tests with more reliable selectors and screen recordings.
|
||||
|
||||
## 🛣️ Roadmap
|
||||
We are planning some nice new features and improvements for the next releases – PowerDisplay, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.97][github-next-release-work]!
|
||||
We are planning some nice new features and improvements for the next releases – PowerDisplay, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.98][github-next-release-work]!
|
||||
|
||||
## ❤️ PowerToys Community
|
||||
The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn't be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Your contributions and feedback improve PowerToys month after month!
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
### Building PowerToys Locally
|
||||
|
||||
#### One stop script for building installer
|
||||
1. Open developer powershell for vs 2022
|
||||
1. Open `Developer Powershell for VS 2022` or `Developer PowerShell for VS` for VS 2026.
|
||||
2. Run tools\build\build-installer.ps1
|
||||
> For the first-time setup, please run the installer as an administrator. This ensures that the Wix tool can move wix.target to the desired location and trust the certificate used to sign the MSIX packages.
|
||||
|
||||
@@ -109,7 +109,7 @@ dotnet tool install --global wix --version 5.0.2
|
||||
|
||||
##### From the command line
|
||||
|
||||
1. From the start menu, open a `Developer Command Prompt for VS 2022`
|
||||
1. From the start menu, open a `Developer Command Prompt for VS 2022` or `Developer Command Prompt for VS`
|
||||
1. Ensure `nuget.exe` is in your `%path%`
|
||||
1. In the repo root, run these commands:
|
||||
|
||||
@@ -140,7 +140,7 @@ If you prefer, you can alternatively build prerequisite projects for the install
|
||||
|
||||
The resulting installer will be available in the `installer\PowerToysSetupVNext\x64\Release\` folder.
|
||||
|
||||
To build the installer from the command line, run `Developer Command Prompt for VS 2022` in admin mode and execute the following commands. The generated installer package will be located at `\installer\PowerToysSetupVNext\{platform}\Release\MachineSetup`.
|
||||
To build the installer from the command line, run `Developer Command Prompt for VS 2022` or `Developer Command Prompt for VS` in admin mode and execute the following commands. The generated installer package will be located at `\installer\PowerToysSetupVNext\{platform}\Release\MachineSetup`.
|
||||
|
||||
```
|
||||
git clean -xfd -e *exe -- .\installer\
|
||||
|
||||
@@ -15,7 +15,7 @@ Before you can start debugging PowerToys, you need to set up your development en
|
||||
|
||||
You can build the entire solution from the command line, which is sometimes faster than building within Visual Studio:
|
||||
|
||||
1. Open Developer Command Prompt for VS 2022
|
||||
1. Open `Developer Command Prompt for VS 2022` or `Developer Command Prompt for VS`
|
||||
2. Navigate to the repository root directory
|
||||
3. Run the following command(don't forget to set the correct platform):
|
||||
```pwsh
|
||||
@@ -105,7 +105,7 @@ If you encounter build errors about missing image files (e.g., `.png`, `.ico`, o
|
||||
|
||||
1. **Clean the solution in Visual Studio**: Build > Clean Solution
|
||||
|
||||
Or from the command line (Developer Command Prompt for VS 2022):
|
||||
Or from the command line (Developer Command Prompt for VS 2022 or Developer Command Prompt for VS):
|
||||
```pwsh
|
||||
msbuild PowerToys.slnx /t:Clean /p:Platform=x64 /p:Configuration=Debug
|
||||
```
|
||||
|
||||
@@ -15,9 +15,11 @@ VS Code extensions Needed:
|
||||
---
|
||||
|
||||
## Building in VS Code
|
||||
### Configure developer powershell for vs2022 for more convenient dev in vscode.
|
||||
### Configure Developer Powershell for VS 2022 or Developer Powershell for VS for more convenient dev in vscode.
|
||||
1. Configure profile in in settings, entry: "terminal.integrated.profiles.windows"
|
||||
2. Add below config as entry:
|
||||
2. Add below config as entry (choose VS 2022 or VS 2026 based on your installation):
|
||||
|
||||
**For Visual Studio 2022:**
|
||||
```json
|
||||
"Developer PowerShell for VS 2022": {
|
||||
// Configure based on your preference
|
||||
@@ -27,16 +29,35 @@ VS Code extensions Needed:
|
||||
"-Command",
|
||||
"& {",
|
||||
"$orig = Get-Location;",
|
||||
// Configure based on your environment
|
||||
// Adjust path based on your edition (Community/Professional/Enterprise)
|
||||
"& 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\Common7\\Tools\\Launch-VsDevShell.ps1';",
|
||||
"Set-Location $orig",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
```
|
||||
3. [Optional] Set Developer PowerShell for VS 2022 as your default profile, so that you can get a deep integration with vscode coding agent.
|
||||
|
||||
4. Now You can build with plain `msbuild` or configure tasks.json in below section
|
||||
**For Visual Studio 2026:**
|
||||
```json
|
||||
"Developer PowerShell for VS": {
|
||||
// Configure based on your preference
|
||||
"path": "C:\\Program Files\\WindowsApps\\Microsoft.PowerShell_7.5.2.0_arm64__8wekyb3d8bbwe\\pwsh.exe",
|
||||
"args": [
|
||||
"-NoExit",
|
||||
"-Command",
|
||||
"& {",
|
||||
"$orig = Get-Location;",
|
||||
// Adjust path based on your edition (Community/Professional/Enterprise)
|
||||
"& 'C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\Common7\\Tools\\Launch-VsDevShell.ps1';",
|
||||
"Set-Location $orig",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
3. [Optional] Set your Developer PowerShell profile as the default, so that you can get a deep integration with vscode coding agent.
|
||||
|
||||
4. Now you can build with plain `msbuild` or configure tasks.json in below section.
|
||||
Or reach out to "tools\build\BUILD-GUIDELINES.md"
|
||||
|
||||
### Sample plain msbuild command
|
||||
|
||||
311
doc/devdocs/development/new-powertoy.md
Normal file
311
doc/devdocs/development/new-powertoy.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# 🧭 Creating a new PowerToy: end-to-end developer guide
|
||||
|
||||
First of all, thank you for wanting to contribute to PowerToys. The work we do would not be possible without the support of community supporters like you.
|
||||
|
||||
This guide documents the process of building a new PowerToys utility from scratch, including architecture decisions, integration steps, and common pitfalls.
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview and prerequisites
|
||||
|
||||
A PowerToy module is a self-contained utility integrated into the PowerToys ecosystem. It can be UI-based, service-based, or both.
|
||||
|
||||
### Requirements
|
||||
|
||||
- [Visual Studio 2026](https://visualstudio.microsoft.com/downloads/) and the following workloads/individual components:
|
||||
- Desktop Development with C++
|
||||
- WinUI application development
|
||||
- .NET desktop development
|
||||
- Windows 10 SDK (10.0.22621.0)
|
||||
- Windows 11 SDK (10.0.26100.3916)
|
||||
- .NET 8 SDK
|
||||
- Fork the [PowerToys repository](https://github.com/microsoft/PowerToys/tree/main) locally
|
||||
- [Validate that you are able to build and run](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/development/debugging.md) `PowerToys.slnx`.
|
||||
|
||||
Optional:
|
||||
- [WiX v5 toolset](https://github.com/microsoft/PowerToys/tree/main) for the installer
|
||||
|
||||
> [!NOTE]
|
||||
> To ensure all the correct VS Workloads are installed, use [the WinGet configuration files](https://github.com/microsoft/PowerToys/tree/e13d6a78aafbcf32a4bb5f8581d041e1d057c3f1/.config) in the project repository. (Use the one that matches your VS distribution. ie: VS Community would use `configuration.winget`)
|
||||
|
||||
### Folder structure
|
||||
|
||||
```
|
||||
src/
|
||||
modules/
|
||||
your_module/
|
||||
YourModule.sln
|
||||
YourModuleInterface/
|
||||
YourModuleUI/ (if needed)
|
||||
YourModuleService/ (if needed)
|
||||
```
|
||||
|
||||
---
|
||||
## 2. Design and planning
|
||||
|
||||
### Decide the type of module
|
||||
|
||||
Think about how your module works and which existing modules behave similarly. You are going to want to think about the UI needed for the application, the lifecycle, whether it is a service that is always running or event based. Below are some basic scenarios with some modules to explore. You can write your application in C++ or C#.
|
||||
- **UI-only:** e.g., ColorPicker
|
||||
- **Background service:** e.g., LightSwitch, Awake
|
||||
- **Hybrid (UI + background logic):** e.g., ShortcutGuide
|
||||
- **C++/C# interop:** e.g., PowerRename
|
||||
|
||||
### Write your module interface
|
||||
|
||||
Begin by setting up the [PowerToy module template project](https://github.com/microsoft/PowerToys/tree/main/tools/project_template). This will generate boilerplate for you to begin your new module. Below are the key headers in the Module Interface (`dllmain.cpp`) and an explanation of their purpose:
|
||||
1. This is where module settings are defined. These can be anything from strings, bools, ints, and even custom Enums.
|
||||
```c++
|
||||
struct ModuleSettings {};
|
||||
```
|
||||
|
||||
2. This is the header for the full class. It inherits the PowerToyModuleIface
|
||||
```c++
|
||||
class ModuleInterface : public PowertoyModuleIface
|
||||
{
|
||||
private:
|
||||
// the private members of the class
|
||||
// Can include the enabled variable, logic for event handlers, or hotkeys.
|
||||
public:
|
||||
// the public members of the class
|
||||
// Will include the constructor and initialization logic.
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Many of the class functions are boilerplate and need simple string replacements with your module name. The rest of the functions below will require bigger changes.
|
||||
|
||||
3. GPO stands for "Group Policy Object" and allows for administrators to configure settings across a network of machines. It is required that your module is on this list of settings. You can right click the `powertoys_gpo` object to go to the definition and set up the `getConfiguredModuleEnabledValue` for your module.
|
||||
```c++
|
||||
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
|
||||
{
|
||||
return powertoys_gpo::getConfiguredModuleEnabledValue();
|
||||
}
|
||||
```
|
||||
|
||||
4. `init_settings()` initializes the settings for the interface. Will either pull from existing settings.json or use defaults.
|
||||
```c++
|
||||
void ModuleInterface::init_settings()
|
||||
```
|
||||
|
||||
5. `get_config` retrieves the settings from the settings.json file.
|
||||
```c++
|
||||
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
|
||||
```
|
||||
|
||||
6. `set_config` sets the new settings to the settings.json file.
|
||||
```c++
|
||||
virtual void set_config(const wchar_t* config) override
|
||||
```
|
||||
|
||||
7. `call_custom_action` allows custom actions to be called based on signals from the settings app.
|
||||
```c++
|
||||
void call_custom_action(const wchar_t* action) override
|
||||
```
|
||||
|
||||
8. Lifecycle events control whether the module is enabled or not, as well as the default status of the module.
|
||||
```c++
|
||||
virtual void enable() // starts the module
|
||||
virtual void disable() // terminates the module and performs any cleanup
|
||||
virtual bool is_enabled() // returns if the module is currently enabled
|
||||
virtual bool is_enabled_by_default() const override // allows the module to dictate whether it should be enabled by default in the PowerToys app.
|
||||
```
|
||||
|
||||
9. Hotkey functions control the status of the hotkey.
|
||||
```c++
|
||||
// takes the hotkey from settings into a format that the interface can understand
|
||||
void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
|
||||
|
||||
// returns the hotkeys from settings
|
||||
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
|
||||
|
||||
// performs logic when the hotkey event is fired
|
||||
virtual bool on_hotkey(size_t hotkeyId) override
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Keep module logic isolated under `/modules/<YourModule>`
|
||||
- Use shared utilities from [`common`](https://github.com/microsoft/PowerToys/tree/main/src/common) instead of cross-module dependencies
|
||||
- init/set/get config use preset functions to access the settings. Check out the [`settings_objects.h`](https://github.com/microsoft/PowerToys/blob/main/src/common/SettingsAPI/settings_helpers.h) in `src\common\SettingsAPI`
|
||||
|
||||
---
|
||||
## 3. Bootstrapping your module
|
||||
|
||||
1. Use the [template](https://github.com/microsoft/PowerToys/tree/main/tools/project_template) to generate the module interface starter code.
|
||||
2. Update all projects and namespaces with your module name.
|
||||
3. Update GUIDs in `.vcxproj` and solution files.
|
||||
4. Update the functions mentioned in the above section with your custom logic.
|
||||
5. In order for your module to be detected by the runner you are required to add references to various lists. In order to register your module, add the corresponding module reference to the lists that can be found in the following files. (Hint: search other modules names to find the lists quicker)
|
||||
- `src/runner/modules.h`
|
||||
- `src/runner/modules.cpp`
|
||||
- `src/runner/resource.h`
|
||||
- `src/runner/settings_window.h`
|
||||
- `src/runner/settings_window.cpp`
|
||||
- `src/runner/main.cpp`
|
||||
- `src/common/logger.h` (for logging)
|
||||
6. ModuleInterface should build your `ModuleInterface.dll`. This will allow the runner to interact with your service.
|
||||
|
||||
> [!TIP]
|
||||
> Mismatched module IDs are one of the most common causes of load failures. Keep your ID consistent across manifest, registry, and service.
|
||||
|
||||
---
|
||||
## 4. Write your service
|
||||
|
||||
This is going to look different for every PowerToy. It may be easier to develop the application independently, and then link in the PowerToys settings logic later. But you have to write the service first, before connecting it to the runner.
|
||||
|
||||
### Notes
|
||||
|
||||
- This is a separate project from the Module Interface.
|
||||
- You can develop this project using C# or C++.
|
||||
- Set the service icon using the `.rc` file.
|
||||
- Set the service name in the `.vcxproj` by setting the `<TargetName>`
|
||||
```
|
||||
<PropertyGroup>
|
||||
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
|
||||
<TargetName>PowerToys.LightSwitchService</TargetName>
|
||||
</PropertyGroup>
|
||||
```
|
||||
- To view the code of the `.vcxproj`, right click the item and select **Unload project**
|
||||
- Use the following functions to interact with settings from your service
|
||||
```
|
||||
ModuleSettings::instance().InitFileWatcher();
|
||||
ModuleSettings::instance().LoadSettings();
|
||||
auto& settings = ModuleSettings::instance().settings();
|
||||
```
|
||||
These come from the `ModuleSettings.h` file that lives with the Service. You can copy this from another module (e.g., Light Switch) and adjust to fit your needs.
|
||||
|
||||
If your module has a user interface:
|
||||
- Use the **WinUI Blank App** template when setting up your project
|
||||
- Use [Windows design best practices](https://learn.microsoft.com/windows/apps/design/basics/)
|
||||
- Use the [WinUI 3 Gallery](https://apps.microsoft.com/detail/9p3jfpwwdzrc) for help with your UI code, and additional guidance.
|
||||
|
||||
## 5. Settings integration
|
||||
|
||||
PowerToys settings are stored per-module as JSON under:
|
||||
|
||||
```
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\<module>\settings.json
|
||||
```
|
||||
|
||||
### Implementation steps
|
||||
|
||||
- In `src\settings-ui\Settings.UI.Library\` create `<module>Properties.cs` and `<module>Settings.cs`
|
||||
- `<module>Properties.cs` is where you will define your defaults. Every setting needs to be represented here. This should match what was set in the Module Interface.
|
||||
- `<module>Settings.cs`is where your settings.json will be built from. The structure should match the following
|
||||
```cs
|
||||
public ModuleSettings()
|
||||
{
|
||||
Name = ModuleName;
|
||||
Version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
|
||||
Properties = new ModuleProperties(); // settings properties you set above.
|
||||
}
|
||||
```
|
||||
|
||||
- In `src\settings-ui\Settings.UI\ViewModels` create `<module>ViewModel.cs` this is where the interaction happens between your settings page in the PowerToys app and the settings file that is stored on the device. Changes here will trigger the settings watcher via a `NotifyPropertyChanged` event.
|
||||
- Create a `SettingsPage.xaml` at `src\settings-ui\Settings.UI\SettingsXAML\Views`. This will be the page where the user interacts with the settings of your module.
|
||||
- Be sure to use resource strings for user facing strings so they can be localized. (`x:Uid` connects to Resources.resw)
|
||||
```xaml
|
||||
// LightSwitch.xaml
|
||||
<ComboBoxItem
|
||||
x:Uid="LightSwitch_ModeOff"
|
||||
AutomationProperties.AutomationId="OffCBItem_LightSwitch"
|
||||
Tag="Off" />
|
||||
|
||||
// Resources.resw
|
||||
<data name="LightSwitch_ModeOff.Content" xml:space="preserve">
|
||||
<value>Off</value>
|
||||
</data>
|
||||
```
|
||||
> [!IMPORTANT]
|
||||
> In the above example we use `.Content` to target the content of the Combobox. This can change per UI element (e.g., `.Text`, `.Header`, etc.)
|
||||
|
||||
> **Reminder:** Manual changes via external editors (VS Code, Notepad) do **not** trigger the settings watcher. Only changes written through PowerToys trigger reloads.
|
||||
|
||||
---
|
||||
|
||||
### Gotchas:
|
||||
|
||||
- Only use the WinUI 3 framework, _not_ UWP.
|
||||
- Use [`DispatcherQueue`](https://learn.microsoft.com/windows/apps/develop/dispatcherqueue) when updating UI from non-UI threads.
|
||||
|
||||
---
|
||||
## 6. Building and debugging
|
||||
|
||||
### Debugging steps
|
||||
|
||||
1. If this is your first time debugging PowerToys, be sure to follow [these steps first](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/development/debugging.md#pre-debugging-setup).
|
||||
2. Set "runner" as the start up project and ensure your build configuration is set to match your system (ARM64/x64)
|
||||
3. Select <kbd>F5</kbd> or the **Local Windows Debugger** button to begin debugging. This should start the PowerToys runner.
|
||||
4. To set breakpoints in your service, select Ctrl+Alt+P and search for your service to attach to the runner.
|
||||
5. Use logs to document changes. The logs live at `%LOCALAPPDATA%\Microsoft\PowerToys\RunnerLogs` and `%LOCALAPPDATA%\Microsoft\PowerToys\Module\Service\<version>` for the specific module.
|
||||
|
||||
> [!TIP]
|
||||
> PowerToys caches `.nuget` artifacts aggressively. Use `git clean -xfd` when builds behave unexpectedly.
|
||||
|
||||
---
|
||||
## 7. Installer and packaging (WiX)
|
||||
|
||||
### Add your module to installer
|
||||
|
||||
1. Install [`WixToolset.Heat`](https://www.nuget.org/packages/WixToolset.Heat/) for Wix5 via nuget
|
||||
2. Inside `installer\PowerToysInstallerVNext` add a new file for your module: `Module.wxs`
|
||||
3. Inside of this file you will need copy the format from another module (ie: Light Switch) and replace the strings and GUID values.
|
||||
4. The key part will be `<!--ModuleNameFiles_Component_Def-->` which is a placeholder for code that will be generated by `generateFileComponents.ps1`.
|
||||
5. Inside `Product.wxs` add a line item in the `<Feature Id="CoreFeature" ... >` section. It will look like a list of ` <ComponentGroupRef Id="ModuleComponentGroup" />` items.
|
||||
6. Inside `generateFileComponents.ps1` you will need to add an entry to the bottom for your new module. It will follow the following format. `-fileListName <Module>Files` will match the string you set in `Module.wxs`, `<ModuleServiceName>` will match the name of your exe.
|
||||
```bash
|
||||
# Module Name
|
||||
Generate-FileList -fileDepsJson "" -fileListName <Module>Files -wxsFilePath $PSScriptRoot\<Module>.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\<ModuleServiceName>"
|
||||
Generate-FileComponents -fileListName "<Module>Files" -wxsFilePath $PSScriptRoot\<Module>.wxs -regroot $registryroot
|
||||
```
|
||||
---
|
||||
## 8. Testing and validation
|
||||
|
||||
### UI tests
|
||||
|
||||
- Place under `/modules/<YourModule>/Tests`
|
||||
- Create a new [WinUI Unit Test App](https://learn.microsoft.com/windows/apps/winui/winui3/testing/create-winui-unit-test-project)
|
||||
- Write unit tests following the format from previous modules (ie: Light Switch). This can be to test your standalone UI (if you're a module like Color Picker) or to verify that the Settings UI in the PowerToys app is controlling your service.
|
||||
|
||||
### Manual validation
|
||||
|
||||
- Enable/disable in PowerToys Settings
|
||||
- Check initialization in logs
|
||||
- Confirm icons, tooltips, and OOBE page appear correctly
|
||||
|
||||
### Pro tips
|
||||
|
||||
1. Validate wake/sleep and elevation states. Background modules often fail silently after resume if event handles aren’t recreated.
|
||||
2. Use Windows Sandbox to simulate clean install environments
|
||||
3. To simulate a "new user" you can delete the PowerToys folder from `%LOCALAPPDATA%\Microsoft`
|
||||
|
||||
### Shortcut conflict detection
|
||||
|
||||
If your module has a shortcut, ensure that it is properly registered following [the steps listed in the documentation](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/core/settings/settings-implementation.md#shortcut-conflict-detection) for conflict detection.
|
||||
|
||||
---
|
||||
## 9. The final touches
|
||||
|
||||
### Out-of-Box experience (OOBE) page
|
||||
|
||||
The OOBE page is a custom settings page that gives the user at a glance information about each module. This window opens before the Settings application for new users and after updates. Create `OOBE<ModuleName>.xaml` at `src\settings-ui\Settings.UI\SettingsXAML\OOBE\Views`. You will also need to add your module name to the enum at `src\settings-ui\Settings.UI\OOBE\Enums\PowerToysModules.cs`.
|
||||
|
||||
### Module assets
|
||||
|
||||
Now that your PowerToy is _done_ you can start to think about the assets that will represent your module.
|
||||
- Module Icon: This will be displayed in a number of places: OOBE page, in the README, on the home screen of PowerToys, on your individual module settings page, etc.
|
||||
- Module Image: This is the image you see at the top of each individual settings page.
|
||||
- OOBE Image: This is the header you see on the OOBE page for each module
|
||||
|
||||
> [!NOTE]
|
||||
> This step is something that the Design team will handle internally to ensure consistency throughout the application. If you have ideas or recommendations on what the icon or screenshots should be for your module feel free to leave it in the "Additional Comments" section of the PR and the team will take it into consideration.
|
||||
|
||||
### Documentation
|
||||
|
||||
There are two types of documentation that will be required when submitting a new PowerToy:
|
||||
1. Developer documentation: This will live in the [PowerToys repo](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/modules) at `/doc/devdocs/modules/` and should tell a developer how to work on your app. It should outline the module architecture, key files, testing, and tips on debugging if necessary.
|
||||
2. Microsoft Learn documentation: When your new Module is ready to be merged into the PowerToys repository, an internal team member will create Microsoft Learn documentation so that users will understand how to use your module. There is not much work on your end as the developer for this step, but keep an eye on your PR in case we need more information about your PowerToy for this step.
|
||||
|
||||
---
|
||||
Thank you again for contributing! If you need help, feel free to [open an issue](https://github.com/microsoft/PowerToys/issues/new/choose) and use the `Needs-Team-Response` label so we know you need attention.
|
||||
@@ -18,13 +18,28 @@ Advanced Paste is a PowerToys module that provides enhanced clipboard pasting wi
|
||||
|
||||
TODO: Add implementation details
|
||||
|
||||
### Paste with AI Preview
|
||||
|
||||
The "Show preview" setting (`ShowCustomPreview`) controls whether AI-generated results are displayed in a preview window before pasting. **The preview feature does not consume additional AI credits**—the preview displays the same AI response that was already generated, cached locally from a single API call.
|
||||
|
||||
The implementation flow:
|
||||
1. User initiates "Paste with AI" action
|
||||
2. A single AI API call is made via `ExecutePasteFormatAsync`
|
||||
3. The result is cached in `GeneratedResponses`
|
||||
4. If preview is enabled, the cached result is displayed in the preview UI
|
||||
5. User can paste the cached result without any additional API calls
|
||||
|
||||
See the `ExecutePasteFormatAsync(PasteFormat, PasteActionSource)` method in `OptionsViewModel.cs` for the implementation.
|
||||
|
||||
## Debugging
|
||||
|
||||
TODO: Add debugging information
|
||||
|
||||
## Settings
|
||||
|
||||
TODO: Add settings documentation
|
||||
| Setting | Description |
|
||||
|---------|-------------|
|
||||
| `ShowCustomPreview` | When enabled, shows AI-generated results in a preview window before pasting. Does not affect AI credit consumption. |
|
||||
|
||||
## Future Improvements
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ FancyZones is divided into several projects:
|
||||
## Development Environment Setup
|
||||
|
||||
### Prerequisites
|
||||
- Visual Studio 2022: Required for building and debugging
|
||||
- Visual Studio 2022 or 2026: Required for building and debugging
|
||||
- Windows 10 SDK: Ensure the latest version is installed
|
||||
- PowerToys Repository: Clone from GitHub
|
||||
|
||||
@@ -183,7 +183,7 @@ FancyZones is divided into several projects:
|
||||
## Debugging
|
||||
|
||||
### Setup for Debugging
|
||||
1. In Visual Studio 2022, set FancyZonesEditor as the startup project
|
||||
1. In Visual Studio 2022 or 2026, set FancyZonesEditor as the startup project
|
||||
2. Set breakpoints in the code where needed
|
||||
3. Click Run to start debugging
|
||||
|
||||
|
||||
1616
doc/devdocs/modules/powerdisplay/design.md
Normal file
1616
doc/devdocs/modules/powerdisplay/design.md
Normal file
File diff suppressed because it is too large
Load Diff
223
doc/devdocs/modules/powerdisplay/mccsParserDesign.md
Normal file
223
doc/devdocs/modules/powerdisplay/mccsParserDesign.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# MCCS Capabilities String Parser - Recursive Descent Design
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the recursive descent parser implementation for DDC/CI MCCS (Monitor Control Command Set) capabilities strings.
|
||||
|
||||
### Attention!
|
||||
This document and the code implement are generated by Copilot.
|
||||
|
||||
## Grammar Definition (BNF)
|
||||
|
||||
```bnf
|
||||
capabilities ::= ['('] segment* [')']
|
||||
segment ::= identifier '(' segment_content ')'
|
||||
segment_content ::= text | vcp_entries | hex_list
|
||||
vcp_entries ::= vcp_entry*
|
||||
vcp_entry ::= hex_byte [ '(' hex_list ')' ]
|
||||
hex_list ::= hex_byte*
|
||||
hex_byte ::= [0-9A-Fa-f]{2}
|
||||
identifier ::= [a-z_A-Z]+
|
||||
text ::= [^()]+
|
||||
```
|
||||
|
||||
## Example Input
|
||||
|
||||
```
|
||||
(prot(monitor)type(lcd)model(PD3220U)cmds(01 02 03 07)vcp(10 12 14(04 05 06) 16 60(11 12 0F) DC DF)mccs_ver(2.2)vcpname(F0(Custom Setting)))
|
||||
```
|
||||
|
||||
## Parser Architecture
|
||||
|
||||
### Component Hierarchy
|
||||
|
||||
```
|
||||
MccsCapabilitiesParser (main parser)
|
||||
├── ParseCapabilities() → MccsParseResult
|
||||
├── ParseSegment() → ParsedSegment?
|
||||
├── ParseBalancedContent() → string
|
||||
├── ParseIdentifier() → ReadOnlySpan<char>
|
||||
├── ApplySegment() → void
|
||||
│ ├── ParseHexList() → List<byte>
|
||||
│ ├── ParseVcpEntries() → Dictionary<byte, VcpCodeInfo>
|
||||
│ └── ParseVcpNames() → void
|
||||
│
|
||||
├── VcpEntryParser (sub-parser for vcp() content)
|
||||
│ └── TryParseEntry() → VcpEntry
|
||||
│
|
||||
├── VcpNameParser (sub-parser for vcpname() content)
|
||||
│ └── TryParseEntry() → (byte code, string name)
|
||||
│
|
||||
└── WindowParser (sub-parser for windowN() content)
|
||||
├── Parse() → WindowCapability
|
||||
└── ParseSubSegment() → (name, content)?
|
||||
```
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **ref struct for Zero Allocation**
|
||||
- Main parser uses `ref struct` to avoid heap allocation
|
||||
- Works with `ReadOnlySpan<char>` for efficient string slicing
|
||||
- No intermediate string allocations during parsing
|
||||
|
||||
2. **Recursive Descent Pattern**
|
||||
- Each grammar rule has a corresponding parse method
|
||||
- Methods call each other recursively for nested structures
|
||||
- Single-character lookahead via `Peek()`
|
||||
|
||||
3. **Error Recovery**
|
||||
- Errors are accumulated, not thrown
|
||||
- Parser attempts to continue after errors
|
||||
- Returns partial results when possible
|
||||
|
||||
4. **Sub-parsers for Specialized Content**
|
||||
- `VcpEntryParser` for VCP code entries
|
||||
- `VcpNameParser` for custom VCP names
|
||||
- Each sub-parser handles its own grammar subset
|
||||
|
||||
## Parse Methods Detail
|
||||
|
||||
### ParseCapabilities()
|
||||
Entry point. Handles optional outer parentheses and iterates through segments.
|
||||
|
||||
```csharp
|
||||
private MccsParseResult ParseCapabilities()
|
||||
{
|
||||
// Handle optional outer parens
|
||||
// while (!IsAtEnd()) { ParseSegment() }
|
||||
// Return result with accumulated errors
|
||||
}
|
||||
```
|
||||
|
||||
### ParseSegment()
|
||||
Parses a single `identifier(content)` segment.
|
||||
|
||||
```csharp
|
||||
private ParsedSegment? ParseSegment()
|
||||
{
|
||||
// 1. ParseIdentifier()
|
||||
// 2. Expect '('
|
||||
// 3. ParseBalancedContent()
|
||||
// 4. Expect ')'
|
||||
}
|
||||
```
|
||||
|
||||
### ParseBalancedContent()
|
||||
Extracts content between balanced parentheses, handling nested parens.
|
||||
|
||||
```csharp
|
||||
private string ParseBalancedContent()
|
||||
{
|
||||
int depth = 1;
|
||||
while (depth > 0) {
|
||||
if (char == '(') depth++;
|
||||
if (char == ')') depth--;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ParseVcpEntries()
|
||||
Delegates to `VcpEntryParser` for the specialized VCP entry grammar.
|
||||
|
||||
```csharp
|
||||
vcp_entry ::= hex_byte [ '(' hex_list ')' ]
|
||||
|
||||
Examples:
|
||||
- "10" → code=0x10, values=[]
|
||||
- "14(04 05 06)" → code=0x14, values=[4, 5, 6]
|
||||
- "60(11 12 0F)" → code=0x60, values=[0x11, 0x12, 0x0F]
|
||||
```
|
||||
|
||||
## Comparison with Other Approaches
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
|----------|------|------|
|
||||
| **Recursive Descent** (this) | Clear structure, handles nesting, extensible | More code |
|
||||
| **Regex** (DDCSharp) | Concise | Hard to debug, limited nesting |
|
||||
| **Mixed** (original) | Pragmatic | Inconsistent, hard to maintain |
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
- **Time Complexity**: O(n) where n = input length
|
||||
- **Space Complexity**: O(1) for parsing + O(m) for output where m = number of VCP codes
|
||||
- **Allocations**: Minimal - only for output structures
|
||||
|
||||
## Supported Segments
|
||||
|
||||
| Segment | Description | Parser |
|
||||
|---------|-------------|--------|
|
||||
| `prot(...)` | Protocol type | Direct assignment |
|
||||
| `type(...)` | Display type (lcd/crt) | Direct assignment |
|
||||
| `model(...)` | Model name | Direct assignment |
|
||||
| `cmds(...)` | Supported commands | ParseHexList |
|
||||
| `vcp(...)` | VCP code entries | VcpEntryParser |
|
||||
| `mccs_ver(...)` | MCCS version | Direct assignment |
|
||||
| `vcpname(...)` | Custom VCP names | VcpNameParser |
|
||||
| `windowN(...)` | PIP/PBP window capabilities | WindowParser |
|
||||
|
||||
### Window Segment Format
|
||||
|
||||
The `windowN` segment (where N is 1, 2, 3, etc.) describes PIP/PBP window capabilities:
|
||||
|
||||
```
|
||||
window1(type(PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10))
|
||||
```
|
||||
|
||||
| Sub-field | Format | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `type` | `type(PIP)` or `type(PBP)` | Window type (Picture-in-Picture or Picture-by-Picture) |
|
||||
| `area` | `area(x1 y1 x2 y2)` | Window area coordinates in pixels |
|
||||
| `max` | `max(width height)` | Maximum window dimensions |
|
||||
| `min` | `min(width height)` | Minimum window dimensions |
|
||||
| `window` | `window(id)` | Window identifier |
|
||||
|
||||
All sub-fields are optional; missing fields default to zero values.
|
||||
|
||||
## Error Handling
|
||||
|
||||
```csharp
|
||||
public readonly struct ParseError
|
||||
{
|
||||
public int Position { get; } // Character position
|
||||
public string Message { get; } // Human-readable error
|
||||
}
|
||||
|
||||
public sealed class MccsParseResult
|
||||
{
|
||||
public VcpCapabilities Capabilities { get; }
|
||||
public IReadOnlyList<ParseError> Errors { get; }
|
||||
public bool HasErrors => Errors.Count > 0;
|
||||
public bool IsValid => !HasErrors && Capabilities.SupportedVcpCodes.Count > 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Example
|
||||
|
||||
```csharp
|
||||
// Parse capabilities string
|
||||
var result = MccsCapabilitiesParser.Parse(capabilitiesString);
|
||||
|
||||
if (result.IsValid)
|
||||
{
|
||||
var caps = result.Capabilities;
|
||||
Console.WriteLine($"Model: {caps.Model}");
|
||||
Console.WriteLine($"MCCS Version: {caps.MccsVersion}");
|
||||
Console.WriteLine($"VCP Codes: {caps.SupportedVcpCodes.Count}");
|
||||
}
|
||||
|
||||
if (result.HasErrors)
|
||||
{
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
Console.WriteLine($"Parse error at {error.Position}: {error.Message}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Edge Cases Handled
|
||||
|
||||
1. **Missing outer parentheses** (Apple Cinema Display)
|
||||
2. **No spaces between hex bytes** (`010203` vs `01 02 03`)
|
||||
3. **Nested parentheses** in VCP values
|
||||
4. **Unknown segments** (logged but not fatal)
|
||||
5. **Malformed input** (partial results returned)
|
||||
@@ -68,6 +68,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
|
||||
- Add the `In progress` label to the issue, if not already present. Also add a `Cost-Small/Medium/Large` estimate and make sure all appropriate labels are set.
|
||||
- If you are a community contributor, you will not be able to add labels to the issue; in that case just add a comment saying that you have started work on the issue and try to give an estimate for the delivery date.
|
||||
- If the work item has a medium/large cost, using the markdown task list, list each sub item and update the list with a check mark after completing each sub item.
|
||||
- **Before opening a PR, ensure your changes build successfully locally and functionality tests pass.** This is especially important for AI-assisted (vibe coding) contributions—always verify AI-generated code works as intended. Exploratory PRs or draft PRs for discussion are exceptions.
|
||||
- When opening a PR, follow the PR template.
|
||||
- When you'd like the team to take a look (even if the work is not yet fully complete) mark the PR as 'Ready For Review' so that the team can review your work and provide comments, suggestions, and request changes. It may take several cycles, but the end result will be solid, testable, conformant code that is safe for us to merge.
|
||||
- When the PR is approved, let the owner of the PR merge it. For community contributions, the reviewer who approved the PR can also merge it.
|
||||
@@ -79,7 +80,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
|
||||
### Prerequisites for Compiling PowerToys
|
||||
|
||||
1. Windows 10 April 2018 Update (version 1803) or newer
|
||||
1. Visual Studio Community/Professional/Enterprise 2022 17.4 or newer
|
||||
1. Visual Studio Community/Professional/Enterprise 2022 17.4 or newer, or Visual Studio 2026
|
||||
1. A local clone of the PowerToys repository
|
||||
1. Enable long paths in Windows (see [Enable Long Paths](https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation#enabling-long-paths-in-windows-10-version-1607-and-later) for details)
|
||||
|
||||
|
||||
@@ -1549,7 +1549,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
}
|
||||
processes.resize(bytes / sizeof(processes[0]));
|
||||
|
||||
std::array<std::wstring_view, 44> processesToTerminate = {
|
||||
std::array<std::wstring_view, 45> processesToTerminate = {
|
||||
L"PowerToys.PowerLauncher.exe",
|
||||
L"PowerToys.Settings.exe",
|
||||
L"PowerToys.AdvancedPaste.exe",
|
||||
@@ -1565,6 +1565,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
L"PowerToys.PowerRename.exe",
|
||||
L"PowerToys.ImageResizer.exe",
|
||||
L"PowerToys.LightSwitchService.exe",
|
||||
L"PowerToys.PowerDisplay.exe",
|
||||
L"PowerToys.GcodeThumbnailProvider.exe",
|
||||
L"PowerToys.BgcodeThumbnailProvider.exe",
|
||||
L"PowerToys.PdfThumbnailProvider.exe",
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<Import Project="..\..\deps\spdlog.props" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<Project>
|
||||
<Import Project="..\..\src\Version.props" Condition="Exists('..\..\src\Version.props')" />
|
||||
<Import Project="..\..\Directory.Build.props" />
|
||||
<PropertyGroup>
|
||||
<!-- Set BaseIntermediateOutputPath for each project to avoid conflicts -->
|
||||
<BaseIntermediateOutputPath Condition="'$(MSBuildProjectName)' == 'PowerToysInstallerVNext'">obj\Installer\</BaseIntermediateOutputPath>
|
||||
|
||||
29
installer/PowerToysSetupVNext/PowerDisplay.wxs
Normal file
29
installer/PowerToysSetupVNext/PowerDisplay.wxs
Normal file
@@ -0,0 +1,29 @@
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
|
||||
xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util" >
|
||||
|
||||
<?include $(sys.CURRENTDIR)\Common.wxi?>
|
||||
|
||||
<?define PowerDisplayAssetsFiles=?>
|
||||
<?define PowerDisplayAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\PowerDisplay?>
|
||||
|
||||
<Fragment>
|
||||
<!-- Power Display -->
|
||||
<DirectoryRef Id="WinUI3AppsAssetsFolder">
|
||||
<Directory Id="PowerDisplayAssetsInstallFolder" Name="PowerDisplay" />
|
||||
</DirectoryRef>
|
||||
<DirectoryRef Id="PowerDisplayAssetsInstallFolder" FileSource="$(var.PowerDisplayAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--PowerDisplayAssetsFiles_Component_Def-->
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="PowerDisplayComponentGroup">
|
||||
<Component Id="RemovePowerDisplayFolder" Guid="B8F2E3A5-72C1-4A2D-9B3F-8E5D7C6A4F9B" Directory="PowerDisplayAssetsInstallFolder" >
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="RemovePowerDisplayFolder" Value="" KeyPath="yes"/>
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderPowerDisplayAssetsFolder" Directory="PowerDisplayAssetsInstallFolder" On="uninstall"/>
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
</Wix>
|
||||
@@ -47,6 +47,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
|
||||
call move /Y ..\..\..\NewPlus.wxs.bk ..\..\..\NewPlus.wxs
|
||||
call move /Y ..\..\..\Peek.wxs.bk ..\..\..\Peek.wxs
|
||||
call move /Y ..\..\..\PowerRename.wxs.bk ..\..\..\PowerRename.wxs
|
||||
call move /Y ..\..\..\PowerDisplay.wxs.bk ..\..\..\PowerDisplay.wxs
|
||||
call move /Y ..\..\..\Product.wxs.bk ..\..\..\Product.wxs
|
||||
call move /Y ..\..\..\RegistryPreview.wxs.bk ..\..\..\RegistryPreview.wxs
|
||||
call move /Y ..\..\..\Resources.wxs.bk ..\..\..\Resources.wxs
|
||||
@@ -123,6 +124,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
|
||||
<Compile Include="KeyboardManager.wxs" />
|
||||
<Compile Include="Peek.wxs" />
|
||||
<Compile Include="PowerRename.wxs" />
|
||||
<Compile Include="PowerDisplay.wxs" />
|
||||
<Compile Include="DscResources.wxs" />
|
||||
<Compile Include="RegistryPreview.wxs" />
|
||||
<Compile Include="Run.wxs" />
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
<ComponentGroupRef Id="LightSwitchComponentGroup" />
|
||||
<ComponentGroupRef Id="PeekComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerRenameComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
|
||||
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
|
||||
<ComponentGroupRef Id="RunComponentGroup" />
|
||||
<ComponentGroupRef Id="SettingsComponentGroup" />
|
||||
@@ -146,7 +147,7 @@
|
||||
<Custom Action="UnRegisterCmdPalPackage" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
||||
<Custom Action="UninstallCommandNotFound" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
||||
<Custom Action="UpgradeCommandNotFound" After="InstallFiles" Condition="WIX_UPGRADE_DETECTED" />
|
||||
<Custom Action="UninstallPackageIdentityMSIX" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
||||
<Custom Action="UninstallPackageIdentityMSIX" Before="RemoveFiles" Condition="Installed AND (REMOVE="ALL")" />
|
||||
<Custom Action="UninstallServicesTask" After="InstallFinalize" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
||||
<!-- TODO: Use to activate embedded MSIX -->
|
||||
<!--<Custom Action="UninstallEmbeddedMSIXTask" After="InstallFinalize">
|
||||
|
||||
@@ -367,6 +367,12 @@
|
||||
</RegistryKey>
|
||||
<File Id="BgcodePreviewHandler_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.BgcodePreviewHandler.resources.dll" />
|
||||
</Component>
|
||||
<Component Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER" Guid="$(var.CompGUIDPrefix)23">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<File Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\Microsoft.CmdPal.Ext.PowerToys.resources.dll" />
|
||||
</Component>
|
||||
<?undef IdSafeLanguage?>
|
||||
<?undef CompGUIDPrefix?>
|
||||
<?endforeach?>
|
||||
|
||||
@@ -37,14 +37,14 @@
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
@@ -68,11 +68,10 @@
|
||||
<ClCompile Include="SilentFilesInUseBAFunctions.cpp" />
|
||||
<ClCompile Include="bafunctions.cpp">
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>precomp.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="precomp.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
|
||||
|
||||
#include "precomp.h"
|
||||
#include "pch.h"
|
||||
#include "BalBaseBAFunctions.h"
|
||||
#include "BalBaseBAFunctionsProc.h"
|
||||
|
||||
@@ -18,7 +18,6 @@ public: // IBootstrapperApplication
|
||||
|
||||
BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "*** CUSTOM BA FUNCTION SYSTEM ACTIVE *** Running detect begin BA function. fCached=%d, registrationType=%d, cPackages=%u, fCancel=%d", fCached, registrationType, cPackages, *pfCancel);
|
||||
|
||||
LExit:
|
||||
return hr;
|
||||
}
|
||||
|
||||
@@ -37,7 +36,6 @@ public: // IBAFunctions
|
||||
// BalExitOnFailure(hr, "Change this message to represent real error handling.");
|
||||
//-------------------------------------------------------------------------------------------------
|
||||
|
||||
LExit:
|
||||
return hr;
|
||||
}
|
||||
|
||||
@@ -58,7 +56,7 @@ public: // IBAFunctions
|
||||
__in DWORD cFiles,
|
||||
__in_ecount_z(cFiles) LPCWSTR* rgwzFiles,
|
||||
__in int nRecommendation,
|
||||
__in BOOTSTRAPPER_FILES_IN_USE_TYPE source,
|
||||
__in BOOTSTRAPPER_FILES_IN_USE_TYPE /* source */,
|
||||
__inout int* pResult
|
||||
)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
|
||||
|
||||
#include "precomp.h"
|
||||
#include "pch.h"
|
||||
|
||||
static HINSTANCE vhInstance = NULL;
|
||||
|
||||
|
||||
@@ -176,6 +176,10 @@ Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PS
|
||||
Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService"
|
||||
Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs
|
||||
|
||||
#PowerDisplay
|
||||
Generate-FileList -fileDepsJson "" -fileListName PowerDisplayAssetsFiles -wxsFilePath $PSScriptRoot\PowerDisplay.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerDisplay"
|
||||
Generate-FileComponents -fileListName "PowerDisplayAssetsFiles" -wxsFilePath $PSScriptRoot\PowerDisplay.wxs
|
||||
|
||||
#New+
|
||||
Generate-FileList -fileDepsJson "" -fileListName NewPlusAssetsFiles -wxsFilePath $PSScriptRoot\NewPlus.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\NewPlus"
|
||||
Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
</PropertyGroup>
|
||||
<Import Project="..\..\deps\expected.props" />
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
As a temporary workaround, create a .NET 8 project and use file links
|
||||
to include the code that needs testing. -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -55,26 +55,26 @@
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Utility</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Utility</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>Utility</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>Utility</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
</PropertyGroup>
|
||||
<Import Project="..\..\deps\expected.props" />
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<GenerateManifest>false</GenerateManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -3,9 +3,27 @@
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace ExprtkCalculator::internal
|
||||
{
|
||||
static double factorial(const double n)
|
||||
{
|
||||
// Only allow non-negative integers
|
||||
if (n < 0.0 || std::floor(n) != n)
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
return std::tgamma(n + 1.0);
|
||||
}
|
||||
|
||||
static double sign(const double n)
|
||||
{
|
||||
if (n > 0.0) return 1.0;
|
||||
if (n < 0.0) return -1.0;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
std::wstring ToWStringFullPrecision(double value)
|
||||
{
|
||||
@@ -25,6 +43,9 @@ namespace ExprtkCalculator::internal
|
||||
symbol_table.add_constant(name, value);
|
||||
}
|
||||
|
||||
symbol_table.add_function("factorial", factorial);
|
||||
symbol_table.add_function("sign", sign);
|
||||
|
||||
exprtk::expression<double> expression;
|
||||
expression.register_symbol_table(symbol_table);
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ namespace Common.UI
|
||||
NewPlus,
|
||||
CmdPal,
|
||||
ZoomIt,
|
||||
PowerDisplay,
|
||||
}
|
||||
|
||||
private static string SettingsWindowNameToString(SettingsWindow value)
|
||||
@@ -115,6 +116,8 @@ namespace Common.UI
|
||||
return "CmdPal";
|
||||
case SettingsWindow.ZoomIt:
|
||||
return "ZoomIt";
|
||||
case SettingsWindow.PowerDisplay:
|
||||
return "PowerDisplay";
|
||||
default:
|
||||
{
|
||||
return string.Empty;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<ProjectGuid>{CABA8DFB-823B-4BF2-93AC-3F31984150D9}</ProjectGuid>
|
||||
@@ -10,7 +11,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
@@ -39,5 +40,18 @@
|
||||
<ClCompile Include="monitors.cpp" />
|
||||
<ClCompile Include="dpi_aware.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
</ImportGroup>
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
4
src/common/Display/packages.config
Normal file
4
src/common/Display/packages.config
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -32,6 +32,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredLightSwitchEnabledValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetConfiguredPowerDisplayEnabledValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredPowerDisplayEnabledValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetConfiguredFancyZonesEnabledValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredFancyZonesEnabledValue());
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredPowerDisplayEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace PowerToys
|
||||
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredPowerDisplayEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<GenerateManifest>false</GenerateManifest>
|
||||
<DesktopCompatible>true</DesktopCompatible>
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace ManagedCommon
|
||||
PowerRename,
|
||||
PowerLauncher,
|
||||
PowerAccent,
|
||||
PowerDisplay,
|
||||
RegistryPreview,
|
||||
MeasureTool,
|
||||
ShortcutGuide,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseOfMfc>false</UseOfMfc>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\tests\UnitTestsCommonLib\</OutDir>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
|
||||
120
src/common/UnitTests-CommonUtils/AppMutex.Tests.cpp
Normal file
120
src/common/UnitTests-CommonUtils/AppMutex.Tests.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <appMutex.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(AppMutexTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(CreateAppMutex_ValidName_ReturnsHandle)
|
||||
{
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_1";
|
||||
auto handle = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle.get());
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_SameName_ReturnsExistingHandle)
|
||||
{
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_2";
|
||||
|
||||
auto handle1 = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle1.get());
|
||||
|
||||
auto handle2 = createAppMutex(mutexName);
|
||||
Assert::IsNull(handle2.get());
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_DifferentNames_ReturnsDifferentHandles)
|
||||
{
|
||||
std::wstring mutexName1 = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_A";
|
||||
std::wstring mutexName2 = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_B";
|
||||
|
||||
auto handle1 = createAppMutex(mutexName1);
|
||||
auto handle2 = createAppMutex(mutexName2);
|
||||
|
||||
Assert::IsNotNull(handle1.get());
|
||||
Assert::IsNotNull(handle2.get());
|
||||
Assert::AreNotEqual(handle1.get(), handle2.get());
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_EmptyName_ReturnsHandle)
|
||||
{
|
||||
// Empty name creates unnamed mutex
|
||||
auto handle = createAppMutex(L"");
|
||||
// CreateMutexW with empty string should still work
|
||||
Assert::IsTrue(true);
|
||||
// Test passes regardless - just checking it doesn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_LongName_ReturnsHandle)
|
||||
{
|
||||
// Create a long mutex name
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_";
|
||||
for (int i = 0; i < 50; ++i)
|
||||
{
|
||||
mutexName += L"LongNameSegment";
|
||||
}
|
||||
|
||||
auto handle = createAppMutex(mutexName);
|
||||
// Long names might fail, but shouldn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_SpecialCharacters_ReturnsHandle)
|
||||
{
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_Special!@#$%";
|
||||
|
||||
auto handle = createAppMutex(mutexName);
|
||||
// Some special characters might not be valid in mutex names
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_GlobalPrefix_ReturnsHandle)
|
||||
{
|
||||
// Global prefix for cross-session mutex
|
||||
std::wstring mutexName = L"Global\\TestMutex_" + std::to_wstring(GetCurrentProcessId());
|
||||
|
||||
auto handle = createAppMutex(mutexName);
|
||||
// Might require elevation, but shouldn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_LocalPrefix_ReturnsHandle)
|
||||
{
|
||||
std::wstring mutexName = L"Local\\TestMutex_" + std::to_wstring(GetCurrentProcessId());
|
||||
|
||||
auto handle = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle.get());
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_MultipleCalls_AllSucceed)
|
||||
{
|
||||
std::vector<wil::unique_mutex_nothrow> handles;
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) +
|
||||
L"_Multi_" + std::to_wstring(i);
|
||||
auto handle = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle.get());
|
||||
handles.push_back(std::move(handle));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(CreateAppMutex_ReleaseAndRecreate_Works)
|
||||
{
|
||||
std::wstring mutexName = L"TestMutex_" + std::to_wstring(GetCurrentProcessId()) + L"_Recreate";
|
||||
|
||||
auto handle1 = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle1.get());
|
||||
handle1.reset();
|
||||
|
||||
// After closing, should be able to create again
|
||||
auto handle2 = createAppMutex(mutexName);
|
||||
Assert::IsNotNull(handle2.get());
|
||||
}
|
||||
};
|
||||
}
|
||||
220
src/common/UnitTests-CommonUtils/ColorUtils.Tests.cpp
Normal file
220
src/common/UnitTests-CommonUtils/ColorUtils.Tests.cpp
Normal file
@@ -0,0 +1,220 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <color.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ColorUtilsTests)
|
||||
{
|
||||
public:
|
||||
// checkValidRGB tests
|
||||
TEST_METHOD(CheckValidRGB_ValidBlack_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#000000", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_ValidWhite_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#FFFFFF", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_ValidRed_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#FF0000", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_ValidGreen_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#00FF00", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_ValidBlue_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#0000FF", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_ValidMixed_ReturnsTrue)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#AB12CD", &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0xAB), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x12), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0xCD), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_MissingHash_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"FFFFFF", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_TooShort_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#FFF", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_TooLong_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#FFFFFFFF", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_InvalidChars_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#GGHHII", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_LowercaseInvalid_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#ffffff", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_EmptyString_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidRGB_OnlyHash_ReturnsFalse)
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
bool result = checkValidRGB(L"#", &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
// checkValidARGB tests
|
||||
TEST_METHOD(CheckValidARGB_ValidBlackOpaque_ReturnsTrue)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#FF000000", &a, &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), a);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_ValidWhiteOpaque_ReturnsTrue)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#FFFFFFFF", &a, &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), a);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_ValidTransparent_ReturnsTrue)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#00FFFFFF", &a, &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), a);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_ValidSemiTransparent_ReturnsTrue)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#80FF0000", &a, &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x80), a);
|
||||
Assert::AreEqual(static_cast<uint8_t>(255), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_ValidMixed_ReturnsTrue)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#12345678", &a, &r, &g, &b);
|
||||
Assert::IsTrue(result);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x12), a);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x34), r);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x56), g);
|
||||
Assert::AreEqual(static_cast<uint8_t>(0x78), b);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_MissingHash_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"FFFFFFFF", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_TooShort_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#FFFFFF", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_TooLong_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#FFFFFFFFFF", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_InvalidChars_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#GGHHIIJJ", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_LowercaseInvalid_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"#ffffffff", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckValidARGB_EmptyString_ReturnsFalse)
|
||||
{
|
||||
uint8_t a, r, g, b;
|
||||
bool result = checkValidARGB(L"", &a, &r, &g, &b);
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
};
|
||||
}
|
||||
228
src/common/UnitTests-CommonUtils/ComObjectFactory.Tests.cpp
Normal file
228
src/common/UnitTests-CommonUtils/ComObjectFactory.Tests.cpp
Normal file
@@ -0,0 +1,228 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <com_object_factory.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
// Test COM object for testing the factory
|
||||
class TestComObject : public IUnknown
|
||||
{
|
||||
public:
|
||||
TestComObject() : m_refCount(1) {}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override
|
||||
{
|
||||
if (riid == IID_IUnknown)
|
||||
{
|
||||
*ppvObject = static_cast<IUnknown*>(this);
|
||||
AddRef();
|
||||
return S_OK;
|
||||
}
|
||||
*ppvObject = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE AddRef() override
|
||||
{
|
||||
return InterlockedIncrement(&m_refCount);
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE Release() override
|
||||
{
|
||||
ULONG count = InterlockedDecrement(&m_refCount);
|
||||
if (count == 0)
|
||||
{
|
||||
delete this;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private:
|
||||
LONG m_refCount;
|
||||
};
|
||||
|
||||
TEST_CLASS(ComObjectFactoryTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ComObjectFactory_Construction_DoesNotCrash)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_QueryInterface_IUnknown_Succeeds)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
IUnknown* pUnknown = nullptr;
|
||||
|
||||
HRESULT hr = factory.QueryInterface(IID_IUnknown, reinterpret_cast<void**>(&pUnknown));
|
||||
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
Assert::IsNotNull(pUnknown);
|
||||
|
||||
if (pUnknown)
|
||||
{
|
||||
pUnknown->Release();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_QueryInterface_IClassFactory_Succeeds)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
IClassFactory* pFactory = nullptr;
|
||||
|
||||
HRESULT hr = factory.QueryInterface(IID_IClassFactory, reinterpret_cast<void**>(&pFactory));
|
||||
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
Assert::IsNotNull(pFactory);
|
||||
|
||||
if (pFactory)
|
||||
{
|
||||
pFactory->Release();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_QueryInterface_InvalidInterface_Fails)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
void* pInterface = nullptr;
|
||||
|
||||
// Random GUID that we don't support
|
||||
GUID randomGuid = { 0x12345678, 0x1234, 0x1234, { 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0 } };
|
||||
HRESULT hr = factory.QueryInterface(randomGuid, &pInterface);
|
||||
|
||||
Assert::AreEqual(E_NOINTERFACE, hr);
|
||||
Assert::IsNull(pInterface);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_AddRef_IncreasesRefCount)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
ULONG count1 = factory.AddRef();
|
||||
ULONG count2 = factory.AddRef();
|
||||
|
||||
Assert::IsTrue(count2 > count1);
|
||||
|
||||
// Clean up
|
||||
factory.Release();
|
||||
factory.Release();
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_Release_DecreasesRefCount)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
factory.AddRef();
|
||||
factory.AddRef();
|
||||
ULONG count1 = factory.Release();
|
||||
ULONG count2 = factory.Release();
|
||||
|
||||
Assert::IsTrue(count2 < count1);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_CreateInstance_NoAggregation_Succeeds)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
IUnknown* pObj = nullptr;
|
||||
|
||||
HRESULT hr = factory.CreateInstance(nullptr, IID_IUnknown, reinterpret_cast<void**>(&pObj));
|
||||
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
Assert::IsNotNull(pObj);
|
||||
|
||||
if (pObj)
|
||||
{
|
||||
pObj->Release();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_CreateInstance_WithAggregation_Fails)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
TestComObject outer;
|
||||
IUnknown* pObj = nullptr;
|
||||
|
||||
// Aggregation should fail for our simple test object
|
||||
HRESULT hr = factory.CreateInstance(&outer, IID_IUnknown, reinterpret_cast<void**>(&pObj));
|
||||
|
||||
Assert::AreEqual(CLASS_E_NOAGGREGATION, hr);
|
||||
Assert::IsNull(pObj);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_CreateInstance_NullOutput_Fails)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
HRESULT hr = factory.CreateInstance(nullptr, IID_IUnknown, nullptr);
|
||||
|
||||
Assert::AreEqual(E_POINTER, hr);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_LockServer_Lock_Succeeds)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
HRESULT hr = factory.LockServer(TRUE);
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
|
||||
// Unlock
|
||||
factory.LockServer(FALSE);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_LockServer_Unlock_Succeeds)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
factory.LockServer(TRUE);
|
||||
HRESULT hr = factory.LockServer(FALSE);
|
||||
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
}
|
||||
|
||||
TEST_METHOD(ComObjectFactory_LockServer_MultipleLocks_Work)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
|
||||
factory.LockServer(TRUE);
|
||||
factory.LockServer(TRUE);
|
||||
factory.LockServer(TRUE);
|
||||
|
||||
factory.LockServer(FALSE);
|
||||
factory.LockServer(FALSE);
|
||||
HRESULT hr = factory.LockServer(FALSE);
|
||||
|
||||
Assert::AreEqual(S_OK, hr);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(ComObjectFactory_ConcurrentCreateInstance_Works)
|
||||
{
|
||||
com_object_factory<TestComObject> factory;
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&factory, &successCount]() {
|
||||
IUnknown* pObj = nullptr;
|
||||
HRESULT hr = factory.CreateInstance(nullptr, IID_IUnknown, reinterpret_cast<void**>(&pObj));
|
||||
if (SUCCEEDED(hr) && pObj)
|
||||
{
|
||||
successCount++;
|
||||
pObj->Release();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(10, successCount.load());
|
||||
}
|
||||
};
|
||||
}
|
||||
146
src/common/UnitTests-CommonUtils/Elevation.Tests.cpp
Normal file
146
src/common/UnitTests-CommonUtils/Elevation.Tests.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <elevation.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ElevationTests)
|
||||
{
|
||||
public:
|
||||
// is_process_elevated tests
|
||||
TEST_METHOD(IsProcessElevated_ReturnsBoolean)
|
||||
{
|
||||
bool result = is_process_elevated(false);
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsProcessElevated_CachedValue_ReturnsSameResult)
|
||||
{
|
||||
bool result1 = is_process_elevated(true);
|
||||
bool result2 = is_process_elevated(true);
|
||||
|
||||
// Cached value should be consistent
|
||||
Assert::AreEqual(result1, result2);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsProcessElevated_UncachedValue_ReturnsBoolean)
|
||||
{
|
||||
bool result = is_process_elevated(false);
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsProcessElevated_CachedAndUncached_AreConsistent)
|
||||
{
|
||||
// Both should return the same value for the same process
|
||||
bool cached = is_process_elevated(true);
|
||||
bool uncached = is_process_elevated(false);
|
||||
|
||||
Assert::AreEqual(cached, uncached);
|
||||
}
|
||||
|
||||
// check_user_is_admin tests
|
||||
TEST_METHOD(CheckUserIsAdmin_ReturnsBoolean)
|
||||
{
|
||||
bool result = check_user_is_admin();
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(CheckUserIsAdmin_ConsistentResults)
|
||||
{
|
||||
bool result1 = check_user_is_admin();
|
||||
bool result2 = check_user_is_admin();
|
||||
bool result3 = check_user_is_admin();
|
||||
|
||||
Assert::AreEqual(result1, result2);
|
||||
Assert::AreEqual(result2, result3);
|
||||
}
|
||||
|
||||
// Relationship between elevation and admin
|
||||
TEST_METHOD(ElevationAndAdmin_Relationship)
|
||||
{
|
||||
bool elevated = is_process_elevated(false);
|
||||
bool admin = check_user_is_admin();
|
||||
(void)admin;
|
||||
|
||||
// If elevated, user should typically be admin
|
||||
// But user can be admin without process being elevated
|
||||
if (elevated)
|
||||
{
|
||||
// Elevated process usually means admin user
|
||||
// (though there are edge cases)
|
||||
}
|
||||
// Just verify both functions return without crashing
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// IsProcessOfWindowElevated tests
|
||||
TEST_METHOD(IsProcessOfWindowElevated_DesktopWindow_ReturnsBoolean)
|
||||
{
|
||||
HWND desktop = GetDesktopWindow();
|
||||
if (desktop)
|
||||
{
|
||||
bool result = IsProcessOfWindowElevated(desktop);
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(IsProcessOfWindowElevated_InvalidHwnd_DoesNotCrash)
|
||||
{
|
||||
bool result = IsProcessOfWindowElevated(nullptr);
|
||||
// Should handle null HWND gracefully
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
// ProcessInfo struct tests
|
||||
TEST_METHOD(ProcessInfo_DefaultConstruction)
|
||||
{
|
||||
ProcessInfo info{};
|
||||
Assert::AreEqual(static_cast<DWORD>(0), info.processID);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(IsProcessElevated_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
is_process_elevated(j % 2 == 0);
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
|
||||
// Performance of cached value
|
||||
TEST_METHOD(IsProcessElevated_CachedPerformance)
|
||||
{
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (int i = 0; i < 10000; ++i)
|
||||
{
|
||||
is_process_elevated(true);
|
||||
}
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||
|
||||
// Cached calls should be very fast
|
||||
Assert::IsTrue(duration.count() < 1000);
|
||||
}
|
||||
};
|
||||
}
|
||||
182
src/common/UnitTests-CommonUtils/ExcludedApps.Tests.cpp
Normal file
182
src/common/UnitTests-CommonUtils/ExcludedApps.Tests.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <excluded_apps.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ExcludedAppsTests)
|
||||
{
|
||||
public:
|
||||
// find_app_name_in_path tests
|
||||
TEST_METHOD(FindAppNameInPath_ExactMatch_ReturnsTrue)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsTrue(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_NoMatch_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"calc.exe" };
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_MultipleApps_FindsMatch)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"calc.exe", L"notepad.exe", L"word.exe" };
|
||||
Assert::IsTrue(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_EmptyPath_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_EmptyApps_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\notepad.exe";
|
||||
std::vector<std::wstring> apps = {};
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_PartialMatchInFolder_ReturnsFalse)
|
||||
{
|
||||
// "notepad" appears in folder name but not as the exe name
|
||||
std::wstring path = L"C:\\notepad\\other.exe";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_CaseSensitive_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\NOTEPAD.EXE";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
// The function does rfind which is case-sensitive
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_MatchWithDifferentExtension_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\App\\notepad.com";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_MatchAtEndOfPath_ReturnsTrue)
|
||||
{
|
||||
std::wstring path = L"C:\\Windows\\System32\\notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsTrue(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_UNCPath_Works)
|
||||
{
|
||||
std::wstring path = L"\\\\server\\share\\folder\\app.exe";
|
||||
std::vector<std::wstring> apps = { L"app.exe" };
|
||||
Assert::IsTrue(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
// find_folder_in_path tests
|
||||
TEST_METHOD(FindFolderInPath_FolderExists_ReturnsTrue)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\MyApp\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"Program Files" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_FolderNotExists_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Windows\\System32\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"Program Files" };
|
||||
Assert::IsFalse(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_MultipleFolders_FindsMatch)
|
||||
{
|
||||
std::wstring path = L"C:\\Windows\\System32\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"Program Files", L"System32", L"Users" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_EmptyPath_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"";
|
||||
std::vector<std::wstring> folders = { L"Windows" };
|
||||
Assert::IsFalse(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_EmptyFolders_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\Windows\\app.exe";
|
||||
std::vector<std::wstring> folders = {};
|
||||
Assert::IsFalse(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_PartialMatch_ReturnsTrue)
|
||||
{
|
||||
// find_folder_in_path uses rfind which finds substrings
|
||||
std::wstring path = L"C:\\Windows\\System32\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"System" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_NestedFolder_ReturnsTrue)
|
||||
{
|
||||
std::wstring path = L"C:\\Program Files\\Company\\Product\\bin\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"Product" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_RootDrive_ReturnsTrue)
|
||||
{
|
||||
std::wstring path = L"C:\\folder\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"C:\\" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_UNCPath_Works)
|
||||
{
|
||||
std::wstring path = L"\\\\server\\share\\folder\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"share" };
|
||||
Assert::IsTrue(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_CaseSensitive_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"C:\\WINDOWS\\app.exe";
|
||||
std::vector<std::wstring> folders = { L"windows" };
|
||||
// rfind is case-sensitive
|
||||
Assert::IsFalse(find_folder_in_path(path, folders));
|
||||
}
|
||||
|
||||
// Edge case tests
|
||||
TEST_METHOD(FindAppNameInPath_AppNameInMiddleOfPath_HandlesCorrectly)
|
||||
{
|
||||
// The app name appears both in folder and as filename
|
||||
std::wstring path = L"C:\\notepad\\bin\\notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
Assert::IsTrue(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindAppNameInPath_JustFilename_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"notepad.exe";
|
||||
std::vector<std::wstring> apps = { L"notepad.exe" };
|
||||
// find_app_name_in_path expects a path separator to validate the executable segment
|
||||
Assert::IsFalse(find_app_name_in_path(path, apps));
|
||||
}
|
||||
|
||||
TEST_METHOD(FindFolderInPath_JustFilename_ReturnsFalse)
|
||||
{
|
||||
std::wstring path = L"app.exe";
|
||||
std::vector<std::wstring> folders = { L"Windows" };
|
||||
Assert::IsFalse(find_folder_in_path(path, folders));
|
||||
}
|
||||
};
|
||||
}
|
||||
148
src/common/UnitTests-CommonUtils/Exec.Tests.cpp
Normal file
148
src/common/UnitTests-CommonUtils/Exec.Tests.cpp
Normal file
@@ -0,0 +1,148 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <exec.h>
|
||||
#include <cctype>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(ExecTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(ExecAndReadOutput_EchoCommand_ReturnsOutput)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c echo hello", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
// Output should contain "hello"
|
||||
Assert::IsTrue(result->find("hello") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_WhereCommand_ReturnsPath)
|
||||
{
|
||||
auto result = exec_and_read_output(L"where cmd", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
// Should contain path to cmd.exe
|
||||
Assert::IsTrue(result->find("cmd") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_DirCommand_ReturnsListing)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c dir /b C:\\Windows", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsFalse(result->empty());
|
||||
// Should contain some common Windows folder names
|
||||
std::string output = *result;
|
||||
std::transform(output.begin(), output.end(), output.begin(), [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
||||
Assert::IsTrue(output.find("system32") != std::string::npos ||
|
||||
output.find("system") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_InvalidCommand_ReturnsEmptyOrError)
|
||||
{
|
||||
auto result = exec_and_read_output(L"nonexistentcommand12345", 5000);
|
||||
|
||||
// Invalid command should either return nullopt or an error message
|
||||
Assert::IsTrue(!result.has_value() || result->empty() ||
|
||||
result->find("not recognized") != std::string::npos ||
|
||||
result->find("error") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_EmptyCommand_DoesNotCrash)
|
||||
{
|
||||
auto result = exec_and_read_output(L"", 5000);
|
||||
// Should handle empty command gracefully
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_TimeoutExpires_ReturnsAvailableOutput)
|
||||
{
|
||||
// Use a command that produces output slowly
|
||||
// ping localhost will run for a while
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
// Very short timeout
|
||||
auto result = exec_and_read_output(L"ping localhost -n 10", 100);
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start);
|
||||
|
||||
// Should return within reasonable time
|
||||
Assert::IsTrue(elapsed.count() < 5000);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_MultilineOutput_PreservesLines)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c \"echo line1 & echo line2 & echo line3\"", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
// Should contain multiple lines
|
||||
Assert::IsTrue(result->find("line1") != std::string::npos);
|
||||
Assert::IsTrue(result->find("line2") != std::string::npos);
|
||||
Assert::IsTrue(result->find("line3") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_UnicodeOutput_Works)
|
||||
{
|
||||
// Echo a simple ASCII string (Unicode test depends on system codepage)
|
||||
auto result = exec_and_read_output(L"cmd /c echo test123", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsTrue(result->find("test123") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_LongTimeout_Works)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c echo test", 60000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsTrue(result->find("test") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_QuotedArguments_Work)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c echo \"hello world\"", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsTrue(result->find("hello") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_EnvironmentVariable_Expanded)
|
||||
{
|
||||
auto result = exec_and_read_output(L"cmd /c echo %USERNAME%", 5000);
|
||||
|
||||
Assert::IsTrue(result.has_value());
|
||||
// Should not contain the literal %USERNAME% but the actual username
|
||||
// Or if not expanded, still should not crash
|
||||
Assert::IsFalse(result->empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_ExitCode_CommandFails)
|
||||
{
|
||||
// Command that exits with error
|
||||
auto result = exec_and_read_output(L"cmd /c exit 1", 5000);
|
||||
|
||||
// Should still return something (possibly empty)
|
||||
// Just verify it doesn't crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(ExecAndReadOutput_ZeroTimeout_DoesNotHang)
|
||||
{
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
auto result = exec_and_read_output(L"cmd /c echo test", 0);
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start);
|
||||
|
||||
// Should complete quickly with zero timeout
|
||||
Assert::IsTrue(elapsed.count() < 5000);
|
||||
}
|
||||
};
|
||||
}
|
||||
68
src/common/UnitTests-CommonUtils/GameMode.Tests.cpp
Normal file
68
src/common/UnitTests-CommonUtils/GameMode.Tests.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <game_mode.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(GameModeTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(DetectGameMode_ReturnsBoolean)
|
||||
{
|
||||
// This function queries Windows game mode status
|
||||
bool result = detect_game_mode();
|
||||
|
||||
// Result depends on current system state, but should be a valid boolean
|
||||
Assert::IsTrue(result == true || result == false);
|
||||
}
|
||||
|
||||
TEST_METHOD(DetectGameMode_ConsistentResults)
|
||||
{
|
||||
// Multiple calls should return consistent results (unless game mode changes)
|
||||
bool result1 = detect_game_mode();
|
||||
bool result2 = detect_game_mode();
|
||||
bool result3 = detect_game_mode();
|
||||
|
||||
// Results should be consistent across rapid calls
|
||||
Assert::AreEqual(result1, result2);
|
||||
Assert::AreEqual(result2, result3);
|
||||
}
|
||||
|
||||
TEST_METHOD(DetectGameMode_DoesNotCrash)
|
||||
{
|
||||
// Call multiple times to ensure no crash or memory leak
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
detect_game_mode();
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(DetectGameMode_ThreadSafe)
|
||||
{
|
||||
// Test that calling from multiple threads is safe
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
detect_game_mode();
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
};
|
||||
}
|
||||
218
src/common/UnitTests-CommonUtils/Gpo.Tests.cpp
Normal file
218
src/common/UnitTests-CommonUtils/Gpo.Tests.cpp
Normal file
@@ -0,0 +1,218 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <gpo.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace powertoys_gpo;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(GpoTests)
|
||||
{
|
||||
public:
|
||||
// Helper to check if result is a valid gpo_rule_configured_t value
|
||||
static constexpr bool IsValidGpoResult(gpo_rule_configured_t result)
|
||||
{
|
||||
return result == gpo_rule_configured_wrong_value ||
|
||||
result == gpo_rule_configured_unavailable ||
|
||||
result == gpo_rule_configured_not_configured ||
|
||||
result == gpo_rule_configured_disabled ||
|
||||
result == gpo_rule_configured_enabled;
|
||||
}
|
||||
|
||||
// gpo_rule_configured_t enum tests
|
||||
TEST_METHOD(GpoRuleConfigured_EnumValues_AreDistinct)
|
||||
{
|
||||
Assert::AreNotEqual(static_cast<int>(gpo_rule_configured_not_configured),
|
||||
static_cast<int>(gpo_rule_configured_enabled));
|
||||
Assert::AreNotEqual(static_cast<int>(gpo_rule_configured_enabled),
|
||||
static_cast<int>(gpo_rule_configured_disabled));
|
||||
Assert::AreNotEqual(static_cast<int>(gpo_rule_configured_not_configured),
|
||||
static_cast<int>(gpo_rule_configured_disabled));
|
||||
}
|
||||
|
||||
// getConfiguredValue tests
|
||||
TEST_METHOD(GetConfiguredValue_NonExistentKey_ReturnsNotConfigured)
|
||||
{
|
||||
auto result = getConfiguredValue(L"NonExistentPolicyValue12345");
|
||||
Assert::IsTrue(result == gpo_rule_configured_not_configured ||
|
||||
result == gpo_rule_configured_unavailable);
|
||||
}
|
||||
|
||||
// Utility enabled getters - these all follow the same pattern
|
||||
TEST_METHOD(GetAllowExperimentationValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getAllowExperimentationValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredAlwaysOnTopEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredAlwaysOnTopEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredAwakeEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredAwakeEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredColorPickerEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredColorPickerEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredFancyZonesEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredFancyZonesEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredFileLocksmithEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredFileLocksmithEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredImageResizerEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredImageResizerEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredKeyboardManagerEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredKeyboardManagerEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredPowerRenameEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredPowerRenameEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredPowerLauncherEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredPowerLauncherEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredShortcutGuideEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredShortcutGuideEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredTextExtractorEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredTextExtractorEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredHostsFileEditorEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredHostsFileEditorEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredMousePointerCrosshairsEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredMousePointerCrosshairsEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredMouseHighlighterEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredMouseHighlighterEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredMouseJumpEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredMouseJumpEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredFindMyMouseEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredFindMyMouseEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredMouseWithoutBordersEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredMouseWithoutBordersEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredAdvancedPasteEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredAdvancedPasteEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredPeekEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredPeekEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredRegistryPreviewEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredRegistryPreviewEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredScreenRulerEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredScreenRulerEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredCropAndLockEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredCropAndLockEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
TEST_METHOD(GetConfiguredEnvironmentVariablesEnabledValue_ReturnsValidState)
|
||||
{
|
||||
auto result = getConfiguredEnvironmentVariablesEnabledValue();
|
||||
Assert::IsTrue(IsValidGpoResult(result));
|
||||
}
|
||||
|
||||
// All GPO functions should not crash
|
||||
TEST_METHOD(AllGpoFunctions_DoNotCrash)
|
||||
{
|
||||
getAllowExperimentationValue();
|
||||
getConfiguredAlwaysOnTopEnabledValue();
|
||||
getConfiguredAwakeEnabledValue();
|
||||
getConfiguredColorPickerEnabledValue();
|
||||
getConfiguredFancyZonesEnabledValue();
|
||||
getConfiguredFileLocksmithEnabledValue();
|
||||
getConfiguredImageResizerEnabledValue();
|
||||
getConfiguredKeyboardManagerEnabledValue();
|
||||
getConfiguredPowerRenameEnabledValue();
|
||||
getConfiguredPowerLauncherEnabledValue();
|
||||
getConfiguredShortcutGuideEnabledValue();
|
||||
getConfiguredTextExtractorEnabledValue();
|
||||
getConfiguredHostsFileEditorEnabledValue();
|
||||
getConfiguredMousePointerCrosshairsEnabledValue();
|
||||
getConfiguredMouseHighlighterEnabledValue();
|
||||
getConfiguredMouseJumpEnabledValue();
|
||||
getConfiguredFindMyMouseEnabledValue();
|
||||
getConfiguredMouseWithoutBordersEnabledValue();
|
||||
getConfiguredAdvancedPasteEnabledValue();
|
||||
getConfiguredPeekEnabledValue();
|
||||
getConfiguredRegistryPreviewEnabledValue();
|
||||
getConfiguredScreenRulerEnabledValue();
|
||||
getConfiguredCropAndLockEnabledValue();
|
||||
getConfiguredEnvironmentVariablesEnabledValue();
|
||||
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
200
src/common/UnitTests-CommonUtils/HDropIterator.Tests.cpp
Normal file
200
src/common/UnitTests-CommonUtils/HDropIterator.Tests.cpp
Normal file
@@ -0,0 +1,200 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <HDropIterator.h>
|
||||
#include <shlobj.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(HDropIteratorTests)
|
||||
{
|
||||
public:
|
||||
// Helper to create a test HDROP structure
|
||||
static HGLOBAL CreateTestHDrop(const std::vector<std::wstring>& files)
|
||||
{
|
||||
// Calculate required size
|
||||
size_t size = sizeof(DROPFILES);
|
||||
for (const auto& file : files)
|
||||
{
|
||||
size += (file.length() + 1) * sizeof(wchar_t);
|
||||
}
|
||||
size += sizeof(wchar_t); // Double null terminator
|
||||
|
||||
HGLOBAL hGlobal = GlobalAlloc(GHND, size);
|
||||
if (!hGlobal) return nullptr;
|
||||
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
if (!pDropFiles)
|
||||
{
|
||||
GlobalFree(hGlobal);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
pDropFiles->pFiles = sizeof(DROPFILES);
|
||||
pDropFiles->fWide = TRUE;
|
||||
|
||||
wchar_t* pData = reinterpret_cast<wchar_t*>(reinterpret_cast<BYTE*>(pDropFiles) + sizeof(DROPFILES));
|
||||
for (const auto& file : files)
|
||||
{
|
||||
wcscpy_s(pData, file.length() + 1, file.c_str());
|
||||
pData += file.length() + 1;
|
||||
}
|
||||
*pData = L'\0'; // Double null terminator
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
return hGlobal;
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_EmptyDrop_IsDoneImmediately)
|
||||
{
|
||||
HGLOBAL hGlobal = CreateTestHDrop({});
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true); // Skip if allocation failed
|
||||
return;
|
||||
}
|
||||
|
||||
STGMEDIUM medium = {};
|
||||
medium.tymed = TYMED_HGLOBAL;
|
||||
medium.hGlobal = hGlobal;
|
||||
|
||||
// Without a proper IDataObject, we can't fully test
|
||||
// Just verify the class can be instantiated conceptually
|
||||
GlobalFree(hGlobal);
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_Iteration_Conceptual)
|
||||
{
|
||||
// This test verifies the concept of iteration
|
||||
// Full integration testing requires a proper IDataObject
|
||||
|
||||
std::vector<std::wstring> testFiles = {
|
||||
L"C:\\test\\file1.txt",
|
||||
L"C:\\test\\file2.txt",
|
||||
L"C:\\test\\file3.txt"
|
||||
};
|
||||
|
||||
HGLOBAL hGlobal = CreateTestHDrop(testFiles);
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify we can create the HDROP structure
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
Assert::IsNotNull(pDropFiles);
|
||||
Assert::IsTrue(pDropFiles->fWide);
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
GlobalFree(hGlobal);
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_SingleFile_Works)
|
||||
{
|
||||
std::vector<std::wstring> testFiles = { L"C:\\test\\single.txt" };
|
||||
|
||||
HGLOBAL hGlobal = CreateTestHDrop(testFiles);
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify structure
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
Assert::IsNotNull(pDropFiles);
|
||||
|
||||
// Read back the file name
|
||||
wchar_t* pData = reinterpret_cast<wchar_t*>(reinterpret_cast<BYTE*>(pDropFiles) + pDropFiles->pFiles);
|
||||
Assert::AreEqual(std::wstring(L"C:\\test\\single.txt"), std::wstring(pData));
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
GlobalFree(hGlobal);
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_MultipleFiles_Structure)
|
||||
{
|
||||
std::vector<std::wstring> testFiles = {
|
||||
L"C:\\file1.txt",
|
||||
L"C:\\file2.txt",
|
||||
L"C:\\file3.txt"
|
||||
};
|
||||
|
||||
HGLOBAL hGlobal = CreateTestHDrop(testFiles);
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
Assert::IsNotNull(pDropFiles);
|
||||
|
||||
// Count files by iterating through null-terminated strings
|
||||
wchar_t* pData = reinterpret_cast<wchar_t*>(reinterpret_cast<BYTE*>(pDropFiles) + pDropFiles->pFiles);
|
||||
int count = 0;
|
||||
while (*pData)
|
||||
{
|
||||
count++;
|
||||
pData += wcslen(pData) + 1;
|
||||
}
|
||||
|
||||
Assert::AreEqual(3, count);
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
GlobalFree(hGlobal);
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_UnicodeFilenames_Work)
|
||||
{
|
||||
std::vector<std::wstring> testFiles = {
|
||||
L"C:\\test\\file.txt"
|
||||
};
|
||||
|
||||
HGLOBAL hGlobal = CreateTestHDrop(testFiles);
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
Assert::IsTrue(pDropFiles->fWide == TRUE);
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
GlobalFree(hGlobal);
|
||||
}
|
||||
|
||||
TEST_METHOD(HDropIterator_LongFilenames_Work)
|
||||
{
|
||||
std::wstring longPath = L"C:\\";
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
longPath += L"LongFolderName\\";
|
||||
}
|
||||
longPath += L"file.txt";
|
||||
|
||||
std::vector<std::wstring> testFiles = { longPath };
|
||||
|
||||
HGLOBAL hGlobal = CreateTestHDrop(testFiles);
|
||||
if (!hGlobal)
|
||||
{
|
||||
Assert::IsTrue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
DROPFILES* pDropFiles = static_cast<DROPFILES*>(GlobalLock(hGlobal));
|
||||
Assert::IsNotNull(pDropFiles);
|
||||
|
||||
wchar_t* pData = reinterpret_cast<wchar_t*>(reinterpret_cast<BYTE*>(pDropFiles) + pDropFiles->pFiles);
|
||||
Assert::AreEqual(longPath, std::wstring(pData));
|
||||
|
||||
GlobalUnlock(hGlobal);
|
||||
GlobalFree(hGlobal);
|
||||
}
|
||||
};
|
||||
}
|
||||
152
src/common/UnitTests-CommonUtils/HttpClient.Tests.cpp
Normal file
152
src/common/UnitTests-CommonUtils/HttpClient.Tests.cpp
Normal file
@@ -0,0 +1,152 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <HttpClient.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(HttpClientTests)
|
||||
{
|
||||
public:
|
||||
// Note: Network tests may fail in offline environments
|
||||
// These tests are designed to verify the API doesn't crash
|
||||
|
||||
TEST_METHOD(HttpClient_DefaultConstruction)
|
||||
{
|
||||
http::HttpClient client;
|
||||
// Should not crash
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HttpClient_Request_InvalidUri_ReturnsEmpty)
|
||||
{
|
||||
http::HttpClient client;
|
||||
|
||||
try
|
||||
{
|
||||
// Invalid URI should not crash, may throw or return empty
|
||||
auto result = client.request(winrt::Windows::Foundation::Uri(L"invalid://not-a-valid-uri"));
|
||||
// If we get here, result may be empty or contain error
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Exception is acceptable for invalid URI
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HttpClient_Download_InvalidUri_DoesNotCrash)
|
||||
{
|
||||
http::HttpClient client;
|
||||
TestHelpers::TempFile tempFile;
|
||||
|
||||
try
|
||||
{
|
||||
auto result = client.download(
|
||||
winrt::Windows::Foundation::Uri(L"https://invalid.invalid.invalid"),
|
||||
tempFile.path());
|
||||
// May return false or throw
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Exception is acceptable for invalid/unreachable URI
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HttpClient_Download_WithCallback_DoesNotCrash)
|
||||
{
|
||||
http::HttpClient client;
|
||||
TestHelpers::TempFile tempFile;
|
||||
std::atomic<int> callbackCount{ 0 };
|
||||
|
||||
try
|
||||
{
|
||||
auto result = client.download(
|
||||
winrt::Windows::Foundation::Uri(L"https://invalid.invalid.invalid"),
|
||||
tempFile.path(),
|
||||
[&callbackCount]([[maybe_unused]] float progress) {
|
||||
callbackCount++;
|
||||
});
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Exception is acceptable
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(HttpClient_Download_EmptyPath_DoesNotCrash)
|
||||
{
|
||||
http::HttpClient client;
|
||||
|
||||
try
|
||||
{
|
||||
auto result = client.download(
|
||||
winrt::Windows::Foundation::Uri(L"https://example.com"),
|
||||
L"");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Exception is acceptable for empty path
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// These tests require network access and may be skipped in offline environments
|
||||
TEST_METHOD(HttpClient_Request_ValidUri_ReturnsResult)
|
||||
{
|
||||
// Skip this test in most CI environments
|
||||
// Only run manually to verify network functionality
|
||||
http::HttpClient client;
|
||||
|
||||
try
|
||||
{
|
||||
// Use a reliable, fast-responding URL
|
||||
auto result = client.request(winrt::Windows::Foundation::Uri(L"https://www.microsoft.com"));
|
||||
// Result may or may not be successful depending on network
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Network errors are acceptable in test environment
|
||||
}
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// Thread safety test (doesn't require network)
|
||||
TEST_METHOD(HttpClient_MultipleInstances_DoNotCrash)
|
||||
{
|
||||
std::vector<std::unique_ptr<http::HttpClient>> clients;
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
clients.push_back(std::make_unique<http::HttpClient>());
|
||||
}
|
||||
|
||||
// All clients should coexist without crashing
|
||||
Assert::AreEqual(static_cast<size_t>(10), clients.size());
|
||||
}
|
||||
|
||||
TEST_METHOD(HttpClient_ConcurrentConstruction_DoesNotCrash)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
http::HttpClient client;
|
||||
successCount++;
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(5, successCount.load());
|
||||
}
|
||||
};
|
||||
}
|
||||
283
src/common/UnitTests-CommonUtils/Json.Tests.cpp
Normal file
283
src/common/UnitTests-CommonUtils/Json.Tests.cpp
Normal file
@@ -0,0 +1,283 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <json.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace winrt::Windows::Data::Json;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(JsonTests)
|
||||
{
|
||||
public:
|
||||
// from_file tests
|
||||
TEST_METHOD(FromFile_NonExistentFile_ReturnsNullopt)
|
||||
{
|
||||
auto result = json::from_file(L"C:\\NonExistent\\File\\Path.json");
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromFile_ValidJsonFile_ReturnsJsonObject)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
tempFile.write("{\"key\": \"value\"}");
|
||||
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsTrue(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromFile_InvalidJson_ReturnsNullopt)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
tempFile.write("not valid json {{{");
|
||||
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromFile_EmptyFile_ReturnsNullopt)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
// File is empty
|
||||
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsFalse(result.has_value());
|
||||
}
|
||||
|
||||
TEST_METHOD(FromFile_ValidComplexJson_ParsesCorrectly)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
tempFile.write("{\"name\": \"test\", \"value\": 42, \"enabled\": true}");
|
||||
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsTrue(result.has_value());
|
||||
|
||||
auto& obj = *result;
|
||||
Assert::IsTrue(obj.HasKey(L"name"));
|
||||
Assert::IsTrue(obj.HasKey(L"value"));
|
||||
Assert::IsTrue(obj.HasKey(L"enabled"));
|
||||
}
|
||||
|
||||
// to_file tests
|
||||
TEST_METHOD(ToFile_ValidObject_WritesFile)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateStringValue(L"value"));
|
||||
json::to_file(tempFile.path(), obj);
|
||||
|
||||
// Read back and verify
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::IsTrue(result->HasKey(L"key"));
|
||||
}
|
||||
|
||||
TEST_METHOD(ToFile_ComplexObject_WritesFile)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"name", JsonValue::CreateStringValue(L"test"));
|
||||
obj.SetNamedValue(L"value", JsonValue::CreateNumberValue(42));
|
||||
obj.SetNamedValue(L"enabled", JsonValue::CreateBooleanValue(true));
|
||||
json::to_file(tempFile.path(), obj);
|
||||
|
||||
auto result = json::from_file(tempFile.path());
|
||||
Assert::IsTrue(result.has_value());
|
||||
Assert::AreEqual(std::wstring(L"test"), std::wstring(result->GetNamedString(L"name")));
|
||||
Assert::AreEqual(42.0, result->GetNamedNumber(L"value"));
|
||||
Assert::IsTrue(result->GetNamedBoolean(L"enabled"));
|
||||
}
|
||||
|
||||
// has tests
|
||||
TEST_METHOD(Has_ExistingKey_ReturnsTrue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateStringValue(L"value"));
|
||||
Assert::IsTrue(json::has(obj, L"key", JsonValueType::String));
|
||||
}
|
||||
|
||||
TEST_METHOD(Has_NonExistingKey_ReturnsFalse)
|
||||
{
|
||||
JsonObject obj;
|
||||
Assert::IsFalse(json::has(obj, L"key", JsonValueType::String));
|
||||
}
|
||||
|
||||
TEST_METHOD(Has_WrongType_ReturnsFalse)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateStringValue(L"value"));
|
||||
Assert::IsFalse(json::has(obj, L"key", JsonValueType::Number));
|
||||
}
|
||||
|
||||
TEST_METHOD(Has_NumberType_ReturnsTrue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateNumberValue(42));
|
||||
Assert::IsTrue(json::has(obj, L"key", JsonValueType::Number));
|
||||
}
|
||||
|
||||
TEST_METHOD(Has_BooleanType_ReturnsTrue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateBooleanValue(true));
|
||||
Assert::IsTrue(json::has(obj, L"key", JsonValueType::Boolean));
|
||||
}
|
||||
|
||||
TEST_METHOD(Has_ObjectType_ReturnsTrue)
|
||||
{
|
||||
JsonObject obj;
|
||||
JsonObject nested;
|
||||
obj.SetNamedValue(L"key", nested);
|
||||
Assert::IsTrue(json::has(obj, L"key", JsonValueType::Object));
|
||||
}
|
||||
|
||||
// value function tests
|
||||
TEST_METHOD(Value_IntegerType_CreatesNumberValue)
|
||||
{
|
||||
auto val = json::value(42);
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::Number);
|
||||
Assert::AreEqual(42.0, val.GetNumber());
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_DoubleType_CreatesNumberValue)
|
||||
{
|
||||
auto val = json::value(3.14);
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::Number);
|
||||
Assert::AreEqual(3.14, val.GetNumber());
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_BooleanTrue_CreatesBooleanValue)
|
||||
{
|
||||
auto val = json::value(true);
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::Boolean);
|
||||
Assert::IsTrue(val.GetBoolean());
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_BooleanFalse_CreatesBooleanValue)
|
||||
{
|
||||
auto val = json::value(false);
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::Boolean);
|
||||
Assert::IsFalse(val.GetBoolean());
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_String_CreatesStringValue)
|
||||
{
|
||||
auto val = json::value(L"hello");
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::String);
|
||||
Assert::AreEqual(std::wstring(L"hello"), std::wstring(val.GetString()));
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_JsonObject_ReturnsJsonValue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"key", JsonValue::CreateStringValue(L"value"));
|
||||
auto val = json::value(obj);
|
||||
Assert::IsTrue(val.ValueType() == JsonValueType::Object);
|
||||
}
|
||||
|
||||
TEST_METHOD(Value_JsonValue_ReturnsIdentity)
|
||||
{
|
||||
auto original = JsonValue::CreateStringValue(L"test");
|
||||
auto result = json::value(original);
|
||||
Assert::AreEqual(std::wstring(L"test"), std::wstring(result.GetString()));
|
||||
}
|
||||
|
||||
// get function tests
|
||||
TEST_METHOD(Get_BooleanValue_ReturnsValue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"enabled", JsonValue::CreateBooleanValue(true));
|
||||
|
||||
bool result = false;
|
||||
json::get(obj, L"enabled", result);
|
||||
Assert::IsTrue(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_IntValue_ReturnsValue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"count", JsonValue::CreateNumberValue(42));
|
||||
|
||||
int result = 0;
|
||||
json::get(obj, L"count", result);
|
||||
Assert::AreEqual(42, result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_DoubleValue_ReturnsValue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"ratio", JsonValue::CreateNumberValue(3.14));
|
||||
|
||||
double result = 0.0;
|
||||
json::get(obj, L"ratio", result);
|
||||
Assert::AreEqual(3.14, result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_StringValue_ReturnsValue)
|
||||
{
|
||||
JsonObject obj;
|
||||
obj.SetNamedValue(L"name", JsonValue::CreateStringValue(L"test"));
|
||||
|
||||
std::wstring result;
|
||||
json::get(obj, L"name", result);
|
||||
Assert::AreEqual(std::wstring(L"test"), result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_MissingKey_UsesDefault)
|
||||
{
|
||||
JsonObject obj;
|
||||
|
||||
int result = 0;
|
||||
json::get(obj, L"missing", result, 99);
|
||||
Assert::AreEqual(99, result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_MissingKeyNoDefault_PreservesOriginal)
|
||||
{
|
||||
JsonObject obj;
|
||||
|
||||
int result = 42;
|
||||
json::get(obj, L"missing", result);
|
||||
// When key is missing and no default, original value is preserved
|
||||
Assert::AreEqual(42, result);
|
||||
}
|
||||
|
||||
TEST_METHOD(Get_JsonObject_ReturnsObject)
|
||||
{
|
||||
JsonObject obj;
|
||||
JsonObject nested;
|
||||
nested.SetNamedValue(L"inner", JsonValue::CreateStringValue(L"value"));
|
||||
obj.SetNamedValue(L"nested", nested);
|
||||
|
||||
JsonObject result;
|
||||
json::get(obj, L"nested", result);
|
||||
Assert::IsTrue(result.HasKey(L"inner"));
|
||||
}
|
||||
|
||||
// Roundtrip tests
|
||||
TEST_METHOD(Roundtrip_ComplexObject_PreservesData)
|
||||
{
|
||||
TestHelpers::TempFile tempFile(L"", L".json");
|
||||
|
||||
JsonObject original;
|
||||
original.SetNamedValue(L"string", JsonValue::CreateStringValue(L"hello"));
|
||||
original.SetNamedValue(L"number", JsonValue::CreateNumberValue(42));
|
||||
original.SetNamedValue(L"boolean", JsonValue::CreateBooleanValue(true));
|
||||
|
||||
JsonObject nested;
|
||||
nested.SetNamedValue(L"inner", JsonValue::CreateStringValue(L"world"));
|
||||
original.SetNamedValue(L"object", nested);
|
||||
|
||||
json::to_file(tempFile.path(), original);
|
||||
auto loaded = json::from_file(tempFile.path());
|
||||
|
||||
Assert::IsTrue(loaded.has_value());
|
||||
Assert::AreEqual(std::wstring(L"hello"), std::wstring(loaded->GetNamedString(L"string")));
|
||||
Assert::AreEqual(42.0, loaded->GetNamedNumber(L"number"));
|
||||
Assert::IsTrue(loaded->GetNamedBoolean(L"boolean"));
|
||||
Assert::AreEqual(std::wstring(L"world"), std::wstring(loaded->GetNamedObject(L"object").GetNamedString(L"inner")));
|
||||
}
|
||||
};
|
||||
}
|
||||
180
src/common/UnitTests-CommonUtils/LoggerHelper.Tests.cpp
Normal file
180
src/common/UnitTests-CommonUtils/LoggerHelper.Tests.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <logger_helper.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
using namespace LoggerHelpers;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
TEST_CLASS(LoggerHelperTests)
|
||||
{
|
||||
public:
|
||||
// get_log_folder_path tests
|
||||
TEST_METHOD(GetLogFolderPath_ValidAppPath_ReturnsPath)
|
||||
{
|
||||
auto result = get_log_folder_path(L"TestApp");
|
||||
|
||||
Assert::IsFalse(result.empty());
|
||||
// Should contain the app name or be a valid path
|
||||
auto pathStr = result.wstring();
|
||||
Assert::IsTrue(pathStr.length() > 0);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLogFolderPath_EmptyAppPath_ReturnsPath)
|
||||
{
|
||||
auto result = get_log_folder_path(L"");
|
||||
|
||||
// Should still return a base path
|
||||
Assert::IsTrue(true); // Just verify no crash
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLogFolderPath_SpecialCharacters_Works)
|
||||
{
|
||||
auto result = get_log_folder_path(L"Test App With Spaces");
|
||||
|
||||
// Should handle spaces in path
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetLogFolderPath_ConsistentResults)
|
||||
{
|
||||
auto result1 = get_log_folder_path(L"TestApp");
|
||||
auto result2 = get_log_folder_path(L"TestApp");
|
||||
|
||||
Assert::AreEqual(result1.wstring(), result2.wstring());
|
||||
}
|
||||
|
||||
// dir_exists tests
|
||||
TEST_METHOD(DirExists_WindowsDirectory_ReturnsTrue)
|
||||
{
|
||||
bool result = dir_exists(std::filesystem::path(L"C:\\Windows"));
|
||||
Assert::IsTrue(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DirExists_NonExistentDirectory_ReturnsFalse)
|
||||
{
|
||||
bool result = dir_exists(std::filesystem::path(L"C:\\NonExistentDir12345"));
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DirExists_FileInsteadOfDir_ReturnsTrue)
|
||||
{
|
||||
// notepad.exe is a file, not a directory
|
||||
bool result = dir_exists(std::filesystem::path(L"C:\\Windows\\notepad.exe"));
|
||||
Assert::IsTrue(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DirExists_EmptyPath_ReturnsFalse)
|
||||
{
|
||||
bool result = dir_exists(std::filesystem::path(L""));
|
||||
Assert::IsFalse(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(DirExists_TempDirectory_ReturnsTrue)
|
||||
{
|
||||
wchar_t tempPath[MAX_PATH];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
|
||||
bool result = dir_exists(std::filesystem::path(tempPath));
|
||||
Assert::IsTrue(result);
|
||||
}
|
||||
|
||||
// delete_old_log_folder tests
|
||||
TEST_METHOD(DeleteOldLogFolder_NonExistentFolder_DoesNotCrash)
|
||||
{
|
||||
delete_old_log_folder(std::filesystem::path(L"C:\\NonExistentLogFolder12345"));
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(DeleteOldLogFolder_ValidEmptyFolder_Works)
|
||||
{
|
||||
TestHelpers::TempDirectory tempDir;
|
||||
|
||||
// Create a subfolder structure
|
||||
auto logFolder = std::filesystem::path(tempDir.path()) / L"logs";
|
||||
std::filesystem::create_directories(logFolder);
|
||||
|
||||
Assert::IsTrue(std::filesystem::exists(logFolder));
|
||||
|
||||
delete_old_log_folder(logFolder);
|
||||
|
||||
// Folder may or may not be deleted depending on implementation
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// delete_other_versions_log_folders tests
|
||||
TEST_METHOD(DeleteOtherVersionsLogFolders_NonExistentPath_DoesNotCrash)
|
||||
{
|
||||
delete_other_versions_log_folders(L"C:\\NonExistent\\Path", L"1.0.0");
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
TEST_METHOD(DeleteOtherVersionsLogFolders_EmptyVersion_DoesNotCrash)
|
||||
{
|
||||
wchar_t tempPath[MAX_PATH];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
|
||||
delete_other_versions_log_folders(tempPath, L"");
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
|
||||
// Thread safety tests
|
||||
TEST_METHOD(GetLogFolderPath_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount, i]() {
|
||||
auto path = get_log_folder_path(L"TestApp" + std::to_wstring(i));
|
||||
if (!path.empty())
|
||||
{
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(10, successCount.load());
|
||||
}
|
||||
|
||||
TEST_METHOD(DirExists_ThreadSafe)
|
||||
{
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> successCount{ 0 };
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
threads.emplace_back([&successCount]() {
|
||||
for (int j = 0; j < 10; ++j)
|
||||
{
|
||||
dir_exists(std::filesystem::path(L"C:\\Windows"));
|
||||
successCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : threads)
|
||||
{
|
||||
t.join();
|
||||
}
|
||||
|
||||
Assert::AreEqual(100, successCount.load());
|
||||
}
|
||||
|
||||
// Path construction tests
|
||||
TEST_METHOD(GetLogFolderPath_ReturnsValidFilesystemPath)
|
||||
{
|
||||
auto result = get_log_folder_path(L"TestApp");
|
||||
|
||||
// Should be a valid path that we can use with filesystem operations
|
||||
Assert::IsTrue(result.is_absolute() || result.has_root_name() || !result.empty());
|
||||
}
|
||||
};
|
||||
}
|
||||
173
src/common/UnitTests-CommonUtils/ModulesRegistry.Tests.cpp
Normal file
173
src/common/UnitTests-CommonUtils/ModulesRegistry.Tests.cpp
Normal file
@@ -0,0 +1,173 @@
|
||||
#include "pch.h"
|
||||
#include "TestHelpers.h"
|
||||
#include <modulesRegistry.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace UnitTestsCommonUtils
|
||||
{
|
||||
static std::wstring GetInstallDir()
|
||||
{
|
||||
wchar_t path[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
return std::filesystem::path{ path }.parent_path().wstring();
|
||||
}
|
||||
|
||||
TEST_CLASS(ModulesRegistryTests)
|
||||
{
|
||||
public:
|
||||
// Test that all changeset generator functions return valid changesets
|
||||
TEST_METHOD(GetSvgPreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getSvgPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetSvgThumbnailProviderChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getSvgThumbnailHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMarkdownPreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getMdPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetMonacoPreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getMonacoPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetPdfPreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getPdfPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetPdfThumbnailProviderChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getPdfThumbnailHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetGcodePreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getGcodePreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetGcodeThumbnailProviderChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getGcodeThumbnailHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetStlThumbnailProviderChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getStlThumbnailHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetQoiPreviewHandlerChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getQoiPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
TEST_METHOD(GetQoiThumbnailProviderChangeSet_ReturnsChangeSet)
|
||||
{
|
||||
auto changeSet = getQoiThumbnailHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
// Test enabled vs disabled state
|
||||
TEST_METHOD(ChangeSet_EnabledVsDisabled_MayDiffer)
|
||||
{
|
||||
auto enabledSet = getSvgPreviewHandlerChangeSet(GetInstallDir(), true);
|
||||
auto disabledSet = getSvgPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
// Both should be valid change sets
|
||||
Assert::IsFalse(enabledSet.changes.empty());
|
||||
Assert::IsFalse(disabledSet.changes.empty());
|
||||
}
|
||||
|
||||
// Test getAllOnByDefaultModulesChangeSets
|
||||
TEST_METHOD(GetAllOnByDefaultModulesChangeSets_ReturnsMultipleChangeSets)
|
||||
{
|
||||
auto changeSets = getAllOnByDefaultModulesChangeSets(GetInstallDir());
|
||||
|
||||
// Should return multiple changesets for all default-enabled modules
|
||||
Assert::IsTrue(changeSets.size() > 0);
|
||||
}
|
||||
|
||||
// Test getAllModulesChangeSets
|
||||
TEST_METHOD(GetAllModulesChangeSets_ReturnsChangeSets)
|
||||
{
|
||||
auto changeSets = getAllModulesChangeSets(GetInstallDir());
|
||||
|
||||
// Should return changesets for all modules
|
||||
Assert::IsTrue(changeSets.size() > 0);
|
||||
}
|
||||
|
||||
TEST_METHOD(GetAllModulesChangeSets_ContainsMoreThanOnByDefault)
|
||||
{
|
||||
auto allSets = getAllModulesChangeSets(GetInstallDir());
|
||||
auto defaultSets = getAllOnByDefaultModulesChangeSets(GetInstallDir());
|
||||
|
||||
// All modules should be >= on-by-default modules
|
||||
Assert::IsTrue(allSets.size() >= defaultSets.size());
|
||||
}
|
||||
|
||||
// Test that changesets have valid structure
|
||||
TEST_METHOD(ChangeSet_HasValidKeyPath)
|
||||
{
|
||||
auto changeSet = getSvgPreviewHandlerChangeSet(GetInstallDir(), false);
|
||||
|
||||
Assert::IsFalse(changeSet.changes.empty());
|
||||
}
|
||||
|
||||
// Test all changeset functions don't crash
|
||||
TEST_METHOD(AllChangeSetFunctions_DoNotCrash)
|
||||
{
|
||||
auto installDir = GetInstallDir();
|
||||
getSvgPreviewHandlerChangeSet(installDir, true);
|
||||
getSvgPreviewHandlerChangeSet(installDir, false);
|
||||
getSvgThumbnailHandlerChangeSet(installDir, true);
|
||||
getSvgThumbnailHandlerChangeSet(installDir, false);
|
||||
getMdPreviewHandlerChangeSet(installDir, true);
|
||||
getMdPreviewHandlerChangeSet(installDir, false);
|
||||
getMonacoPreviewHandlerChangeSet(installDir, true);
|
||||
getMonacoPreviewHandlerChangeSet(installDir, false);
|
||||
getPdfPreviewHandlerChangeSet(installDir, true);
|
||||
getPdfPreviewHandlerChangeSet(installDir, false);
|
||||
getPdfThumbnailHandlerChangeSet(installDir, true);
|
||||
getPdfThumbnailHandlerChangeSet(installDir, false);
|
||||
getGcodePreviewHandlerChangeSet(installDir, true);
|
||||
getGcodePreviewHandlerChangeSet(installDir, false);
|
||||
getGcodeThumbnailHandlerChangeSet(installDir, true);
|
||||
getGcodeThumbnailHandlerChangeSet(installDir, false);
|
||||
getStlThumbnailHandlerChangeSet(installDir, true);
|
||||
getStlThumbnailHandlerChangeSet(installDir, false);
|
||||
getQoiPreviewHandlerChangeSet(installDir, true);
|
||||
getQoiPreviewHandlerChangeSet(installDir, false);
|
||||
getQoiThumbnailHandlerChangeSet(installDir, true);
|
||||
getQoiThumbnailHandlerChangeSet(installDir, false);
|
||||
|
||||
Assert::IsTrue(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user